~~~~~~ 由于调试HI3516A进行RTP流媒体播放时,需要清楚怎么把H.264数据包 封包为 RTP数据包并发出去。本文章将详细解析H.264数据包 封包为 RTP数据包的协议格式和源代码。
硬件平台:hi3516a
软件平台:Hi3516A_SDK_V1.0.5.0
~~~~~~ H.264数据包 封包为 RTP数据包,网上找了很多资料,但是都不全,所以我尝试结合实例整理出一个比较全面的解析,结合具体的实例也更容易理解些。文章借鉴了很多文章,我都列在了文章最后,在此表示感谢。
~~~~~~ 无私分享,从我做起!
下面是H.264数据包 封包为 RTP数据包的源代码,这里面加了很多打印信息,不需要的可以自己去掉。
/**************************************************************************************************
RTSP(Real Time Streaming Protocol),RFC2326
RTP :Real Time Protocol 实时协议
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC |M| PT | sequence number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| synchronization source (SSRC) identifier |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| contributing source (CSRC) identifiers |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| |
| |
| payload |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
************************************************************************************************/
extern unsigned char sps_tmp[256];
extern unsigned char pps_tmp[256];
extern int sps_len;
extern int pps_len;
static int SendNalu264(HndRtp hRtp, char *pNalBuf, int s32NalBufSize)
{
printf("input H.264 raw data----count=%ld------\r\n",s32NalBufSize);
int i=0;
printf("0x");
while(i<100)
{
printf("%x ",pNalBuf[i]);
i++;
}
printf("......\r\n");
char *pNaluPayload;
char *pSendBuf;
int s32Bytes = 0;
int s32Ret = 0;
struct timeval stTimeval;
char *pNaluCurr;
int s32NaluRemain;
unsigned char u8NaluBytes;
pSendBuf = (char *)calloc(MAX_RTP_PKT_LENGTH + 100, sizeof(char)); //#define MAX_RTP_PKT_LENGTH 1400
if(NULL == pSendBuf)
{
s32Ret = -1;
goto cleanup;
}
hRtp->pRtpFixedHdr = (StRtpFixedHdr *)pSendBuf;
hRtp->pRtpFixedHdr->u7Payload = H264;
hRtp->pRtpFixedHdr->u2Version = 2;
hRtp->pRtpFixedHdr->u1Marker = 0;
hRtp->pRtpFixedHdr->u32SSrc = hRtp->u32SSrc;
//计算时间戳
hRtp->pRtpFixedHdr->u32TimeStamp = htonl(hRtp->u32TimeStampCurr * (90000 / 1000));
printf("timestamp:%lld\n",hRtp->u32TimeStampCurr);
if(gettimeofday(&stTimeval, NULL) == -1)
{
printf("Failed to get os time\n");
s32Ret = -1;
goto cleanup;
}
//保存nalu首byte
u8NaluBytes = *(pNalBuf+4);
//设置未发送的Nalu数据指针位置
pNaluCurr = pNalBuf + 5;
//设置剩余的Nalu数据数量
s32NaluRemain = s32NalBufSize - 5;
if ((u8NaluBytes&0x1f)==0x7&&0)
{
printf("(u8NaluBytes&0x1f)==0x7&&0\r\n");
pNaluPayload = (pSendBuf + 12);
if(sps_len>0)
{
memcpy(pNaluPayload, sps_tmp, sps_len);
if(sendto(hRtp->s32Sock, pSendBuf, sps_len+12, 0, (struct sockaddr *)&hRtp->stServAddr, sizeof(hRtp->stServAddr)) < 0)
{
s32Ret = -1;
goto cleanup;
}
}
if(pps_len>0)
{
memcpy(pNaluPayload, pps_tmp, pps_len);
if(sendto(hRtp->s32Sock, pSendBuf, pps_len+12, 0, (struct sockaddr *)&hRtp->stServAddr, sizeof(hRtp->stServAddr)) < 0)
{
s32Ret = -1;
goto cleanup;
}
}
}
//NALU包小于等于最大包长度,直接发送
if(s32NaluRemain <= MAX_RTP_PKT_LENGTH)
{
hRtp->pRtpFixedHdr->u1Marker = 1;
hRtp->pRtpFixedHdr->u16SeqNum = htons(hRtp->u16SeqNum ++);
hRtp->pNaluHdr = (StNaluHdr *)(pSendBuf + 12);
hRtp->pNaluHdr->u1F = (u8NaluBytes & 0x80) >> 7;
hRtp->pNaluHdr->u2Nri = (u8NaluBytes & 0x60) >> 5;
hRtp->pNaluHdr->u5Type = u8NaluBytes & 0x1f;
pNaluPayload = (pSendBuf + 13);
memcpy(pNaluPayload, pNaluCurr, s32NaluRemain);
s32Bytes = s32NaluRemain + 13;
printf("s32Sock, pSendBuf, s32Bytes, 0, (struct sockaddr *)&hRtp->stServAddr, sizeof(hRtp->stServAddr)) < 0)
{
s32Ret = -1;
goto cleanup;
}
#ifdef SAVE_NALU
fwrite(pSendBuf, s32Bytes, 1, hRtp->pNaluFile);
#endif
}
//NALU包大于最大包长度,分批发送
else
{
//指定fu indicator位置
hRtp->pFuInd = (StFuIndicator *)(pSendBuf + 12);
hRtp->pFuInd->u1F = (u8NaluBytes & 0x80) >> 7;
hRtp->pFuInd->u2Nri = (u8NaluBytes & 0x60) >> 5;
hRtp->pFuInd->u5Type = 28;
//指定fu header位置
hRtp->pFuHdr = (StFuHdr *)(pSendBuf + 13);
hRtp->pFuHdr->u1R = 0;
hRtp->pFuHdr->u5Type = u8NaluBytes & 0x1f;
//指定payload位置
pNaluPayload = (pSendBuf + 14);
//当剩余Nalu数据多于0时分批发送nalu数据
while(s32NaluRemain > 0)
{
/*配置fixed header*/
//每个包序号增1
hRtp->pRtpFixedHdr->u16SeqNum = htons(hRtp->u16SeqNum ++);
hRtp->pRtpFixedHdr->u1Marker = (s32NaluRemain <= MAX_RTP_PKT_LENGTH) ? 1 : 0;
/*配置fu header*/
//最后一批数据则置1
hRtp->pFuHdr->u1E = (s32NaluRemain <= MAX_RTP_PKT_LENGTH) ? 1 : 0;
if(hRtp->pFuHdr->u1E==1)
printf("***********the last data**************\r\n");
//第一批数据则置1
hRtp->pFuHdr->u1S = (s32NaluRemain == (s32NalBufSize - 5)) ? 1 : 0;
if(hRtp->pFuHdr->u1S==1)
printf("***********the first data**************\r\n");
s32Bytes = (s32NaluRemain < MAX_RTP_PKT_LENGTH) ? s32NaluRemain : MAX_RTP_PKT_LENGTH;
memcpy(pNaluPayload, pNaluCurr, s32Bytes);
//发送本批次
s32Bytes = s32Bytes + 14;
printf("fu ----count=%d\r\n",s32Bytes);
int i=0;
printf("send data:0x");
while(i<50)
{
printf("%x ",pSendBuf[i]);
i++;
}
printf("......\r\n");
fflush(stdout);
if(sendto(hRtp->s32Sock, pSendBuf, s32Bytes, 0, (struct sockaddr *)&hRtp->stServAddr, sizeof(hRtp->stServAddr)) < 0)
{
s32Ret = -1;
goto cleanup;
}
#ifdef SAVE_NALU
fwrite(pSendBuf, s32Bytes, 1, hRtp->pNaluFile);
#endif
//指向下批数据
pNaluCurr += MAX_RTP_PKT_LENGTH;
//计算剩余的nalu数据长度
s32NaluRemain -= MAX_RTP_PKT_LENGTH;
}
}
cleanup:
if(pSendBuf)
{
free((void *)pSendBuf);
}
printf("\n");
fflush(stdout);
return s32Ret;
}
注意:
上面的代码中加入了很多打印信息,是为了调试看H264封包成RTP数据包的详细过程。实际使用中,要把上述打印信息屏蔽了,不然打印信息占用的线程很多时间,导致视频出现花屏。
具体h264数据格式建议先查看这篇文章(传送门),对H264数据结构有所了解。
00 00 00 01 67: 0x67&0x1f = 0x07 :SPS
00 00 00 01 68: 0x68&0x1f = 0x08 :PPS
00 00 00 01 06: 0x06&0x1f = 0x06 :SEI信息
00 00 00 01 65: 0x65&0x1f = 0x05: IDR Slice
00 00 00 01 61: 0x61&0x1f = 0x01: P帧
(2)RTP包头数据结构
下面先来查看相关的结构体
typedef struct _tagStRtpHandle
{
int s32Sock;
struct sockaddr_in stServAddr;
unsigned short u16SeqNum;
unsigned long long u32TimeStampInc;
unsigned long long u32TimeStampCurr;
unsigned long long u32CurrTime;
unsigned long long u32PrevTime;
unsigned int u32SSrc;
StRtpFixedHdr *pRtpFixedHdr; //rtp固定头,12个字节
StNaluHdr *pNaluHdr; //nalu头,1个字节
StFuIndicator *pFuInd; //fu分包,fu indicator
StFuHdr *pFuHdr; //fu分包,fu header
EmRtpPayload emPayload; //载荷类型
#ifdef SAVE_NALU
FILE *pNaluFile;
#endif
} StRtpObj, *HndRtp;
其中, StRtpFixedHdr结构体是RTP的固定头,共12个字节 (CSRC先忽略),其对应下图的结构。
typedef struct
{
/**//* byte 0 */
unsigned char u4CSrcLen:4; /**//* expect 0 */
unsigned char u1Externsion:1; /**//* expect 1, see RTP_OP below */
unsigned char u1Padding:1; /**//* expect 0 */
unsigned char u2Version:2; /**//* expect 2 */
/**//* byte 1 */
unsigned char u7Payload:7; /**//* RTP_PAYLOAD_RTSP */
unsigned char u1Marker:1; /**//* expect 1 */
/**//* bytes 2, 3 */
unsigned short u16SeqNum;
/**//* bytes 4-7 */
unsigned long u32TimeStamp;
/**//* bytes 8-11 */
unsigned long u32SSrc; /**//* stream number is used here. */
} StRtpFixedHdr;
RTP包头各标识说明如下:
V:RTP协议的版本号,占2位,当前协议版本号为2
P:填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。
X:扩展标志,占1位,如果X=1,则在RTP报头后跟有一个扩展报头
CC:CSRC计数器,占4位,指示CSRC 标识符的个数
M: 标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。
PT: 有效荷载类型,占7位,用于说明RTP报文中有效载荷的类型,如GSM音频、JPEM图像等,在流媒体中大部分是用来区分音频流和视频流的,这样便于客户端进行解析。
序列号:占16位,用于标识发送者所发送的RTP报文的序列号,每发送一个报文,序列号增1。这个字段当下层的承载协议用UDP的时候,网络状况不好的时候可以用来检查丢包。同时出现网络抖动的情况可以用来对数据进行重新排序,序列号的初始值是随机的,同时音频包和视频包的sequence是分别记数的。
时戳(Timestamp):占32位,必须使用90 kHz 时钟频率。时戳反映了该RTP报文的第一个八位组的采样时刻。接收者使用时戳来计算延迟和延迟抖动,并进行同步控制。
同步信源(SSRC)标识符:占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。
特约信源(CSRC)标识符:每个CSRC标识符占32位,可以有0~15个。每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源。
注:基本的RTP说明并不定义任何头扩展本身,如果遇到X=1,需要特殊处理
如上图,红色标记的12个字节就是RTP包头。
下面来详细分析下这个12个字节:0x80 60 0 0 18 37 6f 6e 16 1 a8 c0
0x80 是V_P_X_CC
60 是M_PT
00 00 是SequenceNum
18 37 6f 6e 是Timestamp
16 1 a8 c0 是SSRC
把前两字节换成二进制如下
1000 0000 0110 0000
按顺序解释如下:
10 是V;
0 是P;
0 是X;
0000 是CC;
0 是M;对于视频,标记一帧的结束,注意后面讲解最后一包时,M会设置成1,表示这一帧的结束。
110 0000 是PT;
00 00 是SequenceNum,可以发现自己在自加一,依次增加;
18 37 6f 6e 是Timestamp,同一个rtp分包内的时间戳都是一样的,不同rtp包的时间戳不一样;
16 1 a8 c0是SSRC ,代码里把SSRC设成了自己的ip地址。c0是192,a8是168,1是1,16是22,故本机地址是192.168.1.22,注意这里是网络字节顺序。
对于 NALU 的长度小于 MTU 大小的包, 一般采用单一 NAL 单元模式。其封包结构如下图所示。
也就是说,单包发送时,实际的数据包为:
12个字节RTP头 + 1个字节的nalu(F、NRI、type) + 后面的有效数据
NALU 头由一个字节组成, 它的语法如下:
F:forbidden_zero_bit.1 位,如果有语法冲突,则为 1。当网络识别此单元存在比特错误时,可将其设为 1,以便接收方丢掉该单元。
NRI:nal_ref_idc.2 位,用来指示该NALU 的重要性等级。值越大,表示当前NALU越重要。具体大于0 时取何值,没有具体规定。
Type:5 位,指出NALU 的类型。具体如表所示:
注意:NRI 值为 7 和 8 的NALU 分别为序列参数集(sps)和图像参数集(pps),上面说过。参数集是一组很少改变的,为大量VCL NALU 提供解码信息的数据。其中序列参数集作用于一系列连续的编码图像,而图像参数集作用于编码视频序列中一个或多个独立的图像。如果解码器没能正确接收到这两个参数集,那么其他NALU 也是无法解码的。因此它们一般在发送其它 NALU 之前发送,并且使用不同的信道或者更加可靠的传输协议(如TCP)进行传输,也可以重复传输。
Fragmentation Units (FUs).
而当 NALU 的长度超过 MTU 时, 就必须对 NALU 单元进行分片封包. 也称为 Fragmentation Units (FUs).
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| FU indicator | FU header | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| |
| FU payload |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 14. RTP payload format for FU-A
The FU indicator octet has the following format:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
FU indicator 指示字节的类型域的28,29表示FU-A和FU-B。NRI域的值必须根据分片NAL单元的NRI域的值设置。(此处FU indicator Type是rtp分片类型,和FU header里的type是不一样) 见下表:
0 undefined -
1-23 NAL unit Single NAL unit packet per H.264
24 STAP-A Single-time aggregation packet
25 STAP-B Single-time aggregation packet
26 MTAP16 Multi-time aggregation packet
27 MTAP24 Multi-time aggregation packet
28 FU-A Fragmentation unit
29 FU-B Fragmentation unit
30-31 undefined
The FU header has the following format:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|S|E|R| Type |
+---------------+
S: 1 bit 当设置成1,开始位指示分片NAL单元的开始。当跟随的FU荷载不是分片NAL单元荷载的开始,开始位设为0。
E: 1 bit 当设置成1, 结束位指示分片NAL单元的结束,即, 荷载的最后字节也是分片NAL单元的最后一个字节。
当跟随的FU荷载不是分片NAL单元的最后分片,结束位设置为0。
R: 1 bit
保留位必须设置为0,接收者必须忽略该位。
Type: 5 bits
此处的Type就是NALU头中的Type,取1-23的那个值,表示 NAL单元荷载类型定义
也就是说,RTP分包的数据包结构是:
12个字节RTP头 + 1个字节的FU indicator(F、NRI、type) + 1个字节的FU header + 后面的有效数据
下面结合本例的代码进行讲解。
可以看到,本例中的RTP一直是分包发送的,没有单个的NAL单元包发送。
先看第一个分包:
12个字节的RTP头上面已经解析过了。
7c是FU indicator,二进制为0111 1100,f为0(无语法冲突),NRI为11(表示重要,不可丢弃),type是11100. 即28 ,是FU-A 分包类型。具体FU-A和FU-B分包的详细区别还没查到较好的资料,麻烦知道的网友留言告知一下。
87是FU header ,二进制是1000 0111,S是1(表示start,开始的一包),E是0,R 是0,type是111,即7,表示序列参数集(sps)。
后面接着发的是多个字节的有效数据,这些数据是从原始的H264数据包拆分成多包的有效数据,见下图。
注意观察第二包的FU header是7 ,二进制是0000 0111,S是0(,E是0,R 是0,type是111,即7,表示序列参数集(sps)。
下面再看下最后一个数据包的数据。
可以看到M_PT由60变成了e0,即0110 0000 变为1110 0000,即最高位M变成1,标记一帧的结束,表示是最后一包。
FU header从7变成了47 ,二进制由0000 0111变成0100 0111,S是0,E是1(表示end,最后一包),R 是0,type是111,即7,表示序列参数集(sps)。
下面看下多帧的数据包图片。
第一帧:
第二帧:
第三帧:
第四帧:
第五帧、第六帧等等…
00 00 00 01 67: 0x67&0x1f = 0x07 :SPS
00 00 00 01 68: 0x68&0x1f = 0x08 :PPS
00 00 00 01 06: 0x06&0x1f = 0x06 :SEI信息
00 00 00 01 65: 0x65&0x1f = 0x05: IDR Slice
00 00 00 01 61: 0x61&0x1f = 0x01: P帧
从上面可以看出,只有第一帧类型是67(SPS),并且第一帧中包含68(PPS)、0x06 (SEI信息)和0x65( IDR Slice), 后续的帧中只包含0x61(P帧)的数据。由于第一帧包含SPS、PPS、SEI和IDR slice,所以数据量要比后面的帧多很多,这也恰切体现了压缩了概念,后续的帧基于IDR 帧将进行对比,减少存储的数据量,进而实现压缩。
知道了以上的协议知识,下面详细分析代码,一些打印信息已去掉了。
static int SendNalu264(HndRtp hRtp, char *pNalBuf, int s32NalBufSize)
{
char *pNaluPayload;
char *pSendBuf;
int s32Bytes = 0;
int s32Ret = 0;
struct timeval stTimeval;
char *pNaluCurr;
int s32NaluRemain;
unsigned char u8NaluBytes;
pSendBuf = (char *)calloc(MAX_RTP_PKT_LENGTH + 100, sizeof(char)); // 分配一包数据的内存空间,MAX_RTP_PKT_LENGTH是1400,如果单包发送的最大是1413,即1400有效数据+13个字节,分包发送的话是1414,即1400有效数据+14个字节,这里直接分配1500字节的空间,留了几十字节的余量。
//这里的1500便是MTU,Maximum Transmission Unit,最大传输单元,下面会介绍MTU
if(NULL == pSendBuf)
{
s32Ret = -1;
goto cleanup;
}
hRtp->pRtpFixedHdr = (StRtpFixedHdr *)pSendBuf; //指向分配的内存空间,往这部分内存空间里放数
hRtp->pRtpFixedHdr->u7Payload = H264; //96,视频
hRtp->pRtpFixedHdr->u2Version = 2; //版本2
hRtp->pRtpFixedHdr->u1Marker = 0; //M=0,后面是最后一包时会置1
hRtp->pRtpFixedHdr->u32SSrc = hRtp->u32SSrc; //在RtpCreate初始化函数中已经把hRtp->u32SSrc配置为本机的ip地址了
//计算时间戳
hRtp->pRtpFixedHdr->u32TimeStamp = htonl(hRtp->u32TimeStampCurr * (90000 / 1000));
printf("timestamp:%lld\n",hRtp->u32TimeStampCurr);
if(gettimeofday(&stTimeval, NULL) == -1)
{
printf("Failed to get os time\n");
s32Ret = -1;
goto cleanup;
}
//保存nalu首byte
u8NaluBytes = *(pNalBuf+4); //取出h264原始数据的第5个字节,即nalu头数据(F、NRI、type)
//设置未发送的Nalu数据指针位置
pNaluCurr = pNalBuf + 5; //指向有效数据包
//设置剩余的Nalu数据数量
s32NaluRemain = s32NalBufSize - 5;
if ((u8NaluBytes&0x1f)==0x7&&0) //该分支屏蔽了,不调用
{
printf("(u8NaluBytes&0x1f)==0x7&&0\r\n");
pNaluPayload = (pSendBuf + 12);
if(sps_len>0)
{
memcpy(pNaluPayload, sps_tmp, sps_len);
if(sendto(hRtp->s32Sock, pSendBuf, sps_len+12, 0, (struct sockaddr *)&hRtp->stServAddr, sizeof(hRtp->stServAddr)) < 0)
{
s32Ret = -1;
goto cleanup;
}
}
if(pps_len>0)
{
memcpy(pNaluPayload, pps_tmp, pps_len);
if(sendto(hRtp->s32Sock, pSendBuf, pps_len+12, 0, (struct sockaddr *)&hRtp->stServAddr, sizeof(hRtp->stServAddr)) < 0)
{
s32Ret = -1;
goto cleanup;
}
}
}
//NALU包小于等于最大包长度,直接发送
if(s32NaluRemain <= MAX_RTP_PKT_LENGTH)
{
//12个字节RTP头 + 1个字节的nalu(F、NRI、type) + 后面的有效数据
hRtp->pRtpFixedHdr->u1Marker = 1; //由于是单包,所以这一包发送完了就没下一包了,M置1
hRtp->pRtpFixedHdr->u16SeqNum = htons(hRtp->u16SeqNum ++); //序列号自加1
hRtp->pNaluHdr = (StNaluHdr *)(pSendBuf + 12); //RTP包数据的第13个字节地址
hRtp->pNaluHdr->u1F = (u8NaluBytes & 0x80) >> 7; //F
hRtp->pNaluHdr->u2Nri = (u8NaluBytes & 0x60) >> 5; //NRI
hRtp->pNaluHdr->u5Type = u8NaluBytes & 0x1f; //type
pNaluPayload = (pSendBuf + 13);
memcpy(pNaluPayload, pNaluCurr, s32NaluRemain); //把后面有效数据包拷贝过来
s32Bytes = s32NaluRemain + 13; //12个字节RTP头 + 1个字节的nalu(F、NRI、type) + 后面的有效数据
//以udp方式把pSendBuf指向的RTP数据包发出去
if(sendto(hRtp->s32Sock, pSendBuf, s32Bytes, 0, (struct sockaddr *)&hRtp->stServAddr, sizeof(hRtp->stServAddr)) < 0)
{
s32Ret = -1;
goto cleanup;
}
#ifdef SAVE_NALU
fwrite(pSendBuf, s32Bytes, 1, hRtp->pNaluFile);
#endif
}
//NALU包大于最大包长度,分批发送
else
{
//12个字节RTP头 + 1个字节的FU indicator(F、NRI、type) + 1个字节的FU header + 后面的有效数据
//指定fu indicator位置
hRtp->pFuInd = (StFuIndicator *)(pSendBuf + 12); //RTP包数据的第13个字节地址,fu indicator位置
hRtp->pFuInd->u1F = (u8NaluBytes & 0x80) >> 7;//F
hRtp->pFuInd->u2Nri = (u8NaluBytes & 0x60) >> 5;//NRI
hRtp->pFuInd->u5Type = 28; //FU-A Fragmentation unit
//指定fu header位置
hRtp->pFuHdr = (StFuHdr *)(pSendBuf + 13); ////RTP包数据的第14个字节地址,即fu header位置
hRtp->pFuHdr->u1R = 0; //R=0,后面会配置S 、 E
hRtp->pFuHdr->u5Type = u8NaluBytes & 0x1f; //与Nalu头的type是一样的
//指定payload位置
pNaluPayload = (pSendBuf + 14); //有效数据包
//当剩余Nalu数据多于0时分批发送nalu数据
while(s32NaluRemain > 0)
{
/*配置fixed header*/
hRtp->pRtpFixedHdr->u16SeqNum = htons(hRtp->u16SeqNum ++);//每个包序号增1
hRtp->pRtpFixedHdr->u1Marker = (s32NaluRemain <= MAX_RTP_PKT_LENGTH) ? 1 : 0; //如果剩余字节小于一包的最大值,说明是最后一包,M置1,否则不是最后一包,置0
/*配置fu header*/
//最后一批数据则置1
hRtp->pFuHdr->u1E = (s32NaluRemain <= MAX_RTP_PKT_LENGTH) ? 1 : 0;//如果剩余字节小于一包的最大值,说明是最后一包,E置1,否则不是最后一包,置0
//第一批数据则置1
hRtp->pFuHdr->u1S = (s32NaluRemain == (s32NalBufSize - 5)) ? 1 : 0; //s32NaluRemain 等于 (s32NalBufSize - 5),则说明是第一包数据,S置1
s32Bytes = (s32NaluRemain < MAX_RTP_PKT_LENGTH) ? s32NaluRemain : MAX_RTP_PKT_LENGTH; // 确定一包的字节数,剩余字节小于一包的最大值,说明是最后一包,即一包的数据为剩余字节,否则是一包的最大值
memcpy(pNaluPayload, pNaluCurr, s32Bytes);
//发送本批次
s32Bytes = s32Bytes + 14; //有效数据加上前面的14个字节
if(sendto(hRtp->s32Sock, pSendBuf, s32Bytes, 0, (struct sockaddr *)&hRtp->stServAddr, sizeof(hRtp->stServAddr)) < 0)
{
s32Ret = -1;
goto cleanup;
}
#ifdef SAVE_NALU
fwrite(pSendBuf, s32Bytes, 1, hRtp->pNaluFile);
#endif
//指向下批数据
pNaluCurr += MAX_RTP_PKT_LENGTH; //发完一包,指针移动一包的字节数
//计算剩余的nalu数据长度
s32NaluRemain -= MAX_RTP_PKT_LENGTH;//发完一包,剩余字节减去一包字节数
}
}
cleanup:
if(pSendBuf)
{
free((void *)pSendBuf);
}
printf("\n");
fflush(stdout);
return s32Ret;
}
上面的分包大小涉及到MTU,下面说下MTU。
MTU 最大传输单元(Maximum Transmission Unit,MTU)是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为单位)。最大传输单元这个参数通常与通信接口有关(网络接口卡、串口等)。
下面引自百度的一个例子来说明MTU:
因为协议数据单元的包头和包尾的长度是固定的,MTU越大,则一个协议数据单元的承载的有效数据就越长,通信效率也越高。MTU越大,传送相同的用户数据所需的数据包个数也越低。
MTU也不是越大越好,因为MTU越大, 传送一个数据包的延迟也越大;并且MTU越大,数据包中 bit位发生错误的概率也越大。
MTU越大,通信效率越高而传输延迟增大,所以要权衡通信效率和传输延迟选择合适的MTU。
以以太网传送IPv4报文为例。MTU表示的长度包含IP包头的长度,如果IP层以上的协议层发送的数据报文的长度超过了MTU,则在发送者的IP层将对数据报文进行分片,在接收者的IP层对接收到的分片进行重组。
这里举一个具体的例子说明IP包分片的原理。以太网的MTU值是1500 bytes,假设发送者的协议高层向IP层发送了长度为3008 bytes的数据报文,则该报文在添加20 bytes的IP包头后IP包的总长度是 3028 bytes,因为3028 > 1500,所以该数据报文将被分片,
注意:分片时仅仅对上层的数据进行分片,不需要对原来的IP首部分片,所以要分片的数据长度只有3008,而不是3028. 这特别容易出错。
分片过程如下:
自此,H.264数据包 封包为 RTP数据包的协议格式和源代码终于分析完了。如有错误,还请指出。
累死了,码字不易,先休息会,后面继续分析其他部分,欢迎关注!
参考:
http://www.iosxxx.com/blog/2017-08-09-从零了解H264结构.html
https://blog.csdn.net/chen495810242/article/details/39207305
https://www.cnblogs.com/lidabo/p/4582040.html