网上的视频很多都是分片的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 代码:
void Main()
{
String path1 = "D:\\Videos\\Subtitle\\OutputCache\\1.flv";
String path2 = "D:\\Videos\\Subtitle\\OutputCache\\2.flv";
String path3 = "D:\\Videos\\Subtitle\\OutputCache\\3.flv";
String output = "D:\\Videos\\Subtitle\\OutputCache\\output.flv"; using(FileStream fs1 = new FileStream(path1, FileMode.Open))
using(FileStream fs2 = new FileStream(path2, FileMode.Open))
using(FileStream fs3 = new FileStream(path3, FileMode.Open))
using(FileStream fsMerge = new FileStream(output, FileMode.Create))
{
Console.WriteLine(IsFLVFile(fs1));
Console.WriteLine(IsFLVFile(fs2));
Console.WriteLine(IsFLVFile(fs3)); if(IsSuitableToMerge(GetFLVFileInfo(fs1),GetFLVFileInfo(fs2)) == false
|| IsSuitableToMerge(GetFLVFileInfo(fs1),GetFLVFileInfo(fs3)) == false)
{
Console.WriteLine("Video files not suitable to merge");
} int time = Merge(fs1,fsMerge,true,);
time = Merge(fs2,fsMerge,false,time);
time = Merge(fs3,fsMerge,false,time);
Console.WriteLine("Merge finished");
}
} const int FLV_HEADER_SIZE = ;
const int FLV_TAG_HEADER_SIZE = ;
const int MAX_DATA_SIZE = ; class FLVContext
{
public byte soundFormat;
public byte soundRate;
public byte soundSize;
public byte soundType;
public byte videoCodecID;
} bool IsSuitableToMerge(FLVContext flvCtx1, FLVContext flvCtx2)
{
return (flvCtx1.soundFormat == flvCtx2.soundFormat) &&
(flvCtx1.soundRate == flvCtx2.soundRate) &&
(flvCtx1.soundSize == flvCtx2.soundSize) &&
(flvCtx1.soundType == flvCtx2.soundType) &&
(flvCtx1.videoCodecID == flvCtx2.videoCodecID);
} bool IsFLVFile(FileStream fs)
{
int len;
byte[] buf = new byte[FLV_HEADER_SIZE];
fs.Position = ;
if( FLV_HEADER_SIZE != fs.Read(buf,,buf.Length))
return false; if (buf[] != 'F' || buf[] != 'L' || buf[] != 'V' || buf[] != 0x01)
return false;
else
return true;
} FLVContext GetFLVFileInfo(FileStream fs)
{
bool hasAudioParams, hasVideoParams;
int skipSize, readLen;
int dataSize;
byte tagType;
byte[] tmp = new byte[FLV_TAG_HEADER_SIZE+];
if (fs == null) return null; FLVContext flvCtx = new FLVContext();
fs.Position = ;
skipSize = ;
fs.Position += skipSize;
hasVideoParams = hasAudioParams = false;
skipSize = ;
while (!hasVideoParams || !hasAudioParams)
{
fs.Position += skipSize; if (FLV_TAG_HEADER_SIZE+ != fs.Read(tmp,,tmp.Length))
return null; tagType = (byte)(tmp[] & 0x1f);
switch (tagType)
{
case :
flvCtx.soundFormat = (byte)((tmp[FLV_TAG_HEADER_SIZE] & 0xf0) >> ) ;
flvCtx.soundRate = (byte)((tmp[FLV_TAG_HEADER_SIZE] & 0x0c) >> ) ;
flvCtx.soundSize = (byte)((tmp[FLV_TAG_HEADER_SIZE] & 0x02) >> ) ;
flvCtx.soundType = (byte)((tmp[FLV_TAG_HEADER_SIZE] & 0x01) >> ) ;
hasAudioParams = true;
break;
case :
flvCtx.videoCodecID = (byte)((tmp[FLV_TAG_HEADER_SIZE] & 0x0f));
hasVideoParams = true;
break;
default :
break;
} dataSize = FromInt24StringBe(tmp[],tmp[],tmp[]);
skipSize = dataSize - + ;
} return flvCtx;
} int FromInt24StringBe(byte b0, byte b1, byte b2)
{
return (int)((b0<<) | (b1<<) | (b2));
} int GetTimestamp(byte b0, byte b1, byte b2, byte b3)
{
return ((b3<<) | (b0<<) | (b1<<) | (b2));
} void SetTimestamp(byte[] data, int idx, int newTimestamp)
{
data[idx + ] = (byte)(newTimestamp>>);
data[idx + ] = (byte)(newTimestamp>>);
data[idx + ] = (byte)(newTimestamp>>);
data[idx + ] = (byte)(newTimestamp);
} int Merge(FileStream fsInput, FileStream fsMerge, bool isFirstFile, int lastTimestamp = )
{
int readLen;
int curTimestamp = ;
int newTimestamp = ;
int dataSize;
byte[] tmp = new byte[];
byte[] buf = new byte[MAX_DATA_SIZE]; fsInput.Position = ;
if (isFirstFile)
{
if(FLV_HEADER_SIZE+ == (fsInput.Read(tmp,,FLV_HEADER_SIZE+)))
{
fsMerge.Position = ;
fsMerge.Write(tmp,,FLV_HEADER_SIZE+);
}
}
else
{
fsInput.Position = FLV_HEADER_SIZE + ;
} while(fsInput.Read(tmp, , FLV_TAG_HEADER_SIZE) > )
{
dataSize = FromInt24StringBe(tmp[],tmp[],tmp[]);
curTimestamp = GetTimestamp(tmp[],tmp[],tmp[],tmp[]);
newTimestamp = curTimestamp + lastTimestamp;
SetTimestamp(tmp,, newTimestamp);
fsMerge.Write(tmp,,FLV_TAG_HEADER_SIZE); readLen = dataSize+;
if (fsInput.Read(buf,,readLen) > ) {
fsMerge.Write(buf, , readLen);
} else {
goto failed;
}
} return newTimestamp; failed:
throw new Exception("Merge Failed");
}
测试通过,合并速度很快!
不过,这个方法有一个缺点:没有将各个文件里的关键帧信息合并,这个关键帧信息,切分flv文件时很重要,合并时就没那么重要了。如果确实需要的话,可以用 yamdi 来处理。