简介:该资源是一份基于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 字节 // V=2, P=0, X=0, CC=0 -> 第一个字节 0x80 // M=0, PT=96(动态负载类型) -> 第二个字节 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的增量取决于采样率,用字符串模拟时通常按固定步长加,比如每包加 160;ssrc在会话开始时随机生成一次,整个会话保持不变。参数上,负载类型 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; // V=2, P=0, RC=0 sr[1] = 200; // PT=200 表示 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 _WinMain@16。这时候检查「链接器 → 系统 → 子系统」是不是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 里关键行是m=audio 5004 RTP/AVP 96和a=rtpmap:96 PCMU/8000,负载类型和编码要和你发送端一致。
# 用 FFmpeg 拉 RTP 流,需要先写一个 sdp 文件 ffplay -protocol_whitelist file,rtp,udp -i test.sdp逻辑说明:-protocol_whitelist是必须的,否则 FFmpeg 会拒绝解析本地 sdp 里的 rtp 地址。参数上,sdp 文件里的端口要和发送端目标端口一致,负载类型也要一致。
5.4 一个我踩过的坑:时间戳基准别用GetTickCount
GetTickCount精度只有 15.6 毫秒,而且会回绕。RTP 时间戳需要按采样率递增,用GetTickCount算增量会导致抖动。我后来改成用timeGetTime或者直接按包数乘步长,稳定得多。从那以后我每次做 RTP 发送端,都强制把时间戳生成逻辑单独写一个函数,并且用固定步长先跑通,再考虑真实时钟。希望帮到你。
本文还有配套的精品资源,点击获取