Hi3516DV300音视频同步播放实战:从PTS到主时钟策略
工试云启 考证服务中心整理

Hi3516DV300这颗芯片说白了就是为视频监控、车载记录、可视对讲这类设备设计的视频处理器跑Linux系统配上厂家提供的MMP媒体处理平台视频采集、编码、解码这一套能很快跑起来。但如果你打算做本地回放、带屏预览这类功能就会发现音视频同步播放是个绕不开的坎。画面和声音各自都能出但放在一起就各种对不上要么音画延迟差了几百毫秒要么越播越漂这种问题做项目的朋友应该都深有体会。这篇文章就把我在Hi3516DV300上搞音视频同步播放的过程完整记录下来从原理到实操再到排坑一次性讲透。1. 先说清楚Hi3516DV300在这个项目里的角色1.1 这颗芯片的定位与用武之地Hi3516DV300属于嵌入式视频处理器里比较典型的一类内部集成了双核ARM Cortex-A7主频虽然不算高但视频硬编解码能力很强支持H.264/H.265的编解码最大到1080P30fps是没问题的还带了一个智能加速单元做移动侦测、人脸抓拍这类轻量级算法也够用。厂家SDK里封装好的MPP库把视频输入组、视频处理子系统、视频编码、视频解码、视频输出这些硬件模块统一管理起来对外提供一套比较抽象的操作接口。这颗芯片常见的应用场景包括网络摄像机、可视门铃、行车记录仪、无人机图传地面端之类的设备。我的项目场景是这样的设备端一边从摄像头采集视频并编码存储一边还需要把之前录制的文件在本地屏幕上播放出来同时通过音频接口输出声音。说白了就是给设备加了一个“本地回放”功能这和纯网络摄像头只负责推流上传的做法不一样对音视频同步的要求就高了不少。注意我不能在这里提具体厂商名字但如果你接触过这套体系应该知道它的典型套路内存管理靠VBVideo Buffer视频缓冲池模块连接靠Bind绑定机制Codec通道通过Chn号来管理还有一组Event事件机制用于上报异常。搞懂这几个概念后面做应用层开发就顺了。1.2 音视频同步播放项目的典型架构这类带屏设备的播放链路通常由解码、显示、音频输出、控制逻辑四部分组成。视频侧MP4或者H.264裸流文件被读取后按帧送入视频解码通道解码器输出YUV数据送进视频输出模块VO往屏幕上显示。音频侧AAC或者G.711的音频数据送进音频解码通道解出PCM脉冲编码调制数据再送入音频输出模块AO驱动扬声器发出声音。但解码和输出只是硬件通道层面的基本能力。真正实现“音画同步”还需要在应用层维护一个调度引擎实时比较视频帧和音频帧的播放时间戳决定哪一帧应该立刻送显、哪一帧应该等待、哪一帧应该丢弃。这是整篇文章的核心。我在做这个项目之前一直以为按下播放键之后硬件会自己搞定一切只要把编码数据丢给VDEC把音频丢给ADEC画面和声音自然就同步了。真正开始调才发现MPP平台只保证“解码正确”不保证“播放同步”。时间戳对齐、帧率节奏控制、启动缓冲这些事全得软件来做。2. 音视频是怎么“不同步”的2.1 不同步的根源先理解一个日常现象拍到人说话的画面声音却晚了几百毫秒才到或者声音先出来嘴巴还没张开。这在任何播放设备上都可能出现原因可以归纳为三条路径上的时间差。第一条是采集端造成的摄像头模组本身感光到ISP处理有延迟麦克风也有自己的ADC采样延迟过程这两个延迟不一样。第二条是编码封装造成的视频帧因为I帧间隔大往往要压缩得更慢音频帧很小编码很快这样混合进文件时时间戳已经存在偏差。第三条是播放端造成的解码器排队、显示刷新率限制、音频缓冲大小、系统调度波动都会引入额外延迟。在Hi3516DV300上做本地回放前两条原因通常影响不大因为文件是设备自己录的采集路径很固定。最大麻烦出在第三条也就是播放链路各个环节的延迟不一致。视频帧经过VDEC解码后在VB里排队再等VO按显示时序取走音频帧经过ADEC解码后进入AO内部缓冲区AO再按采样率节奏播放。这两条链路的延迟特性完全不同视频的帧周期由帧率决定33ms一帧音频的播放节奏由采样率决定20ms一段缓冲两边的“时钟源”就不同步。2.2 时间戳、参考时钟、PTS这些概念从工程上解决同步核心思路是给数据帧打上时间标签。视频每一帧记录一个PTSPresentation Time Stamp显示时间戳表示这一帧应该什么时候展现给用户音频每一段也记录一个PTS表示这一段应该什么时候被听众听到。播放器只要保证“同一时间点上的视频帧和音频帧同时被输出”音画就同步了。但这里有个问题PTS本身是用什么时钟计量的如果视频时钟和音频时钟是两套独立硬件时钟哪怕初始对齐过也会因为各自的晶振偏差慢慢漂移。所以播放器需要选一个主时钟基准通常是音频时钟原因很简单人耳对声音断续比眼睛对画面卡顿更敏感而且音频播放本来就是严格连续的PCM采样流。在Hi3516DV300平台上MPP生态里通常会把系统时钟当作时间基准用相对时间差来计算。视频解码器输出帧时附带的时间戳由输入侧决定音频解码器同理。我们应用层拿到这些时间戳和主时钟比较做差异计算和控制。2.3 同步误差容忍度行业里对音视频同步有一个普遍接受的体验范围声音比画面提前不超过40毫秒或者延后不超过-40毫秒大部分观众察觉不到超过100毫秒基本所有人都会明显觉得不对劲到了200毫秒以上这就是严重的不同步事故。我项目里定的目标是整个播放过程中误差控制在±30毫秒以内启动阶段允许短暂偏大但稳态必须保持住。不同内容类型的敏感程度还不一样。视频通话、卡拉OK对同步要求极高电影对话稍微差一点也能忍受但如果做直播类应用延迟要求还会更激进通常是看总延迟而不是相对偏移。我这边的场景是本地回放最高优先级就是保证音画相对关系正确。3. 在Hi3516DV300上搭建解码与播放通路3.1 视频通路VDEC-VO的绑定与VB管理连接视频解码和视频输出在MPP上最省事的办法是用Bind把VDEC的通道直接绑定到VO的通道上。绑定之后解码器每出一帧YUVVO会自动取走并按显示帧率输出软件层几乎不用干预。这个方案在SDK的Sample里很常见很多demo就是跑通了绑定关系然后播放一个文件就完事了。但做同步播放时我建议别直接用Bind至少不要把整条链路都绑定死。原因很直接绑定之后帧从VDEC到VO之间没有软件插手的机会你没法在中间做时间戳比较和丢帧处理同步控制就无从下手。比较灵活的做法是VDEC和VO不绑定解码输出后帧缓冲交给应用层应用层判断该不该显示再手动把帧送入VO的通道。这里涉及VB缓冲池的管理。MPP的VB是一块大内存池解码器输出的帧缓冲区从VB中申请应用层从解码通道拿到的帧头结构体里带有buffer句柄处理完之后要把buffer释放回VB否则缓冲池很快耗尽解码器就会停在那里不输出帧。踩过一次坑之后我就记住了每取一帧必须对应释放一次取帧和放帧的计数绝对要一致。显示侧VO还要配好接口类型。Hi3516DV300的VO输出可以走BT.1120并口、MIPI DSI或者接一个小的LCD屏。我这次用的是一块并口屏幕加HDMI转接方案硬件上支持1080P输出。VO的帧率模式要配成和源视频一致或者让VO自己按固定帧率从队列取帧否则显示节奏会不稳定。3.2 音频通路ADEC-AO与采样率参数音频侧相对简单但细节不少。ADEC通道接收编码音频流解码后输出PCM格式的音频帧AO通道则负责把PCM数据按采样率播放出去。Hi3516DV300的AO接口支持多种采样率配置常用的8K、16K、44.1K、48K都有位深一般16bit声道数单双可选。配参数时有几个容易忽略的点。第一AO内部有一个比较大的数据缓冲如果填入的PCM数据过多声音会比视频晚出来一大截填得太少又会因为缓冲不足导致播放卡顿甚至爆音。需要根据实际缓冲能力和系统调度周期选择一个合适的预填量。第二AO以DMA方式搬运数据DMA搬运是周期性的通常按帧周期来比如20ms一触发。这就要求应用层投递数据的节奏也得匹配这个周期不能一次塞很多然后长时间不塞。音频解码器的输出时间戳要仔细保存。ADEC自身并不负责播放调度它只负责把编码流解成PCMPCM什么时候送到AO是由应用层控制的。这个“什么时候送”就是我们同步逻辑可以操作的位置。3.3 缓冲队列与线程模型实际工程里我把整个播放模块拆成三个线程加两个队列。一个线程负责读文件、分离音频视频流把视频编码数据送进VDEC把音频编码数据送进ADEC另一个线程从VDEC输出取视频帧做同步判断后再送VO第三个线程从ADEC输出取PCM帧同样的同步判断后送AO。音频线程和视频线程之间通过一个共享的同步控制块交换主时钟状态。队列的设计直接关系到能不能做到平滑播放。视频线程和音频线程各自维护一个已解码帧队列队列长度需要动态调节。队列太长延迟变大队列太短遇到系统调度抖动容易断。我最终用的是每个队列深度不超过20帧视频按1080P估算占用VB空间也不小达不到深度时解码线程阻塞等待超过深度时就丢掉最老的一帧保证实时性优先。线程间的同步我选择了条件变量加互斥锁而不是简单sleep。原因很简单靠sleep方式的固定延时来假装同步抖动会很大进程被抢占一次就露馅了。条件变量可以让音频线程在AO需要更多数据的时刻被唤醒天然跟硬件的消耗节奏对齐。4. 同步策略落地音频主时钟方案4.1 同步引擎整体逻辑我采用的同步方案很直接就是用音频时钟当主时钟。每一轮音频线程送一段PCM给AO前记录这段PCM的PTS以及当前实际播放到的位置视频线程每一帧在准备送VO之前先拿自己的PTS和音频当前PTS做差差值记为A_V_Delta单位为毫秒。如果A_V_Delta在容忍范围内比如-30ms到30ms之间视频帧立刻送显示。如果A_V_Delta超过30ms也就是视频比音频晚太多说明画面明显落后这时需要让视频快点赶上常见做法是跳过若干帧或者降低输出间隔。如果A_V_Delta小于-30ms也就是视频比音频超前太多这时需要让视频等一下常见做法是暂时不送帧给VO等音频追上来。看起来简单但实现时有两个细节很容易翻车。第一个音频当前PTS怎么获取AO硬件不会主动把“当前正在播放的时间”上文给你只能根据你总共喂了多少毫秒PCM数据减去AO内部缓冲里还没播出去的数据量来推算。我在音频线程里维护了一个累计已送PCM时长计数器同时每次调用AO获取缓冲状态接口把已缓冲但未播放的时长减去得到的就是“大约正在播放的位置”。这个值不是绝对精确但作为主时钟已经够用。第二个A_V_Delta的更新频率。视频帧率只有25fps左右每秒只有25次机会去修正视频节奏音频每20ms送一次数据主时钟每20ms就会更新一次。两者的节奏天然不同步视频线程的同步判断应该以自己取到帧的时刻为准取主时钟的最新值而不是隔几帧才取一次。4.2 视频晚到与早到的处理策略视频晚到是最常见的情况。比如从文件读出的视频帧排队等待解码解码耗时或长或短帧到达视频线程的时间点并不均匀。如果某帧的PTS比音频当前时间晚了50ms我建议直接丢弃这一帧而不是等着追。为什么因为对这一帧来说它该显示的时刻已经过去了等它显示出来音画误差会更大而且延迟会越积越多。但无脑丢帧也是有代价的。帧丢掉太多画面会感觉一顿一顿的所以有一个阈值判断如果A_V_Delta在30ms到80ms之间我会选择丢一帧然后立即送下一帧如果超过80ms说明问题比较大这时候光丢一帧没用还要把后面进来的帧在队列里也丢掉一部分让视频整体快进追上音频。这个过程有点像变速播放只是我们用跳帧来近似实现。视频早到就反着处理。如果视频PTS比音频提前了50ms也就是画面先出来了那就直接把这一帧留在手上不送VO等主时钟追上来。等待过程中要注意别卡死如果长期等待说明音频出了问题比如解码堵了或者文件音频流断了要有超时保护逻辑超过500ms就强制送帧并重启同步。这里还要提一种特殊情况有些视频文件本身PTS就不连续可能是录制封装时丢过帧。如果完全按PTS差值来做控制会出现频繁丢帧和等待的振荡。我的办法是加一个低通滤波把A_V_Delta做一个指数滑动平均用平滑后的值做判断原始值只用来做突发检测。这样系统稳定性好很多。4.3 启动阶段的缓冲策略与首帧优化播放器按下播放键的一瞬间最容易出现不同步。因为视频解码器从开始输入码流到输出第一帧YUV需要一定的解封装和解码初始化时间而音频解码器出第一段PCM往往快一些。如果两边各自就绪后立刻开始输出一定会出现一段时间只有声音没有画面或者画面先出来的尴尬。正确做法是启动时先不着急输出而是先解码一小段数据让视频和音频各有约100ms到200ms的可播放缓冲。然后以音频为准等音频缓冲预填到目标深度再开闸放行同时视频线程在第一帧可显示时立刻送出去后续按PTS对齐。这个“开闸”动作要做得干净利落让观众感知上是“啪”一下就出来了画面和声音而不是拖泥带水。首帧还有一个优化点发送给VO的第一帧最好使用视频流里的关键帧也就是IDR帧。如果从非关键帧开始播放画面会出现绿屏或花屏直到下一个关键帧到来才会恢复。我这边做了一个轻量封装文件解析时先扫描到第一个关键帧位置播放从那里开始。代价是起始时间会往后挪零点几秒换来的是首帧即可用画面值。5. 真机调试中的典型问题5.1 问题一画面卡顿但音频正常这个现象的本质是视频输出节奏跟不上主时钟。我在调试早期遇到过表现是音频一路顺畅画面隔一两秒就顿一下仿佛视频在“追”声音。排查思路是看队列深度。因为音频为主时钟时视频帧如果持续晚到同步逻辑就会持续丢帧丢得太多效果就变成卡顿。最终定位到两个原因一是从SD卡读文件的IO速度不稳定有几次突然变慢导致视频解码输入不足二是VB缓冲池给视频解码分配的帧数太少解码器不得不等待释放的buffer才能继续工作相当于流水线断流。解法也简单文件读取加了一个预读缓冲提前读几百KB到内存里VB缓冲池根据实测需要调整帧数确保解码器始终有可用的输出缓冲。调整之后画面就顺了。5.2 问题二音频比画面提前明显另一种常见现象是嘴型对不上声音明显领先。排查日志后发现并不是同步逻辑没工作而是AO的缓冲预填量太大了。也就是说音频线程很早就把大量PCM塞进了AOAO里面排队的数据足够播好几百毫秒同步逻辑看到的主时钟PTS自然一直是落后的视频再怎么追也追不上。这个问题的根源是对AO缓冲状态理解不深。AO内部有一个“水位”的概念数据填得越多播放端延迟越大。需要根据AO实际缓冲能力设定一个合理目标水位比如预期延迟在80ms左右。然后在播放过程中持续监控实际水位过高就减少投喂过低就补充。我后来把目标水位控制在一个20ms到30ms的区间让音频延迟降了下来同步就恢复正常了。5.3 问题三长时间播放后出现漂移播放半小时以后逐渐音画偏离这是最恶心的一个问题因为刚启动时明明是同步的。遇到这种情况基本可以断定主时钟本身有问题。我排查后发现音频主时钟的推算依赖“累计送PCM时长”和“AO缓冲状态”这两个数值都有累计误差。AO缓冲状态查询返回的剩余时长单位可能不是精确到1ms而累计送PCM时长也会因为丢了一半帧、塞了一次空数据等因素出现偏差。时间越长主时钟与真实时间的偏差累积越大最后反映成音画漂移。解决办法是对主时钟做周期校准。校准的参考源是什么我用的是系统实时时钟在Linux下可以获取单调递增的纳秒级时间。每隔一段时间比如播放了5秒就拿系统时钟和当前音频主时钟做一次对比计算出一个修正系数然后后续PTS计算时乘上这个系数。这个办法实测效果很好半小时播放误差能控制在正负20ms以内。5.4 调试日志与工具技巧音视频同步调试最怕“感觉好像偏了”这种模糊结论一定要量化。我在代码里加了一个同步调试模块每秒钟输出一条结构化日志内容包括当前音频PTS、当前视频PTS、AV差、播放队列深度、AO水位、本次是否丢帧。这样在PC上用串口看日志就能非常直观地看到偏差曲线。有条件的话我建议做一个测试视频画面里放一个每秒跳动的数字同时配一段有节奏的“滴答”声。播放时用手机慢动作拍摄屏幕和扬声器一帧一帧数画面数字和声音触发之间的偏差。这个方法不用示波器也能定量测出同步精度我们在实验室就是这么干的效果立竿见影。6. 交付后我个人的几条经验项目从搭环境到调通前后花了两周多时间。回头看最大的心得有几点。第一不要迷信SDK自带的Sample。Sample为了展示功能代码路径通常是最简单的绑定式跳过了很多业务场景必需的细节。做产品功能时宁愿多花时间自己写一层播放控制逻辑也别在Sample基础上打补丁否则同步控制根本无从下手。第二时间戳这件事一定要从设计第一天就想清楚。我最初没太在意随手把解码器返回的帧直接送显示后面所有同步逻辑都是在那之后返工补的。如果一开始就明确“音频主时钟、视频随动、丢帧策略分档”开发节奏会顺很多。第三代码里的每一个丢帧、等待分支都要有完整的统计计数。上线以后客户报问题第一个要回答的就是“同步引擎到底丢了多少帧、等了多少次、主时钟偏差均值是多少”。没有这些数据排查故障全靠猜。我后来保留了一张简单的统计表用一次按键就能导出来这比任何日志都管用。第四同步效果必须在真实播放内容下验证。单纯的测试序列能看出偏差但看不出观感好坏。我用了一段人物对话视频连续播了两个小时重点观察长时间稳定性和说话起始瞬间的响应比看几十遍测试短片有效得多。最后再分享一个小技巧如果在Hi3516DV300这类芯片上做本地回放尽量把文件解析放在单独线程不要和播放控制线程耦合。文件格式解析、seek操作、甚至从网络拉流播放时的缓冲管理都容易引入不确定的延迟。分离得越干净播放控制越稳定同步引擎的输入就越可控。这套思路不仅适用于这个项目后面换其他平台做播放功能依然能用上。