Deep Dive: mejores prácticas de MediaPlayer

Foto de Marcela Laskoski en Unsplash

MediaPlayer parece ser engañosamente simple de usar, pero la complejidad vive justo debajo de la superficie. Por ejemplo, puede ser tentador escribir algo como esto:

MediaPlayer.create (contexto, R.raw.cowbell) .start ()

Esto funciona bien la primera y probablemente la segunda, tercera o incluso más veces. Sin embargo, cada nuevo MediaPlayer consume recursos del sistema, como memoria y códecs. Esto puede degradar el rendimiento de su aplicación y posiblemente de todo el dispositivo.

Afortunadamente, es posible usar MediaPlayer de una manera simple y segura siguiendo algunas reglas simples.

El caso simple

El caso más básico es que tenemos un archivo de sonido, quizás un recurso en bruto, que solo queremos reproducir. En este caso, crearemos un solo jugador para reutilizarlo cada vez que necesitemos reproducir un sonido. El jugador debe ser creado con algo como esto:

private val mediaPlayer = MediaPlayer (). apply {
    setOnPreparedListener {start ()}
    setOnCompletionListener {reset ()}
}

El reproductor se crea con dos oyentes:

  • OnPreparedListener, que iniciará automáticamente la reproducción una vez que el reproductor haya sido preparado.
  • OnCompletionListener que limpia automáticamente los recursos cuando finaliza la reproducción.

Con el reproductor creado, el siguiente paso es crear una función que tome un ID de recurso y use ese MediaPlayer para reproducirlo:

anular la diversión playSound (@RawRes rawResId: Int) {
    val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: return
    mediaPlayer.run {
        Reiniciar()
        setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.declaredLength)
        prepareAsync ()
    }
}

Están sucediendo muchas cosas en este breve método:

  • El ID de recurso debe convertirse a un AssetFileDescriptor porque esto es lo que MediaPlayer usa para reproducir recursos sin procesar. La verificación nula asegura que el recurso existe.
  • Llamar a reset () asegura que el reproductor esté en el estado Inicializado. Esto funciona sin importar en qué estado se encuentre el jugador.
  • Establecer la fuente de datos para el reproductor.
  • prepareAsync prepara al jugador para jugar y regresa inmediatamente, manteniendo la interfaz de usuario receptiva. Esto funciona porque OnPreparedListener adjunto comienza a reproducirse una vez que se ha preparado la fuente.

Es importante tener en cuenta que no llamamos release () en nuestro reproductor ni lo configuramos como nulo. ¡Queremos reutilizarlo! Entonces, en su lugar, llamamos a reset (), que libera la memoria y los códecs que estaba usando.

Reproducir un sonido es tan simple como llamar:

playSound (R.raw.cowbell)

¡Simple!

Más cencerros

Reproducir un sonido a la vez es fácil, pero ¿qué sucede si desea iniciar otro sonido mientras todavía se reproduce el primero? Llamar a playSound () varias veces como esta no funcionará:

playSound (R.raw.big_cowbell)
playSound (R.raw.small_cowbell)

En este caso, R.raw.big_cowbell comienza a prepararse, pero la segunda llamada reinicia al jugador antes de que algo pueda suceder, por lo que solo escuchará a R.raw.small_cowbell.

¿Y si quisiéramos reproducir varios sonidos juntos al mismo tiempo? Necesitaríamos crear un MediaPlayer para cada uno. La forma más sencilla de hacer esto es tener una lista de jugadores activos. Quizás algo como esto:

clase MediaPlayers (contexto: contexto) {
    contexto val privado: Context = context.applicationContext
    private val playersInUse = mutableListOf  ()

    private fun buildPlayer () = MediaPlayer (). apply {
        setOnPreparedListener {start ()}
        setOnCompletionListener {
            it.release ()
            playersInUse - = it
        }
    }

    anular la diversión playSound (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: return
        val mediaPlayer = buildPlayer ()

        mediaPlayer.run {
            playersInUse + = it
            setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset,
                    assetFileDescriptor.declaredLength)
            prepareAsync ()
        }
    }
}

¡Ahora que cada sonido tiene su propio reproductor, es posible jugar R.raw.big_cowbell y R.raw.small_cowbell juntos! ¡Perfecto!

... Bueno, casi perfecto. No hay nada en nuestro código que limite la cantidad de sonidos que pueden reproducirse a la vez, y MediaPlayer aún necesita tener memoria y códecs para trabajar. Cuando se agotan, MediaPlayer falla en silencio, solo observando "E / MediaPlayer: Error (1, -19)" en logcat.

Ingrese a MediaPlayerPool

Queremos admitir la reproducción de múltiples sonidos a la vez, pero no queremos quedarnos sin memoria o códecs. La mejor manera de gestionar estas cosas es tener un grupo de jugadores y luego elegir uno para usar cuando queramos reproducir un sonido. Podríamos actualizar nuestro código para que sea así:

clase MediaPlayerPool (contexto: contexto, maxStreams: Int) {
    contexto val privado: Context = context.applicationContext

    private val mediaPlayerPool = mutableListOf  (). también {
        para (i en 0..maxStreams) + + buildPlayer ()
    }
    private val playersInUse = mutableListOf  ()

    private fun buildPlayer () = MediaPlayer (). apply {
        setOnPreparedListener {start ()}
        setOnCompletionListener {recyclePlayer (it)}
    }

    / **
     * Devuelve un [MediaPlayer] si hay uno disponible,
     * de lo contrario nulo.
     * /
    Private Fun RequestPlayer (): MediaPlayer? {
        return if (! mediaPlayerPool.isEmpty ()) {
            mediaPlayerPool.removeAt (0). también {
                playersInUse + = it
            }
        } más nulo
    }

    private fun recyclePlayer (mediaPlayer: MediaPlayer) {
        mediaPlayer.reset ()
        playersInUse - = mediaPlayer
        mediaPlayerPool + = mediaPlayer
    }

    playSound divertido (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId)?: return
        val mediaPlayer = requestPlayer ()?: return

        mediaPlayer.run {
            setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset,
                    assetFileDescriptor.declaredLength)
            prepareAsync ()
        }
    }
}

Ahora pueden reproducirse múltiples sonidos a la vez, y podemos controlar la cantidad máxima de reproductores simultáneos para evitar usar demasiada memoria o demasiados códecs. Y, dado que estamos reciclando las instancias, el recolector de basura no tendrá que correr para limpiar todas las instancias antiguas que han terminado de jugar.

Hay algunas desventajas de este enfoque:

  • Después de que se reproducen los sonidos maxStreams, cualquier llamada adicional a playSound se ignora hasta que se libera a un jugador. Podría solucionar este problema "robando" un reproductor que ya está en uso para reproducir un sonido nuevo.
  • Puede haber un retraso significativo entre llamar a playSound y reproducir el sonido. Aunque MediaPlayer se está reutilizando, en realidad es un envoltorio delgado que controla un objeto nativo C ++ subyacente a través de JNI. El reproductor nativo se destruye cada vez que llama a MediaPlayer.reset (), y debe volver a crearse siempre que MediaPlayer esté preparado.

Mejorar la latencia mientras se mantiene la capacidad de reutilizar jugadores es más difícil de hacer. Afortunadamente, para ciertos tipos de sonidos y aplicaciones donde se requiere baja latencia, hay otra opción que analizaremos la próxima vez: SoundPool.