欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

2.3 H264数据封装RTP包

程序员文章站 2022-07-14 18:45:43
...

前面已经说过,整个程序是利用多线程的生产者消费者模式,

1、线程SAMPLE_COMM_VENC_GetVencStreamProc产生视频数据-----生产者
	//不同于前面的1.1 海思3518 H264编码,这次不是将视频数据保存在本地文件中,
	//而是直接将数据放到一个环形缓冲区里让消费者取走通过网络发送出去。

2、线程SAMPLE_COMM_VENC_GetVencStreamProc
	通过select多路复用IO来取得数据,但不保存,直接调用HisiPutH264DataToBuffer函数将数据发送出去

2、schedule_do线程发送出去----消费者


★不直接发送,使用缓冲区的原因:
1、生产和消费(发送)速度可能不匹配
2、以空间换取程序的低耦合性
	消费者也不是直接发送缓冲区里的数据,而是先将缓冲区的数据复制到一个RTP的结构体里再发送,
	这种操作增加了内存的消耗,但却降低了程序的耦合性,可以很方便地分离、修改各个子模块。

base64编码处理

对base64处理后的sps pps进行解读的工具下载

视频数据其实就是一些普通的char数据,有些数据如果太小用ascii码会显示不出来,如ACK是0x06,用ascii显示不出来,因为网络设备的多样性,一些网络设备如路由器交换机会将一些不能用ascii码显示出来的数据会过滤掉,直接丢弃,

SPS、PPS数据:如果直接按照原样发送出去,很可能有数据被过滤掉了
			所以必须进行处理,防止被网络过滤

一般业内最常用的是base64编码。base64编码就是将AZ,az,0~9,+,/总共64个字符(网络设备不会过滤这64个字符)做成一个base64表,然后将SPS、PPS每3个数据总计24位分成4个6位的数据,每6位数据不会大于64,对应base64表中的一个字符,这样就可以将sps、pps安全发送出去了。

用RTP传输H264的时候,需要用到sdp协议描述,其中有两项:Sequence Parameter Sets (SPS) 和Picture Parameter Set (PPS)需要用到,
那么这两项从哪里获取呢?答案是从H264码流中获取.在H264码流中,都是以"0x00 0x00 0x01"或者"0x00 0x00 0x00 0x01"为开始码的,
找到开始码之后,使用开始码之后的第一个字节的低5位判断是否为7(sps)或者8(pps), 及data[4] & 0x1f == 7 || data[4] & 0x1f == 8.
然后对获取的nal去掉开始码之后进行base64编码,得到的信息就可以用于sdp.sps和pps需要用逗号分隔开来.

SDP中的H.264的SPS和PPS串,包含了初始化H.264解码器所需要的信息参数,包括编码所用的profile,level,图像的宽和高,deblock滤波器等。

base64解码工具使用:
用法是在命令行中输入:spsparser sps.txt pps.txt output.txt
例如 sps.txt中的内容为:Z0LgFNoFglE=
pps.txt中的内容为:aM4wpIA=
最终解析得到的结果为:
2.3 H264数据封装RTP包
这里需要特别提一下这两个参数
pic_width_in_mbs_minus1 = 21
pic_height_in_mbs_minus1 = 17
分别表示图像的宽和高,以宏块(16x16)为单位的值减1
因此,实际的宽为 (21+1)*16 = 352 高为 (17+1)*16 = 288

SAVEH264_TO_LOCAL宏定义抉择:保存成文件 or 放入fifo
//保存成文件后于抓包数据可以拿来对比观察打包实现方式

#if SAVEH264_TO_LOCAL
                    s32Ret = SAMPLE_COMM_VENC_SaveStream(enPayLoadType[i], pFile[i], &stStream);
                    if (HI_SUCCESS != s32Ret)
                    {
                       free(stStream.pstPack);
                        stStream.pstPack = NULL;
                        SAMPLE_PRT("save stream failed!\n");
                        break;
                    }
		#endif
                   HisiPutH264DataToBuffer(&stStream);		//	发送出去

