avcodec_send_packet 与 avcodec_receive_frame 这一对异步接口是在大概ffmpeg3.1 版本的时候出现的,即自这时用户便已经可以直接调用该接口。但是在升级过程中,中间的内部实现过程改动很多,甚至最开始是由异步接口去调用同步接口,而大概 3.4 到 4.3 期间的过渡期,内部实现才有了现在版本的雏形:异步接口去与 decoder 打交道,用户接触的同步接口内部调用两个异步接口。
这样做的好处:我们做一些高清 4K 视频、做一些实时处理等计算复杂、时效性较高的解码时,如果解码器支持可以 send 一个 slice,或者一个 packet (这个 packet 是 frame 的一部分),用户便可以这样做。而旧的同步接口只能等待他全部解码完成才能解除阻塞。由此在 ffmpeg 4.4 后控制权交由用户,旧接口全部废弃。
avcodec_send_packet 部分
当用户在调用 avcodec_send_packet 给 ffmpeg 送 packet 时,一共做了两件事情:
- 将 AVPacket 送给该 AVCodecContext 内置的 AVBitStreamFilterContext 数据缓冲区:解码操作的第一步,FFmpeg 会先将用户输入的 AVPacket,使用av_bsf_send_packet函数传递,通过av_move_ref的方式,转给AVBSFInternal的缓存中。上一小节我们已经描述了AVCodec与AVBitStreamFilter之间的关系,所以无论该decoder是否配置了filter,该缓冲区都会存在,只是filter的形式有所区别。
- 调用 AVBitStreamFilter(如果有) + decoder 实现的解码函数(receive_frame或decode):解码操作的第二步,decode_receive_frame_internal会根据AVCodecInternal->AVFrame是否有已经解好的缓存来判断本次是否需要解码,如果有已经解好的,直接拿过来用,否则即刻动手解码。真正的解码工作就此开始,根据不同decoder的内部实现情况适配解码方案:优先使用decoder内自实现的int (*receive_frame)(struct AVCodecContext *avctx, struct AVFrame *frame);的函数。
若该函数指针为空,回落至通用的decode_simple_internal函数,该函数的主要处理流程为:
- 过滤数据包:av_bsf_receive_packet,拿到从decoder内的av_bsf_get_null_filter的空链表直接返回或经过BSF过滤的AVPacket。
- draining_done状态判断:如果是,及时喊停,返回给上层EOF。
- 开始解码:如果定义了AVCodecContext->thread_type(这里后面会有介绍),则通过对应的多线程并行方式解码,否则直接调用decoder->decode实现的指针。
- 其他处理细节:判断视频是否丢帧;音频丢弃采样数;赋值该帧其他参数,如size、pts、duration等。
解码完成后,将其输入给 cache…
avcodec_receive_frame 部分
send 完成后,receive_frame 的工作变得分外简单:取回帧,所以只需要将刚刚已经放到cache中Frame,move reference给出参即可。若cache没有解码完成(可能是上一个没有解完),则转向复用send内已经调用过的decode_receive_frame_internal亲自解码。
收到真正的帧,在返回前夕会对帧进行处理:通过av_frame_apply_cropping工具函数进行裁剪;判断AVCodecCtx->flag中的AV_CODEC_FLAG_DROPCHANGED,如果帧参数有变,返回AVERROR_INPUT_CHANGED
关于flush与drain的一点讨论
draining的概念是编解码环节中必不可少的一环,目的是告诉编/解码器,“我已经将所有帧都送给你了,后续不会再有新的帧输入和作为参考了,下次请直接将剩下的帧返回吧”。
无论是decoder还是encoder,ffmpeg对于drain的操作都是一样的:让用户在avcodec_send_packet / frame的接口中输入空包/空帧以示此意。收到此指令后,ffmpeg会将decoder或encoder对应的AVCodecContext置为draining状态,此后整个上下文都无法再去输入新的包/帧(若有输入会在send时返回EOF),仅能吐帧,并在所有帧吐完后变为draining_done状态,无法复还成正常的编/解码状态。也就是说,drain一个AVCodecContext是不可逆的,只能通过重新分配上下文,reopen编解码器的方式重新编/解码。
如果不想以drain的方式,去flush一个de(en)coder,可以通过函数avcodec_flush_buffers来清空AVCodecContext的解码状态,该函数内部主要做了以下几个事:
● 清空AVCodecContext内与draining有关的状态,复原成初始态
● 清空AVCodecInternal所有的缓存帧/缓存包
● 若是decoder的上下文,还会顺带刷新AVCodecInternal->AVBSFContext
● 调用AVCodec实现的void (*flush)(struct AVCodecContext *);函数,或多线程编解码实现的ff_thread_flush
所以avcodec_flush_buffers只是相当于重置了整个解码的上下文和缓存,可以seek到某一处,继续编解码。注意的是,flush后重新seek或demux时,要输入一个I帧,若seek任意位置,给decoder一个P帧可能会出现解码器找不到引用的报错,返回invalid data。
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。