一.H.264的意义

对于一段视频数据,如果我按照原封不动的存储,如用yuv420p或者rgb24这些原始格式。

可以看到差距是非常对应的大小差距是非常大的。
那么对于存储的数据会非常大,所以这个时候就需要对数据做编码/压缩。
制订H.264的主要目标有两个:
(1)视频编码层(VCL,全称:Video Coding Layer):得到高的视频压缩比。
(2)网络提取层(NAL,全称:Network Abstraction Layer):具有良好的网络亲和性,即可适用于各种传输网络。而NAL则是以NALU(NAL Unit)为单元来支持编码数据在基于包交换技术网络中传输的。
对于数据是如何做压缩 的这里不做过多介绍,主要介绍如何存储。
二.H.264的存储
那么对于压缩后的数据该如何存储呢?
H264采用了NALU的结构进行存储,可以看到分为了 一个个的单元,所以说对于网络传输的话是非常的友好的,避免一个包过大从而因为网络问题丢包,从而导致数据的丢失。
那么为了区分这一个个单元采用了起始码,起始码一般为0x00 00 00 01 或者 0x00 00 01
-
0x00 00 00 01 (4字节) :高等级信号。
-
它在喊:"注意!新的一帧(Access Unit)开始了!"或者是 SPS/PPS 这种极其重要的参数集开始了。
-
通常用于一帧图像的 第 1 个 Slice。
-
-
0x00 00 01 (3字节) :低等级信号。
-
它在喊:"别紧张,我还属于刚才那一帧,我是这一帧里的第 2、3、4... 个碎片。"
-
通常用于一帧图像的 非第 1 个 Slice。
-

对于NALU来说,存储的东西可能是不一样的,比如上面可能是SPS/PPS/I帧/P帧/B帧等。
- SPS:序列参数集,SPS中保存了一组编码视频序列(Coded video sequence)的全局参数。
- PPS: 图像参数集,对应的是一个序列中某一副图像或者某几副图像的参数。
- I帧:帧内编码,可独立解码生成完整的图片
- P帧:前向预测编码帧,需要参考其前⾯的⼀个I 或者B 来⽣成⼀张完整的图⽚。
- B帧: 双向预测内插编码帧,则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完整的 图⽚。
然后再把NALU在做拆分

1.NAL header

