天天看点

犹豫很久终于写下来了,从海康摄像头的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码流。

【多余的解释:】

其实也不想解释了,写这么多,想看懂的,肯定会去看懂。欢迎点赞+收藏,谢谢老铁。

继续阅读