16、RTMP推流实现:librtmp集成、连接与发布、音视频数据推送、断线重连
RTMP推流,说白了就是把手机摄像头采集到的画面和麦克风录到的声音,实时地推到服务器上去。很多初学者觉得这步很神秘,其实拆开来看,无非就是三板斧:连上服务器、塞数据、断了重连。我最早做直播项目时,光在断线重连这块就踩了无数坑,今天咱们一次性聊透。
16.1 librtmp的集成方式
librtmp是RTMP协议最经典的C语言实现库。Android项目里集成它,我习惯用预编译so的方式,省心。
具体步骤很简单:
- 从rtmpdump项目里把源码拉下来,交叉编译出arm64-v8a和armeabi-v7a的so
- 把so丢进
app/src/main/jniLibs/对应目录 - 头文件放到
src/main/cpp/include里
CMakeLists.txt里这样写:
cmake_minimum_required(VERSION 3.10)
project("rtmp_push")
add_library(rtmp SHARED IMPORTED)
set_target_properties(rtmp PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/librtmp.so)
include_directories(include)
add_library(native-lib SHARED native-lib.cpp)
target_link_libraries(native-lib rtmp)
嗯,这里要注意:librtmp依赖openssl和zlib,编译时记得一起编进去。我遇到过有人只编了librtmp,运行时直接崩溃,查了半天才发现是openssl没链上。
16.2 RTMP连接与发布流程
连接服务器,说白了就是三步走:
- RTMP_Connect:建立TCP连接
- RTMP_ConnectStream:建立流连接
- RTMP_EnableWrite:开启写入模式(发布流)
核心代码长这样:
RTMP *rtmp = RTMP_Alloc();
RTMP_Init(rtmp);
// 设置推流地址
const char *url = "rtmp://live.example.com/live/streamkey";
RTMP_SetupURL(rtmp, (char *)url);
// 开启写入模式,表示我们是发布者
RTMP_EnableWrite(rtmp);
// 建立连接
if (RTMP_Connect(rtmp, NULL) == FALSE) {
// 连接失败,释放资源
RTMP_Free(rtmp);
return -1;
}
// 连接流
if (RTMP_ConnectStream(rtmp, 0) == FALSE) {
RTMP_Close(rtmp);
RTMP_Free(rtmp);
return -1;
}
这里有个坑:RTMP_SetupURL内部会解析URL,如果地址里带了签名参数,它可能会解析失败。我建议在Java层就把URL拼好,不要依赖librtmp去处理复杂参数。
16.3 音视频数据推送
连接成功后,剩下的就是往服务器塞数据了。RTMP推送数据包的核心函数是RTMP_SendPacket。
视频数据推送(H.264):
// 构造视频包
RTMPPacket packet;
RTMPPacket_Reset(&packet);
// 设置包类型为视频
packet.m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet.m_nChannel = 0x04; // 视频通道
packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
packet.m_nTimeStamp = timestamp; // 相对时间戳,单位毫秒
packet.m_nInfoField2 = rtmp->m_stream_id;
packet.m_hasAbsTimestamp = FALSE;
// 填充数据
packet.m_body = (char *)h264_data;
packet.m_nBodySize = data_size;
// 发送
RTMP_SendPacket(rtmp, &packet, FALSE);
音频数据推送(AAC):
RTMPPacket packet;
RTMPPacket_Reset(&packet);
packet.m_packetType = RTMP_PACKET_TYPE_AUDIO;
packet.m_nChannel = 0x05; // 音频通道
packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
packet.m_nTimeStamp = timestamp;
packet.m_nInfoField2 = rtmp->m_stream_id;
packet.m_hasAbsTimestamp = FALSE;
packet.m_body = (char *)aac_data;
packet.m_nBodySize = data_size;
RTMP_SendPacket(rtmp, &packet, FALSE);
m_headerType = RTMP_PACKET_SIZE_LARGE,非关键帧可以用RTMP_PACKET_SIZE_MEDIUM。这样做能减少一些带宽开销。我优化过的一个项目,光改这个就省了15%的流量。
16.4 音视频数据推送的时序控制
你想想看,如果音视频数据一股脑全塞给librtmp,服务器那边肯定乱套。所以时序控制很重要。
我的做法是:
- 视频按照帧率(比如25fps)每40ms推送一帧
- 音频按照采样率(比如44100Hz)每23ms推送一帧
- 音视频时间戳要同步,以音频时间为基准
实际项目中,我会用一个单独的推送线程,从队列里取数据:
void *push_thread(void *arg) {
while (is_pushing) {
// 从队列取一帧数据
Frame *frame = dequeue_frame();
if (frame == NULL) {
usleep(5000); // 5ms轮询一次
continue;
}
// 根据时间戳判断是否该推送
int64_t now = get_current_time_ms();
if (frame->timestamp > now) {
// 还没到推送时间,放回去
enqueue_frame(frame);
usleep(1000);
continue;
}
// 推送
if (frame->type == VIDEO_FRAME) {
send_video_frame(frame);
} else {
send_audio_frame(frame);
}
free_frame(frame);
}
return NULL;
}
16.5 断线重连机制
做直播最怕什么?推着推着断了,观众那边直接黑屏。断线重连是必须的。
我的重连策略是这样的:
- 检测到
RTMP_SendPacket返回FALSE,或者RTMP_IsConnected返回FALSE - 立即关闭当前连接,释放资源
- 等待1秒后尝试重连
- 重连失败,等待时间翻倍(2秒、4秒、8秒...),最多30秒
- 重连成功后,先发送音视频序列头(sps/pps和音频配置),再继续推流
代码实现:
int reconnect(RTMP **rtmp, const char *url) {
int retry_count = 0;
int max_retry = 5;
int delay = 1; // 初始延迟1秒
while (retry_count < max_retry) {
// 关闭旧连接
if (*rtmp != NULL) {
RTMP_Close(*rtmp);
RTMP_Free(*rtmp);
*rtmp = NULL;
}
// 等待
sleep(delay);
// 重新连接
*rtmp = RTMP_Alloc();
RTMP_Init(*rtmp);
RTMP_SetupURL(*rtmp, (char *)url);
RTMP_EnableWrite(*rtmp);
if (RTMP_Connect(*rtmp, NULL) && RTMP_ConnectStream(*rtmp, 0)) {
// 重连成功
return 0;
}
// 重连失败,延迟翻倍
retry_count++;
delay = min(delay * 2, 30);
}
return -1; // 重连失败
}
16.6 整体架构图
下面这张图展示了RTMP推流的完整流程:
16.7 避坑指南
最后,我把这些年踩过的坑总结一下:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| RTMP_Connect返回成功但推流失败 | 服务器收不到数据 | 检查是否调用了RTMP_EnableWrite,发布流必须开启写入模式 |
| 推流一段时间后卡死 | RTMP_SendPacket阻塞 | 网络波动导致,加超时机制,超时后主动断开重连 |
| 重连后花屏 | 画面全是马赛克 | 重连后必须重新发送sps/pps和音频配置帧 |
| 内存持续增长 | OOM崩溃 | 队列设置最大长度,超过则丢弃非关键帧 |
RTMP推流这块,说白了就是跟网络较劲。网络好的时候一切顺利,网络一波动各种问题都来了。但只要你把连接、推送、重连这三个环节处理好,大部分问题都能解决。我曾经在一个弱网环境下测试,重连策略调优后,推流成功率从70%提升到了95%以上。
嗯,今天就聊到这儿。代码都在上面了,照着写就能跑起来。遇到问题别慌,先检查日志,八成是连接参数或者时序控制的问题。