Neuigkeiten zum Produkt

Verbesserte Medienwiedergabe: Ein detaillierter Blick auf den PreloadManager von Media3 – Teil 2

Lesezeit: 9 Minuten
Mayuri Khinvasara Khabya
Developer Relations Engineer

Willkommen zum zweiten Teil unserer dreiteiligen Serie zum Vorabladen von Medien mit Media3. In dieser Serie erfahren Sie, wie Sie in Ihren Android-Apps Medien mit geringer Latenz und hoher Reaktionsfähigkeit erstellen.

  • Teil 1: Einführung in das Vorabladen mit Media3 behandelte die Grundlagen. Wir haben den Unterschied zwischen PreloadConfiguration für einfache Playlists und dem leistungsstärkeren DefaultPreloadManager für dynamische Benutzeroberflächen untersucht. 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 untersuchen wir die erweiterten Funktionen des DefaultPreloadManager. Wir zeigen Ihnen, wie Sie mit PreloadManagerListener Statistiken erhalten, Best Practices für die Produktion implementieren, z. B. die gemeinsame Nutzung von Kernkomponenten mit ExoPlayer, und das Sliding-Window-Muster verwenden, um den Arbeitsspeicher effektiv zu verwalten.
  • Teil 3: Im letzten Teil dieser Serie geht es um die Integration von PreloadManager in einen nichtflüchtigen Festplattencache. So können Sie den Datenverbrauch mit der Ressourcenverwaltung reduzieren und eine nahtlose Nutzererfahrung bieten.

Wenn Sie noch keine Erfahrung mit dem Vorabladen in Media3 haben, empfehlen wir Ihnen, zuerst Teil 1 zu lesen. Für alle, die über die Grundlagen hinausgehen möchten, zeigen wir Ihnen, wie Sie die Implementierung der Medienwiedergabe verbessern können.

Zuhören: Analysen mit PreloadManagerListener abrufen

Wenn Sie eine Funktion in der Produktion starten möchten, möchten Sie als App-Entwickler auch die entsprechenden Analysen verstehen und erfassen. Wie können Sie sicher sein, dass Ihre Strategie zum Vorabladen in einer realen Umgebung effektiv ist? Um diese Frage zu beantworten, sind Daten zu Erfolgsraten, Fehlern und Leistung erforderlich. Die Schnittstelle PreloadManagerListener ist der primäre Mechanismus zum Erfassen dieser Daten.

PreloadManagerListener bietet zwei wichtige Callbacks, die wichtige Informationen zum Vorabladen und zum Status liefern.

  • onCompleted(MediaItem mediaItem): Dieser Callback wird aufgerufen, wenn eine Vorabladungsanfrage erfolgreich abgeschlossen wurde, wie in TargetPreloadStatusControl definiert.
  • onError(PreloadException error): Dieser Callback kann für die Fehlerbehebung und das Monitoring nützlich sein. Er wird aufgerufen, wenn eine Vorabladung fehlschlägt, und liefert die zugehörige Ausnahme.

Sie können einen Listener mit einem einzigen 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)

Statistiken aus dem Listener extrahieren

Diese Listener-Callbacks können mit Ihrer Analysepipeline verknüpft werden. Wenn Sie diese Ereignisse an Ihre Analyse-Engine weiterleiten, können Sie wichtige Fragen beantworten, z. B.:

  • Wie hoch ist unsere Erfolgsrate beim Vorabladen? (Verhältnis von onCompleted-Ereignissen zu allen Vorabladungsversuchen)
  • Bei welchen CDNs oder Videoformaten sind die Fehlerraten am höchsten? (Durch Parsen der Ausnahmen von onError)
  • Wie hoch ist unsere Fehlerrate beim Vorabladen? (Verhältnis von onError-Ereignissen zu allen Vorabladungsversuchen)

Diese Daten können Ihnen quantitatives Feedback zu Ihrer Strategie zum Vorabladen geben und A/B-Tests sowie datengestützte Verbesserungen der Nutzererfahrung ermöglichen. Außerdem können Sie damit die Dauer des Vorabladens und die Anzahl der Videos, die Sie vorabladen möchten, sowie die zugewiesenen Puffer intelligent optimieren.

Mehr als nur Fehlerbehebung: onError für einen reibungslosen UI-Fallback verwenden

