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

main入口函数分析

ffplay.c 里面main() 入口函数的流程图如下:
在这里插入图片描述

代码如下:

/* Called from the main */
int main(int argc, char **argv)
{
    int flags;
    VideoState *is;

    init_dynload();--------------------------init_dynload

    av_log_set_flags(AV_LOG_SKIP_REPEATED);
    parse_loglevel(argc, argv, options);

    /* register all codecs, demux and protocols */
#if CONFIG_AVDEVICE
    avdevice_register_all();
#endif
    avformat_network_init();

    signal(SIGINT , sigterm_handler); /* Interrupt (ANSI).    */
    signal(SIGTERM, sigterm_handler); /* Termination (ANSI).  */

    show_banner(argc, argv, options);------------------show_banner
    parse_options(NULL, argc, argv, options, opt_input_file);----------------命令行参数解析

    if (!input_filename) {......}
    
    if (display_disable) {......}-------------------------start,
    flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;
    if (audio_disable)
        flags &= ~SDL_INIT_AUDIO;
    else {......}
    
    if (display_disable)
        flags &= ~SDL_INIT_VIDEO;-------------------------end,flags标记不显示SDL窗口之类的,可以设置只播放声音不播放画面
    if (SDL_Init (flags)) {
        av_log(NULL, AV_LOG_FATAL, "Could not initialize SDL - %s\n", SDL_GetError());
        av_log(NULL, AV_LOG_FATAL, "(Did you set the DISPLAY variable?)\n");
        exit(1);
    }
    
    SDL_EventState(SDL_SYSWMEVENT, SDL_IGNORE);
    SDL_EventState(SDL_USEREVENT, SDL_IGNORE);

    if (!display_disable) {......}
    
    is = stream_open(input_filename, file_iformat);-------------stream_open
    if (!is) {
        av_log(NULL, AV_LOG_FATAL, "Failed to initialize VideoState!\n");
        do_exit(NULL);
    }

    event_loop(is);------------------event_loop

    /* never returns */

    return 0;
}

上面的流程 分两部分讲,非重点函数 跟 重点函数。

非重点函数如下

**1,**init_dynload(),设置动态库加载规则,这是一个安全函数,在 Windows 系统,默认会从当前目录加载 DLL,这容易被攻击。这个函数就是把当前目录的路径从加载规则里面去掉,里面调的是 SetDllDirectory(“”)。

**2,**show_banner(),打印 ffplay 这个软件的 版权,版本之类的信息。可以删掉他,让控制台更简洁。

重点函数如下:

1,parse_options(),解析命令行参数,虽然这是一个重点函数,但是为了力求简单,我会一笔带过。本文只用到 一个 -i 参数,所以这个函数在这里的作用就是设置 input_filename 全局变量。在 《parse_options函数分析》会详细讲解 命令行解析。

2,SDL_CreateWindow(),创建 SDL 窗口,具体请看 SDL官方文档。

3,stream_open(),这个函数是重中之重,上图中可以看到,可能会有 4 个线程从 stream_open() 里面诞生。先来讲一下这 4 个线程的作用。

read_thread() :从 网络或者硬盘里面读取 AVPacket,读取到之后放进去 PacketQueue 队列。

audio_thread() :从 PacketQueue audioq 队列拿 AVPacket,然后丢给解码器解码,解码出来 AVFrame 之后,再把 AVFrame 丢到 FrameQueue 队列。

video_thread() :从 PacketQueue videoq 队列拿 AVPacket,然后丢给解码器解码,解码出来 AVFrame 之后,再把 AVFrame 丢到 FrameQueue 队列。

subtitle_thread() :字幕线程,由于 ffplay 的字幕播放有点不完善,不必关注。

上面的 4 个线程 不一定会创建,如果 mp4 文件里面没有音频流,就不会创建 audio_thread() 线程,其他的线程 类推。

这 4 个线程之间的关系如下:
在这里插入图片描述

read_thread 是生产者,而 audio_threadvideo_thread 是消费者。

最后一个重点函数是 event_loop(),这个函数是一个死循环,主要的任务就是不断 处理键盘按键事件播放视频帧

stream_open函数分析

在讲 stream_open() 函数之前,需要先了解 stream_open() 里面使用到的一些基本的数据结构。如下:

第一个数据结构是 struct VideoState ,VideoState 可以说是播放器的全局管理器。字段非常多,时钟,队列,解码器,各种状态 都放在 VideoState 里面。

但是本文不会把 VideoState 的所有字段都讲一遍,只会讲 stream_open() 函数用到的字段,如下是精简过的字段,顺序也经过调整,方便阅读。

typedef struct VideoState {
    int last_video_stream, last_audio_stream, last_subtitle_stream;
    char* filename;
    AVInputFormat* iformat;
    int width, height, xleft, ytop;
    FrameQueue pictq;
    FrameQueue sampq;
    PacketQueue videoq;
    PacketQueue audioq;
    SDL_cond *continue_read_thread;
    SDL_Thread *read_tid;
    Clock audclk;
    Clock vidclk;
    Clock extclk;    
    int audio_clock_serial;
    int audio_volume;
    int muted;
    int av_sync_type;
} VideoState;

1,int last_video_stream 代表最后一个视频流,如果你的音视频文件 里面有多个视频流,last_video_stream 就代表最后一个视频流。另外两个 last_audio_stream, last_subtitle_stream 一样代表最后一个。

2,char* filename 存储的是打开的 音视频文件名,或者是网络地址url。

3,AVInputFormat* iformat,容器格式,ffplay 默认是根据 filename 的后缀来确定容器格式,但是你也可以指定按某种容器格式来解析文件。命令如下:

