【AVRCP】规范精讲[27]: 音箱开机后发生了什么?媒体接收器完整初始化流程深度拆解

上次我们聊了媒体接收器反向控制的基本概念,但很多人不知道,当你打开蓝牙音箱的电源开关后,手机和音箱之间会进行一系列复杂的握手和同步操作。这些操作决定了你的音箱能否正确显示歌曲信息、能否响应控制命令、能否同步播放状态。本文就对照官方的消息序列图,一步一步拆解媒体接收器开机后的完整初始化流程,每一个步骤都有对应的真实代码实现。


目录

一、先搞清楚谁在主动连接

二、第一阶段:播放器发现与基本信息同步

[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能够更快地响应状态变化。


相关推荐
qq_366566501 小时前
短视频批量翻译+配音自动化:Python脚本处理TikTok/Reels/Shorts全流程
python·chatgpt·自动化·音视频·媒体
3DVisionary2 小时前
模具电极3D检测真实案例:手机后盖注塑模石墨电极全流程实录
人工智能·3d·智能手机·案例分析·蓝光三维扫描·模具检测·石墨电极
阿乔外贸日记19 小时前
埃塞俄比亚出口全流程注意事项
大数据·人工智能·智能手机·云计算·汽车
智讯天下1 天前
专业的高端智能照明品牌哪家好?从光学技术、系统稳定性、设计认证、服务保障四个维度看
人工智能·智能手机
STDD1 天前
ntfy 自托管推送通知服务搭建:一条 curl 命令向手机发送通知
java·开发语言·智能手机
ai产品老杨1 天前
【架构深评】打破多品牌壁垒:如何基于 GB28181 与 RTSP 栈,构建高解耦的 AI 视频流媒体管理平台?(附源码交付)
人工智能·架构·媒体
歪歪歪比巴卜1 天前
企业新媒体矩阵规模化后的治理结构与数据能力研究(2026)
大数据·矩阵·媒体
2601_954706491 天前
2026 上半年云手机终极对接测评:雷电云、红手指、VMOS、川川云与傲晨云深度横评
智能手机
wulechun1 天前
深度解析PaddlePaddle/awesome-DeepLearning:从理论到实战的全栈深度学习资源库
智能手机