ffplay源码分析3-代码框架

本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10301831.html

ffplay 是 FFmpeg 工程自带的简单播放器,使用 FFmpeg 提供的解码器和 SDL 库进行视频播放。本文基于 FFmpeg 工程 8.0 版本进行分析,其中 ffplay 源码清单如下:
https://github.com/FFmpeg/FFmpeg/blob/n8.0/fftools/ffplay.c

在尝试分析源码前,可先阅读如下参考文章作为铺垫:
[1]. 雷霄骅,视音频编解码技术零基础学习方法
[2]. 视频编解码基础概念
[3]. 色彩空间与像素格式
[4]. 音频参数解析
[5]. FFmpeg基础概念

“ffplay源码分析”系列文章如下:
[1]. ffplay源码分析1-概述
[2]. ffplay源码分析2-数据结构
[3]. ffplay源码分析3-代码框架
[4]. ffplay源码分析4-音视频同步
[5]. ffplay源码分析5-图像格式转换
[6]. ffplay源码分析6-音频重采样
[7]. ffplay源码分析7-播放控制

3. 代码框架

本节简单梳理 ffplay.c 代码框架。一些关键问题及细节问题在后续章节探讨。本文只关注视频、音频的解码和播放,不关注字幕。

3.1 流程图

ffplay流程图

3.2 主线程:视频播放

主线程主要实现三项功能:视频播放(音视频同步)、字幕播放、SDL 消息处理。

看一下 main() 函数主要实现:

int main(int argc, char **argv)
{
    ...
    // 1. 解析命令行参数
    ret = parse_options(NULL, argc, argv, options, opt_input_file);
    ...

    // 2. 初始化与配置SDL系统
    flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;
    ...
    if (SDL_Init (flags)) {
        ...
    }
    ...

    // 3. 初始化队列、时钟,创建解复用线程
    is = stream_open(input_filename, file_iformat);
    ...

    // 4. SDL消息处理和视频播放
    event_loop(is);

    /* never returns */

    return 0;
}

主线程在进行一些必要的初始化工作、创建解复用线程后,即进入 event_loop() 主循环,处理视频播放和 SDL 消息。

3.2.1 stream_open()

看一下 stream_open() 函数实现:

