H264/265码流数据包格式分析
前言:
最近在学习使用MP4v2将H264/H265码流以及AAC音频封装成MP4格式,然后在网上疯狂查资料调试,发现学习过程中,很多知识点比较分散,我在完成这个目标后就打算整理一份完整一点的对H264/265码流数据包格式分析的学习总结,方便自己查看,也希望给有需要的人提供点帮助。
下文总结多来源于各个博主文章修改总结,比较多,就不一一贴出链接来源了,望原创作者理解,下面开始学习。
一、H.264码流解析
一个原始的H.264 NALU 单元常由 [Start Code] [NALU Header] [NALU Payload] 三部分组成,下图是一串H.264码流结构简图:
这里首先要了解下IDR帧与非IDR帧:
-
IDR(Instantaneous Decoding Refresh)–即时解码刷新。
-
I帧:帧内编码帧是一种自带全部信息的独立帧,无需参考其它图像便可独立进行解码,视频序列中的第一个帧始终都是I帧。
I和IDR帧都是使用帧内预测的。它们都是同一个东西而已,在编码和解码中为了方便,要首个I帧和其他I帧区别开,所以才把第一个首个I帧叫IDR,这样就方便控制编码和解码流程。
、、、、、、、、、、、、、、、、、、、、 第一次新增(推荐看完整个文档,再浏览新增内容)、、、、、、、、、、、、
I帧P帧B帧说明:
他们在H264码流中的结构图分别如下:
IDR帧:
非IDR帧:
本文档主要分析码流中第一个I帧之前的数据流,即IDR之前的数据流,详见下文:
NALU 单元组成解析:
[Start Code]:Start Code 用于标示这是一个NALU 单元的开始(也称分隔符),必须是”00 00 00 01” 或”00 00 01”。
[NALU Header]:H264中此部分占一个字节,在一个完整的NALU单元中,它都为第一个字节,用以表示其包含数据的类型及其他信息。头字节又可以被解析成3个部分,详见下面的列子。
[NALU Payload]:顾名思义,这个部分代表着相应[NALU Header]中NALU类型的有效数据集。详见下面的列子。
举个列子分析,通过编辑器打开一个H264文件,如下图:
可以看到第一个黄框加蓝框 的组合体就是一个NALU单元,黄框就是[NALU Header],随后的蓝框就是[NALU Payload],所以这一个IDR帧的数据结构大致为下图:
我们再更仔细的分析下,我们以第一个头信息字节为0x67作为例子分析:
如图所示,头字节[NALU Header]可以被解析成3个部分,其中(B:二进制表示 D:十进制表示):
红框forbidden_zero_bit = B:0[D:0]:占1个bit,禁止位,用以检查传输过程中是否发生错误,0表示正常,1表示违反语法;
黄框nal_ref_idc = B:11 [D:3]:占2个bit,用来表示当前NAL单元的优先级。非0值表示参考字段/帧/图片数据,其他不那么重要的数据则为0。对于非0值,值越大表示NALU重要性越高
蓝框nal_unit_type = B:00111[D:7]:最后5位用以指定NALU类型,NALU类型定义见下文:
头字节[NALU Header]结构解析:
** NALU组成结构表**
从表中我们可以获知,NALU类型1-5为视频帧,其余则为非视频帧。在解码过程中,我们只需要取出NALU头字节的后5位,即将NALU头字节和0x1F进行与计算即可得知NALU类型,即表达式为:
NALU类型 = NALU头字节 & 0x1F
注意: 可以将[Start Code]理解为不同NALU的分隔符,[NALU Header]是某种类型的key,[NALU Payload]是该key的value。
二、H.265码流解析
H.265标准围绕H.264编码标准,保留原有的某些技术,同时对一些技术进行改进,编码结构大致上和H.264的架构类似。
同H.264一样,H.265也是以NALU的形式组织起来。而在[NALU Header]上,H.264是一个字节,而H.265则是两个字节。
H.265的NALU 单元组成结构和H.264类似,也是由[Start Code] [NALU Header] [NALU Payload] 三部分组成,下面主要解析它们的主要区别,也就是在[NALU Header]上的区别。
H.265的[NALU Header]的组成结构中,舍弃了H.264的nal_ref_idc,此信息合并到了Type中,其组成结构定义如下:
H.265的 NALU 单元NALType 的成员:
这些其实和H264都大同小异,了解下即可。那么在解码过程中,我们只需要取出NALU头字节的后6位,即将NALU头字节和0x7E进行与计算并左移1位即可得知NALU类型,即表达式为:
NALU类型 = (NALU头字节 & 0x7E)>> 1
同样举个列子分析,通过编辑器打开一个H265文件,如下图:
可以看到每个NALU单元的结构和H.264的都是一样的,由三部分组成,不过在一帧数据上的结构就有一点差异了,H.265的帧数据结构大致为:
三、主要源码
支持h265的库的下载地址: 链接: https://github.com/Pandalzm/mp4v2-h265.
生成makefile指令:(需安装arm交叉编译工具链)
./configure --prefix=/home/wzj/work/nihui/output/ --host=arm-linux --disable-debug --enable-shared CC=arm-linux-gcc CXX=arm-linux-g++
输出库与头文件至output/
make clean;make;make install
下面是需要自己写的代码,其余都是调用库以及包含头文件即可,具体通过最后的makefile应该都能看出需要什么库和头文件,就不一一细说了。
main.c
#include "mp4v2_code.h"
#define H264_PATH "/mnt/nfs/1.h264"
#define H265_PATH "/mnt/nfs/1.h265"
int main(int argc, char *argv[])
{
switch(atoi(argv[1]))
{
case 1:
{
if (packet2Mp4(H264_PATH, "h264.mp4"))
{
printf("Error:Packet to Mp4 fail.\n");
return -1;
}
}
break;
case 2:
{
if (packet2Mp4(H264_PATH, "h264.mp4"))
{
printf("Error:Packet to Mp4 fail.\n");
return -1;
}
}
break;
default:
printf("Error:format error\n");
return -1;
}
return 0;
}
mp4v2_code.c
#include "mp4v2_code.h"
static int h264_getNalu(FILE *pFile, unsigned char *pNalu)
{
unsigned char c;
int pos = 0;
int len;
if(!pFile)
return -1;
if((len = fread(pNalu, 1, 4, pFile)) <= 0)
return -1;
if(pNalu[0] != 0 || pNalu[1] != 0 || pNalu[2] != 0 || pNalu[3] != 1)
return -1;
pos = 4;
while(1)
{
if(feof(pFile))
break;
pNalu[pos] = fgetc(pFile);
if(pNalu[pos-3] == 0 && pNalu[pos-2] == 0 && pNalu[pos-1] == 0 && pNalu[pos] == 1)
{
fseek(pFile, -4, SEEK_CUR);
pos -= 4;
break;
}
pos++;
}
len = pos+1;
return len;
}
int h264_packet2Mp4(const char *inputFile, const char *outputFiles)
{
FILE *pIn = NULL;
unsigned char *pBuf = malloc(1024*1024);
unsigned char *pNalu = NULL;
unsigned char naluType;
int len;
int num = 0;
MP4FileHandle pHandle = NULL;
MP4TrackId videoId;
int width = 640;
int height = 480;
int frameRate = 15;
int timeScale = 90000;
int addStream = 1;
pIn = fopen(inputFile, "rb");
if(!pIn)
return -1;
pHandle = MP4Create(outputFiles, 0);
if(pHandle == MP4_INVALID_FILE_HANDLE)
{
printf("ERROR:Create mp4 handle fialed.\n");
return -1;
}
MP4SetTimeScale(pHandle, timeScale);
while(1)
{
len = getNalu(pIn, pBuf);
if (len <= 0)
break;
if (pBuf[0] != 0 || pBuf[1] != 0 || pBuf[2] != 0 || pBuf[3] != 1)
continue;
len -= 4;
pNalu = pBuf+4;
naluType = pNalu[0]&0x1F;
switch (naluType)
{
case 0x07: // SPS
printf("sps(%d)\n", len);
if (addStream)
{
videoId = MP4AddH264VideoTrack
(pHandle,
timeScale, // 一秒钟多少timescale
timeScale/frameRate, // 每个帧有多少个timescale
width, // width
height, // height
pNalu[1], // sps[1] AVCProfileIndication
pNalu[2], // sps[2] profile_compat
pNalu[3], // sps[3] AVCLevelIndication
3); // 4 bytes length before each NAL unit
if (videoId == MP4_INVALID_TRACK_ID)
{
printf("Error:Can't add track.\n");
return -1;
}
MP4SetVideoProfileLevel(pHandle, 0x7F);
addStream = 0;
}
MP4AddH264SequenceParameterSet(pHandle, videoId, pNalu, len);
break;
case 0x08: // PPS
printf("pps(%d)\n", len);
MP4AddH264PictureParameterSet(pHandle, videoId, pNalu, len);
break;
default:
printf("slice(%d)\n", len);
pBuf[0] = (len>>24)&0xFF;
pBuf[1] = (len>>16)&0xFF;
pBuf[2] = (len>>8)&0xFF;
pBuf[3] = (len>>0)&0xFF;
MP4WriteSample(pHandle, videoId, pBuf, len+4, MP4_INVALID_DURATION, 0, 1);
break;
}
}
free(pBuf);
fclose(pIn);
MP4Close(pHandle, 0);
return 0;
}
static int h265_getNalu(FILE *pFile, unsigned char *pNalu)
{
unsigned char c;
int pos = 0;
int len;
if(!pFile)
return -1;
if((len = fread(pNalu, 1, 4, pFile)) <= 0)
return -1;
if(pNalu[0] != 0 || pNalu[1] != 0 || pNalu[2] != 0 || pNalu[3] != 1)
return -1;
pos = 4;
while(1)
{
if(feof(pFile))
break;
pNalu[pos] = fgetc(pFile);
if(pNalu[pos-3] == 0 && pNalu[pos-2] == 0 && pNalu[pos-1] == 0 && pNalu[pos] == 1)
{
fseek(pFile, -4, SEEK_CUR);
pos -= 4;
break;
}
pos++;
}
len = pos+1;
return len;
}
int h265_packet2Mp4(const char *inputFile, const char *outputFiles)
{
FILE *pIn = NULL;
unsigned char *pBuf = malloc(1024*1024);
unsigned char *pNalu = NULL;
unsigned char naluType;
int len;
int num = 0;
MP4FileHandle pHandle = NULL;
MP4TrackId videoId;
int width = 640;
int height = 480;
int frameRate = 15;
int timeScale = 90000;
int addStream = 1;
pIn = fopen(inputFile, "rb");
if(!pIn)
return -1;
pHandle = MP4Create(outputFiles, 0);
if(pHandle == MP4_INVALID_FILE_HANDLE)
{
printf("ERROR:Create mp4 handle fialed.\n");
return -1;
}
MP4SetTimeScale(pHandle, timeScale);
while(1)
{
len = h265_getNalu(pIn, pBuf);
if (len <= 0)
break;
if (pBuf[0] != 0 || pBuf[1] != 0 || pBuf[2] != 0 || pBuf[3] != 1)
continue;
len -= 4;
pNalu = pBuf+4;
naluType = (pNalu[0]&0x7E)>>1;
switch (naluType)
{
case 32: //VPS
printf("vps(%d)\n", len);
if (addStream)
{
videoId = MP4AddH265VideoTrack
(pHandle,
timeScale, // 一秒钟多少timescale
timeScale/frameRate, // 每个帧有多少个timescale
width, // width
height, // height
pNalu[2], // sps[1] AVCProfileIndication
pNalu[3], // sps[2] profile_compat
pNalu[4], // sps[3] AVCLevelIndication
3); // 4 bytes length before each NAL unit
if (videoId == MP4_INVALID_TRACK_ID)
{
printf("Error:Can't add track.\n");
return -1;
}
MP4SetVideoProfileLevel(pHandle, 0x7F);
MP4AddH265VideoParameterSet(pHandle, videoId, pNalu, len);
addStream = 0;
}
case 33: // SPS
MP4AddH265SequenceParameterSet(pHandle, videoId, pNalu, len);
break;
case 34: // PPS
printf("pps(%d)\n", len);
MP4AddH265PictureParameterSet(pHandle, videoId, pNalu, len);
break;
default:
printf("slice(%d)\n", len);
pBuf[0] = (len>>24)&0xFF;
pBuf[1] = (len>>16)&0xFF;
pBuf[2] = (len>>8)&0xFF;
pBuf[3] = (len>>0)&0xFF;
MP4WriteSample(pHandle, videoId, pBuf, len+4, MP4_INVALID_DURATION, 0, 1);
break;
}
}
free(pBuf);
fclose(pIn);
MP4Close(pHandle, 0);
return 0;
}
mp4v2_code.h
#ifndef __MP4V2_CODE_H_
#define __MP4V2_CODE_H_
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <mp4v2/mp4v2.h>
int h264_packet2Mp4(const char *inputFile, const char *outputFiles);
int h265_packet2Mp4(const char *inputFile, const char *outputFiles);
#endif
Makefile
CROSS = arm-linux-
CC = $(CROSS)gcc
CXX = $(CROSS)g++
SPWD = /home/wzj/work/rtt
DEBUG =
CFLAGS = $(DEBUG) -c
HEADER_PATH := -I $(SPWD)/include/mp4v2/ -I $(SPWD)/include/
LIB_PATH := $(SPWD)/lib/libmp4v2.a
TARGET = main
HIDE = @
RM = rm -rf
CP = cp $(TARGET) ~/nfsshare/
SRCS = $(wildcard $(SPWD)/src/*.c)
OBJS = $(patsubst %.c, %.o, $(SRCS))
$(TARGET):$(OBJS)
$(CXX) -o $@ $^ $(LIB_PATH)
$(OBJS):%.o : %.c
$(HIDE) $(CC) $(CFLAGS) $< -o $@ $(HEADER_PATH)
cp:
$(CP)
clean:
$(RM) $(TARGET) *.o $(SPWD)/src/*.o
整理不易,感觉有用的话,希望三连支持一下,wink