RTP协议详解
RTP协议负责将流媒体数据封装成特定格式并实现实时传输;即采用RPT数据包格式对流媒体数据进行封装,并通过绑定的协议完成数据包的传输过程;从功能角度来看,RTP仅确保实时数据能够顺利传输,但不具备可靠地按顺序传送数据包的功能,同时也未包含流量控制或拥塞控制功能,这些服务则由其配套的RTCP协议来完成。
一、RTP数据包格式

独立RTU数据流通常由多个RTU数据包构成。每个RTU数据包都包含四个部分:标准头部(固定12字节)、扩展头部、负载信息以及填充信息。其中特殊的数据被存储在特定的扩展字段中。当RTU编码完成时生成的文件无法直接播放这些原始的数据流内容;相反,在捕获到这些原始的数据流时往往还会包含一些多余的协议交互信息以及冗余的内容。
(1) V:RTP协议的版本号,占2位,当前协议版本号为2;
(2) P:填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。如果P=1,需要查看这一个RTP包文最后一个字节(该字节的值只可能为1,2,3),如果字节值是0x01,说明只有一个字节填充;如果是0x02, 说明有两个字节填充;如果字节值是0x03,说明有三个字节填充;
(3) X:扩展标志,占1位,如果X=1,则在RTP报头后跟有一个扩展报头。一般只有hik私有帧(90 F0开头)才会有扩展数据。如果X=1,在RTP头的12个字节之后,会带有以下内容:两个字节的扩展内容说明extenProfile;两个字节的扩展数据长度extenLength;扩展数据4extenLength字节;
(4) CC:CSRC计数器,占4位,指示CSRC 标识符的个数(作用信源CSRC计数器,字节数为4n个)。比如一个rtp头中,cc的值为2,则在RTP的12个字节后会带上42=8个字节的数据,表示CSRC;
(5) M: 标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始;
(6) PT: 有效荷载类型,占7位,用于说明RTP报文中有效载荷的类型,H264:96; G711u:0; g711a:8; aac:104; hik私有数据:112,其他荷载类型,参考官网https://www.ietf.org/assignments/rtp-parameters/rtp-parameters.xml
(7) 序列号:占16位,用于标识发送者所发送的RTP报文的序列号,每发送一个报文,序列号增1。这个字段当下层的承载协议用UDP的时候,网络状况不好的时候可以用来检查丢包。同时出现网络抖动的情况可以用来对数据进行重新排序,序列号的初始值是随机的,同时音频包和视频包的sequence是分别记数的;
(8) 时戳(Timestamp):占32位,必须使用90 kHz 时钟频率。时戳反映了该RTP报文的第一个八位组的采样时刻。接收者使用时戳来计算延迟和延迟抖动,并进行同步控制;
(9) 同步信源(SSRC)标识符:占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。可以简单理解为一路媒体信息的标记,比如码流中所有H264视频的RTP头中这个标记为0x12345678;所有G711音频的RTP头中这个标记为0x11111111;对于hik私有帧,这个标记很多时候都是0x55667788;
(10) 特约信源(CSRC)标识符:每个CSRC标识符占32位,可以有0~15个。每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源。具体数量由前面的CC决定。
二、RTP数据格式解析
我们经常会看到“RTP中视频帧第1个字节是0x80或0xA0,第2个字节是0x60或0xE0”这句话。它具体是怎么来的呢? 首先,将0x80 0x60拆成二进制:1000 0000 0110 0000,第一个字节前两位是10,也就是版本号是2,这个是当前标准规定的;其余位都是0。然而平时填充位是有可能存在的,所以第一个字节为1010 0000,也就是A0;
第二个字节第一位是mark位,如果这位是0,那个第二个字节就是pt值,平时264,265的pt值都是0x60。但是如果是视频帧的首帧,那么mark位就是1,也就是E0。
详细介绍:

