SuperAPI AI 工具集

fgs-state-ended-trap

编程与开发

查看项目

更新于 10-17 16:15

技能介绍

用途

该技能解决 Android 媒体应用中,前台服务(FGS)在曲目切换瞬间被意外拆除的问题。当应用为每首曲目创建独立播放器对象时,从播放器 A 报告 ended 到播放器 B 接管播放,中间存在约 25 毫秒的窗口。在此期间媒体会话查询播放状态会得到 ended;而前台状态仅当播放器意图播放且状态为 ready 或 buffering 时才得以维持,ended 不满足条件,服务于是撤下通知并退出前台状态。在标准系统构建上应用通常能撑到播放器 B 就位,但在部分厂商激进电源管理构建上,应用会在该窗口内被冻结,播放于队列中途停止。

主要能力

该技能在转发播放器边界实施修复,核心是一个标志与一个 getter:

@Volatile var suppressPlaybackEnded = false

override fun getPlaybackState(): Int {
    val state = super.getPlaybackState()
    return if (state == Player.STATE_ENDED && suppressPlaybackEnded) Player.STATE_BUFFERING else state
}

包装器报告 buffering——这在实质上是真实的,因为下一个播放器正在准备中——服务便没有理由退出。技能同时列出多项需要规避的陷阱:

  • 在 getter 中重映射,而非改写事件:会话层不仅监听,还会在构建状态时主动查询播放器。在 onPlaybackStateChanged 到达监听器之前过滤,不会影响拉取路径,而拉取路径才是决定服务命运的那条。应覆写 getter,让事件照常发出。
  • 按“是否存在下一项”分支:无条件重映射会导致队列真正结束也永不上报,UI 一直转圈、通知不清除、服务永不退出,这是更隐蔽也更严重的缺陷。仅在即将继续的分支设置标志:
Player.STATE_ENDED -> {
    if (hasNextMediaItem()) {
        forwardingPlayer.suppressPlaybackEnded = true
        transitionToState(InternalState.PREPARING)
    } else {
        transitionToState(InternalState.ENDED)
    }
    handleTrackEndInternal()
}
  • 每条退出路径都要清除:只设置不清除会让播放器永久回答 buffering。必须清除的路径包括:下一个播放器实际开始播放(预期退出)、曲目加载完成并到达 ready、加载失败(错误路径若遗留标志,应用会永远显示忙碌而非报错)、pause() 与 stop()(用户自行结束播放,标志不得超越该决定)。建议用 grep 搜索赋值而非声明,= false 的出现次数应与转换退出路径数一致。
  • 标记 @Volatile:标志在曲目结束时由播放线程写入,由会话查询所在线程读取,缺少该注解可能导致清除不可见,播放器在进程余下时间一直报告 buffering。

使用方式与验证

该问题不会在普通调试中复现。测试需开启设备最严格的电池优化设置、将应用置于后台、锁屏并让长队列无人值守运行,而非单步调试(调试器会把窗口拉宽到不现实的长度)。应记录包装器返回的值而非底层播放器报告的值。

在仓库根目录执行以下命令验证,标志及调用点位于 core/media/media3/src/main/java/com/maxrave/media3/exoplayer/:

grep -n -B1 "var suppressPlaybackEnded" \
  core/media/media3/src/main/java/com/maxrave/media3/exoplayer/DelegatingForwardingPlayer.kt

通过条件:声明上一行为 @Volatile。

grep -n "suppressPlaybackEnded = " \
  core/media/media3/src/main/java/com/maxrave/media3/exoplayer/CrossfadeExoPlayerAdapter.kt

通过条件:一个 = true(转换开始)与五个 = false 点位,分别对应四类退出(下一播放器播放、到达 ready、加载失败、暂停/停止)。= false 少于状态机退出路径数即会卡住。

grep -n "override fun getPlaybackState\|override fun onPlaybackStateChanged" \
  core/media/media3/src/main/java/com/maxrave/media3/exoplayer/DelegatingForwardingPlayer.kt

通过条件:仅覆写 getPlaybackState。若改为在 onPlaybackStateChanged 上过滤,会话自身的拉取式查询仍会看到真实的 ENDED 状态。

手工测试:开启最严格电池优化、播放四首以上队列、后台、锁屏、无人值守运行。正确表现为音频跨过每个曲目边界;回归表现为播放恰好在边界处停止(而非曲中)且解锁后通知消失。

适用场景与限制

适用于使用媒体会话与前台服务的 Android 媒体应用,尤其是每首曲目使用独立播放器对象并采用交叉淡入等切换机制的场景。限制包括:抑制会向自身代码隐藏真实的流结束,任何通过包装器读取状态的组件(作为 ended 时重置的 UI、统计完成次数的分析)也会看到 buffering,因此内部状态机应依赖自身信号,将重映射值视为导出给平台的视图;不要为求稳妥而延长窗口,设置应尽可能晚、清除应尽可能早,并在语言允许时置于 finally 形态的无条件位置。