米德宝 AIDL 音乐接口定义¶
最后更新:
迁移提示:EVCC 开放 API 1.1 已提供通用音乐、歌词、封面及播放控制。旧服务保留过渡期,字段和原授权规则不变;新接入请阅读 https://evcc.coauto.cc/docs/music/ 与 https://evcc.coauto.cc/docs/migration-midebao/ 。停用须完成客户端验收并另行公告。 适用版本:EVCC 3.1.3 起。AIDL 能力版本 3;音乐 JSON
schemaVersion为 1;原车辆属性快照仍为 2。
本文件可单独交给银河悬浮窗歌词开发者。音乐数据为独立通道,无需 EVCC 用户配置属性或变量。
1. 端点、开关与身份¶
| 项目 | 定义 |
|---|---|
| EVCC 包名 | com.kooo.evcc |
| Service 类 | com.kooo.evcc.midebao.MidebaoLinkBridgeService |
| 绑定 Action | com.kooo.evcc.midebao.BIND |
| 主 AIDL 描述符 | com.kooo.evcc.midebao.IMidebaoLinkBridge |
| 音乐回调描述符 | com.kooo.evcc.midebao.IMidebaoMusicCallback |
| 唯一接收应用 | 银河悬浮窗歌词 com.galaxy.lyricsnext,且安装签名通过 EVCC 既有米德宝签名校验 |
| 音乐共享条件 | 云端允许米德宝联动,并开启“米德宝联动”及“米德宝 AIDL 联动” |
| 音乐来源 | 已运行的 EVCC 音乐转发模块;共享不会自行启动音乐转发 |
新的三个音乐方法始终校验 Binder 调用 UID、唯一包名和 APK 签名。普通旧版 AIDL 的开放读取和应用写入授权不授予音乐权限。已验证的银河在共享关闭时仍可调用音乐方法、注册回调,只能取得 enabled=false 的空状态。
原 getProtocolVersion() 的读取开关校验保留:首次在米德宝 AIDL 关闭时握手可能得到 SecurityException;客户端应等待用户开启后重试,不通过 EVCC 应用版本猜测接口能力。成功查询且版本至少为 3 后使用音乐方法。已经注册的音乐回调在关闭后收到清空状态,并在重新开启时自动恢复,无需反复注册。
银河 Manifest 需要 EVCC 的包可见性声明:
绑定示例:
Intent intent = new Intent("com.kooo.evcc.midebao.BIND").setPackage("com.kooo.evcc");
context.bindService(intent, connection, Context.BIND_AUTO_CREATE);
2. 完整 AIDL 定义¶
以下三个文件置于客户端 src/main/aidl/com/kooo/evcc/midebao/,保留包名、方法顺序和类型。
IMidebaoLinkBridge.aidl:
package com.kooo.evcc.midebao;
import com.kooo.evcc.midebao.IMidebaoLinkCallback;
import com.kooo.evcc.midebao.IMidebaoMusicCallback;
interface IMidebaoLinkBridge {
int getProtocolVersion();
String getStatus();
String getSnapshotJson();
void registerCallback(IMidebaoLinkCallback callback);
void unregisterCallback(IMidebaoLinkCallback callback);
boolean writeProperty(int propertyId, int areaId, String value);
String getMusicSnapshotJson();
void registerMusicCallback(IMidebaoMusicCallback callback);
void unregisterMusicCallback(IMidebaoMusicCallback callback);
}
IMidebaoLinkCallback.aidl,原车辆属性回调:
package com.kooo.evcc.midebao;
oneway interface IMidebaoLinkCallback {
void onSnapshotChanged(String snapshotJson);
void onStatusChanged(String status);
}
IMidebaoMusicCallback.aidl,独立音乐回调:
package com.kooo.evcc.midebao;
oneway interface IMidebaoMusicCallback {
void onMusicStateChanged(String stateJson);
void onLyricsChunk(String lyricsChunkJson);
}
主接口原六个方法保持 FIRST_CALL_TRANSACTION + 0..5。新增方法按上述顺序为 +6、+7、+8;音乐回调的两个方法分别为 +0、+1。主接口同步返回,音乐回调为 oneway;EVCC 不等待银河完成业务处理,没有逐帧 ACK 或持久化重放。
客户端发布时应保留这三个 AIDL 接口及内部类的名称。特别是启用 -adaptclassstrings 时,避免混淆改写 Binder 描述符;示例目录提供 consumer-rules.pro。
| 新方法 | 语义 |
|---|---|
getMusicSnapshotJson() |
返回最新音乐状态 JSON,不包含整首歌词正文,不强制刷新播放器或启动音乐模块 |
registerMusicCallback(callback) |
立即安排当前状态和已有完整歌词;同一 Binder 重复注册不增加订阅,重新发送当前内容 |
unregisterMusicCallback(callback) |
取消该回调及其待发送任务;不会关闭 EVCC 音乐转发 |
onMusicStateChanged(stateJson) |
一份完整的音乐状态,包含当前行和完整歌词版本信息 |
onLyricsChunk(lyricsChunkJson) |
当前完整歌词的一片;收齐同版本所有片后替换完整歌词 |
3. 音乐状态 JSON¶
{
"schemaVersion": 1,
"streamId": "e806e643-c608-47d2-b29d-f78e317c1104",
"sequence": 42,
"trackId": "opaque-track-identifier",
"enabled": true,
"available": true,
"playbackState": "playing",
"title": "示例歌曲",
"artist": "示例歌手",
"album": "示例专辑",
"sourcePackage": "com.example.music",
"sourceAppName": "音乐",
"positionMs": 12500,
"positionAtElapsedMs": 800000,
"durationMs": 210000,
"playbackSpeed": 1.0,
"currentLyric": "当前这一句歌词",
"lineStartMs": 12000,
"lineEndMs": 16000,
"lyricOffsetMs": 0,
"lyricsAvailable": true,
"lyricsFormat": "lrc",
"lyricsRevision": 3,
"bpm": 120.0,
"bpmStatus": "matched",
"textTruncated": false
}
| 字段 | 类型 | 约定 |
|---|---|---|
schemaVersion |
int | 当前为 1;与 AIDL 能力版本独立 |
streamId |
string | EVCC 音乐发布器运行周期标识;进程重启后改变,普通开关切换不必改变 |
sequence |
long | 同一个 stream 内递增;首次订阅、同步读取与回调可能交错,允许序号有间隔 |
trackId |
string | 根据来源和歌曲元数据生成的不透明歌曲标识;客户端只做相等比较,无音乐时为空 |
enabled |
boolean | 音乐共享是否开启 |
available |
boolean | EVCC 音乐转发正在运行且存在音乐会话 |
playbackState |
string | playing、paused_or_stopped、none;当前源不区分暂停与停止 |
title / artist / album |
string | 歌名、歌手、专辑;缺少时为空 |
sourcePackage / sourceAppName |
string | EVCC 当前选中的音乐来源,与 EVCC 音乐模块保持一致 |
positionMs |
long | 音频播放位置,毫秒,非负 |
positionAtElapsedMs |
long | 该位置对应的 Android SystemClock.elapsedRealtime(),非 Unix 时间 |
durationMs |
long | 总时长,毫秒;0 表示未知 |
playbackSpeed |
number | 播放倍速;客户端仅在 playing 且数值有效时外推 |
currentLyric |
string | 当前歌词行;未知时为空 |
lineStartMs / lineEndMs |
long | 当前行在原歌词时间轴上的开始与结束毫秒;未知为 -1,最后一行结束可能未知 |
lyricOffsetMs |
long | 沿用 EVCC 音乐桥接偏移;以 positionMs + lyricOffsetMs 在原歌词时间轴中定位 |
lyricsAvailable |
boolean | 是否有可发送的完整歌词正文;不表示当前句一定非空 |
lyricsFormat |
string | lrc(带时间)、plain(纯文本整首)、none |
lyricsRevision |
long | 完整歌词内容或歌曲变化时递增;清空也使旧版本失效 |
bpm |
number | 当前有效 BPM;优先当前高精分段,未覆盖时使用整曲值;未识别为 0 |
bpmStatus |
string | unknown、matched、no_match;暂停保留已经识别的 BPM |
textTruncated |
boolean | 异常长的元数据或当前行是否被截短;不影响完整歌词正文 |
元数据的 UTF-16 长度上限:歌名、歌手、专辑、来源包名各 256,应用名 128,当前行 1024;截短不切断代理对。高频状态不包含封面、音频、整首歌词正文或完整 BPM 分段表。
enabled=false 或 available=false 时,歌曲、来源、当前行、完整歌词、BPM、进度全部清空,播放状态为 none。清空帧仍保留发布序号和歌词版本,不得继续显示缓存歌曲。普通暂停保留当前歌曲、歌词和 BPM,并冻结位置。
4. 完整歌词分片¶
{
"schemaVersion": 1,
"streamId": "e806e643-c608-47d2-b29d-f78e317c1104",
"trackId": "opaque-track-identifier",
"lyricsRevision": 3,
"chunkIndex": 0,
"chunkCount": 2,
"format": "lrc",
"text": "[00:00.00]第一句\n[00:12.00]当前这一句歌词\n"
}
chunkIndex从 0 开始,chunkCount是当前文档总片数。按序拼接text,不要自行添加换行或再次 URL 解码。- 分片身份为
(streamId, trackId, lyricsRevision),必须匹配当前音乐状态。相同身份重复片可忽略,旧歌和旧版本片直接丢弃。 - 每片
text最多 4096 个 UTF-16 单元,不切断 Unicode 代理对;包含 JSON 转义和接口头的实际 Parcel 不超过 64 KiB。完整歌词不被静默截断。 - EVCC 先向同一个回调发送关联音乐状态,再发送歌词片。歌词只在初次订阅、版本变化或重新注册时发送,不随每次进度重复。
- 切歌、歌词版本改变、共享停用或不可用时,立即丢弃未完成的旧文档。整首歌词尚未收齐时,可显示
currentLyric。 - 上游仅提供当前一句时,
currentLyric正常更新,lyricsAvailable=false,没有完整歌词片。当前句不能作为整首歌词保存。 - 清空通过音乐状态通知,不发送空歌词文档。短暂丢片可重新调用
registerMusicCallback()获取当前状态及整首歌词;客户端等待期间使用“最后收到歌词片后 3 秒”超时,不能从第一片起固定计时,以免超长文档被不断打断。
5. 推送频率与时间处理¶
首次注册、歌曲或来源变化、播放状态变化、可识别的进度跳转、当前行变化、歌词加载完成及 BPM 改变会立即安排推送;同一个调度轮次中的更新会合并。只有进度变化时最多每 500 ms 一帧。暂停期间不持续发送重复帧。
源没有显式跳转事件时,EVCC 将与原播放时钟偏差超过 250 ms 的位置更新视为跳转;更小的位置修正随下一进度帧同步。银河可以在两帧之间平滑外推:
仅当 enabled、available 为 true 且 playbackState 为 playing:
position = positionMs + max(0, elapsedRealtimeNow - positionAtElapsedMs) * playbackSpeed
否则:position = positionMs
position 至少为 0;durationMs > 0 时不得超过 durationMs。
歌词定位使用 position + lyricOffsetMs;音频进度条使用 position。
两端位于同一 Android 系统,可以共享 elapsedRealtime 时间基准。不要用 System.currentTimeMillis() 外推;EVCC/车机重启后以新绑定和新状态重新建立基线。
6. 连接、清空与恢复时序¶
银河绑定 EVCC
→ 查询 getProtocolVersion(),确认 >= 3
→ registerMusicCallback(callback)
← 当前音乐状态
← 当前完整歌词各片(如果有)
→ 可选 getMusicSnapshotJson(),按 sequence 去重
← 后续状态、当前行、进度及 BPM
关闭米德宝 AIDL / 云端撤销
← enabled=false 的清空状态
→ 银河清理歌曲、歌词缓存和进度动画
重新开启
← 已保留的合法订阅自动恢复当前状态及完整歌词
Binder 断开 / EVCC 重启
→ 银河立即清空展示,取消旧回调任务
→ 重新绑定,为新连接创建新回调对象
→ 重置 stream/sequence 基线并重新注册
注册后主动读取与初次回调可能重复或交错。只接收当前绑定代次的数据,同一 stream 丢弃 sequence <= 已应用序号 的状态。只有建立新绑定后才接受新的 stream;旧绑定已排队的回调不得覆盖新连接。
回调在 Binder 线程触发,JSON 解析、歌词拼接与 UI 更新应转移到客户端工作线程及主线程。EVCC 只保留最新音乐状态和当前歌词版本;回调异常或客户端死亡会清理订阅,恢复后需重连注册。回调入队不代表银河已执行任何业务动作。
7. 接入示例与验收¶
仓库提供可编译 Java 示例,目录为 docs/examples/midebao-music/:
GalaxyMusicClient.java:绑定、版本查询、独立回调、重连、歌词丢片重同步、主线程通知。MusicReceiverState.java:状态去重、旧版本丢弃、歌词重组、进度外推,无 Android 依赖,可单独测试。aidl/com/kooo/evcc/midebao/:与服务端一致的三个 AIDL 文件。README.md:集成方法和验证说明。consumer-rules.pro:发布包的 AIDL 描述符保留规则。
示例应集成进已获认可签名的银河应用,单独编译同名测试 APK 不会自动获得授权。示例的重组缓存限制为 8192 片,每片 4096 UTF-16 单元,超过时报告异常,不展示部分文档。
对接验收至少包括:不配置车辆属性即可接收音乐;仅开启旧版 AIDL 的其他应用无法读取新增音乐;暂停/恢复/切歌/跳转/倍速;完整、仅当前句及无歌词;歌词时间偏移;BPM 延迟、分段及无匹配;开关关闭清空及重新开启;EVCC 与银河各自重启;长歌词分片、重复片、旧版本和断线恢复。EVCC 编译或逻辑测试通过不能替代双方车机联调。