HisiPutH264DataToBuffer函数代码如下:

**************************************************************************************************
**将H264流数据放到ringfifo[iput].buffer里以便schedule_do线程从ringfifo[iput].buffer里取出数据发送出去
**同是在DESCRIBE步骤中会对SPS,PPS编码发送给客户端,后面好像就只编码但没有发送出去
**
**************************************************************************************************/
HI_S32 HisiPutH264DataToBuffer(VENC_STREAM_S *pstStream)
{
	HI_S32 i,j;
	HI_S32 len=0,off=0,len2=2;
	unsigned char *pstr;
	int iframe=0;
	for (i = 0; i < pstStream->u32PackCount; i++)
	{
		len+=pstStream->pstPack[i].u32Len-pstStream->pstPack[i].u32Offset;
	}
 
	int testlen=0;
    if(n<NMAX)
    {
		for (i = 0; i < pstStream->u32PackCount; i++)
		{
			memcpy(ringfifo[iput].buffer+off,pstStream->pstPack[i].pu8Addr+pstStream->pstPack[i].u32Offset,pstStream->pstPack[i].u32Len-pstStream->pstPack[i].u32Offset);
			//printf("\nff=%x",pstStream->pstPack[i].u32Offset);
			pstr=pstStream->pstPack[i].pu8Addr+pstStream->pstPack[i].u32Offset;//计算当前PACK的有效数据的首地址 = 基地址 + 偏移
			/*
			if(pstr[4]==0x68)
			{
				printf("\nx=%p",ringfifo[iput].buffer+off);
				for(testlen=0;testlen<pstStream->pstPack[i].u32Len-pstStream->pstPack[i].u32Offset;testlen++)
					printf(" %x ",*(ringfifo[iput].buffer+off+testlen));
			}
			*/
			//pstr=pstStream->pstPack[i].pu8Addr+pstStream->pstPack[i].u32Offset;//这一行一定要,要不然后面的if判断pstr[4]
			//如果在这里不要,pstr[4]会不存在,从而提示段错误
/*
			if(pstr[4]==0x67)
			{
				printf("a=%d,%p ",*(ringfifo[iput].buffer+off),ringfifo[iput].buffer+off);
			}
			*/
			off+=pstStream->pstPack[i].u32Len-pstStream->pstPack[i].u32Offset;//计算下个PACK存放到ring里的首地址
			//上面那个off计算应该移到下面//aaaaaaaaaaaaaa处???????????????????????????????
			//pstr=pstStream->pstPack[i].pu8Addr+pstStream->pstPack[i].u32Offset;//计算当前PACK的有效数据的首地址
 
			if(pstr[4]==0x67)
			{//使用 base64 对 data 进行编码,设计此种编码是为了使二进制数据可以通过
			//非纯 8-bit 的传输层传输,例如电子邮件的主体
			//在网络上基本上是非纯8位传输,所以要将数据在服务器编码为
			//base64(非8位的,6位的),然后由客户端解码
			//故需要将H264数据用base64编码然后才发送
				UpdateSps(ringfifo[iput].buffer+off,9);
				//printf("b=%d,%p ",*(ringfifo[iput].buffer+off),ringfifo[iput].buffer+off);
				iframe=1;
			}
			if(pstr[4]==0x68)
			{
				//printf("\ny=%p",ringfifo[iput].buffer+off);
				//for(testlen=0;testlen<pstStream->pstPack[i].u32Len-pstStream->pstPack[i].u32Offset;testlen++)
				//	printf(" %x ",*(ringfifo[iput].buffer+off+testlen));
				UpdatePps(ringfifo[iput].buffer+off,4);
			}
			//off+=pstStream->pstPack[i].u32Len-pstStream->pstPack[i].u32Offset;//计算下个PACK存放到ring里的首地址
			//aaaaaaaaaaaaaa
			// if (pstStream->pstPack[i].u32Len[1] > 0)
			// {
				// memcpy(ringfifo[iput].buffer+off,pstStream->pstPack[i].pu8Addr[1], pstStream->pstPack[i].u32Len[1]);
				// off+=pstStream->pstPack[i].u32Len[1];
			// }
		}
 
        ringfifo[iput].size= len;
		//printf("iframe=%d\n",iframe);
		if(iframe)
		{
			ringfifo[iput].frame_type = FRAME_TYPE_I;
		}
        	
		else
			ringfifo[iput].frame_type = FRAME_TYPE_P;
        iput = addring(iput);
        n++;
    }
 
	 return HI_SUCCESS;
}

