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码流按以下层次组织:
-
序列(Sequence)
由一组连续的帧组成,以SPS开头
-
图像(Picture)
对应一帧视频,可能包含多个Slice
-
Slice
包含多个宏块
-
宏块(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的各个字段
