decoder_decode_frame解码函数分析—ffplay.c源码分析
作者:罗上文,微信:Loken1,公众号:FFmpeg弦外之音
decoder_decode_frame()
其实是一个通用的解码函数,可以解码 音频,视频,字幕的 AVPacket
。不过本文主要侧重于分析音频流的解码,但其他的流也是类似的逻辑。
decoder_decode_frame()
函数的参数如下:
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub)
1,Decoder *d
,这个 Decoder
结构,我把它称为解码管理实例。由于 Decoder
结构里面有解码器实例 跟 PacketQueue
队列,所以只需要传递 Decoder
给 decoder_decode_frame()
函数就能进行解码了。如下:
typedef struct Decoder {
...
PacketQueue *queue; // AVPacket 队列
AVCodecContext *avctx; //解码器实例
...
} Decoder
2, AVFrame *frame
,用来存储解码出来的音频或者视频的 AVFrame
。
3, AVSubtitle *sub
,用来存储解码出来的字幕数据,字幕流的使用的数据结构不是 AVFrame
,而是 AVSubtitle
。
decoder_decode_frame()
函数的流程图如下:
decoder_decode_frame()
函数的代码逻辑跟我上面画的流程图的顺序有点不一样,但是整体的逻辑就是这么一个逻辑:从 PacketQueue
队列拿数据去解码。
下面来分析一下 decoder_decode_frame()
的代码,如下:
从上图可以看到,一开始就会用 avcodec_receive_frame()
去解码器读数据,这里读者可能会有疑问,明明都还没往解码器发送 AVPacket
, avcodec_receive_frame()
函数怎么可能读取到 AVFrame
呢?
答:没错,就是读取不到,因为还没发 AVPacket
给解码器解码。所以,首次 avcodec_receive_frame()
必然返回 EAGAIN
,所以就会跳出这个 do{}while{}
循环。
跳出第一个 do{}while{}
循环之后,就会进入第二个 do{}while{}
循环,如下:
虽然代码比较少,但是句句都是重点。
1,SDL_CondSignal(d->empty_queue_cond)
,首先,如果 PacketQueue
队列里面如果没有数据可读了,就需要唤醒 read_thread()
线程来读数据,之前说过, empty_queue_cond
实际上就是 continue_read_thread
,这两个指针都指向同一个条件变量。
2,判断之前发送 AVPacket
给解码器是否失败了?如果失败,d->packet_peding
会是 1。如果上次失败了,d->pkt
本身就是有值的,就不需要重队列里面拿数据,直接把 d->pkt
发送给解码器即可。
3,调用 packet_queue_get()
从 队列读取 AVPacket
。简单讲解一下 packet_queue_get()
函数的参数,定义如下:
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
int block
是控制packet_queue_get()
函数阻塞读取的,如果PacketQueue
队列里面如果没有数据可读,可以一直阻塞等到read_thread()
线程读到数据放进去队列 为止。AVPacket *pkt
,用来放从队列读取到的AVPacket
。int *serial
,读取到的AVPacket
的序列号,这是一个返回值。传的是指针。
PacketQueue
队列是一个 FIFO 的内存管理器,存的是 MyAVPacketList
,如下:
typedef struct MyAVPacketList {
AVPacket *pkt;
int serial;
} MyAVPacketList;
也就是说,队列里的每一个 AVPacket
都有一个序列号的。
再回到 decoder_decode_frame()
调用 packet_queue_get()
时的传参,如下:
可以看到从队列取出来的 AVPacket
就放在 d->pkt
里面,而序列号就放在 d->pkt_serial
。
FFplay播放器其实有 3 种序列号:
1, MyAVPacketList
的 serial
,队列里面的 AVPacket
的序列号。可以看成是临时值,旧值。
2,PackeQueue
的 serial
,这是队列本身的序列号。可以看成是最新的序列号的值。
3,Frame
的 serial
,本文不需要关注这个,在《FFplay序列号分析》一文会详细讲解。
MyAVPacketList
的 serial
就是用PackeQueue
队列的 serial
来赋值的 。如下代码:只要不进行跳转播放,他们的序列号就是一样的。
当快进快退,或者跳转播放时间点的时候,PackeQueue
队列的序列号就会 +1
,而之前已经放进去队列的 MyAVPacketList
的序列号则保持不变。
举个例子,当前MP4已经播放到了 第20秒的时刻,此时 PackeQueue
队列缓存了 5 帧第 21秒的数据,此时,我快进30秒,跳转到第 50 秒的时刻播放。
由于跳转了,所以队列的序列号会 +1
,变成了 2,而之前的 5 帧 MyAVPacketList
的序列号还是 1。两者就会不一样。
因为要开始播放第 50 秒的数据,所以 PackeQueue
队列之前缓存的 5 帧数据就不可用了。丢弃这 5 帧数据就是由下面的代码实现的。
注意看上面圈出来的代码,只有两者相等,才会 break 退出,如果不相等,就会直接 av_packet_unref()
释放,然后再进入 while
循环再从队列取AVPacket
。
这样,就能把无效的 5 帧数据全部丢弃,直到从队列读取到序列号一致的 AVPacket
为止。
还有一个重点,就是当从队列取出来的 AVPacket
跟上一次取的 AVPacket
序列号不一样,就会刷新解码器的缓存。
序列号不一样,肯定是因为跳转了播放时间点,而解码器要按顺序解码的,如果不清空缓存,可能会导致马赛克。
假设现在已经从队列读取到 序列号跟队列一样的 AVPacket
,就会把 AVPacket
发送给解码器,如下:
由于发送解码器可能会失败,所以 ffplay
做了一下处理,如果失败,就不 unref
,直接标记一下 d->packet_pending
,下次再继续发送。
此时,已经成功发送 AVPacket
给解码器了。这时候,decoder_decode_frame()
还没有结束,此时此刻还没跳出 最开始的 for (;;) {}
循环。
所以又会重新进入一开始的往解码器读 AVFrame
的逻辑,decoder_decode_frame()
函数的逻辑可以说是反着来的,所以看起来有点奇怪。
现在回到一开始的逻辑,如下:
此时,avcodec_receive_frame()
仍然可能还是会返回 EAGAIN
,因为不是往解码器发一个AVPacket
,就一定有数据可读的。有些是 B 帧,还需要多一个P帧来解码。所以如果 avcodec_receive_frame()
返回 EAGAIN
,就会从 PacketQueue
队列再拿出一个 AVPacket
往解码器丢。
现在我们假设 avcodec_receive_frame()
能读出数据了,可以看到,它赋值给了参数 frame
,也就是第二个参数。
读到 AVFrame
之后,decoder_decode_frame()
就会直接 return 1
退出了。
注意,decoder_decode_frame()
只从解码器读取到一个 AVFrame
就返回了,如果解码器里面还有缓存的 AVFrame
,下次就可以直接取,而不用再从队列拿 AVPacket
再发送给解码器。
这就是为什么从解码器读 AVFrame
要加上这个 if
判断:
if (d->queue->serial == d->pkt_serial) {
...
}
因为已经跳转到别的时间播放了,解码器的缓存是以前的时间点缓存的。如果还继续取,窗口画面就有短暂的不准确。
最后做下总结,decoder_decode_frame()
函数的逻辑就是从解码器读取到 一个 AVFrame
,为了解码出一个AVFrame
,它会从 PacketQueue
队列取 AVPacekt
发送给解码器,需要多少个就取多少个 AVPacekt
,直至到能解码出一个 AVFrame
。
decoder_decode_frame()
函数有 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
播放器。
感谢 NETINT(镕铭微电子) 赞助《FFmpeg原理》免费版一书的服务器费用,下面是 VPU 产品介绍