在蓝牙音频开发中,相信很多人都遇到过这样的问题:手机连接车载蓝牙后,播放一首名字很长的歌曲,车机屏幕上只显示了前几个字;或者获取专辑信息时,总是少了一部分内容。这背后其实是AVRCP协议的续传机制在起作用。很多开发者对这个机制一知半解,导致出现各种奇怪的兼容性问题。本文就来深入拆解AVRCP中的续传流程,包括RequestContinuingResponse和AbortContinuingResponse的工作原理、规范细节和实战中的坑点。
目录
[二、核心字段:Packet Type的四种状态](#二、核心字段:Packet Type的四种状态)
三、RequestContinuingResponse完整流程拆解
四、AbortContinuingResponse:随时中止的能力
一、为什么需要续传机制?
要理解续传机制,首先得从AVRCP的底层协议栈说起。AVRCP基于AV/C数字接口命令集,而AV/C协议有一个硬性规定:每个AV/C帧的最大长度只能是512字节。这个限制是历史遗留问题,在AV/C协议设计之初,512字节已经足够传输大部分控制命令和简单的元数据。
但随着蓝牙音频的发展,我们需要传输的元数据越来越多,比如完整的歌曲名、艺术家名、专辑名、歌词、专辑封面信息等,这些数据很容易就超过了512字节的限制。为了解决这个问题,AVRCP引入了续传机制,允许将一个大的响应拆分成多个AV/C帧进行传输。
这里需要特别注意一个关键点:
Note that Continuation is required due to the limit of 512 octets per AV/C frame. Continuation is therefore only necessary for AV/C commands.
这句话明确告诉我们,续传机制只适用于控制通道上的AV/C命令。而浏览通道上的命令因为不使用AV/C帧封装,直接通过AVCTP传输,所以不需要续传机制,它们的大小限制由L2CAP的MTU决定,通常可以达到几千字节。
二、核心字段:Packet Type的四种状态
在AVRCP特定的AV/C PDU格式中,有一个至关重要的字段叫做Packet Type,它占2位,用来标识数据包在整个续传过程中的状态。这个字段是整个续传机制的核心,所有的传输逻辑都围绕它展开。
Packet Type字段有四种可能的取值:
00 非分片消息:整个响应数据可以在一个AV/C帧中传输完成,不需要续传
01 开始包:这是分片响应的第一个包,后面还有更多的数据需要传输
10 继续包:这是分片响应的中间包,前面已经有数据,后面还有更多数据
11 结束包:这是分片响应的最后一个包,整个传输到此完成
规范对发送方和接收方的行为有严格的规定:
-
发送方不能交错发送不同PDU的分片。一旦发送了某个PDU的开始包,就必须连续发送该PDU的所有后续分片,直到传输完成或者被接收方中止。
-
如果接收方收到一个新的开始包或非分片包,而之前还有未完成的分片传输,那么之前的分片传输会被自动视为已中止。
-
所有属于同一个PDU的分片响应,必须使用和原始请求相同的PDU ID,这样接收方才能正确地将它们关联起来。
三、RequestContinuingResponse完整流程拆解
下面我们通过一个典型的场景来完整拆解续传流程。假设CT(控制器,比如车机)向TG(目标设备,比如手机)发送GetElementAttributes命令,请求当前播放歌曲的所有元数据。由于歌曲的元数据非常丰富,总大小达到了1200字节,超过了512字节的限制,这时候就需要触发续传机制。

整个流程可以分为以下几个步骤:
-
CT发送原始请求:CT向TG发送GetElementAttributes命令(PDU ID 0x20),请求所有元数据属性。
-
TG 返回开始包:TG收到请求后,计算响应数据大小,发现超过了512字节。于是将数据拆分成三个部分,先返回第一个部分,Packet Type设置为01(开始包)。
-
CT请求继续传输:CT收到开始包后,识别出Packet Type为01,知道还有后续数据。于是发送RequestContinuingResponse命令(PDU ID 0x40),参数是原始请求的PDU ID 0x20。
-
TG 返回继续包:TG收到续传请求后,返回第二部分数据,Packet Type设置为10(继续包)。
-
CT再次请求继续传输:CT收到继续包后,识别出Packet Type为10,知道还有最后一部分数据。于是再次发送RequestContinuingResponse命令。
-
TG 返回结束包:TG收到续传请求后,返回最后一部分数据,Packet Type设置为11(结束包)。
-
CT 拼接数据:CT收到结束包后,将三个包的数据拼接在一起,得到完整的元数据响应,整个传输过程完成。
这个流程可以用一句话总结:TG 分片发送,CT主动拉取,直到收到结束包。这种设计的好处是CT可以控制传输的节奏,在不需要的时候随时中止传输。
四、AbortContinuingResponse:随时中止的能力
在实际使用中,CT可能在续传过程中不再需要后续的数据。比如用户在元数据传输过程中切换了歌曲,这时候之前的元数据请求就没有意义了。为了避免浪费带宽和资源,AVRCP提供了AbortContinuingResponse命令,允许CT随时中止正在进行的续传。

AbortContinuingResponse的使用非常简单:
CT在收到任何一个分片包(开始包或继续包)之后,都可以发送AbortContinuingResponse命令(PDU ID 0x41),参数是要中止的PDU ID。
TG收到中止命令后,立即停止发送后续的分片,返回一个空的响应。
CT收到响应后,丢弃已经收到的部分数据,整个传输过程中止。
这个机制在实际开发中非常重要,尤其是在用户操作频繁的场景下,可以大大提高系统的响应速度和资源利用率。
五、实战中的常见坑点与解决方案
1. 续传与L2CAP分片的区别
很多开发者容易混淆AVRCP的续传机制和L2CAP的分片机制,其实它们是完全不同层面的东西:
-
AVRCP 续传:是应用层的分片机制,解决的是AV/C帧512字节的限制,只适用于控制通道的AV/C命令。
-
L2CAP分片:是链路层的分片机制,解决的是基带数据包最大长度的限制,适用于所有蓝牙数据传输。
在实际传输中,一个AV/C帧可能会被L2CAP进一步拆分成多个基带数据包进行传输。这两层分片是独立工作的,互不影响。
2. 超时处理策略
规范规定,TG必须在TMTP(1000毫秒)时间内响应任何AV/C命令,包括续传请求。如果CT在发送RequestContinuingResponse后,1000毫秒内没有收到响应,应该如何处理?
规范没有明确规定,但行业内的通用做法是:
重试1-2次续传请求
如果重试仍然失败,中止整个请求,返回超时错误
不要无限重试,以免造成资源泄漏
3. 错误处理
如果在续传过程中发生错误,TG应该返回一个REJECTED响应,包含相应的错误代码。CT收到错误响应后,应该立即中止续传,不要继续发送续传请求。
常见的错误代码包括:
0x00 无效命令
0x01 无效参数
0x03 内部错误
六、代码示例:CT端续传处理逻辑
下面是一个简化的CT端续传处理逻辑的伪代码示例,展示了如何判断Packet Type,如何发送续传请求,以及如何拼接数据:
// 处理AV/C响应
void handle_avc_response(uint8_t pdu_id, uint8_t packet_type, uint8_t* data, uint16_t len) {
static uint8_t* complete_data = NULL;
static uint16_t complete_len = 0;
switch (packet_type) {
case 0x00: // 非分片消息
// 直接处理完整数据
process_complete_response(pdu_id, data, len);
break;
case 0x01: // 开始包
// 分配内存,保存第一部分数据
complete_data = malloc(len);
memcpy(complete_data, data, len);
complete_len = len;
// 发送续传请求
send_request_continuing_response(pdu_id);
break;
case 0x10: // 继续包
// 重新分配内存,拼接数据
complete_data = realloc(complete_data, complete_len + len);
memcpy(complete_data + complete_len, data, len);
complete_len += len;
// 发送续传请求
send_request_continuing_response(pdu_id);
break;
case 0x11: // 结束包
// 拼接最后一部分数据
complete_data = realloc(complete_data, complete_len + len);
memcpy(complete_data + complete_len, data, len);
complete_len += len;
// 处理完整数据
process_complete_response(pdu_id, complete_data, complete_len);
// 释放内存
free(complete_data);
complete_data = NULL;
complete_len = 0;
break;
}
}
// 发送RequestContinuingResponse命令
void send_request_continuing_response(uint8_t target_pdu_id) {
uint8_t cmd[10];
// 填充AV/C命令头
cmd[0] = 0x00; // ctype: CONTROL
cmd[1] = 0x90; // subunit_type: PANEL, subunit_id: 0
cmd[2] = 0x00; // opcode: VENDOR DEPENDENT
// 填充Bluetooth SIG公司ID
cmd[3] = 0x00;
cmd[4] = 0x19;
cmd[5] = 0x58;
// 填充AVRCP PDU头
cmd[6] = 0x40; // PDU ID: RequestContinuingResponse
cmd[7] = 0x00; // Reserved, Packet Type: 00
cmd[8] = 0x00; // Parameter Length: 1
cmd[9] = 0x01;
cmd[10] = target_pdu_id; // 目标PDU ID
// 发送命令
avctp_send_command(cmd, sizeof(cmd));
}
七、测验
题目:为什么AVRCP需要RequestContinuingResponse机制?它和L2CAP的分片有什么区别?
答案:
AVRCP需要续传机制是因为它基于AV/C协议,而AV/C协议规定每个AV/C帧的最大长度只能是512字节。当需要传输的元数据(如长歌曲名、专辑信息)超过这个限制时,就需要将数据拆分成多个AV/C帧进行传输。
它和L2CAP分片的区别在于:
-
层面不同:AVRCP续传是应用层的分片机制,L2CAP分片是链路层的分片机制。
-
适用范围不同:AVRCP续传只适用于控制通道的AV/C命令,L2CAP分片适用于所有蓝牙数据传输。
-
触发条件不同:AVRCP续传由512字节的AV/C帧限制触发,L2CAP分片由基带数据包最大长度限制触发。
-
控制方式不同:AVRCP续传是CT主动拉取的模式,L2CAP分片是自动完成的,对上层透明。
题目:请描述RequestContinuingResponse的完整工作流程,并说明Packet Type字段的各个取值含义。
答案:
完整工作流程:
-
CT发送原始AV/C命令给TG。
-
TG发现响应数据超过512字节,将数据拆分成多个部分,先返回Packet Type为01的开始包。
-
CT收到开始包后,发送RequestContinuingResponse命令,请求后续数据。
-
TG返回Packet Type为10的继续包,包含下一部分数据。
-
步骤3和4重复,直到TG返回Packet Type为11的结束包。
-
CT将所有分片数据拼接在一起,得到完整的响应。
Packet Type字段的取值含义:
-
00:非分片消息,整个响应在一个AV/C帧中完成。
-
01:开始包,分片响应的第一个包,后面还有更多数据。
-
10:继续包,分片响应的中间包,前后都有数据。
-
11:结束包,分片响应的最后一个包,传输完成。
题目:在AVRCP中,哪些命令需要使用续传机制?浏览通道的命令为什么不需要?
答案:
只有控制通道上的AV/C命令需要使用续传机制,包括:
-
GetElementAttributes(获取当前播放媒体的元数据)
-
GetFolderItems(获取文件夹内容)
-
GetItemAttributes(获取特定媒体项的属性)
-
其他可能返回大量数据的AV/C命令
浏览通道的命令不需要续传机制,原因是:
浏览通道的命令不使用AV/C帧封装,直接通过AVCTP协议传输。AVCTP协议没有512字节的帧限制,它的大小限制由L2CAP的MTU决定,通常可以达到几千字节,足够传输大部分浏览数据。因此浏览通道不需要应用层的续传机制。