Actualités des produits

Améliorer la lecture multimédia : présentation détaillée du PreloadManager de Media3 (2e partie)

Temps de lecture : 9 min
Mayuri Khinvasara Khabya
Ingénieure en relations avec les développeurs

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 le processus de création d'expériences multimédias à faible latence et très réactives dans vos applications Android.

  • Partie 1 : Présentation du préchargement avec Media3 : les bases. 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 l'API de base : ajouter des contenus multimédias avec add(), récupérer un MediaSource préparé avec getMediaSource(), gérer les priorités avec setCurrentPlayingIndex() et invalidate(), et libérer des ressources avec remove() et release().
  • Partie 2 (cet article) : dans cet article de blog, nous explorons les fonctionnalités avancées du DefaultPreloadManager. Nous expliquons comment obtenir des insights avec PreloadManagerListener, implémenter des bonnes pratiques prêtes pour la production, comme le partage de composants principaux avec ExoPlayer, et maîtriser le modèle de fenêtre coulissante pour gérer efficacement la mémoire.
  • Partie 3 : La dernière partie de cette série portera sur l'intégration de PreloadManager à un cache 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 première partie avant de continuer. Pour ceux qui sont prêts à aller au-delà des bases, voyons comment améliorer votre implémentation de lecture multimédia.

Écoute : récupérer des analyses avec PreloadManagerListener

Lorsque vous souhaitez lancer une fonctionnalité en production, en tant que développeur d'applications, vous souhaitez également comprendre et capturer les analyses qui y sont associées. Comment pouvez-vous être sûr 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.

Le 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é une fois la requête de préchargement terminée, comme défini par votre TargetPreloadStatusControl.
  • onError(PreloadException error) : ce rappel peut être utile pour le débogage et la surveillance. Il est appelé lorsqu'un préchargement échoue, 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 telles que :

  • Quel est notre taux de réussite du 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 d'onError)
  • Quel est notre taux d'erreur de préchargement ? (rapport entre les événements onError et le 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 votre expérience utilisateur en fonction des données. Ces données peuvent également vous aider à affiner intelligemment la durée de votre 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 repli d'interface utilisateur fluide

Un préchargement ayant échoué 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épondre de manière réactive. Au lieu de simplement consigner l'erreur, vous pouvez adapter l'interface utilisateur. Par exemple, si la vidéo à venir ne parvient pas à se précharger, votre application peut désactiver la lecture automatique pour le prochain balayage, ce qui nécessite une pression de l'utilisateur 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 de l'interface utilisateur en conséquence pour éviter que les problèmes de chargement ne se propagent à l'expérience utilisateur. Vous pouvez également obtenir des données plus précises à partir de PreloadException, comme HttpDataSourceException, pour approfondir les erreurs. En savoir plus sur le dépannage de ExoPlayer.

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

Le DefaultPreloadManager et ExoPlayer sont conçus pour fonctionner ensemble. Pour garantir la stabilité et l'efficacité, ils doivent partager plusieurs composants principaux. S'ils fonctionnent avec des composants distincts et non coordonnés, cela peut avoir un impact sur la sécurité des threads et la facilité d'utilisation des pistes préchargées sur le lecteur, car nous devons nous assurer que les pistes préchargées sont lues sur le lecteur approprié. Les composants distincts peuvent également être en concurrence pour 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 la suppression appropriée. L'ordre de suppression 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 permettant d'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 la façon dont ces composants interagissent avec la lecture ExoPlayer.

preloadManager2.png

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

