Android 音视频焦点 处理

Android 官方文档解析:两个或两个以上的 Android 应用可同时向同一输出流播放音频。系统会将所有音频流混合在一起。虽然这是一项出色的技术,但却会给用户带来很大的困扰。为了避免所有音乐应用同时播放,Android 引入了“音频焦点”的概念。 一次只能有一个应用获得音频焦点。

  1. 当您的应用需要输出音频时,它需要请求获得音频焦点,获得焦点后,就可以播放声音了。不过,在您获得音频焦点后,您可能无法将其一直持有到播放完成。其他应用可以请求焦点,从而占有您持有的音频焦点。如果发生这种情况,您的应用应暂停播放或降低音量,以便于用户听到新的音频源。

  2. 音频焦点采用合作模式。我们建议应用遵守音频焦点准则,但系统不会强制执行这些准则。如果应用想要在失去音频焦点后继续大声播放,系统无法阻止它。这是一种不好的体验,用户很可能会卸载具有这种不良行为的应用。

  3. 行为恰当的音频应用应根据以下一般准则来管理音频焦点:

  • 在即将开始播放之前调用 requestAudioFocus(),并验证调用是否返回 [AUDIOFOCUS_REQUEST_GRANTED](https://developer.android.google.cn/reference/android/media/AudioManager#AUDIOFOCUS_REQUEST_GRANTED)。如果按照本指南中的说明设计应用,则应在媒体会话的 onPlay() 回调中调用 requestAudioFocus()
  • 在其他应用获得音频焦点时,停止或暂停播放,或降低音量。
  • 播放停止后,放弃音频焦点。

运行的 Android 版本不同,音频焦点的处理方式也会不同:

  • 从 Android 2.2(API 级别 8)开始,应用通过调用 [requestAudioFocus()](https://developer.android.google.cn/reference/android/media/AudioManager#requestAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener,%20int,%20int))[abandonAudioFocus()](https://developer.android.google.cn/reference/android/media/AudioManager#abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener)) 来管理音频焦点。应用还必须为这两个调用注册 [AudioManager.OnAudioFocusChangeListener](https://developer.android.google.cn/reference/android/media/AudioManager.OnAudioFocusChangeListener),以便接收回调并管理自己的音量。
  • 对于以 Android 5.0(API 级别 21)及更高版本为目标平台的应用,音频应用应使用 [AudioAttributes](https://developer.android.google.cn/reference/android/media/AudioAttributes) 来描述应用正在播放的音频类型。例如,播放语音的应用应指定 [CONTENT_TYPE_SPEECH](https://developer.android.google.cn/reference/android/media/AudioAttributes#CONTENT_TYPE_SPEECH)
  • 面向 Android 8.0(API 级别 26)或更高版本的应用应使用 [requestAudioFocus()](https://developer.android.google.cn/reference/android/media/AudioManager#requestAudioFocus(android.media.AudioFocusRequest)) 方法,该方法会接受 [AudioFocusRequest](https://developer.android.google.cn/reference/android/media/AudioFocusRequest) 参数。AudioFocusRequest 包含有关应用的音频上下文和功能的信息。系统使用这些信息来自动管理音频焦点的得到和失去。
    处理音频焦点一些规则
    以下是官方建议的处理音频焦点应该遵循的一些规则:
  1. 在开始播放之前,调用requestAudioFocus()方法,并检查返回值是否是AUDIOFOCUS_REQUEST_GRANTED,若成功获取,则开始播放。
    当App失去音频焦点时,根据失去的焦点类型,应该暂停播放,或者将音量调低。
    当播放结束时,释放音频焦点。
  2. 音频焦点如何处理
  • 处理音频焦点都是通过AudioManager这个类,如下是获得该类实例的方法:
    AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
    下面介绍音频焦点处理相关的一些方法(不同的Android版本处理音频焦点的方式略有差别,以下内容基于Android 4.4)
requestAudioFocus():申请音频焦点
abandonAudioFocus(): 释放音频焦点
AudioManager.OnAudioFocusChangeListener接口,提供了onAudioFocusChange()方法来监听音频焦点变化
  • 5.1 requestAudioFocus
    参数:
    AudioManager.OnAudioFocusChangeListener
    用于监听音频焦点变化,从而可以进行适当的操作,例如暂停播放等。
    streamType
    申请音频焦点处理的音频类型,例如,当播放音乐时,可以传入STREAM_MUSIC;当播放铃声时,可以传入STREAM_RING。表中列出了所有的可选值:durationHint
    可选值有以下五个:
    (1) AUDIOFOCUS_GAIN: 此参数表示希望申请一个永久的音频焦点,并且希望上一个持有音频焦点的App停止播放;例如在需要播放音乐时。
    (2) AUDIOFOCUS_GAIN_TRANSIENT:表示申请一个短暂的音频焦点,并且马上就会被释放,此时希望上一个持有音频焦点的App暂停播放。例如播放一个提醒声音。
    (3) AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:效果同AUDIOFOCUS_GAIN_TRANSIENT,只是希望上一个持有焦点的App减小其播放声音(但仍可以播放),此时会混音播放。例如导航播报。
    (4) AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE: 表示申请一个短暂的音频焦点,并且会希望系统不要播放任何突然的声音(例如通知,提醒等),例如用户在录音。
    返回值:AUDIOFOCUS_REQUEST_GRANTED或者AUDIOFOCUS_REQUEST_FAILED
  • 5.2 abandonAudioFocus(OnAudioFocusChangeListener l)
    5.2.1 播放完成后,请调用 abandonAudioFocus()
    audioManager.abandonAudioFocus(afChangeListener)

5.2.2 这会通知系统您不再需要焦点,并注销关联的 OnAudioFocusChangeListener。如果您请求的是暂时性焦点,则会通知已暂停或降低音量的应用它可以继续播放或恢复其音量。

  • 5.3 AudioManager.OnAudioFocusChangeListener
    当音频焦点发生变化时,可以在OnAudioFocusChangeListener的onAudioFocusChange(int focusChange)方法中监听到,下面详细说明该方法。
    onAudioFocusChange(int focusChange)
  • 参数:focusChange可以表明当前音频焦点发生的是何种变化,需要根据该参数状态做出正确的响应。
    分为获得和丢失两种情况:
    获得:AUDIOFOCUS_GAIN
  • 5.3.1 表示获得音频焦点,此时应该开始播放,丢失音频焦点,这时分为以下两种情况:
  • 5.3.2 短暂的丢失:
    如果focusChange的值是AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK或者 AUDIOFOCUS_LOSS_TRANSIENT时,你的App需要降低播放音量 或者暂停播放(但是需要记录当前的播放状态,以便后续恢复播放)。
    在短暂丢失焦点期间,你的App应该持续关注音频焦点的变化,当再次获得焦点时,恢复播放。
  • 5.3.3 永久的丢失
    若值为AUDIOFOCUS_LOSS,你的App应该立即停止播放,并且当再次获得音频焦点时,也不会恢复播放,只有当用户主动播放时,再开始播放
  1. 构建 AudioFocusRequest来请求和放弃音频焦点:
    audioManager = (AudioManager) Context.getSystemService(Context.AUDIO_SERVICE);
    playbackAttributes = new AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_GAME)
            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
            .build();
    focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
            .setAudioAttributes(playbackAttributes)
            .setAcceptsDelayedFocusGain(true)
            .setOnAudioFocusChangeListener(afChangeListener, handler)
            .build();
    mediaPlayer = new MediaPlayer();
    final Object focusLock = new Object();

    boolean playbackDelayed = false;
    boolean playbackNowAuthorized = false;

    // ...
    int res = audioManager.requestAudioFocus(focusRequest);
    synchronized(focusLock) {
        if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
            playbackNowAuthorized = false;
        } else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            playbackNowAuthorized = true;
            playbackNow();
        } else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
           playbackDelayed = true;
           playbackNowAuthorized = false;
        }
    }

    // ...
    @Override
    public void onAudioFocusChange(int focusChange) {
        switch (focusChange) {
            case AudioManager.AUDIOFOCUS_GAIN:
                if (playbackDelayed || resumeOnFocusGain) {
                    synchronized(focusLock) {
                        playbackDelayed = false;
                        resumeOnFocusGain = false;
                    }
                    playbackNow();
                }
                break;
            case AudioManager.AUDIOFOCUS_LOSS:
                synchronized(focusLock) {
                    resumeOnFocusGain = false;
                    playbackDelayed = false;
                }
                pausePlayback();
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                synchronized(focusLock) {
                    resumeOnFocusGain = true;
                    playbackDelayed = false;
                }
                pausePlayback();
                break;
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                // ... pausing or ducking depends on your app
                break;
            }
        }
    }

参考 Android 开发者-文档-指南-管理音频焦点

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容