13、直播推流基础:RTMP协议、FLV封装、H.264/AAC裸流打包、推流SDK集成

做直播推流,说白了就是把手机摄像头采集的画面和麦克风采集的声音,实时地送到服务器上去。这个过程听起来简单,但里面涉及的协议、封装、编码打包,每一个环节都可能让你翻车。我最早接触推流时,以为只要调个SDK就完事了,结果连RTMP握手都搞不明白,画面卡得跟幻灯片似的。

这一章,咱们就把推流这条链路拆开来看。从RTMP协议怎么建立连接,到FLV怎么封装音视频数据,再到H.264和AAC裸流怎么打包成FLV Tag,最后再聊推流SDK的集成思路。嗯,内容不少,但都是实战中绕不开的硬骨头。

直播推流核心链路 音视频采集 H.264 / AAC 编码 FLV 封装打包 RTMP 推流 Camera + AudioRecord MediaCodec 硬编 FLV muxer RTMP 客户端 底层传输:TCP(长连接) 关键:音视频同步、GOP 缓存、丢包重传策略 常见坑:首帧延迟、音频卡顿、FLV 时间戳回绕

13.1 RTMP协议:直播推流的“老大哥”

RTMP(Real-Time Messaging Protocol)是Adobe搞出来的协议,虽然年头不短了,但到今天依然是国内直播推流的主流选择。为什么?因为它基于TCP,能保证数据不丢包,而且延迟可以控制在1-3秒内,对于互动直播来说够用了。

RTMP的核心是“握手 + 连接 + 流”。握手阶段交换版本号和窗口大小,连接阶段建立NetConnection,流阶段才是真正的音视频数据传输。我刚开始调试时,经常卡在握手这一步,后来发现是AMF编码格式没对齐——RTMP用AMF0或AMF3来封装控制消息,服务器和客户端必须一致。

RTMP握手流程(简化版)

  • C0: 客户端发送版本号(0x03)
  • S0: 服务器回复版本号
  • C1/S1: 交换随机数据(1536字节)
  • C2/S2: 确认对方数据
  • 之后进入 NetConnection 连接阶段

握手完成后,客户端发送 connect 命令,带上应用名(比如 live)和流密钥。服务器回复 _result 表示成功。然后创建 NetStream,开始推流。嗯,这里要注意:RTMP的流ID是动态分配的,别写死。

个人经验:我在项目中遇到过RTMP推流偶尔断连的问题,排查后发现是TCP keepalive没设置。Android默认的TCP超时时间很长,一旦网络波动,RTMP连接就断了。我建议在推流前设置Socket的keepalive为true,并缩短探测间隔。

13.2 FLV封装:轻量级的容器格式

FLV(Flash Video)是Adobe的另一个作品。它结构简单,解析快,非常适合直播场景。RTMP传输的数据,本质上就是FLV格式的流——只不过去掉了FLV的文件头,直接传Tag。

一个FLV文件的结构是这样的:

组成部分 说明
FLV Header 9字节,标识文件类型和版本
PreviousTagSize0 4字节,固定为0
Tag1 (Audio/Video/Script) 包含Tag Header + Tag Data
PreviousTagSize1 4字节,上一个Tag的大小
Tag2 ... 循环直到文件结束

每个Tag由11字节的Header和可变长度的Data组成。Header里最关键的是Tag类型(8=音频,9=视频,18=脚本)和时间戳。时间戳是相对时间,单位毫秒。我踩过的一个坑是:时间戳用int32存储,如果直播时间超过4小时,时间戳会溢出回绕,导致画面错乱。解决办法是用扩展时间戳字段。

注意:FLV的音频Tag和视频Tag是交错存放的。如果你先发了一堆视频Tag,再发音频Tag,播放器会因为没有音频数据而静音。我曾经在调试时发现推流出去只有画面没有声音,查了半天才发现是Tag顺序写错了。

13.3 H.264/AAC裸流打包:把编码数据塞进FLV Tag

编码器出来的H.264数据是NAL Unit流,AAC数据是ADTS帧。要把它们塞进FLV Tag,需要做一层“包装”。

H.264视频Tag打包

H.264的FLV Tag Data分为两种:AVC Sequence Header(配置信息)和AVC NALU(实际帧数据)。

  • AVC Sequence Header:包含SPS和PPS,在推流开始时发送一次。播放器需要先收到这个才能解码。
  • AVC NALU:每个Tag可以包含一个或多个NAL Unit。用4字节的长度前缀来分隔。