ffplay -i juren-5s.mp4 -f flv
通过命令行参数指定的 -f flv 就会被存储到 AVInputFormat* iformat,当然不是存的字符,有一个根据字符串找到 AVInputFormat 的过程。

4,int width, height, xleft, ytop;,分别代表播放器窗口的 宽高 跟 位置。位置通过 xleft 跟 ytop 来定位的。

5,FrameQueue pictq, FrameQueue sampq,视频跟音频的 AVFrame 队列。

6,PacketQueue videoq,PacketQueue audioq,视频跟音频的 AVPacket 队列。

7,SDL_cond *continue_read_thread,这是一个 SDL 的条件变量,用于线程间通信的。read_thread() 线程在以下两种情况会进入休眠 10ms。

第一种情况:PacketQueue 队列满了,无法再塞数据进去。

第二种情况:超过最小缓存size。

如果在 10ms 内,PacketQueue 队列全部被消耗完毕,audio_thread() 或者 video_thread() 线程 没有 AVPakcet 能读了,就需要尽快唤醒 read_thread() 线程。

还有,如果进行了 seek 操作,也需要快速把 read_thread() 线程 从休眠中唤醒。

所以 SDL_cond *continue_read_thread 条件变量,主要用于 read_thread 跟 audio_thread ,video_thread 线程进行通信的。

8,SDL_Thread *read_tid;,read_thread 的线程ID。

C++14 标准库有跨平台的线程库,但是 C语言 是没有跨平台的线程库,所以 ffplay 取巧了,使用了 SDL 库的线程跟条件变量,SDL 是跨平台的。

9,Clock audclk;,音频时钟,记录音频流的目前的播放时刻 。

10,Clock vidclk;,视频时钟,记录视频流的目前的播放时刻 。

11,Clock extclk;,外部时钟,取第一帧 音频 或 视频的 pts 作为 起始时间,然后随着物理时间的消逝增长,所以是物理时间的当前时刻。到底是以音频的第一帧,还是视频的第一帧?取决于 av_read_frame() 函数第一次读到的是音频还是视频。

12,int audio_clock_serial;,这个字段比较独特,只有音频有,视频没有,没有一个 video_clock_serial 字段。

audio_clock_serial 只是一个用做临时用途的变量,实际上存储的就是 AVFrame 的 serial 字段。不用特别关注。而视频直接用的 AVFrame 的 serial。

13,int audio_volume,播放器的声音大小。

14,int muted,是否静音,C语言C99标准是没有 bool 类型的,都用 int 代替。

15,int av_sync_type,音视频同步方式,有 3 种同步方式,以音频时钟为准,以视频时钟为准,以外部时钟为准。默认方式是以音频时钟为准。


上面的数据结构,有一些字段我会说得比较简洁,因为现在只需你对这些字段有个简单的了解,后面文章会具体详细用到这些字段的场景。

由于 FrameQueuePacketQueue 这两个数据结构非常重要,所以放一整图片方便理解。
在这里插入图片描述

FrameQueue 里面的 queue 是一个数组,16 在代码里是一个宏,那个宏通常等于 16。PacketQueue 里面的 pkt_list 是一个 AVFifoBuffer,推荐阅读《FifoBuffer函数库详解》。

FrameQueue 跟 PakcetQueue 是通过一个 pktq 指针来关联的。这两个队列都有自己的 锁 mutex 跟 条件变量 cond。操作这两个队列都需要加锁操作的。


下面开始分析 stream_open() 函数,流程图,代码如下:
在这里插入图片描述

static VideoState *stream_open(const char *filename,
                               const AVInputFormat *iformat)
{
    VideoState *is;

    is = av_mallocz(sizeof(VideoState));--------------------
    if (!is)
        return NULL;
    is->last_video_stream = is->video_stream = -1;
    is->last_audio_stream = is->audio_stream = -1;
    is->last_subtitle_stream = is->subtitle_stream = -1;
    is->filename = av_strdup(filename);
    if (!is->filename)
        goto fail;
    is->iformat = iformat;
    is->ytop    = 0;
    is->xleft   = 0;

    /* start video display */
    if (frame_queue_init(&is->pictq, &is->videoq, VIDEO_PICTURE_QUEUE_SIZE, 1) < 0)-----------
        goto fail;
    if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 0)-----------
        goto fail;
    if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)------------------
        goto fail;

    if (packet_queue_init(&is->videoq) < 0 ||---------------------
        packet_queue_init(&is->audioq) < 0 ||---------------------
        packet_queue_init(&is->subtitleq) < 0)--------------------
        goto fail;

    if (!(is->continue_read_thread = SDL_CreateCond())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        goto fail;
    }
    
    init_clock(&is->vidclk, &is->videoq.serial);------------
    init_clock(&is->audclk, &is->audioq.serial);------------
    init_clock(&is->extclk, &is->extclk.serial);------------
    is->audio_clock_serial = -1;
    if (startup_volume < 0)
        av_log(NULL, AV_LOG_WARNING, "-volume=%d < 0, setting to 0\n", startup_volume);
    if (startup_volume > 100)
        av_log(NULL, AV_LOG_WARNING, "-volume=%d > 100, setting to 100\n", startup_volume);
    startup_volume = av_clip(startup_volume, 0, 100);
    startup_volume = av_clip(SDL_MIX_MAXVOLUME * startup_volume / 100, 0, SDL_MIX_MAXVOLUME);
    is->audio_volume = startup_volume;
    is->muted = 0;
    is->av_sync_type = av_sync_type;
    is->read_tid     = SDL_CreateThread(read_thread, "read_thread", is);------------------
    if (!is->read_tid) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateThread(): %s\n", SDL_GetError());
fail:
        stream_close(is);
        return NULL;
    }
    return is;
}

