媒体会话能力补全规格
定义媒体通知、前台服务、音频焦点、后台租约与传输控制事件。
定义媒体通知、传输控制、Android 前台服务与后台 WebView 租约以及 Windows 等价控制的目标边界。
| 状态项 | 说明 |
|---|---|
当前 26.11.5 | 当前 26.11.5 基线没有 mediaSession.* 命令或事件,没有 MediaSession、媒体通知、传输控制、前台服务、唤醒锁或后台 WebView 租约;当前事件只有 webview.navigated、webview.reloaded、window.resized。 |
目标规格(未发布) | 规格 / 待实现:新增 mediaSession.update/clear 与 mediaSession.command;Android 接入前台服务、MediaStyle、音频焦点、唤醒锁和运行时租约;Windows 通过托盘菜单与全局快捷键事件提供等价控制。 |
实施阶段 | P3;依赖 P2 音频引擎和 P4 系统托盘与快捷键事件。 |
验收标准 | 锁屏或系统通知的播放、暂停、上一项、下一项、seek 能回到已订阅窗口;停止播放时释放前台服务、音频焦点、唤醒锁和后台租约。 |
媒体会话只维护系统可识别的播放元数据、播放状态和传输控制事件。播放列表、队列顺序、收藏与业务状态留在应用扩展层。会话与音频实例都按 windowId 绑定;一个窗口关闭不能结束其他窗口的后台播放。
| 层级 | 真实入口 | 责任 |
|---|---|---|
| 协议 | protocol/commands.json、protocol/permissions.schema.json、protocol/plugins.json | 目标命令、事件、权限、插件与平台矩阵。 |
| 前端 | frontend/src/bridge/ksm.ts、frontend/src/index.ts | KSM.invoke、事件订阅、重载恢复和 64 KiB 消息边界。 |
| Windows | windows/src/plugins/、windows/src/platform/ksm_windows_host_services.cpp、ksm_webview2_host.cpp | 托盘菜单、全局快捷键事件、窗口消息泵与播放状态转发。 |
| Android | KSMWebViewHost.kt、KSMWebViewRuntime.kt、KSMHostServices.kt、KSMRouter.kt、android/kasumi/src/main/AndroidManifest.xml | 前台服务、MediaSession、MediaStyle 通知、音频焦点、唤醒锁、通知权限与后台 WebView 租约。 |
| 依赖 | android/kasumi/build.gradle.kts、Gradle version catalog | 固定 Media3 Session 版本;权限与服务声明集中在 manifest。 |
| 能力 | 当前 26.11.5 基线 | 目标规格 / 待实现 |
|---|---|---|
| 会话状态 | 无系统媒体会话。 | mediaSession.update 接收标题、艺术家、专辑、时长、位置、播放状态和封面虚拟路径;mediaSession.clear 清理会话。 |
| 传输控制 | 无 mediaSession.command 事件。 | 播放、暂停、停止、上一项、下一项、seek 与自定义动作统一进入 mediaSession.command,按 windowId 投递给已订阅窗口。 |
| Android 前台服务 | 默认 WebView 与宿主没有媒体前台服务。 | 播放期间启动前台服务并显示 MediaStyle 通知;停止或会话清理时停止服务。 |
| 音频焦点与唤醒锁 | 无音频焦点与后台保活租约。 | 请求音频焦点,按焦点丢失暂停或降音量;持有部分唤醒锁;释放时归还全部资源。 |
| 后台 WebView 租约 | 后台 WebView 可能被系统暂停。 | KSMWebViewRuntime 持有按 windowId 计数的租约,保证 JS 事件与命令可调度;最后一个租约释放后恢复系统默认策略。 |
| Windows 等价控制 | 托盘与快捷键是占位实现。 | tray.action、tray.click、globalShortcut.triggered 转成等价传输控制;不创建 Android 风格前台服务。 |
| 通知权限 | notification.show 已有 Android 运行时权限流程。 | 媒体通知复用系统通知权限;未授权返回 PERMISSION_DENIED,不静默隐藏。 |
| 资源 | 获取时机 | 释放时机 |
|---|---|---|
| 前台服务与 MediaStyle 通知 | 首次 mediaSession.update 且状态为播放。 | mediaSession.clear、播放结束、窗口销毁或服务被系统终止。 |
| 音频焦点 | 播放开始前。 | 暂停、停止、焦点永久丢失或会话清理。 |
| 唤醒锁与后台 WebView 租约 | 后台播放需要 JS 调度时。 | 最后一个后台租约结束、停止播放或窗口销毁。 |
| MediaSession 与事件订阅 | 会话建立并订阅 mediaSession.command。 | 会话清理、重载恢复失败或窗口销毁。 |
真实入口是 kasumi-bridge 的 KSM 类。KSM.initialize() 创建传输,KSM.capabilities() 查询能力,KSM.on() 与 KSM.subscribe() 接收传输控制,KSM.invoke() 更新会话状态。
以下示例描述目标接口,尚未在 26.11.5 发布:
import { KSM, KSMWebMessageTransport } from 'kasumi-bridge';
KSM.initialize(new KSMWebMessageTransport());
const capabilities = await KSM.capabilities();
const permission = await KSM.permission.request('notification.show');
if (!permission.granted || !capabilities.commands.includes('mediaSession.update')) {
throw new Error('The media session is unavailable.');
}
const off = KSM.on<{ action: string; positionMs?: number }>(
'mediaSession.command',
async (payload) => {
if (payload.action === 'pause') {
await KSM.invoke('audio.pause', {});
}
if (payload.action === 'seek' && payload.positionMs !== undefined) {
await KSM.invoke('audio.seek', { positionMs: payload.positionMs });
}
}
);
await KSM.subscribe('mediaSession.command');
await KSM.invoke('mediaSession.update', {
title: 'Track',
artist: 'Artist',
durationMs: 240000,
positionMs: 0,
state: 'playing',
cover: 'app://cache/cover.jpg'
});
await KSM.invoke('mediaSession.clear', {});
off();
await KSM.unsubscribe('mediaSession.command');mediaSession.clear 是资源释放边界。调用后宿主必须停止前台服务、移除通知、放弃音频焦点、释放唤醒锁、减少后台租约引用并清空 mediaSession.command 订阅;不得只把通知设为不可见。
| 平台 | 实现边界 | 行为 |
|---|---|---|
| Windows | windows/src/platform/ksm_windows_host_services.cpp、ksm_webview2_host.cpp、windows/src/plugins/ | 托盘菜单与全局快捷键事件映射到 mediaSession.command;窗口消息泵把事件投到对应 windowId;窗口销毁时注销热键、移除托盘项并释放播放实例引用。 |
| Android | KSMHostServices.kt、KSMWebViewRuntime.kt、KSMWebViewHost.kt、KSMRouter.kt、android/kasumi/src/main/AndroidManifest.xml | 前台服务持有 MediaSession 与 MediaStyle 通知;音频焦点回调更新播放状态;唤醒锁与后台 WebView 租约按 windowId 引用计数;onTrimMemory 只影响当前窗口;Activity 或 Application 管理共享服务。 |
两端都不复制播放状态到进程级单例。webview.reloaded 后重新查询能力并恢复 mediaSession.command 订阅;迟到响应按 windowId 丢弃。
content:// URI;封面使用 app:// 虚拟路径。addJavascriptInterface,不直接调用私有 WebView 注入对象,不在前端按平台写分支。{ file },不以 base64 内联。mediaSession.clear 后保留前台服务、通知、音频焦点、唤醒锁或后台 WebView 租约;不把 shown: true 当作真实通知已显示。close() 不释放其他窗口的共享服务。| 错误码 | 触发条件 | 期望行为 |
|---|---|---|
INVALID_ARGS | 标题、状态、时长、位置、封面虚拟路径或动作非法。 | 前端守卫与宿主校验同时拒绝。 |
PERMISSION_DENIED | 通知权限、命令白名单或媒体会话权限未通过。 | 启动前台服务前拒绝。 |
NOT_SUPPORTED | 平台未实现媒体会话或等价控制。 | 能力查询排除相关命令与事件。 |
NOT_FOUND | 会话、播放实例或租约不存在。 | clear 保持幂等,不创建空服务。 |
IO_ERROR | 封面读取、通知构建或系统服务绑定失败。 | 释放已获取资源并返回可操作诊断。 |
CANCELLED / TIMEOUT | 窗口销毁、重载或请求超时。 | 取消令牌传播,抑制迟到事件。 |
MEDIA_UNSUPPORTED | 封面或媒体元数据无法解析。 | 会话保留文本元数据,返回结构化原因。 |
AUDIO_DEVICE_BUSY | 音频设备被占用。 | 不启动前台服务,等待显式释放后重试。 |
EXTENSION_CONFLICT | 应用扩展抢占 mediaSession.* 保留名。 | CLI 配置阶段拒绝。 |
| 现象 | 优先检查 |
|---|---|
| 锁屏按钮无响应 | MediaSession 回调、事件订阅表、windowId 路由、mediaSession.command 恢复流程。 |
| 后台播放一段时间后停止 | 前台服务、唤醒锁、音频焦点、后台 WebView 租约、系统电池策略。 |
| 停止后通知仍在 | mediaSession.clear 是否释放通知、服务、焦点、锁与租约。 |
| 多窗口互相影响 | 租约引用计数、close() 所有权、onTrimMemory 隔离、Host Services 生命周期。 |
| Android 通知不显示 | POST_NOTIFICATIONS 运行时权限、通知通道、MediaStyle 构建与前台服务类型声明。 |
| Windows 快捷键无响应 | 托盘/热键真实实现、窗口消息泵、命令到事件的映射。 |
| 重载后传输控制丢失 | webview.reloaded 恢复流程、能力缓存、订阅恢复。 |
pnpm validate:protocol、pnpm codegen、pnpm typecheck、pnpm test:contract、pnpm test:packages 通过;命令、事件、通知权限和平台矩阵无漂移。onTrimMemory 与多窗口隔离。mediaSession.clear 后前台服务、通知、音频焦点、唤醒锁、后台 WebView 租约与订阅全部释放;重复调用保持幂等。addJavascriptInterface,不在 JSON 中传递超过 64 KiB 的载荷,不引入 Rust/Node 中间 runtime,不写前端平台分支。
评论