網上的視訊很多都是分片的flv檔案,怎麼把他們合為一體呢?GUI工具就不考慮了,不适合批量執行,不适合在背景運作。有沒有指令行工具或庫可以實作呢?本文提供了C# 版 flvmerge 程式,能夠快速合并多個flv檔案。
網上的視訊很多都是分片的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 來處理。
版權所有,歡迎轉載