audio_thread音频解码线程分析—ffplay.c源码分析
作者:罗上文,微信:Loken1,公众号:FFmpeg弦外之音
之前在 stream_component_open()
里面的 decode_start()
函数开启了 audio_thread
线程,如下:
audio_thread
线程主要是负责 解码 PacketQueue
队列里面的 AVPacket
的,解码出来 AVFrame
,然后丢给入口滤镜,再从出口滤镜把 AVFrame
读出来,再插入 FrameQueue
队列。
流程图如下:
如上,audio_thread
函数一开始就进入 一个 do{...}while{...}
,不断地调 decoder_decode_frame()
函数来解码出 AVFrame
,然后把 AVFrame
往 入口滤镜 丢,再循环调 av_buffersink_get_frame_flags()
,不断从出口滤镜收割经过 Filter
的AVFrame
,最后调 frame_queue_push()
把 AVFrame
插入 FrameQueue
队列。
但是如果解码出来的 AVFrame
的音频格式与入口滤镜要求的音频格式不一样,会重建滤镜(reconfigure
),如下:
首先,开始的入口滤镜 (audio_filter_src)的音频格式 是直接从解码器实例(avctx)里面取的,如下:
ffplay.c 2648行
is->audio_filter_src.freq = avctx->sample_rate;
is->audio_filter_src.channels = avctx->channels;
is->audio_filter_src.channel_layout = get_valid_channel_layout(avctx->channel_layout, avctx->channels);
is->audio_filter_src.fmt = avctx->sample_fmt;
而解码器实例(avctx)的音频信息又是从 容器层的流信息里面取出来的。
ffpaly.c 2592 行
ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);
所以以下 3 种场景会重新创建滤镜:
1,容器层记录的采样率等信息是错误的,与实际解码出来的不符。
2,解码过程中,中途解码出来的 AVFrame
的采样率,声道数或者采样格式 出现变动,与上一次解码出来的 AVFrame
不一样。
3,进行了快进快退操作,因为快进快退会导致 is->auddec.pkt_serial
递增。详情请阅读《FFplay序列号分析》。我也不知道为什么序列号变了要重建滤镜。
这3种情况,ffplay
都会处理,只要解码出来的 AVFrame
跟入口滤镜的格式不一致,都会重建滤镜,把入口滤镜的格式设置为当前的 AVFrame
的格式,这样滤镜处理才不会出错。
补充:last_serial
变量一开始是 -1
,而 is->auddec.pkt_serial
一开始是 0,所以一开始是必然会执行一次 reconfigure
操作。
由于每次读取出口滤镜的数据,都会用 while
循环把缓存刷完,不会留数据在滤镜容器里面,所以重建滤镜不会导致音频数据丢失。我圈一下代码里面的重点,如下:
audio_thread
线程的逻辑比较简单,复杂的地方都封装在它调用的子函数里面,所以本文简单讲解一下,audio_thread()
里面调用的各个函数的作用。
1,decoder_decode_frame()
,从 PacketQueue
里面解码出来 AVFrame
,此函数会阻塞,直到解码出来 AVFrame
,或者返回错误。这个函数有 3 个返回值。
- 返回 1,获取到
AVFrame
。 - 返回 0 ,获取不到
AVFrame
,0 代表已经解码完MP4的所有AVPacket
。这种情况一般是ffplay
播放完了整个 MP4 文件,窗口画面停在最后一帧。但是由于你可以按 C 键重新循环播放,所以即便返回 0 也不能退出audio_thread
线程。 - 返回 -1,代表
PacketQueue
队列关闭了(abort_request
)。返回-1
会导致audio_thread()
函数用goto the_end
跳出do{}whlle{}
循环,跳出循环之后,audio_thread
线程就会自己结束了。返回-1
通常是因为关闭了ffplay
播放器。
更详细的分析请阅读《decoder_decode_frame函数分析》。
2,configure_audio_filters()
,创建音频滤镜函数,之前在《FFplay音频滤镜分析》已经讲过此函数。
3,av_buffersrc_add_frame()
,往入口滤镜发送 AVFrame
。
4,av_buffersink_get_frame_flags()
,从出口滤镜读取 AVFrame
。
滤镜相关的函数推荐阅读 FFmpeg实战之路 一章的 《FFmpeg的scale滤镜介绍》
5,frame_queue_peek_writable()
,从 FrameQueue
里面取一个可以写的 Frame
出来。此函数也可能会阻塞。
6,frame_queue_push()
,这个函数有点奇怪,他其实不是把之前的 Frame 塞进去队列,而是把队列的写索引值 +1。
跟 FrameQueue
队列相关的函数都在 《FrameQueue队列分析》一文中。
audio_thread()
函数最后还有一个重点,就是当 出口滤镜 结束的时候,finished
就会设置为 非 0 。
if (ret == AVERROR_EOF)
is->auddec.finished = is->auddec.pkt_serial;
提示:只有往入口滤镜发送了 NULL
的 AVFrame
,出口滤镜才会结束。读完数据 跟 结束是两种状态,读完代表滤镜暂时没有数据可读,但是只要再往入口滤镜 发 AVFrame
,出口滤镜就会又有数据可读。
而结束,代表不会再有 AVFrame
往入口滤镜发。
至此,audio_thread()
线程分析完毕。
感谢 NETINT(镕铭微电子) 赞助《FFmpeg原理》免费版一书的服务器费用,下面是 VPU 产品介绍