網上的視訊很多都是分片的flv檔案,怎麼把他們合為一體呢?GUI工具就不考慮了,不適合批量執行,不適合在後臺執行。有沒有命令列工具或庫可以實現呢?
ffmpeg 提供了一個方法:
(1)先把flv檔案轉換成mpeg;
(2)將多個mpeg檔案合併成1個獨立的mpeg檔案(二進位制合併即可)
(3)將獨立的mpeg檔案轉換成獨立的flv檔案。
網上搜到的最多的也是這種解決辦法。這種方法有兩個缺點:
(1)需要兩遍轉碼,非常耗時;
(2)轉換後的獨立的mpeg檔案比原視訊要短一點點。
木有辦法了,只好另尋他路。有人說有一個flvmerge.exe 程式可以將多個flv合併成一個,可惜的是俺搜了很久,都沒找到這個程式,最後還是在一款免費軟體裡把這個“flvmerge.exe”檔案給揪出來了,不幸的是,這個“flvmerge.exe”得不到正確的結果。
潤之同學說過,自己動手,豐衣足食。上 github 上搜“flvmerge”,發現兩個專案,“flvmerge”和“flvmerger”,都是C寫的。前者不依賴於第三方庫,後者依賴於第三方庫,那麼就從第一個開始吧。
看了看它的程式碼,知道了flv檔案合併的原理:
(1) flv 檔案由1個header和若干個tag組成;
(2) header記錄了視訊的後設資料;
(3) tag 是有時間戳的資料;
(4) flv合併的原理就是把多個檔案裡的tag組裝起來,調整各tag的時間戳,再在檔案起始處按個頭部。
下面是我參照 flvmerge 專案,用linqpad寫的一個C#版本的 flvmerge 程式碼:
1 void Main() 2 { 3 String path1 = "D:\\Videos\\Subtitle\\OutputCache\\1.flv"; 4 String path2 = "D:\\Videos\\Subtitle\\OutputCache\\2.flv"; 5 String path3 = "D:\\Videos\\Subtitle\\OutputCache\\3.flv"; 6 String output = "D:\\Videos\\Subtitle\\OutputCache\\output.flv"; 7 8 using(FileStream fs1 = new FileStream(path1, FileMode.Open)) 9 using(FileStream fs2 = new FileStream(path2, FileMode.Open)) 10 using(FileStream fs3 = new FileStream(path3, FileMode.Open)) 11 using(FileStream fsMerge = new FileStream(output, FileMode.Create)) 12 { 13 Console.WriteLine(IsFLVFile(fs1)); 14 Console.WriteLine(IsFLVFile(fs2)); 15 Console.WriteLine(IsFLVFile(fs3)); 16 17 if(IsSuitableToMerge(GetFLVFileInfo(fs1),GetFLVFileInfo(fs2)) == false 18 || IsSuitableToMerge(GetFLVFileInfo(fs1),GetFLVFileInfo(fs3)) == false) 19 { 20 Console.WriteLine("Video files not suitable to merge"); 21 } 22 23 int time = Merge(fs1,fsMerge,true,0); 24 time = Merge(fs2,fsMerge,false,time); 25 time = Merge(fs3,fsMerge,false,time); 26 Console.WriteLine("Merge finished"); 27 } 28 } 29 30 const int FLV_HEADER_SIZE = 9; 31 const int FLV_TAG_HEADER_SIZE = 11; 32 const int MAX_DATA_SIZE = 16777220; 33 34 class FLVContext 35 { 36 public byte soundFormat; 37 public byte soundRate; 38 public byte soundSize; 39 public byte soundType; 40 public byte videoCodecID; 41 } 42 43 bool IsSuitableToMerge(FLVContext flvCtx1, FLVContext flvCtx2) 44 { 45 return (flvCtx1.soundFormat == flvCtx2.soundFormat) && 46 (flvCtx1.soundRate == flvCtx2.soundRate) && 47 (flvCtx1.soundSize == flvCtx2.soundSize) && 48 (flvCtx1.soundType == flvCtx2.soundType) && 49 (flvCtx1.videoCodecID == flvCtx2.videoCodecID); 50 } 51 52 bool IsFLVFile(FileStream fs) 53 { 54 int len; 55 byte[] buf = new byte[FLV_HEADER_SIZE]; 56 fs.Position = 0; 57 if( FLV_HEADER_SIZE != fs.Read(buf,0,buf.Length)) 58 return false; 59 60 if (buf[0] != 'F' || buf[1] != 'L' || buf[2] != 'V' || buf[3] != 0x01) 61 return false; 62 else 63 return true; 64 } 65 66 FLVContext GetFLVFileInfo(FileStream fs) 67 { 68 bool hasAudioParams, hasVideoParams; 69 int skipSize, readLen; 70 int dataSize; 71 byte tagType; 72 byte[] tmp = new byte[FLV_TAG_HEADER_SIZE+1]; 73 if (fs == null) return null; 74 75 FLVContext flvCtx = new FLVContext(); 76 fs.Position = 0; 77 skipSize = 9; 78 fs.Position += skipSize; 79 hasVideoParams = hasAudioParams = false; 80 skipSize = 4; 81 while (!hasVideoParams || !hasAudioParams) 82 { 83 fs.Position += skipSize; 84 85 if (FLV_TAG_HEADER_SIZE+1 != fs.Read(tmp,0,tmp.Length)) 86 return null; 87 88 tagType = (byte)(tmp[0] & 0x1f); 89 switch (tagType) 90 { 91 case 8 : 92 flvCtx.soundFormat = (byte)((tmp[FLV_TAG_HEADER_SIZE] & 0xf0) >> 4) ; 93 flvCtx.soundRate = (byte)((tmp[FLV_TAG_HEADER_SIZE] & 0x0c) >> 2) ; 94 flvCtx.soundSize = (byte)((tmp[FLV_TAG_HEADER_SIZE] & 0x02) >> 1) ; 95 flvCtx.soundType = (byte)((tmp[FLV_TAG_HEADER_SIZE] & 0x01) >> 0) ; 96 hasAudioParams = true; 97 break; 98 case 9 : 99 flvCtx.videoCodecID = (byte)((tmp[FLV_TAG_HEADER_SIZE] & 0x0f)); 100 hasVideoParams = true; 101 break; 102 default : 103 break; 104 } 105 106 dataSize = FromInt24StringBe(tmp[1],tmp[2],tmp[3]); 107 skipSize = dataSize - 1 + 4; 108 } 109 110 return flvCtx; 111 } 112 113 int FromInt24StringBe(byte b0, byte b1, byte b2) 114 { 115 return (int)((b0<<16) | (b1<<8) | (b2)); 116 } 117 118 int GetTimestamp(byte b0, byte b1, byte b2, byte b3) 119 { 120 return ((b3<<24) | (b0<<16) | (b1<<8) | (b2)); 121 } 122 123 void SetTimestamp(byte[] data, int idx, int newTimestamp) 124 { 125 data[idx + 3] = (byte)(newTimestamp>>24); 126 data[idx + 0] = (byte)(newTimestamp>>16); 127 data[idx + 1] = (byte)(newTimestamp>>8); 128 data[idx + 2] = (byte)(newTimestamp); 129 } 130 131 int Merge(FileStream fsInput, FileStream fsMerge, bool isFirstFile, int lastTimestamp = 0) 132 { 133 int readLen; 134 int curTimestamp = 0; 135 int newTimestamp = 0; 136 int dataSize; 137 byte[] tmp = new byte[20]; 138 byte[] buf = new byte[MAX_DATA_SIZE]; 139 140 fsInput.Position = 0; 141 if (isFirstFile) 142 { 143 if(FLV_HEADER_SIZE+4 == (fsInput.Read(tmp,0,FLV_HEADER_SIZE+4))) 144 { 145 fsMerge.Position = 0; 146 fsMerge.Write(tmp,0,FLV_HEADER_SIZE+4); 147 } 148 } 149 else 150 { 151 fsInput.Position = FLV_HEADER_SIZE + 4; 152 } 153 154 while(fsInput.Read(tmp, 0, FLV_TAG_HEADER_SIZE) > 0) 155 { 156 dataSize = FromInt24StringBe(tmp[1],tmp[2],tmp[3]); 157 curTimestamp = GetTimestamp(tmp[4],tmp[5],tmp[6],tmp[7]); 158 newTimestamp = curTimestamp + lastTimestamp; 159 SetTimestamp(tmp,4, newTimestamp); 160 fsMerge.Write(tmp,0,FLV_TAG_HEADER_SIZE); 161 162 readLen = dataSize+4; 163 if (fsInput.Read(buf,0,readLen) > 0) { 164 fsMerge.Write(buf, 0, readLen); 165 } else { 166 goto failed; 167 } 168 } 169 170 return newTimestamp; 171 172 failed: 173 throw new Exception("Merge Failed"); 174 }
測試通過,合併速度很快!
不過,這個方法有一個缺點:沒有將各個檔案裡的關鍵幀資訊合併,這個關鍵幀資訊,切分flv檔案時很重要,合併時就沒那麼重要了。如果確實需要的話,可以用 yamdi 來處理。