学习笔记(新手):从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

相关文章

Python将博客内容html导出为Markdown格式

《Python将博客内容html导出为Markdown格式》Python将博客内容html导出为Markdown格式,通过博客url地址抓取文章,分析并提取出文章标题和内容,将内容构建成html,再转... 目录一、为什么要搞?二、准备如何搞?三、说搞咱就搞!抓取文章提取内容构建html转存markdown

Linux换行符的使用方法详解

《Linux换行符的使用方法详解》本文介绍了Linux中常用的换行符LF及其在文件中的表示,展示了如何使用sed命令替换换行符,并列举了与换行符处理相关的Linux命令,通过代码讲解的非常详细,需要的... 目录简介检测文件中的换行符使用 cat -A 查看换行符使用 od -c 检查字符换行符格式转换将

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

使用Jackson进行JSON生成与解析的新手指南

《使用Jackson进行JSON生成与解析的新手指南》这篇文章主要为大家详细介绍了如何使用Jackson进行JSON生成与解析处理,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 核心依赖2. 基础用法2.1 对象转 jsON(序列化)2.2 JSON 转对象(反序列化)3.

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

使用C#代码在PDF文档中添加、删除和替换图片

《使用C#代码在PDF文档中添加、删除和替换图片》在当今数字化文档处理场景中,动态操作PDF文档中的图像已成为企业级应用开发的核心需求之一,本文将介绍如何在.NET平台使用C#代码在PDF文档中添加、... 目录引言用C#添加图片到PDF文档用C#删除PDF文档中的图片用C#替换PDF文档中的图片引言在当

详解C#如何提取PDF文档中的图片

《详解C#如何提取PDF文档中的图片》提取图片可以将这些图像资源进行单独保存,方便后续在不同的项目中使用,下面我们就来看看如何使用C#通过代码从PDF文档中提取图片吧... 当 PDF 文件中包含有价值的图片,如艺术画作、设计素材、报告图表等,提取图片可以将这些图像资源进行单独保存,方便后续在不同的项目中使

Java中的String.valueOf()和toString()方法区别小结

《Java中的String.valueOf()和toString()方法区别小结》字符串操作是开发者日常编程任务中不可或缺的一部分,转换为字符串是一种常见需求,其中最常见的就是String.value... 目录String.valueOf()方法方法定义方法实现使用示例使用场景toString()方法方法

Java中List的contains()方法的使用小结

《Java中List的contains()方法的使用小结》List的contains()方法用于检查列表中是否包含指定的元素,借助equals()方法进行判断,下面就来介绍Java中List的c... 目录详细展开1. 方法签名2. 工作原理3. 使用示例4. 注意事项总结结论:List 的 contain

C#使用SQLite进行大数据量高效处理的代码示例

《C#使用SQLite进行大数据量高效处理的代码示例》在软件开发中,高效处理大数据量是一个常见且具有挑战性的任务,SQLite因其零配置、嵌入式、跨平台的特性,成为许多开发者的首选数据库,本文将深入探... 目录前言准备工作数据实体核心技术批量插入:从乌龟到猎豹的蜕变分页查询:加载百万数据异步处理:拒绝界面