這段時間,一直在忙一件事情。通過GB28181協定,從海康攝像頭的PS流中,提取出原始的H264視訊流。經過一段嘗試,最終還是達到了效果,當然,不少地方還是借鑒了網上的一些資料。這裡我加以總結,友善我以後回憶學習。也順便給有同樣需要的朋友一些參考。
這裡提前說下,以下内容中,對PS流的各種header格式、H264的格式,我不會詳細的去展開,我也确實沒那麼強的能力,我隻講述關鍵要用的一些點,能讓你弄懂原理,并且能完成把PS流中的H264流提取出來。
<-----------簡單介紹下H264格式----------->
為了實作H264兩個目标,H264功能上進行了分層,視訊編碼層(VCL)和網絡提取層(NAL)。
VCL(Video Coding Layer):視訊編碼層,包括核心壓縮引擎和塊、宏塊和片的文法級别定義,設計目标是盡可能地獨立于網絡進行高效的編碼,負責有效表示視訊資料的内容。
NAL(Network Abstraction Layer):網絡提取層,負責将 VCL 産生的比特字元串适配到各種各樣的網絡和多元環境中,覆寫了所有片級以上的文法級别;
咱們主要講的是NAL網絡提取層:
一個NALU 單元常由 [NALU Header] + [NALU Payload] 部分組成,NAL對VCL進行了封裝包裹。常見的H264視訊流的包格式如下:
H264封裝格式
這四個常見的關鍵詞用圖展示下,SPS 序列參數集、PPS 圖像參數集、I幀、P幀如下圖:
根據NALU的header判斷幀的類型
從上圖可以看出,一個完整NALU單元格,是由1位元組的NALU header和NALU payload組成的。其中,我們可以把每個NALU的第一個位元組(header)與0x1F做按位與,結果是十進制的數值,7代表該幀為SPS,8代表該幀為PPS,6代表該幀為資訊增強SEI,5代表該幀為I幀,1代表該幀為P幀。這是你寫代碼,判斷該幀的類型的必要步驟。
<------------簡單介紹下RTP包格式------------>
RTP資料包一般由:RTPHeader+有效載荷資料構成,RTP Header一般為12位元組,有效載荷資料可以是音頻資料,h264碼流,PS碼流等等.
RTP頭部組成:頭部一般至少包含12個固定位元組,也包括若擴充幹位元組。
RTP包格式
最最常見的都是RTP Hearder的長度都是固定12個位元組,也就是說,你在網絡傳輸的socket出接收到的RTP音視訊資料Buffer,前12個位元組就是RTP Header的東西,從第13個位元組開始,才是真正的音視訊的實體載荷。(載荷payload就是h264或者G711等編碼後的,真實音視訊資料,是音視訊本身的内容資料哦,不包含什麼協定、算法之類的内容)
如何解析RTP標頭,看我以前的一篇文章:解析RTP包的頭部結構
<------------簡單介紹下PS流格式----------->
因為一般視訊資料都是采用rtp打包發送,是以這裡我就把ps封裝和rtp封裝放在一起講.
-
視訊關鍵幀的封裝(I幀)
RTP + PSheader + PS system header + PS system Map + PES header +h264 data
-
視訊非關鍵幀的封裝(P幀、B幀)
RTP +PS header + PES header + h264 data
其實,要在這裡三句兩句,是很難講清楚PS流的封裝格式,是以呢,借助抓包,用圖解的方式,更容易了解PS的封裝格式。這是我用GB28181協定呼叫的一個海康攝像頭,通過udp.port==26876的方式,過濾出攝像頭傳過來的PS流,其實就是一堆以UDP方式,發送過來的RTP包,隻不過是RTP包裡面有PS包,PS包裡面裝的才是H264的包。用圖解一下:
一個真實的PS包
就像我在圖中寫的,PS流中,PES包才是關鍵。對方傳過來的H264的資料,最終是放到這個PES包裡面的,前面那些PS標頭、PS system系統頭、PSM頭都是PS流協定的格式,不包含H264的真實資料,是以找到PES包是關鍵,因為資料在它裡面。下圖展示下PS流包的裝載東西的層次:
PS流包裝載内容的層次
下面就用剛才那個抓包的截圖,把裡面的十六進制的資料複制出來:
就解析紅框中PS流内容
内容如下:
0x00 00 01 ba是PS頭的開始碼,標頭最少有14個位元組,第14個位元組的最後3bit說明了標頭14位元組後填充資料的長度,這裡是pack_stuffing_length=fe&0x07=6,有6byte的填充資料,既是ff ff 00 00 00 01内容。也就是說這個PS包的頭長度:14+填充的6 = 20個位元組長度。
0x00 00 01 bb是PS系統頭的開始碼,起始碼後的00 12說明了其後資料的長度,要用0x0012換成十進制,可以用電腦,這裡是18個位元組長度,記住是從00 12後面開始,數18個位元組哦。
0x00 00 01 bc是PSM的開始碼,開始碼後的00 4a說明了其後資料的長度,要用0x004a換成十進制,可以用電腦,這裡是74個位元組長度。記住是從00 4a後面開始計算的哦。
其實上面3個不是關鍵,下面PES才關鍵。
0x00 00 01 e0是PES包視訊時的開始碼,開始碼後面的00 2a說明了其後資料的長度,要用0x002a換成十進制,可以用電腦,這裡時42個位元組長度。記住是從00 2a後面開始計算的哦。也就是說0x00 00 01 e0 + 00 2a + 42個位元組長度,該個PES包就讀完了。
還要注意,每個pes包的第9個位元組,也就是從0x00 00 01 e0的開始數,這就已經4個位元組了,再數5個位元組,就是圖中的0a位元組,換成十進制為10。這個意思是,在第9個填充位之後,又加了10個位元組長度的内容,再數完這10個位元組,後面才是h264的資料。0x00 00 01 67 就是SPS幀了。
以這個規律,你看下,剩下的0x00 00 01 e0都是這樣的計算數的,就可以找到0x00 00 01 68 PPS幀,0x00 00 01 65 I幀。
這裡有個小公式,可以計算開始碼後面2個位元組的十進制值,比如00 2a,就可以寫成:
假如你已經找到0x00 00 01 e0開始碼,
if(buff[k]==0x00 && buff[k+1]==0x00 && buff[k+2]==0x01 && buff[k+3]==0xE0){
//其實就是第5位,第6位,計算pes包從第6位元組後的剩餘長度
int nPesLen = (buff[k+4]<<8)|buff[k+5];。
//根據第9位位元組計算出的填充長度,這個長度是在第9位位元組後開始數的。
int nPesEx = buff[k+8];
}
<--------啥也不說了,看代碼-------->
好了,pes包基本講完了。下面,我把一段C語言寫的,如何從接收到的rtp包的ps流包裡面,提取出來h264資料。
int inline ProgramStreamPackHeader(char* Pack, int length, char **NextPack, int *leftlength) {
//printf("[%s]%x %x %x %x\n", __FUNCTION__, Pack[0], Pack[1], Pack[2], Pack[3]);
//通過 00 00 01 ba頭的第14個位元組的最後3位來确定頭部填充了多少位元組
program_stream_pack_header *PsHead = (program_stream_pack_header *)Pack;
unsigned char pack_stuffing_length = PsHead->stuffinglen & '\x07';
*leftlength = length - sizeof(program_stream_pack_header) - pack_stuffing_length;//減去頭和填充的位元組
*NextPack = Pack+sizeof(program_stream_pack_header) + pack_stuffing_length;
if(*leftlength<4) return 0;
return *leftlength;
}
inline int ProgramStreamMap(char* Pack, int length, char **NextPack, int *leftlength, char **PayloadData, int *PayloadDataLen)
{
program_stream_map* PSMPack = (program_stream_map*)Pack;
//no payload
*PayloadData = 0;
*PayloadDataLen = 0;
if((unsigned int)length < sizeof(program_stream_map)) return 0;
littel_endian_size psm_length;
psm_length.byte[0] = PSMPack->PackLength.byte[1];
psm_length.byte[1] = PSMPack->PackLength.byte[0];
*leftlength = length - psm_length.length - sizeof(program_stream_map);
if(*leftlength<=0) return 0;
*NextPack = Pack + psm_length.length + sizeof(program_stream_map);
return *leftlength;
}
inline int ProgramShHead(char* Pack, int length, char **NextPack, int *leftlength, char **PayloadData, int *PayloadDataLen) {
program_stream_map* PSMPack = (program_stream_map*)Pack;
//no payload
*PayloadData = 0;
*PayloadDataLen = 0;
if((unsigned int)length < sizeof(program_stream_map)) return 0;
littel_endian_size psm_length;
psm_length.byte[0] = PSMPack->PackLength.byte[1];
psm_length.byte[1] = PSMPack->PackLength.byte[0];
*leftlength = length - psm_length.length - sizeof(program_stream_map);
if(*leftlength<=0) return 0;
*NextPack = Pack + psm_length.length + sizeof(program_stream_map);
return *leftlength;
}
inline int Pes(char* Pack, int length, char **NextPack, int *leftlength, char **PayloadData, int *PayloadDataLen)
{
program_stream_e* PSEPack = (program_stream_e*)Pack;
*PayloadData = 0;
*PayloadDataLen = 0;
if((unsigned int)length < sizeof(program_stream_e)) return 0;
littel_endian_size pse_length;
pse_length.byte[0] = PSEPack->PackLength.byte[1];
pse_length.byte[1] = PSEPack->PackLength.byte[0];
*PayloadDataLen = pse_length.length - 2 - 1 - PSEPack->stuffing_length;
if(*PayloadDataLen>0)
*PayloadData = Pack + sizeof(program_stream_e) + PSEPack->stuffing_length;
*leftlength = length - pse_length.length - sizeof(pack_start_code) - sizeof(littel_endian_size);
if(*leftlength<=0) return 0;
*NextPack = Pack + sizeof(pack_start_code) + sizeof(littel_endian_size) + pse_length.length;
return *leftlength;
}
int inline GetH246FromPs(char* buffer,int length, char *h264Buffer, int *h264length, char *sipId) {
int leftlength = 0;
char *NextPack = 0;
*h264length = 0;
if(ProgramStreamPackHeader(buffer, length, &NextPack, &leftlength)==0)
return 0;
char *PayloadData=NULL;
int PayloadDataLen=0;
while((unsigned int)leftlength >= sizeof(pack_start_code)) {
PayloadData=NULL;
PayloadDataLen=0;
if(NextPack
&& NextPack[0]=='\x00'
&& NextPack[1]=='\x00'
&& NextPack[2]=='\x01'
&& NextPack[3]=='\xE0') {
//接着就是流包,說明是非i幀
if(Pes(NextPack, leftlength, &NextPack, &leftlength, &PayloadData, &PayloadDataLen)) {
if(PayloadDataLen) {
if(PayloadDataLen + *h264length < H264_FRAME_SIZE_MAX) {
memcpy(h264Buffer, PayloadData, PayloadDataLen);
h264Buffer += PayloadDataLen;
*h264length += PayloadDataLen;
}
else {
APP_WARRING("h264 frame size exception!! %d:%d", PayloadDataLen, *h264length);
}
}
}
else {
if(PayloadDataLen) {
if(PayloadDataLen + *h264length < H264_FRAME_SIZE_MAX) {
memcpy(h264Buffer, PayloadData, PayloadDataLen);
h264Buffer += PayloadDataLen;
*h264length += PayloadDataLen;
}
else {
APP_WARRING("h264 frame size exception!! %d:%d", PayloadDataLen, *h264length);
}
}
break;
}
}
else if(NextPack
&& NextPack[0]=='\x00'
&& NextPack[1]=='\x00'
&& NextPack[2]=='\x01'
&& NextPack[3]=='\xBB') {
if(ProgramShHead(NextPack, leftlength, &NextPack, &leftlength, &PayloadData, &PayloadDataLen)==0)
break;
}
else if(NextPack
&& NextPack[0]=='\x00'
&& NextPack[1]=='\x00'
&& NextPack[2]=='\x01'
&& NextPack[3]=='\xBC') {
if(ProgramStreamMap(NextPack, leftlength, &NextPack, &leftlength, &PayloadData, &PayloadDataLen)==0)
break;
}
else if(NextPack
&& NextPack[0]=='\x00'
&& NextPack[1]=='\x00'
&& NextPack[2]=='\x01'
&& (NextPack[3]=='\xC0' || NextPack[3]=='\xBD')) {
//printf("audio ps frame, skip it\n");
break;
}
else {
printf("[%s]no know %x %x %x %x\n", sipId, NextPack[0], NextPack[1], NextPack[2], NextPack[3]);
break;
}
}
return *h264length;
}
static void *rtp_recv_thread(void *arg) {
int socket_fd;
CameraParams *p = (CameraParams *)arg;
int rtp_port = p->recvPort;
struct sockaddr_in servaddr;
socket_fd = init_udpsocket(rtp_port, &servaddr, NULL);
if(socket_fd >= 0) {
//printf("start socket port %d success\n", rtp_port);
}
unsigned char *buf = (unsigned char *)malloc(RTP_MAXBUF);
if(buf == NULL) {
APP_ERR("malloc failed buf");
return NULL;
}
unsigned char *psBuf = (unsigned char *)malloc(PS_BUF_SIZE);
if(psBuf == NULL) {
APP_ERR("malloc failed");
return NULL;
}
char *h264buf = (char *)malloc(H264_FRAME_SIZE_MAX);
if(h264buf == NULL) {
APP_ERR("malloc failed");
return NULL;
}
int recvLen;
int addr_len = sizeof(struct sockaddr_in);
int rtpHeadLen = sizeof(RTP_header_t);
char filename[128];
snprintf(filename, 128, "%s.264", p->sipId);
p->fpH264 = fopen(filename, "wb");
if(p->fpH264 == NULL) {
APP_ERR("fopen %s failed", filename);
return NULL;
}
APP_DEBUG("%s:%d starting ...", p->sipId, p->recvPort);
int cnt = 0;
int rtpPsLen, h264length, psLen = 0;
unsigned char *ptr;
memset(buf, 0, RTP_MAXBUF);
while(p->running) {
recvLen = recvfrom(socket_fd, buf, RTP_MAXBUF, 0, (struct sockaddr*)&servaddr, (socklen_t*)&addr_len);
if(recvLen > rtpHeadLen) {
ptr = psBuf + psLen;
rtpPsLen = recvLen - rtpHeadLen;
if(psLen + rtpPsLen < PS_BUF_SIZE) {
memcpy(ptr, buf + rtpHeadLen, rtpPsLen);
}
else {
APP_WARRING("psBuf memory overflow, %d\n", psLen + rtpPsLen);
psLen = 0;
continue;
}
if(ptr[0] == 0x00 && ptr[1] == 0x00 && ptr[2] == 0x01 && ptr[3] == 0xBA && psLen > 0) {
if(cnt % 10000 == 0) {
printf("rtpRecvPort:%d, cnt:%d, pssize:%d\n", rtp_port, cnt ++, psLen);
}
if(cnt % 25 == 0) {
p->status = 1;
}
GetH246FromPs((char *)psBuf, psLen, h264buf, &h264length, p->sipId);
if(h264length > 0) {
fwrite(h264buf, 1, h264length, p->fpH264);
}
memcpy(psBuf, ptr, rtpPsLen);
psLen = 0;
cnt ++;
}
psLen += rtpPsLen;
}
else {
perror("recvfrom()");
}
if(recvLen > 1500) {
printf("udp frame exception, %d\n", recvLen);
}
}
release_udpsocket(socket_fd, NULL);
if(buf != NULL) {
free(buf);
}
if(psBuf != NULL) {
free(psBuf);
}
if(h264buf != NULL) {
free(h264buf);
}
if(p->fpH264 != NULL) {
fclose(p->fpH264);
p->fpH264 = NULL;
}
APP_DEBUG("%s:%d run over", p->sipId, p->recvPort);
return NULL;
}
一些頭檔案結構體:
#ifndef __MEDIA_PSTREAM__H__
#define __MEDIA_PSTREAM__H__
#ifndef uint16_t
typedef unsigned short uint16_t;
#endif
#ifndef uint32_t
typedef unsigned int uint32_t;
#endif
typedef struct RTP_HEADER
{
uint16_t cc:4;
uint16_t extbit:1;
uint16_t padbit:1;
uint16_t version:2;
uint16_t paytype:7; //負載類型
uint16_t markbit:1; //1表示前面的包為一個解碼單元,0表示目前解碼單元未結束
uint16_t seq_number; //序号
uint32_t timestamp; //時間戳
uint32_t ssrc; //循環校驗碼
//uint32_t csrc[16];
} RTP_header_t;
#pragma pack (1)
typedef union littel_endian_size_s {
unsigned short int length;
unsigned char byte[2];
} littel_endian_size;
typedef struct pack_start_code_s {
unsigned char start_code[3];
unsigned char stream_id[1];
} pack_start_code;
typedef struct program_stream_pack_header_s {
pack_start_code PackStart;// 4
unsigned char Buf[9];
unsigned char stuffinglen;
} program_stream_pack_header;
typedef struct program_stream_map_s {
pack_start_code PackStart;
littel_endian_size PackLength;//we mast do exchange
//program_stream_info_length
//info
//elementary_stream_map_length
//elem
} program_stream_map;
typedef struct program_stream_e_s {
pack_start_code PackStart;
littel_endian_size PackLength;//we mast do exchange
char PackInfo1[2];
unsigned char stuffing_length;
} program_stream_e;
#endif //__MEDIA_PSTREAM__H__
代碼還是,看懂最重要,這個肯定能把海康攝像頭的PS流寫成H264碼流。
【多餘的解釋:】
其實也不想解釋了,寫這麼多,想看懂的,肯定會去看懂。歡迎點贊+收藏,謝謝老鐵。