2026/4/18 10:27:05
网站建设
项目流程
淘宝店可以做团购的网站吗,别墅装修装饰,网站响应式和电脑手机,哪些网站可以免费申请第一章#xff1a;时间的相对论——从采集源头扼杀“穿越”很多工程师拿到音画不同步的Bug单#xff0c;第一反应是去查播放器#xff0c;去查FFmpeg的av_sync_type。错了#xff0c;大错特错。如果你在源头打的时间戳#xff08;PTS#xff09;就是歪的#xff0c;后面…第一章时间的相对论——从采集源头扼杀“穿越”很多工程师拿到音画不同步的Bug单第一反应是去查播放器去查FFmpeg的av_sync_type。错了大错特错。如果你在源头打的时间戳PTS就是歪的后面就算也是大罗神仙来也救不回来。在嵌入式Linux环境下比如你正在用的海思、瑞芯微或者安霸平台采集端的“同步”本质上是一场关于时钟源Clock Source的博弈。1.1 谁在撒谎—— 统一时钟基准首先你得问自己一个灵魂问题音频和视频用的是同一个表吗很多初级代码是这样的视频采集线程gettimeofday()拿到一个时间戳塞给视频帧。音频采集线程gettimeofday()拿到一个时间戳塞给音频包。这代码跑几个小时可能没问题但一旦系统负载高了或者NTP服务悄悄校准了一下系统时间你的音画同步瞬间就会崩盘。gettimeofday是受系统墙上时间Wall Clock影响的它会跳变铁律一所有的时间戳必须绑定到CLOCK_MONOTONIC或者硬件的统一时钟上。如果你的视频走的是V4L2接口音频走的是ALSA你必须确认驱动层吐出来的时间戳基准是什么。查看你的内核源码或驱动文档。绝大多数现代SoC的ISP图像处理单元中断里打时间戳用的是ktime_get_ts()。而ALSA驱动里DMA中断触发时往往也用类似的内核态单调时钟。实战排查技巧不要相信应用层打的时间戳。在应用层Application Layer打标意味着你把“内核中断-调度器唤醒进程-用户态执行”这段不可控的系统抖动Jitter计入了时间戳。假如CPU满载你的视频采集线程被卡了50ms才调度到这时候你再调用clock_gettime这帧画面的PTS就比实际发生时间晚了50ms。这50ms的误差就是音画不同步的“原罪”。你应该直接提取驱动层带上来的时间戳Video:v4l2_buffer.timestampAudio:snd_pcm_status_get_tstamp1.2 视频采集的“隐形延迟”这里有个极易被忽视的陷阱Sensor曝光时间与ISP处理延迟。当你拿到V4L2的一帧数据时驱动给你的时间戳通常代表什么是“光线打到CMOS的那一刻”还是“DMA传输完成的那一刻”绝大多数BSP板级支持包的实现是后者——Frame Done Interrupt。看看上面这个流程。从Sensor曝光结束Exposure End到MIPI传输再到ISP做Debayer、3DNR降噪、锐化最后写入内存触发中断这中间可能有几十毫秒的延时。重点来了 如果你开启了强力的3D降噪或WDR宽动态ISP内部往往需要缓存2-3帧进行参考计算。这就意味着你读到的第N帧图像实际上是(N-3)帧那个时刻进来的光。但驱动给你的时间戳却是第N帧DMA完成的时间。这中间差了整整3帧的时间如果是30fps这就是100ms的偏差。人眼对音画同步的敏感度大约在40ms到-60ms之间。光这一个ISP堆积就足够让你的产品被测试测出“口型对不上”。怎么修查Datasheet确认ISP Pipeline的Latency。硬修正在封装层人为地将视频PTS减去固定的ISP处理延时Offset。甚至更激进如果能改驱动尝试在“V-Sync中断”代表曝光开始或结束打时间戳而不是“DMA Done中断”。1.3 音频采集的“块”效应音频比视频更麻烦。视频是一帧一帧的音频是连绵不断的“流”。在Linux ALSA中我们是按Period周期来收数据的。比如你设置采样率48kHz双声道16bit。你设置Period Size为1024 frames。这意味着每采集1024个采样点大约21.3msALSA驱动会触发一次中断通知应用层来收数据。这里的问题在于当你收到这1024个点时这21.3ms的时间已经过去了。如果你在read完这1024个点后简单粗暴地用当前时间作为这块音频的PTS那你所有的音频时间戳都偏晚了至少21.3ms。更要命的是加上ALSA内部的Ring Buffer缓冲这个延迟可能是不固定的。高阶玩法使用snd_pcm_delay不要瞎猜。ALSA API提供了一个神级函数snd_pcm_delay()。它会告诉你当前硬件Buffer里还积压了多少个采样点没被读走。1.4 晶振偏差两个世界的撕裂就算你把上面的采集逻辑都做对了还有一个物理层的鬼故事等着你采样率漂移Sample Rate Drift。你以为你设置了48kHz采样率ADC就真的每秒吐出48000个点吗天真。板子上的晶振通常有几十ppm的误差。真实的采样率可能是48005Hz也可能是47990Hz。而视频Sensor也有自己的晶振它的30fps可能实际上是29.97fps或者30.01fps。如果你的设备需要长时间录制比如行车记录仪、执法仪录制1小时以上这种微小的频率误差会累积。音频快了视频慢了 - 声音越来越超前。音频慢了视频快了 - 声音越来越滞后。怎么查写一个小工具挂机跑10个小时。统计一下实际收到的音频Sample总数 / 理论采样率 vs 实际收到的视频帧数 / 理论帧率 vs 系统流逝的物理时间。如果发现音频每小时多出几百毫秒的数据恭喜你遇到晶振偏差了。解决策略这在采集端很难彻底修通常是传给下游编码或复用层去处理。但你必须在元数据里意识到这一点。如果是做即时通讯RTC你需要做重采样Resample根据Buffer的水位动态调整采样率比如把48k微调成48.05k来对齐时间轴。这叫自适应重采样是WebRTC里极其核心的技术后面章节我们会细聊。第二章编码器的黑盒——PTS与DTS的爱恨情仇数据采集上来是Raw DataYUV/PCM这时候时间戳通常叫Capture Timestamp还是线性的、单纯的。一旦送进编码器H.264/H.265/AAC事情就开始变得“魔幻”了。很多新人在这里会搞混两个概念PTS (Presentation Time Stamp)和DTS (Decoding Time Stamp)。在没有B帧双向预测帧的年代PTS等于DTS世界很和平。但在H.264 High Profile引入B帧后为了压缩效率编码顺序和播放顺序不一样了。2.1 编码器丢帧引发的PTS断裂嵌入式设备的VPUVideo Processing Unit通常也是个黑盒。你喂进去30帧YUV它出来的可能只有28帧H.264数据。为什么码率控制RC切断画面突变码率爆了编码器主动丢帧保码率。性能瓶颈编码速度跟不上采集速度输入Buffer溢出丢弃旧帧。这里有个致命的Bug模式有些为了省事的开发者在送入编码器前不维护PTS而是等着编码器出来的数据包AVPacket再给它打时间。如果编码器中间丢了一帧你的时间戳逻辑就会错位。你会把第10帧的时间戳贴到第11帧的数据上。排查手段在YUV送入编码器之前给每一帧YUV打上一个唯一的Serial Number序列号甚至可以将这个ID烧录到画面的第一行像素里OSD水印或者利用编码器User Data字段带过去。当码流出来解码看的时候检查画面上的ID和时间戳是否依然对应。这个方法虽然土但是极其有效能瞬间判断是采集侧错了还是编码侧乱了。2.2 音频编码的“帧长”陷阱AAC编码通常是以1024个采样点为一个Frame。但是当你把PCM喂给AAC编码器比如FAAC, FDK-AAC时为了算法预读Lookahead编码器通常会有几帧的输出延迟。也就是说你喂进去第1个Buffer编码器可能吐出来0个字节。喂进去第2个吐出来第1个的包。典型错误开发者忽略了这个Lookahead delay直接把输入PCM的时间戳拷贝给了输出的AAC包。结果所有的音频都早了几十毫秒取决于Lookahead的大小。修正必须维护一个FIFO队列来存时间戳。进编码器把PCM的时间戳Push进队列。出编码器如果拿到了有效数据包Pop一个时间戳出来赋给它。注意如果编码器内部有Buffer你需要加上这个固定的算法延时补偿。2.3 封装Muxing时的基准转换好了现在你手里有了H.264裸流和AAC裸流你要把它们封装成MP4或者TS流推流。这时候涉及到Timebase时间基的转换。采集和内核层通常是纳秒ns或者微秒us。MP4封装通常Video Track是90000 timescale为了兼容30fps, 60fps, 24fps等整除Audio Track是采样率如44100。FLV/RTMP毫秒ms。检查清单确认FFmpeg或你是用的Muxer库av_packet_rescale_ts调用是否正确。重点检查回绕Wrap-around32位的时间戳在90kHz下跑26小时就会溢出。如果你的设备要7x24小时运行必须处理好溢出回绕逻辑否则播放器会认为时间倒流直接卡死或声画不同步。第三章传输层的迷雾——网络抖动如何篡改“时间”在嵌入式安防或直播领域RTSP/RTCP/RTP 是绝对的主流。很多工程师觉得我只要把包发出去TCP/UDP会负责送到时间戳都在RTP头里写着呢怎么会乱幼稚。网络传输是音画同步最大的“毁容现场”。3.1 关键帧I帧的“突发”噩梦你做过网络抓包分析吗如果你用 Wireshark 抓一下海思或瑞芯微编码出来的H.264流你会发现一个可怕的现象I帧的体积巨大。一个P帧可能只有2KB而一个I帧可能有100KB。 当编码器吐出这100KB数据时如果你的网络发送线程Send Thread写得太“老实”它会尝试在一个极短的时间内比如1ms把这100KB全部塞进网卡驱动的队列。后果是什么UDP丢包路由器的缓冲区瞬间被撑爆物理层丢包。网络拥塞这一瞬间的带宽峰值极高挤占了音频包的发送通道。接收端抖动接收端一下子收到了几十个视频包然后是一段长长的静默。这对同步的影响是致命的。音频包是细水长流的视频包是“脉冲式”的。在弱网环境下音频包很容易被夹在巨大的I帧数据洪流中间被“饿死”或者延迟发送。 这就导致了在接收端的Buffer里音频数据来晚了。如果你在接收端没有足够大的 Jitter Buffer抖动缓冲你就会因为没数据而被迫从Buffer里取静音帧Silence或者暂停播放等待。这一等时间轴就滑移了。硬核优化方案平滑发送Pacing不要一拿到编码数据就无脑sendto。你需要一个漏桶算法Leaky Bucket。 在应用层做一个Ring Buffer编码器把数据扔进去。发送线程按照当前估算的带宽均匀地把I帧拆分成小片在33ms假设30fps的时间窗内慢慢发出去。 让网络流量变成一条平滑的直线而不是心电图一样的脉冲。这对保证音频包的及时到达至关重要。3.2 RTCP SR被遗忘的“对表”机制很多自研的RTSP服务器为了图省事根本不发或者乱发 RTCP Sender Report (SR)。这是大忌。RTP头里的时间戳Timestamp是相对的且单位和起始值在Audio和Video之间完全不同Video通常90kHzAudio通常44.1kHz或48kHz且随机起始。 接收端怎么知道视频的PTS1000和音频的PTS500到底是不是同一个时刻靠的就是 RTCP SR。 SR包里包含了一组映射关系RTP TimestampNTP Timestamp (墙上绝对时间)播放器如VLC, ffplay收到SR包后会建立一个线性回归模型计算出视频流和音频流相对于NTP时间的偏差。如果你的设备不发SR或者NTP时间填得是错的比如全是0播放器只能瞎猜。它可能会根据收到第一个包的本地时间来强行对齐这在网络有延迟波动时就是一场灾难。排查命令用 Wireshark 过滤rtcp。 点开 Sender Report检查NTP Timestamp和RTP Timestamp。 如果你发现 NTP 时间和你系统的date对不上或者 Video 和 Audio 的 NTP 时间差值巨大赶紧去修你的 RTSP Server 代码。3.3 TCP 下的队头阻塞Head-of-Line Blocking现在很多场景为了穿透防火墙不得不使用 RTSP over TCP。 TCP 是可靠传输丢包了会重传。这对画质是好事对同步是坏事。假设一个视频P帧丢失了。TCP 协议栈会卡住后续所有的包包括那个可能已经到达的、急需播放的音频包如果你们复用同一个socket通道的话直到那个丢失的视频包重传成功。 这时候播放器端的数据流会突然“断流”。 等到重传成功一堆数据瞬间涌入。如果播放器的逻辑不够健壮比如 FFmpeg 的ffplay默认逻辑它可能会为了追赶进度快速消耗缓冲区导致音画加速鬼畜效果或者直接丢弃过期的音频包导致时间轴错乱。建议在嵌入式设备资源允许的情况下尽量保持 Audio 和 Video 使用独立的TCP Connection 或者 Channel。这样视频丢包重传卡顿画面卡住不会影响音频的传输声音继续播。画面卡一下还能接受声音卡顿或声画不同步是绝对无法忍受的。第四章播放器的终极仲裁——渲染延迟的黑洞好了数据历经九九八十一难终于到了播放器Decoder Renderer。这是你作为嵌入式开发者的“不可控区域”如果是做推流设备或者是你的“主战场”如果是做解码终端。在这里所有的同步问题都会最终爆发。4.1 谁是老大—— Master Clock 的选择在播放器内部必须有一个主时钟Master Clock。所有的流都要盯着它看快了就等慢了就跳。FFmpeg 提供了三种策略Audio Master (默认/推荐)以音频播放进度为基准。视频去追音频。Video Master以视频播放进度为基准。音频去追视频。External Clock以系统时间为基准。为什么 99% 的播放器都选 Audio Master因为人耳比人眼难伺候得多。视频重复一帧或丢一帧用户可能觉得只是“卡了一下”。音频如果为了同步去拉伸变调或填充静音爆音用户会瞬间觉得“这设备烂透了”。音频硬件的 Buffer 消耗是非常稳定的由晶振决定天然适合做时钟源。同步算法核心逻辑伪代码double current_audio_pts get_audio_clock(); // 当前声音播到哪了 double current_video_pts frame-pts; double diff current_video_pts - current_audio_pts; if (diff SYNC_THRESHOLD) { // 视频太快了比音频早到了 // 策略让显示线程睡一会儿等音频追上来 delay_display(diff); } else if (diff -SYNC_THRESHOLD) { // 视频太慢了音频已经播到前面去了 // 策略丢弃当前视频帧直接解下一帧Catch up drop_frame(); } else { // 在允许误差范围内比如 /- 10ms render_frame(); }4.2 隐形的杀手渲染路径延迟Presentation Delay这是本章最干货的部分。绝大多数人死在这里。当你调用get_audio_clock()时你以为得到的是耳朵听到的声音时间吗错你得到的是“你把数据塞给音频驱动”的时间。在嵌入式 Linux (ALSA) 或 Android (AudioFlinger) 中从你写入数据到喇叭发出声音中间隔着Audio Server Buffer(PulseAudio/AudioFlinger)Kernel Driver Ring Buffer(DMA)DAC Hardware Buffer(Codec芯片内部)这个链路可能有50ms ~ 200ms的延迟同样视频那边SurfaceFlinger / Composer(排队合成)Display Hardware Buffer(FrameBuffer)HDMI/Panel Link(传输)TV/Monitor Internal Processing(电视机的画质引擎处理可能有100ms延迟)经典的“音画不同步”场景你的代码逻辑完美diff计算为 0。但是音频通路总延迟200ms视频通路总延迟50ms结果用户听到声音比画面晚了 150ms。口型先动声音后出。怎么解你必须校准这个HW_Latency。 大部分播放器允许你设置一个Audio Delay或Video Delay的全局 Offset。实战校准法低成本土办法你需要一台高帧率手机现在的 iPhone 或安卓旗舰都支持 240fps 慢动作拍摄。做一个测试视频黑屏每秒闪一次白光同时发出一声“滴”。在你的设备上播放。用手机慢动作拍摄屏幕和声音。在视频编辑软件里逐帧数。看“白光亮起”的那一帧和波形图上“波峰出现”的那一刻差了多少毫秒。这个差值就是你的系统固有的渲染时差。你必须在代码里硬编码把这个差值补回去通常是推迟视频渲染或者让音频先跑一会再开始播视频。4.3 动态频漂矫正The Drift Compensation回到我们第一章提到的晶振问题。 如果发送端音频采样率是 48005Hz接收端声卡是 48000Hz。 每秒钟接收端的 Buffer 就会积压 5 个 Sample。 一分钟积压 300 个。 十分钟积压 3000 个约 62ms。随着播放时间拉长音频 Buffer 里的数据越来越多声音播放就会越来越滞后于当前的时间戳。最终导致 Buffer 溢出Overrun爆音然后重置循环往复。高级播放器如 WebRTC 的 NetEQ的做法监控 Buffer 的水位Water Level。如果水位持续上涨说明发得快播得慢。启用 WSOLA 算法或简单的重采样把音频压缩着播加速播放但不变调。如果水位持续下降说明发得慢播得快。把音频拉伸着播减速播放。在嵌入式设备资源受限时搞不定 WSOLA 怎么办简单粗暴法监控 Buffer 水位。如果积压超过阈值比如 200ms直接悄悄丢掉 10ms 的数据最好在静音段丢。虽然会有一点点听感瑕疵但比音画不同步要强一百倍。第五章实战排查手记——定位问题的“手术刀”理论讲完了现在给你一套我们内部使用的排查流程表Checklist。当你下次遇到 Bug 时照着这个表一项一项打勾。5.1 只有特定文件不同步还是所有流都不同步如果是特定文件扔进MediaInfo看一下。是不是 Variable Frame Rate (VFR)很多嵌入式播放器对 VFR 支持极差把它当 CFR固定帧率播必然不同步。解决转码成 CFR或者修复播放器的 PTS 累加逻辑。5.2 是起播就不同步还是越播越不同步起播就不同步且偏差固定嫌疑人采集端的初始打标 offset或者渲染端的 HW_Latency 没校准。排查调整播放器的全局 Audio Delay 参数看能不能人为对齐。如果能就是固定延迟问题。越播越不同步Drift嫌疑人晶振采样率不匹配或者丢帧后 PTS 没修正。排查检查播放日志。看Video Buffer是否经常空看Audio Buffer水位是否在缓慢爬升如果是说明音视频生成速率不匹配。5.3 甚至“回声”和“鬼影”如果在会议模式RTC你听到了回声这通常不是同步问题是AEC回声消除失效。但要注意AEC 的前提是严格的音画同步参考信号对齐。如果你的参考信号Reference Signal给晚了AEC 就消不掉回声。所以回声问题往往也是同步问题的一个副作用。5.4 必杀技嵌入式日志埋点不要只看 Logcat 或者是 dmesg。你需要自己在关键节点埋点。 我建议在代码里定义一个结构体记录每个 Frame 流转全生命周期的时刻struct FrameTrace { int64_t capture_ts; // 采集时刻 int64_t encode_done_ts;// 编码完成时刻 int64_t mux_ts; // 封装时刻 (PTS) int64_t recv_ts; // 接收时刻 int64_t decode_done_ts;// 解码完成时刻 int64_t render_ts; // 渲染时刻 };挑几帧典型数据打印出来画一个甘特图。你一眼就能看出时间到底被哪只“怪兽”吃掉了。是编码器卡了 100ms还是网络堵了 500ms还是渲染队列里积压了 200ms数据不会撒谎只有直觉会骗人。第六章蓝牙的黑洞——当声音由于物理协议“迟到”现在哪个嵌入式设备不带蓝牙耳机、车载、智能音箱。但对于做音画同步的人来说蓝牙就是个巨大的、不可预测的延时黑洞。你可能把板子上的延时优化到了极致做到了 50ms 以内的超低延时。但是用户连上一个几十块钱的某宝爆款蓝牙耳机瞬间延时飙升到 300ms 甚至 500ms。用户骂的是谁骂的是你的产品垃圾。6.1 A2DP 的先天残疾传统的蓝牙立体声协议A2DP为了保证传输不断连设计了极大的缓冲机制。音频数据从你的应用层出来经过 PulseAudio/AudioFlinger进入蓝牙协议栈BlueZ 或 Fluoride再经过基带Baseband最后空口传输到耳机。耳机那边收到后还要进 Jitter Buffer再解码SBC/AAC/LDAC最后播放。这整个链路根本不在你的控制范围内。而且每款耳机的 Buffer 大小都不一样有的耳机为了信号稳定缓存了 200ms 的数据有的只缓存 50ms。怎么救方法一协议栈的反馈靠谱但难做现代的蓝牙协议AVRCP 1.3支持一个功能叫 Delay Reporting。耳机是可以注意是“可以”不是“必须”告诉手机/设备“哥们我的初始延时大约是 150ms。”如果你的嵌入式 Linux 的蓝牙协议栈支持解析这个字段你可以拿到这个值然后动态调整视频的 PTS把画面往后推迟 150ms。但现实是骨感的大部分廉价蓝牙耳机根本不发这个 Report或者发个假的固定值。方法二人肉经验值最常用做一张白名单。检测到蓝牙连接时默认给一个 200ms 的视频延时。这虽然不精确但至少能把 400ms 的差异缩减到 200ms让“口型完全对不上”变成“稍微有点不跟手”。对于看电影来说这通常是可以接受的。方法三强制切换编码如果你的场景是游戏或实时通话A2DP 根本没法用。你必须强制切到 HFP/HSP (Hands-Free Profile) 或者使用 aptX-LL (Low Latency)。HFP 的延时极低通常 60ms因为它是为打电话设计的牺牲了音质8kHz/16kHz 单声道换取了速度。在嵌入式设备上你可能需要在应用层检测场景用户看电影保持 A2DP加视频延时缓冲。用户开黑语音暴力切到 HFP音质渣就渣点但声音能对上。6.2 绝对时间在无线链路的失效在蓝牙链路中不要指望还能用 NTP 或者系统时间来对齐。无线干扰会导致重传Retransmission。一旦发生重传蓝牙控制器会暂停发送新数据优先重传旧包。这会导致瞬间的音频堆积。调试陷阱你发现在蓝牙干扰严重比如微波炉旁边时视频还在流畅播声音却断断续续然后突然快进Catch-up。这是因为底层的 FIFO 被塞满了。解决思路不要死磕 PTS。在这种恶劣链路下丢包优于等待。改写你的蓝牙 Sink 模块逻辑如果检测到协议栈的 Write Buffer 满了EAGAIN不要阻塞等待直接丢弃这段 PCM 数据并且重置内部的时间戳计数器。即使听起来会有“咔哒”一声也比后面十秒钟的音画不同步要强。第七章榨干性能——低端芯片上的“削肉”战术如果不幸你的老板让你在一颗几年前的、主频只有 800MHz 的低端 ARM 芯片上跑 1080p H.264 解码还要保证音画同步。这时候标准的 FFmpeg 流程Read - Demux - Decode - Convert - Render太重了。每一次内存拷贝Memcpy都是在给同步挖坑。7.1 Zero-Copy零拷贝是救命稻草在低端芯片上memcpy占用的 CPU 周期可能高达 30%。CPU 忙着搬砖调度就会延迟音画同步线程就得不到及时的执行时间片。你必须打通从解码器到渲染器的“直通车”。Linux/Android: 使用 DMA-BUF (Ion/Dmabuf heap)。解码器VPU解出来的帧直接就在物理连续内存里。把这个内存的文件描述符fd直接传给 DRM/KMS 显示子系统或者传给 OpenGL ES 的 EGLImage。中间连哪怕一次 yuv2rgb 的软转换都不要做。音频侧 使用 mmap 直接写 ALSA 的环形缓冲区Ring Buffer而不是用 snd_pcm_writei。减少一次用户态到内核态的拷贝。7.2 优先级反转Priority Inversion的坑这是一个极难复现的同步 Bug。现象系统空闲时同步很好。一旦后台有个 Log 写入或者文件解压任务音画就开始飘。原因 音频播放线程的优先级不够高或者被低优先级的锁给卡住了。在嵌入式 Linux 上你必须手动调整线程调度策略。不要用默认的 SCHED_OTHER。把你的音频渲染线程和视频同步逻辑线程设置为 SCHED_FIFO 或 SCHED_RR实时调度类并给一个较高的优先级比如 50-90。# 哪怕你不会写代码也可以在 shell 里用 chrt 救急 chrt -f -p 90 AudioThreadPID但是小心如果你的实时线程里写了个死循环整个系统会直接 Lockup连 shell 都进不去。7.3 砍掉 Jitter Buffer对于低延时场景如对讲机、无人机图传标准的“缓冲 200ms 以抗抖动”过于奢侈。你要做的是动态缩放 Buffer。激进策略设定目标 Buffer 只有 20ms。一旦网络抖动导致 Buffer 空了立刻重复上一帧音频PLC, Packet Loss Concealment或者插入舒适噪音Comfort Noise。一旦 Buffer 稍微多了点比如积压了 40ms立刻丢弃一帧数据。这种策略会让音质听起来有点毛刺但能确保留在用户耳朵里的声音永远是最“新鲜”的。对于指挥调度类产品实时性 音质。第八章终局之战——那些让你怀疑人生的“鬼影”Bug在专栏的最后我分享两个我亲身经历的、足以写进教科书反面教材的 Bug。这都是真实发生的希望能给你省下几个通宵的时间。案例一由于“闰秒”引发的血案现象某款监控设备运行非常稳定。但在某年某月某日的早上 8 点全球几万台设备同时出现音画不同步偏差正好 1 秒。重启后恢复。原因我们的代码里用了 NTP 更新系统时间。那天国际地球自转服务IERS插入了一个闰秒。Linux 内核处理闰秒的方式多种多样有的版本是让时间“停”一秒有的是回退一秒。我们的应用层逻辑current_time get_system_time()pts_diff current_time - frame_pts当系统时间突然“跳变”了 1 秒而采集端的单调时钟Monotonic没变。计算出来的 pts_diff 瞬间这就错乱了。播放器以为视频发早了拼命等待导致画面卡住 1 秒而音频继续播。从此音画差了 1 秒。教训永远、永远不要用 CLOCK_REALTIME墙上时间来做音画同步计算。只能用 CLOCK_MONOTONIC 或 CLOCK_BOOTTIME。因为它们不会受到用户修改时间、NTP 校时、闰秒的影响。案例二32位整型的溢出回旋镖现象设备连续运行13.25 小时后音画必定不同步且音频直接静音。排查为什么是 13.25 小时计算器按一下$13.25 \times 3600 \times 1000 47,700,000$ 毫秒。好像也没溢出 32 位整数啊再深挖音频驱动。发现音频的时间戳是以采样点数Frames 计数的。如果是 96kHz 采样率.等一下无符号 32 位整数的最大值是 4,294,967,295。溢出了当采样计数器溢出归零后应用层拿到的音频时间戳突然从“巨大”变成了“0”。而视频时间戳还在几十亿上跑。同步逻辑判断音频落后视频几十亿毫秒。策略疯狂加速播放音频以追赶视频。结果音频驱动被喂得太快直接挂死。教训在涉及时间戳计算时全部强制使用 64 位整数int64_t / uint64_t。不要存在侥幸心理。嵌入式设备往往是 7x24 小时运行的任何 32 位的计数器最终都会变成定时炸弹。