ffplay_learn

ffplay源码分析

熟悉FFmpeg项目从源码看起,以下是我阅读FFplay的源代码的总结;FFplay是FFmpeg项目提供的播放器示例,它的源代码的量也是不少的,其中很多知识点是我们可以学习和借鉴的。

总结构图

参照雷神(雷霄骅)的FFplay的总体函数调用结构图,自己总结了一个最新版本的结构,其中还有诸多不足,以后有机会慢慢完善;如下图所示。

在这里插入图片描述

这就不对主要函数分别解析,我来学习一下其中关键性的思想和ffplay的体系结构。

视频部分

ffplay video的线程模式

在这里插入图片描述

ffplay选择了sdl作为显示SDK,以实现跨平台支持;因为使用了SDL,而video的显示也依赖SDL的窗口显示系统,所以先从main函数的SDL初始化看起:

int main(int argc, char **argv)
{
  ...
       /* register all codecs, demux and protocols */
#if CONFIG_AVDEVICE
    avdevice_register_all(); //注册所有解码器
#endif
    avformat_network_init();

    init_opts();

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

    show_banner(argc, argv, options); //打印ffmpag库版本信息,编译时间,编译选项,类库信息等

    parse_options(NULL, argc, argv, options, opt_input_file); //解析输入的命令。
...
     if (SDL_Init(flags))
    { //初始化sdl
        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);
		
  	//注册flush packet 只是一个标记作用,用于packet队列中,在对packet队列分析时有说明
    av_init_packet(&flush_pkt);
    flush_pkt.data = (uint8_t *)&flush_pkt;
  
 ...
       if (!display_disable)
    {
        int flags = SDL_WINDOW_HIDDEN;
        if (alwaysontop)
#if SDL_VERSION_ATLEAST(2, 0, 5)
            flags |= SDL_WINDOW_ALWAYS_ON_TOP;
#else
            av_log(NULL, AV_LOG_WARNING, "Your SDL version doesn't support SDL_WINDOW_ALWAYS_ON_TOP. Feature will be inactive.\n");
#endif
        if (borderless)
            flags |= SDL_WINDOW_BORDERLESS;
        else
            flags |= SDL_WINDOW_RESIZABLE;
         //创建sdl 窗口
        window = SDL_CreateWindow(program_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, default_width, default_height, flags);
        SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
        if (window)
        {
          //创建sdl 窗口的渲染器
            renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
            if (!renderer)
            {
                av_log(NULL, AV_LOG_WARNING, "Failed to initialize a hardware accelerated renderer: %s\n", SDL_GetError());
                renderer = SDL_CreateRenderer(window, -1, 0);
            }
            if (renderer)
            {
                if (!SDL_GetRendererInfo(renderer, &renderer_info))
                    av_log(NULL, AV_LOG_VERBOSE, "Initialized %s renderer.\n", renderer_info.name);
            }
        }
        if (!window || !renderer || !renderer_info.num_texture_formats)
        {
            av_log(NULL, AV_LOG_FATAL, "Failed to create window or renderer: %s", SDL_GetError());
            do_exit(NULL);
        }
    }

  	is = stream_open(input_filename, file_iformat); //创建read_thread
    if (!is)
    {
        av_log(NULL, AV_LOG_FATAL, "Failed to initialize VideoState!\n");
        do_exit(NULL);
    }

    event_loop(is); //主线程 ,sdl——event事件监听和处理

}

而这里我们主要的两个函数是stream_openevent_loopstream_open函数的作用是创建read_threadread_thread会打开文件,解析封装,获取AVStream信息,启动解码器(创建解码线程),并开始读取文件;event_loop函数的作用是处理SDL事件队列中的事件和刷新显示数据,下面会针对这两个函数视频部分的内容进行详细说明。

stream_open

其实在stream_open函数里的关键内容都是初始化一些参数,主要的处理逻辑在read_thread中进行。

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

    is = av_mallocz(sizeof(VideoState)); //创建VideoState 这个很重要,它贯穿整个ffplay
  ...
    /* start video display */ //初始化解码后的帧队列
    if (frame_queue_init(&is->pictq, &is->videoq, VIDEO_PICTURE_QUEUE_SIZE, 1) < 0)
        goto fail;
  ...
     //初始化解码前的帧队列
    if (packet_queue_init(&is->videoq) < 0 ||
        ...
        //创建读线程的条件信号
    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);
    ...
    is->read_tid = SDL_CreateThread(read_thread, "read_thread", is); //创建读线程
    ...
    fail:
        stream_close(is);//出错free一些初始化参数
        return NULL;
}

**VideoState**结构的参数详细说明:

/*视频状态器,贯穿整个ffplay的结构*/
typedef struct VideoState
{
    SDL_Thread *read_tid;      //解复用(或读)线程
    AVInputFormat *iformat;    //输入格式
    int abort_request;         //中止请求
    int force_refresh;         //强制刷新
    int paused;                //暂停
    int last_paused;           //最后一次暂停状态
    int queue_attachments_req; //队列附件请求
    int seek_req;              //快进请求
    int seek_flags;            //快进标志
    int64_t seek_pos;          //快进位置
    int64_t seek_rel;
    int read_pause_return; //读暂停
    AVFormatContext *ic;   //解码格式上下文
    int realtime;          //是否实时码流

    Clock audclk; //音频时钟
    Clock vidclk; //视频时钟
    Clock extclk; //外部时钟

    FrameQueue pictq; //视频队列
    FrameQueue subpq; //字幕队列
    FrameQueue sampq; //pcm流队列

    Decoder auddec; //音频解码器
    Decoder viddec; //视频解码器
    Decoder subdec; //字幕解码器

    int audio_stream; //音频码流id

    int av_sync_type; //时钟同步类型

    double audio_clock;
    int audio_clock_serial; //音频时钟序号
    double audio_diff_cum;  // 用于音频差分计算 /* used for AV difference average computation */
    double audio_diff_avg_coef;
    double audio_diff_threshold;        //音频差分阈值
    int audio_diff_avg_count;           // 平均差分数量
    AVStream *audio_st;                 // 音频码流
    PacketQueue audioq;                 // 音频源包队列
    int audio_hw_buf_size;              // 硬件缓冲大小
    uint8_t *audio_buf;                 // 音频缓冲区
    uint8_t *audio_buf1;                // 音频缓冲区1
    unsigned int audio_buf_size;        // 音频缓冲大小 /* in bytes */
    unsigned int audio_buf1_size;       // 音频缓冲大小 1
    int audio_buf_index; /* in bytes */ // 音频缓冲索引
    int audio_write_buf_size;           // 音频写入缓冲大小
    int audio_volume;                   // 音量
    int muted;                          // 是否静音
    struct AudioParams audio_src;       // 音频参数
#if CONFIG_AVFILTER
    struct AudioParams audio_filter_src; // 音频过滤器
#endif
    struct AudioParams audio_tgt; // 音频参数
    struct SwrContext *swr_ctx;   // 音频转码上下文
    int frame_drops_early;
    int frame_drops_late;

    enum ShowMode
    { // 显示类型
        SHOW_MODE_NONE = -1,
        SHOW_MODE_VIDEO = 0,
        SHOW_MODE_WAVES,
        SHOW_MODE_RDFT,
        SHOW_MODE_NB
    } show_mode;
    int16_t sample_array[SAMPLE_ARRAY_SIZE]; // 采样数组
    int sample_array_index;                  // 采样索引
    int last_i_start;                        // 上一开始
    RDFTContext *rdft;                       // 自适应滤波器上下文
    int rdft_bits;                           // 自使用比特率
    FFTSample *rdft_data;                    // 快速傅里叶采样
    int xpos;
    double last_vis_time;
    SDL_Texture *vis_texture; // 音频Texture
    SDL_Texture *sub_texture; // 字幕Texture
    SDL_Texture *vid_texture; // 视频Texture

    int subtitle_stream;   // 字幕码流Id
    AVStream *subtitle_st; // 字幕码流
    PacketQueue subtitleq; // 字幕源包队列

    double frame_timer;                 // 帧计时器
    double frame_last_returned_time;    // 上一次返回时间
    double frame_last_filter_delay;     // 上一个过滤器延时
    int video_stream;                   // 视频码流Id
    AVStream *video_st;                 // 视频码流
    PacketQueue videoq;                 // 视频包队列
    double max_frame_duration;          // 最大帧间显示时间     // maximum duration of a frame - above this, we consider the jump a timestamp discontinuity
    struct SwsContext *img_convert_ctx; // 视频转码上下文
    struct SwsContext *sub_convert_ctx; // 字幕转码上下文
    int eof;                            // 结束标志

    char *filename;                 // 文件名
    int width, height, xleft, ytop; // 宽高,其实坐标
    int step;

#if CONFIG_AVFILTER
    int vfilter_idx;                   // 过滤器索引
    AVFilterContext *in_video_filter;  // the first filter in the video chain
    AVFilterContext *out_video_filter; // the last filter in the video chain
    AVFilterContext *in_audio_filter;  // the first filter in the audio chain
    AVFilterContext *out_audio_filter; // the last filter in the audio chain
    AVFilterGraph *agraph;             // audio filter graph
#endif

    int last_video_stream, last_audio_stream, last_subtitle_stream;

    SDL_cond *continue_read_thread;
} VideoState;
读取线程(read_thread)

