decoder_decode_frame解码函数分析

FFmpeg 的社群来了,想加入微信社群的朋友请购买《FFmpeg原理》VIP版 电子书,里有更高级的内容与答疑服务。​​​​​​​


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 播放器。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
av_frame_alloc函数用于为AVFrame结构体分配内存空间,AVFrame结构体用于存储解码后的音视频数据。在音频解码中,可以通过调用该函数为AVFrame结构体分配内存空间,然后将解码后的音频数据存储到该结构体中,最后再进行后续的处理操作。 具体使用方法如下: ```c AVFrame *frame = av_frame_alloc(); if (!frame) { // 内存分配失败处理 } ``` 其中,AVFrame结构体的定义如下: ```c typedef struct AVFrame { /** * pointers to the data planes/channels. * This might be different from the first allocated byte */ uint8_t *data[AV_NUM_DATA_POINTERS]; /** * For video, size in bytes of each picture line. * For audio, size in bytes of each plane. */ int linesize[AV_NUM_DATA_POINTERS]; /** * pointers to the start of each picture line. * This is used for both video and audio. */ uint8_t **extended_data; /** * width and height of the video frame */ int width, height; /** * number of audio samples (per channel) described by this frame */ int nb_samples; /** * format of the frame, -1 if unknown or unset * Values correspond to enum AVPixelFormat for video frames, * enum AVSampleFormat for audio) */ int format; /** * 1 -> keyframe, 0-> not */ int key_frame; /** * Picture type of the frame. */ enum AVPictureType pict_type; /** * Quality (between 1 (good) and FF_LAMBDA_MAX (bad)). */ int quality; /** * for AV_PIX_FMT_*_BE formats, linesize alignment */ int64_t best_effort_timestamp; /** * PTS copied from the AVPacket that was decoded to produce this frame. */ int64_t pkt_pts; /** * DTS copied from the AVPacket that triggered returning this frame. */ int64_t pkt_dts; /** * duration of the corresponding packet, expressed in * AVStream->time_base units, 0 if unknown. */ int64_t pkt_duration; uint64_t metadata; int decode_error_flags; /** * number of audio channels */ int channels; /** * size of the corresponding packet containing the compressed * frame. It is set to a negative value if unknown. */ int pkt_size; /** * data type */ enum AVSampleFormat sample_fmt; /** * presentation timestamp in timebase units * (time when frame should be shown to user) * If AV_NOPTS_VALUE then frame_rate = 1/time_base will be assumed. */ int64_t pts; /** * reordered PTS from the last AVPacket that has been input into the decoder */ int64_t best_effort_timestamp; /** * sample aspect ratio (0 if unknown) * - encoding: Set by user. * - decoding: Set by libavcodec. */ AVRational sample_aspect_ratio; /** * motion vector table, used for MPEG1/2/4 and H.263 decoding */ struct AVPanScan *pan_scan; /** * macroblock decision mode */ int coded_picture_number; int display_picture_number; /** * quality (between 1 (good) and FF_LAMBDA_MAX (bad)) */ int quality; /** * buffer age (1->was last buffer and dint change, 2->..., ...) */ int age; /** * is this picture used as reference */ int reference; /** * QP table */ int8_t *qp_table_buf; /** * QP store stride */ int qp_table_linesize; /** * QP values for each macroblock */ int8_t *qp_table; /** * For hwaccel-format frames, this should be a reference to the * AVHWFramesContext describing the frame. * For normal-format frames, this should be NULL. */ AVBufferRef *hw_frames_ctx; } AVFrame; ``` 需要注意的是,AVFrame结构体中的成员变量很多,具体使用哪些成员变量取决于具体的应用场景和需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Loken2020

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值