Cómo administrar el foco de audio

Dos o más apps para Android pueden reproducir audio en la misma transmisión de salida de forma simultánea, y el sistema se encarga de mezclar todo. Si bien esta función es técnicamente impresionante, puede resultar muy molesta para el usuario. Para evitar que todas las apps de música se reproduzcan al mismo tiempo, Android presenta el concepto de foco de audio. Solo una app por vez puede mantener el foco de audio.

Cuando tu app necesita transmitir audio, debe solicitar el foco de audio. Cuando lo recibe, puede reproducir sonido. Sin embargo, después de adquirir el foco de audio, es posible que no puedas mantenerlo hasta que termines de reproducir. Si otra app solicita el foco de audio, tu app dejará de tenerlo. Si eso ocurre, la app debería pausar la reproducción o bajar el volumen para que los usuarios escuchen la nueva fuente de audio con más facilidad.

Antes de Android 12 (nivel de API 31), el sistema no administraba el foco de audio. Por lo tanto, si bien se recomienda a los desarrolladores de apps que cumplan con los lineamientos de enfoque de audio, si una app sigue reproduciendo contenido a un volumen alto, incluso después de perder el enfoque de audio en un dispositivo que ejecuta Android 11 (nivel de API 30) o versiones anteriores, el sistema no puede evitarlo. Sin embargo, este comportamiento de la app genera una mala experiencia del usuario y, a menudo, puede llevar a que los usuarios desinstalen la app que se comporta de manera incorrecta.

Una app de audio bien diseñada debe administrar el foco de audio de acuerdo con estos lineamientos generales:

  • Llama a requestAudioFocus() justo antes de comenzar a reproducir contenido y verifica que la llamada muestre AUDIOFOCUS_REQUEST_GRANTED. Realiza la llamada a requestAudioFocus() en la devolución de llamada onPlay() de tu sesión multimedia.

  • Cuando otra app obtiene el foco de audio, la tuya debe detener o pausar la reproducción, o bajar el volumen.

  • Cuando se detenga la reproducción (por ejemplo, cuando la app no tenga nada más para reproducir), abandona el foco de audio. Tu app no tiene que abandonar el foco de audio si el usuario pausa la reproducción, pero podría reanudarla más adelante.

  • Usa AudioAttributes para describir el tipo de audio que reproduce tu app. Por ejemplo, para las apps que reproducen contenido de voz, especifica CONTENT_TYPE_SPEECH.

El foco de audio se maneja de manera diferente según la versión de Android que se ejecute:

Android 12 (nivel de API 31) o versiones posteriores
El sistema administra el foco de audio. El sistema aplica de manera forzosa un fundido de salida en la reproducción de audio de una app cuando otra app solicita el foco de audio. El sistema también silencia la reproducción de audio cuando se recibe una llamada entrante.
Android 8.0 (nivel de API 26) a Android 11 (nivel de API 30)
El sistema no administra el foco de audio, pero incluye algunos cambios que se introdujeron a partir de Android 8.0 (nivel de API 26).
Android 7.1 (nivel de API 25) y versiones anteriores
El sistema no administra el foco de audio, y las apps lo administran con requestAudioFocus() y abandonAudioFocus().

Enfoque de audio en Android 12 y versiones posteriores

Una app multimedia o de videojuego que usa foco de audio no debe reproducir audio después de que pierde el foco. En Android 12 (nivel de API 31) y versiones posteriores, el sistema aplica este comportamiento. Cuando una app solicita el foco de audio mientras otra en reproducción lo tiene, el sistema aplica de manera forzosa un fundido de salida en la app en reproducción. Agregar este fundido de salida ofrece una transición más fluida cuando se pasa de una app a otra.

Este comportamiento se produce cuando se cumplen las siguientes condiciones:

  1. La primera app que se está reproduciendo cumple con todos estos criterios:

  2. Una segunda app solicita el foco de audio con AudioManager.AUDIOFOCUS_GAIN.

Cuando se cumplen estas condiciones, el sistema de audio aplica el fundido de salida en la primera app. Cuando finaliza este fundido, el sistema notifica a la primera app sobre la pérdida de foco. Los reproductores de la app permanecerán silenciados hasta que esta vuelva a solicitar el foco de audio.

Comportamientos existentes del foco de audio

También debes tener en cuenta estos otros casos que implican un cambio en el foco de audio.

Disminución automática del volumen

