Novedades de productos

Mejora la reproducción de contenido multimedia: Presentamos la carga previa con Media3 (parte 1)

Lectura de 8 min
Ver el perfil de Mayuri Khinvasara Khabya
Mayuri Khinvasara Khabya Ingeniero de relaciones con desarrolladores

En las apps actuales centradas en el contenido multimedia, ofrecer una experiencia de reproducción fluida y sin interrupciones es clave para brindar una experiencia del usuario agradable. Los usuarios esperan que sus videos comiencen de inmediato y se reproduzcan sin problemas ni pausas.

El desafío principal es la latencia. Tradicionalmente, un reproductor de video solo comienza su trabajo (conectarse, descargar, analizar y almacenar en búfer) después de que el usuario elige un elemento para reproducirlo. Este enfoque reactivo es lento para el contexto actual de los videos de formato corto. La solución es ser proactivo. Debemos anticipar lo que el usuario mirará a continuación y preparar el contenido con anticipación. Esta es la esencia de la carga previa.

Entre los beneficios clave de la carga previa, se incluyen los siguientes:

  • 🚀 Inicio de reproducción más rápido: Los videos ya están listos para reproducirse, lo que genera transiciones más rápidas entre los elementos y un inicio más inmediato.
  • 📉 Menos almacenamiento en búfer: Al cargar datos de forma proactiva, es mucho menos probable que la reproducción se detenga, por ejemplo, debido a problemas de red.
  • ✨ Experiencia del usuario más fluida: La combinación de inicios más rápidos y menos almacenamiento en búfer crea una interacción más fluida y sin interrupciones para que los usuarios disfruten.

En esta serie de tres partes, presentaremos y analizaremos en profundidad las potentes utilidades de Media3 para (pre)cargar componentes.

  • En la parte 1, abordaremos los aspectos básicos: comprender las diferentes estrategias de precarga disponibles en Media3, habilitar PreloadConfiguration y configurar DefaultPreloadManager, lo que permite que tu app precargue elementos. Al final de este blog, deberías poder precargar y reproducir elementos multimedia con la clasificación y la duración que configuraste.
  • En la parte 2, abordaremos temas más avanzados de DefaultPreloadManager: el uso de objetos de escucha para las estadísticas, la exploración de las prácticas recomendadas listas para producción, como el patrón de ventana deslizante, y los componentes compartidos personalizados de DefaultPreloadManager y ExoPlayer.
  • En la parte 3, profundizaremos en la memoria caché del disco con DefaultPreloadManager.

La precarga al rescate 🦸‍♀️

La idea principal detrás de la precarga es simple: cargar el contenido multimedia antes de que lo necesites. Cuando un usuario desliza el dedo hacia el siguiente video, los primeros segmentos ya se descargaron y están disponibles, listos para reproducirse de inmediato.

Piensa en ello como si fuera un restaurante. Una cocina concurrida no espera a que llegue un pedido para empezar a picar cebollas. 🧅 Hacen el trabajo de preparación con anticipación. La precarga es el trabajo de preparación del reproductor de video.

Cuando está habilitada, la carga previa puede ayudar a minimizar la latencia de unión cuando un usuario pasa al siguiente elemento antes de que el búfer de reproducción llegue a ese elemento. Se prepara el primer período de la siguiente ventana y se almacenan en el búfer las muestras de video, audio y texto. El período precargado se pone en cola más adelante en el reproductor con muestras almacenadas en búfer disponibles de inmediato y listas para enviarse al códec para su renderización.

En Media3, hay dos APIs principales para la carga previa, cada una adecuada para diferentes casos de uso. Elegir la API adecuada es el primer paso.

1. Cómo precargar elementos de la playlist con PreloadConfiguration

Este es el enfoque simple, útil para medios lineales y secuenciales, como las playlists, en las que el orden de reproducción es predecible (como una serie de episodios). Le proporcionas al reproductor la lista completa de elementos multimedia con las APIs de lista de reproducción de ExoPlayer y estableces la PreloadConfiguration para el reproductor. Luego, este precarga automáticamente los siguientes elementos de la secuencia según la configuración. Esta API intenta optimizar la latencia de unión cuando un usuario salta al siguiente elemento antes de que el búfer de reproducción ya se superponga con el siguiente elemento.

La carga previa solo se inicia cuando no se carga ningún contenido multimedia para la reproducción en curso, lo que evita que compita por el ancho de banda con la reproducción principal.

Si aún no tienes certeza de si necesitas la carga previa, esta API es una excelente opción de bajo esfuerzo para probarla.

player.preloadConfiguration =
    PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L)

Con la PreloadConfiguration anterior, el reproductor intenta precargar cinco segundos de contenido multimedia para el siguiente elemento de la playlist.

