简介:这是一份基于Linux平台、使用C语言实现的RTSP客户端下载器源码资源,面向希望深入理解RTSP/RTP协议交互、并具备一定C语言基础的网络开发者。程序能够从指定的RTSP服务器(如live555)拉取TS格式多媒体流,依次完成RTSP会话建立、请求发送、RTP数据包接收与解析,最终将媒体数据还原为本地文件,且生成的文件可直接使用VLC播放器正常播放。资源包共4个文件,包括3个C源文件与1个头文件,压缩包整体仅5KB,代码量小但覆盖了RTSP客户端涉及的核心流程。当前已有203人学习浏览,说明其在同类协议学习资源中有一定参考价值。通过阅读源码,可以具体掌握RTSP的OPTIONS、DESCRIBE、SETUP、PLAY等命令交互过程,熟悉RTP报文头与负载的解析方法,并学会在Linux环境下用Socket编程实现流媒体下载,是一份适合协议入门与二次开发的简洁示例。
1. RTSP 三件套落在 Linux C:live555 的完整技术地图
“RTSP.rar_Linux C下载器_RTSP播放器_RTSP服务器_live555_rtsp”这个交付包,把三件事放在一起:能用 C 落地的 RTSP 下载器、一个能拉流播放的最小播放器、一个能对外分发媒体流的 RTSP 服务器。三者的共同底座是 live555。很多从安防、车载、边缘网关转 Linux 国产化平台的团队,拿到这类工程包的第一反应是找现成命令跑通,但真正要改的是会话管理、数据回调和落盘策略。live555 用 C++ 实现,对外却保留了一层很像 C 回调的接口,工程上可以把它当协议栈用,本地代码仍然用 C 写业务。下文按协议、播放器、服务器、下载器、验证的顺序,把整条能复现的链路走一遍。
2. RTSP 协议详解与 live555 的线程模型
2.1 信令状态机与 RTP/RTCP 通道的关系
RTSP 本质上是文本控制协议,在 TCP 上跑,控制的是另一对 UDP 通道上的 RTP/RTCP 数据流。一个完整的拉流过程,客户端依次发 OPTIONS、DESCRIBE、SETUP、PLAY、TEARDOWN;服务器端通过 SDP 描述媒体类型、编码参数、采样率和端口映射。RTSP 不是 HTTP 那种一问一答就完事的协议,它会经历状态迁移:SETUP 成功后进入 Ready,PLAY 之后进入 Playing,PAUSE 回到 Ready,TEARDOWN 直接释放资源。
实现里最容易忽略的是 RTCP 通道。RTCP 承担两件事:周期报告丢包率和往返时延,以及同步音视频的 RTP 时间戳。live555 的 RTSPClient 会在内部维护这两者的解析,但前提是 SETUP 阶段协商出来的端口对能正常收发 UDP。如果服务器在 NAT 后面,端口协商只在几秒内有效,后续 UDP 阻塞一次,整个会话就可能被判定超时。所以检视 RTSP 问题,先看信令状态能不能推进到 Playing,再看 RTP 包有没有真正进入接收循环。
| 阶段 | 协议状态 | 典型触发现象 |
|---|---|---|
| Init | 尚未建立会话 | DESCRIBE 发送前 |
| Ready | SETUP 成功 | 可发送 PLAY / PAUSE |
| Playing | PLAY 成功 | RTP 数据持续到达 |
| Terminated | TEARDOWN 或超时 | 连接断开、回调停止 |
2.2 live555 的模块边界:RTSPClient、MediaSession、Sink 与 Groupsock
live555 的项目结构可以看成四层。UsageEnvironment 负责日志、错误和定时器;Groupsock 封装 UDP socket,负责 RTP 包收发和 multicast 地址管理;liveMedia 是核心库,里面是 RTSPClient、MediaSession、MediaSubsession、MediaSink 这一系列类;BasicUsageEnvironment 则提供了一个默认的单线程事件循环实现。你写的业务代码基本都挂在三个回调面上:RTSPClient 的信令回调、MediaSink 的数据回调、以及自定义 Scheduler 里的定时器。
| live555 组件 | 承担角色 | 业务侧需要处理的内容 |
|---|---|---|
| UsageEnvironment | 错误输出、日志、定时器 | 重定向日志到 syslog 或文件 |
| Groupsock | UDP 收发、multicast | 一般不用碰 |
| RTSPClient | OPTIONS/DESCRIBE/SETUP/PLAY 信令 | 处理回调里的 failCode |
| MediaSession | 解析 SDP、创建 Subsession | 按轨创建 Sink |
| MediaSink | 接收解帧后的媒体数据 | C 业务回调的入口 |
一个常见误解是 live555 是多线程的。其实默认实现是单线程事件循环,全部网络读取和回调都在doEventLoop()里串行执行。这对 C 侧开发反而是好事:回调里能安全访问共享状态,不用加锁;坏处是任何阻塞操作,比如直接在一个回调里写磁盘或调用同步网络 API,都会把整个事件循环卡死,导致 RTCP 超时和掉线。后面讲下载器落盘时还会再提这个点。
2.3 为什么不用 GStreamer:live555 的取舍点
团队做 RTSP 选型时经常在 live555 和 GStreamer rtsp 服务器之间摇摆。GStreamer 的优势是管线完整,从网络输入到解码显示一路打通,调试工具 gst-launch 很方便;但它的依赖树很庞大,交叉编译到 ARM 板或 Linux 国产化平台时,需要带一堆插件包,光 rtsp 服务器相关的就有 gstreamer-rtsp-server、gst-plugins-base、gst-plugins-good,体积和启动耗时都上来了。live555 编译产物就是几个静态库,应用层只依赖三四个 .a 文件,控制点全部暴露在代码里。
取舍的边界在哪里?如果业务重点是播放器本身,后面要接视频渲染和音频输出,选 GStreamer 更省事;如果业务重点是“拉流、推流、落盘”这三个动作,而且希望信令流程完全自主可控,live555 明显更合适。还有一种场景是设备带宽有限,只需要提取 H.264 裸流交给硬件编码器或自己的推流模块。live555 不会夹带额外的编解码格式,拿到的就是最原始的 Annex-B 流,正好对应标题里说的“Linux C 下载器”这类轻量工具。
2.4 Linux C 环境下编译 minimal live555
先把 live555 编一遍,目录建议单独建一个工作目录放源码:
cd /opt/rtsp tar xzf live555-latest.tar.gz cd live ./genMakefiles linux make -j4genMakefiles linux会根据config.linux里的参数生成当前平台对应的 Makefile,它决定了编译器选项、头文件路径和安装目录。make 完成后,liveMedia/libliveMedia.a、groupsock/libgroupsock.a、BasicUsageEnvironment/libBasicUsageEnvironment.a这三个静态库是应用层需要链接的。bin 目录下会生成 openRTSP、testOnDemandRTSPServer 等可执行文件,后续验证阶段直接用它们当参照物。
提示:主程序用 C 语言写、链接 live555 时,必须用 g++ 或让 gcc 加上
-lstdc++,否则会报一堆未定义的 C++ 运行时符号。这是“C 调用 C++ 协议栈”最常见的编译期坑。
3. 基于 live555 的 RTSP 播放器实现
3.1 最小播放器骨架
openRTSP 是 live555 自带的参考客户端,代码量大,适合查问题但不适合二次开发。实际项目里写播放器,一般把流程压缩成“创建 Environment → 创建 RTSPClient → 发送 DESCRIBE → 在回调里逐级往下走”。下面这个骨架是常见做法的浓缩版:
#include "liveMedia.hh" #include "BasicUsageEnvironment.hh" #include "RTSPClient.hh" int main(int argc, char** argv) { UsageEnvironment& env = *BasicUsageEnvironment::createNew(NULL); RTSPClient* client = RTSPClient::createNew( env, "rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov", 1, "rtsp_player"); client->sendDescribeCommand(continueAfterDESCRIBE); env.taskScheduler().doEventLoop(); return 0; }createNew的四个参数依次是环境、RTSP URL、verbosity 级别和应用名。verbosity 传 1 时会在终端打印每次信令的收发原文,排错期建议打开,上线前改成 0。sendDescribeCommand只是把请求发出,结果要到回调函数里拿;doEventLoop()是 live555 的单线程事件循环,程序运行期间不能阻塞它。
continueAfterDESCRIBE是自定义的回调函数,处理逻辑一般分三步:检查 resultCode 是否为 0,从sessionDescription创建MediaSession,然后遍历每个MediaSubsession调用setupMediaSubsession。这一步成功之后再发 PLAY,PLAY 成功就代表码流通道建立完成。
3.2 数据回调与 H.264 输出
真正干活的是 Sink。live555 里每个轨道对应一个 MediaSubsession,需要为它创建一个 MediaSink 的子类。子类里最重要的是afterGettingFrame方法,它就是 C 业务回调的入口。收到数据后可以直接交给自己的 C 函数处理:
class RtspSink : public MediaSink { public: static RtspSink* createNew(UsageEnvironment& env, MediaSubsession& sub) { return new RtspSink(env, sub); } void afterGettingFrame(unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime, unsigned durationInMicroseconds) override { // 把 H.264 帧数据和 PTS 交给外部 C 函数 c_on_rtsp_video_frame(fReceiveBuffer, frameSize, &presentationTime); } };live555 的 Sink 基类维护了一个接收缓冲fReceiveBuffer,默认大小可通过构造函数参数调整,但有几个关键点必须留意。afterGettingFrame收到的已经是一个完整的帧,而不是裸 RTP 包;对 H.264 来说,一个 IDR 帧很可能被拆分到多个 RTP 包,这在 Sink 内部由H264VideoStreamFramer负责拼接,回调拿到的已经是连续码流。如果numTruncatedBytes不为 0,说明缓冲区不够,一帧数据被截断了,这通常发生在高分辨率高码率场景,需要把接收缓冲调大,例如increaseBufferSizeTo(2 * 1024 * 1024)。
3.3 Linux C 侧解码与显示格式的衔接
播放器拉到流之后,最终要把数据交给解码器。常见做法有两种:一是把 live555 回调的数据直接送入硬解码器,比如海思 MPP、瑞芯微 MPP 的 H.264 解码输入;二是走软解,交给 FFmpeg 的avcodec_send_packet。无论走哪条路,首先要确认回调里出来的数据格式。
| 数据情况 | 处理方式 |
|---|---|
| Annex-B 格式(起始码 00 00 00 01) | 可以直接送给硬解或 FFmpeg |
| AVCC 格式(NALU 长度前缀) | 需要转换成 Annex-B 再送解码器 |
| 缺少 SPS/PPS | 从 SDP 的 sprop-parameter-sets 提取并插入码流 |
用 C 写个 NAL 检测函数,把回调数据按起始码切开,是后面的服务器和下载器都要复用的基础能力:
static int extract_nal_unit(const uint8_t *buf, size_t len, const uint8_t **nal, size_t *nal_len) { for (size_t i = 0; i + 4 < len; i++) { if (buf[i] == 0x00 && buf[i+1] == 0x00 && buf[i+2] == 0x00 && buf[i+3] == 0x01) { *nal = buf + i + 4; /* 跳过 00 00 00 01 起始码 */ *nal_len = /* 取到下一个起始码或帧尾 */; return 1; } } return 0; }拿到 NAL type 之后就能决定是继续等待下一帧,还是立刻切文件。整个拉流链路的坑大多在这里:回调到数据后没有判断 SPS/PPS 是否完整就直接送解码器,导致解码器反复报错。建议在 C 侧维护一个两帧左右的前置缓冲,确认拿到关键帧再启动解码。
提示:live555 按帧回调,帧边界不等于 RTP 包边界。自行用原始 socket 去抓 RTP 再手工组帧属于重复造轮子,花屏排查到最后往往发现是自己绕过了现成的 Framer。
4. 自建 RTSP 服务器:会话注册与端口分配
4.1 服务器启动与媒体会话注册
标题里的 RTSP 服务器部分,live555 同样提供了完整实现。它沿用“会话(Session)+ 子会话(Subsession)”的结构,一个会话对外暴露成一个 URL,一个会话里可以挂视频轨、音频轨。创建一个只服务单路 H.264 文件的实时服务器,核心代码是构造 RTSPServer 和 ServerMediaSession:
#include "liveMedia.hh" #include "BasicUsageEnvironment.hh" int main(int argc, char** argv) { UsageEnvironment& env = *BasicUsageEnvironment::createNew(NULL); RTSPServer* server = RTSPServer::createNew(env, 8554, NULL); ServerMediaSession* sms = ServerMediaSession::createNew( env, "channel1", "Channel One", "rtsp-live-session"); ServerMediaSubsession* sub = H264VideoFileServerMediaSubsession::createNew(env, "input.h264", True); sms->addSubsession(sub); server->addServerMediaSession(sms); env.taskScheduler().doEventLoop(); return 0; }RTSPServer::createNew的第二个参数是监听端口,8554 是 RTSP 测试常用端口,生产环境可以直接用 554。ServerMediaSession::createNew的第二个参数决定 URL 路径,上面的代码会让这个会话对外变成rtsp://<ip>:8554/channel1。最后一个True表示重复发送源文件,也就是常说的循环推流模式;传 False 则只播一遍。
H264VideoFileServerMediaSubsession读取裸 H.264 文件并按实时速率发送 RTP 包,源文件要求是去掉 MP4 容器头的 Annex-B 流。如果手里是 MP4 或 TS,先用 FFmpeg 把轨道提出来:
ffmpeg -i input.mp4 -c:v copy -bsf:v h264_mp4toannexb -an input.h2644.2 多路流的 URL 映射与实时推流源
多路输出只需要重复addServerMediaSession。在同 一个 RTSPServer 上注册两个不同名字的 ServerMediaSession,客户端就能用rtsp://ip:8554/channel1和rtsp://ip:8554/channel2分别访问。live555 没有内置路由规则,URL 就是会话名的直接映射,这反而好管理,适合配置文件里维护一对一的通道映射表。
如果是 IPC 相机采集、编码器实时输出的场景,不适用H264VideoFileServerMediaSubsession,应改用H264VideoStreamServerMediaSubsession。后者允许你用自己的线程把编码器产出的 H.264 帧推进 live555 的帧源对象,实现真正的实时推流。差异点在于自定义一个 FrameSource,把doGetNextFrame和采集线程绑定,采集完成后调用框架的afterGetting通知取走数据。这一层是 RTSP 服务器里最需要业务定制的部分,也是标题所指工程包的核心价值:框架 live555 已经给了,但推流源必须自己接。
4.3 服务器端 TCP/UDP 传输策略与动态端口分配
RTSP 服务器在 SETUP 响应时,会给出 RTP 数据走的传输方式。默认是 UDP,RTP 使用一个偶数的端口,RTCP 占随后的奇数端口,端口由服务器从可用端口里挑。这个行为在公网环境经常出问题,因为防火墙未必放行这段动态 UDP 端口。
| 传输模式 | 端口特征 | 典型场景 |
|---|---|---|
| RTP/UDP | 偶端口 + 相邻奇端口 | 内网直连、低延迟 |
| RTP/AVP/TCP | 复用 8554/554 | 跨防火墙、公网拉流 |
| multicast | 224.x.0.0/16 段 | 局域网多路广播 |
客户端请求 TCP 模式,只需在 SETUP 时携带Transport: RTP/AVP/TCP;interleaved=0-1,live555 服务端会自动响应 interleaved 模式。多路并发时,动态端口分配可能存在用尽或重叠风险,尤其是同一台机器上多个 RTSPServer 实例。常见的解决办法是把服务器的可用端口范围配置成固定区间,给每路流预设端口段,和主业务端口隔离。
提示:服务器验证的第一件事,是用 openRTSP 或 VLC 打开
rtsp://<ip>:8554/<session>。这一步不通过不要急着改业务代码,先看信令和端口连通性。
5. RTSP 下载器与录像落盘
5.1 拉流侧数据重组与最小录制命令
标题里的 Linux C 下载器,本质是把 RTSP 实时流转成文件的程序。最省事的做法是用 live555 自带的 openRTSP 拉裸流,再交给 FFmpeg 封装:
openRTSP -d 30 -q rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov > raw.h264 ffmpeg -f h264 -i raw.h264 -c copy recorded.mp4-d 30控制录制时长,单位是秒;-q关闭调试输出,避免日志混进重定向的码流。下载器内部的要点是:openRTSP 从网络拿到的是 RTP 包,live555 的H264VideoStreamFramer会把 RTP payload 还原成 Annex-B 码流,所以重定向出来的 raw.h264 已经是连续可用的 H.264。FFmpeg 再用-c copy做容器封装,不重新编码,速度很快。
自定义下载器时,数据链路有两处要自己处理。一是afterGettingFrame里需要判断当前帧是不是关键帧,因为文件分段要在 IDR 处切;二是时间戳换算关系。live555 回调带出来的presentationTime已经由 RTCP SR 包校准过,直接把它作为录制文件的 PTS 源即可,不要在本地自己打时间戳,否则播放器的快进慢放会对不上点。
5.2 存储时机、文件分片与回调线程解耦
录像需求多半要求按时间分片,比如每 30 分钟一个 MP4。分片时机不能只看墙钟时间,如果正好切在一片连续帧中间,切片文件会缺关键帧,导致录像打不开。我习惯的做法是检测到 IDR 时再补刀:先看上一条分片时长是否达到阈值,达到就关闭当前文件,否则继续写。
static int on_h264_frame(uint8_t *buf, size_t len, struct timeval *pts, rec_ctx *ctx) { int nal_type = buf[0] & 0x1f; if (nal_type == 5) { /* IDR 帧 */ if (ctx->file_open && (pts->tv_sec - ctx->segment_start) >= ctx->segment_duration) { close_segment(ctx); /* 关旧文件 */ open_segment(ctx, pts->tv_sec); /* 开新文件 */ } } return mux_write(ctx, buf, len, pts); }这里的思路是每收到一帧先判断 NAL 类型和分片时长,分片超时且当前是关键帧时才切换文件,避免切片落在非关键帧上。mux_write是自己实现的 MP4 muxer,或者直接调 FFmpeg 的av_interleaved_write_frame。无论哪种,都不要把这个写磁盘动作直接放在 live555 的回调线程里。
回调线程卡 100ms,播放端就可能出现 RTCP 超时;卡 500ms 以上基本要被判定断流。下载器设计上一定要有队列接口:回调线程只入队,落盘线程负责写文件和切分。这是“回调里不落盘”这条铁律的直接应用。
5.3 参数调优:缓冲、超时与断线重连
下载器相比播放器多了一层“持续运行”的约束,超时和重连参数决定它能连续跑多久。弱网环境下,如果不主动处理会话超时,一次断流会让下载器一直空转直到录制定时结束。常见做法是启动一个独立看门狗,超过 N 秒没有新帧就主动 TEARDOWN 并重新走一遍“创建客户端 → DESCRIBE → SETUP → PLAY”流程。
| 参数 | 推荐初值 | 调大后的代价 |
|---|---|---|
| 看门狗超时 | 5 秒 | 断流识别变慢,误重连减少 |
| 接收缓冲 | 2 MB | 内存上涨,弱网抗抖更好 |
| 分片时长 | 30 分钟 | 文件变大,seek 变慢 |
| RTCP 报告间隔 | 跟随丢包率 | 改大后状态更新变慢 |
缓冲大小直接影响高码率下的稳定性。假设视频码率 8 Mbps,一秒就产生 1 MB 码流,2 MB 缓冲只够扛住 2 秒以内的网络抖动,再往上调到 4 MB 也是合理范围。live555 的 RTP 接收器对乱序报文的重排窗口很小,超过窗口的包会被当作丢包处理。下载场景优先容忍乱序,宁可多等一拍也不要丢关键帧,所以回调里发现丢的是非关键帧时,优先等待下一个 IDR 再续传,而不是一丢包就重连。
6. 验证 RTSP 三件套与排错技巧
6.1 用公开 RTSP 测试地址验证播放器与下载器
拉通验证不需要先自建服务器,直接拉公网 RTSP 测试流更快。WOWZA 的演示流是比较稳定的测试源,命令如下:
openRTSP -d 15 -q rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov自己的播放器程序完成后,把上面的 URL 换到RTSPClient::createNew里,观察能否经过 DESCRIBE 和 PLAY 两轮回调,并且回调里真的持续收到帧。如果 openRTSP 能拉通但你的程序不行,差异通常出在回调链处理不完整:没给每个 Subsession 创建 Sink,或共用了一个全局缓冲导致音视频轨道数据互相覆盖。下载器验证则按第 5 章的思路,录制 60 秒后直接ffprobe recorded.mp4看时长和关键帧间隔,时长不在 60±2 秒说明时间戳换算有误差。
自制服务器也走同一套验证:先 openRTSP 拉自己起的服务,再让播放器程序拉自己起的服务,两条线都通才算闭环。服务器绑定 8554 端口时遇到“端口已被占用”,用netstat -tlnp看占用进程,再考虑换端口或杀掉残留测试进程。
6.2 常见故障表与解决手段
RTSP 链路故障按信令阶段归类,比较好定位:
| 故障现象 | 典型原因 | 处理手段 |
|---|---|---|
| DESCRIBE 408 超时 | 控制端口不可达或 DNS 解析失败 | 检查 554/8554 连通性,改用 IP 直连 |
| SETUP 一直失败 | 服务器 UDP 端口段被防火墙遮挡 | 改用 rtp-over-tcp 模式测试 |
| PLAY 成功但无帧 | RTP 端口进不来,或 Sink 缓冲区溢出 | tcpdump 确认 UDP 是否到本机,调大缓冲 |
| 播放花屏 | 少了 SPS/PPS,或丢包严重 | 从 SDP 提取编码参数插入码流 |
| 一段后自动断开 | RTCP 超时或没有 keep-alive | 定期发 OPTIONS 保活,调整看门狗阈值 |
PLAY 成功但无帧的问题,先用tcpdump -i eth0 udp观察 RTP 包是否到达主机。包到了但应用层读不到,问题多半在 Groupsock 绑定的端口或地址上。一个更隐蔽的坑是服务端 SDP 里写的是多播组地址,客户端没加入多播组,就会像黑洞一样收完包不报错,这种情况只有抓包才能看出来。
最后的调试技巧:临时把RTSPClient::createNew的 verbosity 参数从 0 改成 1,终端里会打出每次信令的收发原文,包括服务器端不识别的方法、不支持的 Transport 组合。这是排查与第三方 RTSP 设备兼容性问题时最有效的入口,对比两端打印的 SDP 选项,就能定位是哪一侧的端口或编码参数不一致。
本文还有配套的精品资源,点击获取