h264码流结构

h264码流结构

H.264原始码流(又称为"裸流")是由一个一个的NALU组成的

NALU 是 H.264 码流的基本单元。每个 NALU 包含一个 NAL 头和一个 RBSP(原始字节序列载荷,Raw Byte Sequence Payload)。NALU 可以包含视频数据、参数集、补充增强信息(SEI)等。

1. 码流分层结构

H.264码流分为两个逻辑层:

VCL (Video Coding Layer,视频编码层)

负责视频内容的压缩编码,生成Slice(片)数据。每个Slice包含压缩后的宏块(Macroblock)信息。

NAL (Network Abstraction Layer,网络抽象层)

将VCL数据封装为独立的传输单元(NAL Unit),以适应不同网络或存储介质的传输需求

2. NAL单元(NAL Unit)结构

NALU 结构:

NAL 头(NAL Header):包含 NALU 类型、参考标志等信息。

RBSP(Raw Byte Sequence Payload):实际的视频数据或其他信息。

结构如下:

Start Code Prefix\] + \[NAL Header\] + \[NAL Payload (RBSP)

起始码 :用于标识一个NALU的开始,通常是0x000001或0x00000001。

NAL Header (1字节)

格式:

复制代码
| forbidden_zero_bit (1bit) | nal_ref_idc (2bit) | nal_unit_type (5bit) |
  • forbidden_zero_bit:通常为0,用于错误检测。
  • nal_ref_idc:重要性标识(0表示该NAL单元不用于参考)。
  • nal_unit_type :定义NAL单元类型(见下表)。
    NAL负载:即RBSP,它是由VCL输出的SODB经过字节对齐(添加尾部比特)后得到的。为防止负载中的数据与起始码冲突,还会在编码时插入防竞争字节0x03,形成EBSP进行传输,解码时再移除。

3. NAL单元类型(nal_unit_type)

类型值 名称 说明
1 Non-IDR Slice 非关键帧的Slice数据(如P帧、B帧)。
5 IDR Slice 关键帧(Instantaneous Decoding Refresh),清空参考帧缓存,支持随机访问。
6 SEI(Supplemental Enhancement Information) 增强信息,如时间戳、版权信息等。
7 SPS(Sequence Parameter Set) 序列参数集,包含全局编码参数(分辨率、帧率、Profile/Level等)。
8 PPS(Picture Parameter Set) 图像参数集,包含Slice级的编码参数(熵编码模式、参考帧数等)。
9 AUD(Access Unit Delimiter) 访问单元分隔符,标识视频帧的边界。

NAL代码解析:

cpp 复制代码
#include <stdint.h>

// NAL Unit 的基本属性 --- 每个 NAL 单元解析后填充
typedef struct {
    uint8_t  nal_unit_type;   // 类型号: 1=P帧 5=I帧 7=SPS 8=PPS
    uint8_t  nal_ref_idc;     // 重要性: 值越大越不能被丢弃
    const char *type_name;    // 人类可读的名字
    int      offset;          // 在原始 buffer 中的位置偏移
    int      size;            // 这个 NAL 单元的总大小 (含 start code)
} NalUnitInfo;

offset + size 让你可以直接定位到原始数据中的任意一个 NAL 单元。

nal_ref_idc 告诉你这个 NAL 被多少帧参考------I 帧的 ref_idc=3(最高),因为所有 P 帧都依赖它。

cpp 复制代码
#define MAX_NAL_UNITS 32

// 获取 NAL 类型名称
static const char* get_nal_type_name(uint8_t nal_unit_type) {
    switch (nal_unit_type) {
        case 1:  return "P/B Slice (非关键帧)";
        case 5:  return "IDR Slice (I帧/关键帧)";
        case 6:  return "SEI";
        case 7:  return "SPS (序列参数集)";
        case 8:  return "PPS (图像参数集)";
        case 9:  return "AUD";
        default: return "其他";
    }
}

// 在 buffer 中查找 start code,返回下一个 start code 的偏移
static int find_start_code(const uint8_t *buf, int start, int total_size) {
    for (int i = start; i < total_size - 3; i++) {
        // 00 00 00 01 或 00 00 01
        if (buf[i] == 0 && buf[i+1] == 0) {
            if (buf[i+2] == 0 && buf[i+3] == 1) return i;    // 4字节 start code
            if (buf[i+2] == 1) return i;                     // 3字节 start code
        }
    }
    return -1;
}

// 解析一帧 H264 数据中的所有 NAL 单元
static int parse_h264_nal_units(const uint8_t *buf, int size, 
                                 NalUnitInfo *nal_list, int max_count) {
    int nal_count = 0;
    int offset = 0;
    int start_code_len;

    while (offset < size - 4 && nal_count < max_count) {
        // 找下一个 start code
        int nal_start = find_start_code(buf, offset, size);
        if (nal_start < 0) break;

        // 确定 start code 长度
        if (buf[nal_start+2] == 1) start_code_len = 3;   // 00 00 01
        else start_code_len = 4;                           // 00 00 00 01

        // NAL header 在 start code 之后
        int header_offset = nal_start + start_code_len;
        if (header_offset >= size) break;

        uint8_t nal_header = buf[header_offset];
        uint8_t nal_unit_type = nal_header & 0x1F;       // 低5位
        uint8_t nal_ref_idc  = (nal_header >> 5) & 0x03; // 接下来2位

        // 找下一个 NAL 的起始位置来确定当前 NAL 的大小
        int next_start = find_start_code(buf, header_offset + 1, size);
        int nal_size = (next_start > 0) ? (next_start - nal_start) 
                                        : (size - nal_start);

        // 记录 NAL 信息
        nal_list[nal_count].nal_unit_type = nal_unit_type;
        nal_list[nal_count].nal_ref_idc  = nal_ref_idc;
        nal_list[nal_count].type_name    = get_nal_type_name(nal_unit_type);
        nal_list[nal_count].offset       = nal_start;
        nal_list[nal_count].size         = nal_size;
        nal_count++;

        offset = header_offset + 1;
    }

    return nal_count;
}

在拷贝数据后,调用上面的解析函数:

cpp 复制代码
AVPacket *get_ffmpeg_video_avpacket(AVPacket *pkt)
{
    video_data_packet_t *video_data_packet = video_queue->getVideoPacketQueue();
    if (video_data_packet != NULL)
    {
        int ret = av_buffer_realloc(&pkt->buf, video_data_packet->video_frame_size + 70);
        if (ret < 0) return NULL;
        
        pkt->size = video_data_packet->video_frame_size;
        memcpy(pkt->buf->data, video_data_packet->buffer, video_data_packet->video_frame_size);
        pkt->data = pkt->buf->data;

        // ★ 新增:解析并打印当前帧的 H264 码流结构
        dump_h264_stream_structure(video_data_packet->buffer, 
                                   video_data_packet->video_frame_size);

        // ★ 新增:根据实际 NAL type 设置关键帧标志,而不是无脑 KEY
        // 检查第一个 slice NAL 的 type 是否为 5 (IDR)
        uint8_t first_byte_after_startcode = video_data_packet->buffer[4]; // 跳过 00 00 00 01
        uint8_t nal_type = first_byte_after_startcode & 0x1F;
        if (nal_type == 5) {
            pkt->flags |= AV_PKT_FLAG_KEY;  // 真的是 I 帧
        }

        if (video_data_packet != NULL) {
            free(video_data_packet);
            video_data_packet = NULL;
        }
        return pkt;
    }
    return NULL;
}

4. 关键参数集(SPS/PPS)

  • SPS(Sequence Parameter Set)

    定义视频序列的全局参数,如:

    • 编码档次(Profile)和级别(Level)
    • 分辨率(pic_width_in_mbs、pic_height_in_map_units)
    • 帧率(time_scale、num_units_in_tick)
    • 参考帧数量等。
      重要性:SPS和PPS必须在解码前传输,且需可靠传输(如通过带外传输或重复插入码流)。

SPS 是 H264 码流中唯一包含分辨率的地方。如果你拿到一段裸 H264 码流但没有 SPS,你就不知道它的分辨率,解码器也无法工作。这就是为什么 IDR 帧前面总是先发 SPS+PPS。

cpp 复制代码
typedef struct {
    int width;                // 从 SPS 中解析出的实际宽度
    int height;               // 从 SPS 中解析出的实际高度
    int profile_idc;          // 66=Baseline, 77=Main, 100=High
    int level_idc;            // Level, 如 41 = Level 4.1
    int valid;                // 标记解析是否成功
    ...
} SpsInfo;
cpp 复制代码
/* ================================================================
 * SPS 解析
 *
 * SPS RBSP 语法结构 (H.264 7.3.2.1.1):
 *   profile_idc               u(8)   ← 第1个字节
 *   constraint_set0_flag      u(1)
 *   constraint_set1_flag      u(1)
 *   constraint_set2_flag      u(1)
 *   constraint_set3_flag      u(1)
 *   constraint_set4_flag      u(1)
 *   constraint_set5_flag      u(1)
 *   reserved_zero_2bits       u(2)
 *   level_idc                 u(8)   ← 第3个字节
 *   seq_parameter_set_id      ue(v)
 *   ... (根据 profile 不同, 后续字段有差异)
 *   pic_width_in_mbs_minus1   ue(v)  ← ★分辨率关键字段
 *   pic_height_in_map_units_minus1 ue(v)
 *   frame_mbs_only_flag       u(1)   ← ★场编码标志
 *
 * 分辨率计算:
 *   Width  = (pic_width_in_mbs_minus1 + 1) × 16
 *   Height = (2 - frame_mbs_only_flag) × (pic_height_in_map_units_minus1 + 1) × 16
 * ================================================================ */

这里就不parse sps了

5. Slice(片)结构

一个视频帧(Frame)可划分为多个Slice,每个Slice独立编码。Slice类型包括:

  • I-Slice:帧内预测,独立解码。
  • P-Slice:利用前向参考帧进行帧间预测。
  • B-Slice:利用双向参考帧进行帧间预测。

每个Slice包含:

  • Slice Header:参考帧选择、量化参数等。
  • Slice Data:宏块(Macroblock)数据,含预测模式、残差系数等。

在 H264 中完全没有 I 帧、P 帧、B 帧、IDR 帧的概念,之所以沿用这些说法是为了表明数据的编码模式。

H264 码流的组织形式从大到小排序是:视频序列(video sequence)、图像(frame/field-picture)、片组(slice group)、片(slice)、宏块(macroblock)、子块(sub-block)、像素(pixel)。

6. 码流层次结构示例

H.264码流按以下层次组织:

  1. 序列(Sequence)

    由一组连续的帧组成,以SPS开头

  2. 图像(Picture)

    对应一帧视频,可能包含多个Slice

  3. Slice

    包含多个宏块

  4. 宏块(Macroblock)

    16x16像素的基本编码单元,可进一步划分为子宏块(4x4~16x16)

    如何区分这几种NALU类型:

7. 码流起始码与封装

  • 起始码(Start Code)

    NAL单元之间通过起始码分隔,通常为0x000001或0x00000001。

  • 封装格式

    Annex B格式:使用起始码(常见于TS流或RTP传输)。

    AVCC格式 :使用长度前缀(如MP4文件中的avcC盒子)。

    一个原始的H.264 NALU 单元常由 StartCode NALU Header NALU Payload其中 Start Code 用于标示这是一个NALU 单元的开始,必须是"00 00 00 01" 或"00 00 01",具体的如下图:

8. 典型码流示例

cpp 复制代码
[起始码]0x00000001 [NAL Header]0x67 [SPS数据]  
[起始码]0x0000001 [NAL Header]0x68 [PPS数据]  
[起始码]0x0000001 [NAL Header]0x65 [IDR Slice数据]  
[起始码]0x0000001 [NAL Header]0x41 [P Slice数据]  
...  
  • 0x00000001起始码标识SPS(关键参数)。
  • 0x000001起始码标识普通NALU(如PPS、IDR帧)。
cpp 复制代码
byte_stream_nal_unit( NumBytesInNALunit ) { 
    // 循环读取字节,直到找到起始码前缀
    while( next_bits( 24 ) != 0x000001 && next_bits( 32 ) != 0x00000001 )
        leading_zero_8bits /* 等于 0x00 */
    
    // 如果下一个 24 位不是 0x000001,则读取一个零字节
    if( next_bits( 24 ) != 0x000001 )
        zero_byte /* 等于 0x00 */
    
    // 读取起始码前缀 0x000001
    start_code_prefix_one_3bytes /* 等于 0x000001 */
    
    // 读取 NAL 单元
    nal_unit( NumBytesInNALunit )
    
    // 循环读取字节,直到找到下一个起始码前缀或字节流结束
    while( more_data_in_byte_stream( ) && next_bits( 24 ) != 0x000001 && next_bits( 32 ) != 0x00000001 )
        trailing_zero_8bits /* 等于 0x00 */
}

每个NALU之间通过startcode(起始码)进行分隔,起始码分成两种:0x000001(3Byte)或者0x00000001(4Byte)。如果NALU对应的Slice为一帧的开始就用0x00000001,否则就用0x000001

https://github.com/0voice/audio_video_streaming/blob/main/article/039-H.264简单码流分析.md

书籍:

https://github.com/codec2021/video_codec_learn/tree/main/H264

h264 NALU :

https://www.cnblogs.com/linuxAndMcu/p/14533228.html#_labelTop

H.264原始码流(又称为"裸流")是由一个一个的NALU组成的。

其中每个NALU之间通过startcode(起始码)进行分隔,起始码分成两种:0x000001(3Byte)或者0x00000001(4Byte)。如果NALU对应的Slice为一帧的开始就用0x00000001,否则就用0x000001。 H.264码流解析的步骤就是首先从码流中搜索0x000001和0x00000001,分离出NALU;然后再分析NALU的各个字段

相关推荐
大蚂蚁2号3 小时前
深度解析:2026短视频批量生成底层技术、架构演进与企业落地实战
架构·音视频
sitellla5 小时前
Pydub:用 Python 处理音频,不写废话
开发语言·python·其他·音视频
大蚂蚁2号6 小时前
短视频批量生成技术深度解析与实战方案
python·aigc·音视频
chase。7 小时前
【学习笔记】Unified World Models:基于视频-动作耦合扩散的机器人预训练新范式
笔记·学习·音视频
VidDown8 小时前
VidDown 工具站:视频分辨率技术
javascript·网络·编辑器·音视频·视频编解码·视频
Cxiaomu8 小时前
React接入WebRTC实时视频实践
react.js·音视频·webrtc
小鹿研究点东西9 小时前
AI直播复盘实操:如何自动录制并拆解直播话术
人工智能·自动化·音视频
chase。10 小时前
【学习笔记】RIGVid:通过模仿生成视频实现机器人操作,无需物理演示
笔记·学习·音视频
黑科技研究僧10 小时前
蘑兔AI的12轨分轨功能:编曲师深度测评
人工智能·经验分享·vscode·学习·新媒体运营·音视频