做蓝牙音频开发的同学都知道,两台设备建立A2DP连接的第一步,不是直接传输音频数据,而是先通过AVDTP信令通道完成一轮完整的摸底交互:先找到对方有哪些可用的音视频流端点,再逐个查询每个端点支持的编码格式、保护机制等核心能力,最后才能选中匹配的端点完成配置与建流。这几步交互看似流程简单,实则是整个音频链路的基石,市面上绝大多数的兼容性问题、播放无声问题、配对后连接失败问题,追根溯源都能在这几步信令交互里找到根因。
目录
[1.1 命令帧:极简设计的2字节请求](#1.1 命令帧:极简设计的2字节请求)
[1.2 响应帧:紧凑排列的SEP信息列表](#1.2 响应帧:紧凑排列的SEP信息列表)
[1.3 实战:逐字节拆解真实抓包响应](#1.3 实战:逐字节拆解真实抓包响应)
[1.4 开发中最容易踩的两个坑](#1.4 开发中最容易踩的两个坑)
二、GET_CAPABILITIES:深挖单个端点的核心能力
[2.1 命令帧:指定目标端点地址](#2.1 命令帧:指定目标端点地址)
[2.2 响应帧:TLV格式的可扩展能力列表](#2.2 响应帧:TLV格式的可扩展能力列表)
[2.3 核心能力详解:以SBC编码为例](#2.3 核心能力详解:以SBC编码为例)
三、GET_ALL_CAPABILITIES:全量能力的完整画像
[3.1 两个能力查询的核心区别](#3.1 两个能力查询的核心区别)
[3.2 帧格式与实现差异](#3.2 帧格式与实现差异)
[3.3 场景选择的实用原则](#3.3 场景选择的实用原则)
[3.4 实战:全量能力响应报文逐段拆解](#3.4 实战:全量能力响应报文逐段拆解)
[4.1 完整交互时序](#4.1 完整交互时序)
[4.2 四类常见问题与排查思路](#4.2 四类常见问题与排查思路)
[4.3 抓包调试的核心技巧](#4.3 抓包调试的核心技巧)
本文把端点发现、能力查询这两组核心信令彻底讲透,从帧格式的每一个比特位定义,到真实抓包的逐字节拆解,再到工程开发里的常见坑点与调试技巧,一次性梳理成完整的知识体系,看完既能应对面试,也能直接解决工作里的实际问题。
一、DISCOVER信令:先摸清对端的流端点家底
就像走进一家陌生的餐厅,坐下第一件事不是直接点菜,而是先了解店里有哪些菜系、每个菜系的基本定位。DISCOVER命令就是发起端抛出的第一个问题:你这边一共有多少个可用的流端点,每个端点是发送数据还是接收数据,属于音频还是视频类型。
这是所有AVDTP交互的起点,也是两台设备第一次交换音视频相关的信息。只有完成了发现流程,后续的能力查询、配置、建流等操作才有明确的寻址目标。
1.1 命令帧:极简设计的2字节请求
DISCOVER是所有AVDTP信令里结构最简单的命令,整条命令除了固定的2字节信令头,没有任何额外的载荷参数。
信令头的通用格式我们在之前的文章里讲过,第一字节由事务标签、包类型、消息类型三部分组成,第二字节由信号标识符和保留位组成。对于DISCOVER命令来说,消息类型为Command,信号标识符固定为0x01。

规范中明确说明:
The DISCOVER command is used to discover the stream endpoints of the ACP device. The command has no payload.
这种极简设计有非常明确的工程考量。发现阶段是连接建立的第一步,此时发起端对接收端的能力一无所知,不需要也没办法传递更多参数。越简单的帧格式,出现解析错误的概率就越低,即使不同厂商的协议栈实现有细微差异,也能正确解析这条基础命令,最大程度保证基础交互的成功率。
也正因为命令没有载荷,DISCOVER命令的总长度固定为2字节,在L2CAP层就能一眼识别出来,抓包分析的时候非常好定位。
1.2 响应帧:紧凑排列的SEP信息列表
DISCOVER的响应载荷是一串连续的流端点信息条目,每个条目固定占用2字节,设备有多少个流端点,响应里就会有多少个条目。规范对这2字节的比特位做了非常紧凑的定义,在极小的空间里塞下了端点编号、占用状态、端点类型、媒体类型四个核心信息。

规范中对SEP信息条目的定义如下:
Each SEP information element consists of 2 octets. The first octet contains the SEID and the In Use flag. The second octet contains the Media Type and the TSEP flag.
我们用表格把每一位的定义拆解清楚,方便对照查阅:
表1 DISCOVER响应SEP条目比特分布
|------|----|----|----|-----------------|----|----|------|--------|
| 字节序号 | 位7 | 位6 | 位5 | 位4 | 位3 | 位2 | 位1 | 位0 |
| 字节0 | 保留 | | | SEID(共6位) | | | | In Use |
| 字节1 | | | | Media Type(共6位) | | | TSEP | 保留 |
下面逐个解释每个字段的含义:
SEID:全称Stream Endpoint ID,流端点标识符,由接收端分配,是每个流端点的唯一编号。6位的长度决定了SEID的合法取值范围是1到63,0和63为协议保留值,不允许分配给实际端点。
In Use:占用标志位,0表示该端点当前处于空闲状态,可以被配置使用;1表示该端点已经被其他流占用,暂时不可用。
Media Type:媒体类型,6位长度,0代表音频,1代表视频,2代表多媒体,其余取值为保留值。
TSEP:端点类型标志位,0代表Source源端,也就是输出音视频数据的一方;1代表Sink宿端,也就是接收并播放音视频数据的一方。
所有标注为保留的位,发送时必须置为0,接收端处理时应当忽略这些位的取值。
这个设计是协议里空间换效率的经典案例。6位SEID足够覆盖绝大多数设备的端点数量,普通蓝牙耳机一般只有2-3个音频端点,即使是复杂的车载影音设备也很少超过几十个,6位的寻址空间完全够用。用2字节承载四个核心信息,既保证了功能完整,又把空口传输开销降到了最低。
1.3 实战:逐字节拆解真实抓包响应
以真实抓包的DISCOVER完整交互为例,从原始十六进制数据逐层拆解,对照协议定义验证每一个字段。
1.3.1 命令包快速校验

剥离HCI与L2CAP层封装后,AVDTP有效命令载荷为40 01:
0x40:高4位为事务标签4,中间2位标识单包类型,低2位为Command命令类型
0x01:信号标识符为AVDTP_DISCOVER,无额外载荷,符合规范定义
1.3.2 响应包逐层拆解
解析L2CAP帧头后,AVDTP响应载荷共8字节:前2字节为信令固定头部,后6字节为3个SEP信息条目,每个条目固定占2字节。

(1)信令头 42 01
-
0x42:事务标签与请求一致为4,单包类型,消息类型为Response Accept接受响应
-
0x01:信号标识符匹配DISCOVER命令,完成请求与响应的一一对应
(2)SEP条目逐一枚举
每个SEP条目的第一字节承载SEID编号与占用状态,第二字节承载媒体类型与端点角色。
①SEP1(对应字节 04 00 )
SEID编号为1,当前空闲未占用;媒体类型为Audio,端点角色为Source源端
②SEP2(对应字节 08 00 )
SEID编号为2,当前空闲未占用;媒体类型为Audio,端点角色为Source源端
③SEP3(对应字节 0C 08 )
SEID编号为3,当前空闲未占用;媒体类型为Audio,端点角色为Sink宿端
拆解结果与抓包工具解析完全吻合:该设备共暴露3个空闲音频端点,前两个为音频源,第三个为音频宿。常规音乐播放场景下,发起端会选择SEID=3的Sink端点,开展后续能力查询与流配置操作。
1.4 开发中最容易踩的两个坑
这部分看起来简单,但实际开发里踩坑的人不在少数,甚至很多商用协议栈都出过相关问题。
第一个坑是SEID的合法范围。规范明确规定SEID取值必须在1到63之间,0和63是保留值,不能使用。但有些厂商的实现不规范,把SEID设为0,发起端收到后参数校验不通过,直接返回错误,导致发现流程直接失败。调试的时候如果遇到DISCOVER失败的情况,先核对对端返回的SEID是否在合法区间,往往能快速定位问题。
第二个坑是In Use标志的语义理解。这个标志位只代表响应发送的那一刻,端点是否被占用,是一个瞬时状态。等你后续发送配置命令的时候,端点可能已经被其他流占用了。所以配置阶段还会再做一次状态校验,不能认为发现阶段显示空闲,就一定能配置成功。
二、GET_CAPABILITIES:深挖单个端点的核心能力
拿到端点列表之后,下一步就要针对具体的目标端点,查询详细的服务能力。就像你知道餐厅有川菜菜系,接下来就要问清楚川菜里有哪些具体菜品、支持什么口味、有什么忌口。GET_CAPABILITIES命令就是针对指定的SEID,查询该端点支持的服务能力列表。
这一步是整个建流流程里的关键环节,后续的配置参数必须从双方共同支持的能力里选取,能力查询的结果直接决定了音频的编码格式、音质上限和功能特性。
2.1 命令帧:指定目标端点地址
GET_CAPABILITIES的命令载荷非常简单,只有1字节,用来指定要查询的流端点SEID。
命令的信令头之后,跟着1字节的SEID字段,格式和DISCOVER响应里的SEID定义完全一致:高6位是SEID的有效数值,低2位为保留位,必须置0。

规范中的定义很直接:
The GET_CAPABILITIES command requests the service capabilities of the addressed SEP. The command payload contains the SEID of the SEP to be addressed.
这里有一个非常容易忽略的细节,很多新手甚至有经验的开发者都踩过坑:SEID是放在高6位的,不是直接放在整个字节里。比如SEID=1,正确的写法是左移1位得到0x02,而不是直接写0x01。
虽然大部分协议栈实现都做了兼容,即使格式不对也能解析出正确的SEID,但严格来说直接填写数值是不符合规范的。遇到校验逻辑严格的设备,就会出现参数错误,导致能力查询失败。开发的时候严格按照规范的比特位来写,能避免很多隐性的兼容性问题。
2.2 响应帧:TLV格式的可扩展能力列表
GET_CAPABILITIES的响应载荷是一组连续的服务能力条目,每个条目采用经典的TLV结构,也就是Type-Length-Value,类型、长度、值三段式组织。这是通信协议设计里非常经典的可扩展结构,后续新增能力类型时,不需要修改整体帧格式,只需要新增对应的类型码即可,前后向兼容性非常好。

表2 服务能力TLV条目结构
|---------|------------------|--------|-----------------------|
| 偏移量 | 字段名称 | 长度 | 说明 |
| 0 | Service Category | 1字节 | 服务类别,标识能力的类型 |
| 1 | Length | 1字节 | 能力值部分的字节长度 |
| 2 | Value | N字节 | 具体的能力参数,长度由Length字段决定 |
规范中定义了多种标准的服务类别,最常用的有以下几种:
Media Transport:类别码0x00,媒体传输能力,所有端点必选,定义了媒体数据包的基本传输格式。
Reporting:类别码0x01,报告能力,可选,支持延迟报告、数据包统计等功能。
Media Codec:类别码0x02,媒体编解码能力,所有端点必选,定义了支持的编码类型和具体的编码参数范围。
Content Protection:类别码0x03,内容保护能力,可选,比如SCMS-T、各类DRM保护机制。
Header Compression:类别码0x04,头压缩能力,可选,支持ROHC等IP头压缩算法。
Multiplexing:类别码0x05,多路复用能力,可选,支持多个流复用同一个传输通道。
其中Media Transport和Media Codec是每个流端点必须支持的能力,如果响应里缺少这两个必选项,说明对端的协议栈实现有缺陷,后续的配置流程必然会失败。
2.3 核心能力详解:以SBC编码为例
我们拿最通用的SBC编码举例,看看Media Codec能力的具体内部结构,更直观地理解TLV的Value部分是如何组织的。
Media Codec的Service Category为0x02,对于音频SBC编码来说,Length通常为4字节,Value部分的结构依次为:
第一字节:Media Type媒体类型,0代表音频
第二字节:Media Codec Type编码类型,0代表SBC
第三字节:SBC编码参数第一部分,包含采样频率、声道模式
第四字节:SBC编码参数第二部分,包含块长度、子带数量、分配方式
第五、六字节:可选,最小比特池和最大比特池范围
每一个比特位都对应SBC编码的一个配置项,位掩码的方式可以同时表示支持多种配置。比如采样频率字段,位0代表16kHz,位1代表32kHz,位2代表44.1kHz,位3代表48kHz,如果同时支持44.1kHz和48kHz,该字段的值就是0x0C。
下面给出一段C语言代码示例,演示如何解析SBC的编码能力参数,实际开发中可以直接参考使用:
cpp
typedef struct {
uint8_t sampling_freq_mask; // 支持的采样频率位掩码
uint8_t channel_mode_mask; // 支持的声道模式位掩码
uint8_t block_len_mask; // 支持的块长度位掩码
uint8_t subbands_mask; // 支持的子带数量位掩码
uint8_t alloc_method_mask; // 支持的分配方式位掩码
uint8_t min_bitpool; // 最小比特池值
uint8_t max_bitpool; // 最大比特池值
} sbc_capability_t;
int avdtp_parse_sbc_cap(const uint8_t *cap_data, uint8_t cap_len, sbc_capability_t *out_cap)
{
// 长度校验,至少需要媒体类型+编码类型+2字节参数
if (cap_data == NULL || out_cap == NULL || cap_len < 4) {
return -1;
}
// 跳过前两字节的媒体类型和编码类型
const uint8_t *param = cap_data + 2;
uint8_t param_len = cap_len - 2;
// 解析第一参数字节:高4位采样频率,低4位声道模式
out_cap->sampling_freq_mask = (param[0] >> 4) & 0x0F;
out_cap->channel_mode_mask = param[0] & 0x0F;
// 解析第二参数字节:高4位块长度,中2位子带,低2位分配方式
out_cap->block_len_mask = (param[1] >> 4) & 0x0F;
out_cap->subbands_mask = (param[1] >> 2) & 0x03;
out_cap->alloc_method_mask = param[1] & 0x03;
// 解析比特池范围,长度足够时才读取
if (param_len >= 4) {
out_cap->min_bitpool = param[2];
out_cap->max_bitpool = param[3];
} else {
out_cap->min_bitpool = 2;
out_cap->max_bitpool = 53; // SBC默认最大值
}
return 0;
}
实际开发中,我们拿到对端的能力参数后,会和本地支持的参数做按位与运算,求出双方共同支持的配置集合,再从中选出最优的配置项,填入后续的Set Configuration命令中下发给对端。能力解析的正确性,直接决定了最终音频配置是否合理。
三、GET_ALL_CAPABILITIES:全量能力的完整画像
很多开发者都会有疑问,既然已经有了GET_CAPABILITIES,为什么还要多一个GET_ALL_CAPABILITIES?两者看起来功能高度相似,难道是规范的冗余设计?
当然不是。这两个命令的定位有非常明确的区分,对应不同的使用场景,只是因为格式高度相似,才容易被混淆。
3.1 两个能力查询的核心区别
我们先看规范中对两者的定义差异:
GET_CAPABILITIES returns the service capabilities that are currently applicable for the SEP.
GET_ALL_CAPABILITIES returns all service capabilities supported by the SEP, including those that may be optionally enabled.
简单来说,GET_CAPABILITIES返回的是端点当前可用的、默认生效的基础能力;而GET_ALL_CAPABILITIES返回的是这个端点硬件和固件全部支持的能力,包括那些需要额外配置才能启用的可选功能。
举个实际的例子帮助理解:一款耳机硬件支持SCMS-T内容保护功能,但默认状态下是关闭的。此时发送GET_CAPABILITIES命令,响应里不会包含Content Protection能力条目,因为当前并没有启用;但发送GET_ALL_CAPABILITIES命令,响应里就会带上这个能力条目,告知发起端该端点硬件支持这个功能,只是当前未生效。如果业务需要开启内容保护,就可以在后续的配置命令中带上对应的参数,启用该功能。
这种设计的核心目的,是简化常规场景的交互开销。绝大多数普通音频播放场景,只需要基础的编码和传输能力,使用GET_CAPABILITIES就能拿到全部需要的信息,响应包更小,解析速度更快。只有当需要使用高级功能,或者需要全面评估对端能力的时候,才需要调用GET_ALL_CAPABILITIES获取全量信息。
3.2 帧格式与实现差异
从帧格式上来说,两个命令的结构完全一致,都是1字节的SEID作为命令载荷。响应的格式也完全相同,都是TLV结构的能力列表。唯一的区别就是响应中包含的能力条目数量不同,GET_ALL_CAPABILITIES的响应通常会更长,包含更多可选的能力项。
也正因为格式高度相似,很多协议栈的实现里,这两个命令的处理函数是共用的,只是返回的能力列表不同。还有一些简化实现,直接让两个命令返回完全相同的内容。这种做法虽然不符合规范的设计初衷,但在绝大多数普通场景下也能正常工作,这也是很多开发者用了很久都没注意到两者区别的原因。
3.3 场景选择的实用原则
实际开发中,可以遵循这个原则来选择使用哪个命令:
如果只是实现普通的音频播放功能,只用到基础的编码和传输能力,使用GET_CAPABILITIES就足够了,交互更轻量,也能减少对端的处理压力。
如果需要实现高级功能,比如内容保护、低延迟模式、多路复用,或者做设备兼容性适配,需要全面了解对端的能力边界,就使用GET_ALL_CAPABILITIES。
调试排查问题的时候,优先抓取GET_ALL_CAPABILITIES的响应,这样能看到对端的全部能力集合,更容易定位配置不匹配的问题。
3.4 实战:全量能力响应报文逐段拆解
以真实抓包中SEID=3的端点查询为例,逐段拆解GET_ALL_CAPABILITIES的交互报文,直观理解TLV格式的组织逻辑。
(1)命令帧拆解

-
0x50:高4位为事务标签5,中间2位标识单包类型,低2位为Command命令类型
-
0x0C:信号标识符对应AVDTP_GET_ALL_CAPABILITIES
-
0x06:高6位对应目标SEID=3,低2位为保留位,指定查询3号端点的全量能力
(2)响应帧拆解
前2字节为信令固定头部:0x52表示事务标签5、单包类型、Response Accept接受响应;0x0C与请求信号标识符匹配。后续12字节为3个TLV结构的能力条目:

①Media Transport 媒体传输
服务类别0x00,长度0,无额外参数,是所有流端点的必选基础能力。
②Media Codec 媒体编解码(SBC)
服务类别0x02,长度6,载荷共6字节:
前2字节:媒体类型为Audio,编码类型为SBC
第3字节0x3F:支持44.1kHz/48kHz采样率,全量支持单声道/双声道/立体声/联合立体声
第4字节0xFF:块长度、子带数量、分配方式全档位支持
后2字节:最小比特池为2,最大比特池为53,符合SBC标准取值范围
③Delay Reporting 延迟报告
服务类别0x01,长度0,无额外参数,属于扩展可选能力,支持延迟统计与上报。
拆解结果与抓包完全吻合:该Sink端点除了基础传输与SBC编码能力外,还额外支持延迟报告功能。这部分扩展能力在基础GET_CAPABILITIES查询中不会返回,也是全量查询的核心价值所在。
四、完整交互流程与工程调试经验
讲完了单个信令的细节,我们把整个发现到查询的流程串起来,形成完整的时序认知,再分享一些实际工程里调试这类问题的实用经验。
4.1 完整交互时序
整个建流前的准备流程,完整的步骤如下:
发起端与接收端先建立L2CAP信令通道,PSM值为0x0019,这是AVDTP信令的专用通道。
发起端发送DISCOVER命令,查询接收端所有的流端点信息。
接收端返回DISCOVER接受响应,枚举所有SEP的基本信息。
发起端从端点列表中筛选出符合需求的目标SEP,比如音频Sink端点。
发起端发送GET_CAPABILITIES命令,查询该端点的基础服务能力。
接收端返回能力响应,包含必选的传输能力和编解码能力。
可选步骤:发起端发送GET_ALL_CAPABILITIES命令,查询全量能力。
发起端根据双方能力做交集运算,生成最优的配置参数。
流程进入后续的Set Configuration配置阶段。
这几步是所有A2DP连接的必经之路,任何一步出错,后续的音频传输都无法建立。排查音频连接问题的时候,第一反应就应该是抓包看这几步有没有正常完成,哪一步返回了错误响应,问题基本就出在对应的环节。
4.2 四类常见问题与排查思路
实际开发中,这部分最容易遇到的问题可以归为四类,每一类都有对应的快速排查思路。
(1)第一类是DISCOVER失败,对端返回拒绝响应。大概率是信令头格式错误,比如事务标签重复、消息类型字段写错,或者对端协议栈状态机异常。可以先核对信令头的每一位是否符合规范,再检查对端的状态机是否处于正确的空闲状态。
**(2)第二类是编码能力不匹配,配置阶段返回不支持。**比如发起端只支持AAC编码,接收端只支持SBC,两边没有交集,自然无法完成配置。这种情况要么是设备本身硬件不支持,要么是能力解析出错,把支持的编码漏判了。调试的时候可以把原始能力字节打印出来,手动对照规范解析,确认是对端没有返回对应能力,还是本地解析逻辑有误。
**(3)第三类是TLV格式错误,导致能力解析全部错位。**比如某个能力条目的Length字段填错,后面所有的能力条目都会跟着错位,解析出来的类型和参数全是错的。这种问题非常隐蔽,表面看响应正常返回,但解析结果完全不对。遇到解析出来的能力类型很离谱的情况,先逐个核对每个TLV条目的长度是否正确,大概率是长度字段填写错误。
**(4)第四类是SEID不匹配,发现阶段返回的SEID,后续查询能力时提示端点不存在。**这种通常有两个原因,一是端点状态动态变化,发现后端点被销毁了;二是SEID格式传错了,比如没有左移1位,导致对端解析出来的SEID和实际的对不上。
4.3 抓包调试的核心技巧
最后分享一个非常实用的调试技巧。分析AVDTP抓包的时候,不要只依赖抓包工具解析出来的树形结构,一定要多看原始的十六进制字节。
很多时候抓包工具的解析逻辑会有bug,或者对一些厂商自定义的能力类型解析错误,很容易误导判断。自己对照规范逐字节拆解,虽然速度慢一点,但永远是最准确的方法。尤其是遇到跨厂商的兼容性问题,两边都声称自己符合规范,但就是连不上的时候,逐字节比对规范是唯一的排查路径。绝大多数情况下,总能找到某一方的字段有细微的不规范之处,就是这点细微的差异,导致了整个交互流程的失败。
做蓝牙协议开发,字节级的分析能力是基本功,也是区分普通开发和资深开发的核心标志。
五、测验
问题:AVDTP的DISCOVER响应中,SEID的合法取值范围是多少?每个SEP信息条目占用几个字节,分别包含哪些核心信息?
答案:
SEID由6个比特位表示,合法取值范围是1到63,0和63为协议保留值,不可使用。
每个SEP信息条目固定占用2字节。第一字节包含6位SEID编号和1位In Use占用标志,最高位为保留位;第二字节包含6位Media Type媒体类型和1位TSEP端点类型标志,最低位为保留位。
问题:AVDTP中GET_CAPABILITIES与GET_ALL_CAPABILITIES两个命令有什么核心区别?分别适用于什么场景?
答案:
核心区别在于返回的能力范围不同。GET_CAPABILITIES返回端点当前可用、默认生效的基础服务能力;GET_ALL_CAPABILITIES返回端点硬件与固件支持的全部服务能力,包括需要额外配置才能启用的可选扩展能力。
适用场景:普通音频播放场景使用GET_CAPABILITIES即可,交互更轻量;需要使用内容保护、延迟报告等高级功能,或做全量设备兼容性评估时,使用GET_ALL_CAPABILITIES。
问题:AVDTP的服务能力条目采用什么格式组织?请列举至少3种标准的服务类别。
答案:
采用TLV即Type-Length-Value格式组织,每个能力条目依次包含1字节服务类别、1字节长度、N字节具体参数。
标准服务类别包括:媒体传输Media Transport、媒体编解码Media Codec、内容保护Content Protection、报告能力Reporting、头压缩Header Compression等。