Como gerenciar a seleção de áudio

Dois ou mais apps para Android podem reproduzir áudio para o mesmo stream de saída simultaneamente. O sistema mistura tudo. Embora isso seja tecnicamente impressionante, pode ser muito irritante para o usuário. Para evitar que todos os apps de música sejam reproduzidos ao mesmo tempo, o Android apresenta a ideia de seleção de áudio. Somente um app pode manter a seleção de áudio por vez.

Quando seu app precisa da saída de áudio, é necessário solicitar a seleção de áudio. Quando tiver a seleção, ele poderá reproduzir o som. Porém, depois que você consegue a seleção de áudio, talvez não seja possível mantê-la até que reprodução termine. Outro app pode solicitar a seleção, apropriando-se dela. Se isso ocorrer, o app precisará pausar a reprodução ou reduzir o volume para que os usuários ouçam a nova fonte de áudio com mais facilidade.

A seleção de áudio é cooperativa. Os apps são incentivados a cumprir as diretrizes de seleção de áudio, mas o sistema não aplica as regras. Se um app quiser continuar tocando em volume alto, mesmo depois de perder a seleção de áudio, nada poderá impedir isso. Essa é uma experiência ruim, e há uma boa chance de que os usuários desinstalem um app que se comporte dessa maneira.

Um app de áudio que queira favorecer a experiência do usuário precisa gerenciar a seleção de áudio de acordo com estas diretrizes gerais:

  • Chamar requestAudioFocus() imediatamente antes de começar a tocar e verificar se a chamada retorna AUDIOFOCUS_REQUEST_GRANTED. Se você desenvolver o app conforme descrito neste guia, a chamada para requestAudioFocus() precisará ser feita no callback onPlay() da sessão de mídia.
  • Quando outro app receber a seleção de áudio, pare ou pause a reprodução ou diminua o volume.
  • Quando a reprodução parar, desative a seleção de áudio.

A seleção de áudio é gerenciada de forma diferente dependendo da versão do Android em execução:

  • A partir do Android 2.2 (API de nível 8), os apps gerenciam a seleção de áudio chamando requestAudioFocus() e abandonAudioFocus(). Os apps também precisam registrar um AudioManager.OnAudioFocusChangeListener com as duas chamadas para receber callbacks e gerenciar o próprio nível de áudio.
  • Para apps que segmentam o Android 5.0 (API de nível 21) ou versão posterior, os apps de áudio precisam usar AudioAttributes para descrever o tipo de áudio que o app está reproduzindo. Por exemplo, apps que reproduzem fala precisam especificar CONTENT_TYPE_SPEECH.
  • Apps com Android 8.0 (API de nível 26) ou versão posterior precisam usar o método requestAudioFocus(), que utiliza um parâmetro AudioFocusRequest. O AudioFocusRequest contém informações sobre o contexto de áudio e os recursos do app. O sistema usa essas informações para gerenciar automaticamente o ganho e a perda da seleção de áudio.

Seleção de áudio no Android 8.0 e versões posteriores

A partir do Android 8.0 (API de nível 26), quando você chama requestAudioFocus(), é necessário fornecer um parâmetro AudioFocusRequest. Para liberar a seleção de áudio, chame o método abandonAudioFocusRequest(), que também usa um AudioFocusRequest como argumento. A mesma instância AudioFocusRequest precisa ser usada ao solicitar e abandonar a seleção.

Para criar um AudioFocusRequest, use um AudioFocusRequest.Builder. Como uma solicitação de seleção sempre precisa especificar o tipo, ele é incluído no construtor para o builder. Use os métodos do builder para definir os outros campos da solicitação.

O campo FocusGain é obrigatório. Todos os outros são opcionais.

MétodoObservações
setFocusGain() Este campo é obrigatório em todas as solicitações. Ele usa os mesmos valores que o durationHint usado na chamada anterior ao Android 8.0 para requestAudioFocus(): AUDIOFOCUS_GAIN, AUDIOFOCUS_GAIN_TRANSIENT, AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK ou AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE.
setAudioAttributes() AudioAttributes descrevem o caso de uso do app. O sistema olha para eles quando um app ganha e perde a seleção de áudio. Os atributos substituem a noção de tipo de stream. No Android 8.0 (API de nível 26) ou versões posteriores, os tipos de stream para qualquer operação diferente de controles de volume estão desativados. Use os mesmos atributos na solicitação de seleção que você usa no player de áudio, conforme o exemplo mostrado depois desta tabela.

