ffplay源码分析__音视频同步video_refresh

前言

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步开始执行。

五、参考链接

ffplay.c学习-5-视频输出和尺⼨变换_ffplay指定视频输出-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值