read_thread主要按以下步骤执行:

  1. 准备阶段:打开文件,检测Stream信息,打开解码器
  2. 主循环读数据,解封装:读取Packet,存入PacketQueue

read_thread的函数比较长,这里不贴完整代码,直接根据其功能分步分析。

准备阶段

主要执行一下几个步骤的函数:

  1. avformat_open_input
  2. avformat_find_stream_info
  3. av_find_best_stream
  4. stream_component_open
avformat_open_input

该函数用于打开输入流(这个包括文件和网络流,在ffmpeg内部会把每一个协议封装成URLProtocol,文件对于ffmpeg也是一种协议“file”)

    ic = avformat_alloc_context(); //创建 AVformatContext
    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;
    }
    err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts); //打开文件或网络流
    if (err < 0)
    {
        print_error(is->filename, err);
        ret = -1;
        goto fail;
    }

重点强调:**interrupt_callback**用于ffmpeg内部在执行耗时操作时检查是否有退出请求,并提前中断,避免用户退出请求没有及时响应;

avformat_find_stream_info

该函数是通过读取媒体文件的部分数据来分析流信息;在一些缺少头信息的封装下特别有用,如注释:

Read packets of a media file to get stream information. This
is useful for file formats with no headers such as MPEG. This
function also computes the real framerate in case of MPEG-2 repeat
rame mode.
The logical file position is not changed by this function;
examined packets may be buffered for later processing.
av_find_best_stream

该函数选择对应的媒体流,ffplay主要通过下述注释中的3个参数找到“最佳流”;

int av_find_best_stream(AVFormatContext *ic,
                        enum AVMediaType type,//要选择的流类型
                        int wanted_stream_nb,//目标流索引
                        int related_stream,//参考流索引
                        AVCodec **decoder_ret,
                        int flags);
stream_component_open

该函数是根据目标流打开对应的解码器;stream_component_open的函数内容比较长,接下来就逐步分析一下ffplay是如何打开解码器的:

  1. 创建和初始化AVCodecContex,然后通过avcodec_parameters_to_context把所选流的解码参数赋给avctx,最后设了time_base.代码如下:
   //创建编解码器上下文
    avctx = avcodec_alloc_context3(NULL);
    if (!avctx)
        return AVERROR(ENOMEM);
    //从找到对应的流中的codecpar,codecpar其实是avcodec_parameters,
    // 然后将它完全复制到创建的AVCodecContext
    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;
  1. 通过avcodec_find_decoder找到对应的解码器(AVCodec),如果用户设置了forced_codec_name,则通过avcodec_find_decoder_by_name找到对应的解码器;在找到解码器后通过avcodec_open2是否能打开解码器

        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;
        }
    ...
          //打开解码器
        if ((ret = avcodec_open2(avctx, codec, &opts)) < 0)
        {
            goto fail;
        }
    
  2. 对于解码器特定参数的初始化和创建对应流的解码线程;(节选自AVMEDIA_TYPE_VIDEO分支)

            is->video_stream = stream_index;
            is->video_st = ic->streams[stream_index];
    
            decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
            if ((ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)) < 0)
                goto out;
            is->queue_attachments_req = 1;
    

    看看decoder_initdecoder_start两个函数的定义:

    static void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond)
    {
        memset(d, 0, sizeof(Decoder));
        d->avctx = avctx;
        d->queue = queue;
        d->empty_queue_cond = empty_queue_cond;
        d->start_pts = AV_NOPTS_VALUE;
        d->pkt_serial = -1;
    }
    
    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;
    }
    

    decoder_init比较简单,看decoder_startdecoder_start中“启动”了PacketQueue,并创建了一个名为"decoder"的线程专门用于解码,具体的解码流程由传入参数fn决定。比如对于视频,是video_thread

主循环读数据

