【FFMPEG】FFplay音视频同步分析(中)

audio_thread音频解码线程分析

之前在 stream_component_open() 里面的 decode_start() 函数开启了 audio_thread 线程,如下:

        if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)------------------audio_thread
            goto out;
        SDL_PauseAudioDevice(audio_dev, 0);
        break;

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),如下:

static int audio_thread(void *arg)
{
。。。。。。。

        if (got_frame) {
                tb = (AVRational){1, frame->sample_rate};

#if CONFIG_AVFILTER
                reconfigure =
                    cmp_audio_fmts(is->audio_filter_src.fmt, is->audio_filter_src.ch_layout.nb_channels,
                                   frame->format, frame->ch_layout.nb_channels)    ||
                    av_channel_layout_compare(&is->audio_filter_src.ch_layout, &frame->ch_layout) ||
                    is->audio_filter_src.freq           != frame->sample_rate ||
                    is->auddec.pkt_serial               != last_serial;---------------判断是否需要重置filter

                if (reconfigure) {
                    char buf1[1024], buf2[1024];

首先,开始的入口滤镜 (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 循环把缓存刷完,不会留数据在滤镜容器里面,所以重建滤镜不会导致音频数据丢失。我圈一下代码里面的重点,如下:

#if CONFIG_AVFILTER
                reconfigure =
                    cmp_audio_fmts(is->audio_filter_src.fmt, is->audio_filter_src.ch_layout.nb_channels,
                                   frame->format, frame->ch_layout.nb_channels)    ||
                    av_channel_layout_compare(&is->audio_filter_src.ch_layout, &frame->ch_layout) ||
                    is->audio_filter_src.freq           != frame->sample_rate ||
                    is->auddec.pkt_serial               != last_serial;

                if (reconfigure) {......}---------------------格式不一致,重建filter

            if ((ret = av_buffersrc_add_frame(is->in_audio_filter, frame)) < 0)
                goto the_end;

            while ((ret = av_buffersink_get_frame_flags(is->out_audio_filter, frame, 0)) >= 0) {---------不断刷完filter里面的缓存
                tb = av_buffersink_get_time_base(is->out_audio_filter);
#endif
                if (!(af = frame_queue_peek_writable(&is->sampq)))
                    goto the_end;

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

2,configure_audio_filters(),创建音频滤镜函数。

3,av_buffersrc_add_frame(),往入口滤镜发送 AVFrame。

4,av_buffersink_get_frame_flags(),从出口滤镜读取 AVFrame。

5,frame_queue_peek_writable(),从 FrameQueue 里面取一个可以写的 Frame 出来。此函数也可能会阻塞。

6,frame_queue_push(),这个函数有点奇怪,他其实不是把之前的 Frame 塞进去队列,而是把队列的写索引值 +1。


audio_thread() 函数最后还有一个重点,就是当 出口滤镜 结束的时候,finished 就会设置为 非 0 。

if (ret == AVERROR_EOF)
    is->auddec.finished = is->auddec.pkt_serial;

提示:只有往入口滤镜发送了 NULL 的 AVFrame ,出口滤镜才会结束。读完数据 跟 结束是两种状态,读完代表滤镜暂时没有数据可读,但是只要再往入口滤镜 发 AVFrame,出口滤镜就会又有数据可读。

而结束,代表不会再有 AVFrame 往入口滤镜发。
至此,audio_thread() 线程分析完毕。

decoder_decode_frame解码函数分析

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() 的代码,如下:

static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
    int ret = AVERROR(EAGAIN);

    for (;;) {
        if (d->queue->serial == d->pkt_serial) {--------------
            do {
                if (d->queue->abort_request)
                    return -1;

                switch (d->avctx->codec_type) {
                    case AVMEDIA_TYPE_VIDEO:
                        ret = avcodec_receive_frame(d->avctx, frame);
                        if (ret >= 0) {
                            if (decoder_reorder_pts == -1) {
                                frame->pts = frame->best_effort_timestamp;
                            } else if (!decoder_reorder_pts) {
                                frame->pts = frame->pkt_dts;
                            }
                        }
                        break;
                    case AVMEDIA_TYPE_AUDIO:
                        ret = avcodec_receive_frame(d->avctx, frame);----------------首次ret必然等于EAGAIN
                        if (ret >= 0) {
                            AVRational tb = (AVRational){1, frame->sample_rate};
                            if (frame->pts != AV_NOPTS_VALUE)
                                frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);
                            else if (d->next_pts != AV_NOPTS_VALUE)
                                frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);
                            if (frame->pts != AV_NOPTS_VALUE) {
                                d->next_pts = frame->pts + frame->nb_samples;
                                d->next_pts_tb = tb;
                            }
                        }
                        break;
                }
                if (ret == AVERROR_EOF) {
                    d->finished = d->pkt_serial;
                    avcodec_flush_buffers(d->avctx);
                    return 0;
                }
                if (ret >= 0)
                    return 1;
            } while (ret != AVERROR(EAGAIN));----------------EAGAIN
        }

从上代码可以看到,一开始就会用 avcodec_receive_frame()去解码器读数据,这里读者可能会有疑问,明明都还没往解码器发送 AVPacket, avcodec_receive_frame() 函数怎么可能读取到 AVFrame 呢?

答:没错,就是读取不到,因为还没发 AVPacket 给解码器解码。所以,首次 avcodec_receive_frame() 必然返回 EAGAIN,所以就会跳出这个 do{}while{} 循环。

跳出第一个 do{}while{} 循环之后,就会进入第二个 do{}while{} 循环,如下:

        do {
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);--------------唤醒read_thread线程
            if (d->packet_pending) {
                d->packet_pending = 0;
            } else {
                int old_serial = d->pkt_serial;
                if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)
                    return -1;
                if (old_serial != d->pkt_serial) {--------------清空解码器缓存
                    avcodec_flush_buffers(d->avctx);
                    d->finished = 0;
                    d->next_pts = d->start_pts;
                    d->next_pts_tb = d->start_pts_tb;
                }
            }
            if (d->queue->serial == d->pkt_serial)
                break;
            av_packet_unref(d->pkt);
        } while (1);

        if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
            int got_frame = 0;
            ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, d->pkt);
            if (ret < 0) {
                ret = AVERROR(EAGAIN);
            } else {
                if (got_frame && !d->pkt->data) {
                    d->packet_pending = 1;
                }
                ret = got_frame ? 0 : (d->pkt->data ? AVERROR(EAGAIN) : AVERROR_EOF);
            }
            av_packet_unref(d->pkt);
        } else {
            if (avcodec_send_packet(d->avctx, d->pkt) == AVERROR(EAGAIN)) {
                av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
                d->packet_pending = 1;-----------------发送解码器失败,把AVPacket缓存起来,下次继续发送
            } else {
                av_packet_unref(d->pkt);
            }
        }
    }
}

