【车载Android】多媒体开发入门(下)- 音频焦点

150 阅读5分钟

想象一下这个场景:你正开着车,点开媒体中心听着音乐;突然电话铃声大作,屏幕弹出来电提醒;紧接着导航又大声喊出“前方左转”——而你的音乐却依然我行我素地轰鸣着…… 这无疑是一场听觉灾难!

在 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+ 更严格

  • 若应用在后台且未持有音频焦点,尝试播放音频可能被系统直接拦截并抛出异常。

实践建议

  1. 准确设置 Usage:按实际场景选择,不要为了“声音更大”而误用 ALARM
  2. 动态切换属性:若应用同时支持短视频(MEDIA)和语音通话(VOICE_COMMUNICATION),请根据状态切换 AudioAttributes
  3. 配合设备特性:使用 VOICE_COMMUNICATION 可自动启用系统级回声消除(AEC),提升通话质量。

总结

音频焦点虽小,却是构建专业级车载多媒体应用不可或缺的一环,好的音视频应用,不仅会发声,更应懂得何时闭嘴🤐。