H264 NALU详解

第一步:先搞懂NALU的全称与核心定位

1.1 NALU完整全称

NALU = N etwork A bstraction L ayer U nit

中文:网络抽象层单元

逐词拆解(通俗版)
英文单词 中文释义 通俗理解
Network 网络 适配网络传输(比如TCP/UDP、RTSP/RTMP),解决"怎么传"的问题
Abstraction 抽象 把复杂的编码数据"封装"成统一格式,屏蔽底层差异(不管是帧数据还是参数,都包成一样的结构)
Layer H264标准里的一个独立功能层(NAL层),专门负责数据的"传输封装"
Unit 单元 最小的独立传输单位(一个NALU就是一个"可独立处理的数据包")

1.2 NALU在H264中的定位(分层理解)

H264编码后的数据流分为两层,NALU是NAL层的核心产物,用"快递物流"比喻更易理解:

复制代码
H264码流 = VCL层(视频编码层) + NAL层(网络抽象层)
  • VCL层(Video Coding Layer,视频编码层)
    负责"压缩视频数据"(比如帧内/帧间预测、熵编码),输出的是原始编码数据(比如SPS/PPS、帧切片)------相当于"仓库打包好的货物"。
  • NAL层(Network Abstraction Layer,网络抽象层)
    负责把VCL层的"货物"封装成NALU(快递包裹),添加"封条(起始码)"和"面单(NALU头)",适配网络传输------相当于"快递站把货物装成标准包裹"。

核心结论:NALU是H264为了网络传输设计的"标准数据包",所有H264码流都是由一个个NALU拼接而成的


第二步:NALU的完整结构(一步一步拆解)

一个完整的NALU(传输形态)由三部分组成,对应快递包裹的"封条+面单+货物":

复制代码
NALU(传输形态) = 起始码(封条) + NALU头(面单,1字节) + NALU载荷(货物)

2.1 第一部分:起始码(Start Code)------ 包裹的"封条"

作用

H264码流是连续的二进制数据,起始码用来标记每个NALU的开始(区分不同包裹)。

两种形式(FFmpeg核心处理逻辑)
起始码类型 二进制值 十六进制值 适用场景
3字节 00000001 0x00 0x00 0x01 绝大多数场景(SPS/PPS/普通帧)
4字节 00000000000001 0x00 0x00 0x00 0x01 防止"伪起始码"(载荷里误出现3字节起始码)
FFmpeg源码:找"封条"(起始码检测)

源码位置:libavcodec/h264_parser.c(函数:ff_h264_find_start_code)
简化版核心代码(保留逻辑,去掉冗余):

c 复制代码
// 输入:buf=H264码流,buf_size=码流长度,pos=当前检测位置
// 输出:返回起始码长度(3/4/0),pos更新为起始码起始位置
static int ff_h264_find_start_code(const uint8_t *buf, int buf_size, int *pos) {
    int i;
    // 从当前位置开始逐字节扫描
    for (i = *pos; i < buf_size - 3; i++) {
        // 检测3字节起始码
        if (buf[i] == 0 && buf[i+1] == 0 && buf[i+2] == 1) {
            *pos = i;
            return 3;
        }
        // 检测4字节起始码
        if (buf[i] == 0 && buf[i+1] == 0 && buf[i+2] == 0 && buf[i+3] == 1) {
            *pos = i;
            return 4;
        }
    }
    *pos = buf_size;
    return 0; // 未找到
}

通俗解释

FFmpeg像"快递分拣员",用"扫描枪"(循环)逐字节扫码流,找到"封条"(起始码)后,记录位置和封条长度,方便后续拆分包裹。

2.2 第二部分:NALU头(NALU Header)------ 包裹的"面单"

起始码后的1字节是NALU头,是NALU分析的核心(面单上写清"包裹属性")。

1字节NALU头的位结构(bit7~bit0)

把1字节想象成8个"小格子",每个格子对应一个属性,FFmpeg会逐个解析:

位序号 字段名 位数 通俗含义 FFmpeg解析逻辑(位运算)
bit7 forbidden_zero_bit 1位 0=包裹完好,1=包裹破损(直接丢弃) (header >> 7) & 0x01(右移7位,取最高位)
bit6~5 nal_ref_idc 2位 重要性(0=可丢,3=最重要,比如IDR帧) (header >> 5) & 0x03(右移5位,取2位)
bit4~0 nal_unit_type 5位 包裹类型(装的是SPS/PPS/关键帧?) header & 0x1F(取低5位)
FFmpeg源码:解析"面单"(NALU头)