虽然代码比较少,但是句句都是重点。

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() 时的传参,如下:

        do {
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);
            if (d->packet_pending) {
                d->packet_pending = 0;
            } else {
                int old_serial = d->pkt_serial;-----------------
                if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)---------------
                    return -1;
                if (old_serial != d->pkt_serial) {--------------
                    avcodec_flush_buffers(d->avctx);
                    d->finished = 0;
                    d->next_pts = d->start_pts;
                    d->next_pts_tb = d->start_pts_tb;
                }
            }
            if (d->queue->serial == d->pkt_serial)------------------
                break;
            av_packet_unref(d->pkt);
        } while (1);

可以看到从队列取出来的 AVPacket 就放在 d->pkt 里面,而序列号就放在 d->pkt_serial。

FFplay播放器其实有 3 种序列号:

1, MyAVPacketList 的 serial ,队列里面的 AVPacket 的序列号。可以看成是临时值,旧值。

2,PackeQueue 的 serial ,这是队列本身的序列号。可以看成是最新的序列号的值。

3,Frame 的 serial,本文不需要关注这个。

MyAVPacketList 的 serial 就是用PackeQueue 队列的 serial 来赋值的 。如下代码:只要不进行跳转播放,他们的序列号就是一样的。

static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
    MyAVPacketList pkt1;
    int ret;

    if (q->abort_request)
       return -1;


    pkt1.pkt = pkt;
    pkt1.serial = q->serial;---------------取队列本身的序列号来赋值

    ret = av_fifo_write(q->pkt_list, &pkt1, 1);
    if (ret < 0)
        return ret;
    q->nb_packets++;
    q->size += pkt1.pkt->size + sizeof(pkt1);
    q->duration += pkt1.pkt->duration;
    /* XXX: should duplicate packet data in DV case */
    SDL_CondSignal(q->cond);
    return 0;
}