Eine fehlgeschlagene Vorabladung ist ein starker Hinweis auf ein bevorstehendes Pufferereignis für den Nutzer. Mit dem Callback onError können Sie reaktiv reagieren. Anstatt den Fehler nur zu protokollieren, können Sie die Benutzeroberfläche anpassen. Wenn beispielsweise das nächste Video nicht vorab geladen werden kann, kann Ihre Anwendung die automatische Wiedergabe für den nächsten Wisch deaktivieren, sodass der Nutzer auf das Video tippen muss, um die Wiedergabe zu starten.

Außerdem können Sie durch Untersuchen des PreloadException-Typs eine intelligentere Strategie für Wiederholungsversuche 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 die Nutzererfahrung beeinträchtigen. Sie können auch detailliertere Daten von PreloadException abrufen, z. B. die HttpDataSourceException, um die Fehler weiter zu untersuchen. Weitere Informationen zur Fehlerbehebung in ExoPlayer.

Das Buddy-System: Warum ist die gemeinsame Nutzung von Komponenten mit ExoPlayer erforderlich?

Der DefaultPreloadManager und ExoPlayer sind für die Zusammenarbeit konzipiert. 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 von vorab geladenen Titeln auf dem Player beeinträchtigen, da vorab geladene Titel auf dem richtigen Player wiedergegeben werden müssen. Die separaten Komponenten können auch um begrenzte Ressourcen wie Netzwerkbandbreite und Arbeitsspeicher konkurrieren, was zu einer Leistungsminderung führen kann. Ein wichtiger Teil des Lebenszyklus ist die ordnungsgemäße Freigabe. Die empfohlene Reihenfolge ist, zuerst den PreloadManager und dann den ExoPlayer freizugeben.

Der DefaultPreloadManager.Builder wurde entwickelt, um diese gemeinsame Nutzung zu erleichtern, und bietet APIs zum Instanziieren sowohl des 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. Sehen Sie sich die visuelle Darstellung der Interaktion dieser Komponenten mit der ExoPlayer-Wiedergabe an.

preloadManager2.png

Bandbreitenkonflikte mit einem gemeinsam genutzten BandwidthMeter vermeiden

Der BandwidthMeter liefert eine Schätzung der verfügbaren Netzwerkbandbreite basierend auf bisherigen Übertragungsraten. Wenn der PreloadManager und der Player separate Instanzen verwenden, wissen sie nichts von der Netzwerkaktivität des jeweils anderen, was zu Fehlerszenarien führen kann. Stellen Sie sich beispielsweise vor, ein Nutzer sieht sich ein Video an, die Netzwerkverbindung wird schlechter und die vorab geladene MediaSource startet gleichzeitig einen aggressiven Download für ein zukünftiges Video. Die Aktivität der vorab geladenen MediaSource würde die Bandbreite verbrauchen, die vom aktiven Player benötigt wird, wodurch das aktuelle Video angehalten wird. Ein Anhalten während der Wiedergabe ist ein erhebliches Problem für die Nutzererfahrung.

Durch die gemeinsame Nutzung eines einzelnen BandwidthMeter kann der TrackSelector unter den aktuellen Netzwerkbedingungen und dem Status des Puffers die Titel mit der höchsten Qualität auswählen, sowohl beim Vorabladen als auch bei der Wiedergabe. Er kann dann intelligente Entscheidungen treffen, um die aktive Wiedergabesitzung zu schützen und eine reibungslose Wiedergabe zu gewährleisten.

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

Konsistenz mit gemeinsam genutzten LoadControl-, TrackSelector- und Renderer-Komponenten von ExoPlayer gewährleisten

  • LoadControl: Diese Komponente bestimmt die Pufferrichtlinie, z. B. wie viele Daten vor dem Start der Wiedergabe gepuffert werden sollen und wann weitere Daten geladen werden sollen. Durch die gemeinsame Nutzung von LoadControl wird sichergestellt, dass der Speicherverbrauch von Player und PreloadManager durch eine einheitliche, koordinierte Pufferstrategie für vorab geladene und aktiv wiedergegebene Medien gesteuert wird, wodurch Ressourcenkonflikte vermieden werden. Sie müssen die Puffergröße intelligent zuweisen und dabei berücksichtigen, wie viele Elemente Sie vorabladen und wie lange das Vorabladen dauert, um Konsistenz zu gewährleisten. Bei Konflikten priorisiert der Player die Wiedergabe des aktuellen Elements, das auf dem Bildschirm angezeigt wird. Mit einem gemeinsam genutzten LoadControl lädt der PreloadManager so lange vorab, bis das für das Vorabladen zugewiesene Ziel für die Puffer-Bytes erreicht ist. Er wartet nicht, bis das Laden für die Wiedergabe abgeschlossen ist.