从上面的代码可以看出来, stream_open() 函数的内部实现非常的简单。无非就是 初始化 队列,初始化时钟,然后创建一个 read_thread() 线程去跑。

其中 frame_queue_init() 的内部实现也比较简单,不过有几个重点,如下:

static int frame_queue_init(FrameQueue *f, PacketQueue *pktq, int max_size, int keep_last)
{
    int i;
    memset(f, 0, sizeof(FrameQueue));
    if (!(f->mutex = SDL_CreateMutex())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    if (!(f->cond = SDL_CreateCond())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    f->pktq = pktq;
    f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE);
    f->keep_last = !!keep_last;-------------------
    for (i = 0; i < f->max_size; i++)
        if (!(f->queue[i].frame = av_frame_alloc()))
            return AVERROR(ENOMEM);
    return 0;
}

这个 !! 操作没有什么特别,实际上就是把大于 1 的数字转成 1。如果 keep_last 等于 5,取反两次之后,就会变成 1 了。


packet_queue_init() 函数里面也有一句代码需要注意。

q->abort_request = 1;

abort_request 字段如果置为 1, audio_thread() 跟 video_thread() 解码线程就会退出。所以在创建解码线程 之前,ffplay 会把 abort_request 置为 0 ,如下:

static int decoder_start(Decoder *d, int (*fn)(void *), const char *thread_name, void* arg)
{
    packet_queue_start(d->queue);---------------------packet_queue_start
    d->decoder_tid = SDL_CreateThread(fn, thread_name, arg);
    if (!d->decoder_tid) {
        av_log(NULL, AV_LOG_ERROR, "SDL_CreateThread(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    return 0;
}
static void packet_queue_start(PacketQueue *q)
{
    SDL_LockMutex(q->mutex);
    q->abort_request = 0;----------------------------abort_request
    q->serial++;
    SDL_UnlockMutex(q->mutex);
}

至此,stream_open() 函数已经讲解完毕,现在逻辑流程已经流转到新的线程 read_thread() 函数里面了,下面文章继续分析 read_thread() 函数的内部原理。

read_thread解复用线程分析

read_thread() 线程的主要作用从 MP4 里面读取 AVPacket,然后丢进去 PacketQueue 队列。所以需要先学习一下 strcut PacketQueuestruct MyAVPacketList 数据结构。如下:

typedef struct MyAVPacketList {
    AVPacket *pkt;
    int serial;
} MyAVPacketList;
typedef struct PacketQueue {
    AVFifoBuffer *pkt_list; //存储的是 MyAVPacketList
    int nb_packets;
    int size;
    int64_t duration;
    int abort_request;
    int serial;
    SDL_mutex *mutex;
    SDL_cond *cond;
} PacketQueue;

1,AVFifoBuffer *pkt_list ,AVFifoBuffer 是一个 circular buffer FIFO,一个环形的先进先出的缓存实现。里面存储的是 struct MyAVPacketList 结构的数据。

2,int nb_packets;,代表队列里面有多少个 AVPacket。

3,int size; ,队列缓存的数据大小 ,算法是所有的 AVPacket 本身的大小加上 AVPacket->size 。

4,int64_t duration,队列的时长,通过累加 队列里所有的 AVPacket->duration 得到。

5,abort_request,代表队列终止请求,变成 1 会导致 audio_thread 跟 video_thread 退出。

6,int serial,队列的序号,每次跳转播放时间点 ,serial 就会 +1。另一个数据结构 MyAVPacketList 里面也有一个 serial 字段。

两个 serial 通过比较匹配来丢弃无效的缓存帧,什么情况会导致队列的缓存帧无效?跳转播放时间点的时候。

例如此时此刻,PacketQueue 队列里面缓存了 8 个帧,但是这 8 个帧都 第30分钟 才开始播放的,如果你通过 ➔ 按键前进到 第35分钟 的位置播放,那队列的 8 个缓存帧就无效了,需要丢弃。

由于每次跳转播放时间点, PacketQueue::serial 都会 +1 ,而 MyAVPacketList::serial 的值还是原来的,两个 serial 不一样,就会丢弃帧。

7,SDL_mutex *mutex ,SDL 互斥锁,主要用于修改队列的时候加锁。

8,SDL_cond *cond,SDL 条件变量,用于 read_thread() 线程 跟 audio_thread() ,video_thread() 线程 进行通信的。


ffplay -i juren-5s.mp4 的场景下,read_thread 线程的流程图如下:
在这里插入图片描述

read_thread() 线程里面的逻辑相对比较复杂,重点也挺多。首先讲解一下 st_index[] 这个数组变量的含义,如下:

/* this thread gets the stream from the disk or the network */
static int read_thread(void *arg)
{
    VideoState *is = arg;
    AVFormatContext *ic = NULL;
    int err, i, ret;
    int st_index[AVMEDIA_TYPE_NB]; -------- st_index
    AVPacket *pkt = NULL;
    int64_t stream_start_time;
    int pkt_in_play_range = 0;
    const AVDictionaryEntry *t;
    SDL_mutex *wait_mutex = SDL_CreateMutex();
    int scan_all_pmts_set = 0;
    int64_t pkt_ts;

    if (!wait_mutex) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
        ret = AVERROR(ENOMEM);
        goto fail;
    }

    memset(st_index, -1, sizeof(st_index));-------- st_index
    is->eof = 0;
...........

st_index[] 这个数组用的宏是 AVMEDIA_TYPE_NB,也就是这个数组涵盖了各种数据流,音频,视频,字幕,附件流等等。因为一个MP4里面可能会有多个视频流。

例如 第 5,第 6 个流都是视频流。这时候 st_index[AVMEDIA_TYPE_VIDEO] 保存的可能就是 5 或者 6 ,代表要播放哪个视频流,其他数据流类推。

默认 st_index[] 数组的值是通过 av_find_best_stream() 确定的,是通过 bit_rate 最大比特率,codec_info_nb_frames 等参数找出 最好的那个音频流 或者 视频流。


第二个重点是 interrupt_callback 这个操作,指定了中断回调函数。

    pkt = av_packet_alloc();
    if (!pkt) {
        av_log(NULL, AV_LOG_FATAL, "Could not allocate packet.\n");
        ret = AVERROR(ENOMEM);
        goto fail;
    }
    ic = avformat_alloc_context();
    if (!ic) {
        av_log(NULL, AV_LOG_FATAL, "Could not allocate context.\n");
        ret = AVERROR(ENOMEM);
        goto fail;
    }
    ic->interrupt_callback.callback = decode_interrupt_cb; ------------------中断回调函数
    ic->interrupt_callback.opaque = is;
    if (!av_dict_get(format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) {
        av_dict_set(&format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE);
        scan_all_pmts_set = 1;
    }

decode_interrupt_cb() 函数实现如下:

static int decode_interrupt_cb(void *ctx)
{
    VideoState *is = ctx;
    return is->abort_request;
}

首先,is->abort_request 这个变量控制着整个播放器要不要停止播放,然后退出。

在播放本地文件的时候,interrupt_callback 回调函数的作用不是特别明显,因为本地读取MP4, av_read_frame() 会非常快返回。

但是如果在播放网络流的时候,网络卡顿,av_read_frame() 可能要 8 秒才能返回,这时候如果想关闭播放器,就需要 av_read_frame() 尽快地返回,不要再阻塞了。这时候,就需要 interrupt_callback 了,因为在 8 秒 内,av_read_frame() 内部也会定时执行 interrupt_callback(),只要 interrupt_callback() 返回 1,av_read_frame() 就会不再阻塞,立即返回。

提醒:播放网络流的时候,avformat_find_stream_info() 可能会跟 av_read_frame() 一样阻塞很久。


read_thread() 线程的第三个重点avformat_open_input() 函数的使用。

    err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
    if (err < 0) {
        print_error(is->filename, err);
        ret = -1;
        goto fail;
    }
    if (scan_all_pmts_set)
        av_dict_set(&format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE);

    if ((t = av_dict_get(format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {
        av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);
        ret = AVERROR_OPTION_NOT_FOUND;
        goto fail;
    }

最后的参数 format_opts 是一个 AVDictionary (字典)。注意,如果 avformat_open_input 函数内部使用了字典的某个选项,就会把这个选项从字典剔除。

所以可以看到,后面判断了还有哪些 option 没使用,这些无法使用的 option (选项),通常是因为命令行参数写错了。

MP4,FLV,TS,等等容器格式,都有一些相同的 option,也有一些不同的 options。具体可以通过以下命令查看容器支持哪些 option ?

ffmpeg -h demuxer=mp4

提示:各种流媒体格式 也可以看成是 容器。


read_thread() 里面会处理 seek 操作,简单场景下,不会跑进去 seek 条件。


read_thread() 线程的第四个重点AVRational sar 变量的应用,如下:

    is->show_mode = show_mode;
    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
        AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];
        AVCodecParameters *codecpar = st->codecpar;
        AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL);
        if (codecpar->width)
            set_default_window_size(codecpar->width, codecpar->height, sar);--------------第四个重点
    }

sar 这个值是不太容易理解的,我刚开始也被这个 sar 搞懵。我之前以为 sar 等于 width/height (宽高比) ,后来发现不是宽高比。

其实 sar 是以前的显示设备设计的历史遗留问题,不用过多关注,只需要知道,显示的时候用 sar 这个比例拉伸 width 跟 height 作为显示窗口,图像播放就不会扭曲了。sar 在大部分情况都是 1:1。


接下来来到 read_thread() 线程里最重要的重点,stream_component_open() 函数的调用,audio_thread()video_thread() 等解码线程就是从 stream_component_open() 里 创建出来的


上面所有代码干的活,主要是找出最好的音视频流,设置回调,各种初始化,打开容器实例。

现在到了 read_thread() 线程的主要任务,那就是进入 for (;😉 {…} 死循环不断 从 容器实例 读取 AVPacket ,然后丢进去对应的 PacketQueue 队列

for 循环里面也有一些重点,如下:

    for (;;) {
        if (is->abort_request)
            break;
        if (is->paused != is->last_paused) {
            is->last_paused = is->paused;
            if (is->paused)
                is->read_pause_return = av_read_pause(ic);
            else
                av_read_play(ic);
        }
#if CONFIG_RTSP_DEMUXER || CONFIG_MMSH_PROTOCOL

对于播放本地文件,av_read_pause() 函数其实是没有作用的。av_read_pause() 只对网络流播放有效,有些流媒体协议支持暂停操作,暂停了,服务器就不会再往 ffplay 推送数据,如果想重新推数据,需要调用 av_read_play()

for 循环里面的第二个重点是 判断 队列缓存中的 AVPacket 是否够用,够用就会休眠 10ms。如下:

        /* if the queue are full, no need to read more */
        if (infinite_buffer<1 &&
              (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
            || (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
                stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
                stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
            /* wait 10 ms */
            SDL_LockMutex(wait_mutex);
            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);-----------------休眠 10ms
            SDL_UnlockMutex(wait_mutex);
            continue;
        }

在播放本地文件的时候,infinite_buffer 总是 0,所以不用管它。

可以看到,判断 AVPacket 是否够用,就是根据 size 来判断,还有 stream_has_enough_packets() 函数,实现如下:

static int stream_has_enough_packets(AVStream *st, int stream_id, PacketQueue *queue) {
    return stream_id < 0 ||
           queue->abort_request ||
           (st->disposition & AV_DISPOSITION_ATTACHED_PIC) ||
           queue->nb_packets > MIN_FRAMES && (!queue->duration || av_q2d(st->time_base) * queue->duration > 1.0);
}

stream_has_enough_packets() 主要就是确认 队列至少有 MIN_FRAMES 个帧,而且所有帧的播放时长加起来大于 1 秒钟。


当 队列缓存中的 AVPacket 未满的时候,就会直接去读磁盘数据,把 AVPacket 读出来,但是也不是读出来就会立即丢进去 PacketQueue 队列,而是会判断一下AVPacket 是否在期待的播放时间范围内。如下:

        ret = av_read_frame(ic, pkt);
        if (ret < 0) {
            if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {
                if (is->video_stream >= 0)
                    packet_queue_put_nullpacket(&is->videoq, pkt, is->video_stream);
                if (is->audio_stream >= 0)
                    packet_queue_put_nullpacket(&is->audioq, pkt, is->audio_stream);
                if (is->subtitle_stream >= 0)
                    packet_queue_put_nullpacket(&is->subtitleq, pkt, is->subtitle_stream);
                is->eof = 1;
            }
            if (ic->pb && ic->pb->error) {
                if (autoexit)
                    goto fail;
                else
                    break;
            }
            SDL_LockMutex(wait_mutex);
            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
            SDL_UnlockMutex(wait_mutex);
            continue;
        } else {
            is->eof = 0;
        }
        /* check if packet is in play range specified by user, then queue, otherwise discard */
        stream_start_time = ic->streams[pkt->stream_index]->start_time;
        pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
        pkt_in_play_range = duration == AV_NOPTS_VALUE ||-------------------是否在期待的播放时间范围内
                (pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *
                av_q2d(ic->streams[pkt->stream_index]->time_base) -
                (double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
                <= ((double)duration / 1000000);
        if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
            packet_queue_put(&is->audioq, pkt);
        } else if (pkt->stream_index == is->video_stream && pkt_in_play_range
                   && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
            packet_queue_put(&is->videoq, pkt);
        } else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
            packet_queue_put(&is->subtitleq, pkt);
        } else {
            av_packet_unref(pkt);
        }

可以看到 定义了 一个 变量 pkt_in_play_range 来确定是否在播放时间范围内。播放时间范围这个概念是这样的。如果下面这样播放一个视频:

ffplay -i juren-5s.mp4

因为 juren-5s.mp4 是一个 5 秒的视频,而且命令行没有指定 -t,所以这时候 播放时间范围 就是 0 ~ 5 秒。只要读出来的 AVPacket 的 pts 在 0 ~ 5秒范围内,pkt_in_play_range 变量就为真。因此所有读出来的 AVPacket 都是符合播放时间范围的。

但是如果加了 -t 参数,如下:

ffplay -t 2 -i juren-5s.mp4

上面的的命令是 只播放 2秒视频,也就是 播放时间范围 变成了 0 ~ 2 秒,如果读出来的 AVPacket 的 pts 大于 2 秒,就会被丢弃。

这里就有一个有趣的事情,当视频播放到 第二秒的时候,虽然画面停止了,但是 read_thread() 还是会一直读数据,但由于不符合播放时间范围,会一直丢弃。直到读到文件结尾,返回 AVERROR_EOF 才会停下来休眠一小段时间。

读出来的 AVPacket 符合播放时间之后,就会 用 packet_queue_put() 丢进去 PacketQueue 队列。

可以看到,音频,视频流,是有各自的 PacketQueue 队列的,is->audioqis->videoq


FFplay 播放器的逻辑流转,目前就转到 for (;;) {...} 循环里面不断读取 AVPacket 数据。

read_thread() 线程函数最后的 fail: 标签代码,是播放器退出之后的清理逻辑,这个目前不需要理会。

stream_component_open函数分析

stream_component_open() 函数主要作用是打开 音频流或者视频流 对应的解码器,开启解码线程去解码。

流程图如下:
在这里插入图片描述

stream_component_open() 的函数定义如下:

/* open a given stream. Return 0 if OK */
static int stream_component_open(VideoState *is, int stream_index)

可以看到,函数的参数非常简单,第一个参数是 VideoState *is 全局管理器,第二个参数 stream_index 是 数据流 的索引值。

下面来分析 stream_component_open() 的函数里面的重点代码:

/* open a given stream. Return 0 if OK */
static int stream_component_open(VideoState *is, int stream_index)
{
    AVFormatContext *ic = is->ic;
    AVCodecContext *avctx;
    const AVCodec *codec;
    const char *forced_codec_name = NULL;
    AVDictionary *opts = NULL;
    const AVDictionaryEntry *t = NULL;
    int sample_rate;
    AVChannelLayout ch_layout = { 0 };
    int ret = 0;
    int stream_lowres = lowres;

    if (stream_index < 0 || stream_index >= ic->nb_streams)
        return -1;

    avctx = avcodec_alloc_context3(NULL);-----------------重点代码
    if (!avctx)
        return AVERROR(ENOMEM);

    ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);-----------------重点代码
    if (ret < 0)
        goto fail;
    avctx->pkt_timebase = ic->streams[stream_index]->time_base;

    codec = avcodec_find_decoder(avctx->codec_id);-----------------重点代码

一开始的 avcodec_alloc_context3()avcodec_parameters_to_context() ,这可以说是常规操作了,就是申请一个解码器实例的内存,然后把容器流里面的信息拷贝过去。容器里面通常都是有编码器信息的


第二个重点是,使用指定的编码器,例如你不用想 libx264 编码器,而是使用 openh264 编码器,就可以用 -c:v openh264 参数指定编码器。如下:

ffplay -c:v openh264 juren.mp4

也有另一种情况,就是容器里面记录的编码器信息是错误的,而你又知道正确的编码器信息,就可以强制指定。命令行的参数会赋值给 forced_codec_name 变量。

    if (ret < 0)
        goto fail;
    avctx->pkt_timebase = ic->streams[stream_index]->time_base;

    codec = avcodec_find_decoder(avctx->codec_id);-----------------关键代码

    switch(avctx->codec_type){
        case AVMEDIA_TYPE_AUDIO   : is->last_audio_stream    = stream_index; forced_codec_name =    audio_codec_name; break;-----------------关键代码
        case AVMEDIA_TYPE_SUBTITLE: is->last_subtitle_stream = stream_index; forced_codec_name = subtitle_codec_name; break;-----------------关键代码
        case AVMEDIA_TYPE_VIDEO   : is->last_video_stream    = stream_index; forced_codec_name =    video_codec_name; break;-----------------关键代码
    }
    if (forced_codec_name)
        codec = avcodec_find_decoder_by_name(forced_codec_name);-----------------关键代码
    if (!codec) {
        if (forced_codec_name) av_log(NULL, AV_LOG_WARNING,
                                      "No codec could be found with name '%s'\n", forced_codec_name);
        else                   av_log(NULL, AV_LOG_WARNING,
                                      "No decoder could be found for codec %s\n", avcodec_get_name(avctx->codec_id));
        ret = AVERROR(EINVAL);
        goto fail;
    }

第三个重点,只有两个函数,filter_codec_opts()avcodec_open2()

    if (fast)
        avctx->flags2 |= AV_CODEC_FLAG2_FAST;

    opts = filter_codec_opts(codec_opts, avctx->codec_id, ic, ic->streams[stream_index], codec);-----------filter_codec_opts()
    if (!av_dict_get(opts, "threads", NULL, 0))
        av_dict_set(&opts, "threads", "auto", 0);
    if (stream_lowres)
        av_dict_set_int(&opts, "lowres", stream_lowres, 0);
    if ((ret = avcodec_open2(avctx, codec, &opts)) < 0) {-----------------avcodec_open2()
        goto fail;
    }
    if ((t = av_dict_get(opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {
        av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);
        ret =  AVERROR_OPTION_NOT_FOUND;
        goto fail;
    }

filter_codec_opts() 这个函数实际上就是把命令行参数的相关参数提取出来。举个例子:

ffpaly -b:v 2000k -i juren-5s.mp4

上面的命令,指定了解码器的码率,但是他指定的是视频的码率,当 stream_component_open() 打开视频流的时候,这个 码率参数才会被 filter_codec_opts() 提取出来。

而stream_component_open() 打开音频流的时候,b:v 不会被提取出来,因为这个参数是跟 视频流 相关的。

所以你可以把 filter_codec_opts() 看成是一个处理命令行参数的函数,提取相关的参数。至于什么是相关,可以自行看这个函数的内部实现。
然后 avcodec_open2() 就会接受 filter_codec_opts() 返回 的 AVDictionary 参数。

至此,解码器参数已经设置完毕,解码器也已经打开了了。


第四个重点是 把流属性设置为不丢弃, 就是下面这一句代码。

ic->streams[stream_index]->discard = AVDISCARD_DEFAULT;

可以看到,stream_component_open() 函数会把打开的流的 discard 设置为 AVDISCARD_DEFAULT,这样这个流的数据就可以从 av_read_frame() 函数里面读出来了。

注意,ffplay.c 之前 在 read_thread() 函数里面,是把所有的流都设置为了 AVDISCARD_ALL,也就是会丢弃所有流的数据包。

    for (i = 0; i < ic->nb_streams; i++) {
        AVStream *st = ic->streams[i];
        enum AVMediaType type = st->codecpar->codec_type;
        st->discard = AVDISCARD_ALL;---------------------- read_thread() 函数
        if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1)
            if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)
                st_index[type] = i;
    }

所以,如果 mp4 里面有多个视频流,av_read_frame() 只会读取最好的那个视频流的包,音频流同理。

最后一个重点,就是一个 switch case 的逻辑,如下:

    is->eof = 0;
    ic->streams[stream_index]->discard = AVDISCARD_DEFAULT;
    switch (avctx->codec_type) {---------------- switch case 的逻辑
    case AVMEDIA_TYPE_AUDIO:
#if CONFIG_AVFILTER
        {
            AVFilterContext *sink;

这段代码非常多,这里分别对 音频,视频,字幕做了区别处理。但是可以看到,音频的逻辑代码明显是最多的。

下面开始分析重点,如下:

#if CONFIG_AVFILTER
        {
            AVFilterContext *sink;

            is->audio_filter_src.freq           = avctx->sample_rate;----------------audio_filter_src
            ret = av_channel_layout_copy(&is->audio_filter_src.ch_layout, &avctx->ch_layout);
            if (ret < 0)
                goto fail;
            is->audio_filter_src.fmt            = avctx->sample_fmt;
            if ((ret = configure_audio_filters(is, afilters, 0)) < 0)----------------configure_audio_filters
                goto fail;
            sink = is->out_audio_filter;
            sample_rate    = av_buffersink_get_sample_rate(sink);
            ret = av_buffersink_get_ch_layout(sink, &ch_layout);
            if (ret < 0)
                goto fail;
        }
#else

首先可以看到,他有一个宏判断,大部分情况 AVFILTER 滤镜模块都是启用,所以不用管第二个 else。这里需要注意一下,虽然 ffplay -i juren-5s.mp4 这条命令没有使用滤镜,但是 ffplay 的逻辑还是会创建滤镜实例的,只不过这是一个空的实例。这样做是为了代码逻辑更加通用。

无论命令行参数使不使用滤镜,他都是同样的逻辑。

然后需要注意上图中的 is->audio_filter_src 变量,这个变量存储的实际上是从解码器出来的音频信息。然后调 configure_audio_filters() 这个函数来创建音频流的滤镜。

configure_audio_filters() 函数最重要的地方就是搞好了 is->in_audio_filter 跟 is->out_audio_filter 两个滤镜。解码器输出 AVFrame 之后需要往 in_audio_filter 里面丢,然后播放的时候,需要从 out_audio_filter 读取 AVFrame。

后面的av_buffersink_get_sample_rate() 等函数的调用实际上就是从 buffsink 出口滤镜里面获取到最后的音频信息。


第二个重点如下:

        /* prepare audio output */
        if ((ret = audio_open(is, &ch_layout, sample_rate, &is->audio_tgt)) < 0)-----------------重点
            goto fail;
        is->audio_hw_buf_size = ret;
        is->audio_src = is->audio_tgt;-----------------------重点
        is->audio_buf_size  = 0;
        is->audio_buf_index = 0;

        /* init averaging filter */---------------------------------------start,用在音频向视频同步的场景上的
        is->audio_diff_avg_coef  = exp(log(0.01) / AUDIO_DIFF_AVG_NB);
        is->audio_diff_avg_count = 0;
        /* since we do not have a precise anough audio FIFO fullness,
           we correct audio sync only if larger than this threshold */
        is->audio_diff_threshold = (double)(is->audio_hw_buf_size) / is->audio_tgt.bytes_per_sec;----end,用在音频向视频同步的场景上的

        is->audio_stream = stream_index;-----------------------重点
        is->audio_st = ic->streams[stream_index];--------------重点

audio_open() 函数的内部逻辑就是调 SDL_OpenAudioDevice() 打开音频设备,不过由于音频设备各种各样,从 buffersink 滤镜出来的音频帧,不一定被硬件设备支持,所以可能需要降低采样率之类。例如:有些比较差的音响不支持太高采样或者太多的声道数。

audio_open() 函数会选出被硬件设备支持的采样率,声道数 去打开。这些最终的声道数,采样率等信息,就放在 is->audio_tgt 变量返回。

所以 audio_open() 函数的重点是,打开音频设备,并且把最终的音频信息放在 is->audio_tgt 变量里面了。


接着,会把 audio_tgt 拷贝给 audio_src,如下:

is->audio_src = is->audio_tgt;

这句代码看起来会有点莫名其妙,为什么把 audio_tgt 赋值给 audio_src 呢?

首先 is->audio_src 是一个 struct AudioParams ,一个存储音频格式信息的结构体。变量名里有个 src ,代表音频的源头,也就是音频源头的格式是怎样的。但是注意这个源头不是指 MP4 文件里面的音频格式,虽然这个也是源头。

但是它的 src 指的是 is->swr_ctx 重采样实例的源头,也就是当需要进行重采样的时候,要输入给 is->swr_ctx 的原始音频格式就是 is->audio_src。流程图如下:

在这里插入图片描述

上面的流程图看起来比较容易理解,这是需要重采样的流程,但是不一定总是需要重采样的,当 buffersink 出口滤镜出来的音频格式,跟打开硬件设备时候的音频格式(is->audio_tgf)一致的时候,就不需要重采样了。

上面的流程图,如果去掉重采样,是不是就直接是 is->audio_src = is->audio_tgt; 了?

因此 is->audio_src 存储的其实是 buffersink 出口滤镜的音频格式,但是因为出口滤镜的音频格式可能跟 is->audio_tgt 本身是一样的,所以它上面那句代码就这样写了。

buffersink 跟 audio_tgt 音频格式不一样,就需要重采样。从重采样实例 is->swr_ctx 角度来看, is->audio_src 确实是源头。只是他的代码取巧了一下。

先剧透一下后面 audio_decode_frame() 函数中的重采样代码,如下

    if (af->frame->format        != is->audio_src.fmt            ||
        av_channel_layout_compare(&af->frame->ch_layout, &is->audio_src.ch_layout) ||
        af->frame->sample_rate   != is->audio_src.freq           ||
        (wanted_nb_samples       != af->frame->nb_samples && !is->swr_ctx)) {
        swr_free(&is->swr_ctx);
        swr_alloc_set_opts2(&is->swr_ctx,
                            &is->audio_tgt.ch_layout, is->audio_tgt.fmt, is->audio_tgt.freq,
                            &af->frame->ch_layout, af->frame->format, af->frame->sample_rate,
                            0, NULL);
        if (!is->swr_ctx || swr_init(is->swr_ctx) < 0) {
            av_log(NULL, AV_LOG_ERROR,
                   "Cannot create sample rate converter for conversion of %d Hz %s %d channels to %d Hz %s %d channels!\n",
                    af->frame->sample_rate, av_get_sample_fmt_name(af->frame->format), af->frame->ch_layout.nb_channels,
                    is->audio_tgt.freq, av_get_sample_fmt_name(is->audio_tgt.fmt), is->audio_tgt.ch_layout.nb_channels);
            swr_free(&is->swr_ctx);
            return -1;
        }
        if (av_channel_layout_copy(&is->audio_src.ch_layout, &af->frame->ch_layout) < 0)
            return -1;
        is->audio_src.freq = af->frame->sample_rate;------------------重新赋值为源头
        is->audio_src.fmt = af->frame->format;
    }

小总结:ffplay 有两个处理音频的地方,一个是 滤镜(is->agraph),一个是重采样(is->swr_ctx)。


最后,就是记录播放的音频流信息,其他的视频流,字幕流也有类似的操作,如下:

is->audio_stream = stream_index;
is->audio_st = ic->streams[stream_index];

最后一个重点就是调用 decoder_init() 与 decoder_start(),如下:

        if ((ret = decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread)) < 0)--------decoder_init
            goto fail;
        if ((is->ic->iformat->flags & (AVFMT_NOBINSEARCH | AVFMT_NOGENSEARCH | AVFMT_NO_BYTE_SEEK)) && !is->ic->iformat->read_seek) {
            is->auddec.start_pts = is->audio_st->start_time;
            is->auddec.start_pts_tb = is->audio_st->time_base;
        }
        if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)----------------decoder_start
            goto out;
        SDL_PauseAudioDevice(audio_dev, 0);---------------SDL_PauseAudioDevice
        break;

decoder_init() 函数是比较简单的,不过它用了一个新的数据结构 struct Decoder,所以我们先讲一下这个结构,如下:

typedef struct Decoder {
    AVPacket *pkt; //要进行解码的 AVPacket,也是要发送给解码器的 AVPacket
    PacketQueue *queue; // AVPacket 队列
    AVCodecContext *avctx; //解码器实例
    int pkt_serial; //序列号
    int finished; //已完成的时候,finished 等于上面的 pkt_serial。当 buffersink 输出 EOF 的时候就是已完成。
    int packet_pending; //代表上一个 AVPacket 已经从队列取出来了,但是未发送成功给解码器。未发生成功的会保留在第一个字段 pkt 里面,下次会直接发送,不从队列取。
    SDL_cond *empty_queue_cond; //条件变量,AVPacket 队列已经没有数据的时候会激活这个条件变量。
    int64_t start_pts; //流的第一帧的pts
    AVRational start_pts_tb; //流的第一帧的pts的时间基
    int64_t next_pts; //下一帧的pts,只有音频用到这个 next_pts 字段
    AVRational next_pts_tb; //下一帧的pts的时间基
    SDL_Thread *decoder_tid; //解码线程 ID。
} Decoder;

我讲解讲一下 struct Decoder 结构的一些字段,首先是第一个 AVPacket *pkt ,这个实际上就是从 AVPacket 队列拿出来的。然后把这个 pkt 发送给解码器,如果发送成功,那当然是 unref 这个 pkt,但是如果发送给解码器失败,就会把 packet_pending 置为1,pkt 不进行 unref,下次再继续发送。

还有一个需要讲解的是 next_pts 字段,一些读者可能会疑惑,不是每一个 AVFrame 都有 pts 的吗? 为什么还需要这个 next_pts 这个字段?

这就是因为解码出来的 AVFrame 的 pts 有些是 AV_NOPTS_VALUE,这时候就需要 next_pts 来纠正。

next_pts 的计算规则就是上一帧的 pts 加上他的样本数(也就是播放多久)。

注意:视频流没有使用 next_pts 来纠正,只有音频流用了 next_pts,如下:

                    case AVMEDIA_TYPE_AUDIO:--------------------AVMEDIA_TYPE_AUDIO
                        ret = avcodec_receive_frame(d->avctx, frame);
                        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);----------------next_pts
                            if (frame->pts != AV_NOPTS_VALUE) {
                                d->next_pts = frame->pts + frame->nb_samples;
                                d->next_pts_tb = tb;
                            }
                        }
                        break;

接下来分析decoder_init() 函数,代码如下:

static int decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond) {
    memset(d, 0, sizeof(Decoder));
    d->pkt = av_packet_alloc();
    if (!d->pkt)
        return AVERROR(ENOMEM);
    d->avctx = avctx;
    d->queue = queue;
    d->empty_queue_cond = empty_queue_cond;
    d->start_pts = AV_NOPTS_VALUE;
    d->pkt_serial = -1;
    return 0;
}

可以看到,就是做一些赋值,比较简单,但是也有一个重点,就是他的 empty_queue_cond 实际上就是 continue_read_thread,只是换了个名字。

        if ((ret = decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread)) < 0)
            goto fail;
        if ((is->ic->iformat->flags & (AVFMT_NOBINSEARCH | AVFMT_NOGENSEARCH | AVFMT_NO_BYTE_SEEK)) && !is->ic->iformat->read_seek) {
            is->auddec.start_pts = is->audio_st->start_time;
            is->auddec.start_pts_tb = is->audio_st->time_base;
        }

接着分析下一个函数 decoder_start(),代码如下:

static int decoder_start(Decoder *d, int (*fn)(void *), const char *thread_name, void* arg)
{
    packet_queue_start(d->queue);
    d->decoder_tid = SDL_CreateThread(fn, thread_name, arg);
    if (!d->decoder_tid) {
        av_log(NULL, AV_LOG_ERROR, "SDL_CreateThread(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    return 0;
}

比较简单,就是开启 SDL 解码线程。


至此,switch case 里面对于音频的处理就讲解完毕,对于视频的处理更加简单,仅仅调了 decoder_init()decoder_start(),如下:

    case AVMEDIA_TYPE_VIDEO:
        is->video_stream = stream_index;
        is->video_st = ic->streams[stream_index];

        if ((ret = decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread)) < 0)
            goto fail;
        if ((ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)) < 0)
            goto out;
        is->queue_attachments_req = 1;
        break;
    case AVMEDIA_TYPE_SUBTITLE:
        is->subtitle_stream = stream_index;
        is->subtitle_st = ic->streams[stream_index];

        if ((ret = decoder_init(&is->subdec, avctx, &is->subtitleq, is->continue_read_thread)) < 0)
            goto fail;
        if ((ret = decoder_start(&is->subdec, subtitle_thread, "subtitle_decoder", is)) < 0)
            goto out;
        break;

stream_component_open()函数分析完毕。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值