当快进快退,或者跳转播放时间点的时候,PackeQueue 队列的序列号就会 +1,而之前已经放进去队列的 MyAVPacketList 的序列号则保持不变。

举个例子,当前MP4已经播放到了 第20秒的时刻,此时 PackeQueue 队列缓存了 5 帧第 21秒的数据,此时,我快进30秒,跳转到第 50 秒的时刻播放。

由于跳转了,所以队列的序列号会 +1,变成了 2,而之前的 5 帧 MyAVPacketList 的序列号还是 1。两者就会不一样。

因为要开始播放第 50 秒的数据,所以 PackeQueue 队列之前缓存的 5 帧数据就不可用了。丢弃这 5 帧数据就是由下面的代码实现的。

        do {
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);
            if (d->packet_pending) {
                d->packet_pending = 0;
            } else {
                int old_serial = d->pkt_serial;
                if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)
                    return -1;
                if (old_serial != d->pkt_serial) {
                    avcodec_flush_buffers(d->avctx);
                    d->finished = 0;
                    d->next_pts = d->start_pts;
                    d->next_pts_tb = d->start_pts_tb;
                }
            }
            if (d->queue->serial == d->pkt_serial)-------start
                break;
            av_packet_unref(d->pkt);---------------------end
        } while (1);

注意看上面圈出来的代码,只有两者相等,才会 break 退出,如果不相等,就会直接 av_packet_unref() 释放,然后再进入 while 循环再从队列取AVPacket

这样,就能把无效的 5 帧数据全部丢弃,直到从队列读取到序列号一致的 AVPacket 为止。

还有一个重点,就是当从队列取出来的 AVPacket 跟上一次取的 AVPacket 序列号不一样,就会刷新解码器的缓存。

序列号不一样,肯定是因为跳转了播放时间点,而解码器要按顺序解码的,如果不清空缓存,可能会导致马赛克。

        do {
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);
            if (d->packet_pending) {
                d->packet_pending = 0;
            } else {
                int old_serial = d->pkt_serial;
                if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)
                    return -1;
                if (old_serial != d->pkt_serial) {-----------start
                    avcodec_flush_buffers(d->avctx);
                    d->finished = 0;
                    d->next_pts = d->start_pts;
                    d->next_pts_tb = d->start_pts_tb;
                }--------------------------------------------end,刷新解码器缓存
            }
            if (d->queue->serial == d->pkt_serial)
                break;
            av_packet_unref(d->pkt);
        } while (1);

假设现在已经从队列读取到 序列号跟队列一样的 AVPacket,就会把 AVPacket 发送给解码器,如下:

    if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {......} else {
        if (avcodec_send_packet(d->avctx, d->pkt) == AVERROR(EAGAIN)) {
            av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
            d->packet_pending = 1;
        } else {
            av_packet_unref(d->pkt);
        }
    }

由于发送解码器可能会失败,所以 ffplay 做了一下处理,如果失败,就不 unref,直接标记一下 d->packet_pending,下次再继续发送。

