基于Live555的多路RTSP摄像头流汇聚转发服务器实战

发布时间:2026/10/26 14:07:22
基于Live555的多路RTSP摄像头流汇聚转发服务器实战

简介:面向流媒体开发与监控系统集成人员的一份实战型源码包,围绕开源Live555框架构建RTSP服务器,解决多路IP摄像头视频流同时接入并向多个客户端转发的问题。代码中实现了摄像头连接管理、RTSP会话调度与H264媒体子会话等模块,并附带Live555静态库与cJSON解析支持,适合具备一定网络协议与多线程基础的开发者研究复用。压缩包共192个文件,以175个hh头文件为主,辅以6个cpp源文件、若干静态库与h/hpp声明文件,整体约686KB,体量轻便、结构紧凑。目前已有1739人学习下载。读者可从中获得多路复用与并发处理的实现思路、DESCRIBE/SETUP/PLAY等请求的响应组织方式,以及摄像头参数配置与RTP/RTCP传输的参考写法,便于快速搭建监控、视频会议等场景下的流转发原型。

1. 多路摄像头汇聚转发的 RTSP 服务器:一个能跑起来的 Live555 工程

手里有几台网络摄像头,每台都只肯吐自己的 RTSP 流,可下游的播放器、录像机、算法盒子只想拉一个地址。这种场景在安防集成、边缘计算盒子里太常见了,最省事的做法就是自己搭一个 RTSP 服务器,把多路摄像头的流拉过来再统一转发出去。这个基于 Live555 的 Rtsp 服务器项目干的就是这件事:它用 Live555 作为 RTSP 协议栈底座,从多个摄像头获取数据,再以标准 RTSP 服务的形式对外提供。适合谁?适合手上有一堆 IPC、需要做流汇聚和二次分发的开发者,也适合想搞懂 Live555 服务端骨架、拿它当练手工程的人。下面按「它是什么、怎么跑、怎么改」的顺序拆开讲。

2. 技术选型与整体架构:为什么是 Live555,数据怎么流动

2.1 选 Live555 而不是从零写 RTSP 的理由

RTSP 协议本身不算复杂,但真正难的是 RTP/RTCP 那一套:打包、时间戳、SSRC、RTCP 的 SR/RR 报告、会话超时回收。自己从零撸一遍,光是把 VLC 拉流时的握手对齐就要耗掉不少时间。Live555 把这些都封装好了,服务端侧有RTSPServer、ServerMediaSession、ServerMediaSubsession三层抽象,客户端侧有RTSPClient和MediaSink,正好覆盖「拉多路 + 转发」这个需求的两端。

常见做法是:用RTSPClient去连每个摄像头,把收到的帧通过自定义的MediaSink接住,再喂给服务端侧的ServerMediaSubsession往外发。这样协议细节全交给库,业务代码只关心「帧从哪来、往哪去」。选它还有一个现实原因——编译依赖少,一个源码包加个 make 就能出静态库,塞进嵌入式环境不费劲。

2.2 整体数据流:从摄像头到播放器的三段链路

整个系统的数据流可以拆成三段,理解这三段,后面看代码就不会迷路:

第一段是拉流段。每个摄像头对应一个RTSPClient实例,连上之后发DESCRIBE拿到 SDP,再SETUP建立 RTP 接收通道,最后PLAY开始收数据。收到的 RTP 包由 Live555 内部的MultiFramedRTPSource解包成帧,回调到我们注册的 sink。

第二段是缓冲与转换段。摄像头来的帧通常是 H.264/H.265 的 NAL 单元,需要按帧缓存,必要时做时间戳重映射。多路之间要隔离,一路卡了不能拖垮其他路,所以每路一个独立缓冲队列是常规做法。

第三段是转发段。服务端为每路摄像头建一个ServerMediaSession,里面挂一个ServerMediaSubsession(H.264 用H264VideoFileServerMediaSubsession的变体,或者自己实现OnDemandServerMediaSubsession)。当播放器来拉流时,OnDemandServerMediaSubsession的createNewStreamSource和createNewRTPSink被调用,把缓冲里的帧打包成 RTP 发出去。

提示:三段链路里最容易翻车的是第二段。拉流和转发速率不匹配时,缓冲会涨,涨到内存扛不住就崩。后面避坑章节会专门讲这个。

2.3 目录结构:源码包大概长什么样

这类工程通常是在 Live555 源码包基础上加自己的业务目录,结构大致如下(不同版本略有差异,以实际源码为准):

