------------------------------------全系列文章目录------------------------------------
本文是根据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_duration和compute_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; }
- 当视频帧率在10—25帧,即两帧理想间隔在40—100ms,此时sync_threshold为40—100ms,因此当差值达到或超过40—100ms(即1帧时)才会校准
-
我们再来看看音频时钟和视频时钟是如何更新的
- 视频时钟更新在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,表示该帧已经过时了,需要丢弃该帧,直接显示下一帧
-