此时,已经成功发送 AVPacket 给解码器了。这时候,decoder_decode_frame() 还没有结束,此时此刻还没跳出 最开始的 for (;😉 {} 循环。

所以又会重新进入一开始的往解码器读 AVFrame 的逻辑,decoder_decode_frame()函数的逻辑可以说是反着来的,所以看起来有点奇怪。

现在回到一开始的逻辑,如下:

static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
    int ret = AVERROR(EAGAIN);

    for (;;) {
        if (d->queue->serial == d->pkt_serial) {-----------------------
            do {
                if (d->queue->abort_request)
                    return -1;

                switch (d->avctx->codec_type) {
                    case AVMEDIA_TYPE_VIDEO:
                        ret = avcodec_receive_frame(d->avctx, frame);
                        if (ret >= 0) {.......}
                        break;
                    case AVMEDIA_TYPE_AUDIO:
                        ret = avcodec_receive_frame(d->avctx, frame);
                        if (ret >= 0) {......}
                        break;
                }
                if (ret == AVERROR_EOF) {
                    d->finished = d->pkt_serial;
                    avcodec_flush_buffers(d->avctx);
                    return 0;
                }
                if (ret >= 0)
                    return 1;------------已经成功解码并读取到AVFrame,直接return返回
            } while (ret != AVERROR(EAGAIN));
        }

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

FFplay序列号分析

在之前的几篇文章里面,都零零散散提及过序列号这个概念。但是 序列号 这个概念对于 FFplay 播放器 非常重要的,很多代码都跟序列号有关。所以单独写一篇文章介绍序列号。

序列号 主要是给 快进快退 这个功能准备的。如果不能快进快退,那其实就不需要序列号。只要解复用线程不断读取 AVPacket 放进去 PacketQueue 队列,解码线程不断从 PacketQueue 取数据来解码放进去 FrameQueue,最后有播放线程来取 FrameQueue 的数据取播放就行。

整体的流程图如下:

在这里插入图片描述

PacketQueue 跟 FrameQueue 都是缓存队列,都是先提取准备好要用的数据。但是就是因为加了 快进快退 这个功能,一旦跳转到别的时间点播放,之前提前准备好的数据是不是就不能用了?

所以就需要一个序列号来判断,之前缓存的数据是不是最新的,所以序列号可以看成是版本号一样的东西。

FFplay 播放器主要有 4 个序列号字段,分别如下:

1,struct PackeQueue 的 serial ,这是队列本身的序列号。可以看成是最新的序列号的值。

2, struct MyAVPacketList 的 serial ,队列里面 AVPacket 的序列号。

3,struct Decoder 的 pkt_serial,记录的是解码器上一次用来解码的 AVPacket 的序列号

4,struct Frame 的 serial,队列里面 AVFrame 的序列号。

首先,MyAVPacketList 的 serial 来源与 PackeQueue 的 serial,在 packet_queue_put_private() 函数里面可以看到:

static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
    MyAVPacketList pkt1;-----------------MyAVPacketList
    int ret;

    if (q->abort_request)
       return -1;


    pkt1.pkt = pkt;
    pkt1.serial = q->serial;---------------serial
......
}

Frameserial 是来源于 Decoderpkt_serial,如下:

                if (!(af = frame_queue_peek_writable(&is->sampq)))
                    goto the_end;

                af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
                af->pos = frame->pkt_pos;
                af->serial = is->auddec.pkt_serial;------------------
                af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});

Decoder 结构里面的 pkt_serial 又是来自哪里的呢?

**答:**是在 decoder_decode_frame() 里面进行赋值的。记录的是最近一次输入解码器的 AVPacket 的序列号,如下:

        do {
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);
            if (d->packet_pending) {
                d->packet_pending = 0;
            } else {
                int old_serial = d->pkt_serial;
                if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)---------------记录最近一次输入解码器的AVPacket的序列号
                    return -1;
                if (old_serial != d->pkt_serial) {
                    avcodec_flush_buffers(d->avctx);

因此,可以说,Frameserial间接来源AVPacketserial 的。

现在已经弄懂了 4 个序列号赋值的地方,那这 4 个序列号字段用来哪些代码里面呢?

答:只要跟缓存有关的逻辑,都会判断序列号。

因为序列号是为了 快进快退 功能准备的,所以先简单讲解一下 快进快退的逻辑,更详细的分析请看《FFplay跳转时间点播放》。

当你按下上下左右其中一个键的时候,就会产生一个 seek 操作,这是调 stream_seek() 函数实现的,如下:

        case SDLK_UP:
            incr = 60.0;
            goto do_seek;------------
        case SDLK_DOWN:
            incr = -60.0;
        do_seek:
                if (seek_by_bytes) {
                    pos = -1;
                    if (pos < 0 && cur_stream->video_stream >= 0)
                        pos = frame_queue_last_pos(&cur_stream->pictq);
                    if (pos < 0 && cur_stream->audio_stream >= 0)
                        pos = frame_queue_last_pos(&cur_stream->sampq);
                    if (pos < 0)
                        pos = avio_tell(cur_stream->ic->pb);
                    if (cur_stream->ic->bit_rate)
                        incr *= cur_stream->ic->bit_rate / 8.0;
                    else
                        incr *= 180000.0;
                    pos += incr;
                    stream_seek(cur_stream, pos, incr, 1);
                } else {
                    pos = get_master_clock(cur_stream);
                    if (isnan(pos))
                        pos = (double)cur_stream->seek_pos / AV_TIME_BASE;
                    pos += incr;
                    if (cur_stream->ic->start_time != AV_NOPTS_VALUE && pos < cur_stream->ic->start_time / (double)AV_TIME_BASE)
                        pos = cur_stream->ic->start_time / (double)AV_TIME_BASE;
                    stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0);--------------
                }
            break;

