Produktneuheiten

Optimierte Medienwiedergabe: PreloadManager von Media3 – Teil 2

Lesezeit: 9 Minuten
Mayuri Khinvasara Khabya
Developer Relations Engineer

Willkommen zum zweiten Teil unserer dreiteiligen Serie zum Media3-Preloading. In dieser Reihe erfahren Sie, wie Sie in Ihren Android-Apps reaktionsschnelle Medien mit geringer Latenz entwickeln.

  • Teil 1: Einführung in das Vorabladen mit Media3 behandelte die Grundlagen. Wir haben uns den Unterschied zwischen PreloadConfiguration für einfache Playlists und dem leistungsstärkeren DefaultPreloadManager für dynamische Benutzeroberflächen angesehen. Sie haben gelernt, wie Sie den grundlegenden API-Lebenszyklus implementieren: Medien mit add() hinzufügen, eine vorbereitete MediaSource mit getMediaSource() abrufen, Prioritäten mit setCurrentPlayingIndex() und invalidate() verwalten und Ressourcen mit remove() und release() freigeben.
  • Teil 2 (dieser Beitrag): In diesem Blogbeitrag werden die erweiterten Funktionen von DefaultPreloadManager erläutert. Wir zeigen, wie Sie mit PreloadManagerListener Statistiken erhalten, produktionsreife Best Practices wie das Teilen von Kernkomponenten mit ExoPlayer implementieren und das Sliding-Window-Muster verwenden, um den Arbeitsspeicher effektiv zu verwalten.
  • Teil 3: Im letzten Teil dieser Reihe geht es um die Integration von PreloadManager in einen persistenten Festplatten-Cache. So können Sie den Datenverbrauch durch Ressourcenverwaltung reduzieren und eine nahtlose Nutzererfahrung bieten.

Wenn Sie noch nicht mit dem Preloading in Media3 vertraut sind, empfehlen wir Ihnen, zuerst Teil 1 zu lesen. Wenn Sie bereit sind, über die Grundlagen hinauszugehen, sehen wir uns an, wie Sie Ihre Implementierung der Medienwiedergabe optimieren können.

Zuhören: Analysen mit PreloadManagerListener abrufen

Wenn Sie als App-Entwickler eine Funktion in der Produktion einführen möchten, möchten Sie auch die entsprechenden Analysen nachvollziehen und erfassen. Wie können Sie sicher sein, dass Ihre Vorabladestrategie in einer realen Umgebung effektiv ist? Dazu sind Daten zu Erfolgsraten, Fehlern und Leistung erforderlich. Die Schnittstelle PreloadManagerListener ist der primäre Mechanismus zum Erfassen dieser Daten.

Der PreloadManagerListener bietet zwei wichtige Callbacks, die wichtige Informationen zum Vorabladeprozess und ‑status liefern.

  • onCompleted(MediaItem mediaItem): Dieser Callback wird aufgerufen, wenn eine Preload-Anfrage erfolgreich abgeschlossen wurde, wie durch TargetPreloadStatusControl definiert.
  • onError(PreloadException error): Dieser Callback kann für das Debugging und Monitoring nützlich sein. Sie wird aufgerufen, wenn ein Preload fehlschlägt, und stellt die zugehörige Ausnahme bereit.

Sie können einen Listener mit einem einzelnen Methodenaufruf registrieren, wie im folgenden Beispielcode gezeigt:

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)

Informationen aus dem Listener extrahieren 

Diese Listener-Callbacks können in Ihre Analysepipeline eingebunden werden. Wenn Sie diese Ereignisse an Ihre Analyse-Engine weiterleiten, können Sie wichtige Fragen wie die folgenden beantworten:

  • Wie hoch ist unsere Erfolgsrate bei Preloads? (Verhältnis von „onCompleted“-Ereignissen zu allen Preload-Versuchen)
  • Welche CDNs oder Videoformate weisen die höchsten Fehlerraten auf? (Durch Parsen der Ausnahmen aus „onError“)
  • Wie hoch ist unsere Vorabladungsfehlerrate? (Verhältnis von onError-Ereignissen zu allen Vorabladen-Versuchen)