Hinweis: Durch die gemeinsame Nutzung von LoadControl in der neuesten Version von Media3 (1.8) wird sichergestellt, dass der Allocator korrekt mit PreloadManager und Player gemeinsam genutzt werden kann. Die Verwendung von LoadControl zur effektiven Steuerung des Vorabladens ist eine Funktion, die in der kommenden Version Media3 1.9 verfügbar sein wird.

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector: Diese Komponente ist dafür verantwortlich, auszuwählen, welche Titel geladen und wiedergegeben werden sollen (z. B. Video mit einer bestimmten Auflösung, Audio in einer bestimmten Sprache). Durch die gemeinsame Nutzung wird sichergestellt, dass die beim Vorabladen ausgewählten Titel dieselben sind, die der Player verwendet. So wird ein Szenario vermieden, in dem ein 480p-Videotitel vorab geladen wird, nur damit der Player ihn sofort verwirft und bei der Wiedergabe einen 720p-Titel abruft.< br /> Der PreloadManager sollte nicht dieselbe Instanz von TrackSelector mit dem Player gemeinsam nutzen. Stattdessen sollten sie die unterschiedliche TrackSelector-Instanz, aber mit derselben Implementierung verwenden. Deshalb legen wir im DefaultPreloadManager.Builder die TrackSelectorFactory und nicht einen TrackSelector fest.

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • Renderer: Diese Komponente ist dafür verantwortlich, die Funktionen des Players zu verstehen, ohne die vollständigen Renderer zu erstellen. Sie prüft anhand dieser Blaupause, welche Video-, Audio- und Textformate der endgültige Player unterstützt. So kann sie intelligent nur den kompatiblen Medientitel auswählen und herunterladen und verhindert, dass Bandbreite für Inhalte verschwendet wird, 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, auf dem auf eine ExoPlayer-Instanz zugegriffen werden kann, kann explizit angegeben werden, indem beim Erstellen des Players ein Looper übergeben wird. Der Looper des Threads, von dem aus auf den Player zugegriffen werden muss, kann mit Player.getApplicationLooper abgefragt werden. Durch die gemeinsame Nutzung eines Loopers zwischen Player und PreloadManager wird sichergestellt, dass alle Vorgänge für diese gemeinsam genutzten Medienobjekte in die Nachrichtenwarteschlange eines einzelnen Threads serialisiert werden. Dadurch können Fehler bei der Parallelität reduziert werden.

Alle Interaktionen zwischen dem PreloadManager und dem Player mit Medienquellen, die geladen oder vorab geladen werden sollen, müssen im selben Wiedergabe-Thread erfolgen. Die gemeinsame Nutzung des Loopers ist für die Threadsicherheit unerlässlich. Daher müssen wir den PlaybackLooper zwischen dem PreloadManager und dem Player gemeinsam nutzen.

Der PreloadManager bereitet im Hintergrund ein zustandsbehaftetes MediaSource-Objekt vor. Wenn Ihr UI-Code player.setMediaSource(mediaSource) aufruft, ü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 im selben PlaybackLooper erfolgen.

Wenn der PreloadManager und ExoPlayer in verschiedenen Threads ausgeführt würden, könnte es zu einer Race Condition 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 IllegalStateException, die schwer zu beheben ist.

preloadManagerBuilder.setPreloadLooper(playbackLooper)

Sehen wir uns an, wie Sie alle oben genannten Komponenten in der Einrichtung selbst zwischen ExoPlayer und DefaultPreloadManager gemeinsam nutzen 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 verwenden, z. B. DefaultLoadControl, müssen Sie sie nicht explizit für DefaultPreloadManager freigeben. Wenn Sie Ihre ExoPlayer-Instanz über buildExoPlayer des DefaultPreloadManager.Builder erstellen, werden diese Komponenten automatisch miteinander verknüpft, wenn Sie die Standardimplementierungen mit Standardkonfigurationen verwenden. Wenn Sie jedoch benutzerdefinierte Komponenten oder Konfigurationen verwenden, sollten Sie den DefaultPreloadManager explizit über die oben genannten APIs darüber informieren.

