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,并送显。

    /*以下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加上一帧持续时间,减去写入到音频设备的帧数据占用时间
      • 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,表示该帧已经过时了,需要丢弃该帧,直接显示下一帧
      在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值