project/ ├── live/ # Live555 原始库目录 │ ├── liveMedia/ # 核心媒体库,RTSPServer 等都在这里 │ ├── groupsock/ # 网络组播/单播封装 │ ├── UsageEnvironment/ # 事件循环与任务调度 │ └── BasicUsageEnvironment/ # 基础环境实现 ├── app/ │ ├── main.cpp # 程序入口,解析配置、启动服务 │ ├── CameraClient.cpp/.h # 单路摄像头的 RTSPClient 封装 │ ├── CameraSink.cpp/.h # 自定义 MediaSink,接住拉下来的帧 │ ├── ForwardServer.cpp/.h # 服务端会话管理,对外提供 RTSP │ └── FrameBuffer.cpp/.h # 每路独立的帧缓冲队列 ├── config/ │ └── cameras.conf # 摄像头地址、账号、通道配置 └── Makefile

live/目录基本不动,业务全在app/下。这种分层的好处是升级 Live555 版本时,业务代码不受影响。config/cameras.conf是运维入口,加一路摄像头改配置就行,不用重编。

2.4 关键类职责对照

把核心类列成表,改代码时对着找:

类 / 文件职责改动频率
CameraClient封装单路 RTSPClient,负责连接、DESCRIBE、SETUP、PLAY、断线重连中
CameraSink继承MediaSink,接收解包后的帧,写入 FrameBuffer高
FrameBuffer环形队列,按帧存 NAL,带容量上限和丢弃策略高
ForwardServer管理RTSPServer和每路的ServerMediaSession中
H264LiveServerMediaSubsession自定义 subsession,从 FrameBuffer 取帧喂给 RTP sink高
main.cpp读配置、建环境、启动事件循环低

改得最多的是 sink、buffer 和 subsession 这三块,因为它们直接决定帧怎么进、怎么存、怎么出。

3. 把工程跑起来:编译、配置与首轮验证

3.1 编译 Live555 与业务代码

Live555 官方用 genMakefiles 那套流程,先编库再编业务。假设源码包已经解压到project/,进入live/目录:

cd project/live ./genMakefiles linux make -j4

genMakefiles linux会生成适配 Linux 的 Makefile,-j4按核数并行编译。编完在liveMedia/、groupsock/等目录下会生成.a静态库。接着回业务目录:

cd ../app make

业务 Makefile 里要链接这几个库,顺序不能乱(静态库有依赖顺序):

LIBS = -lliveMedia -lgroupsock -lUsageEnvironment -lBasicUsageEnvironment -lpthread

如果链接时报undefined reference,八成是库顺序反了或者漏了-lpthread。Live555 内部用了线程和定时器,-lpthread不能省。

3.2 摄像头配置怎么写

config/cameras.conf是纯文本,一行一路,字段用空格或制表符分隔。常见格式:

# 名称 摄像头RTSP地址 对外通道号 cam01 rtsp://admin:pass@192.168.1.64:554/Streaming/Channels/101 1 cam02 rtsp://admin:pass@192.168.1.65:554/Streaming/Channels/101 2

第一列是内部标识,第二列是摄像头完整 RTSP 地址(含账号密码和通道),第三列是对外暴露的通道号,播放器拉rtsp://服务器IP:8554/cam01就能看到对应画面。地址里的通道路径各家摄像头不一样,海康常见Channels/101,大华常见cam/realmonitor?channel=1&subtype=0,填之前先用 VLC 单独验证摄像头地址能拉通,别把问题带到服务器侧。

3.3 启动与首轮验证

启动命令一般带配置路径和监听端口:

./rtsp_server -c ../config/cameras.conf -p 8554

-c指定配置,-p指定对外 RTSP 端口,默认 8554。启动后看日志,正常会打印每路摄像头的连接状态和对外 URL。验证分两步:先用ffprobe确认服务端流可读:

ffprobe -rtsp_transport tcp rtsp://127.0.0.1:8554/cam01

能打印出视频流信息(编码、分辨率、帧率)说明转发链路通了。再用 VLC 打开同一个地址看画面。如果ffprobe通但 VLC 花屏,多半是 SPS/PPS 没随流下发,检查 subsession 里有没有把参数集塞进 sink。

注意:首轮验证建议用 TCP 传输(-rtsp_transport tcp),UDP 在本地回环也可能因为防火墙规则丢包,排查时先排除网络因素。

3.4 多路并发时的观察点

单路通了不代表多路稳。开 4 路以上时重点看三个指标:进程内存是否持续上涨(缓冲泄漏)、CPU 是否单核打满(事件循环阻塞)、每路帧率是否掉(拉流超时)。日志里给每路加个帧计数,每秒打印一次,掉帧能第一时间发现。这一步别偷懒,很多问题在 2 路时看不出来,8 路才暴露。