在读线程中的主循环读数据阶段,主要的代码就av_read_framepacket_queue_putav_read_frame从文件中读取视频数据,并获取一个AVPacket,packet_queue_put把它放入到对应的PacketQueue中。

    for (;;)
    {
        if (is->abort_request) //中断,结束播放
            break;
        if (is->paused != is->last_paused)//暂停/恢复的处理
        {
          ...
        }
       	if (is->seek_req)
        { //跳帧请求
         ...
       	}
      	   /* 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);
            SDL_UnlockMutex(wait_mutex);
            continue;
        }
      //循环播放处理
        if (!is->paused &&
            (!is->audio_st || (is->auddec.finished == is->audioq.serial && frame_queue_nb_remaining(&is->sampq) == 0)) &&
            (!is->video_st || (is->viddec.finished == is->videoq.serial && frame_queue_nb_remaining(&is->pictq) == 0)))
        {
            if (loop != 1 && (!loop || --loop))
            {
                stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);
            }
            else if (autoexit)
            {
                ret = AVERROR_EOF;
                goto fail;
            }
        }
        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, is->video_stream);
                if (is->audio_stream >= 0)
                    packet_queue_put_nullpacket(&is->audioq, is->audio_stream);
                if (is->subtitle_stream >= 0)
                    packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream);
                is->eof = 1;
            }
            if (ic->pb && ic->pb->error)
            {
                if (autoexit)
                    goto fail;
                else
                    break;
            }
            /*读取失败的话,读取失败的原因有很多,其他地方可能会重新Signal这个锁condition。如果没有singal这个condition的话,就会等待10ms之后,
            再释放,重新循环读取. 那这个continue_read_thread 到底是锁了哪呢?*/
            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
        stream_start_time = ic->streams[pkt->stream_index]->start_time;
        //如果没有pts, 就用dts
        pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
        /*判断是否在范围内。如果duration还没被定义的话,通过
        或者在定义的duration内才可以,用当前的pts-start_time .
        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);
        // 将解复用得到的数据包添加到对应的待解码队列中
        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);
        }
    }
暂停/恢复处理:
if (is->paused != is->last_paused) {//如果paused变量改变,说明暂停状态改变
    is->last_paused = is->paused;
    if (is->paused)//如果暂停调用av_read_pause
        is->read_pause_return = av_read_pause(ic);
    else//如果恢复播放调用av_read_play
        av_read_play(ic);
}

ffmpeg有专门针对暂停和恢复的函数,所以直接调用就可以了。

seek的处理:

主要的seek操作通过avformat_seek_file完成。根据avformat_seek_file的返回值,如果seek成功,需要:

  1. 清除PacketQueue的缓存,并放入一个flush_pkt。放入的flush_pkt可以让PacketQueue的serial增1,以区分seek前后的数据;(在分析PacketQueue会详细说明)
  2. 同步外部时钟;(在音视频同步部分会详细说明)
  3. 最后清理一些变量,通过step_to_next_frame完成
 						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
            {
                //清空缓冲队列,向解码线程传入flush事件
                if (is->audio_stream >= 0)
                {
                    packet_queue_flush(&is->audioq);
                    packet_queue_put(&is->audioq, &flush_pkt);
                }
                if (is->subtitle_stream >= 0)
                {
                    packet_queue_flush(&is->subtitleq);
                    packet_queue_put(&is->subtitleq, &flush_pkt);
                }
                if (is->video_stream >= 0)
                {
                    packet_queue_flush(&is->videoq);
                    packet_queue_put(&is->videoq, &flush_pkt);
                }
              	//同步外部时钟信号
                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);
                }
            }
            is->seek_req = 0;
            is->queue_attachments_req = 1;
            is->eof = 0;
            if (is->paused)
                step_to_next_frame(is);
缓冲区大小判断:

缓冲区大小满的情况判断有两种:

  1. 所有流队列缓冲大小总和大于MAX_QUEUE_SIZE(15M)时;
  2. 各种流的队列都已有够用的包;
/* 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 //所有流队列缓冲大小总和大于MAX_QUEUE_SIZE(15M)时
            || (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);
            SDL_UnlockMutex(wait_mutex);
            continue;
        }

在看看函数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);
}

从函数内部结构可以看出,在满足PacketQueue总时长为0,或总时长超过1s的前提下:

有这么几种情况包是够用的:

  1. 流没有打开(stream_id < 0)
  2. 有退出请求(queue->abort_request)
  3. 配置了AV_DISPOSITION_ATTACHED_PIC,流以附件图片/“封面图片”的形式存储在文件;
  4. 队列内包个数大于MIN_FRAMES(=25)
在播放完的情况下处理:
    if (!is->paused &&
            (!is->audio_st || (is->auddec.finished == is->audioq.serial 
                               && frame_queue_nb_remaining(&is->sampq) == 0)) 
        											&&(!is->video_st || (is->viddec.finished == is->videoq.serial 
                             && frame_queue_nb_remaining(&is->pictq) == 0)))
        {
            if (loop != 1 && (!loop || --loop))
            {
                stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);
            }
            else if (autoexit)
            {
                ret = AVERROR_EOF;
                goto fail;
            }
        }

判断播放已完成的条件,需要满足:

  1. 不在暂停状态
  2. 音频未打开,或者打开了,但是解码已解码完毕,serial等于PacketQueue的serial,并且PacketQueue中没有节点了
  3. 视频未打开,或者打开了,但是解码已解码完毕,serial等于PacketQueue的serial,并且PacketQueue中没有节点了

在确认已结束的情况下,用户有两个变量可以控制播放器行为:

  1. loop: 控制播放次数(当前这次也算在内,也就是最小就是1次了),0表示无限次
  2. autoexit:自动退出,也就是播放完成后自动退出。
读(解复用)处理:

解复用处理的步骤如下:

  1. 通过av_read_frame读取一个包(AVPacket
  2. 返回值处理,一些出错处理过程
  3. pkt_ts重计算过程
  4. packet_queue_put放入各自队列,或者丢弃
 ret = av_read_frame(ic, pkt); //将数据读取出,送入队列
        if (ret < 0)
        {
             //文件读取完了,调用packet_queue_put_nullpacket通知解码线程
            if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof)
            {
                if (is->video_stream >= 0)
                    packet_queue_put_nullpacket(&is->videoq, is->video_stream);
                if (is->audio_stream >= 0)
                    packet_queue_put_nullpacket(&is->audioq, is->audio_stream);
                if (is->subtitle_stream >= 0)
                    packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream);
                is->eof = 1;
            }
            //发生错误了,退出主循环
            if (ic->pb && ic->pb->error)
            {
                if (autoexit)
                    goto fail;
                else
                    break;
            }
            /*读取失败的话,读取失败的原因有很多,其他地方可能会重新Signal这个锁condition。
            如果没有singal这个condition的话,就会等待10ms之后,
            再释放,重新循环读取. 那这个continue_read_thread 到底是锁了哪呢?*/
            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
        stream_start_time = ic->streams[pkt->stream_index]->start_time;
        //如果没有pts, 就用dts
        pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
        /*判断是否在范围内。如果duration还没被定义的话,通过
        或者在定义的duration内才可以,用当前的pts-start_time .
        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);
        // 将解复用得到的数据包添加到对应的待解码队列中
        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);
        }
视频解码线程(video_thread)

在read_thread中已经创建了对应需要的解码器(AVCodec);而在video_thread中需要创建AVFrame,来接收解码后的数据;确定视频帧率,开启循环解码;

参数初始化

创建AVFrame和得到大致的视频帧率

  VideoState *is = arg;
    AVFrame *frame = av_frame_alloc(); //创建AVFrame
    double pts;
    double duration;
    int ret;
    AVRational tb = is->video_st->time_base;
    //猜测视频帧率
    AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
		 if (!frame)
        return AVERROR(ENOMEM);
循环解码

循环解码的总流:

  1. get_video_frame获取解码后的一帧图像
  2. “计算”时长和pts
  3. 调用queue_picture将一帧图像放入FrameQueue
 for (;;)
    {
        ret = get_video_frame(is, frame);
        if (ret < 0)
            goto the_end;
        if (!ret)
            continue;
            //获取当前帧播放时长
            duration = (frame_rate.num && 
                        frame_rate.den ? 
                        av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
            //当前帧显示时间戳
            pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
            // 将当前帧压入frame_queue
            ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
            av_frame_unref(frame); //释放frame

        if (ret < 0)
            goto the_end;
    }
get_video_frame

​ 调用decoder_decode_frame解码,获取成功后主要做丢帧处理,丢帧的主要条件是diff - is->frame_last_filter_delay < 0frame_last_filter_delay与滤镜有关,可以先忽略,也就是diff < 0的时候丢帧——pts < get_master_clock(is)的时候丢帧。decoder_decode_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)//通过 pts*av_q2d(timebase)可以得到准确的时间
            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;
}
decoder_decode_frame

decoder_decode_frame的主干代码是一个循环,要拿到一帧解码数据,或解码出错、文件结束,才会返回。

循环总共3个步骤:

  1. 流连续的情况下,不断调用avcodec_receive_frame获取解码后的frame
  2. 取一个packet,顺带过滤“过时”的packet
  3. 将packet送入解码器

有一个packet_pending的概念,用于在send失败时重新发送;当收到flush_pkt时进行相应的flush事件处理,PacketQueue发生改变时第一个pkt将是flush_pkt,根据ffmpeg的API要求,需要调用avcodec_flush_buffers

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

    for (;;)
    {
        AVPacket pkt;
        //1. 流连续的情况下,不断调用avcodec_receive_frame获取解码后的frame
        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;
                      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));
        }
         //2. 取一个packet,顺带过滤“过时”的packet
        do
        {
            // 队列为空
            if (d->queue->nb_packets == 0)
                SDL_CondSignal(d->empty_queue_cond);
            //如果有待重发的pkt,则先取待重发的pkt,否则从队列中取一个pkt
            if (d->packet_pending)
            {
                av_packet_move_ref(&pkt, &d->pkt);
                d->packet_pending = 0;
            }
            else
            {
                //取出下一帧
                if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
                    return -1;
            }
            //队列的序列不相同时
            if (d->queue->serial == d->pkt_serial)
                break;
            av_packet_unref(&pkt);
        } while (1);
				//针对flush_pkt的处理
        if (pkt.data == flush_pkt.data)
        {
            avcodec_flush_buffers(d->avctx);
            d->finished = 0;
            d->next_pts = d->start_pts;
            d->next_pts_tb = d->start_pts_tb;
        }
        else
        {
                //3. 将packet送入解码器
                if (avcodec_send_packet(d->avctx, &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;
                    av_packet_move_ref(&d->pkt, &pkt);
                }
   
            av_packet_unref(&pkt);
        }
    }
}
queue_picture

queue_picture主要用于把get_video_frame函数取到正确解码后的一帧数据放入FrameQueue;

  1. frame_queue_peek_writable取FrameQueue的当前写节点;
  2. 把该解码后的帧数据拷贝给节点(struct Frame)保存
  3. frame_queue_push,“push”节点到队列中

AVFrame的拷贝是通过av_frame_move_ref实现的,所以拷贝后src_frame就是无效的;


static int queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{
    Frame *vp;

#if defined(DEBUG_SYNC)
    printf("frame_type=%c pts=%0.3f\n",
           av_get_picture_type_char(src_frame->pict_type), pts);
#endif

    if (!(vp = frame_queue_peek_writable(&is->pictq))) //判断是否有空间可以写入,并取FrameQueue的当前写节点
        return -1;

    vp->sar = src_frame->sample_aspect_ratio;
    vp->uploaded = 0;

    vp->width = src_frame->width;
    vp->height = src_frame->height;
    vp->format = src_frame->format;

    vp->pts = pts;
    vp->duration = duration;
    vp->pos = pos;
    vp->serial = serial;
    //修改窗口大小
    set_default_window_size(vp->width, vp->height, vp->sar);

    av_frame_move_ref(vp->frame, src_frame); //将src_frame的内存空间指向vp->frame
    frame_queue_push(&is->pictq);            //重新推入
    return 0;
}

ffplay audio输出的线程分析

ffplay audio的解码过程与video的一样,都在decoder_decode_frame函数中执行,在 video已经进行分析,这里就不做单独分析了,主要分析audio的输出;ffplay的audio输出同样也是通过SDL实现的;audio输出相关内容,且尽量不涉及音视频同步知识,音视频同步将专门一个章节分析。

audio的输出在SDL下是被动输出,即在开启SDL会在需要输出时,回调通知,在回调函数中,SDL会告知要发送多少的数据。

sdl通过sdl_audio_callback函数向ffplay要音频数据,ffplay将sampq中的数据通过audio_decode_frame函数取出,放入is->audio_buf,然后送出给sdl。在后续回调时先找audio_buf要数据,数据不足的情况下,再调用audio_decode_frame补充audio_buf

sdl打开音频输出

stream_component_open中的audio分支进行调用audio_open打开sdl音频输出;代码如下:

case AVMEDIA_TYPE_AUDIO: //音频相关
#if CONFIG_AVFILTER          //过滤器
    {
        AVFilterContext *sink;
        //从avctx(即AVCodecContext)中获取音频格式参数
        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;
        if ((ret = configure_audio_filters(is, afilters, 0)) < 0)
            goto fail;
        sink = is->out_audio_filter;
        sample_rate = av_buffersink_get_sample_rate(sink);
        nb_channels = av_buffersink_get_channels(sink);
        channel_layout = av_buffersink_get_channel_layout(sink);
    }
#else
        sample_rate = avctx->sample_rate;
        nb_channels = avctx->channels;
        channel_layout = avctx->channel_layout;
#endif

        /* prepare audio output */ //打开音频输出通道
        /*调用audio_open打开sdl音频输出,实际打开的设备参数保存在audio_tgt,返回值表示输出设备的缓冲区大小*/
        if ((ret = audio_open(is, channel_layout, nb_channels, sample_rate, &is->audio_tgt)) < 0)
            goto fail;
        is->audio_hw_buf_size = ret;
        is->audio_src = is->audio_tgt;
        //初始化audio_buf相关参数
        is->audio_buf_size = 0;
        is->audio_buf_index = 0;

        /* init averaging filter */
        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;

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

        decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread); //初始化对应的解码线程
        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) // 正式开启解码线程。
            goto out;
        SDL_PauseAudioDevice(audio_dev, 0);
        break;

