蓝牙音频协议——安卓开发

协议

AVRCP(Audio Vidoe Remote Control Protocol,音频视频远程控制协议),区分为CT(Control)和TG(Target)两端,TG就是受控端。
image

A2DP(Advenced Audio Distribution Profile,即蓝牙音频传输模型协定),和音频模型一样区分Sink和Source两端
image

AVRCP 用于控制,如播放/音量等按键。安卓属性 bluetooth.profile.avrcp.target.enabled
A2DP 用于音频流传输,除此之外还有LDCP等等更好的

底层

基于蓝牙HCI串口协议

抓包btsnoop后使用协议分析软件
如Wireshark/ellisys better_analysis打开

setprop persist.bluetooth.btsnooppath /data/misc/bluetooth/logs/btsnoop_hci.cfa
setprop persist.bluetooth.btsnoopenable true
setprop persist.bluetooth.btsnooplogmode full

# 重启
# 抓 btsnoop  之前,先关蓝牙,然后再打开
复现问题后需要把 btsnoop 文件及时报错,否则重新开关蓝牙后之前的 btsnoop 会被覆盖
同时,导出此过程 logcat
adb pull /data/misc/bluetooth/logs/ ./
adb logcat -d > bt_log.log
```c

## RK蓝牙文档说明
### Libbt
负责蓝牙初始化,如加载蓝牙固件等操作。不同厂家都有定制化。
如 hardware/broadcom/libbt和 hardware/realtek/rtkbt/code/lifbt-vendor。
libbt一般只需要配置蓝牙端口和固件路径即可。
配置文件:
 - hardware/broadcom/libbt/conf/rockchip/rksdk/bt_vendor.conf
 - hardware/realtek/rtkbt/code/vendor/etc/bluetooth/rtkbt.conf

### bluedroid
位于 system/bt 下,产物为 libbluetooth.so

日志级别: 
 - 板卡 /system/etc/bt_stack.conf
 - 属性 persist.bluetooth.btsnoopenable 和 persist.bluetooth.btsnoopsize
   - 日志路径:/data/misc/bluetooth/logs/btsnoop_hci.log

例子
```c
[persist.bluetooth.btsnooppath]: [/data/misc/bluetooth/logs/btsnoop_hci.log]
[persist.bluetooth.btsnoopsize]: [0xffff]
[persist.bluetooth.disableabsvol]: [true] 绝对音量,禁用后CT可以调节TG音量

pcba bt测试

测试代码默认端口为 /dev/ttyS0 ,可以通过 /system/etc/bt_stack.conf 配置

AVRCP报文

image

音量控制按键走的是VENDOR DEPENDENT操作码[0x1958]报文,CT发送,TG确认。
播放暂停走的是Pass Through
AVRCP CT和TG都支持 CONTROL/STATUS/NOTIFY三种。
image

例子:
image

image

安卓源码

AN16为例,蓝牙模块 /packages/modules/Bluetooth

键值转换 android/app/src/com/android/bluetooth/avrcp/helpers/AvrcpPassthrough.java
image

例子:某平台音量键正常,但播放暂停下一首没有响应
image

发送KEYCODE_MEDIA_PAUSE

input keyevent 127 

可以看到歌曲正常暂停了,这里可以确认上层没有发送该键值。

过滤TG日志:

logcat|grep -i AvrcpTargetService &
logcat|grep -i OSMediaPlayer &

image

image

查看源码,发现发送按键是在sendMediaKeyEvent

过滤日志没找到,查看源码发现日志等级不够,但有dump接口

 dumpsys bluetooth_manager|grep sendMediaKeyEvent

image
日志文件

底层一切正常,那么就是软件本身的问题
实际上,安卓APP需要适配一个叫 MediaSession 的东西,才能有媒体控制的功能。
image

sendMediaKeyEvent 调用分析

架构框架图

flowchart TB subgraph "远程蓝牙设备" Remote["蓝牙耳机/车机<br/>发送AVRCP按键"] end subgraph "Bluetooth Native Stack" AVCTP["AVCTP协议层<br/>(AV/C Transport Protocol)"] Device["Device.cc<br/>MessageReceived()"] Interface["AvrcpMediaInterfaceImpl<br/>SendKeyEvent()"] end subgraph "JNI Layer" JNI["com_android_bluetooth_avrcp_target.cpp<br/>sendMediaKeyEvent()"] end subgraph "Java Framework" NativeInterface["AvrcpNativeInterface<br/>sendMediaKeyEvent()"] AvrcpService["AvrcpTargetService<br/>sendMediaKeyEvent()"] Passthrough["AvrcpPassthrough<br/>toKeyCode()"] end subgraph "Android System" AudioManager["AudioManager<br/>dispatchMediaKeyEvent()"] MediaSession["MediaSessionManager<br/>分发到活跃媒体播放器"] end Remote -->|"AVRCP<br/>PassThrough命令"| AVCTP AVCTP -->|"解析命令"| Device Device -->|"调用接口"| Interface Interface -->|"JNI回调"| JNI JNI -->|"CallVoidMethod"| NativeInterface NativeInterface -->|"转发"| AvrcpService AvrcpService -->|"转换按键码"| Passthrough AvrcpService -->|"KeyEvent"| AudioManager AudioManager --> MediaSession

调用流程图

flowchart TD Start["远程设备发送AVRCP PassThrough命令<br/>例如: PLAY/PAUSE/NEXT/PREV"] Receive["Device::MessageReceived()<br/>接收PassThrough消息包"] CheckOpcode{"检查Opcode<br/>是否为<br/>PASS_THROUGH?"} CheckActive{"设备是否<br/>为活跃设备?"} GetKeyInfo["获取按键信息<br/>• OperationId (按键类型)<br/>• KeyState (PUSHED/RELEASED)"] CallInterface["media_interface_->SendKeyEvent()<br/>AvrcpMediaInterfaceImpl"] JNICallback["sendMediaKeyEvent()<br/>Native函数(C++)"] JavaCallback["CallVoidMethod()<br/>调用Java方法"] NativeIntf["AvrcpNativeInterface<br/>.sendMediaKeyEvent()"] CheckService{"AvrcpTargetService<br/>是否为null?"} ServiceMethod["AvrcpTargetService<br/>.sendMediaKeyEvent()"] ConvertKey["AvrcpPassthrough.toKeyCode()<br/>AVRCP ID -> Android KeyCode<br/>例如: 0x44 -> KEYCODE_MEDIA_PLAY"] CreateEvent["创建KeyEvent对象<br/>action: ACTION_DOWN/ACTION_UP<br/>keyCode: Android标准按键码"] DispatchEvent["AudioManager<br/>.dispatchMediaKeyEvent()"] MediaTarget["分发到活跃的<br/>MediaSession控制器"] End["媒体播放器响应按键"] ErrorLog1["Log.w: AvrcpTargetService is null"] ErrorLog2["返回,不处理"] Start --> Receive Receive --> CheckOpcode CheckOpcode -->|"是"| CheckActive CheckOpcode -->|"否"| ErrorLog2 CheckActive -->|"是"| GetKeyInfo CheckActive -->|"否 (特殊处理)"| GetKeyInfo GetKeyInfo --> CallInterface CallInterface --> JNICallback JNICallback --> JavaCallback JavaCallback --> NativeIntf NativeIntf --> CheckService CheckService -->|"否"| ServiceMethod CheckService -->|"是"| ErrorLog1 ErrorLog1 --> ErrorLog2 ServiceMethod --> ConvertKey ConvertKey --> CreateEvent CreateEvent --> DispatchEvent DispatchEvent --> MediaTarget MediaTarget --> End style Start fill:#e1f5fe style End fill:#c8e6c9 style ErrorLog1 fill:#ffcdd2 style ErrorLog2 fill:#ffcdd2 style ConvertKey fill:#fff9c4 style DispatchEvent fill:#f3e5f5

关键代码位置

1. Native Stack

  • 文件: system/profile/avrcp/device.cc
  • 函数: Device::MessageReceived() (line ~810)
  • 功能: 接收并解析AVRCP PassThrough命令

2. Native Interface Implementation

  • 文件: android/app/jni/com_android_bluetooth_avrcp_target.cpp
  • : AvrcpMediaInterfaceImpl
  • 函数: SendKeyEvent() (line ~131)
  • 功能: 调用JNI层的sendMediaKeyEvent

3. JNI Layer

  • 文件: android/app/jni/com_android_bluetooth_avrcp_target.cpp
  • 函数: sendMediaKeyEvent() (line ~361)
  • 功能: 通过JNI CallVoidMethod回调Java层

4. Java Native Interface

  • 文件: android/app/src/com/android/bluetooth/avrcp/AvrcpNativeInterface.java
  • 函数: sendMediaKeyEvent() (line 119)
  • 功能: 接收native回调,转发给Service

5. AVRCP Service

  • 文件: android/app/src/com/android/bluetooth/avrcp/AvrcpTargetService.java
  • 函数: sendMediaKeyEvent() (line 511)
  • 功能: 转换按键码并分发给AudioManager

6. Key Code Converter

  • 文件: android/app/src/com/android/bluetooth/avrcp/helpers/AvrcpPassthrough.java
  • 函数: toKeyCode() (line 24)
  • 功能: 将AVRCP按键ID转换为Android KeyEvent码

按键映射示例

AVRCP ID 常量名 Android KeyCode 说明
0x44 PASSTHROUGH_ID_PLAY KEYCODE_MEDIA_PLAY 播放
0x46 PASSTHROUGH_ID_PAUSE KEYCODE_MEDIA_PAUSE 暂停
0x4B PASSTHROUGH_ID_FORWARD KEYCODE_MEDIA_NEXT 下一曲
0x4C PASSTHROUGH_ID_BACKWARD KEYCODE_MEDIA_PREVIOUS 上一曲
0x41 PASSTHROUGH_ID_VOL_UP KEYCODE_VOLUME_UP 音量增加
0x42 PASSTHROUGH_ID_VOL_DOWN KEYCODE_VOLUME_DOWN 音量减少

数据流

  1. 输入: AVRCP PassThrough命令 (操作ID + 按键状态)
  2. C++处理: 解析命令并通过接口回调
  3. JNI桥接: Native到Java的跨越
  4. Java转换: AVRCP ID → Android KeyCode
  5. 系统分发: AudioManager → MediaSession
  6. 输出: 媒体播放器响应(播放/暂停/切歌等)
posted @ 2025-11-27 19:07  蓝天上的云℡  阅读(6)  评论(0)    收藏  举报