首页 > 代码库 > 关于对H264码流的PS的封装的相关代码实现
关于对H264码流的PS的封装的相关代码实现
1、写在开始之前:
最近因为新工作要维护别人留下的GB模块代码,先熟悉了流程,然后也试着封装了下ps流,结果也能通过测试正常预览了,当然,其中开发读文档的头疼,预览花屏,卡帧的事情都有遇到,当时慢慢的看文档,整理逻辑,也就都顺利解决了,下面把大致的一些流程代码贴出来分享下。既然是对接国标,自然少不了通读它的标准文档和相关的RFC文档了!具体的我就不说了,可以用百度google下的。
注意:因为是GB要求ps封装后再加上rtp头的格式来的, 所以下面代码中我也加上了rtp头,如果不需要的话,直接屏蔽代码中的rtp即可。
2、封装的重点
当我们从读缓冲区中取得一帧音视频数据的时候,封装时其实每一帧数据有且只有一个ps头和psm头,如果是I帧的话,就还多一个system头,一个或者多个pes头和rtp头,
像如果帧数据过长的话,就得进行分片,每片都会包含一个pes头,rtp负载最好长度1460,所以会进行再分包操作!所以每一个包数据至少一个rtp+databuf,每一片数据,至少有个rtp+pes+databuf,每一帧数据至少有rtp+ps+psm+pes+databuf(关键帧的话:多一个system头)
3、具体的各个封装的代码实现
首先给去一个整体的封装rtp->ps->sys->psm->pes(如果只要ps的话,则为ps->sys->psm->pes)的大致流程,
然后再一一罗列出各个部件的封装接口
/*** *@remark: 音视频数据的打包成ps流,并封装成rtp *@param : pData [in] 需要发送的音视频数据 * nFrameLen [in] 发送数据的长度 * pPacker [in] 数据包的一些信息,包括时间戳,rtp数据buff,发送的socket相关信息 * stream_type[in] 数据类型 0 视频 1 音频 *@return: 0 success others failed */ int gb28181_streampackageForH264(char *pData, int nFrameLen, Data_Info_s* pPacker, int stream_type) { char szTempPacketHead[256]; int nSizePos = 0; int nSize = 0; char *pBuff = NULL; memset(szTempPacketHead, 0, 256); // 1 package for ps header gb28181_make_ps_header(szTempPacketHead + nSizePos, pPacker->s64CurPts); nSizePos += PS_HDR_LEN; //2 system header if( pPacker->IFrame == 1 ) { // 如果是I帧的话,则添加系统头 gb28181_make_sys_header(szTempPacketHead + nSizePos); nSizePos += SYS_HDR_LEN; //这个地方我是不管是I帧还是p帧都加上了map的,貌似只是I帧加也没有问题 // gb28181_make_psm_header(szTempPacketHead + nSizePos); // nSizePos += PSM_HDR_LEN; } // psm头 (也是map) gb28181_make_psm_header(szTempPacketHead + nSizePos); nSizePos += PSM_HDR_LEN; //加上rtp发送出去,这样的话,后面的数据就只要分片分包就只有加上pes头和rtp头了 if(gb28181_send_rtp_pack(szTempPacketHead, nSizePos, 0, pPacker) != 0 ) return -1; // 这里向后移动是为了方便拷贝pes头 //这里是为了减少后面音视频裸数据的大量拷贝浪费空间,所以这里就向后移动,在实际处理的时候,要注意地址是否越界以及覆盖等问题 pBuff = pData - PES_HDR_LEN; while(nFrameLen > 0) { //每次帧的长度不要超过short类型,过了就得分片进循环行发送 nSize = (nFrameLen > PS_PES_PAYLOAD_SIZE) ? PS_PES_PAYLOAD_SIZE : nFrameLen; // 添加pes头 gb28181_make_pes_header(pBuff, stream_type ? 0xC0:0xE0, nSize, (pPacker->s64CurPts / 100), (pPacker->s64CurPts/300)); //最后在添加rtp头并发送数据 if( gb28181_send_rtp_pack(pBuff, nSize + PES_HDR_LEN, ((nSize == nFrameLen)?1:0), pPacker) != 0 ) { printf("gb28181_send_pack failed!\n"); return -1; } //分片后每次发送的数据移动指针操作 nFrameLen -= nSize; //这里也只移动nSize,因为在while向后移动的pes头长度,正好重新填充pes头数据 pBuff += nSize; } return 0; }
上面列出来了整个打包发包的过程,接下来一个一个接口的看
/*** *@remark: ps头的封装,里面的具体数据的填写已经占位,可以参考标准 *@param : pData [in] 填充ps头数据的地址 * s64Src [in] 时间戳 *@return: 0 success, others failed */ int gb28181_make_ps_header(char *pData, unsigned long long s64Scr) { unsigned long long lScrExt = (s64Scr) % 100; s64Scr = s64Scr / 100; // 这里除以100是由于sdp协议返回的video的频率是90000,帧率是25帧/s,所以每次递增的量是3600, // 所以实际你应该根据你自己编码里的时间戳来处理以保证时间戳的增量为3600即可, //如果这里不对的话,就可能导致卡顿现象了 bits_buffer_s bitsBuffer; bitsBuffer.i_size = PS_HDR_LEN; bitsBuffer.i_data = http://www.mamicode.com/0;>/*** *@remark: sys头的封装,里面的具体数据的填写已经占位,可以参考标准 *@param : pData [in] 填充ps头数据的地址 *@return: 0 success, others failed */ int gb28181_make_sys_header(char *pData) { bits_buffer_s bitsBuffer; bitsBuffer.i_size = SYS_HDR_LEN; bitsBuffer.i_data = http://www.mamicode.com/0;>/*** *@remark: psm头的封装,里面的具体数据的填写已经占位,可以参考标准 *@param : pData [in] 填充ps头数据的地址 *@return: 0 success, others failed */ int gb28181_make_psm_header(char *pData) { bits_buffer_s bitsBuffer; bitsBuffer.i_size = PSM_HDR_LEN; bitsBuffer.i_data = http://www.mamicode.com/0;>/*** *@remark: pes头的封装,里面的具体数据的填写已经占位,可以参考标准 *@param : pData [in] 填充ps头数据的地址 * stream_id [in] 码流类型 * paylaod_len[in] 负载长度 * pts [in] 时间戳 * dts [in] *@return: 0 success, others failed */ int gb28181_make_pes_header(char *pData, int stream_id, int payload_len, unsigned long long pts, unsigned long long dts) { bits_buffer_s bitsBuffer; bitsBuffer.i_size = PES_HDR_LEN; bitsBuffer.i_data = http://www.mamicode.com/0;>/*** *@remark: rtp头的打包,并循环发送数据 *@param : pData [in] 发送的数据地址 * nDatalen [in] 发送数据的长度 * mark_flag [in] mark标志位 * curpts [in] 时间戳 * pPacker [in] 数据包的基本信息 *@return: 0 success, others failed */ int gb28181_send_rtp_pack(char *databuff, int nDataLen, int mark_flag, Data_Info_s* pPacker) { int nRes = 0; int nPlayLoadLen = 0; int nSendSize = 0; char szRtpHdr[RTP_HDR_LEN]; memset(szRtpHdr, 0, RTP_HDR_LEN); if(nDataLen + RTP_HDR_LEN <= RTP_MAX_PACKET_BUFF)// 1460 pPacker指针本来有一个1460大小的buffer数据缓存 { // 一帧数据发送完后,给mark标志位置1 gb28181_make_rtp_header(szRtpHdr, ((mark_flag == 1 )? 1 : 0 ), ++pPacker->u16CSeq, (pPacker->s64CurPts /300), pPacker->u32Ssrc); memcpy(pPacker->szBuff, szRtpHdr, RTP_HDR_LEN); memcpy(pPacker->szBuff + RTP_HDR_LEN, databuff, nDataLen); nRet = SendDataBuff(databuff, RTP_HDR_LEN + nSendSize, pPacker); if (nRes != (RTP_HDR_LEN + nDataLen)) { printf(" udp send error !\n"); return -1; } } else { nPlayLoadLen = RTP_MAX_PACKET_BUFF - RTP_HDR_LEN; // 每次只能发送的数据长度 除去rtp头 gb28181_make_rtp_header(pPacker->szBuff, 0, ++pPacker->u16CSeq, (pPacker->s64CurPts/100), pPacker->u32Ssrc); memcpy(pPacker->szBuff + RTP_HDR_LEN, databuff, nPlayLoadLen); nRet = SendDataBuff(databuff, RTP_HDR_LEN + nSendSize, pPacker); if (nRes != (RTP_HDR_LEN + nPlayLoadLen)) { printf(" udp send error !\n"); return -1; } nDataLen -= nPlayLoadLen; // databuff += (nPlayLoadLen - RTP_HDR_LEN); databuff += nPlayLoadLen; // 表明前面到数据已经发送出去 databuff -= RTP_HDR_LEN; // 用来存放rtp头 while(nDataLen > 0) { if(nDataLen <= nPlayLoadLen) { //一帧数据发送完,置mark标志位 gb28181_make_rtp_header(databuff, mark_flag, ++pPacker->u16CSeq, (pPacker->s64CurPts/100), pPacker->u32Ssrc); nSendSize = nDataLen; } else { gb28181_make_rtp_header(databuff, 0, ++pPacker->u16CSeq, (pPacker->s64CurPts/100), pPacker->u32Ssrc); nSendSize = nPlayLoadLen; } nRet = SendDataBuff(databuff, RTP_HDR_LEN + nSendSize, pPacker); if (nRes != (RTP_HDR_LEN + nSendSize)) { printf(" udp send error !\n"); return -1; } nDataLen -= nSendSize; databuff += nSendSize; //因为buffer指针已经向后移动一次rtp头长度后, //所以每次循环发送rtp包时,只要向前移动裸数据到长度即可,这是buffer指针实际指向到位置是 //databuff向后重复的rtp长度的裸数据到位置上 } } return 0; }还有一个很重要的宏定义,之所以把它定义成宏,是因为会频繁调用,其功能是循环将一个字节的8位按位一个一个的压入数据,防止出现夸字节的导致字序出错问题具体实现如下,其实是挪用了vlc源码中的实现过来的,这也是读源码的一个好处,能很好的利用里面比较高级而又方便的功能代码模块
#define PS_HDR_LEN 14 #define SYS_HDR_LEN 18 #define PSM_HDR_LEN 24 #define PES_HDR_LEN 19 #define RTP_HDR_LEN 12 /*** *@remark: 讲传入的数据按地位一个一个的压入数据 *@param : buffer [in] 压入数据的buffer * count [in] 需要压入数据占的位数 * bits [in] 压入的数值 */ #define bits_write(buffer, count, bits){ bits_buffer_s *p_buffer = (buffer); int i_count = (count); uint64_t i_bits = (bits); while( i_count > 0 ) { i_count--; if( ( i_bits >> i_count )&0x01 ) { p_buffer->p_data[p_buffer->i_data] |= p_buffer->i_mask; } else { p_buffer->p_data[p_buffer->i_data] &= ~p_buffer->i_mask; } p_buffer->i_mask >>= 1; /*操作完一个字节第一位后,操作第二位*/ if( p_buffer->i_mask == 0 ) /*循环完一个字节的8位后,重新开始下一位*/ { p_buffer->i_data++; p_buffer->i_mask = 0x80; } }}上面忘记贴出rtp封装头了,这次补充,如果在实际不需要rtp的话,可以直接在gb28181_send_rtp_pack函数接口中屏蔽gb28181_make_rtp_header函数接口即可,当然需要注意一点问题,就是对应的buffer指针的位置就不需要移动rtp头的长度了!
int gb28181_make_rtp_header(char *pData, int marker_flag, unsigned short cseq, long long curpts, unsigned int ssrc) { bits_buffer_s bitsBuffer; if (pData =http://www.mamicode.com/= NULL)>4、封装相关心得
博主已经分步验证过rtp或者ps又或者rtp+ps封装都能正常预览。其实这个封装真心没什么理论知识说的,看看标准都知道了,只要仔细看标准一步一步的向下走,分析各个封装的各个字段,就没有什么问题了,当然在实际中也有很多小问题苦恼我很久,但是无不是因为标准没注意或者理解有误。现在我也只是简单的把自己实现的代码大致贴出来分享了,希望相关开发的少走一些弯路而已。有问题或者有更好的方法,大家可以相互交流。
关于对H264码流的PS的封装的相关代码实现