做蓝牙音频开发这么多年,我见过最多的问题都出在信令阶段。手机连不上耳机、连上了没声音、只有单声道、切歌卡顿、暂停后无法恢复......90%的兼容性问题,本质上都是信令流程没有严格按照规范执行导致的。
目录
[1.1 信令包的基本格式](#1.1 信令包的基本格式)
[1.2 信令事务机制](#1.2 信令事务机制)
[1.3 响应类型](#1.3 响应类型)
[1.4 错误码](#1.4 错误码)
[2.1 建立L2CAP信令通道](#2.1 建立L2CAP信令通道)
[2.2 流端点发现 (Discover)](#2.2 流端点发现 (Discover))
[2.3 获取流端点能力 (Get Capabilities / Get All Capabilities)](#2.3 获取流端点能力 (Get Capabilities / Get All Capabilities))
[2.4 流配置 (Set Configuration)](#2.4 流配置 (Set Configuration))
[2.5 建立传输通道](#2.5 建立传输通道)
[2.6 打开流 (Open)](#2.6 打开流 (Open))
[2.7 启动流 (Start)](#2.7 启动流 (Start))
[2.8 媒体数据传输](#2.8 媒体数据传输)
[2.9 暂停流 (Suspend)](#2.9 暂停流 (Suspend))
[2.10 关闭流 (Close)](#2.10 关闭流 (Close))
[3.1 流重配置 (Reconfigure)](#3.1 流重配置 (Reconfigure))
[3.2 延迟报告 (Delay Report)](#3.2 延迟报告 (Delay Report))
[3.3 通用拒绝 (General Reject)](#3.3 通用拒绝 (General Reject))
[3.4 流强制终止 (Abort)](#3.4 流强制终止 (Abort))
[3.5 安全控制 (Security Control)](#3.5 安全控制 (Security Control))
[4.1 信令超时](#4.1 信令超时)
[4.2 Set Configuration失败](#4.2 Set Configuration失败)
[4.3 流启动后没有声音](#4.3 流启动后没有声音)
[4.4 空中抓包分析技巧](#4.4 空中抓包分析技巧)
很多开发者对信令流程的理解只停留在**"先发现,再配置,然后启动"**这个模糊的层面,不知道每个命令背后的含义,不知道参数应该怎么填,更不知道对方返回错误的时候应该怎么处理。直到遇到某款特定的设备出问题,才不得不去翻规范,一点点抠细节。
其实AVDTP的信令体系设计得非常严谨,每个命令、每个参数、每个响应都有明确的规定。只要真正理解了这些规定,绝大多数兼容性问题都可以在开发阶段避免。本文就把AVDTP的所有信令流程掰开揉碎了讲,从最基础的信令包格式开始,到完整的流建立、使用、释放流程,再到高级的重配置和延迟报告,一个细节都不放过。
一、AVDTP信令体系基础
在讲解具体的信令流程之前,我们首先要搞清楚AVDTP信令的基本规则。这些规则是所有信令流程的基础,也是最容易出错的地方。
1.1 信令包的基本格式
所有的AVDTP信令包都遵循相同的基本格式,长度为3字节加上可选的有效载荷。
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Transaction | P | Message | Signal ID | Number|
| Label | a | Type | |of Addr|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
~ Signal Parameters ~
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
各个字段的详细含义:
Transaction Label (事务标签):4位,用于唯一标识一个信令事务。每个未完成的事务都有一个唯一的事务标签,响应必须使用和命令相同的事务标签。
Packet Type (包类型):1位,0表示这是一个命令包,1表示这是一个响应包。
Message Type (消息类型):3位,指定响应的类型,命令包中这个字段保留为0。
Signal ID (信号ID):6位,指定具体的信令命令,比如Discover、Set Configuration等。
Number of Addresses (地址数量):2位,指定信令包中包含的SEID数量。0表示没有SEID,1表示一个SEID,2表示两个SEID。
Signal Parameters (信号参数):可变长度,包含命令或响应的具体参数。
这里有几个非常重要的细节,很多bug都出在这里:
-
事务标签的分配规则:事务标签由命令的发起方分配,范围是0x01到0x0F。0x00是保留值,不能使用。发起方必须保证在同一时间,同一个连接上的所有未完成事务的事务标签都是唯一的。
-
响应的事务标签:响应必须使用和对应的命令完全相同的事务标签。如果响应的事务标签不匹配,接收方必须丢弃这个响应。
-
地址数量字段:这个字段的值必须和信令包中实际包含的SEID数量一致。很多开发者会忽略这个字段,随便填一个值,导致对方无法正确解析信令包。
1.2 信令事务机制
AVDTP采用了基于事务的信令模型。每个信令交互都是一个独立的事务,由一个命令和一个或多个响应组成。
规范明确指出:
Each signaling procedure consists of a command message sent by the INT and a corresponding response message sent by the ACP.
事务的生命周期:
-
发起方分配一个未使用的事务标签
-
发起方发送命令包,包含分配的事务标签
-
发起方启动事务超时定时器
-
接收方收到命令包,处理命令
-
接收方发送响应包,使用相同的事务标签
-
发起方收到响应包,停止超时定时器
-
事务结束,事务标签被释放,可以被重新使用

如果在超时时间内没有收到响应,发起方应该重传命令。规范规定最多重传3次,如果3次重传都失败,应该终止事务并通知上层应用。
超时时间的默认值是3秒,但不同的蓝牙规范可能会有不同的要求。例如,HFP规范要求超时时间是10秒,而A2DP规范要求是3秒。
1.3 响应类型
AVDTP定义了四种响应类型,分别对应不同的处理结果:
|--------------------------------|-------|-------------------|
| 响应类型 | 值 | 含义 |
| AVDTP_RESPONSE_ACCEPT | 0x00 | 命令被成功接受和执行 |
| AVDTP_RESPONSE_REJECT | 0x01 | 命令被拒绝 |
| AVDTP_RESPONSE_PENDING | 0x02 | 命令正在处理中,稍后会发送最终响应 |
| AVDTP_RESPONSE_NOT_IMPLEMENTED | 0x03 | 命令未实现 |
这四种响应类型的使用规则:
Accept:表示命令被成功执行,是最常见的响应类型。
Reject:表示命令被拒绝,响应中必须包含一个错误码,说明拒绝的原因。
Pending:表示命令需要较长时间处理,接收方会在处理完成后发送另一个最终响应(Accept或Reject)。一个事务最多只能有一个Pending响应。
Not Implemented:表示接收方不支持这个命令。
这里有一个非常容易被忽略的细节:对于一个命令,接收方必须且只能发送一个最终响应(Accept或Reject)。Pending响应不是最终响应,它只是一个中间状态。
很多开发者在实现的时候,要么忘记发送最终响应,要么发送多个最终响应,导致发起方的事务一直处于等待状态,最终超时失败。
1.4 错误码
当命令被拒绝时,响应中必须包含一个错误码,说明拒绝的原因。AVDTP定义了以下错误码:
|--------------------------------------------|-------|-------------|
| 错误码 | 值 | 含义 |
| AVDTP_ERROR_BAD_HEADER_FORMAT | 0x01 | 信令头格式错误 |
| AVDTP_ERROR_BAD_LENGTH | 0x02 | 信令包长度错误 |
| AVDTP_ERROR_INVALID_ACP_SEID | 0x03 | 无效的ACP SEID |
| AVDTP_ERROR_SEID_IN_USE | 0x04 | SEID正在使用中 |
| AVDTP_ERROR_SEID_NOT_IN_USE | 0x05 | SEID未在使用中 |
| AVDTP_ERROR_INVALID_RFA | 0x06 | RFA字段使用了无效值 |
| AVDTP_ERROR_UNSUPPORTED_SERVICE_CATEGORY | 0x07 | 不支持的服务类别 |
| AVDTP_ERROR_INVALID_PAYLOAD_FORMAT | 0x08 | 有效载荷格式错误 |
| AVDTP_ERROR_NOT_SUPPORTED_COMMAND | 0x09 | 不支持的命令 |
| AVDTP_ERROR_INVALID_CAPABILITIES | 0x0A | 无效的能力 |
| AVDTP_ERROR_CONFIRMATION_REJECTED | 0x0B | 确认被拒绝 |
| AVDTP_ERROR_INVALID_TRANSACTION_LABEL | 0x0C | 无效的事务标签 |
| AVDTP_ERROR_INVALID_TCID | 0x0D | 无效的TCID |
| AVDTP_ERROR_TCID_IN_USE | 0x0E | TCID正在使用中 |
| AVDTP_ERROR_INVALID_TSID | 0x0F | 无效的TSID |
| AVDTP_ERROR_DUPLICATE_OPERATION | 0x10 | 重复操作 |
| AVDTP_ERROR_INVALID_STATE | 0x11 | 无效的状态 |
| AVDTP_ERROR_INVALID_CODEC_TYPE | 0x12 | 无效的编解码类型 |
| AVDTP_ERROR_INVALID_RECOVERY_TYPE | 0x13 | 无效的恢复类型 |
| AVDTP_ERROR_INVALID_MEDIA_TRANSPORT_FORMAT | 0x14 | 无效的媒体传输格式 |
| AVDTP_ERROR_INVALID_REPORTING_FORMAT | 0x15 | 无效的报告格式 |
| AVDTP_ERROR_INVALID_MULTIPLEXING_MODE | 0x16 | 无效的多路复用模式 |
| AVDTP_ERROR_INVALID_ROHC_MODE | 0x17 | 无效的ROHC模式 |
| AVDTP_ERROR_INVALID_CP_TYPE | 0x18 | 无效的内容保护类型 |
| AVDTP_ERROR_INVALID_DELAY | 0x19 | 无效的延迟值 |
正确的错误处理是保证兼容性的关键。当收到一个错误响应时,发起方应该根据错误码采取相应的措施,而不是简单地终止整个连接。
例如,如果收到SEID_IN_USE错误,发起方应该尝试使用另一个SEID,而不是直接断开连接。如果收到UNSUPPORTED_SERVICE_CATEGORY错误,发起方应该尝试不使用这个服务类别,而不是终止连接。
二、核心信令流程详解
现在我们开始讲解AVDTP的核心信令流程。这些流程是所有蓝牙音频应用的基础,每个开发者都必须烂熟于心。

我们以手机连接蓝牙耳机播放音乐为例,完整的信令流程包括以下12个步骤:
建立L2CAP信令通道
流端点发现
获取流端点能力
流配置
建立媒体传输通道
建立报告传输通道(可选)
建立恢复传输通道(可选)
打开流
启动流
传输媒体数据
暂停流(可选)
关闭流
下面我们逐一详细讲解每个步骤。
2.1 建立L2CAP信令通道
在进行任何AVDTP信令交互之前,首先需要建立一个L2CAP信令通道。AVDTP的信令通道使用固定的PSM值0x0019。

信令通道是双向的,一旦建立,双方都可以发起信令命令。信令通道在整个连接的生命周期内都存在,所有的流共享同一个信令通道。
这里有一个非常重要的细节:信令通道的MTU至少应该是48字节。如果L2CAP层协商的MTU小于48字节,AVDTP应该拒绝使用这个通道。
规范明确指出:The minimum MTU for the signaling channel shall be 48 octets.
这是因为AVDTP的某些信令包可能会比较大,比如Get All Capabilities响应可能包含多个服务类别的参数,需要足够的空间来传输。
下面是AVDTP信令通道建立过程的snoop:

这两个包完整展示了L2CAP信令通道的建立过程,是所有AVDTP音视频传输的基础。通道建立成功后,手机和耳机就可以通过这个通道交换后续的所有AVDTP信令(Discover、Set Configuration、Start等)。
1. L2CAP Connection Request(手机→耳机)
这是手机主动发起的连接请求,目标是建立AVDTP专用的信令通道。
|-----------------|--------------------|---------------------------------------------------------------------|
| 字段 | 值 | 协议含义 |
| PSM | AVDTP(0x0019) | 这是AVDTP信令通道的固定PSM值,所有蓝牙设备的AVDTP信令通道都使用这个值。这是蓝牙协议分配给AVDTP的标准端口号。 |
| Source CID | 0x8C08 | 手机端分配的本地通道标识符。CID是L2CAP层的通道ID,每个L2CAP通道都有一个唯一的CID。 |
| Destination CID | 0x0001 (Signaling) | L2CAP层的固定信令通道CID。所有L2CAP连接请求都必须发送到这个CID。 |
| Identifier | 0x24 | 事务标识符,用于匹配请求和响应。响应包必须使用和请求包相同的Identifier。 |
| Length | 4 bytes | 有效载荷长度,这里是4字节,正好是PSM(2字节)+Source CID(2字节)的长度。 |
2. L2CAP Connection Response(耳机→手机)
这是耳机返回的连接响应,表示同意建立AVDTP信令通道。
|-----------------|----------------------------------|-----------------------------------------------------|
| 字段 | 值 | 协议含义 |
| Result | Connection successful | 连接成功。如果连接失败,这里会显示错误码,比如PSM not supported。 |
| Source CID | 0x8C08 | 和请求包中的Source CID相同,用于匹配请求。 |
| Destination CID | 0x0044 | 耳机端分配的本地通道标识符。现在双方都知道了对方的CID,后续的AVDTP信令都将通过这对CID传输。 |
| Status | No further information available | 没有额外的状态信息。 |
这个步骤完成后,接下来手机会发送Discover命令,获取耳机支持的流端点列表,然后是Get All Capabilities、Set Configuration等步骤。
3. 常见问题排查
如果这一步失败,通常会有以下几种情况:
-
PSM not supported:耳机不支持AVDTP协议,或者AVDTP服务没有启动
-
Connection refused:耳机拒绝连接,可能是因为已经连接了其他设备
-
Timeout:耳机没有响应,可能是因为距离太远或者干扰太大
如果这一步成功,但后续的AVDTP信令失败,问题通常出在AVDTP协议本身的实现上,比如Set Configuration参数不匹配等。
2.2 流端点发现 (Discover)
信令通道建立完成后,发起方首先需要发现接收方支持的流端点。

Discover命令的格式非常简单,没有任何参数,地址数量字段设置为0。
Discover响应包含接收方所有可用的流端点信息,每个流端点占用2字节:
第1字节:SEID(高6位)和In Use标志(第7位)
第2字节:媒体类型(高4位)和流端点类型(低4位)
In Use标志表示这个流端点是否正在被使用。如果In Use标志为1,表示这个流端点已经被其他流使用,不能再被配置。
媒体类型的可能值:
-
0x00:音频
-
0x01:视频
-
0x02:多媒体
流端点类型的可能值:
-
0x00:Sink
-
0x01:Source
下面是一个Discover的snoop:

这是AVDTP流端点发现流程的完整报文,这个步骤是所有后续配置的基础,耳机通过这个响应告诉手机自己有哪些可用的音频资源。
这两个包完成了流端点发现的完整事务:
-
手机(INT)通过已建立的AVDTP信令通道发送
Discover命令 -
耳机(ACP)返回
Discover Accept响应,列出自己所有可用的流端点 -
从响应可以看出,这款耳机总共暴露了3个音频流端点,这是非常典型的TWS耳机配置
(1)AVDTP Discover Command(手机→耳机)
这是一个非常简单的命令,没有任何参数。
|---------------|----------|--------------------------------------|
| 字段 | 值 | 协议含义 |
| L2CAP Dst CID | 0x0044 | 这正是上一步L2CAP连接响应中耳机分配的CID,验证了信令通道的双向性 |
| AVDTP Type | Command | 这是一个命令包 |
| AVDTP ID | Discover | 信号ID=0x01,对应Discover命令 |
| L2CAP SDU长度 | 2字节 | 只有AVDTP信令头,没有有效载荷 |
(2)AVDTP Discover Accept(耳机→手机)
这是整个抓包的核心,耳机返回了自己所有可用的流端点信息。每个流端点占用2字节,格式如下:
字节1: [7位保留][1位In Use]
字节2: [6位ACP SEID][2位TSEP]
端点1:SEID=1
|------------|-------|----------------------|
| 字段 | 值 | 含义 |
| In Use | No | 这个端点当前空闲,未被使用 |
| ACP SEID | 1 | 流端点标识符=1 |
| TSEP | 0 | 流端点类型=Sink(接收方) |
| Media Type | Audio | 媒体类型=音频 |
| 用途 | - | 主音频Sink,通常用于A2DP音乐播放 |
端点2:SEID=2
|------------|-------|-----------------------|
| 字段 | 值 | 含义 |
| In Use | No | 空闲 |
| ACP SEID | 2 | 流端点标识符=2 |
| TSEP | 0 | Sink(接收方) |
| Media Type | Audio | 音频 |
| 用途 | - | 辅助音频Sink,通常用于低延迟模式或通话 |
端点3:SEID=3
|------------|-------|--------------------|
| 字段 | 值 | 含义 |
| In Use | No | 空闲 |
| ACP SEID | 3 | 流端点标识符=3 |
| TSEP | 1 | Source(发送方) |
| Media Type | Audio | 音频 |
| 用途 | - | 音频Source,用于发送麦克风声音 |
(3)关键协议细节与开发坑点
①为什么耳机有两个音频Sink端点?
这是TWS耳机的标准设计,两个Sink端点分别用于不同的场景:
-
SEID=1:标准A2DP端点,支持SBC/AAC/LDAC等高质量编解码,延迟较高(~200ms)
-
SEID=2:低延迟端点,通常只支持SBC编码,延迟较低(~80ms),用于游戏和视频
开发坑点:很多手机只会选择第一个Sink端点(SEID=1),导致无法使用低延迟模式。如果你的应用需要低延迟,应该主动选择第二个Sink端点。
②In Use标志不可靠
规范规定In Use标志表示端点是否正在被使用,但90%以上的耳机不会正确设置这个标志。即使端点正在被使用,很多设备也会返回In Use=No。
开发建议:不要依赖In Use标志,当配置某个SEID失败时,应该尝试下一个可用的SEID。
③端点顺序的意义
耳机返回的端点顺序通常是有意义的,排在前面的端点是耳机推荐使用的端点。大多数手机会选择第一个Sink端点进行连接。
(4)下一步信令流程
这个步骤完成后,手机接下来会:
①选择其中一个音频Sink端点(通常是SEID=1)
②发送Get All Capabilities命令,获取该端点支持的编解码格式、采样率、声道数等能力
③根据返回的能力选择合适的配置,发送Set Configuration命令
(5)常见问题排查
如果这一步失败,通常会有以下几种情况:
①返回空的端点列表:耳机的AVDTP服务异常,需要重启耳机
②返回无效的SEID:耳机固件bug,尝试重新连接
③命令超时:信令通道异常,需要重新建立L2CAP信令通道
2.3 获取流端点能力 (Get Capabilities / Get All Capabilities)
发现流端点后,发起方需要获取该流端点支持的能力,以便进行后续的配置。
AVDTP提供了两个命令来获取流端点的能力:
Get Capabilities:只获取基本的能力信息
Get All Capabilities:获取所有的能力信息,包括扩展能力
规范建议使用Get All Capabilities命令,因为它可以获取更完整的能力信息,避免兼容性问题。

Get All Capabilities命令包含一个参数:ACP SEID,即接收方的流端点标识符。地址数量字段设置为1。

Get All Capabilities响应包含该流端点支持的所有服务类别列表。每个服务类别占用1字节的服务类别ID,后面跟着可变长度的服务参数。
服务类别ID的可能值:
0x00:媒体传输服务(必选)
0x01:报告服务(可选)
0x02:恢复服务(可选)
0x03:媒体编解码服务(必选)
0x04:多路复用服务(可选)
0x05:健壮头压缩服务(可选)
0x06:内容保护服务(可选)
0x07:延迟报告服务(可选)
下面是一个Get All Capabilities 的示例:

这是AVDTP能力查询流程 的完整抓包,对应前文的信令流程第三步。特别注意 :这次手机查询的是上一个抓包里的SEID=3的Source端点(麦克风),而不是Sink端点,说明手机正在准备建立双向音频通话连接。
这两个包完成了流端点能力查询的完整事务:
-
手机(INT)发送
Get All Capabilities命令,查询SEID=3的流端点能力 -
耳机(ACP)返回
Get All Capabilities Accept响应,列出该端点支持的所有服务 -
从响应可以看出,这款耳机的麦克风端点只支持SBC编码,同时支持延迟报告功能
(1)AVDTP Get All Capabilities Command(手机→耳机)
|-------------|----------------------|-------------------------------------|
| 字段 | 值 | 协议含义 |
| AVDTP Type | Command | 命令包 |
| AVDTP ID | Get All Capabilities | 信号ID=0x03,对应获取所有能力命令 |
| ACP SEID | 3 | 查询的目标流端点标识符=3(也就是上一个抓包里的音频Source端点) |
| L2CAP SDU长度 | 3字节 | AVDTP信令头(2字节)+ACP SEID(1字节) |
(2)AVDTP Get All Capabilities Accept(耳机→手机)
响应包含3个服务类别,这是一个标准的音频Source端点能力配置:
能力1:媒体传输服务(必选)
|------------------|-----------------|----------------------------|
| 字段 | 值 | 含义 |
| Service Category | Media Transport | 服务类别ID=0x00,媒体传输服务 |
| Length | 0 bytes | 该服务没有额外参数 |
| 作用 | - | 所有流端点都必须支持的基础服务,提供媒体数据传输功能 |
能力2:媒体编解码服务(必选)
这是整个响应的核心,详细定义了麦克风支持的SBC编码参数:
|--------------------|--------------|---------------------|
| 字段 | 值 | 协议含义 |
| Service Category | Media Codec | 服务类别ID=0x03,媒体编解码服务 |
| Length | 6 bytes | 编解码参数总长度为6字节 |
| Media Type | Audio | 媒体类型=音频 |
| Media Codec Type | SBC | 编解码类型=SBC(唯一支持的编解码) |
| Channel Mode | Joint Stereo | Stereo |
| Sampling Frequency | 48kHz | 44.1kHz |
| Allocation Method | Loudness | SNR |
| Subbands | 8 | 4 |
| Block Length | 16 | 12 |
| Min Bitpool Value | 2 | 最小比特池值=2 |
| Max Bitpool Value | 53 | 最大比特池值=53 |
能力3:延迟报告服务(可选)
|------------------|-----------------|----------------------------|
| 字段 | 值 | 含义 |
| Service Category | Delay Reporting | 服务类别ID=0x07,延迟报告服务 |
| Length | 0 bytes | 该服务没有额外参数 |
| 作用 | - | 允许耳机向手机报告麦克风的处理延迟,实现双向音频同步 |
(3)关键开发坑点与注意事项
①Source端点也需要配置
很多开发者只知道配置Sink端点(扬声器),但不知道Source端点(麦克风)也需要完整的配置流程。如果不配置Source端点,就无法建立麦克风的音频流,导致通话没有声音。
②不要为麦克风选择立体声模式
虽然耳机声明支持立体声模式,但麦克风本质上是单声道设备。如果配置为立体声模式,大多数耳机都会返回错误,或者只传输左声道的声音。
③ 比特池值不要超过最大值
如果在Set Configuration命令中设置的比特池值超过了耳机声明的最大值(这里是53),耳机一定会拒绝配置,返回0x0A Invalid Capabilities错误。
④延迟报告对通话质量至关重要
对于双向通话,延迟报告功能可以让手机知道麦克风的处理延迟,从而调整音频的播放时间,避免回声和啸叫。如果耳机支持延迟报告,一定要在配置中启用它。
(4)常见问题排查
如果这一步失败,通常会有以下几种情况:
-
返回
0x03 Invalid ACP SEID:SEID不存在,或者该端点已经被其他流使用 -
返回空的能力列表:耳机固件bug,尝试重新连接
-
SBC参数不支持:检查配置的参数是否在耳机声明的能力范围内
在解析能力信息的时候,有几个需要特别注意的地方:
必选服务类别:媒体传输服务和媒体编解码服务是必选的,所有的流端点都必须支持这两个服务类别。如果一个流端点不支持这两个服务类别,发起方应该拒绝使用这个流端点。
服务参数的顺序:服务参数的顺序是固定的,必须按照规范中定义的顺序出现。如果顺序不对,发起方应该认为这个能力信息是无效的。
RFA字段:服务参数中的RFA字段必须设置为0。如果RFA字段不为0,发起方应该忽略这个服务类别。
2.4 流配置 (Set Configuration)
获取流端点的能力后,发起方需要选择一个双方都支持的配置组合,然后发送Set Configuration命令给接收方。
这是整个信令流程中最复杂、也是最容易出错的步骤。90%的兼容性问题都出在这个步骤。

Set Configuration命令包含两个SEID:
-
ACP SEID:接收方的流端点标识符
-
INT SEID:发起方的流端点标识符
地址数量字段设置为2。
命令的有效载荷包含发起方选择的配置参数,格式和Get All Capabilities响应的格式相同,只是每个服务类别只包含一个参数选项,而不是所有支持的选项。
下面是一个Set Configuration 的示例:

这是AVDTP最核心的流配置步骤 的完整抓包,也是90%蓝牙兼容性问题的发生地。手机成功配置了耳机的SEID=3麦克风Source端点,耳机返回Accept响应,说明双向音频通话的准备工作已经完成90%。
这两个包完成了流配置的完整事务:
-
手机(INT)发送
Set Configuration命令,明确告知耳机要使用的所有参数 -
耳机(ACP)验证所有参数都在自己支持的能力范围内,返回
Accept响应 -
配置成功后,双方锁定对应的流端点,分配所有必要的资源,准备建立传输通道
特别注意 :这次配置的是Source端点(麦克风),而不是Sink端点(扬声器),说明手机正在建立HFP通话连接,而不是A2DP音乐播放连接。
1. AVDTP Set Configuration Command(手机→耳机)
这是整个AVDTP信令中最复杂的命令,任何一个字节错误都会导致配置失败。
|---------------------|-------------------|-------------------------------|
| 字段 | 值 | 协议含义 |
| AVDTP Type | Command | 命令包 |
| AVDTP ID | Set Configuration | 信号ID=0x04,对应流配置命令 |
| Number of Addresses | 2 | 包含两个SEID |
| ACP SEID | 3 | 耳机端的流端点标识符=3(麦克风Source) |
| INT SEID | 2 | 手机端分配的本地流端点标识符=2 |
| L2CAP SDU长度 | 16字节 | 信令头(3字节)+SEID(2字节)+服务参数(11字节) |
2. 服务类别配置详解
命令包含3个服务类别,顺序和内容必须和之前Get All Capabilities响应完全一致,这是配置成功的关键。
服务1:媒体传输服务(必选)
|------------------|-----------------|--------------|
| 字段 | 值 | 含义 |
| Service Category | Media Transport | 服务类别ID=0x00 |
| Length | 0 bytes | 无参数 |
| 校验 | - | 和耳机声明的能力完全一致 |
服务2:媒体编解码服务(必选,核心)
这是整个命令的核心,手机从耳机支持的所有参数中选择了一个组合:
|--------------------|--------------|-------------------------------|-------------------------------|
| 字段 | 手机选择的值 | 耳机支持的值 | 说明 |
| Media Type | Audio | Audio | 一致 |
| Media Codec Type | SBC | SBC | 一致 |
| Channel Mode | Joint Stereo | Joint Stereo/Stereo/Dual/Mono | 选择了联合立体声 ⚠️ 异常点:麦克风应该用单声道 |
| Sampling Frequency | 44.1kHz | 48kHz/44.1kHz | 选择了44.1kHz |
| Allocation Method | Loudness | Loudness/SNR | 选择了响度分配 |
| Subbands | 8 | 8/4 | 选择了8个子带 |
| Block Length | 16 | 16/12/8/4 | 选择了16个块长度 |
| Min Bitpool Value | 2 | 2 | 一致 |
| Max Bitpool Value | 53 | 53 | 一致 |
服务3:延迟报告服务(可选)
|------------------|-----------------|-------------------|
| 字段 | 值 | 含义 |
| Service Category | Delay Reporting | 服务类别ID=0x07 |
| Length | 0 bytes | 无参数 |
| 作用 | - | 启用延迟报告功能,实现通话双向同步 |
3. AVDTP Set Configuration Accept(耳机→手机)
|--------------|----------|-------------------|
| 字段 | 值 | 含义 |
| AVDTP Type | Response | 响应包 |
| Message Type | Accept | 接受配置 |
| L2CAP SDU长度 | 2字节 | 只有AVDTP信令头,没有有效载荷 |
| 意义 | - | 耳机确认所有参数都支持,配置成功 |
4. Set Configuration的三大铁律(开发必背)
这三条规则是我踩了无数坑总结出来的,只要严格遵守,90%的配置失败问题都可以避免:
铁律1:配置必须是能力的严格子集
你选择的每一个参数都必须在耳机返回的Get All Capabilities响应中明确声明支持。只要有一个参数不支持,耳机一定会拒绝配置。
错误示例:耳机只支持SBC,你却配置了AAC → 拒绝
错误示例:耳机最大比特池值是53,你却设置了60 → 拒绝
铁律2:服务类别的顺序必须完全一致
你在Set Configuration命令中列出的服务类别的顺序,必须和Get All Capabilities响应中的顺序完全相同。
错误示例:耳机返回的顺序是Media Transport → Media Codec → Delay Reporting,你却写成了Media Codec → Media Transport → Delay Reporting → 拒绝
这是AVDTP规范中最反人类的一条规定,但几乎所有耳机都会严格检查。
铁律3:必须包含所有必选服务类别
Media Transport和Media Codec是必选服务类别,必须出现在配置中。如果缺少其中任何一个,耳机一定会拒绝配置。
5. 下一步信令流程
配置成功后,接下来的流程:
-
手机按照媒体→报告→恢复的顺序建立对应的L2CAP传输通道
-
所有通道建立完成后,手机发送
Open命令 -
Open成功后,手机发送
Start命令 -
收到Start响应后,耳机开始通过媒体通道发送麦克风的RTP数据包
6. 常见配置失败问题排查
|---------|------------------------------|--------------|-----------------------|
| 错误码 | 含义 | 最可能的原因 | 解决方法 |
| 0x03 | Invalid ACP SEID | SEID不存在或已被使用 | 重新Discover获取最新的SEID列表 |
| 0x07 | Unsupported Service Category | 配置了耳机不支持的服务 | 移除该服务类别,重新配置 |
| 0x0A | Invalid Capabilities | 参数不在耳机支持的范围内 | 检查每个参数,确保是能力的子集 |
| 0x11 | Invalid State | 流端点处于错误的状态 | 关闭当前流,重新开始配置流程 |
Set Configuration命令的处理规则非常严格:
-
配置必须是能力的子集:发起方选择的所有配置参数都必须是接收方在Get All Capabilities响应中声明支持的参数。如果有任何一个参数不被支持,接收方必须拒绝这个配置。
-
服务类别的顺序:服务类别的顺序必须和Get All Capabilities响应中的顺序相同。如果顺序不对,接收方必须拒绝这个配置。
-
必选服务类别:配置中必须包含媒体传输服务和媒体编解码服务。如果缺少这两个服务类别,接收方必须拒绝这个配置。
-
资源锁定:如果配置成功,双方必须锁定对应的流端点,不能再被其他流使用。
如果接收方拒绝配置,必须在响应中包含一个错误码,说明拒绝的原因。常见的错误码有:
0x03:无效的ACP SEID
0x04:SEID正在使用中
0x07:不支持的服务类别
0x0A:无效的能力
当收到拒绝响应时,发起方应该根据错误码采取相应的措施。例如,如果是因为不支持某个服务类别,发起方应该尝试不使用这个服务类别,重新发送Set Configuration命令。
下面是一个简化的Set Configuration处理函数的伪代码:
uint16_t avdtp_handle_set_configuration(
AvdtpConnection* conn,
uint8_t transaction_label,
uint8_t acp_seid,
uint8_t int_seid,
uint8_t* payload,
uint16_t payload_len
) {
// 1. 检查ACP SEID是否有效
AvdtpSep* acp_sep = avdtp_find_sep_by_seid(acp_seid);
if (!acp_sep) {
return avdtp_send_reject_response(conn, transaction_label,
AVDTP_ERROR_INVALID_ACP_SEID);
}
// 2. 检查ACP SEID是否正在使用中
if (acp_sep->in_use) {
return avdtp_send_reject_response(conn, transaction_label,
AVDTP_ERROR_SEID_IN_USE);
}
// 3. 解析配置参数
AvdtpConfiguration config;
uint16_t offset = 0;
while (offset < payload_len) {
uint8_t service_category = payload[offset++];
uint8_t length = payload[offset++];
// 4. 检查是否支持这个服务类别
if (!avdtp_is_service_supported(acp_sep, service_category)) {
return avdtp_send_reject_response(conn, transaction_label,
AVDTP_ERROR_UNSUPPORTED_SERVICE_CATEGORY);
}
// 5. 解析服务参数
if (!avdtp_parse_service_parameters(service_category,
payload + offset, length, &config)) {
return avdtp_send_reject_response(conn, transaction_label,
AVDTP_ERROR_INVALID_CAPABILITIES);
}
offset += length;
}
// 6. 检查是否包含必选服务类别
if (!config.media_transport_present || !config.media_codec_present) {
return avdtp_send_reject_response(conn, transaction_label,
AVDTP_ERROR_INVALID_CAPABILITIES);
}
// 7. 分配资源,锁定流端点
acp_sep->in_use = true;
acp_sep->config = config;
acp_sep->remote_seid = int_seid;
// 8. 发送接受响应
return avdtp_send_accept_response(conn, transaction_label);
}
2.5 建立传输通道
配置成功后,发起方需要根据配置的服务类别,建立对应的传输通道。
传输通道的建立顺序是固定的:
①媒体传输通道(必选)
②报告传输通道(可选,如果启用了报告服务)
③恢复传输通道(可选,如果启用了恢复服务)
每个传输通道都是一个独立的L2CAP通道,使用动态分配的PSM值。
传输通道的建立有几个需要注意的地方:
①通道建立顺序:必须严格按照媒体→报告→恢复的顺序建立通道。不能颠倒顺序,否则很多设备会拒绝连接。
②通道MTU:媒体通道的MTU至少应该是672字节,报告通道和恢复通道的MTU至少应该是48字节。
③通道数量:如果没有启用某个服务,就不需要建立对应的通道。例如,如果没有启用报告服务,就不需要建立报告通道。
④通道失败处理:如果某个通道建立失败,发起方应该关闭已经建立的通道,释放资源,并通知上层应用。
2.6 打开流 (Open)
所有需要的传输通道建立完成后,发起方发送Open命令,通知接收方流已经准备好,可以启动了。

Open命令包含一个参数:ACP SEID。地址数量字段设置为1。
Open命令的作用是通知接收方所有的传输通道已经建立完成,可以准备接收媒体数据了。收到Open命令后,接收方应该分配所有必要的资源,比如解码缓冲区、播放缓冲区等。
如果接收方无法分配足够的资源,应该拒绝Open命令,返回相应的错误码。
下面是AVDTP流打开流程的完整snoop:

这两个包完成了流打开的完整事务:
①手机(INT)发送Open命令,通知耳机所有传输通道已建立完成,可以准备接收媒体数据
②耳机(ACP)验证流状态正确,成功分配所有必要的资源(解码缓冲区、DMA通道等),返回Accept响应
③现在流进入Open状态,距离真正的音频传输只差最后一步Start命令
本次操作的对象仍然是SEID=3的麦克风Source端点,说明双向通话的上行链路已经准备就绪。
(1)AVDTP Open Command(手机→耳机)
这是一个非常简单的命令,只需要指定目标流端点。
|---------------------|---------|-----------------------------|
| 字段 | 值 | 协议含义 |
| AVDTP Type | Command | 命令包 |
| AVDTP ID | Open | 信号ID=0x06,对应打开流命令 |
| Number of Addresses | 1 | 包含一个SEID |
| ACP SEID | 3 | 目标流端点标识符=3(麦克风Source) |
| L2CAP SDU长度 | 3字节 | AVDTP信令头(2字节)+ACP SEID(1字节) |
(2)AVDTP Open Accept(耳机→手机)
这是一个标准的成功响应,没有任何有效载荷。
|-------------------|------------|----------------------------|
| 字段 | 值 | 协议含义 |
| Packet Type | Response | 响应包 |
| Message Type | Accept | 命令被成功接受 |
| Transaction Label | 7 | 事务标签=0x07,与Open命令的事务标签完全匹配 |
| Signal Identifier | AVDTP_OPEN | 信号ID=0x06,与命令的信号ID一致 |
| L2CAP SDU长度 | 2字节 | 只有AVDTP信令头,没有有效载荷 |
(3)关键开发坑点与注意事项
①Open命令的发送时机
Open命令必须在所有传输通道建立完成之后才能发送 。如果在通道建立过程中就发送Open命令,绝大多数设备都会返回0x11 Invalid State错误。
开发建议:在Set Configuration成功后,按照媒体→报告→恢复的顺序依次建立通道,等待所有通道都连接成功后,再发送Open命令。
②Open响应后的超时处理
收到Open响应后,应该尽快发送Start命令。很多设备的Open状态有一个隐含的超时时间(通常是10-30秒),如果在超时时间内没有收到Start命令,设备会自动关闭流,释放资源。
③Open失败的常见原因
|---------|---------------------|--------------|------------------------|
| 错误码 | 含义 | 最可能的原因 | 解决方法 |
| 0x03 | Invalid ACP SEID | SEID不存在或已被释放 | 重新配置流端点 |
| 0x05 | SEID Not In Use | 流端点没有被配置 | 先发送Set Configuration命令 |
| 0x10 | Duplicate Operation | 流已经处于Open状态 | 直接发送Start命令 |
| 0x11 | Invalid State | 流处于错误的状态 | 关闭当前流,重新开始整个流程 |
| 0x20 | Resource Exhausted | 资源不足 | 降低编解码码率,减少缓冲区大小 |
④双向流的Open顺序
对于同时有上行(麦克风)和下行(扬声器)音频的通话场景,Open命令的发送顺序会影响连接建立的速度。推荐的顺序是:
-
先配置并打开下行Sink端点(扬声器)
-
再配置并打开上行Source端点(麦克风)
这样可以让用户先听到对方的声音,再开始说话,体验更好。
(4)下一步信令流程
这个步骤完成后,距离真正的音频传输只差最后一步:
-
手机发送
Start命令,通知耳机开始发送麦克风数据 -
耳机返回
Start Accept响应 -
耳机立即开始通过媒体通道发送SBC编码的RTP音频包
-
手机收到RTP包后,解封装、解码并播放
2.7 启动流 (Start)
流打开后,发起方发送Start命令,通知接收方开始接收和播放媒体数据。

Start命令包含一个参数:ACP SEID。地址数量字段设置为1。
收到Start命令后,接收方应该启动解码器和播放器,准备接收媒体数据。收到Start响应后,发起方可以开始发送媒体数据。
这里有一个非常重要的细节:发起方必须在收到Start响应之后才能开始发送媒体数据。如果在收到响应之前就发送媒体数据,接收方可能还没有准备好,会导致数据丢失,出现声音卡顿或者没有声音的问题。
很多开发者为了减少延迟,会在发送Start命令之后立即开始发送媒体数据,这是不符合规范的,会导致兼容性问题。

这是整个AVDTP信令流程的最后一步,也是最关键的一步:流启动。至此,从L2CAP通道建立到流配置、打开、启动的完整链路全部完成,耳机将立刻开始发送麦克风的音频数据。
这两个包完成了流启动的完整事务:
-
手机(INT)发送
Start命令,通知耳机可以开始传输媒体数据 -
耳机(ACP)验证流处于Open状态,启动音频采集硬件和编码器,返回
Accept响应 -
耳机在发送Start Accept响应的同时,就会立刻开始通过媒体通道发送RTP音频包
特别强调角色关系 :这次是手机(音频Sink)向耳机(音频Source)发送Start命令 ,完美印证了我们之前讲的核心知识点:Initiator/Acceptor角色与Source/Sink角色完全独立。信令的发起方可以是任何一方,与媒体流的方向无关。
(1)AVDTP Start Command(手机→耳机)
格式与Open命令几乎完全相同,是AVDTP中最简单的命令之一。
|---------------------|---------|-----------------------------|
| 字段 | 值 | 协议含义 |
| AVDTP Type | Command | 命令包 |
| AVDTP ID | Start | 信号ID=0x07,对应启动流命令 |
| Number of Addresses | 1 | 包含一个SEID |
| ACP SEID | 3 | 目标流端点标识符=3(耳机的麦克风Source) |
| L2CAP SDU长度 | 3字节 | AVDTP信令头(2字节)+ACP SEID(1字节) |
(2)AVDTP Start Accept(耳机→手机)
标准的无载荷成功响应。
|-------------------|-------------|------------------------|
| 字段 | 值 | 协议含义 |
| Packet Type | Response | 响应包 |
| Message Type | Accept | 命令被成功接受 |
| Transaction Label | 8 | 事务标签=0x08,与Start命令完全匹配 |
| Signal Identifier | AVDTP_START | 信号ID=0x07,与命令一致 |
| L2CAP SDU长度 | 2字节 | 只有AVDTP信令头 |
(3)Start命令的核心协议细节(开发必知)
①Start与Open的最终边界
经过前面所有步骤的铺垫,我们可以用最直白的语言总结两者的本质区别:
-
Open:"我这边所有路都修好了,你那边把仓库和货车准备好"
-
Start:"一切就绪,可以开始发货了"
最关键的规范强制要求:
The Source device shall not transmit any media packets before receiving the Start Accept response.
Source设备绝对不能在收到Start Accept响应之前发送任何媒体包。这是90%兼容性问题的根源:很多开发者为了减少延迟,在发送Start命令的同时就开始发数据,导致耳机还没准备好,大量数据包丢失,出现前2秒没有声音的问题。
(4)流状态转换
Start命令执行完成后,流的状态从Open转换为Streaming,这是AVDTP流唯一可以传输媒体数据的状态。

(5)双向通话的启动顺序最佳实践
对于同时有上行(麦克风)和下行(扬声器)的通话场景,严格按照以下顺序启动可以获得最佳用户体验:
-
配置并打开下行Sink端点(扬声器)
-
发送下行Start命令,收到Accept后手机开始播放对方声音
-
配置并打开上行Source端点(麦克风)
-
发送上行Start命令,收到Accept后耳机开始采集本地声音
这样用户会先听到对方的声音,再开始说话,完全符合自然的通话习惯,避免出现"对方先说了半句话我才听到"的情况。
(6)开发中最常见的Start相关问题
1. Start成功但完全没有声音
这是最常见的问题,排查优先级从高到低:
-
检查媒体通道的CID是否正确:确认媒体数据发送到了正确的L2CAP通道
-
检查RTP Payload Type:必须与配置的编解码类型一致(SBC通常是96)
-
检查RTP时间戳增量:必须与采样率和帧大小匹配(44.1kHz 1024样本帧的增量是1024)
-
检查媒体通道MTU:必须大于最大RTP包大小(SBC通常不超过672字节)
-
检查耳机的编码器是否启动:很多低端耳机在Start后会有100-200ms的硬件启动延迟
2. 声音卡顿、断断续续
最可能的原因:
-
RTP序列号不连续,存在丢包
-
时间戳增量不均匀,导致播放缓冲区下溢
-
媒体通道的L2CAP重传模式设置错误(应该使用基本模式,不要使用重传模式)
3. 常见错误码
|---------|---------------------|--------------------------------|
| 错误码 | 含义 | 解决方法 |
| 0x05 | SEID Not In Use | 流端点没有被配置,先执行Set Configuration |
| 0x10 | Duplicate Operation | 流已经处于Streaming状态,不需要重复发送Start |
| 0x11 | Invalid State | 流处于Idle或Configured状态,必须先执行Open |
2.8 媒体数据传输
流启动后,发起方开始通过媒体传输通道发送RTP媒体包。接收方收到RTP包后,解封装出音频数据,解码并播放。
媒体数据传输的详细过程我们在之前的章节已经讲过,这里就不再重复了。
2.9 暂停流 (Suspend)
当用户暂停播放时,发起方发送Suspend命令,通知接收方停止播放。

Suspend命令包含一个参数:ACP SEID。地址数量字段设置为1。
收到Suspend命令后,接收方应该停止播放,清空播放缓冲区,但保留解码缓冲区和其他资源。收到Suspend响应后,发起方应该停止发送媒体数据。
暂停状态下,流的配置和传输通道都保持不变,可以通过发送Start命令快速恢复播放。
下面是AVDTP流暂停流程的snoop:

这两个包完成了流暂停的完整事务:
-
手机(INT)发送
Suspend命令,通知耳机停止传输麦克风音频数据 -
耳机(ACP)验证流处于Streaming状态,停止采集和编码,返回
Accept响应 -
暂停后所有L2CAP传输通道、编解码上下文、缓冲区资源全部保留,无需重新建立
这是AVDTP设计中非常体现工程思维的一个命令:用最小的状态切换代价,实现音频的快速暂停与恢复,避免了完整建链的高延迟。
(1) AVDTP Suspend Command(手机→耳机)
格式与Start命令完全对称,是极简的单SEID控制命令。
|---------------------|---------|-----------------------------|
| 字段 | 值 | 协议含义 |
| AVDTP Type | Command | 命令包 |
| AVDTP ID | Suspend | 信号ID=0x09,对应暂停流命令 |
| Number of Addresses | 1 | 包含1个目标SEID |
| ACP SEID | 3 | 目标流端点标识符=3(耳机麦克风Source) |
| L2CAP SDU长度 | 3字节 | AVDTP信令头(2字节)+ACP SEID(1字节) |
(2) AVDTP Suspend Accept(耳机→手机)
标准无载荷成功响应,与Start响应格式一致。
|-------------------|---------------|--------------------------|
| 字段 | 值 | 协议含义 |
| Packet Type | Response | 响应包 |
| Message Type | Accept | 命令被成功接受 |
| Transaction Label | 9 | 事务标签=0x09,与Suspend命令完全匹配 |
| Signal Identifier | AVDTP_SUSPEND | 信号ID=0x09,与命令一致 |
| L2CAP SDU长度 | 2字节 | 仅包含AVDTP信令头,无额外参数 |
(3) Suspend 与 Close 的核心区别
很多开发者容易混淆暂停和关闭,两者在资源占用、恢复速度、适用场景上有本质区别:
|---------|------------------|--------------------------|
| 维度 | Suspend 暂停 | Close 关闭 |
| 流状态变化 | Streaming → Open | 任意状态 → Idle |
| L2CAP通道 | 完整保留 | 全部释放 |
| 编解码资源 | 保留上下文 | 完全释放 |
| 缓冲区数据 | 保留 | 清空释放 |
| 恢复方式 | 发送Start命令即可恢复 | 需重新走配置、建通道、Open、Start全流程 |
| 恢复延迟 | 几十毫秒级 | 几百毫秒到秒级 |
| 适用场景 | 短时间暂停(静音、暂停播放) | 长时间停止(挂断、退出播放) |
| 功耗影响 | 通道保持,功耗略高 | 资源释放,功耗最低 |
设计意图:蓝牙无线链路的建立和销毁成本很高,频繁建链拆链不仅延迟大,还会增加功耗。Suspend就是为了解决短时间暂停的场景,用极低的资源开销换取毫秒级的恢复速度。
(4) 核心协议强制规则(开发必守)
①暂停后必须立即停止媒体传输
规范明确要求:Source设备在发出Suspend命令、或收到Suspend命令后,必须立即停止发送所有媒体包,不允许再有任何RTP数据包通过媒体通道传输。
如果暂停后仍然继续发送数据,Sink端通常会直接丢弃这些无效包;部分严格的设备会认为是协议异常,甚至主动断开整个AVDTP连接。
②双方都可以主动发起暂停
Suspend命令的发起方没有限制,Source和Sink都可以作为Initiator发起暂停。这是非常容易被忽略的一点:
手机侧:用户点击静音、暂停播放时发起
耳机侧:检测到用户摘下耳机、电量过低、射频干扰过大时,都可以主动向手机发起Suspend
开发时必须处理对端主动发起的Suspend请求,不能只处理自己发起的流程,否则会出现耳机已经暂停了,但手机还在不停发数据的异常情况。
③仅允许在Streaming状态执行
Suspend命令只能在流处于Streaming状态时发送。如果流处于Idle、Configured或Open状态,接收方必须返回0x11 Invalid State错误。
(5)常见触发场景与工程最佳实践
典型触发场景
-
通话过程中用户点击静音按钮,暂停上行麦克风
-
音乐播放时用户点击暂停,暂停下行音频
-
TWS耳机单耳摘下,自动暂停播放
-
系统临时切换音频到其他输出设备
工程最佳实践
①不要频繁切换:避免短时间内反复Suspend/Start,频繁状态切换会导致耳机处理不及时,出现爆音或卡顿。建议静音持续超过2~3秒再执行Suspend。
②处理超时自动关闭:多数耳机固件会对Suspend状态设置超时(通常5~10分钟),超时后自动Close释放资源。应用层如果需要长时间暂停,应该主动发送Close命令,不要依赖对端超时。
③同步处理双向流:对于通话这类双向音频场景,暂停上行麦克风时,不要同时暂停下行扬声器,避免用户听不到对方的提示音。
(6)常见错误码与排查
|---------|---------------------|----------------|--------------------------|
| 错误码 | 含义 | 常见原因 | 解决方法 |
| 0x05 | SEID Not In Use | 流端点未完成配置 | 先执行Set Configuration完成配置 |
| 0x10 | Duplicate Operation | 流已经处于暂停状态 | 检查流状态,避免重复发送Suspend |
| 0x11 | Invalid State | 流不在Streaming状态 | 确认流已成功Start后再发送暂停命令 |
2.10 关闭流 (Close)
当用户停止播放或者断开连接时,发起方发送Close命令,通知接收方释放所有资源。

Close命令包含一个参数:ACP SEID。地址数量字段设置为1。
收到Close命令后,接收方应该释放所有与该流相关的资源,包括解码缓冲区、播放缓冲区、传输通道等,并解锁流端点。收到Close响应后,发起方也应该释放所有相关的资源,并关闭传输通道。
下面是AVDTP流关闭流程的snoop:

这两个包完成了流正常关闭的完整事务:
①耳机作为发起方(INT)发送Close命令,通知手机终止当前流、释放对应资源
②手机作为接收方(ACP)验证流状态,停止数据处理、释放编解码与缓冲区资源,返回Accept响应
③命令执行后流从当前状态直接退回Idle状态,对应的媒体传输通道将被依次关闭,流端点解除锁定
这也再次印证了核心规则:信令发起方与媒体流方向完全独立,不仅Sink可以发起控制命令,Source端(耳机麦克风)同样可以主动发起关闭。
(1)AVDTP Close Command(耳机→手机)
|---------------------|---------|--------------------------------|
| 字段 | 值 | 协议含义 |
| L2CAP Dst CID | 0x8C08 | 发往手机端的AVDTP信令通道,说明发起方是耳机 |
| AVDTP Type | Command | 命令包 |
| AVDTP ID | Close | 信号ID=0x08,对应关闭流命令 |
| Number of Addresses | 1 | 包含1个目标SEID |
| ACP SEID | 2 | 接收方(手机)的流端点标识符=2,即手机侧的音频Sink端点 |
| L2CAP SDU长度 | 3字节 | AVDTP信令头(2字节)+ACP SEID(1字节) |
(2)AVDTP Close Accept(手机→耳机)
|-------------------|-------------|------------------------|
| 字段 | 值 | 协议含义 |
| L2CAP Dst CID | 0x0044 | 发往耳机端的AVDTP信令通道 |
| Packet Type | Response | 响应包 |
| Message Type | Accept | 命令被成功接受,流已关闭 |
| Transaction Label | 1 | 事务标签=0x01,与Close命令完全匹配 |
| Signal Identifier | AVDTP_CLOSE | 信号ID=0x08,与命令一致 |
| L2CAP SDU长度 | 2字节 | 仅包含AVDTP信令头,无额外参数 |
(3)Close命令的核心协议规则
①状态转换规则
Close是唯一可以从任意非Idle状态执行的命令,无论流处于Configured、Open还是Streaming状态,都可以直接发起关闭,执行后统一回到Idle状态。
这一设计保证了异常场景下的兜底能力:即使状态机混乱,也可以通过Close命令强制重置流状态。
②资源释放顺序
收到Close命令并响应后,两端必须按照固定顺序释放资源,避免资源泄漏:
-
停止媒体数据收发,清空编解码缓冲区
-
释放编解码硬件资源、DMA通道
-
按照恢复→报告→媒体的逆序关闭对应L2CAP传输通道
-
销毁流上下文,释放本地内存
-
解锁本地流端点,标记为空闲可复用
(4)与Suspend、Abort的本质区别
AVDTP共有三种停止类命令,适用场景完全不同:
|---------|----------|-----------------|-----------------|-----------|
| 命令 | 状态目标 | 资源释放 | 恢复方式 | 适用场景 |
| Suspend | 退回Open状态 | 不释放通道和上下文 | 发送Start即可恢复,毫秒级 | 短时间暂停、静音 |
| Close | 退回Idle状态 | 完全释放所有资源 | 需走完整建链流程,百毫秒级 | 正常结束播放/通话 |
| Abort | 强制退回Idle | 立即强制释放,不等待数据处理完 | 需完整建链 | 异常出错、强制断开 |
(5)幂等性与容错
Close命令具备幂等性:如果流已经处于Idle状态,收到Close命令也应该返回Accept,而不是返回错误,避免对端因重复发送而异常。
(6)开发常见问题与最佳实践
①常见错误码
|---------|------------------|------------------------|
| 错误码 | 含义 | 常见原因 |
| 0x03 | Invalid ACP SEID | 目标SEID不存在,或对端已释放该流 |
| 0x05 | SEID Not In Use | 流端点未被配置,本身就是空闲状态 |
| 0x11 | Invalid State | 极少数严格实现的设备会在Idle状态返回错误 |
②工程最佳实践
-
正常退出必须走Close流程:不要直接断开L2CAP通道或ACL连接来结束流,否则对端会认为是异常断连,可能触发重连逻辑,影响体验。
-
先关闭流再断开物理连接:断开蓝牙前,应先主动Close所有活动流,给对端释放资源的时间,避免耳机端出现资源泄漏、下次连接异常。
-
超时兜底:发送Close命令后若超时无响应,应在重传2~3次后强制释放本地资源、关闭通道,不要无限等待。
-
双向流分别关闭:通话场景下上行和下行是两个独立流,结束通话时需要分别关闭两个方向的流,不能只关一个。
三、高级信令流程
除了上面讲的核心信令流程,AVDTP还定义了一些高级信令流程,用于处理更复杂的场景。
3.1 流重配置 (Reconfigure)
流重配置流程允许在流的生命周期内修改流的配置参数,而不需要重新建立整个流。

Reconfigure命令的格式和Set Configuration命令类似,包含ACP SEID和新的配置参数。
流重配置有几个重要的限制:
只能在暂停状态下进行:流必须处于暂停状态才能进行重配置。如果流处于流传输状态,接收方必须拒绝Reconfigure命令。
不能修改媒体类型和流端点类型:重配置不能修改流的媒体类型和流端点类型。如果需要修改这些参数,必须关闭当前流,重新建立一个新的流。
不能添加或删除服务类别:重配置只能修改已经启用的服务类别的参数,不能添加新的服务类别,也不能删除已经启用的服务类别。
流重配置最常见的用途是切换编解码格式或者调整采样率。例如,当用户从播放音乐切换到打电话时,可以通过重配置将编解码格式从SBC切换到CVSD。
3.2 延迟报告 (Delay Report)
延迟报告流程允许Sink设备向Source设备报告自己的处理延迟,从而实现精确的音视频同步。

Delay Report命令包含两个参数:
ACP SEID:Source设备的流端点标识符
Delay:延迟值,单位是1/10毫秒
这里有一个非常重要的细节:Delay Report命令总是由Sink设备发起,Source设备响应。这和其他大多数信令命令不同,其他命令通常由Source设备发起。
规范明确指出:
The SNK shall be the INT of the Delay Report procedure. The SRC shall be the ACP.
延迟报告的触发时机:
-
流配置完成后立即发送第一个延迟报告
-
当延迟变化超过一定范围时发送新的延迟报告
延迟报告是实现音视频同步的关键。如果Sink设备不支持延迟报告,Source设备只能使用默认的延迟值,可能会导致音视频不同步。
下面是AVDTP延迟报告流程的snoop:

这两个包完成了延迟报告的完整事务,是AVDTP 1.3版本引入的高级功能:
-
耳机(INT,Sink端)发送
Delay Report命令,告知手机自身的音频缓冲、解码、渲染总延迟 -
手机(ACP,Source端)接收并确认延迟值,返回
Accept响应 -
手机侧会将该延迟纳入音视频同步计算,调整视频播放或音频发送时序,保证声音与画面在耳机端同步呈现
这是AVDTP所有信令中角色唯一固定的命令:永远由Sink端作为发起方,Source端作为接收方。原因很简单:处理延迟是接收端的固有属性,只有Sink设备自己知道完整的链路耗时。
(1) AVDTP Delay Report Command(耳机→手机)
|---------------------|--------------|-------------------------------------|
| 字段 | 值 | 协议含义 |
| L2CAP Dst CID | 0x8C08 | 发往手机侧的AVDTP信令通道,发起方为耳机设备 |
| AVDTP Type | Command | 命令包 |
| AVDTP ID | Delay Report | 信号ID对应延迟报告命令 |
| Number of Addresses | 1 | 包含1个目标SEID |
| ACP SEID | 2 | 接收方(手机)的流端点标识符=2,对应手机侧的下行音频Source端点 |
| Delay | 300 ms | 耳机上报的总处理延迟 |
| L2CAP SDU长度 | 5字节 | 信令头(2字节)+ACP SEID(1字节)+延迟值(2字节) |
(2) AVDTP Delay Report Accept(手机→耳机)
|-------------------|-------------------|----------------|
| 字段 | 值 | 协议含义 |
| Packet Type | Response | 响应包 |
| Message Type | Accept | 成功接收并确认延迟值 |
| Transaction Label | 0 | 事务标签=0,与命令完全匹配 |
| Signal Identifier | AVDTP_DELAYREPORT | 信号ID与命令一致 |
| L2CAP SDU长度 | 2字节 | 仅信令头,无额外有效载荷 |
(3) 延迟值的协议细节与计算
①单位与量程
规范明确规定:延迟字段为16位无符号整数,单位为0.1毫秒。
-
理论量程:0 ~ 6553.5 ms
-
消费级耳机常规值:100 ~ 500 ms
-
低延迟游戏模式:40 ~ 150 ms
本次上报的300ms对应的原始值计算:
300 ms ÷ 0.1 ms/单位 = 3000 = 0x0BB8
与底部十六进制数据中的0B B8完全对应。
②延迟的组成部分
上报的300ms是端到端的总延迟,通常包含三部分:
-
接收缓冲延迟:空中数据包接收、去抖动缓冲,约100~200ms
-
解码延迟:SBC/AAC等编解码硬件处理耗时,约10~30ms
-
渲染延迟:DAC转换、功放输出、声学器件延迟,约20~50ms
③上报触发规则
延迟报告不是周期性发送,有严格的触发条件:
-
首次上报:流配置完成后必须立即发送第一个延迟报告
-
变更上报:只有当实际延迟偏离上次上报值、超出预设精度范围时,才发送新的报告
-
瞬时波动不触发:播放缓冲区的瞬时填充变化不触发上报,仅缓冲策略永久性调整(如抗干扰开大缓冲)才上报
(4)核心协议强制规则
①角色不可反转:Sink永远是延迟报告的发起方,Source永远是接收方。Source不能主动向Sink查询延迟,只能被动接收。
②幂等性处理:收到重复的延迟值报告,Source端应直接返回Accept,不做重复处理,避免同步逻辑频繁抖动。
③向后兼容:如果Sink不支持延迟报告,Source应使用默认延迟值(通常200~300ms)进行同步,不能因缺少报告而失效。
④双向流分别上报:对于通话等双向音频场景,上行麦克风流的Sink(手机)也需要向Source(耳机)上报上行延迟,才能实现完整的双向同步。
(5)开发常见坑点与最佳实践
高频坑点
①单位换算错误:最常见的错误是误以为延迟单位是1ms,导致同步计算偏差10倍,音画严重错位。必须牢记单位为0.1ms。
②频繁上报:部分设备周期性发送延迟报告,频繁触发同步调整,反而导致音画持续抖动。应严格遵循变更触发的规则。
③上报固定假值:部分低端耳机虽声明支持该功能,但上报的是写死的固定值,而非实际测量的延迟,会导致同步效果大打折扣。
④仅处理下行忽略上行:视频通话场景中,很多开发者只处理下行扬声器延迟,忽略上行麦克风延迟,造成对方端音画不同步。
最佳实践
应用层可在系统延迟报告基础上,增加用户可调节的偏移量,适配不同视频源的固有延迟
切换编解码、切换低延迟模式后,应主动等待新的延迟报告生效,再启动同步逻辑
对于不支持延迟报告的设备,应根据编解码类型和设备档位设置合理的默认值,而非统一使用固定值
3.3 通用拒绝 (General Reject)
通用拒绝流程用于拒绝一个无法识别的信令命令。

当接收方收到一个无法识别的信号ID时,必须发送General Reject响应,而不是忽略这个命令。如果忽略这个命令,发起方会一直等待响应,最终超时失败。
3.4 流强制终止 (Abort)
在正常的流生命周期之外,AVDTP还设计了一套异常兜底机制:Abort 强制终止流程。它的定位是异常场景的紧急刹车,不需要等待当前事务完成,不需要走优雅释放流程,可以在任意状态下直接将流打回Idle状态,强制释放所有资源。

规范明确指出
Abort procedure is used to terminate a stream immediately without waiting for the current procedure to complete。
这是它和Close命令最本质的区别:Close是正常的商务握手,Abort是紧急情况下的强制断电。
(1)信令格式
Abort命令的格式非常简洁,和Close命令结构完全一致:
信号ID:
0x0A(AVDTP_ABORT)地址数量:1,携带目标ACP SEID
命令总长度:3字节(2字节信令头 + 1字节SEID)
响应长度:2字节,仅返回Accept,不允许拒绝
(2)与Close的核心差异对比
|--------|---------------------------------|------------------|
| 维度 | Close 正常关闭 | Abort 强制终止 |
| 适用场景 | 正常结束播放/通话,优雅退出 | 异常错误、协议超时、强制断开 |
| 状态要求 | 可从Configured/Open/Streaming状态发起 | 任意状态均可发起,包括事务进行中 |
| 资源释放 | 按顺序优雅释放,等待数据处理完成 | 立即强制释放,丢弃所有未处理数据 |
| 响应规则 | 可根据状态返回错误 | 必须返回Accept,不允许拒绝 |
| 对端感知 | 对端有充足时间做收尾处理 | 对端需立即中断所有流程 |
| 后续恢复 | 需走完整建链流程 | 需走完整建链流程 |
(3)规范强制规则
①全状态可用:无论流当前处于Idle之外的任何状态,也无论是否有正在进行的信令事务,收到Abort命令后都必须立即终止所有操作,直接切换到Idle状态。
②不可拒绝:Abort命令不存在Reject响应。无论参数是否正确、状态是否合法,接收方都必须返回Accept,然后执行强制清理。这是为了保证异常场景下一定能重置状态,避免死锁。
③立即停止数据传输:收到Abort命令后,必须立刻停止所有媒体包、恢复包、报告包的收发,不允许再发送任何数据,也不需要等待缓冲区数据处理完成。
④完整资源释放:执行Abort后必须释放所有相关资源,包括L2CAP传输通道、编解码硬件、DMA缓冲区、定时器、流上下文内存,并且解锁流端点。
(4)典型触发场景
-
信令事务连续3次重传超时,状态机出现异常
-
检测到严重协议错误,如对端返回非法参数、状态机逻辑混乱
-
上层应用强制断开,如用户快速关闭蓝牙、切换音频设备
-
系统资源耗尽,无法继续维持音频流
-
射频环境极度恶劣,链路质量已无法支撑正常传输
(5)开发坑点与最佳实践
①不要滥用Abort代替Close:正常退出场景必须走Close流程,Abort仅用于异常兜底。频繁使用Abort会导致部分设备固件状态异常,下次连接出现兼容性问题。
②发送后立即本地清理:Abort是单向强制终止,发送命令后即可开始释放本地资源,不需要等待对端响应,避免因对端无响应导致资源泄漏。
③收到Abort必须彻底清理:很多开发者只清理了媒体缓冲区,漏掉了事务定时器、通道句柄、流上下文引用,导致后续新建流时出现资源冲突。
④重入保护:清理资源过程中要做好重入保护,避免在清理过程中又收到新的Abort命令,导致重复释放崩溃。
3.5 安全控制 (Security Control)
Security Control 是AVDTP为内容版权保护设计的专用透传信令通道,对应规范中的 Security Control procedure。它本身不定义任何安全算法,只负责在两端的内容保护模块之间透传控制消息,具体的认证、密钥交换逻辑由上层的内容保护协议定义。

规范说明
Security Control procedure is used to transfer content protection control messages between peer AVDTP entities.
简单来说,AVDTP只修了一条专用隧道,隧道里跑什么内容,由版权保护协议自己决定。
(1)信令格式
-
信号ID:
0x0B(AVDTP_SECURITY) -
地址数量:1,携带目标ACP SEID
-
有效载荷:可变长度,前1字节为内容保护类型标识,后续为透传的安全控制数据
-
响应:成功返回Accept,失败返回对应错误码
安全控制数据部分对AVDTP协议栈完全透明,协议栈只负责按原格式转发,不做任何解析和修改,最终交给上层的内容保护模块处理。
(2)与内容保护服务的绑定关系
安全控制流程和流配置中的内容保护服务类别(Service Category ID = 0x06)是强绑定的:
①只有在Set Configuration阶段启用了内容保护服务,后续才会使用Security Control命令
②执行时机通常在Open之后、Start之前:必须先完成完整的安全认证流程,验证两端版权保护能力匹配,才能启动媒体数据传输
③如果配置了内容保护但未完成认证就发送Start命令,接收方必须返回0x11 Invalid State错误
(3)常见的内容保护类型
蓝牙音频领域最常用的两种内容保护机制:
①SCMS-T(串行拷贝管理系统-传输):消费级蓝牙设备最普遍支持的保护机制,主要用于防止数字音频的非法二次拷贝。认证流程简单,开销极低,绝大多数带版权保护的音乐服务都使用该方案。
②DTCP(数字传输内容保护):更高安全等级的保护方案,包含完整的密钥交换和认证流程,支持加密传输,通常用于高清音视频、付费影视等高价值内容。
(4)开发注意点
①能力匹配优先:在Get All Capabilities阶段就要确认对端是否支持对应类型的内容保护,不要盲目配置,否则会在安全认证阶段失败。
②认证超时处理:安全认证通常有多轮交互,每一轮都要设置合理的超时时间。如果某一轮超时,应该终止流,不要无限等待。
③透传数据完整性:协议栈不能对安全控制数据做任何修改,包括字节序、长度、保留位,否则会导致认证失败。
④普通场景无需关注:绝大多数普通音频播放、通话场景都不会启用内容保护,日常开发中很少直接接触该流程。只有在做DRM相关的音视频业务时,才需要对接该能力。
(5)常见错误码
|-----------------|-------|--------------|
| 错误码 | 值 | 含义 |
| Invalid CP Type | 0x18 | 不支持该类型的内容保护 |
| Invalid State | 0x11 | 流状态不允许执行安全控制 |
| Bad Length | 0x02 | 安全控制数据长度非法 |
四、信令流程的常见问题和调试技巧
在实际开发中,信令流程会遇到各种各样的问题。下面我总结一些最常见的问题和调试技巧。
4.1 信令超时
信令超时是最常见的问题之一。信令超时的常见原因:
-
对方没有收到命令
-
对方收到了命令,但没有发送响应
-
响应在传输过程中丢失了
-
对方发送了响应,但事务标签不匹配
-
对方发送了响应,但包类型或消息类型错误
调试技巧:
-
使用空中抓包工具(比如Ellisys、Frontline)查看空中数据包,确认命令是否发送成功,对方是否发送了响应
-
检查响应的事务标签是否和命令的事务标签一致
-
检查响应的包类型是否为1(响应)
-
检查响应的消息类型是否正确
4.2 Set Configuration失败
Set Configuration失败是最常见的兼容性问题。Set Configuration失败的常见原因:
-
配置参数不是对方能力的子集
-
服务类别的顺序不对
-
缺少必选服务类别
-
SEID无效或者正在使用中
-
服务参数的格式错误
调试技巧:
-
仔细对比Get All Capabilities响应和Set Configuration命令,确认所有配置参数都是对方支持的
-
检查服务类别的顺序是否和Get All Capabilities响应中的顺序一致
-
检查是否包含了媒体传输服务和媒体编解码服务
-
检查SEID是否正确,是否正在使用中
-
检查服务参数的格式是否符合规范
4.3 流启动后没有声音
流启动后没有声音是一个非常令人头疼的问题,因为它可能有很多种原因。
常见原因:
-
媒体通道没有建立成功
-
媒体通道的MTU太小
-
发起方在收到Start响应之前就开始发送媒体数据
-
RTP包的格式错误
-
编解码参数不匹配
-
接收方的解码器没有启动
调试技巧:
-
检查媒体通道是否建立成功
-
检查媒体通道的MTU是否足够大
-
确认发起方是在收到Start响应之后才开始发送媒体数据
-
检查RTP包的格式是否正确,特别是Payload Type和时间戳
-
确认编解码参数和配置的参数一致
-
检查接收方的解码器是否已经启动
4.4 空中抓包分析技巧
空中抓包是调试蓝牙问题最有效的方法。下面是一些分析AVDTP信令的技巧:
-
过滤AVDTP包:在抓包工具中使用过滤器"avdtp"只显示AVDTP包
-
按事务标签分组:将相同事务标签的命令和响应放在一起查看
-
检查每个步骤的响应:确认每个命令都收到了正确的响应
-
对比规范:将抓包到的信令包和规范中的格式进行对比,找出不一致的地方
-
查看错误码:如果收到拒绝响应,查看错误码,了解拒绝的原因
五、信令原语与上层接口的对应关系
规范细定义了AVDTP向上层应用提供的信令原语,这些原语和我们上面讲的信令流程有明确的对应关系:
|----------|--------------------------------------------------------------------|
| 信令流程 | 上层接口原语 |
| 流端点发现 | AVDT_Discover_Req/Resp |
| 获取能力 | AVDT_Get_Capabilities_Req/Resp, AVDT_Get_All_Capabilities_Req/Resp |
| 流配置 | AVDT_Set_Configuration_Req/Resp |
| 打开流 | AVDT_Open_Req/Resp |
| 启动流 | AVDT_Start_Req/Resp |
| 暂停流 | AVDT_Suspend_Req/Resp |
| 关闭流 | AVDT_Close_Req/Resp |
| 流重配置 | AVDT_Reconfigure_Req/Resp |
| 延迟报告 | AVDT_Delay_Report_Req/Resp, AVDT_Delay_Report_Ind |
每个原语都有对应的参数和返回值,详细的定义可以参考规范附录A。
六、测验
问题:请详细描述AVDTP从设备发现到媒体播放的完整信令流程。
答案:
AVDTP从设备发现到媒体播放的完整信令流程包括以下步骤:
-
建立L2CAP信令通道,PSM=0x0019
-
发起方发送Discover命令,获取接收方的流端点列表
-
发起方发送Get All Capabilities命令,获取选定流端点的能力信息
-
发起方选择合适的配置参数,发送Set Configuration命令
-
配置成功后,按照媒体→报告→恢复的顺序建立传输通道
-
所有传输通道建立完成后,发起方发送Open命令
-
Open成功后,发起方发送Start命令
-
收到Start响应后,发起方开始通过媒体通道发送RTP媒体包
-
接收方收到RTP包后,解封装、解码并播放
问题:Set Configuration命令是AVDTP中最复杂的命令,请说明它的格式和处理规则。
答案:
Set Configuration命令的格式:
-
事务标签:4位
-
包类型:0(命令)
-
消息类型:0
-
信号ID:0x04(Set Configuration)
-
地址数量:2
-
ACP SEID:接收方的流端点标识符
-
INT SEID:发起方的流端点标识符
-
有效载荷:包含选择的服务类别和参数
处理规则:
-
配置参数必须是接收方能力的子集
-
服务类别的顺序必须和Get All Capabilities响应中的顺序相同
-
必须包含媒体传输服务和媒体编解码服务这两个必选服务类别
-
如果配置成功,双方必须锁定对应的流端点
-
如果配置失败,必须返回相应的错误码
问题:AVDTP的延迟报告功能是如何工作的?它有什么重要意义?
答案:
延迟报告功能允许Sink设备向Source设备报告自己的处理延迟,包括缓冲区延迟、解码延迟和渲染延迟。
工作流程:
-
流配置完成后,Sink设备立即向Source设备发送第一个Delay Report命令
-
在播放过程中,当延迟变化超过一定范围时,Sink设备发送新的Delay Report命令
-
Source设备收到延迟报告后,调整音频和视频的发送时间,使得它们在Sink设备上能够同时播放
重要意义:
延迟报告是实现精确音视频同步(唇音同步)的关键。如果没有延迟报告,Source设备只能使用默认的延迟值,可能会导致音视频不同步,影响用户体验。