H.264视频码流解析

一.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 · 秦城季&音视频

H264之NALU解析! - 知乎

H264的NALU结构 - 技术栈

相关推荐
s09071362 小时前
FPGA视频编码器:H.264/H.265实现核心技术解析
图像处理·算法·fpga开发·音视频·h.264
猿小路10 天前
视频流熟知
ffmpeg·h.264
桃杬13 天前
用现代 C++ 封装 FFmpeg:从摄像头采集到 H.264 编码的完整实践
c++·ffmpeg·h.264
zymill15 天前
hysAnalyser --- 常见MPEG-TS问题指南
h.265·h.264·分析工具·智能电视·视频分析·mpegts·mpegts分析
CodeOfCC18 天前
C++ 基于kmp解析nalu
c++·音视频·实时音视频·h.265·h.264
lusasky1 个月前
H.264 (AVC) 与 H.265 (HEVC) 全方位对标
h.265·h.264
聊天QQ:276998851 个月前
探秘大厂逆向 ADC 电路:从原理到实践
h.264
小柯博客2 个月前
STM32MP1 没有硬件编解码,如何用 CPU 实现 H.264 编码支持 WebRTC?
c语言·stm32·嵌入式硬件·webrtc·h.264·h264·v4l2
撬动未来的支点3 个月前
【音视频】H.264关键帧识别
音视频·h.264