27、直播推流场景:音频采集与编码、RTMP推流延时优化、硬件编码器 vs 软件编码器

直播推流,说白了就是一场与时间的赛跑。你这边采集到声音,那边观众就要听到。中间但凡多卡个几百毫秒,弹幕里就开始刷「卡了卡了」。我在做某头部直播平台的低延时方案时,最深的一个体会就是:音频链路上的每一毫秒,都是抠出来的

今天我们就来拆解直播推流场景下的音频延时问题。我会从采集、编码、再到RTMP推流,把每个环节的坑和优化手段都过一遍。最后再聊聊硬件编码器和软件编码器到底该怎么选——嗯,这个问题其实挺多人问我的。

音频采集:第一关的延时陷阱

很多人以为采集就是调个麦克风权限那么简单。其实不然。Android的音频采集链路里,藏着好几个延时大户。

核心要点:采集延时的关键参数是 bufferSize采样率。buffer越小,延时越低,但CPU压力越大,也越容易爆音。

我个人习惯用 AudioRecord 配合 READ_NON_BLOCKING 模式。为什么?因为阻塞模式下,如果系统调度不及时,你的采集线程会被卡住,直接导致音频断流。这在直播场景里是致命的。

// 推荐的低延时采集配置
int bufferSize = AudioRecord.getMinBufferSize(44100, 
        AudioFormat.CHANNEL_IN_MONO, 
        AudioFormat.ENCODING_PCM_16BIT);
// 注意:不要直接用getMinBufferSize的返回值,它往往偏大
// 我一般会在此基础上除以2,然后做2的幂次对齐
int optimizedBuffer = 512; // 实测44100Hz下,512样本点约11.6ms
AudioRecord recorder = new AudioRecord(
        MediaRecorder.AudioSource.MIC,
        44100,
        AudioFormat.CHANNEL_IN_MONO,
        AudioFormat.ENCODING_PCM_16BIT,
        optimizedBuffer * 2 // 双缓冲,防止溢出
);
recorder.startRecording();

我曾经踩过的坑:在部分骁龙665机型上,设置过小的buffer会导致采集线程频繁被优先级更高的GPU任务抢占,出现周期性「啵啵」声。后来我加了一个自适应buffer机制——如果连续3次采集超时,自动将buffer翻倍,直到稳定为止。

音频编码:延时与质量的博弈

采集完的PCM数据,不能直接推流。你得编码。编码器选什么?这里就有讲究了。

直播场景下,我推荐使用 AAC-LC 编码。为什么不是HE-AAC?因为HE-AAC虽然压缩率更高,但它的编码延时通常在200ms以上,而AAC-LC可以做到20-30ms。你想想看,观众那边等半秒才听到声音,体验能好吗?

编码格式 典型延时 适用场景 我的建议
AAC-LC 20-30ms 直播推流 首选,延时可控
HE-AAC v1/v2 200-300ms 点播、低码率场景 直播慎用
Opus 5-20ms 实时通话 延时最低,但RTMP支持差
MP3 40-60ms 兼容性优先 不推荐,编码质量不如AAC

编码器的配置上,有一个参数容易被忽略——帧大小。AAC编码默认每帧1024个样本点,在44100Hz下就是23.2ms。如果你用48000Hz采样,那就是21.3ms。这个值基本决定了编码延时的下限。

小技巧:如果你用MediaCodec做AAC编码,记得设置 KEY_BIT_RATE_MODEBITRATE_MODE_CBR(恒定码率)。VBR模式下,码率波动会导致推流buffer忽大忽小,间接增加延时。

RTMP推流:延时优化的核心战场

采集和编码搞定了,接下来就是推流。RTMP协议本身是基于TCP的,这就意味着它天生有重传机制。网络一抖,重传一来,延时就上去了。

我当年优化RTMP推流延时,主要做了三件事:

  1. 调整GOP大小:GOP(关键帧间隔)越大,延时越高。直播场景我一般设到1-2秒。也就是每1-2秒一个关键帧。太频繁了带宽扛不住,太稀疏了拉流端起播慢。
  2. 控制发送buffer:RTMP内部有一个发送缓冲区,默认可能8KB甚至更大。我习惯把它压到2KB。buffer越小,数据越及时发出,但也要小心网络抖动导致丢包。
  3. 启用Nagle算法优化:TCP的Nagle算法默认会把小包合并成大包再发送,这对音频流来说是灾难。一定要用 TCP_NODELAY 禁用Nagle。
