ffplay源码分析2-数据结构

本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10301253.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-播放控制

2. 数据结构

几个关键的数据结构如下:

2.1 struct VideoState

VideoState 是 ffplay 源码中最大的结构体,定义如下:

typedef struct VideoState {
    SDL_Thread *read_tid;           // demux解复用线程
    const AVInputFormat *iformat;
    int abort_request;
    int force_refresh;
    int paused;
    int last_paused;
    int queue_attachments_req;
    int seek_req;                   // 标识一次SEEK请求
    int seek_flags;                 // SEEK标志,诸如AVSEEK_FLAG_BYTE等
    int64_t seek_pos;               // SEEK的目标位置(当前位置+增量)
    int64_t seek_rel;               // 本次SEEK的位置增量
    int read_pause_return;
    AVFormatContext *ic;
    int realtime;

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

    FrameQueue pictq;               // 视频frame队列
    FrameQueue subpq;               // 字幕frame队列
    FrameQueue sampq;               // 音频frame队列

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

    int audio_stream;               // 音频流索引

    int av_sync_type;

    double audio_clock;             // 每个音频帧更新一下此值,以pts形式表示
    int audio_clock_serial;         // 播放序列,seek可改变此值
    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;             // 音频packet队列
    int audio_hw_buf_size;          // SDL音频缓冲区大小(单位字节)
    uint8_t *audio_buf;             // 指向待播放的一帧音频数据,指向的数据区将被拷入SDL音频缓冲区。若经过重采样则指向audio_buf1,否则指向解码后生成的原始帧中的音频
    uint8_t *audio_buf1;            // 音频重采样的输出缓冲区
    unsigned int audio_buf_size;    // audio_buf缓冲区大小
    unsigned int audio_buf1_size;   // audio_buf1缓冲区大小
    int audio_buf_index;            // 当前音频帧中已拷入SDL音频缓冲区的位置索引(指向第一个待拷贝字节)
    int audio_write_buf_size;       // 当前音频帧中尚未拷入SDL音频缓冲区的数据量,audio_buf_size = audio_buf_index + audio_write_buf_size
    int audio_volume;               // 音量
    int muted;                      // 静音状态
    struct AudioParams audio_src;   // 音频frame的参数
    struct AudioParams audio_filter_src;
    struct AudioParams audio_tgt;   // SDL支持的音频参数,重采样转换:audio_src->audio_tgt
    struct SwrContext *swr_ctx;     // 音频重采样context
    int frame_drops_early;          // 解码线程中视频frame丢帧计数
    int frame_drops_late;           // 播放线程中视频frame丢帧计数

    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;
    AVTXContext *rdft;
    av_tx_fn rdft_fn;
    int rdft_bits;
    float *real_data;
    AVComplexFloat *rdft_data;
    int xpos;
    double last_vis_time;
    SDL_Texture *vis_texture;
    SDL_Texture *sub_texture;
    SDL_Texture *vid_texture;

    int subtitle_stream;            // 字幕流索引
    AVStream *subtitle_st;          // 字幕流
    PacketQueue subtitleq;          // 字幕packet队列

    double frame_timer;             // 记录最后一帧播放的时刻(单位是秒)
    double frame_last_returned_time;
    double frame_last_filter_delay;
    int video_stream;
    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 *sub_convert_ctx;
    int eof;

    char *filename;
    int width, height, xleft, ytop;
    int step;

    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

    int last_video_stream, last_audio_stream, last_subtitle_stream;

    SDL_cond *continue_read_thread;
} VideoState;

main() 函数中声明了 VideoState 类型的变量 is,如下:

int main(int argc, char **argv)
{
    ...
    VideoState *is;
    ...
}

整个 ffplay.c 文件到处都在使用这个 is 变量。VideoState 结构体太大,包含杂揉了所有信息,这个超大的结构体变量又被当作全局变量在使用,这种做法其实是不太好的。

2.2 struct Clock

Clock 结构体定义如下:

