8、RTMP推流核心逻辑:RTMP推流库选择(librtmp)、RTMP连接与发布、发送FLV数据包、推流状态管理
各位同学,欢迎来到第八章。这一章我们正式进入RTMP推流的“发动机舱”。前面几章我们聊了音视频采集、编码、封装,但这些东西最终怎么送到服务器上?靠的就是RTMP协议。
RTMP推流,说白了就是三件事:连上去、发数据、管状态。听起来简单,但实际坑不少。我当年第一次做推流时,以为就是调几个API完事,结果线上用户反馈“推流卡死”、“断连不重连”,排查了整整两天才发现是状态机没处理好。
嗯,咱们今天就把这些核心逻辑掰开揉碎,一个一个讲清楚。
8.1 RTMP推流库选择:为什么选librtmp?
市面上RTMP库不少,比如librtmp、srs-librtmp、nginx-rtmp-module自带的客户端库,甚至有些商业SDK。我个人习惯用librtmp,原因有三:
- 轻量:纯C实现,无依赖,编译后体积很小,适合嵌入到Android Native层。
- 稳定:RTMP规范实现完整,社区验证多年,主流CDN都兼容。
- 可控:源码开放,遇到问题可以直接改底层逻辑,不用等第三方修复。
当然,librtmp也有缺点——它没有内置重连机制,也没有完善的异步回调。所以我们需要自己封装一层状态管理。这个后面会讲。
核心结论:librtmp是RTMP推流的“发动机”,但你需要自己给它配一个“变速箱”和“仪表盘”。
8.2 RTMP连接与发布:三步走
RTMP连接和发布,本质上是一个握手+命令交互的过程。我把它拆成三步:
- 握手(Handshake):客户端和服务器互相确认版本,交换随机数据。
- 连接(Connect):发送connect命令,建立NetConnection。
- 发布(Publish):发送releaseStream、FCPublish、createStream、publish命令,建立NetStream。
代码上,librtmp封装了这些细节。你只需要调用几个函数:
RTMP *rtmp = RTMP_Alloc();
RTMP_Init(rtmp);
// 设置推流地址
RTMP_SetupURL(rtmp, "rtmp://your-server/live/streamkey");
// 设置为推流模式
RTMP_EnableWrite(rtmp);
// 建立连接(内部完成握手+connect+publish)
if (RTMP_Connect(rtmp, NULL) == FALSE) {
// 连接失败处理
return -1;
}
if (RTMP_ConnectStream(rtmp, 0) == FALSE) {
// 流连接失败处理
return -1;
}
这里有个坑:RTMP_ConnectStream 这个函数名容易让人误解。它其实不是“连接流”,而是“发送publish命令并等待服务器确认”。我曾经在调试时以为它只是建立TCP连接,结果发现它阻塞了很长时间——因为服务器在等待我发送FLV数据。
注意:RTMP_ConnectStream 成功后,服务器端会开始接收数据。但此时如果你不立即发送音视频数据,部分CDN会在几秒后断开连接。所以连接成功后,要尽快启动发送线程。
8.3 发送FLV数据包:核心中的核心
连接建立后,剩下的工作就是不断往librtmp的发送缓冲区里塞FLV Tag。RTMP协议底层会把FLV Tag封装成RTMP Chunk,然后通过TCP发送出去。
发送一个FLV Tag的流程:
// 假设你已经有了一个FLV Tag(音频或视频)
// tagData 是Tag数据(不含TagHeader)
// tagType 是0x08(音频)或0x09(视频)
// timestamp 是时间戳(毫秒)
int send_flv_tag(RTMP *rtmp, uint8_t tagType, uint8_t *tagData,
uint32_t dataSize, uint32_t timestamp) {
RTMPPacket packet;
RTMPPacket_Reset(&packet);
packet.m_packetType = tagType; // 0x08 或 0x09
packet.m_nChannel = 0x04; // 音频视频通道
packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
packet.m_nTimeStamp = timestamp;
packet.m_nInfoField2 = rtmp->m_stream_id;
packet.m_nBodySize = dataSize;
packet.m_body = tagData;
return RTMP_SendPacket(rtmp, &packet, FALSE);
}
这里有几个关键点:
- 时间戳:必须是递增的,不能回退。如果编码器出来的时间戳有抖动,你需要自己做平滑处理。
- 通道号:音频视频一般用通道4,控制命令用通道2或3。不要混用。
- 发送频率:不要一次性把所有数据塞进去,要根据时间戳间隔发送。否则服务器会认为你“发送过快”,可能断开连接。
经验之谈:我在项目中遇到过一个问题——推流到一半画面卡住,但音频还在。排查后发现是视频时间戳突然跳变(从1000ms跳到500ms),导致服务器认为视频帧乱序,直接丢弃了后续视频帧。解决方案是在编码器输出层加一个时间戳矫正器,确保时间戳严格单调递增。
8.4 推流状态管理:别让状态机失控
推流不是“一锤子买卖”。网络波动、服务器重启、用户切换网络,都会导致推流中断。你需要一个健壮的状态机来管理推流生命周期。
我常用的状态定义如下:
| 状态 | 说明 | 触发条件 |
|---|---|---|
| IDLE | 空闲状态 | 初始化完成,未开始推流 |
| CONNECTING | 正在连接服务器 | 用户点击“开始推流” |
| CONNECTED | 连接建立成功 | RTMP_Connect返回成功 |
| PUBLISHING | 正在推流 | RTMP_ConnectStream成功,开始发送数据 |
| DISCONNECTED | 连接断开 | 网络异常、服务器断开、用户主动停止 |
| RECONNECTING | 正在重连 | 检测到断开,自动尝试重连 |
状态转换的核心逻辑:
// 伪代码
void on_push_event(PushEvent event) {
switch (current_state) {
case IDLE:
if (event == START_PUSH) {
set_state(CONNECTING);
start_connect_thread();
}
break;
case CONNECTING:
if (event == CONNECT_SUCCESS) {
set_state(CONNECTED);
start_publish();
} else if (event == CONNECT_FAIL) {
set_state(RECONNECTING);
schedule_reconnect();
}
break;
case PUBLISHING:
if (event == NETWORK_ERROR) {
set_state(DISCONNECTED);
stop_sending();
set_state(RECONNECTING);
schedule_reconnect();
} else if (event == USER_STOP) {
set_state(IDLE);
close_rtmp();
}
break;
// ... 其他状态处理
}
}
这里有个容易被忽略的点:重连时不要直接复用旧的RTMP对象。librtmp内部有状态残留,直接重连会导致各种诡异问题。我习惯的做法是:先RTMP_Close,再RTMP_Free,然后重新RTMP_Alloc + RTMP_Init。
避坑指南:我曾经在重连逻辑里偷懒,只调了RTMP_Reconnect(librtmp自带的),结果发现重连成功后发送数据一直报错。后来看源码发现RTMP_Reconnect只是重新建立了TCP连接,但内部的流ID、chunk状态都没有重置。从那以后,我再也不用RTMP_Reconnect了,全部手动重建。
8.5 核心逻辑流程图
下面这张图展示了RTMP推流的完整流程,从初始化到数据发送,再到状态管理。你可以把它当作推流模块的“架构蓝图”。
8.6 总结
RTMP推流的核心逻辑,说白了就是三件事:
- 选对库:librtmp是业界标准,轻量稳定,值得信赖。
- 连对流程:握手→连接→发布,三步缺一不可,每一步都要做错误处理。
- 管好状态:状态机是推流的“大脑”,重连逻辑要彻底,不要留隐患。
嗯,这一章的内容就到这里。代码示例都是我在实际项目中验证过的,你可以直接拿去用。但记住,每个线上环境都不一样,一定要根据自己的场景做适配。