本文从字节级拆解 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)10 = 合法,1 = 语法错误
NRI(nal_ref_idc)2参考级别,0 = 非参考帧,1~3 越大越重要
Type(nal_unit_type)5NALU 类型

NALU 类型表:

Type名称说明
1非 IDR sliceP/B 帧
5IDR sliceI 帧(关键帧)
6SEI补充增强信息
7SPS序列参数集
8PPS图像参数集
9AUD访问单元分隔符
24STAP-ARTP 聚合包
28FU-ARTP 分片包

位操作示例:

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 格式:

偏移长度内容
01configurationVersion = 1
11AVCProfileIndication(SPS[1])
21profile_compatibility(SPS[2])
31AVCLevelIndication(SPS[3])
416bit reserved + 2bit lengthSizeMinusOne
513bit reserved + 5bit numOfSPS
62SPS 长度
8NSPS 数据
...2PPS 长度
...NPPS 数据

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_idPPS 编号
seq_parameter_set_id引用的 SPS 编号
entropy_coding_mode_flagCAVLC / 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 传输方式

场景传输方式
RTMPconfig 帧(AVCDecoderConfigurationRecord)
RTSPSDP 的 sprop-parameter-sets
RTPSTAP-A 聚合包
HLSTS 文件里的独立 NALU

五、AAC 三种格式

格式用途头大小
raw AAC内存传递0 字节
ADTSTS / 裸流 .aac7 或 9 字节
ASCRTMP/RTSP config2 字节

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采样率
096000722050
188200816000
264000912000
3480001011025
444100118000
532000127350
624000

六、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) | ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

字段详解表

字段位宽位置典型值
syncword12字节 0 + 字节 1 高 4 位0xFFF
ID1字节 1 bit 40(MPEG-4)
layer2字节 1 bit 3~200
protection_absent1字节 1 bit 11(无 CRC)
profile2字节 2 bit 7~61(AAC LC)
samplingFreqIdx4字节 2 bit 5~24(44100)
private_bit1字节 2 bit 10
channelConfig3字节 2 bit 0 + 字节 3 bit 7~62(Stereo)
original_copy1字节 3 bit 50
home1字节 3 bit 40
copyright_bit1字节 3 bit 30
copyright_start1字节 3 bit 20
frame_length13字节 3 bit 1~0 + 字节 4 + 字节 5 bit 7~5帧总长
buffer_fullness11字节 5 bit 4~0 + 字节 6 bit 7~20x7FF(VBR)
num_raw_data_blocks2字节 6 bit 1~00

关键点

  1. aac_frame_length 是含 ADTS 头的总长度:

    int rawAacSize = frameSize - 7;
  2. profile = audioObjectType - 1:AAC LC 的 audioObjectType=2,ADTS profile=1。

  3. 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                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段位宽说明
V2版本,固定 2
P1填充标志
X1扩展头标志
CC4CSRC 数量(0~15)
M1标记位(帧最后一包)
PT7载荷类型(96=H264, 97=AAC)
Seq16序列号(每包 +1)
Timestamp32时间戳
SSRC32同步源标识

7.2 时间戳规则

编码时钟频率每帧增量
H26490000 Hz90000 / fps
AAC 4410044100 Hz1024
AAC 4800048000 Hz1024

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 发送
Logo

邀请您加入社区

更多推荐