typedef struct Clock {
    // 当前帧(待播放)显示时间戳,播放后,当前帧变成上一帧
    double pts;           /* clock base */
    // 当前帧显示时间戳与当前系统时钟时间的差值,即pts与当前时刻的差值
    double pts_drift;     /* clock base minus time at which we updated the clock */
    // 当前时钟(如视频时钟)最后一次更新时间,也即上一帧(视频帧/音频帧)播放时刻
    double last_updated;
    // 时钟速度控制,用于控制播放速度,仅用于外部时钟同步这种同步方式中,不关注
    double speed;
    // 播放序列,所谓播放序列就是一段连续的播放动作,一个seek操作会启动一段新的播放序列
    int serial;           /* clock is based on a packet with this serial */
    // 暂停标志
    int paused;
    // 指向packet_serial
    int *queue_serial;    /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

这里有三个时钟相关的变量比较关键:pts 是音视频帧的 pts,来自码流;last_update 是上一帧播放时刻,来自系统时钟;pts_drift 是 pts 和 系统时钟的差值,看下面的代码,pts_drift 实际就是 pts - last_updated 的值:

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;
}

2.3 struct PacketQueue

PacketQueue 结构体定义如下:

typedef struct PacketQueue {
    AVFifo *pkt_list;
    int nb_packets;                 // 队列中packet的数量
    int size;                       // 队列所占内存空间大小(单位字节)
    int64_t duration;               // 队列中所有packet总的播放时长
    int abort_request;
    int serial;                     // 播放序列,所谓播放序列就是一段连续的播放动作,一个seek操作会启动一段新的播放序列
    SDL_mutex *mutex;
    SDL_cond *cond;
} PacketQueue;

这里涉及到数据结构的一些基本概念。在数据结构中,栈 (LIFO) 是一种表,队列 (FIFO) 也是一种表。数组是表的一种实现方式,链表也是表的一种实现方式,例如 FIFO 既可以用数组实现,也可以用链表实现。PacketQueue 就是用链表实现的一个 FIFO。PacketQueue 的操作比较简单,不详述。

2.4 struct FrameQueue

FrameQueue 结构定义如下:

typedef struct FrameQueue {
    Frame queue[FRAME_QUEUE_SIZE];
    int rindex;                     // 读索引。为方便,此索引所指向的帧称为ri帧
    int windex;                     // 写索引。为方便,此索引所指向的帧称为wi帧
    int size;                       // 队列中有效帧计数
    int max_size;                   // 队列可存储最大帧数。音频和视频队列初始化时可指定不同的容量
    int keep_last;                  // 是否保留已播放的最后一帧使能标志
    int rindex_shown;               // 配合keep_last变量使用,当队列keep_last值为1时,rindex_shown值被设为1
                                    // 为方便,将rindex+rindex_shown所指向的帧称为ris帧
                                    // 那么,ris帧是本次待播放的帧,ri帧是上次已播放的帧
    SDL_mutex *mutex;
    SDL_cond *cond;
    PacketQueue *pktq;              // 指向对应(音频/视频/字幕)的packet_queue
} FrameQueue;

2.4.1 环形缓冲区原理

FrameQueue 是一个环形缓冲区 (ring buffer),是用数组实现的一个 FIFO。下面先讲一下环形缓冲区的基本原理,其示意图如下:

ring buffer示意图

使用环形缓冲区实现 FIFO 的好处是,读出或写入一个元素后,其余元素不需要移动存储位置。环形缓冲区适合于事先明确了缓冲区的最大容量的情形。扩展一个环形缓冲区的容量,需要搬移其中的数据。因此一个缓冲区如果需要经常调整其容量,用链表实现更为合适。

环形缓冲区使用中要避免读空和写满,但在空和满这两种状态下读指针和写指针均相等,因此其实现中的关键点就是如何区分出空和满。有多种策略可以用来区分空和满两种状态:

  1. 总是保持一个存储单元为空:“读指针”等于“写指针”时为空,“读指针”等于“写指针加 1”时为满;
  2. 使用有效数据计数:每次读写都更新有效数据计数,计数等于 0 时为空,等于 BUF_SIZE 时为满;
  3. 记录最后一次操作:用一个标志记录最后一次是读还是写,在 "读指针" 等于 "写指针" 时若最后一次是写,则为满状态;若最后一次是读,则为空状态。