forbidden_zero_bit(1 位) :禁止位,固定为 0。若在接收端检测到该位为 1,表示 NALU 可能存在传输错误(如比特翻转),需丢弃该 NALU。
nal_ref_idc(2 位) :
参考重要性指示,取值范围为 0-3,值越大表示该 NALU 对解码的重要性越高:
0:表示该 NALU 不用于参考(如 B 帧数据),丢弃后不影响后续帧解码;
1-3:表示该 NALU 用于参考(如 I 帧、P 帧数据),丢弃可能导致错误传播。
nal_unit_type(5 位) :
指示 NALU 的类型,共 32 种(0-31),常见类型如下表:
| nal_unit_type 值 | NALU 类型 | 说明 |
|---|---|---|
| 0 | 未指定 | 保留,不使用 |
| 1 | 非 IDR 图像的片(Slice) | P 帧或 B 帧的 Slice 数据 |
| 2 | 数据分区 A | 用于分片编码,存放重要的运动信息 |
| 3 | 数据分区 B | 存放次要的运动信息 |
| 4 | 数据分区 C | 存放残差数据 |
| 5 | IDR 图像的片(Slice) | 立即刷新图像(关键帧)的 Slice 数据,解码时需清空参考帧缓冲区 |
| 6 | SEI(补充增强信息) | 包含额外信息(如时间戳、用户数据),不影响基本解码 |
| 7 | SPS(序列参数集) | 包含视频序列的全局参数(如分辨率、profile 等) |
| 8 | PPS(图像参数集) | 包含图像级参数(如量化参数、熵编码方式) |
| 9 | 访问单元分隔符 | 标记视频帧的开始 |
| 10 | 序列结束符 | 标记视频序列的结束 |
| 11 | 流结束符 | 标记整个码流的结束 |
| 12 | 填充数据 | 用于增加码流长度(如测试场景) |
| 13-23 | 保留 | 用于 H.264 的扩展功能 |
| 24-31 | 未指定 | 通常用于 RTP 等网络协议的封装 |
2.RBSP(Raw Byte Sequence Payload)
RBSP 是 NALU 的负载数据,包含 VCL 层的压缩信息(如 Slice 数据、参数集内容)。它由SODB(String of Data Bits) 经过处理后得到:
SODB:VCL 层输出的原始比特流(如预测残差、运动矢量等);
RBSP:在 SODB 末尾添加停止位(1 个 "1" 比特后跟若干 "0" 比特),使其字节对齐,形成 RBSP。
3.nal_unit_type某些类型的具体介绍
1.SPS(序列参数集,nal_unit_type=7)
SPS 是视频序列的全局配置,包含影响整个序列的参数,解析时需优先处理。常见参数如下:
// SPS参数示例(部分关键参数)
profile_idc // 编码profile(如Baseline=66,Main=77,High=100)
level_idc // 编码level(如3.0=30,3.1=31)
seq_parameter_set_id // SPS的ID(用于关联PPS)
chroma_format_idc // 色度格式(如1=4:2:0,2=4:2:2,3=4:4:4)
bit_depth_luma_minus8 // 亮度位深度(通常为8)
bit_depth_chroma_minus8 // 色度位深度(通常为8)
log2_max_frame_num_minus4 // 最大帧号的对数(用于计算帧号范围)
pic_order_cnt_type // 图像顺序计数类型(0-2,控制POC的计算方式)
max_num_ref_frames // 最大参考帧数量
pic_width_in_mbs_minus1 // 视频宽度(以宏块为单位,实际宽度=(值+1)*16)
pic_height_in_map_units_minus1 // 视频高度(以宏块为单位)
frame_mbs_only_flag // 是否仅帧编码(0=支持帧/场混合,1=仅帧)
2. PPS(图像参数集,nal_unit_type=8)
PPS 定义单幅图像的参数,依赖于 SPS,常见参数如下:
// PPS参数示例(部分关键参数)
pic_parameter_set_id // PPS的ID
seq_parameter_set_id // 关联的SPS的ID
entropy_coding_mode_flag // 熵编码方式(0=CAVLC,1=CABAC)
num_ref_idx_l0_default_active_minus1 // 默认的前向参考帧列表长度
num_ref_idx_l1_default_active_minus1 // 默认的后向参考帧列表长度
weighted_pred_flag // 是否使用加权预测(对P帧)
weighted_bipred_idc // 双向预测加权模式(0-2)
pic_init_qp_minus26 // 初始量化参数(QP=值+26)
deblocking_filter_control_present_flag // 是否存在去块滤波控制信息
3. IDR Slice(即时解码刷新,nal_unit_type=5)
IDR Slice 是一种特殊的 I Slice,属于关键帧:
解码 IDR Slice 时,解码器会清空所有参考帧缓冲区,确保后续帧的解码不依赖之前的错误帧,从而终止错误传播。
IDR Slice 必须包含完整的帧内预测信息,不依赖其他帧。
4. 非 IDR Slice(nal_unit_type=1)
包括 P Slice 和 B Slice:
P Slice:依赖前向参考帧(已解码的 I/P 帧)进行预测;
B Slice:依赖双向参考帧(前向和后向的 I/P 帧)进行预测,压缩效率更高。
5. SEI(补充增强信息,nal_unit_type=6)
携带与解码无关的辅助信息,常见类型:
时间戳信息(如 NTP 时间);
用户数据(如字幕、水印);
场景切换标记;
码流统计信息。
三.NALU解析代码实战
本文以Annex B码流为例,也就是上文提到的码流方式,
对于MP4模式的话,对于PPS和SPS被封装到对应的containter中,而每个nalu的前面不再是startcode,而是对应的NALU大小。
cpp
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <iomanip>
#include <cstdint>
enum class NaluType : uint8_t {
SLICE = 1, // 非IDR图像的片(例如P帧或B帧的片)
DPA = 2, // 分区编码:运动矢量(重要信息)
DPB = 3, // 分区编码:帧内宏块信息
DPC = 4, // 分区编码:残差系数
IDR = 5, // IDR图像的片(关键帧/瞬时解码刷新帧的片)
SEI = 6, // 补充增强信息(如定时信息、版权信息)
SPS = 7, // 序列参数集(包含视频分辨率、帧率等全局参数)
PPS = 8, // 图像参数集(包含量化参数、熵编码模式等图像级参数)
AUD = 9, // 访问单元分隔符(帧边界标记)
EOSEQ = 10,// 序列结束
EOSTREAM = 11,// 流结束
FILL = 12 // 填充数据(比特率调整)
};
enum class NaluPriority : uint8_t {
DISPOSABLE = 0,
LOW = 1,
HIGH = 2,
HIGHEST = 3
};
struct NALU {
int startCodeLen; // 起始码长度 (3 或 4)
uint32_t len; // NALU 数据长度 (不含起始码)
uint8_t forbidden_bit; // 必须为 0
uint8_t nal_reference_idc; // 优先级
uint8_t nal_unit_type; // 类型
const uint8_t* data; // 指向数据的指针 (不拥有内存)
};
class H264Parser {
public:
// 加载文件到内存
bool loadFile(const std::string& filename) {
std::ifstream file(filename, std::ios::binary | std::ios::ate);
if (!file.is_open()) {
std::cerr << "Error: Could not open file " << filename << std::endl;
return false;
}
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
buffer_.resize(size);
if (file.read(reinterpret_cast<char*>(buffer_.data()), size)) {
data_ptr_ = buffer_.data();
data_end_ = buffer_.data() + size;
return true;
}
return false;
}
// 解析整个流
void parse() {
if (buffer_.empty()) return;
const uint8_t* current = data_ptr_;
int nalu_idx = 0;
std::cout << "-----+-------- NALU Table ------+---------+\n";
std::cout << " NUM | POS | IDC | TYPE | LEN |\n";
std::cout << "-----+--------+---------+-------+---------+\n";
while (current < data_end_) {
NALU nalu;
// 1. 寻找当前 NALU 的起始码
if (!findStartCode(current, nalu.startCodeLen)) {
// 找不到起始码,说明结束了或数据损坏
break;
}
// 2. 寻找下一个起始码,从而确定当前 NALU 的长度
const uint8_t* next_start = current + nalu.startCodeLen;
const uint8_t* p = next_start;
// 快速扫描下一个起始码
while (p < data_end_ - 4) {
if (p[2] == 0x01 && p[0] == 0x00 && p[1] == 0x00) {
// Found 0x00 00 01
break;
}
if (p[2] == 0x00 && p[3] == 0x01 && p[0] == 0x00 && p[1] == 0x00) {
// Found 0x00 00 00 01
break;
}
p++;
}
// 如果到了文件末尾,p 就是 end
if (p >= data_end_ - 4) {
p = data_end_;
}
// 3. 填充 NALU 信息
nalu.len = (p - current) - nalu.startCodeLen;
nalu.data = current + nalu.startCodeLen;
// 解析 NALU Header (第1个字节)
if (nalu.len > 0) {
uint8_t header = nalu.data[0];
nalu.forbidden_bit = (header >> 7) & 0x01;
nalu.nal_reference_idc = (header >> 5) & 0x03;
nalu.nal_unit_type = header & 0x1F;
}
// 4. 打印信息
printNALUInfo(nalu, nalu_idx, (current - data_ptr_));
// 5. 移动指针到下一个 NALU
current = p;
nalu_idx++;
}
}
private:
std::vector<uint8_t> buffer_;
const uint8_t* data_ptr_ = nullptr;
const uint8_t* data_end_ = nullptr;
// 判断是否是起始码,并返回长度
bool findStartCode(const uint8_t* p, int& len) {
if (p + 3 >= data_end_) return false;
// Check 0x000001
if (p[0] == 0 && p[1] == 0 && p[2] == 1) {
len = 3;
return true;
}
// Check 0x00000001
if (p + 4 <= data_end_ && p[0] == 0 && p[1] == 0 && p[2] == 0 && p[3] == 1) {
len = 4;
return true;
}
return false;
}
void printNALUInfo(const NALU& n, int num, size_t pos) {
std::string type_str;
switch ((NaluType)n.nal_unit_type) {
case NaluType::SLICE: type_str = "SLICE"; break;
case NaluType::DPA: type_str = "DPA"; break;
case NaluType::DPB: type_str = "DPB"; break;
case NaluType::DPC: type_str = "DPC"; break;
case NaluType::IDR: type_str = "IDR"; break;
case NaluType::SEI: type_str = "SEI"; break;
case NaluType::SPS: type_str = "SPS"; break;
case NaluType::PPS: type_str = "PPS"; break;
case NaluType::AUD: type_str = "AUD"; break;
case NaluType::EOSEQ: type_str = "EOSEQ"; break;
case NaluType::EOSTREAM: type_str = "EOSTR"; break;
case NaluType::FILL: type_str = "FILL"; break;
default: type_str = "UNK"; break;
}
std::string idc_str;
switch ((NaluPriority)n.nal_reference_idc) {
case NaluPriority::DISPOSABLE: idc_str = "DISPOS"; break;
case NaluPriority::LOW: idc_str = "LOW"; break;
case NaluPriority::HIGH: idc_str = "HIGH"; break;
case NaluPriority::HIGHEST: idc_str = "HIGHEST"; break;
}
// 格式化输出
std::cout << std::setw(5) << num << " | "
<< std::setw(6) << pos << " | "
<< std::setw(7) << idc_str << " | "
<< std::setw(5) << type_str << " | "
<< std::setw(7) << n.len << " |" << std::endl;
}
};
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cout << "Usage: " << argv[0] << " <input_h264_file>" << std::endl;
return -1;
}
H264Parser parser;
if (parser.loadFile(argv[1])) {
parser.parse();
}
return 0;
}

参考博客:
视音频数据处理入门:H.264视频码流解析_视频码流分析-CSDN博客
视频编码之H.264 · 秦城季&音视频