HisiPutH264DataToBuffer函数首先会判断环形队列是否未满if(n<NMAX),如果满了,直接跳过if,如果没满进入if,将数据从pstStream复制到环形缓冲区ringfifo里,然后根据第4个字节(从0开始计算,因为H264数据开头都是00 00 00 01 xx,所以是第4个字节)来判断是sps还是pps,然后分别调用不同长度的base64编码并将编码结果保存到psp.base64sps全局变量里。
至于UpdateSps(ringfifo[iput].buffer+off,9)函数,只编码9个字节,并不是一个H264片的长度,这长度为9与H264片长度无关,这里编码是为了发送SPS信息过去,取个名字而已,VLC那边定了9个字节,所以这边也要弄9个字节,在session0106.log里有一行打印为psp.base64sps=0X2Lt7/k0PXC,这里的“0X2Lt7/k0PXC”就是对SPS编码得到的,另一个psp.base64pps=whMx9P里的“whMx9P”就是UpdatePps(ringfifo[iput].buffer+off,4)函数编码得到的。

消费者—发送线程schedule_do

可以发现schedule_do线程是个死循环,nanosleep休眠,让出CPU,33微秒超时时间,超时时间一到就唤醒重新获得CPU往下运行先从环形缓冲区里取得数据ringbuflen = ringget(&ringinfo);然后正常情况下计算时间戳,判断帧类型,用结构体成员函数指针sched[i].play_action((unsigned int)(sched[i].rtp_session->hndRtp), ringinfo.buffer, ringinfo.size, mnow);调用RtpSend函数将ringinfo的数据打包RTP包。在source insight里搜索play_action引用,就会发现在schedule_add函数里有sched[i].play_action=RtpSend;说明play_action函数指针指向RtpSend函数,调用的是RtpSend函数。代码注释上也注释了play_action函数指针调用的是RtpSend函数。我们进入RtpSend函数发现代码如下:

