Novedades sobre productos

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

Lectura de 9 min
Mayuri Khinvasara Khabya
Ingeniera de Relaciones con Desarrolladores

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

  • En la Parte 1: Introducción a la precarga con Media3, se explicaron los conceptos básicos. Exploramos la distinción entre PreloadConfiguration para playlists simples y el DefaultPreloadManager más potente 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() y invalidate(), y liberar recursos con remove() y release().
  • Parte 2 (esta publicación): En este blog, exploramos las capacidades avanzadas de DefaultPreloadManager. Abordamos 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: La parte final de esta serie se centrará en la integración de PreloadManager con una caché de disco persistente, lo que te permitirá reducir el consumo de datos con la administración de recursos y brindar una experiencia sin interrupciones.

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.

Cómo escuchar: Recupera estadísticas con PreloadManagerListener

Cuando quieras lanzar una función en producción, como desarrollador de apps, también querrás comprender y capturar las estadísticas que la respaldan. ¿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.

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

  • onCompleted(MediaItem mediaItem): Se invoca esta devolución de llamada 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)

Cómo extraer estadísticas del objeto de escucha 

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

  • ¿Cuál es nuestra tasa de éxito de la precarga? (proporción de eventos onCompleted en relación con el total de intentos de precarga)
  • ¿Qué CDN o formatos de video muestran los porcentajes de error más altos? (Analizando las excepciones de onError)
  • ¿Cuál es nuestro porcentaje de errores de precarga? (proporción de eventos onError en relación con la cantidad total de intentos de precarga)

Estos datos podrían brindarte comentarios cuantitativos sobre tu estrategia de precarga, lo que te permitiría realizar pruebas A/B y mejoras basadas en datos en tu experiencia del usuario. Estos datos también pueden ayudarte a ajustar de forma inteligente la configuración de la precarga, como la duración, la cantidad de videos que deseas precargar y los búferes que asignas.

Más allá de la depuración: Cómo usar onError para una IU de resguardo correcta

Una precarga fallida 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 reintentos más inteligente. Una app puede optar por quitar de inmediato una fuente con errores del administrador según el mensaje de error o el código de estado HTTP. El elemento debería quitarse del flujo de la IU según corresponda para que los problemas de carga no afecten la experiencia del usuario. También puedes obtener datos más detallados de PreloadException, como HttpDataSourceException, para investigar más 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 en conjunto. Para garantizar la estabilidad y la eficiencia, deben compartir varios componentes principales. Si operan con componentes separados y descoordinados, podría verse afectada 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 la red y la memoria, lo que podría provocar una degradación del rendimiento. Una parte importante del ciclo de vida es el manejo de la eliminación adecuada. El orden recomendado para la eliminación es liberar primero el PreloadManager y, luego, el ExoPlayer.

El objeto DefaultPreloadManager.Builder está diseñado para facilitar este uso compartido y tiene APIs para crear instancias de tu PreloadManager y de 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 objeto 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 tienen conocimiento de la actividad de red del otro, lo que puede generar situaciones de falla. Por ejemplo, considera la situación en la que un usuario está mirando un video, su conexión de red se degrada y el MediaSource de precarga inicia simultáneamente una descarga agresiva para un video futuro. La actividad de MediaSource de precarga consumiría el ancho de banda que necesita el reproductor activo, lo que provocaría que el video actual se detenga. Una interrupción durante la reproducción es un error significativo en la experiencia del usuario.

Al compartir un solo BandwidthMeter, el TrackSelector puede seleccionar las pistas de mayor calidad según las condiciones de red actuales y el estado del búfer, durante la precarga 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 LoadControl, TrackSelector y Renderer compartidos de ExoPlayer

  • LoadControl: Este componente dicta la política de almacenamiento en búfer, por ejemplo, la cantidad de datos que se deben almacenar en búfer antes de iniciar la reproducción y cuándo se debe comenzar 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 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 la cantidad de elementos que precargas y la duración de la precarga 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 carga previa seguirá realizando la carga previa mientras los bytes de búfer objetivo asignados para la carga previa no alcancen el límite superior. No esperará a que finalice 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 de Media3 1.9.

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector: Este componente es responsable de seleccionar qué pistas (por ejemplo, video de una resolución determinada, audio en un idioma específico) se deben cargar y reproducir. 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 precarga 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 precarga 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)

  • Renderer: Este componente se encarga de comprender las capacidades del reproductor sin crear los renderizadores completos. Verifica este esquema 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 medios compatible, y evita desperdiciar 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 Repetidor de reproducción 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. El Looper del subproceso desde el que se debe acceder al reproductor se puede consultar con Player.getApplicationLooper. Al mantener un Looper compartido entre el reproductor y PreloadManager, se garantiza que todas las operaciones en estos objetos de medios 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 fuentes de medios que se cargarán o precargarán deben ocurrir en el mismo subproceso de reproducción. Compartir el Looper es fundamental para la seguridad de los subprocesos, por lo que 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), realizas una transferencia de este objeto complejo y con estado desde el objeto MediaSource de carga previa 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 diferentes subprocesos, podría producirse una condición de carrera. El subproceso de PreloadManager podría estar modificando el estado interno de MediaSource (p. ej., escribiendo datos nuevos en un búfer) en el momento exacto en que el subproceso del reproductor intenta leerlo. Esto genera un comportamiento impredecible y una excepción 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 es necesario que los compartas de forma explícita con DefaultPreloadManager. Cuando compilas tu instancia de ExoPlayer a través de buildExoPlayer de DefaultPreloadManager.Builder, estos componentes se referencian automáticamente entre sí si usas las implementaciones predeterminadas con configuraciones predeterminadas. Sin embargo, si usas componentes o configuraciones personalizados, debes notificar explícitamente al DefaultPreloadManager sobre ellos a través de las APIs anteriores.

Precarga 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 a DefaultPreloadManager sin una estrategia de eliminación correspondiente, inevitablemente provocarás un OutOfMemoryError. Cada MediaSource precargado contiene un SampleQueue, que asigna búferes de memoria. A medida que se acumulan, pueden agotar el espacio de montón de la aplicación. La solución es un algoritmo que tal vez ya conozcas, llamado ventana deslizante. El patrón de ventana deslizante mantiene un conjunto pequeño y manejable de elementos en la memoria 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, agregando elementos nuevos que entran en la vista y quitando elementos que ahora están lejos.

slidingwindow.png

Implementa el patrón de ventana deslizante

Es fundamental comprender que PreloadManager no proporciona un método setWindowSize() integrado. La ventana deslizante 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 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 el contenido que mira el 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 dentro de los límites y estable.

Ajusta una estrategia de precarga 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 granularidad con la configuración de TargetPreloadStatusControl en la Parte 1.

Para recordar, 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, 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 análisis a través de PreloadManagerListener, como se explicó en las secciones anteriores, para decidir tu estrategia de duración de la precarga.

Conclusión y próximos pasos

Ahora tienes el conocimiento avanzado para compilar feeds de medios 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, luego, implementar un manejo de errores sólido.
  • Siempre usa un solo DefaultPreloadManager.Builder para crear tus instancias de administrador y reproductor, y así 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 precarga 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 carga previa con el almacenamiento en caché en disco. Esta función está en desarrollo activo y estará disponible en los próximos meses.

¿Tienes algún comentario que compartir? Esperamos recibir noticias tuyas.

Mantente atento y acelera la reproducción de tus videos. 🚀

Escrito por:

Seguir leyendo