stream_seek() 函数的代码如下:

/* seek in the stream */
static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
{
    if (!is->seek_req) {
        is->seek_pos = pos;
        is->seek_rel = rel;
        is->seek_flags &= ~AVSEEK_FLAG_BYTE;
        if (seek_by_bytes)
            is->seek_flags |= AVSEEK_FLAG_BYTE;
        is->seek_req = 1;
        SDL_CondSignal(is->continue_read_thread);
    }
}

上面的代码重点是设置 了 is->seek_req 标记 以及 要跳到到哪些位置播放。然后就会唤醒 read_thread() 线程。 read_thread() 线程看到 is->seek_req 这个标记,就会开始进行 seek 操作。如下:

所以,真正进行 seek 操作的地方是在 read_thread() 解复用线程里面,而不是 main 线程。

        if (is->seek_req) {---------------------
            int64_t seek_target = is->seek_pos;
            int64_t seek_min    = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
            int64_t seek_max    = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
// FIXME the +-2 is due to rounding being not done in the correct direction in generation
//      of the seek_pos/seek_rel variables

            ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);-------------
            if (ret < 0) {
                av_log(NULL, AV_LOG_ERROR,
                       "%s: error while seeking\n", is->ic->url);
            } else {
                if (is->audio_stream >= 0)
                    packet_queue_flush(&is->audioq);---------------
                if (is->subtitle_stream >= 0)
                    packet_queue_flush(&is->subtitleq);------------
                if (is->video_stream >= 0)
                    packet_queue_flush(&is->videoq);---------------
                if (is->seek_flags & AVSEEK_FLAG_BYTE) {
                   set_clock(&is->extclk, NAN, 0);
                } else {
                   set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
                }
            }

从上代码可以看出来,用 avformat_seek_file() 进行 seek 操作之后,就会 调 packet_queue_flush() 函数来刷新 序列号,来达到丢弃之前的无效缓存的效果。

packet_queue_flush() 是一个非常重点的函数,代码如下:

static void packet_queue_flush(PacketQueue *q)
{
    MyAVPacketList pkt1;

    SDL_LockMutex(q->mutex);
    while (av_fifo_read(q->pkt_list, &pkt1, 1) >= 0)--------清空队列的数据
        av_packet_free(&pkt1.pkt);
    q->nb_packets = 0;
    q->size = 0;
    q->duration = 0;
    q->serial++;--------------刷新序列号
    SDL_UnlockMutex(q->mutex);
}

可以看到,一开始就清空了 PacketQueue 队列里面的数据,然后把序列号 +1。

我个人有个疑问,既然 +1 之前已经把 数据清空,那 解码线程 从 PakcetQueue 取数据的时候根本不需要判断序列号。因为已经清空了,解码线程不可能取到旧的 AVPacket。

先不管这个,反正 PacketQueue 的 serial 序列号刷新了。

PacketQueue 队列本身的 serial 序列号 +1 刷新,会导致一系列的连环反应。

首先是后面 解复用线程往 PacketQueue 队列 加进去的 MyAVPacketList 的 serial 也会是最新的。但是,解码管理器(struct Decoder)里面的 pkt_serial 还是旧值。

之前说过,struct Decoder 的 pkt_serial 记录的是上一次解码的 AVPacket 的序列号。

这就会导致,直接放弃解码器里面的缓存,用新的 AVPacket 进行解码,如下:

