• 【视频编解码·学习笔记】4. H.264的码流封装格式 & 提取NAL有效数据


    一、码流封装格式简单介绍:

    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)
    1

    有用数据前面会加 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打开此文件,可以看到该文件的数据流:
    2
    接下来将写一个小程序,从二进制码流文件中截取实际的NAL数据。

    二、C++程序 从码流中提取NAL有效数据:

    新建一个VS工程,配置工程属性。将【常规-输出目录】和【调试-工作目录】改为$(SolutionDir)bin\$(Configuration)\,【调试-命令参数】改为test.264编译、运行程序。
    3
    在 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

  • 相关阅读:
    SELECT INTO 和 INSERT INTO SELECT 两种表复制语句
    Oracle中如何插入特殊字符: &amp;amp; 和 &amp;#39; (多种解决方案)
    oracle学习链接
    ORACLE中的游标Cursor总结
    SVN使用教程总结
    SVN使用教程之-分支/标记 合并 subeclipse
    weblogic启动 web应用ssh关闭 nohup命令
    Oracle 学习之:ASCII,CHR函数的作用和用法
    Oracle数据库单表循环提取输出
    设计模式-七大原则
  • 原文地址:https://www.cnblogs.com/shuofxz/p/8416222.html
Copyright © 2020-2023  润新知