Actualités sur les produits

Améliorer la lecture de contenus multimédias : présentation détaillée de PreloadManager de Media3 – Partie 2

Temps de lecture : 9 min
Voir le profil de Mayuri Khinvasara Khabya
Mayuri Khinvasara Khabya Ingénieur Developer Relations

Bienvenue dans le deuxième volet de notre série en trois parties sur le préchargement de contenus multimédias avec Media3. Cette série est conçue pour vous guider dans la création d'expériences multimédias très réactives et à faible latence dans vos applications Android.

  • La partie 1 : Présentation du préchargement avec Media3 couvrait les principes de base. Nous avons exploré la distinction entre PreloadConfiguration pour les playlists simples et le DefaultPreloadManager, plus puissant, pour les interfaces utilisateur dynamiques. Vous avez appris à implémenter le cycle de vie de base de l'API : ajouter des contenus multimédias avec add(), récupérer une MediaSource préparée avec getMediaSource(), gérer les priorités avec setCurrentPlayingIndex() et invalidate(), et libérer des ressources avec remove() et release().
  • Partie 2 (cet article) : nous explorons les fonctionnalités avancées de DefaultPreloadManager. Nous verrons comment obtenir des insights avec PreloadManagerListener, implémenter des bonnes pratiques prêtes pour la production, comme le partage de composants de base avec ExoPlayer, et maîtriser le modèle de fenêtre glissante pour gérer efficacement la mémoire.
  • Partie 3 : La dernière partie de cette série abordera l'intégration de PreloadManager à un cache de disque persistant, ce qui vous permettra de réduire la consommation de données grâce à la gestion des ressources et d'offrir une expérience fluide.

Si vous débutez avec le préchargement dans Media3, nous vous recommandons vivement de lire la partie 1 avant de continuer. Si vous êtes prêt à aller au-delà des principes de base, découvrons comment améliorer l'implémentation de la lecture multimédia.

Écoute : récupérer des données analytiques avec PreloadManagerListener

Lorsque vous souhaitez lancer une fonctionnalité en production, en tant que développeur d'applications, vous voulez également comprendre et capturer les données analytiques qui s'y rapportent. Comment pouvez-vous être certain que votre stratégie de préchargement est efficace dans un environnement réel ? Pour répondre à cette question, vous avez besoin de données sur les taux de réussite, les échecs et les performances. L'interface PreloadManagerListener est le principal mécanisme de collecte de ces données.

PreloadManagerListener fournit deux rappels essentiels qui offrent des insights essentiels sur le processus et l'état du préchargement.

  • onCompleted(MediaItem mediaItem) : ce rappel est appelé lorsque la requête de préchargement aboutit, comme défini par votre TargetPreloadStatusControl.
  • onError(PreloadException error) : ce rappel peut être utile pour le débogage et la surveillance. Elle est appelée lorsqu'un préchargement échoue, en fournissant l'exception associée.

Vous pouvez enregistrer un écouteur avec un seul appel de méthode, comme illustré dans l'exemple de code suivant :

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)

Extraire des insights de l'écouteur 

Ces rappels d'écouteur peuvent être associés à votre pipeline d'analyse. En transférant ces événements à votre moteur d'analyse, vous pouvez répondre à des questions clés, par exemple :

  • Quel est notre taux de réussite de préchargement ? (rapport entre les événements onCompleted et le nombre total de tentatives de préchargement)
  • Quels CDN ou formats vidéo présentent les taux d'erreur les plus élevés ? (en analysant les exceptions de onError)
  • Quel est notre taux d'erreur de préchargement ? (ratio d'événements onError par rapport au nombre total de tentatives de préchargement)

Ces données peuvent vous fournir des commentaires quantitatifs sur votre stratégie de préchargement, ce qui vous permet d'effectuer des tests A/B et d'améliorer l'expérience utilisateur en fonction des données. Ces données peuvent également vous aider à ajuster intelligemment la durée de préchargement et le nombre de vidéos que vous souhaitez précharger, ainsi que les tampons que vous allouez.

Au-delà du débogage : utiliser onError pour un remplacement d'UI fluide

Un échec de préchargement est un indicateur fort d'un événement de mise en mémoire tampon à venir pour l'utilisateur. Le rappel onError vous permet de réagir de manière réactive. Au lieu de simplement consigner l'erreur, vous pouvez adapter l'UI. Par exemple, si le préchargement de la vidéo suivante échoue, votre application peut désactiver la lecture automatique pour le balayage suivant, ce qui nécessite que l'utilisateur appuie pour lancer la lecture.