static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
    int ret = AVERROR(EAGAIN);

    for (;;) {
        if (d->queue->serial == d->pkt_serial) {
            do {
                if (d->queue->abort_request)
                    return -1;

                switch (d->avctx->codec_type) {
                    case AVMEDIA_TYPE_VIDEO:
                        ret = avcodec_receive_frame(d->avctx, frame);
                        if (ret >= 0) {......}
                        break;
                    case AVMEDIA_TYPE_AUDIO:
                        ret = avcodec_receive_frame(d->avctx, frame);
                        if (ret >= 0) {......}
                        break;
                }
                if (ret == AVERROR_EOF) {......}
                if (ret >= 0)
                    return 1;
            } while (ret != AVERROR(EAGAIN));
        }

同样会导致 解码器的缓存被刷新,如下:

        do {
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);
            if (d->packet_pending) {
                d->packet_pending = 0;
            } else {
                int old_serial = d->pkt_serial;--------------
                if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)------------
                    return -1;
                if (old_serial != d->pkt_serial) {------------
                    avcodec_flush_buffers(d->avctx);----------
                    d->finished = 0;
                    d->next_pts = d->start_pts;
                    d->next_pts_tb = d->start_pts_tb;
                }
            }
            if (d->queue->serial == d->pkt_serial)
                break;
            av_packet_unref(d->pkt);
        } while (1);

上图的逻辑是,只要本次解码的 AVPacket 的序列号跟上一次用的 AVPacket 的序列号不一样,立即刷新解码器缓存

还有一个被连环影响的地方就是 读取 FrameQueue 的逻辑,FrameQueuePakcetQueue 不太一样,

FrameQueue 本身是没有序列号的,只是它队列里面的 struct Frame 有 序列号,所以也会受到影响,如下:

static int audio_decode_frame(VideoState *is)
{
    int data_size, resampled_data_size;
    av_unused double audio_clock0;
    int wanted_nb_samples;
    Frame *af;

    if (is->paused)
        return -1;

    do {
#if defined(_WIN32)
......
#endif
        if (!(af = frame_queue_peek_readable(&is->sampq)))----------------音频播放线程取数据
            return -1;
        frame_queue_next(&is->sampq);
    } while (af->serial != is->audioq.serial);----------------

    data_size = av_samples_get_buffer_size(NULL, af->frame->ch_layout.nb_channels,
                                           af->frame->nb_samples,
                                           af->frame->format, 1);

可以看到,音频播放线程取数据的时候,会判断 Frame 的序列号是不是最新的,不是就立即丢弃。

序列号应用的场景还有一个,那就是时钟(Clock),如下:

typedef struct Clock {
       ...
    int serial;           /* clock is based on a packet with this serial */
    ...
    int *queue_serial;    /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

这个场景是音视频同步的时候用的,有点复杂。后面讲解音视频同步的时候会再分析 Clock 里面的序列号。

sdl_audio_callback音频播放线程分析

音频播放线程是之前在 audio_open() 函数里面创建的,实际上就是回调函数 ( wanted_spec.callback)。当用 SDL 打开音频硬件设备的时候,SDL 库就会创建一个线程,来及时执行回调函数 sdl_audio_callback(),至于 SDL 线程多久回调一次函数,这个我们不需要太关心,只要调 SDL_OpenAudioDevice() 函数的时候设置好相关参数即可。如下:

static int audio_open(void *opaque, AVChannelLayout *wanted_channel_layout, int wanted_sample_rate, struct AudioParams *audio_hw_params)
{
......
    while (next_sample_rate_idx && next_sample_rates[next_sample_rate_idx] >= wanted_spec.freq)
        next_sample_rate_idx--;
    wanted_spec.format = AUDIO_S16SYS;
    wanted_spec.silence = 0;---------------
    wanted_spec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / SDL_AUDIO_MAX_CALLBACKS_PER_SEC));-----每次回调取的样本数
    wanted_spec.callback = sdl_audio_callback;------
    wanted_spec.userdata = opaque;
    while (!(audio_dev = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_CHANNELS_CHANGE))) {---------------SDL_OpenAudioDevice
        av_log(NULL, AV_LOG_WARNING, "SDL_OpenAudio (%d channels, %d Hz): %s\n",
               wanted_spec.channels, wanted_spec.freq, SDL_GetError());
        wanted_spec.channels = next_nb_channels[FFMIN(7, wanted_spec.channels)];
        if (!wanted_spec.channels) {
            wanted_spec.freq = next_sample_rates[next_sample_rate_idx--];
            wanted_spec.channels = wanted_nb_channels;
            if (!wanted_spec.freq) {
                av_log(NULL, AV_LOG_ERROR,
                       "No more combinations to try, audio open failed\n");
                return -1;
            }
        }
        av_channel_layout_default(wanted_channel_layout, wanted_spec.channels);
    }

