【AVRCP】规范精讲[20]: 播放器设置全打通,让车载与手机的播放控制完全同步

你有没有过这样的经历:开车时想打开随机播放,按了车载方向盘上的随机按钮,手机上的音乐却毫无反应;或者在手机上开启了单曲重复,车载屏幕上却还是显示全部重复。这些看似简单的功能不同步问题,其实都源于AVRCP协议中一个非常重要但经常被忽略的部分------播放器应用设置。


目录

一、播放器应用设置:播放器的远程控制面板

二、标准播放器应用设置全解析

三、播放器应用设置的完整交互流程

[3.1 第一步:获取支持的设置](#3.1 第一步:获取支持的设置)

[3.2 第二步:获取当前设置值](#3.2 第二步:获取当前设置值)

[3.3 第三步:设置新的值](#3.3 第三步:设置新的值)

[3.4 主动通知机制](#3.4 主动通知机制)

四、常见问题与解决方案

[4.1 车载按钮控制无效](#4.1 车载按钮控制无效)

[4.2 设置不同步](#4.2 设置不同步)

[4.3 均衡器设置无效](#4.3 均衡器设置无效)

五、代码示例:Android中控制播放器应用设置

六、测验


很多开发者只关注AVRCP的基本播放控制和媒体属性传输,却不知道规范还定义了一整套完整的播放器设置交互机制。这套机制允许控制器设备比如车载、耳机直接控制目标设备比如手机上的播放器设置,包括重复模式、随机模式、均衡器等等。规范中专门用了一个完整的附录来定义所有标准的播放器应用设置和它们的可能值,本文我们就来全面解析这部分内容,彻底解决播放设置不同步的问题。


一、播放器应用设置:播放器的远程控制面板

播放器应用设置本质上是媒体播放器的一组可配置参数,就像是播放器的远程控制面板。每个设置对应控制面板上的一个开关或旋钮,比如重复模式对应一个三档开关,均衡器对应一个下拉菜单。

规范中明确指出:

Player Application Settings are parameters that control the behavior of the media player application. Each setting is identified by a unique 8-bit identifier, and each setting has a set of possible values.

定义这些标准设置的核心目的是实现跨设备的兼容性。如果没有统一的标准,每个厂商都自己定义一套设置ID和值,那么不同品牌的设备之间就无法互相控制这些功能。有了标准之后,不管你用的是苹果手机还是安卓手机,不管你连接的是宝马的车载还是索尼的耳机,都可以正常控制重复、随机这些基本设置。

二、标准播放器应用设置全解析

规范中定义了13个标准的播放器应用设置,覆盖了从基本的播放模式到高级的音效设置。下面我们来逐一介绍这些设置,包括它们的ID、可能的值和实际用途。

|----------|---------------------------|-------------------------------------------------------------------------------------------------------------------|------------------|
| 设置ID | 设置名称 | 可能的值 | 说明 |
| 0x01 | Equalizer | 0x00(Off), 0x01(Rock), 0x02(Pop), 0x03(Jazz), 0x04(Classical), 0x05(Dance), 0x06(Bass), 0x07(Treble), 0x08(Vocal) | 音频均衡器预设 |
| 0x02 | Repeat Mode | 0x00(Off), 0x01(Single Track), 0x02(All Tracks), 0x03(Group) | 播放重复模式 |
| 0x03 | Shuffle Mode | 0x00(Off), 0x01(All Tracks), 0x02(Group) | 播放随机模式 |
| 0x04 | Scan Mode | 0x00(Off), 0x01(All Tracks), 0x02(Group) | 扫描播放模式,每首歌播放前几秒 |
| 0x05 | Loudness | 0x00(Off), 0x01(On) | 响度增强,提升低音量时的听觉效果 |
| 0x06 | Bass Boost | 0x00(Off), 0x01(On) | 低音增强 |
| 0x07 | 3D Effect | 0x00(Off), 0x01(On) | 3D环绕音效 |
| 0x08 | Bass Level | 0x00(-7) 到 0x0E(+7) | 低音级别,0x07为中间值 |
| 0x09 | Treble Level | 0x00(-7) 到 0x0E(+7) | 高音级别,0x07为中间值 |
| 0x0A | Balance | 0x00(Left 15) 到 0x1E(Right 15) | 左右声道平衡,0x0F为中间值 |
| 0x0B | Fader | 0x00(Rear 15) 到 0x1E(Front 15) | 前后声道平衡,0x0F为中间值 |
| 0x0C | Surround Mode | 0x00(Off), 0x01(Movie), 0x02(Music), 0x03(Night) | 环绕声模式 |
| 0x0D | Dynamic Range Compression | 0x00(Off), 0x01(On) | 动态范围压缩,降低音量波动 |

需要注意的是,并不是所有设备都支持所有这些设置。大多数消费级设备只支持前4个基本设置,而音效相关的设置通常只有高端车载和音响设备才会支持。此外,不同版本的AVRCP规范可能会增加新的设置,上面列出的是AVRCP 1.6版本的标准设置。

三、播放器应用设置的完整交互流程

要实现播放器设置的同步,需要经过三个完整的步骤:获取支持的设置、获取当前设置值、设置新的值。这三个步骤缺一不可,任何一个步骤出错都会导致设置不同步。

3.1 第一步:获取支持的设置

控制器设备在连接建立后,首先需要向目标设备发送Get Player Application Settings Attributes命令,操作码0x10,询问目标设备支持哪些播放器应用设置。

命令格式非常简单,没有操作数:

复制代码
0x10 0x00

目标设备收到命令后,会返回一个包含所有支持的设置ID的响应。响应格式如下:

复制代码
字节1:操作码0x10
字节2:操作数长度
字节3:设置数量
字节4及以后:设置ID列表

例如,如果目标设备支持均衡器、重复模式和随机模式,响应会是这样的:

复制代码
0x10 0x04 0x03 0x01 0x02 0x03

这个响应表示目标设备支持3个设置:均衡器0x01、重复模式0x02和随机模式0x03。

这一步非常重要,很多开发者会跳过这一步,直接假设目标设备支持所有设置,结果就是发送了不支持的设置命令,导致功能失效。正确的做法是,在显示任何设置按钮之前,先获取目标设备支持的设置列表,对于不支持的设置,应该隐藏对应的按钮或者禁用它们。

3.2 第二步:获取当前设置值

知道了目标设备支持哪些设置之后,控制器设备需要发送Get Player Application Settings命令,操作码0x11,获取这些设置的当前值。

(1)命令包逐字节解析

原始十六进制数据:

复制代码
0E 00 09 8D E0 11 0E 01 48 00 00 19 58 11 00 00

1. AVCTP帧头(前3字节)

  • 0E:事务标签0xE(14) + 数据包类型0x0(命令帧) + 响应类型0x0

  • 11 0E:PID 0x110E(AVRCP远程控制规范)

2. AVRCP AV/C命令头(接下来4字节)

  • 01:命令类型0x0(Status查询状态)

  • 48:子单元类型0x09(Panel面板) + 子单元ID 0x0

  • 00:操作码0x00(Vendor Dependent厂商相关)

3. 厂商特定数据(剩余部分)

  • 00 19 58:公司ID 0x001958(Bluetooth SIG官方)

  • 11:PDU ID 0x11(List Player Application Setting Attributes)

  • 00:数据包类型0x0(Single单包)

  • 00:参数长度0字节(查询所有支持的属性)

(2)响应包逐字节解析

原始十六进制数据:

复制代码
0C 00 09 8C E0 11 0E 0C 48 00 00 19 58 11 00 03 02 02 03

1. AVCTP帧头(前3字节)

  • 0C:事务标签0xE(14) + 数据包类型0x1(响应帧) + 响应类型0x0(接受)

  • 11 0E:PID 0x110E

2. AVRCP AV/C响应头(接下来4字节)

  • 0C:响应类型0x0C(Implemented By Device / Stable)

  • 48:子单元类型0x09(Panel) + 子单元ID 0x0

  • 00:操作码0x00(Vendor Dependent)

3. 厂商特定数据(剩余部分)

  • 00 19 58:公司ID 0x001958

  • 11:PDU ID 0x11(与命令相同)

  • 00:数据包类型0x0(Single)

  • 03:参数长度3字节

  • 02:支持的属性数量2个

  • 02:属性ID 1: 0x02(Repeat Mode重复模式)

  • 03:属性ID 2: 0x03(Shuffle Mode随机模式)

3.3 第三步:设置新的值

当用户在控制器设备上修改了某个设置时,控制器设备需要发送Set Player Application Settings命令,操作码0x12,将新的值发送给目标设备。

这是一次完整的List Player Application Setting Values命令-响应交互,控制器向目标设备查询**重复模式(Repeat Mode)**支持的所有可能值。

|--------|----------|-----------|------------|--------|
| 方向 | 事务标签 | 命令/响应 | PDU ID | 长度 |
| 控制器→目标 | 10(0xA) | Command | 0x12 | 11字节 |
| 目标→控制器 | 10(0xA) | Response | 0x12 | 14字节 |

(1)命令包逐字节解析

复制代码
0E 00 09 8D A0 11 0E 01 48 00 00 19 58 12 00 00 01 02

1. AVCTP帧头(前3字节)

  • 0E:事务标签0xA(10) + 数据包类型0x0(命令帧) + 响应类型0x0

  • 11 0E:PID 0x110E(AVRCP远程控制规范)

2. AVRCP AV/C命令头(接下来4字节)

  • 01:命令类型0x0(Status)

  • 48:子单元类型0x09(Panel面板) + 子单元ID 0x0

  • 00:操作码0x00(Vendor Dependent厂商相关)

3. 厂商特定数据(剩余部分)

  • 00 19 58:公司ID 0x001958(Bluetooth SIG官方)

  • 12:PDU ID 0x12(List Player Application Setting Values)

  • 00:数据包类型0x0(Single)

  • 01:参数长度1字节

  • 02:属性ID 0x02(Repeat Mode重复模式)

(2)响应包逐字节解析

复制代码
0C 00 09 8C A0 11 0E 0C 48 00 00 19 58 12 00 04 03 01 02 03

1. AVCTP帧头(前3字节)

  • 0C:事务标签0xA(10) + 数据包类型0x1(响应帧) + 响应类型0x0(接受)

  • 11 0E:PID 0x110E

2. AVRCP AV/C响应头(接下来4字节)

  • 0C:响应类型0x0C(Implemented By Device / Stable)

  • 48:子单元类型0x09(Panel) + 子单元ID 0x0

  • 00:操作码0x00(Vendor Dependent)

3. 厂商特定数据(剩余部分)

  • 00 19 58:公司ID 0x001958

  • 12:PDU ID 0x12(与命令相同)

  • 00:数据包类型0x0(Single)

  • 04:参数长度4字节

  • 03:支持的值数量3个

  • 01:值ID 1: 0x01(Single Track单曲重复)

  • 02:值ID 2: 0x02(All Tracks全部重复)

  • 03:值ID 3: 0x03(Group组重复)

如果目标设备不支持某个设置,或者设置的值无效,会返回一个错误响应,错误代码为0x02无效参数或0x01不支持的操作码。

3.4 主动通知机制

为了保持设置的实时同步,规范还定义了主动通知机制。当目标设备上的播放器设置发生变化时,比如用户在手机上手动修改了重复模式,目标设备会主动向控制器发送Player Application Settings Changed通知,操作码0x13。

通知的格式与获取当前设置值响应的格式完全相同。控制器收到通知后,应该立即更新UI显示,保持与目标设备的设置同步。

主动通知机制大大提高了用户体验。如果没有这个机制,控制器需要定期轮询目标设备的设置值,这不仅会增加蓝牙数据传输量,还会导致设置更新不及时。

四、常见问题与解决方案

在实际开发和使用过程中,播放器应用设置是最容易出现兼容性问题的地方。下面我们来看几个最常见的问题及其根本原因和解决方案。

4.1 车载按钮控制无效

这是最常见的问题。按下车载上的重复或随机按钮,手机上的音乐应用没有任何反应。

根本原因:

  1. 手机的音乐应用没有实现AVRCP播放器应用设置的处理逻辑

  2. 目标设备手机不支持该设置

  3. 控制器设备车载没有正确发送设置命令

解决方案:

  1. 对于音乐应用开发者来说,需要正确实现AVRCP播放器应用设置的处理逻辑,接收并处理来自控制器的设置命令

  2. 对于车载开发者来说,在显示设置按钮之前,应该先获取目标设备支持的设置列表,对于不支持的设置,应该隐藏对应的按钮或者禁用它们

  3. 对于用户来说,可以尝试使用支持AVRCP 1.4及以上版本的音乐应用,比如网易云音乐、QQ音乐等

4.2 设置不同步

在手机上修改了设置,车载屏幕上没有更新;或者在车载上修改了设置,手机上没有更新。

根本原因:

  1. 目标设备没有在设置发生变化时主动发送通知

  2. 控制器设备没有注册接收设置变化通知

  3. 双方的设置值定义不一致

解决方案:

  1. 目标设备应该在任何播放器设置发生变化时,主动向控制器发送Player Application Settings Changed通知

  2. 控制器设备应该注册接收这个通知,并在收到通知时更新UI显示

  3. 双方都应该严格按照规范定义的设置值来实现,不要自定义值

4.3 均衡器设置无效

很多设备上的均衡器按钮只是个摆设,按下后没有任何效果。

根本原因:

  1. 均衡器设置的实现非常复杂,很多音乐应用没有实现这个功能

  2. 不同设备对均衡器预设的定义不同,导致设置后效果不符合预期

  3. 系统的音频框架会覆盖应用的均衡器设置

解决方案:

  1. 对于音乐应用开发者来说,如果不支持均衡器设置,应该明确告知控制器设备,而不是返回成功响应

  2. 对于控制器开发者来说,应该谨慎使用均衡器设置,因为不同设备的效果差异很大

  3. 对于用户来说,如果均衡器无效,可以尝试在手机上直接调整均衡器

五、代码示例:Android中控制播放器应用设置

在Android系统中,我们可以通过BluetoothAvrcpController类来获取和设置播放器应用设置。下面我们来看一段简单的代码示例,展示如何实现这些功能。

复制代码
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAvrcpController;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;

import java.util.List;

public class AvrcpPlayerSettingsManager {
    private static final String TAG = "AvrcpPlayerSettings";
    
    // 标准设置ID
    public static final byte SETTING_EQUALIZER = 0x01;
    public static final byte SETTING_REPEAT_MODE = 0x02;
    public static final byte SETTING_SHUFFLE_MODE = 0x03;
    
    // 重复模式值
    public static final byte REPEAT_OFF = 0x00;
    public static final byte REPEAT_SINGLE = 0x01;
    public static final byte REPEAT_ALL = 0x02;
    
    // 随机模式值
    public static final byte SHUFFLE_OFF = 0x00;
    public static final byte SHUFFLE_ALL = 0x01;
    
    private BluetoothAvrcpController mAvrcpController;
    private final Context mContext;
    
    public AvrcpPlayerSettingsManager(Context context) {
        mContext = context.getApplicationContext();
        initAvrcpController();
    }
    
    private void initAvrcpController() {
        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
        if (adapter == null || !adapter.isEnabled()) {
            Log.e(TAG, "Bluetooth adapter not available");
            return;
        }
        
        adapter.getProfileProxy(mContext, new BluetoothProfile.ServiceListener() {
            @Override
            public void onServiceConnected(int profile, BluetoothProfile proxy) {
                if (profile == BluetoothProfile.AVRCP_CONTROLLER) {
                    mAvrcpController = (BluetoothAvrcpController) proxy;
                    Log.d(TAG, "AVRCP controller connected");
                    
                    // 连接成功后获取支持的设置
                    getSupportedSettings();
                }
            }
            
            @Override
            public void onServiceDisconnected(int profile) {
                if (profile == BluetoothProfile.AVRCP_CONTROLLER) {
                    mAvrcpController = null;
                    Log.d(TAG, "AVRCP controller disconnected");
                }
            }
        }, BluetoothProfile.AVRCP_CONTROLLER);
    }
    
    // 获取支持的设置
    public void getSupportedSettings() {
        if (mAvrcpController == null) return;
        
        List<BluetoothDevice> devices = mAvrcpController.getConnectedDevices();
        if (devices.isEmpty()) return;
        
        BluetoothDevice device = devices.get(0);
        
        mAvrcpController.getPlayerApplicationSettingsAttributes(device, 
            new BluetoothAvrcpController.PlayerApplicationSettingsCallback() {
                @Override
                public void onSupportedAttributesReceived(BluetoothDevice device, byte[] attributes) {
                    Log.d(TAG, "Supported settings: " + bytesToHex(attributes));
                    
                    // 获取当前设置值
                    getCurrentSettings(device, attributes);
                }
                
                @Override
                public void onSettingsReceived(BluetoothDevice device, Bundle settings) {
                    // 处理当前设置值
                    Log.d(TAG, "Current settings: " + settings);
                    
                    // 更新UI
                    updateSettingsUI(settings);
                }
                
                @Override
                public void onSetSettingsResult(BluetoothDevice device, boolean success) {
                    Log.d(TAG, "Set settings result: " + success);
                }
            });
    }
    
    // 获取当前设置值
    public void getCurrentSettings(BluetoothDevice device, byte[] attributes) {
        if (mAvrcpController == null) return;
        
        mAvrcpController.getPlayerApplicationSettings(device, attributes);
    }
    
    // 设置重复模式
    public void setRepeatMode(BluetoothDevice device, byte repeatMode) {
        if (mAvrcpController == null) return;
        
        Bundle settings = new Bundle();
        settings.putByte(String.valueOf(SETTING_REPEAT_MODE), repeatMode);
        
        mAvrcpController.setPlayerApplicationSettings(device, settings);
    }
    
    // 设置随机模式
    public void setShuffleMode(BluetoothDevice device, byte shuffleMode) {
        if (mAvrcpController == null) return;
        
        Bundle settings = new Bundle();
        settings.putByte(String.valueOf(SETTING_SHUFFLE_MODE), shuffleMode);
        
        mAvrcpController.setPlayerApplicationSettings(device, settings);
    }
    
    private void updateSettingsUI(Bundle settings) {
        // 在这里更新UI显示
        if (settings.containsKey(String.valueOf(SETTING_REPEAT_MODE))) {
            byte repeatMode = settings.getByte(String.valueOf(SETTING_REPEAT_MODE));
            Log.d(TAG, "Current repeat mode: " + repeatMode);
        }
        
        if (settings.containsKey(String.valueOf(SETTING_SHUFFLE_MODE))) {
            byte shuffleMode = settings.getByte(String.valueOf(SETTING_SHUFFLE_MODE));
            Log.d(TAG, "Current shuffle mode: " + shuffleMode);
        }
    }
    
    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b));
        }
        return sb.toString();
    }
    
    public void release() {
        if (mAvrcpController != null) {
            BluetoothAdapter.getDefaultAdapter().closeProfileProxy(
                BluetoothProfile.AVRCP_CONTROLLER, mAvrcpController);
            mAvrcpController = null;
        }
    }
}

这段代码展示了如何在Android应用中获取支持的播放器设置、获取当前设置值以及设置新的值。需要注意的是,这些方法是在API 21 Android 5.0之后才引入的,并且需要BLUETOOTH和BLUETOOTH_ADMIN权限。

六、测验

题目:请列举AVRCP规范中定义的至少3个播放器应用设置,并说明它们的ID和可能的值。(吉利汽车车载蓝牙开发工程师面试题)

答案:

AVRCP规范中定义了多个标准的播放器应用设置,以下是其中最常用的3个:

  1. 重复模式ID: 0x02:用于控制播放的重复方式。可能的值包括0x00关闭、0x01单曲重复、0x02全部重复和0x03组重复。

  2. 随机模式ID: 0x03:用于控制播放的随机方式。可能的值包括0x00关闭、0x01全部随机和0x02组随机。

  3. 均衡器ID: 0x01:用于控制音频的均衡效果。可能的值包括0x00关闭、0x01摇滚、0x02流行、0x03爵士、0x04古典等。

题目:请详细描述AVRCP播放器应用设置的完整交互流程。(华为鸿蒙蓝牙协议工程师面试题)

答案:

AVRCP播放器应用设置的交互流程分为三个主要步骤:

  1. 获取支持的设置:控制器设备在连接建立后,首先发送Get Player Application Settings Attributes命令操作码0x10,询问目标设备支持哪些播放器应用设置。目标设备返回一个包含所有支持的设置ID的响应。

  2. 获取当前设置值:控制器设备根据支持的设置列表,发送Get Player Application Settings命令操作码0x11,获取这些设置的当前值。目标设备返回一个包含每个设置当前值的响应。

  3. 设置新的值:当用户在控制器设备上修改了某个设置时,控制器设备发送Set Player Application Settings命令操作码0x12,将新的值发送给目标设备。目标设备修改设置后返回成功响应,或者在失败时返回错误响应。

此外,目标设备还可以在设置发生变化时,主动向控制器发送Player Application Settings Changed通知操作码0x13,保持双方设置同步。

题目:为什么很多蓝牙设备上的重复或随机播放按钮无法正常工作?如何排查和解决这个问题?(字节跳动车载音频开发工程师面试题)

答案:

重复或随机播放按钮无法正常工作的常见原因及排查解决方法如下:

  1. 音乐应用未实现AVRCP设置处理:很多第三方音乐应用没有正确实现AVRCP播放器应用设置的处理逻辑。排查方法:尝试使用系统自带的音乐应用,如果系统应用正常,说明是第三方应用的问题。解决方法:使用支持AVRCP 1.4及以上版本的音乐应用。

  2. 设备不支持该设置:目标设备或控制器设备可能不支持对应的播放器应用设置。排查方法:通过抓包工具查看目标设备返回的支持设置列表,如果列表中没有对应的设置ID,说明设备不支持。解决方法:对于不支持的设置,隐藏对应的UI元素。

  3. 命令发送或接收错误:控制器设备可能没有正确发送设置命令,或者目标设备没有正确接收命令。排查方法:使用蓝牙抓包工具如Wireshark抓取蓝牙数据包,查看命令是否正确发送和响应。解决方法:检查命令格式是否符合规范,确保事务标签和PID正确。


相关推荐
开开心心就好11 小时前
视频压缩工具推荐,绿色版免安装双击即用
java·前端·人工智能·spring·智能手机·intellij-idea·excel
redfred11 小时前
HyperFrames 部署教程:5.6 万 Star 开源「写 HTML 出视频」框架,Windows 从零上手(CLI 三条命令 + 7 个避坑)
ffmpeg·开源·自动化·html·音视频
martindelophy11 小时前
用自然语言剪视频:我们在 Timeline Studio 中实现了 ChatCut 对话剪辑
android·音视频
特视界说12 小时前
没有完整 3D 模型,能做产品三维动画吗?
3d·微信·音视频·新浪微博
老李IT笔记12 小时前
从备份恢复会把管理状态带回来吗:iOS 27 之后答案变了|MDM.Plus
ios·智能手机
特视界说12 小时前
产品宣传动画和企业宣传片有什么区别,应该先做哪个?
微信·音视频·新浪微博·新人首发
bluebonnet2712 小时前
【docker】容器内映射外部音频驱动
docker·容器·音视频
可乐鸡翅yeah_1 天前
FFmpeg 生成 HLS 独立分片 independent‑segments 参数到底有什么用
javascript·ffmpeg·音视频·safari·m3u8
企业数字化笔记1 天前
视频理解怎样从“看见”变成“判断”?几何规则、轨迹特征与动作模型的选型
音视频
DP DPharness1 天前
StudyMate 从安装到第一节课的完整操作路径
人工智能·websocket·网络协议·智能手机·dpharness