从一个.ps文件中提取出H.264裸码,博主此前从未了解过音视频的相关知识,以下内容适合新手理解这些概念和过程,并可以进行相关实验。
初学,以下内容根据我的理解来表述,希望大家能够指出我的错误!
所谓封装就是把一段视频流进行一个包装,相当于给它套上塑料袋、装进盒子里,一个盒子装不下的就分开装。因此对于计算机里的一段视频,它本身是一段二进制的码流,经过封装之后,分成了若干部分,在每一部分前就会有该部分的相关信息。比如说,接下来一段是视频还是音频,长度多少,是否有加密信息,其他信息等等。
那么解封装也就是一个“撕开包装”的过程。因此就需要了解你所要解封装的这个格式,是如何进行“包装”的,每一层都有哪些信息。
H.264分为I帧(关键帧)、P帧和B帧,这三类具体是如何的跟算法相关,此篇不详述,简单来说就是I帧包含这一帧的全部信息,P帧和B帧就包含了与I帧的差异(以此达到压缩的目的)。
先看下I帧 和B、P帧的包格式。
最外一层是PSH头,相当于一个产品最外面的纸箱,里面是PSM头,相当于纸箱里小盒子(只有I帧有),然后就是小盒子里的塑料袋包装,也就PES头,然后就是实际的数据。在一个PSH里面可能有1个或n个PES。
先看PSH头的格式,具体每一位是代表什么,我也理解的不深刻,所以就讲一些能够帮助我们找到H.264码流的信息。(具体内容看文末链接)0x000001BA是PSH头的标志,在数据中出现了这个就表示找到了一个PSH的头,头的长度最短为14个字节,在第14个字节的末尾3位,代表了后面的附加信息的长度,以此确定PSH头的长度。
对于I帧,可能还有拓展头(系统头)0x000001BB ,BB后两个字节的大小代表了拓展头接下来的长度,比如“00 00 01 BB 00 0C”,就表示接下来还有12个字节。
到此为止,确定了PSH头的全部内容
0x000001BC是PSM头的标志,跟在000001BC后的两位是说明了Program Stream map,他也是pes包的一种,包的长度program_stream_map_length,比如是00 5A,说明跟在其后的数据长度为90,跳过这其后的90byte数据是以000001E0开始的包。
what is 000001E0?找到了这个差不多就快成功了,因为这是代表了PES头,E0表示视频流,C0表示音频流,BD表示私有数据流。
e.g.00 00 01 E0 00 1A 8C 80 0A 21 1C C9 AE 0D FF FF FF FF FC
00 1A: 2字节表示长度,表示再这两个字节之后的数据长度(如果有附加数据包括了其后的附加数据,和负载数据,我们希望得到的是负载数据,因此要略过附加数据部分)
8C 80 这两个字节跟长度无关,跳过。
此后的一个字节就是附加信息的长度,0A就是10个字节,也就是21 1C C9 AE 0D FF FF FF FF FC ,此后就是帧数的数据。
最后的五个字节的FF FF FF FF FC是海康自己的一个自减计数值 ,具体几个字节也是根据“0A”这一位的值来定的,不需要过度纠结。
至此,把之后的数据单独取出来直到下一个“00 00 01 E0”/“00 00 01 C0”/“00 00 01 BA”(一个PES后跟的可能是PES 也可能是PS)
每个视频帧分为若干NAL单元(NALU)。视频PS格式码流以NALU为单位进行打包。若当前为I帧或P帧的第一个NALU则需加PSH头部。若当前为I帧的第一个NALU还需要加PSM头部。每个NALU分为若干段,每段前需加PES头部,每段数据与PES头部组成PES包。
我们在提取出H.264裸码之后,再做一步,就是找出其中每一个Nalu的起始位置、长度和类别。
Nalu头的标志有两种 0x000001或者0x00000001,这只是标志,不是种类。
标志后的一位的值 0x67代表SPS,0x68代表PPS
我认为这篇博客已经写得非常详细和易懂了,除了0x67,0x68还有很多,了解Nalu的规则有利于我们之后学习RTP。
编写代码的时候,就只要标志头就可以了,但要小心两种标志都是可以的,不要出bug。
这里就不放这部分代码了,只放提取H.264的代码。
链接: https://blog.csdn.net/qq_29350001/article/details/78226286.
对于码流的提取,最关键的就是搞清楚实际的码流数据从哪一位开始,到哪一位结束,因此就要了解清楚头信息里的关于其他信息的长度,才能给准确定位。在编写代码时,我的思路就是:
1、遍历每一字节,先找到PSH头,然后确定PSH头的长度
2、取紧接的4个字节,判断是系统头还是PSM头还是PES头
3、根据上面说的规则确定长度
4、提取数据,检测下面4个字节是哪一种头,直到遍历结束。
代码没有经过优化和简化,作为参考,如果看完上面的内容知道了,那就不需要看代码也行。
当然,为了达到我们要的目的,可以直接遍历,然后找PES头的标志E0,C0,这样子代码三十行就可以搞定了。
(不建议大家直接看代码,先要理解,再看代码)
// An highlighted block
#include <stdio.h>
#pragma pack(1)
#include <WINSOCK2.H>
#pragma comment(lib, "ws2_32.lib")
#define SEEK_CUR 1
#define PRINTF_DEBUG
#define SEEK_CUR2 2
#define MAX_PS_STARTCODE_LEN 4
#define MAX_PDTS_LEN 5
#define MAX_ES_NUMS 6
#define MAX_PDTS_STRING_LEN 12
#define MMIN_PS_HEADER_LEN 14
#define MAX_PES_PACKET_LEN 65496
#define SCODE_PS_END 0x000001B9
#define SCODE_PS_HEADER 0x000001BA
#define SCODE_PES_E0 0x000001E0
#define SCODE_PES_C0 0x000001C0
#define SCODE_PES_BD 0x000001BD
#define SCODE_PS_SYSTEM_HEADER 0x000001BB
#define SCODE_PS_SYSTEM_MAP_HEADER 0x000001BC
unsigned char *p = NULL;
int psheader(FILE *fp, char *p)
{
//printf("!!!!!!!!1");
unsigned char PS_header_lencode;//
unsigned char F_lencode;//
unsigned int PS_header_systemcode = 0;
unsigned int pes_header_code = 0;
unsigned long long data = 0;
unsigned short stream_map_lencode = 0;
unsigned short int PS_system_header_lencode = 0;
int PS_header_len = 0;
int PS_system_header_len = 0;
int stream_map_len = 0;
int F_len = 0;
int count = 1;
fseek(fp, 9, SEEK_CUR);//指向PS头第14个字节
if (fread(&PS_header_lencode, 1, 1, fp))//读取第14个字节(1个字节)
{
//
PS_header_len = PS_header_lencode & 0x07;//第14个字节最后3位说明了包头14字节后填充数据的长度
printf("5找到了PS_header_lencode+PS_header_lencode: 0x%8X\n", PS_header_lencode);
printf("6 %d\n", PS_header_len);
fseek(fp, PS_header_len, SEEK_CUR);//跳过填充数据的长度
fread(&PS_header_systemcode, 4, 1, fp);//读取系统头的标识(4个字节)BB或BC
PS_header_systemcode = htonl(PS_header_systemcode);
printf("7找到了PS_header_systemcode+PS_header_systemcode: 0x%8X\n", PS_header_systemcode);
//判断是否有系统头
if (SCODE_PS_SYSTEM_HEADER == PS_header_systemcode)
{
printf("8BB");
fseek(fp, 4, SEEK_CUR);//跳过系统头
fread(&PS_system_header_lencode, 2, 1, fp);
PS_system_header_len = PS_system_header_lencode;
printf("9 %d", PS_system_header_lencode);
fseek(fp, PS_system_header_len, SEEK_CUR);
}
if (SCODE_PS_SYSTEM_MAP_HEADER == PS_header_systemcode)
{
fread(&stream_map_lencode, 2, 1, fp);
stream_map_lencode = htons(stream_map_lencode);
stream_map_len = stream_map_lencode;
printf("11找到了stream_map_lencode+stream_map_lencode: 0x%8X\n", stream_map_lencode);
printf("12 %d\n", stream_map_len);
fseek(fp, stream_map_len, SEEK_CUR);
fread(&pes_header_code, 4, 1, fp);//接下来就是pes包 000001E0/C0
pes_header_code = htonl(pes_header_code);
printf("13找到了pes_header_code+pes_header_code: 0x%8X +%d\n", pes_header_code,count);
count++;
}
else
{pes_header_code = PS_header_systemcode;}
while (pes_header_code == SCODE_PES_E0 || pes_header_code == SCODE_PES_C0 || pes_header_code == SCODE_PES_BD)
{
unsigned short data_lencode;
int data_len;
fread(&data_lencode, 2, 1, fp);
data_lencode = htons(data_lencode);
data_len = data_lencode;//Y
printf("14 %d\n", data_lencode);
//00 00 01 E0 Y Y 8c 80 X (x个)+h264数据
//E0后3+X是要跳过的数据
fseek(fp, 2, SEEK_CUR);
fread(&F_lencode, 1, 1, fp);//取F_lencode
F_len = F_lencode;
fseek(fp, F_len, SEEK_CUR);
printf("15 %d\n", F_len);
fread(p, data_len - 3 - F_len, 1, fp);//读出264数据
//写入
if (SCODE_PES_E0 == pes_header_code)
{
FILE *fp2;
fp2 = fopen("C:\\Users\\zhangjindong\\Desktop\\Vdata.txt", "ab+");
fwrite(p, data_len - 3 - F_len, 1, fp2);
fclose(fp2);
printf("写入成功\n");
}
if (pes_header_code == SCODE_PES_C0)
{
FILE *fp2;
fp2 = fopen("C:\\Users\\zhangjindong\\Desktop\\newA.txt", "ab+");
fwrite(p, data_len - 3 - F_len, 1, fp2);
fclose(fp2);
printf("写入成功\n");
}
fread(&pes_header_code, 4, 1, fp);
pes_header_code = htonl(pes_header_code);//更新pes标识
printf("16找到了pes_header_code+pes_header_code: 0x%8X +%d\n", pes_header_code, count);
count++;
//fseek(fp, 4, SEEK_CUR);
//break;调试用中断
}
}
return pes_header_code;
}
int main()
{
int readLen = 0;
int t = 2;
int h264Data;
unsigned int startCode = 0;
p = malloc(MAX_PES_PACKET_LEN);
//char *data;
FILE *fp;
if (fp = fopen("C:\\Users\\zhangjindong\\Desktop\\first.mp4", "rb"))
printf("1打开成功\n");
else
printf("2打开失败\n");
while (1)
{
readLen = fread(&startCode, 4, 1, fp);
startCode = htonl(startCode);
if (SCODE_PS_HEADER == startCode)
{
break;
}
}
while (SCODE_PS_HEADER == startCode )
{
printf("3找到了PS_header+startCode: 0x%08X\n", startCode);
startCode = psheader(fp,p);
//break;//向后解析函数调用
}
if(SCODE_PS_HEADER != startCode)
{
printf("4不是: 0x%08X\n", startCode);
fseek(fp, 0, SEEK_CUR);
}
free(p);
fclose(fp);
printf("OVER!");
}
(代码有好多我分步测试的打印,有些多余的部分也没有删掉,大家凑合看看吧,不要学习我这种不好的编程习惯!!)
链接: https://www.cnblogs.com/lihaiping/p/4181607.html.
链接: https://blog.csdn.net/wwyyxx26/article/details/15224879.
希望大家能多指出我理解不足或者错误的部分!欢迎讨论!