
简介该资源是一份基于DirectShow框架实现的RTP/RTCP实时传输协议发送端程序面向学习流媒体传输、网络编程与音视频开发的初学者及进阶开发者用于理解RTP数据封装、RTCP控制反馈与发送流程。压缩包共16个文件约21KB包含4个h头文件与4个cpp源文件构成核心逻辑另有ico图标、dsp/dsw工程文件、rc资源脚本及ReadMe说明整体为可直接编译的VC工程结构。程序以字符串模拟数据流通过RTP协议完成发送端收发演示便于读者对照代码梳理协议栈调用关系与线程处理思路。目前已有142人学习下载。借助该工程读者可快速搭建RTP发送实验环境掌握DirectShow过滤器与RTP/RTCP协同工作机制并在此基础上扩展音视频真实数据流传输是入门流媒体协议不可多得的实践参考。1. 拆开 rtp_send.rar一个用字符串模拟数据流的 RTP/RTCP 发送端到底能跑出什么如果你手头正好有一个rtp_send.rar解压后看到rtp_send.dsw、rtp_sendDlg.cpp、rtp_send.cpp、StdAfx.cpp这一串 MFC 工程文件第一反应大概率是「这玩意儿还能编译吗」。它不是什么新潮的流媒体框架而是一个典型的 VC6 时代 MFC 对话框工程核心干的事只有一件把一段字符串当作数据流通过 RTP 协议打包发出去同时用 RTCP 做最基本的会话控制。换句话说它把「实时传输」这件事从抽象协议拉到了你能单步调试的代码层面。适合谁适合刚接触 Directshow 采集、想搞明白 RTP 包到底怎么组、RTCP 包长什么样的嵌入式或音视频方向从业者。你不需要先有一整套摄像头采集链路用字符串就能把发送端的骨架跑通这是它最实在的价值。2. RTP 发送端在 MFC 工程里怎么落地从工程结构到发包线程2.1 先认清这套工程的文件分工解压rtp_send.rar之后文件列表本身就是一张地图。rtp_send.dsw和rtp_send.dsp是 VC6 的工作区和工程文件负责把后面这些源码串起来编译。rtp_send.cpp里是CRtp_sendApp的实现管应用启动和退出rtp_sendDlg.cpp和rtp_sendDlg.h是主对话框所有按钮响应、发送触发都挂在这里rtp_send.h和Resource.h是资源与常量定义rtp_send.rc、rtp_send.rc2、res目录、MSN.ICO、rtp_send.ico是界面资源。1.cpp和ReadMe.txt通常是作者留的补充说明或临时测试代码别急着删先读一遍。真正跟 RTP 发包逻辑相关的集中在rtp_sendDlg.cpp里。常见做法是对话框上放一个「发送」按钮点击后启动一个工作线程线程里循环构造 RTP 包并调用sendto。StdAfx.cpp和StdAfx.h负责预编译头VC6 工程里如果预编译头配置不对编译报错会非常难查这一点后面避坑章节会细说。2.2 RTP 包头的字段到底怎么填RTP 包头固定 12 字节字段顺序是 V/P/X/CC、M/PT、序列号、时间戳、SSRC。用字符串模拟数据流时最容易翻车的是序列号和时间戳的递增策略。下面这段是发送线程里构造包头的典型写法你可以直接对照自己的rtp_sendDlg.cpp看// 构造 RTP 包头12 字节 // V2, P0, X0, CC0 - 第一个字节 0x80 // M0, PT96(动态负载类型) - 第二个字节 0x60 BYTE rtpHeader[12]; rtpHeader[0] 0x80; // 版本2无填充、无扩展、无CSRC rtpHeader[1] 0x60; // 标记位0负载类型96 rtpHeader[2] (seq 8) 0xFF; // 序列号高字节 rtpHeader[3] seq 0xFF; // 序列号低字节 rtpHeader[4] (ts 24) 0xFF; // 时间戳大端序 rtpHeader[5] (ts 16) 0xFF; rtpHeader[6] (ts 8) 0xFF; rtpHeader[7] ts 0xFF; rtpHeader[8] (ssrc 24) 0xFF; // SSRC标识同步源 rtpHeader[9] (ssrc 16) 0xFF; rtpHeader[10] (ssrc 8) 0xFF; rtpHeader[11] ssrc 0xFF;逻辑说明seq每发一个包加一回绕到 65535 后归零ts的增量取决于采样率用字符串模拟时通常按固定步长加比如每包加 160ssrc在会话开始时随机生成一次整个会话保持不变。参数上负载类型 96 属于动态范围接收端必须知道这个值才能正确解析如果你改成 0 就是 PCMU语义完全不同。2.3 发送线程与 socket 初始化MFC 对话框里直接在主线程sendto会卡界面所以一般开一个AfxBeginThread工作线程。socket 用WSASocket或socket(AF_INET, SOCK_DGRAM, 0)创建 UDP绑定本地端口后向目标地址发送。下面这段是线程函数骨架UINT SendThread(LPVOID pParam) { CRtp_sendDlg* pDlg (CRtp_sendDlg*)pParam; SOCKET s socket(AF_INET, SOCK_DGRAM, 0); sockaddr_in dst; dst.sin_family AF_INET; dst.sin_port htons(5004); // 常见 RTP 端口 dst.sin_addr.s_addr inet_addr(192.168.1.100); UINT16 seq 0; UINT32 ts 0; UINT32 ssrc rand(); char payload[] hello rtp stream; while (pDlg-m_bSending) { BYTE packet[12 sizeof(payload)]; // ... 填充 rtpHeader略 ... memcpy(packet 12, payload, sizeof(payload)); sendto(s, (char*)packet, 12 sizeof(payload), 0, (sockaddr*)dst, sizeof(dst)); seq; ts 160; Sleep(20); // 约50包/秒 } closesocket(s); return 0; }逻辑说明m_bSending是对话框成员按钮控制它来启停线程Sleep(20)决定发包节奏20 毫秒一包对应 50pps这个值直接影响到接收端的抖动缓冲。参数上目标端口 5004 是 RTP 默认端口RTCP 通常用 5005两者成对出现。如果你只发 RTP 不发 RTCP很多标准接收端会认为会话不完整。2.4 RTCP 发送端最小实现RTCP 至少要有 SR发送者报告和 BYE。SR 包结构比 RTP 复杂包含 NTP 时间戳、RTP 时间戳、包计数、字节计数。用字符串模拟时包计数和字节计数可以真实累加NTP 时间戳用GetSystemTime转换。下面是一个 SR 包的关键字段填充// RTCP SR 包简化版不含报告块 BYTE sr[28]; sr[0] 0x80; // V2, P0, RC0 sr[1] 200; // PT200 表示 SR sr[2] 0x00; sr[3] 0x06; // 长度字段单位是32位字减一 // ... 写入 SSRC、NTP、RTP时间戳、包计数、字节计数 ...逻辑说明RTCP 包长度字段以 32 位字为单位值等于总长度除以 4 再减一。SR 里的 NTP 时间戳用于接收端做音视频同步RTP 时间戳则和 RTP 包头里的一致。常见做法是每发送若干个 RTP 包就插一个 RTCP SR比例大约 1:20具体看带宽。3. 编译与联调VC6 工程在现代环境下的存活姿势3.1 用 VS 打开 dsw 的正确流程直接双击rtp_send.dsw在现代 Windows 上大概率会失败因为 VC6 的工作区格式太老。我一般会先用 VS2019 或 VS2022 的「打开项目」选rtp_send.dsp让 IDE 走一遍升级向导。升级后重点检查三处字符集VC6 默认多字节新版默认 Unicode、预编译头设置、以及#include winsock2.h和#include windows.h的顺序。顺序反了会报一堆重定义错误这是血泪经验。3.2 链接 ws2_32.lib 与设置入口点RTP 发送端必须链接 Winsock 库。在工程属性里找到「链接器 → 输入 → 附加依赖项」加上ws2_32.lib。如果是 MFC 工程入口点通常是WinMain但如果你把rtp_send.cpp里的InitInstance改坏了会报unresolved external symbol _WinMain16。这时候检查「链接器 → 系统 → 子系统」是不是Windows以及「高级 → 入口点」有没有被误填。3.3 用 Wireshark 验证包到底发出去没有编译通过只是第一步包有没有正确发出要用 Wireshark 看。过滤条件写rtp或udp.port 5004。正常应该看到序列号连续、时间戳按固定步长递增的 RTP 包。如果 Wireshark 解析不出来右键选「Decode As」强制按 RTP 解析。这一步能帮你区分是发送端没发还是接收端没解。# Wireshark 显示过滤器示例 udp.port 5004 rtp # 只看 RTCP udp.port 5005 rtcp逻辑说明RTP 和 RTCP 通常成对出现端口相邻。如果只看到 RTP 没有 RTCP说明你的发送端漏了 RTCP 定时器。参数上Wireshark 的 RTP 解析依赖负载类型和 SSRC如果 SSRC 每次发包都变解析会乱。4. 避坑与排查字符串模拟数据流时最容易翻车的五件事4.1 现象接收端能收到包但播放全是杂音原因负载类型和实际数据不匹配。发送端填了 96但接收端按 PCMU 解或者采样率对不上。字符串模拟时没有真实采样率概念接收端如果按 8000Hz 解时间戳步长 160 刚好但如果按 44100Hz 解步长就错了。解决固定负载类型和采样率约定在ReadMe.txt里写清楚或者直接在代码里用宏定义。发送端和接收端必须用同一套参数。4.2 现象编译报错error C2065: sockaddr_in : undeclared identifier原因Winsock 头文件包含顺序不对或者没有定义WIN32_LEAN_AND_MEAN。MFC 的afxwin.h会间接包含windows.h而windows.h里旧版 Winsock 和winsock2.h冲突。解决在StdAfx.h最顶部加#define WIN32_LEAN_AND_MEAN然后#include winsock2.h放在#include afxwin.h之前。如果还不行把#include windows.h显式去掉让 MFC 自己管。4.3 现象程序运行后界面卡死按钮点不动原因sendto或Sleep写在了主线程的消息响应函数里。MFC 对话框的消息循环被阻塞界面自然不刷新。解决把发送循环放进AfxBeginThread创建的工作线程主线程只负责按钮状态切换。线程里不要直接操作控件用PostMessage通知主线程更新 UI。4.4 现象Wireshark 抓到包但序列号跳变或重复原因seq变量作用域不对或者多线程同时改。如果发送线程里seq是局部变量每次线程重启都从 0 开始接收端会认为是旧包丢弃。解决seq、ts、ssrc都做成对话框类的成员变量线程启动时不要重置ssrc。序列号回绕是正常的但短时间内跳变一定是代码问题。4.5 现象RTCP 包发不出去或者接收端不认原因RTCP 包长度字段算错或者端口没对上。RTCP 默认端口是 RTP 端口加一但很多实现允许自定义。长度字段如果填错接收端解析会直接丢弃。解决用sizeof(sr)算总字节数长度字段填(sizeof(sr) / 4) - 1。发送 RTCP 的 socket 可以和 RTP 共用一个但目标端口要加一。定时器用SetTimer或线程Sleep控制间隔一般 5 秒以内。5. 进阶技巧把字符串发送端改造成可复用的 RTP 发包骨架5.1 把负载抽成独立函数现在你的rtp_sendDlg.cpp里构造包和发送是揉在一起的。想复用到真实音视频数据第一步是把负载来源抽象出来。我一般会定义一个回调或虚函数// 负载提供者接口返回实际字节数和时间戳增量 class IPayloadSource { public: virtual int GetData(BYTE* buf, int maxLen) 0; virtual UINT32 GetTimestampStep() 0; virtual ~IPayloadSource() {} };逻辑说明字符串模拟时GetData返回固定字符串GetTimestampStep返回 160。换成真实采集时GetData从 Directshow 的 SampleGrabber 回调里取数据GetTimestampStep按实际采样率算。这样发送线程完全不用改。5.2 用 RTCP SR 里的包计数做丢包统计RTCP SR 里的「发送包计数」和「发送字节计数」不是摆设。接收端用这两个值结合自己收到的包数就能算出丢包率。你可以在发送端加一个定时器每秒打印一次当前计数和 Wireshark 抓到的包数对一下很快就能发现哪里漏了。字段含义字符串模拟时的取值SSRC同步源标识会话开始时随机一次包计数累计发送的 RTP 包数每发一包加一字节计数累计负载字节数每包加负载长度NTP 时间戳绝对时间GetSystemTime 转换RTP 时间戳相对时间与 RTP 包头一致5.3 验证方法自己发自己收最省事的验证是写一个极简接收端用同一个 socket 绑定 5004收到包后打印序列号和时间戳。如果序列号连续、时间戳步长稳定说明发送端没问题。然后再用 VLC 或 FFmpeg 的 SDP 文件去拉流看能不能解。SDP 里关键行是maudio 5004 RTP/AVP 96和artpmap:96 PCMU/8000负载类型和编码要和你发送端一致。# 用 FFmpeg 拉 RTP 流需要先写一个 sdp 文件 ffplay -protocol_whitelist file,rtp,udp -i test.sdp逻辑说明-protocol_whitelist是必须的否则 FFmpeg 会拒绝解析本地 sdp 里的 rtp 地址。参数上sdp 文件里的端口要和发送端目标端口一致负载类型也要一致。5.4 一个我踩过的坑时间戳基准别用GetTickCountGetTickCount精度只有 15.6 毫秒而且会回绕。RTP 时间戳需要按采样率递增用GetTickCount算增量会导致抖动。我后来改成用timeGetTime或者直接按包数乘步长稳定得多。从那以后我每次做 RTP 发送端都强制把时间戳生成逻辑单独写一个函数并且用固定步长先跑通再考虑真实时钟。希望帮到你。本文还有配套的精品资源点击获取