可以看到,FrameQueue 使用上述第 2 种方式,使用 FrameQueue.size 记录环形缓冲区中元素数量,作为有效数据计数。

ffplay 中创建了三个 FrameQueue:音频 FrameQueue,视频 FrameQueue 和字幕 FrameQueue。每个 FrameQueue 有一个写端和一个读端,写端位于解码线程,读端位于播放线程。

为了叙述方便,环形缓冲区的一个元素也称作节点 (或帧),将 rindex 称作读指针或读索引 (这里的读指针是泛指,实际读指针有多个,详参 2.4.4 节),将 windex 称作写指针或写索引,叫法有混用的情况,不作文字上的严格区分。

2.4.2 初始化和销毁队列

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;
}

队列初始化函数确定了队列大小,并为队列中每一个节点的 frame (即 f->queue[i].frame) 分配内存,注意只是分配 frame 对象本身,而不关注 frame 中的数据缓冲区。frame 中的数据缓冲区是 AVBuffer 结构,使用引用计数机制。在将音视频帧 (frame) 写入队列时,就是通过 AVBuffer 的引用计数机制来避免对音视频数据缓冲区做拷贝动作,从而避免了帧数据拷贝带来的开销。f->max_size 是队列的大小,它可以小于预分配的数组尺寸 FRAME_QUEUE_SIZE,音频、视频和字幕队列在初始化时通过 max_size 参数设置了不同的大小。f->keep_last 是队列中是否保留最后一次播放的帧的标志。f->keep_last = !!keep_last; 是将 int 类型的 keep_last 转换为 bool 型取值 (0 或 1)。

static void frame_queue_destroy(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);
}

队列销毁函数对队列中的每个节点作了如下处理:

  1. frame_queue_unref_item(vp) 释放对 vp->frame 中 AVBuffer 类型的数据缓冲区的引用
  2. av_frame_free(&vp->frame) 释放 vp->frame 对象本身

2.4.3 写队列操作

写队列的步骤是:

  1. 获取写指针 (若写满则等待);
  2. 将元素写入队列;
  3. 更新写指针。

2.4.3.1 写队列相关函数实现

写队列涉及下列两个函数:

frame_queue_peek_writable()     // 获取写指针
frame_queue_push()              // 更新写指针

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];
}

获取写指针 (wi 指针):从队列尾部申请一个可写的帧空间,若无空间可写,则阻塞等待。

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);
}

更新写指针 (wi 指针):更新帧计数与写指针,因此调用此函数前应先将帧数据写入队列写指针的位置。

2.4.3.2 写队列函数的用法

通过实例看一下写队列的用法:

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)))
        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);

    // 将AVFrame拷入队列相应位置
    av_frame_move_ref(vp->frame, src_frame);
    // 更新队列计数及写索引
    frame_queue_push(&is->pictq);
    return 0;
}

上面一段代码是视频解码线程向视频 FrameQueue 中写入一帧的代码,步骤如下:

  1. frame_queue_peek_writable(&is->pictq) 获取写指针:向队列尾部申请一个可写的帧空间,若队列已满则阻塞等待。
  2. av_frame_move_ref(vp->frame, src_frame) 写数据:将 src_frame 中所有数据拷贝到 vp->frame 并复位 src_frame,vp->
    frame 中的实际数据缓冲区 (AVBuffer) 使用引用计数机制,不会执行缓冲区拷贝动作。为避免内存泄漏,一般在调用 av_frame_move_ref(dst, src) 之前应先调用 av_frame_unref(dst),这里没有调用,是因为 vp 节点在队列读操作中被读出并删除后变为可写入节点,在删除节点时已经释放了 vp->frame 中的各数据缓冲区。
  3. frame_queue_push(&is->pictq) 更新写指针:此步仅将 FrameQueue 中的写指针加 1,实际的数据写入在此步之前已经完成。

2.4.4 读队列操作

在写队列操作中,应用程序写入一个新帧后通常总是将写指针加 1。而在读队列操作中,“读取”和“更新读指针 (同时删除旧帧)”二者是独立的,可以只读取而不更新读指针,也可以只更新读指针 (同时删除旧帧) 而不读取。而且读队列引入了是否保留已显示的最后一帧的机制 (即 keep_last 机制),导致读队列比写队列要复杂很多。

