H264的NAL介绍

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了H264的NAL介绍相关的知识,希望对你有一定的参考价值。

参考技术A

原文: https://blog.csdn.net/hbuxiaofei/article/details/50502330

****1、H264 NAL头。****

NAL全称Network Abstract Layer,即网络抽象层。在H.264/AVC视频编码标准中,整个系统框架被分为了两个层面:视频编码层面(VCL)和网络抽象层面(NAL)。其中,前者负责有效表示视频数据的内容,而后者则负责格式化数据并提供头信息,以保证数据适合各种信道和存储介质上的传输。NAL单元是NAL的基本语法结构,它包含一个字节的头信息和一系列来自VCL的称为原始字节序列载荷(RBSP)的字节流。

如果NALU对应的Slice为一帧的开始,则用4字节表示,即0x00000001;否则用3字节表示,0x000001。

NAL Header : forbidden_bit , nal_reference_bit (优先级) 2bit , nal_unit_type (类型) 5bit 。 标识NAL单元中的RBSP数据类型,其中,nal_unit_type为1, 2, 3, 4, 5的NAL单元称为VCL的NAL单元,其他类型的NAL单元为非VCL的NAL单元。

NAL的头占用了一个字节,按照比特自高至低排列可以表示如下:

0AABBBBB

其中,AA用于表示该NAL是否可以丢弃(有无被其后的NAL参考),00b表示没有参考作用,可丢弃,如B slice、SEI等,非零——包括01b、10b、11b——表示该NAL不可丢弃,如SPS、PPS、I Slice、P Slice等。常用的 NAL 头 的取值如:

由于NAL的语法中没有给出长度信息,实际的传输、存储系统需要增加额外的头实现各个NAL单元的定界。

其中,AVI文件和MPEG TS广播流采取的是字节流的语法格式,即在NAL单元之前增加0x00000001的同步码,则从AVI文件或MPEG TS PES包中读出的一个H.264视频帧以下面的形式存在:

而对于MP4文件,NAL单元之前没有同步码,却有若干字节的长度码,来表示NAL单元的长度,这个长度码所占用的字节数由MP4文件头给出;此外,从MP4读出来的视频帧不包含PPS和SPS,这些信息位于MP4的文件头中,解析器必须在打开文件的时候就获取它们。从MP4文件读出的一个H.264帧往往是下面的形式(假设长度码为2字节):

2、帧分割。 在实际的H264数据帧中,往往帧前面带有00 00 00 01 或 00 00 01分隔符,一般来说编码器编出的首帧数据为PPS与SPS,接着为I帧……

如下图:

[图片上传失败...(image-3914e4-1623751823950)]

3、如何判断帧类型(是图像参考帧还是I、P帧等)?

NALU类型是我们判断帧类型的利器,从官方文档中得出如下图:

[图片上传失败...(image-6c3738-1623751823950)]

我们还是接着看最上面图的码流对应的数据来层层分析,以00 00 00 01分割之后的下一个字节就是NALU类型,将其转为二进制数据后,解读顺序为从左往右算,如下: (1)第1位禁止位,值为1表示语法出错
(2)第2~3位为参考级别
(3)第4~8为是nal单元类型

例如上面00000001后有67,68以及65

其中0x67的二进制码为: 0110 0111
4-8为00111,转为十进制7,参考第二幅图:7对应序列参数集SPS

其中0x68的二进制码为: 0110 1000
4-8为01000,转为十进制8,参考第二幅图:8对应图像参数集PPS

其中0x65的二进制码为: 0110 0101
4-8为00101,转为十进制5,参考第二幅图:5对应IDR图像中的片(I帧)

所以判断是否为I帧的算法为: (NALU类型 & 0001 1111) = 5 即 NALU类型 & 31 = 5

比如0x65 & 31 = 5

参见: http://blog.csdn.net/jefry_xdz/article/details/8461343

视频编解码·学习笔记4. H.264的码流封装格式 & 提取NAL有效数据

一、码流封装格式简单介绍:

H.264的语法元素进行编码后,生成的输出数据都封装为NAL Unit进行传递,多个NAL Unit的数据组合在一起形成总的输出码流。对于不同的应用场景,NAL规定了一种通用的格式适应不同的传输封装类型。
通常NAL Unit的传输格式分两大类:字节流格式和RTP包格式

