做过车载蓝牙、智能音箱AVRCP开发的同学,一定都处理过用户在车机上浏览手机歌单、点歌直接播放的需求。很多时候车机点了歌,要么手机里还是放着旧歌,要么车机界面的播放状态和队列半天不刷新,用户体验直接拉胯。本文就把AVRCP里这套浏览并直接播放的完整交互流程讲透,从时序、状态机到避坑要点一次性梳理清楚,彻底解决车机点歌不同步的兼容性问题。
目录
[一、什么是AVRCP Browse and Play?](#一、什么是AVRCP Browse and Play?)
一、什么是AVRCP Browse and Play?
简单说,就是控制端CT(比如车机)先浏览目标端TG(比如手机)的媒体库,选好歌曲后,直接发送播放命令,让TG清空当前播放队列、播放新歌曲并重建队列,同时让车机同步更新播放状态、歌曲信息和队列列表。
这个场景和我们之前讲的搜索播放、添加到队列都不一样:它的核心是一键切换播放上下文,用户点一首歌,就相当于完全替换了之前的播放列表,从头开始播放新歌。
打个比方,就像你在视频App里,从一个播放列表点进了另一个视频,App会自动停掉旧视频、清空旧列表,直接播放新视频并生成新的播放队列。
二、核心交互流程
我们结合时序图,一步步拆解CT和TG的每一步交互,搞懂每个动作背后的协议逻辑。

1. 初始状态:正在播放,队列正常
TG端的播放器正在播放队列中的歌曲,CT和TG的AVRCP会话正常,车机上显示着当前的播放队列和歌曲标题。
2. 第一步:CT浏览媒体库,用户选择歌曲
用户在车机上浏览手机的媒体库,比如歌手列表、专辑列表,最终选好了要播放的歌曲。这一步是CT端的用户交互,也是整个流程的起点。
3. 第二步:CT发送PlayItem命令,请求播放指定歌曲
CT发送PlayItem命令,携带目标歌曲的UID,请求TG直接播放这首歌。
伪代码示例:
cpp
// 伪代码:发送播放指定歌曲命令
avrcp_pkt_t play_item_pkt;
uint64_t target_song_uid = 0x0000000000012345; // 目标歌曲的唯一标识
avrcp_build_play_item(&play_item_pkt, target_song_uid);
avrcp_send_pkt(session, &play_item_pkt);
TG收到命令后,会执行一系列内部操作:停止当前播放、清空旧队列、把新歌加入新队列并开始播放。
4. 第三步: TG 发送状态变更通知,同步播放状态变化
TG会按顺序发送三条关键通知,这是整个流程的灵魂,直接决定了车机界面能不能正确更新:
(1)PlayStatusChangedNotification(Stopped):先通知CT播放状态已变为停止,告诉车机旧歌停了。
(2)NowPlayingContentChangedNotification:通知CT当前播放队列的内容已经变更,旧队列被清空、新队列已生成。
(3)PlayStatusChangedNotification(Playing):通知CT播放状态已恢复为播放,新歌开始播放了。
(4)TrackChangedNotification:通知CT当前播放的轨道已变更,同步新歌的播放状态。
5. 第四步:CT拉取新歌信息,更新界面
CT收到通知后,需要主动获取两个关键信息,才能完成界面刷新:
发送GetAttributes_Cmd,请求获取当前播放歌曲的元数据,比如歌曲名、歌手名、专辑名。
发送GetFolderItems(NowPlaying),请求获取更新后的播放队列列表,拿到新队列里的所有歌曲信息。
拿到这些数据后,车机就可以显示新的歌曲标题和新的播放队列了。
三、关键协议逻辑与原文解析
整个流程里,有几个容易被忽略的关键点,直接影响用户体验和兼容性:
Player stops, replaces queue with new item (and context) and starts playing the new queue
这句话点明了TG端的核心要求:收到PlayItem命令后,TG不能只是简单切歌,必须完成三个动作:停止旧播放、替换播放队列上下文、开始播放新队列。很多手机App在这里实现不规范,只切了歌没清空队列,导致车机收到NowPlayingContentChanged通知后,拉取队列还是旧的,出现状态不同步。
Queue and current song title is shown → New queue and new song title is shown
这句话是对CT端的核心要求:整个流程的最终目标,是让车机上的队列和歌曲标题,完全同步为新的播放上下文。如果CT只更新了歌曲标题,没拉取新队列,用户看到的队列还是旧的,会以为车机卡了。
四、核心知识点与避坑指南
1. 关键命令与事件梳理
|--------------------------------------|-----------------|---------|
| 命令/事件 | 作用 | 发送方 |
| PlayItem(UID) | 请求直接播放指定歌曲 | CT |
| PlayStatusChangedNotification | 同步播放状态变化(停止/播放) | TG |
| NowPlayingContentChangedNotification | 同步播放队列内容变更 | TG |
| TrackChangedNotification | 同步当前播放轨道变更 | TG |
| GetAttributes_Cmd | 获取当前歌曲的元数据 | CT |
| GetFolderItems(NowPlaying) | 获取更新后的播放队列列表 | CT |
2. 开发中最容易踩的3个坑
- 坑1:CT收到通知后,不主动拉取队列和元数据
很多CT实现只处理了通知事件,没有调用GetAttributes和GetFolderItems,导致车机上的歌曲名还是旧的,队列也没更新,用户以为点歌失败了。
- 坑2:TG不按顺序发送通知,导致CT状态混乱
部分手机App会跳过Stopped状态的通知,直接发Playing通知,或者先发TrackChanged再发队列变更通知,导致CT处理逻辑混乱,界面刷新异常。协议推荐的顺序是:Stopped → 队列变更 → Playing → 轨道变更。
- 坑3:CT只处理了歌曲名,没处理队列同步
很多车机只显示当前播放的歌曲,不显示队列,或者收到队列变更通知后不刷新列表,导致用户在车机上看到的队列和手机实际播放的队列不一致,影响体验。
3. 为什么这套流程对时序要求这么严格?
因为它涉及到播放状态、队列内容、轨道信息 三个维度的同步。如果时序混乱,比如先发Playing再发队列变更通知,CT会先以为歌曲已经播放,再收到队列变更,很容易出现状态回退、界面闪一下的问题。协议推荐的通知顺序,就是为了让CT能按**"停止→队列更新→播放"**的逻辑,平滑地更新界面。
五、测验
问:车机发送PlayItem命令后,手机App需要按什么顺序发送通知?(车载蓝牙开发面试真题)
答:
协议推荐的顺序是:先发送PlayStatusChanged(Stopped),再发送NowPlayingContentChanged,接着发送PlayStatusChanged(Playing),最后发送TrackChanged。这样能保证CT按停止→队列更新→播放的逻辑平滑刷新界面。
问:CT收到NowPlayingContentChanged通知后,为什么必须调用GetFolderItems(NowPlaying)?(蓝牙协议栈开发面试题)
答:
因为NowPlayingContentChanged通知只是告诉CT队列变了,不会携带完整的队列内容。CT必须主动拉取队列列表,才能更新车机上的播放队列显示,否则用户看到的还是旧队列。
问:车机点歌直接播放和添加到队列,核心区别是什么?(嵌入式蓝牙开发面试题)
答:
核心区别是播放上下文的处理:直接播放会清空旧队列、重建新队列并从头播放;添加到队列只是把新歌加到现有队列末尾,不会替换当前播放上下文。