IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++ 读取h264中的nalu -> 正文阅读

[C++知识库]C++ 读取h264中的nalu

前言

h264帧通常包含多个nalu,当我们需要封装为mp4的时候,就需要获取这些nalu,读取其中的sps和pps信息,以及视频帧。h264的打包格式有2种,一种是Annex-B,另一种是AVCC,本文提供Annex-B的解析方法。


一、原理说明

1.h264-Annex-B的结构

h264内部包含多个nalu,Annex-B格式的h264是通过startcode区分每个nalu。startcode的值为001或者0001,结构如下:
在这里插入图片描述

2.如何解析?

由于h264的’防止竞争 emulation prevention"机制确保了nalu内部不会出现startcode:001or0001,所以h264是可以按流解析的。其解析方法也很简单,首先查找startcode作为起点,接着查找下一个startcode作为终点,然后完成一个nalu的解析,循环往复。

在这里插入图片描述


二、代码实现

NaluParse.h

#pragma once
#include<vector>
/************************************************************************
* @Project:  	AC::NaluParse
* @Decription:  nalu解析工具,只适用于Annex-B
* @Verision:  	v1.0.0.0
* @Author:  	Xin Nie
* @Create:  	2022/2/20 13:10:17
* @LastUpdate:  2022/2/24 15:03:48
************************************************************************
* Copyright @ 2022. All rights reserved.
************************************************************************/
namespace AC {
	/// <summary>
	/// nalu实体
	/// </summary>
	class Nalu
	{
		friend class NaluParse;
	public:
		/// <summary>
		/// 获取nalu数据,不包含startcode
		/// </summary>
		/// <returns>nalu数据</returns>
		unsigned char* GetData();
		/// <summary>
		/// 获取nalu数据长度
		/// </summary>
		/// <returns>nalu数据长度</returns>
		int GetDataLength();
	private:
		unsigned char* _data;
		int _dataLength;
	};
	/// <summary>
	/// nalu解析工具
	/// </summary>
	class NaluParse {
	public:
		/// <summary>
		/// 写入流
		/// </summary>
		/// <param name="h264Stream">h264流</param>
		/// <param name="len">h264流长度</param>
		void SendH264Stream(unsigned char* h264Stream, int len);
		/// <summary>
		/// 解析h264流
		/// </summary>
		/// <param name="nalu">获取的nalu,内部提供的缓存头部前面有4bytes预留空间,此时只需外部流的缓冲也预留4bytes空间,则可以方便mp4视频帧的头部4bytes写入。</param>
		/// <returns>是否获取nalu</returns>
		bool ReceiveNalu(Nalu& nalu);
	private:
		static bool GetNextNalu(unsigned char* h264Data, int len, int& offset, int& lastNaluLen, int& startcodeLen);
		std::vector<unsigned char>_nalu;
		unsigned char* _h264Stream;
		int _len;
		int _offset;
		int _startcodeLength;
		int _state = 0;
	};
}

NaluParse .cpp

#include"NaluParse.h"
namespace AC {
	unsigned char* Nalu::GetData()
	{
		return _data;
	}
	int Nalu::GetDataLength()
	{
		return _dataLength;
	}
	void NaluParse::SendH264Stream(unsigned char* h264Stream, int len)
	{
		_h264Stream = h264Stream;
		_len = len;
		_offset = 0;
	}
	bool NaluParse::ReceiveNalu(Nalu& nalu)
	{
		while (1)
		{
			//状态机
			switch (_state)
			{
			case 0://查找起点
			{
				if (GetNextNalu(_h264Stream, _len, _offset, nalu._dataLength, _startcodeLength))
					//找到起点
				{
					_state = 2;
				}
				else
					//未找到起点
				{
					int i = _offset - 2;
					if (i < 0)
						i = 0;
					for (i; i < _len; i++)
						_nalu.push_back(_h264Stream[i]);
					_state = 1;
					return false;
				}
				break;
			}
			case 1://未找到起点
			{
				for (int i = _nalu.size(); i < 2; i++)
				{
					if (_offset < _len)
					{
						_nalu.push_back(_h264Stream[_offset++]);
					}
					else
					{
						return false;
					}
				}
				if (_offset < _len)
				{
					//检查上下部分拼接是否是起点,上一部分是00,下一部分是1
					if (_nalu[_nalu.size() - 2] == 0 && _nalu[_nalu.size() - 1] == 0 && _h264Stream[_offset] == 1)
					{
						
						_offset++;
						_state = 2;
						break;
					}
					else
					{
						_nalu.erase(_nalu.begin());
					}
				}
				else
				{
					return false;
				}
				if (_offset + 1 < _len)
				{
					//检查上下部分拼接是否是起点,上一部分是0,下一部分是01
					if (_nalu[_nalu.size() - 1] == 0 && _h264Stream[_offset] == 0 && _h264Stream[_offset + 1] == 1)
					{
						_offset += 2;
						_state = 2;
						break;
					}
					else
					{
						_nalu.clear();
					}
				}
				else
				{
					_nalu.push_back(_h264Stream[_offset++]);					
					return false;
				}
				_state = 0;
				break;
			}
			case 2://查找nalu
			{
				if (GetNextNalu(_h264Stream, _len, _offset, nalu._dataLength, _startcodeLength))
					//找到结尾
				{
					nalu._data = &_h264Stream[_offset - _startcodeLength - nalu._dataLength];
					_state = 2;
					return true;
				}
				else
					//未到结尾
				{
					if (nalu._dataLength > 0)
					{
						_state = 3;
						_nalu.resize(nalu._dataLength + 4);//+4是提供给外部的头部缓冲,便mp4的封装。
						_nalu[3] = 1;
						memcpy(_nalu.data() + 4, &_h264Stream[_offset - _startcodeLength - nalu._dataLength], nalu._dataLength);
					}
					return false;
				}
				break;
			}
			case 3://未找到结尾			
				for (int i = _nalu.size(); i < 6; i++)
				{
					if (_offset < _len)
					{
						_nalu.push_back(_h264Stream[_offset++]);
					}
					else
					{
						return false;
					}
				}
				if (_offset < _len)
				{
					//检查上下部分拼接是否是结尾,上一部分是00,下一部分是1
					if (_nalu[_nalu.size() - 2] == 0 && _nalu[_nalu.size() - 1] == 0 && _h264Stream[_offset] == 1)
					{
						if (_nalu[_nalu.size() - 3] == 0)
							//上一部分是000 下一部分是1的情况
						{
							nalu._dataLength = _nalu.size() - 3 - 4;
						}
						else
						{
							nalu._dataLength = _nalu.size() - 2 - 4;
						}
						nalu._data = _nalu.data() + 4;
						_state = 2;
						_offset += 1;
						return true;
					}
				}
				if (_offset + 1 < _len)
				{   //检查上下部分拼接是否是结尾,上一部分是0,下一部分是01
					if (_nalu[_nalu.size() - 1] == 0 && _h264Stream[_offset] == 0 && _h264Stream[_offset + 1] == 1)
					{
						if (_nalu[_nalu.size() - 2] == 0)
							//上一部分是00 下一部分是01的情况
						{
							nalu._dataLength = _nalu.size() - 2 - 4;
						}
						else
						{
							nalu._dataLength = _nalu.size() - 1 - 4;
						}
						nalu._data = _nalu.data() + 4;
						_state = 2;
						_offset += 2;
						return true;
					}
				}
				if (_offset + 2 < _len)
				{   //检查上下部分拼接是否是结尾,上一部分是0,下一部分是001
					if (_nalu[_nalu.size() - 1] == 0 && _h264Stream[_offset] == 0 && _h264Stream[_offset + 1] == 0 && _h264Stream[_offset + 2] == 1)
					{
						nalu._dataLength = _nalu.size() - 1 - 4;
						nalu._data = _nalu.data() + 4;
						_state = 2;
						_offset += 3;
						return true;
					}
				}
				bool find = GetNextNalu(_h264Stream, _len, _offset, nalu._dataLength, _startcodeLength);
				if (nalu._dataLength > 0)
				{
					int oldSize = _nalu.size();
					_nalu.resize(_nalu.size() + nalu._dataLength);
					memcpy(_nalu.data() + oldSize, &_h264Stream[_offset - _startcodeLength - nalu._dataLength], nalu._dataLength);
				}
				if (find)
				{
					_state = 2;
					nalu._dataLength = _nalu.size() - 4;
					nalu._data = _nalu.data() + 4;
					return  true;
				}
				else
				{
					return  false;
				}
				break;
			}
		}
	}
	bool NaluParse::GetNextNalu(unsigned char* h264Data, int len, int& offset, int& lastNaluLen, int& startcodeLen)
	{
		int oldOffset = offset;
		offset += 3;
		startcodeLen = 0;
		while (offset <= len)
		{
			if (h264Data[offset - 3] == 0 && h264Data[offset - 2] == 0 && h264Data[offset - 1] == 1)
			{
				if (offset > 3) {
					if (h264Data[offset - 4] == 0) {
						startcodeLen = 4;
					}
					else
					{
						startcodeLen = 3;
					}
				}
				else {
					startcodeLen = 3;
				}
				lastNaluLen = offset - oldOffset - startcodeLen;
				return true;
			}
			offset++;
		}
		if (offset > len)
			offset = len;
		lastNaluLen = offset - oldOffset;
		return false;
	}
}

三、使用示例

1.解析h264文件

#include<stdio.h>
#include"NaluParse.h"
int main(int argc, char* argv[])
{
	FILE* f;
	AC::NaluParse parse;
	AC::Nalu nalu;
	unsigned char fullBuffer[1028] = { 0,0,0,0,0 };
	//预留4bytes空间用于mp4封装
	unsigned char* buffer = fullBuffer + 4;
	f = fopen("test.h264", "rb+");
	if (!f)
	{
		printf("ERROR:Open h264 file fialed.\n");
		return -1;
	}
	while (1)
	{
		//读取h264流
		int size = fread(buffer, 1, 1024, f);
		if (size < 1)
		{
			break;
		}
		//写入h264流
		parse.SendH264Stream(buffer, size);
		//读取nalu
		while (parse.ReceiveNalu(nalu))
		{		
			switch (nalu.GetData()[0] & 0x1F)
			{
			case 01:
			case 02:
			case 03:
			case 04:
			case 05:
			{	unsigned char* pNalu = nalu.GetData();
				pNalu -= 4;
				pNalu[0] = (nalu.GetDataLength() >> 24) & 0xFF;
				pNalu[1] = (nalu.GetDataLength() >> 16) & 0xFF;
				pNalu[2] = (nalu.GetDataLength() >> 8) & 0xFF;
				pNalu[3] = (nalu.GetDataLength() >> 0) & 0xFF;
				//写入视频帧(pNalu,nalu.GetDataLength()+4);
			}
			break;
			case 7: // SPS
			{				
				//写入sps(nalu.GetData(),nalu.GetDataLength());			
			}
			break;
			case 8: // PPS
			{
				//写入pps(nalu.GetData(),nalu.GetDataLength());
			}
			break;
			default:
				break;
			}
		}
	}
	fclose(f);
	return 0;
}

总结

以上就是今天要讲的内容,对h264-Annex-B的解析原理上是比较简单的而且易于理解,但是在实现上却不太容易,由于是基于数据流解析的,很多情况都需要考虑。比如startcode刚好被上下两部分数据分割,就需要特殊判断。又比如流数据每次只能获取1byte时需要建立额外的缓冲存储流数据,达到基本长度后才能解析。还有流数据的长度随机不固定的时候也需要一定的处理等等。总的来说,实现起来并不容易,但是实现之后使用效果还是挺好的。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-02-26 11:12:28  更:2022-02-26 11:14:09 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 6:46:51-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码