读队列的常规步骤如下:

  1. 获取读指针 (若读空则等待);
  2. 读取一个节点;
  3. 更新读指针(同时删除旧节点)。

2.4.4.1 keep_last 机制

要弄清楚读队列的上述几个函数,需要先弄清楚 keep_last 机制。keep_last 机制用于在 FrameQueue 中保留最后一帧已播放的帧。keep_last 有什么用呢?比如视频播放过程中按了空格键,播放暂停了,就可以不断从视频队列中取出上一帧已播放的帧在 SDL 窗口渲染出来,这就实现了暂停功能。通过下面代码可以看出,音频 FrameQueue 和视频 FrameQueue 都启用了 keep_last 机制,字幕 FrameQueue 未启用 keep_last 机制。

static int frame_queue_init(FrameQueue *f, PacketQueue *pktq, int max_size, int keep_last)
{
    ...
    f->keep_last = !!keep_last;
    ...
}

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

keep_last 机制由 FrameQueue 中的 keep_last 和 rindex_shown 两个变量实现。在队列启用 keep_last 机制后,第一次调用 frame_queue_next() 函数时会将 rindex_shown 值设为 1,代码如下:

static void frame_queue_next(FrameQueue *f)
{
    if (f->keep_last && !f->rindex_shown) {
        f->rindex_shown = 1;
        return;
    }
    ...
}

rindex_shown 的引入增加了读队列操作的理解难度。为了叙述方便,我们把 rindex 简记为 ri,ri 帧表示上一次播放的帧;把 rindex+rindex_shown 简记为 ris,ris 帧表示本次待播放的帧;把 windex 简记为 wi,wi 帧表示待写入队列帧。写队列只有一个 wi 指针。而读队列涉及 ri,ris,ris+1 三个指针,其中的关键是要弄清 ri 指针和 ris 指针的区别,如果队列未开启 keep_last 机制,ri 和 ris 指向同一帧,因为 rindex_shown 等于 0。

2.4.4.2 读队列相关函数实现

读队列涉及如下函数:

frame_queue_peek_readable()     // 获取本次待播放帧指针(若读空则等待),即获取ris帧
frame_queue_peek()              // 获取本次待播放帧指针,即获取ris帧
frame_queue_peek_next()         // 获取本次待播放帧的下一帧指针,即获取ris+1帧
frame_queue_peek_last()         // 获取上次已播放帧指针,即获取ri帧
frame_queue_next()              // 更新读指针(同时删除旧节点),即删除ri帧并将ri指针后移一位

来看一下读队列相关函数的完整实现:

frame_queue_peek_xxx()

// 获取ris指针:当前待播放帧
static Frame *frame_queue_peek(FrameQueue *f)
{
    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}

// 获取ris+1指针:下一待播放帧
static Frame *frame_queue_peek_next(FrameQueue *f)
{
    return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size];
}

// 获取ri指针:上一已播放帧
static Frame *frame_queue_peek_last(FrameQueue *f)
{
    return &f->queue[f->rindex];
}

frame_queue_peek_readable()

// 获取ris指针:从队列头部读取一帧,只读取不删除,若无帧可读则阻塞等待
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);

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

    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}

这个函数实际是获取当前待显示的帧。这个函数和 frame_queue_peek() 函数的区别仅仅是多了不可读时等待的操作。什么时候不可读呢?对于启用了 keep_last 机制的队列,队列中帧数小于等于 1 时不可读,对于未启用 keep_last 机制的队列,队列为空时不可读。

frame_queue_next()

// 更新ri指针:后移一个位置,删除ri帧,ri指针加1,
static void frame_queue_next(FrameQueue *f)
{
    if (f->keep_last && !f->rindex_shown) {
        f->rindex_shown = 1;
        return;
    }
    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);
}

更新 ri 指针。三个动作:删除 ri 帧,更新 ri 指针 (f->rindex) 和队列中有效帧计数 (f->size)。

frame_queue_nb_remaining()

