ffplay源码之音视频同步分析

本文是根据ffplay源码-https://ffmpeg.org/download.html,分析其音视频同步的方式

视频显示的操作在主线程的refresh_loop_wait_event函数中,该函数及相关注释如下

static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
    double remaining_time = 0.0;
    /*从设备收集所有待处理的输入信息并将其放入事件队列中*/
    SDL_PumpEvents();
    /*检测是否有事件发生*/
    while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) {
        /*隐藏鼠标的操作*/
        if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
            SDL_ShowCursor(0);
            cursor_hidden = 1;
        }
        if (remaining_time > 0.0)
            av_usleep((int64_t)(remaining_time * 1000000.0));	//微秒延时函数
        remaining_time = REFRESH_RATE;							//REFRESH_RATE = 0.01
        if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
            video_refresh(is, &remaining_time);					//显示视频
        SDL_PumpEvents();
    }
}

从中我们可以看到,该函数一直在检测有没事件发生,有事件发生则不进行后续显示操作,否则进行显示操作。而显示操作之间的时间间隔是依靠av_usleep((int64_t)(remaining_time * 1000000.0))来实现的,默认为10ms,后续数值的改变是通过video_refresh函数实现的。

因此,我们可以断定音视频同步以音频为基准的话,video_refresh函数内必定有根据音视频时间差来控制延时时间remaining_time的操作。

video_refresh函数主要用于从视频解码线程中获取帧数据,根据前后两帧的pts计算出一帧持续的时间,并初定为下一帧的延时时间,然后基于音频时钟校准视频时钟,修正显示下一帧的延时时间;使用该延时时间和当前系统时间,判断该帧是否到时间显示;如果未到时间,则选取一个合适的值作为remaining_time,并跳过显示该帧;如果到时间显示了,则更新当前帧显示时间frame_timer,并送显。

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

/*以下video_refresh函数以 同步模式为音频、着重同步操作 为标准进行删减显示*/
static void video_refresh(void *opaque, double *remaining_time)
{
    ......
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 */
        lastvp = frame_queue_peek_last(&is->pictq);		//获取上一帧图像
        vp = frame_queue_peek(&is->pictq);				//获取当前帧图像

        if (vp->serial != is->videoq.serial) {			/*不连续,即文件发生重定位*/
            frame_queue_next(&is->pictq);				/*读游标移动,用于获取下一帧*/
            goto retry;
        }

        /*上下两帧不连续,当前帧时间frame_timer重新获取,不以之前的frame_timer为基准*/
        if (lastvp->serial != vp->serial)
            is->frame_timer = av_gettime_relative() / 1000000.0;

        /* compute nominal last_duration */
        last_duration = vp_duration(is, lastvp, vp);		/*获取上一帧持续时间*/
        /*结合上一帧延时时间、音频/视频时钟,计算当前帧需要延时时间*/
        delay = compute_target_delay(last_duration, is);	

        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 += delay;	/*以之前的frame_timer为基准,计算当前帧时间frame_timer*/
        /*如果当前帧显示时间仍然小于当前时间,并且超过阈值,那么校正为当前系统时间*/
        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);		/*更新pts*/
        SDL_UnlockMutex(is->pictq.mutex);
        
        frame_queue_next(&is->pictq);	/*读游标移动,用于获取下一帧*/
        is->force_refresh = 1;
    }
    ......
}
  • 延时时间remaining_time的更改,是通过vp_durationcompute_target_delay这两个函数实现的,下面我们重点看看这两个函数。

  • vp_duration函数是用于根据两帧数据的pts,获取上一帧的持续时间。

static double vp_duration(VideoState *is, Frame *vp, Frame *nextvp) {
    if (vp->serial == nextvp->serial) {				/*帧数据连续*/
        double duration = nextvp->pts - vp->pts;	/*两个数据帧的pts之差*/
        /*duration异常*/
        if (isnan(duration) || duration <= 0 || duration > is->max_frame_duration)
            return vp->duration;	/*根据帧率得出*/
        else
            return duration;
    } else {
        return 0.0;
    }
}

compute_target_delay函数是用于计算需要延时的时间(根据视频时钟和音频时钟之差计算出来的),以实现音视频时钟同步。

当视频帧率在10—25帧,即两帧理想间隔在40—100ms,此时sync_threshold为40—100ms,因此当差值达到或超过40—100ms(即1帧时)才会校准

视频帧落后,则延时时间变为40—100ms减去diff,最小阈值为0(用落后的diff来缩短1帧延时时间)

视频帧超前,则延时时间变为两倍的40—100ms(双倍延时时间来补偿超前1帧的时间)

当视频帧率小于10帧,即两帧理想间隔在100ms以上,此时sync_threshold为100ms,因此当差值超过或达到100ms(即接近1帧时)才会校准

