24

一场微秒级的同步事故

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzA4MjU1MDk3Ng%3D%3D&%3Bmid=2451526783&%3Bidx=1&%3Bsn=cf2485b34ef2ce629a4f6d539022bc63
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

EfiamiU.jpg!web

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

作者:jackzhou

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

事故场景复现

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

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

事故问题分析

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

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

播放原理

eUJBjun.jpg!web

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

  • 解协议 从一帧帧协议数据里面,提取协议中媒体流字段的数据,为封装数据

  • 解封装 封装数据是对音视频以及字母等编码数据的集合封装,将封装数据分离开来,变为编码的音视频流数据

  • 解码 不同算法的编码格式要使用对应的解码算法进行解码,解码为可播放的数据,某些解码后格式不同的数据可以使用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函数进行延时

bEjiim2.jpg!web 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也确实有大的变化;可是音视频仍然不同步;哎,八阿哥多啊!不要气馁,攻克他你就上升一步,臣服他你只能原地踏步

再次仔细看以下日志:

iYZ3Ijm.jpg!web 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. 问题一般的是由于疏忽导致,这些基础性的问题一定要编码时注意,就不会出现这些问题了


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK