ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DirectShow下RTP发送的实现:从过滤图到H.264打包避坑指南

DirectShow下RTP发送的实现:从过滤图到H.264打包避坑指南 简介DirectShow框架下基于RTP/RTCP协议的实时传输发送端程序面向流媒体协议学习者与网络编程初学者解决如何将自定义数据流按RTP规范封装并发送的问题。程序使用字符串模拟连续数据流通过发送端模块完成序列号分配、时间戳标记、负载打包及RTCP控制信息生成演示了RTP会话建立到数据发送的完整链路。资源包共16个文件包括4个头文件和4个源文件分别对应RTP封装、发送逻辑及对话框界面另有工程配置、图标和说明文档整体压缩后仅21KB结构精简便于快速阅读与二次开发。目前已有142人学习下载适合希望借助可运行示例理解RTP/RTCP协议字段含义、发送时机和DirectShow对接过程的开发者可直接在Visual Studio环境中编译查看发送效果。1. rtp_send 到底是什么DirectShow 里做 RTP 发送卡住的不是协议而是图拿到 rtp_send 这种包名的老工程十有八九是用 DirectShow 把摄像头或本地文件读出来自己打成 RTP 包往网络发。这活儿说难不难说简单也不简单——RTP 协议本身只有 12 字节头RFC 3550 翻二十页就能懂真正让人在 Win32 下反复翻车的是 DirectShow 过滤图里那堆 COM 引脚和媒体类型。下面按 rtp_send 的常见工程结构拆一遍过滤图怎么搭、RTP/RTCP 收发怎么写、SIP 信令怎么和 RTP 配合最后给一份能落地的避坑与验证清单。适合手里攥着 DirectShow 老代码要改、或者正准备从零做 Windows 端 RTP 发送的人。2. 从采集到网络包DirectShow 发送 RTP 的完整链路与选型2.1 过滤图怎么搭源、抓帧器、UDP 出口的三种接法DirectShow 里做 RTP 发送核心不是写协议而是决定数据怎么从采集端走到你的打包代码里。常见的接法有三种差别在中间那一段归谁管。第一种是用老 DirectX SDK 自带的 RTP Render Filter 和 RTP Source Filter把它们当普通滤镜塞进图里连接好之后滤镜内部自己完成打包和 socket 收发。这套东西的优点是省事缺点是微软早就停止维护滤镜行为跟黑匣子一样而且在新版 Windows SDK 里根本找不到对应头文件和 lib想改一个字节的 RTP 头都没有入口。工程上除非是维护十年前的老项目否则不建议新代码往这个方向走。第二种是自己写一个 push source 滤镜在 FillBuffer 里直接推编码后的帧下游接自写的 RTP 发送滤镜。这是最彻底的做法可控性最强但要同时维护两个滤镜的生命周期、引脚协商和媒体类型匹配起步成本高。对一个 rtp_send 这样的小工具来说属于杀鸡用牛刀。第三种是现在的主流做法源滤镜加 SampleGrabber抓帧回调里拿到 IMediaSample自己在回调函数里做 RTP 打包然后直接 sendto 一个 UDP socket。整个媒体链路还是 DirectShow 在管但网络部分完全是你自己的代码。这样做的好处是调试链路短打包逻辑能单测出问题还能用 Wireshark 直接对照抓包。我一般会这么搭[文件源 .avi / 采集设备] → [Splitter] → [解码器] → [SampleGrabber] → 回调函数 ↓ [RTP 打包 sendto UDP socket]这一路里 SampleGrabber 是关键节点。它是一个中转滤镜不改变媒体类型只把经过它的采样复制一份给回调。你既可以在解码器后面抓裸帧YUY2 / I420也可以在某些封装格式分流后直接抓编码流H.264 / MJPEG取决于前面接什么滤镜。对发送 H.264 来说最省带宽、最通用。2.2 负载类型与打包方式为什么 H.264 要选 PT96RTP 头里第 2 字节的低 7 位是 payload type它告诉接收端这包数据是什么编码、用什么时钟频率解析时间戳。RFC 3551 把 0 到 34 之间的值分配给了固定编码比如 0 是 PCMU、8 是 PCMA、26 是 MJPEG96 到 127 是动态区间留给 H.264、H.265、AAC 这类需要 SDP 额外描述参数的编码。实际工程里 H.264 几乎都写在动态区间最常见就是 96。PT编码时钟频率典型场景0PCMU 语音8000 HzSIP 电话8PCMA 语音8000 HzSIP 电话26MJPEG90000 Hz老式 IP 摄像头直出96H.264动态90000 Hz视频通话、监控推流97H.263动态90000 Hz旧式视频终端选 96 不是因为协议规定而是因为它成了事实约定。接收端解析你流的时候不会去猜 PT 对应的编码它只认两份信息RTP 头里的 PT 字段以及 SDP 里 artpmap 行写的映射关系。这两处对不上播放器直接拒收。所以 96 这个值真正的意义是让你在写 SDP 和写 RTP 头的时候少一个出错变量。另一个要早决定的是打包方式。H.264 的 NAL 单元超过 MTU 时不能整包塞进 RTP必须按 RFC 6184 做 FU-A 分片小于 MTU 的直接用单 NAL 单元包。这两条分支的代码都要写但逻辑很简单分片时多两个字节FU indicator 和 FU headerS 位标起始分片、E 位标结束分片中间分片两个标志位都清零。2.3 端口与 SDPRTP 和 RTCP 怎么配对和 SIP 怎么配合协议上有个容易被忽略的硬约定RTCP 的端口是 RTP 端口加 1。RTP 走 5004RTCP 必然走 5005这写死在 RFC 3550 里不是配置项。所以发送端要准备两个 socket接收端也一样。很多人只 bind 了 RTP 端口结果 RTCP 的 SR/RR 包全被系统拒收表现就是画面正常但没有任何质量反馈。那 SIP 在这里面干什么一句话SIP 负责把 SDP 送到对端SDP 决定 RTP 的参数。SIP 信令本身不搬运任何媒体字节INVITE 和 200 OK 里带的 SDP offer/answer把 IP、端口、PT、编码、时钟全部敲定随后媒体流就在这些参数指定的地址上跑 RTP/RTCP。一个最简的 SDP 长这样v0 o- 3415121907 3415121907 IN IP4 192.168.1.20 srtp_send test cIN IP4 192.168.1.20 t0 0 mvideo 5004 RTP/AVP 96 artpmap:96 H264/90000 afmtp:96 packetization-mode1m 行写了传输地址和端口RTP 就往这个端口打artpmap 写了 PT96 对应 H.264、时钟 90000afmtp 里的 packetization-mode1 表示允许非交织的 FU-A 分片。一个案例配合看来就是A 给 B 发 INVITE带上这份 SDPB 回 200 OK在 m 行改成自己的端口A 收到后把 RTP 目标地址改成 B 的 IP 和端口开始推流。整个过程中 SIP 只出现两次之后全交给 UDP 上的 RTP 和 RTCP。3. 用代码把 RTP 发送跑通构建过滤图、抓帧回调与打包3.1 最小可跑的 DirectShow 图源文件进 SampleGrabber一段能编译的最小代码从文件源开始把视频流送进 SampleGrabber。这里假设输入是解码后的原始帧打包逻辑在回调里做。#include dshow.h #include dvdmedia.h IGraphBuilder* pGraph nullptr; ICaptureGraphBuilder2* pBuilder nullptr; CoInitialize(nullptr); // 1. 建过滤图和采集图构建器 CoCreateInstance(CLSID_FilterGraph, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(pGraph)); CoCreateInstance(CLSID_CaptureGraphBuilder2, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(pBuilder)); pBuilder-SetFiltergraph(pGraph); // 2. 加文件源 IBaseFilter* pSrc nullptr; pGraph-AddSourceFilter(LD:\\sample.avi, LFileSrc, pSrc); // 3. 加 SampleGrabber 并设置媒体类型与回调 IBaseFilter* pGrabBase nullptr; CoCreateInstance(CLSID_SampleGrabber, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(pGrabBase)); pGraph-AddFilter(pGrabBase, LGrabber); ISampleGrabber* pGrab nullptr; pGrabBase-QueryInterface(IID_PPV_ARGS(pGrab)); CSampleCB cb; // 自定义回调见 3.2 节 pGrab-SetCallback(cb, 0); // 0 SampleCB 模式 AM_MEDIA_TYPE mt; ZeroMemory(mt, sizeof(mt)); mt.majortype MEDIATYPE_Video; mt.subtype MEDIASUBTYPE_I420; // 让解码器输出 I420 pGrab-SetMediaType(mt); // 4. 自动连接源到抓帧器之间的所有滤镜 pBuilder-RenderStream(PIN_CATEGORY_PREVIEW, MEDIATYPE_Video, pSrc, nullptr, pGrabBase); // 5. 跑起来 IMediaControl* pCtrl nullptr; pGraph-QueryInterface(IID_PPV_ARGS(pCtrl)); pCtrl-Run();这里的几个关键点SetCallback 第二个参数传 0表示回调走 SampleCB收到的 IMediaSample 由 DirectShow 管理你只能在回调函数体内用传 1 的话走 BufferCB那是拿一块连续缓冲。SampleGrabber 的 SetMediaType 必须在连接之前调用它起的是一个类型约束作用让上游解码器知道输出引脚要协商成 I420。如果解码器不支持 I420 而是只给 YUY2把 mt.subtype 换成 MEDIASUBTYPE_YUY2 就行。RenderStream 是偷懒但可靠的写法它会自动在源和抓帧器之间补上 splitter 和解码器。注意别把 PIN_CATEGORY 传错采集设备通常有 preview 和 capture 两个引脚preview 引脚的数据可能不走完整链路用 PIN_CATEGORY_CAPTURE 拿到的才是真正的视频流。3.2 抓帧回调里做 RTP 打包12 字节头、FU-A 分片与关键帧处理回调类实现 ISampleGrabberCB在 SampleCB 里取指针和长度丢给打包函数。class CSampleCB : public ISampleGrabberCB { public: STDMETHODIMP_(ULONG) AddRef() { return 2; } STDMETHODIMP_(ULONG) Release() { return 1; } STDMETHODIMP QueryInterface(REFIID riid, void** ppv) { if (riid IID_IUnknown || riid IID_ISampleGrabberCB) { *ppv this; return S_OK; } return E_NOINTERFACE; } STDMETHODIMP SampleCB(double SampleTime, IMediaSample* pSample) override { BYTE* pData nullptr; pSample-GetPointer(pData); long len pSample-GetActualDataLength(); if (len 0) { uint32_t rtp_ts (uint32_t)(SampleTime * 90000.0 0.5); RtpSendH264(pData, (int)len, rtp_ts); } return S_OK; } STDMETHODIMP BufferCB(double, BYTE*, long) override { return E_NOTIMPL; } };SampleTime 的单位是秒视频时钟是 90000 Hz所以直接相乘取整就是 RTP 时间戳。下一步是打包函数本身包括 RTP 头写入、单 NAL 包和 FU-A 分片三部分。static void PutRtpHeader(BYTE* buf, uint16_t seq, uint32_t ts, uint32_t ssrc, BYTE pt, int marker) { buf[0] 0x80; /* V2, 无填充无扩展 */ buf[1] (BYTE)((marker ? 0x80 : 0) | (pt 0x7F)); buf[2] (BYTE)(seq 8); buf[3] (BYTE)(seq 0xFF); buf[4] (BYTE)(ts 24); buf[5] (BYTE)(ts 16); buf[6] (BYTE)(ts 8); buf[7] (BYTE)(ts 0xFF); buf[8] (BYTE)(ssrc 24); buf[9] (BYTE)(ssrc 16); buf[10] (BYTE)(ssrc 8); buf[11] (BYTE)(ssrc 0xFF); } SOCKET g_sock socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); sockaddr_in g_dst; /* 目标 IP 端口 */ void UdpSend(BYTE* data, int len) { sendto(g_sock, (const char*)data, len, 0, (const sockaddr*)g_dst, sizeof(g_dst)); } void RtpSendH264(BYTE* nal, int len, uint32_t rtp_ts) { static uint16_t seq 0; static uint32_t ssrc 0x12345678; const BYTE PT 96; const int MAX_PAYLOAD 1200; /* 留出余量, 不碰 MTU */ int nal_type nal[0] 0x1F; if (nal_type 5) { /* IDR 帧前先发 SPS/PPS */ RtpSendH264(g_sps, g_sps_len, rtp_ts); RtpSendH264(g_pps, g_pps_len, rtp_ts); } if (len MAX_PAYLOAD) { /* 单 NAL 单元包 */ BYTE pkt[1216]; int marker (nal_type 1 || nal_type 5) ? 1 : 0; PutRtpHeader(pkt, seq, rtp_ts, ssrc, PT, marker); memcpy(pkt 12, nal, len); UdpSend(pkt, 12 len); return; } /* FU-A 分片 */ int parts (len - 1 MAX_PAYLOAD - 1) / MAX_PAYLOAD; for (int i 0; i parts; i) { int pos 1 i * MAX_PAYLOAD; int sz min(MAX_PAYLOAD, len - pos); BYTE pkt[1216]; int marker (i parts - 1) ? 1 : 0; PutRtpHeader(pkt, seq, rtp_ts, ssrc, PT, marker); pkt[12] (BYTE)((nal[0] 0x60) | 28); /* FU indicator, NRI 保留 */ BYTE fh nal[0] 0x1F; /* 原始 NAL type */ if (i 0) fh | 0x80; /* S 位 */ if (i parts - 1) fh | 0x40; /* E 位 */ pkt[13] fh; memcpy(pkt 14, nal pos, sz); UdpSend(pkt, 14 sz); } }这里最容易被忽略的是关键帧处理。H.264 解码器没有 SPS 和 PPS 就解不了 IDR 帧所以发送端必须在每个 IDR 前面把缓存的参数集重新发一遍。g_sps 和 g_pps 从哪里来从回调里捕获 NAL type 为 7 和 8 的包缓存内容或者从解码器输出的媒体类型里拿。新接收端中途加入时也靠这一机制在下一个 IDR 处恢复画面。marker 位置在帧的最后一个 RTP 包上接收端拿它判断一帧边界别随手置 1。3.3 序列号和时间戳两个必须自己维护的计数器RTP 协议不帮你管序列号和时间戳这两个值每个 RTP 包都必须正确错一个就是播放端花屏或者音画不同步。容易混淆的是它们各自的变化节奏序列号是每发一个 RTP 包就加 1不管这一帧切成了几片时间戳是每一帧才变一次同一帧的所有分片共享同一个时间戳。这就是为什么上面的回调把时间戳算好后传进去而 seq 在打包函数内部自增。时间戳时钟必须和 SDP 里 artpmap 写的 90000 一致。视频 25 帧每秒时帧间增量是 90000 / 25 360030 帧时是 3000。如果你直接拿系统毫秒当地时间戳接收端会把你的帧间隔读成 90 毫秒缓冲区越积越厚延迟越来越大。固定帧率可以用增量累加但更稳的是像上面那样用 SampleTime 乘 90000因为 SampleGrabber 给出的时间本身就是基于 DirectShow 参考时钟的不会因为解码器掉帧而漂移。序列号还有一个工程约定初始值建议随机化不要每次启动都从 0 开始。接收端做丢包检测时靠序列号连续性判断如果程序重启后新流和旧流的序列号连续对端可能把两段流合并成一条丢包统计直接报废。随机起点成本为零值得养成习惯。4. RTCP 反馈与时钟映射SR/RR 报文怎么写、读了怎么用4.1 SR 包28 字节里最容易写错的是 NTP 时间RTCP Sender Report 的作用是告诉接收端三个信息我发了多少包、发了多少字节、我本地的 NTP 时钟对应哪个 RTP 时间戳。前两个是纯计数第三个用于音画同步和延迟计算。一个不带报告块的 SR 包固定 28 字节结构如下。void BuildRtcpSR(BYTE* buf, uint32_t ssrc, uint32_t rtp_ts, uint32_t pkt_cnt, uint32_t octet_cnt) { FILETIME ft; GetSystemTimeAsFileTime(ft); uint64_t t100ns ((uint64_t)ft.dwHighDateTime 32) | ft.dwLowDateTime; /* FILETIME 从 1601 年起算, NTP 从 1900 年起算, 差 11644473600 秒 */ const uint64_t NTP_EPOCH_OFFSET 11644473600ULL; uint32_t ntp_sec (uint32_t)(t100ns / 10000000ULL NTP_EPOCH_OFFSET); uint32_t ntp_frac (uint32_t)((t100ns % 10000000ULL) * 4294967296ULL / 10000000ULL); buf[0] 0x80; /* V2, 无填充 */ buf[1] 200; /* PT200: Sender Report */ buf[2] 0; /* 报告块数 0 */ buf[3] 6; /* 长度字段 28/4 - 1 6 */ Write32BigEndian(buf 4, ssrc); Write32BigEndian(buf 8, ntp_sec); Write32BigEndian(buf 12, ntp_frac); Write32BigEndian(buf 16, rtp_ts); Write32BigEndian(buf 20, pkt_cnt); Write32BigEndian(buf 24, octet_cnt); }Write32BigEndian 就是字节序翻转RTP/RTCP 所有整数字段都是大端。NTP 时间是 64 位定点数高 32 位是秒低 32 位是秒的小数部分所以代码里把 FILETIME 的 100 纳秒单位换算成秒再补上 1900 到 1601 之间的偏移。写错偏移量是 RTCP 换算里最高频的坑表现是接收端算出来的单向延迟为负值或者几百年的量级。如果你的对端不做音画同步只读丢包率这块看着像玄学但一旦做延迟统计就绕不过去。SR 包发送周期按 RFC 3550 是 5 秒内至少一次工程上常用 1 到 5 秒可配。周期本身带随机扰动因子避免多个源同时发 RTCP 撞车。发送时机放在一个独立的定时线程里别在 SampleCB 里发回调里做阻塞操作会让 DirectShow 的流直接卡死。提示RFC 3550 要求复合 RTCP 包必须包含 SDES CNAME 项但大量实际实现只发裸 SR/RR接收端也照常工作。要严格合规在 SR 后面追一个 SDES 包即可不影响解析。4.2 RR 包解析丢包率和抖动的读取位置Receiver Report 是接收端发回给发送端的里面带接收质量统计。对发送端来说它是你做码率自适应的唯一天然数据源。RR 包长得和 SR 很像但 PT201而且没有 NTP 和发送计数直接跟在固定的 8 字节头后面就是报告块。第一个报告块从偏移 8 开始/* 收到的 RTCP 报文按 RR 解析 */ BYTE frac_lost rr[12]; /* 丢包率, 单位 1/256 */ DWORD cum_lost (rr[13] 16) | (rr[14] 8) | rr[15]; DWORD max_seq (rr[16] 24) | (rr[17] 16) | (rr[18] 8) | rr[19]; DWORD jitter (rr[20] 24) | (rr[21] 16) | (rr[22] 8) | rr[23]; double lost_ratio frac_lost / 256.0;四个字段的解读各有讲究。frac_lost 是最近一段时间内的丢包比例八位精度255 就是百分之百全丢cum_lost 是从会话开始累计丢包数这个值会被序列号回绕影响判断趋势比判断绝对值有意义jitter 是接收端按 RFC 3550 定义算出的抖动单位是 RTP 时间戳 tick对 90 kHz 时钟换算成毫秒要除以 90。接收端每收到两个 RTP 包就算一次抖动统计平滑后放进 RR所以你看到的 jump 值是瞬时网络状态的平均化。解析 RR 的时候注意一个坑收到的 RTCP 包可能是复合的SR/RR 后面还跟着 SDES、BYE 之类的子包。不要假设包长 28 或 32 字节就结束了一定要用包头里的 length 字段跳转逐个类型识别否则把 SDES 的字节当报告块解析丢包率会是一堆没法看的乱码。4.3 反馈落地最简单的一档码率自适应拿到 RR 反馈最朴素的用法就是阶梯式调码率。这个方案不追求精确但稳而且完全基于反馈闭环不依赖任何网络探测。做法是每个 RTCP 周期做一次评估丢包率超过 2% 就降一档低于 0.5% 且持续一段时间就尝试升一档。/* 伪代码, 放在 RTCP 解析线程里 */ if (frac_lost 2) { bitrate_kbps max(bitrate_kbps / 2, 128); /* 降到下一档 */ up_timer.Reset(); } else if (frac_lost 0.5 up_timer.Elapsed() 10s) { bitrate_kbps min(bitrate_kbps * 3 / 2, max_kbps); up_timer.Reset(); } /* 把 bitrate_kbps 写进编码器, 或者用丢帧策略实现 */这个自适应逻辑放在发送端改的是编码器的目标码率。如果你的链路里编码器是第三方黑匣子没有码率接口降级方案是抽帧设置一个丢帧计数器每 N 帧丢一帧等效降码率画面会有一点卡顿感但延迟能稳住。注意 RTCP 反馈天然滞后至少一个周期才能反映当前网络所以步长要保守每次降一半、升 1.5 倍比 PID 式微调更不容易震荡。5. rtp_send 避坑清单5 个能直接抄的排查记录以下 5 条都是从实际联调里踩出来的前 4 条是血泪经验第 5 条是帮别人查日志查出来的。每条按现象、原因、解决三步给照着对照就行。5.1 画面花屏或者一片绿SPS/PPS 没有跟着 IDR 帧发现象发送端和接收端都跑起来了画面偶尔能出但一切换场景就花或者刷新画面后大块马赛克严重时直接绿屏。原因H.264 解码器解每一个 IDR 帧都需要先拿到 SPS 和 PPS 参数集。你只在程序启动时发过一次参数集播放器中途加入或者丢了一个 UDP 包之后参数集就丢了后续所有帧都属于非法解码状态。解决在回调里识别 NAL type 7 和 8把这两个 NAL 单元缓存成全局变量每次发 IDRtype 5之前先把这个缓存的 SPS/PPS 按单 NAL 包发出去。更稳妥的做法是在 SDP 的 afmtp 行里用 sprop-parameter-sets 带上 Base64 编码的参数集这样接收端从 SDP 就能初始化解码器不依赖 RTP 流里有没有带。5.2 播放器一直不上流SDP 的 rtpmap 和 RTP 头对不上现象ffplay 用你给的 SDP 文件收流窗口能开但一直黑屏控制台报 non-existing PPS 0 referenced 或者直接无输出。原因SDP 里写的是 artpmap:96 H264/90000但代码里打包时 pkt[1] 低 7 位填的是 97或者 fmtp 行的 packetization-mode 写成了 0导致接收端拒绝解 FU-A 分片。解决统一三个地方的 PTSDP 的 m 行、artpmap 行、RTP 头。自查命令一句话tshark -r send.pcap -Y rtp -T fields -e rtp.payload_type -e rtp.ssrc | head -10抓包里显示的实际 PT 和 SDP 对得上问题就在别处对不上改代码重发。播放器对 SDP 的解析是个黑匣子不要猜测先验证三处一致再谈其他。5.3 延迟越拉越大最后卡死RTP 时间戳单位用错了现象刚开始延迟感觉正常几十秒后延迟明显变大几分钟后画面像慢动作最后缓冲爆掉。原因打包代码里直接用 GetTickCount 的毫秒值当 RTP 时间戳。75000 毫秒对应 75 秒的 RTP 时间接收端按 90 kHz 换算后认为这一帧的时间戳前进了 75 秒 × 90000缓冲区只能无限膨胀。解决时间戳必须按 90 kHz 计算。固定帧率用帧序号累加25 帧每秒增量 360030 帧每秒增量 3000用 SampleGrabber 的话直接用 SampleTime 乘 90000见 3.2 节代码。音频如果也要发用 8 kHz 时钟20 毫秒的包增量是 160别和视频共用一个时间戳函数。5.4 两边都能收 RTPRTCP 却互相看不见现象画面正常但发送端永远收不到接收端回的 RR接收端的统计面板里丢包率也是空的。原因RTCP 端口约定是 RTP 端口加 1这是协议默认不是应用层配置。你的发送端只 bind 了 5004RTCP 的 SR 包从 5004 发出去接收端回了 5005 的 RR操作系统直接丢弃因为 5005 上没有任何 socket 在听。解决发送端 bind 两个 UDP socket5004 走 RTP、5005 走 RTCP接收端同样处理。启动后用 netstat 确认监听netstat -un | grep 500两条 UDP 监听都在再抓包看 5005 端口有没有 RTCP 报文进出。这一步排查完RTCP 链路不通的情况基本就剩防火墙了。5.5 SampleGrabber 回调一次都不进接错引脚或者类型约束失败现象过滤图状态是 running视频窗口也能出画面但 SampleCB 里打的日志一行都没打出来。原因两类最常见。一是 RenderStream 用了 PIN_CATEGORY_PREVIEW某些 USB 采集设备的 preview 引脚根本不送数据只有 capture 引脚有流二是 SampleGrabber 的 SetMediaType 设了 I420但前面的解码器只输出 YUY2引脚协商失败DirectShow 为了不让整个图挂掉往往把抓帧器直接桥接成透明传输回调自然一次不触发。解决采集设备一律用 PIN_CATEGORY_CAPTURE拿不准解码器输出什么类型时先用 GraphEdit 或 GraphStudioNext 看实际连接的媒体类型再决定 SetMediaType 传 YUY2 还是 I420。回调函数里加一个计数器Run 起来 3 秒后打印一次确认回调确实在走。另外回调里千万不要做锁、sleep、I/O 这类阻塞操作DirectShow 的流在线程里跑回调卡住整个图就停转。6. 用 Wireshark 和 ffplay 给发送端做体检6.1 抓包先看三个字段SSRC、序列号、时间戳发送端代码写完后第一件事不是接对端而是对着 127.0.0.1 自己抓包。Wireshark 打开 pcapRTP 流的三大体检项是 SSRC、序列号和时间戳。用一条命令看tshark -r send.pcap -Y rtp -T fields \ -e rtp.ssrc -e rtp.seq -e rtp.timestamp | head -20序列号应该每包递增 1中间不能有跳变除非你主动丢包时间戳要按帧阶梯式变化同一帧的分片值相同SSRC 全程不变。三项都正常再进 Wireshark 的 Telephony → RTP → RTP Streams 看丢包率和抖动这两个值在本地回环里应该全零。常备一份好抓包是聊天记录之外的后悔药排查联调问题比看日志直接得多。6.2 SDP 文件加 ffplay 一条命令验证收流准备一个指向本机的 SDP 文件然后直接拉流v0 o- 0 0 IN IP4 127.0.0.1 sRTP recv cIN IP4 127.0.0.1 t0 0 mvideo 5004 RTP/AVP 96 artpmap:96 H264/90000 afmtp:96 packetization-mode1ffplay -protocol_whitelist file,udp,rtp -i receive.sdpffplay 能出画面说明 RTP 打包、端口、SDP 参数全链路是通的。如果画面花回去查第 5.1 条如果不解码查第 5.2 条。这套验证流程里我保留的个人习惯是改完发送端先自拉一条流再看抓包最后才敢往对端推。顺序反了半夜排查的就是两个变量叠加的混合故障。希望帮到你。本文还有配套的精品资源点击获取
返回列表