一场微秒级的同步事故
导读:诺兰导演作品《星际穿越》里面有这样一个片段,母舰损坏以后,处于高速旋转状态,库珀为了对接母舰,必须要使自己的飞船高速旋转, 与母舰同步成一样的状态,才能进行对接成功;只要同步成功才能对接登上母舰,同步失败则会机毁人亡。
事故场景复现
一场高端大型的直播真人xx秀,xxx人正线下观看,刹那间直播画面出现卡顿,画面播放缓慢,某一瞬间还会有倒放前一个画面,直播画面与声音不匹配的状态。
接上级任务,小白临危受命来处理这一问题
事故问题分析
小白查看了现场播放的画面状态,初步认定这是由于音视频不同步导致的(废话,当然是不同步导致的,要是同步的话能导致这问题)
如何解决这一问题?首先,我们需要先掌握播放器的原理,在对播放的各个环节予以检测,才能定位出问题所在,就像庖丁解牛对牛的身体构造有足够的了解才行
播放原理
播放流程大致如上图所示:
- 解协议
从一阵阵协议数据里面,提取协议中媒体流字段的数据,为封装数据 - 解封装
封装数据是对音视频以及字母等编码数据的集合封装,将封装数据分离开来,变为编码的音视频流数据 - 解码
不同算法的编码格式要使用对应的解码算法进行解码,解码为可播放的数据,某些解码后格式不同的数据可以使用ffmpeg进行转码在播放 - 同步
对解码后的数据直接进行播放,由于显卡、声卡播放速度不同,以及一些业务逻辑干预,会导致音视频播放不一致,也就是声音和画面不匹配的状态(就像夏天打雷的时候,先看到画面,一会后才能听到声音),为了解决这一问题,我们必须进行同步控制,在对的时间播放对的画面
音视频同步控制分析
在进行音视频同步检查之前,我们要确保从解码后的数据音频和视频数据AVFrame是对的,以及他们的时间戳pts也是对的,方能进行后续的同步分析
音视频是如何进行同步的?
详细来说,请参考我的音视频同步原理分析;
简单来说,我们分别为音视频设置了自己的时钟,每播完一帧音频,我们就更新音频时钟;视频时钟同理,我们选择音频时钟作为参考时钟,视频在播放每一帧画面时,与音频时钟对比,如果计算当前画面播放的时间慢于音频时钟,就赶紧播;如果播放时间大于音频时钟,那画面就等等,休眠一段时间在播放这个画面,休眠多少时间,也就是同步算法计算的最终结果
事故解决
首先你必须保证解码后的音视频数据AVFrame以及显示时间戳pts是正确的,才能进行后续的同步问题分析
定位方法
依小白的理解,定位问题应该有两种方法,一种是聪明的方法,能快速定位解决问题,可是小白目前的功率,办不到啊
还有一种是比较笨的方法,我取名为“关键点插值方法”
关键点插值方法
也就是在代码逻辑的关键处,插入日志,输出各个换件的变量状态,逐步了解每个状态并分析之
分析
从事故播放画面来看,有可能是视频时钟快了,导致视频播放缓慢不断的延时,让音频时钟追赶上来,问题是音频时钟一直没有追上来,从而视频时钟一直处于快的一方,不停的延时,也就导致画面不停延时播放(每个画面就像等一会,在播下一个画面)
所以,小白选择了两个地方作为关键点进行日志插入,小白的代码是参考ffplay源码修改的,对这块感兴趣的盆友可以去查看ffplay源码
- 关键点1
音视频时钟对比处,计算出延时的函数:
double MediaSync::calculateDelay(double delay) {
double syncThreshold, diff = 0;
if(playerStatus->syncType != AV_SYNC_VIDEO){
diff = videoClock->getClock() - getMasterClock(); //计算两个时钟的差值
LOGI("video clock %f master clock %f", videoClock->getClock(), getMasterClock());
//约定delay的值不超过MIN MAX之间
syncThreshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(delay, AV_SYNC_THRESHOLD_MAX));
if(!isnan(diff) && fabs(diff) < maxFrameDuration){
//视频时钟小于主时钟,要减小时延
if(diff < -syncThreshold){
delay = FFMAX(0, delay+diff);
LOGI("视频时钟落后");
//视频时钟大大超过主时钟,增大延时
} else if(diff >= syncThreshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD){
delay = delay + diff;
LOGI("视频时钟大大超前");
//视频时钟超前,增大时延即可
} else if(diff >= syncThreshold){
delay = 2 * delay;
LOGI("视频时钟超前");
}
}
}
return delay;
}
- 关键点2
每一帧画面播放的时间framerTime以及系统时钟和该画面应该延时的时间
//计算上一次显示的时长
lastDuration = calculateDuration(lastFrame, currentFrame);
//根据上一次显示时长来计算时延
delay = calculateDelay(lastDuration);
if(fabs(delay) > AV_SYNC_THRESHOLD_MAX){
if(delay > 0){
delay = AV_SYNC_THRESHOLD_MAX;
} else{
delay = 0;
}
}
time = av_gettime_relative() / 1000000.0;
LOGI("framer time %f, current time %f delay %f", frameTimer, time, delay);
if(isnan(frameTimer) || time < frameTimer){
frameTimer = time;
}
- 日志输出
日志为开头播放的前面几帧数据,framer time是上一帧的播放时间,current time为当前系统时间,delay是该帧的延时时间,delay会av_usleep函数进行延时
从上面看出端倪了吗?
端倪就是:每个画面都会延时0.05s左右,下一次代码在来时,日志显示的current time时间有问题,current time并没有并没有比上一次时间加0.05s大,也就是延时根本没有延时0.05s,那我们看看延时代码是怎么写的?
if(remaining_time > 0.0){
av_usleep((int64_t)remaining_time * 1000000.0);
}
remaining_time就是日志中的delay,就是这一句出问题了;你看出问题了吗?
问题出在类型强制转换int64_t那里,int64_t就是long long类型,上一句他默认只会对remaining_time进行转换,而remaining_time是0.05,这个转换结果就是0;所以延时几乎不消耗时间,也就是上图日志的current time时间每次延时后都不会有大的变化
修正后,每次延时正确了,current time也确实有大的变化;可是音视频仍然不同步;哎,八阿哥多啊!不要气馁,攻克他你就上升一步,臣服他你只能原地踏步
再次仔细看以下日志:
仔细分析每一个环节的数字,在第一次video clock视频时钟更新时为0.388173,是不是没看出来,那在看看主时钟(也就是音频时钟)为0.082576;看出来没?两者相差10倍左右,但是按照音视频编码时,他们的时间戳几乎不会相差这么大,那么这里很有可能是视频时钟更新出了问题,检查下代码:
void MediaClock::setClock(double pts) {
double time = av_gettime_relative() / 1000000;
setClock(pts, time);
}
看到没,av_gettime_relative() / 1000000这个结果赋值给了一个double类型,也就是long/int=double,这样会丢失很多精度的,转为1000000.0这样就弥补了精度问题
以上两个问题修正后,音视频终于同步了,画面声音都正常播放,成功解决问题
总结
- 定位问题要有耐心,不是一下就找到了问题所在,要有不解决不放弃的决心
- 问题一般的是由于疏忽导致,这些基础性的问题一定要编码时注意,就不会出现这些问题了
上一篇: Harris 角点检测