一场微秒级的同步事故

阅读 13
收藏 2
2019-11-21
原文链接:mp.weixin.qq.com

导读:诺兰导演作品《星际穿越》里面有这样一个片段,母舰损坏以后,处于高速旋转状态,库珀为了登上母舰,必须使自己的飞船也高速旋转, 与母舰同步成一样的旋转状态,才能进行对接成功;只要同步成功才能对接登上母舰,同步失败则会机毁人亡。

作者:jackzhou

地址:https://www.jianshu.com/p/54ca5c64b2d2

事故场景复现

一场高端大型的直播真人xx秀,xxx人正线下观看,刹那间直播画面出现卡顿,画面播放缓慢,某一瞬间还会有倒放前一个画面,直播画面与声音不匹配的状态。

接上级任务,小白临危受命来处理这一问题

事故问题分析

小白查看了现场播放的画面状态,初步认定这是由于音视频不同步导致的(废话,当然是不同步导致的,要是同步的话能导致这问题)

如何解决这一问题?首先,我们需要先掌握播放器的原理,在对播放的各个环节予以检测,才能定位出问题所在,就像[庖丁解牛][1]对牛的身体构造有足够的了解才行

播放原理

播放流程大致如上图所示:

  • 解协议 从一帧帧协议数据里面,提取协议中媒体流字段的数据,为封装数据
  • 解封装 封装数据是对音视频以及字母等编码数据的集合封装,将封装数据分离开来,变为编码的音视频流数据
  • 解码 不同算法的编码格式要使用对应的解码算法进行解码,解码为可播放的数据,某些解码后格式不同的数据可以使用ffmpeg进行转码在播放
  • 同步 对解码后的数据直接进行播放,由于显卡、声卡播放速度不同,以及一些业务逻辑干预,会导致音视频播放不一致,也就是声音和画面不匹配的状态(就像夏天打雷的时候,先看到画面,一会后才能听到雷声),为了解决这一问题,我们必须进行同步控制,在对的时间播放对的画面

音视频同步控制分析

在进行音视频同步检查之前,我们要确保从解码后的数据音频和视频数据AVFrame是对的,以及他们的时间戳pts也是对的,方能进行后续的同步分析

音视频是如何进行同步的?

详细来说,请参考我的[音视频同步原理分析][2];

简单来说,我们分别为音视频设置了自己的时钟,每播完一帧音频,我们就更新音频时钟;视频时钟同理,我们选择音频时钟作为参考时钟,视频在播放每一帧画面时,与音频时钟对比,如果计算当前画面播放的时间慢于音频时钟,就赶紧播;如果播放时间大于音频时钟,那画面就等等,休眠一段时间在播放这个画面,休眠多少时间,也就是同步算法计算的最终结果

事故解决

首先你必须保证解码后的音视频数据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函数进行延时

log1

从上面日志看出端倪了吗?

端倪就是:每个画面都会延时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也确实有大的变化;可是音视频仍然不同步;哎,八阿哥多啊!不要气馁,攻克他你就上升一步,臣服他你只能原地踏步

再次仔细看以下日志:

image.png

仔细分析每一个环节的数字,在第一次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这样就弥补了精度问题

以上两个问题修正后,音视频终于同步了,画面声音都正常播放,成功解决问题

总结

  1. 定位问题要有耐心,不是一下就找到了问题所在,要有不解决不放弃的决心
  2. 问题一般的是由于疏忽导致,这些基础性的问题一定要编码时注意,就不会出现这些问题了

推荐阅读

Android MediaCodec 硬编码 H264 文件
如何优雅地实现一个分屏滤镜
OpenGL ES 学习资源分享
加微信 ezglumes,备注 OpenGL,拉你入 OpenGL ES 技术交流群~~
欢迎关注微信公众号【纸上浅谈】,看更多音视频、OpenGL、多媒体开发文章。
公众号回复 OpenGL,领取 OpenGL 学习资源大礼包~~~

评论