schedule_do线程概述:
{
	nanosleep休眠33微妙
	先从环形缓冲区里取得数据ringbuflen = ringget(&ringinfo);
	然后正常情况下计算时间戳,判断帧类型,
	用结构体成员函数指针sched[i].play_action((unsigned int)(sched[i].rtp_session->hndRtp), ringinfo.buffer, ringinfo.size, mnow);
			即调用RtpSend函数将ringinfo的数据打包RTP包。
			//在source insight里搜索play_action引用,就会发现在schedule_add函数里有sched[i].play_action=RtpSend;说明play_action函数指针指向RtpSend函数,调用的是RtpSend函数。
			//代码注释上也注释了play_action函数指针调用的是RtpSend函数。
}
void *schedule_do(void *arg)
{
    int i=0;
    struct timeval now;
    unsigned long long mnow;
    char *pDataBuf, *pFindNal;
    unsigned int ringbuffer;
    struct timespec ts = {0,33333};
    int s32FileId;
    unsigned int u32NaluToken;
    char *pNalStart=NULL;
    int s32NalSize;
    int s32FindNal = 0;
    int buflen=0,ringbuflen=0,ringbuftype;
    struct ringbuf ringinfo;
//=====================
#ifdef RTSP_DEBUG
    printf("The pthread %s start\n", __FUNCTION__);
#endif
 
    do
    {
        nanosleep(&ts, NULL);  	//nanosleep休眠,让出CPU,33微秒超时时间
        						//超时时间一到就唤醒重新获得CPU往下运行
//      trace_point();
 
        s32FindNal = 0;
 
        //如果有客户端连接,则g_s32DoPlay大于零
      //  if(g_s32DoPlay>0)
        {
            ringbuflen = ringget(&ringinfo);
            if(ringbuflen ==0)
                continue ;
        }
        s32FindNal = 1;
        for(i=0; i<MAX_CONNECTION; ++i)
        {
            if(sched[i].valid)
            {
                if(!sched[i].rtp_session->pause)
                {
                    //计算时间戳
                    gettimeofday(&now,NULL);
                    mnow = (now.tv_sec*1000 + now.tv_usec/1000);//毫秒
                    //sched[i].rtp_session->hndRtp这里只是判断hndRtp是否不为空
                    //sched[i].rtp_session->hndRtp=RtpCreate(unsigned int u32IP, int s32Port, EmRtpPayload emPayload),RtpCreate会返回一个数字,然后被强制
                    //转为hndRtp结构体指针
                    if((sched[i].rtp_session->hndRtp)&&(s32FindNal))//sched[i].rtp_session->hndRtp=RtpCreate(unsigned int u32IP, int s32Port, EmRtpPayload emPayload)
                    {
                        //printf("send i frame,length:%d,pointer:%x,timestamp:%lld\n",ringinfo.size,(int)(ringinfo.buffer),mnow);
                        buflen=ringbuflen;
                        if(ringinfo.frame_type ==FRAME_TYPE_I)
                            sched[i].BeginFrame=1;
                        //if(sched[i].BeginFrame== 1)
                        //sched[i].play_action=RtpSend(unsigned int u32Rtp, char *pData, int s32DataSize, unsigned int u32TimeStamp)
                        sched[i].play_action((unsigned int)(sched[i].rtp_session->hndRtp), ringinfo.buffer, ringinfo.size, mnow);
                    }
                }
            }
 
        }
        //============add================
        //===============================
    }
    while(!stop_schedule);
 
cleanup:
    //free(pDataBuf);
    //close(s32FileId);
 
#ifdef RTSP_DEBUG
    printf("The pthread %s end\n", __FUNCTION__);
#endif
    return ERR_NOERROR;
}

play_action函数指针调用 即RtpSend函数:

unsigned int RtpSend(unsigned int u32Rtp, char *pData, int s32DataSize, unsigned int u32TimeStamp)
{
    int s32NalSize = 0;
    char *pNalBuf, *pDataEnd;
    HndRtp hRtp = (HndRtp)u32Rtp;
    unsigned int u32NaluToken;
 
    hRtp->u32TimeStampCurr = u32TimeStamp;
 
    if(_h264 == hRtp->emPayload)//发送H264文件,有多个NALU单元,需要找出00000001来分离NALU
    {//printf("\n\t\th264");
        pDataEnd = pData + s32DataSize;
        //搜寻第一个nalu起始标志0x01000000
        for(; pData < pDataEnd-5; pData ++)
        {
            memcpy(&u32NaluToken, pData, 4 * sizeof(char));
            if(0x01000000 == u32NaluToken)
            {
                //标记nalu起始位置
                pData += 4;
                pNalBuf = pData;
                break;
            }
        }
        //发送nalu
        for(; pData < pDataEnd-5; pData ++)
        {
            //搜寻第二个nalu起始标志0x01000000,找到nalu起始位置,发送该nalu数据
            //
            memcpy(&u32NaluToken, pData, 4 * sizeof(char));
            if(0x01000000 == u32NaluToken)
            {
                s32NalSize = (int)(pData - pNalBuf);//二者一相减就是第一个nalu的内容
                if(SendNalu264(hRtp, pNalBuf, s32NalSize) == -1)
                {
                    return -1;
                }
 
                //标记nalu起始位置
                pData += 4;
                pNalBuf = pData;
            }
        }//while
//最后一个nalu
        if(pData > pNalBuf)
        {
            s32NalSize = (int)(pData - pNalBuf);
            if(SendNalu264(hRtp, pNalBuf, s32NalSize) == -1)
            {
                return -1;
            }
        }
    }
    else if(_h264nalu == hRtp->emPayload)//直接发送NALU单元,所以不需要分享NALU
    {//在rtsp的setup阶段时创建RTP套接字时设置了负荷类型为_h264nalu,所以程序执行这个分支
    //原因3518编码已经帮我们分开了每一个H264的slice,即直接是一个NALU,所以直接添加东西组成RTP即可
    	//printf("\n\t\th264nalu");经打印也验证是执行这个分支,即是_h264nalu
        if(SendNalu264(hRtp, pData, s32DataSize) == -1)
        {
            return -1;
        }
    }
    else if(_g711 == hRtp->emPayload)
    {printf("\n\t\tg711");
        if(SendNalu711(hRtp, pData, s32DataSize) == -1)
        {
            return -1;
        }
    }
    else
    {
        return -1;
    }
 
    return 0;
}

