1.问题描述
1.1 背景
之前基于ffmpeg
做二次开发,完成常见的视频处理功能,并用ffmpeg
命令行做兜底。在此基础上,还做一个转码接入和调度系统对外提供服务。有个功能需要是这样的:快速从指定的视频中裁剪某一时间范围的子视频, 两个要求:1. 要快,不能像转码一样耗时;2.要精确,剪辑的时候能指定从哪一秒开始,到哪一秒结束。
1.2 难点
用ffmpeg
很容易从一个长视频剪辑出一段小视频。比如命令ffmpeg -i input.mp4 -ss 00:10:03 -t 00:03:00 -vcodec copy -acodec copy output.mp4
就是从input.mp4
的第10分钟03秒开始剪辑出一个3分钟的视频并且保存为output.mp4
文件。参数-vcodec copy -acodec copy
就是直接拷贝原始视频的音视频流,不进行编解码。虽然上面的方法很方便,但有一个致命的缺陷:画面在一开始会卡住(但声音一直是正常的),几秒后画面才正常滚动。下面视频是一个例子。
2.原因分析
究其原因,剪辑的开始时间落在视频GOP
的中间位置而不是第一个I
帧。稍微了解过视频编码的同学应该都听过I
、B
、P
帧。简单来说,I
帧是一张完整的图像,P
帧则根据I帧
做差分编码,B
帧根据前后的I
、P
、B
帧作差分编码。也就是说I
帧具有完整的内容,而P
和B
帧不具有,所以如果缺少I
帧,那么P
和B
帧是不能正常解码的。通常来说,一个GOP
里面第一帧是I
帧,后面是若干个P
和B
帧。一个GOP
长达10秒都是有可能的。下图是一个真实视频的I
、B
、P
帧信息图,红色的表示I
帧,可以看到两个I
帧相隔深远(实际是隔了10秒)。
从上面分析可知:剪辑的开始时间很大可能不是落在I
帧,由于缺少I
帧会使得后面的P
和B
帧无法解码导致画面卡住。上面的分析都是基于不编解码的直接拷贝视频内容的,如果考虑先解码成一张张的图像,然后再对符合时间要求的图像编码,那么剪辑时间可以做到非常精准。但这样做的就是耗时过长:需求花费大量的CPU完成编解码操作。
3.解决方案
解决的办法还是有的:对前面第一个符合时间要求的GOP
编解码,而之后的GOP
内容则直接拷贝到目标视频。一来,第一个GOP
的帧由于是重新编码所以会重新分配I
帧从而能播放,二来,之后的GOP
内容是直接拷贝的所以基本不消耗CPU,性能杠杆的。如下图所示:
当然这里面还是有一些坑的,下面开始填坑。
3.1 拼接
源视频可能会惊讶:我凭本事编的码,为什么你直接拷贝就能解码?一般来说解码依赖于SPS
和PPS
,而源视频与目标视频的SPS
和PPS
会有所不同,因此直接拷贝是不能正确解码的。对于mp4
文件,SPS
和PPS
一般是放到文件头。一个文件只能有一个文件头,也就不能存放两个不同的SPS
和PPS
。为了能正确解码目标视频必须得有源视频的SPS
和PPS
。不能放文件头的话,那能放哪里?能不能放到拷贝的帧的前面呢?如何放?一筹莫展、无处下手,直到有一天突然想起之前为了填一个坑,追踪到h264_mp4toannexb
的实现,它的作用就是将SPS
和PPS
拷贝到帧(准确来说应该是AVPacket
)的前面。来!温习一下h264_mp4toannexb的具体实现:在所有AVPacket
前面增加0x000001
或者0x00000001
,在I
帧的前面插入SPS
和PPS
。也就是通过h264_mp4toexannb
就能把解码所需的SPS
和PPS
正确插入到视频中。h264_mp4toannexb
使用起来也比较简单,代码如下:
AVBSFContext* initBSF(const std::string &filter_name, const AVCodecParameters *codec_par, AVRational tb)
{
const AVBitStreamFilter *filter = av_bsf_get_by_name(m_filter_name.c_str());
AVBSFContext *bsf_ctx = nullptr;
av_bsf_alloc(filter, &bsf_ctx);
avcodec_parameters_copy(bsf_ctx->par_in, codec_par);
bsf_ctx->time_base_in = tb;
av_bsf_init(bsf_ctx);
return bsf_ctx;
}
AVPacket* feedPacket(AVBSFContext *bsf_ctx, AVPacket &packet)
{
av_bsf_send_packet(bsf_ctx, packet);
AVPacket *dst_packet = av_packet_alloc();
av_bsf_receive_packet(bsf_ctx, dst_packet);
return dst_packet;
}
void test()
{
AVBSFContext *bsf_ctx = initBSF("h264_mp4toannexb", video_stream->codecpar, video_stream->time_base);
AVPacket *packet = readVideoPacket();
AVPacket *dst_packet = feedPacket(bsf_ctx, packet);
}
注意:编解码第一个GOP
和原始视频后续GOP
拼接时的时间戳要小心处理,不然视频播放时可能会出现抖动现象。
3.2 花屏
以为就完了吗?没有!!你会发现有些视频会在最后一秒出现花屏。。。。
出现花屏的原因其实也不难猜到:最后一帧是B
帧。由于不是所有剪辑的视频最后一帧都是B
帧,所以花屏也不是必现的。知道是B
帧引起的,那解决方案也就明确了:保证最后一帧是P
帧。即使时间上稍微超一点(音频流也应该跟着视频流稍微超一下时间)。不过呢,由于不能直接从AVPacket
判断一个帧是否为P
帧,所以最后一个GOP
也得解码(无需编码)。记录超出时间范围后的第一个P
帧的pts
,后面拷贝GOP
的时候,拷贝到这个pts
就可以停止了。
4.总结
起初觉得问题很难解决,毕竟ffmpeg
命令行都裁剪出来的都有问题。而万变不离其宗,从问题的原因出发,一步步寻找解决方案,并将一路上碰到的问题逐一击破。记住,明白原理才能解决问题。
原文 如何使用FFmpeg精确剪辑视频 - 知乎
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓文章来源:https://www.toymoban.com/news/detail-655318.html
文章来源地址https://www.toymoban.com/news/detail-655318.html
到了这里,关于如何使用FFmpeg精确剪辑视频的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!