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

本文主要是介绍学习笔记(新手):从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帧的包格式。

在这里插入图片描述
在这里插入图片描述
最外一层是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或BCPS_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/C0pes_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;//Yprintf("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_lencodeF_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(方法+资料+代码)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/235912

相关文章

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Git中恢复已删除分支的几种方法

《Git中恢复已删除分支的几种方法》:本文主要介绍在Git中恢复已删除分支的几种方法,包括查找提交记录、恢复分支、推送恢复的分支等步骤,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录1. 恢复本地删除的分支场景方法2. 恢复远程删除的分支场景方法3. 恢复未推送的本地删除分支场景方法4. 恢复

Python将大量遥感数据的值缩放指定倍数的方法(推荐)

《Python将大量遥感数据的值缩放指定倍数的方法(推荐)》本文介绍基于Python中的gdal模块,批量读取大量多波段遥感影像文件,分别对各波段数据加以数值处理,并将所得处理后数据保存为新的遥感影像... 本文介绍基于python中的gdal模块,批量读取大量多波段遥感影像文件,分别对各波段数据加以数值处

Window Server2016加入AD域的方法步骤

《WindowServer2016加入AD域的方法步骤》:本文主要介绍WindowServer2016加入AD域的方法步骤,包括配置DNS、检测ping通、更改计算机域、输入账号密码、重启服务... 目录一、 准备条件二、配置ServerB加入ServerA的AD域(test.ly)三、查看加入AD域后的变

Window Server2016 AD域的创建的方法步骤

《WindowServer2016AD域的创建的方法步骤》本文主要介绍了WindowServer2016AD域的创建的方法步骤,文中通过图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录一、准备条件二、在ServerA服务器中常见AD域管理器:三、创建AD域,域地址为“test.ly”

NFS实现多服务器文件的共享的方法步骤

《NFS实现多服务器文件的共享的方法步骤》NFS允许网络中的计算机之间共享资源,客户端可以透明地读写远端NFS服务器上的文件,本文就来介绍一下NFS实现多服务器文件的共享的方法步骤,感兴趣的可以了解一... 目录一、简介二、部署1、准备1、服务端和客户端:安装nfs-utils2、服务端:创建共享目录3、服

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

python实现pdf转word和excel的示例代码

《python实现pdf转word和excel的示例代码》本文主要介绍了python实现pdf转word和excel的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录一、引言二、python编程1,PDF转Word2,PDF转Excel三、前端页面效果展示总结一

在MyBatis的XML映射文件中<trim>元素所有场景下的完整使用示例代码

《在MyBatis的XML映射文件中<trim>元素所有场景下的完整使用示例代码》在MyBatis的XML映射文件中,trim元素用于动态添加SQL语句的一部分,处理前缀、后缀及多余的逗号或连接符,示... 在MyBATis的XML映射文件中,<trim>元素用于动态地添加SQL语句的一部分,例如SET或W

IDEA如何将String类型转json格式

《IDEA如何将String类型转json格式》在Java中,字符串字面量中的转义字符会被自动转换,但通过网络获取的字符串可能不会自动转换,为了解决IDEA无法识别JSON字符串的问题,可以在本地对字... 目录问题描述问题原因解决方案总结问题描述最近做项目需要使用Ai生成json,可生成String类型