Anhand dieser Daten erhalten Sie quantitatives Feedback zu Ihrer Vorabinstallationsstrategie. So können Sie A/B-Tests durchführen und die Nutzerfreundlichkeit datengestützt verbessern. Anhand dieser Daten können Sie die Dauer des intelligenten Vorabladens, die Anzahl der Videos, die Sie vorab laden möchten, sowie die von Ihnen zugewiesenen Puffer weiter optimieren.

Über das Debugging hinaus: onError für einen reibungslosen UI-Fallback verwenden

Ein fehlgeschlagener Preload ist ein starker Indikator für ein bevorstehendes Pufferungsereignis für den Nutzer. Mit dem onError-Callback können Sie reaktiv reagieren. Anstatt den Fehler nur zu protokollieren, können Sie die Benutzeroberfläche anpassen. Wenn das nächste Video beispielsweise nicht vorab geladen werden kann, könnte Ihre Anwendung die automatische Wiedergabe für den nächsten Wischvorgang deaktivieren, sodass die Wiedergabe erst durch Tippen des Nutzers gestartet wird.

Außerdem können Sie durch Untersuchen des Typs PreloadException eine intelligentere Wiederholungsstrategie definieren. Eine App kann eine fehlerhafte Quelle basierend auf der Fehlermeldung oder dem HTTP-Statuscode sofort aus dem Manager entfernen. Das Element muss entsprechend aus dem UI-Stream entfernt werden, damit keine Ladeprobleme auftreten. Sie können auch detailliertere Daten aus PreloadException abrufen, z. B. die HttpDataSourceException, um die Fehler genauer zu untersuchen. Weitere Informationen zur Fehlerbehebung bei ExoPlayer

Das Buddy-System: Warum ist es notwendig, Komponenten mit ExoPlayer zu teilen?

Der DefaultPreloadManager und ExoPlayer sind so konzipiert, dass sie zusammenarbeiten. Um Stabilität und Effizienz zu gewährleisten, müssen sie mehrere Kernkomponenten gemeinsam nutzen. Wenn sie mit separaten, nicht koordinierten Komponenten arbeiten, kann dies die Threadsicherheit und die Nutzbarkeit vorab geladener Tracks auf dem Player beeinträchtigen, da wir sicherstellen müssen, dass vorab geladene Tracks auf dem richtigen Player wiedergegeben werden. Die einzelnen Komponenten können auch um begrenzte Ressourcen wie Netzwerkbandbreite und Arbeitsspeicher konkurrieren, was zu Leistungseinbußen führen kann. Ein wichtiger Teil des Lebenszyklus ist die ordnungsgemäße Entsorgung. Die empfohlene Reihenfolge ist, zuerst den PreloadManager und dann den ExoPlayer freizugeben.

Der DefaultPreloadManager.Builder wurde entwickelt, um diese gemeinsame Nutzung zu erleichtern. Er enthält APIs zum Instanziieren sowohl Ihres PreloadManager als auch einer verknüpften Player-Instanz. Sehen wir uns an, warum Komponenten wie BandwidthMeter, LoadControl, TrackSelector und Looper gemeinsam genutzt werden müssen. Visuelle Darstellung der Interaktion dieser Komponenten mit der ExoPlayer-Wiedergabe

preloadManager2.png

Bandbreitenkonflikte mit einem gemeinsam genutzten BandwidthMeter verhindern

BandwidthMeter schätzt die verfügbare Netzwerkbandbreite anhand der bisherigen Übertragungsraten. Wenn der PreloadManager und der Player separate Instanzen verwenden, wissen sie nichts voneinander. Das kann zu Fehlerszenarien führen. Stellen Sie sich beispielsweise vor, ein Nutzer sieht sich ein Video an, seine Netzwerkverbindung verschlechtert sich und die MediaSource für das Preloading startet gleichzeitig einen aggressiven Download für ein zukünftiges Video. Die Aktivität der vorab geladenen MediaSource würde Bandbreite verbrauchen, die für den aktiven Player benötigt wird, was dazu führen würde, dass das aktuelle Video angehalten wird. Ein Pufferungsvorgang während der Wiedergabe ist ein schwerwiegender Fehler in der Nutzerfreundlichkeit.

