一、RTP协议
RTP包由RTP头部和RTP荷载构成: RTP Packet = RTP Header + RTP payload.
1.1 RTP头部(RTP Header)
版本号(V):2Bit,用来标志使用RTP版本
填充位§:1Bit,如果该位置位,则该RTP包的尾部就包含填充的附加字节
扩展位(X):1Bit,如果该位置位,则该RTP包的固定头部后面就跟着一个扩展头部
CSRC技术器(CC):4Bit,含有固定头部后面跟着的CSRC的数据
标记位(M):1Bit,该位的解释由配置文档来承担
载荷类型(PT):7Bit,标识了RTP载荷的类型
序列号(SN):16Bit,发送方在每发送完一个RTP包后就将该域的值增加1,可以由该域检测包的丢失及恢复
包的序列。序列号的初始值是随机的
时间戳(timestamp):32比特,记录了该包中数据的第一个字节的采样时刻, H264/HEVC统一采用90kHz采样时钟,如果使用帧率fps来设置时间戳,则递增数值为90000/fps。
同步源标识符(SSRC):32比特,同步源就是RTP包源的来源。在同一个RTP会话中不能有两个相同的SSRC值
贡献源列表(CSRC List):0-15项,每项32比特,这个不常用
1.2 RTP荷载(RTP Payload)
RTP Payload结构一般分为3种:
- 单NALU分组(Single NAL Unit Packet): 一个分组只包含一个NALU。
- 聚合分组(Aggregation Packet): 一个分组包含多个NALU。
- 分片分组(Fragmentation Unit):一个比较长的NALU分在多个RTP包中。
二、H.264的RTP打包
2.1 H.264格式
H.264由一个一个的NALU组成,每个NALU之间使用00 00 00 01
或00 00 01
分隔开
每个NALU的第一次字节都有特殊的含义,其内容如下
位 | 描述 |
---|---|
bit[7] | 必须为0 |
bit[5-6] | 标记该NALU的重要性 |
bit[0-4] | NALU单元的类型 |
好,对于H.264格式了解这么多就够了,我们的目的是想从一个H.264的文件中将一个一个的NALU提取出来,然后封装成RTP包,下面介绍如何将NALU封装成RTP包
2.2 H.264的RTP打包方式
H.264可以由三种RTP打包方式
-
单NALU打包
一个RTP包包含一个完整的NALU
-
聚合打包
对于较小的NALU,一个RTP包可包含多个完整的NALU
-
分片打包
对于较大的NALU,一个NALU可以分为多个RTP包发送
注意:这里要区分好概念,每一个RTP包都包含一个RTP头部和RTP荷载,这是固定的。而H.264发送数据可支持三种RTP打包方式
比较常用的是单NALU打包
和分片打包
,本文也只介绍这两种
单NALU打包
所谓单NALU打包就是将一整个NALU的数据放入RTP包的载荷中
这是最简单的一种方式,无需过多的讲解
分片打包
每个RTP包都有大小限制的,因为RTP一般都是使用UDP发送,UDP没有流量控制,所以要限制每一次发送的大小,所以如果一个NALU的太大,就需要分成多个RTP包发送,如何分成多个RTP包,下面来好好讲一讲
首先要明确,RTP包的格式是绝不会变的,永远多是RTP头+RTP载荷
RTP头部是固定的,那么只能在RTP载荷中去添加额外信息来说明这个RTP包是表示同一个NALU
如果是分片打包的话,那么在RTP载荷开始有两个字节的信息,然后再是NALU的内容
-
第一个字节位
FU Indicator
,其格式如下
高三位:与NALU第一个字节的高三位相同Type:28,表示该RTP包一个分片,为什么是28?因为H.264的规范中定义的,此外还有许多其他Type,这里不详讲
-
第二个字节位
FU Header
,其格式如下S:标记该分片打包的第一个RTP包
E:比较该分片打包的最后一个RTP包
Type:NALU的Type
2.3 H.264 RTP打包的sdp描述
sdp文件有什么用?
sdp描述着媒体信息,当使用vlc打开这个sdp文件后,会根据这些信息做相应的操作(创建套接字…),然后等待接收RTP包
这里给出RTP打包H.264
的sdp文件,并描述每一行是什么意思
m=video 9832 RTP/AVP 96
a=rtpmap:96 H264/90000
a=framerate:25
c=IN IP4 127.0.0.1
这个一个媒体级的sdp描述,关于sdp文件描述详情可看从零开始写一个RTSP服务器(一)不一样的RTSP协议讲解
-
m=video 9832 RTP/AVP 96
格式为 m=<媒体类型> <端口号> <传输协议> <媒体格式 >
媒体类型:video,表示这是一个视频流端口号:9832,表示UDP发送的目的端口为9832
传输协议:RTP/AVP,表示RTP OVER UDP,通过UDP发送RTP包
媒体格式:表示负载类型(payload type),一般使用96表示H.264
-
a=rtpmap:96 H264/90000
格式为a=rtpmap:<媒体格式><编码格式>/<时钟频率>
-
a=framerate:25
表示帧率
-
c=IN IP4 127.0.0.1
IN:表示internet
IP4:表示IPV4
127.0.0.1:表示UDP发送的目的地址为127.0.0.1
特别注意:这段sdp文件描述的udp发送的目的IP为127.0.0.1,目的端口为9832
type | type value |
---|---|
vps | 32 |
sps | 33 |
pps | 34 |
SEI | 39 |
IDR | 19或者20 |
常用P帧,后置图像帧 | 1 |
H264 的RTP打包代码
int rtpSendH264Frame(int socket, const char* ip, int16_t port,
struct RtpPacket* rtpPacket, uint8_t* frame, uint32_t frameSize)
{
uint8_t naluType; // nalu第一个字节
int sendBytes = 0;
int ret;
naluType = frame[0];
if (frameSize <= RTP_MAX_PKT_SIZE) // nalu长度小于最大包场:单一NALU单元模式
{
/*
* 0 1 2 3 4 5 6 7 8 9
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |F|NRI| Type | a single NAL unit ... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
memcpy(rtpPacket->payload, frame, frameSize);
ret = rtpSendPacket(socket, ip, port, rtpPacket, frameSize);
if(ret < 0)
return -1;
rtpPacket->rtpHeader.seq++;
sendBytes += ret;
if ((naluType & 0x1F) == 7 || (naluType & 0x1F) == 8) // 如果是SPS、PPS就不需要加时间戳
goto out;
}
else // nalu长度小于最大包场:分片模式
{
/*
* 0 1 2
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | FU indicator | FU header | FU payload ... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
/*
* FU Indicator
* 0 1 2 3 4 5 6 7
* +-+-+-+-+-+-+-+-+
* |F|NRI| Type |
* +---------------+
*/
/*
* FU Header
* 0 1 2 3 4 5 6 7
* +-+-+-+-+-+-+-+-+
* |S|E|R| Type |
* +---------------+
*/
int pktNum = frameSize / RTP_MAX_PKT_SIZE; // 有几个完整的包
int remainPktSize = frameSize % RTP_MAX_PKT_SIZE; // 剩余不完整包的大小
int i, pos = 1;
/* 发送完整的包 */
for (i = 0; i < pktNum; i++)
{
rtpPacket->payload[0] = (naluType & 0x60) | 28;
rtpPacket->payload[1] = naluType & 0x1F;
if (i == 0) //第一包数据
rtpPacket->payload[1] |= 0x80; // start
else if (remainPktSize == 0 && i == pktNum - 1) //最后一包数据
rtpPacket->payload[1] |= 0x40; // end
memcpy(rtpPacket->payload+2, frame+pos, RTP_MAX_PKT_SIZE);
ret = rtpSendPacket(socket, ip, port, rtpPacket, RTP_MAX_PKT_SIZE+2);
if(ret < 0)
return -1;
rtpPacket->rtpHeader.seq++;
sendBytes += ret;
pos += RTP_MAX_PKT_SIZE;
}
/* 发送剩余的数据 */
if (remainPktSize > 0)
{
rtpPacket->payload[0] = (naluType & 0x60) | 28;
rtpPacket->payload[1] = naluType & 0x1F;
rtpPacket->payload[1] |= 0x40; //end
memcpy(rtpPacket->payload+2, frame+pos, remainPktSize+2);
ret = rtpSendPacket(socket, ip, port, rtpPacket, remainPktSize+2);
if(ret < 0)
return -1;
rtpPacket->rtpHeader.seq++;
sendBytes += ret;
}
}
out:
return sendBytes;
}
static void rtpSendNAL(RTPMuxContext *ctx, const uint8_t *nal, int size, int last){
printf("rtpSendNAL len = %d M=%d\n", size, last);
// Single NAL Packet or Aggregation Packets
if (size <= RTP_PAYLOAD_MAX){
// Aggregation Packets
if (ctx->aggregation){
/*
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |STAP-A NAL HDR | NALU 1 Size | NALU 1 HDR & Data | NALU 2 Size | NALU 2 HDR & Data | ... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* */
int buffered_size = (int)(ctx->buf_ptr - ctx->buf); // size of data in ctx->buf
uint8_t curNRI = (uint8_t)(nal[0] & 0x60); // NAL NRI
// The remaining space in ctx->buf is less than the required space
if (buffered_size + 2 + size > RTP_PAYLOAD_MAX) {
rtpSendData(ctx, ctx->buf, buffered_size, 0);
buffered_size = 0;
}
/*
* STAP-A/AP NAL Header
* +---------------+
* |0|1|2|3|4|5|6|7|
* +-+-+-+-+-+-+-+-+
* |F|NRI| Type |
* +---------------+
* */
if (buffered_size == 0){
*ctx->buf_ptr++ = (uint8_t)(24 | curNRI); // 0x18
} else {
uint8_t lastNRI = (uint8_t)(ctx->buf[0] & 0x60);
if (curNRI > lastNRI){ // if curNRI > lastNRI, use new curNRI
ctx->buf[0] = (uint8_t)((ctx->buf[0] & 0x9F) | curNRI);
}
}
// set STAP-A/AP NAL Header F = 1, if this NAL F is 1.
ctx->buf[0] |= (nal[0] & 0x80);
// NALU Size + NALU Header + NALU Data
Load16(ctx->buf_ptr, (uint16_t)size); // NAL size
ctx->buf_ptr += 2;
memcpy(ctx->buf_ptr, nal, size); // NALU Header & Data
ctx->buf_ptr += size;
// meet last NAL, send all buf
if (last == 1){
rtpSendData(ctx, ctx->buf, (int)(ctx->buf_ptr - ctx->buf), 1);
}
}
// Single NAL Unit RTP Packet
else {
/*
* 0 1 2 3 4 5 6 7 8 9
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |F|NRI| Type | a single NAL unit ... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* */
rtpSendData(ctx, nal, size, last);
}
} else { // 分片分组
/*
*
* 0 1 2
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | FU indicator | FU header | FU payload ... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* */
if (ctx->buf_ptr > ctx->buf){
rtpSendData(ctx, ctx->buf, (int)(ctx->buf_ptr - ctx->buf), 0);
}
int headerSize;
uint8_t *buff = ctx->buf;
uint8_t type = nal[0] & 0x1F;
uint8_t nri = nal[0] & 0x60;
/*
* FU Indicator
* 0 1 2 3 4 5 6 7
* +-+-+-+-+-+-+-+-+
* |F|NRI| Type |
* +---------------+
* */
buff[0] = 28; // FU Indicator; FU-A Type = 28
buff[0] |= nri;
/*
* FU Header
* 0 1 2 3 4 5 6 7
* +-+-+-+-+-+-+-+-+
* |S|E|R| Type |
* +---------------+
* */
buff[1] = type; // FU Header uses NALU Header
buff[1] |= 1 << 7; // S(tart) = 1
headerSize = 2;
size -= 1;
nal += 1;
while (size + headerSize > RTP_PAYLOAD_MAX) {
memcpy(&buff[headerSize], nal, (size_t)(RTP_PAYLOAD_MAX - headerSize));
rtpSendData(ctx, buff, RTP_PAYLOAD_MAX, 0);
nal += RTP_PAYLOAD_MAX - headerSize;
size -= RTP_PAYLOAD_MAX - headerSize;
buff[1] &= 0x7f; // buff[1] & 0111111, S(tart) = 0
}
buff[1] |= 0x40; // buff[1] | 01000000, E(nd) = 1
memcpy(&buff[headerSize], nal, size);
rtpSendData(ctx, buff, size + headerSize, last);
}
}
三. H.265 RTP 打包原理
如果NALU对应的Slice为一帧的开始,则用4字节表示,即0x00 00 00 01;否则用3字节表示,0x00 00 01。下面都以4个字节为一帧来描述。 RTP打包就是前四个字节0x00 00 00 01去掉,添加上PayloadHdr,然后发送出去。
3.1 H.265 RTP打包格式
单个NAL包:Single NAL unit packet
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| PayloadHdr | DONL (conditional) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| NAL unit payload data |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
在SDP中sprop-max-don-diff = 0时,DONL可以省略;当 0<sprop-max-don-diff<=32767时,DONL不能省略。
去掉原始码流中的“00 00 00 01”,在头部添加tcp(12 bits + 2bits(length))或者udp(12bits)的头,然后发送出去。
分片单元:Fragmentation unit (FU)
1.The structure of an FU
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| PayloadHdr (Type=49) | FU header | DONL (cond) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|
| DONL (cond) | |
|-+-+-+-+-+-+-+-+ |
| FU payload |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2.H.265 PayloadHdr
---------------------------------------
|0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7|
---------------------------------------
|F| Type | LayerId |Tid|
3.The structure of FU header
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|S|E| FuType |
+---------------+
* S = variable
* E = variable
* FuType = NAL unit type
* 第一個包: S=1,E=0;
* 中間包: S=0,E=0
* 最後一包: S=0,E=1
单个NAL包是指size <1500-RTP_HEADER_LENGTH - bits(一般情况为1500,有些可能更小是1300)时,采用单个NAL包发送。当大于这个值时候通常采用FU的发送方式,RTP切片,分为首包,中间包,尾包数据。
将PayloadHdr中type设置为49,设置起始位置S和结束位置E
3.2 H.265 RTP打包的sdp描述
v=0
o=- 0 0 IN IP4 192.168.1.77
s=No Name
c=IN IP4 192.168.1.77
t=0 0
a=tool:libavformat 58.12.100
m=video 1234 RTP/AVP 96
a=rtpmap:96 H265/90000
a=fmtp:96 sprop-vps=QAEMAf//AWAAAAMAsAAAAwAAAwB7rAk=; sprop-sps=QgEBAWAAAAMAsAAAAwAAAwB7oAPAgBDlja5JFL03AQEBAIA=; sprop-pps=RAHA8s5wOzQA
H265的RTP程序代码
int rtpSendH265Frame(int socket, const char* ip, int16_t port,
struct RtpPacket* rtpPacket, uint8_t* frame, uint32_t frameSize)
{
int naluType; // nalu第一个字节
int sendBytes = 0;
int ret;
naluType = (frame[0]>>1) & 0x3F;
if (frameSize <= RTP_MAX_PKT_SIZE) // nalu长度小于最大包场:单一NALU单元模式
{
/*
* 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |F| Type | LayerId |TID|a single NAL unit ...
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
memcpy(rtpPacket->payload, frame, frameSize);
ret = rtpSendPacket(socket, ip, port, rtpPacket, frameSize);
if(ret < 0)
return -1;
rtpPacket->rtpHeader.seq++;
sendBytes += ret;
if (((naluType & 0x7F)>>1) == 32 || ((naluType & 0x7F)>>1) == 33) // 如果是SPS、PPS就不需要加时间戳
goto out;
}
else // nalu长度小于最大包场:分片模式
{ /*
create the HEVC payload header and transmit the buffer as fragmentation units (FU)
0 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F| Type | LayerId | TID |
+-------------+-----------------+
F = 0
Type = 49 (fragmentation unit (FU))
LayerId = 0
TID = 1
*/
/*
* FU Header
* 0 1 2 3 4 5 6 7
* +-+-+-+-+-+-+-+-+
* |S|E|R| Type |
* +---------------+
*/
int pktNum = frameSize / RTP_MAX_PKT_SIZE; // 有几个完整的包
int remainPktSize = frameSize % RTP_MAX_PKT_SIZE; // 剩余不完整包的大小
int i, pos = 2;
/* 发送完整的包 */
for (i = 0; i < pktNum; i++)
{
rtpPacket->payload[0] = 49<<1;
rtpPacket->payload[1] = 1;
rtpPacket->payload[2] = naluType;
//rtpPacket->payload[2] |= 1<<7;
if (i == 0) //第一包数据
rtpPacket->payload[2] |= 1<<7;// start
else if (remainPktSize == 0 && i == pktNum - 1) //最后一包数据
rtpPacket->payload[2] &= ~(1 << 7); // end
memcpy(rtpPacket->payload+3, frame+pos, RTP_MAX_PKT_SIZE);
ret = rtpSendPacket(socket, ip, port, rtpPacket, RTP_MAX_PKT_SIZE+3);
if(ret < 0)
return -1;
rtpPacket->rtpHeader.seq++;
sendBytes += ret;
pos += RTP_MAX_PKT_SIZE;
}
/* 发送剩余的数据 */
if (remainPktSize > 0)
{
rtpPacket->payload[0] = 49<<1;
rtpPacket->payload[1] = 1;
rtpPacket->payload[2] = naluType;
rtpPacket->payload[2] |= 1<<6; //end
memcpy(rtpPacket->payload+3, frame+pos, remainPktSize+3);
ret = rtpSendPacket(socket, ip, port, rtpPacket, remainPktSize+3);
if(ret < 0)
return -1;
rtpPacket->rtpHeader.seq++;
sendBytes += ret;
}
}
out:
return sendBytes;
}
static void rtpSendNAL(RTPMuxContext *ctx, const uint8_t *buf, int size, int last)
{
//RTPMuxContext *rtp_ctx = ctx->priv_data;
int rtp_payload_size = RTP_PAYLOAD_MAX - 3;
int nal_type = (buf[0] >> 1) & 0x3F;
/* send it as one single NAL unit? */
if (size <= RTP_PAYLOAD_MAX) {
/* use the original NAL unit buffer and transmit it as RTP payload */
rtpSendData(ctx, buf, size, last);
} else {
/*
create the HEVC payload header and transmit the buffer as fragmentation units (FU)
0 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F| Type | LayerId | TID |
+-------------+-----------------+
F = 0
Type = 49 (fragmentation unit (FU))
LayerId = 0
TID = 1
*/
ctx->buf[0] = 49 << 1;
ctx->buf[1] = 1;
/*
create the FU header
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|S|E| FuType |
+---------------+
S = variable
E = variable
FuType = NAL unit type
*/
ctx->buf[2] = nal_type;
/* set the S bit: mark as start fragment */
ctx->buf[2] |= 1 << 7;
/* pass the original NAL header */
buf += 2;
size -= 2;
while (size+3 > rtp_payload_size) {
/* complete and send current RTP packet */
memcpy(&ctx->buf[3], buf, rtp_payload_size);
rtpSendData(ctx, ctx->buf, RTP_PAYLOAD_MAX, 0);
buf += rtp_payload_size;
size -= rtp_payload_size;
/* reset the S bit */
ctx->buf[2] &= ~(1 << 7);
}
/* set the E bit: mark as last fragment */
ctx->buf[2] |= 1 << 6;
/* complete and send last RTP packet */
memcpy(&ctx->buf[3], buf, size);
rtpSendData(ctx, ctx->buf, size + 3, last);
}
}
参考文献1:H.265 RTP打包发送以及RTSP抓包分析
参考文献2:从零开始写一个RTSP服务器(一)RTSP协议讲解
参考文献3:FFmpeg
参考文献4:RTP协议介绍以及C语言实现具有发送H.264视频功能的RTP服务器