【视频编解码·学习笔记】4. H.264的码流封装格式
一、码流封装格式简单介绍:
H.264的语法元素进行编码后,生成的输出数据都封装为NAL Unit进行传递,多个NAL Unit的数据组合在一起形成总的输出码流。对于不同的应用场景,NAL规定了一种通用的格式适应不同的传输封装类型。
通常NAL Unit的传输格式分两大类:字节流格式和RTP包格式
字节流格式:
- 大部分编码器的默认输出格式
- 每个NAL Unit以规定格式的起始码分割
- 起始码:0x 00 00 00 01 或 0x 00 00 01
RTP数据包格式:
- NAL Unit按照RTP数据包的格式封装
- 使用RTP包格式不需要额外的分割识别码,在RTP包的封装信息中有相应的数据长度信息。
- 可以在NAL Unit的起始位置用一个固定长度的长度码表示整个NAL Unit的长度
实际应用中字节流格式更为常用,下面的均以字节流格式来介绍。
通过查阅H.264官方说明文档,了解NAL字节流格式(在附录B)
![]()
有用数据前面会加 0x 00 00 00 01 或 0x 00 00 01,作为起始码,两个起始码中间包含的即为有用数据流
如: 00 00 00 01 43 23 56 78 32 1A 59 2D 78 00 00 00 01 C3 E2 …… 中,红色的部分即为有效数据。
本次使用上一篇笔记中生成的test.264作为例子。
使用Ultra Edit打开此文件,可以看到该文件的数据流:
![]()
接下来将写一个小程序,从二进制码流文件中截取实际的NAL数据。
二、C++程序 从码流中提取NAL有效数据:
新建一个VS工程,配置工程属性。将【常规-输出目录】和【调试-工作目录】改为$(SolutionDir)bin\$(Configuration)\,【调试-命令参数】改为test.264编译、运行程序。
![]()
在 bin\debug 目录下可看到生成的exe执行文件
接下来编写程序的功能:
提取起始码之间的有效数据
程序思路:
从码流中寻找 00 00 00 01 或 00 00 01序列,后面就是有效数据流,将之后的数据保存起来,直到遇到下一个(00) 00 00 01 停止。
下面开始编写程序:
① 打开码流文件
使用下面的代码测试,比较简单,不再解释,最后记得要把文件流关掉。
int _tmain(int argc, _TCHAR* argv[])
{
FILE *pFile_in = NULL;
// 打开刚才导入的二进制码流文件
_tfopen_s(&pFile_in, argv[1], _T("rb"));
// 判断文件是否打开成功
if (!pFile_in)
{
printf("Error: Open File failed. \n");
}
fclose(pFile_in);
return 0;
}
② 寻找起始码
- 使用数据类型
unsigned char数据类型来存储单个字节码 - 为了减少内存使用,使用数组 refix3,存储连续的三个字节码
- 数组循环使用,新进来的数据放在弹出那位数据的位置上
- 即:数组的存数顺序为 [0][1][2],下一个字符放在[0]的位置上,此时数据顺序为[1][2][0],再下一次[2][0][1]以此类推
- 由于起始码有两种格式00 00 01 和 00 00 00 01,因此需要有两个判断分别对应
代码如下:
typedef unsigned char uint8;
static int find_nal_prefix(FILE **pFileIn)
{
FILE *pFile = *pFileIn;
// 00 00 00 01 x x x x x 00 00 00 01
// 以下方法为了减少内存,及向回移动文件指针的操作
uint8 prefix[3] = { 0 };
/*
依次比较 [0][1][2] = {0 0 0}; 若不是,将下一个字符放到[0]的位置 -> [1][2][0] = {0 0 0} ; 下次放到[1]的位置,以此类推
找到三个连0之后,还需判断下一个字符是否为1, getc() = 1 -> 00 00 00 01
以及判断 [0][1][2] = {0 0 1} -> [1][2][0] = {0 0 1} 等,若出现这种序列则表示找到文件头
*/
// 标记当前文件指针位置
int pos = 0;
// 标记查找的状态
int getPrefix = 0;
// 读取三个字节
for (int idx = 0; idx < 3; idx++)
{
prefix[idx] = getc(pFile);
}
while (!feof(pFile))
{
if ((prefix[pos % 3] == 0) && (prefix[(pos + 1) % 3] == 0) && (prefix[(pos + 2) % 3] == 1))
{
// 0x 00 00 01 found
getPrefix = 1;
break;
}
else if((prefix[pos % 3] == 0) && (prefix[(pos + 1) % 3] == 0) && (prefix[(pos + 2) % 3] == 0))
{
if (1 == getc(pFile))
{
// 0x 00 00 00 01 found
getPrefix = 2;
break;
}
}
else
{
fileByte = getc(pFile);
prefix[(pos++) % 3] = fileByte;
}
}
return getPrefix;
}
③ 提取有效数据
- 使用容器vector 存储有效数据
- 函数find_nal_prefix() 添加参数 vector &nalBytes
- 每次读取的数据都直接push到nalBytes中,若遇到起始码再把起始码pop掉
- 本函数需要重复执行,第一次文件指针移动到有效数据起始位置;第二次提取两段起始码间的有效数据;第三次在移动到下一个起始码后;第四次提取有效数据... 以此类推。
函数调整为:
static int find_nal_prefix(FILE **pFileIn, vector<uint8> &nalBytes)
{
FILE *pFile = *pFileIn;
// 00 00 00 01 x x x x x 00 00 00 01
// 以下方法为了减少内存,及向回移动文件指针的操作
uint8 prefix[3] = { 0 };
// 表示读进来字节的数值
uint8 fileByte;
/*
依次比较 [0][1][2] = {0 0 0}; 若不是,将下一个字符放到[0]的位置 -> [1][2][0] = {0 0 0} ; 下次放到[1]的位置,以此类推
找到三个连0之后,还需判断下一个字符是否为1, getc() = 1 -> 00 00 00 01
以及判断 [0][1][2] = {0 0 1} -> [1][2][0] = {0 0 1} 等,若出现这种序列则表示找到文件头
*/
nalBytes.clear();
// 标记当前文件指针位置
int pos = 0;
// 标记查找的状态
int getPrefix = 0;
// 读取三个字节
for (int idx = 0; idx < 3; idx++)
{
prefix[idx] = getc(pFile);
// 每次读进来的字节 都放入vector中
nalBytes.push_back(prefix[idx]);
}
while (!feof(pFile))
{
if ((prefix[pos % 3] == 0) && (prefix[(pos + 1) % 3] == 0) && (prefix[(pos + 2) % 3] == 1))
{
// 0x 00 00 01 found
getPrefix = 1;
// 这三个字符没用,pop掉
nalBytes.pop_back();
nalBytes.pop_back();
nalBytes.pop_back();
break;
}
else if((prefix[pos % 3] == 0) && (prefix[(pos + 1) % 3] == 0) && (prefix[(pos + 2) % 3] == 0))
{
if (1 == getc(pFile))
{
// 0x 00 00 00 01 found
getPrefix = 2;
// 这三个字符没用,pop掉 (最后那个1没填到vector中,不用pop)
nalBytes.pop_back();
nalBytes.pop_back();
nalBytes.pop_back();
break;
}
}
else
{
fileByte = getc(pFile);
prefix[(pos++) % 3] = fileByte;
nalBytes.push_back(fileByte);
}
}
return getPrefix;
}
主函数调整为:
#include "stdafx.h"
#include <stdio.h>
#include <vector>
typedef unsigned char uint8;
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
FILE *pFile_in = NULL;
// 打开刚才导入的二进制码流文件
_tfopen_s(&pFile_in, argv[1], _T("rb"));
// 判断文件是否打开成功
if (!pFile_in)
{
printf("Error: Open File failed. \n");
}
vector<uint8> nalBytes;
find_nal_prefix(&pFile_in, nalBytes);
find_nal_prefix(&pFile_in, nalBytes);
for (int idx = 0; idx < nalBytes.size(); idx++)
{
printf("%x ", nalBytes.at(idx));
}
printf("\n");
find_nal_prefix(&pFile_in, nalBytes);
for (int idx = 0; idx < nalBytes.size(); idx++)
{
printf("%x ", nalBytes.at(idx));
}
printf("\n");
fclose(pFile_in);
return 0;
}
以第一节最后数据流为例,执行以上代码后,程序输出结果如下:
![]()
【视频编解码·学习笔记】4. H.264的码流封装格式的更多相关文章
- 【视频编解码·学习笔记】4. H.264的码流封装格式 & 提取NAL有效数据
一.码流封装格式简单介绍: H.264的语法元素进行编码后,生成的输出数据都封装为NAL Unit进行传递,多个NAL Unit的数据组合在一起形成总的输出码流.对于不同的应用场景,NAL规定了一种通 ...
- 视音频编解码学习工程:H.264分析器
=====================================================视音频编解码学习工程系列文章列表: 视音频编解码学习工程:H.264分析器 视音频编解码学习工 ...
- 【视频编解码·学习笔记】8. 熵编码算法:基本算法列举 & 指数哥伦布编码
一.H.264中的熵编码基本方法: 熵编码具有消除数据之间统计冗余的功能,在编码端作为最后一道工序,将语法元素写入输出码流 熵解码作为解码过程的第一步,将码流解析出语法元素供后续步骤重建图像使用 在H ...
- 【视频编解码·学习笔记】11. 提取SPS信息程序
一.准备工作: 回到之前SimpleH264Analyzer程序,找到SPS信息,并对其做解析 调整项目目录结构: 修改Global.h文件中代码,添加新数据类型UINT16,之前编写的工程中,UIN ...
- 【视频编解码·学习笔记】3. H.264视频编解码工程JM的下载与编解码
一.下载JM工程: JM是H.264标准制定团队所认可的官方参考软件.网址如下 http://iphome.hhi.de/suehring/tml/ 从页面中可找到相应的工程源码,本次选择JM 8.6 ...
- 【视频编解码·学习笔记】6. H.264码流分析工程创建
一.准备工作: 新建一个VS工程SimpleH264Analyzer, 修改工程属性参数-> 输出目录:$(SolutionDir)bin\$(Configuration)\,工作目录:$(So ...
- 【视频编解码·学习笔记】2. H.264简介
一.H.264视频编码标准 H.264视频编码标准是ITU-T与MPEG合作产生的又一巨大成果,自颁布之日起就在业界产生了巨大影响.严格地讲,H.264标准是属于MPEG-4家族的一部分,即MPEG- ...
- 【视频编解码·学习笔记】5. NAL Unit 结构分析
在上篇笔记中通过一个小程序,可以提取NAL Unit所包含的的字节数据.H.264码流中的每一个NAL Unit的作用并不是相同的,而是根据不同的类型起不同的作用.下面将对NAL Unit中的数据进行 ...
- 【视频编解码·学习笔记】10. 序列参数集(SPS)介绍
一.SPS 相关概念: SPS即 "Sequence Paramater Set",又称作序列参数集. SPS中保存了一组编码视频序列(Coded video sequence)的 ...
随机推荐
- 数据结构-栈(应用篇)之快速排序法-C和C++的实现
一.原理解析 快速排序法: 基本思路是,从第一个元素开始,把所有比它大的元素放在它后面,把所有比它小的元素放前面.然后划分它前面和后面的所有元素,分别再做快速排序,直到无法再划分为止. 在以下程序案例 ...
- 解决:java.io.IOException: No FileSystem for scheme: hdfs
解决:java.io.IOException: No FileSystem for scheme: hdfs 开发项目初期,写完代码开始放到服务器上开始测试的时候,报出这样的一个错,不知道怎么处理了, ...
- Django----->一周后的重温
Django 一 什么是web框架? 框架,即framework,特指为解决一个开放性问题而设计的具有一定约束性的支撑结构,使用框架可以帮你快速开发特定的系统,简单地说,就是你用别人搭建好的舞台来 ...
- BZOJ:4659&&BZOJ:2694: Lcm
Description 给出A,B,考虑所有满足l<=a<=A,l<=b<=B,且不存在n>1使得n^2同时整除a和b的有序数 对(a,b),求其lcm(a,b)之和.答 ...
- SecureCRT连接虚拟机中的Linux系统(Ubuntu)_Linux教程
有道云笔记链接地址: https://note.youdao.com/share/?id=826781e7ca1fd1223f6a43f4dc2c9b5d&type=note#/
- 三 : spring-uploadify上传文件
一 : applicationContext.xml中:必须声明不然获取不到<!-- 上传文件的配置 --> <bean id="multipartResolver&quo ...
- [国嵌攻略][072][Linux应用程序地址布局]
程序构成 代码段.数据段.BSS段(Block Started by Symbol,又叫:未初始化数据段).堆(heap)和栈(stack).这些部分构成了Linux应用程序的重要组成部分. 内存布局 ...
- Mybatis中是否需要依赖配置文件的名称要和mapper接口的名称一致 params错误
一:当核心配置文件mapper标签下以resource形式指向依赖配置文件时,不需要 这样就可以加载到其相应的依赖配置文件通过namespace找到其相应的方法 二:如果mapper标签下以packa ...
- 织梦dedecsm系统"企业简介"类单栏目模版如何修改和调用
2013-1-12 14:46 | 发布者: moke | 栏目:dedecms教程 我们的模版里应该都有article_article.htm这个模版,这个模版是文章内容页模板,也就是 ...
- vi命令加行号查找替换等命令
一.加行号 : set nu二.vi查找: 当你用vi打开一个文件后,因为文件太长,如何才能找到你所要查找的关键字呢?在vi里可没有菜单-〉查找, ...