上面代码中,设置了每次回调取的样本数,设置了样本数就相当于设置了回调次数,ffplay 默认是 1秒钟最多回调 30 次 sdl_audio_callback() 函数。

sdl_audio_callback() 函数的参数如下:

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)

1,void *opaque,实际上就是之前设置的 wanted_spec.userdata,传递的是 VideoState *is 全局管理器。

2,Uint8 *stream,这个指针是 SDL 内部音频数据内存的指针,只要把数据拷贝到这个指针的地址,就能播放声音了。

3,int len,此次回调需要写多少字节的数据进去 stream 指针。

虽然 len 这个参数是 SDL 传递给我们的回调函数的,但是 len 其实是可以根据 wanted_spec的样本数,声道数,格式, 算出来的。

例如,ffplay 播放 juren-5s.mp4 的场景,命令如下:

ffplay -i juren-5s.mp4

在上面的命令中,wanted_spec.samples 会被赋值为 2048,也就是说每次回调,需要往 stream 指针写 2048 个采样。那2048个样本又是多少字节?
由于 SDL_OpenAudioDevice() 打开音频设备的时候写死了 16位格式 AUDIO_S16SYS ,所以一个采样是2个字节。然后因为 juren-5s.mp4 文件的音频是双声道的,每个声道都取2048个样本,那就是 2048 2 2 = 8196 字节。有兴趣可以打印一下 sdl_audio_callback() 里面的 len 变量,在本文命令下,一直都是8196 字节。

sdl_audio_callback() 函数的流程图如下:

在这里插入图片描述

从上图的流程可以很容易看出, sdl_audio_callback() 函数干的事情,就是调 audio_decode_frame() 函数,把 is->audio_buf 指针指向要传输的音频数据。然后再调 SDL_MixAudioFormat() 把音频数据拷贝给 stream 指针指向的内存。这样 SDL 内部就会读 stream 指针指向的内存来播放。

虽然流程图画得比较简单,但是 sdl_audio_callback() 函数内部的代码逻辑是非常复杂的。下面我们一起来分析一下这里面的重点:

/* prepare a new audio buffer */
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
.......
    while (len > 0) {
        if (is->audio_buf_index >= is->audio_buf_size) {--------------audio_buf缓存已经读完
           audio_size = audio_decode_frame(is);
           if (audio_size < 0) {
                /* if error, just output silence */
               is->audio_buf = NULL;
               is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
           } else {
               if (is->show_mode != SHOW_MODE_VIDEO)
                   update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
               is->audio_buf_size = audio_size;------------设置buf的大小以及读取位置
           }
           is->audio_buf_index = 0;-----------------------
        }
        len1 = is->audio_buf_size - is->audio_buf_index;
        if (len1 > len)
            len1 = len;
        if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)------以最大音量进行拷贝
            memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);------start
        else {
            memset(stream, 0, len1);
            if (!is->muted && is->audio_buf)
                SDL_MixAudioFormat(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, AUDIO_S16SYS, len1, is->audio_volume);----------以调整后的音量进行拷贝
        }---------------------------------------------------end,复制音频数据至SDL的内存
        len -= len1;
        stream += len1;
        is->audio_buf_index += len1;-----------------更新读取位置
    }
......
    }
}

上面代码中的 while(len>0){…} 的逻辑简单概括就是从 FrameQueue 读取数据,不断写进入 SDL 的内存,直到写入了 len 大小的字节为止。

audio_decode_frame() 函数就是负责从 FrameQueue 读取 AVFrame,然后 把 is->audio_buf 指针指向 AVFrame 的 data ,如果经过重采样, is->audio_buf 指针会指向重采样后的数据,也就是 is->audio_buf1。

audio_decode_frame() 函数的内部逻辑是比较复杂的,本文不分析它的内部逻辑,只是给你讲一下它最后做到了什么事情,这也就是函数封装的意义。

audio_decode_frame() 函数最后做到的事情就是,把is->audio_buf 指针指向可以传输的音频数据,然后返回值代表可以传输的音频数据有多少字节。

至于这个音频数据是在哪个内存里面,我们外部调用不需要关心,这是它的内部实现。