我习惯在MediaCodec的INFO_OUTPUT_FORMAT_CHANGED回调中提取SPS和PPS,然后组装成Sequence Header。注意:SPS和PPS的NAL头(0x00 0x00 0x00 0x01)要去掉,换成4字节长度。

// H.264 AVC Sequence Header 结构
// 5字节配置版本 + 1字节SPS个数 + 2字节SPS长度 + SPS数据 + 1字节PPS个数 + 2字节PPS长度 + PPS数据
byte[] sps = ...; // 从MediaCodec获取
byte[] pps = ...;
ByteBuffer header = ByteBuffer.allocate(16 + sps.length + pps.length);
header.put((byte) 0x01); // 版本
header.put(sps[1]);      // AVC profile
header.put(sps[2]);      // profile compatibility
header.put(sps[3]);      // AVC level
header.put((byte) 0xFF); // 6位保留 + 2位nalu长度减1
header.put((byte) 0xE1); // 3位保留 + 5位SPS个数
header.putShort((short) sps.length);
header.put(sps);
header.put((byte) 0x01); // PPS个数
header.putShort((short) pps.length);
header.put(pps);

AAC音频Tag打包

AAC的FLV Tag Data也分两种:AAC Sequence Header(AudioSpecificConfig)和AAC Raw Data。

  • AAC Sequence Header:包含音频编码参数,如采样率、声道数。在推流开始时发送一次。
  • AAC Raw Data:去掉ADTS头的AAC裸数据。注意:FLV要求AAC数据不带ADTS头,只保留原始帧数据。

我在项目中遇到过一个问题:某些播放器对AAC Sequence Header的解析很严格,如果AudioSpecificConfig里的采样率索引写错了,播放器会直接不出声。后来我改用MediaCodec输出的AudioSpecificConfig,不再自己拼,问题就解决了。

避坑指南:我曾经在推流时发现音频有“滋滋”的杂音,排查后发现是AAC帧的PTS没有对齐。视频帧的PTS是整数倍,音频帧的PTS是浮点数累加,如果直接取整,音频和视频的同步就会出问题。我建议音频PTS用double计算,最后再转成int。

13.4 推流SDK集成:从零到一

如果你不想自己手写RTMP协议和FLV打包,可以直接用现成的推流SDK。Android上常用的有:

  • librtmp:C语言写的RTMP客户端库,轻量级,但需要自己处理FLV打包。
  • FFmpeg:集成了RTMP协议和FLV muxer,功能强大,但体积大。
  • 腾讯云/阿里云推流SDK:商业SDK,开箱即用,但依赖厂商服务。

我个人习惯用librtmp + 自己写FLV打包。为什么?因为灵活。你可以控制每一帧的发送时机,可以自定义丢包策略,还可以在推流前做美颜、滤镜等处理。商业SDK虽然方便,但遇到问题你只能等厂商修复。

集成librtmp的步骤大致如下:

  1. 编译librtmp的so库(或者用预编译版本)
  2. JNI层调用RTMP_Connect建立连接
  3. 编码器输出H.264/AAC数据后,组装成FLV Tag
  4. 调用RTMP_SendPacket发送数据
  5. 推流结束时调用RTMP_Close断开连接

关键代码片段(JNI层推流循环)

// 伪代码,实际需要处理线程同步
while (isPushing) {
    Frame frame = frameQueue.take(); // 从编码队列取帧
    RTMPPacket packet = flvMuxer.mux(frame); // 打包成FLV Tag
    RTMP_SendPacket(rtmp, &packet, 0); // 发送
    freePacket(&packet);
}

嗯,这里要注意:推流线程不能和编码线程共用同一个锁,否则容易死锁。我建议用无锁队列(比如ConcurrentLinkedQueue)来传递帧数据,编码线程只管放,推流线程只管取。

警告:不要在UI线程里做任何推流操作。RTMP的发送是阻塞的,一旦网络不好,UI线程会被卡死。我曾经见过有人直接在Activity的onCreate里调RTMP_Connect,结果应用启动就ANR了。

最后聊一下推流的优化。推流最怕什么?卡顿和延迟。卡顿是因为网络带宽不够,延迟是因为缓冲区太大。我常用的策略是:

  • 动态调整视频码率:根据网络状况实时降低或升高码率
  • 设置合理的GOP大小:GOP越大,延迟越高,但压缩率更好。直播一般用1-2秒的GOP
  • 开启B帧:B帧能提高压缩率,但会增加解码延迟。如果对延迟敏感,建议只用P帧

说白了,推流就是一个“在有限带宽下,尽可能保证画质和流畅度”的平衡游戏。没有银弹,只有不断调参和测试。


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