Durch die gemeinsame Nutzung eines einzelnen BandwidthMeter kann der TrackSelector während des Vorabladens oder der Wiedergabe anhand der aktuellen Netzwerkbedingungen und des Pufferstatus die Tracks mit der höchsten Qualität auswählen. Das System kann dann intelligente Entscheidungen treffen, um die aktive Wiedergabesitzung zu schützen und für eine reibungslose Nutzung zu sorgen.

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

Konsistenz mit den gemeinsam genutzten LoadControl-, TrackSelector- und Renderer-Komponenten von ExoPlayer sicherstellen

  • LoadControl: Diese Komponente bestimmt die Pufferungsrichtlinie, z. B. wie viele Daten vor dem Start der Wiedergabe gepuffert werden sollen und wann mit dem Laden weiterer Daten begonnen oder beendet werden soll. Durch die gemeinsame Nutzung von LoadControl wird sichergestellt, dass der Speicherverbrauch von Player und PreloadManager durch eine einzige, koordinierte Pufferungsstrategie für sowohl vorab geladene als auch aktiv wiedergegebene Medien gesteuert wird. So wird Ressourcenkonflikten vorgebeugt. Sie müssen die Puffergröße sorgfältig festlegen und dabei berücksichtigen, wie viele Elemente Sie mit welcher Dauer vorab laden, um die Konsistenz zu gewährleisten. In solchen Fällen wird die Wiedergabe des aktuellen Elements auf dem Bildschirm priorisiert. Bei einem gemeinsam genutzten LoadControl wird das Vorabladen durch den Preload-Manager fortgesetzt, solange die für das Vorabladen zugewiesenen Zielpuffer-Bytes nicht das obere Limit erreicht haben. Es wird nicht gewartet, bis das Laden für die Wiedergabe abgeschlossen ist.

Hinweis: Durch die Freigabe von LoadControl in der aktuellen Version von Media3 (1.8) wird dafür gesorgt, dass der Allocator korrekt für PreloadManager und Player freigegeben werden kann. Die Verwendung von LoadControl zur effektiven Steuerung des Preloadings ist eine Funktion, die in der kommenden Media3-Version 1.9 verfügbar sein wird.

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector: Diese Komponente ist dafür verantwortlich, welche Tracks (z. B. Video mit einer bestimmten Auflösung, Audio in einer bestimmten Sprache) geladen und wiedergegeben werden. Durch die Freigabe wird sichergestellt, dass die beim Vorabladen ausgewählten Tracks auch vom Player verwendet werden. So wird vermieden, dass ein 480p-Videotrack vorab geladen wird, nur damit der Player ihn sofort verwirft und bei der Wiedergabe einen 720p-Track abruft.< br /> Der Preload-Manager sollte NICHT dieselbe Instanz von TrackSelector wie der Player verwenden. Stattdessen sollten sie die unterschiedliche TrackSelector-Instanz, aber mit derselben Implementierung verwenden. Deshalb legen wir die TrackSelectorFactory anstelle eines TrackSelector im DefaultPreloadManager.Builder fest.

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • Renderer: Diese Komponente ist dafür verantwortlich, die Funktionen des Players zu ermitteln, ohne die vollständigen Renderer zu erstellen. Anhand dieses Blueprints wird geprüft, welche Video-, Audio- und Textformate der endgültige Player unterstützt. So kann nur der kompatible Medientrack ausgewählt und heruntergeladen werden. Bandbreite wird nicht für Inhalte verschwendet, die der Player nicht wiedergeben kann.

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

Weitere Informationen zu ExoPlayer-Komponenten

Die goldene Regel: Ein gemeinsamer Playback Looper für alle

