1908 字
10 分钟
Android 音频焦点与共存播放机制研究

Cover Image 画中游 | 九道梵音 #Pixiv

Android 音频焦点与共存播放机制研究#

前言#

最开始处理 Flutter 音视频播放的“后台被抢焦点就暂停”问题时,很容易先入为主地认为:既然想和网易云或其它音乐软件“同时播放”,那我不去抢音频焦点不就得了?毕竟按照直觉,你不惹事,别人自然也不会来弄停你。

但真往 Android 的底层和实现里翻,就会发现事情没有这么简单。在现代 Android(尤其是 Android 15+ 引入了 AudioHardening 机制后),音频焦点的管理可以说是一门非常“玄学”且充满强权的艺术。

本文主要基于 PiliNara 的实际迭代经验和系统级日志,整理一下 Android 音频焦点的运作机制,以及在 Flutter 环境下,如何利用系统协议的“潜规则”来实现与其他应用的完美混音共存。

什么是音频焦点(Audio Focus)?#

它并不是物理排他的#

我们常听说的“音频焦点(Audio Focus)”,在 Android 系统里本质上并不是一个物理开关,而是一种**“礼貌协议”(Cooperative Protocol)**。

当你想播放声音时,你需要向系统(AudioManager)发出一个 requestAudioFocus 请求,声明你需要占用声道。系统会根据你请求的等级,给当前正在播放的其他 App 发送不同级别的 LOSS(丢失焦点)回调:

  • AUDIOFOCUS_LOSS (-1):永久丢失。比如接到了电话,或者用户打开了另一款大型的独占音乐 App。
  • AUDIOFOCUS_LOSS_TRANSIENT (-2):临时丢失。比如微信来了条提示音,或者用户在那边刷了一个短视频。
  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK (-3):临时丢失,但允许你“低头”(Duck)。比如导航播报时,你可以把自己的音量压低一半,不用完全闭嘴。
TIP

对应谷歌文档的链接:AudioManager

核心事实:收到这些信号后,系统通常不会直接物理做相关的操作(前提是你是合法的媒体应用),而是看你自己的代码怎么写。常规的、老实的 App 收到 -1-2 时,代码里会乖乖调用 player.pause()。于是外界看起来就像是“被抢走焦点,声音断了”。

“不申请焦点”行不通吗?#

既然收到信号就要乖乖暂停,那最直觉的想法自然是:那我干脆一上来就不申请音频焦点,是不是系统就不管我了?

在早年间的 Android 系统中,这确实可行。但在最新的 Android 15+ 机制下,这其实是在“裸奔”。系统引入了 AudioHardening 策略。如果你作为一个后台媒体应用,试图在没有持有有效音频焦点(或相应权限等级不足)的情况下向硬件输出音频数据,系统会立刻探测到你的非法行为,并从硬件层强制执行 Mute(静音)。

抓到的底层日志非常直白:

AudioHardening background playback would be muted for com.example.pilinara (10164), level: full

所以得出一个非常明确的结论:想要播放,你必须去拿焦点,这是“准入证”。

“厚脸皮”协议:两个独占焦点如何共存#

既然必须老老实实当“大哥”去申请最高级别的独占媒体身份(AUDIOFOCUS_GAIN,即 req=1),那当其他音乐软件也申请 req=1 的时候,必定有一方会收到 LOSS_TRANSIENTLOSS 信号。那为什么我们在用网易云音乐时,开启了对应的与其他应用同时播放功能却能一边听歌一边顺畅地刷 B站?

这就是 Android 音频焦点机制中最有意思的地方。

如前所述,系统发出的 LOSS 信号更像是一种“道德约束”。当网易云(App B)发起 req=1 抢焦点时,系统给当前正在播放的前台 App A 发送了一个 LOSS_TRANSIENT (-2) 信号。

  • 普通 App 的反应:收到信号 -> 触发监听 -> 乖乖暂停。
  • 网易云 / 厚脸皮 App 的反应:收到信号 -> 代码里直接写一个 return // ignore this signal -> 拒不暂停,继续疯狂往底层的 AudioTrack 塞数据。

