Novedades de productos
Mejorar la reproducción de contenido multimedia: análisis detallado de PreloadManager de Media3 (parte 2)
Lectura de 9 minutos
Te damos la bienvenida a la segunda parte de nuestra serie de tres artículos sobre la precarga de contenido multimedia con Media3. Esta serie está diseñada para guiarte en el proceso de creación de experiencias multimedia con alta capacidad de respuesta y baja latencia en tus aplicaciones Android.
- En la parte 1: Introducción a la precarga con Media3, se trataron los conceptos básicos. Hemos analizado la diferencia entre PreloadConfiguration para listas de reproducción sencillas y DefaultPreloadManager, que es más potente y se usa en interfaces de usuario dinámicas. Has aprendido a implementar el ciclo de vida básico de la API: añadir contenido multimedia con add(), obtener un objeto MediaSource preparado con getMediaSource(), gestionar prioridades con setCurrentPlayingIndex() e invalidate() y liberar recursos con remove() y release().
- Parte 2 (esta entrada): en esta entrada de blog, analizamos las funciones avanzadas de DefaultPreloadManager. Hablaremos sobre 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 gestionar la memoria de forma eficaz.
- Parte 3: En la última parte de esta serie, se explicará cómo integrar PreloadManager con una caché de disco persistente para que puedas reducir el consumo de datos con la gestión de recursos y ofrecer una experiencia fluida.
Si no tienes experiencia con la precarga en Media3, te recomendamos que leas la parte 1 antes de continuar. Si ya dominas los conceptos básicos, vamos a ver cómo mejorar la implementación de la reproducción multimedia.
Escuchar: obtener analíticas con PreloadManagerListener
Cuando quieres lanzar una función en producción, como desarrollador de aplicaciones, también quieres conocer y registrar las analíticas que hay detrás. ¿Cómo puedes asegurarte de que tu estrategia de precarga es eficaz en un entorno real? Para responder a esta pregunta, se necesitan datos sobre las tasas de éxito, los errores y el rendimiento. La interfaz PreloadManagerListener es el mecanismo principal para recoger estos datos.
PreloadManagerListener proporciona dos retrollamadas esenciales que ofrecen información valiosa sobre el proceso y el estado de la precarga.
- onCompleted(MediaItem mediaItem): se invoca esta devolución de llamada cuando se completa correctamente una solicitud de precarga, tal como se define en TargetPreloadStatusControl.
- onError(PreloadException error): esta retrollamada puede ser útil para depurar y monitorizar. Se invoca cuando falla una precarga y proporciona la excepción asociada.
Puedes registrar un procesador 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 información valiosa de la audiencia
Estas retrollamadas de escucha se pueden conectar a tu canalización de analíticas. Si reenvía estos eventos a su motor de analíticas, podrá responder a preguntas clave como las siguientes:
- ¿Cuál es nuestro porcentaje de éxito de precarga? Relación entre los eventos onCompleted y el total de intentos de precarga.
- ¿Qué CDNs o formatos de vídeo muestran las tasas de error más altas? Analizando las excepciones de onError
- ¿Cuál es nuestro porcentaje de errores de precarga? relación entre eventos onError y el número total de intentos de precarga
Estos datos pueden proporcionarte comentarios cuantitativos sobre tu estrategia de precarga, lo que te permite hacer pruebas A/B y mejorar la experiencia de usuario basándote en datos. Estos datos pueden ayudarte a ajustar de forma inteligente la duración de la precarga y el número de vídeos que quieres precargar, así como los búferes que asignes.
Más allá de la depuración: usar onError para una alternativa de interfaz de usuario correcta
Una precarga fallida es un indicador claro de que el usuario va a experimentar un evento de almacenamiento en búfer. La retrollamada onError te permite responder de forma reactiva. En lugar de registrar el error, puedes adaptar la interfaz de usuario. Por ejemplo, si el vídeo que se va a reproducir 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 tocara la pantalla para iniciar la reproducción.
Además, si inspeccionas el tipo PreloadException, puedes definir una estrategia de reintento más inteligente. Una aplicación puede decidir quitar inmediatamente una fuente que no funcione del gestor en función del mensaje de error o del código de estado HTTP. El elemento se tendría que quitar del flujo de la interfaz de usuario para que los problemas de carga no afecten a la experiencia del usuario. También puedes obtener datos más detallados de PreloadException, como HttpDataSourceException, para investigar más a fondo los errores. Consulta más información sobre la solución de problemas de ExoPlayer.
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 funcionan con componentes independientes y descoordinados, podría afectar a la seguridad de los hilos y a 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 independientes 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 la gestión adecuada de la eliminación. El orden recomendado para la eliminación es liberar primero PreloadManager y, después, ExoPlayer.
DefaultPreloadManager.Builder se ha diseñado para facilitar este uso compartido y tiene APIs para instanciar tanto tu PreloadManager como 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.
Evitar conflictos de ancho de banda con un BandwidthMeter compartido
BandwidthMeter proporciona una estimación del ancho de banda de red disponible basada en las tasas de transferencia históricas. Si PreloadManager y el reproductor usan instancias independientes, no tienen conocimiento de la actividad de red del otro, lo que puede provocar errores. Por ejemplo, supongamos que un usuario está viendo un vídeo, su conexión de red se degrada y el MediaSource de precarga inicia simultáneamente una descarga agresiva de un vídeo futuro. La actividad de MediaSource precargado consumiría el ancho de banda que necesita el reproductor activo, lo que provocaría que el vídeo actual se detuviera. Un bloqueo durante la reproducción es un fallo importante en la experiencia de usuario.
Al compartir un solo BandwidthMeter, TrackSelector puede seleccionar las pistas de mayor calidad en función de las condiciones de la red y del estado del búfer durante la precarga o la reproducción. Después, puede tomar decisiones inteligentes para proteger la sesión de reproducción activa y garantizar una experiencia fluida.
preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)
Asegurar la coherencia con los componentes LoadControl, TrackSelector y Renderer compartidos de ExoPlayer
- LoadControl: este componente determina 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 se debe iniciar o detener la carga de más datos. Compartir LoadControl asegura que el consumo de memoria del reproductor y de PreloadManager se rija por una única estrategia de almacenamiento en búfer coordinada tanto para el contenido multimedia precargado como para el que se está reproduciendo, lo que evita la contención de recursos. Para garantizar la coherencia, tendrás que asignar de forma inteligente el tamaño del búfer en función del número de elementos que estés precargando y de su duración. En caso de conflicto, el reproductor dará prioridad a la reproducción del elemento que se muestre en la pantalla. Con un LoadControl compartido, el gestor de precarga seguirá precargando mientras no se haya alcanzado el límite superior de los bytes de búfer de destino asignados a la precarga. No esperará a 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) asegura que su Allocator se pueda compartir correctamente con PreloadManager y el reproductor. Usar LoadControl para controlar de forma eficaz la precarga 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, vídeo con una resolución determinada o audio en un idioma específico) se deben cargar y reproducir. Al compartir, te aseguras de que las pistas seleccionadas durante la precarga sean las mismas que usará el reproductor. De esta forma, se evita que se precargue una pista de vídeo de 480p y que el reproductor la descarte inmediatamente para obtener una de 720p al reproducir el vídeo.< br /> El gestor de precarga NO debe compartir la misma instancia de TrackSelector con el reproductor. En su lugar, deben usar una instancia de TrackSelector diferente, pero con la misma implementación. Por eso, definimos TrackSelectorFactory en lugar de TrackSelector en DefaultPreloadManager.Builder.
preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)
- Renderizador: este componente se encarga de conocer las funciones del reproductor sin crear los renderizadores completos. Comprueba este plano para ver qué formatos de vídeo, audio y texto admitirá el reproductor final. De esta forma, puede seleccionar y descargar de forma inteligente solo la pista multimedia compatible, lo que evita que se malgaste ancho de banda en contenido que el reproductor no puede reproducir.
preloadManagerBuilder.setRenderersFactory(customRenderersFactory)
Consulta más información sobre los componentes de ExoPlayer.
La regla de oro: un Playback Looper común para controlarlos a todos
El hilo en el que se puede acceder a una instancia de ExoPlayer se puede especificar explícitamente pasando un Looper al crear el reproductor. El Looper del hilo desde el que se debe acceder al reproductor se puede consultar mediante Player.getApplicationLooper. Al mantener un Looper compartido entre el reproductor y PreloadManager, se garantiza que todas las operaciones de estos objetos multimedia compartidos se serialicen en la cola de mensajes de un solo hilo. Esto puede reducir los errores de simultaneidad.
Todas las interacciones entre PreloadManager y el reproductor con las fuentes multimedia que se van a cargar o precargar deben producirse en el mismo hilo de reproducción. Compartir el Looper es imprescindible para la seguridad de los hilos, por lo que debemos compartir el PlaybackLooper entre PreloadManager y el reproductor.
PreloadManager prepara un objeto MediaSource con estado en segundo plano. Cuando el código de tu interfaz de usuario llama a player.setMediaSource(mediaSource), estás transfiriendo este objeto complejo con estado de MediaSource precargado al reproductor. En este caso, todo el PreloadMediaSource se mueve del gestor al reproductor. Todas estas interacciones y transferencias deben producirse en el mismo PlaybackLooper.
Si PreloadManager y ExoPlayer funcionaran en diferentes subprocesos, podría producirse una condición de carrera. El hilo de PreloadManager podría modificar el estado interno de MediaSource (por ejemplo, escribir datos nuevos en un búfer) en el momento exacto en que el hilo del reproductor intenta leerlo. Esto provoca 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 propia 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 de ExoPlayer, como DefaultLoadControl, etc., no es necesario que los compartas explícitamente con DefaultPreloadManager. Cuando creas 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 las configuraciones predeterminadas. Sin embargo, si usas componentes o configuraciones personalizadas, debes notificarlo explícitamente a DefaultPreloadManager a través de las APIs anteriores.
Precarga lista para producción: el patrón de ventana deslizante
En un feed dinámico, los usuarios pueden desplazarse por una cantidad de contenido prácticamente infinita. Si añades vídeos continuamente a DefaultPreloadManager sin una estrategia de eliminación correspondiente, inevitablemente se producirá un error OutOfMemoryError. Cada MediaSource precargado contiene un SampleQueue, que asigna búferes de memoria. A medida que se acumulan, pueden agotar el espacio de montículo de la aplicación. La solución es un algoritmo que puede que ya conozcas, llamado ventana deslizante. El patrón de ventana deslizante mantiene en la memoria un conjunto pequeño y fácil de gestionar de elementos que están lógicamente adyacentes a la posición actual del usuario en el feed. A medida que el usuario se desplaza, esta "ventana" de elementos gestionados se mueve con él, añadiendo nuevos elementos que aparecen y quitando los que se alejan.
Implementar el patrón de ventana deslizante
Es fundamental saber que PreloadManager no proporciona un método setWindowSize() integrado. La ventana deslizante es un patrón de diseño que debes implementar como desarrollador mediante los métodos primitivos add() y remove(). La lógica de su aplicación debe conectar los eventos de la interfaz de usuario, como un desplazamiento o un cambio de página, con estas llamadas a la API. Si quieres una referencia de código para esto, hemos implementado este patrón de ventana deslizante en el ejemplo de socialite, que también incluye un PreloadManagerWrapper que imita una ventana deslizante.
No olvides añadir preloadManager.remove(mediaItem) en tu implementación cuando sea poco 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 carga previa. La llamada remove() asegura que se liberen los recursos que te ayudan a mantener el uso de memoria de tu aplicación limitado y estable.
Ajustar una estrategia de precarga categorizada con TargetPreloadStatusControl
Ahora que hemos definido qué precargar (los elementos de nuestra ventana), podemos aplicar una estrategia bien definida sobre cuánto precargar de cada elemento. Ya hemos visto cómo conseguir esta granularidad 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, memoria) a los elementos que el usuario tenga más probabilidades de ver a continuación. De esta forma, se crea una estrategia de precarga basada en la proximidad, que es la clave para equilibrar la reproducción inmediata con un uso eficiente de los recursos.
Puede usar los datos analíticos a través de PreloadManagerListener, como se ha explicado en las secciones anteriores, para decidir su estrategia de duración de la precarga.
Conclusión y próximos pasos
Ahora tienes los conocimientos avanzados necesarios para crear feeds multimedia rápidos, estables y eficientes en cuanto a recursos con DefaultPreloadManager de Media3.
Vamos a repasar los puntos clave:
- Usa PreloadManagerListener para obtener estadísticas y aplicar una gestión de errores sólida.
- Usa siempre un solo DefaultPreloadManager.Builder para crear tanto las instancias del gestor como las del reproductor. De esta forma, te asegurarás de que se compartan los componentes importantes.
- Implementa el patrón de ventana deslizante gestionando de forma activa las llamadas add() y remove() para evitar el error OutOfMemoryError.
- Usa TargetPreloadStatusControl para crear una estrategia de precarga inteligente y por niveles que equilibre el rendimiento y el consumo de recursos.
Qué veremos en la parte 3: almacenamiento en caché con contenido multimedia precargado
Precargar datos en la memoria proporciona una mejora inmediata del rendimiento, pero puede conllevar ciertas desventajas. Una vez que se cierra la aplicación o se elimina el contenido multimedia precargado del administrador, los datos desaparecen. Para conseguir un nivel de optimización más persistente, podemos combinar la precarga con el almacenamiento en caché en disco. Esta función está en desarrollo activo y estará disponible en los próximos meses.
¿Quieres enviarnos algún comentario? Estamos deseando saber de ti.
No te pierdas las novedades y acelera la reproducción de tus vídeos. 🚀
Seguir leyendo
-
Noticias sobre productos
En las aplicaciones centradas en contenido multimedia actuales, ofrecer una experiencia de reproducción fluida e ininterrumpida es fundamental para que los usuarios disfruten de la aplicación. Los usuarios esperan que sus vídeos empiecen a reproducirse al instante y que lo hagan de forma fluida, sin pausas.
Mayuri Khinvasara Khabya • Lectura de 8 minutos
-
Noticias sobre productos
Nos complace anunciar importantes actualizaciones de nuestros recursos de diseño, que te ofrecen la guía completa que necesitas para crear aplicaciones Android adaptables y de alta calidad en todos los factores de forma. Ahora tenemos una guía sobre la experiencia de escritorio y una galería de diseño de Android renovada.
Ivy Knight • Tiempo de lectura: 2 min
-
Noticias sobre productos
Se ha lanzado la primera versión alfa de Room 3.0. Room 3.0 es una versión principal de la biblioteca que introduce cambios importantes y se centra en Kotlin Multiplatform (KMP). Además, añade compatibilidad con JavaScript y WebAssembly (WASM) a la compatibilidad con Android, iOS y JVM para ordenadores.
Daniel Santiago Rivera • Tiempo de lectura: 4 min
Mantente al día
Recibe cada semana en tu bandeja de entrada las últimas novedades sobre el desarrollo para Android.