
简介这份源码包面向网络流媒体开发者定位为轻量级RTSP服务器实现重点支持H.264视频流的传输与播放控制。包内共19个文件其中12个头文件与3个C源文件构成核心逻辑覆盖RTSP请求处理、RTP/RTCP交互、H.264 NAL单元解析和会话状态管理另附in与ac格式的构建辅助文件方便跨平台编译与二次集成压缩包仅30KB结构紧凑适合具备C语言基础的中高级开发者深入学习目前已有401人学习。通过阅读源码可以掌握DESCRIBE、SETUP、PLAY等RTSP方法的状态机实现理解媒体数据在RTP中的打包方式与RTCP同步机制并借鉴其线程调度、资源复用及简单QoS控制的设计思路。代码注释简明模块划分清晰适合作为学习协议栈的参考范式。资源包还附带MIT开源协议文本可作为自建流媒体服务或研究RTSP协议栈的实用参考。1. librtsp 是什么为什么选它来搭 H.264 的 RTSP 服务端你手里有一路 H.264 编码流可能是海康摄像头的主码流可能是开发板上的硬编码器输出也可能只是一个 .h264 文件。你要做的是把它变成局域网内一个稳定的 rtsp:// 地址让 VLC、FFmpeg、监控大屏这些 RTSP 拉流协议客户端随拉随到。librtsp 就是这类场景里比 GStreamer 轻得多的一把螺丝刀它把 RTSP 状态机、SDP 生成、RTP 打包和 TCP 转发全封装进一个 C 库你只需要注册回调、喂入 H.264 NAL 单元和时间戳。这篇文章按我实际调试过的路径讲RTSP 与 H.264 为什么必须这样配对、最小服务端怎么写、真实摄像头怎么接、五个容易翻车的坑最后给调延迟和验证的命令。2. RTSP 拉流协议与 H.264 的配对逻辑不搞懂 RTP 封装就别碰 server2.1 四次握手把人骗进来OPTIONS、DESCRIBE、SETUP、PLAY先看 RTSP 拉流协议的握手顺序。一个客户端比如 VLC 或 ffplay连上服务端之后不会上来就让你传数据而是按标准顺序发四个请求OPTIONS探活顺便问服务端支持哪些方法DESCRIBE要一份 SDP 描述里面写着这个地址有什么流、编码格式是什么、参数集在哪SETUP协商传输方式客户端告诉服务端“我想用 TCP 还是 UDP、端口是多少”PLAY正式开播。每一个请求都是 HTTP 风格但 RTSP 靠状态码和会话 ID 把四次请求串成一条状态链。刚开始做服务端的人最容易犯的错就是在DESCRIBE之前就发 RTP 数据——客户端确实会收到但解码器没拿到任何参数集画面根本起不来。我当年第一次搭就是没等 PLAY 就开始往 socket 写 RTP结果 VLC 一直转圈用 tcpdump 一看才发现握手顺序整个反了。librtsp 这种库的价值就在这里它内部把上面这条状态机当成黑匣子维护好谁来OPTIONS、谁发了SETUP、会话 ID 怎么换你都不用管。你要做的是在它暴露的几个回调里填空。常见做法是用set_media_handler注册一个路径比如/live然后告诉 librtsp 这个路径走 H.264剩下的OPTIONS、TEARDOWN它默认都会回。第一次上手的人把精力放在DESCRIBE回调要返回什么 SDP、以及 PLAY 之后你要往 session 里塞什么数据上就够了。偶尔你还会看到客户端主动发GET_PARAMETER做保活这种请求在 librtsp 里默认回 200 就行如果自己在维护状态机千万别把它当成 DESCRIBE 的变种不然 session 会乱。2.2 H.264 在 RTP 里怎么装SPS/PPS、NAL 分片与 90k 时间戳H.264 裸流Annex-B 格式是由一长串 NAL 单元组成的每个 NAL 用00 00 00 01或00 00 01分隔。不是所有 NAL 都能直接塞进一个 RTP 包。RTP 对 H.264 的封装遵循 RFC 6184常见三种模式单 NAL 包、STAP-A 聚合包、FU-A 分片包。如果你的帧大于 MTU1080p 的 IDR 帧经常几十 KB 甚至上百 KBRTP 层就要按 FU-A 切成多个 1400 字节左右的小包。librtsp 这类库通常会帮你做分片你喂给它的应该是一个完整的 NAL 单元而不是已经切好的 RTP 包——记住这一点能省很多排查时间。比分片更关键的是 SPS 和 PPS。SPS/PPS 是解码参数集分别对应 NAL type 7 和 8。没有它们解码器不知道分辨率、帧率、profile哪怕 RTP 包一个都不丢也解不出来。所以 H.264 over RTP 里有“带外”和“带内”两种传法带外指在 SDP 的fmtp字段里用sprop-parameter-sets带上 Base64 编码的 SPS/PPS带内指在每一个 IDR 关键帧前面把 SPS/PPS 像普通数据一样再发一遍。实际工程里我一般两者都做SDP 里带一份保证 VLC 这种严格走 SDP 的客户端能立刻开工推流时在 IDR 前再带内发一遍防止记性不好的客户端丢掉了带外参数。时间戳是这个环节最隐蔽的坑。RTP 时间戳的时钟是 90kHz不是毫秒。也就是说假设视频是 25fps那么相邻两帧的 RTP 时间戳增量应该是 90000/25360030fps 则是 3000。如果你从编码器拿到的是毫秒级 PTS发送前要乘上 90。很多刚开始写 librtsp 服务的人直接拿毫秒时间戳当 RTP 时间戳结果就是画面一顿一顿或者越拉越卡还以为是网络问题。这个换算应该在靠近发送的那一层做别在采集端做不然你换一路源还得改一遍。2.3 为什么 librtsp 默认走 RTP/AVP/TCP 反而省心RTSP 的传输模式分两种RTP/AVP/UDP和RTP/AVP/TCP。H.264 裸流从架构上看更适合 UDP因为丢包不影响后续帧解码的顺序。但现实中服务端要处理的客户端五花八门有在 NAT 后面的有防火墙只放行 80/443 的。UDP 意味着要在中间网络上开至少两个端口对很多内网环境是灾难。TCP 模式也叫 RTP over RTSPinterleaved 方式把所有 RTP 数据混在同一个 TCP 连接里通过SETUP响应里的interleaved0-1约定通道号客户端只用一个端口就全搞定。我实际做监控视频拉流 rtsp yolo 这类推理链路时基本都选 TCP 模式。原因很简单推理程序用 FFmpeg 拉流FFmpeg 对 TCP 的兼容性最好基本不会出现连不上、拉两秒断流的问题而且 TCP 的重传机制是现成的网络抖动时不会直接花屏最多延迟变大。librtsp 这类轻量库一般会在 SETUP 回调里自动回RTP/AVP/TCP;unicast;interleaved0-1你要做的只是别在客户端强制 UDP 时把别人踢掉。若你的库同时支持 UDP建议生产环境统一让客户端用 TCP测试时再放 UDP这样排查问题少一个变量。GStreamer 的 rtsp server 库当然也能做同样的事但它会扯出一大堆 pipeline 和 plugin 依赖。librtsp 的优势是接口直白尤其是嵌入式板子上交叉编译一个静态库拖进去就行。这里不是否定 GStreamer只是如果你的需求就是“收一路 H.264、发一路 H.264、不转码”librtsp 更对胃口。3. 用 librtsp 在本地跑通 H.264 RTSP server最小代码与三个关键回调3.1 下载编译与 CMake 配置依赖项与头文件路径这里不贴具体仓库地址标题本身就能搜到。下载后第一件事是看include/librtsp下有哪几个头文件。常见做法是 librtsp 自身不带 SPS/PPS 解析需要你程序里自己维护有些分支则带了简单的 Annex-B 解析能少写不少胶水代码。我一般选不带解析器的版本因为输入的 H.264 源大多是 FFmpeg 的h264_mp4toannexb或摄像头已经处理好的 Annex-B解析逻辑自己写也不复杂。编译上librtsp 本身只需要 pthread可选 openssl如果你要 TLS 的话但 RTSP 做 TLS 属于少数需求。CMake 里这样配置就行cmake_minimum_required(VERSION 3.10) project(rtsp_h264_server C) set(CMAKE_C_STANDARD 11) find_path(LRTSP_INCLUDE_DIR librtsp/lrtsp_server.h) find_library(LRTSP_LIBRARY librtsp) add_executable(rtsp_h264_server main.c) target_include_directories(rtsp_h264_server PRIVATE ${LRTSP_INCLUDE_DIR}) target_link_libraries(rtsp_h264_server PRIVATE ${LRTSP_LIBRARY} pthread)这里find_path和find_library是让 CMake 自己去找你本地编译时如果库没放到系统目录记得用-DCMAKE_INCLUDE_PATH...和-DCMAKE_LIBRARY_PATH...指向下载目录。比起把/usr/local/lib写死在target_link_libraries里这样换板子时少改一处是一处。编译时如果报lrtsp_server.h not found先检查头文件路径如果报undefined reference to pthread_*把pthread放到最后一个链接库位置。3.2 注册 H.264 会话DESCRIBE 与 PLAY 回调怎么写librtsp 这类库的核心抽象是“路径 媒体处理器”。你注册了/live之后客户端访问rtsp://host:8554/live就触达这组回调。下面这段是我实际用过的骨架#include librtsp/lrtsp_server.h #include string.h #include stdio.h /* 我习惯把 SPS/PPS 存在全局因为编码器参数基本不变 */ static uint8_t g_sps[64]; static size_t g_sps_len; static uint8_t g_pps[64]; static size_t g_pps_len; /* 客户端 DESCRIBE 时会调用这个回调我们要把 H.264 信息写进 SDP */ static int on_describe(lrtsp_session_t *sess, lrtsp_sdp_t *sdp, void *opaque) { /* payload type 96 是动态负载类型H.264 一般不占用固定编号 如果库里没有 lrtsp_sdp_add_h264就自己拼 fmtp 行 */ lrtsp_sdp_add_h264(sdp, 96, g_sps, g_sps_len, g_pps, g_pps_len); return 0; } /* 客户端 PLAY 之后librtsp 会回调这里可以在这统计并发数 */ static int on_play(lrtsp_session_t *sess, void *opaque) { /* 返回 0 表示放行返回非 0 会给客户端回 403 */ return 0; }on_describe是决定客户端能不能立刻开播的关键。如果这个回调里g_sps_len还是 0SDP 里就没有sprop-parameter-setsFFmpeg 这类严格客户端会直接拒绝。所以注册路径之后、启动服务器之前必须先解析一次 H.264 流把 type 7/8 NAL 缓存进全局。有的分支用lrtsp_media_set_h264_extra_data(media, sps, pps)这样的接口作用一样函数名不同——拿到库后先看头文件里带h264和sdp的那几个函数逻辑都在那里。3.3 启动服务并按帧喂数据一个最小 main 函数服务端启动本身特别简单难的是把数据送进去。先看主函数lrtsp_server_t *g_srv; int main(int argc, char **argv) { g_srv lrtsp_server_create(0.0.0.0, 8554); if (!g_srv) { fprintf(stderr, create server failed\n); return 1; } lrtsp_server_set_media_handler(g_srv, /live, on_describe, on_play, NULL); if (lrtsp_server_start(g_srv) ! 0) { fprintf(stderr, server start failed\n); return 1; } /* 这里假设你已经有线程在调 feed_h264_frame(), 主线程留着做信号处理和统计即可 */ for (;;) { sleep(1); } return 0; }然后是发送函数你可以从文件、编码器回调或 FFmpeg 管道里调它/* feed_h264_frame 接收一个完整 NAL 单元不是 RTP 包 */ void feed_h264_frame(const uint8_t *nal, size_t len, uint64_t pts_ms) { lrtsp_media_t *media lrtsp_server_get_media(g_srv, /live); if (!media) return; /* 毫秒转 RTP 90k 时间戳如果 librtsp 内部自动转就别再乘 */ lrtsp_media_send_h264(media, nal, len, pts_ms * 90); }参数说明nal指向的是去掉了起始码的 NAL 负载还是带了00 00 00 01的完整 Annex-B取决于库的实现。我的经验是 librtsp 大多接收“带起始码的完整 Annex-B”因为它在内部识别 type 7/8/5 并决定要不要带内发送。如果你传的是已经拆好的 NAL反而容易丢失 SPS/PPS 重复注入的机会。pts_ms * 90是关键这里一定要在库外面算清楚别指望库倒过来迁就你的时间单位。3.4 用 FFmpeg/VLC 验证拉流三行命令见分晓ffprobe -rtsp_transport tcp rtsp://127.0.0.1:8554/live 21 | grep Video如果 ffprobe 正常输出 Video 行说明 SDP 没问题。接着用 ffplay 看画面ffplay -rtsp_transport tcp -fflags nobuffer -probesize 32 \ rtsp://127.0.0.1:8554/live注意-probesize 32和-fflags nobuffer是让 ffplay 尽快开画不然它默认要探测 500KB 甚至更多数据本地测试时你会以为服务器没反应。如果 ffprobe 有 Video、ffplay 黑屏先回头查时间戳增量。如果两个都报404 Not Found八成是路径注册错了看 server 启动日志里有没有打印注册的路径。4. 把海康摄像头 RTSP 主码流接进 librtspSPS/PPS 注入与取流参数调整4.1 海康/小米摄像头 RTSP 取流地址格式与主码流/子码流接真实摄像头时先拿到它的 RTSP 取流地址。海康摄像头的协议路径是公开惯例主码流是rtsp://用户名:密码IP:554/Streaming/Channels/101子码流把结尾换成102。主码流分辨率高、码率大不管你是转给推理程序还是 librtsp 服务器都要先确认带宽吃不吃得下子码流给 YOLO 做目标跟踪够用但做质检或车牌识别就得上主码流。小米摄像头的 RTSP 取流教程更特殊要先登录拿 stok 再拼实时流路径跟安防厂商的标准路径不一样如果你只是做本地调试建议先用海康或模拟摄像机少走弯路。拿到地址后先单独用 VLC 验一遍能不能拉再接入你的服务端。这一步能过滤掉 60% 的“librtsp 有问题”的误判实际上是摄像头用户名密码错、地址拼错或码流格式不是 H.264。4.2 用 FFmpeg 把摄像头流转为 librtsp 能接收的 H.264 管道要把摄像头流送进 librtsp常见做法是让 FFmpeg 做“拉流但不转码”的管道输出到 stdout然后你的程序从 stdin 读ffmpeg -rtsp_transport tcp \ -i rtsp://admin:你的密码192.168.1.64:554/Streaming/Channels/101 \ -an -c:v copy -f h264 -flags low_delay -muxdelay 0 pipe:1参数说明-c:v copy是直接拷贝 H.264不做转码CPU 占用极低-an丢掉音频免得管道里混进不需要的流-flags low_delay和-muxdelay 0是降低 FFmpeg 内部缓冲让数据尽快吐给下游。如果你在 Windows 上跑pipe:1可能默认是二进制模式用setmode(_fileno(stdout), _O_BINARY)或者干脆把输出写到命名管道 FIFO避免莫名其妙的0x0a被替换成0x0d 0x0a。C 程序里读管道并拆 NAL 的骨架长这样/* 从 stdin 读 FFmpeg 输出的 Annex-B按 00 00 01 切 NAL */ static void *pipe_reader(void *arg) { uint8_t buf[4096]; /* 读缓冲 */ uint8_t frame[2 * 1024 * 1024]; /* 单帧缓冲2MB 足够 1080p */ size_t frame_len 0; size_t n, i; int zero_count 0; while ((n fread(buf, 1, sizeof(buf), stdin)) 0) { for (i 0; i n; i) { if (buf[i] 0) { zero_count; } else if (buf[i] 1 zero_count 2) { /* 命中起始码前面的内容是一个完整 NAL */ if (frame_len 0) { feed_h264_frame(frame, frame_len, g_pts_ms); g_pts_ms 40; /* 25fps每帧 40ms示意 */ frame_len 0; } zero_count 0; } else { zero_count 0; if (frame_len sizeof(frame)) frame[frame_len] buf[i]; } } } return NULL; }这个切分逻辑只适合演示真实项目里必须处理00 00 00 01和00 00 01的严格状态机边界情况很多。一个比较省事的替代方案是用 FFmpeg 的h264_mp4toannexb滤镜配合-bsf:v h264_mp4toannexb让 FFmpeg 把 Annex-B 里该有的起始码都处理好你的程序只需要做“遇到连续 3 字节以上 0 后跟 1 就算分隔”的简化判断。4.3 3 个必须调的参数GOP 间隔、时间戳基准、Socket 发送缓冲第一是 GOP 间隔。librtsp 不转码只透传如果源摄像头 GOP 太大海康默认可能 50 甚至 100 帧客户端随机拉流时等一个 IDR 要等好几秒表现为黑屏很久。处理方式优先在摄像头后台把 I 帧间隔调到 25~50 帧或者在 librtsp 里缓存最近一个 IDR 帧新客户端 SETUP/PLAY 后立即把缓存的 IDR 连同 SPS/PPS 发给它播放器秒开。第二是时间戳基准。海康摄像头主码流的时间戳基于采集时钟FFmpeg 转管道时一般建议在-i后追加-fflags genpts或-use_wallclock_as_timestamps 1让输出的 PTS 连续递增。librtsp 侧只做乘 90别自己加偏移。如果你发现摄像头拉一段时间后客户端画面严重跳变多半是源时间戳回绕不是 librtsp 的问题。第三是 socket 发送缓冲。TCP 模式下服务端 socket 发送缓冲过大客户端读得慢你会积压几百毫秒数据。我一般用 256KB 到 512KB配合 librtsp 的发送队列上限如果你用的分支提供设置为 5 帧左右。太小会触发背压导致摄像头端 NAL 被丢弃太大则延迟直线上升。这块没有绝对正确按你的码率算码率 2Mbps250ms 缓冲大约 62KB码率 8Mbps250ms 缓冲 250KB。4.4 监控视频拉流 rtsp yolo要不要在 librtsp 前加解码如果你接摄像头的目的是做目标检测常见的链路是摄像头 RTSP - FFmpeg 解码 - YOLO 推理 - 输出画面。这时 librtsp 放在哪一端我一般放在“推理后的编码输出”上YOLO 处理完的画面用 x264/NVENC 编码成 H.264再喂给 librtsp 让浏览器和大屏去拉。摄像头源直接接 librtsp 不做推理只适合做转发或录播系统。原因很简单librtsp 不是解码器你跟它要 decoded frame 属于使错劲解码交给 FFmpeg编码交给 x264librtsp 只占最后一环的传输层各司其职排查问题时责任边界也清晰。5. librtsp 服务 H.264 的 5 个常见问题避坑现象、原因、处理5.1 VLC 能出图但 ffprobe 报 SPS/PPS missing现象VLC 打开 rtsp 地址正常ffprobe 和 YOLO 的avformat却报h264 sps/pps missing。原因VLC 容忍带内 SPS/PPS在收到 IDR 后自己起播ffprobe 走严格 SDP 路径要求 DESCRIBE 阶段就必须返回sprop-parameter-sets。librtsp 的on_describe回调里没填入 SPS/PPS就会这样。解决在on_describe里确保调用lrtsp_sdp_add_h264时两个参数非空。如果库没提供解析注册媒体路径之前先用一个简单函数扫一遍 H.264 流把 type 7 和 type 8 NAL 缓存到全局。判断方法从起始码后的第一个字节看高 5 位0x67是 SPS0x68是 PPS。5.2 画面拉到第 5 秒突然冻结现象拉流开始正常5~10 秒后画面不再刷新重连又正常。原因源 GOP 较长播放器接入后一直在等下一个 IDR等不到就一直干看或者 librtsp 缓存的关键帧只在新连接SETUP时发PLAY之后就不再发。解决在on_play回调里把最近一个 IDR 帧连同 SPS/PPS 立即发给该会话。更稳妥的做法是维护一个 GOP 循环缓存只缓存最近一个 IDR 及它后面的非 IDR 帧新客户端进来先把这一串发给它再续上实时流。这个缓存只保留 1 个 GOP别把历史全存下来否则内存涨得很快。5.3 多客户端并发时花屏现象单客户端正常两个甚至更多客户端同时拉流画面出现切块或花屏。原因发送函数里共享同一个发送缓冲两个 session 的写指针相互覆盖典型的并发 bug。如果 librtsp 没帮你做 per-session 缓冲就是库外代码的问题。解决给每个 session 单独复制一份 NAL 数据再入队发送时用互斥锁保护链表。另一个更深入的做法是开启线程池每个客户端独立发送线程避免一个慢客户端拉低所有人的速度。我见过有人靠usleep(1000)想避开竞态那是玄学别学。5.4 延迟从 200ms 涨到 2s越拉越大现象开始延时很小几分钟后画面缓冲占满延迟越来越大。原因TCP 模式自带拥塞控制服务端发送缓冲太大客户端拉流速度小于服务端生产速度队列越积越久。GOP 缓存没清理也会叠加这个现象。解决缩小 socket 发送缓冲代码里int sz 256 * 1024; setsockopt(fd, SOL_SOCKET, SO_SNDBUF, sz, sizeof(sz));同时把 librtsp 的发送队列长度限制在 5 帧左右宁可丢帧不可拖延迟。注意SO_SNDBUF在内核会把你的值翻倍所以看到实际 512KB 别惊讶。网络层也别乱调先改应用层队列。5.5 客户端报 401 / connection reset现象VLC 弹窗要求输入密码ffplay 直接Connection Reset。原因librtsp 默认开启了 Basic / Digest 鉴权回调里没有实现auth接口或者你传进去的用户名密码与客户端不一致。也有分支默认不允许DESCRIBE带非授权路径。解决在注册媒体时看一眼有没有auth_callback参数。如果不需要鉴权把鉴权关闭如果需要实现回调里对Authorization头做一次比对。生产环境别把 RTSP 端口直接暴露公网可以在前面加一层带鉴权的反向代理或用iptables限定来源 IP。6. 把 librtsp server 从能跑调成好用低延迟参数与验证命令6.1 先敲一遍验证命令我把这些命令固定成肌肉记忆。FFmpeg 侧ffprobe -rtsp_transport tcp rtsp://127.0.0.1:8554/live ffplay -rtsp_transport tcp -fflags nobuffer -probesize 32 rtsp://127.0.0.1:8554/live有时还想手工看握手响应用 nc 发一个 OPTIONSprintf OPTIONS rtsp://127.0.0.1:8554/live RTSP/1.0\r\nCSeq: 1\r\n\r\n | nc 127.0.0.1 8554服务器应该在 200 OK 后面列出OPTIONS, DESCRIBE, SETUP, PLAY, TEARDOWN。6.2 低延迟的参数整法第一开TCP_NODELAY。librtsp 创建 socket 的位置不确定若它不暴露参数就在自己的发送回调里拿到 fd 后手动 setsockopt。第二GOP 缓存永远只存 1 个关键帧不要为了“保险”多存几个 GOP那只会加大内存和发送开销。第三不要在采集线程里直接调用发送函数否则采集阻塞会直接传染给所有客户端。第四客户端侧同样要把 buffer 调低ffplay 加-fflags nobufferFFmpeg 的avformat里probesize设小、analyzeduration设小。这是双端配合的事只调服务端不调客户端效果减半。如果你更习惯 GStreamer可以用gst-launch-1.0 rtspsrc locationrtsp://127.0.0.1:8554/live latency0 ! fakesink来验证对齐它走的是标准 RTSPSrc 插件如果这一步正常说明服务端没有明显实现偏差。6.3 从能跑变为稳定的判断标准我现在每起一个 librtsp 服务第一件事不是写业务代码而是先用 ffprobe 验证 SDP再用 ffplay 看首帧秒开最后才并行跑多客户端压测。压测时盯着三个值延迟是否单调上涨、花屏是否只在多路并发时出现、SPS/PPS 是否在每次重连后被重新注入。这套习惯帮我少熬夜改了好几次代码。写网络协议这东西调试时乱试只会变成玄学把变量一一项列出来问题会立刻清晰。希望这篇笔记帮到你。本文还有配套的精品资源点击获取