Produktionsreifes Vorabladen: Das Sliding-Window-Muster

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 Strategie zum Entfernen zu haben, verursachen Sie unweigerlich einen OutOfMemoryError. Jede vorab geladene MediaSource enthält eine SampleQueue, die Speicherpuffer zuweist. Wenn sich diese ansammeln, kann der Heap-Speicher der Anwendung erschöpft sein. Die Lösung ist ein Algorithmus, mit dem Sie möglicherweise bereits vertraut sind: das Sliding Window. Das Sliding-Window-Muster verwaltet eine kleine, überschaubare Menge von Elementen im Arbeitsspeicher, die sich logisch in der Nähe der aktuellen Position des Nutzers im Feed befinden. Wenn der Nutzer scrollt, verschiebt sich dieses „Fenster“ mit den verwalteten Elementen mit ihm. Es werden neue Elemente hinzugefügt, die in den Blick kommen, und Elemente entfernt, die sich jetzt weiter entfernt befinden.

slidingwindow.png

Das Sliding-Window-Muster implementieren

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

Vergessen Sie nicht, preloadManager.remove(mediaItem) in Ihre Implementierung aufzunehmen, wenn das Element wahrscheinlich nicht bald in der Ansicht des Nutzers angezeigt wird. Wenn Sie Elemente nicht entfernen, die sich nicht mehr in der Nähe des Nutzers befinden, ist dies die Hauptursache für Speicherprobleme bei Implementierungen zum Vorabladen. Der Aufruf remove() sorgt dafür, dass Ressourcen freigegeben werden, mit denen Sie die Speichernutzung Ihrer App begrenzen und stabil halten können.

Eine kategorisierte Strategie zum Vorabladen 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 für die Menge an Inhalten anwenden, die 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.

Zur Erinnerung: Ein Element an Position +/- 1 hat möglicherweise eine höhere Wahrscheinlichkeit, wiedergegeben zu werden, als ein Element an Position +/- 4. Sie können Elementen, die der Nutzer am wahrscheinlichsten als Nächstes ansehen wird, mehr Ressourcen (Netzwerk, CPU, Arbeitsspeicher) zuweisen. So entsteht eine Strategie zum Vorabladen basierend auf der Nähe, die der Schlüssel ist, um ein Gleichgewicht zwischen sofortiger Wiedergabe und effizienter Ressourcennutzung zu schaffen.

Sie können Analysedaten über PreloadManagerListener verwenden, wie in den vorherigen Abschnitten beschrieben, um Ihre Strategie für die Dauer des Vorabladens zu bestimmen.

Fazit und nächste Schritte

Sie verfügen jetzt über das erweiterte Wissen, um mit dem DefaultPreloadManager von Media3 schnelle, stabile und ressourceneffiziente Media-Feeds zu erstellen.

Fassen wir die wichtigsten Erkenntnisse zusammen:

  • Verwenden Sie PreloadManagerListener, um Analysen zu erhalten und eine robuste Fehlerbehandlung zu implementieren.
  • Verwenden Sie immer einen einzelnen DefaultPreloadManager.Builder, um sowohl Ihre Manager- als auch Ihre Player-Instanzen zu erstellen, damit wichtige Komponenten gemeinsam genutzt werden.
  • Implementieren Sie das Sliding-Window-Muster, indem Sie add()- und remove()-Aufrufe aktiv verwalten, um OutOfMemoryError zu vermeiden.
  • Verwenden Sie TargetPreloadStatusControl, um eine intelligente, mehrstufige Strategie zum Vorabladen zu erstellen, die Leistung und Ressourcennutzung in Einklang bringt.

Nächster Schritt in 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 weg. Um eine dauerhaftere Optimierung zu erreichen, können wir das Vorabladen mit dem Festplattencaching kombinieren. Diese Funktion wird aktiv entwickelt und wird in einigen Monaten verfügbar sein.

Haben Sie Feedback für uns, das Sie teilen möchten? Wir freuen uns auf Ihre Meinung.

Bleiben Sie auf dem Laufenden und beschleunigen Sie die Wiedergabe Ihrer Videos! 🚀

Verfasst von:

Weiterlesen