当前位置: 首页 > news >正文

一场微秒级的同步事故

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

作者: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 学习资源大礼包~~~

相关文章:

  • 在HTML5上开发音视频应用的五种思路
  • 如何把微信打造成一个学习利器|微信阅读与笔记技巧
  • 「圣诞特辑」纯前端实现人脸识别自动佩戴圣诞帽
  • LearnOpenGL 源码在 MAC 上的编译与调试
  • 在 iOS 中使用 OpenGL ES 实现绘画板
  • 技术开发故事会连载
  • OpenGL ES 学习资源分享
  • 【音视频连载-001】基础学习篇- SDL 介绍以及工程配置
  • GLSL加载纹理颠倒的六种解决方案
  • 内存都没了,还能运行程序?
  • 比 SharedPreferences 更高效?微信 MMKV 源码解析
  • 【音视频连载-002】基础学习篇-SDL 创建窗口并显示颜色
  • 【音视频连载-003】基础学习篇-SDL 消息循环和事件响应
  • Android JNI 之 Bitmap 操作
  • 游戏中的角色是如何“动”起来的?
  • IE9 : DOM Exception: INVALID_CHARACTER_ERR (5)
  • (十五)java多线程之并发集合ArrayBlockingQueue
  • [nginx文档翻译系列] 控制nginx
  • 《剑指offer》分解让复杂问题更简单
  • 【node学习】协程
  • Apache的80端口被占用以及访问时报错403
  • Fabric架构演变之路
  • git 常用命令
  • java取消线程实例
  • scrapy学习之路4(itemloder的使用)
  • underscore源码剖析之整体架构
  • 阿里云爬虫风险管理产品商业化,为云端流量保驾护航
  • 百度贴吧爬虫node+vue baidu_tieba_crawler
  • 当SetTimeout遇到了字符串
  • 区块链技术特点之去中心化特性
  • 驱动程序原理
  • 译自由幺半群
  • 做一名精致的JavaScripter 01:JavaScript简介
  • 【运维趟坑回忆录】vpc迁移 - 吃螃蟹之路
  • # 计算机视觉入门
  • #我与Java虚拟机的故事#连载09:面试大厂逃不过的JVM
  • (rabbitmq的高级特性)消息可靠性
  • (六)库存超卖案例实战——使用mysql分布式锁解决“超卖”问题
  • (三维重建学习)已有位姿放入colmap和3D Gaussian Splatting训练
  • (四)模仿学习-完成后台管理页面查询
  • (图)IntelliTrace Tools 跟踪云端程序
  • (转)创业家杂志:UCWEB天使第一步
  • .bat批处理(八):各种形式的变量%0、%i、%%i、var、%var%、!var!的含义和区别
  • .NET 6 Mysql Canal (CDC 增量同步,捕获变更数据) 案例版
  • .net CHARTING图表控件下载地址
  • .NET Standard / dotnet-core / net472 —— .NET 究竟应该如何大小写?
  • .vollhavhelp-V-XXXXXXXX勒索病毒的最新威胁:如何恢复您的数据?
  • @requestBody写与不写的情况
  • [ vulhub漏洞复现篇 ] Django SQL注入漏洞复现 CVE-2021-35042
  • [@Controller]4 详解@ModelAttribute
  • [AHOI2009]中国象棋 DP,递推,组合数
  • [APUE]进程关系(下)
  • [bzoj1006]: [HNOI2008]神奇的国度(最大势算法)
  • [C#]OpenCvSharp结合yolov8-face实现L2CS-Net眼睛注视方向估计或者人脸朝向估计
  • [C#]使用PaddleInference图片旋转四种角度检测