Der Thread, über den auf eine ExoPlayer-Instanz zugegriffen werden kann, kann explizit angegeben werden, indem beim Erstellen des Players ein Looper übergeben wird. Der Looper des Threads, über den auf den Player zugegriffen werden muss, kann mit Player.getApplicationLooper abgefragt werden. Durch die Verwendung eines gemeinsamen Loopers zwischen dem Player und PreloadManager wird sichergestellt, dass alle Vorgänge für diese gemeinsam genutzten Media-Objekte in der Message Queue eines einzelnen Threads serialisiert werden. Dadurch können Fehler bei der Gleichzeitigkeit reduziert werden.

Alle Interaktionen zwischen dem PreloadManager und dem Player mit zu ladenden oder vorab geladenen Media-Quellen müssen im selben Wiedergabethread erfolgen. Die Weitergabe des Looper ist für die Threadsicherheit unerlässlich. Daher muss der PlaybackLooper zwischen dem PreloadManager und dem Player weitergegeben werden.

Der PreloadManager bereitet im Hintergrund ein zustandsbehaftetes MediaSource-Objekt vor. Wenn in Ihrem UI-Code „player.setMediaSource(mediaSource)“ aufgerufen wird, übergeben Sie dieses komplexe, zustandsbehaftete Objekt von der vorab geladenen MediaSource an den Player. In diesem Szenario wird die gesamte PreloadMediaSource vom Manager zum Player verschoben. Alle diese Interaktionen und Übergaben sollten auf demselben PlaybackLooper erfolgen.

Wenn PreloadManager und ExoPlayer auf unterschiedlichen Threads ausgeführt wurden, konnte es zu einer Race-Bedingung kommen. Der Thread des PreloadManager könnte den internen Status der MediaSource ändern (z. B. neue Daten in einen Puffer schreiben), genau in dem Moment, in dem der Thread des Players versucht, daraus zu lesen. Dies führt zu unvorhersehbarem Verhalten und zu einer schwer zu debuggenden IllegalStateException.

preloadManagerBuilder.setPreloadLooper(playbackLooper)

Sehen wir uns an, wie Sie alle oben genannten Komponenten zwischen ExoPlayer und DefaultPreloadManager in der Einrichtung selbst freigeben können.

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

Tipp: Wenn Sie die Standardkomponenten in ExoPlayer wie DefaultLoadControl usw. verwenden, müssen Sie sie nicht explizit für DefaultPreloadManager freigeben. Wenn Sie Ihre ExoPlayer-Instanz über die buildExoPlayer des DefaultPreloadManager.Builder erstellen, werden diese Komponenten automatisch aufeinander verwiesen, wenn Sie die Standardimplementierungen mit Standardkonfigurationen verwenden. Wenn Sie jedoch benutzerdefinierte Komponenten oder Konfigurationen verwenden, sollten Sie den DefaultPreloadManager über die oben genannten APIs explizit darüber informieren.

Produktionsreifes Preloading: Das gleitende Fenster

In einem dynamischen Feed kann ein Nutzer durch eine nahezu unendliche Menge an Inhalten scrollen. Wenn Sie dem DefaultPreloadManager kontinuierlich Videos hinzufügen, ohne eine entsprechende Entfernungsstrategie zu verwenden, kommt es unweigerlich zu einem OutOfMemoryError. Jede vorab geladene MediaSource enthält eine SampleQueue, in der Speicherpuffer zugewiesen werden. Wenn sich diese ansammeln, kann der Heap-Speicher der Anwendung erschöpft sein. Die Lösung ist ein Algorithmus, den Sie vielleicht schon kennen: das gleitende Fenster. Beim Muster mit gleitendem Fenster wird eine kleine, überschaubare Gruppe von Elementen im Arbeitsspeicher beibehalten, die logisch an die aktuelle Position des Nutzers im Feed angrenzen. Beim Scrollen verschiebt sich dieses „Fenster“ mit den verwalteten Elementen mit dem Nutzer. Es werden neue Elemente hinzugefügt, die in den Blick kommen, und Elemente entfernt, die sich nun in der Ferne befinden.

slidingwindow.png

Fließendes Zeitfenster implementieren