// RTMP推流时的socket优化示例
Socket socket = new Socket();
socket.setTcpNoDelay(true); // 禁用Nagle算法
socket.setSendBufferSize(2048); // 发送缓冲区压到2KB
// 注意:不要设置接收缓冲区,推流端不需要收数据

一个容易被忽视的点:音频和视频的PTS(显示时间戳)必须严格对齐。如果音频PTS比视频快,播放器会缓冲音频等视频,导致音画不同步。我见过一个项目,就是因为音频采集时钟和视频采集时钟不同源,导致推流后延时越来越严重。

硬件编码器 vs 软件编码器:到底选哪个?

这个问题几乎每次技术评审都会被问到。我的回答是:看场景,没有绝对的好坏

先说说各自的优缺点:

对比维度 硬件编码器 软件编码器
延时 低(通常5-15ms) 较高(20-50ms,取决于CPU)
CPU占用 极低(专用DSP处理) 高(占用主核)
编码质量 一般(厂商实现参差不齐) 高(可精细调参)
兼容性 差(不同芯片差异大) 好(纯代码实现)
功耗 高(手机发烫)

我个人习惯这样选:

  • 旗舰机型(骁龙8系、天玑9系):优先用硬件编码器。这些芯片的DSP性能强,延时低,画质也过得去。
  • 中低端机型:如果硬件编码器质量太差(比如出现马赛克、色块),果断切软件编码器。宁可多耗点电,也不能让观众骂画质糊。
  • 实时性要求极高的场景(比如连麦):必须硬件编码器。软件编码器的延时在低端机上可能飙到100ms以上,没法用。

我曾经踩过的坑:某次在MTK平台上用硬件编码器,发现编码出来的AAC音频每隔几秒就出现一个「噗」的杂音。查了两天才发现是硬件编码器的码率控制算法有bug,在低码率下会周期性溢出。最后只能切回软件编码器,并给MTK提了bug。

整体延时链路图

下面这张图,是我整理的一个典型直播推流音频链路。每个环节的延时我都标出来了,你可以对照着看自己的瓶颈在哪。

直播推流音频延时链路 音频采集 延时:5-15ms 前处理(降噪/AGC) 延时:2-5ms AAC编码 延时:20-30ms RTMP推流 延时:50-200ms 各环节优化手段 采集优化 • 减小bufferSize • 使用非阻塞模式 • 自适应buffer机制 编码优化 • 选AAC-LC格式 • 固定帧大小1024 • CBR码率模式 推流优化 • 禁用Nagle算法 • 减小发送buffer • GOP设为1-2秒 编码器选择 • 旗舰机:硬件 • 低端机:软件 • 连麦:硬件 总延时 = 采集(5-15ms) + 前处理(2-5ms) + 编码(20-30ms) + 推流(50-200ms) 优化目标:将总延时控制在 100ms 以内,达到「无感」直播体验

实战中的一些碎碎念

最后,分享几个我在项目中积累的经验:

  • 不要迷信「低延时」模式:有些厂商的硬件编码器有「低延时模式」,但开启后画质会明显下降。我建议你先关掉,实测延时如果超过50ms再考虑开启。
  • 音频和视频的编码器要分开配置:音频用硬件编码器,视频用软件编码器,这种混搭方案在某些机型上会导致PTS不同步。最好统一用硬件或统一用软件。
  • 推流地址的DNS解析也会引入延时:我遇到过因为DNS解析慢,导致推流启动延迟了300ms的情况。建议提前做DNS预解析,或者直接用IP地址推流。

一个小工具推荐:dumpsys media.audio_flinger 可以查看当前音频链路的延时情况。我在调优时经常用这个命令来验证优化效果。

好了,关于直播推流场景下的音频延时优化,今天就聊这么多。说白了,就是每个环节都抠一点,积少成多,最后才能给观众一个流畅的体验。嗯,希望这些经验对你有用。


公众号:蓝海资料掘金营,微信deep3321