hi3516a——H.264数据包 封包为 RTP数据包(附封包源码和详细解析)

前言

       ~~~~~~       由于调试HI3516A进行RTP流媒体播放时,需要清楚怎么把H.264数据包 封包为 RTP数据包并发出去。本文章将详细解析H.264数据包 封包为 RTP数据包的协议格式和源代码。
硬件平台:hi3516a
软件平台:Hi3516A_SDK_V1.0.5.0

       ~~~~~~       H.264数据包 封包为 RTP数据包,网上找了很多资料,但是都不全,所以我尝试结合实例整理出一个比较全面的解析,结合具体的实例也更容易理解些。文章借鉴了很多文章,我都列在了文章最后,在此表示感谢。
       ~~~~~~       无私分享,从我做起!

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("<MAX_RTP_PKT_LENGTH----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
    }
    //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数据包的详细过程。实际使用中,要把上述打印信息屏蔽了,不然打印信息占用的线程很多时间,导致视频出现花屏。
在这里插入图片描述

H.264数据包和RTP数据包详细解析

(1)h264原始数据解析

具体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包头各标识说明如下:

  1. V:RTP协议的版本号,占2位,当前协议版本号为2

  2. P:填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。

  3. X:扩展标志,占1位,如果X=1,则在RTP报头后跟有一个扩展报头

  4. CC:CSRC计数器,占4位,指示CSRC 标识符的个数

  5. M: 标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。

  6. PT: 有效荷载类型,占7位,用于说明RTP报文中有效载荷的类型,如GSM音频、JPEM图像等,在流媒体中大部分是用来区分音频流和视频流的,这样便于客户端进行解析。

  7. 序列号:占16位,用于标识发送者所发送的RTP报文的序列号,每发送一个报文,序列号增1。这个字段当下层的承载协议用UDP的时候,网络状况不好的时候可以用来检查丢包。同时出现网络抖动的情况可以用来对数据进行重新排序,序列号的初始值是随机的,同时音频包和视频包的sequence是分别记数的。

  8. 时戳(Timestamp):占32位,必须使用90 kHz 时钟频率。时戳反映了该RTP报文的第一个八位组的采样时刻。接收者使用时戳来计算延迟和延迟抖动,并进行同步控制。

  9. 同步信源(SSRC)标识符:占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。

  10. 特约信源(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,注意这里是网络字节顺序。

(2)单个NAL单元包

对于 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)进行传输,也可以重复传输。

(3) RTP分包发送

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是不一样) 见下表:

Type Packet Type name

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)。

(4)多帧之间的关联性

下面看下多帧的数据包图片。
第一帧:
在这里插入图片描述
第二帧:
在这里插入图片描述
第三帧:
在这里插入图片描述
第四帧:
在这里插入图片描述
第五帧、第六帧等等…

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 帧将进行对比,减少存储的数据量,进而实现压缩。

(5)封包源码详细剖析

知道了以上的协议知识,下面详细分析代码,一些打印信息已去掉了。

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. 这特别容易出错。
分片过程如下:

  1. 首先计算最大的IP包中IP净荷的长度 =MTU-IP包头长度=1500-20= 1480 bytes。
  2. 然后把3008 bytes按照1480 bytes的长度分片,将要分为3片,3008= 1480+1480+48。
  3. 最后发送者将为3个分片分别添加IP包头,组成3个IP包后再发送,3个IP包的长度分别为1500 bytes、1500 bytes和 68 bytes。
    从以上分片例子可以看出第一、二个分片包组成的IP包的长度都等于MTU即1500 bytes

自此,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

猜你喜欢

转载自blog.csdn.net/u014470361/article/details/89496895