Es ist wichtig zu wissen, dass PreloadManager keine integrierte setWindowSize()-Methode bietet. Das gleitende Fenster ist ein Designmuster, das Sie als Entwickler mit den primitiven Methoden „add()“ und „remove()“ implementieren müssen. In Ihrer Anwendungslogik müssen UI-Ereignisse wie Scrollen oder Seitenwechsel mit diesen API-Aufrufen verknüpft werden. Wenn Sie einen Codeverweis dafür benötigen, haben wir dieses gleitende Fenster-Muster im Socialite-Beispiel implementiert. Es enthält auch einen PreloadManagerWrapper, der ein gleitendes Fenster imitiert.

Vergessen Sie nicht, preloadManager.remove(mediaItem) in Ihre Implementierung aufzunehmen, wenn das Element wahrscheinlich nicht mehr bald in der Wiedergabe des Nutzers angezeigt wird. Wenn Elemente, die sich nicht mehr in der Nähe des Nutzers befinden, nicht entfernt werden, ist das die Hauptursache für Speicherprobleme bei Implementierungen des Vorabladens. Durch den Aufruf von remove() werden Ressourcen freigegeben, die dazu beitragen, die Arbeitsspeichernutzung Ihrer App zu begrenzen und stabil zu halten.

Kategorisierte Preloading-Strategie mit TargetPreloadStatusControl optimieren

Nachdem wir nun definiert haben, was vorab geladen werden soll (die Elemente in unserem Fenster), können wir eine genau definierte Strategie dafür anwenden, wie viel für jedes Element vorab geladen werden soll. Wie Sie diese Granularität mit der Einrichtung von TargetPreloadStatusControl erreichen, haben wir bereits in Teil 1 gesehen.

Ein Element an Position +/- 1 hat eine höhere Wahrscheinlichkeit, abgespielt zu werden, als ein Element an Position +/- 4. Sie könnten Elementen, die der Nutzer wahrscheinlich als Nächstes ansehen wird, mehr Ressourcen (Netzwerk, CPU, Arbeitsspeicher) zuweisen. So wird eine auf Nähe basierende „Preloading“-Strategie erstellt, die der Schlüssel zum Ausgleich zwischen sofortiger Wiedergabe und effizienter Ressourcennutzung ist.

Sie können Analysedaten über PreloadManagerListener verwenden, wie in den vorherigen Abschnitten beschrieben, um Ihre Strategie für die Vorabladedauer festzulegen.

Fazit und weitere Informationen

Sie haben jetzt das nötige Wissen, um mit dem DefaultPreloadManager von Media3 schnelle, stabile und ressourceneffiziente Media-Feeds zu erstellen.

Zusammenfassung:

  • Mit PreloadManagerListener können Sie Analysedaten erfassen und eine robuste Fehlerbehandlung implementieren.
  • Verwenden Sie immer einen einzelnen DefaultPreloadManager.Builder, um sowohl die Manager- als auch die Player-Instanzen zu erstellen, damit wichtige Komponenten gemeinsam genutzt werden.
  • Implementieren Sie das Gleitfenstermuster, indem Sie add()- und remove()-Aufrufe aktiv verwalten, um OutOfMemoryError zu vermeiden.
  • Mit TargetPreloadStatusControl können Sie eine intelligente, mehrstufige Preloading-Strategie erstellen, die Leistung und Ressourcenverbrauch in Einklang bringt.

Teil 3: Caching mit vorab geladenen Medien

Das Vorabladen von Daten in den Arbeitsspeicher bietet einen sofortigen Leistungsvorteil, kann aber auch Nachteile haben. Sobald die Anwendung geschlossen oder die vorab geladenen Medien aus dem Manager entfernt werden, sind die Daten nicht mehr vorhanden. Um eine dauerhaftere Optimierung zu erreichen, können wir das Vorladen mit dem Festplatten-Caching kombinieren. Diese Funktion befindet sich in der Entwicklung und wird in einigen Monaten eingeführt.

Möchten Sie uns Feedback geben? Wir freuen uns auf Ihre Nachricht.

Wir halten dich auf dem Laufenden. Bis dahin kannst du dir ja schon mal überlegen, wie du die Wiedergabe deiner Videos beschleunigen kannst. 🚀

Verfasst von:

Weiterlesen