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的解析,于是很好的优化了代码。