源码位置:libavcodec/h264.c(函数:ff_h264_decode_nal_header)
简化版核心代码

c 复制代码
// H264Context:FFmpeg的H264解码上下文;NALU:存储解析后的NALU信息
static int ff_h264_decode_nal_header(H264Context *h, NALU *nal) {
    // 取NALU头:起始码后的第一个字节(跳过封条)
    uint8_t header = nal->data[nal->startcodeprefix_len];
    
    // 1. 解析禁止位(破损检测)
    nal->forbidden_bit = (header >> 7) & 0x01;
    if (nal->forbidden_bit) {
        av_log(h->avctx, AV_LOG_ERROR, "NALU破损(禁止位=1),丢弃!\n");
        return AVERROR_INVALIDDATA;
    }
    
    // 2. 解析重要性
    nal->ref_idc = (header >> 5) & 0x03;
    
    // 3. 解析NALU类型
    nal->type = header & 0x1F;
    
    av_log(h->avctx, AV_LOG_DEBUG, "NALU类型:%d,重要性:%d\n", nal->type, nal->ref_idc);
    return 0;
}

通俗解释

FFmpeg先"撕开封条"(跳过起始码),然后读"面单第一行"(NALU头),用位运算"抠出"每个属性------比如"禁止位=1"就判定包裹破损,直接丢;"类型=7"就知道是SPS(快递规则)。

关键:nal_unit_type(包裹类型)对照表(新手必记)

FFmpeg会根据这个值决定"怎么处理包裹里的货物":

nal_unit_type 类型名称 通俗含义 FFmpeg处理逻辑
7 SPS 序列参数集(快递规则) 解析分辨率/帧率,保存到解码上下文
8 PPS 图像参数集(单包裹规则) 解析单帧编码参数,保存到解码上下文
5 IDR Slice IDR帧(关键包裹) 标记关键帧,重置解码状态
1 Non-IDR Slice 普通帧(普通包裹) 解析P/B帧数据,进行解码

2.3 第三部分:NALU载荷(NALU Payload)------ 包裹的"货物"

NALU头后的所有字节,内容由nal_unit_type决定(面单写"规则",货物就是规则;写"关键帧",货物就是关键帧数据)。

FFmpeg源码:处理"货物"(按类型解析载荷)

源码位置:libavcodec/h264.c(函数:ff_h264_decode_nal)
简化版核心代码

c 复制代码
static int ff_h264_decode_nal(H264Context *h, NALU *nal) {
    // 跳过"封条+面单",取货物(载荷)
    uint8_t *payload = nal->data + nal->startcodeprefix_len + 1;
    int payload_len = nal->size - nal->startcodeprefix_len - 1;
    
    switch (nal->type) {
        case 7: // SPS(快递规则)
            av_log(h->avctx, AV_LOG_INFO, "解析SPS(全局规则)\n");
            return ff_h264_decode_sps(h, payload, payload_len); // 解析分辨率/帧率等
        case 8: // PPS(单包裹规则)
            av_log(h->avctx, AV_LOG_INFO, "解析PPS(单帧规则)\n");
            return ff_h264_decode_pps(h, payload, payload_len); // 解析单帧参数
        case 5: // IDR帧(关键包裹)
            h->is_idr = 1; // 标记为关键帧
            av_log(h->avctx, AV_LOG_INFO, "解析IDR关键帧\n");
            return ff_h264_decode_slice(h, nal); // 解码关键帧
        case 1: // 普通帧(普通包裹)
            av_log(h->avctx, AV_LOG_DEBUG, "解析普通帧\n");
            return ff_h264_decode_slice(h, nal); // 解码普通帧
        default:
            av_log(h->avctx, AV_LOG_WARNING, "未知包裹类型:%d,跳过\n", nal->type);
            return 0;
    }
}

通俗解释

FFmpeg根据"面单上的类型"处理货物------SPS/PPS是"配送规则",先存起来;IDR帧是"核心货物",优先解码;普通帧是"常规货物",正常解码。

2.4 补充:FFmpeg处理"伪起始码"(防误判)