结果就是,Android 系统的底层混音器(AudioFlinger)看到两个流都有数据源,就会机械地把两个 App 的数据帧混合在一起。只要你具备了前台媒体服务(mediaPlayback)的系统权限,系统并不会直接封杀你。这就是实现两个 GAIN 级别共存的底层密码。

Flutter (audio_session) 下的最佳实践#

分析完了底层逻辑,在 Flutter 项目中,尤其是当底层视频库(如基于 mpv 的 media_kit)把焦点管理都全权交给我们另外引入的 audio_session 插件时,该如何写这套逻辑呢?

这也是 PiliNara 在兼容混音播放时踩过的一个大坑。这块逻辑真正难的地方,不是“怎么去骗过系统”,而是你到底该在什么时候该装聋作哑,什么时候该乖乖闭嘴

如果无脑忽略所有系统中断,那在用户接到语音通话时,你的视频声音还在震天响,这就是严重的体验事故了。

目前在 PiliNara 里的处理方式如下:

1. 配置层:持证上岗#

首先是需要请求相关的权限

  1. 修改 AndroidManifest.xml (必须) 从 Android 14 开始,系统要求必须显式声明前台服务类型。对于视频/音频播放,必须声明 mediaPlayback。
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application ...>
<service
android:name=".YourPlaybackService"
android:foregroundServiceType="mediaPlayback"
android:export="false">
</service>
</application>

对于希望“与其他应用共存”的场景,我们在 Android 端坚决不再使用类似 androidAudioFocusGainType: null 这样企图隐身的危险写法(实际上这也是违背 Dart 强类型语法的),而是恢复它作为主权媒体应用的声明。

// 在 Android 上保持不变,老老实实申请最高级别焦点
await session.configure(const AudioSessionConfiguration.music());

这确保了我们的 App 在系统层面拥有“准入证”。

2. 逻辑层:精准拦截“劝退”信号#

真正的核心手术在 interruptionEventStream 监听器里。当产生冲突时:

  • 系统发来 AudioInterruptionType.pause(对应原生的 LOSS_TRANSIENT):比如打开了别的短视频软件。这时候如果用户开启了 mixWithOthers 选项,选择“厚脸皮”,直接 return 忽视他。
  • 系统发来 AudioInterruptionType.unknown(对应原生的 LOSS 高危信号):比如打进来了电话。这种涉及严重系统资源回收或高优先级通话时,必须强制暂停。

具体的代码实现实际上非常轻量:

session.interruptionEventStream.listen((event) {
if (event.begin) {
switch (event.type) {
case AudioInterruptionType.duck:
// 被压低音量,比如系统导航语音
PlPlayerController.setVolumeIfExists(... * 0.5);
break;
case AudioInterruptionType.pause:
// 如果系统发来临时切断(比如外部播放音乐),且我们开启了同时播放选项,直接拦截(装聋作哑)
if (Pref.mixWithOthers) return;
// 否则乖乖暂停
PlPlayerController.pauseIfExists(isInterrupt: true);
break;
case AudioInterruptionType.unknown:
// 来了电话这种硬断事件,无论如何都要老实暂停
PlPlayerController.pauseIfExists(isInterrupt: true);
break;
}
} else {
// 恢复逻辑...
}
});

总结#

到这里其实已经能得出一个明确的结论:

  • 完全避开焦点申请不仅是错的,而且非常危险,尤其在现代 Android 系统的 AudioHardening 环境下。
  • 真正想要实现像网易云一样的系统级完美并存,本质上就是脸皮厚:去申请最大、最长效的焦点保护自己的存活,然后在收到别人想抢你位置的 LOSS_TRANSIENT 通知时装作没听见。
Android 音频焦点与共存播放机制研究
https://blog.170529.xyz/posts/android_audio_focus/
作者
Starfallen
发布于
2026-04-03
许可协议
CC BY-NC-SA 4.0