天天看點

猶豫很久終于寫下來了,從海康攝像頭的ps流中提取h264原始資料

作者:記錄生活那些事兒

這段時間,一直在忙一件事情。通過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視訊流的包格式如下:

猶豫很久終于寫下來了,從海康攝像頭的ps流中提取h264原始資料

H264封裝格式

這四個常見的關鍵詞用圖展示下,SPS 序列參數集、PPS 圖像參數集、I幀、P幀如下圖:

猶豫很久終于寫下來了,從海康攝像頭的ps流中提取h264原始資料

根據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個固定位元組,也包括若擴充幹位元組。

猶豫很久終于寫下來了,從海康攝像頭的ps流中提取h264原始資料

RTP包格式

最最常見的都是RTP Hearder的長度都是固定12個位元組,也就是說,你在網絡傳輸的socket出接收到的RTP音視訊資料Buffer,前12個位元組就是RTP Header的東西,從第13個位元組開始,才是真正的音視訊的實體載荷。(載荷payload就是h264或者G711等編碼後的,真實音視訊資料,是音視訊本身的内容資料哦,不包含什麼協定、算法之類的内容)

如何解析RTP標頭,看我以前的一篇文章:解析RTP包的頭部結構

<------------簡單介紹下PS流格式----------->

因為一般視訊資料都是采用rtp打包發送,是以這裡我就把ps封裝和rtp封裝放在一起講.

  1. 視訊關鍵幀的封裝(I幀)

    RTP + PSheader + PS system header + PS system Map + PES header +h264 data

  2. 視訊非關鍵幀的封裝(P幀、B幀)

    RTP +PS header + PES header + h264 data

其實,要在這裡三句兩句,是很難講清楚PS流的封裝格式,是以呢,借助抓包,用圖解的方式,更容易了解PS的封裝格式。這是我用GB28181協定呼叫的一個海康攝像頭,通過udp.port==26876的方式,過濾出攝像頭傳過來的PS流,其實就是一堆以UDP方式,發送過來的RTP包,隻不過是RTP包裡面有PS包,PS包裡面裝的才是H264的包。用圖解一下:

猶豫很久終于寫下來了,從海康攝像頭的ps流中提取h264原始資料

一個真實的PS包

就像我在圖中寫的,PS流中,PES包才是關鍵。對方傳過來的H264的資料,最終是放到這個PES包裡面的,前面那些PS標頭、PS system系統頭、PSM頭都是PS流協定的格式,不包含H264的真實資料,是以找到PES包是關鍵,因為資料在它裡面。下圖展示下PS流包的裝載東西的層次:

猶豫很久終于寫下來了,從海康攝像頭的ps流中提取h264原始資料

PS流包裝載内容的層次

下面就用剛才那個抓包的截圖,把裡面的十六進制的資料複制出來:

猶豫很久終于寫下來了,從海康攝像頭的ps流中提取h264原始資料

就解析紅框中PS流内容

内容如下:

猶豫很久終于寫下來了,從海康攝像頭的ps流中提取h264原始資料

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碼流。

【多餘的解釋:】

其實也不想解釋了,寫這麼多,想看懂的,肯定會去看懂。歡迎點贊+收藏,謝謝老鐵。

繼續閱讀