Use AudioAttributes.Builder para especificar os atributos primeiro e depois para atribuí-los à solicitação.

Se não for especificado, AudioAttributes será usado como padrão para AudioAttributes.USAGE_MEDIA.

setWillPauseWhenDucked() Quando outro app solicita a seleção com AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, o app selecionado não costuma receber um callback onAudioFocusChange() porque o sistema pode reduzir o volume do áudio por si só. Quando precisar pausar a reprodução em vez de reduzir o volume, chame setWillPauseWhenDucked(true), crie e defina um OnAudioFocusChangeListener, conforme descrito em redução automática de volume.
setAcceptsDelayedFocusGain() Uma solicitação de seleção de áudio pode falhar quando bloqueada por outro app. Esse método permite ganho atrasado de seleção: a capacidade de adquirir seleção de maneira assíncrona quando ele estiver disponível.

Observe que o ganho atrasado de seleção só funciona se você também especifica um AudioManager.OnAudioFocusChangeListener na solicitação de áudio, já que seu app precisa receber o callback para saber que a seleção foi concedida.

setOnAudioFocusChangeListener() Um OnAudioFocusChangeListener só será necessário se você também especificar willPauseWhenDucked(true) ou setAcceptsDelayedFocusGain(true) na solicitação.

Há dois métodos para definir o listener: um com e um sem um argumento de gerenciador. O gerenciador é a sequência em que o listener é executado. Se você não especificar um gerenciador, será usado aquele que está associado ao Looper principal.

O exemplo a seguir mostra como usar um AudioFocusRequest.Builder para criar um AudioFocusRequest e solicitar e abandonar a seleção de áudio:

Kotlin

    audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
        setAudioAttributes(AudioAttributes.Builder().run {
            setUsage(AudioAttributes.USAGE_GAME)
            setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
            build()
        })
        setAcceptsDelayedFocusGain(true)
        setOnAudioFocusChangeListener(afChangeListener, handler)
        build()
    }
    mediaPlayer = MediaPlayer()
    val focusLock = Any()

    var playbackDelayed = false
    var playbackNowAuthorized = false

    // ...
    val res = audioManager.requestAudioFocus(focusRequest)
    synchronized(focusLock) {
        playbackNowAuthorized = when (res) {
            AudioManager.AUDIOFOCUS_REQUEST_FAILED -> false
            AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
                playbackNow()
                true
            }
            AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
                playbackDelayed = true
                false
            }
            else -> false
        }
    }

    // ...
    override fun onAudioFocusChange(focusChange: Int) {
        when (focusChange) {
            AudioManager.AUDIOFOCUS_GAIN ->
                if (playbackDelayed || resumeOnFocusGain) {
                    synchronized(focusLock) {
                        playbackDelayed = false
                        resumeOnFocusGain = false
                    }
                    playbackNow()
                }
            AudioManager.AUDIOFOCUS_LOSS -> {
                synchronized(focusLock) {
                    resumeOnFocusGain = false
                    playbackDelayed = false
                }
                pausePlayback()
            }
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
                synchronized(focusLock) {
                    resumeOnFocusGain = true
                    playbackDelayed = false
                }
                pausePlayback()
            }
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
                // ... pausing or ducking depends on your app
            }
        }
    }
    

Java

    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;
            }
        }
    }
    

Redução automática de volume

No Android 8.0 (API de nível 26), quando outro app solicita a seleção de áudio com AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, o sistema pode reduzir e restaurar o volume sem invocar o callback de onAudioFocusChange() do app.

Embora a redução automática de volume seja um comportamento aceitável para apps de reprodução de música e vídeo, ela não é útil para reproduzir conteúdo falado, como em um app de audiolivro. Nesse caso, o app precisa pausar.

