Novedades de productos

Mejora la reproducción de contenido multimedia: Análisis detallado de PreloadManager de Media3 (parte 2)

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

Te damos la bienvenida a la segunda entrega de nuestra serie de tres partes sobre la carga previa de contenido multimedia con Media3. Esta serie está diseñada para guiarte en el proceso de creación de experiencias multimedia de baja latencia y alta capacidad de respuesta en tus apps para Android.

  • En la Parte 1: Introducción a la carga previa con Media3, se abordaron los aspectos básicos. Exploramos la distinción entre PreloadConfiguration para listas de reproducción simples y el más potente DefaultPreloadManager para interfaces de usuario dinámicas. Aprendiste a implementar el ciclo de vida básico de la API: agregar contenido multimedia con add(), recuperar un MediaSource preparado con getMediaSource(), administrar prioridades con setCurrentPlayingIndex() e invalidate(), y liberar recursos con remove() y release().
  • Parte 2 (esta publicación): En este blog, exploramos las funciones avanzadas de DefaultPreloadManager. Explicamos cómo obtener estadísticas con PreloadManagerListener, implementar prácticas recomendadas listas para producción, como compartir componentes principales con ExoPlayer, y dominar el patrón de ventana deslizante para administrar la memoria de manera eficaz.
  • Parte 3: En la parte final de esta serie, se profundizará en la integración de PreloadManager con una memoria caché del disco persistente, lo que te permitirá reducir el consumo de datos con la administración de recursos y brindar una experiencia fluida.

Si es la primera vez que usas la carga previa en Media3, te recomendamos que leas la parte 1 antes de continuar. Para quienes estén listos para ir más allá de los conceptos básicos, exploremos cómo mejorar la implementación de la reproducción de contenido multimedia.

Escucha: Recupera estadísticas con PreloadManagerListener

Cuando quieres lanzar una función en producción, como desarrollador de apps, también quieres comprender y capturar las estadísticas detrás de ella. ¿Cómo puedes asegurarte de que tu estrategia de carga previa sea eficaz en un entorno real? Para responder esta pregunta, se necesitan datos sobre las tasas de éxito, los errores y el rendimiento. La interfaz PreloadManagerListener es el mecanismo principal para recopilar estos datos.

PreloadManagerListener proporciona dos devoluciones de llamada esenciales que ofrecen información clave sobre el proceso y el estado de la carga previa.

  • onCompleted(MediaItem mediaItem): Esta devolución de llamada se invoca cuando se completa correctamente una solicitud de carga previa, según lo define tu TargetPreloadStatusControl.
  • onError(PreloadException error): Esta devolución de llamada puede ser útil para la depuración y la supervisión. Se invoca cuando falla una carga previa y proporciona la excepción asociada.

Puedes registrar un objeto de escucha con una sola llamada de método, como se muestra en el siguiente código de ejemplo:

val preloadManagerListener = object : PreloadManagerListener {
    override fun onCompleted(mediaItem: MediaItem) {
        // Log success for analytics. 
        Log.d("PreloadAnalytics", "Preload completed for $mediaItem")
    }

    override fun onError( preloadError: PreloadException) {
        // Log the specific error for debugging and monitoring.
        Log.e("PreloadAnalytics", "Preload error ", preloadError)
    }
}

preloadManager.addListener(preloadManagerListener)

Extraer estadísticas del oyente 

Estas devoluciones de llamada de objetos de escucha se pueden conectar a tu canalización de estadísticas. Si reenvías estos eventos a tu motor de análisis, puedes responder preguntas clave como las siguientes:

  • ¿Cuál es nuestra tasa de éxito de carga previa? (proporción de eventos onCompleted con respecto al total de intentos de carga previa)
  • ¿Qué CDN o formatos de video presentan los porcentajes de errores más altos? (analizando las excepciones de onError)
  • ¿Cuál es nuestro porcentaje de errores de carga previa? (proporción de eventos onError en relación con el total de intentos de precarga)

Estos datos podrían brindarte comentarios cuantitativos sobre tu estrategia de carga previa, lo que permitiría realizar pruebas A/B y mejoras basadas en datos para la experiencia del usuario. Estos datos pueden ayudarte a ajustar de forma inteligente la duración de la carga previa y la cantidad de videos que quieres cargar previamente, así como los búferes que asignas.