RtpSend函数里面根据H264类型有很多分枝,那用得是哪个分枝呢?我们可以在emPayload的引用里发现对emPayload赋值的只有一个地方即在函数RtpCreate里hRtp->emPayload = emPayload;再搜索RtpCreate函数引用,发现这个函数只有一个地方被调用了rtp_s->hndRtp = (struct _tagStRtpHandle*)RtpCreate((unsigned int)(((struct sockaddr_in *)(&pRtsp->stClientAddr))->sin_addr.s_addr), Transport.u.udp.cli_ports.RTP, _h264nalu);可以确定emPayload类型是_h264nalu了,所以RtpSend函数里用的是else if(_h264nalu == hRtp->emPayload)分支。当然如果觉得查看代码的方式确定分支有点困难,可以添加打印语句,如在if(_h264 == hRtp->emPayload)分支里添加打印语句printf("\n\t\th264");在else if(_h264nalu == hRtp->emPayload)分支里添加语句printf("\n\t\th264nalu");然后运行程序,在session0106.log里发现打印了h264nalu,也验证了我们前面分析代码确定是if(_h264 == hRtp->emPayload)分支的想法是正确的。

SendNalu264这个函数:H264打包RTP包并发送sendto

注:pNalBuf实际应该使不包含start_code的 不然代码逻辑有问题

