本次尝试在视频A中的任意位置插入视频B.shell
在上一篇中,咱们经过调整PTS能够实现视频的加减速。这只是对同一个视频的调转,本次咱们尝试对多个视频进行合并处理。ide
ffmpeg提供了一个concat
滤镜来合并多个视频,例如:要合并视频Video A和Video B,经过调用指针
ffmpeg -i va.mp4 -i vb.mp4 -filter_complex "[0][1]concat[out]" -map '[out]' -y output.mp4
concat
支持多个Input Source,上面的命令只合并了两个视频,经过生成concat
流程图能够看到一些细节:code
echo "movie=va.mp4[0];movie=vb.mp4[1];[0][1]concat,nullsink" | graph2dot -o graph.tmp dot -Tpng graph.tmp -o graph.png
这是concat
典型用法,循环读取输入源,而后经过修改pts完成合并。视频
concat
是顺序修改,若是须要在video A中某个时间点插入video B,那么concat
就没法完成了。 顺序合并是经过修改PTS实现,那么变序合并也能够经过修改PTS来实现,下面借助concat
的逻辑来看看如何实现变序合并。blog
为了方便说明问题,咱们来看一下顺序和变序不一样点到底在哪里。get
咱们仍然假设须要合并的两个视频分别是Video A和Video B, 须要将Video B插入在Video A中。AF表示Video A的帧, BF表示Video B的帧。class
顺序合并ffmpeg
+---------------------------------------------------------------------------------------------------------------+ | AF1 AF2 AF3 AF4 AF5 AF6 AF7 BF1 BF2 BF3 BF4 BF5 BF6 | | |--------------|--------------|--------------|--------------|--------------|--------------|---> | |Time 0 10 20 30 40 50 60 | |PTS 0 100 200 250 300 350 400 500 600 650 700 750 800 | +---------------------------------------------------------------------------------------------------------------+
顺序合并就是读取Video B的帧,而后将pts以Video A结束时的PTS为基准进行修改。循环
变序合并
+---------------------------------------------------------------------------------------------------------------+ | AF1 AF2 AF3 AF4 BF1 BF2 BF3 BF4 BF5 BF6 AF5 AF6 AF7 | | |--------------|--------------|--------------|--------------|--------------|--------------|---> | |Time 0 10 20 30 40 50 60 | |PTS 0 100 200 250 300 350 400 500 600 650 700 750 800 | +---------------------------------------------------------------------------------------------------------------+
变序合并时先读取Video A的帧,当达到规定的PTS时,开始读取Video B的帧,而后以A截断
时的PTS为基准从新计算PTS。当Video B全部的帧都处理完毕以后,在从截断
处开始从新处理Video A的帧。
从上面两个图来看,问题好像不是很难解决。 只要达到截断
的条件,就去处理另一个视频,等待视频处理完毕以后。再返回来处理被截断
的视频。
但在实现的道路上有以下三个问题须要解决:
下面就须要逐个问题解决了。
由于咱们是须要在视频A中插入视频B,因此须要首先找到插入点。 而根据时间来判断插入点无疑是最简单的一种形式,计算时间就能够依靠前几篇中介绍的PTS知识了。
当从视频源中读取到每帧后,咱们经过帧的PTS和Time-Base根据pts * av_q2d(time_base)
转换成播放时间。 这样第一个问题就顺利解决。
当找到插入点后,咱们须要暂存当前的位置,等待插入结束后,须要从断点处从新加载帧。
执行插入本质就是读取视频B的数据帧,而后修改PTS值。但咱们须要得知视频B已经处理完毕,这样才能返回到视频A的断点处继续处理。 因此如何获取到视频处理完毕就是第二个问题。
若是抛开ffmpeg来讲,处理视频本质也是一个IO流(从视频文件中读取的IO流),当判断到IO流结束时(经过seek来判断EOF)时就是视频处理完毕的时候。 但ffmpeg将这一层屏蔽掉了,也就是在filter中是没法直接获取到IO流状态的。
ffmpeg在屏蔽的同时,也提供了一种判断方式。filter在处理完每一帧以后,须要确认下一帧的状态(有下一帧/无下一帧),因此若是ffmpeg在读取到下一帧时返回了无下一帧,那就表示当前视频处理完毕。
经过ff_inlink_acknowledge_status(AVFilterLink *link, int *rstatus, int64_t *rpts)
来获取下一帧的状态,当返回的ret>0表示没有下一帧,这个时候就能够经过判断当前处理状态来决定是否关闭输出流。
if 当前处理视频B 切换到视频A的断点 else 当前处理视频A 关闭全部的输入流 关闭输出流
这是最后一个待解决的问题了,当视频B的数据都处理完以后,就须要从视频A的断点处从新读取数据帧。上面说到对视频流的读取,本质就是对一个文件的IO流处理,而在IO时都会有一个指针来表示当前位置。
而ff_inlink_acknowledge_status
有两个做用,一方面获取下一帧,另外一方面是确认当前帧处理结束。 换言之,当调用ff_inlink_acknowledge_status
以后,ffmpeg会将IO流的指针向后移动到下一帧的起始位置,若是移动失败,则表示没有下一帧了。 若是移动成功,那么下次ff_inlink_consume_frame
读取帧时,就从这个位置开始读取。
所以如何从断点处从新读取Frame
其实不是问题,只要断点处的帧被确认处理结束了,ffmpeg会自动的移到下一帧位置。当咱们将输入源切换到视频A时,就自动
从断点处开始读取帧了。
经过下面的伪代码简要描述上述的过程:
经过ff_outlink_get_status判断输出流状态 if 输出流已关闭 退出 for { 经过ff_inlink_consume_frame 获取下一帧 经过frame->pts * av_q2d(time_base)计算时间 if 时间达到插入点 修改当前状态, 进入暂存状态。 经过push_frame处理每一帧 } 经过ff_inlink_acknowledge_status确认帧状态 if 当前是暂存状态 切换到视频B if 没有下一帧 if 当前是视频B && 当前是暂存状态 关闭视频B 切换回视频A if 当前是视频A && 当前是暂存状态 关闭视频A 关闭输出流
大体就是这个处理流程, 完整代码能够参考iconcat
里面的代码。