这段时间,一直在忙一件事情。通过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码流。
【多余的解释:】
其实也不想解释了,写这么多,想看懂的,肯定会去看懂。欢迎点赞+收藏,谢谢老铁。