/**************************************************************************************************
**将H264NALU变成RTP,需要加一些东西
**参数:
	hRtp:				in/out	需要填写的RTP结构体
	pNalBuf:			in 		H264NALU的内容(包含start code :00 00 00 01)
	s32NalBufSize:		in 		H264NALU的大小
**
//注意这里的pNalBuf包含了00 00 00 01这几个首字节
抓包第0x2a-0x35=12个字节表示RTP头
		第0x36为FA-U指示器(需要H264第一个00参与计算)
		第0x37为FA-U头(需要H264第一个00参与计算)
		第0x38以后为真正的H264数据,如果是第一个FA-U包的话的,因为H264的第一个00未编进来,
		所以抓包看H264开头为00 00 01,少了一个00
发送的缓冲区pSendBuf第0-11字节为RTP头
							第12字节为FA-U指示器
							第13字节为FA-U头部
							第14字节开始与H264数据的第1个字节一一对应(第0字节没有存进来)
查资料感觉FA-U头低5位表示这个FA-U包的类型,应该与真正的H264的NALU的类型一致,即应该是
00 00 00 01 67的第4个字节67参与计算
**************************************************************************************************/
static int SendNalu264(HndRtp hRtp, char *pNalBuf, int s32NalBufSize)
{
    char *pNaluPayload;
    char *pSendBuf;
    int s32Bytes = 0;//本次发送的rtp包长度
    int s32Ret = 0;		
    struct timeval stTimeval;
    char *pNaluCurr;
    int s32NaluRemain;		//输入原始数据的余下待打包发送的长度
    unsigned char u8NaluBytes;		//输入数据的nalu头
 
    pSendBuf = (char *)calloc(MAX_RTP_PKT_LENGTH + 100, sizeof(char));
    if(NULL == pSendBuf){
        s32Ret = -1;
        goto cleanup;
    }
//由H264NALU变成RTP,需要增加一些东西,具体内容网上查RTP协议
//pRtpFixedHdr:RTP固定头部fix固定,Hdr:header头部
//1、单包打包:pNaluHdr:Nalu包头部
//2、UF-A分包打包:(FU-A=FU-A indicator+FU-A header +分片单元荷载)
//			pFuInd:FU-A indicator	FU-A 包指示器
//			pFuHdr:FU-A header 		FU-A 包头部

/*
pSendBuf指向一块大小1500字节的缓冲区
	单个NAL包: rtp固定头(0-11字节) + nalu头(12字节) + nalu数据(13字节开始)
	FU-A分包打包: rtp固定头(0-11字节) + fu indicator(12字节) + fu header(13字节) + nalu数据(13字节开始)
*/


//0、rtp头数据填充
  	hRtp->pRtpFixedHdr = (StRtpFixedHdr *)pSendBuf;	//表明hRtp->pRtpFixedHdr指向了pSendBuf的前面12字节
	hRtp->pRtpFixedHdr->u2Version   = 2;//RTP协议的版本号
 
	hRtp->pRtpFixedHdr->u1Marker    = 0;//对视频来说表示标记一帧的结束,后面会被改写为1
    	hRtp->pRtpFixedHdr->u7Payload   = H264;//有效荷载类型,简单点用96表示无类型
 
	//hRtp->pRtpFixedHdr->u16SeqNum每一个包不同,在后面填写
	

    hRtp->pRtpFixedHdr->u32TimeStamp = htonl(hRtp->u32TimeStampCurr * (90000 / 1000));//时间戳
	//printf("sendnalu264 timestamp:%lld\n",hRtp->u32TimeStampCurr);
 
	hRtp->pRtpFixedHdr->u32SSrc  = hRtp->u32SSrc;//同步信源(SSRC)标识符,一般填客户机的IP
	//hRtp->pRtpFixedHdr->u32CSrc//协议里还有特约信源(CSRC)标识符,这里没有,不填
    
    if(gettimeofday(&stTimeval, NULL) == -1)
    {
        printf("Failed to get os time\n");
        s32Ret = -1;
        goto cleanup;
    }
 
    //保存nalu首byte
    u8NaluBytes = *pNalBuf;//注意这里的pNalBuf包含了00 00 00 01这几个首字节,所以u8NaluBytes=00
    //设置未发送的Nalu数据指针位置
    pNaluCurr = pNalBuf + 1;
/*这里的pNaluCurr是真正的H264数据的第二个,第一个被赋值给了u8NaluBytes参与FU-A indicator与FU-A header 	的计算
如果原来的H264数据为:00 00 00 01 xx xx xx......
那么u8NaluBytes为第一个00
pNaluCurr则为00 00 01 xx xx xx......
所以后面 memcpy(pNaluPayload, pNaluCurr, s32Bytes)时pNaluPayload也为00 00 01 67 xx xx xx......这样发送出去后抓包发现
FU-A的第一个包为00 00 01 xx xx xx......
*/
    //设置剩余的Nalu数据数量
    s32NaluRemain = s32NalBufSize - 1;
 
    //NALU包小于等于最大包长度,直接发送
    if(s32NaluRemain <= MAX_RTP_PKT_LENGTH)    //单包打包
    {
        hRtp->pRtpFixedHdr->u1Marker    = 1;//对视频来说表示标记一帧的结束,这里一只一个NALU包,一包即结束,所以为1
        hRtp->pRtpFixedHdr->u16SeqNum   = htons(hRtp->u16SeqNum ++);//标识发送者所发送的RTP报文的***
        															//用网络字节表示
        //nalu头
        hRtp->pNaluHdr                  = (StNaluHdr *)(pSendBuf + 12);	//单包打包 hRtp->pNaluHdr 指向了pSendBuf的第12字节(从0开始计算)
        hRtp->pNaluHdr->u1F             = (u8NaluBytes & 0x80) >> 7;
        hRtp->pNaluHdr->u2Nri           = (u8NaluBytes & 0x60) >> 5;
        hRtp->pNaluHdr->u5Type          = u8NaluBytes & 0x1f;
 
 		//nalu数据
        pNaluPayload = (pSendBuf + 13);	//单包打包  真正的H264数据存在pSendBuf的第13字节处
        memcpy(pNaluPayload, pNaluCurr, s32NaluRemain);
 
        s32Bytes = s32NaluRemain + 13;  //总发送数据量=12+1+实际数据长度
	/*int sendto ( socket s , const void * msg, int len, unsigned int flags, const struct sockaddr * to , int tolen ) ;
		s 套接字
		msg 待发送数据的缓冲区
		len 缓冲区长度
		flags 调用方式标志位, 一般为0, 改变Flags,将会改变Sendto发送的形式
		to (可选)指针,指向目的套接字的地址
		tolen 所指目的地址的长度
		返回:成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中
	*/
        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);//将pSendBuf数据写入文件hRtp->pNaluFile
#endif
    }
    //NALU包大于最大包长度,分批发送  FA-U模式 
    else
    {
        //fu indicator
        hRtp->pFuInd            = (StFuIndicator *)(pSendBuf + 12);
        hRtp->pFuInd->u1F       = (u8NaluBytes & 0x80) >> 7;
        hRtp->pFuInd->u2Nri     = (u8NaluBytes & 0x60) >> 5;
        hRtp->pFuInd->u5Type    = 28;//前面说明了u8NaluBytes=00,计算hRtp->pFuInd=28= 0x1c,抓包发现第一个会有0x1c(抓包的第0x36个)
 
        //fu header
        hRtp->pFuHdr            = (StFuHdr *)(pSendBuf + 13);
        hRtp->pFuHdr->u1R       = 0;//协议规定必须为0
        hRtp->pFuHdr->u5Type    = u8NaluBytes & 0x1f;//前面说明了u8NaluBytes=00,所以hRtp->pFuHdr->u5Type =0
 
        //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;//最后一个包u1Marker才置1
 
            /*配置fu header*/
            //最后一批数据则置1    分包尾包标志位
            hRtp->pFuHdr->u1E       = (s32NaluRemain <= MAX_RTP_PKT_LENGTH) ? 1 : 0;//u1E:end,代表最后一批,不是最后一批数据则为0
            //第一批数据则置1  分包首包标志位
            hRtp->pFuHdr->u1S       = (s32NaluRemain == (s32NalBufSize - 1)) ? 1 : 0;//u1S:start,代表第一批,第一批数据则为1
            /*
            	hRtp->pFuHdr->u5Type    = u8NaluBytes & 0x1f;//前面说明了u8NaluBytes=00,所以hRtp->pFuHdr->u5Type=0
		hRtp->pFuHdr->u1R       = 0;

       	这样计算起来FA-U包第一个包为10000000=0x80,所以抓包看到0x80(抓包的第0x37个)
       							第二个包为00000000=0x00,	所以抓包看到0x00(抓包的第0x37个)
       							最后一个包为01000000=0x40,所以抓包看到0x00(抓包的第0x37个)
		*/
 
            s32Bytes = (s32NaluRemain < MAX_RTP_PKT_LENGTH) ? s32NaluRemain : MAX_RTP_PKT_LENGTH;  //本次发送包长度
 
 
            memcpy(pNaluPayload, pNaluCurr, s32Bytes); //将数据放入rtp包的数据位置
 
            //发送本批次
            s32Bytes = s32Bytes + 14;   //分包rtp包的长度=12+1+1+数据长度
            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);
 
    return s32Ret;
}

