资讯中心

DirectShow采集与RTP/RTCP发送实战:从rtp_send到Wireshark排查

📅 2026/9/28 16:28:48
DirectShow采集与RTP/RTCP发送实战:从rtp_send到Wireshark排查
简介这是一份基于DirectShow框架实现的RTP/RTCP实时传输协议发送端程序源码面向学习流媒体传输、网络编程与音视频开发的初学者及进阶开发者帮助理解RTP协议在真实工程中的收发流程与数据封装方式。压缩包共16个文件约21KB包含4个h头文件与4个cpp源文件构成核心逻辑另有ico图标、dsp/dsw工程文件、rc资源脚本及ReadMe说明文档可直接用Visual Studio打开编译运行。程序以字符串模拟数据流通过RTP协议完成发送端的数据传输便于读者对照代码梳理RTCP反馈机制与RTP打包发送的调用关系。目前已有142人学习下载适合作为网络协议课程实验、流媒体入门练手或二次开发的基础模板帮助快速搭建可调试的RTP发送环境并理解DirectShow过滤器协作方式。1. 从 rtp_send.rar 说起DirectShow 采集 RTP/RTCP 发送到底解决什么问题如果你手头有一个叫rtp_send.rar的压缩包标题里还带着 Directshow、RTP、RTCP、rtp_send 这几个词那它大概率是一个 Windows 平台上的实时音视频发送端示例用 DirectShow 从摄像头或采集卡抓帧编码之后通过 RTP 打包发出去同时用 RTCP 做会话控制和状态反馈。这类东西在安防国标平台对接、远程预览、录播推流里非常常见很多人搜「rtp_send」就是因为在现场遇到了start preview failed maybe rtp session false or preview links nun这种报错想找一个能跑通的最小实现。它适合两类人一类是刚接手 DirectShow 采集、需要把本地画面推到网络上的新手另一类是做过多路摄像头、被 RTCP 反馈和会话状态折磨过的老手。核心链路其实就三段——采集、打包、发送难点全在细节DirectShow 的 SampleGrabber 回调在哪个线程、RTP 时间戳怎么递增、RTCP 的 SR/RR 什么时候发。下面按「先立住原理再动手复现最后讲坑」的顺序拆开讲。2. DirectShow 采集链路从 UVC 摄像头到一帧可发送的原始数据2.1 为什么用 DirectShow 而不是 Media Foundation在 Windows 上抓摄像头常见做法有两套DirectShow 和 Media Foundation。rtp_send这类老项目基本都用 DirectShow原因是它的 Filter Graph 模型足够简单SampleGrabber 能直接拿到未压缩帧兼容的 UVC 摄像头和采集卡范围也广。Media Foundation 更现代但异步模型对新手不友好很多国标平台 SDK 至今还在用 DirectShow 的接口约定。选型上我一般这样判断如果只是抓 YUV/RGB 原始帧自己编码DirectShow SampleGrabber 最省事如果要硬编码、低延迟、多路高分辨率再考虑 Media Foundation 或直接上采集卡的 SDK。rtp_send走的是前者所以理解它的采集部分重点就是 Filter Graph 怎么搭、回调怎么接。2.2 搭一个最小采集 Graph 的代码下面这段是典型的 DirectShow 采集骨架用 SampleGrabber 抓帧。注意它只是采集侧编码和 RTP 打包在后面章节。// 初始化 COMDirectShow 所有接口都依赖它 HRESULT hr CoInitializeEx(NULL, COINIT_MULTITHREADED); IGraphBuilder* pGraph NULL; ICaptureGraphBuilder2* pBuilder NULL; IBaseFilter* pCap NULL; // 采集源 IBaseFilter* pGrabberF NULL; // SampleGrabber 过滤器 ISampleGrabber* pGrabber NULL; // 抓帧接口 CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void**)pGraph); CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2, (void**)pBuilder); pBuilder-SetFiltergraph(pGraph); // 绑定第一个视频采集设备多摄像头时这里要按名称/索引选 CoCreateInstance(CLSID_VideoInputDeviceCategory, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)pCap); pGraph-AddFilter(pCap, LCapture Source); // 插入 SampleGrabber并设置它要拿的媒体类型 CoCreateInstance(CLSID_SampleGrabber, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)pGrabberF); pGraph-AddFilter(pGrabberF, LSample Grabber); pGrabberF-QueryInterface(IID_ISampleGrabber, (void**)pGrabber); AM_MEDIA_TYPE mt; ZeroMemory(mt, sizeof(mt)); mt.majortype MEDIATYPE_Video; mt.subtype MEDIASUBTYPE_YUY2; // 常见 UVC 输出格式也可设 RGB24 pGrabber-SetMediaType(mt); pGrabber-SetBufferSamples(FALSE); // 不缓存回调里直接处理 pGrabber-SetOneShot(FALSE); // 连接 采集源 - SampleGrabber - NullRenderer pBuilder-RenderStream(PIN_CATEGORY_CAPTURE, MEDIATYPE_Video, pCap, pGrabberF, NULL);逻辑说明SetBufferSamples(FALSE)表示不复制到内部缓冲回调拿到的是采集缓冲的引用处理要快否则会丢帧。SetMediaType里指定的 subtype 必须和摄像头实际输出一致设错了RenderStream会失败。参数上YUY2 每像素 2 字节RGB24 是 3 字节带宽差 1.5 倍直接影响后面 RTP 打包的 MTU 分片数量。2.3 回调里区分多路摄像头热搜里有人问「c# directshow uvc 回调里区分多个摄像头」这是多路采集的经典问题。SampleGrabber 的回调是全局的如果你给每个摄像头都挂一个 SampleGrabber回调函数里拿不到「这是哪一路」的信息。常见做法是给每路封装一个类回调里通过pSample反查或者干脆每路用独立的 Grabber 对象把路号作为成员变量传进回调上下文。// 每路摄像头一个 GrabberContext回调里靠它区分 struct GrabberContext { int channelId; ISampleGrabber* grabber; }; HRESULT STDMETHODCALLTYPE SampleCB(double t, IMediaSample* pSample) { GrabberContext* ctx (GrabberContext*)m_pContext; // 构造时绑定 BYTE* pData NULL; pSample-GetPointer(pData); long len pSample-GetActualDataLength(); // 用 ctx-channelId 区分是哪一路再送进对应编码器 OnFrame(ctx-channelId, pData, len, pSample-GetTime(NULL, NULL)); return S_OK; }参数说明GetActualDataLength是这一帧的有效字节数不要用GetSize后者是缓冲上限。GetTime拿到的参考时钟时间戳是后面换算 RTP 时间戳的基准单位是 100ns。多路场景下每路的时钟可能不同步建议统一用一个系统时钟做基准否则 RTCP 的抖动计算会失真。3. RTP 打包时间戳、序列号与 MTU 分片怎么设3.1 RTP 头里每个字段的取值逻辑RTP 头 12 字节真正需要你操心的就四个字段序列号、时间戳、SSRC、负载类型。序列号每发一个包加一回绕到 65535 后从 0 继续时间戳按采样率递增视频常用 90000Hz一帧 40ms 就加 3600SSRC 标识这一路流多路摄像头必须不同否则接收端会混流负载类型要和 SDP 里协商的一致比如 H264 用 96动态或 102。很多人第一次写 rtp_send 会犯的错是时间戳按帧号递增结果接收端播放速度完全不对。正确做法是时间戳增量 帧间隔秒数 × 时钟频率。25fps 就是 1/25 × 90000 3600。3.2 一个可复用的 RTP 打包函数import struct, time class RtpSender: def __init__(self, ssrc, payload_type96, clock_rate90000): self.seq 0 self.ssrc ssrc self.pt payload_type self.clock clock_rate self.ts 0 self.last_time None def next_packet(self, payload: bytes, marker: bool, frame_ts: float): # frame_ts 是采集回调给的秒级时间换算成 RTP 时间戳 if self.last_time is None: self.ts 0 else: self.ts int((frame_ts - self.last_time) * self.clock) self.last_time frame_ts header struct.pack( !BBHII, 0x80, # V2, 无填充/扩展/CSRC (0x80 if marker else 0) | self.pt, # M 位 负载类型 self.seq 0xFFFF, self.ts 0xFFFFFFFF, self.ssrc ) self.seq (self.seq 1) 0xFFFF return header payload逻辑说明marker位标记一帧的最后一个包接收端靠它判断帧边界。struct.pack的!表示网络字节序RTP 所有多字节字段都是大端。参数上clock_rate视频固定 90000音频按采样率8000/16000/48000。ssrc建议用随机数生成后固定不要每帧变。3.3 MTU 分片为什么 1400 是常用值以太网 MTU 1500减去 IP 头 20、UDP 头 8、RTP 头 12留给负载的是 1460。但实际链路里可能有 PPPoE、隧道封装所以工程上普遍取 1400 甚至 1200 做保守值。一帧 H264 如果 8KB就要切成 6 个包每个包带同样的时间戳只有最后一个包 marker 置 1。分片本身 RTP 不管是编码层如 H264 的 FU-A负责的。rtp_send里如果直接把整帧塞进一个 UDP 包超过 MTU 后 IP 层会分片一旦丢一个分片整帧就废了而且 NAT 环境下分片包很容易被丢。所以正确做法是在应用层按 1400 切再交给 RTP 打包。4. RTCP 会话控制SR/RR 反馈与 start preview failed 的排查4.1 RTCP 到底在传什么RTP 只管发RTCP 负责「发得怎么样」。核心两种包SRSender Report由发送端周期性发出带发送的包数、字节数和 NTP 时间戳RRReceiver Report由接收端回带丢包率、抖动、最高序列号。发送端拿到 RR 后可以判断网络状况决定是否降码率。rtp_send里如果只发 RTP 不发 RTCP很多平台会认为会话没建立直接报start preview failed maybe rtp session false。因为平台侧要靠 RTCP 的 SR 来同步音视频时间戳没有 SR 它不知道你的时间基准。4.2 发一个最小 SR 包的代码import struct, time def build_sr(ssrc, ntp_sec, ntp_frac, rtp_ts, pkt_count, octet_count): # RTCP 头V2, P0, RC0, PT200(SR), length6 (28字节/4 - 1) header struct.pack(!BBH, 0x80, 200, 6) sender_info struct.pack(!IIIIII, ssrc, ntp_sec, ntp_frac, rtp_ts, pkt_count 0xFFFFFFFF, octet_count 0xFFFFFFFF) return header sender_info # 每 5 秒发一次 SR这是 RFC 3550 推荐的视频最小间隔 def ntp_now(): t time.time() sec int(t) 2208988800 # 1900 到 1970 的秒差 frac int((t - int(t)) * (1 32)) return sec, frac逻辑说明SR 包固定 28 字节length字段是「包长/4 - 1」所以是 6。ntp_sec要加 2208988800 把 Unix 时间转成 NTP 纪元。pkt_count和octet_count是累计值从会话开始一直加不要清零。参数上视频 SR 间隔建议 5 秒音频可以 5 秒以内太频繁会占带宽。4.3 start preview failed 的三条排查路径遇到start preview failed maybe rtp session false or preview links nun按这个顺序查第一看 RTCP 有没有发出去抓包确认对端有没有收到 SR没有 SR 平台就认为会话没起来第二看 SDP 协商的负载类型和实际发的 RTP 头里的 PT 是否一致不一致平台直接丢包第三看 SSRC 是否冲突多路流用了同一个 SSRC平台侧会当成一路预览自然失败。这三条覆盖了现场八成以上的报错。5. 避坑与排查rtp_send 落地时最容易翻车的五个点5.1 现象画面能出但花屏、卡顿原因RTP 分片时 MTU 设太大或者分片后没按序发。IP 层分片在 NAT 下丢一片整帧就废表现就是花屏。解决应用层按 1400 切每个分片带同一时间戳最后一个包 marker 置 1发送顺序严格递增序列号。5.2 现象接收端播放速度忽快忽慢原因RTP 时间戳增量算错常见的是按帧号递增而不是按时间递增或者采集回调的时间戳单位没统一。解决统一用 100ns 或秒做基准时间戳增量 实际帧间隔 × 90000不要用固定值。5.3 现象多路摄像头只有一路能预览原因SSRC 重复或者 RTCP 的 SR 里 SSRC 和 RTP 头不一致。解决每路生成独立随机 SSRC 并全程固定SR 里的 SSRC 必须和 RTP 头一致多路 RTCP 端口可以用同一对端口靠 SSRC 区分。5.4 现象SampleGrabber 回调里处理稍慢就丢帧原因SetBufferSamples(FALSE)时回调持有采集缓冲处理时间超过帧间隔采集源就会覆盖缓冲。解决回调里只做拷贝把编码和发送放到独立线程队列队列满了主动丢旧帧而不是阻塞回调。5.5 现象平台报 rtp session false 但抓包能看到 RTP原因只发了 RTP 没发 RTCP或者 RTCP 端口和 SDP 里声明的不一致。解决确认 SR 按 5 秒周期发出RTCP 端口是 RTP 端口 1除非 SDP 显式指定抓包看对端有没有回 RR。6. 进阶技巧用 Wireshark 验证 rtp_send 的完整链路调 rtp_send 最有效的手段不是加日志是抓包。Wireshark 能直接解析 RTP 和 RTCP把「发得对不对」变成看得见的东西。具体做法在发送端用dumpcap抓 UDP 端口过滤rtp || rtcp然后看三个指标。第一看 RTP 序列号有没有跳变。在 Wireshark 里点开一个 RTP 包展开 RTP 头序列号应该是连续递增的出现大跳变说明中间丢了包或者发送端序列号管理有 bug。第二看时间戳增量是否稳定。选中同一路流的多个包对比时间戳差值25fps 下应该稳定在 3600 附近波动大说明采集回调的时间基准有问题。第三看 RTCP SR 的间隔和内容。过滤rtcpSR 包应该每 5 秒一个里面的包计数和字节计数应该单调递增如果计数不涨说明发送端统计逻辑没接上。下面这个过滤表达式可以直接用# 只看 RTP 和 RTCP排除其他 UDP 干扰 udp and (rtp or rtcp) # 只看某一路 SSRC 的流SSRC 换成你实际的十六进制值 rtp and rtp.ssrc 0x1a2b3c4d # 看 RTCP 的 SR 包 rtcp and rtcp.pt 200参数说明rtp.ssrc在 Wireshark 里是十六进制显示你代码里如果是十进制要转换。rtcp.pt 200过滤 SR205 是 RRBYE 是 203。抓包时建议在发送端和接收端同时抓对比两边看到的序列号能快速定位是发送丢了还是网络丢了。我自己的习惯是任何 rtp_send 的问题先抓 30 秒包看序列号、时间戳、SR 三样八成问题当场就能定位比翻代码快得多。这套链路搭通一次之后后面换编码格式、换分辨率都只是改参数的事。希望帮到你。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案