字节流格式:

  • 大部分编码器的默认输出格式
  • 每个NAL Unit以规定格式的起始码分割
  • 起始码:0x 00 00 00 01 或 0x 00 00 01

RTP数据包格式:

  • NAL Unit按照RTP数据包的格式封装
  • 使用RTP包格式不需要额外的分割识别码,在RTP包的封装信息中有相应的数据长度信息。
  • 可以在NAL Unit的起始位置用一个固定长度的长度码表示整个NAL Unit的长度

实际应用中字节流格式更为常用,下面的均以字节流格式来介绍。

通过查阅H.264官方说明文档,了解NAL字节流格式(在附录B)
技术分享图片

有用数据前面会加 0x 00 00 00 01 或 0x 00 00 01,作为起始码,两个起始码中间包含的即为有用数据流
如: 00 00 00 01 43 23 56 78 32 1A 59 2D 78 00 00 00 01 C3 E2 …… 中,红色的部分即为有效数据。

本次使用上一篇笔记中生成的test.264作为例子。
使用Ultra Edit打开此文件,可以看到该文件的数据流:
技术分享图片
接下来将写一个小程序,从二进制码流文件中截取实际的NAL数据。

二、C++程序 从码流中提取NAL有效数据:

新建一个VS工程,配置工程属性。将【常规-输出目录】和【调试-工作目录】改为$(SolutionDir)bin\$(Configuration)\,【调试-命令参数】改为test.264编译、运行程序。
技术分享图片
在 bin\debug 目录下可看到生成的exe执行文件

接下来编写程序的功能:
提取起始码之间的有效数据

程序思路:
从码流中寻找 00 00 00 01 或 00 00 01序列,后面就是有效数据流,将之后的数据保存起来,直到遇到下一个(00) 00 00 01 停止。

下面开始编写程序:

① 打开码流文件

使用下面的代码测试,比较简单,不再解释,最后记得要把文件流关掉。

int _tmain(int argc, _TCHAR* argv[])
{
    FILE *pFile_in = NULL;
    // 打开刚才导入的二进制码流文件
    _tfopen_s(&pFile_in, argv[1], _T("rb"));
    
    // 判断文件是否打开成功
    if (!pFile_in)
    {
        printf("Error: Open File failed. \n");
    }
    fclose(pFile_in);
    return 0;
}

② 寻找起始码

  • 使用数据类型unsigned char数据类型来存储单个字节码
  • 为了减少内存使用,使用数组 refix3,存储连续的三个字节码
  • 数组循环使用,新进来的数据放在弹出那位数据的位置上
  • 即:数组的存数顺序为 [0][1][2],下一个字符放在[0]的位置上,此时数据顺序为[1][2][0],再下一次[2][0][1]以此类推
  • 由于起始码有两种格式00 00 01 和 00 00 00 01,因此需要有两个判断分别对应

代码如下:

typedef unsigned char uint8;

static int find_nal_prefix(FILE **pFileIn)
{
    FILE *pFile = *pFileIn;
    // 00 00 00 01 x x x x x 00 00 00 01
    // 以下方法为了减少内存,及向回移动文件指针的操作
    uint8 prefix[3] = { 0 };

    /*
    依次比较 [0][1][2] = {0 0 0};  若不是,将下一个字符放到[0]的位置 -> [1][2][0] = {0 0 0} ; 下次放到[1]的位置,以此类推
    找到三个连0之后,还需判断下一个字符是否为1, getc() = 1  -> 00 00 00 01
    以及判断 [0][1][2] = {0 0 1} -> [1][2][0] = {0 0 1} 等,若出现这种序列则表示找到文件头
    */

    // 标记当前文件指针位置
    int pos = 0;
    // 标记查找的状态
    int getPrefix = 0;
    // 读取三个字节
    for (int idx = 0; idx < 3; idx++)
    {
        prefix[idx] = getc(pFile);
    }

    while (!feof(pFile))
    {
        if ((prefix[pos % 3] == 0) && (prefix[(pos + 1) % 3] == 0) && (prefix[(pos + 2) % 3] == 1))
        {
            // 0x 00 00 01 found
            getPrefix = 1;
            break;
        }
        else if((prefix[pos % 3] == 0) && (prefix[(pos + 1) % 3] == 0) && (prefix[(pos + 2) % 3] == 0))
        {
            if (1 == getc(pFile))
            {
                // 0x 00 00 00 01 found
                getPrefix = 2;
                break;
            }
        }
        else
        {
            fileByte = getc(pFile);
            prefix[(pos++) % 3] = fileByte;
        }
    }

    return getPrefix;
}