De plus, en inspectant le type PreloadException, vous pouvez définir une stratégie de nouvelle tentative plus intelligente. Une application peut choisir de supprimer immédiatement une source défaillante du gestionnaire en fonction du message d'erreur ou du code d'état HTTP. L'élément doit être supprimé du flux d'UI en conséquence pour éviter que les problèmes de chargement n'affectent l'expérience utilisateur. Vous pouvez également obtenir des données plus précises à partir de PreloadException, comme HttpDataSourceException, pour examiner plus en détail les erreurs. En savoir plus sur la résolution des problèmes liés à ExoPlayer

Le système de binôme : pourquoi est-il nécessaire de partager des composants avec ExoPlayer ?

DefaultPreloadManager et ExoPlayer sont conçus pour fonctionner ensemble. Pour garantir la stabilité et l'efficacité, ils doivent partager plusieurs composants de base. S'ils fonctionnent avec des composants distincts et non coordonnés, cela peut avoir un impact sur la sécurité des threads et l'utilisation des titres préchargés sur le lecteur, car nous devons nous assurer que les titres préchargés sont lus sur le bon lecteur. Les composants distincts peuvent également se disputer des ressources limitées, telles que la bande passante réseau et la mémoire, ce qui peut entraîner une dégradation des performances. Une partie importante du cycle de vie consiste à gérer l'élimination appropriée. L'ordre d'élimination recommandé consiste à libérer d'abord le PreloadManager, puis l'ExoPlayer.

Le DefaultPreloadManager.Builder est conçu pour faciliter ce partage et dispose d'API pour instancier à la fois votre PreloadManager et une instance de lecteur associée. Voyons pourquoi des composants tels que BandwidthMeter, LoadControl, TrackSelector et Looper doivent être partagés. Consultez la représentation visuelle de l'interaction de ces composants avec la lecture ExoPlayer.

preloadManager2.png

Éviter les conflits de bande passante avec un BandwidthMeter partagé

BandwidthMeter fournit une estimation de la bande passante réseau disponible en fonction des taux de transfert historiques. Si PreloadManager et le lecteur utilisent des instances distinctes, ils ne sont pas au courant de l'activité réseau de l'autre, ce qui peut entraîner des scénarios d'échec. Prenons l'exemple d'un utilisateur qui regarde une vidéo, dont la connexion réseau se dégrade et dont la MediaSource de préchargement lance simultanément un téléchargement agressif pour une vidéo future. L'activité de préchargement de MediaSource consommerait la bande passante nécessaire au lecteur actif, ce qui entraînerait l'arrêt de la vidéo en cours. Un blocage pendant la lecture est un problème majeur pour l'expérience utilisateur.

En partageant un seul BandwidthMeter, le TrackSelector peut sélectionner les pistes de la plus haute qualité en fonction des conditions réseau actuelles et de l'état du tampon, pendant le préchargement ou la lecture. Il peut ensuite prendre des décisions intelligentes pour protéger la session de lecture active et garantir une expérience fluide.

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

Assurer la cohérence avec les composants LoadControl, TrackSelector et Renderer partagés d'ExoPlayer

  • LoadControl : ce composant détermine la règle de mise en mémoire tampon, par exemple la quantité de données à mettre en mémoire tampon avant de lancer la lecture, et le moment où il faut commencer ou arrêter de charger des données supplémentaires. Le partage de LoadControl garantit que la consommation de mémoire du lecteur et de PreloadManager est guidée par une stratégie de mise en mémoire tampon unique et coordonnée pour les contenus multimédias préchargés et en cours de lecture, ce qui évite les conflits de ressources. Vous devrez allouer intelligemment la taille de la mémoire tampon en fonction du nombre d'éléments que vous préchargez et de leur durée, afin d'assurer la cohérence. En cas de conflit, le lecteur donne la priorité à la lecture de l'élément actuellement affiché à l'écran. Avec un LoadControl partagé, le gestionnaire de préchargement continue de précharger tant que les octets de la mémoire tampon cible alloués au préchargement n'ont pas atteint la limite supérieure. Il n'attend pas que le chargement pour la lecture soit terminé.

