做蓝牙音频开发这么多年,我发现一个很有意思的现象:90%的开发者把精力都花在了信令流程上,反复调试Discover、Set Configuration、Start这些步骤,但真正上线后遇到的性能问题、卡顿问题、兼容性问题,90%都出在传输层。
目录
[2.1 RTP头的完整格式](#2.1 RTP头的完整格式)
[2.2 媒体包的封装完整示例](#2.2 媒体包的封装完整示例)
[2.3 媒体传输的发送规则](#2.3 媒体传输的发送规则)
[2.4 接收端的抖动缓冲机制](#2.4 接收端的抖动缓冲机制)
[3.1 RTCP的两种核心报文](#3.1 RTCP的两种核心报文)
[3.2 RTCP的发送间隔](#3.2 RTCP的发送间隔)
[3.3 丢包统计的计算方法](#3.3 丢包统计的计算方法)
[3.4 报告传输的通道选择](#3.4 报告传输的通道选择)
[4.1 FEC的基本原理](#4.1 FEC的基本原理)
[4.2 AVDTP的FEC参数](#4.2 AVDTP的FEC参数)
[4.3 恢复包的格式](#4.3 恢复包的格式)
[4.4 恢复传输的工作流程](#4.4 恢复传输的工作流程)
[4.5 FEC的适用边界](#4.5 FEC的适用边界)
[5.1 为什么需要适配层](#5.1 为什么需要适配层)
[5.2 适配层PDU的完整格式](#5.2 适配层PDU的完整格式)
[5.3 分片与重组机制](#5.3 分片与重组机制)
[5.4 多路复用的打包策略](#5.4 多路复用的打包策略)
[5.5 多路复用的实际收益](#5.5 多路复用的实际收益)
[6.1 ROHC的基本原理](#6.1 ROHC的基本原理)
[6.2 AVDTP对ROHC的修改](#6.2 AVDTP对ROHC的修改)
[6.3 ROHC的三种工作模式](#6.3 ROHC的三种工作模式)
[6.4 ROHC的状态机](#6.4 ROHC的状态机)
[6.5 ROHC的实际收益](#6.5 ROHC的实际收益)
[7.1 通道建立的顺序规则](#7.1 通道建立的顺序规则)
[7.2 通道MTU要求](#7.2 通道MTU要求)
[7.3 L2CAP传输模式的选择](#7.3 L2CAP传输模式的选择)
[7.4 流控与流量整形](#7.4 流控与流量整形)
声音断断续续、延迟忽大忽小、切换编码就爆音、特定环境下频繁卡顿......这些问题,本质上都不是信令的问题,而是传输层的参数配置、封包策略、流量控制没有做好。很多人以为AVDTP的传输就是把音频数据打个RTP头发出去这么简单,真的深入抓包分析才会发现,里面的门道远比信令复杂得多。
AVDTP的传输体系是一套经过精心设计的分层架构,从最上层的媒体采样,到RTP封装,到FEC冗余保护,到适配层复用压缩,再到最底层的L2CAP通道传输,每一层都有明确的职责和严格的规范。只有把每一层的规则都吃透,才能真正调优出低延迟、高稳定的蓝牙音频链路。
本文从最基础的RTP数据包格式开始,一层一层拆解AVDTP的传输机制,把媒体流、报告流、恢复流三条传输通道的运行原理全部讲透,再讲清楚适配层的多路复用和头压缩是怎么工作的。看完这篇,你再去抓包分析音频卡顿问题,就能一眼看出问题出在哪一层。
一、AVDTP传输体系整体架构
AVDTP的传输体系采用了分层解耦的设计,一个逻辑上的音频流,在传输层会拆分成最多三条独立的传输会话,分别承载不同类型的数据包。这三条会话可以各自走独立的L2CAP通道,也可以通过适配层复用到同一条通道上,取决于是否启用了多路复用服务。
规范对传输层的定位非常明确:
The transport procedures define how media, recovery and reporting packets are transported between peer AVDTP entities.
也就是说,传输层不关心音频内容是什么,只负责把三类数据包可靠、高效、准时地送到对端。
我们可以用一个现代化的物流系统来比喻整个传输架构:
媒体传输会话:主货运车队,负责运输真正的货物(音频数据),优先级最高
恢复传输会话:应急备件车队,负责运输冗余备件,货物丢了可以用备件补上
报告传输会话:物流监控车队,负责来回传递路况和运输状态报表
适配层:物流调度中心,负责把多个车队的货物拼装到同一辆货车上,优化运力
L2CAP通道:高速公路,是真正跑数据的物理车道

三种传输会话的核心属性对比:
|----------|-------------|---------------|----------|------------|------------|
| 会话类型 | 承载数据 | 流向 | 是否必选 | 对实时性要求 | 对可靠性要求 |
| 媒体传输会话 | RTP媒体包 | Source → Sink | 必选 | 极高 | 较高,允许少量丢包 |
| 恢复传输会话 | FEC恢复包 | Source → Sink | 可选 | 高 | 较高 |
| 报告传输会话 | RTCP SR/RR包 | 双向 | 可选 | 中 | 高,不允许丢包 |
这个设计非常符合实时音视频传输的客观规律:媒体数据是核心,必须保证低延迟,少量丢包可以通过纠错或者插值掩盖;报告数据虽然量小,但必须可靠送达,否则发送端无法准确掌握链路质量;恢复数据是保险,只有在丢包的时候才派上用场。
二、媒体传输流程:RTP封装的所有细节
媒体传输是整个传输体系的核心,所有其他机制都是围绕它服务的。AVDTP直接复用了IETF标准的RTP协议作为媒体封装格式,没有自己重新发明轮子。这也是蓝牙音频能够和众多音视频框架无缝对接的重要原因。
规范明确指出:
AVDTP shall use RTP as specified in IETF RFC 3550 for transport of media packets.
这句话看似简单,实际上把RFC3550的所有规则都引入了进来,包括头格式、时间戳规则、序列号规则、SSRC规则等等。
2.1 RTP头的完整格式
一个标准的RTP头固定12字节,后面跟着可选的CSRC列表和扩展头。AVDTP场景下几乎不会用到CSRC和扩展头,所以绝大多数时候RTP头就是12字节。

每一个字段都有严格的使用规则,很多音频卡顿的bug都出在这些细节上。

①版本号 V
固定为2。这是RTP协议的版本号,所有符合RFC3550的实现都必须设置为2。如果这个字段不对,接收端会直接丢弃整个包。
②填充位 P
表示包末尾是否有填充字节。AVDTP场景下几乎不会用到填充,所以这个位通常为0。如果设置为1,包的最后一个字节表示填充的长度,接收端需要去掉这些填充字节才能拿到有效载荷。
③扩展位 X
表示是否有RTP扩展头。AVDTP规范没有定义任何扩展头,所以这个位必须为0。如果设备发送了带扩展头的RTP包,很多耳机无法正确解析,会导致没有声音。
④CSRC计数 CC
表示CSRC标识符的数量。AVDTP是点对点传输,不存在混合器,所以CSRC计数永远是0,也就没有后续的CSRC列表。
⑤Marker标记 M
这是最容易用错的字段。Marker位的作用是标记帧边界。对于音频来说,每个RTP包通常承载一个完整的音频帧,所以每个包的Marker位都应该设置为1。
很多开发者会搞反,以为第一个包设1后面设0,或者所有包都设0。规范明确规定,对于音频,每个talkspurt的第一个包设置Marker为1。而蓝牙音频是连续流,不存在talkspurt的间隙,所以通常每个完整帧都设置Marker为1。
如果Marker位用错,接收端的抖动缓冲区可能无法正确判断帧边界,导致播放卡顿或者爆音。
⑥负载类型 PT
7位,标识RTP包承载的媒体编码类型。AVDTP场景下常用的负载类型:
96:SBC编码
97:MPEG-2 AAC
98:MPEG-4 AAC
99:ATRAC
100及以上:厂商自定义编码,比如LDAC、aptX、LHDC
负载类型是在流配置阶段协商好的,整个流的生命周期内不能改变。如果中途突然改变负载类型,接收端会无法解码。
⑦序列号 Sequence Number
16位无符号整数,每个媒体包发送后序列号加1。接收端通过序列号检测丢包和重排包顺序。
规范规定:
The sequence number increments by one for each media packet sent.
起始值应该是随机的,不能从0开始,这是为了防止明文攻击。不过实际开发中很多图省事的实现直接从0开始,也能正常工作。
序列号是循环递增的,到0xFFFF之后回到0。这里有一个经典的坑:比较序列号先后的时候不能直接比大小,必须考虑回绕。正确的比较算法是:如果两个序列号的差值在0x8000以内,数值大的更新;否则数值小的更新。
// 判断序列号seq2是否比seq1更新
bool rtp_seq_is_newer(uint16_t seq1, uint16_t seq2) {
return (seq2 > seq1 && seq2 - seq1 < 0x8000) ||
(seq2 < seq1 && seq1 - seq2 > 0x8000);
}
这个函数看着简单,但90%的新手都会写错,导致序列号回绕的时候出现大面积丢包误判,声音突然卡住几秒钟。
⑧时间戳 Timestamp
32位无符号整数,记录了该包中第一个采样点的采样时刻。这是实现音视频同步和抖动缓冲的核心字段。
规范反复强调:
The Time Stamp reflects the sampling instant of the first octet in the media packet.
注意,是采样时刻,不是发送时刻,也不是接收时刻。这一点非常非常重要,很多人理解错了,导致时间戳完全失去意义。
时间戳的单位由采样率决定。比如44.1kHz采样率下,时间戳加1对应1/44100秒。如果一个SBC帧包含1024个采样点,那么每个RTP包的时间戳增量就是1024。
时间戳同样是循环递增的,但因为是32位,回绕周期很长,44.1kHz下大概27小时才会回绕一次,所以实际开发中遇到的概率不高,但设计算法的时候还是要考虑到。
时间戳的起始值也必须是随机的,不能从0开始。和序列号一样,这是安全要求,实际实现中很多人忽略了。
⑨同步源 SSRC
32位,唯一标识一个RTP流。同一个流的所有包SSRC必须相同。如果SSRC变了,接收端会认为来了一个新的流,重置所有状态。
SSRC的值必须随机生成,不能有两个流使用相同的SSRC。蓝牙点对点场景下只有一个流,所以冲突概率很低,但规范上还是要求随机生成。
很多开发者会随便填一个固定值比如0x12345678,虽然大多时候能工作,但严格来说是不符合规范的。
2.2 媒体包的封装完整示例
以真实A2DP音乐播放数据包为例,完整拆解一个AVDTP媒体包从原始音频采样到L2CAP空中包的四层封装过程。这个包是典型的SBC编码立体声音乐,代表了绝大多数消费级蓝牙音频场景的标准封装格式。

本次示例的配置参数与抓包完全对应:
编码格式:SBC
采样率:44.1 kHz
声道模式:联合立体声 Joint Stereo
分配方式:响度分配 Loudness
子带数:8
块数:16
比特池值:53
单RTP包打包帧数:8帧

2.2.1 第一层:SBC单帧封装
最内层是经过SBC编码器输出的单个音频帧,这是音频编码的最小处理单元。一个SBC帧由帧头、比例因子、音频采样数据三部分组成,snoop中展开的Frame 1就是完整的单帧结构。
(1)帧头部分(4字节)
SBC帧头固定4字节,包含了解码所需的全部核心配置信息:
①同步字 Syncword :1字节,固定值0x9C,用于接收端识别SBC帧的起始边界,相当于帧的同步标记。如果同步字不匹配,接收端会丢弃该帧并重新寻找帧头。
②帧头控制字段:1字节,按位拆分所有编码参数:
采样率:2bit,二进制
00对应44.1kHz块数:2bit,二进制
11对应每帧16块声道模式:2bit,二进制
01对应联合立体声分配方式:1bit,二进制
1对应响度分配子带数:1bit,二进制
1对应8个子带
③CRC校验 :1字节,抓包值为0xFE,对帧头和比例因子做8位循环冗余校验,用于检测传输过程中出现的比特错误。如果校验失败,接收端会直接丢弃该帧,避免错误解码产生爆音。
④联合立体声标志 Join :1字节,仅联合立体声模式存在,每一位对应一个子带是否启用联合编码。抓包中8个子带全部启用联合编码,对应值为0xFF。
(2)比例因子部分(8字节)
比例因子是SBC子带编码的核心参数,每个子带对应一个4bit的比例因子,用于控制该子带的量化步长,是心理声学模型的直接体现。
左声道8个子带,共8×4=32bit
右声道8个子带,共8×4=32bit
合计64bit = 8字节
snoop中可以直观看到两个声道各自8个子带的比例因子数值,比如左声道子带1为13、子带2为9,数值越大说明该子带的信号能量越强,分配的量化步长越大。
(3)音频采样数据部分(106字节)
这是SBC帧的有效载荷,是量化编码后的子带采样数据。数据长度由块数和比特池值共同决定,计算公式:
采样数据长度(字节) = (块数 × 比特池值) / 8
代入本次参数计算:
(16 × 53) / 8 = 106 字节
和抓包中显示的Audio Samples 106字节完全一致。比特池值53是SBC立体声支持的最大值,对应最高的编码质量和码率。
单帧总长度计算
单帧总长度 = 4字节帧头 + 8字节比例因子 + 106字节采样数据 = 118 字节
2.2.2 第二层:RTP SBC载荷封装
单个SBC帧只有118字节,如果每个帧都单独加12字节的RTP头发送,头开销占比超过10%,对带宽紧张的蓝牙来说浪费严重。因此AVDTP遵循RFC 3119规范,支持将多个SBC帧打包到同一个RTP包中传输,显著降低协议头开销。
(1)SBC载荷头(1字节)
多个SBC帧打包时,载荷最前面有1字节的载荷头,包含4个控制字段:
Number of Frames:高4位,值为8,表示这个RTP包中包含8个SBC帧
Last:1位,值为0,表示这不是一个分片的最后一包
Starting:1位,值为0,表示这不是一个分片的第一包
Fragmented:1位,值为0,表示没有分片,包内所有帧都是完整的
(2)多帧打包的带宽收益
我们可以直观对比两种打包方式的开销差异:
单帧打包:12字节RTP头 + 1字节载荷头 + 118字节数据 = 131字节,协议开销占比约9.9%
8帧打包:12字节RTP头 + 1字节载荷头 + 8×118字节数据 = 957字节,协议开销占比约1.4%
仅仅是将8个帧合并到一个RTP包中,协议开销就从近10%降到了1.4%,节省了大量宝贵的空中带宽。这也是为什么所有商用蓝牙音频实现几乎都使用多帧打包的原因。
载荷总长度计算
RTP载荷总长度 = 1字节载荷头 + 8 × 118字节单帧 = 945 字节
2.2.3 第三层:RTP头封装
SBC载荷准备完成后,加上标准的12字节RTP头,就构成了完整的RTP媒体包。我们对照抓包逐字段拆解说明:
|-----------------|--------------|-------------------------------------------------------|
| 字段 | 抓包值 | 含义说明 |
| Version | 2 | RTP协议版本号,固定为2,所有符合RFC3550的实现必须使用该值 |
| Padding | No | 无尾部填充字节 |
| Extension | No | 无RTP扩展头,AVDTP规范未定义扩展头 |
| CSRC Count | 0 | 无贡献源,蓝牙为点对点传输,不存在混音器 |
| Marker | No | Marker位为0。连续播放过程中的普通数据包Marker位均为0,仅通话起始的第一个包会置1标记通话突增 |
| Payload Type | 0x60 (十进制96) | 动态负载类型96,蓝牙SBC编码默认使用该值 |
| Sequence Number | 352 | 当前包的序列号,每发送一个RTP包递增1,接收端据此检测丢包和重排 |
| Time Stamp | 41377987 | 时间戳,以原始采样率为单位。每个包包含8帧共1024个原始采样,因此每个包的时间戳增量固定为1024 |
| SSRC | 0x00000000 | 同步源标识符。规范要求随机生成SSRC避免冲突,但大量实际产品简化使用固定值0,点对点场景下不影响正常通信 |
RTP包总长度计算
完整RTP包长度 = 12字节RTP头 + 945字节SBC载荷 = 957 字节
2.2.4 第四层:L2CAP通道封装
最后,完整的RTP包作为L2CAP的服务数据单元SDU,通过流建立阶段协商好的媒体通道发送出去。
目标通道CID:
0x004A,这是流建立阶段由对端分配的媒体通道标识符L2CAP载荷长度:965字节
传输模式:Basic Mode,基本模式,无重传无流控,保证最低延迟
抓包中L2CAP帧总长度为965字节,与计算值的微小差异来自底层字节对齐填充,属于正常实现差异,不影响协议解析。
2.2.5 封装函数代码示例
下面的代码实现了对应参数的SBC多帧RTP封装,完全匹配本次抓包的格式与参数:
// 封装8个SBC帧到一个RTP包,返回封装后的总长度
uint16_t avdtp_sbc_pack_8frames(
uint8_t* out_buf,
const uint8_t sbc_frames[8][118],
uint16_t seq,
uint32_t timestamp,
uint32_t ssrc
) {
// 1. 填充标准RTP头
rtp_header_t* rtp_hdr = (rtp_header_t*)out_buf;
rtp_hdr->version = 2;
rtp_hdr->padding = 0;
rtp_hdr->extension = 0;
rtp_hdr->csrc_count = 0;
rtp_hdr->marker = 0; // 连续流普通包marker置0
rtp_hdr->payload_type = 96; // SBC编码对应动态PT值96
rtp_hdr->seq = htons(seq);
rtp_hdr->timestamp = htonl(timestamp);
rtp_hdr->ssrc = htonl(ssrc);
// 2. 填充SBC载荷头
uint8_t* sbc_hdr = out_buf + sizeof(rtp_header_t);
// 高4位=8帧,低4位无分片、非起始、非结束
*sbc_hdr = (8 << 4) | (0 << 3) | (0 << 2) | (0 << 1) | 0;
// 3. 依次复制8个SBC帧数据
uint8_t* payload = sbc_hdr + 1;
for (int i = 0; i < 8; i++) {
memcpy(payload + i * 118, sbc_frames[i], 118);
}
// 返回封装后的总字节数
return sizeof(rtp_header_t) + 1 + 8 * 118;
}
接收端的解封装过程正好是封装的逆过程:先剥离L2CAP头得到RTP包,解析RTP头校验序列号与时间戳,再读取SBC载荷头拆分出每个独立的SBC帧,最后送入SBC解码器解码输出PCM音频数据。
2.3 媒体传输的发送规则
发送端的媒体传输有几条不成文但必须遵守的规则,规范里没有明说,但所有稳定的实现都是这么做的:
第一,发送间隔必须均匀。不能一下子发一堆包,然后空转很长时间。均匀的发送间隔可以降低对端的抖动缓冲压力,减少延迟。对于44.1kHz 1024样本帧,每帧时长约23.2ms,就应该每隔23.2ms发一个包,而不是攒10个包一起发。
第二,包大小不能超过通道MTU。如果一个音频帧太大,超过了L2CAP通道的MTU,就会被底层分片,增加延迟和丢包概率。SBC编码通常不会有这个问题,但LDAC、aptX HD等高码率编码很容易超过MTU,需要做分片处理。
第三,序列号和时间戳必须连续单调递增。不能跳号,不能回退,不能忽大忽小。任何不连续都会导致接收端认为丢包了,触发错误隐藏,产生爆音。
第四,暂停后恢复发送时序列号必须连续。很多人暂停之后重启会重置序列号和时间戳,这是大错特错的。暂停只是不发数据了,流的状态还在,恢复之后应该接着之前的序列号和时间戳继续发,不能清零。
2.4 接收端的抖动缓冲机制
接收端收到RTP包之后,不是直接送给解码器播放的,而是先放进一个抖动缓冲区。抖动缓冲区的作用是吸收网络抖动,把到达时间不均匀的数据包,变成均匀的播放节奏输出给解码器。
抖动缓冲的大小直接决定了延迟和流畅度的平衡:缓冲越大,抗抖动能力越强,越不容易卡顿,但延迟越高;缓冲越小,延迟越低,但稍微有点抖动就会下溢,产生爆音。
规范附录D给出了标准的抖动计算算法,这是动态调整缓冲大小的基础:
D(i,j) = (R_i - R_j) - (S_i - S_j)
J(i) = J(i-1) + (|D(i-1,i)| - J(i-1)) / 16
其中R是接收时间,S是时间戳对应的发送时间。这个算法本质上是一个一阶低通滤波器,对瞬时抖动做平滑,得到一个稳定的抖动估计值。接收端根据这个抖动值动态调整缓冲区大小,在延迟和流畅度之间取得最佳平衡。
typedef struct {
uint32_t last_timestamp;
uint32_t last_arrival_time;
uint32_t jitter;
} rtp_jitter_t;
void rtp_jitter_update(
rtp_jitter_t* jitter,
uint32_t timestamp,
uint32_t arrival_time_ms,
uint32_t sample_rate
) {
if (jitter->last_arrival_time == 0) {
jitter->last_timestamp = timestamp;
jitter->last_arrival_time = arrival_time_ms;
jitter->jitter = 0;
return;
}
// 计算时间戳差,转换为毫秒
uint32_t ts_diff = timestamp - jitter->last_timestamp;
uint32_t ts_diff_ms = (ts_diff * 1000) / sample_rate;
// 计算到达时间差
uint32_t arrival_diff_ms = arrival_time_ms - jitter->last_arrival_time;
// 计算D值
int32_t d = arrival_diff_ms - ts_diff_ms;
if (d < 0) d = -d;
// 更新抖动值
jitter->jitter += (d - (int32_t)jitter->jitter) / 16;
jitter->last_timestamp = timestamp;
jitter->last_arrival_time = arrival_time_ms;
}
这个算法看起来简单,但参数调优非常考验功力。分母取16是规范推荐值,代表平滑系数。分母越大,抖动值越稳定,但响应越慢;分母越小,响应越快,但波动越大。16是一个经验上的最优值。
三、报告传输流程:RTCP质量监控的运行原理
光发媒体包不行,发送端还得知道链路质量怎么样,丢包多不多,延迟大不大,抖动严不严重。这些信息靠RTCP协议来传递,也就是报告传输会话干的活。

规范规定:
RTCP shall be used to carry transmission quality information between the source and the sink.
RTCP和RTP是成对出现的,RTP传媒体数据,RTCP传控制信息。
3.1 RTCP的两种核心报文
AVDTP场景下只用到两种RTCP报文:发送方报告SR和接收方报告RR。
(1)发送方报告 SR

由Source端周期性发送,包含发送统计信息:
SSRC:发送方的同步源标识
NTP时间戳:发送该SR包的绝对时间,64位
RTP时间戳:和NTP时间对应的RTP时间戳,用于时间同步
发送的包数:累计发送的RTP包总数
发送的字节数:累计发送的RTP载荷字节总数
SR的主要作用是让接收端计算往返延迟,同时做时间同步。接收端可以把自己收到包的情况和SR里的发送情况做对比,算出丢包率等指标。

(2)接收方报告 RR

由Sink端周期性发送,包含接收统计信息:
SSRC:接收方自己的同步源标识
被报告的SSRC:对应的媒体流SSRC
丢包率:从上一次报告到现在的丢包比例
累计丢包数:从流开始到现在总共丢了多少包
最高收到序列号:收到的最大连续序列号
到达间隔抖动:我们上面讲的抖动计算结果
上次SR时间戳:最近一次收到SR的NTP时间戳
接收SR到发送RR的延迟:收到SR到发出RR之间的时间差

发送端收到RR之后,就可以算出往返时间、丢包率、抖动这些关键指标,然后根据这些指标调整编码码率、启用FEC、调整缓冲大小等等。

3.2 RTCP的发送间隔
RTCP不能发得太频繁,否则会占用太多带宽;也不能发得太稀疏,否则质量反馈不及时。
规范推荐的RTCP发送间隔是5秒。也就是说,每5秒发一次报告。这个值是针对普通音视频会议场景设计的,蓝牙音频场景下也基本适用。
实际开发中,很多设备会发得更频繁一些,比如1秒一次,以便更快地响应链路质量变化。但不建议低于500ms,否则RTCP本身的带宽开销就太大了。
RTCP的发送间隔还有一个重要规则:发送间隔应该加入随机抖动,不能严格按固定间隔发。这是为了避免多个设备同时发送RTCP造成网络拥塞。蓝牙点对点场景下只有两个设备,这个问题不明显,但规范上还是要求这么做。
3.3 丢包统计的计算方法
丢包率是最重要的QoS指标之一,但很多人算不对。正确的计算方法是这样的:
每次收到一个包,更新期望收到的包数 = 最高序列号 - 初始序列号 + 1。
实际收到的包数 = 期望收到的包数 - 累计丢包数。
本次间隔内的丢包数 = 本次期望增量 - 本次实际收到的数量。
丢包率 = 本次间隔丢包数 / 本次期望收到的数量。
这里面有两个坑:
第一,必须考虑序列号回绕。如果最高序列号从0xFFFF回到了0,期望包数的计算要加上0x10000。
第二,乱序包不算丢包。如果收到了后面的包,前面的包还没到,不能立刻算丢包,要等一会儿,说不定是乱序了。只有确认再也收不到了,才算丢包。
3.4 报告传输的通道选择
报告包既可以走独立的报告通道,也可以和媒体包复用同一个通道,取决于是否启用了多路复用服务。
如果没有启用复用,报告包走独立的L2CAP通道,好处是互不干扰,报告包不会被媒体包挤占;坏处是多占了一个通道,开销大。
如果启用了复用,报告包和媒体包走同一个L2CAP通道,通过适配层头的TSID来区分。好处是节省通道资源;坏处是媒体流量大的时候可能会延迟报告包的发送。
实际产品中,绝大多数实现都用独立通道,因为报告包量很小,多一个通道的开销可以接受,而且可靠性更有保障。
四、恢复传输流程:FEC前向纠错的实现机制
无线传输难免丢包,音频丢包就会产生爆音。重传的话延迟太大,不适合实时音频。所以AVDTP引入了前向纠错FEC机制,发数据的时候顺便发一些冗余数据,丢了包可以用冗余数据恢复,不需要重传。

规范明确说明:Recovery is based on IETF RFC 2733. 也就是直接复用了标准的通用FEC方案。
4.1 FEC的基本原理
FEC的核心思想很简单:用多个媒体包异或生成一个恢复包。如果其中一个媒体包丢了,可以用剩下的媒体包和恢复包异或,把丢的那个包算出来。
举个最简单的例子:3个媒体包M1、M2、M3,生成一个恢复包R = M1 ^ M2 ^ M3。
如果M2丢了,接收端可以计算 M2 = M1 ^ M3 ^ R,完美恢复出原始数据。
这个方案的好处是完全不需要重传,没有额外延迟;坏处是需要额外的带宽开销。上面的例子就是用33%的带宽开销,换取最多丢1个包的恢复能力。
4.2 AVDTP的FEC参数
AVDTP定义了两个关键的FEC参数,在流配置阶段协商:
①最大恢复窗口大小 MRWS
接收端为了恢复丢包,最多需要缓存多少个媒体包。取值范围1到24。这个值直接决定了接收端的内存开销和额外延迟。MRWS越大,能恢复的连续丢包越多,但缓冲越大,延迟越高。
②最大媒体包数 MNMP
生成一个恢复包最多使用多少个媒体包。取值范围1到24。这个值决定了带宽开销。MNMP越大,每个恢复包保护的媒体包越多,带宽开销越低,但同时只能恢复一个丢包的概率也越高。
这两个参数需要根据场景权衡:
音乐播放场景:延迟要求不高,可以设MRWS=8,MNMP=4,开销25%,能抗连续4个丢包
游戏语音场景:延迟要求高,可以设MRWS=4,MNMP=2,开销50%,能抗1个丢包
4.3 恢复包的格式
恢复包也用RTP封装,负载类型和媒体包不同。恢复包的RTP头后面跟着FEC头,然后是FEC冗余数据。
FEC头包含了这个恢复包保护了哪些媒体包的信息,接收端根据这些信息来决定用哪些包来做恢复运算。
具体的FEC头格式和算法在RFC2733里有详细定义,AVDTP直接沿用,没有做修改。所以只要实现了标准的RFC2733 FEC,就可以和AVDTP兼容。
4.4 恢复传输的工作流程
发送端流程:
①媒体包生成后,一边发给适配层,一边送一份给恢复模块
②恢复模块缓存媒体包,缓存数量达到MNMP时,计算生成一个恢复包
③恢复包通过恢复传输会话发出去
④滑动窗口继续缓存后续的媒体包,循环生成恢复包
接收端流程:
①收到媒体包,先送给恢复模块
②恢复模块检查序列号是否连续,如果连续,直接交给上层
③如果发现丢包,检查缓存里有没有对应的恢复包
④如果有恢复包且满足恢复条件,计算恢复出丢失的媒体包,交给上层
⑤如果无法恢复,记录丢包,交给上层做错误隐藏
⑥收到恢复包,缓存起来备用
整个恢复过程对上层完全透明。规范强调:No intervention of the application is required during the active recovery phase. 上层应用根本不知道发生了丢包和恢复,一切都在传输层自动处理了。
4.5 FEC的适用边界
FEC不是万能的,它有明确的适用边界:
只能恢复随机的零星丢包,不能恢复连续大量丢包
只能抗一定比例的丢包,通常丢包率超过10%就扛不住了
会增加额外的带宽开销和计算开销
会增加少量的处理延迟,因为接收端需要多缓存几个包才能做恢复
所以,不是所有场景都适合开FEC。信号好的环境下不开FEC音质更好延迟更低;信号差的环境下开FEC可以明显减少卡顿。
五、适配层传输:多路复用与分片重组
讲完了三种传输会话,我们来看适配层是怎么把它们组织到底层L2CAP通道上的。适配层是传输层中最容易被忽略,但对效率影响最大的一层。
5.1 为什么需要适配层
非复用模式下,每个传输会话对应一个独立的L2CAP通道。三个会话就要三个L2CAP通道。每个L2CAP通道都有自己的协议开销、调度开销、状态维护开销。如果同时有多个流,通道数量会成倍增加。
而且每个通道单独发包,小包特别多。比如RTCP报告包很小,只有几十字节,单独占一个L2CAP包发送,协议头占比非常高,带宽浪费严重。
多路复用服务就是为了解决这个问题的:把多个传输会话的数据包,打包到同一个L2CAP包里发送,提高带宽利用率,减少通道数量。
规范对适配层的定位是:
The Adaptation Layer provides multiplexing of several transport sessions onto one transport channel.
5.2 适配层PDU的完整格式
每个适配层PDU(AL-PDU)对应一个L2CAP包。AL-PDU里面可以装多个子包,每个子包属于一个传输会话。

每个子包的AL头格式:
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
| TSID |F| LCODE |
+-+-+-+-+-+-+-+-+

各个字段的含义:
①TSID 传输会话ID
5位,标识这个子包属于哪个传输会话。接收端根据TSID把数据分发到对应的会话处理。TSID由流的发起方分配,在配置阶段协商好。
5位意味着最多可以有32个传输会话复用同一个通道,对于绝大多数场景来说完全够用。
②F 分片标志
1位,表示这个子包是不是完整的包。
F=0:这是一个完整的包,或者是一个大包的第一个分片
F=1:这是一个大包的后续分片
③LCODE 长度编码
2位,表示长度字段的格式:
00:没有长度字段。这个子包是AL-PDU的最后一个子包,长度就是AL-PDU剩下的所有字节
01:16位长度字段,大端序
10:9位长度字段,高1位在LCODE,低8位在后续字节
11:9位长度字段,高1位在LCODE,低8位在后续字节
这个设计非常巧妙,用变长的长度字段来最小化头开销。小包用短长度字段,大包用长长度字段,最后一个包甚至可以省略长度字段,充分利用每一个字节。
5.3 分片与重组机制
如果一个传输包太大,装不进剩下的AL-PDU空间,可以拆分成多个分片,分散到多个AL-PDU里传输。
分片规则:
第一个分片F=0,长度字段填整个包的总长度
后续分片F=1,长度字段填当前分片的长度
接收端根据TSID和F标志识别分片,缓存起来,收齐所有分片后重组为完整的包
如果中间某个分片丢了,整个包都算丢了
分片机制可以充分利用每个L2CAP包的剩余空间,避免浪费。比如一个L2CAP包MTU是672字节,已经装了一个300字节的媒体包,剩下300多字节,不够装下一个完整的媒体包,但可以装下一个媒体包的一部分。如果不分片,这300多字节就浪费了。
5.4 多路复用的打包策略
什么时候打包、怎么打包,是影响复用效率的关键。常见的打包策略有两种:
①立即打包
有数据就立刻打包发送,不等。这种策略延迟最低,但如果每次只有一个小包,复用效率不高。
②延迟打包
攒一小段时间,等多个包到了一起打包发送。这种策略复用效率高,但会增加一点延迟。
蓝牙音频场景下通常用立即打包策略,因为延迟优先级更高。反正媒体包是按固定间隔来的,每次来一个媒体包,顺便带上攒的报告包或者其他小包,效率也足够。
5.5 多路复用的实际收益
很多人会好奇,多路复用到底能省多少带宽?我们可以算一笔账:
非复用模式下,每个RTP包12字节头,加上L2CAP头4字节,总共16字节开销。一个57字节的SBC帧,总开销16字节,开销占比22%。
复用模式下,假设每个AL-PDU装3个媒体包,AL头每个子包平均1.5字节,L2CAP头4字节分摊到3个包,每个包分摊约1.3字节。总开销约2.8字节,开销占比不到5%。
也就是说,同样的音频码率下,复用模式可以节省将近17%的空中带宽。这在蓝牙这种带宽紧张的环境下,收益是非常可观的。
**但为什么实际产品中开启复用的并不多呢?**主要是因为实现复杂,而且增加了出错的概率。对于单流单会话的场景,开启复用的收益不明显,反而增加了代码复杂度。只有在同时存在多个流、多个会话的场景下,复用的价值才会体现出来。
六、ROHC头压缩传输:进一步压榨带宽
多路复用已经省了不少带宽,但还有一块大肥肉可以啃:RTP头。12字节的RTP头,对于几十字节的音频包来说占比太高了。ROHC健壮头压缩就是专门用来压缩RTP头的。
规范说明:
ROHC shall be used to reduce the overhead of the RTP headers.
这里的ROHC基于RFC3095,但做了针对性的修改,因为AVDTP没有IP层和UDP层,只有RTP头。
6.1 ROHC的基本原理
ROHC的核心思想是:RTP头里的大部分字段,在连续的包之间要么不变,要么是可预测的增量。既然双方都知道变化规律,那就不需要每次都发完整的头,只发变化的部分就行。
发送端和接收端各自维护一个相同的上下文,记录RTP头各个字段的当前值。发送端只发送和上下文有差异的部分,接收端根据上下文和收到的差异,恢复出完整的RTP头。
RTP头中,版本号、P、X、CC、PT、SSRC这些字段,整个流的生命周期都是不变的,只需要在建立上下文的时候发一次。序列号和时间戳是每次固定增量的,也只需要发增量,不需要发完整值。
这样一来,12字节的RTP头,通常可以压缩到1到3字节,压缩率非常高。
6.2 AVDTP对ROHC的修改
标准ROHC是针对IP/UDP/RTP整个协议栈设计的,而AVDTP只有RTP层,所以做了几处修改:
第一,去掉了IP和UDP相关的部分。只压缩RTP头,不压缩IP头和UDP头。CRC计算也只基于RTP头,不包含IP和UDP字段。
第二,不使用上下文ID。因为一个通道只有一个RTP流,不需要CID来区分多个流,默认隐式使用CID=0。
第三,反馈通道可选。标准ROHC需要反馈通道来发送确认和错误通知,AVDTP下可以选择不使用反馈通道,只用单向模式,也可以用同一条通道的反向作为反馈通道。
第四,不使用IP-ID相关的包类型。因为没有IP头,自然也就没有IP-ID字段,所有相关的包类型都不使用。
这些修改都是为了适配蓝牙的传输场景,在保证压缩率的前提下,尽量简化实现。
6.3 ROHC的三种工作模式
ROHC定义了三种工作模式,适用于不同的信道环境:
①U模式 单向模式
不需要反馈,发送方周期性地发送完整的头信息来刷新上下文,接收方自己做上下文同步。这种模式适合广播场景,或者反向通道不可用的场景。优点是简单,不需要反馈;缺点是压缩率低一些,出错之后恢复慢。
②O模式 乐观模式
需要反馈通道。接收方检测到上下文失步的时候,发送NACK请求刷新;正常情况下不发反馈。这种模式压缩率最高,是最常用的模式。AVDTP场景下通常用这种模式。
③R模式 可靠模式
每个状态变化都需要确认。发送方发了新的头信息之后,要等接收方确认才能继续。这种模式最可靠,出错概率最低,但开销最大,延迟也最高。
蓝牙音频场景下,O模式是最佳选择,在压缩率和可靠性之间取得了很好的平衡。
6.4 ROHC的状态机
ROHC的压缩端和解压端都有状态机,根据信道情况在三种状态之间切换:
①压缩端状态
IR状态:发送完整的头信息,建立和刷新上下文
FO状态:发送一阶差分,字段按固定增量变化的时候用
SO状态:发送二阶差分,字段增量也变化的时候用
信道好的时候,压缩端会逐渐进入SO状态,压缩率最高;信道差、丢包多的时候,压缩端会回退到IR状态,多发完整头,保证接收端能同步。
②解压端状态
无上下文状态:还没建立有效的上下文
静态上下文状态:只有静态字段同步了
完整上下文状态:所有字段都同步了
解压端收到的包越多,上下文越完整,解压成功率越高;丢包多了,上下文失步,就会退回到低级状态,请求发送端刷新。
6.5 ROHC的实际收益
我们还是用SBC的例子来算:RTP头12字节,压缩后平均2字节,每个包节省10字节。
一个57字节的SBC帧,原来总开销16字节,开启ROHC之后,RTP头省了10字节,总开销降到6字节,开销占比从22%降到约10%。
如果再加上多路复用,开销可以进一步降到几乎可以忽略的程度。两者叠加,总的带宽节省可以达到20%以上。这对于带宽紧张的经典蓝牙来说,相当于凭空多了五分之一的带宽,可以用来提升音质,或者降低发射功率省电。
七、传输通道管理:底层通道的规则与约束
所有传输层的数据,最终都要通过L2CAP通道发出去。通道本身的配置和管理,对传输质量的影响非常大。很多传输问题,根源都在L2CAP通道的参数配置不对。
7.1 通道建立的顺序规则
建立传输通道有严格的顺序要求,不能乱。原规范给出了明确的优先级:
①媒体传输通道,优先级最高
②报告传输通道,优先级次之
③恢复传输通道,优先级最低
必须按照这个顺序依次建立。不能先建恢复通道再建媒体通道,很多设备会直接拒绝。
**为什么要有顺序要求?**因为媒体通道是必须的,报告和恢复是可选的。按优先级从高到低建立,如果建到一半资源不够了,至少保证媒体通道是通的,基本功能还能用。
拆除通道的顺序正好相反:先拆恢复通道,再拆报告通道,最后拆媒体通道。
7.2 通道MTU要求
每种通道都有最小MTU要求:
信令通道:最小48字节
媒体通道:最小672字节
报告通道:最小48字节
恢复通道:最小48字节
媒体通道的672字节是怎么来的?是根据最大的RTP包大小算出来的。SBC编码最大帧长加上RTP头,再加上一点余量,差不多就是672字节。
如果L2CAP协商出来的MTU小于最小值,AVDTP应该拒绝使用这个通道,终止流建立。因为MTU太小的话,大包会被底层分片,增加延迟和丢包概率。
实际产品中,媒体通道的MTU通常都会设得比最小值大,常见的是1021字节,也就是L2CAP的默认最大MTU。大一点总没坏处,余量充足。
7.3 L2CAP传输模式的选择
L2CAP有多种传输模式:基本模式、重传模式、流控模式、增强重传模式等等。AVDTP的传输通道应该用哪种?
答案是:媒体通道必须用基本模式,不能用重传模式。
为什么?因为重传模式会自动重传丢失的数据包,带来额外的延迟。对于实时音频来说,晚到的包和丢了没区别,重传毫无意义,反而会挤占后续数据包的带宽,造成更大的卡顿。
信令通道和报告通道可以考虑用重传模式,因为它们对可靠性要求高,对延迟不敏感。但实际实现中,为了简化,通常所有通道都用基本模式,信令的可靠性靠AVDTP自己的重传机制来保证。
7.4 流控与流量整形
虽然L2CAP基本模式没有流控,但发送端不能想发多快就发多快,必须做流量整形。
蓝牙的空中带宽是有限的,经典蓝牙2Mbps的速率,实际有效吞吐量也就1Mbps左右。如果发送端一下子把数据全部推下去,底层缓冲区会溢出,造成大量丢包。
正确的做法是按照音频的实际码率,均匀地发送数据包。比如328kbps的SBC立体声,算上协议头大概400kbps,那就按照这个速率均匀地把数据发出去,不要突发。
流量整形是调优蓝牙音频延迟和稳定性的关键。发送节奏控制得好,延迟可以降很多,卡顿也会明显减少。
八、传输层与上层接口的对应关系
规范附录A定义的上层接口原语中,和传输层直接相关的有以下几个:
|-------------------------|---------------------------|
| 上层原语 | 对应传输层功能 |
| AVDT_Write_Req | 发送媒体数据,由流管理器封装成RTP包后发送 |
| AVDT_ReadStreamData | 接收媒体数据,从抖动缓冲区取出完整的帧交给上层 |
| AVDT_Event_Registration | 注册QoS事件,RTCP报告到达时通过事件通知上层 |
| AVDT_Delay_Report_Ind | 延迟报告事件,和传输延迟计算相关 |
上层应用只需要调用Write和Read这两个接口,就能完成媒体数据的收发。所有的RTP封装、FEC处理、复用压缩、抖动缓冲,全部在传输层内部自动完成,上层完全不需要关心。
这种分层设计大大降低了上层应用的开发难度。上层只需要专注于音频的编解码和业务逻辑,不需要了解传输层的复杂细节。
九、测验
问题:RTP时间戳的含义是什么?它和发送时间有什么区别?时间戳的单位由什么决定?
答案:
RTP时间戳记录的是数据包中第一个采样点的采样时刻,而不是数据包的发送时刻或接收时刻。它的单位由媒体的采样率决定,例如44.1kHz采样率下,时间戳加1对应1/44100秒。
时间戳和发送时间的区别在于:发送时间会受到系统调度、网络拥塞等因素影响,是波动的;而时间戳是按照采样时钟均匀递增的,是稳定的。接收端正是利用时间戳的稳定性来还原播放时序,抵消传输抖动的影响。如果错误地用发送时间作为时间戳,会导致接收端无法正确做抖动缓冲,产生严重的卡顿和爆音。
问题:AVDTP的FEC前向纠错机制和重传机制相比,有什么优缺点?为什么AVDTP选择FEC而不是重传?
答案:
FEC的优点:不需要反向通道请求重传,不会引入额外的延迟;可以在接收端立刻恢复丢包,实时性好。
**缺点:**需要额外的带宽开销,发送和接收都需要额外的计算资源;只能恢复一定比例和数量的丢包,丢包严重时无法恢复。
重传的优点:带宽开销小,只在丢包的时候才重传;丢包率很低的时候效率高。
**缺点:**需要往返交互,会引入显著的额外延迟,重传的包到达时已经错过了播放时间。
AVDTP面向实时音视频传输,对延迟非常敏感,重传带来的延迟是不可接受的,因此选择FEC作为丢包恢复机制,用带宽开销换取低延迟。
问题:AVDTP适配层的多路复用机制是如何工作的?它带来了什么收益?
答案:
多路复用机制允许多个传输会话(媒体、报告、恢复)的数据包共享同一个L2CAP传输通道。每个子包带有1到2字节的适配层头,包含TSID标识所属会话、分片标志和长度信息。接收端根据TSID分发给对应的会话处理,支持大包分片和重组。
主要收益:第一,减少L2CAP通道的数量,降低通道维护开销和调度开销;第二,将多个小包打包成一个大包发送,降低协议头的占比,显著提高带宽利用率,单流场景下可减少约15%的传输开销,多流场景收益更明显;第三,可以灵活利用每个L2CAP包的剩余空间,减少带宽浪费。