En Android 8.0 (nivel de API 26), se introdujo la disminución automática del volumen (que reduce de manera temporal el nivel de audio de una app para que se pueda escuchar otra con claridad).

Si el sistema implementa la disminución automática, no tienes que implementarla en tu app.

La disminución automática del volumen también se produce cuando una notificación de audio toma el foco de una app en reproducción. El inicio de la reproducción de notificaciones se sincroniza con el final de la rampa de disminución del volumen.

La disminución automática del volumen se produce cuando se cumplen las siguientes condiciones:

  1. La primera app que se está reproduciendo cumple con todos estos criterios:

  2. Una segunda app solicita el foco de audio con AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK.

Cuando se cumplen estas condiciones, el sistema de audio disminuye el volumen de todos los reproductores activos de la primera app mientras la segunda tiene el foco. Cuando la segunda app abandona el foco, deja de disminuir el volumen de estos. La primera app no recibe notificaciones cuando pierde el foco, por lo que no tiene que realizar ninguna acción.

Ten en cuenta que la disminución automática del volumen no se realiza cuando el usuario escucha contenido de voz, ya que es posible que se pierda parte del programa. Por ejemplo, las instrucciones de voz para las instrucciones sobre cómo llegar no se silencian.

Cómo silenciar la reproducción de audio actual para las llamadas telefónicas entrantes

Algunas apps no se comportan de manera correcta y continúan reproduciendo audio durante las llamadas telefónicas. Esta situación obliga al usuario a encontrar y silenciar la app ofensiva o salir de esta para escuchar su llamada. Para evitar esto, el sistema puede silenciar el audio de otras apps mientras hay una llamada entrante. El sistema invoca esta función cuando se recibe una llamada telefónica entrante y una app cumple con las siguientes condiciones:

  • La app tiene el atributo de uso AudioAttributes.USAGE_MEDIA o AudioAttributes.USAGE_GAME.
  • La app solicitó correctamente el foco de audio (mediante cualquier tipo de obtención) y está reproduciendo audio.

Si una app continúa reproduciéndose durante la llamada, se silencia la reproducción hasta que la llamada finaliza. Sin embargo, si una app comienza a reproducirse durante la llamada, no se silencia este reproductor, ya que se considera que el usuario inició la reproducción de forma intencional.

Foco de audio en Android 8.0 a Android 11

A partir de Android 8.0 (nivel de API 26), cuando llames a requestAudioFocus(), debes proporcionar un parámetro AudioFocusRequest. AudioFocusRequest contiene información sobre el contexto de audio y las funciones de tu app. El sistema usa esta información para administrar automáticamente la obtención y la pérdida del foco de audio. Para liberar el foco de audio, llama al método abandonAudioFocusRequest(), que también toma un AudioFocusRequest como argumento. Usa la misma instancia de AudioFocusRequest cuando solicites y abandones el enfoque.

Para crear un AudioFocusRequest, usa un AudioFocusRequest.Builder. Dado que una solicitud de foco siempre debe especificar de qué tipo es, este se incluye en el constructor para el compilador. Usa los métodos del compilador para establecer los otros campos de la solicitud.

El campo FocusGain es obligatorio; todos los demás son opcionales.

MétodoNotas
setFocusGain() Este campo es obligatorio en todas las solicitudes. Toma los mismos valores que el objeto durationHint utilizado en la llamada de versiones previas a Android 8.0 a requestAudioFocus(): AUDIOFOCUS_GAIN, AUDIOFOCUS_GAIN_TRANSIENT, AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK o AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE.
setAudioAttributes() AudioAttributes describe el caso de uso de tu app. El sistema observa esos atributos cuando una app obtiene y pierde el foco de audio. Los atributos reemplazan la noción de tipo de transmisión. En Android 8.0 (nivel de API 26) y versiones posteriores, dejaron de estar disponibles los tipos de transmisión para cualquier operación que no sea la de los controles de volumen. Usa los mismos atributos en la solicitud del foco que usas en tu reproductor de audio (como se muestra en el ejemplo que sigue a esta tabla).

Primero usa un AudioAttributes.Builder para especificar los atributos y, luego, usa este método para asignar los atributos a la solicitud.

Si no se especifica, el valor predeterminado de AudioAttributes es AudioAttributes.USAGE_MEDIA.

