srs-librtmp轻量级RTMP客户端库:编译、推流拉流与踩坑

srs-librtmp轻量级RTMP客户端库:编译、推流拉流与踩坑 简介资源为SRS开源项目中的客户端库srs-librtmp源码包面向需要基于RTMP协议进行推流、拉流或开发直播工具的程序员。库以C实现提供简洁API与标准RTMP兼容能力可对接SRS、Nginx-RTMP、FMS等常见服务器适用于Linux、Windows、macOS。该库源于SRS服务器项目可独立使用覆盖直播推流、拉流、录制以及流元数据分析等典型场景。资源共11个文件包含2个cpp实现、1个h头文件、1个c辅助文件以及vs2010/2015的vcxproj与sln工程文件便于快速构建另有README.md与LICENSE等文档。压缩包仅226KB轻量但结构完整。已有1774人学习。通过阅读源码可深入理解RTMP握手、连接、音视频数据发送等底层细节也可直接复用库能力搭建自己的直播推流或录制工具是学习RTMP协议与C网络编程的实用参考。 做音视频开发的人大多听过 SRS 这个开源流媒体服务器。但很多人没注意到SRS 的源码仓库里其实还藏着一个可以直接拿出来用的客户端库——srs-librtmp。我第一次接触它是在一个嵌入式摄像头网关项目里设备只有 128MB 内存却要把视频流推到自建服务器按 RTMP 协议走。当时第一反应是用 FFmpeg 的 libavformat可一交叉编译依赖库的体积和复杂度立刻把我劝退了。后来翻 SRS 源码时发现trunk/src/libs/srs-librtmp这个独立目录才意识到 SRS 团队早就给这类场景留了后手。这篇文章不打算讲 SRS 服务器怎么搭只专注一件事srs-librtmp 这个客户端库怎么编译、怎么集成、推流和拉流怎么调、实际项目里有哪些坑。无论你是做直播推流、摄像头接入、智能硬件还是只是想搞一个轻量的 RTMP 测试工具这篇都值得看完。1. srs-librtmp的来历与定位为什么需要单独一个客户端库1.1 SRS服务器和srs-librtmp到底什么关系SRSSimple Realtime Server本身是一套流媒体服务器负责接收推流、分发拉流支持 RTMP、HLS、HTTP-FLV、WebRTC 等一堆协议。srs-librtmp 则是 SRS 源码树里的一个子模块路径在trunk/src/libs/srs-librtmp。它不依赖服务器代码也不是服务器的一部分而是一个纯正的 RTMP 客户端库。换句话说srs-librtmp 和 SRS 是同一窝出来的两种生物SRS 是端坐在机房里的那头大象负责收流和放流srs-librtmp 是千千万万跑到设备里去的小蚂蚁负责把音视频数据送到大象嘴边。它管的是客户端这一侧推流出去、把流拉回来就这么简单。虽然名字里带 SRS但它完全可以脱离 SRS 服务器使用推流到 nginx-rtmp、Red5、其他任意兼容 RTMP 的服务器都行。当然和 SRS 配合时体验最好后面我会讲为什么。1.2 它到底适合谁来用我在项目里见过三种典型用法嵌入式设备、摄像头、智能硬件需要推 RTMP 流但不想在设备上背一个庞大的 FFmpeg 重依赖团队用 SRS 搭了直播服务客户端希望和服务器的协议细节完全同源少踩兼容性坑想研究 RTMP 协议但官方规范太长太绕直接读一个几千行的清晰实现比看文档快得多。如果你只是写 PC 端播放器那大可直接用 FFmpeg但如果你是在资源受限的端侧做推流或者需要一个自己能完全掌控的轻量客户端srs-librtmp 就是那个去掉 F1 赛车的引擎只留一台小摩托的选择。2. 编译接入从源码到第一个可运行的Demo2.1 获取源码与目录结构srs-librtmp 不是单独一个仓库而是跟随 SRS 主仓库一起发布。获取方式很简单git clone https://github.com/ossrs/srs.git cd srs/trunk真正要用的核心目录就两个src/libs/srs-librtmp/include/srs_librtmp.h和src/libs/srs-librtmp/src/srs_librtmp.cpp。对整个库本质上就是一个头文件加一个实现文件没有几十个源文件层层依赖对于要移植到业务工程里的场景来说非常友好。建议直接切到 release 分支比如 SRS 4.0、5.0 或 6.0不要用 master 开发分支编译因为开发分支可能随时调整接口今天能编过明天就未必了。2.2 构建方式随SRS一起构建还是单独拿走如果只是想跑 demo最简单的是跟着 SRS 一起构建cd trunk ./configure --with-librtmp make编译完成之后在trunk/src/libs/srs-librtmp/objs/下面会生成静态库和测试程序。SRS 的 Linux 构建体系对常见发行版兼容性都不错。但更常见的真实场景是把 srs-librtmp 当第三方源码直接拿进自己的工程比如放到third_party/srs_librtmp/目录然后让业务代码 include 头文件、链接 cpp 文件。我一般用 CMake 组织add_library(srs_librtmp STATIC third_party/srs_librtmp/src/srs_librtmp.cpp ) target_include_directories(srs_librtmp PUBLIC third_party/srs_librtmp/include ) find_package(OpenSSL REQUIRED) target_link_libraries(srs_librtmp PUBLIC OpenSSL::SSL OpenSSL::Crypto)这里有一个必须注意的点srs-librtmp 的 RTMP 复杂握手默认依赖 OpenSSL。如果你的平台完全没有 OpenSSL编译时会报一堆跟 SHA256、HMAC 相关的错误。解决办法是给 srs_librtmp 配置关闭复杂握手的编译选项但我个人不推荐因为很多公网 RTMP 服务器在握手时要求使用 complex handshake强行用 simple handshake 会连不上。所以在嵌入式平台上建议把 OpenSSL 能装就装上实在不行就换用 mbedTLS 之类的库做适配但工作量会大不少。另外很多人关心 Windows 下能不能用。答案是能。把srs_librtmp.cpp和include目录直接加进 Visual Studio 工程然后链接libssl.lib和libcrypto.lib或使用 vcpkg 拉 OpenSSL就可以跑。SRS 服务器本身主要面向 Linux但 srs-librtmp 这个客户端库并没有绑定操作系统Windows、macOS、Linux、各种小嵌入式系统都能用这也是它作为轻量库的一个重要优势。2.3 验证编译产物最简单的方式编译出库之后先别急着接业务用 srs-librtmp 自带的测试程序验证一把最稳妥。在trunk/src/libs/srs-librtmp/下有现成的示例可以指定一个推流地址和一个播放地址来跑通环回。比如先本地起一个 SRS 服务器然后用测试程序向rtmp://127.0.0.1:1935/live/livestream推流再另起一个测试程序去拉同一路流。如果两边日志都正常说明环境没问题后面接自己的业务代码就会顺手很多。这一步千万别跳。我见过有人跳过验证直接接业务后面排查了半天发现在自己的工程里 OpenSSL 版本冲突白折腾一天。3. 核心API与推流链路拆解真正把流送出去3.1 srs_rtmp_t句柄的生命周期理解srs-librtmp 的 API 设计非常直白大多数接口都围绕一个不透明句柄srs_rtmp_t来操作。你可以把它理解成一个RTMP 会话对象创建、连接、发布、读写、销毁全部围绕它展开。常用的几个核心 API 看这张表就够API作用srs_rtmp_create(url)创建 RTMP 会话句柄解析 url不发起网络连接srs_rtmp_handshake(rtmp)完成 RTMP 握手交换版本和随机数是通信前的第一步srs_rtmp_connect_server(rtmp)发送 connect 命令在服务器上建立应用层连接srs_rtmp_publish(rtmp)发送 publish 命令请求推流权限srs_rtmp_play(rtmp)发送 play 命令请求播放指定流srs_rtmp_write_packet(rtmp, type, ts, data, size)推流侧写一个 FLV 格式的音视频数据包srs_rtmp_read_packet(...)拉流侧读一个 FLV 格式的数据包srs_rtmp_close(rtmp)关闭服务器连接srs_rtmp_destroy(rtmp)彻底销毁句柄释放内存这个 API 顺序本质上就是 RTMP 协议的会话流程。很多人只记 API 不记流程最后调用了半天发现总是超时就是因为顺序搞错了。记住一个口诀先握手后连接先发布后写包用完必须销毁。3.2 推流标准流程为什么是这个顺序推流侧完整的调用顺序是这样srs_rtmp_create(url) - srs_rtmp_handshake(rtmp) - srs_rtmp_connect_server(rtmp) - srs_rtmp_publish(rtmp) - 循环 srs_rtmp_write_packet(...) - srs_rtmp_close(rtmp) - srs_rtmp_destroy(rtmp)这个顺序不是哪个开发拍脑袋定的而是 RTMP 协议本身的规定。握手阶段完成的是底层传输层面的版本协商connect 阶段告诉服务器我要进哪个应用apppublish 阶段告诉服务器我要往这个流名stream发布数据。任何一步跳跃都会导致服务器拒绝或直接断连。有个细节值得提一下srs_rtmp_create传入的 url 格式是rtmp://ip:port/app/stream。app 对应服务器上的应用名stream 是流名。大多数默认配置下SRS 的 app 是 livestream 你自己起比如 livestream 或 room_001。这些信息会在 connect 和 publish 阶段被分别使用如果 url 写错很可能 connect 成功但 publish 失败。3.3 一版可直接参考的最小推流示例下面这段代码是我实际项目里抽出来的简化版用来演示最核心的推流调用。它假设你已经从摄像头或编码器拿到了 H.264 视频帧和 AAC 音频帧#include stdio.h #include unistd.h #include srs_librtmp.h int main(int argc, char** argv) { const char* url rtmp://127.0.0.1:1935/live/livestream; srs_rtmp_t rtmp srs_rtmp_create(url); if (!rtmp) { fprintf(stderr, create rtmp failed\n); return -1; } if (srs_rtmp_handshake(rtmp) ! 0) { fprintf(stderr, handshake failed\n); srs_rtmp_destroy(rtmp); return -1; } if (srs_rtmp_connect_server(rtmp) ! 0) { fprintf(stderr, connect failed\n); srs_rtmp_destroy(rtmp); return -1; } if (srs_rtmp_publish(rtmp) ! 0) { fprintf(stderr, publish failed\n); srs_rtmp_destroy(rtmp); return -1; } // 1. 先发 onMetaDatatype18把编码信息告诉服务器和播放器 char metadata[512] {0}; // 注意这里需要展开成合法的 AMF0 格式 onMetaData // 实际项目中通常由编码参数动态生成不能直接用空数据 srs_rtmp_write_packet(rtmp, 18, 0, metadata, (int)sizeof(metadata)); // 2. 业务循环里拿到一帧视频就写 type9拿到音频就写 type8 while (running) { uint32_t timestamp get_frame_timestamp_ms(); if (got_video_frame) { srs_rtmp_write_packet(rtmp, 9, timestamp, video_data, video_size); } if (got_audio_frame) { srs_rtmp_write_packet(rtmp, 8, timestamp, audio_data, audio_size); } usleep(2 * 1000); } srs_rtmp_close(rtmp); srs_rtmp_destroy(rtmp); return 0; }这段代码看起来很简洁但有一个大坑你必须知道metadata 那个数组我在这里只留了一个空壳。真正跑业务时metadata 必须是一个合法的 AMF0 编码的 onMetaData 消息里面携带了width、height、videocodecid、audiocodecid等关键信息。如果不发这一段很多播放器虽然能拉到流但画面比例、编码格式识别不出来表现就是花屏、无图或者播放失败。所以实际项目里建议用一套成熟的 AMF0 编码函数去构造或者在你熟悉的开源库里拷一份现成的 metadata 生成逻辑。3.4 write_packet里的type参数很多人从这一步开始翻车write_packet的 type 参数对应 FLV 封装的 Tag Type就三种常用的8音频 Tag数据是音频帧通常带音频 Tag Header9视频 Tag数据是视频帧通常带视频 Tag Header18脚本 Tag也就是 metadata以 H.264 视频为例发送一个关键帧时数据部分并不是纯 NALU而是前面要带 5 个字节的 FLV VideoTagHeader第一个字节FrameType(4bit) CodecID(4bit)比如 0x17 表示关键帧 AVC 第二个字节AVCPacketType0AVC sequence header1AVC NALU 第三到第五个字节CompositionTimePTS 与 DTS 的差值补帧顺序时要用这就是为什么很多人明明拿了 H.264 裸流直接塞给 write_packet服务器那头怎么都花屏。因为裸 NALU 少了一个封装层RTMP 协议里要求的是 FLV 风格的数据不是裸流。AAC 音频也类似裸 AAC 帧前面要带 2 个字节的 AudioTagHeader其中第一个字节标识音频格式、采样率、声道数第二个字节标识 AAC packet type0 表示 AudioSpecificConfig1 表示原始 AAC 帧。新手最容易忘的就是一上来直接发 AAC 帧数据而漏掉了必须最先发一次 AudioSpecificConfig。这个配置信息通常由编码器提供只有编码器告诉播放器我这路 AAC 长什么样播放器才能正确解码后续的音频帧。4. 拉流侧也能打读取、录制与播放的衔接4.1 从publish到play拉流API的对称性srs-librtmp 不只是推流库拉流也支持。拉流侧的流程和推流高度对称srs_rtmp_create(url) - srs_rtmp_handshake(rtmp) - srs_rtmp_connect_server(rtmp) - srs_rtmp_play(rtmp) - 循环 srs_rtmp_read_packet(...) - srs_rtmp_close(rtmp) - srs_rtmp_destroy(rtmp)差别只在 publish 换成 playwrite_packet 换成 read_packet。srs_rtmp_read_packet的签名和 write 很像int srs_rtmp_read_packet(srs_rtmp_t rtmp, char* type, uint32_t* timestamp, char** data, int* size);type、timestamp 这两个参数是出参每次读包后你会拿到一个 FLV Tag 的类型和时间戳。data 是指向一份新分配内存的指针这个指针需要你自己释放不用再单独调用 srs-librtmp 的释放接口直接用free()就行。这里非常容易内存泄漏我见过有人循环读包几百次进程内存肉眼可见往上涨最后发现是没释放 read_packet 返回的 data。4.2 读包后的数据怎么处理三种典型去向拿到 type、timestamp、data、size 之后怎么处理取决于你要做什么。第一个去向直接写 FLV 文件做录制。FLV 文件本质上就是FLV header 一堆 Tag。srs-librtmp 已经帮你把 Tag 的 body 都准备好了你只要自己补一个 9 字节的 FLV header 和 11 字节的 Tag header然后按序写文件即可。由于 read_packet 是边收边写即使中途断流已经写入的部分也能用很多播放器正常播放这对直播录制、录像回看类需求很实用。第二个去向喂给解码器做实时预览或转封装。比如把 H.264 的 AVC NALU 从 type9 的数据里拆出来组合成 Annex-B 格式后送给解码器把 AAC 原始帧解出来送进音频解码器。需要注意读到的第一个视频 Tag 往往是 AVC sequence header里面包含 SPS/PPS不要把它当成普通视频帧丢给解码器而应该解析保存等后续关键帧到来时配合使用。第三个去向和播放器对接。如果不想自己写解码可以把拉到的流直接转给 FFmpeg 的处理器或者把 data 重新封装成 HTTP-FLV 推给前端播放器。我在一个直播流转发项目里就是用 srs-librtmp 从上游拉 RTMP然后按 HTTP-FLV 格式转发给 Web 端靠一个库同时解决了拉和喂两个环节省掉了中间很多协议转换的脏活。4.3 拉流时最容易忽略的metadata处理很多人在拉流时只盯着音视频数据忽略了 type18 的 metadata。实际上metadata 非常关键它里面包含了width和height视频宽高videocodecid视频编码格式7 表示 AVC/H.264audiocodecid音频编码格式10 表示 AACvideodatarate/audiodatarate码率信息encoder编码器名称方便排查源头如果你的播放器不支持从 SPS/PPS 里自动解析宽高那就必须依赖 metadata。稳妥做法是在 play 之后先循环读几个包把第一次遇到的 type18 数据解析出来缓存好再开始处理后续音频视频。不要假设服务器一定会在音视频之前发 metadata虽然绝大多数服务器会但不排除例外防御性处理永远没坏处。5. 我把srs-librtmp用进生产环境后踩过的坑与性能建议5.1 和FFmpeg libavformat怎么选一个能用脑图就能看的对比很多人在选型时纠结到底用 srs-librtmp 还是 libavformat我把实际对比列出来对比项srs-librtmpFFmpeg libavformat代码量一个 cpp几千行适合阅读和裁剪整体庞大动辄几十万行依赖复杂协议范围以 RTMP 为核心简洁专一RTMP、RTSP、HLS、SRT、WebRTC 等全支持依赖主要依赖 OpenSSL无明显重量级依赖依赖大量编解码库、协议库交叉编译成本很高学习成本一天基本能跑通推流链路想真正搞懂至少需要数周可定制性源码简单改起来容易架构复杂改一个协议细节可能涉及很多层适合场景嵌入式端侧、轻量网关、协议学习桌面播放器、转码服务器、全协议中转如果你是在服务器上做转码、转封装无脑用 FFmpeg如果你是在资源受限的端侧做推流或者只想维护一个可控的 RTMP 通道srs-librtmp 明显更合适。我那个摄像头网关项目最终选择 srs-librtmp 的原因就一条交叉编译 FFmpeg 那一串依赖太痛苦而 srs-librtmp 只需要两个源文件加一个 OpenSSL一个小时就能编到 ARM 板子上。5.2 几个容易翻车的真实场景场景一时间戳不单调导致播放器断断续续。有一次我把系统墙上时钟的毫秒值直接当作 timestamp 传进去结果发现播放器每隔一段时间就卡一下。排查下来发现是系统时钟回拨导致 timestamp 往回跳。RTMP 流的时间戳必须是单调递增的最稳妥的维护方式是代码里维护一个累加计数器每次发帧时以帧间隔累加而不是直接读系统时间。场景二只推视频不发音频播放器起播很慢。有段业务只推视频结果观众端打开播放器后黑屏好几秒。原因是很多播放器在同时收到音频和视频数据后才会触发起播流程。虽然纯视频流也能播但兼容性差很多。如果你的源只有视频建议在 metadata 里声明没有音频或者做一路静音音频流一起推起播体验会好很多。场景三断线后没有自动重连机制。srs-librtmp 本身不提供断线重连网络抖动导致 write_packet 返回非 0 后整个会话基本就废了必须重新走一遍 create、handshake、connect、publish。我在业务里是另外包了一层重连状态机断线后先退避 1 秒再重试连续失败就逐步延长到 10 秒避免服务端被疯狂重建连接打爆。5.3 工程化建议把srs-librtmp用得更稳个人经验用 srs-librtmp 做生产项目时有几条建议值得沉淀单连接只在一个线程里操作。srs_rtmp_t 句柄内部没有做线程安全保护多线程同时 write 同一个句柄会导致数据错乱。如果业务侧有多个编码线程务必在推流入口加锁或者用一个发送队列把所有音频视频帧送给同一个推流线程。发送侧要做好音视频交织。不要连续发几十帧视频再发一帧音频尽量让每个发送周期内都有音频和视频。这样不仅能提升播放端体验也符合 RTMP 的实时语义。打开调试日志辅助排查。srs-librtmp 内置了一些日志输出关键时刻能救命。生产环境如果觉得日志太吵可以加编译开关控制输出级别但开发阶段别轻易关掉。版本锁死。srs-librtmp 和 SRS 服务器虽然同源但不同版本的握手、chunk 大小协商细节会有差异。我通常固定 SRS 的某个 release tag把 srs_librtmp.cpp 打进自己的版本管理不随主仓库频繁升级保证服务器端和客户端永远同步。这样即使未来 SRS 更新了协议细节只要我不主动升级线上就不会因此出问题。最后再分享一个小技巧如果遇到的场景只是临时验证服务器能不能正常收流或放流不需要写代码直接使用 srs-librtmp 自带的测试程序就行。把推流地址和拉流地址分别作为参数传进去自己在终端里看日志比从零写业务代码验证快得多。等确认服务器链路没问题再回到业务代码里排查定位问题会省力不少。本文还有配套的精品资源点击获取