From e36fe18c483b8e520752f61975e44ddd0317a332 Mon Sep 17 00:00:00 2001 From: Klaus Schmidinger Date: Sun, 23 Jan 2005 18:00:00 +0100 Subject: =?UTF-8?q?Version=201.3.19=20-=20Making=20sure=20at=20least=20the?= =?UTF-8?q?=20default=20skin=20is=20available=20at=20program=20start=20in?= =?UTF-8?q?=20case=20a=20=20=20plugin=20needs=20to=20issue=20an=20error=20?= =?UTF-8?q?message=20(thanks=20to=20Achim=20Tuffentshammer=20for=20=20=20r?= =?UTF-8?q?eporting=20a=20crash=20in=20such=20a=20case).=20Also=20checking?= =?UTF-8?q?=20if=20there=20is=20a=20current=20skin=20=20=20in=20cSkins::Me?= =?UTF-8?q?ssage().=20-=20Completed=20the=20Finnish=20OSD=20texts=20and=20?= =?UTF-8?q?fixed=20internationalization=20of=20the=20text=20=20=20for=20"S?= =?UTF-8?q?etup/DVB/Audio=20language(s)"=20(thanks=20to=20Rolf=20Ahrenberg?= =?UTF-8?q?).=20-=20Completed=20the=20Estonian=20OSD=20texts=20and=20switc?= =?UTF-8?q?hed=20to=20iso8859-13=20character=20set=20=20=20(thanks=20to=20?= =?UTF-8?q?Arthur=20Konovalov).=20-=20Made=20cCondWait::SleepMs()=20sleep?= =?UTF-8?q?=20at=20least=203ms=20to=20avoid=20a=20possible=20busy=20wait.?= =?UTF-8?q?=20-=20Fixed=20canceling=20the=20LIRC=20thread=20(thanks=20to?= =?UTF-8?q?=20Marco=20Schl=C3=BC=C3=9Fler=20for=20pointing=20out=20=20=20t?= =?UTF-8?q?his=20one).=20-=20The=20"Green"=20button=20in=20the=20"Main"=20?= =?UTF-8?q?menu=20is=20now=20always=20"Audio",=20since=20the=20audio=20=20?= =?UTF-8?q?=20channel=20might=20be=20changed=20even=20if=20there=20is=20on?= =?UTF-8?q?ly=20one=20actual=20audio=20PID.=20-=20Fixed=20handling=20the?= =?UTF-8?q?=20'-E'=20option=20which=20was=20broken=20in=20version=201.3.18?= =?UTF-8?q?=20(thanks=20to=20=20=20Christian=20Jacobsen=20for=20reporting?= =?UTF-8?q?=20this=20one).=20-=20Added=20'channels.conf.terr'=20entries=20?= =?UTF-8?q?for=20Mainz=20(thanks=20to=20Michael=20Heyse).=20-=20Implemente?= =?UTF-8?q?d=20cDolbyRepacker=20for=20better=20handling=20of=20Dolby=20Dig?= =?UTF-8?q?ital=20PES=20packets=20=20=20(thanks=20to=20Reinhard=20Nissl).?= =?UTF-8?q?=20-=20Fixed=20playing=20files=20with=20PES=20packets=20longer?= =?UTF-8?q?=20than=202048=20byte=20through=20the=20full=20=20=20featured?= =?UTF-8?q?=20DVB=20card=20(thanks=20to=20Marco=20Kremer=20for=20reporting?= =?UTF-8?q?=20this=20one=20and=20providing=20=20=20a=20test=20sample).=20-?= =?UTF-8?q?=20Recording=20and=20Transfer=20Mode=20now=20handle=20more=20th?= =?UTF-8?q?an=202=20audio=20PIDs.=20For=20this=20the=20=20=20interfaces=20?= =?UTF-8?q?of=20the=20following=20functions=20have=20been=20changed:=20=20?= =?UTF-8?q?=20cTransferControl::cTransferControl()=20=20=20cTransfer::cTra?= =?UTF-8?q?nsfer()=20=20=20cRecorder::cRecorder()=20=20=20cReceiver::cRece?= =?UTF-8?q?iver()=20=20=20cRemux::cRemux()=20-=20Fixed=20a=20possible=20ra?= =?UTF-8?q?ce=20condition=20in=20cDevice::Action()=20and=20cTSBuffer::Acti?= =?UTF-8?q?on()=20=20=20(thanks=20to=20Stefan=20Huelswitt).=20-=20Extended?= =?UTF-8?q?=20some=20buffer=20sizes=20to=20allow=20handling=20HDTV=20strea?= =?UTF-8?q?ms=20(thanks=20to=20Reinhard=20=20=20Nissl).=20-=20Added=20'cha?= =?UTF-8?q?nnels.conf.terr'=20entries=20for=20D=C3=BCsseldorf=20and=20K?= =?UTF-8?q?=C3=B6ln=20(thanks=20to=20Walter=20Koch).=20-=20Falling=20back?= =?UTF-8?q?=20to=20'stereo'=20when=20switching=20channels=20in=20case=20th?= =?UTF-8?q?e=20user=20had=20switched=20=20=20to=20'left'=20or=20'right'=20?= =?UTF-8?q?(suggested=20by=20Rolf=20Groppe).=20-=20Completed=20the=20Danis?= =?UTF-8?q?h=20OSD=20texts=20(thanks=20to=20Mogens=20Elneff).=20-=20Record?= =?UTF-8?q?ing=20and=20Transfer=20Mode=20can=20now=20handle=20up=20to=208?= =?UTF-8?q?=20Dolby=20Digital=20tracks=20(thanks=20=20=20to=20Marco=20Schl?= =?UTF-8?q?=C3=BC=C3=9Fler=20for=20a=20patch=20that=20implements=20substre?= =?UTF-8?q?am=20handling=20into=20=20=20cDevice::PlayPesPacket(),=20and=20?= =?UTF-8?q?Reinhard=20Nissl=20for=20adding=20substream=20handling=20to=20?= =?UTF-8?q?=20=20cDolbyRepacker).=20-=20Added=20PlayPes(NULL,=200)=20to=20?= =?UTF-8?q?cTransfer::Action()=20when=20clearing=20the=20transfer=20buffer?= =?UTF-8?q?=20=20=20to=20avoid=20overflows=20(thanks=20to=20Marco=20Schl?= =?UTF-8?q?=C3=BC=C3=9Fler=20for=20pointing=20this=20out).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- remux.c | 412 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 370 insertions(+), 42 deletions(-) (limited to 'remux.c') diff --git a/remux.c b/remux.c index 554359c..06fb13b 100644 --- a/remux.c +++ b/remux.c @@ -8,14 +8,296 @@ * the Linux DVB driver's 'tuxplayer' example and were rewritten to suit * VDR's needs. * - * $Id: remux.c 1.23 2004/12/18 13:15:02 kls Exp $ + * The cDolbyRepacker code was originally written by Reinhard Nissl , + * and adapted to the VDR coding style by Klaus.Schmidinger@cadsoft.de. + * + * $Id: remux.c 1.27 2005/01/23 12:56:39 kls Exp $ */ #include "remux.h" #include +#include "channels.h" #include "thread.h" #include "tools.h" +// --- cRepacker ------------------------------------------------------------- + +class cRepacker { +protected: + uint8_t subStreamId; +public: + cRepacker(void) { subStreamId = 0; } + virtual ~cRepacker() {} + virtual int Put(cRingBufferLinear *ResultBuffer, const uchar *Data, int Count) = 0; + virtual int BreakAt(const uchar *Data, int Count) = 0; + void SetSubStreamId(uint8_t SubStreamId) { subStreamId = SubStreamId; } + }; + +// --- cDolbyRepacker -------------------------------------------------------- + +class cDolbyRepacker : public cRepacker { +private: + static int frameSizes[]; + uchar fragmentData[6 + 65535]; + int fragmentLen; + uchar pesHeader[6 + 3 + 255 + 4 + 4]; + int pesHeaderLen; + uchar chk1; + uchar chk2; + int ac3todo; + enum { + find_0b, + find_77, + store_chk1, + store_chk2, + get_length + } state; + void Reset(void); +public: + cDolbyRepacker(void); + virtual int Put(cRingBufferLinear *ResultBuffer, const uchar *Data, int Count); + virtual int BreakAt(const uchar *Data, int Count); + }; + +int cDolbyRepacker::frameSizes[] = { + // fs = 48 kHz + 64, 64, 80, 80, 96, 96, 112, 112, 128, 128, 160, 160, 192, 192, 224, 224, + 256, 256, 320, 320, 384, 384, 448, 448, 512, 512, 640, 640, 768, 768, 896, 896, + 1024, 1024, 1152, 1152, 1280, 1280, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // fs = 44.1 kHz + 69, 70, 87, 88, 104, 105, 121, 122, 139, 140, 174, 175, 208, 209, 243, 244, + 278, 279, 348, 349, 417, 418, 487, 488, 557, 558, 696, 697, 835, 836, 975, 976, + 1114, 1115, 1253, 1254, 1393, 1394, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // fs = 32 kHz + 96, 96, 120, 120, 144, 144, 168, 168, 192, 192, 240, 240, 288, 288, 336, 336, + 384, 384, 480, 480, 576, 576, 672, 672, 768, 768, 960, 960, 1152, 1152, 1344, 1344, + 1536, 1536, 1728, 1728, 1920, 1920, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + +cDolbyRepacker::cDolbyRepacker(void) +{ + pesHeader[0] = 0x00; + pesHeader[1] = 0x00; + pesHeader[2] = 0x01; + pesHeader[3] = 0xBD; + pesHeader[4] = 0x00; + pesHeader[5] = 0x00; + Reset(); +} + +void cDolbyRepacker::Reset() +{ + state = find_0b; + pesHeader[6] = 0x80; + pesHeader[7] = 0x00; + pesHeader[8] = 0x00; + pesHeaderLen = 9; + ac3todo = 0; + chk1 = 0; + chk2 = 0; + fragmentLen = 0; +} + +int cDolbyRepacker::Put(cRingBufferLinear *ResultBuffer, const uchar *Data, int Count) +{ + // check for MPEG 2 + if ((Data[6] & 0xC0) != 0x80) + return 0; + // copy header information for later use + if (Data[6] != 0x80 || Data[7] != 0x00 || Data[8] != 0x00) { + pesHeaderLen = Data[8] + 6 + 3; + memcpy(pesHeader, Data, pesHeaderLen); + } + + const uchar *data = Data + pesHeaderLen; + int done = pesHeaderLen; + int todo = Count - done; + + // finish remainder of ac3 frame + if (ac3todo > 0) { + int bite; + // enough data available to put PES packet into buffer? + if (ac3todo <= todo) { + // output a previous fragment first + if (fragmentLen > 0) { + bite = fragmentLen; + int n = ResultBuffer->Put(fragmentData, bite); + if (bite != n) { + Reset(); + return done; + } + fragmentLen = 0; + } + bite = ac3todo; + int n = ResultBuffer->Put(data, bite); + if (bite != n) { + Reset(); + return done + n; + } + } + else { + // copy the fragment into separate buffer for later processing + bite = todo; + if (fragmentLen + bite > (int)sizeof(fragmentData)) { + Reset(); + return done; + } + memcpy(fragmentData + fragmentLen, data, bite); + fragmentLen += bite; + } + data += bite; + done += bite; + todo -= bite; + ac3todo -= bite; + } + + // look for 0x0B 0x77 + while (todo > 0) { + switch (state) { + case find_0b: + if (*data == 0x0B) + ++(int &)state; + data++; + done++; + todo--; + continue; + case find_77: + if (*data != 0x77) { + state = find_0b; + continue; + } + data++; + done++; + todo--; + ++(int &)state; + continue; + case store_chk1: + chk1 = *data++; + done++; + todo--; + ++(int &)state; + continue; + case store_chk2: + chk2 = *data++; + done++; + todo--; + ++(int &)state; + continue; + case get_length: { + ac3todo = 2 * frameSizes[*data]; + // frameSizeCode was invalid => restart searching + if (ac3todo <= 0) { + if (chk1 == 0x0B) { + if (chk2 == 0x77) { + state = store_chk1; + continue; + } + if (chk2 == 0x0B) { + state = find_77; + continue; + } + state = find_0b; + continue; + } + if (chk2 == 0x0B) { + state = find_77; + continue; + } + state = find_0b; + continue; + } + // adjust PES packet length and output packet + if (subStreamId) { + pesHeader[pesHeaderLen++] = subStreamId; + pesHeader[pesHeaderLen++] = 0x00; + pesHeader[pesHeaderLen++] = 0x00; + pesHeader[pesHeaderLen++] = 0x00; + } + int packetLen = pesHeaderLen - 6 + ac3todo; + pesHeader[4] = packetLen >> 8; + pesHeader[5] = packetLen & 0xFF; + pesHeader[pesHeaderLen + 0] = 0x0B; + pesHeader[pesHeaderLen + 1] = 0x77; + pesHeader[pesHeaderLen + 2] = chk1; + pesHeader[pesHeaderLen + 3] = chk2; + ac3todo -= 4; + int bite = pesHeaderLen + 4; + // enough data available to put PES packet into buffer? + if (ac3todo <= todo) { + int n = ResultBuffer->Put(pesHeader, bite); + if (bite != n) { + Reset(); + return done; + } + bite = ac3todo; + n = ResultBuffer->Put(data, bite); + if (bite != n) { + Reset(); + return done + n; + } + } + else { + // copy the fragment into separate buffer for later processing + if (fragmentLen + bite > (int)sizeof(fragmentData)) { + Reset(); + return done; + } + memcpy(fragmentData + fragmentLen, pesHeader, bite); + fragmentLen += bite; + bite = todo; + if (fragmentLen + bite > (int)sizeof(fragmentData)) { + Reset(); + return done; + } + memcpy(fragmentData + fragmentLen, data, bite); + fragmentLen += bite; + } + data += bite; + done += bite; + todo -= bite; + ac3todo -= bite; + // prepare for next packet + pesHeader[6] = 0x80; + pesHeader[7] = 0x00; + pesHeader[8] = 0x00; + pesHeaderLen = 9; + state = find_0b; + } + } + } + return Count; +} + +int cDolbyRepacker::BreakAt(const uchar *Data, int Count) +{ + // enough data for test? + if (Count < 6 + 3) + return -1; + // check for MPEG 2 + if ((Data[6] & 0xC0) != 0x80) + return -1; + int headerLen = Data[8] + 6 + 3; + // break after fragment tail? + if (ac3todo > 0) + return headerLen + ac3todo; + // enough data for test? + if (Count < headerLen + 5) + return -1; + const uchar *data = Data + headerLen; + // break after ac3 frame? + if (data[0] == 0x0B && data[1] == 0x77 && frameSizes[data[4]] > 0) + return headerLen + frameSizes[data[4]]; + return -1; +} + // --- cTS2PES --------------------------------------------------------------- #include @@ -52,7 +334,7 @@ #define ADAPT_FIELD 0x20 #define MAX_PLENGTH 0xFFFF // the maximum PES packet length (theoretically) -#define MMAX_PLENGTH (8*MAX_PLENGTH) // some stations send PES packets that are extremely large, e.g. DVB-T in Finland +#define MMAX_PLENGTH (64*MAX_PLENGTH) // some stations send PES packets that are extremely large, e.g. DVB-T in Finland or HDTV 1920x1080 #define IPACKS 2048 @@ -64,12 +346,14 @@ class cTS2PES { private: + int pid; int size; int found; int count; uint8_t *buf; uint8_t cid; uint8_t audioCid; + uint8_t subStreamId; int plength; uint8_t plen[2]; uint8_t flag1; @@ -83,6 +367,7 @@ private: int tsErrors; int ccErrors; int ccCounter; + cRepacker *repacker; static uint8_t headr[]; void store(uint8_t *Data, int Count); void reset_ipack(void); @@ -90,19 +375,25 @@ private: void write_ipack(const uint8_t *Data, int Count); void instant_repack(const uint8_t *Buf, int Count); public: - cTS2PES(cRingBufferLinear *ResultBuffer, int Size, uint8_t AudioCid = 0x00); + cTS2PES(int Pid, cRingBufferLinear *ResultBuffer, int Size, uint8_t AudioCid = 0x00, uint8_t SubStreamId = 0x00, cRepacker *Repacker = NULL); ~cTS2PES(); + int Pid(void) { return pid; } void ts_to_pes(const uint8_t *Buf); // don't need count (=188) void Clear(void); }; uint8_t cTS2PES::headr[] = { 0x00, 0x00, 0x01 }; -cTS2PES::cTS2PES(cRingBufferLinear *ResultBuffer, int Size, uint8_t AudioCid) +cTS2PES::cTS2PES(int Pid, cRingBufferLinear *ResultBuffer, int Size, uint8_t AudioCid, uint8_t SubStreamId, cRepacker *Repacker) { + pid = Pid; resultBuffer = ResultBuffer; size = Size; audioCid = AudioCid; + subStreamId = SubStreamId; + repacker = Repacker; + if (repacker) + repacker->SetSubStreamId(subStreamId); tsErrors = 0; ccErrors = 0; @@ -119,6 +410,7 @@ cTS2PES::~cTS2PES() if (tsErrors || ccErrors) dsyslog("cTS2PES got %d TS errors, %d TS continuity errors", tsErrors, ccErrors); free(buf); + delete repacker; } void cTS2PES::Clear(void) @@ -128,7 +420,7 @@ void cTS2PES::Clear(void) void cTS2PES::store(uint8_t *Data, int Count) { - int n = resultBuffer->Put(Data, Count); + int n = repacker ? repacker->Put(resultBuffer, Data, Count) : resultBuffer->Put(Data, Count); if (n != Count) esyslog("ERROR: result buffer overflow, dropped %d out of %d byte", Count - n, Count); } @@ -178,17 +470,48 @@ void cTS2PES::write_ipack(const uint8_t *Data, int Count) count = 6; } - if (count + Count < size) { - memcpy(buf + count, Data, Count); - count += Count; + // determine amount of data to process + int bite = Count; + if (count + bite > size) + bite = size - count; + if (repacker) { + int breakAt = repacker->BreakAt(buf, count); + // avoid memcpy of data after break location + if (0 <= breakAt && breakAt < count + bite) { + bite = breakAt - count; + if (bite < 0) // should never happen + bite = 0; + } + } + + memcpy(buf + count, Data, bite); + count += bite; + + if (repacker) { + // determine break location + int breakAt = repacker->BreakAt(buf, count); + if (breakAt > size) // won't fit into packet? + breakAt = -1; + if (breakAt > count) // not enough data? + breakAt = -1; + // push out data before break location + if (breakAt > 0) { + // adjust bite if above memcpy was to large + bite -= count - breakAt; + count = breakAt; + send_ipack(); + // recurse for data after break location + if (Count - bite > 0) + write_ipack(Data + bite, Count - bite); + } } - else { - int rest = size - count; - memcpy(buf + count, Data, rest); - count += rest; + + // push out data when buffer is full + if (count >= size) { send_ipack(); - if (Count - rest > 0) - write_ipack(Data + rest, Count - rest); + // recurse for remaining data + if (Count - bite > 0) + write_ipack(Data + bite, Count - bite); } } @@ -395,35 +718,42 @@ void cTS2PES::ts_to_pes(const uint8_t *Buf) // don't need count (=188) #define RESULTBUFFERSIZE KILOBYTE(256) -cRemux::cRemux(int VPid, int APid1, int APid2, int DPid1, int DPid2, bool ExitOnFailure) +cRemux::cRemux(int VPid, const int *APids, const int *DPids, const int *SPids, bool ExitOnFailure) { - vPid = VPid; - aPid1 = APid1; - aPid2 = APid2; - dPid1 = DPid1; - dPid2 = DPid2; exitOnFailure = ExitOnFailure; + isRadio = VPid == 0 || VPid == 1 || VPid == 0x1FFF; numUPTerrors = 0; synced = false; skipped = 0; + numTracks = 0; resultSkipped = 0; resultBuffer = new cRingBufferLinear(RESULTBUFFERSIZE, IPACKS, false, "Result"); resultBuffer->SetTimeouts(0, 100); - vTS2PES = new cTS2PES(resultBuffer, IPACKS); - aTS2PES1 = new cTS2PES(resultBuffer, IPACKS, 0xC0); - aTS2PES2 = aPid2 ? new cTS2PES(resultBuffer, IPACKS, 0xC1) : NULL; - dTS2PES1 = dPid1 ? new cTS2PES(resultBuffer, IPACKS) : NULL; - //XXX don't yet know how to tell apart primary and secondary DD data... - dTS2PES2 = /*XXX dPid2 ? new cTS2PES(resultBuffer, IPACKS) : XXX*/ NULL; + if (VPid) + ts2pes[numTracks++] = new cTS2PES(VPid, resultBuffer, IPACKS); + if (APids) { + int n = 0; + while (*APids && numTracks < MAXTRACKS && n < MAXAPIDS) + ts2pes[numTracks++] = new cTS2PES(*APids++, resultBuffer, IPACKS, 0xC0 + n++); + } + if (DPids) { + int n = 0; + while (*DPids && numTracks < MAXTRACKS && n < MAXDPIDS) + ts2pes[numTracks++] = new cTS2PES(*DPids++, resultBuffer, IPACKS, 0x00, 0x80 + n++, new cDolbyRepacker); + } + /* future... + if (SPids) { + int n = 0; + while (*SPids && numTracks < MAXTRACKS && n < MAXSPIDS) + ts2pes[numTracks++] = new cTS2PES(*SPids++, resultBuffer, IPACKS, 0x00, 0x28 + n++); + } + */ } cRemux::~cRemux() { - delete vTS2PES; - delete aTS2PES1; - delete aTS2PES2; - delete dTS2PES1; - delete dTS2PES2; + for (int t = 0; t < numTracks; t++) + delete ts2pes[t]; delete resultBuffer; } @@ -495,11 +825,12 @@ int cRemux::Put(const uchar *Data, int Count) break; // A cTS2PES might write one full packet and also a small rest int pid = GetPid(Data + i + 1); if (Data[i + 3] & 0x10) { // got payload - if (pid == vPid) vTS2PES->ts_to_pes(Data + i); - else if (pid == aPid1) aTS2PES1->ts_to_pes(Data + i); - else if (pid == aPid2 && aTS2PES2) aTS2PES2->ts_to_pes(Data + i); - else if (pid == dPid1 && dTS2PES1) dTS2PES1->ts_to_pes(Data + i); - else if (pid == dPid2 && dTS2PES2) dTS2PES2->ts_to_pes(Data + i); + for (int t = 0; t < numTracks; t++) { + if (ts2pes[t]->Pid() == pid) { + ts2pes[t]->ts_to_pes(Data + i); + break; + } + } } used += TS_SIZE; } @@ -537,7 +868,7 @@ uchar *cRemux::Get(int &Count, uchar *PictureType) // Special VPID case to enable recording radio channels: - if (vPid == 0 || vPid == 1 || vPid == 0x1FFF) { + if (isRadio) { // XXX actually '0' should be enough, but '1' must be used with encrypted channels (driver bug?) // XXX also allowing 0x1FFF to not break Michael Paar's original patch, // XXX but it would probably be best to only use '0' @@ -615,11 +946,8 @@ void cRemux::Del(int Count) void cRemux::Clear(void) { - if (vTS2PES) vTS2PES->Clear(); - if (aTS2PES1) aTS2PES1->Clear(); - if (aTS2PES2) aTS2PES2->Clear(); - if (dTS2PES1) dTS2PES1->Clear(); - if (dTS2PES2) dTS2PES2->Clear(); + for (int t = 0; t < numTracks; t++) + ts2pes[t]->Clear(); resultBuffer->Clear(); } -- cgit v1.2.3