setWillPauseWhenDucked() Cuando otra app solicita el foco con AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, la app que lo tiene en ese momento no suele recibir una devolución de llamada onAudioFocusChange() porque el sistema puede disminuir el volumen por sí mismo. Cuando necesites pausar la reproducción en lugar de bajar el volumen, llama a setWillPauseWhenDucked(true) y crea y configura un OnAudioFocusChangeListener, como se describe en Disminución automática del volumen.
setAcceptsDelayedFocusGain() Una solicitud de foco de audio puede fallar cuando otra app bloquea el foco. Este método permite una obtención demorada del foco: la capacidad de adquirir el foco de manera asíncrona cuando esté disponible.

Ten en cuenta que la obtención demorada del foco solo funciona si también especificas AudioManager.OnAudioFocusChangeListener en la solicitud de audio, ya que tu app necesita recibir la devolución de llamada para saber si se otorgó el foco.

setOnAudioFocusChangeListener() Un OnAudioFocusChangeListener solo es necesario si también especificas willPauseWhenDucked(true) o setAcceptsDelayedFocusGain(true) en la solicitud.

Hay dos métodos para configurar el objeto de escucha: uno con un argumento de controlador y otro sin él. El controlador es el subproceso en el que se ejecuta el objeto de escucha. Si no especificas un controlador, se usa el que está asociado con el Looper principal.

En el siguiente ejemplo, se muestra cómo usar un AudioFocusRequest.Builder para crear una AudioFocusRequest y solicitar y abandonar el foco de audio:

Kotlin

// initializing variables for audio focus and playback management
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()
}
val focusLock = Any()

var playbackDelayed = false
var playbackNowAuthorized = false

// requesting audio focus and processing the response
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
    }
}

// implementing OnAudioFocusChangeListener to react to focus changes
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) {
                // only resume if playback is being interrupted
                resumeOnFocusGain = isPlaying()
                playbackDelayed = false
            }
            pausePlayback()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // ... pausing or ducking depends on your app
        }
    }
}

Java

// initializing variables for audio focus and playback management
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();
final Object focusLock = new Object();

boolean playbackDelayed = false;
boolean playbackNowAuthorized = false;

// requesting audio focus and processing the response
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;
    }
}

// implementing OnAudioFocusChangeListener to react to focus changes
@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) {
                // only resume if playback is being interrupted
                resumeOnFocusGain = isPlaying();
                playbackDelayed = false;
            }
            pausePlayback();
            break;
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            // ... pausing or ducking depends on your app
            break;
        }
    }
}

Disminución automática del volumen

En Android 8.0 (nivel de API 26), cuando otra app solicita el foco con AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, el sistema puede disminuir y restaurar el volumen sin invocar la devolución de llamada onAudioFocusChange() de la app.

Si bien la disminución automática del volumen es un comportamiento aceptable para las apps de reproducción de música y video, no es útil cuando se reproduce contenido de voz, como en una app de audiolibros. En este caso, la aplicación debería pausar la reproducción.

Si deseas que tu app pause la reproducción cuando se le pida que disminuya volumen, crea un OnAudioFocusChangeListener con un método de devolución de llamada onAudioFocusChange() que implemente el comportamiento de pausa/reanudación deseado. Llama a setOnAudioFocusChangeListener() para registrar el objeto de escucha y llama a setWillPauseWhenDucked(true) para indicarle al sistema que use tu devolución de llamada en lugar de aplicar la disminución automática del volumen.

Obtención demorada del foco

En ocasiones, el sistema no puede aceptar una solicitud de foco de audio porque otra app lo está "bloqueando"; por ejemplo, cuando hay una llamada telefónica en curso. En este caso, requestAudioFocus() muestra AUDIOFOCUS_REQUEST_FAILED. Cuando esto sucede, tu app no debe continuar con la reproducción de audio porque no obtuvo el foco.

El método setAcceptsDelayedFocusGain(true) le permite a tu app manejar una solicitud de foco de manera asíncrona. Con esta marca establecida, una solicitud que se realiza cuando el foco está bloqueado muestra AUDIOFOCUS_REQUEST_DELAYED. Cuando la condición que bloqueó el foco de audio deja de existir, como cuando termina una llamada telefónica, el sistema acepta la solicitud de foco pendiente y llama a onAudioFocusChange() para informárselo a tu app.

Para controlar la obtención demorada del foco, debes crear un OnAudioFocusChangeListener con un método de devolución de llamada onAudioFocusChange() que implemente el comportamiento deseado y llamar a setOnAudioFocusChangeListener() para registrar el objeto de escucha.

Foco de audio en Android 7.1 y versiones anteriores

