AVSessionKit:播客播放状态发布与会话销毁边界【鸿蒙心迹】
做播客播放器时,有一个问题往往要到接入系统媒体控制中心才看得出来:应用内已经暂停,系统控制卡片却还显示播放;或者用户离开页面后,卡片仍然保留上一集的标题。播放按钮和系统控制中心不是同一个视图,不能以为给按钮换个图标,系统那一份状态就自动同步了。这次用 WaveJournal 做一个很小的播客状态发布 Demo。示例节目固定为 POD_020_03,标题《城市漫步》,会话标签 WAVE_020。演示终态为 PAUSED,当前进度显示 12:36 / 28:00,本地版本号为 revision=4,另有一次过期更新被忽略。这里展示的是会话元数据和状态的一致性设计,并没有实际播放音频、申请后台长时任务或取得真实设备控制中心的运行截图。
图 01:播客会话状态主题封面。画面里的媒体卡片是概念设计,不代表系统媒体控制中心的真实截图。## 一、先区分播放器状态与 AVSession 状态播放引擎负责真正的声音:资源准备、解码、暂停、进度和错误。AVSession 则是应用向系统暴露媒体元数据、播放状态并响应外部控制的通道。它不是新的播放器,更不应成为应用内部唯一的播放状态来源。否则用户在应用里点了暂停,业务模型没有更新,系统卡片却被提前修改,两个界面就会开始互相打架。我倾向于把播放器确认过的状态整理成一个 SessionSnapshot。它至少包含内容 ID、播放状态和本地代次。代次不是 AVSessionKit 的字段,而是我们自己防止异步结果倒灌的一把尺子。例如用户先选 A,再快速切到 B,A 晚到的准备完成回调不应该把 B 的标题改回去。下面这一段代码解决的是只接受当前节目当前代次的状态更新,尚未调用任何系统 API。arktstype PodcastStage = 'IDLE' | 'PLAYING' | 'PAUSED'interface SessionSnapshot { assetId: string title: string stage: PodcastStage revision: number}class PodcastStateGate { private current: SessionSnapshot = { assetId: 'POD_020_03', title: '城市漫步', stage: 'IDLE', revision: 4 } ignored: number = 0 accept(assetId: string, revision: number, stage: PodcastStage): boolean { if (assetId !== this.current.assetId || revision !== this.current.revision) { this.ignored++ return false } this.current.stage = stage return true } snapshot(): SessionSnapshot { return { ...this.current } }}这里的校验刻意同时比较 assetId 和 revision。只比较节目 ID 不足以识别同一节目被重新打开后的旧回调;只比较代次则会把节目身份藏起来,不利于排查。ignored 是诊断计数,不要将它当作平台统计。节目总时长和当前进度应该来自播放器确认结果,示例界面上显示的 12:36 / 28:00 是预设演示值,不是这段状态类计算出来的。## 二、创建会话以后,元数据与播放状态要成对发布华为 AVSession Provider 文档明确给出了 createAVSession()、activate()、setAVMetadata()、setAVPlaybackState() 和 destroy()。在同一个 UIAbility 里还要遵守会话数量限制,不能每次打开播客详情页就创建一份新会话。页面可以反复出现,代表一个播放任务的系统媒体会话却应该由更长寿命的业务拥有者管理。下面的关键代码解决创建一份 audio 会话,并发布节目标题与暂停状态的问题。它应当放在应用里专门管理媒体会话的服务中;这里为了便于阅读,仅展示必须调用的路径。arktsimport { avSession } from '@kit.AVSessionKit'class PodcastSessionOwner { private session?: avSession.AVSession async ensureStarted(context: Context): Promise<void> { if (this.session !== undefined) return const created = await avSession.createAVSession( context, 'WAVE_020', 'audio') try { await created.activate() await created.setAVMetadata({ assetId: 'POD_020_03', title: '城市漫步', artist: '李游播客' }) await created.setAVPlaybackState({ state: avSession.PlaybackState.PLAYBACK_STATE_PAUSE }) this.session = created } catch (error) { await created.destroy() throw error } }}````activate()` 成功不等于播放已经开始,`PLAYBACK_STATE_PAUSE` 才是这次示例要向系统发布的业务判断。元数据至少需要稳定的 `assetId`,后续切集时要先确认新资源归属,再配套修改系统侧的元数据和状态。因为这些调用都是异步的,生产项目还要串行化发布,或者给发布任务附带本地代次,不然先发出的旧请求可能晚于新请求完成。`ensureStarted()` 是业务方法名,不是平台接口。示例还省略了播放控制命令的订阅、封面像素数据、错误码分类与后台音频任务申请。尤其不能据此写出“切到后台仍然稳定播放”的结论,那是另外一组权限、任务与播放器生命周期问题。*图 02:DevEco Studio 白色主题演示图。右侧演示 `WaveJournal` 页面,底部 HiLog 文字是预设日志:`publish POD_020_03 PAUSED revision=4` 和 `stale update ignored 1`,不是真实设备日志。*## 三、页面关闭不等于会话销毁播放页面离开时有两种情况。第一种是用户只是返回播客列表,音频还会继续或者允许后台控制;这时销毁会话会让控制中心失去继续操作的入口。第二种是用户明确停止播放,播放资源和会话的业务寿命已经结束;这时才应该解除订阅、停播并销毁会话。把 `destroy()` 直接写在某个页面的 `aboutToDisappear()` 里,往往是系统卡片闪烁、来回重建的来源。第三段代码展示明确结束播放任务时的释放顺序:先废弃当前异步发布代次,停止继续提交状态,再停用会话并销毁;释放过程中失败时也不能把会话对象当作仍然可用。arktsclass SessionLifetime { private revision: number = 4 private closed: boolean = false nextRevision(): number { return ++this.revision } isCurrent(value: number): boolean { return !this.closed && value === this.revision } close(): void { this.closed = true; this.revision++ }}// PodcastSessionOwner 内的业务释放方法// 真实项目应在停止播放器和取消监听后调用async function disposeAVSession( session: avSession.AVSession | undefined): Promise { if (session === undefined) return try { await session.deactivate() } finally { await session.destroy() }}```这里 SessionLifetime 是纯业务状态工具,用来拦住关闭后才返回的旧发布任务;并不提供跨进程锁,也不能取消已经发送给 AVSession 服务的系统调用。disposeAVSession() 演示的是调用次序,真正的拥有者还要把内部 session 引用清空,并保证两个并发释放请求不会同时触发 destroy()。如果应用有媒体控制回调监听,也应当先解除对应监听,避免销毁期间仍收到操作事件。## 四、调试时应当盯住三份快照我会把调试表拆成三列:播放器实际状态、业务层 SessionSnapshot、系统会话最后一次成功发布的值。只有三列在同一代次里一致,才能说状态同步链路做到了可解释。比如本轮演示的 POD_020_03,业务终态 PAUSED,版本号 4,过期回调忽略一次。示例 UI 另外显示 12 分 36 秒进度,但因为没有音频引擎参与,这只是用于验证文字一致性的固定值。
*图 03:9:16 纯手机效果图,显示《城市漫步》、POD_020_03、PAUSED、revision 4 和 12:36 / 28:00。图中“演示状态”用于提醒读者它不是实际播放截图。*准备在 DevEco 中验证时,我建议先从单线程的“启动→发布暂停→切换节目→停止”路径开始,再专门制造旧异步回调:先提交代次 3 的更新,再提交代次 4,把代次 3 的完成安排到最后。若系统卡片又变回旧节目,说明状态发布链缺少最终身份检查。真实系统控制中心还可能有刷新延迟,不能拿刚点击后的瞬间截屏替代等待稳定状态的观察。为了避免“释放”和“隐藏”两个词混淆,我还会在状态表中增加业务结束原因:用户主动停止、播放到末尾、服务端资源失效与应用异常退出。四种情况不一定采用完全相同的 UI 文案,却都必须有明确的会话所有权判断。尤其是资源失效时,不应立刻用另一个节目复写系统卡片,应该先确认播放引擎已经停止旧资源,再决定是否启动下一集。排查手段也尽量朴素:用同一个 assetId 串起播放器事件、业务模型记录和媒体会话发布记录;每次切集写入 revision;当异步结果被拒绝时留下旧、新代次,而不是笼统打印“更新失败”。这样一旦用户反馈系统卡片显示错集,就能回答到底是哪一层晚到了,而不是靠肉眼猜回调次序。## 五、适用边界与工程取舍AVSessionKit 的职责是把媒体信息与控制能力提供给系统,而不是替代音频播放器。本文只讨论发布与释放,不涵盖媒体焦点竞争、蓝牙耳机按键响应、锁屏控制、自定义会话队列或后台持续播放。实际项目可能还需要音频焦点策略、WantAgent 跳转和系统能力版本检查。我最终保留的设计原则是:播放器拥有事实,业务快照拥有版本,AVSession 负责对外发布。 页面消失只处理页面;音频任务真正结束才回收媒体会话。用这条边界去梳理状态,开发阶段的疑难杂症往往比反复刷新 UI 更容易解释。资料与验证范围: 华为 AVSession Provider 指南 https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V13/using-avsession-developer-V13 。其中列出的会话接口从 API 10 起支持,具体设备能力与 SDK 应以实际工程为准。本稿代码和配图未经过 DevEco 编译或真机运行验证。
更多推荐



所有评论(0)