简介这是一份面向Linux系统C语言开发者与音视频服务学习者的RTSP流媒体服务器实战源码聚焦H.264编码视频的实时推流与控制功能解决从零构建轻量级RTSP服务的核心技术难点。资源共11个文件含5个C源文件如rtsptserver.c、rtspservice.c、ringfifo.c等实现主服务逻辑、会话管理与环形缓冲区、4个头文件定义协议结构、工具函数与线程同步机制以及1个H.264测试码流和1个可执行二进制文件整体压缩包仅6.57MB结构精简、模块职责清晰便于逐层剖析RTSP交互流程与RTP打包机制。已有1113人学习下载读者可直接编译运行深入理解DESCRIBE/SETUP/PLAY等RTSP命令解析、套接字多线程并发处理、H.264帧时间戳封装、RTP负载格式构造及基础错误日志体系设计是掌握嵌入式或边缘端流媒体服务开发的典型入门范例。1. 一个能跑通rtsp://127.0.0.1:8554/test的轻量级 RTSP 服务器为什么不用 FFmpeg 或 GStreamer你手头有一份名为linux-c语言-RTSP服务器H264.rar的压缩包解压后是十几个.c和.h文件rtsptserver.c、rtspservice.c、ringfifo.c、rtputils.c还有1080P.h264这个原始码流文件。它不依赖 FFmpeg 动态库不调用 GStreamer pipeline也不需要 Python 或 Java 环境——纯 C 编写、静态链接、gcc -o rtsptserver rtsptserver.c rtspservice.c ringfifo.c rtputils.c -lpthread一条命令就能编译出可执行文件。这不是教学 Demo而是真实部署在嵌入式设备或边缘网关上的最小可行 RTSP 服务它只做三件事——响应 DESCRIBE/SETUP/PLAY 请求、按 RTP 时间戳打包 H264 NALU、用环形缓冲区ringfifo平滑帧间抖动。适合需要低延迟、可控内存占用、无第三方依赖的场景比如国产 Linux 工控机对接海康/大华 IPC 的私有协议转 RTSP 桥接层或教育类实验平台中让学生亲手拆解 RTSP 会话状态机。如果你正被gst-launch-1.0的复杂 pipeline 卡住或发现ffmpeg -re -i 1080P.h264 -f rtsp rtsp://localhost:8554/test启动慢、无法精确控制 SPS/PPS 插入时机这份源码就是调试 RTSP 协议栈的“显微镜”。2. RTSP 会话状态机与 RTP 封装从DESCRIBE到PLAY的七步握手RTSP 不是 HTTP但复用了其文本格式和部分语义它也不是单次请求响应而是一套带状态的会话协议。本项目通过rtspservice.c中的rtsp_handle_request()函数实现核心解析其逻辑严格遵循 RFC 2326 定义的状态迁移INIT → READY → PLAY。理解这三步才能看懂为何ringfifo.c必须存在、为何rtputils.c要手动构造 RTP Header。2.1 DESCRIBE 请求解析与 SDP 生成客户端首次连接时发送DESCRIBE rtsp://127.0.0.1:8554/test RTSP/1.0\r\nCSeq: 1\r\nAccept: application/sdp\r\n\r\n。服务器需返回标准 SDPSession Description Protocol描述其中关键字段必须与后续 RTP 流严格一致// rtspservice.c 片段生成 SDP 响应 char sdp[2048]; snprintf(sdp, sizeof(sdp), v0\r\n o- %ld %ld IN IP4 127.0.0.1\r\n // o 行含会话 ID 和版本需随会话递增 sH264 Stream\r\n cIN IP4 0.0.0.0\r\n // c 行声明媒体地址实际由 SETUP 决定 t0 0\r\n mvideo 0 RTP/AVP 96\r\n // m 行定义媒体类型、端口0 表示动态、传输协议、payload type artpmap:96 H264/90000\r\n // artpmap 告知 payload type 96 对应 H264采样率 90kHz afmtp:96 packetization-mode1;profile-level-id420029;sprop-parameter-sets%s\r\n // 关键SPS/PPS Base64 编码 acontrol:trackID0\r\n, time(NULL), time(NULL), sprop_str); // sprop_str 来自 1080P.h264 文件前导 NALU 提取注意sprop-parameter-sets字段必须从 H264 文件头提取 SPSSequence Parameter Set和 PPSPicture Parameter Set并 Base64 编码。本项目在rtputils.c的parse_h264_sps_pps()函数中完成此操作——它扫描.h264文件前若干字节定位起始码0x00000001后的 NALU 类型0x67 为 SPS0x68 为 PPS将其内容拼接为base64_encode(sps_data, sps_len) , base64_encode(pps_data, pps_len)。若此处错误客户端将无法解码表现为 VLC 显示“无法识别的视频格式”。2.2 SETUP 请求处理与 RTP 端口分配客户端收到 SDP 后发送SETUP请求指定传输通道SETUP rtsp://127.0.0.1:8554/test/trackID0 RTSP/1.0 CSeq: 2 Transport: RTP/AVP;unicast;client_port5000-5001服务器必须解析client_port参数记录客户端 RTP/RTCP 接收端口本例为 5000 和 5001分配服务端 RTP 发送端口通常为偶数如 5002RTCP 端口为rtp_port1在响应中返回Transport头明确告知端口映射// rtspservice.c 中 SETUP 响应构造 char transport_hdr[256]; snprintf(transport_hdr, sizeof(transport_hdr), Transport: RTP/AVP;unicast;client_port%d-%d;server_port%d-%d\r\n, client_rtp_port, client_rtcp_port, server_rtp_port, server_rtcp_port); // 同时更新会话状态session-state RTSP_STATE_READY;提示ringfifo.c的环形缓冲区在此阶段开始预热。ringfifo_init(session-fifo, FIFO_SIZE)创建固定大小如 1MB的内存池用于暂存待发送的 RTP 包。它避免了频繁 malloc/free且支持多线程安全写入生产者H264 帧读取线程消费者RTP 发送线程。2.3 PLAY 请求触发 RTP 流发送PLAY请求不含新参数仅激活会话PLAY rtsp://127.0.0.1:8554/test RTSP/1.0 CSeq: 3 Range: npt0.000-服务器响应后立即启动rtp_send_thread()线程从ringfifo中读取已封装好的 RTP 包通过sendto()发往客户端指定的client_rtp_port。关键在于 RTP Header 构造// rtputils.c 中 rtp_pack_h264_nalu() 函数核心逻辑 struct rtp_header { uint8_t cc:4, x:1, p:1, v:2; // CC0, X0, P0, V2 (RTP v2) uint8_t pt:7, m:1; // PT96 (H264), M1 表示该包为帧末尾 uint16_t seq; // 序列号每包递增 uint32_t ts; // 时间戳基于 90kHz 时钟每帧增量 90000 / fps uint32_t ssrc; // 同一会话内唯一标识 } __attribute__((packed)); // 封装一帧 H264 NALU可能分片为 RTP 包 void rtp_pack_h264_nalu(uint8_t *rtp_pkt, int *pkt_len, uint8_t *nalu, int nalu_len, uint16_t seq, uint32_t ts, uint32_t ssrc) { struct rtp_header *hdr (struct rtp_header*)rtp_pkt; hdr-v 2; hdr-p 0; hdr-x 0; hdr-cc 0; hdr-m (nalu[0] 0x1F) 0x01 ? 1 : 0; // I帧设M1 hdr-pt 96; hdr-seq htons(seq); hdr-ts htonl(ts); hdr-ssrc htonl(ssrc); // H264 单 NALU 直接填充长度 1400 字节 if (nalu_len MAX_RTP_PAYLOAD) { memcpy(rtp_pkt sizeof(struct rtp_header), nalu, nalu_len); *pkt_len sizeof(struct rtp_header) nalu_len; } else { // FU-A 分片逻辑本项目未实现需自行补全 // 实际部署中1080P 帧常超 MTU必须分片 } }关键参数说明ts时间戳必须严格按帧率计算。若1080P.h264是 25fps则每帧ts增量为90000 / 25 3600。seq序列号从 0 开始每发一包加 1丢包时客户端靠此检测。ssrc需全局唯一建议用getpid() ^ time(NULL)生成。3. 环形缓冲区与多线程协同如何让 1080P 视频不卡顿H264 文件的帧间隔不均匀网络发送速率受 socket 缓冲区和网卡影响二者节奏不同步必然导致卡顿。ringfifo.c提供的环形缓冲区Circular FIFO是解耦读写速度的核心组件其设计直接影响 1080P 流的流畅度。3.1 ringfifo 数据结构与线程安全设计ringfifo.h定义了无锁lock-free环形队列的基础结构但为简化实现本项目采用互斥锁保护关键区域// ringfifo.h typedef struct { uint8_t *buffer; size_t size; size_t head; // 下一个写入位置 size_t tail; // 下一个读取位置 pthread_mutex_t lock; pthread_cond_t not_empty; pthread_cond_t not_full; } ringfifo_t; // ringfifo.c 中 write 操作生产者H264 读取线程 int ringfifo_write(ringfifo_t *fifo, const void *data, size_t len) { pthread_mutex_lock(fifo-lock); while (ringfifo_space(fifo) len) { // 缓冲区满则等待 pthread_cond_wait(fifo-not_full, fifo-lock); } // 执行写入处理跨 head 边界情况 size_t first_chunk MIN(len, fifo-size - fifo-head); memcpy(fifo-buffer fifo-head, data, first_chunk); if (first_chunk len) { memcpy(fifo-buffer, (uint8_t*)data first_chunk, len - first_chunk); } fifo-head (fifo-head len) % fifo-size; pthread_cond_signal(fifo-not_empty); // 通知消费者有新数据 pthread_mutex_unlock(fifo-lock); return len; }注意ringfifo_space()计算空闲空间时需预留一个字节避免 headtail 的歧义即满与空状态区分。本项目FIFO_SIZE设为1024*10241MB足够容纳约 30 帧 1080P H264按平均 30KB/帧估算。3.2 双线程模型H264 读取与 RTP 发送分离rtsptserver.c的main()函数启动两个关键线程H264 文件读取线程h264_reader_thread打开1080P.h264循环读取每个 NALU以0x00000001或0x000001为分隔符调用rtp_pack_h264_nalu()封装为 RTP 包调用ringfifo_write()写入缓冲区按目标帧率如 25fpssleep 控制节奏usleep(1000000 / 25);RTP 发送线程rtp_send_thread循环调用ringfifo_read()从缓冲区取出 RTP 包调用sendto()发往客户端 RTP 端口若缓冲区为空pthread_cond_wait(fifo-not_empty, fifo-lock)阻塞等待# 编译与运行验证确保 1080P.h264 存在同目录 gcc -o rtsptserver rtsptserver.c rtspservice.c ringfifo.c rtputils.c -lpthread ./rtsptserver 8554 # 监听端口 8554提示使用ffplay -rtsp_transport tcp rtsp://127.0.0.1:8554/test测试。若卡顿优先检查usleep()时间是否匹配实际帧率——用ffprobe 1080P.h264查看真实 FPS而非假设值。4. H264 帧解析与 RTP 分片处理超过 MTU 的大帧1080P H264 的 IDR 帧关键帧常达 50–100KB远超以太网 MTU1500 字节。若直接封装为单个 RTP 包IP 层将分片导致任意一片丢失即整帧失效。RFC 3984 规定了 H264 的 FU-AFragmentation Unit A分片机制本项目虽未内置但rtputils.c预留了扩展接口需手动补全。4.1 识别大帧并触发分片逻辑在h264_reader_thread()中读取到 NALU 后判断长度// rtputils.c 片段判断是否需 FU-A 分片 if (nalu_len MAX_RTP_PAYLOAD) { // MAX_RTP_PAYLOAD 1400 rtp_pack_h264_fu_a(rtp_pkt, pkt_len, nalu, nalu_len, seq, ts, ssrc); } else { rtp_pack_h264_nalu(rtp_pkt, pkt_len, nalu, nalu_len, seq, ts, ssrc); }4.2 FU-A 分片 RTP Header 构造规则FU-A 分片要求每个 RTP 包携带特殊 NALU Header并设置 startS、endE、reservedR位字段含义值NALU Header (1 byte)fu_indicator0x61(type28, FU-A)NALU Header (1 byte)fu_headerS1,R0,E0,Type1for first;S0,R0,E1,Type1for last// rtp_pack_h264_fu_a() 核心逻辑需自行实现 void rtp_pack_h264_fu_a(uint8_t *rtp_pkt, int *pkt_len, uint8_t *nalu, int nalu_len, uint16_t seq, uint32_t ts, uint32_t ssrc) { struct rtp_header *hdr (struct rtp_header*)rtp_pkt; // ... 设置通用 RTP Header ... uint8_t *payload rtp_pkt sizeof(struct rtp_header); int offset 0; int fragment_num 0; while (offset nalu_len) { int frag_len MIN(nalu_len - offset, MAX_RTP_PAYLOAD - 2); // 预留 2 字节 FU header uint8_t *frag_ptr payload (fragment_num * (MAX_RTP_PAYLOAD 1)); // 每包独立内存 // FU Indicator: type28 (FU-A) frag_ptr[0] 0x61; // 01100001 - F0,NRI1,type28 // FU Header: S/E/R/Type if (offset 0) { frag_ptr[1] 0x81; // S1,E0,R0,Type1 (original NALU type) } else if (offset frag_len nalu_len) { frag_ptr[1] 0x41; // S0,E1,R0,Type1 } else { frag_ptr[1] 0x01; // S0,E0,R0,Type1 } // Copy NALU fragment (skip original NALU header) memcpy(frag_ptr 2, nalu offset 1, frag_len); *pkt_len sizeof(struct rtp_header) 2 frag_len; // 发送此分片包 sendto(sockfd, rtp_pkt, *pkt_len, 0, (struct sockaddr*)client_addr, addr_len); offset frag_len; fragment_num; seq; // 每个分片独立序列号 ts 3600; // 每分片时间戳递增按帧率 } }关键点FU-A 分片中第一个包的fu_header.S1最后一个包的fu_header.E1中间包S0,E0。所有分片共享同一ssrc和timestamp因属同一帧但seq必须连续递增。客户端解码器靠S/E位重组原始 NALU。5. 实战调试技巧用 tcpdump 和 wireshark 定位 RTSP 协议层问题当ffplay连接失败或画面花屏不要急于改代码——先抓包确认问题发生在哪一层。本项目日志极简仅printf(RTSP %s received\n, method)因此网络层验证至关重要。5.1 抓取 RTSP 控制信令与 RTP 数据流在服务器运行时执行# 抓取本地所有端口含 RTSP 8554 和动态 RTP 端口 sudo tcpdump -i lo -w rtsp_debug.pcap port 8554 or portrange 5000-5100 # 或更精准只抓与客户端 IP 的交互假设客户端为 192.168.1.100 sudo tcpdump -i eth0 -w rtsp_client.pcap host 192.168.1.100 and \(port 8554 or portrange 5000-5100\)5.2 Wireshark 分析关键指标导入rtsp_client.pcap后按以下步骤排查问题现象Wireshark 检查点正常表现异常表现无法建立会话过滤rtsp查看DESCRIBE响应返回200 OK SDP含artpmap:96 H264/90000返回404 Not Found或 SDP 缺失afmtp行黑屏无图像过滤rtp rtp.pt96看是否有 RTP 包持续出现 UDP 包Length 20 字节Timestamp稳定递增无 RTP 包或Length恒为 12仅 RTP Header无 Payload画面卡顿/撕裂统计rtp rtp.pt96的Inter-arrival jitter 5ms 50ms且Sequence number出现跳变丢包花屏/马赛克右键 RTP 包 →Decode As→RTP→H264Wireshark 能解析出NAL Unit Type如 7SPS, 8PPS, 5IDR解析失败显示Malformed Packet或NALU Type0非法提示若 Wireshark 无法解析 H264右键 RTP 流 →Protocol Preferences→RTP→H264→Add填入96和90000Clock Rate。这能强制启用 H264 解码器直观看到帧类型分布。5.3 验证 H264 文件合规性1080P.h264必须是 Annex B 格式起始码0x00000001而非 MP4 封装。用hexdump -C 1080P.h264 | head -20检查开头00000000 00 00 00 01 67 42 00 29 00 7e 03 c0 80 00 00 00 |....gB.).~......| 00000010 01 68 ce 3c 80 00 00 00 01 65 88 80 40 00 00 00 |.h......e.....|首 4 字节00 00 00 01确认 Annex B。若为00 00 00 1cMP4 的ftypbox需用ffmpeg -i input.mp4 -c:v copy -vbsf h264_mp4toannexb -f h264 output.h264转换。最终当你在 VLC 地址栏输入rtsp://127.0.0.1:8554/test并看到 1080P 画面流畅播放且tcpdump显示 RTP 包Sequence连续、Timestamp稳定、Jitter低于 10ms就证明这个纯 C 实现的 RTSP 服务器已越过协议栈最陡峭的门槛——它不再是一个玩具而是可嵌入真实系统的协议基石。本文还有配套的精品资源点击获取