Más allá de la depuración: Usa onError para un resguardo de la IU correcto

Un error de carga previa es un indicador sólido de un próximo evento de almacenamiento en búfer para el usuario. La devolución de llamada onError te permite responder de forma reactiva. En lugar de solo registrar el error, puedes adaptar la IU. Por ejemplo, si el próximo video no se precarga, tu aplicación podría inhabilitar la reproducción automática para el siguiente deslizamiento, lo que requeriría que el usuario presione para comenzar la reproducción.

Además, si inspeccionas el tipo PreloadException, puedes definir una estrategia de reintento más inteligente. Una app puede quitar de inmediato una fuente con errores del administrador según el mensaje de error o el código de estado HTTP. El elemento se debería quitar del flujo de la IU según corresponda para que los problemas de carga no se filtren en la experiencia del usuario. También puedes obtener datos más detallados de PreloadException, como HttpDataSourceException, para investigar más a fondo los errores. Obtén más información para solucionar problemas de ExoPlayer.

El sistema de compañeros: ¿Por qué es necesario compartir componentes con ExoPlayer?

DefaultPreloadManager y ExoPlayer están diseñados para funcionar juntos. Para garantizar la estabilidad y la eficiencia, deben compartir varios componentes principales. Si operan con componentes separados y no coordinados, podría afectar la seguridad de subprocesos y la usabilidad de las pistas precargadas en el reproductor, ya que debemos asegurarnos de que las pistas precargadas se reproduzcan en el reproductor correcto. Los componentes separados también podrían competir por recursos limitados, como el ancho de banda de red y la memoria, lo que podría provocar una degradación del rendimiento. Una parte importante del ciclo de vida es controlar la eliminación adecuada. El orden recomendado para la eliminación es liberar primero el PreloadManager y, luego, el ExoPlayer.

DefaultPreloadManager.Builder está diseñado para facilitar este uso compartido y tiene APIs para crear una instancia de PreloadManager y una instancia de reproductor vinculada. Veamos por qué se deben compartir componentes como BandwidthMeter, LoadControl, TrackSelector y Looper. Consulta la representación visual de cómo interactúan estos componentes con la reproducción de ExoPlayer.

preloadManager2.png

Cómo evitar conflictos de ancho de banda con un BandwidthMeter compartido

El BandwidthMeter proporciona una estimación del ancho de banda de red disponible en función de las tasas de transferencia históricas. Si PreloadManager y el reproductor usan instancias separadas, no conocen la actividad de red del otro, lo que puede provocar fallas. Por ejemplo, considera la situación en la que un usuario mira un video, su conexión de red se degrada y el MediaSource de carga previa inicia simultáneamente una descarga agresiva para un video futuro. La actividad de la MediaSource de carga previa consumiría el ancho de banda que necesita el reproductor activo, lo que provocaría que el video actual se detuviera. Una detención durante la reproducción es una falla significativa en la experiencia del usuario.

Cuando se comparte un solo BandwidthMeter, el TrackSelector puede seleccionar las pistas de mayor calidad según las condiciones actuales de la red y el estado del búfer, durante la carga previa o la reproducción. Luego, puede tomar decisiones inteligentes para proteger la sesión de reproducción activa y garantizar una experiencia fluida.

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

Garantizar la coherencia con los componentes compartidos LoadControl, TrackSelector y Renderer de ExoPlayer

  • LoadControl: Este componente dicta la política de almacenamiento en búfer, como la cantidad de datos que se deben almacenar en búfer antes de iniciar la reproducción y cuándo iniciar o detener la carga de más datos. Compartir LoadControl garantiza que el consumo de memoria del reproductor y de PreloadManager se guíe por una sola estrategia de almacenamiento en búfer coordinada en el contenido multimedia precargado y en el que se reproduce de forma activa, lo que evita la contención de recursos. Deberás asignar de forma inteligente el tamaño del búfer coordinando cuántos elementos precargas y con qué duración para garantizar la coherencia. En momentos de contención, el reproductor priorizará la reproducción del elemento actual que se muestra en la pantalla. Con un LoadControl compartido, el administrador de precarga continuará precargando siempre que los bytes de búfer de destino asignados para la precarga no hayan alcanzado el límite superior, no espera hasta que se complete la carga para la reproducción.

