在蓝牙音频的世界里,版本不兼容是开发者最头疼的问题之一。你可能遇到过这样的情况:用一个老款蓝牙耳机控制新手机播放音乐,有时候能正常工作,有时候却毫无反应。这背后其实是AVRCP协议不同版本之间的交互逻辑在起作用。本文就来深入拆解一个最常见也最容易被忽视的场景:传统控制器(Legacy CT)如何与1.4版本目标设备(v1.4 TG)完成播放命令的交互。
目录
[2.1 命令发送阶段](#2.1 命令发送阶段)
[2.2 命令响应阶段](#2.2 命令响应阶段)
[2.3 状态通知阶段](#2.3 状态通知阶段)
[3.1 版本协商过程](#3.1 版本协商过程)
[3.2 命令的幂等性问题](#3.2 命令的幂等性问题)
[3.3 超时处理](#3.3 超时处理)
[5.1 播放命令没有响应](#5.1 播放命令没有响应)
[5.2 播放状态不同步](#5.2 播放状态不同步)
[5.3 连续按两次播放按钮导致暂停](#5.3 连续按两次播放按钮导致暂停)
一、为什么这个场景如此重要
蓝牙技术发展了二十多年,市场上同时存在着从AVRCP 1.0到1.6的各种设备。一个优秀的蓝牙产品必须能够与尽可能多的旧设备兼容,否则就会失去大量用户。
Legacy CT指的是支持AVRCP 1.0、1.3或更早版本的控制器设备,比如老款蓝牙耳机、车载蓝牙系统、遥控器等。这些设备只实现了最基本的控制功能,不支持1.4版本新增的浏览通道和绝对音量调节。
v1.4 TG则是支持AVRCP 1.4版本的目标设备,比如现代智能手机、平板电脑、智能音箱等。这些设备不仅支持所有传统命令,还实现了1.4版本的新特性。
当这两种设备连接时,v1.4 TG必须能够正确理解并响应Legacy CT发送的所有传统命令,同时不能期望Legacy CT能够理解1.4版本的新特性。这就像一个现代人要和一个只会说古文的人交流,必须用对方能听懂的语言说话。
二、播放命令交互的完整流程
播放命令是AVRCP中最基本也是最常用的命令。整个交互过程可以分为三个阶段:命令发送、命令响应和状态通知。

2.1 命令发送阶段
当用户按下Legacy CT上的播放按钮时,CT会通过AVCTP控制通道向TG发送一个AV/C Pass Through命令。这个命令的格式如下:
cpp
AV/C Command Frame:
CType: 0x00 (CONTROL)
Subunit_type: 0x09 (Panel)
Subunit_id: 0x00
Opcode: 0x7C (Pass Through)
Operation_id: 0x44 (PLAY)
State: 0x00 (PRESSED)
这里有几个关键点需要注意:
-
CType字段设置为CONTROL,表示这是一个控制命令
-
Subunit_type设置为Panel,表示这是面板按钮操作
-
Opcode设置为Pass Through,表示这是一个透传命令
-
Operation_id设置为PLAY,表示具体的操作是播放
-
State字段设置为PRESSED,表示按钮被按下
紧接着,CT会发送第二个相同的命令,但State字段设置为RELEASED,表示按钮被释放。这两个命令必须成对出现,否则TG可能不会执行任何操作。
2.2 命令响应阶段
当v1.4 TG收到第一个PLAY PRESSED命令时,它会立即检查自己的当前状态。如果当前处于暂停或停止状态,TG会开始播放音乐;如果已经在播放状态,TG可能会忽略这个命令或者执行其他操作,具体取决于设备的实现。
然后,TG会向CT发送一个AV/C响应帧:
cpp
AV/C Response Frame:
CType: 0x09 (ACCEPTED)
Subunit_type: 0x09 (Panel)
Subunit_id: 0x00
Opcode: 0x7C (Pass Through)
Operation_id: 0x44 (PLAY)
State: 0x00 (PRESSED)
CType字段设置为ACCEPTED,表示TG已经接受并执行了这个命令。如果TG无法执行这个命令,比如当前没有可播放的媒体,它会返回REJECTED响应。
当TG收到第二个PLAY RELEASED命令时,它会再次发送一个ACCEPTED响应。至此,整个播放命令的请求-响应过程就完成了。
2.3 状态通知阶段
这是最容易被误解的部分。很多人以为播放命令执行完成后,交互就结束了。但实际上,v1.4 TG还会主动向CT发送一个播放状态变化的通知。
这个通知使用的是AVRCP 1.3版本引入的RegisterNotification机制。当TG的播放状态从暂停变为播放时,它会向所有已经注册了播放状态变化通知的CT发送一个事件:
cpp
AV/C Notification Frame:
CType: 0x0F (NOTIFICATION)
Subunit_type: 0x09 (Panel)
Subunit_id: 0x00
Opcode: 0x00 (Vendor Dependent)
Company ID: 0x001958 (Bluetooth SIG)
PDU ID: 0x51 (RegisterNotification)
Event ID: 0x01 (Playback Status Changed)
Playback Status: 0x01 (PLAYING)
这里有一个非常重要的细节:Legacy CT可能没有注册任何通知,甚至可能不理解这个通知帧。那么v1.4 TG应该怎么做呢?
规范中明确规定:TG必须能够处理CT不响应通知的情况。如果CT没有注册播放状态变化通知,TG可以选择不发送这个通知;如果发送了通知但没有收到CT的响应,TG不能因此而断开连接或停止工作。
这就是为什么有些老款蓝牙耳机在播放音乐时,耳机上的指示灯不会变化,因为它们不支持播放状态通知。
三、关键技术细节解析
3.1 版本协商过程
在AVRCP连接建立之初,CT和TG会通过SDP服务发现对方支持的版本。Legacy CT会在SDP记录中声明自己支持的最高版本,比如1.3。v1.4 TG会看到这个版本号,并在后续的交互中只使用CT支持的特性。
具体来说,v1.4 TG不会尝试建立浏览通道,因为Legacy CT不支持;也不会发送绝对音量调节命令,因为Legacy CT可能无法理解。
3.2 命令的幂等性问题
播放命令是幂等的吗?规范中没有明确规定。不同的设备可能有不同的实现:
有些设备在播放状态下收到播放命令会忽略
有些设备会重新开始播放当前歌曲
有些设备会切换到下一首歌曲
这就是为什么有时候你按一下播放按钮,却跳到了下一首歌曲的原因。为了保证兼容性,建议在实现时采用第一种行为:在播放状态下收到播放命令时忽略。
3.3 超时处理
当CT发送一个命令后,它会等待TG的响应。如果在规定的时间内没有收到响应,CT会认为命令失败。规范中规定的超时时间是1000毫秒。
v1.4 TG必须在收到命令后的1000毫秒内发送响应。如果需要执行耗时操作,比如加载媒体文件,TG应该先发送一个INTERIM响应,然后在操作完成后再发送最终的ACCEPTED或REJECTED响应。
四、Android系统中的代码实现
下面我们来看一下在Android系统中,v1.4 TG是如何处理Legacy CT发送的播放命令的。
cpp
// packages/modules/Bluetooth/system/stack/avrcp/avrcp_api.cc
void AvrcpApi::HandlePassThroughCommand(uint8_t operation_id, uint8_t state) {
ALOGD("HandlePassThroughCommand: operation_id=0x%02X, state=0x%02X",
operation_id, state);
if (operation_id == AVRCP_PASS_THROUGH_OP_PLAY) {
if (state == AVRCP_PASS_THROUGH_STATE_PRESSED) {
// 处理播放按钮按下事件
media_session_->GetTransportControls()->Play();
// 发送ACCEPTED响应
SendPassThroughResponse(AVRC_CTYPE_ACCEPTED, operation_id, state);
// 通知所有注册的CT播放状态已经变化
NotifyPlaybackStatusChanged(AVRC_PLAY_STATUS_PLAYING);
} else if (state == AVRCP_PASS_THROUGH_STATE_RELEASED) {
// 处理播放按钮释放事件
// 通常不需要做任何操作,只需要发送响应
SendPassThroughResponse(AVRC_CTYPE_ACCEPTED, operation_id, state);
}
}
// 处理其他操作...
}
void AvrcpApi::NotifyPlaybackStatusChanged(uint8_t playback_status) {
ALOGD("NotifyPlaybackStatusChanged: playback_status=0x%02X",
playback_status);
// 遍历所有已连接的CT
for (auto& ct : connected_cts_) {
// 检查CT是否注册了播放状态变化通知
if (ct->IsNotificationRegistered(AVRC_EVT_PLAY_STATUS_CHANGED)) {
// 发送通知
SendRegisterNotificationResponse(
ct->GetAddress(),
AVRC_CTYPE_NOTIFICATION,
AVRC_EVT_PLAY_STATUS_CHANGED,
&playback_status,
sizeof(playback_status));
}
}
}
从这段代码可以看出:
-
Android系统会分别处理按钮按下和释放事件
-
在处理按下事件时,会调用媒体会话的Play方法开始播放
-
立即发送ACCEPTED响应
-
只向已经注册了通知的CT发送播放状态变化通知
五、常见问题与调试技巧
5.1 播放命令没有响应
如果Legacy CT发送了播放命令但v1.4 TG没有响应,可能的原因有:
CT发送的命令格式不正确
TG当前没有可播放的媒体
TG的媒体会话没有被激活
蓝牙连接不稳定
调试方法:使用HCI日志工具抓取蓝牙数据包,检查CT是否发送了正确的PLAY PRESSED和PLAY RELEASED命令,以及TG是否发送了ACCEPTED响应。
5.2 播放状态不同步
有时候会出现CT显示的播放状态和TG实际的播放状态不一致的情况。这通常是因为CT没有注册播放状态变化通知,或者TG没有正确发送通知。
调试方法:检查CT是否在连接建立后发送了RegisterNotification命令,以及TG是否在播放状态变化时发送了通知。
5.3 连续按两次播放按钮导致暂停
有些设备在播放状态下收到播放命令会切换到暂停状态。这是因为这些设备将播放命令实现为切换命令,而不是单纯的播放命令。
解决方法:在TG端实现时,确保在播放状态下收到播放命令时忽略,而不是切换到暂停状态。
六、测验
题目:当Legacy CT(AVRCP 1.3)与v1.4 TG连接时,TG是否会建立浏览通道?为什么?(某蓝牙芯片公司2025年校招面试题)
答案:
不会建立浏览通道。因为浏览通道是AVRCP 1.4版本新增的特性,Legacy CT不支持。在SDP服务发现阶段,TG会发现CT支持的最高版本是1.3,因此不会尝试建立浏览通道。如果TG强行建立浏览通道,CT会拒绝连接,导致整个AVRCP连接失败。
题目:AVRCP播放命令为什么需要发送PRESSED和RELEASED两个状态?只发送一个PRESSED状态可以吗?(某手机厂商2024年社招面试题)
答案:
不可以只发送一个PRESSED状态。规范中明确规定,所有Pass Through命令都必须包含PRESSED和RELEASED两个状态。这是为了模拟真实的按钮操作,区分短按和长按。有些设备会根据按钮按下的时间长短执行不同的操作,比如短按播放,长按快进。如果只发送PRESSED状态,TG可能会认为按钮一直被按住,从而执行长按操作,或者干脆忽略这个命令。
题目:v1.4 TG在执行完播放命令后,是否必须向Legacy CT发送播放状态变化通知?为什么?
答案:
不是必须的。只有当Legacy CT已经注册了播放状态变化通知时,TG才需要发送通知。如果CT没有注册通知,TG可以选择不发送。即使发送了通知,如果CT没有响应,TG也不能因此而断开连接或停止工作。这是为了保证向下兼容性,因为很多Legacy CT不支持RegisterNotification机制。