Remarque : Le partage de LoadControl dans la dernière version de Media3 (1.8) garantit que son Allocator peut être partagé correctement avec PreloadManager et le lecteur. L'utilisation de LoadControl pour contrôler efficacement le préchargement est une fonctionnalité qui sera disponible dans la prochaine version de Media3 1.9.

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector : ce composant est chargé de sélectionner les pistes (par exemple, une vidéo d'une certaine résolution, un contenu audio dans une langue spécifique) à charger et à lire. Le partage garantit que les pistes sélectionnées lors du préchargement sont les mêmes que celles que le lecteur utilisera. Cela évite de précharger inutilement une piste vidéo 480p, que le lecteur rejettera immédiatement pour récupérer une piste 720p lors de la lecture.< br /> Le gestionnaire de préchargement ne doit PAS partager la même instance de TrackSelector avec le lecteur. Ils doivent plutôt utiliser une autre instance TrackSelector, mais de la même implémentation. C'est pourquoi nous définissons TrackSelectorFactory plutôt que TrackSelector dans DefaultPreloadManager.Builder.

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • Renderer : ce composant est chargé de comprendre les capacités du lecteur sans créer les moteurs de rendu complets. Il vérifie ce plan pour voir quels formats vidéo, audio et texte le lecteur final prendra en charge. Cela lui permet de sélectionner et de télécharger intelligemment uniquement la piste multimédia compatible, et d'éviter de gaspiller de la bande passante pour du contenu que le lecteur ne peut pas lire.

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

En savoir plus sur les composants ExoPlayer

La règle d'or : un Playback Looper commun pour tous les contrôler

Le thread sur lequel une instance ExoPlayer est accessible peut être explicitement spécifié en transmettant un Looper lors de la création du lecteur. Le Looper du thread à partir duquel le lecteur doit être accessible peut être interrogé à l'aide de Player.getApplicationLooper. En conservant un Looper partagé entre le lecteur et PreloadManager, vous avez la garantie que toutes les opérations sur ces objets multimédias partagés sont sérialisées dans la file d'attente de messages d'un seul thread. Cela peut réduire les bugs de simultanéité.

Toutes les interactions entre PreloadManager et le lecteur avec les sources multimédias à charger ou à précharger doivent se produire sur le même thread de lecture. Le partage de Looper est indispensable pour la thread safety. Nous devons donc partager PlaybackLooper entre PreloadManager et le lecteur.

PreloadManager prépare un objet MediaSource avec état en arrière-plan. Lorsque votre code d'interface utilisateur appelle player.setMediaSource(mediaSource), vous transférez cet objet complexe avec état de la MediaSource de préchargement au lecteur. Dans ce scénario, l'intégralité de PreloadMediaSource est déplacée du gestionnaire vers le lecteur. Toutes ces interactions et transferts doivent se produire sur le même PlaybackLooper.

Si PreloadManager et ExoPlayer fonctionnaient sur des threads différents, une condition de concurrence pourrait se produire. Le thread de PreloadManager peut modifier l'état interne de MediaSource (par exemple, en écrivant de nouvelles données dans un tampon) au moment exact où le thread du lecteur tente de le lire. Cela entraîne un comportement imprévisible, une exception IllegalStateException difficile à déboguer.

preloadManagerBuilder.setPreloadLooper(playbackLooper)

Voyons comment partager tous les composants ci-dessus entre ExoPlayer et DefaultPreloadManager dans la configuration elle-même.

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()

Conseil : Si vous utilisez les composants par défaut d'ExoPlayer, comme DefaultLoadControl, etc., vous n'avez pas besoin de les partager explicitement avec DefaultPreloadManager. Lorsque vous créez votre instance ExoPlayer via buildExoPlayer de DefaultPreloadManager.Builder, ces composants sont automatiquement référencés les uns avec les autres, si vous utilisez les implémentations par défaut avec les configurations par défaut. Toutefois, si vous utilisez des composants ou des configurations personnalisés, vous devez en informer explicitement le DefaultPreloadManager via les API ci-dessus.

Préchargement prêt pour la production : le modèle de fenêtre glissante

Dans un flux dynamique, un utilisateur peut faire défiler une quantité de contenu pratiquement infinie. Si vous ajoutez continuellement des vidéos à DefaultPreloadManager sans stratégie de suppression correspondante, vous provoquerez inévitablement une erreur OutOfMemoryError. Chaque MediaSource préchargé conserve une SampleQueue, qui alloue des tampons de mémoire. À mesure qu'ils s'accumulent, ils peuvent épuiser l'espace de tas de l'application. La solution est un algorithme que vous connaissez peut-être déjà, appelé "fenêtre glissante". Le modèle de fenêtre glissante conserve en mémoire un petit ensemble d'éléments gérables qui sont logiquement adjacents à la position actuelle de l'utilisateur dans le flux. À mesure que l'utilisateur fait défiler l'écran, cette "fenêtre" d'éléments gérés glisse avec lui, ajoutant les nouveaux éléments qui apparaissent et supprimant ceux qui sont désormais éloignés.

slidingwindow.png

Implémenter le modèle de fenêtre glissante

Il est essentiel de comprendre que PreloadManager ne fournit pas de méthode setWindowSize() intégrée. La fenêtre glissante est un modèle de conception que vous, le développeur, devez implémenter à l'aide des méthodes primitives add() et remove(). La logique de votre application doit associer les événements d'interface utilisateur, tels qu'un défilement ou un changement de page, à ces appels d'API. Si vous souhaitez obtenir une référence de code pour cela, nous avons implémenté ce modèle de fenêtre glissante dans l'exemple socialite, qui inclut également un PreloadManagerWrapper qui imite une fenêtre glissante.

N'oubliez pas d'ajouter preloadManager.remove(mediaItem) dans votre implémentation lorsque l'élément n'est plus susceptible d'apparaître prochainement dans la vue de l'utilisateur. La principale cause des problèmes de mémoire dans les implémentations de préchargement est l'absence de suppression des éléments qui ne sont plus à proximité de l'utilisateur. L'appel remove() garantit que les ressources sont libérées, ce qui vous aide à maintenir l'utilisation de la mémoire de votre application limitée et stable.

Ajuster une stratégie de préchargement catégorisée avec TargetPreloadStatusControl

Maintenant que nous avons défini ce qu'il faut précharger (les éléments de notre fenêtre), nous pouvons appliquer une stratégie bien définie pour déterminer la quantité à précharger pour chaque élément. Nous avons déjà vu comment atteindre ce niveau de précision avec la configuration de TargetPreloadStatusControl dans la partie 1.

Pour rappel, un élément en position +/- 1 peut avoir une probabilité plus élevée d'être lu qu'un élément en position +/- 4. Vous pouvez allouer plus de ressources (réseau, processeur, mémoire) aux éléments que l'utilisateur est le plus susceptible de consulter ensuite. Cela crée une stratégie de "préchargement" basée sur la proximité, qui est essentielle pour équilibrer la lecture immédiate et l'utilisation efficace des ressources.

Vous pouvez utiliser les données d'analyse via PreloadManagerListener, comme indiqué dans les sections précédentes, pour définir votre stratégie de durée de préchargement.

Conclusion et étapes suivantes

Vous disposez désormais des connaissances avancées nécessaires pour créer des flux multimédias rapides, stables et économes en ressources à l'aide de DefaultPreloadManager de Media3.

Récapitulons les points clés à retenir :

  • Utilisez PreloadManagerListener pour recueillir des insights analytiques et implémenter une gestion des exceptions robuste.
  • Utilisez toujours un seul DefaultPreloadManager.Builder pour créer vos instances de gestionnaire et de lecteur afin de vous assurer que les composants importants sont partagés.
  • Implémentez le modèle de fenêtre glissante en gérant activement les appels add() et remove() pour éviter les erreurs OutOfMemoryError.
  • Utilisez TargetPreloadStatusControl pour créer une stratégie de préchargement intelligente et hiérarchisée qui équilibre les performances et la consommation de ressources.

À venir dans la partie 3 : Mise en cache avec des contenus multimédias préchargés

Le préchargement des données en mémoire offre un avantage immédiat en termes de performances, mais il peut s'accompagner de compromis. Une fois l'application fermée ou le contenu multimédia préchargé supprimé du gestionnaire, les données sont supprimées. Pour obtenir un niveau d'optimisation plus persistant, nous pouvons combiner le préchargement avec le cache du disque. Cette fonctionnalité est en cours de développement et sera disponible dans quelques mois.

Avez-vous des commentaires à partager ? Nous avons hâte de vous lire.

Restez à l'écoute et accélérez la lecture de vos vidéos ! 🚀

Written by:
Continuer la lecture