想象一下这个场景:你正开着车,点开媒体中心听着音乐;突然电话铃声大作,屏幕弹出来电提醒;紧接着导航又大声喊出“前方左转”——而你的音乐却依然我行我素地轰鸣着…… 这无疑是一场听觉灾难!
在 Android 这个多任务操作系统中,为了避免多个应用同时“吵架”,系统引入了一套协作机制:音频焦点(Audio Focus) 。作为开发者,能否优雅地申请、响应和释放音频焦点,是衡量一个多媒体应用是否成熟的关键指标。
【车载Android】多媒体开发入门(上)- MediaSession
【车载Android】多媒体开发入门(下)- 音频焦点
一、什么是音频焦点?
音频焦点并非强制性的“独占锁”,而更像一套应用间的“君子协定” :
-
核心原则:同一时间,通常只有一个应用持有“长时焦点”并主导音频输出。
-
运作方式:
- 当你的应用需要播放声音时,应主动向系统申请焦点;
- 当系统因更高优先级事件(如来电、闹钟、导航)需要焦点时,会通知你;
- 你需根据通知类型,暂停播放或降低音量(Duck) ,以配合全局音频体验。
二、传统方式:手动管理音频焦点(Media3 之前)
如果你的应用尚未采用 Jetpack Media3 框架,则需手动处理焦点申请与监听。
1. 明确播放意图:设置 AudioAttributes
在申请焦点前,必须先声明音频的用途(Usage) 和内容类型(Content Type) :
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA) // 用途:媒体播放
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) // 类型:音乐
.build()
Usage 决定系统如何路由声音(扬声器/蓝牙/听筒)和音量键控制哪个流(媒体音量 vs 通话音量)。 Content Type 影响底层音效处理(如人声增强、环绕声等)。
2. 申请与释放焦点
| 焦点类型 | 对应参数 | 使用场景 |
|---|---|---|
| 长焦点 (Gain) | AUDIOFOCUS_GAIN | 音乐播放器、播客、长视频。 |
| 瞬时焦点 (Transient) | AUDIOFOCUS_GAIN_TRANSIENT | 导航播报、短信提示音。 |
| 瞬时损耗降低音量 (Duck) | AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK | 简单的通知音,希望背景音乐压低音量但不停止。 |
| 独占焦点 | AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE | 录音机、语音助手,不希望被任何声音干扰。 |
Android 8.0+(推荐方式):
private var audioFocusRequest: AudioFocusRequest? = null
fun requestAudioFocus(): Boolean {
val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(audioAttributes)
.setWillPauseWhenDucked(true) // 表示愿意在 duck 时暂停
.setOnAudioFocusChangeListener(audioFocusChangeListener)
.build()
audioFocusRequest = request
val result = audioManager.requestAudioFocus(request)
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
}
fun abandonAudioFocus() {
audioFocusRequest?.let { audioManager.abandonAudioFocusRequest(it) }
}
兼容旧版本(API < 26):
使用已弃用的 requestAudioFocus(listener, streamType, durationHint) 方法,现在的车载应用已经用不上了。
3. 监听焦点变化并响应
无论新旧 API,都必须实现 OnAudioFocusChangeListener,并根据不同的 focusChange 采取相应措施:
| 焦点变化类型 | 常量 | 触发场景示例 | 建议行为 | 是否可恢复 |
|---|---|---|---|---|
| 永久失去焦点 | AUDIOFOCUS_LOSS | 用户打开其他音乐 App 并开始播放 | 立即停止播放,释放 MediaPlayer 等资源 | 通常不会自动恢复;需用户主动切回你的 App |
| 暂时失去焦点 | AUDIOFOCUS_LOSS_TRANSIENT | 来电、闹钟响起、语音助手唤醒(如“Hey 小度”) | 暂停播放,保持资源(不释放 MediaPlayer) | 预期很快恢复(如通话结束),收到 AUDIOFOCUS_GAIN 后恢复播放 |
| 可降低音量继续播放 | AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK | 导航语音提示、系统提示音 | 不暂停,但将音量临时降低(如降至 20%) | 焦点很快归还,收到 AUDIOFOCUS_GAIN 后恢复原音量 |
| 重新获得焦点 | AUDIOFOCUS_GAIN | 干扰结束 | 继续播放并恢复音量 | N/A |
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_LOSS -> pauseOrStopPlayback()
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> pausePlayback()
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> lowerVolume()
AudioManager.AUDIOFOCUS_GAIN -> resumePlayback()
}
}
三、现代方案:Media3 自动管理音频焦点
Jetpack Media3(ExoPlayer 的新一代框架)极大简化了焦点管理,几乎无需写任何音频焦点代码!
1. 开启自动焦点管理
只需在初始化 ExoPlayer 时传入 AudioAttributes 并启用自动管理:
val audioAttributes = AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.CONTENT_TYPE_MUSIC)
.build()
val player = ExoPlayer.Builder(context)
.setAudioAttributes(audioAttributes, /* handleAudioFocus= */ true) // 关键!
.build()
开启后,ExoPlayer 会自动:
- 播放时申请焦点;
- 来电时自动暂停;
- 电话挂断后自动恢复;
- 导航提示时自动 Duck(降音量);
- 申请失败时推迟播放。
2. 如需监听焦点导致的播放状态变化
虽然大部分逻辑被封装,但仍可通过 Player.Listener 感知因焦点丢失导致的暂停:
player.addListener(object : Player.Listener {
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
if (reason == Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS) {
// 系统因焦点丢失暂停了播放
// 可用于更新 UI 或记录日志
}
}
})
四、音频属性(AudioAttributes)
AudioAttributes 是音频焦点的基础,它告诉系统“你播放的是什么声音”。
1. Usage(用途)— 决定声音去哪、怎么控
| 常用值 | 适用场景 | 路由行为 |
|---|---|---|
| USAGE_MEDIA | 音乐、视频 | 蓝牙媒体通道、媒体音量键 |
| USAGE_VOICE_COMMUNICATION | 语音通话、连麦 | 听筒/耳机、触发 AEC 回声消除 |
| USAGE_NOTIFICATION | 通知音 | 可被 Duck,短暂播放 |
| USAGE_ALARM | 闹钟 | 即使静音也播放(按需使用!) |
切勿为了声音大就滥用
USAGE_ALARM!例如将音乐播放器设为此类型,会导致插耳机或连接蓝牙耳机时声音仍从扬声器外放,严重破坏体验。
2. Content Type(内容类型)— 决定如何处理声音
-
CONTENT_TYPE_MUSIC:高保真,适合音乐; -
CONTENT_TYPE_MOVIE:强调动态范围与环绕感; -
CONTENT_TYPE_SPEECH:增强人声清晰度,适合播客、有声书。
3. 高级选项:防止音频被捕获
对于版权敏感内容(如VIP音乐),可禁止录屏或辅助功能抓取音频:
.setAllowedCapturePolicy(C.ALLOW_CAPTURE_BY_NONE)
五、常见误区与实践建议
误区 1:“静音键 = 失去音频焦点”
-
事实:物理静音键仅控制系统音量流,不影响音频焦点逻辑。即使静音,你的应用仍可能持有焦点。
注意 2:Android 12+ 更严格
-
若应用在后台且未持有音频焦点,尝试播放音频可能被系统直接拦截并抛出异常。
实践建议
- 准确设置 Usage:按实际场景选择,不要为了“声音更大”而误用
ALARM。 - 动态切换属性:若应用同时支持短视频(
MEDIA)和语音通话(VOICE_COMMUNICATION),请根据状态切换AudioAttributes。 - 配合设备特性:使用
VOICE_COMMUNICATION可自动启用系统级回声消除(AEC),提升通话质量。
总结
音频焦点虽小,却是构建专业级车载多媒体应用不可或缺的一环,好的音视频应用,不仅会发声,更应懂得何时闭嘴🤐。