音乐与歌词(API 1.1)¶
最后更新:
音乐是授权会话的一部分,面向任何独立 UID 的第三方 Android 应用。无需打开米德宝联动、配置旧属性 JSON 或申请固定包名白名单。先由用户开启 EVCC 音乐转发,并在开放 API 中授予应用所需权限。接口只观察已有音乐来源,不因读取、监听或能力查询启动播放器、查询歌词网站或请求 BPM 分析。
最小接入¶
val client = EvccApiClient(context)
client.addListener(object : EvccApiClient.Listener {
override fun onSessionReady(apiInfo: EvccApiInfo, sessionInfo: EvccSessionInfo) {
if (apiInfo.apiMinor < 1 || "music.listen" !in sessionInfo.grantedPermissions) return
executor.execute { client.setMusicSubscription(true) }
}
override fun onMusicStateChanged(state: EvccMusicState, sequence: Long, eventType: String) {
// SDK 回调在主线程:更新标题、当前行;进度可用 EvccMusicResources.positionAt(state)。
// status 非 AVAILABLE 时清空旧内容;暂停保留歌曲。
}
override fun onMusicLyrics(lyrics: EvccMusicResources.Lyrics) {
// SDK 默认自动获取完整歌词;text 为空时清空旧歌词。
}
})
client.connect()
// 销毁消费者时 client.close();临时停止监听可 setMusicSubscription(false)。
可编译 Java/Kotlin 示例随 Demo 源码包提供:MusicClientExample.java、MusicClientExampleKt.kt。它们只订阅,控制必须由用户操作显式触发。Flow musicEvents() 只观察事件,不隐式开启订阅;它合并完整快照并保留最新状态,适合较慢的界面消费者。Flow 可能合并中间序号,Binder 缺口由 SDK 在合并前处理。Flow 消费者同样需要处理连接回调,在断开时清空页面。
状态与歌词¶
readMusicState() 与回调返回同一个 EvccMusicState。成功为 AVAILABLE;音乐转发未运行为 SERVICE_DISABLED,没有真实当前播放器或只有历史占位信息为 UNAVAILABLE。这些音乐状态不关闭整个 API 会话。歌曲标题、当前行等轻量文本沿用有界投影,textTruncated 标识摘要是否截断,完整歌词资源不截断。
trackId 是不透明的运行周期内标识,不是音乐网站 ID,重启后必须重新读取。歌词和封面各自维护 revision;只在对应版本变化时重新读取资源。当前行可能有值但完整歌词仍为 NOT_FOUND,不能把当前行当作整首歌词。
playbackState 为 PLAYING、PAUSED_OR_STOPPED 或 NONE,不根据暂停快照虚构停止状态。positionAtElapsedMs 使用同一设备的 SystemClock.elapsedRealtime() 毫秒;不能用 Unix 时间替代。未知进度和时长为 -1,未知歌词/分段边界为 -1,未知时钟锚点为 0。播放进度最多每 500 ms 推送一次;歌词行、状态或资源版本变化可更及时发送。
lyricOffsetMs 是共享歌词时间偏移。当前行已经按“播放位置 + 偏移”计算;客户端自行解析完整 LRC 时应用一次偏移,不要重复叠加。lineStartMs / lineEndMs 是歌词时间轴上的原始边界。无歌词、当前行不带时间或末行结束未知时使用 -1。
完整歌词与封面¶
// 工作线程调用;SDK 自动歌词加载可用 setAutomaticMusicLyrics(false) 关闭。
val state = client.readMusicState()
if (state.lyricsStatus == EvccApiContract.Status.AVAILABLE) {
val lyrics = client.readMusicLyrics(state)
// 展示前确认 trackId/revision 仍是当前版本。
}
if (state.artworkStatus == EvccApiContract.Status.AVAILABLE) {
client.openMusicArtworkStream(state).use { input -> /* 解码图片 */ }
}
Raw AIDL 调用 openMusicLyrics(trackId, lyricsRevision) 或 openMusicArtwork(trackId, artworkRevision),得到 EvccMusicResource。仅 AVAILABLE 时读取 descriptor,使用后关闭资源/流。歌词为 UTF-8 LRC 或纯文本(格式见状态的 lyricsFormat);封面来自 EVCC 已有本地缓存。
资源通过只读文件描述符传递,不将大歌词、图片字节放进频繁回调,不要求拼接 JSON 分片,也不暴露私有路径。每次打开重新检查会话授权;已交付的字节或描述符无法被远程收回。文件版本不可变,切歌不改变已打开内容。
STALE_RESOURCE:歌曲或版本已过期。丢弃结果,重新读取当前状态;不要把新歌数据填进旧歌。PENDING:封面尚未由音乐来源准备好。等待版本/状态更新,避免紧密轮询。NOT_FOUND:该内容不存在;PAYLOAD_TOO_LARGE:单个资源超过 8 MiB,正文不静默截断。PERMISSION_DENIED/SESSION_CLOSED:停止读取,处理授权或连接。
SDK 自动歌词只按版本加载,在切歌、取消、断开、重连后丢弃旧任务结果;显式读取助手仍由调用方决定如何展示,并在展示前确认目标仍是同一首歌。控制台预览最多展示 65536 个字符,资源本身保持完整。
权限与能力¶
| 权限 | 范围 |
|---|---|
music.read |
状态、歌词、封面、收藏状态、已有 BPM |
music.listen |
监听,必须同时有 music.read |
music.control |
显式播放控制,可独立授予 |
只读预设包含读取/监听,标准联动增加控制;升级不会给已有授权自动增权。queryCapabilities() 包含这些权限能力及 music.lyrics、music.artwork、music.bpm、music.favorite 读取能力。能力存在不代表当前歌曲有内容,具体看状态;supportedCommands 描述当前可执行命令,未授权控制时为空。
播放控制¶
val state = client.readMusicState()
val result = client.sendMusicCommand(EvccMusicCommand(
command = EvccMusicCommand.SEEK,
positionMs = 12000,
expectedTrackId = state.trackId,
validateOnly = false,
))
支持 PLAY、PAUSE、TOGGLE、NEXT、PREVIOUS、SEEK 和可选 SET_FAVORITE。收藏使用 favorite: Boolean,定位使用非负毫秒;已知时长时不能超过时长。validateOnly=true 只校验权限、参数、当前目标和支持情况。
控制只针对当前真实播放器,不唤醒历史播放器。明确的 PAUSE 不解释为切换播放。当前实现依据 Android MediaSession 声明的 actions 开放命令;没有可验证控制能力的原生来源仍可提供音乐数据,但控制返回 UNSUPPORTED。收藏必须具备心形收藏类型及设置评分能力。
expectedTrackId 非空时防止用户点按后目标已切歌;空表示当前目标。ACCEPTED 表示命令已交付,最终效果看状态回调;OK 用于预检查成功。权限、目标和能力在实际执行前再次检查。每 UID 控制突发 10 次、持续每秒 2 次;SDK 不重放控制,超时也不要盲目重试切歌或切换播放。
BPM 与生命周期¶
BPM 来自已有分析结果:bpmStatus 为 matched、no_match、unknown,无有效值时为 0。整曲值和当前值分开,真实分段存在时提供起止时间;这是速度/BPM 信息,不是逐拍时间事件。
音乐事件与属性、变量、来源事件共用会话序号。重复开启订阅替换本会话音乐订阅并发送初始快照,不叠加监听;慢消费者遇到缺口后 SDK 重新订阅。取消、撤权和 Binder 死亡释放监听;最后一个消费者离开后停止音乐 API 自身计时。
1.0 客户端无需实现新回调,因为没有请求新订阅。新 SDK 连接 1.0 服务时音乐方法返回 UNSUPPORTED,原属性/变量能力继续使用。Raw AIDL 必须先确认 apiMinor >= 1。
迁移旧客户端请阅读米德宝迁移指南。