如果 audio_decode_frame() 提取数据的时候发生错误。会返回 -1,然后把 audio_buf 指向 NULL 指针,这样会导致写入静音数据。

要理解上面的 while(len>0){…} 循环,有几个变量是必须要讲解一下的:

1,is->audio_buf,可以传输的音频数据的内存指针,指向内存的第一个字节。也就是开头。

2,is->audio_buf_size,可以传输的音频数据有多大,有多少个字节。

3,is->audio_buf_index,当前已经读取到第几个字节。

理解了这 3 个变量,就容易看懂上图中的 while 逻辑,就是不断拷贝数据到 SDL 的内存,

如果拷贝够了 len 字节,is->audio_buf 里面还有数据剩下来,就下一次回调来的时候继续拷贝剩下的数据。

如果当前 is->audio_buf 的数据不足以塞满 len 长度,就循环调 audio_decode_frame() 来提取数据,然后一直到 塞满 len 长度为止。
拷贝数据到 SDL 内存的时候有一个重点,会根据音量来调不同的函数

可以看到,如果直接 memcpy 拷贝数据,会是以最大音量进行拷贝。如果我们调整了音量,就会调 SDL_MixAudioFormat() 来拷贝数据。

因为 SDL_MixAudioFormat() 函数有调整音量的功能。

至此,sdl_audio_callback() 函数已经把 SDL 所需的音频数据全部拷贝给它了,就会跳出 while 循环,然后设置音频时钟。如下:

is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
    /* Let's assume the audio driver that is used by SDL has two periods. */
    if (!isnan(is->audio_clock)) {
        set_clock_at(&is->audclk,is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec,is->audio_clock_serial, audio_callback_time / 1000000.0);
        sync_clock_to_slave(&is->extclk, &is->audclk);
    }

上面的代码主要是设置音频的时钟,记录当前这一刻,音频流播放到哪里了。下面分析一下一些变量:

is->audio_write_buf_size 代表当前缓存里还剩多少数据没有拷贝给 SDL 。

is->audio_clock 这个变量是在 audio_decode_frame() 函数里面赋值的,因为 audio_decode_frame() 会从 FrameQueue 里面提取 一个AVFrame,而 is->audio_clock 记录的就是当这个 AVFrame 播放完之后,音频流所处的位置,也就是当 这个 AVFrame 播放完了,音频流 的 pts 是多少。。

理解了 is->audio_clock 就比较容易理解如何确定当前音频流播放到哪里了。

is->audio_clock 记录的是播放完那个 AVFrame 之后的 pts,但是此时此刻 只是把 这个 AVFrame 的内存数据拷贝给了 SDL,SDL 还没开始播放呢?

同时,SDL 还没播放的数据还有它内部的 audio_hw_buf_size 长度的数据,之前说过:

SDL线程并不是没有音频数据可以播放了才调 sdl_audio_callback() 来拿数据,而是他内部还剩 audio_hw_buf_size 长度的数据就会调 sdl_audio_callback() 来拿数据,是提前拿数据的。

所以,SDL 内部还剩 audio_hw_buf_size 字节,现在又来取了 len 字节,同时我们的 audio_buf 缓存还剩 audio_write_buf_size字节。总共有 3 块内存等待播放,而这 3 块内存播放完之后的 pts 就是 is->audio_clock ,如下:

在这里插入图片描述

从上图可以看到,我们已经知道播放完这 3 块内存之后,音频流的位置,那就可以反向求音频流当前的播放时刻。而 len 实际上就是等于 audio_write_buf_size,只是换了下名字,所以可以直接用 audio_hw_buf_size 乘以 2,所以就产生了下面的这句代码:

音频流当前的播放时刻 = is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec

这里要注意 is->audio_clock 的单位是秒。至此,就可以正确设置音频流当前播放到哪里了。

代码里有一句注释,也讲解了为什么要乘以 2。

/* Let's assume the audio driver that is used by SDL has two periods. */

它假设了 SDL 打开的设备里面是有两段缓存的,也就是 audio_hw_buf_size + len。

最后的那句代码主要是用音频时钟来同步一下外部时钟,如下:。

sync_clock_to_slave(&is->extclk, &is->audclk);

时钟是用来记录当前的播放时刻的,方便做音视频同步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值