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标记重要性。
相关推荐
lucky-billy1 天前
Ubuntu 下一键部署 ROS2
linux·ubuntu·ros2
阿梦Anmory1 天前
Ubuntu配置代理最详细教程
linux·运维·ubuntu
getapi1 天前
Ubuntu 22.04 服务器的系统架构是否为 amd64 x86_64
linux·服务器·ubuntu
小天源1 天前
Cacti在Debian/Ubuntu中安装及其使用
运维·ubuntu·debian·cacti
独自归家的兔1 天前
ubuntu系统安装dbswitch教程 - 备份本地数据到远程服务器
linux·运维·ubuntu
ONE_SIX_MIX1 天前
ubuntu 24.04 用rdp连接,桌面黑屏问题,解决
linux·运维·ubuntu
老师用之于民2 天前
【DAY21】Linux软件编程基础&Shell 命令、脚本及系统管理实操
linux·运维·chrome·经验分享·笔记·ubuntu
qinyia2 天前
通过本地构建解决Cartographer编译中absl依赖缺失问题
linux·运维·服务器·mysql·ubuntu
郝亚军2 天前
ubuntu启一个udp server,由一个client访问
linux·ubuntu·udp
予枫的编程笔记2 天前
【Linux入门篇】Linux入门不踩坑:内核、发行版解析+环境搭建全流程
linux·ubuntu·centos·vmware·xshell·linux入门·linux环境搭建