Se você quiser que seu app pause quando receber uma solicitação, em vez de reduzir o volume, crie um método de callback OnAudioFocusChangeListener com onAudioFocusChange() que implemente o comportamento de pausa/retomada desejado. Chame setOnAudioFocusChangeListener() para registrar o listener e chame setWillPauseWhenDucked(true) para dizer ao sistema para usar o callback em vez de executar a redução automática de volume.

Ganho atrasado de seleção

Às vezes, o sistema não pode conceder uma solicitação de seleção de áudio porque ela está "bloqueada" por outro app, por exemplo, durante uma chamada telefônica. Nesse caso, requestAudioFocus() retorna AUDIOFOCUS_REQUEST_FAILED. Quando isso acontece, o app não pode prosseguir com a reprodução de áudio porque não conseguiu a seleção.

O método setAcceptsDelayedFocusGain(true) permite que o app gerencie uma solicitação de seleção de áudio de forma assíncrona. Com essa sinalização definida, uma solicitação feita quando a seleção de áudio está bloqueada retorna AUDIOFOCUS_REQUEST_DELAYED. Quando a condição que bloqueou a seleção de áudio não existir mais, por exemplo, quando uma chamada telefônica termina, o sistema concede a solicitação de seleção pendente e chama onAudioFocusChange() para notificar o app.

Para gerenciar o ganho de seleção atrasado, crie um OnAudioFocusChangeListener com um método de callback onAudioFocusChange() que implemente o comportamento desejado e registre o listener chamando setOnAudioFocusChangeListener().

Seleção de áudio para versões anteriores ao Android 8.0

Quando chamar requestAudioFocus(), especifique uma dica de duração, que pode ser atendida por outro app que esteja mantendo a seleção e a reprodução no momento:

  • Quando você planeja tocar áudio no futuro previsível (por exemplo, ao tocar música) e espera que a seleção anterior de áudio pare a reprodução, solicite a seleção de áudio permanente (AUDIOFOCUS_GAIN).
  • Quando você espera tocar áudio por pouco tempo e que a seleção anterior pause a reprodução, solicite a seleção de áudio temporária (AUDIOFOCUS_GAIN_TRANSIENT).
  • Solicite a seleção de áudio temporária com redução do volume (AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) para indicar que você espera tocar áudio apenas por pouco tempo e quando o proprietário da seleção anterior pode manter a reprodução se reduzir o volume da saída de áudio. As duas saídas ficam misturadas no stream de áudio. A redução de volume é particularmente adequada para apps que usam o stream de áudio de maneira intermitente, como para rotas de carro audíveis.

O método requestAudioFocus() também requer um AudioManager.OnAudioFocusChangeListener. Esse listener precisa ser criado na mesma atividade ou serviço que possui sua sessão de mídia. Ele implementa o callback onAudioFocusChange() que o app recebe quando algum outro app adquire ou abandona a seleção de áudio.

O snippet a seguir solicita a seleção de áudio permanente no stream STREAM_MUSIC e registra um OnAudioFocusChangeListener para gerenciar as alterações subsequentes na seleção de áudio. O listener de mudança é discutido em Como responder a uma mudança de seleção de áudio.

Kotlin

    audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    lateinit var afChangeListener AudioManager.OnAudioFocusChangeListener

    ...
    // Request audio focus for playback
    val result: Int = audioManager.requestAudioFocus(
            afChangeListener,
            // Use the music stream.
            AudioManager.STREAM_MUSIC,
            // Request permanent focus.
            AudioManager.AUDIOFOCUS_GAIN
    )

    if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
        // Start playback
    }
    

Java

    AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    AudioManager.OnAudioFocusChangeListener afChangeListener;

    ...
    // Request audio focus for playback
    int result = audioManager.requestAudioFocus(afChangeListener,
                                 // Use the music stream.
                                 AudioManager.STREAM_MUSIC,
                                 // Request permanent focus.
                                 AudioManager.AUDIOFOCUS_GAIN);

    if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
        // Start playback
    }
    

Quando terminar a reprodução, chame abandonAudioFocus().

Kotlin

    audioManager.abandonAudioFocus(afChangeListener)
    

Java

    // Abandon audio focus when playback complete
    audioManager.abandonAudioFocus(afChangeListener);
    