随后是两个字节的序号字段(即0x8D 0xA9),从上图中可以看到第二个RTP头中序号字段显示为0x8D 0xAA(即序号会按顺序递增1),最后部分包含4个字节的时间戳字段和4个字节的源端识别码字段。
通常可以通过以下步骤来识别H264编码的流中的RTCP(Round-Trip Congestion)Header:首先,在二进制数据流中搜索特定的十六进制序列(如B'89 57 37'),这通常是RTCP Header的标准起始标志;接着认为该序列可能是RTCP Header,并提取相关的源控制报告编号(SSRC)。一旦确认该序列为RTCP Header,则需要提取相关的源控制报告编号(SSRC)。接下来,在数据流中查找与该SSRC对应的源控制报告编号(SCRP),以验证其有效性。如果发现存在多个可能对应的SCRP,则需进一步分析其起始字节可能以...或其他形式表示,并检查是否存在连续或相近的序号标记以确保其真实性。
三、RTP组包
采用TCP传输协议进行通信时,在网络层中使用的报文格式由以下三部分构成:首先是带有标识符的RTCP标记字段(即标准型RTCP字段),接着是标准型RTCP头部字段,在其后则是标准型RTCP负载数据字段;此外还支持另一种编码方案:即使用RFC编码方式对流媒体进行编码(简称为RFC),以及另一种基于RTSP的编码方案。
其中RTP字段由四个部分组成:起始标记符$加上通道ID占用1个字节、长度占用了2个字节
//以$字符开头,见标准文档rfc2326 10.12 Embedded (Interleaved) Binary Data
一种结构名为INTERLEAVED.Head的方式定义如下:
该结构由以下字段组成:
魔术数:(8位无符号整数)
通道数量:(8位无符号整数)
数据长度:(16位无符号整数)
INTERLEAVED_HEAD() {
nMagic = '$';
nChan = 0;
nDataLen = 0;
}
};
INTERLEAVE_DDimensionalArrayStructure* interleavedArrayHeader = (INTERLEAVE_DDimensionalArrayStructure*)((char*)data - 4);
设置其魔法字段为$。
stInterleavedHead->nChan = iInterLeaved; //用于配置信令交互流程,并将Transport参数传递
stInterleavedHead->nDataLen = Htons(len);
2、基于UDP传输中,无RTP标志;
3、时间戳
计算的时间单位并非常见的秒之类的单位;相反,则采用了由采样频率所替代的其他时间单位来表示。
相邻两个RTP包之间的时间差被称为时间间隔值,并且是以统一的时间间隔值为基准进行测量的。
这里的"间隔值"表示每隔一定时间抽取一次样本。
UINT32 GetRTPTimeStamp(BOOL bMark)
{
//上面个判断,处理初始值
bool bFirstFrame = false;
if ((UINT32)(-1) == m_beginRtpCalcTime) { //first packet
m_beginRtpCalcTime = GetTimeTick();//返回值为微妙
}
if ((UINT32)(-1) == m_lastRtpStampTime) { //first packet
bFirstFrame = true;
m_lastRtpStampTime = 0;
}
//下面逻辑判断,计算时间戳
HPR_UINT32 tmpRtpStampTime = m_lastRtpStampTime;
if (bMark) {
//视频的采样频率是90khz,相当于1秒内采样90000次,则1毫秒采样90次
m_lastRtpStampTime = HPR_UINT32((GetTimeTick()-m_beginRtpCalcTime)*90); //溢出后,自动取低bit,理论上pts不会溢出
if (tmpRtpStampTime >= m_lastRtpStampTime && !bFirstFrame)
{//如果网络发生拥塞,两帧时间戳一样,在前一帧基础上加上
m_lastRtpStampTime = tmpRtpStampTime + 90;
}
}
return tmpRtpStampTime;
}
4、RTP分包发送、流控、mark位
由于RTP(实时传输协议)规定了每组数据的最大容量限制,在实际应用中可能需要将完整的单帧图像分割为多个独立的RTP数据包进行传输。对于视频流中的某一帧而言,在编码过程中通常会将其拆分为若干个独立的分组进行传输以便提高传输效率。需要注意的是,在最后一个分组中应设置其标志位为1以表明该分组尚未结束;而位于同一单个分组中的所有时间戳均应保持一致以保证解码端能够正确恢复原始图像完整性。
int g_iUdpMaxOnePackLen = 1276;
#define TCP_MAX_ONE_FRAME_DATA_LEN 8192
Defines a struct named - of type - with the following fields:
- data buffer pointer: char* pszDataBuf
- integer data length: INT32 iDataLen
- integer payload: INT32 iPayload
- boolean mark: BOOL bMark
- unsigned int timestamp: UINT32 nTimestamp
布尔变量g_isUdpSpeedControl设置为HPR_FALSE(即未启用)。\n\n整数变量g_iUdpIntervalNum设置为3(即每隔一定数量的包执行一次延时 sleep)。\n\n整数变量g_iUdpSleepTimeMs设置为1(即每次睡眠时长设置为多少毫秒)。\n\n定义一个名为SendData的函数接收PACKET_T类型的参数stPack。\n\n在if条件判断是否启用UDP分包配置。\n\n计算并返回数据包总数与最大单帧长度之间的关系。\n\n如果bSubPackageByUdp则会根据数据总长度与最大单帧长度的关系来确定iSendTimes值。\n\n如果数据总长度能被最大单帧长度整除则会将iSendTimes设为其商值;否则会将商值加一作为结果返回)。
当iSendTimes大于1时,
分配src_data缓冲区地址至pSrcData。
设置src markings标志位为true。
初始化data_offset为0。
进入循环体:
若当前使用RTP-over-UDP协议且进行速度控制且UDP间隔计数大于零时,
执行内部逻辑:
自增m_iSendFrameNum并执行模运算:
若运算结果等于零,则执行睡眠操作。
计算当前包的数据长度:
若当前是最后一包,则数据长度设为总长度减去已发送数据长度;
否则,
数据长度设为单包最大值。
最后一包时,
将src markings标志位设为原始标记位;
将data_len字段赋值给stPack.iDataLen;
将data_offset加上前一次的数据偏移量。
发送数据至编码层:
若返回值非零,
执行容错处理流程。
更新data_offset至下一包起始位置。
跳出循环体:
执行直接发送操作。
四、RTP解析
1、RTP解包
struct RTP_HEAD {
unsigned char count : 4;
unsigned char extension : 1;
unsigned char padding : 1;
unsigned char version : 2;
unsigned char payload : 7;
unsigned char marker : 1;
unsigned short sequence;
UINT32 timestamp;
UINT32 ssrc;
};
const HPR_INT32 RTP_HEAD_LEN = sizeof(RTP_HEAD);
struct RTP_HEAD_EXT {
unsigned short id;
unsigned short len;
};
函数体开始:
void UnpackRtpPacket(PACKET_T* pstData, UINT32& ssrc) {
当 pstData->iDataLen 超过 RTP.头的长度时,
获取一个指向 RTP.头指针。
计算填充位的长度:pPaddingBytes 是一个整型变量。
初始化填充位字节为零。
如果 pRtpHead 的 padding 字段等于 1,
计算 CSRC 的长度:iCSRCLen 等于 pRtpHead->count 乘以 sizeof(UINT32)。
计算扩展头字段的长度:iExtenHeadLen 等于零。
如果 pRtpHead 的 extension 字段等于 1,
获取一个指向扩展头字段指针。
将扩展头字段长度转换为整数:将 HPR_Ntohs(pRtpExtHead->len) 转换为 int 型变量。
将 extension 字段设置为零。
计算数据字段的实际长度:pstData->i DataLen 减去 RTP.头的长度减去 CSRC 长度减去扩展头字段长再减去填充位字节长。
如果数据字段的实际长度大于零,
更新数据缓冲区指针至原始位置加上传输参数加上CSRC 长度加上扩展头字段长再加填充位字节长。
更新数据缓冲区的数据字段长至实际计算值。
}
函数体结束:
}
2、RTP排序
可以根据序号,也可以根据时间戳排序;
五、RTP开源库
Jrtplib、ORTP
JRTPLIB 是一个高度集成的 RTP 库,在通常情况下使用它时,并不需要关注 RTCP 数据包的具体发送和接收方式。其内部机制已经完全处理了这些细节。一旦调用 PollData() 或 SendPacket() 方法并获得成功返回值,则该组件能够自动接收并解析所有到达的数据包,并在必要时生成并发送相应的 ACK(确认)。从而保证了整个实时传输协议(RTP)会话的安全性和可靠性。