由于不同的音频输出设备支持的参数不同,音轨的参数不一定能被输出设备支持(此时就需要重采样了),audio_tgt就保存了输出设备参数。

介绍下audio_buf相关的几个变量:

  • audio_buf: 从要输出的AVFrame中取出的音频数据(PCM),如果有必要,则对该数据重采样。
  • audio_buf_size: audio_buf的总大小
  • audio_buf_index: 下一次可读的audio_buf的index位置。
  • audio_write_buf_size:audio_buf已经输出的大小,即audio_buf_size - audio_buf_index
音频输出逻辑

audio_open函数内,通过通过SDL_OpenAudioDevice注册sdl_audio_callback函数为音频输出的回调函数。那么,主要的音频输出的逻辑就在sdl_audio_callback函数内了。

/* 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)//循环发送,直到发够所需数据长度
    {
       //如果audio_buf消耗完了,就调用audio_decode_frame重新填充audio_buf
        if (is->audio_buf_index >= is->audio_buf_size)
        {
            audio_size = audio_decode_frame(is);//填充audio_buf
            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;
        }
       	//根据缓冲区剩余大小量力而行
        len1 = is->audio_buf_size - is->audio_buf_index;
        if (len1 > len)
            len1 = len;
      	//根据audio_volume决定如何输出audio_buf
        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);
        }
       	//调整各buffer
        len -= len1;
        stream += len1;
        is->audio_buf_index += len1;
    }
    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))//更新audclk
    {
        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_audio_callback的缓冲区输出过程:

  1. 输出audio_buf到stream,如果audio_volume为最大音量,则只需memcpy复制给stream即可。否则,可以利用SDL_MixAudioFormat进行音量调整和混音
  2. 如果audio_buf消耗完了,就调用audio_decode_frame重新填充audio_buf
  3. set_clock_at更新audclk时,完整的帧包含的时间戳-实际写入的帧数+2个硬件buffer的延迟,audio_clock是当前audio_buf的显示结束时间(pts+duration),由于audio driver本身会持有一小块缓冲区,典型地,会是两块交替使用,所以有2 * is->audio_hw_buf_size. 因为我们的写入的时候,还需要考虑传入的buffer的大小,预期情况下,如果buffer相同,则这里就是原来的pts-硬件延迟的时间。
填充audio_buf

调用audio_decode_frame重新填充audio_buf,audio_decode_frame并没有真正意义上的decode代码,最多是进行了重采样。主流程有以下步骤:

  1. 从sampq取一帧,必要时丢帧。如发生了seek,此时serial会不连续,就需要丢帧处理
  2. 计算这一帧的字节数。通过av_samples_get_buffer_size可以方便计算出结果
  3. 获取这一帧的数据。对于frame格式和输出设备不同的,需要重采样;如果格式相同,则直接拷贝指针输出即可。总之,需要在audio_buf中保存与输出设备格式相同的音频数据
  4. 更新audio_clock,audio_clock_serial。用于设置audclk.
/**
 * Decode one audio frame and return its uncompressed size.
 *
 * The processed audio frame is decoded, converted if required, and
 * stored in is->audio_buf, with size in bytes given by the return
 * value.
 */
