前言
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解析工具
* @添加了GetNaluType方法 2022/3/5 13:03:48
* @添加了GetNalusFromFrame方法、查找startcode优化为kmp算法 2022/3/6 1:05:36
* @Verision: v1.0.2.0
* @Author: Xin Nie
* @Create: 2022/2/20 13:10:17
* @LastUpdate: 2022/3/6 1:05:36
************************************************************************
* 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();
/// <summary>
/// 获取nalutype,值为:GetData()&0x1f
/// </summary>
/// <returns>nalutype</returns>
int GetNaluType();
};
/// <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);
/// <summary>
/// 通过帧的方式解析h264
/// </summary>
/// <param name="h264Frame">h264帧,需要确保是完整的帧</param>
/// <param name="h264FrameLen">h264帧长度</param>
/// <returns>帧内的所有nalu</returns>
static std::vector<Nalu> GetNalusFromFrame(unsigned char* h264Frame, int h264FrameLen);
};
}
完整代码:
https://download.csdn.net/download/u013113678/85319771
三、使用示例
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>0)
{
//写入h264流
parse.SendH264Stream(buffer, size);
}
else
{
//TODO:需要添加结尾0001,将最后剩余的数据flush。
parse.SendH264Stream((unsigned char*)"\0\0\0\1", 4);
}
//读取nalu
while (parse.ReceiveNalu(nalu))
{
switch (i.GetData()[0] & 0x1F)
{
case 01:
case 02:
case 03:
case 04:
case 05:
{ unsigned char* pNalu = i.GetData();
pNalu -= 4;
pNalu[0] = (i.GetDataLength() >> 24) & 0xFF;
pNalu[1] = (i.GetDataLength() >> 16) & 0xFF;
pNalu[2] = (i.GetDataLength() >> 8) & 0xFF;
pNalu[3] = (i.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;
}
}
if (size<1)
{
break;
}
}
fclose(f);
return 0;
}
2.逐帧解析
获取到编码器的h264数据通常是以帧为单位的,如果以流的形式去解析,性能稍微差一点,所以提供了专门解析帧的方法。如下所示:
#include"NaluParse.h"
int main(int argc, char* argv[])
{
while (1)
{
//TODO:在编码队列中取得h264帧,videoFrame
略
//获取h264帧内所有nalu。
auto nalus = AC::NaluParse::GetNalusFromFrame(videoFrame.Data, videoFrame.DataLength);
//遍历nalu
for (auto& i : nalus)
{
switch (i.GetNaluType())
{
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;
//写入MP4视频帧(pNalu,nalu.GetDataLength()+4);
}
break;
case 7:
{
//写入sps(nalu.GetData(),nalu.GetDataLength());
}
break;
case 8:
{
//写入pps(nalu.GetData(),nalu.GetDataLength());
}
break;
}
}
}
return 0;
}
总结
以上就是今天要讲的内容,对h264-Annex-B的解析原理上是比较简单的而且易于理解,但是在实现上却不太容易,由于是基于数据流解析的,很多情况都需要考虑。比如startcode刚好被上下两部分数据分割,就需要特殊判断。又比如流数据每次只能获取1byte时需要建立额外的缓冲存储流数据,达到基本长度后才能解析。还有流数据的长度随机不固定的时候也需要一定的处理等等。总的来说,实现起来并不容易,但是实现之后使用效果还是挺好的。