学习笔记(新手):从PS封装格式的视频文件中提取H.264及Nalu(方法+资料+代码)

学习笔记(新手):从PS封装格式的视频文件中提取H.264及Nalu(方法+资料+代码)

目的

从一个.ps文件中提取出H.264裸码,博主此前从未了解过音视频的相关知识,以下内容适合新手理解这些概念和过程,并可以进行相关实验。
初学,以下内容根据我的理解来表述,希望大家能够指出我的错误!

相关概念

1、封装

所谓封装就是把一段视频流进行一个包装,相当于给它套上塑料袋、装进盒子里,一个盒子装不下的就分开装。因此对于计算机里的一段视频,它本身是一段二进制的码流,经过封装之后,分成了若干部分,在每一部分前就会有该部分的相关信息。比如说,接下来一段是视频还是音频,长度多少,是否有加密信息,其他信息等等。
那么解封装也就是一个“撕开包装”的过程。因此就需要了解你所要解封装的这个格式,是如何进行“包装”的,每一层都有哪些信息。

2、PS格式

H.264分为I帧(关键帧)、P帧和B帧,这三类具体是如何的跟算法相关,此篇不详述,简单来说就是I帧包含这一帧的全部信息,P帧和B帧就包含了与I帧的差异(以此达到压缩的目的)。
先看下I帧 和B、P帧的包格式。

在这里插入图片描述
学习笔记(新手):从PS封装格式的视频文件中提取H.264及Nalu(方法+资料+代码)_第1张图片
最外一层是PSH头,相当于一个产品最外面的纸箱,里面是PSM头,相当于纸箱里小盒子(只有I帧有),然后就是小盒子里的塑料袋包装,也就PES头,然后就是实际的数据。在一个PSH里面可能有1个或n个PES。

PSH

先看PSH头的格式,具体每一位是代表什么,我也理解的不深刻,所以就讲一些能够帮助我们找到H.264码流的信息。(具体内容看文末链接)0x000001BA是PSH头的标志,在数据中出现了这个就表示找到了一个PSH的头,头的长度最短为14个字节,在第14个字节的末尾3位,代表了后面的附加信息的长度,以此确定PSH头的长度。
对于I帧,可能还有拓展头(系统头)0x000001BB ,BB后两个字节的大小代表了拓展头接下来的长度,比如“00 00 01 BB 00 0C”,就表示接下来还有12个字节。
到此为止,确定了PSH头的全部内容

PSM

0x000001BC是PSM头的标志,跟在000001BC后的两位是说明了Program Stream map,他也是pes包的一种,包的长度program_stream_map_length,比如是00 5A,说明跟在其后的数据长度为90,跳过这其后的90byte数据是以000001E0开始的包。

PES

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)

Nalu

每个视频帧分为若干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.

希望大家能多指出我理解不足或者错误的部分!欢迎讨论!

你可能感兴趣的:(学习笔记(新手):从PS封装格式的视频文件中提取H.264及Nalu(方法+资料+代码))