Una vez que se habilita la carga previa de la playlist, se puede volver a desactivar con PreloadConfiguration.DEFAULT:

player.preloadConfiguration = PreloadConfiguration.DEFAULT

2. Cómo precargar listas dinámicas con PreloadManager

Para las IU dinámicas, como los feeds verticales o los carruseles, en los que el elemento "siguiente" se determina según la interacción del usuario, la API de PreloadManager es adecuada. Este es un nuevo componente potente e independiente dentro de la biblioteca de ExoPlayer de Media3, diseñado específicamente para realizar precargas de forma proactiva. Administra una colección de MediaSources potenciales, los prioriza según la proximidad a la posición actual del usuario y ofrece un control detallado sobre qué precargar, lo que es adecuado para situaciones complejas, como feeds dinámicos de videos de formato corto.

Configura tu PreloadManager

DefaultPreloadManager es la implementación canónica de PreloadManager.

El compilador de DefaultPreloadManager puede compilar tanto DefaultPreloadManager como cualquier instancia de ExoPlayer que reproducirá su contenido precargado. Para crear un DefaultPreloadManager, deberás pasar un TargetPreloadStatusControl, que el administrador de precarga puede consultar para saber cuánto cargar para un elemento. Explicaremos y definiremos un ejemplo de TargetPreloadStatusControl en la siguiente sección.

val preloadManagerBuilder =
DefaultPreloadManager.Builder(context, targetPreloadStatusControl)
val preloadManager = val preloadManagerBuilder.build()

// Build ExoPlayer with DefaultPreloadManager.Builder
val player = preloadManagerBuilder.buildExoPlayer()

Es necesario usar el mismo compilador para ExoPlayer y DefaultPreloadManager, lo que garantiza que los componentes internos se compartan correctamente.

Eso es todo. Ahora tienes un administrador listo para recibir instrucciones.

Configura la duración y la clasificación con TargetPreloadStatusControl

¿Qué pasa si quieres precargar, por ejemplo, 10 segundos de video? Puedes proporcionar la posición de tus elementos multimedia en el carrusel, y DefaultPreloadManager prioriza la carga de los elementos según la cercanía con el elemento que el usuario está reproduciendo en ese momento.

Si quieres controlar la duración del elemento que se precargará, puedes indicarlo con el DefaultPreloadManager.PreloadStatus que devuelvas.

Por ejemplo:

  • El elemento “A” tiene la prioridad más alta, por lo que se cargan 5 segundos de video.
  • El elemento “B” tiene prioridad media, pero cuando llegues a él, carga 3 segundos de video.
  • El elemento “C” tiene menos prioridad, por lo que solo se cargan las pistas.
  • El elemento “D” tiene aún menos prioridad, solo hay que prepararlo.
  • Los demás elementos están lejos, por lo que no se debe precargar nada.

Este control detallado puede ayudarte a optimizar el uso de recursos, lo que se recomienda para una reproducción sin interrupciones.

import androidx.media3.exoplayer.DefaultPreloadManager.PreloadStatus


class MyTargetPreloadStatusControl(
    currentPlayingIndex: Int = C.INDEX_UNSET
) : TargetPreloadStatusControl<Int,PreloadStatus> {


    // The app is responsible for updating this based on UI state
    override fun getTargetPreloadStatus(index: Int): PreloadStatus? {

        val distance = index - currentPlayingIndex

        // Adjacent items (Next): preload 5 seconds
        if (distance == 1) { 
        // Return a PreloadStatus that is labelled by STAGE_SPECIFIED_RANGE_LOADED and suggest loading // 5000ms from the default start position
                    return PreloadStatus.specifiedRangeLoaded(5000L)
                } 

        // Adjacent items (Previous): preload 3 seconds
        else if (distance == -1) { 
        // Return a PreloadStatus that is labelled by STAGE_SPECIFIED_RANGE_LOADED //and suggest loading 3000ms from the default start position
                    return PreloadStatus.specifiedRangeLoaded(3000L)
                } 

        // Items two positions away: just select tracks
        else if (distance) == 2) {
        // Return a PreloadStatus that is labelled by STAGE_TRACKS_SELECTED
                    return PreloadStatus.TRACKS_SELECTED
                } 

        // Items four positions away: just select prepare
        else if (abs(distance) <= 4) {
        // Return a PreloadStatus that is labelled by STAGE_SOURCE_PREPARED
                    return PreloadStatus.SOURCE_PREPARED
                }

             // All other items are too far away
             return null
            }
}

Nota: PreloadManager puede mantener precargados los elementos anteriores y siguientes, mientras que PreloadConfiguration solo buscará los elementos siguientes.

Administra los elementos de carga previa