4. 核心模块拆解:拉流、缓冲、转发三段代码

4.1 拉流端:RTSPClient 的连接与回调

每路摄像头一个 client,连接成功后依次发 DESCRIBE、SETUP、PLAY。Live555 的客户端是异步回调风格,代码骨架大致这样:

// CameraClient.cpp 片段 void CameraClient::start() { // 创建 RTSPClient,传入环境、地址、端口、超时 client_ = RTSPClient::createNew(*env_, url_.c_str(), 0, "camera_client"); if (client_ == nullptr) { env_->taskScheduler().scheduleDelayedTask(5000000, (TaskFunc*)reconnect, this); // 5 秒后重连 return; } // 发 DESCRIBE,回调里拿 SDP client_->sendDescribeCommand(continueAfterDescribe, this); } void CameraClient::continueAfterDescribe(RTSPClient* client, int resultCode, char* resultString) { if (resultCode != 0) { // 失败:记录错误码,触发重连 scheduleReconnect(); return; } // 解析 SDP,为每个 subsession 发 SETUP MediaSession* session = MediaSession::createNew(*env_, resultString); client->sendSetupCommand(*session, continueAfterSetup, False, True); // TCP 传输 }

createNew的第三个参数是 verbosity,调试时调大能看到协议交互细节。sendSetupCommand的第四个参数streamUsingTCP设 True 走 TCP,摄像头网络不稳时比 UDP 可靠。回调里resultCode != 0表示失败,必须处理,否则这一路就静默死掉了。重连用scheduleDelayedTask延迟触发,别在回调里直接重连,容易递归。

4.2 自定义 MediaSink:把帧接住

Live555 默认的 sink 是往文件写,我们要的是往内存缓冲写,所以继承MediaSink自己实现:

// CameraSink.cpp 片段 Boolean CameraSink::continuePlaying() { if (fSource == nullptr) return False; // 申请一个 buffer,向 source 要数据 fBuffer = fSource->getNextFrame(buffer_, BUFFER_SIZE, afterGettingFrame, this, onSourceClosure, this); return True; } void CameraSink::afterGettingFrame(void* clientData, unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime, unsigned durationInMicroseconds) { CameraSink* sink = (CameraSink*)clientData; // 把帧写入该路的 FrameBuffer,带时间戳 sink->buffer_->push(sink->buffer_, frameSize, presentationTime); // 继续要下一帧 sink->continuePlaying(); }

getNextFrame是拉模式,sink 主动向 source 要数据,要完在回调里再要下一帧,形成循环。presentationTime是这一帧的显示时间戳,转发时要保留或重映射,丢了播放器会卡顿。numTruncatedBytes大于 0 说明 buffer 不够大,帧被截断了,BUFFER_SIZE要按最大关键帧调大,H.264 1080p 关键帧常见几十 KB 到上百 KB,给 512KB 比较稳。

4.3 帧缓冲:环形队列与丢弃策略

多路之间速率不匹配,缓冲是刚需。用环形队列,满了丢最旧的帧:

// FrameBuffer.cpp 片段 void FrameBuffer::push(const uint8_t* data, size_t size, timeval ts) { std::lock_guard<std::mutex> lock(mutex_); if (count_ == capacity_) { // 满了:丢最旧的一帧,给新帧腾位置 head_ = (head_ + 1) % capacity_; --count_; ++dropped_; } Frame& f = frames_[(head_ + count_) % capacity_]; f.data.assign(data, data + size); f.timestamp = ts; ++count_; cond_.notify_one(); }

capacity_按路数调,单路 30~60 帧够用,太多会吃内存。dropped_计数要暴露到日志,丢帧率持续上涨说明下游消费慢或上游太快,是排查的第一线索。加锁范围要小,assign拷贝大帧时持锁会阻塞转发线程,帧大时可以考虑移动语义或预分配池。

4.4 转发端:OnDemandServerMediaSubsession 的实现

服务端每路一个 subsession,播放器来拉时才创建 source 和 sink:

// H264LiveServerMediaSubsession.cpp 片段 FramedSource* H264LiveServerMediaSubsession::createNewStreamSource( unsigned clientSessionId, unsigned& estBitrate) { estBitrate = 2000; // kbps,估算值,影响 RTCP 带宽 // 返回一个从 FrameBuffer 取帧的 source return BufferFramedSource::createNew(envir(), buffer_); } RTPSink* H264LiveServerMediaSubsession::createNewRTPSink( Groupsock* rtpGroupsock, unsigned char rtpPayloadTypeIfDynamic, FramedSource* inputSource) { // H264VideoRTPSink 负责把 NAL 打包成 RTP return H264VideoRTPSink::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic); }

