前言
video_refresh()是ffplay中负责视频刷新的核心函数,涉及音视频同步的关键逻辑。该函数通过FrameQueue管理视频帧,并使用多个辅助函数进行计算和同步。主要流程包括:1) 从FrameQueue中获取当前帧和下一帧;2) 计算帧间隔和显示延迟;3) 根据系统时间和帧显示时间判断是否显示下一帧;4) 更新视频时钟和显示帧。compute_target_delay()函数用于调整显示延迟,确保音视频同步。
一、函数分析
分析video_refresh()前,先分析下几个重要的函数和概念。
1、lastvp,vp,nextvp
rindex:FrameQueue的读索引
rindex_shown:用于配合keep_last保留最后一帧,显示第0帧后一直是1。
lastvp:上一帧,是已经显示了的帧(也是当前屏幕上看到的帧),对应数组的索引为读索引rindex。
vp:将要显示的目标帧(待显示帧),对应数组索引为(rindex+rindex_shown)%f->max_size。
nextvp:是下⼀次要显示的帧(排在vp后⾯,vp的下一帧),对应数组的索引为(rindex+rindex_shown+1)%f->max_size。
2、frame_queue_nb_remaining
/* return the number of undisplayed frames in the queue */
static int frame_queue_nb_remaining(FrameQueue *f)
{
return f->size - f->rindex_shown;
}
返回未显示过的帧数。 f->size代表总帧数,f->rindex_show是配合keep_last保留最后一帧用的。初始时为0,显示第0帧后f->rindex_show一直是1,保证最后一帧一直在队列中,所以显示过的帧数时,要减去1。
3、frame_queue_peek_last
static Frame *frame_queue_peek_last(FrameQueue *f)
{
return &f->queue[f->rindex];
}
peek英文翻译为偷看,短暂的看一眼。frame_queue_peek_last()就是看一下lastvp。
4、frame_queue_peek
static Frame *frame_queue_peek(FrameQueue *f)
{
return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}
frame_queue_peek()看一下vp帧,即看一下接下来要显示的帧。rindex_shown为0时,lastvp和vp指向同一帧。由于f->rindex_shown显示第0帧后为1,当读索引f->rindex到数组的最后一帧时,f->rindex + 1会越界,所以加上% f->max_size的取余操作。取余后(f->rindex + 1) % f->max_size变成0重新开始。加上取余是为了读索引读到最后一帧时,防止越界用的。
5、frame_queue_next
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);
}
frame_queue_next()是出队列操作。读索引右移,队列大小减1,相当于元素Frame[f->rindex]出队了。第一次调用frame_queue_next时,由于keep_last的作用,f->rindex_shown 变为1,没有进行实际的出队操作,配合frame_queue_nb_remaining(),队列中一直会保留最后一帧。
6、frame_queue_peek_next
static Frame *frame_queue_peek_next(FrameQueue *f)
{
return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size];
}
看一下nextvp,返回vp的下一帧。
7、vp_duration
static double vp_duration(VideoState *is, Frame *vp, Frame *nextvp) {
if (vp->serial == nextvp->serial) {
double duration = nextvp->pts - vp->pts;
if (isnan(duration) || duration <= 0 || duration > is->max_frame_duration)
return vp->duration;
else
return duration;
} else {
return 0.0;
}
}
计算两帧的时间间隔。
8、compute_target_delay
计算视频帧实际的显示时长。这是ffplay的核心同步算法,参考链接:
ffplay源码分析__音视频同步compute_target_delay-优快云博客
二、代码分析
static void video_refresh(void *opaque, double *remaining_time)
{
VideoState *is = opaque;
double time;
Frame *sp, *sp2;
if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
check_external_clock_speed(is);
if (!display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) {
time = av_gettime_relative() / 1000000.0;
if (is->force_refresh || is->last_vis_time + rdftspeed < time) {
video_display(is);
is->last_vis_time = time;
}
*remaining_time = FFMIN(*remaining_time, is->last_vis_time + rdftspeed - 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;
/* dequeue the picture */
// 从队列取出上一个Frame
lastvp = frame_queue_peek_last(&is->pictq); //读取上一帧
vp = frame_queue_peek(&is->pictq); // 读取待显示帧
// 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 */
//lastvp正在显示的帧,vp待显示帧 ,nextvp下一帧
//last_duration 计算lastvp应显示的时长
last_duration = vp_duration(is, lastvp, vp);
// 经过compute_target_delay方法,计算出待显示帧vp需要等待的时间
// 如果以video同步,则delay直接等于last_duration。
// 如果以audio或外部时钟同步,则需要比对主时钟调整待显示帧vp要等待的时间。
delay = compute_target_delay(last_duration, is);
time= av_gettime_relative()/1000000.0;
// is->frame_timer 实际上就是上一帧lastvp的播放时间,
// is->frame_timer + delay 是待显示帧vp该播放的时间
if (time < is->frame_timer + delay) {
// 当前系统时刻还未到达上一帧的结束时刻,那么还应该继续显示上一帧。
// 计算出最小等待时间
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
// 走到这一步,说明已经到了或过了该显示的时间,待显示帧vp的状态变更为当前要显示的帧
is->frame_timer += delay; // 更新当前帧播放的时间
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
{
is->frame_timer = time; //如果和系统时间差距太大,就纠正为系统时间
}
SDL_LockMutex(is->pictq.mutex);
if (!isnan(vp->pts))
{
update_video_pts(is, vp->pts, vp->pos, vp->serial); // 更新video时钟
}
SDL_UnlockMutex(is->pictq.mutex);
//丢帧逻辑
if (frame_queue_nb_remaining(&is->pictq) > 1) { //有nextvp才会检测是否该丢帧
Frame *nextvp = frame_queue_peek_next(&is->pictq);
duration = vp_duration(is, vp, nextvp);
// 非逐帧模式才检测是否需要丢帧 is->step==1 为逐帧播放
// cpu解帧过慢 framedrop>0 初始为-1
if(!is->step && (framedrop>0 ||
// 非视频同步方式
(framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER))
// 确实落后了一帧数据
&& time > is->frame_timer + duration){
is->frame_drops_late++; // 统计丢帧情况
frame_queue_next(&is->pictq); // 这里实现真正的丢帧
//(这里不能直接while丢帧,因为很可能audio clock重新对时了,这样delay值需要重新计算)
g_frameCount ++;
goto retry; //回到函数开始位置,继续重试
}
}
frame_queue_next(&is->pictq); // 当前vp帧出队列
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);
}
is->force_refresh = 0;
}
}
简化后的代码结构如下所示:
static void video_refresh(void *opaque, double *remaining_time)
{
VideoState *is = opaque;
double time;
Frame *sp, *sp2;
if (is->video_st) {
retry:
if (frame_queue_nb_remaining(&is->pictq) == 0) {
// nothing to do, no picture to display in the queue
// 什么都不做,队列中没有图像可显示
}
else{ //有数据时的逻辑
...
}
display:
/* display picture */
if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
video_display(is);
}
is->force_refresh = 0;
if (show_status) {...}
}
video_refresh()里主要有两个标签,retry和display。
retry标签:if-else语句,if语句表示如果没有数据可显示时,直接跳到display标签。
else语句的主要功能是计算是否到了显示vp的时刻,若没到,计算还差多长等待时间。若到了vp的显示时间,判断vp是否需要丢帧,不丢帧时显示vp,丢帧时,返回到retry继续计算当前帧的显示时长。
display标签:主要是video_display(),负责用SDL显示视频,执行完display标签后返回refresh_loop_wait_event()继续休眠等待。下面根据流程分析代码:
1、判断队列是否有数据可显示
if (frame_queue_nb_remaining(&is->pictq) == 0) {
// nothing to do, no picture to display in the queue
printf("nothing to do, no picture to display in the queue\n");
}
else{
...
}
若没有视频帧可显示,会跳到display标签。若队列有视频帧可显示,执行else语句。
注意:frame_queue_nb_remaining()是返回未显示过的帧数,并不是队列中没有数据,因为最后一帧一直保留在队列中。
2、从队列取出lastvp和vp
/* dequeue the picture */
lastvp = frame_queue_peek_last(&is->pictq);//&f->queue[f->rindex]
vp = frame_queue_peek(&is->pictq);//f->rindex + f->rindex_shown
lastvp是当前正在显示的帧,对应索引为读索引rindex,vp是接下来要显示的帧,对应索引为rindex+rindex_shown,对于刚开始rindex_shown为0,lastvp和vp都是指向第0帧。
3、判断vp的序列号和视频PacketQueue的序列号是否一致
if (vp->serial != is->videoq.serial) {
frame_queue_next(&is->pictq);
goto retry;
}
在video_thread线程-->queue_picture()中,Frame入队列时,不跳转的情况下,vp的序列号和视频PacketQueue的序列号一致,跳转时,PacketQueue的序列号会增加,所以这句是过滤掉失效的Frame数据。
4、计算lastvp和vp的pts差
/* compute nominal last_duration */
last_duration = vp_duration(is, lastvp, vp);
vp_duration()计算的是相邻两帧的pts差,不丢帧时返回一帧的时长,丢帧时返回两帧或三帧的时长。compute_target_delay()再根据音视频时钟差,修正lastvp显示的时长。
delay = compute_target_delay(last_duration, is);
根据compute_target_delay()得出的lastvp显示时长delay,就可以计算vp的系统显示时间=lastvp的系统显示时间+delay,接下来查看系统时间是否到了vp的显示时间。
5、判断是否到了vp的显示时间
time= av_gettime_relative()/1000000.0;
if (time < is->frame_timer + delay) {
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
time:系统的当前时间(秒),av_gettime_relative()返回微妙数,除以100 0000后是秒。
is->frame_timer:lastvp显示的系统时间
is->frame_timer+delay:vp应该显示的系统时间
所以这句代码判断当前时间time是否到了vp的显示时间。若time<is->frame_timer+delay,说明没到,并计算还差多长(*remaining_time的值),若到了继续向下执行。计算remaining_time值,这里用了如下代码
{
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
is->frame_timer + delay - time是还差多长时间显示vp帧,即等待时间。在refresh_loop_wait_event()中,每次执行video_refresh()前,remaining_time都会重新刷新为0.01,所以若等待时间大于0.01秒,则*remaining_time为0.01秒,若等待时间小于0.01秒则用is->frame_timer + delay - time的值。
这样确保至少每隔0.01秒判断一次,在小于0.01秒时用较小值防止等待时间过长。计算好等待时间后goto display,由于此时is->force_refresh为false,不会运行video_display()。
6、到了vp帧的显示时间
is->frame_timer += delay;
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
is->frame_timer = time;
前面分析过,is->frame_timer是lastvp的开始显示时间,delay为lastvp的显示时长,is->frame_timer +delay代表vp应该显示的系统时间,所以此时is->frame_timer加上delay后,代表vp应该显示的系统时间。这个值是理论值,由于程序没有那么精确,实际vp的显示时间是当前系统时间time,略大于is->frame_timer,不过一般这个差值是很小的,到了微秒级,可以忽略不记。不过由于其他原因可能导致is->frame_timer和当前系统时间time相差很大,当差异达到AV_SYNC_THRESHOLD_MAX(0.1秒)时,就要修正is->frame_timer为当前系统时间。
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
is->frame_timer = time;
这句代码还有个用处就是初始时显示第0帧,给is->frame_timer初始化,赋值为当前的系统时间。
有几种情况会导致当前系统时间time-is->frame_timer>0.1
1) 系统显示第0帧,初始化is->frame_timer时。
2)频繁操作SDL窗口,导致refresh_loop_wait_event一直处理SDL窗口事件,不执行video_refresh(),这样is->frame_timer不会累加,系统时间time一直在增长。当再次运行video_refresh()时,两者差距会大于0.1。
7、更新视频时钟
SDL_LockMutex(is->pictq.mutex);
if (!isnan(vp->pts))
update_video_pts(is, vp->pts, vp->pos, vp->serial);
SDL_UnlockMutex(is->pictq.mutex);
更新视频时钟为vp的pts。
8、判断vp是否需要丢帧
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 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
is->frame_drops_late++;
frame_queue_next(&is->pictq);
goto retry;
}
}
frame_queue_nb_remaining(&is->pictq) > 1表示队列中可显示的帧至少有2帧,duration表示vp和nextvp的pts差。is->frame_timer代表vp的系统开始显示时间,所以is->frame_timer + duration是nextvp应该显示的系统时间。
time>is->frame_timer + duration表示当前系统时间已经到了nextvp的显示时间了,所以vp过时了,此时vp不会显示(注意此时的视频时钟是vp的pts)。执行frame_queue_next()后,之前的lastvp出队列,vp变成lastvp,nextvp变成了vp,跳转到retry标签计算音视频时钟差。
9、frame_queue_next()
frame_queue_next(&is->pictq);
is->force_refresh = 1;
如果vp在显示时间范围内,就可以安心的显示了,并把强制刷新标志赋为1。
lastvp对应的索引是rindex,vp对应的索引是rindex+rindex_shown,nextvp对应的索引是rindex+rindex_shown+1。经过frame_queue_next()后,读指针向右偏移,相当于lastvp出队,vp变成lastvp,nextvp变成vp。
10、显示视频
if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
video_display(is);
video_display()前面有很多变量,满足很多条件后才会执行video_display()。
1)!display_disable:不禁止显示,就是显示
2)is->force_refresh:强制刷新,有几种情况会导致强制刷新
a. video_refresh()里面帧要显示了,这个是常规情况;
b. SDL_WINDOWEVENT_EXPOSED,窗口需要重新绘制
c. SDL_MOUSEBUTTONDOWN && SDL_BUTTON_LEFT 连续⿏标左键点击2次显示窗⼝间隔⼩于0.5秒,进行全屏或者恢复原始窗⼝播放
d. SDLK_f,按f键进行全屏或者恢复原始窗⼝播放
3)is->show_mode == SHOW_MODE_VIDEO:显示模式是SHOW_MODE_VIDEO
4)is->pictq.rindex_shown:
在队列中还没有解码数据时会执行display标签,如果此时SDL窗口变化会强制刷新窗口,此时也会执行video_display(),这样会导致问题,所以加上is->pictq.rindex_shown条件。
流程图如下:
接下来分别针对rindex_shown=0和rindex_shown=1两种情况下分析
三、流程分析rindex_shown=0
1、用frame_queue_nb_remaining()判断队列是否有可显示的数据,若没有跳到display标签处,此时is->force_refresh和is->pictq.rindex_shown为0,不执行video_display(),休眠0.01秒后继续循环检测。若队列有可显示的数据,进行第2步。
2、
/* dequeue the picture */
lastvp = frame_queue_peek_last(&is->pictq);
vp = frame_queue_peek(&is->pictq);
/* compute nominal last_duration */
last_duration = vp_duration(is, lastvp, vp);
delay = compute_target_delay(last_duration, is);
is->pictq.rindex_shown为0时,lastvp和vp都指向Frame数组的第0帧,经过vp_duration()后,last_duration返回vp->duration,即帧间隔。vp->duration是在video_thread线程中插入队列时赋值的,即一帧的时长。此时视频时钟是NAN,compute_target_delay()直接返回delay,接下来判断是否到了显示vp的系统时间。
3、
time= av_gettime_relative()/1000000.0;
if (time < is->frame_timer + delay) {
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
is->frame_timer是lastvp的显示时间,初始是0。is->frame_timer+delay是vp的显示时间,av_gettime_relative()/1000000.0返回系统开机后的秒数,time是个很大的值,所以当前系统时间time>is->frame_timer + delay,即已经到了显示vp的时间,继续执行第4步。
4、
is->frame_timer += delay;
if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
is->frame_timer = time;
is->frame_timer +=delay 后,is->frame_timer的值为delay,代表vp的开始显示时间,此时满足(delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX),is->frame_timer 赋值为当前时间 time,相当于is->frame_timer第一次赋值,单位秒。
5、用update_video_pts()更新视频时钟为当前帧vp的pts,序列号为vp的序列号。接下来判断是否丢帧
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 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
is->frame_drops_late++;
frame_queue_next(&is->pictq);
goto retry;
}
}
如果未显示过的帧数大于1,此时第0帧未显示也算在内,即未显示过的帧数至少2帧,那么frame_queue_peek_next()函数取出第1帧,计算第1帧和第0帧的帧间隔,由于duration>0,前面is->frame_timer刚赋值为time,所以time > is->frame_timer + duration不成立,继续向下执行。
6、接下来执行frame_queue_next(),f->rindex_shown变为1,队列的读索引和队列大小都没变,is->force_refresh 赋值为1后执行video_display()显示视频,video_display()显示的帧是
frame_queue_peek_last()返回的,取的帧为第0帧。这样第一次循环运行结束,此时f->rindex_shown=1。
7、总结:
第一次循环lastvp和vp都指向第0帧。
此时视频时钟是NAN,compute_target_delay()直接返回last_duration,帧间隔。
is->frame_timer第一次赋值为系统当前时间。
执行frame_queue_next()后,f->rindex_shown=1,队列大小和读索引都没变。
video_display()显示的帧是第0帧。
四、流程分析(f->rindex_shown=1)
1、返回到refresh_loop_wait_event(),如果没有其他事件(鼠标事件,键盘事件)时,因为remaining_time的值没变,程序会休眠0.01秒后继续执行video_refresh()。
2、如果队列中只有一帧,frame_queue_nb_remaining()返回0什么也不做,继续休眠0.01秒循环。如果队列有未显示过的帧,继续执行第3步。
3、此时lastvp取的还是第0帧(因为此时读索引是0),vp取的是第1帧(因为rindex_shown=1)计算last_duration为两帧的时间差后,执行compute_target_delay()计算delay值,并根据delay计算出vp的显示时间。
4、接下来不断判断是否到了vp帧的显示时间了。is->frame_timer + delay是vp帧的系统显示时间。如果time<is->frame_timer + delay,说明还没到,并计算出休眠等待的时长。
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
如果等待时长大于0.01,那么程序至少0.01秒检查一次。如果小于0.01,那么休眠时间用较小值。
若time>=is->frame_timer + delay,说明到了显示下一帧(第1帧)的系统时间。程序执行第5步。
5、此时第1帧将要显示,执行is->frame_timer += delay;is->frame_timer更新为第1帧的开始显示时间(系统时间)。用update_video_pts()把视频时钟更新为第1帧的pts。
6、如果队列中还有nextvp帧,就要判断第1帧vp是否需要丢帧,如果time>is->frame_timer+vp_duration,即当前时间到了nextvp的显示时间,就要略过vp不显示,执行第8步。如果vp在可显示范围内,则显示vp帧,执行第7步。不管vp丢帧与否,当前的视频时钟是vp的pts。
7、执行frame_queue_next()函数,读指针右移一个元素,队列减少1帧,相当于把第0帧出队,此时vp帧变成lastvp。执行video_display()函数显示第1帧lastvp。接下来程序继续休眠0.01秒后检查是否到了显示第2帧的时间了。以此类推。
8、如果vp丢帧,执行frame_queue_next()函数,vp变成lastvp,重新返回到retry标签,根据lastvp和vp的pts差以及音视频的时钟差计算lastvp的显示时长,继续从第3步开始执行。