简介这是一份基于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_guardstd::mutex lock(queue_mutex_); if (frame_queue_.size() 5) { // 防止积压丢旧帧 frame_queue_.pop(); } auto frame std::make_sharedstd::vectorBYTE(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.264ssrc是同步源标识同一路流要固定。marker在一帧的最后一个 RTP 包置 1接收端靠它判断帧边界。时间戳增量 90000 / 帧率比如 30fps 就是 3000。这里最容易翻车的是分片时忘了复制原始 NAL 头的前三位F 和 NRI导致解码器不认。2.3 RTCP 反馈用 RR 和 SR 做丢包与抖动统计RTP 只管发RTCP 负责质量反馈。发送端要定期发 SRSender Report里面带 NTP 时间戳和 RTP 时间戳的对应关系还有发送包数、字节数。接收端回 RRReceiver Report带丢包率、累计丢包数、抖动估计。你可以在发送端解析 RR动态调整编码码率。比如丢包超过 10%就降码率或加 FEC。RTCP 包默认用 UDP 端口 5005RTP 是 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] sent1500 pkts, 2.1 Mbps, loss0.8%, jitter12ms。如果 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 抖动看接收端能不能正常解码。如果花屏严重先查分片逻辑再查时间戳。这套链路我踩过的坑比写过的代码还多希望帮到你。本文还有配套的精品资源点击获取