Cuando llames a requestAudioFocus(), debes especificar una sugerencia de duración, que puede respetar otra app que tenga el foco en ese momento y esté reproduciendo contenido:

  • Solicita el foco de audio permanente (AUDIOFOCUS_GAIN) cuando planees reproducir audio en el futuro próximo (por ejemplo, para reproducir música) y esperes que el elemento que tiene el foco de audio en ese momento deje de reproducir su contenido.
  • Solicita el foco transitorio (AUDIOFOCUS_GAIN_TRANSIENT) cuando planees reproducir audio durante un período breve y esperes que el elemento anterior que tenía el foco pause la reproducción.
  • Solicita el foco transitorio con disminución de volumen (AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) para indicar que esperas reproducir audio solo durante un período breve y que está bien que el elemento anterior que tenía el foco siga reproduciendo si disminuye el volumen de su salida de audio. Ambas salidas se mezclan en la transmisión de audio. La disminución del volumen es adecuada en especial para las apps que usan la transmisión de audio de forma intermitente, como las que brindan instrucciones de manejo audibles.

El método requestAudioFocus() también requiere un AudioManager.OnAudioFocusChangeListener. Este objeto de escucha debe crearse en la misma actividad o servicio que posee tu sesión multimedia. Implementa la devolución de llamada onAudioFocusChange() que recibe tu app cuando alguna otra app adquiere o abandona el foco de audio.

El siguiente fragmento solicita el foco de audio permanente en la transmisión STREAM_MUSIC y registra un OnAudioFocusChangeListener para manejar los cambios posteriores en el foco. (El objeto de escucha de cambios se analiza en Cómo responder a un cambio de foco de audio).

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
}

Cuando termines la reproducción, llama a abandonAudioFocus().

Kotlin

audioManager.abandonAudioFocus(afChangeListener)

Java

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

De este modo, se informa al sistema que ya no necesitas el foco y se cancela el registro del OnAudioFocusChangeListener asociado. Si solicitaste un foco transitorio, se informará a la app que haya pausado la reproducción o disminuido su volumen que puede reanudarla o restaurar el volumen.

Cómo responder a un cambio de foco de audio

Cuando una app adquiere el foco de audio, debe poder liberarlo cuando otra app lo solicite para sí misma. Cuando esto sucede, tu app recibe una llamada al método onAudioFocusChange() en el AudioFocusChangeListener que especificaste cuando la app llamó a requestAudioFocus().

El parámetro focusChange pasado a onAudioFocusChange() indica el tipo de cambio que se produce. Corresponde a la sugerencia de duración que usa la app que está adquiriendo el foco. Tu app debería responder de manera adecuada.

Pérdida transitoria del foco
Si el cambio de foco es transitorio (AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK o AUDIOFOCUS_LOSS_TRANSIENT), tu app debe disminuir el volumen (si no usa la disminución automática de volumen) o pausar la reproducción, y no alterar su estado más allá de eso.

Durante una pérdida transitoria del foco de audio, debes seguir supervisando los cambios del foco y estar preparado para reanudar la reproducción normal al recuperar el foco. Cuando la app que bloqueaba el foco lo abandona, recibes una devolución de llamada (AUDIOFOCUS_GAIN). En este punto, puedes restaurar el volumen al nivel normal o reiniciar la reproducción.

Pérdida permanente del foco
Si la pérdida del foco de audio es permanente (AUDIOFOCUS_LOSS), otra app está reproduciendo audio. Tu app debe pausar la reproducción de inmediato, ya que no recibirá una devolución de llamada AUDIOFOCUS_GAIN. Para reiniciar la reproducción, el usuario debe realizar una acción explícita, como presionar el control de transporte de reproducción en una notificación o en la IU de la app.

En el siguiente fragmento de código, se muestra cómo implementar el OnAudioFocusChangeListener y su devolución de llamada onAudioFocusChange(). Observa el uso de un Handler para demorar la devolución de llamada de detención en una pérdida permanente del foco de audio.

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

El controlador usa un Runnable que tiene el siguiente aspecto:

Kotlin

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

Java

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

Para garantizar que la detención demorada no se active si el usuario reinicia la reproducción, llama a mHandler.removeCallbacks(mDelayedStopRunnable) en respuesta a cualquier cambio de estado. Por ejemplo, llama a removeCallbacks() en onPlay(), onSkipToNext(), etc., de tu devolución de llamada. También debes llamar a este método en la devolución de llamada onDestroy() de tu servicio cuando limpies los recursos que utiliza.