estBitrate影响 RTCP 报告里的带宽估算,填个接近实际的码率值,填太小播放器可能降码率请求。BufferFramedSource是自定义 source,doGetNextFrame里从 FrameBuffer 取一帧填进fTo,取不到就scheduleDelayedTask稍后再试,别阻塞事件循环。SPS/PPS 要在第一个关键帧前下发,否则播放器解不出画面。

5. 避坑与排查:多路转发最容易踩的五个坑

5.1 内存持续上涨,跑几小时就 OOM

现象:进程 RSS 每小时涨几十 MB,最终被系统杀掉。原因:FrameBuffer 只进不出,或者转发端 source 创建后没释放,播放器断开连接时 subsession 没回收。解决:给缓冲设硬上限并统计丢弃数;在OnDemandServerMediaSubsession的closeStreamSource里确认 source 被 delete;用valgrind --leak-check=full跑一轮,重点看MediaSink和FramedSource的分配点。

5.2 单路卡死拖垮全部

现象:一路摄像头断网,其他路也跟着卡顿甚至无画面。原因:所有路共用一个事件循环,某路的重连逻辑里做了阻塞操作(比如同步 connect 或 sleep),把循环堵住了。解决:重连一律用scheduleDelayedTask异步化;每路的超时时间独立配置;拉流回调里不做耗时操作,帧处理丢到独立线程或队列。

5.3 播放器能连上但黑屏

现象:VLC 显示已连接,进度条在走,画面全黑。原因:SPS/PPS 没下发,或者时间戳不连续导致解码器丢弃。解决:确认H264VideoRTPSink之前把参数集喂进去了;检查presentationTime是否单调递增,摄像头重连后时间戳会跳变,需要做重映射(记录偏移量,新时间戳减去偏移)。

5.4 多路时 CPU 单核打满

现象:8 路以上时top显示一个核 100%,其他核空闲。原因:Live555 单线程事件循环,所有路的收发包都在一个线程里,路数一多就成瓶颈。解决:轻量场景先优化单帧处理开销(减少拷贝、预分配 buffer);路数再多要考虑多进程方案,每进程管几路,前面加个轻量分发。单进程硬扛不是长久之计。

5.5 摄像头断线后不自动恢复

现象:摄像头重启或网络闪断后,该路永久无画面,日志无报错。原因:onSourceClosure回调没处理,或者重连时旧 client 没释放导致句柄泄漏。解决:在 source closure 回调里显式触发重连,重连前Medium::close(client_)释放旧实例;重连加退避(5s、10s、20s 递增),避免摄像头没起来时疯狂重试。

6. 二次开发:加一路 AI 分析或换编码格式怎么改

拿到这个工程,最常见的二次开发需求有两个:一是把转发出去的流同时喂给算法做分析,二是把 H.265 摄像头接进来。两个需求改的地方不一样,分开说。

接算法分析:不用改转发链路,在CameraSink::afterGettingFrame里加一个旁路,把帧同时推给算法队列。关键是解耦——算法慢不能反压拉流。做法是算法侧用独立的有界队列,满了丢帧并计数,绝不让push阻塞。代码上就是多一个AnalysisQueue,afterGettingFrame里try_push非阻塞写入。验证方法:算法故意 sleep 200ms,看拉流帧率是否不受影响、丢弃计数是否正常增长。

换 H.265:Live555 较新版本带H265VideoRTPSink和H265VideoStreamFramer,把 subsession 里的H264VideoRTPSink换成 H.265 对应类,source 侧确认 NAL 单元类型解析对得上(H.265 的 NAL 头是两字节,和 H.264 不同)。改完用ffprobe确认编码是hevc,再用支持 H.265 的播放器验证。老版本 Live555 没有 H.265 支持,得先升级库,升级时注意genMakefiles流程和 API 变化。

一个具体技巧:改 subsession 时,先别动转发逻辑,在BufferFramedSource::doGetNextFrame里加一行日志,打印每帧大小和时间戳差值。多路跑十分钟,看时间戳差值是否稳定在 1/帧率 附近,抖动大的路就是问题路。这个笨办法帮我定位过好几次「画面卡顿但日志无错」的玄学问题——根因都是某路摄像头时间戳跳变。

改造验证有个固定套路我一直在用:改完先单路ffprobe跑 5 分钟,再 4 路并发跑 30 分钟,最后拔掉一路摄像头网线看恢复。三步都过才算改完。从那以后我每次动 sink 或 buffer 的代码,都强制走一遍这个三步验证,宁可多花半小时,也不想半夜被叫起来看黑屏。希望帮到你。

本文还有配套的精品资源,点击获取