一准备工作:
新建一个 VS 工程 SimpleH264Analyzer, 修改工程属性参数 -> 输出目录:
$(SolutionDir) bin\$(Configuration)\
, 工作目录:
$(SolutionDir) bin\$(Configuration)\
编译一下工程, 工程目录下会生成 bin 文件夹, 其中的 debug 文件夹中有刚才编译生成的 exe 文件将一个. 264 视频文件拷贝到这个文件夹中 (本次使用的仍是学习笔记 3 中生成的. 264 文件)
将这个文件作为输入参数传到工程中: 属性 -> 调试 -> 命令参数: test.264 (最后那个文件名根据自己的改)
更改目录结构, 并新建两个文件 Stream.h Stream.cpp, 更改后目录结构如下:
在 Stream.h 头文件中, 新建一个类 CStreamFile, 用来表示. 264 文件, 其中包括构造函数私有成员变量, 及自定义函数代码如下:
- #ifndef _STREAM_H_
- #define _STREAM_H_
- #include <vector>
- class CStreamFile
- {
- public:
- CStreamFile(TCHAR *fileName);
- ~CStreamFile();
- // Open API
- int Parse_h264_bitstream();
- private:
- FILE *m_InputFile;
- TCHAR *m_fileName;
- std::vector<uint8> m_nalVec;
- // 用来打印日志
- void file_info();
- void file_error(int dex);
- // 提取 NAL 有效数据
- int find_nal_prefix();
- };
- #endif
在 Stream.cpp 文件中, 实现其构造方法及成员函数:
- #include "stdafx.h"
- #include "Stream.h"
- #include <iostream>
- using namespace std;
- // 构造函数完成打开文件操作
- CStreamFile::CStreamFile(TCHAR * fileName)
- {
- m_fileName = fileName;
- file_info();
- // 打开视频文件 (只读二进制)
- _tfopen_s(&m_InputFile, m_fileName, _T("rb"));
- if (NULL == m_InputFile)
- {
- file_error(0);
- }
- }
- // 析构函数完成关闭文件操作
- CStreamFile::~CStreamFile()
- {
- if (NULL != m_InputFile)
- {
- fclose(m_InputFile);
- m_InputFile = NULL;
- }
- }
- int CStreamFile::Parse_h264_bitstream()
- {
- return 0;
- }
- int CStreamFile::find_nal_prefix()
- {
- return 0;
- }
- // 打印文件信息
- void CStreamFile::file_info()
- {
- if (m_fileName)
- {
- wcout << L"File name:" << m_fileName << endl;
- }
- }
- // 打印错误信息
- void CStreamFile::file_error(int idx)
- {
- switch (idx)
- {
- case 0:
- wcout << L"Error: opening input file failed." << endl;
- break;
- default:
- break;
- }
- }
之后在主函数中, 编写打开文件代码, 测试以上代码能否正常执行:
- #include "stdafx.h"
- #include "Stream.h"
- int _tmain(int argc, _TCHAR* argv[])
- {
- CStreamFile h264stream(argv[1]);
- // 此函数作为最上层函数, 执行所有功能 (暂时还未写任何功能实现)
- h264stream.Parse_h264_bitstream();
- return 0;
- }
编译执行后, 在 cmd 窗口中, 能够打印出文件名称, 即为正确执行
接下来, 设置一个全局的头文件, 用来定义所有文件中都会用到的数据类型
在 Application 目录下, 新建 Global.h 头文件, 输入以下代码:
- #ifndef _GLOBAL_H_
- #define _GLOBAL_H_
- typedef unsigned char uint8;
- typedef unsigned int uint32;
- #endif // !_GLOBAL_H_
在 stdafx.h 文件中, 引入刚才新建的头文件:
#include "Global.h"
二提取 NAL Unit:
1. 提取 NAL 有效数据:
实现 find_nal_prefix() 函数实现方法与学习笔记 4 中代码基本相同, 仅修改一些变量名称 (学习笔记 4 中有详细讲解, 这里不再说明)Stream.cpp 文件中, 函数实现如下:
- int CStreamFile::find_nal_prefix()
- {
- uint8 prefix[3] = { 0 };
- uint8 fileByte;
- m_nalVec.clear();
- // 标记当前文件指针位置
- int pos = 0;
- // 标记查找的状态
- int getPrefix = 0;
- // 读取三个字节
- for (int idx = 0; idx < 3; idx++)
- {
- prefix[idx] = getc(m_InputFile);
- // 每次读进来的字节 都放入 vector 中
- m_nalVec.push_back(prefix[idx]);
- }
- while (!feof(m_InputFile))
- {
- if ((prefix[pos % 3] == 0) && (prefix[(pos + 1) % 3] == 0) && (prefix[(pos + 2) % 3] == 1))
- {
- // 0x 00 00 01 found
- getPrefix = 1;
- m_nalVec.pop_back();
- m_nalVec.pop_back();
- m_nalVec.pop_back();
- break;
- }
- else if ((prefix[pos % 3] == 0) && (prefix[(pos + 1) % 3] == 0) && (prefix[(pos + 2) % 3] == 0))
- {
- if (1 == getc(m_InputFile))
- {
- // 0x 00 00 00 01 found
- getPrefix = 2;
- m_nalVec.pop_back();
- m_nalVec.pop_back();
- m_nalVec.pop_back();
- break;
- }
- }
- else
- {
- fileByte = getc(m_InputFile);
- prefix[(pos++) % 3] = fileByte;
- m_nalVec.push_back(fileByte);
- }
- }
- return getPrefix;
- }
修改 Stream.cpp 中 Parse_h264_bitstream() 函数, 循环调用 find_nal_prefix() 函数, 不断获取起始码之间数据
- int CStreamFile::Parse_h264_bitstream()
- {
- int ret = 0;
- do
- {
- ret = find_nal_prefix();
- } while (ret);
- return 0;
- }
对此文件编译调试, 查看以上所写代码是否有问题:
第一次循环时, 文件指针移动到第一个起始码后; 第二次循环时, 读取到两个起始码间的有效数据, 通过调试可看到如下数据, 与 test.264 中第一组有效数据相同:
2. 提取 NAL Unit 类别:
首先提取每一个 NAL Unit 的类别, 修改 Parse_h264_bitstream() 函数如下:
- int CStreamFile::Parse_h264_bitstream()
- {
- int ret = 0;
- do
- {
- ret = find_nal_prefix();
- // 解析 NAL UNIT
- // 第一次执行循环的时候, m_nalVec 为空, 因此加个判断
- if (m_nalVec.size())
- {
- // 识别 NAL Unit 类别
- // NAL Unit 第一个字节为 NAL Header, 后面 5 位表示 NAL Type(使用按位与运算, 截取后面五位数据)
- uint8 nalType = m_nalVec[0] & 0x1F;
- wcout << L"NAL Unit Type:" << nalType << endl;
- }
- } while (ret);
- return 0;
- }
编译运行后, 结果如下:
其所对应的类型为 (可从 H.264 官方文档, 表 7-1 中查到):
三 NAL Unit 解封装:
1. EBSP -> RBSP:
去除竞争校验位 (详细概念看学习笔记 5)
简而言之, 就是去除两个连零后面的 0300 00 03 xx xx xx (其中的 03 即为竞争校验位, 在拆包的时候需要去除)
在 CStreamFile 类中添加私有函数 void ebsp_to_rbsp();
函数实现如下:
- void CStreamFile::ebsp_to_rbsp()
- {
- // 00 00 03 连续两个 00 后面的 03 是防止竞争校验字节, 需要去掉
- // 在序列中找 03, 在查看前面两个是不是 00, 如果是, 就去掉 03
- if (m_nalVec.size() < 3)
- {
- return;
- }
- for (vector<uint8>::iterator itor = m_nalVec.begin() + 2; itor != m_nalVec.end(); )
- {
- // 迭代器增长幅度为空, 写在循环内部, 方便删除元素
- if ((3 == *itor) && (0 == *(itor - 1)) && (0 == *(itor - 2)))
- {
- // 此处使用 erase() 时需要注意:
- // 1 当调用 erase() 后 Itor 迭代器就失效了, 变成了一野指针
- // 2 而 erase() 这个函数会返回一个指针, 仍指向清除元素的位置, 只不过后面所有的数据都向前移动
- itor = m_nalVec.erase(itor);
- }
- else
- {
- itor++;
- }
- }
- }
- 2. RBSP -> SODB:
这里本应还有 RBSP -> SODB 的部分, 也就是去除 rbsp_trailing_bits , 但对于分析 NAL Body 内部语法元素不会造成实际影响, 这部分暂时空缺, 有兴趣的可以自己实现一下
对于 NAL Body 编码方式的解析, 会涉及熵编码知识, 将在后续笔记中进行介绍
来源: https://www.cnblogs.com/shuofxz/p/8443392.html