Isso notifica o sistema de que você não precisa mais da seleção e cancela o registro do OnAudioFocusChangeListener associado. Se você solicitou uma seleção temporária, notificará um app que pausou ou reduziu o volume que ele pode continuar a tocar ou restaurar o volume.

Como responder a uma mudança de seleção de áudio

Quando um app adquire a seleção de áudio, ele precisa ser capaz de liberá-la quando outro app a solicita. Quando isso acontece, o app recebe uma chamada para o método onAudioFocusChange() no AudioFocusChangeListener que você especificou quando o app chamou requestAudioFocus().

O parâmetro focusChange transmitido para onAudioFocusChange() indica o tipo de mudança que está acontecendo. Ele corresponde à dica de duração usada pelo app que está adquirindo a seleção. Seu app precisa responder corretamente.

Perda transitória de seleção
Se a mudança de seleção for temporária (AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK ou AUDIOFOCUS_LOSS_TRANSIENT), o app precisa reduzir o volume (se você não depender da redução automática de volume) ou pausar a reprodução, mas manter o mesmo estado.

Durante uma perda temporária de seleção de áudio, você precisa continuar a monitorar as mudanças de seleção de áudio e estar preparado para retomar a reprodução normal quando recuperá-la. Quando o app que está bloqueando a seleção a abandonar, você receberá um callback (AUDIOFOCUS_GAIN). Nesse ponto, você poderá restaurar o volume para o nível normal ou reiniciar a reprodução.

Perda permanente de seleção
Se a perda da seleção de áudio é permanente (AUDIOFOCUS_LOSS), outro app está reproduzindo áudio. Seu app precisa pausar a reprodução imediatamente, porque não receberá um callback AUDIOFOCUS_GAIN. Para reiniciar a reprodução, o usuário precisa executar uma ação explícita, por exemplo, pressionar o controle de transporte de reprodução em uma notificação ou na IU do app.

O snippet de código a seguir demonstra como implementar o OnAudioFocusChangeListener e seu callback onAudioFocusChange(). Observe o uso de um Handler para atrasar o callback de parada em uma perda permanente de seleção de áudio.

Kotlin

    private val handler = Handler()
    private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
        when (focusChange) {
            AudioManager.AUDIOFOCUS_LOSS -> {
                // Permanent loss of audio focus
                // Pause playback immediately
                mediaController.transportControls.pause()
                // Wait 30 seconds before stopping playback
                handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30))
            }
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
                // Pause playback
            }
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
                // Lower the volume, keep playing
            }
            AudioManager.AUDIOFOCUS_GAIN -> {
                // Your app has been granted audio focus again
                // Raise volume to normal, restart playback if necessary
            }
        }
    }
    

Java

    private Handler handler = new Handler();
    AudioManager.OnAudioFocusChangeListener afChangeListener =
      new AudioManager.OnAudioFocusChangeListener() {
        public void onAudioFocusChange(int focusChange) {
          if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
            // Permanent loss of audio focus
            // Pause playback immediately
            mediaController.getTransportControls().pause();
            // Wait 30 seconds before stopping playback
            handler.postDelayed(delayedStopRunnable,
              TimeUnit.SECONDS.toMillis(30));
          }
          else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
            // Pause playback
          } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
            // Lower the volume, keep playing
          } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
            // Your app has been granted audio focus again
            // Raise volume to normal, restart playback if necessary
          }
        }
      };
    

O gerenciador usa um Runnable que tem esta aparência:

Kotlin

    private var delayedStopRunnable = Runnable {
        mediaController.transportControls.stop()
    }
    

Java

    private Runnable delayedStopRunnable = new Runnable() {
        @Override
        public void run() {
            getMediaController().getTransportControls().stop();
        }
    };
    

Para garantir que a parada atrasada não seja ativada se o usuário reiniciar a reprodução, chame mHandler.removeCallbacks(mDelayedStopRunnable) em resposta a qualquer mudança de estado. Por exemplo, chame removeCallbacks() no onPlay(), onSkipToNext() etc. do callback. Você também precisa chamar esse método no callback do serviço onDestroy() ao limpar os recursos usados pelo serviço.