
简介面向流媒体开发与监控系统集成人员的一份实战型源码包围绕开源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里面挂一个ServerMediaSubsessionH.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 # 摄像头地址、账号、通道配置 └── Makefilelive/目录基本不动业务全在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 -j4genMakefiles 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:pass192.168.1.64:554/Streaming/Channels/101 1 cam02 rtsp://admin:pass192.168.1.65:554/Streaming/Channels/101 2第一列是内部标识第二列是摄像头完整 RTSP 地址含账号密码和通道第三列是对外暴露的通道号播放器拉rtsp://服务器IP:8554/cam01就能看到对应画面。地址里的通道路径各家摄像头不一样海康常见Channels/101大华常见cam/realmonitor?channel1subtype0填之前先用 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 tcpUDP 在本地回环也可能因为防火墙规则丢包排查时先排除网络因素。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_guardstd::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_按路数调单路 3060 帧够用太多会吃内存。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是自定义 sourcedoGetNextFrame里从 FrameBuffer 取一帧填进fTo取不到就scheduleDelayedTask稍后再试别阻塞事件循环。SPS/PPS 要在第一个关键帧前下发否则播放器解不出画面。5. 避坑与排查多路转发最容易踩的五个坑5.1 内存持续上涨跑几小时就 OOM现象进程 RSS 每小时涨几十 MB最终被系统杀掉。原因FrameBuffer 只进不出或者转发端 source 创建后没释放播放器断开连接时 subsession 没回收。解决给缓冲设硬上限并统计丢弃数在OnDemandServerMediaSubsession的closeStreamSource里确认 source 被 delete用valgrind --leak-checkfull跑一轮重点看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阻塞。代码上就是多一个AnalysisQueueafterGettingFrame里try_push非阻塞写入。验证方法算法故意 sleep 200ms看拉流帧率是否不受影响、丢弃计数是否正常增长。换 H.265Live555 较新版本带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 的代码都强制走一遍这个三步验证宁可多花半小时也不想半夜被叫起来看黑屏。希望帮到你。本文还有配套的精品资源点击获取