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 = 0;
- bitsBuffer.i_mask = 0x80; // 二進制:10000000 這里是為了后面對一個字節的每一位進行操作,避免大小端誇字節字序錯亂
- bitsBuffer.p_data = (unsigned char *)(pData);
- memset(bitsBuffer.p_data, 0, PS_HDR_LEN);
- bits_write(&bitsBuffer, 32, 0x000001BA); /*start codes*/
- bits_write(&bitsBuffer, 2, 1); /*marker bits '01b'*/
- bits_write(&bitsBuffer, 3, (s64Scr>>30)&0x07); /*System clock [32..30]*/
- bits_write(&bitsBuffer, 1, 1); /*marker bit*/
- bits_write(&bitsBuffer, 15, (s64Scr>>15)&0x7FFF); /*System clock [29..15]*/
- bits_write(&bitsBuffer, 1, 1); /*marker bit*/
- bits_write(&bitsBuffer, 15, s64Scr&0x7fff); /*System clock [29..15]*/
- bits_write(&bitsBuffer, 1, 1); /*marker bit*/
- bits_write(&bitsBuffer, 9, lScrExt&0x01ff); /*System clock [14..0]*/
- bits_write(&bitsBuffer, 1, 1); /*marker bit*/
- bits_write(&bitsBuffer, 22, (255)&0x3fffff); /*bit rate(n units of 50 bytes per second.)*/
- bits_write(&bitsBuffer, 2, 3); /*marker bits '11'*/
- bits_write(&bitsBuffer, 5, 0x1f); /*reserved(reserved for future use)*/
- bits_write(&bitsBuffer, 3, 0); /*stuffing length*/
- return 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 = 0;
- bitsBuffer.i_mask = 0x80;
- bitsBuffer.p_data = (unsigned char *)(pData);
- memset(bitsBuffer.p_data, 0, SYS_HDR_LEN);
- /*system header*/
- bits_write( &bitsBuffer, 32, 0x000001BB); /*start code*/
- bits_write( &bitsBuffer, 16, SYS_HDR_LEN-6);/*header_length 表示次字節后面的長度,后面的相關頭也是次意思*/
- bits_write( &bitsBuffer, 1, 1); /*marker_bit*/
- bits_write( &bitsBuffer, 22, 50000); /*rate_bound*/
- bits_write( &bitsBuffer, 1, 1); /*marker_bit*/
- bits_write( &bitsBuffer, 6, 1); /*audio_bound*/
- bits_write( &bitsBuffer, 1, 0); /*fixed_flag */
- bits_write( &bitsBuffer, 1, 1); /*CSPS_flag */
- bits_write( &bitsBuffer, 1, 1); /*system_audio_lock_flag*/
- bits_write( &bitsBuffer, 1, 1); /*system_video_lock_flag*/
- bits_write( &bitsBuffer, 1, 1); /*marker_bit*/
- bits_write( &bitsBuffer, 5, 1); /*video_bound*/
- bits_write( &bitsBuffer, 1, 0); /*dif from mpeg1*/
- bits_write( &bitsBuffer, 7, 0x7F); /*reserver*/
- /*audio stream bound*/
- bits_write( &bitsBuffer, 8, 0xC0); /*stream_id*/
- bits_write( &bitsBuffer, 2, 3); /*marker_bit */
- bits_write( &bitsBuffer, 1, 0); /*PSTD_buffer_bound_scale*/
- bits_write( &bitsBuffer, 13, 512); /*PSTD_buffer_size_bound*/
- /*video stream bound*/
- bits_write( &bitsBuffer, 8, 0xE0); /*stream_id*/
- bits_write( &bitsBuffer, 2, 3); /*marker_bit */
- bits_write( &bitsBuffer, 1, 1); /*PSTD_buffer_bound_scale*/
- bits_write( &bitsBuffer, 13, 2048); /*PSTD_buffer_size_bound*/
- return 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 = 0;
- bitsBuffer.i_mask = 0x80;
- bitsBuffer.p_data = (unsigned char *)(pData);
- memset(bitsBuffer.p_data, 0, PS_SYS_MAP_SIZE);
- bits_write(&bitsBuffer, 24,0x000001); /*start code*/
- bits_write(&bitsBuffer, 8, 0xBC); /*map stream id*/
- bits_write(&bitsBuffer, 16,18); /*program stream map length*/
- bits_write(&bitsBuffer, 1, 1); /*current next indicator */
- bits_write(&bitsBuffer, 2, 3); /*reserved*/
- bits_write(&bitsBuffer, 5, 0); /*program stream map version*/
- bits_write(&bitsBuffer, 7, 0x7F); /*reserved */
- bits_write(&bitsBuffer, 1, 1); /*marker bit */
- bits_write(&bitsBuffer, 16,0); /*programe stream info length*/
- bits_write(&bitsBuffer, 16, 8); /*elementary stream map length is*/
- /*audio*/
- bits_write(&bitsBuffer, 8, 0x90); /*stream_type*/
- bits_write(&bitsBuffer, 8, 0xC0); /*elementary_stream_id*/
- bits_write(&bitsBuffer, 16, 0); /*elementary_stream_info_length is*/
- /*video*/
- bits_write(&bitsBuffer, 8, 0x1B); /*stream_type*/
- bits_write(&bitsBuffer, 8, 0xE0); /*elementary_stream_id*/
- bits_write(&bitsBuffer, 16, 0); /*elementary_stream_info_length */
- /*crc (2e b9 0f 3d)*/
- bits_write(&bitsBuffer, 8, 0x45); /*crc (24~31) bits*/
- bits_write(&bitsBuffer, 8, 0xBD); /*crc (16~23) bits*/
- bits_write(&bitsBuffer, 8, 0xDC); /*crc (8~15) bits*/
- bits_write(&bitsBuffer, 8, 0xF4); /*crc (0~7) bits*/
- return 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 = 0;
- bitsBuffer.i_mask = 0x80;
- bitsBuffer.p_data = (unsigned char *)(pData);
- memset(bitsBuffer.p_data, 0, PES_HDR_LEN);
- /*system header*/
- bits_write( &bitsBuffer, 24,0x000001); /*start code*/
- bits_write( &bitsBuffer, 8, (stream_id)); /*streamID*/
- bits_write( &bitsBuffer, 16,(payload_len)+13); /*packet_len*/ //指出pes分組中數據長度和該字節后的長度和
- bits_write( &bitsBuffer, 2, 2 ); /*'10'*/
- bits_write( &bitsBuffer, 2, 0 ); /*scrambling_control*/
- bits_write( &bitsBuffer, 1, 0 ); /*priority*/
- bits_write( &bitsBuffer, 1, 0 ); /*data_alignment_indicator*/
- bits_write( &bitsBuffer, 1, 0 ); /*copyright*/
- bits_write( &bitsBuffer, 1, 0 ); /*original_or_copy*/
- bits_write( &bitsBuffer, 1, 1 ); /*PTS_flag*/
- bits_write( &bitsBuffer, 1, 1 ); /*DTS_flag*/
- bits_write( &bitsBuffer, 1, 0 ); /*ESCR_flag*/
- bits_write( &bitsBuffer, 1, 0 ); /*ES_rate_flag*/
- bits_write( &bitsBuffer, 1, 0 ); /*DSM_trick_mode_flag*/
- bits_write( &bitsBuffer, 1, 0 ); /*additional_copy_info_flag*/
- bits_write( &bitsBuffer, 1, 0 ); /*PES_CRC_flag*/
- bits_write( &bitsBuffer, 1, 0 ); /*PES_extension_flag*/
- bits_write( &bitsBuffer, 8, 10); /*header_data_length*/
- // 指出包含在 PES 分組標題中的可選字段和任何填充字節所占用的總字節數。該字段之前
- //的字節指出了有無可選字段。
- /*PTS,DTS*/
- bits_write( &bitsBuffer, 4, 3 ); /*'0011'*/
- bits_write( &bitsBuffer, 3, ((pts)>>30)&0x07 ); /*PTS[32..30]*/
- bits_write( &bitsBuffer, 1, 1 );
- bits_write( &bitsBuffer, 15,((pts)>>15)&0x7FFF); /*PTS[29..15]*/
- bits_write( &bitsBuffer, 1, 1 );
- bits_write( &bitsBuffer, 15,(pts)&0x7FFF); /*PTS[14..0]*/
- bits_write( &bitsBuffer, 1, 1 );
- bits_write( &bitsBuffer, 4, 1 ); /*'0001'*/
- bits_write( &bitsBuffer, 3, ((dts)>>30)&0x07 ); /*DTS[32..30]*/
- bits_write( &bitsBuffer, 1, 1 );
- bits_write( &bitsBuffer, 15,((dts)>>15)&0x7FFF); /*DTS[29..15]*/
- bits_write( &bitsBuffer, 1, 1 );
- bits_write( &bitsBuffer, 15,(dts)&0x7FFF); /*DTS[14..0]*/
- bits_write( &bitsBuffer, 1, 1 );
- return 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 == NULL)
- return -1;
- bitsBuffer.i_size = RTP_HDR_LEN;
- bitsBuffer.i_data = 0;
- bitsBuffer.i_mask = 0x80;
- bitsBuffer.p_data = (unsigned char *)(pData);
- memset(bitsBuffer.p_data, 0, RTP_HDR_SIZE);
- bits_write(&bitsBuffer, 2, RTP_VERSION); /* rtp version */
- bits_write(&bitsBuffer, 1, 0); /* rtp padding */
- bits_write(&bitsBuffer, 1, 0); /* rtp extension */
- bits_write(&bitsBuffer, 4, 0); /* rtp CSRC count */
- bits_write(&bitsBuffer, 1, (marker_flag)); /* rtp marker */
- bits_write(&bitsBuffer, 7, 96); /* rtp payload type*/
- bits_write(&bitsBuffer, 16, (cseq)); /* rtp sequence */
- bits_write(&bitsBuffer, 32, (curpts)); /* rtp timestamp */
- bits_write(&bitsBuffer, 32, (ssrc)); /* rtp SSRC */
- return 0;
- }
4、封裝相關心得
博主已經分步驗證過rtp或者ps又或者rtp+ps封裝都能正常預覽。其實這個封裝真心沒什么理論知識說的,看看標准都知道了,只要仔細看標准一步一步的向下走,分析各個封裝的各個字段,就沒有什么問題了,當然在實際中也有很多小問題苦惱我很久,但是無不是因為標准沒注意或者理解有誤。現在我也只是簡單的把自己實現的代碼大致貼出來分享了,希望相關開發的少走一些彎路而已。有問題或者有更好的方法,大家可以相互交流。