static int audio_decode_frame(VideoState *is)
{
    int data_size, resampled_data_size;
    int64_t dec_channel_layout;
    av_unused double audio_clock0;
    int wanted_nb_samples;
    Frame *af;

    if (is->paused)//暂停状态,返回-1,sdl_audio_callback会处理为输出静音
        return -1;

    do
    {
#if defined(_WIN32)
        while (frame_queue_nb_remaining(&is->sampq) == 0)
        {
            if ((av_gettime_relative() - audio_callback_time) > 1000000LL * is->audio_hw_buf_size / is->audio_tgt.bytes_per_sec / 2)
                return -1;
            av_usleep(1000);
        }
#endif
        if (!(af = frame_queue_peek_readable(&is->sampq)))//1. 从sampq取一帧,必要时丢帧
            return -1;
        frame_queue_next(&is->sampq);
    } while (af->serial != is->audioq.serial);
    //2. 计算这一帧的字节数
    data_size = av_samples_get_buffer_size(NULL, af->frame->channels,
                                           af->frame->nb_samples,
                                           af->frame->format, 1);
    //[]计算dec_channel_layout,用于确认是否需要重新初始化重采样
    dec_channel_layout =
        (af->frame->channel_layout && af->frame->channels == av_get_channel_layout_nb_channels(af->frame->channel_layout)) ? af->frame->channel_layout : av_get_default_channel_layout(af->frame->channels);
    wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);

     //[]判断是否需要重新初始化重采样
    if (af->frame->format != is->audio_src.fmt ||
        dec_channel_layout != is->audio_src.channel_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
        is->swr_ctx = swr_alloc_set_opts(NULL,
                                         is->audio_tgt.channel_layout, is->audio_tgt.fmt, is->audio_tgt.freq,
                                         dec_channel_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->channels,
                   is->audio_tgt.freq, av_get_sample_fmt_name(is->audio_tgt.fmt), is->audio_tgt.channels);
            swr_free(&is->swr_ctx);
            return -1;
        }
        is->audio_src.channel_layout = dec_channel_layout;
        is->audio_src.channels = af->frame->channels;
        is->audio_src.freq = af->frame->sample_rate;
        is->audio_src.fmt = af->frame->format;
    }
    //3. 获取这一帧的数据
    if (is->swr_ctx)//[]如果初始化了重采样,则对这一帧数据重采样输出
    {
        const uint8_t **in = (const uint8_t **)af->frame->extended_data;
        uint8_t **out = &is->audio_buf1;
        int out_count = (int64_t)wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate + 256;
        int out_size = av_samples_get_buffer_size(NULL, is->audio_tgt.channels, out_count, is->audio_tgt.fmt, 0);
        int len2;
        if (out_size < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "av_samples_get_buffer_size() failed\n");
            return -1;
        }
        if (wanted_nb_samples != af->frame->nb_samples)
        {
            if (swr_set_compensation(is->swr_ctx, (wanted_nb_samples - af->frame->nb_samples) * is->audio_tgt.freq / af->frame->sample_rate,
                                     wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate) < 0)
            {
                av_log(NULL, AV_LOG_ERROR, "swr_set_compensation() failed\n");
                return -1;
            }
        }
        av_fast_malloc(&is->audio_buf1, &is->audio_buf1_size, out_size);
        if (!is->audio_buf1)
            return AVERROR(ENOMEM);
        //进行转换
        len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples);
        if (len2 < 0)
        {
            av_log(NULL, AV_LOG_ERROR, "swr_convert() failed\n");
            return -1;
        }
        if (len2 == out_count)
        {
            av_log(NULL, AV_LOG_WARNING, "audio buffer is probably too small\n");
            if (swr_init(is->swr_ctx) < 0)
                swr_free(&is->swr_ctx);
        }
        is->audio_buf = is->audio_buf1;
        //重新计算采样的数据大小,并返回
        resampled_data_size = len2 * is->audio_tgt.channels * av_get_bytes_per_sample(is->audio_tgt.fmt);
    }
    else
    {
        is->audio_buf = af->frame->data[0];
        resampled_data_size = data_size;
    }
    //audio_clock0用于打印调试信息
    audio_clock0 = is->audio_clock;
    /* update the audio clock with the pts */
    //4. 更新audio_clock,audio_clock_serial,更新pts  这个pts 等于当前的帧包含的所有帧数
    if (!isnan(af->pts)) 
        is->audio_clock = af->pts + (double)af->frame->nb_samples / af->frame->sample_rate;
    else
        is->audio_clock = NAN;
    is->audio_clock_serial = af->serial;
#ifdef DEBUG
    {
        static double last_clock;
        printf("audio: delay=%0.3f clock=%0.3f clock0=%0.3f\n",
               is->audio_clock - last_clock,
               is->audio_clock, audio_clock0);
        last_clock = is->audio_clock;
    }
#endif
    return resampled_data_size;//返回audio_buf的数据大小
}

ffplay时间同步

由于音频和视频的输出不在同一个线程,而且,也不一定会同时解出同一个pts的音频帧和视频帧。因此,在进行音频和视频的播放时,需要对音频和视频的播放速度、播放时刻进行控制,以实现音频和视频保持同步,即所谓的音视频同步。

在ffplay中,音频(audio)和视频(video)有各自的输出线程,其中音频的输出线程是sdl的音频输出回调线程,video的输出线程是程序的主线程。

音视频的同步策略,一般有如下几种:

  • 视频同步到音频,即音频为主时钟
  • 音频同步到视频,即视频为主时钟
  • 视频、音频同步到外部时钟,即外部时钟(系统时间)为主时钟
  • 视频和音频各自输出,即不作同步处理,或称之为各自为主时钟

由于人耳对于声音变化的敏感度比视觉高,因此,一般采样的策略是将视频同步到音频,即对画面进行适当的丢帧或重复以追赶或等待音频。

DTS和PTS

在音视频流中的包都含有DTSPTS,我们以此作为选择基准,到底是播放快了还是慢了,或者正以同步的速度播放。

  • DTS:Decoding Time Stamp 解码时间戳——告诉解码器packet解码顺序
  • PTS:Presenting Time Stamp 显示时间戳——指示从packet中解码出来的数据的显示顺序
计算视频Frame的显示时间

要想知道ffmpeg如果计算视频一帧的显示时间,就需先了解ffmpeg的timebase;因为pts的单位就是timebase;

timebase的类型是结构体AVRational(用于表示分数),如下:

typedef struct AVRational{
    int num; ///< Numerator
    int den; ///< Denominator
} AVRational;

timebase={1, 1000}表示千分之一秒,那么pts=1000,即为1秒,那么这一帧就需要在第一秒的时候呈现在ffplay中,将pts转化为秒,一般做法是:pts * av_q2d(timebase)

"时钟"的概念,ffplay定义的结构体是Clock:

typedef struct Clock
{
    double pts;       /* clock base */// 时钟基准
    double pts_drift; /* clock base minus time at which we updated the clock */// 更新时钟的差值
    double last_updated;// 上一次更新的时间
    double speed;// 速度
    int serial;  // 时钟基于使用该序列的包 /* clock is based on a packet with this serial */
    int paused;// 停止标志
    int *queue_serial; // 指向当前数据包队列序列的指针,用于过时的时钟检测 /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

时钟的工作原理:

  1. 通过set_clock_at不断对时;
  2. 获取的时间是一个估算值。估算是通过对时时记录的pts_drift估算的
/**
 * 更新视频的pts
 * @param is     [description]
 * @param pts    [description]
 * @param pos    [description]
 * @param serial [description]
 */
static void update_video_pts(VideoState *is, double pts, int64_t pos, int serial) {
    /* update current video pts */
    set_clock(&is->vidclk, pts, serial);
   //将尾部的时间钟,用视频的时机钟来进行同步
    sync_clock_to_slave(&is->extclk, &is->vidclk);
}

static void set_clock(Clock *c, double pts, int serial)
{
    double time = av_gettime_relative() / 1000000.0;
    set_clock_at(c, pts, serial, time);
}

//使用当前的事来计算这几个值。也就是这一帧送显之前的操作的时间。
static void set_clock_at(Clock *c, double pts, int serial, double time)
{
    c->pts = pts;
    c->last_updated = time;
    c->pts_drift = c->pts - time;
    c->serial = serial;
}

以一个时间轴,从左往右看。首先我们调用set_clock_at进行一次对时,假设这时的pts是落后系统时间time的,那么计算pts_drift = pts - time

接着,过了一会儿,且在下次对时前,通过get_clock来查询时间,因为这时的pts已经过时,不能直接拿pts当做这个时钟的时间。不过我们前面计算过pts_drift,也就是ptstime的差值,所以我们可以通过当前时刻的系统时间来估算这个时刻的pts:pts = time + pts_drift.

当然,由于pts_drift是一直在变动的(drift与漂移、抖动的意思),所以get_clock是估算值,真实的pts可能落在比如图示虚线圆的位置。
在这里插入图片描述

static double get_clock(Clock *c)
{
    if (*c->queue_serial != c->serial)
        return NAN;
    if (c->paused)
    {
        return c->pts;
    }
    else
    {
        //pts_drift 是更新的时间钟的差值?
        //最后的时间是 更新的差值+ 当前的时间-当前的时间和上一次更新的时间之间的差值*速度
        //默认的情况下,根据上一次的drift计算下一次要出现的时间。
        double time = av_gettime_relative() / 1000000.0;
        return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
    }
}
以音频为主时钟同步

接下来主要讲以音频为主时钟的部分,大致流程如下:

在这里插入图片描述

在这个流程中,“计算上一帧显示时长”这一步骤至关重要。代码如下:

/* called to display each frame */
static void video_refresh(void *opaque, double *remaining_time)
{
  if (is->video_st)
      {
      retry:
          if (frame_queue_nb_remaining(&is->pictq) == 0)
          { //判断队列是否有数据
              // nothing to do, no picture to display in the queue
          }
          else
          {
              double last_duration, duration, delay;
              Frame *vp, *lastvp;
              //lastvp上一帧,vp当前帧 ,nextvp下一帧

              /* dequeue the picture */ //出队
              lastvp = frame_queue_peek_last(&is->pictq);
              vp = frame_queue_peek(&is->pictq);
              /*
              1、刚开始的时候(第一帧)lastvp == vp ,因为还没有调用frame_queue_next f->rindex_shown还未为1
              2、调用frame_queue_next,将f->rindex_shown置1,还没有增加f->rindex
              3、第二帧开始lastvp上一帧,vp 将要显示的一帧
              */

             /*如果将要显示的一帧的序列与现在解码的不同就直接抛弃*/
              if (vp->serial != is->videoq.serial)
              { 
                  frame_queue_next(&is->pictq);//移动读索引
                  goto retry;//重新获取
              }

              //如果上一帧序号不等于将要显示的一帧序号,表示将要显示的一帧是新的播放序列
              /*
              新的播放序列重置当前时间,这样就会走到正常显示将要显示的一帧(新序的第一帧)
              */
              if (lastvp->serial != vp->serial)
                  is->frame_timer = av_gettime_relative() / 1000000.0; //获取当前时间,用于帧间对比

              if (is->paused) //暂停后重新开
                  goto display;

              /* compute nominal last_duration */
              last_duration = vp_duration(is, lastvp, vp);//计算上一帧的持续时长
              delay = compute_target_delay(last_duration, is); //音视频同步信息,参考audio clock计算上一帧真正的持续时

              time = av_gettime_relative() / 1000000.0;//取系统时刻
              /*delay : 是上一帧要持续显示的时长,也就是将要显示的一帧的开始显示时间
              is->frame_timer : 上一帧显示的时间
              is->frame_timer + delay : 将要显示这一帧的时间
              如果time 还没达到显示这一帧的时间,就计算等待时间用于上一层等待,继续显示上一帧
              如果seek后,delay = 0 ,time 就会大于is->frame_timer + delay,就往下走显示
              */
              if (time < is->frame_timer + delay)//如果上一帧显示时长未满,重复显示上一帧
              { //进入视频显示,计算一个等待时间返回上一层,现在视频快了,让视频继续显示上一帧等待
                  *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                  goto display;
              }
              /*第一帧数据时is->frame_timer = 0,会执行到time - is->frame_timer > AV_SYNC_THRESHOLD_MAX
              如果与系统时间的偏离太大,则修正为系统时间*/
              is->frame_timer += delay;//frame_timer更新为上一帧结束时刻,也是当前帧开始时刻
              if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
                  is->frame_timer = time;//如果与系统时间的偏离太大,则修正为系统时间

              SDL_LockMutex(is->pictq.mutex);
              /*更新视频时钟
              注意:这个更新视频时钟在丢帧之前,那么如果这帧pts设置视频时钟后
              下面又将这帧丢弃,视频时钟就是被丢弃的这一帧*/
              if (!isnan(vp->pts))
                  update_video_pts(is, vp->pts, vp->pos, vp->serial); //更新视频时钟检测
              SDL_UnlockMutex(is->pictq.mutex);

              /*drop帧处理
              队列要有将要显示这一帧的下一帧
              第一帧,队列内有2帧
              后面就是,队列内有3帧,因为保留了上一帧(显示帧)*/
              //丢帧逻辑
              if (frame_queue_nb_remaining(&is->pictq) > 1)
              {
                  Frame *nextvp = frame_queue_peek_next(&is->pictq);//获取将要显示帧的下一帧
                  duration = vp_duration(is, vp, nextvp);//当前帧显示时长
                  //如果系统时间已经大于当前帧,则丢弃当前帧
                  if (!is->step //非逐帧模式播放情况下
                  && (framedrop > 0 //允许drop帧处理
                  || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) //主时钟不是视频
                  && time > is->frame_timer + duration)//当前时间已经到了nextvp的播放时间,
                  {
                      //此时is->frame_timer就是将要显示这一帧vp的播放时间了
                      is->frame_drops_late++;//丢帧统计
                      frame_queue_next(&is->pictq);//丢弃将要显示这一帧,移动读索引读到下一帧
                      goto retry;//回到函数开始位置,继续重试(这里不能直接while丢帧,因为很可能audio clock重新对时了,这样delay值需要重新计算)
                  }
              }
              /*上一帧与将要显示这一帧之间的duration用来计算将要显示这一帧的播放时间
              将要显示这一帧与上一帧之间的duration用来计算是否丢弃将要显示这一帧*/
            ...
              frame_queue_next(&is->pictq); 
              is->force_refresh = 1;//刷新画面

              if (is->step && !is->paused)
                  stream_toggle_pause(is);
          }
      display:
          /* display picture */
          if (!display_disable 
              && is->force_refresh 
              && is->show_mode == SHOW_MODE_VIDEO 
              && is->pictq.rindex_shown)
              video_display(is);//显示视频
      }
}

如果视频播放过快,则重复播放上一帧,以等待音频;如果视频播放过慢,则丢帧追赶音频。实现的方式是,参考audio clock,计算上一帧(在屏幕上的那个画面)还应显示多久(含帧本身时长),然后与系统时刻对比,是否该显示下一帧了。

frame_timer

frame_timer:可以理解为帧显示时刻,如更新前,是上一帧的显示时刻;对于更新后(is->frame_timer += delay),则为当前帧显示时刻。

上一帧显示时刻加上delay(还应显示多久(含帧本身时长))即为上一帧应结束显示的时刻。具体原理看如下示意图:

在这里插入图片描述

这里给出了3种情况的示意图:

  • time1:系统时刻小于lastvp结束显示的时刻(frame_timer+dealy),即虚线圆圈位置。此时应该继续显示lastvp
  • time2:系统时刻大于lastvp的结束显示时刻,但小于vp的结束显示时刻(vp的显示时间开始于虚线圆圈,结束于黑色圆圈)。此时既不重复显示lastvp,也不丢弃vp,即应显示vp
  • time3:系统时刻大于vp结束显示时刻(黑色圆圈位置,也是nextvp预计的开始显示时刻)。此时应该丢弃vp。
dealy计算

lastvp的显示时长delay是如何计算的,主要在compute_target_delay中实现,代码如下:

static double compute_target_delay(double delay, VideoState *is)
{
    double sync_threshold, diff = 0;

    /* update delay to follow master synchronisation source */
    //只有同步时钟不是视频时钟时才计算,
    if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)
    { // 判断同步类型,
        /* if video is slave, we try to correct big delays by
           duplicating or deleting a frame */
        /*
        get_clock(&is->vidclk) :返回经过从上次设置时钟到现在数据时间到了什么时间位置
        get_master_clock(is) : 返回主时钟到了什么时间位置(音频时钟或者外部时钟)
        diff : 就是当前视频播放的位置与主时钟之前的差值
        <0 : 视频慢了
        >0 :视频快了
        */
        diff = get_clock(&is->vidclk) - get_master_clock(is);

        /* skip or repeat frame. We take into account the
           delay to compute the threshold. I still don't know
           if it is the best guess */
        /*AV_SYNC_THRESHOLD_MIN 同步阀值最小范围:0.04 (秒) 1/25帧的时间
        AV_SYNC_THRESHOLD_MAX 同步阀值最大范围:0.1 (秒) 1/10帧的时间
        delay : 理论上的两帧之间的时间
        返回一个同步阀值在同步阀值范围内,使用delay设置
        因为delay上层传来是两帧之间的时间,那只要在阀值范围内,这个时间就是sync_threshold*/
        sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
        if (!isnan(diff) && fabs(diff) < is->max_frame_duration)
        {
            //根据阀值判断是快了还是慢了
            if (diff <= -sync_threshold)//差值已经超出阀值最小,视频慢了
                delay = FFMAX(0, delay + diff);/*上一帧需要加快,delay + -diff,这样算出来的delay基本都是0,上一帧还要显示delay时间*/
            else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
                delay = delay + diff;/*视频快了,上一帧就要减慢,delay+diff,AV_SYNC_FRAMEDUP_THRESHOLD 0.1秒,如果帧持续时间超过这个值,它将不会被成倍来补偿进行同步*/
            else if (diff >= sync_threshold)
                delay = 2 * delay;/*视频快了,delay 相当上一帧显示两次,因为diff == sync_threshold也是快了一帧*/
        }
    }

    av_log(NULL, AV_LOG_TRACE, "video: delay=%0.3f A-V=%f\n",
           delay, -diff);

    return delay;
}
sync_threshold理解:

