C# 版 flvmerge:快速合併多個flv檔案

xiaotie發表於2013-11-25

網上的視訊很多都是分片的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 檔案由1header和若干個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 來處理。

 

相關文章