static VideoState *stream_open(const char *filename,
                               const AVInputFormat *iformat)
{
    ...
    /* 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;
    ...

    init_clock(&is->vidclk, &is->videoq.serial);
    init_clock(&is->audclk, &is->audioq.serial);
    init_clock(&is->extclk, &is->extclk.serial);
    ...
    is->read_tid     = SDL_CreateThread(read_thread, "read_thread", is);
    ...
}

stream_open() 中初始化了视频、音频、字幕的 frame 队列 和 packet 队列,以及用于音视频同步的视频时钟、音频时钟和外部时钟,并创建了第一个子线程:解复用线程。解复用线程被创建后,又在解复用线程里进一步创建了音频解码、视频解码和字幕解码线程,另外在解复用线程中打开音频设备时还隐式创建了音频播放线程,详情参考后文。视频播放线程在哪呢?main() 函数所在的主线程就是视频播放线程。这样,所有线程都齐全了。

3.2.2 event_loop()

event_loop() 函数是主线程的主循环,实现如下:

static void event_loop(VideoState *cur_stream)
{
    ...
    for (;;) {
        ...
        refresh_loop_wait_event(cur_stream, &event);
        // SDL消息处理
        switch (event.type) {
        case SDL_KEYDOWN:
            switch (event.key.keysym.sym) {
            case SDLK_f:            // f键:强制刷新
                ...
                break;
            case SDLK_p:            // p键
            case SDLK_SPACE:        // 空格键:暂停
                ...
            case SDLK_s:            // s键:逐帧播放
                ...
                break;
            ...
        ...
        }
    }
}

switch case 语句处理各种 SDL 消息,比如暂停、强制刷新等按键事件,比较简单。refresh_loop_wait_event() 轮询获取 SDL 消息,有消息就取出一条 SDL 消息后由 switch case 语句处理,无消息则在 refresh_loop_wait_event() 内部播放视频。

3.2.3 refresh_loop_wait_event()

主要代码在 refresh_loop_wait_event() 函数中,如下:

static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
    double remaining_time = 0.0;
    SDL_PumpEvents();
    while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) {
        if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
            SDL_ShowCursor(0);
            cursor_hidden = 1;
        }
        if (remaining_time > 0.0)
            av_usleep((int64_t)(remaining_time * 1000000.0));
        remaining_time = REFRESH_RATE;
        if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
            video_refresh(is, &remaining_time);
        SDL_PumpEvents();
    }
}

SDL_PumpEvents() 收集输入设备 (键盘鼠标等) 上报的输入消息,更新 SDL 消息队列。在查询 SDL 消息队列前,必须先调用 SDL_PumpEvents() 来更新 SDL 消息队列,不然消息队列一直是空的。while() 语句用于轮询 SDL 消息队列, 如果 SDL 消息队列中有消息,则从队列头部移出一条消息,返回本函数的上一级函数中去处理此条 SDL 消息;如果没有 SDL 消息,则在此处 while 循环里调用 video_refresh() 播放视频帧,然后继续刷新和查询 SDL 消息队列。video_refresh() 函数实现音视频的同步及视频帧的显示,是 ffplay.c 中最核心函数之一,在“4.4 节 视频同步到音频”中详细分析。

3.2.4 视频帧播放过程

视频解码线程将视频 packet 解码生成视频 frame,并将解码后的视频 frame 存入视频 frame 队列,视频播放线程从视频 frame 队列中取出 frame 进行播放,如下代码流程为视频帧播放流程:

main()
|-> event_loop
    |-> refresh_loop_wait_event()
        |-> video_refresh()           // 从FrameQueue取出帧进行显示
            |-> compute_target_delay()    // 音视频同步时间校正
            |-> video_display()           // 视频帧显示
                |-> SDL_RenderClear()         // 清空SDL渲染目标
                |-> video_image_display()     // 更新SDL渲染目标
                    |-> upload_texture()         // 更新SDL图像数据
                        |-> SDL_UpdateYUVTexture()/SDL_UpdateTexture()
                    |-> SDL_RenderCopyEx()       // 使用SDL图像数据更新SDL渲染目标
                |-> SDL_RenderPresent()       // 执行渲染,更新屏幕显示

3.3 解复用线程

解复用线程读取视频文件,将取到的 packet 根据类型 (音频、视频、字幕) 存入不同的 packet 队列中。

解复用线程的主函数如下,代码流程参考注释:

static int read_thread(void *arg)
{
    ...
    // 1. 打开视频文件,构建AVFormatContext
    err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
    ...
    if (find_stream_info) {
        ...
        err = avformat_find_stream_info(ic, opts);
        ...
    }
    ...

    // 2. 查找用于解码处理的流:视频流、音频流、字幕流
    if (!video_disable)
        st_index[AVMEDIA_TYPE_VIDEO] =          // 视频流
            av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,
                                st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
    if (!audio_disable)
        st_index[AVMEDIA_TYPE_AUDIO] =          // 音频流
            av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,
                                st_index[AVMEDIA_TYPE_AUDIO],
                                st_index[AVMEDIA_TYPE_VIDEO],
                                NULL, 0);
    if (!video_disable && !subtitle_disable)
        st_index[AVMEDIA_TYPE_SUBTITLE] =       // 字幕流
            av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE,
                                st_index[AVMEDIA_TYPE_SUBTITLE],
                                (st_index[AVMEDIA_TYPE_AUDIO] >= 0 ?
                                 st_index[AVMEDIA_TYPE_AUDIO] :
                                 st_index[AVMEDIA_TYPE_VIDEO]),
                                NULL, 0);
     ...

    // 3. 创建对应流的解码线程
    if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
        stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
    }
    ...
    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
        ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
    }
    ...
    if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
        stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);
    }
    ...

    // 4. 解复用处理
    for (;;) {
        ...
        // 4.1 从输入文件中读取一个packet
        ret = av_read_frame(ic, pkt);
        ...
        // 4.2 判断当前packet是否在播放范围内,是则入列,否则丢弃
        stream_start_time = ic->streams[pkt->stream_index]->start_time;     // 第一个显示帧的pts
        pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
        // 简化一下"||"后那个长长的表达式:
        // [pkt_pts  ] - [stream_start_time] - [seek_start_time                  ] <= [duration]
        // [当前帧pts] - [当前流第一帧pts  ] - [当前播放序列第一帧(seek起始点)pts] <= [duration]
        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);
        // 4.3 根据当前packet类型(音频、视频、字幕),将其存入对应的packet队列
        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);
        }
    }
    ...
}

解复用线程实现如下功能:
[1]. 创建音频解码、视频解码、字幕解码三个线程
[2]. 从输入文件读取 packet,根据 packet 类型 (音频、视频、字幕) 将之放入对应的 packet 队列

3.3.1 打开文件查找流

如下代码打开输入视频文件,查找视频文件中的视频流、音频流和字幕流:

static int read_thread(void *arg)
{
    ...
    AVFormatContext *ic = NULL;
    // 打开视频文件,构建AVFormatContext
    err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
    ...
    if (find_stream_info) {
        ...
        err = avformat_find_stream_info(ic, opts);
        ...
    }
    ...

    // 查找用于解码处理的流:视频流、音频流、字幕流
    if (!video_disable)
        st_index[AVMEDIA_TYPE_VIDEO] =          // 视频流
            av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,
                                st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
    if (!audio_disable)
        st_index[AVMEDIA_TYPE_AUDIO] =          // 音频流
            av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,
                                st_index[AVMEDIA_TYPE_AUDIO],
                                st_index[AVMEDIA_TYPE_VIDEO],
                                NULL, 0);
    if (!video_disable && !subtitle_disable)
        st_index[AVMEDIA_TYPE_SUBTITLE] =       // 字幕流
            av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE,
                                st_index[AVMEDIA_TYPE_SUBTITLE],
                                (st_index[AVMEDIA_TYPE_AUDIO] >= 0 ?
                                 st_index[AVMEDIA_TYPE_AUDIO] :
                                 st_index[AVMEDIA_TYPE_VIDEO]),
                                NULL, 0);
    ...
}

avformat_open_input() 和 avformat_find_stream_info() 这两个函数是用来构建 AVFormatContext 的。avformat_open_input() 打开输入文件并读取文件头,将读取到的文件格式信息存储在 AVFormatContext 中,即存储在变量 ic 中。avformat_find_stream_info() 进一步充实 AVFormatContext 信息,它会读取一段视频文件数据尝试解码,将取到的流信息填入 ic->streams 指针数组里。

构建了 AVFormatContext 后,调用了 av_find_best_stream() 查找具体的视频流、音频流和字幕流,并将获取的流的 index 存入 st_index 数组中。

3.3.2 创建各解码线程

如下代码创建了音频流、视频流、字幕流的解码线程:

static int read_thread(void *arg)
{
    ...
    // 创建对应流的解码线程
    if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
        // 创建音频解码线程,同时打开音频设备时也创建了音频播放线程(SDL内置线程)
        stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
    }
    ...
    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
        // 创建视频解码线程
        ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
    }
    ...
    if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
      // 创建字幕解码线程
        stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);
    }
    ...
}

3.3.2.1 stream_component_open()

stream_component_open() 实现如下:

static int stream_component_open(VideoState *is, int stream_index)
{
    ...
    if ((ret = avcodec_open2(avctx, codec, &opts)) < 0) {
        goto fail;
    }
    ...

    switch (avctx->codec_type) {
    case AVMEDIA_TYPE_AUDIO:
        ...
        if ((ret = audio_open(is, &ch_layout, sample_rate, &is->audio_tgt)) < 0)
            goto fail;
        ...
        if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)
            goto out;
        SDL_PauseAudioDevice(audio_dev, 0);     // 启用音频回调,开始播放音频
        break;
    case AVMEDIA_TYPE_VIDEO:
        ...
        if ((ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)) < 0)
            goto out;
        ...
        break;
    case AVMEDIA_TYPE_SUBTITLE:
        ...
        if ((ret = decoder_start(&is->subdec, subtitle_thread, "subtitle_decoder", is)) < 0)
            goto out;
        break;
    default:
        break;
    }
    ...
}

stream_component_open() 函数打开了具体的视频、音频、字幕解码器并调用 decoder_start() 创建了对应流的解码线程。

3.3.2.2 decoder_start()

decoder_start() 实现比较简单,先启动对应的 packet 队列,然后创建和启动对应的解码线程,实现如下:

static int decoder_start(Decoder *d, int (*fn)(void *), const char *thread_name, void* arg)
{
    packet_queue_start(d->queue);   // 启动解码线程前先启动packet队列
    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;
}

创建解码线程的函数调用路径为:

read_thread()
|-> stream_component_open()
    |-> decoder_start()
        |-> SDL_CreateThread()

3.3.2.3 打开音频设备

其中音频除打开音频解码器创建音频解码线程外,还通过调用 audio_open() 函数创建了音频播放线程,即音频播放线程和音频解码线程都是在解复用线程中创建的。

audio_open() 函数内部调用了 SDL_OpenAudioDevice() 来打开音频设备,打开音频设备的同时会创建并启动音频播放线程,音频播放线程是 SDL 内置线程,参考 3.6 节。audio_open() 函数如下: 

static int audio_open(void *opaque, AVChannelLayout *wanted_channel_layout, int wanted_sample_rate, struct AudioParams *audio_hw_params)
{
    ...
    while (!(audio_dev = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_CHANNELS_CHANGE))) { ... }
    ...
}

audio_open() 函数中涉及到很多音频参数,音频播放线程会用到这些参数,音频格式的各参数与重采样强相关,audio_open() 的详细实现在后面第 6 节详述,此处略。

创建音频播放线程和音频解码线程的调用路径如下:

read_thread()
|-> stream_component_open()
    |-> audio_open()
        |-> SDL_OpenAudioDevice()           // 创建音频播放线程
    |-> decoder_start()                     // 创建音频解码线程
    |-> SDL_PauseAudioDevice(audio_dev, 0)  // 启动音频播放:SDL将开始按需回调SDL音频回调函数

3.3.3 解复用处理

解复用线程主循环就是在读包解复用,步骤为:1. 读出一个 packet,2. 从播放时间上判断此 packet 是否在播放范围内,不是则丢弃;3. 根据 packet 的类型存入视频、音频或字幕 packet 队列。相关代码如下:

static int read_thread(void *arg)
{
    // 解复用处理
    for (;;) {
        ...
        // 从输入文件中读取一个packet
        ret = av_read_frame(ic, pkt);
        ...
        // 判断当前packet是否在播放范围内,是则入列,否则丢弃
        stream_start_time = ic->streams[pkt->stream_index]->start_time;     // 第一个显示帧的pts
        pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
        // 简化一下"||"后那个长长的表达式:
        // [pkt_pts  ] - [stream_start_time] - [seek_start_time                  ] <= [duration]
        // [当前帧pts] - [当前流第一帧pts  ] - [当前播放序列第一帧(seek起始点)pts] <= [duration]
        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);
        // 根据当前packet类型(音频、视频、字幕),将其存入对应的packet队列
        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);
        }
    }
    ...
}

3.4 视频解码线程

视频解码线程从视频 packet 队列中取数据,解码后存入视频 frame 队列。

视频解码线程 (video_thread) 是在解复用线程 (read_thread) 中创建的,过程参考 3.3.2 节。

3.4.1 video_thread()

视频解码线程将解码后的视频帧放入视频 frame 队列中:

static int video_thread(void *arg)
{
    ...
    for (;;) {
      // 从packet队列中取出一个packet解码得到一个frame
        ret = get_video_frame(is, frame);
        ...

        // 将frame送入滤镜输入端
        ret = av_buffersrc_add_frame(filt_in, frame);
        ...

        while (ret >= 0) {
            ...
            // 从滤镜输出端取frame
            ret = av_buffersink_get_frame_flags(filt_out, frame, 0);
            ...

            // 将当前帧压入frame_queue
            ret = queue_picture(is, frame, pts, duration, fd ? fd->pkt_pos : -1, is->viddec.pkt_serial);
            ...
        }
        ...
    }
    ...
}

基本流程为:调用 get_video_frame() 从视频 packet 队列中取出一个 packet 解码得到一个 frame,将此 frame 送给滤镜处理 (视频滤镜是处理视频原始帧的,不同滤镜提供不同的功能,如缩放、裁剪、旋转、像素格式转换等),将经过滤镜处理后输出的 frame 存入视频 frame 队列。

3.4.2 get_video_frame()

get_video_frame() 实现视频帧解码和按需丢帧两个动作。get_video_frame() 中调用了一个关键函数 decoder_decode_frame(),decoder_decode_frame() 从视频 packet 队列中取出一个 packet 解码得到一个 frame,然后回到 get_video_frame() 函数中判断是否要根据 framedrop 机制丢弃失去同步的视频帧。get_video_frame() 函数实现如下:

static int get_video_frame(VideoState *is, AVFrame *frame)
{
    int got_picture;

    if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0)
        return -1;

    if (got_picture) {
        double dpts = NAN;

        if (frame->pts != AV_NOPTS_VALUE)
            dpts = av_q2d(is->video_st->time_base) * frame->pts;

        frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);

        if (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
            if (frame->pts != AV_NOPTS_VALUE) {
                double diff = dpts - get_master_clock(is);
                if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD &&
                    diff - is->frame_last_filter_delay < 0 &&
                    is->viddec.pkt_serial == is->vidclk.serial &&
                    is->videoq.nb_packets) {
                    is->frame_drops_early++;
                    av_frame_unref(frame);  // 视频帧失去同步则直接扔掉
                    got_picture = 0;
                }
            }
        }
    }

    return got_picture;
}

这里介绍一下 ffplay 中的 framedrop 机制。ffplay 中 framedrop 处理有两处,第一处就在本函数中,解码后得到的 frame 其 pts 与同步时钟偏差太大,也就是失去了同步,那么直接将 frame 丢弃而不存入 frame 队列;第二处是在视频播放线程中的 video_refresh() 函数中 (代码见 4.4.1 节),从 frame 队列中读出 frame 进行显示时如果判断播太慢了,发现下一帧显示时间都超时了,那就直接丢掉当前帧尝试去播放下一帧,这也是失去同步的一种。解码线程中的丢帧计数用 is->frame_drops_early 表示,播放线程中的丢帧计数用 is->frame_drops_late 表示。

命令行里的 "-framedrop" 选项影响丢帧方式。"-framedrop" 选项用于设置当视频帧失去同步时,是否丢弃视频帧。"-framedrop" 选项以 bool 方式改变变量 framedrop值。ffplay 文档中对 "-framedrop" 选项的说明:

Drop video frames if video is out of sync.Enabled by default if the master clock is not set to video.
Use this option to enable frame dropping for all master clock sources, use - noframedrop to disable it.

音视频同步方式有三种:A同步到视频,B同步到音频,C同步到外部时钟。

  1. 当命令行不带 "-framedrop" 选项或 "-noframedrop" 选项时,framedrop 变量值为默认值 -1,若同步方式是 "同步到视频" 则不丢弃失去同步的视频帧,否则丢弃。
  2. 当命令行带 "-framedrop" 选项时,framedrop 值为 1,无论何种同步方式,均丢弃失去同步的视频帧。
  3. 当命令行带 "-noframedrop" 选项时,framedrop 值为 0,无论何种同步方式,均不丢弃失去同步的视频帧。

3.4.3 decoder_decode_frame()

这个函数是很核心的一个函数,可以解码视频帧和音频帧。视频解码线程中,视频帧实际的解码操作就在此函数中进行。分析过程详参 4.3.2 节。

3.5 音频解码线程

音频解码线程从音频 packet 队列中取数据,解码后存入音频 frame 队列

音频解码线程 (audio_thread) 是在解复用线程 (read_thread) 中创建的,过程参考 3.3.2 节。

3.5.1 audio_thread()

音频解码线程将解码后的音频帧放入音频 frame 队列中:

static int audio_thread(void *arg)
{
    ...
    do {
        if ((got_frame = decoder_decode_frame(&is->auddec, frame, NULL)) < 0)
            goto the_end;

        if (got_frame) {
            ...
            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) {
                ...
                // 将frame数据拷入af->frame,af->frame指向音频frame队列尾部
                av_frame_move_ref(af->frame, frame);
                // 更新音频frame队列大小及写指针
                frame_queue_push(&is->sampq);
                ...
            }
            ...
        }
    } while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);
    ...
}

基本流程为:调用 decoder_decode_frame() 从音频 packet 队列中取出一个 packet 解码得到一个 frame,将此 frame 送给滤镜处理 (音频滤镜是处理音频原始帧的,不同滤镜提供不同的音频处理功能),将经过滤镜处理后输出的 frame 存入音频 frame 队列。

3.5.2 decoder_decode_frame()

这个函数是很核心的一个函数,可以解码视频帧和音频帧。音频解码线程中,音频帧实际的解码操作就在此函数中进行。分析过程详参 4.3.2 节。

3.6 音频播放线程

音频播放线程是 SDL 内建线程,通过回调的方式调用用户提供的回调函数。音频播放线程是在解复用线程 (read_thread) 中打开音频设备时由 SDL 内部隐式创建的。

3.6.1 sdl_audio_callback()

解复用线程 (read_thread) 调用 audio_open()-->SDL_OpenAudioDevice() 打开音频设备时会通过 SDL_OpenAudioDevice() 函数的一个输入参数指定音频回调函数,供 SDL 音频播放线程回调,此音频回调函数就是此节的 sdl_audio_callback() 函数。暂停/继续回调过程由 SDL_PauseAudio() 或 SDL_PauseAudioDevice() 控制。 此过程详参 3.3.2.3 节。

SDL 音频回调函数实现如下:

// 音频处理回调函数。读队列获取音频包,解码,播放
// 此函数被SDL按需调用,此函数不在用户主线程中,因此数据需要保护
// \param[in]  opaque 用户在注册回调函数时指定的参数
// \param[out] stream 音频数据缓冲区地址,将解码后的音频数据填入此缓冲区
// \param[out] len    音频数据缓冲区大小,单位字节
// 回调函数返回后,stream指向的音频缓冲区将变为无效
// 双声道采样点的顺序为LRLRLR
/* prepare a new audio buffer */
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
    VideoState *is = opaque;
    int audio_size, len1;

    audio_callback_time = av_gettime_relative();

    while (len > 0) {   // 输入参数len等于is->audio_hw_buf_size,是audio_open()中申请到的SDL音频缓冲区大小
        if (is->audio_buf_index >= is->audio_buf_size) {
           // 1. 从音频frame队列中取出一个frame,转换为音频设备支持的格式,返回值是重采样音频帧的大小
           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;
           }
           is->audio_buf_index = 0;
        }
        // 引入is->audio_buf_index的作用:防止一帧音频数据大小超过SDL音频缓冲区大小,这样一帧数据需要经过多次拷贝
        // 用is->audio_buf_index标识重采样帧中已拷入SDL音频缓冲区的数据位置索引,len1表示本次拷贝的数据量
        len1 = is->audio_buf_size - is->audio_buf_index;
        if (len1 > len)
            len1 = len;
        // 2. 将转换后的音频数据拷贝到音频缓冲区stream中,之后的播放就是音频设备驱动程序的工作了
        if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
            memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
        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);
        }
        len -= len1;
        stream += len1;
        is->audio_buf_index += len1;
    }
    // is->audio_write_buf_size是本帧中尚未拷入SDL音频缓冲区的数据量
    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. */
    // 3. 更新时钟
    if (!isnan(is->audio_clock)) {
        // 更新音频时钟,更新时刻:每次往声卡缓冲区拷入数据后
        // 前面audio_decode_frame中更新的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);
    }
}

在代码中调用了 SDL_PauseAudio(0)SDL_PauseAudioDevice(xxx, 0) 启动音频播放后,SDL 就会按需调用音频回调函数 sdl_audio_callback()。sdl_audio_callback() 的基本流程为:1) 音频重采样,2) 将重采样后的音频数据拷入音频设备缓冲区,移交给音频设备驱动程序进行音频播放,3) 更新音频时钟。

3.6.2 audio_decode_frame()

sdl_audio_callback() 函数中调用了 audio_decode_frame(),audio_decode_frame() 主要是进行音频重采样,音频重采样也就是音频格式转换,这个函数命名不合理,实际功能只是重采样,和解码没关系。audio_decode_frame() 从音频 frame 队列中取出一个 frame,此 frame 的格式是音频流经解码后的格式,音频设备不一定支持此格式,所以要将 frame 转换为音频设备支持的格式。audio_decode_frame() 的实现细节参考“第 6 节 音频重采样”。

posted @ 2019-01-22 08:46  叶余  阅读(12315)  评论(2)    收藏  举报