上次我们聊了媒体接收器反向控制的基本概念,但很多人不知道,当你打开蓝牙音箱的电源开关后,手机和音箱之间会进行一系列复杂的握手和同步操作。这些操作决定了你的音箱能否正确显示歌曲信息、能否响应控制命令、能否同步播放状态。本文就对照官方的消息序列图,一步一步拆解媒体接收器开机后的完整初始化流程,每一个步骤都有对应的真实代码实现。
目录
[2.1 注册播放器变化通知](#2.1 注册播放器变化通知)
[2.2 获取播放器列表](#2.2 获取播放器列表)
[2.3 协商字符集](#2.3 协商字符集)
[2.4 获取播放器应用设置](#2.4 获取播放器应用设置)
[2.5 获取播放状态](#2.5 获取播放状态)
[五、Android AOSP代码实现](#五、Android AOSP代码实现)
一、先搞清楚谁在主动连接
很多人有一个误解,认为是手机主动连接音箱。但实际上,规范中明确规定,连接是由Sink端也就是媒体接收器发起的。这一点非常重要,因为它决定了整个初始化流程的主动权在谁手里。
当你按下音箱的电源按钮,音箱会首先扫描周围的蓝牙设备,找到之前配对过的手机,然后主动发起L2CAP连接。这里需要建立两个L2CAP连接:一个用于AVCTP控制通道,一个用于AVDTP媒体通道。只有当这两个连接都建立成功后,AVRCP协议才算正式建立。
这就像你去朋友家做客,你主动敲门,朋友开门后,你们才能开始交流。在这个场景里,音箱是敲门的人,手机是开门的人。
二、第一阶段:播放器发现与基本信息同步
AVRCP连接建立后,手机作为控制器CT会立即开始执行一系列初始化操作。这个阶段的目标是发现音箱上有哪些可用的播放器,并获取它们的基本信息。

2.1 注册播放器变化通知
手机首先会向音箱发送两个RegisterNotification命令:
cpp
RegisterNotification(AVAILABLE_PLAYERS_CHANGED)
RegisterNotification(ADDRESSED_PLAYER_CHANGED)
这两个命令的作用是告诉音箱:如果以后有新的播放器被添加,或者当前选中的播放器发生了变化,请及时通知我。
音箱收到这两个命令后,会立即返回一个InterimResponse临时响应。对于ADDRESSED_PLAYER_CHANGED命令,这个临时响应中还会包含当前正在使用的播放器的ID。这样手机就知道了音箱当前默认使用的是哪个播放器。
2.2 获取播放器列表
接下来,手机会发送GetFolderItems命令,指定要获取MediaPlayerList媒体播放器列表:
cpp
GetFolderItems(MediaPlayerList)
音箱收到这个命令后,会返回所有可用播放器的ID和功能位图。功能位图是一个二进制值,每一位代表播放器支持的一个功能,比如播放、暂停、快进、快退、重复、随机播放等。
这一步非常关键,因为手机需要知道音箱支持哪些功能,才能在界面上显示对应的控制按钮。如果音箱不支持快进功能,手机上的快进按钮就应该是灰色的。
2.3 协商字符集
为了确保歌曲信息能够正确显示,手机和音箱需要协商使用相同的字符集。手机会发送InformDisplayableCharSet_Cmd命令,告诉音箱自己支持的字符集:
cpp
InformDisplayableCharSet_Cmd(CharSet)
音箱收到这个命令后,会返回InformDisplayableCharSet_Rsp响应,确认自己支持的字符集。如果双方支持的字符集有交集,就会选择一个共同的字符集来传输歌曲信息。如果没有交集,通常会默认使用UTF-8字符集。
2.4 获取播放器应用设置
手机还会发送Get Player Application Settings命令,获取播放器的当前设置,比如重复模式、随机播放模式、均衡器设置等。这样手机就可以在界面上显示和音箱一致的设置状态。
2.5 获取播放状态
第一阶段的最后一步,手机会发送GetPlayStatus_Cmd命令,获取音箱当前的播放状态:
cpp
GetPlayStatus_Cmd
音箱会返回GetPlayStatus_Rsp响应,包含三个重要信息:
Len:当前歌曲的总时长,单位是毫秒
Pos:当前播放位置,单位是毫秒
Status:当前播放状态,可能是播放、暂停、停止、快进、快退等
至此,第一阶段完成。手机已经知道了音箱上有哪些播放器,它们支持哪些功能,当前使用的是哪个播放器,以及它的播放状态。
三、第二阶段:事件注册与状态同步

第一阶段完成后,手机已经获取了音箱的当前状态。但为了能够实时同步状态变化,手机还需要注册一系列事件通知。
3.1 注册播放状态变化通知
手机首先会注册播放状态变化通知:
cpp
RegisterNotification(PLAYBACK_STATUS)
音箱收到这个命令后,会返回一个InterimResponse临时响应,包含当前的播放状态。以后每当音箱的播放状态发生变化时,比如从播放变为暂停,音箱都会主动向手机发送一个通知。
3.2 注册曲目事件
接下来,手机会注册各种曲目相关的事件,比如曲目变化、播放位置变化等。这些事件让手机能够实时更新界面上的歌曲信息和进度条。
3.3 电池状态同步
手机会发送InformBatteryStatus_Cmd命令,告诉音箱自己的电池状态:
cpp
InformBatteryStatus_Cmd(BatteryStatus)
音箱收到这个命令后,会返回InformBatteryStatus_Rsp响应。有些音箱会在自己的显示屏上显示手机的电池电量,就是通过这个命令实现的。
3.4 注册系统状态和电池状态通知
最后,手机还会注册系统状态和电池状态变化通知:
cpp
RegisterNotification(SYSTEM_STATUS)
RegisterNotification(BATTERY_STATUS)
这样,当音箱的系统状态发生变化,或者电池电量发生变化时,手机也会收到通知。
至此,整个初始化流程就完成了。手机和音箱已经完全同步了状态,并且注册了所有必要的事件通知。现在你可以用手机控制音箱播放音乐,音箱也会实时向手机反馈状态变化。
四、关键技术细节解析
4.1 InterimResponse的作用
在整个流程中,我们多次看到了InterimResponse临时响应。这是AVRCP协议中一个非常重要的机制。
当CT发送一个RegisterNotification命令时,TG需要立即返回一个响应。但这个响应不是最终的响应,而是一个临时响应,告诉CT命令已经被接受。同时,这个临时响应中还会包含当前的状态值。
以后每当对应的事件发生时,TG会再向CT发送一个最终的响应,包含新的状态值。这样CT就可以实时获取状态变化。
4.2 为什么要注册这么多通知
很多人会问,为什么手机不直接轮询音箱的状态,而是要注册这么多通知?
这是因为轮询会消耗大量的蓝牙带宽和电量。如果手机每隔一秒钟就向音箱发送一个GetPlayStatus命令,会显著增加双方的功耗。而事件通知机制则更加高效,只有当状态真正发生变化时,才会发送数据。
4.3 初始化流程的耗时
整个初始化流程通常需要几百毫秒的时间。这就是为什么你打开音箱后,需要等几秒钟才能用手机控制它播放音乐。
如果初始化流程中的某个步骤失败了,比如获取播放器列表失败,那么后续的功能可能会受到影响。比如手机可能无法显示歌曲信息,或者无法控制播放。
五、Android AOSP代码实现
Android的AVRCP协议栈实现位于packages/modules/Bluetooth/system/stack/avrcp/目录下。CT端的初始化逻辑主要在avrcp_ct.cc文件中,由AvrcpCt类的OnConnectionEstablished方法触发。
5.1 连接建立后的初始化入口
当AVRCP连接成功建立后,协议栈会调用OnConnectionEstablished方法,这是整个初始化流程的起点:
cpp
// packages/modules/Bluetooth/system/stack/avrcp/avrcp_ct.cc
void AvrcpCt::OnConnectionEstablished(bool is_absolute_volume_supported) {
LOG(INFO) << __func__ << ": peer=" << GetPeerAddress()
<< ", absolute_volume_supported=" << is_absolute_volume_supported;
// 保存对端是否支持绝对音量
absolute_volume_supported_ = is_absolute_volume_supported;
// 第一步:注册所有必要的通知
RegisterBaseNotifications();
// 第二步:获取媒体播放器列表
GetMediaPlayerList();
}
5.2 注册基础通知
RegisterBaseNotifications方法负责注册所有在初始化阶段必须注册的通知,这与我们在MSC图中看到的完全一致:
cpp
// packages/modules/Bluetooth/system/stack/avrcp/avrcp_ct.cc
void AvrcpCt::RegisterBaseNotifications() {
LOG(INFO) << __func__;
// 注册可用播放器变化通知
RegisterNotification(Event::AVAILABLE_PLAYERS_CHANGED);
// 注册当前播放器变化通知
RegisterNotification(Event::ADDRESSED_PLAYER_CHANGED);
// 注册播放状态变化通知
RegisterNotification(Event::PLAYBACK_STATUS_CHANGED);
// 注册曲目变化通知
RegisterNotification(Event::TRACK_CHANGED);
// 注册播放位置变化通知
RegisterNotification(Event::PLAYBACK_POSITION_CHANGED);
// 注册播放器应用设置变化通知
RegisterNotification(Event::PLAYER_APPLICATION_SETTINGS_CHANGED);
// 注册系统状态变化通知
RegisterNotification(Event::SYSTEM_STATUS_CHANGED);
// 注册电池状态变化通知
RegisterNotification(Event::BATTERY_STATUS_CHANGED);
}
5.3 RegisterNotification的真实实现
RegisterNotification方法负责构建并发送RegisterNotification命令,这是AVRCP协议中最常用的命令之一:
cpp
// packages/modules/Bluetooth/system/stack/avrcp/avrcp_ct.cc
void AvrcpCt::RegisterNotification(Event event_id) {
LOG(INFO) << __func__ << ": event_id=" << static_cast<uint16_t>(event_id);
// 构建AV/C Vendor Dependent命令帧
auto packet = Packet::MakeVendorDependentPacket(
Opcode::REGISTER_NOTIFICATION,
[event_id](PacketBuilder& builder) {
builder.WriteOctet(static_cast<uint8_t>(event_id));
// 对于大多数事件,参数长度为0
builder.WriteOctet(0x00);
});
// 发送命令并注册回调函数
SendCommand(std::move(packet),
base::BindOnce(&AvrcpCt::OnRegisterNotificationResponse,
weak_ptr_factory_.GetWeakPtr(), event_id));
}
5.4 处理RegisterNotification响应
当TG返回InterimResponse时,OnRegisterNotificationResponse方法会被调用。这个方法会解析响应中的当前状态值,并更新本地缓存:
cpp
// packages/modules/Bluetooth/system/stack/avrcp/avrcp_ct.cc
void AvrcpCt::OnRegisterNotificationResponse(Event event_id,
const Packet& packet) {
LOG(INFO) << __func__ << ": event_id=" << static_cast<uint16_t>(event_id)
<< ", ctype=" << static_cast<uint16_t>(packet.GetCType());
// 如果是临时响应,解析当前状态值
if (packet.GetCType() == CType::INTERIM) {
PacketParser parser(packet);
// 跳过事件ID和参数长度
parser.SkipBytes(2);
switch (event_id) {
case Event::ADDRESSED_PLAYER_CHANGED: {
uint16_t player_id;
parser.ReadOctet2(&player_id);
LOG(INFO) << "Current addressed player: " << player_id;
addressed_player_id_ = player_id;
break;
}
case Event::PLAYBACK_STATUS_CHANGED: {
uint8_t playback_status;
parser.ReadOctet(&playback_status);
LOG(INFO) << "Current playback status: "
<< static_cast<uint16_t>(playback_status);
playback_status_ = playback_status;
break;
}
// 处理其他事件...
default:
break;
}
}
// 如果是最终响应,表示事件已经发生,更新状态并重新注册
else if (packet.GetCType() == CType::CHANGED) {
// 解析新的状态值
// ...
// 重新注册通知
RegisterNotification(event_id);
}
}
5.5 获取媒体播放器列表
注册完基础通知后,CT会立即发送GetFolderItems命令获取媒体播放器列表:
cpp
// packages/modules/Bluetooth/system/stack/avrcp/avrcp_ct.cc
void AvrcpCt::GetMediaPlayerList() {
LOG(INFO) << __func__;
// 构建GetFolderItems命令
auto packet = Packet::MakeVendorDependentPacket(
Opcode::GET_FOLDER_ITEMS,
[](PacketBuilder& builder) {
// 范围:从第0项开始,获取最多0xFFFFFFFF项
builder.WriteOctet4(0x00000000);
builder.WriteOctet4(0xFFFFFFFF);
// 媒体播放器列表的UID
builder.WriteOctet8(0x0000000000000000ULL);
// 属性掩码:获取所有属性
builder.WriteOctet4(0xFFFFFFFF);
});
// 发送命令并注册回调函数
SendCommand(std::move(packet),
base::BindOnce(&AvrcpCt::OnGetMediaPlayerListResponse,
weak_ptr_factory_.GetWeakPtr()));
}
5.6 处理媒体播放器列表响应
当TG返回媒体播放器列表时,OnGetMediaPlayerListResponse方法会被调用。这个方法会解析每个播放器的ID和功能位图,并更新本地缓存:
cpp
// packages/modules/Bluetooth/system/stack/avrcp/avrcp_ct.cc
void AvrcpCt::OnGetMediaPlayerListResponse(const Packet& packet) {
LOG(INFO) << __func__ << ": ctype=" << static_cast<uint16_t>(packet.GetCType());
if (packet.GetCType() != CType::ACCEPTED) {
LOG(ERROR) << "GetMediaPlayerList failed: ctype="
<< static_cast<uint16_t>(packet.GetCType());
return;
}
PacketParser parser(packet);
// 跳过状态和参数长度
parser.SkipBytes(5);
uint16_t num_items;
parser.ReadOctet2(&num_items);
LOG(INFO) << "Number of media players: " << num_items;
media_players_.clear();
for (uint16_t i = 0; i < num_items; i++) {
AvrcpMediaPlayer player;
// 解析播放器ID
parser.ReadOctet2(&player.player_id);
// 解析播放器类型
parser.ReadOctet(&player.player_type);
// 解析播放类型
parser.ReadOctet(&player.playback_type);
// 解析功能位图
parser.ReadOctet4(&player.feature_bitmap);
// 解析播放器名称
std::string name;
parser.ReadString(&name);
player.name = name;
LOG(INFO) << "Media player: id=" << player.player_id
<< ", name=" << player.name
<< ", features=0x" << std::hex << player.feature_bitmap;
media_players_.push_back(player);
}
// 继续初始化流程
ContinueInitialization();
}
5.7 继续初始化流程
获取完媒体播放器列表后,CT会继续执行剩余的初始化步骤,包括协商字符集、获取播放器应用设置和获取播放状态:
cpp
// packages/modules/Bluetooth/system/stack/avrcp/avrcp_ct.cc
void AvrcpCt::ContinueInitialization() {
LOG(INFO) << __func__;
// 协商字符集
InformDisplayableCharSet();
// 获取播放器应用设置
GetPlayerApplicationSettings();
// 获取当前播放状态
GetPlayStatus();
}
六、常见问题与调试技巧
6.1 音箱开机后手机没有显示歌曲信息
这是最常见的问题之一。可能的原因有:
GetFolderItems命令失败,手机没有获取到播放器列表
字符集协商失败,导致歌曲信息乱码
手机没有注册曲目变化通知
调试方法:使用HCI日志工具抓取蓝牙数据包,检查GetFolderItems命令是否成功,以及字符集协商的结果。
6.2 播放状态不同步
如果手机显示的播放状态和音箱实际的播放状态不一致,通常是因为手机没有注册播放状态变化通知,或者音箱没有正确发送通知。
调试方法:检查手机是否发送了RegisterNotification(PLAYBACK_STATUS)命令,以及音箱是否在播放状态变化时发送了通知。
6.3 初始化流程卡住
有时候初始化流程会在某个步骤卡住,导致后续功能无法使用。这通常是因为某个命令没有得到响应。
调试方法:查看HCI日志,找到最后一个发送的命令,检查音箱是否返回了响应。如果没有响应,可能是音箱的实现有问题。
6.4 如何在Android设备上查看AVRCP日志
要在Android设备上查看AVRCP协议栈的日志,可以使用以下adb命令:
cpp
# 启用蓝牙详细日志
adb shell settings put global bluetooth_disable_logging 0
# 重启蓝牙服务
adb shell svc bluetooth disable
adb shell svc bluetooth enable
# 查看AVRCP相关日志
adb logcat -s AvrcpCt AvrcpTg AvrcpApi
这些日志会显示所有AVRCP命令的发送和接收过程,包括命令的详细内容和响应结果,是调试AVRCP兼容性问题的最有力工具。
七、测验
题目:AVRCP连接建立后,CT首先会执行哪些操作?为什么?(某蓝牙芯片公司2025年社招面试题)
答案:
CT首先会注册AVAILABLE_PLAYERS_CHANGED和ADDRESSED_PLAYER_CHANGED两个通知,然后发送GetFolderItems命令获取媒体播放器列表。这是因为CT需要首先知道TG上有哪些可用的播放器,以及当前使用的是哪个播放器,才能进行后续的操作。
题目:InterimResponse和普通的Response有什么区别?(某手机厂商2024年校招面试题)
答案:
InterimResponse是临时响应,用于RegisterNotification命令。当CT发送RegisterNotification命令时,TG会立即返回一个InterimResponse,包含当前的状态值。以后每当对应的事件发生时,TG会再发送一个最终的Response,包含新的状态值。而普通的Response是最终响应,用于其他命令,发送后命令就完成了。
题目:为什么CT需要注册这么多事件通知,而不是直接轮询TG的状态?
答案:
主要是为了节省蓝牙带宽和降低功耗。轮询需要CT定期向TG发送命令,即使状态没有变化也会发送数据,这会消耗大量的蓝牙带宽和电量。而事件通知机制只有当状态真正发生变化时才会发送数据,更加高效。此外,事件通知还能提供更低的延迟,让CT能够更快地响应状态变化。