简介:这是一份基于DirectShow框架实现的RTP/RTCP实时传输协议发送端程序源码,面向学习流媒体传输、网络编程与Windows平台音视频开发的初学者及进阶开发者,帮助理解RTP数据包封装、RTCP控制反馈与发送流程的完整实现思路。压缩包共16个文件,约21KB,包含4个h头文件与4个cpp源文件构成核心逻辑,另有ico图标、dsp/dsw工程文件、rc资源脚本及ReadMe说明,工程结构完整,可直接用Visual Studio打开编译调试。程序以字符串模拟数据流,通过RTP协议完成发送端收发演示,便于读者对照代码梳理会话建立、数据打包与传输控制的关键环节。目前已有142人学习下载,适合作为网络协议课程实验、流媒体入门练手或二次开发的参考素材,读者可从中获取RTP发送端从工程配置到代码实现的完整脉络,并在此基础上扩展音视频真实数据的传输实验。
1. rtp_send 到底在做什么:从 DirectShow 采集到 RTP 发送的完整链路
如果你手里有一个叫rtp_send.rar的包,里面是 DirectShow 采集加 RTP/RTCP 发送的实现,那你大概率面对的是这样一个场景:本地摄像头或采集卡的视频,要实时推给远端,延迟要低,还得让接收端能感知丢包和抖动。这不是简单的“推流”,而是把 DirectShow 的 SampleGrabber 或自定义 Renderer 拿到的原始帧,经过编码、打包、RTP 封装,再通过 RTCP 做质量反馈。标题里的rtp_send是动作,Directshow是采集源,RTP/RTCP是传输协议栈。适合谁?做安防监控、远程桌面、工业视觉回传的工程师,尤其是那些不想引入完整 WebRTC 或 RTMP 栈、只想在 Windows 上快速打通“采集→发送”链路的团队。我见过太多人卡在 DirectShow 的帧回调线程和 RTP 时间戳对不上,最后音视频不同步,所以这篇会把这条链路拆开,让你能复现也能排错。
2. DirectShow 采集与 RTP 打包:从帧回调到 sendto 的完整实现
2.1 为什么选 DirectShow 做采集源,而不是 Media Foundation
在 Windows 上做视频采集,DirectShow 虽然老,但它的优势是兼容性极好,从 USB 摄像头到采集卡,基本都能用IAMStreamConfig和ISampleGrabber快速拿到未压缩帧。Media Foundation 更现代,但对某些老驱动支持不如 DirectShow 稳。我一般会先用 GraphEdit 或GraphStudioNext搭一个最小图:Capture Source → SampleGrabber → Null Renderer,确认能拿到 RGB24 或 YUV2 数据,再写代码。SampleGrabber 的回调是同步的,在BufferCB里不能做耗时操作,否则会阻塞采集线程,导致丢帧。所以常见做法是:回调里只做内存拷贝,把帧丢进一个环形缓冲区,由独立发送线程去编码和打包。
// SampleGrabber 回调:只拷贝,不阻塞 HRESULT STDMETHODCALLTYPE SampleGrabberCB::BufferCB(double SampleTime, BYTE *pBuffer, long BufferLen) { if (BufferLen <= 0) return S_OK; // 加锁保护环形缓冲区 std::lock_guard<std::mutex> lock(queue_mutex_); if (frame_queue_.size() > 5) { // 防止积压,丢旧帧 frame_queue_.pop(); } auto frame = std::make_shared<std::vector<BYTE>>(pBuffer, pBuffer + BufferLen); frame_queue_.push(frame); cond_var_.notify_one(); return S_OK; }这段代码的关键是frame_queue_的深度控制。如果发送端编码慢,队列会涨,延迟就上去了。我一般设 3 到 5 帧,超过就丢最旧的,保证实时性。SampleTime是 DirectShow 给的流时间,单位是秒,后面 RTP 时间戳要用它来算,不能直接用系统时间。
2.2 RTP 打包:时间戳、序列号与 MTU 分片
拿到一帧原始数据后,先编码。如果标题里的rtp_send是直接发原始 YUV,那带宽会爆炸,所以通常先 H.264 编码。编码后的 NAL 单元要按 RTP 打包。RFC 6184 规定了 H.264 的 RTP 负载格式,小于 MTU 的单个 NAL 可以一个 RTP 包发,大的要分片(FU-A)。时间戳必须是 90kHz 时钟,且同一帧的多个分片时间戳相同。序列号每发一个 RTP 包加一,初始值随机。
// 简化的 RTP 打包:单 NAL 和 FU-A 分片 void send_rtp_packet(uint8_t* nal, int nal_len, uint32_t timestamp, bool marker) { const int MTU = 1400; if (nal_len <= MTU) { // 单包模式 rtp_header_t rtp = {0}; rtp.version = 2; rtp.payload_type = 96; // 动态负载类型 rtp.seq = htons(seq_num_++); rtp.timestamp = htonl(timestamp); rtp.ssrc = htonl(ssrc_); rtp.marker = marker ? 1 : 0; send_packet((uint8_t*)&rtp, sizeof(rtp), nal, nal_len); } else { // FU-A 分片 int offset = 0; int fu_indicator = (nal[0] & 0xE0) | 28; // FU-A 类型 while (offset < nal_len) { int chunk = std::min(MTU - 2, nal_len - offset); uint8_t fu_header = (offset == 0) ? 0x80 : 0; // 起始位 if (offset + chunk >= nal_len) fu_header |= 0x40; // 结束位 fu_header |= (nal[0] & 0x1F); // 原始 NAL 类型 // 组装 RTP 头 + FU indicator + FU header + 数据 // ... 发送逻辑 offset += chunk; } } }参数说明:payload_type常用 96 表示 H.264,ssrc是同步源标识,同一路流要固定。marker在一帧的最后一个 RTP 包置 1,接收端靠它判断帧边界。时间戳增量 = 90000 / 帧率,比如 30fps 就是 3000。这里最容易翻车的是分片时忘了复制原始 NAL 头的前三位(F 和 NRI),导致解码器不认。
2.3 RTCP 反馈:用 RR 和 SR 做丢包与抖动统计
RTP 只管发,RTCP 负责质量反馈。发送端要定期发 SR(Sender Report),里面带 NTP 时间戳和 RTP 时间戳的对应关系,还有发送包数、字节数。接收端回 RR(Receiver Report),带丢包率、累计丢包数、抖动估计。你可以在发送端解析 RR,动态调整编码码率。比如丢包超过 10%,就降码率或加 FEC。RTCP 包默认用 UDP 端口 5005(RTP 是 5004),间隔一般 5 秒,但可以按带宽算。
// 构造 SR 包 void send_sr(uint32_t rtp_timestamp, uint32_t packet_count, uint32_t octet_count) { rtcp_sr_t sr = {0}; sr.header.version = 2; sr.header.pt = 200; // SR sr.ssrc = htonl(ssrc_); // NTP 时间戳:当前时间转 NTP 格式 uint64_t ntp = get_ntp_time(); sr.ntp_sec = htonl(ntp >> 32); sr.ntp_frac = htonl(ntp & 0xFFFFFFFF); sr.rtp_ts = htonl(rtp_timestamp); sr.sender_packet_count = htonl(packet_count); sr.sender_octet_count = htonl(octet_count); send_rtcp((uint8_t*)&sr, sizeof(sr)); }注意 SR 里的 RTP 时间戳要和最近发送的 RTP 包时间戳对应,不能随便填。接收端用这个做音视频同步。如果只发视频,SR 也不能省,否则接收端算不出往返延迟。
3. 避坑与排查:DirectShow 采集和 RTP 发送的 5 个血泪教训
3.1 现象:采集回调卡死,程序无响应
原因:在BufferCB里直接做了编码或 socket 发送,阻塞了 DirectShow 的流线程。
解决:回调只拷贝到队列,另起线程消费。队列深度别超过 5,否则延迟累积。
3.2 现象:接收端花屏,但丢包率不高
原因:FU-A 分片时,每个分片的 RTP 时间戳不一致,或者 FU header 的起始/结束位没设对。
解决:同一帧所有分片时间戳必须相同,起始位只在第一个分片置 1,结束位只在最后一个分片置 1。用 Wireshark 抓包看 RTP 时间戳是否一致。
3.3 现象:RTCP RR 收不到,发送端无法降码率
原因:RTCP 端口没通,或者 NAT 环境下接收端无法回包。
解决:检查防火墙,RTP 和 RTCP 端口要成对开放(通常 5004/5005)。如果跨 NAT,需要 STUN 或中继,但那是另一个话题了。
3.4 现象:时间戳跳变,播放器花屏
原因:DirectShow 的SampleTime在暂停或切换分辨率时会重置,而 RTP 时间戳还在累加。
解决:监听EC_STREAM_CONTROL_STOPPED等事件,重置时间戳基准。或者用系统单调时钟自己算增量,别依赖SampleTime。
3.5 现象:发送端 CPU 占用高,帧率不稳
原因:每帧都重新分配内存,或者 socket 发送用了阻塞模式。
解决:用内存池复用缓冲区,socket 设非阻塞,发送失败就丢包,别重试。RTP 允许丢包,重试反而增加延迟。
4. 进阶技巧:用 RTCP 动态调整码率和验证发送质量
4.1 根据 RR 丢包率动态调码率
发送端解析 RR 里的fraction_lost字段(0-255,实际丢包率 = 值/256)。如果丢包率 > 5%,就把编码码率降 20%;如果 < 1% 且持续 10 秒,就升 10%。这个逻辑要加滞后,避免震荡。我一般用滑动窗口平均,窗口 5 个 RR 包。
// 解析 RR 中的丢包率并调整码率 void handle_rr(uint8_t* rtcp_pkt, int len) { rtcp_rr_t* rr = (rtcp_rr_t*)rtcp_pkt; if (rr->header.pt != 201) return; // 不是 RR uint8_t fraction = rr->fraction_lost; float loss = fraction / 256.0f; static float avg_loss = 0; avg_loss = avg_loss * 0.8f + loss * 0.2f; // 滑动平均 if (avg_loss > 0.05f) { target_bitrate_ = std::max(200000, (int)(target_bitrate_ * 0.8)); } else if (avg_loss < 0.01f) { target_bitrate_ = std::min(4000000, (int)(target_bitrate_ * 1.1)); } // 通知编码器调整码率 encoder_set_bitrate(target_bitrate_); }参数说明:fraction_lost是自上次 RR 以来的丢包比例,不是累计值。avg_loss用指数滑动平均,系数 0.2 表示新值权重。码率上下限根据你的场景定,安防一般 500kbps 到 2Mbps。
4.2 验证发送质量:用 Wireshark 和自定义统计
Wireshark 能解析 RTP 流,看序列号是否连续、时间戳增量是否稳定。但更实用的是在发送端自己统计:每秒发送的 RTP 包数、字节数、RTCP RR 的丢包率。我习惯在日志里打一行:[RTP] sent=1500 pkts, 2.1 Mbps, loss=0.8%, jitter=12ms。如果 jitter 超过 50ms,说明网络抖动大,可以考虑加抖动缓冲或降码率。
| 指标 | 正常范围 | 异常处理 |
|---|---|---|
| 丢包率 | < 2% | 降码率或加 FEC |
| 抖动 | < 30ms | 增大接收端缓冲 |
| 发送间隔 | 33ms ± 5ms | 检查编码线程是否被阻塞 |
| RTCP 间隔 | 5s ± 1s | 检查定时器精度 |
4.3 一个具体技巧:用 SR 的 NTP 时间戳做唇音同步
如果同时发音频和视频,SR 里的 NTP 时间戳就是同步基准。接收端用 NTP 时间对齐音频和视频的 RTP 时间戳,算出相对偏移。发送端要保证音频和视频的 SR 包在同一个 RTCP 复合包里发,或者至少时间接近。我一般每 5 秒发一个复合 RTCP 包,里面包含视频 SR 和音频 SR,这样接收端一次就能拿到两个流的同步信息。
最后说个我自己的习惯:每次调完 RTP 发送,先别急着上生产,用tcpreplay或netem模拟 5% 丢包和 50ms 抖动,看接收端能不能正常解码。如果花屏严重,先查分片逻辑,再查时间戳。这套链路我踩过的坑比写过的代码还多,希望帮到你。
本文还有配套的精品资源,点击获取