③ 提取有效数据

  • 使用容器vector 存储有效数据
  • 函数find_nal_prefix() 添加参数 vector
  • 每次读取的数据都直接push到nalBytes中,若遇到起始码再把起始码pop掉
  • 本函数需要重复执行,第一次文件指针移动到有效数据起始位置;第二次提取两段起始码间的有效数据;第三次在移动到下一个起始码后;第四次提取有效数据... 以此类推。

函数调整为:

static int find_nal_prefix(FILE **pFileIn, vector<uint8> &nalBytes)
{
    FILE *pFile = *pFileIn;
    // 00 00 00 01 x x x x x 00 00 00 01
    // 以下方法为了减少内存,及向回移动文件指针的操作
    uint8 prefix[3] = { 0 };
    // 表示读进来字节的数值
    uint8 fileByte;
    /*
    依次比较 [0][1][2] = {0 0 0};  若不是,将下一个字符放到[0]的位置 -> [1][2][0] = {0 0 0} ; 下次放到[1]的位置,以此类推
    找到三个连0之后,还需判断下一个字符是否为1, getc() = 1  -> 00 00 00 01
    以及判断 [0][1][2] = {0 0 1} -> [1][2][0] = {0 0 1} 等,若出现这种序列则表示找到文件头
    */

    nalBytes.clear();

    // 标记当前文件指针位置
    int pos = 0;
    // 标记查找的状态
    int getPrefix = 0;
    // 读取三个字节
    for (int idx = 0; idx < 3; idx++)
    {
        prefix[idx] = getc(pFile);
        // 每次读进来的字节 都放入vector中
        nalBytes.push_back(prefix[idx]);
    }

    while (!feof(pFile))
    {
        if ((prefix[pos % 3] == 0) && (prefix[(pos + 1) % 3] == 0) && (prefix[(pos + 2) % 3] == 1))
        {
            // 0x 00 00 01 found
            getPrefix = 1;
            // 这三个字符没用,pop掉
            nalBytes.pop_back();
            nalBytes.pop_back();
            nalBytes.pop_back();
            break;
        }
        else if((prefix[pos % 3] == 0) && (prefix[(pos + 1) % 3] == 0) && (prefix[(pos + 2) % 3] == 0))
        {
            if (1 == getc(pFile))
            {
                // 0x 00 00 00 01 found
                getPrefix = 2;
                // 这三个字符没用,pop掉 (最后那个1没填到vector中,不用pop)
                nalBytes.pop_back();
                nalBytes.pop_back();
                nalBytes.pop_back();
                break;
            }
        }
        else
        {
            fileByte = getc(pFile);
            prefix[(pos++) % 3] = fileByte;
            nalBytes.push_back(fileByte);
        }
    }

    return getPrefix;
}

主函数调整为:

#include "stdafx.h"
#include <stdio.h>
#include <vector>
typedef unsigned char uint8;
using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    FILE *pFile_in = NULL;
    // 打开刚才导入的二进制码流文件
    _tfopen_s(&pFile_in, argv[1], _T("rb"));

    // 判断文件是否打开成功
    if (!pFile_in)
    {
        printf("Error: Open File failed. \n");
    }

    vector<uint8> nalBytes;
    find_nal_prefix(&pFile_in, nalBytes);
    find_nal_prefix(&pFile_in, nalBytes);
    for (int idx = 0; idx < nalBytes.size(); idx++) 
    {
        printf("%x ", nalBytes.at(idx));
    }
    printf("\n");


    find_nal_prefix(&pFile_in, nalBytes);
    for (int idx = 0; idx < nalBytes.size(); idx++)
    {
        printf("%x ", nalBytes.at(idx));
    }
    printf("\n");

    fclose(pFile_in);

    return 0;
}

以第一节最后数据流为例,执行以上代码后,程序输出结果如下:
技术分享图片












以上是关于H264的NAL介绍的主要内容,如果未能解决你的问题,请参考以下文章

流媒体专家H264协议详解II H264的分层结构与NALU介绍

H264 NAL解析

h264原始的nal打包格式怎么获取pps,sps等消息

H264解析——切片头检测

H264码流解析

我如何解析 H264 文件和帧