在这里插入图片描述

从图上可以看出来sync_threshold是建立一块区域,在这块区域内无需调整lastvp的显示时长,直接返回delay即可。也就是在这块区域内认为是准同步的。

如果小于-sync_threshold,那就是视频播放较慢,需要适当丢帧。具体是返回一个最大为0的值。根据前面frame_timer的图,至少应更新画面为vp。

如果大于sync_threshold,那么视频播放太快,需要适当重复显示lastvp。具体是返回2倍的delay,也就是2倍的lastvp显示时长,也就是让lastvp再显示一帧。

总结
  • 基本策略是:如果视频播放过快,则重复播放上一帧,以等待音频;如果视频播放过慢,则丢帧追赶音频。
  • 这一策略的实现方式是:引入frame_timer概念,标记帧的显示时刻和应结束显示的时刻,再与系统时刻对比,决定重复还是丢帧。
  • lastvp的应结束显示的时刻,除了考虑这一帧本身的显示时长,还应考虑了video clock与audio clock的差值。
  • 并不是每时每刻都在同步,而是有一个“准同步”的差值区域。

FrameQueue的分析

ffplay 是通过FrameQueue来保存解码后的数据;

Frame结构体

Frame是用来存储解码后的一帧数据,其中包括视频、音频和字幕;

typedef struct Frame
{
    AVFrame *frame;//音视频解码数据
    AVSubtitle sub;//字幕解码数据
    int serial;
    double pts;      /* presentation timestamp for the frame */
    double duration; /* estimated duration of the frame */
    int64_t pos;     /* byte position of the frame in the input file */
    int width;
    int height;
    int format;
    AVRational sar;
    int uploaded;
    int flip_v;
} Frame;
FrameQueue结构体

FrameQueue是用来表示整个帧队列;

typedef struct FrameQueue
{
    Frame queue[FRAME_QUEUE_SIZE];//队列元素,用数组模拟队列
    int rindex;//读指针
    int windex;//写指针
    int size;//当前存储的节点个数
    int max_size;//最大允许存储的节点个数
    int keep_last;//是否要保留最后一个读节点
    int rindex_shown;//当前节点是否已经显示
    SDL_mutex *mutex;
    SDL_cond *cond;
    PacketQueue *pktq;//关联的PacketQueue
} FrameQueue;

FrameQueue的设计思想是通过数组实现队列(环形缓冲区);

设计理念:

  • 高效率的读写模型
  • 高效的内存模型
  • 环形缓冲区设计,同时可以访问上一读节点
FrameQueue实现函数分析
初始化函数

FrameQueue的初始化函数是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;
}

其中主要是内存初始化和锁初始化,关键参数是max_sizekeep_last;max_size是最大允许存储的节点个数,最大不能超过

FRAME_QUEUE_SIZEFRAME_QUEUE_SIZE定义如下:

#define VIDEO_PICTURE_QUEUE_SIZE 3 //视频显示缓存最大帧数
#define SUBPICTURE_QUEUE_SIZE 16   //字幕缓存最大帧数
#define SAMPLE_QUEUE_SIZE 9        //默认最大帧数
#define FRAME_QUEUE_SIZE FFMAX(SAMPLE_QUEUE_SIZE, FFMAX(VIDEO_PICTURE_QUEUE_SIZE, SUBPICTURE_QUEUE_SIZE))

如此看来最大不能超过16;

keep_last是一个bool值,表示是否在环形缓冲区的读写过程中保留最后一个读节点不被覆写。f->keep_last = !!keep_last;里的双感叹号是C中的一种技巧,旨在让int参数规整为0/1的“bool值”。

数组queue中的每个元素的frame(AVFrame*)的字段调用av_frame_alloc分配内存。

反初始化函数
static void frame_queue_destory(FrameQueue *f)
{
    int i;
    for (i = 0; i < f->max_size; i++)
    {
        Frame *vp = &f->queue[i];
        frame_queue_unref_item(vp);
        av_frame_free(&vp->frame);
    }
    SDL_DestroyMutex(f->mutex);
    SDL_DestroyCond(f->cond);
}

