如何使用FFmpeg精确剪辑视频

·  阅读 1852
如何使用FFmpeg精确剪辑视频

​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帧。稍微了解过视频编码的同学应该都听过IBP帧。简单来说,I帧是一张完整的图像,P帧则根据I帧做差分编码,B帧根据前后的IPB帧作差分编码。也就是说I帧具有完整的内容,而PB帧不具有,所以如果缺少I帧,那么PB帧是不能正常解码的。通常来说,一个GOP里面第一帧是I帧,后面是若干个PB帧。一个GOP长达10秒都是有可能的。下图是一个真实视频的IBP帧信息图,红色的表示I帧,可以看到两个I帧相隔深远(实际是隔了10秒)。

        从上面分析可知:剪辑的开始时间很大可能不是落在I帧,由于缺少I帧会使得后面的PB帧无法解码导致画面卡住。上面的分析都是基于不编解码的直接拷贝视频内容的,如果考虑先解码成一张张的图像,然后再对符合时间要求的图像编码,那么剪辑时间可以做到非常精准。但这样做的就是耗时过长:需求花费大量的CPU完成编解码操作。

3.解决方案

        解决的办法还是有的:对前面第一个符合时间要求的GOP编解码,而之后的GOP内容则直接拷贝到目标视频。一来,第一个GOP的帧由于是重新编码所以会重新分配I帧从而能播放,二来,之后的GOP内容是直接拷贝的所以基本不消耗CPU,性能杠杆的。如下图所示:

当然这里面还是有一些坑的,下面开始填坑。

3.1 拼接

        源视频可能会惊讶:我凭本事编的码,为什么你直接拷贝就能解码?一般来说解码依赖于SPSPPS,而源视频与目标视频的SPSPPS会有所不同,因此直接拷贝是不能正确解码的。对于mp4文件,SPSPPS一般是放到文件头。一个文件只能有一个文件头,也就不能存放两个不同的SPSPPS。为了能正确解码目标视频必须得有源视频的SPSPPS。不能放文件头的话,那能放哪里?能不能放到拷贝的帧的前面呢?如何放?一筹莫展、无处下手,直到有一天突然想起之前为了填一个坑,追踪到h264_mp4toannexb的实现,它的作用就是将SPSPPS拷贝到帧(准确来说应该是AVPacket)的前面。来!温习一下h264_mp4toannexb的具体实现:在所有AVPacket前面增加0x000001或者0x00000001,在I帧的前面插入SPSPPS。也就是通过h264_mp4toexannb就能把解码所需的SPSPPS正确插入到视频中。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命令行都裁剪出来的都有问题。而万变不离其宗,从问题的原因出发,一步步寻找解决方案,并将一路上碰到的问题逐一击破。记住,明白原理才能解决问题。

分类:
后端
标签:
分类:
后端
标签: