C++ 基于kmp解析nalu

nal解析

第一章 基础实现
第二章 优化(本章)


前言

h264、h265帧通常包含多个nalu,当我们需要封装为mp4的时候,就需要获取这些nalu,读取其中的vps、sps和pps信息,以及视频帧。


一、原理说明

1、kmp融入解析逻辑

上一章的实现是接收到缓存后,单独使用kmp查找,找到001则输出,否则将数据缓存起来同时判断边界startcode。由于边界情况有多种(比如:上个缓存0结尾,下个缓存01开头。上个缓存00结尾,下个缓存01开头。上个缓存000结尾,下个缓存1开头等),实现变得很复杂。

解决方法是,将kmp的查找代码拎出来直接放到解析循环中,持久记录查找状态,可以直接解决边界问题。处理边界问题的关键在于用成员变量记录startcode的kmp查找下标,这样0001跨缓存时也能正确匹配。这样做之后解码代码将变得很简单,容易维护。


二、代码实现

NaluParse.h

cpp 复制代码
#pragma once
#include<vector>
/************************************************************************
* @Project:  	AC::NaluParse
* @Decription:  nalu解析工具
* @添加了GetNaluType方法 2022/3/5 13:03:48
* @添加了GetNalusFromFrame方法、查找startcode优化为kmp算法 2022/3/6 1:05:36
* @简化实现,将kmp融入查找逻辑,不再需要复杂的边界判断。NaluParse只提供一个Decode方法,其他多余的方法去除。2025/12/30 15:28:23
* @由于h264、h265判断方式并不一样,去除GetNaluType,由外部自行判断。
* @Nalu添加startcodeLength
* @Verision:  	v1.1.0
* @Author:  	Xin Nie
* @Create:  	2022/02/20 13:10:17
* @LastUpdate:  2025/12/30 15:28:23
************************************************************************
* Copyright @ 2022. All rights reserved.
************************************************************************/
namespace AC {
	/// <summary>
	/// nalu实体
	/// </summary>
	struct Nalu
	{
		unsigned char* data;
		int dataLength;
		int startcodeLength;
	};
	/// <summary>
	/// nalu解析工具
	/// </summary>
	class NaluParse {
	public:
		/// <summary>
		/// 解码nalu,输入数据后返回所有nalu如果没有001则内部会缓存数据直到遇到001。
		/// 支持流式数据,每次输入1字节数据也能解析。
		/// nalu的data一般会指向源数据。
		/// 如果不是一次性输入完整的nalu则data会指向内部缓存,下次调用本方法可能会清除,所以nalu指向的数据建议即取即用。
		/// data指向内部缓存会有0001四字节填充。配合外部缓存也有超过4字节头部则方便转avcc格式,不用重新申请内存。直接pData=nalu.data-4;操作即可。
		/// </summary>
		/// <param name="buffer">nalu数据</param>
		/// <param name="len">数据长度</param>
		/// <param name="isFlush">不需要001,将剩余的数据作为nalu返回。
		/// 通常输入完整的一帧数据isFlush为true可以直接拿到所有nalu。否则会等到下个001才会返回上一个nalu。
		/// 如果是解析rtp,遇到mark时isFlush设为true。
		/// isFlush永远为false不会有任何解析或逻辑错误,只是在一些情况下会延迟一帧拿到数据。
		/// </param>
		/// <returns>nalu对象集合</returns>
		std::vector<Nalu> Decode(unsigned char* buffer, int len,bool isFlush=false);
	private:
		std::vector<unsigned char>_cache1 = { 0,0,0,1 };
		std::vector<unsigned char>_cache2 = { 0,0,0,1 };
		std::vector<unsigned char>* _pCache = &_cache1;
		int _startcodeLength = 0;
		int j;//kmp子串下标
	};
}

完整代码:

https://download.csdn.net/download/u013113678/92520917


三、使用示例

1.解析h264文件

cpp 复制代码
#include<stdio.h>
#include<stdint.h>
#include "mp4v2/mp4v2.h"
#include"NaluParse.h"
#define READ_SIZE 123  //h264读取缓存大小,设置任意大小都能正确解析
//将h264封装成MP4
int main(int argc, char* argv[])
{
	MP4FileHandle mp4 = NULL;
	FILE* h264 = NULL;
	MP4TrackId videoTrack = MP4_INVALID_TRACK_ID;
	try {
		AC::NaluParse naluParse;
		unsigned char bufferFull[READ_SIZE + 4];
		unsigned char* buf = bufferFull + 4;//预留4字节给avcc的头部
		int size;
		int width = 576;
		int height = 320;
		double frameRate = 29.97;
		int timeScale = 90000;
		mp4 = MP4Create("test.mp4", 0);
		if (mp4 == MP4_INVALID_FILE_HANDLE)
		{
			throw std::exception("Create mp4 handle fialed.\n");
		}
		h264 = fopen("test.h264", "rb+");
		if (!h264)
		{
			throw std::exception("Opene h264 handle fialed.\n");

		}
		//读取h264
		while (1)
		{
			size = fread(buf, 1, READ_SIZE, h264);
			std::vector<AC::Nalu> nalus;
			if (size < 1)
			{
				//需要添加结尾001,将最后剩余的数据flush。
				nalus = naluParse.Decode((unsigned char*)"\0\0\1", 3);
			}
			else
			{
				nalus = naluParse.Decode(buf, size);
			}
			for (auto& nalu : nalus)
			{

				bool isIdr = true;
				switch (nalu.data[0] & 0x1F)
				{
				case 01:
				case 02:
				case 03:
				case 04:
					isIdr = false;
				case 05://idr
				{
					auto pNalu = nalu.data;
					pNalu -= 4;//缓存有预留4字节,且NaluParse内存缓存也会预留4字节,-4不会读写非法内存
					pNalu[0] = (nalu.dataLength >> 24) & 0xFF;
					pNalu[1] = (nalu.dataLength >> 16) & 0xFF;
					pNalu[2] = (nalu.dataLength >> 8) & 0xFF;
					pNalu[3] = (nalu.dataLength >> 0) & 0xFF;
					if (!MP4WriteSample(mp4, videoTrack, pNalu, nalu.dataLength + 4, MP4_INVALID_DURATION, 0, isIdr))
					{
						printf("Error:Can't write sample.\n");
					}
				}
				break;
				case 7: // SPS
				{
					if (videoTrack == MP4_INVALID_TRACK_ID)
					{
						videoTrack = MP4AddH264VideoTrack
						(mp4,
							timeScale,
							timeScale / frameRate,
							width,
							height,
							nalu.data[1],
							nalu.data[2],
							nalu.data[3],
							3);                     // 4 bytes length before each NAL unit
						if (videoTrack == MP4_INVALID_TRACK_ID)
						{
							printf("Error:Can't add track.\n");
							return -1;
						}
						MP4SetVideoProfileLevel(mp4, 0x7F);
						MP4AddH264SequenceParameterSet(mp4, videoTrack, nalu.data, nalu.dataLength);
					}
				}
				break;
				case 8: // PPS
				{
					MP4AddH264PictureParameterSet(mp4, videoTrack, nalu.data, nalu.dataLength);
				}
				break;
				}
			}

			if (size < 1)
			{
				break;

			}
		}
	}
	catch (const std::exception& e)
	{
		printf("%s,\n", e.what());
	}
	if (h264)
		fclose(h264);
	if (mp4)
		MP4Close(mp4);
	return 0;
}

2.逐帧解析

如下所示:

cpp 复制代码
#include"NaluParse.h"
int main(int argc, char* argv[])
{
	AC::NaluParse  naluParse;
	while (1)
	{
		//TODO:在编码队列中取得h264帧,videoFrame
		//略
		//获取h264帧内所有nalu。
		//因为确定是一帧所以isFlush为true,不需要等下一个001才取出数据,否则会延迟一帧拿到数据。
		auto nalus = naluParse.Decode(videoFrame.Data, videoFrame.DataLength,true);	
		//遍历nalu
		for (auto& i : nalus)
		{
			switch (i.data[0] & 0x1F)
			{
			case 01:
			case 02:
			case 03:
			case 04:
			case 05:
			{
				unsigned char* pNalu = i.data;
				pNalu -= 4;
				pNalu[0] = (i.dataLength >> 24) & 0xFF;
				pNalu[1] = (i.dataLength >> 16) & 0xFF;
				pNalu[2] = (i.dataLength >> 8) & 0xFF;
				pNalu[3] = (i.dataLength >> 0) & 0xFF;
				//写入MP4视频帧(pNalu,nalu.GetDataLength()+4);
			}
			break;
			case 7:
			{
				//写入sps(nalu.GetData(),nalu.GetDataLength());			
			}
			break;
			case 8:
			{
				//写入pps(nalu.GetData(),nalu.GetDataLength());
			}
			break;
			}
		}
	}
	return 0;
}

总结

以上就是今天要讲的内容,本章是优化上一章的实现,主要原因是上一章代码在由于实现复杂,在解析失败的情况下难以排查问题,基于笔者之前解析rtsp的经验,发现kmp融入解析逻辑的方式也适应于nal的解析,于是很好的优化了代码。

相关推荐
Sheep Shaun2 小时前
STL中的map和set:红黑树的优雅应用
开发语言·数据结构·c++·后端·c#
1001101_QIA3 小时前
【C++笔试题】递归判断数组是否是递增数组
开发语言·c++
venus603 小时前
歌曲文件转换,mgg文件如何转换程ogg,再转换到mp3
音视频
qq_401700414 小时前
C/C++中的signed char和unsigned char详解
c语言·c++·算法
无限进步_4 小时前
【C语言】循环队列的两种实现:数组与链表的对比分析
c语言·开发语言·数据结构·c++·leetcode·链表·visual studio
qq_310658514 小时前
webrtc源码走读(四)核心引擎层——视频引擎
服务器·c++·音视频·webrtc
码界奇点4 小时前
基于React与TypeScript的后台管理系统设计与实现
前端·c++·react.js·typescript·毕业设计·源代码管理
社会零时工4 小时前
【ROS2】海康相机ROS2设备服务节点开发
linux·c++·相机·ros2
Bruce_kaizy4 小时前
2025年年度总结!!!!!!!!!!!!!!!!!!!!!!!!!!!
开发语言·c++