queue元素的释放分两步;

  1. frame_queue_unref_item,释放的内存都是关联的内存,而非结构体自身内存;
  2. av_frame_free,av_frame_free与初始化中的av_frame_alloc对应,用于释放AVFrame.

frame_queue_unref_item定义如下:

static void frame_queue_unref_item(Frame *vp)
{
    av_frame_unref(vp->frame);//frame计数减1
    avsubtitle_free(&vp->sub);//sub关联的内存释放
}

AVFrame内部有许多的AVBufferRef类型字段,而AVBufferRef只是AVBuffer的引用,AVBuffer通过引用计数自动管理内存(简易垃圾回收机制)。因此AVFrame在不需要的时候,需要通过av_frame_unref减少引用计数。(这个还在学习阶段)

FrameQueue的‘写’操作

FrameQueue的‘写’操作分为两个步骤;

  1. frame_queue_peek_writable获取一个可写节点;
  2. frame_queue_push告知FrameQueue“存入”该节点。

FrameQueue始终是一个线程写,另一个线程读。读写没有其他线程产生竞争,唯一需要的是读与写线程间的同步。FrameQueue的整个优化和设计思路正是基于这一点的。这个设计思想类似于Linux内核中的kfifo。

frame_queue_peek_writable定义如下:

static Frame *frame_queue_peek_writable(FrameQueue *f)
{
    /* wait until we have space to put a new frame */
    SDL_LockMutex(f->mutex);
    while (f->size >= f->max_size &&
           !f->pktq->abort_request)
    {
        SDL_CondWait(f->cond, f->mutex);
    }
    SDL_UnlockMutex(f->mutex);

    if (f->pktq->abort_request)
        return NULL;

    return &f->queue[f->windex];
}

函数分3步:

  1. 加锁情况下,等待直到队列有空余空间可写(f->size < f->max_size
  2. 如果有退出请求(f->pktq->abort_request),则返回NULL
  3. 返回windex位置的元素(windex指向当前应写位置)

因为queue数组被当做一个环形缓冲区使用,那么的确存在underrun和overrun的情况,即读过快,或写过快的情况,这时如果不加控制,就会呈现缓冲区覆盖。

FrameQueue的精明之处在于,先通过size判断当前缓冲区内空间是否够写,或者够读,比如这里先通过一个循环的条件等待,判断f->size >= f->max_size,如果f->size >= f->max_size,那么说明队列中的节点已经写满,也就是已经overrun了,此时如果再写,肯定会覆写未读数据,那么就需要继续等待。当无需等待时,windex指向的内存一定是已经读过的(除非代码异常了)。

调用frame_queue_peek_writable取到Frame指针后,就可以对Frame内的字段自由改写,因为只有一个写进程,且无需担心读进程覆写(如上分析,读进程要读一个节点时,也会先判断underrun的情况)。

实现步骤:

Frame* vp = frame_queue_peek_writable(q);
//将要存储的数据写入frame字段,比如:
av_frame_move_ref(vp->frame, src_frame);
//存入队列
frame_queue_push(q);

frame_queue_push代码如下:

static void frame_queue_push(FrameQueue *f)
{
    if (++f->windex == f->max_size)
        f->windex = 0;
    SDL_LockMutex(f->mutex);
    f->size++;
    SDL_CondSignal(f->cond);
    SDL_UnlockMutex(f->mutex);
}

执行两个步骤:

  1. windex加1,如果超过max_size,则回环为0
  2. 加锁情况下大小加1.

frame_queue的写过程总结示意图如下:
在这里插入图片描述

FrameQueue的’读’操作

FrameQueue的‘读’操作分为两个步骤;

  1. frame_queue_peek_readable获取一个可读节点
  2. frame_queue_next告知FrameQueue“取出”该节点。

frame_queue_peek_readable代码如下:

static Frame *frame_queue_peek_readable(FrameQueue *f)
{
    /* wait until we have a readable a new frame */
    SDL_LockMutex(f->mutex);
    //加锁情况下,判断是否有可读节点
    while (f->size - f->rindex_shown <= 0 &&
           !f->pktq->abort_request)
    {
        SDL_CondWait(f->cond, f->mutex);
    }
    SDL_UnlockMutex(f->mutex);
    //如果有退出请求,则返回NULL
    if (f->pktq->abort_request)
        return NULL;
    //读取当前可读节点
    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}

frame_queue_next代码如下:

static void frame_queue_next(FrameQueue *f)
{
    //如果支持keep_last,且rindex_shown为0,则rindex_shown赋1,返回
    if (f->keep_last && !f->rindex_shown)
    {
        f->rindex_shown = 1;
        return;
    }
    //否则,移动rindex指针,并减小size
    frame_queue_unref_item(&f->queue[f->rindex]);
    if (++f->rindex == f->max_size)
        f->rindex = 0;
    SDL_LockMutex(f->mutex);
    f->size--;
    SDL_CondSignal(f->cond);
    SDL_UnlockMutex(f->mutex);
}

frame_queue_next用于在读完一个节点后调用,用于标记一个节点已经被读过。

读过程可以描述为:

Frame* vp = frame_queue_peek_readable(f);
//读取vp的数据,比如
printf("pict_type=%d\n", vp->frame->pict_type);
frame_queue_next(f);

标记一个节点为已读,执行两个步骤:

  1. rindex加1,如果超过max_size,则回环为0
  2. 加锁情况下大小减1.

对于以及读过的节点,需要调用frame_queue_unref_item释放关联内存。

执行rindex操作前,需要先判断rindex_shown的值,如果为0,则赋1。如下图:

在这里插入图片描述

这里模拟了从初始化开始的2次“读”。

还没开始读,rindex和rindex_shown均为0。这时要peek的读节点是节点0(图中黑色块)。

第一次读,调用next,满足条件f->keep_last && !f->rindex_shown,所以rindex仍然是0,而rindex_shown为1.此时节点0(灰色块)是已读节点,也是要keep的last节点,将要读的节点是节点1(黑色块)。(恰好是rindex+rindex_shown)

第二次读,peek了黑色块后,调用next,不满足条件f->keep_last && !f->rindex_shown,所以rindex为1,而rindex_shown为2.此时节点1(灰色块)是last节点,节点2(黑色块)是将要读的节点。(也恰好是rindex+rindex_shown)

继续往后分析,会一直重复第二次读的情况,始终是rindex指向了last,而rindex_shown一直为1,rindex+rindex_shown刚好是将要读的节点。

FrameQueue的读过程也分析完了。

辅助函数
//(上文中的用词是“将要读的节点”,也就是黑色块),与frame_queue_peek_readable等效,但没有检查是否有可读节点
//读当前节点
static Frame *frame_queue_peek(FrameQueue *f)
{
    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}
//读下一个节点
static Frame *frame_queue_peek_next(FrameQueue *f)
{
    return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size];
}
//读上一个节点
static Frame *frame_queue_peek_last(FrameQueue *f)
{
    return &f->queue[f->rindex];
}
/* return the number of undisplayed frames in the queue */
static int frame_queue_nb_remaining(FrameQueue *f)
{
    return f->size - f->rindex_shown;
}
/* return last shown position */
static int64_t frame_queue_last_pos(FrameQueue *f)
{
    Frame *fp = &f->queue[f->rindex];
    if (f->rindex_shown && fp->serial == f->pktq->serial)
        return fp->pos;
    else
        return -1;
}

看下节点位置:

在这里插入图片描述

参考链接:

https://www.zhihu.com/column/avtec

https://cloud.tencent.com/developer/article/1373966

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值