第一步:先搞懂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]
总结(关键点回顾)
- NALU全称与核心:Network Abstraction Layer Unit(网络抽象层单元),是H264适配网络传输的最小数据包,结构为"起始码+NALU头+载荷";
- FFmpeg解析逻辑 :先调用
ff_h264_find_start_code找起始码(封条),再用ff_h264_decode_nal_header解析NALU头(面单),最后按类型处理载荷(货物); - 核心字段:nal_unit_type(类型)决定处理逻辑(SPS存参数、IDR是关键帧),forbidden_bit为1则丢弃NALU,nal_ref_idc标记重要性。