视频帧落后,则延时时间变为>100ms减去diff,最小阈值为0(用落后的diff来缩短1帧延时时间)

视频帧超前,则延时时间变为>100ms加上diff(用超前的diff来延长1帧延时时间)

当视频帧率大于25帧,即两帧理想间隔在40ms以下,此时sync_threshold为40ms,因此当差值超过或达到40ms(即超过1帧时)才会校准

视频帧落后,则延时时间变为<40ms减去diff,最小阈值为0(用落后的diff来缩短1帧延时时间)

视频帧超前,则延时时间变为两倍的<40ms(双倍延时时间来补偿超前1帧的时间)

/*delay传入的是last_duration*/
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 */
        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,AV_SYNC_THRESHOLD_MAX为0.1
        	该阈值等于或接近一帧的持续时间*/
        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+diff表示以上一持续时间为基准,补上落后的时间,算出还需延时的时间;若落后太多,不延时*/
                delay = FFMAX(0, delay + diff);
            /*视频时钟超前于音频时钟,且为最大阈值,AV_SYNC_FRAMEDUP_THRESHOLD为0.1*/
            else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
                /*再原延时基础上,加上超前的时间*/
                delay = delay + diff;
            /*视频时钟超前于音频时钟,但未超过最大阈值*/
            else if (diff >= sync_threshold)
                /*两倍延时*/
                delay = 2 * delay;
        }
    }
    
    return delay;
}

我们再来看看音频时钟和视频时钟是如何更新的

视频时钟更新在update_video_pts(is, vp->pts, vp->pos, vp->serial)

音频时钟更新在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)

is->audio_clock由af->pts + (double) af->frame->nb_samples / af->frame->sample_rate计算出来,nb_samples为一帧采样数,sample_rate为一秒采样数,因此意为帧的pts加上一帧持续的时间。

is->audio_hw_buf_size 为音频播放设备调用回调函数时音频缓冲区大小(该大小由音频参数确定),乘2表示为立体声;is->audio_write_buf_size为帧数据写入到音频设备缓冲区后剩余的大小,bytes_per_sec为每秒数据量,因此第二个参数就表示为当前帧pts加上一帧持续时间,减去写入到音频设备的帧数据占用时间;

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

audio_callback_time为系统时间。

/*update_video_pts最终调用的是set_clock*/
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;	/*pts相较于time的漂移*/
    c->serial = serial;
}

/*get_master_clock最终也是调用get_clock*/
static double get_clock(Clock *c)
{
    if (*c->queue_serial != c->serial)		/*不连续*/
        return NAN;
    if (c->paused) {						/*暂停*/
        return c->pts;
    } else {
        double time = av_gettime_relative() / 1000000.0;	/*当前时间*/
        /*speed为播放速率,time - c->last_updated为 pts时钟 更新到读取 的时间差;
          c->pts_drift + time表示c->pts - time_old + time_now,
          即为pts+更新到读取时间差,即为基于pts并依靠系统时间提示精度的时钟;*/
        return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);
    }
}

所以我们可知,视频和音频时钟是基于其视/音频帧的pts,因为理想情况下当前播放的音频帧和视频帧的pts应该相同,所以这里以pts为基准来做为视/音频时钟,并以音频时钟作为基准校准视频时钟;当两者时钟同步,即pts同步,就实现了音视频同步的效果。

总的来说就是

视频显示依赖于延时函数来控制时间间隔,延时数值为remaining_time(默认为0.01,即10ms)

每一次都通过前一帧lastvp、当前帧vp的pts,计算理想情况下上一帧的持续时间duration_exp

通过音频时钟、视频时钟以及duration_exp,校正得出显示当前帧仍需要延时的时间delay(当且仅当音/视频时钟差接近与一帧持续时间时,才会进行校准)

若视频时钟落后于音频时钟,用duration_exp补上差值diff(<0),来作为显示下一帧的延时时间delay(最小阈值为0)

若视频时钟超前于音频时钟,用duration_exp补上差值diff(>0) 或双倍duration_exp(超前但未超过阈值),来作为显示下一帧的延时时间delay

用上一帧显示时间frame_timer + delay大于当前系统时间time,表明该帧来早了,未到显示的时间,先延时一段时间,更新remaining_time为该帧早到的时间(最大阈值为10ms)

此处用于将delay分批消耗完,若视频时钟落后,则delay相较于理论duration较小(减去落后部分diff),因此可以追上音频时钟;若视频时钟超前,则delay相较于理论duration较大(加上超前部分diff或两倍duration),因此可以等到音频时钟。

更新frame_timer为当前帧显示时间(补上delay)

用当前帧的pts更新视频时钟

若frame_timer + 当前帧理想持续时间duration 小于 time,表示该帧已经过时了,需要丢弃该帧,直接显示下一帧

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

猜你喜欢

转载自blog.csdn.net/m0_60259116/article/details/126469186