现在我们用wireshark打开0106.pcapng文件,发现有一堆数据,这些抓包数据是我调试时抓的,第一次调试没调好,但数据也抓了,所以前面很多数据都没有用,第二次调试的数据是从13133包开始分析数据,但中间还还有些TCP协议,我用的是NFS挂载根文件系统还有NFS协议,数据很多,所以我们要过滤数据,在上面的应用显示过滤器输入rtsp然后回车,定位到13133包,发现第一个H264包是14633包,我们双击14633包,弹出包信息框,上面是解释,下面是数据,点击上面的解释区,就可以知道数据代表什么意义,如第0x0e-0x21数据代表了TCP协议版本是4,头长度20字节等等,当然我们的重点在RTP包,点击Real-Time Transport Protocol发现这些数据从0x2a开始一直到结束,但我们并未发现H264的关键帧(0x67,0x68,0x65).为什么,这是因为客户端请求RTSP播放时,HI3518编码不太可能刚好在编码关键帧,这时HI3518产生的帧很可能是非关键帧,所以找不到(0x67,0x68,0x65)这样的数据,当然,客户端VLC也播放不了视频,所以最开始会有几百毫秒的黑屏。直到HI3518产生关键帧,客户端播放器接收到关键帧才开始显示视频,对应的包就是第14689包。双击14689包,单击Real-Time Transport Protocol我看到的数据如下图,这些数据结合程序才对我们理解H264封装成RTP包有作用。
2.3 H264数据封装RTP包
RTP包=RTP头(版本,时间戳等12字节)+NAL/FU-A
常用FU-A,这里只分析FU-A。
FU-A=FU-A指示器(1byte)+FU-A头(1byte)+负荷
FU-A指示器:低5位为28表示是FU-A,其余三位一般为0
FU-A头=bit7(Start第一个FU-A包)+bit6(End最后一个FU-A包)+bit5(Reserve)+bit4-bit0(H264第0个00参与计算)
查资料感觉FA-U头低5位表示这个FA-U包的类型,应该与真正的H264的NALU的类型一致,即应该是
00 00 00 01 67的第4个字节67参与计算
负荷:H264数据,第一个FU-A包的负荷为00 00 01 xx xx…,只有两个00表示H264开头,因为第0个00参与了FU-A头计算,

