Novedades sobre productos
Mejora de la reproducción de contenido multimedia: Análisis detallado de PreloadManager de Media3 (parte 2)
Lectura de 9 min
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 de contenido multimedia de baja latencia y alta capacidad de respuesta en tus apps para Android.
- Parte 1: Introducción a la carga previa con Media3 abarcó los aspectos fundamentales. 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() e 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 memoria caché del 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 aquellos que 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 deseas lanzar una función en producción, como desarrollador de apps, también deseas 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 esto, se requieren datos sobre las tasas de éxito, las fallas y el rendimiento. La interfaz PreloadManagerListener es el mecanismo principal para recopilar estos datos.
PreloadManagerListener proporciona dos devoluciones de llamada esenciales que ofrecen estadísticas fundamentales 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 TargetPreloadStatusControl.
- onError(PreloadException error): Esta devolución de llamada podría 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 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 a los intentos totales de carga previa)
- ¿Qué CDN o formatos de video muestran las tasas de error más altas? (analizando las excepciones de onError)
- ¿Cuál es nuestra tasa de error de carga previa? (proporción de eventos onError con respecto a los intentos totales de carga previa)
Estos datos podrían proporcionarte comentarios cuantitativos sobre tu estrategia de carga previa, lo que permite realizar pruebas A/B y mejoras basadas en datos en la experiencia del usuario. Además, estos datos pueden ayudarte a ajustar de forma inteligente las duraciones de la carga previa y la cantidad de videos que deseas cargar previamente, así como los búferes que asignas.
Más allá de la depuración: Usa onError para la reserva de IU correcta
Una carga previa 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 desactivar la reproducción automática para el siguiente deslizamiento, lo que requeriría que el usuario presione para iniciar la reproducción.
Además, si inspeccionas el tipo PreloadException, puedes definir una estrategia de reintento 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 en consecuencia 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 sobre la solución de 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 components. Si operan con componentes separados y no coordinados, podría afectar la seguridad de los subprocesos y la usabilidad de las pistas cargadas previamente en el reproductor, ya que debemos asegurarnos de que las pistas cargadas previamente 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 de eliminación es liberar primero PreloadManager y, luego, ExoPlayer.
DefaultPreloadManager.Builder está diseñado para facilitar este uso compartido y tiene APIs para crear instancias de PreloadManager y de un reproductor vinculado. 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.
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 están al tanto 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 mira un video, su conexión de red se degrada y la carga previa de MediaSource inicia simultáneamente una descarga agresiva para un video futuro. La actividad de la precarga de MediaSource consumiría el ancho de banda que necesita el reproductor activo, lo que provocaría que el video actual se detenga. Una detención durante la reproducción es una falla importante en la experiencia del usuario.
Si se comparte un solo BandwidthMeter, TrackSelector puede seleccionar pistas de la más alta calidad dadas 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)
Cómo 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, como la cantidad de datos que se almacenarán 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 PreloadManager se guíe por una sola estrategia de almacenamiento en búfer coordinada en el contenido multimedia cargado previamente y en reproducción activa, lo que evita la contención de recursos. Deberás asignar de forma inteligente el tamaño del búfer en coordinación con la cantidad de elementos que cargas previamente 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 mientras los bytes del 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 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) 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 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 precarga NO debe compartir la misma instancia de TrackSelector con el reproductor. En su lugar, deben usar la diferente instancia de TrackSelector pero de la misma implementación. Por eso, configuramos TrackSelectorFactory en lugar de un TrackSelector en DefaultPreloadManager.Builder.
preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)
- Renderer: Este componente es responsable 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 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 Looper 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. 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 de contenido 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 se cargarán previamente deben ocurrir en el mismo subproceso de reproducción. Compartir el Looper es fundamental para la seguridad de los subprocesos y, por lo tanto, debemos compartir el PlaybackLooper entre PreloadManager y el reproductor.
PreloadManager prepara un objeto MediaSource con estado en segundo plano. Cuando el código de la IU llama a player.setMediaSource(mediaSource), realizas una transferencia de este objeto complejo con estado desde la carga previa de MediaSource al reproductor. En este caso, 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, 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()
Sugerencia: 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 hacen referencia automáticamente entre sí si usas las implementaciones predeterminadas con configuraciones predeterminadas. Sin embargo, si usas componentes o configuraciones personalizadas, debes notificar de forma explícita a 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 a DefaultPreloadManager sin una estrategia de eliminación correspondiente, inevitablemente provocarás un OutOfMemoryError. Cada MediaSource cargado previamente 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 quizás ya conozcas, llamado ventana deslizante. El patrón de ventana deslizante mantiene un conjunto pequeño y administrable 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 aparecen y quitando elementos que ahora están distantes.
Cómo implementar 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ú, el desarrollador, eres responsable de implementar con los métodos primitivos add() y remove(). La lógica de tu aplicación debe conectar eventos de la IU, como un desplazamiento o un cambio de página, a estas llamadas a la API. Si deseas una referencia de código para esto, tenemos este patrón de ventana deslizante implementado en socialite ejemplo, 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 visualización 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 carga previa. La llamada remove() garantiza que se liberen los recursos que te ayudan a mantener el uso de memoria de tu app limitado y estable.
Cómo ajustar una estrategia de carga previa categorizada con TargetPreloadStatusControl
Ahora que definimos qué cargar previamente (los elementos de nuestra ventana), podemos aplicar una estrategia bien definida para la cantidad de carga previa de 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. Puedes 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 "carga previa" basada en la proximidad, que es la clave para equilibrar la reproducción inmediata con el uso eficiente de los recursos.
Puedes usar datos de estadísticas a través de PreloadManagerListener, como se explicó en las secciones anteriores, para decidir tu estrategia de duración de carga previa.
Conclusión y próximos pasos
Ahora tienes el conocimiento avanzado para crear feeds de contenido multimedia rápidos, estables y eficientes en cuanto a recursos con DefaultPreloadManager de Media3.
Repasemos las conclusiones clave:
- Usa PreloadManagerListener para recopilar estadísticas y aplicar un manejo de errores sólido.
- Usa siempre un solo DefaultPreloadManager.Builder para crear instancias del administrador y del reproductor para garantizar que se compartan los componentes importantes.
- Implementa el patrón de ventana deslizante administrando de forma activa las llamadas 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 cargado previamente
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 cargado previamente 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é de disco. Esta función está en desarrollo activo y estará disponible en unos meses.
¿Tienes algún comentario para compartir? Nos encantaría conocer tu opinión.
Mantente al tanto y haz que la reproducción de video sea más rápida. 🚀
Seguir leyendo
-
Novedades sobre productos
En las apps centradas en el contenido multimedia de hoy en día, brindar una experiencia de reproducción fluida y sin interrupciones es clave para una experiencia del usuario agradable. Los usuarios esperan que sus videos comiencen de inmediato y se reproduzcan sin problemas sin pausas.
Mayuri Khinvasara Khabya • Lectura de 8 min
-
Novedades sobre productos
Como se anunció hoy durante The Android Show, Android está pasando de un sistema operativo a un sistema de inteligencia, lo que crea más oportunidades de participación con tus apps.
Matthew McCullough • Lectura de 4 min
-
Novedades sobre productos
El ecosistema móvil siempre está en evolución, lo que genera nuevas oportunidades y nuevas amenazas. A través de estos cambios, Android y Google Play siguen comprometidos a garantizar que miles de millones de usuarios puedan seguir disfrutando de sus apps con confianza y que la innovación de los desarrolladores pueda prosperar.
Vijaya Kaza • Lectura de 3 min
Mantente al día
Recibe la información más reciente sobre el desarrollo de Android en tu bandeja de entrada todas las semanas.