Le BandwidthMeter fournit une estimation de la bande passante réseau disponible en fonction des débits de transfert historiques. Si le PreloadManager et le lecteur utilisent des instances distinctes, ils ne sont pas conscients 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 le MediaSource de préchargement lance simultanément un téléchargement agressif pour une future vidéo. L'activité du MediaSource de préchargement consommerait la bande passante nécessaire au lecteur actif, ce qui entraînerait l'arrêt de la vidéo en cours. Un arrêt pendant la lecture est un échec important de 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, lors du préchargement ou de 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 dicte la stratégie de mise en mémoire tampon, par exemple la quantité de données à mettre en mémoire tampon avant de démarrer la lecture et le moment où commencer ou arrêter de charger davantage de données. Le partage de LoadControl garantit que la consommation de mémoire du lecteur et du 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 la contention des ressources. Vous devrez allouer intelligemment la taille de la mémoire tampon en coordonnant le nombre d'éléments que vous préchargez et leur durée, afin de garantir la cohérence. En cas de contention, le lecteur donne la priorité à la lecture de l'élément actuel affiché à l'écran. Avec un LoadControl partagé, le gestionnaire de préchargement continue de précharger tant que les octets de tampon cibles alloués au préchargement n'ont pas atteint la limite supérieure. Il n'attend pas la fin du chargement pour la lecture.

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 à charger et à lire (par exemple, une vidéo d'une certaine résolution, un son dans une langue spécifique). Le partage garantit que les pistes sélectionnées lors du préchargement sont les mêmes que celles utilisées par le lecteur. Cela évite un scénario inutile dans lequel une piste vidéo 480p est préchargée, mais le lecteur la supprime immédiatement et récupère 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. Au lieu de cela, ils doivent utiliser l'instance TrackSelector différente, mais de la même implémentation. C'est pourquoi nous définissons le TrackSelectorFactory plutôt qu'un TrackSelector dans le DefaultPreloadManager.Builder.

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • Renderer : ce composant est chargé de comprendre les capacités du lecteur sans créer les renderers 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 sur des contenus que le lecteur ne peut pas lire.

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

En savoir plus sur les composants Exoplayer.

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

Le thread sur lequel une instance ExoPlayer est accessible peut être spécifié explicitement 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 le PreloadManager, il est garanti 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 concurrence.

Toutes les interactions entre le PreloadManager et le lecteur avec les sources multimédias à charger ou à précharger doivent avoir lieu sur le même thread de lecture. Le partage du Looper est indispensable pour la sécurité des threads. Nous devons donc partager le PlaybackLooper entre le PreloadManager et le lecteur.

Le PreloadManager prépare un objet MediaSource avec état en arrière-plan. Lorsque votre code d'interface utilisateur appelle player.setMediaSource(mediaSource), vous effectuez un transfert de cet objet complexe avec état du MediaSource de préchargement vers le lecteur. Dans ce scénario, l'ensemble du PreloadMediaSource est déplacé du gestionnaire vers le lecteur. Toutes ces interactions et tous ces transferts doivent avoir lieu sur le même PlaybackLooper.

Si le PreloadManager et ExoPlayer fonctionnaient sur des threads différents, une condition de concurrence pourrait se produire. Le thread du PreloadManager pourrait modifier l'état interne du MediaSource (par exemple, écrire de nouvelles données dans un tampon) au moment précis où le thread du lecteur tente de le lire. Cela entraîne un comportement imprévisible, IllegalStateException, qui est 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 dans ExoPlayer, comme DefaultLoadControl, etc., vous n'avez pas besoin de les partager explicitement avec DefaultPreloadManager. Lorsque vous créez votre instance ExoPlayer via le buildExoPlayer du 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 notifier explicitement le DefaultPreloadManager à leur sujet via les API ci-dessus.

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

Dans un flux dynamique, un utilisateur peut parcourir une quantité de contenu pratiquement infinie. Si vous ajoutez continuellement des vidéos au 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 coulissante. Le modèle de fenêtre coulissante conserve un petit ensemble gérable d'éléments en mémoire qui sont logiquement adjacents à la position actuelle de l'utilisateur dans le flux. Lorsque l'utilisateur fait défiler l'écran, cette "fenêtre" d'éléments gérés glisse avec lui, ajoutant de nouveaux éléments qui apparaissent et supprimant ceux qui sont désormais éloignés.

slidingwindow.png

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

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

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. Le fait de ne pas supprimer les éléments qui ne sont plus proches de l'utilisateur est la principale cause des problèmes de mémoire dans les implémentations de préchargement. L'appel remove() garantit que les ressources sont libérées, ce qui vous permet de 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 la quantité à précharger pour chaque élément. Nous avons déjà vu comment obtenir cette granularité avec la configuration TargetPreloadStatusControl dans la partie 1.

Pour rappel, un élément en position +/- 1 peut avoir une probabilité de lecture plus élevée 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 la clé 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 prochaines étapes

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

Récapitulons les points clés à retenir :

  • Utilisez PreloadManagerListener pour recueillir des insights analytiques et implémenter une gestion des erreurs robuste.
  • Utilisez toujours un seul DefaultPreloadManager.Builder pour créer à la fois votre gestionnaire et vos instances de lecteur afin de vous assurer que les composants importants sont partagés.
  • Implémentez le modèle de fenêtre coulissante 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 à plusieurs niveaux qui équilibre les performances et la consommation de ressources.

Prochainement 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 disparaissent. Pour obtenir un niveau d'optimisation plus persistant, nous pouvons combiner le préchargement avec la mise en cache sur disque. Cette fonctionnalité est en cours de développement et sera disponible dans quelques mois.

Vous avez des commentaires à partager ? Nous avons hâte de vous lire.

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

Écrit par :

Lire la suite