有时候载荷里会出现0x00 0x00,可能被误判为起始码,FFmpeg会做"防竞争处理":

  • 编码时:在0x00 0x00后加0x03(变成0x00 0x00 0x03);
  • 解码时:FFmpeg调用ff_h264_remove_emulation_prevention函数,删除多余的0x03,还原数据。

简化版源码

c 复制代码
static void ff_h264_remove_emulation_prevention(uint8_t *data, int *size) {
    int i, j = 0;
    for (i = 0; i < *size; i++) {
        // 找到0x00 0x00 0x03,跳过0x03
        if (i > 1 && data[i] == 0x03 && data[i-1] == 0x00 && data[i-2] == 0x00) {
            continue;
        }
        data[j++] = data[i];
    }
    *size = j; // 更新载荷长度
}

第三步:FFmpeg中NALU的核心数据结构

FFmpeg先定义"包裹登记表",记录每个NALU的所有信息,源码位置:libavcodec/nal.h
简化版结构体

c 复制代码
typedef struct NALU {
    int forbidden_bit;          // 禁止位(包裹是否破损)
    int ref_idc;                // 重要性(0~3)
    int type;                   // NALU类型(7=SPS,5=IDR等)
    uint8_t *data;              // NALU完整数据(封条+面单+货物)
    int size;                   // NALU总长度
    int startcodeprefix_len;    // 起始码长度(3/4)
} NALU;

通俗解释

这个结构体就是FFmpeg的"快递包裹登记表",把每个NALU的"封条长度、面单信息、包裹大小、货物位置"都记下来,方便后续处理。


第四步:实战验证(用FFmpeg命令行看NALU信息)

不用看源码,直接用FFmpeg命令行就能直观看到NALU解析结果:

bash 复制代码
# 1. 提取H264裸流(从MP4中拆出NALU流)
ffmpeg -i test.mp4 -vcodec copy -an -bsf:v h264_mp4toannexb -f h264 test.h264

# 2. 查看NALU详细信息
ffprobe -v debug -show_frames -show_packets -i test.h264

关键输出示例

复制代码
[PACKET]
nal_ref_idc=3        # NALU重要性(最高)
nal_type=7           # NALU类型(SPS)
width=1920           # 从SPS解析的宽度
height=1080          # 从SPS解析的高度
[/PACKET]

[PACKET]
nal_ref_idc=3
nal_type=8           # NALU类型(PPS)
[/PACKET]

[PACKET]
nal_ref_idc=3
nal_type=5           # NALU类型(IDR关键帧)
[/PACKET]

总结(关键点回顾)

  1. NALU全称与核心:Network Abstraction Layer Unit(网络抽象层单元),是H264适配网络传输的最小数据包,结构为"起始码+NALU头+载荷";
  2. FFmpeg解析逻辑 :先调用ff_h264_find_start_code找起始码(封条),再用ff_h264_decode_nal_header解析NALU头(面单),最后按类型处理载荷(货物);
  3. 核心字段:nal_unit_type(类型)决定处理逻辑(SPS存参数、IDR是关键帧),forbidden_bit为1则丢弃NALU,nal_ref_idc标记重要性。
相关推荐
小林up2 小时前
Ubuntu访问不了Git解决办法
linux·git·ubuntu
小林up2 小时前
Ubuntu使用阿里云安装docker
ubuntu·阿里云·docker
林深现海2 小时前
宇树 Go2 + NaVILA 全栈导航系统详解 (新手入门版)
linux·vscode·yolo·ubuntu·机器人
林深现海3 小时前
基于宇树 Go2 与 NaVILA 的全栈视觉导航系统深度解析
linux·vscode·yolo·ubuntu·机器人
码农小卡拉13 小时前
Ubuntu22.04 安装 Docker 及 Docker Compose v2 详细教程
ubuntu·docker·容器
EndingCoder19 小时前
属性和参数装饰器
java·linux·前端·ubuntu·typescript
生活很暖很治愈1 天前
Linux基础指令——【2】
linux·服务器·后端·ubuntu
胖少年1 天前
Ubuntu 24.04 LTS apt autoremove 误删依赖致程序崩溃 解决与预防笔记
linux·笔记·ubuntu
Source.Liu1 天前
【办公平台】在 Ubuntu 上部署 Synapse Matrix 服务器(本地网络版)
服务器·ubuntu