做蓝牙音频开发的同学都知道,AVRCP是最容易出兼容性问题的协议之一。很多时候你按照规范定义的PDU格式写了代码,却发现和不同手机、耳机配对时,要么收不到切歌通知,要么中文歌名乱码,要么大数据包只收到一半。这些问题的根源,往往不是你对命令格式理解错了,而是对交互时序和流程细节没吃透。
目录
AVRCP规范里最有价值的部分之一,就是附录里的消息序列图示例。这些示例覆盖了从基础切歌到多播放器切换的所有核心场景,把规范里零散的命令串成了完整的交互流程。本文我们就通过这些消息序列,把AVRCP最常用的交互流程讲透,帮你避开开发中的那些坑。
一、基础播放控制:切歌事件的完整链路
切歌是AVRCP最基础也是最常用的功能,它的完整交互流程几乎涵盖了所有核心机制:通知注册、PASS THROUGH命令、元数据获取。

规范给出的切歌流程如下:
控制器首先向目标设备发送RegisterNotification命令,注册TRACK_CHANGED事件
目标设备在1000毫秒内返回INTERIM响应,携带当前播放曲目的UID
用户按下控制器上的下一曲按键,控制器发送PASS THROUGH命令,state_flag设为0表示按键按下
目标设备返回ACCEPTED响应,执行切歌操作
用户松开按键,控制器发送PASS THROUGH命令,state_flag设为1表示按键释放
目标设备返回ACCEPTED响应
切歌完成后,目标设备向控制器发送CHANGED响应,携带新曲目的UID
控制器立即重新发送RegisterNotification命令,注册下一次切歌事件
控制器发送GetElementAttributes命令,获取新曲目的标题、艺术家等元数据
这里有一个最容易踩的坑:AVRCP 的通知是一次性的。规范明确要求,注册通知后,当事件发生并返回CHANGED响应时,本次注册就自动失效。控制器必须重新发送RegisterNotification命令才能接收下一次事件。很多新手以为注册一次就一劳永逸,结果只能收到第一次切歌通知。
// 构造RegisterNotification命令,监听TRACK_CHANGED事件
uint8_t reg_notify_cmd[] = {
0x00, 0x90, 0x00, 0x00, 0x19, 0x58, // AV/C Vendor Dependent头,Company ID 0x001958
0x31, 0x00, 0x00, 0x05, // PDU ID 0x31,Packet Type 0x00,参数长度0x0005
0x02, 0x00, 0x00, 0x00, 0x00 // Event ID 0x02(TRACK_CHANGED),播放间隔0(忽略)
};
二、中文乱码克星:字符集协商流程
中文乱码是AVRCP开发的重灾区,90%的乱码问题都源于字符集协商不当。
规范规定,默认情况下所有字符串都使用UTF-8编码。如果控制器支持其他字符集,可以发送InformDisplayableCharacterSet命令告知目标设备。但有一个硬性要求,控制器必须在支持的字符集列表中包含UTF-8,否则就是违反规范的。
规范给出了两种字符集协商的场景:
-
目标设备不支持控制器指定的其他字符集,继续使用UTF-8响应所有字符串请求
-
目标设备支持控制器指定的字符集,后续使用该字符集响应字符串请求
// 构造InformDisplayableCharacterSet命令,支持UTF-8(0x006A)和GBK(0x0080)
uint8_t inform_charset_cmd[] = {
0x00, 0x90, 0x00, 0x00, 0x19, 0x58,
0x17, 0x00, 0x00, 0x05, // PDU ID 0x17,参数长度0x0005
0x02, // 支持2种字符集
0x00, 0x6A, // UTF-8
0x00, 0x80 // GBK
};
实际开发中建议永远优先使用UTF-8编码,因为几乎所有设备都支持UTF-8,而GBK等编码的支持度参差不齐。如果遇到不支持UTF-8的老设备,再降级到其他编码。
三、大数据包处理:续传与中断续传
AV/C帧的最大长度是512字节,如果元数据(比如长歌词、专辑名)超过这个长度,目标设备会把响应分成多个包发送。这就是续传机制。
规范定义了三种包类型:
0x01 Start:第一个数据包
0x02 Continue:中间数据包
0x03 End:最后一个数据包
控制器收到Start或Continue包后,需要发送RequestContinuingResponse命令请求下一个包,直到收到End包。如果用户在续传过程中切歌了,不需要再接收上一首歌的元数据,可以发送AbortContinuingResponse命令中断续传。
// 续传处理伪代码
void handle_get_element_attr_rsp(uint8_t *pdu, uint16_t len) {
uint8_t pdu_id = pdu[0];
uint8_t packet_type = pdu[1] & 0x03;
static uint8_t *full_data = NULL;
static uint32_t full_len = 0;
if (packet_type == 0x01) { // Start包
full_len = 0;
full_data = realloc(full_data, len);
memcpy(full_data, pdu, len);
full_len += len;
send_request_continuing_rsp(pdu_id); // 请求续传
} else if (packet_type == 0x02) { // Continue包
full_data = realloc(full_data, full_len + len);
memcpy(full_data + full_len, pdu, len);
full_len += len;
send_request_continuing_rsp(pdu_id); // 继续请求续传
} else if (packet_type == 0x03) { // End包
full_data = realloc(full_data, full_len + len);
memcpy(full_data + full_len, pdu, len);
full_len += len;
parse_element_attr(full_data, full_len); // 处理完整数据
free(full_data);
full_data = NULL;
}
}
四、多播放器时代:媒体播放器切换流程
AVRCP 1.4以后支持设备上有多个媒体播放器,比如手机上同时有网易云音乐和QQ音乐。控制器可以通过以下流程切换要控制的播放器:
控制器发送GetFolderItems命令,Scope设为0x00(Media Player List),获取所有播放器的列表
目标设备返回播放器列表,包含每个播放器的ID、类型、名称和功能位掩码
用户选择一个播放器,控制器发送SetAddressedPlayer命令,携带选中的播放器ID
目标设备返回ACCEPTED响应,开始切换播放器
切换完成后,目标设备发送AddressedPlayerChanged通知
控制器重新注册所有相关事件(播放状态、切歌、音量等)
这里有一个重要的细节:当播放器切换时,目标设备会终止所有旧播放器的通知注册,并返回错误码0x16(Addressed Player Changed)。控制器收到这个错误后,必须重新注册新播放器的所有事件。
五、兼容性保障:老设备与新规范的共存
现在还有很多老款车载、耳机只支持AVRCP 1.3,没有通知功能。规范要求新的目标设备必须兼容这些老设备。
当老设备连接到新的目标设备时,老设备不会发送任何RegisterNotification命令。目标设备应该只响应PASS THROUGH等基础命令,不需要等待通知注册。如果老设备发送Play命令,目标设备直接执行并返回ACCEPTED响应即可。
六、测验
**题目:**为什么AVRCP的RegisterNotification是一次性的?开发中如何处理才能持续接收事件?
答案:
AVRCP的通知机制继承自1394 AV/C协议,设计为一次性是为了简化协议实现,避免目标设备维护大量长期有效的注册状态。开发中需要在每次收到CHANGED响应后,立即重新发送RegisterNotification命令注册下一次事件。同时需要处理INTERIM响应超时的情况,超时后应该重试注册。
**题目:**AVRCP中GetElementAttributes返回的数据超过512字节时如何处理?请描述续传流程。
答案:
当响应数据超过512字节时,目标设备会将响应分成多个AV/C帧发送。第一个帧的Packet Type为0x01(Start),中间帧为0x02(Continue),最后一个帧为0x03(End)。控制器收到Start或Continue帧后,发送RequestContinuingResponse命令请求下一个帧,直到收到End帧。然后将所有帧的数据拼接起来,得到完整的响应。如果不需要继续接收,可以发送AbortContinuingResponse命令中断续传。
**题目:**当用户在目标设备(TG)本地切换媒体播放器时,控制器(CT)会收到什么通知?后续需要执行哪些操作?
答案:
控制器会收到AddressedPlayerChanged通知,携带新播放器的ID和UID Counter。后续需要执行以下操作:
-
终止所有旧播放器的事件注册
-
发送GetFolderItems命令获取新播放器的详细信息
-
重新注册新播放器的所有相关事件,包括播放状态、切歌、音量、播放队列变化等
-
发送GetPlayStatus和GetElementAttributes命令获取当前播放状态和曲目信息