流媒体开发】H264/AAC封装格式、ADTS头、SPS/PPS、RTP打包全解
本文从字节级拆解 H264、AAC 封装格式,深入分析 ADTS 头、SPS/PPS 作用,并给出 RTP 封包的完整实现。适合流媒体开发入门与进阶。
一、前言
在做流媒体服务器(RTMP/RTSP/HLS 互转)时,封装格式是最容易踩坑的地方。很多同学能跑通 Demo,但一遇到花屏、音画不同步、SPS/PPS 丢失就抓瞎。
本文从底层字节讲起,覆盖:
-
H264 的 NALU 结构与两种封装(Annex-B / AVCC)
-
AAC 的三种格式(raw / ADTS / ASC)
-
ADTS 头 7 字节逐位分析
-
SPS/PPS 的作用与传输
-
RTP 封包完整实现(H264 FU-A / STAP-A,AAC AU-header)
二、H264 码流结构
2.1 NALU 是什么
H264 码流是一系列 NALU(Network Abstraction Layer Unit) 的顺序拼接:
┌─────────────────┬──────────────────────────────┐ │ NALU Header │ RBSP 数据 │ │ (1 字节) │ (编码后的 slice / SPS / PPS) │ └─────────────────┴──────────────────────────────┘
2.2 NALU Header(1 字节)逐位分析
0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ |F|NRI| Type | +-+-+-+-+-+-+-+-+
| 字段 | 位宽 | 含义 |
|---|---|---|
| F(forbidden_zero_bit) | 1 | 0 = 合法,1 = 语法错误 |
| NRI(nal_ref_idc) | 2 | 参考级别,0 = 非参考帧,1~3 越大越重要 |
| Type(nal_unit_type) | 5 | NALU 类型 |
NALU 类型表:
| Type | 名称 | 说明 |
|---|---|---|
| 1 | 非 IDR slice | P/B 帧 |
| 5 | IDR slice | I 帧(关键帧) |
| 6 | SEI | 补充增强信息 |
| 7 | SPS | 序列参数集 |
| 8 | PPS | 图像参数集 |
| 9 | AUD | 访问单元分隔符 |
| 24 | STAP-A | RTP 聚合包 |
| 28 | FU-A | RTP 分片包 |
位操作示例:
uint8_t nal = 0x65; // 0110 0101 int f = (nal >> 7) & 0x01; // 0 int nri = (nal >> 5) & 0x03; // 3 int type = nal & 0x1F; // 5 (IDR)
三、H264 两种封装:Annex-B vs AVCC
3.1 Annex-B(起始码分隔)
NALU 之间用起始码分隔:
00 00 00 01 | NALU#1 | 00 00 00 01 | NALU#2 | ...
-
3 字节:
00 00 01 -
4 字节:
00 00 00 01(最常见)
使用场景:TS 文件(HLS)、RTP 传输、裸流 .h264 文件。
3.2 AVCC(长度前缀分隔)
NALU 之间用长度前缀分隔(大端):
[4 字节长度] | NALU#1 | [4 字节长度] | NALU#2 | ...
使用场景:MP4 文件、RTMP/FLV、AVCDecoderConfigurationRecord。
3.3 AVCDecoderConfigurationRecord 结构
RTMP config 帧的 payload 格式:
| 偏移 | 长度 | 内容 |
|---|---|---|
| 0 | 1 | configurationVersion = 1 |
| 1 | 1 | AVCProfileIndication(SPS[1]) |
| 2 | 1 | profile_compatibility(SPS[2]) |
| 3 | 1 | AVCLevelIndication(SPS[3]) |
| 4 | 1 | 6bit reserved + 2bit lengthSizeMinusOne |
| 5 | 1 | 3bit reserved + 5bit numOfSPS |
| 6 | 2 | SPS 长度 |
| 8 | N | SPS 数据 |
| ... | 2 | PPS 长度 |
| ... | N | PPS 数据 |
3.4 转换代码(Annex-B ↔ AVCC)
// Annex-B → AVCC
QByteArray annexBToAvcc(const QByteArray& annexB, int naluLengthSize) {
QByteArray out;
auto nalus = splitNalus(annexB);
for (const auto& nalu : nalus) {
uint8_t lenBuf[4];
ByteOrder::set_be32(lenBuf, nalu.size());
out.append(lenBuf, 4);
out.append(nalu);
}
return out;
}
// AVCC → Annex-B
QByteArray avccToAnnexB(const QByteArray& avcc, int naluLengthSize) {
QByteArray out;
const uint8_t* p = (const uint8_t*)avcc.constData();
int n = avcc.size(), offset = 0;
while (offset + naluLengthSize <= n) {
uint32_t naluLen = ByteOrder::load_be32(p + offset);
offset += naluLengthSize;
if (offset + naluLen > n) break;
out.append("\x00\x00\x00\x01", 4);
out.append((const char*)(p + offset), naluLen);
offset += naluLen;
}
return out;
}
四、SPS/PPS 详解
4.1 SPS(Sequence Parameter Set)
序列级配置,一整个视频序列共享:
| 字段 | 作用 |
|---|---|
| profile_idc | 编码 profile(Baseline/Main/High) |
| level_idc | 编码级别 |
| pic_width_in_mbs_minus1 | 图像宽 |
| pic_height_in_map_units_minus1 | 图像高 |
| frame_mbs_only_flag | 逐行/隔行 |
| max_num_ref_frames | 最大参考帧数 |
SPS 决定解码器能否初始化:没有 SPS,解码器不知道宽高,无法分配帧缓冲。
4.2 PPS(Picture Parameter Set)
图像级配置,一张或一组图像共享:
| 字段 | 作用 |
|---|---|
| pic_parameter_set_id | PPS 编号 |
| seq_parameter_set_id | 引用的 SPS 编号 |
| entropy_coding_mode_flag | CAVLC / CABAC |
| weighted_pred_flag | 加权预测 |
| deblocking_filter_control_present_flag | 去块滤波 |
4.3 SPS/PPS 关系图
SPS(全局) ← PPS(图像级) ← Slice(数据)
-
PPS 引用 SPS
-
Slice Header 引用 PPS
-
新客户端必须立即收到 SPS/PPS 才能秒开
4.4 传输方式
| 场景 | 传输方式 |
|---|---|
| RTMP | config 帧(AVCDecoderConfigurationRecord) |
| RTSP | SDP 的 sprop-parameter-sets |
| RTP | STAP-A 聚合包 |
| HLS | TS 文件里的独立 NALU |
五、AAC 三种格式
| 格式 | 用途 | 头大小 |
|---|---|---|
| raw AAC | 内存传递 | 0 字节 |
| ADTS | TS / 裸流 .aac | 7 或 9 字节 |
| ASC | RTMP/RTSP config | 2 字节 |
5.1 AudioSpecificConfig(ASC,2 字节)
13 位布局:
| audioObjectType (5) | samplingFreqIndex (4) | channelConfig (4) |
解析代码:
AacAscInfo parseAudioSpecificConfig(const QByteArray& asc) {
AacAscInfo info;
if (asc.size() < 2) return info;
uint8_t b0 = asc[0], b1 = asc[1];
info.audioObjectType = (b0 >> 3) & 0x1F;
info.samplingFreqIndex = ((b0 & 0x07) << 1) | ((b1 >> 7) & 0x01);
info.channelConfig = (b1 >> 3) & 0x0F;
info.sampleRate = sampleRateFromIndex(info.samplingFreqIndex);
info.valid = (info.audioObjectType > 0) && (info.channelConfig > 0);
return info;
}
示例:ASC = 12 10 → AAC LC, 44100Hz, 2ch
5.2 采样率索引表
| Index | 采样率 | Index | 采样率 |
|---|---|---|---|
| 0 | 96000 | 7 | 22050 |
| 1 | 88200 | 8 | 16000 |
| 2 | 64000 | 9 | 12000 |
| 3 | 48000 | 10 | 11025 |
| 4 | 44100 | 11 | 8000 |
| 5 | 32000 | 12 | 7350 |
| 6 | 24000 |
六、ADTS 头逐位详解(7 字节)
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | syncword (12 bits, 0xFFF) | ID| layer |P| | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | profile(2) | samplingFreqIdx(4) | private| chCfg(3) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | orig| home| copy|frame_len(13 bits) | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | frame_len(cont) | buffer_fullness(11) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | buffer_fullness(cont) | num_raw_data_blocks(2) | ... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段详解表
| 字段 | 位宽 | 位置 | 典型值 |
|---|---|---|---|
| syncword | 12 | 字节 0 + 字节 1 高 4 位 | 0xFFF |
| ID | 1 | 字节 1 bit 4 | 0(MPEG-4) |
| layer | 2 | 字节 1 bit 3~2 | 00 |
| protection_absent | 1 | 字节 1 bit 1 | 1(无 CRC) |
| profile | 2 | 字节 2 bit 7~6 | 1(AAC LC) |
| samplingFreqIdx | 4 | 字节 2 bit 5~2 | 4(44100) |
| private_bit | 1 | 字节 2 bit 1 | 0 |
| channelConfig | 3 | 字节 2 bit 0 + 字节 3 bit 7~6 | 2(Stereo) |
| original_copy | 1 | 字节 3 bit 5 | 0 |
| home | 1 | 字节 3 bit 4 | 0 |
| copyright_bit | 1 | 字节 3 bit 3 | 0 |
| copyright_start | 1 | 字节 3 bit 2 | 0 |
| frame_length | 13 | 字节 3 bit 1~0 + 字节 4 + 字节 5 bit 7~5 | 帧总长 |
| buffer_fullness | 11 | 字节 5 bit 4~0 + 字节 6 bit 7~2 | 0x7FF(VBR) |
| num_raw_data_blocks | 2 | 字节 6 bit 1~0 | 0 |
关键点
-
aac_frame_length是含 ADTS 头的总长度:int rawAacSize = frameSize - 7;
-
profile = audioObjectType - 1:AAC LC 的 audioObjectType=2,ADTS profile=1。
-
channelConfig 跨字节:高 1 位在字节 2,低 2 位在字节 3。
构造代码
QByteArray buildAdtsHeader(int audioObjectType, int samplingFreqIndex,
int channelConfig, int rawFrameSize) {
QByteArray h(7, '\0');
uint8_t* p = (uint8_t*)h.data();
int profile = (audioObjectType <= 0) ? 1 : (audioObjectType - 1);
int frameLen = 7 + rawFrameSize;
p[0] = 0xFF;
p[1] = 0xF1; // MPEG-4, layer 0, no CRC
p[2] = ((profile & 0x03) << 6)
| ((samplingFreqIndex & 0x0F) << 2)
| ((channelConfig >> 2) & 0x01);
p[3] = ((channelConfig & 0x03) << 6)
| ((frameLen >> 11) & 0x03);
p[4] = (frameLen >> 3) & 0xFF;
p[5] = ((frameLen & 0x07) << 5) | 0x1F; // buffer_fullness 高 5 位
p[6] = 0xFC; // 低 6 位 + num_blocks=0
return h;
}
示例输出(AAC LC, 44100Hz, 2ch, raw=380):FF F1 50 80 30 7F FC
七、RTP 封包详解
7.1 RTP 头结构(12 字节)
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |V=2|P|X| CC |M| PT | Sequence Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Timestamp | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 字段 | 位宽 | 说明 |
|---|---|---|
| V | 2 | 版本,固定 2 |
| P | 1 | 填充标志 |
| X | 1 | 扩展头标志 |
| CC | 4 | CSRC 数量(0~15) |
| M | 1 | 标记位(帧最后一包) |
| PT | 7 | 载荷类型(96=H264, 97=AAC) |
| Seq | 16 | 序列号(每包 +1) |
| Timestamp | 32 | 时间戳 |
| SSRC | 32 | 同步源标识 |
7.2 时间戳规则
| 编码 | 时钟频率 | 每帧增量 |
|---|---|---|
| H264 | 90000 Hz | 90000 / fps |
| AAC 44100 | 44100 Hz | 1024 |
| AAC 48000 | 48000 Hz | 1024 |
AAC 时间戳换算:
uint32_t rtpTs = frame->dts * sampleRate / 90000;
7.3 H264 三种打包模式
模式 1:单 NALU 包(NALU ≤ MTU)
[RTP Header 12B] [NALU(无起始码)]
模式 2:STAP-A 聚合(多 NALU)
[RTP Header] [STAP-A Header(1B, type=24)]
[SPS Len 2B] [SPS] [PPS Len 2B] [PPS]
模式 3:FU-A 分片(NALU > MTU)
[RTP Header] [FU Indicator(1B)] [FU Header(1B)] [分片数据]
FU Indicator:
|F|NRI| Type=28 |
FU Header:
|S|E|R| 原始 Type |
-
S=1:分片起始
-
E=1:分片结束
-
最后一包 M=1
7.4 FU-A 分片代码
void sendFuA(const QByteArray& nalu, uint32_t ts, bool lastNaluMarker) {
uint8_t nalHeader = nalu[0];
uint8_t nri = (nalHeader & 0x60) >> 5;
uint8_t nalType = nalHeader & 0x1F;
uint8_t fuIndicator = (nri << 5) | 28;
const int maxPayload = kMtu - 14;
int offset = 1;
bool first = true;
while (offset < nalu.size()) {
int chunkSize = qMin(maxPayload, nalu.size() - offset);
bool last = (offset + chunkSize >= nalu.size());
uint8_t fuHeader = nalType;
if (first) fuHeader |= 0x80; // S=1
if (last) fuHeader |= 0x40; // E=1
QByteArray pkt(14, '\0');
// ... 填 RTP 头
pkt[12] = fuIndicator;
pkt[13] = fuHeader;
pkt.append(nalu.constData() + offset, chunkSize);
_onPacket(pkt);
offset += chunkSize;
first = false;
}
}
7.5 AAC RTP 封装
RTP payload 结构:
[AU-headers-length 2B] [AU-header 2B] [raw AAC]
-
AU-headers-length = 16(固定)
-
AU-header = 13 bits size + 3 bits index(0)
构造代码:
void packetizeAAC(const Frame::Ptr& frame) {
int sampleRate = _audioTrack->sampleRate;
uint32_t rtpTs = frame->dts * sampleRate / 90000;
QByteArray auHeader;
auHeader.append(char(0x00)); // AU-headers-length 高字节
auHeader.append(char(0x10)); // = 16
uint16_t sizeAndIndex = (frame->data.size() << 3) & 0xFFF8;
auHeader.append(char(sizeAndIndex >> 8));
auHeader.append(char(sizeAndIndex & 0xFF));
QByteArray pkt(12, '\0');
// ... 填 RTP 头(M=1)
pkt.append(auHeader);
pkt.append(frame->data);
_onPacket(pkt);
}
八、完整推流数据流
Frame(Annex-B H264) ↓ splitNalus 拆 NALU ↓ [SPS, PPS, IDR] ↓ ┌──────────────────────────────────┐ │ SPS + PPS → STAP-A 包 │ │ IDR → 单包 或 FU-A 分片 │ └──────────────────────────────────┘ ↓ RTP 包(12B 头 + payload) ↓ UDP/TCP 发送
text
Frame(raw AAC) ↓ 构造 AU Header Section(4 字节) ↓ RTP 包(12B 头 + AU Header + raw AAC) ↓ UDP/TCP 发送
更多推荐

所有评论(0)