所以感觉RTP的H264负荷比真正的H264负荷的开头码少了一个00

在SendNalu264函数中hRtp->pRtpFixedHdr->u2Version = 2;看RTP固定头结构可以知道RTP固定头第0字节最高两位就是版本号2,其它6位为0,组合起来就是0x80,所以14689包第0x2a字节为0x80。第1字节低7位是负荷类型,程序里hRtp->pRtpFixedHdr->u7Payload = H264;高位未填充,所以是0,组合起来就是0x60,就是14689包的第0x2b字节,固定头的第2,3字节就是包***,不同RTP包,这个值不同,程序里用如下代码填充hRtp->pRtpFixedHdr->u16SeqNum = htons(hRtp->u16SeqNum ++);对应14689包的第0x2c,0x2d字节(00 37)同理,后面还要填充时间戳,ssrc等东西,对应代码为hRtp->pRtpFixedHdr->u32TimeStamp = htonl(hRtp->u32TimeStampCurr * (90000 / 1000));hRtp->pRtpFixedHdr->u32SSrc = hRtp->u32SSrc;对应的14689包的第0x2e-0x35字节,这样,RTP包固定头就占了12字节。具体后面的将H264打包成FU-A包,程序里每一句代码都有注释,读者可自己结合注释代码与数据分析。

将H264打包成RTP包,实际上就是在熟悉RTP包结构的基础上,定义一个RTP包结构体,然后填充其成员,最后调用linux网络编程的sendto函数发送出去就行了,客户端也会按照相同的RTSP协议和RTP包结构拆分,取得真正的H264数据,然后显示出来。

Hi3518+RTSP下载链接:Hi3518+RTSP

相关标签: 海思项目