当前位置:首页 > 技术 > 正文内容

Android 蓝牙音频协议详解

访客 技术 2026年7月29日 3

核心协议介绍

在蓝牙音频通信中,有两个关键协议:

  • AVRCP(音频视频远程控制协议):用于控制操作,如播放、暂停、音量调节等。分为CT(控制器)与TG(目标设备)两个角色。
  • A2DP(高级音频分发协议):用于高质量音频流的传输,通常配合Sink(接收端)和Source(发送端)使用。

AVRCP结构

A2DP结构

底层实现机制

蓝牙音频协议运行于HCI(主机控制接口)之上,可通过抓取 btsnoop 数据包进行分析。

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

# 重启蓝牙前关闭再开启
# 复现问题后及时导出日志,避免被覆盖
adb pull /data/misc/bluetooth/logs/ ./
adb logcat -d > bt_log.log

RK 平台蓝牙组件说明

  • Libbt:负责初始化工作,包括固件加载。常见路径如下:
    • Broadcom:hardware/broadcom/libbt/conf/rockchip/rksdk/bt_vendor.conf
    • Realtek:hardware/realtek/rtkbt/code/vendor/etc/bluetooth/rtkbt.conf
  • Bluedroid:位于 system/bt 目录下,编译生成 libbluetooth.so 库。

相关配置文件及参数:

  • 日志级别设置:/system/etc/bt_stack.conf
  • 日志保存路径:/data/misc/bluetooth/logs/btsnoop_hci.log
  • 控制绝对音量属性:persist.bluetooth.disableabsvol

AVRCP 控制报文格式

AVRCP报文示意图

  • 音量调整通过 VENDOR DEPENDENT 操作码(如 0x1958)完成,由 CT 发起请求,TG 回应确认。
  • 播放控制类指令则采用 Pass Through 方式传递。
  • CT 和 TG 同时支持 CONTROL、STATUS 和 NOTIFY 功能。

AVRCP 控制模式

调试实例分析

假设某平台音量键可正常使用,但播放/暂停/下一曲无响应:

首先模拟发送暂停事件:

input keyevent 127

若应用未收到该事件,则需进一步排查 TG 端行为:

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

TG 日志输出

如果日志中找不到对应内容,可通过 dumpsys 查看详细调用栈:

dumpsys bluetooth_manager | grep sendMediaKeyEvent

dumpsys 输出

Android 媒体会话机制

要使应用程序能接收并响应蓝牙遥控指令,必须正确注册 MediaSession:

MediaSession 注册示意

sendMediaKeyEvent 执行流程

整体架构图

flowchart TB subgraph "Remote Bluetooth Device" Remote["Remote Device
Sends AVRCP Command"] end subgraph "Bluetooth Native Stack" AVCTP["AVCTP Layer
(Protocol Handler)"] Device["Device.cc
MessageReceived()"] Interface["AvrcpMediaInterfaceImpl
SendKeyEvent()"] end subgraph "JNI Layer" JNI["com_android_bluetooth_avrcp_target.cpp
sendMediaKeyEvent()"] end subgraph "Java Framework" NativeInterface["AvrcpNativeInterface
sendMediaKeyEvent()"] AvrcpService["AvrcpTargetService
sendMediaKeyEvent()"] Passthrough["AvrcpPassthrough
toKeyCode()"] end subgraph "Android System" AudioManager["AudioManager
dispatchMediaKeyEvent()"] MediaSession["MediaSessionManager
Distributes to Active Player"] end Remote -->|"AVRCP PassThrough"| AVCTP AVCTP -->|"Parse Message"| Device Device -->|"Call Interface"| Interface Interface -->|"JNI Callback"| JNI JNI -->|"CallVoidMethod"| NativeInterface NativeInterface -->|"Forward"| AvrcpService AvrcpService -->|"Convert Key Code"| Passthrough AvrcpService -->|"KeyEvent"| AudioManager AudioManager --> MediaSession

具体执行步骤

flowchart TD Start["Remote sends AVRCP PassThrough command
e.g., PLAY/PAUSE/NEXT/PREV"] Receive["Device::MessageReceived()
Parses incoming message"] CheckOpcode{"Is Opcode PASS_THROUGH?"} CheckActive{"Is device active?"} GetKeyInfo["Extract Key Info:
• OperationId
• KeyState (PUSHED/RELEASED)"] CallInterface["media_interface_->SendKeyEvent()
AvrcpMediaInterfaceImpl"] JNICallback["sendMediaKeyEvent()
C++ JNI function"] JavaCallback["CallVoidMethod
JNIEnv callback to Java"] NativeIntf["AvrcpNativeInterface.sendMediaKeyEvent()"] CheckService{"Is AvrcpTargetService null?"} ServiceMethod["AvrcpTargetService.sendMediaKeyEvent()"] ConvertKey["AvrcpPassthrough.toKeyCode()
Map AVRCP ID to Android KeyCode"] CreateEvent["Create KeyEvent object
Action: DOWN/UP
KeyCode: Android standard"] DispatchEvent["AudioManager.dispatchMediaKeyEvent()"] MediaTarget["Deliver to active MediaSession controller"] End["MediaPlayer responds to event"] ErrorLog1["Log.w: AvrcpTargetService is null"] ErrorLog2["Return without processing"] Start --> Receive Receive --> CheckOpcode CheckOpcode -->|"Yes"| CheckActive CheckOpcode -->|"No"| ErrorLog2 CheckActive -->|"Yes"| GetKeyInfo CheckActive -->|"No (special handling)"| GetKeyInfo GetKeyInfo --> CallInterface CallInterface --> JNICallback JNICallback --> JavaCallback JavaCallback --> NativeIntf NativeIntf --> CheckService CheckService -->|"Null"| ErrorLog1 CheckService -->|"Valid"| ServiceMethod 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 层
    • 文件:system/profile/avrcp/device.cc
    • 函数:Device::MessageReceived()(约第810行)
    • 作用:解析 PassThrough 类型的 AVRCP 消息
  2. 接口实现
    • 文件:android/app/jni/com_android_bluetooth_avrcp_target.cpp
    • 类:AvrcpMediaInterfaceImpl
    • 函数:SendKeyEvent()(约第131行)
    • 作用:调用 JNI 函数触发 Java 方法
  3. JNI 桥接
    • 文件:android/app/jni/com_android_bluetooth_avrcp_target.cpp
    • 函数:sendMediaKeyEvent()(约第361行)
    • 作用:从 C++ 到 Java 的跨语言调用
  4. Java 接口层
    • 文件:android/app/src/com/android/bluetooth/avrcp/AvrcpNativeInterface.java
    • 函数:sendMediaKeyEvent()(第119行)
    • 作用:接收 native 调用,并转发至服务层
  5. 服务逻辑
    • 文件:android/app/src/com/android/bluetooth/avrcp/AvrcpTargetService.java
    • 函数:sendMediaKeyEvent()(第511行)
    • 作用:将 AVRCP 键值转换为 Android 标准键码,并交由系统处理
  6. 键码转换工具
    • 文件:android/app/src/com/android/bluetooth/avrcp/helpers/AvrcpPassthrough.java
    • 函数:toKeyCode()(第24行)
    • 作用:建立 AVRCP ID 与 Android KeyEvent 映射关系

常用键值对照表

AVRCP ID 常量名称 Android 键码 功能描述
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. 输入:来自远程设备的 PassThrough 命令(含操作 ID 和按键状态)
  2. C++ 处理阶段:解析命令并调用接口回调
  3. JNI 过渡:连接 Native 与 Java 层的关键桥梁
  4. Java 转换:将 AVRCP ID 映射为 Android 标准键码
  5. 系统分发:AudioManager 将事件传递给当前活跃的 MediaSession
  6. 最终输出:媒体播放器根据事件做出相应动作(如播放、暂停或切换曲目)

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。