// 获取队列中未显示的帧数
static int frame_queue_nb_remaining(FrameQueue *f)
{
    return f->size - f->rindex_shown;
}

视频队列启用了 keep_last 机制,则 f->rindex_shown 值为 1,当队列中只有一帧时,f->size 值为 1,此时 frame_queue_nb_remaining() 返回 0,表示无未显示帧,队列中仅有的一帧是上次已显示帧 (即 ri 帧)。

2.4.4.3 读队列函数的用法

通过实例看一下读队列的用法:

static void video_refresh(void *opaque, double *remaining_time)
{
    ...
    if (frame_queue_nb_remaining(&is->pictq) == 0) {    // 无未显示帧
        // nothing to do, no picture to display in the queue
    } else {                                            // 有未显示帧
        ...
        lastvp = frame_queue_peek_last(&is->pictq);     // 上一帧:上次已显示的帧,即ri帧
        vp = frame_queue_peek(&is->pictq);              // 当前帧:当前待显示的帧,即ris帧
        ...
        // 更新读指针:删除ri帧,然后将ri指针加1,此时ri帧指向vp帧但还未显示
        frame_queue_next(&is->pictq);
        ...
    }
    ...
display:
    // 此函数外部窗口大小变化或用户按刷新键,或此函数内部帧播放时间已到,则is->force_refresh为1
    // 从if分支走到这里:通常无动作,若force_refresh为1,就从视频队列中取出上一帧显示
    // 从else分支走到这里:通常取vp帧显示,若force_refresh为0(帧播放时间未到)则无动作
    if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
        video_display()-->video_image_display()-->frame_queue_peek_last();
    ...
}

上面一段代码是视频播放线程从视频 FrameQueue 中读取视频帧进行显示的基本步骤,其他代码已省略,只保留了读队列和视频显示相关的部分。video_refresh() 的实现详情可参考 4.4.1 节。

记 lastvp 为上次已播放的帧,vp 为本次待播放的帧,下图中方框中的数字表示显示序列中帧的序号 (实际就是Frame.frame.display_picture_number 变量值)。

frame_queue示意图

图中已播放的帧是灰色方框,本次待播放的帧是黑色方框,其他未播放的帧是绿色方框,队列中空位置为白色方框。

假设某次进入 video_refresh() 的时刻为 T0,下次进入的时刻为 T1。在 T0 时刻,读队列显示的步骤如下 (为简便,先只看 else 分支):

  1. 获取 ri 帧和 ris 帧:图中 ri 帧表示上一次显示的帧 lastvp, ris 帧表示本次待显示的帧 vp
  2. 更新读指针:删除旧 ri 帧 (lastvp),然后将 ri 指针加 1,此时新 ri 帧 (vp) 还未显示
  3. 显示视频帧:执行 video_display()-->video_image_display()-->frame_queue_peek_last(),执行完后新 ri 帧 (vp) 就成了上一次显示的帧

这里的 else 分支先更新读指针然后才读出帧进行显示看起来比较奇怪 (不符合常规步骤),其目的是为了让 if 和 else 两个分支能共用后面的 video_display()-->video_image_display()-->frame_queue_peek_last() 代码,frame_queue_peek_last() 是获取 ri 帧,对于 if 分支它获取的是真正的 ri 帧 (上次已播放帧),对于 else 分支它获取的是名不符实的 ri 帧 (即代码中的 vp 帧,实际还未显示过,等显示完成,ri 帧才名符其实成为上一次已显示帧)。

在之后的某一时刻 TX,首先调用 frame_queue_nb_remaining() 判断是否有帧未播放,若无待播放帧 (队列中只有一帧时),通常无动作直接退出 (若 force_refresh 为 1 则取上次已显示帧再次显示)。这样,对于视频队列,keep_last 是 1,rindex_shown 也是 1,队列中总是保留了至少一帧 (如 TX 时刻灰色方框)。注意,在 TX 时刻,无新帧可显示,保留的一帧是已经显示过的。那么最后一帧什么时候被清掉呢?在播放结束或用户中途取消播放时,会调用 frame_queue_destory() 清空播放队列。

posted @ 2019-01-21 21:55  叶余  阅读(8341)  评论(1)    收藏  举报