Produktneuheiten

Verbesserte Medienwiedergabe: Eine detaillierte Betrachtung des PreloadManager von Media3 – Teil 2

Lesezeit: 9 Minuten.
Profil von Mayuri Khinvasara Khabya ansehen
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 erläutert. 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 des DefaultPreloadManager erläutert. Wir zeigen Ihnen, wie Sie mit PreloadManagerListener Insights abrufen, produktionsreife Best Practices implementieren, z. B. die gemeinsame Nutzung von Kernkomponenten mit ExoPlayer, und das Muster des gleitenden Fensters 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 Ressourcenmanagement 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: Statistiken mit PreloadManagerListener abrufen

Wenn Sie eine Funktion in der Produktion einführen möchten, möchten Sie als App-Entwickler auch die Statistiken dazu 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 Einblicke in den Vorabladeprozess und -status bieten.

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

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 Analytics-Pipeline verknüpft werden. Wenn Sie diese Ereignisse an Ihre Analytics-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 treten die höchsten Fehlerraten auf? (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.

Über das Debugging hinaus: 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 Vorabladen des nächsten Videos fehlschlägt, kann Ihre Anwendung die automatische Wiedergabe für den nächsten Swipe 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. HttpDataSourceException, um die Fehler genauer zu untersuchen. Weitere Informationen zur Fehlerbehebung bei ExoPlayer.

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

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 PreloadManager und dann ExoPlayer freizugeben.

Der DefaultPreloadManager.Builder wurde entwickelt, um diese gemeinsame Nutzung zu erleichtern, und bietet 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. Sehen Sie sich die visuelle Darstellung an, wie diese Komponenten mit der ExoPlayer-Wiedergabe interagieren.

preloadManager2.png

Bandbreitenkonflikte mit einem gemeinsam genutzten BandwidthMeter vermeiden

Der BandwidthMeter liefert eine Schätzung der verfügbaren Netzwerkbandbreite basierend auf bisherigen Übertragungsraten. Wenn PreloadManager und Player separate Instanzen verwenden, wissen sie nichts von der Netzwerkaktivität des jeweils anderen, was zu Fehlerszenarien führen kann. Nehmen wir beispielsweise an, ein Nutzer sieht sich ein Video an, die Netzwerkverbindung wird schlechter und die MediaSource zum Vorabladen startet gleichzeitig einen aggressiven Download für ein zukünftiges Video. Die Aktivität der MediaSource zum Vorabladen 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 erheblicher Fehler in der Nutzererfahrung.

Durch die gemeinsame Nutzung eines einzelnen BandwidthMeter kann TrackSelector während des Vorabladens oder der Wiedergabe Titel mit der höchsten Qualität auswählen, die unter den aktuellen Netzwerkbedingungen und dem Status des Puffers möglich sind. So können intelligente Entscheidungen getroffen werden, 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 Arbeitsspeicherverbrauch 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 weiterhin vorab, solange das für das Vorabladen zugewiesene Ziel für die Pufferbytes nicht das obere Limit erreicht hat. Er wartet nicht, bis das Laden für die Wiedergabe abgeschlossen ist.

Hinweis: Durch die gemeinsame Nutzung von LoadControl in der aktuellen 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 für die Auswahl der Titel verantwortlich, die 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 während des Vorabladens ausgewählten Titel dieselben sind, die vom Player verwendet werden. 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 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 diese Blaupause, um zu sehen, welche Video-, Audio- und Textformate der endgültige Player unterstützt. So kann sie nur den kompatiblen Medientitel intelligent 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. So können Fehler bei der Parallelität reduziert werden.

Alle Interaktionen zwischen PreloadManager und Player mit zu ladenden oder vorab zu ladenden MediaSources 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 PreloadManager und 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 MediaSource zum Vorabladen an den Player. In diesem Szenario wird die gesamte PreloadMediaSource vom Manager an den Player verschoben. Alle diese Interaktionen und Übergaben sollten im selben PlaybackLooper erfolgen.

Wenn PreloadManager und ExoPlayer in verschiedenen Threads ausgeführt werden, kann es zu einer Race-Bedingung kommen. Der Thread des PreloadManager könnte den internen Zustand 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 debuggen 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 benutzerdefinierte Konfigurationen verwenden, sollten Sie den DefaultPreloadManager über die oben genannten APIs explizit darüber informieren.

Produktionsreifes Vorabladen: Das Muster des gleitenden Fensters

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 Arbeitsspeicherpuffer 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 gleitende Fenster. Das Muster des gleitenden Fensters 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, gleitet dieses „Fenster“ mit den verwalteten Elementen mit. Es werden neue Elemente hinzugefügt, die in den Blick kommen, und Elemente entfernt, die sich jetzt weiter entfernt befinden.

slidingwindow.png

Das Muster des gleitenden Fensters implementieren

Es ist wichtig zu wissen, dass PreloadManager keine integrierte Methode setWindowSize() bietet. Das gleitende Fenster 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 Muster des gleitenden Fensters 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 bald in der Ansicht des Nutzers angezeigt wird. Das Entfernen von Elementen, die sich nicht mehr in der Nähe des Nutzers befinden, ist die Hauptursache für Arbeitsspeicherprobleme bei Implementierungen zum Vorabladen. Der Aufruf remove() sorgt dafür, dass Ressourcen freigegeben werden, mit denen Sie die Arbeitsspeichernutzung 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 Daten anwenden, die für jedes Element vorab geladen werden soll. In Teil 1 haben wir bereits gesehen, wie diese Granularität mit der Einrichtung von TargetPreloadStatusControl erreicht werden kann.

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 zum Ausgleich zwischen sofortiger Wiedergabe und effizienter Ressourcennutzung ist.

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

Fazit und weitere Informationen

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

Zusammenfassung der wichtigsten Punkte:

  • Verwenden Sie PreloadManagerListener, um Analytics-Statistiken zu erfassen 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 Muster des gleitenden Fensters, 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 ausgleicht.

Nächster Schritt in Teil 3: Caching mit vorab geladenen Medien

Das Vorabladen von Daten in den Arbeitsspeicher bietet sofortige Leistungsvorteile, kann aber auch Nachteile haben. Sobald die Anwendung geschlossen oder die vorab geladenen Medien aus dem Manager entfernt werden, sind die Daten verloren. Um eine dauerhaftere Optimierung zu erreichen, können wir das Vorabladen mit dem Datenträger-Caching kombinieren. Diese Funktion wird aktiv entwickelt und ist demnächst verfügbar.

Haben Sie Feedback zu teilen? Wir freuen uns auf Ihre Meinung.

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

Verfasst von:
Weiterlesen