Nota: El uso compartido de LoadControl en la versión más reciente de Media3 (1.8) garantiza que su Allocator se pueda compartir correctamente con PreloadManager y el reproductor. El uso de LoadControl para controlar de manera eficaz la carga previa es una función que estará disponible en la próxima versión 1.9 de Media3.

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector: Este componente se encarga de seleccionar qué pistas (por ejemplo, un video con una resolución determinada o un audio en un idioma específico) se cargarán y reproducirán. El uso compartido garantiza que las pistas seleccionadas durante la carga previa sean las mismas que usará el reproductor. Esto evita una situación de desperdicio en la que se carga previamente una pista de video de 480p, solo para que el reproductor la descarte de inmediato y recupere una pista de 720p durante la reproducción.< br /> El administrador de carga previa NO debe compartir la misma instancia de TrackSelector con el reproductor. En su lugar, deben usar la instancia de TrackSelector diferente, pero de la misma implementación. Por eso, configuramos TrackSelectorFactory en lugar de TrackSelector en DefaultPreloadManager.Builder.

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • Renderizador: Este componente se encarga de comprender las capacidades del reproductor sin crear los renderizadores completos. Verifica este plano para ver qué formatos de video, audio y texto admitirá el reproductor final. Esto le permite seleccionar y descargar de forma inteligente solo la pista de contenido multimedia compatible y evita que se desperdicie ancho de banda en contenido que el reproductor no puede reproducir.

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

Obtén más información sobre los componentes de ExoPlayer.

La regla de oro: Un Playback Looper común para gobernarlos a todos

El subproceso en el que se puede acceder a una instancia de ExoPlayer se puede especificar de forma explícita pasando un Looper cuando se crea el reproductor. Se puede consultar el Looper del subproceso desde el que se debe acceder al reproductor con Player.getApplicationLooper. Si se mantiene un Looper compartido entre el reproductor y PreloadManager, se garantiza que todas las operaciones en estos objetos multimedia compartidos se serialicen en la cola de mensajes de un solo subproceso. Esto puede reducir los errores de simultaneidad.

Todas las interacciones entre PreloadManager y el reproductor con las fuentes de contenido multimedia que se cargarán o cargarán previamente deben ocurrir en el mismo subproceso de reproducción. Compartir el Looper es un requisito para la seguridad de subprocesos y, por lo tanto, debemos compartir el PlaybackLooper entre el PreloadManager y el reproductor.

PreloadManager prepara un objeto MediaSource con estado en segundo plano. Cuando tu código de IU llama a player.setMediaSource(mediaSource), estás realizando una transferencia de este objeto complejo con estado desde el MediaSource de precarga al reproductor. En esta situación, todo el PreloadMediaSource se mueve del administrador al reproductor. Todas estas interacciones y transferencias deben ocurrir en el mismo PlaybackLooper.

Si PreloadManager y ExoPlayer operaran en subprocesos diferentes, podría producirse una condición de carrera. El subproceso de PreloadManager podría modificar el estado interno de MediaSource (p. ej., escribir datos nuevos en un búfer) en el momento exacto en que el subproceso del reproductor intenta leerlo. Esto genera un comportamiento impredecible, una IllegalStateException que es difícil de depurar.

preloadManagerBuilder.setPreloadLooper(playbackLooper)

Veamos cómo puedes compartir todos los componentes anteriores entre ExoPlayer y DefaultPreloadManager en la configuración.

val preloadManagerBuilder =
DefaultPreloadManager.Builder(context, targetPreloadStatusControl)

// Optional - Share components between ExoPlayer and DefaultPreloadManager
preloadManagerBuilder
     .setBandwidthMeter(customBandwidthMeter)
     .setLoadControl(customLoadControl)
     .setMediaSourceFactory(customMediaSourceFactory)
     .setTrackSelectorFactory(customTrackSelectorFactory)
     .setRenderersFactory(customRenderersFactory)
     .setPreloadLooper(playbackLooper)

val preloadManager = val preloadManagerBuilder.build()

Nota: Si usas los componentes predeterminados en ExoPlayer, como DefaultLoadControl, etc., no necesitas compartirlos de forma explícita con DefaultPreloadManager. Cuando compilas tu instancia de ExoPlayer a través de buildExoPlayer de DefaultPreloadManager.Builder, se hace referencia a estos componentes entre sí de forma automática, si usas las implementaciones predeterminadas con las configuraciones predeterminadas. Sin embargo, si usas componentes o parámetros de configuración personalizados, debes notificar explícitamente al DefaultPreloadManager sobre ellos a través de las APIs anteriores.

Carga previa lista para producción: El patrón de ventana deslizante

En un feed dinámico, un usuario puede desplazarse por una cantidad de contenido prácticamente infinita. Si agregas videos de forma continua al DefaultPreloadManager sin una estrategia de eliminación correspondiente, inevitablemente causarás un OutOfMemoryError. Cada MediaSource precargado contiene un SampleQueue, que asigna búferes de memoria. A medida que se acumulan, pueden agotar el espacio del montón de la aplicación. La solución es un algoritmo que quizás ya conozcas, llamado ventana deslizante. El patrón de ventana deslizante mantiene en la memoria un conjunto pequeño y manejable de elementos que son lógicamente adyacentes a la posición actual del usuario en el feed. A medida que el usuario se desplaza, esta "ventana" de elementos administrados se desliza con él, agrega elementos nuevos que aparecen en la vista y quita los que ahora están distantes.

slidingwindow.png

Implementa el patrón de ventana deslizante

Es fundamental comprender que PreloadManager no proporciona un método setWindowSize() integrado. La ventana variable es un patrón de diseño que tú, como desarrollador, debes implementar con los métodos primitivos add() y remove(). La lógica de tu aplicación debe conectar los eventos de la IU, como un desplazamiento o un cambio de página, a estas llamadas a la API. Si quieres una referencia de código para esto, tenemos este patrón de ventana deslizante implementado en la muestra de socialite, que también incluye un PreloadManagerWrapper que imita una ventana deslizante.

No olvides agregar preloadManager.remove(mediaItem) en tu implementación cuando ya no sea probable que el elemento aparezca pronto en la vista del usuario. No quitar los elementos que ya no están cerca del usuario es la causa principal de los problemas de memoria en las implementaciones de precarga. La llamada a remove() garantiza que se liberen los recursos que te ayudan a mantener el uso de memoria de tu app limitado y estable.

Ajusta una estrategia de carga previa categorizada con TargetPreloadStatusControl

Ahora que definimos qué precargar (los elementos de nuestra ventana), podemos aplicar una estrategia bien definida para determinar cuánto precargar para cada elemento. Ya vimos cómo lograr esta nivel de detalle con la configuración de TargetPreloadStatusControl en la Parte 1.

Recuerda que un elemento en la posición +/- 1 podría tener una mayor probabilidad de reproducirse que un elemento en la posición +/- 4. Podrías asignar más recursos (red, CPU y memoria) a los elementos que es más probable que el usuario vea a continuación. Esto crea una estrategia de "precarga" basada en la proximidad, que es la clave para equilibrar la reproducción inmediata con el uso eficiente de los recursos.

Puedes usar los datos de estadísticas a través de PreloadManagerListener, como se explicó en las secciones anteriores, para decidir tu estrategia de duración de la carga previa.

Conclusión y próximos pasos

Ahora tienes los conocimientos avanzados para compilar feeds de contenido multimedia rápidos, estables y eficientes en el uso de recursos con DefaultPreloadManager de Media3.

Repasemos las conclusiones clave:

  • Usa PreloadManagerListener para recopilar estadísticas y, además, implementar un manejo de errores sólido.
  • Usa siempre un solo DefaultPreloadManager.Builder para crear las instancias del administrador y del reproductor, y garantizar que se compartan los componentes importantes.
  • Implementa el patrón de ventana deslizante administrando de forma activa las llamadas a add() y remove() para evitar OutOfMemoryError.
  • Usa TargetPreloadStatusControl para crear una estrategia de carga previa inteligente y por niveles que equilibre el rendimiento y el consumo de recursos.

Qué sigue en la parte 3: Almacenamiento en caché con contenido multimedia precargado

La carga previa de datos en la memoria proporciona un beneficio de rendimiento inmediato, pero puede tener desventajas. Una vez que se cierra la aplicación o se quita el contenido multimedia precargado del administrador, los datos desaparecen. Para lograr un nivel de optimización más persistente, podemos combinar la precarga con la memoria caché del disco. Esta función se encuentra en desarrollo activo y estará disponible próximamente en unos meses.

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

No te pierdas las novedades y haz que la reproducción de tus videos sea más rápida. 🚀

Escrito por:
Continuar leyendo