Una vez que hayas creado el administrador, puedes comenzar a indicarle en qué trabajar. A medida que el usuario se desplace por un feed, identificarás los próximos videos y los agregarás al administrador. La interacción con PreloadManager es una conversación basada en el estado entre tu IU y el motor de carga previa.

1. Agrega elementos multimedia

A medida que completas tu feed, debes informar al administrador sobre el contenido multimedia del que debe hacer un seguimiento. Si estás comenzando, puedes agregar la lista completa que deseas precargar. Posteriormente, puedes seguir agregando un solo elemento a la lista cuando sea necesario. Tienes control total sobre los elementos que se encuentran en la lista de precarga, lo que significa que también debes administrar lo que se agrega y quita del administrador.

val initialMediaItems = pullMediaItemsFromService(/* count= */ 20)
for (index in 0 until initialMediaItems.size) {
    preloadManager.add(
        initialMediaItems.get(index),index)
    )
}

El administrador ahora comenzará a recuperar datos para este MediaItem en segundo plano.

Después de agregar, indícale al administrador que vuelva a evaluar su nueva lista (lo que indica que algo cambió, como agregar o quitar un elemento, o que el usuario cambia para reproducir un elemento nuevo).

preloadManager.invalidate()

2. Recupera y reproduce un elemento

Aquí viene la lógica de reproducción principal. Cuando el usuario decida reproducir ese video, no necesitarás crear un nuevo MediaSource. En su lugar, le pides al PreloadManager el que ya preparó. Puedes recuperar el MediaSource del administrador de precarga con el MediaItem.

Si el elemento recuperado de PreloadManager es nulo, significa que el mediaItem aún no se cargó previamente ni se agregó a PreloadMamager, por lo que debes configurar el mediaItem directamente.

// When a media item is about to displ​​ay on the screen
val mediaSource = preloadManager.getMediaSource(mediaItem)
if (mediaSource!= null) {
  player.setMediaSource(mediaSource)
} else {
  // If mediaSource is null, that mediaItem hasn't been added yet.
  // So, send it directly to the player.
  player.setMediaItem(mediaItem)
}
player.prepare()
// When the media item is displaying at the center of the screen
player.play()

Cuando preparas el MediaSource recuperado del PreloadManager, realizas una transición fluida de la carga previa a la reproducción, con los datos que ya están en la memoria. Esto es lo que hace que la hora de inicio sea más rápida.

3. Mantén el índice actual sincronizado con la IU

Dado que nuestro feed o lista podría ser dinámico, es importante notificar al PreloadManager sobre tu índice de reproducción actual para que siempre pueda priorizar los elementos más cercanos a tu índice actual para la carga previa.

preloadManager.setCurrentPlayingIndex(currentIndex)
// Need to call invalidate() to update the priorities
preloadManager.invalidate()

4. Cómo quitar un elemento

Para mantener la eficiencia del administrador, debes quitar los elementos que ya no necesita seguir, como los que están lejos de la posición actual del usuario.

// When an item is too far from the current playing index
preloadManager.remove(mediaItem)

Si necesitas borrar todos los elementos a la vez, puedes llamar a preloadManager.reset().

5. Libera al administrador

Cuando ya no necesites el PreloadManager (p.ej., cuando se destruya tu IU), debes liberarlo para liberar sus recursos. Un buen lugar para hacerlo es donde ya estás liberando los recursos de tu reproductor. Se recomienda liberar el administrador antes que el reproductor, ya que este puede seguir reproduciendo contenido si no necesitas más precarga.

// In your Activity's onDestroy() or Composable's onDispose
preloadManager.release()

Hora de la demostración

Míralo en acción 👍

En la siguiente demostración, vemos el impacto de PreloadManager en el lado derecho, que tiene tiempos de carga más rápidos, mientras que el lado izquierdo muestra la experiencia existente. También puedes ver la muestra de código de la demostración. (Bonus: It also displays startup latency for every video)

Demo-PreloadManager_2.webp

¿Qué sigue?

Y con esto terminamos la parte 1. Ahora tienes las herramientas para compilar un sistema de carga previa dinámico. Puedes usar PreloadConfiguration para precargar el siguiente elemento de una playlist en ExoPlayer o configurar un DefaultPreloadManager, agregar y quitar elementos sobre la marcha, configurar el estado de precarga objetivo y recuperar correctamente el contenido precargado para la reproducción.

En la parte 2, profundizaremos en el DefaultPreloadManager. Exploraremos cómo escuchar los eventos de precarga, analizaremos las prácticas recomendadas, como usar una ventana deslizante para evitar problemas de memoria, y analizaremos los componentes compartidos personalizados de ExoPlayer y DefaultPreloadManager.

¿Tienes algún comentario para compartir? Queremos conocer tu opinión.

Mantente al tanto y haz que tu app sea más rápida. 🚀

Escrito por:
Continuar leyendo