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。下面先讲一下环形缓冲区的基本原理,其示意图如下:

使用环形缓冲区实现 FIFO 的好处是,读出或写入一个元素后,其余元素不需要移动存储位置。环形缓冲区适合于事先明确了缓冲区的最大容量的情形。扩展一个环形缓冲区的容量,需要搬移其中的数据。因此一个缓冲区如果需要经常调整其容量,用链表实现更为合适。
环形缓冲区使用中要避免读空和写满,但在空和满这两种状态下读指针和写指针均相等,因此其实现中的关键点就是如何区分出空和满。有多种策略可以用来区分空和满两种状态:
- 总是保持一个存储单元为空:“读指针”等于“写指针”时为空,“读指针”等于“写指针加 1”时为满;
- 使用有效数据计数:每次读写都更新有效数据计数,计数等于 0 时为空,等于 BUF_SIZE 时为满;
- 记录最后一次操作:用一个标志记录最后一次是读还是写,在 "读指针" 等于 "写指针" 时若最后一次是写,则为满状态;若最后一次是读,则为空状态。
可以看到,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);
}
队列销毁函数对队列中的每个节点作了如下处理:
frame_queue_unref_item(vp)释放对 vp->frame 中 AVBuffer 类型的数据缓冲区的引用av_frame_free(&vp->frame)释放 vp->frame 对象本身
2.4.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 中写入一帧的代码,步骤如下:
frame_queue_peek_writable(&is->pictq)获取写指针:向队列尾部申请一个可写的帧空间,若队列已满则阻塞等待。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 中的各数据缓冲区。frame_queue_push(&is->pictq)更新写指针:此步仅将 FrameQueue 中的写指针加 1,实际的数据写入在此步之前已经完成。
2.4.4 读队列操作
在写队列操作中,应用程序写入一个新帧后通常总是将写指针加 1。而在读队列操作中,“读取”和“更新读指针 (同时删除旧帧)”二者是独立的,可以只读取而不更新读指针,也可以只更新读指针 (同时删除旧帧) 而不读取。而且读队列引入了是否保留已显示的最后一帧的机制 (即 keep_last 机制),导致读队列比写队列要复杂很多。
读队列的常规步骤如下:
- 获取读指针 (若读空则等待);
- 读取一个节点;
- 更新读指针(同时删除旧节点)。
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 变量值)。

图中已播放的帧是灰色方框,本次待播放的帧是黑色方框,其他未播放的帧是绿色方框,队列中空位置为白色方框。
假设某次进入 video_refresh() 的时刻为 T0,下次进入的时刻为 T1。在 T0 时刻,读队列显示的步骤如下 (为简便,先只看 else 分支):
- 获取 ri 帧和 ris 帧:图中 ri 帧表示上一次显示的帧 lastvp, ris 帧表示本次待显示的帧 vp
- 更新读指针:删除旧 ri 帧 (lastvp),然后将 ri 指针加 1,此时新 ri 帧 (vp) 还未显示
- 显示视频帧:执行
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() 清空播放队列。

浙公网安备 33010602011771号