Wiadomości o usługach

Ulepszone odtwarzanie multimediów: szczegółowe informacje o klasie PreloadManager w Media3 – część 2

Czas czytania: 9 minut
Wyświetl profil Mayuri Khinvasara Khabya
Mayuri Khinvasara Khabya Inżynier ds. relacji z programistami

Witamy w drugiej części naszej 3-częściowej serii o wstępnym wczytywaniu multimediów za pomocą Media3. Ta seria artykułów ma Cię poprowadzić przez proces tworzenia w aplikacjach na Androida multimediów o wysokiej responsywności i niskim opóźnieniu.

  • Część 1. Wprowadzenie do wstępnego wczytywania za pomocą Media3 zawierała podstawowe informacje. Omówiliśmy różnicę między PreloadConfiguration w przypadku prostych list odtwarzania a bardziej zaawansowanym DefaultPreloadManager w przypadku dynamicznych interfejsów użytkownika. Dowiedzieliśmy się, jak wdrożyć podstawowy cykl życia interfejsu API: dodawanie multimediów za pomocą funkcji add(), pobieranie przygotowanego obiektu MediaSource za pomocą funkcji getMediaSource(), zarządzanie priorytetami za pomocą funkcji setCurrentPlayingIndex() i invalidate() oraz zwalnianie zasobów za pomocą funkcji remove() i release().
  • Część 2 (ten post): w tym poście omawiamy zaawansowane funkcje klasy DefaultPreloadManager. Wyjaśniamy, jak uzyskiwać statystyki za pomocą interfejsu PreloadManagerListener, wdrażać sprawdzone metody gotowe do użycia w produkcji, takie jak udostępnianie podstawowych komponentów odtwarzaczowi ExoPlayer, oraz opanować wzorzec okna przesuwnego, aby skutecznie zarządzać pamięcią.
  • Część 3. Ostatnia część tej serii będzie poświęcona integracji PreloadManager z trwałym pamięcią podręczną na dysku, co pozwoli Ci zmniejszyć zużycie danych dzięki zarządzaniu zasobami i zapewnić płynne działanie.

Jeśli nie masz doświadczenia z wstępnym wczytywaniem w Media3, przed przejściem dalej zalecamy przeczytanie części 1. Jeśli chcesz wyjść poza podstawy, zobacz, jak ulepszyć implementację odtwarzania multimediów.

Nasłuchiwanie: pobieranie statystyk za pomocą PreloadManagerListener

Gdy deweloper aplikacji chce wprowadzić funkcję w wersji produkcyjnej, zależy mu też na poznaniu i rejestrowaniu związanych z nią danych analitycznych. Jak możesz mieć pewność, że strategia wstępnego wczytywania jest skuteczna w rzeczywistym środowisku? Aby na nie odpowiedzieć, potrzebne są dane o wskaźnikach sukcesu, porażkach i wydajności. Interfejs PreloadManagerListener jest podstawowym mechanizmem gromadzenia tych danych.

Interfejs PreloadManagerListener udostępnia 2 niezbędne wywołania zwrotne, które dostarczają kluczowych informacji o procesie i stanie wstępnego wczytywania.

  • onCompleted(MediaItem mediaItem): to wywołanie zwrotne jest wywoływane po pomyślnym zakończeniu żądania wstępnego wczytywania zgodnie z ustawieniami TargetPreloadStatusControl.
  • onError(PreloadException error): to wywołanie zwrotne może być przydatne do debugowania i monitorowania. Jest wywoływana, gdy wstępne wczytywanie się nie powiedzie, i zawiera powiązany wyjątek.

Możesz zarejestrować odbiornik za pomocą jednego wywołania metody, jak pokazano w przykładzie kodu poniżej:

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)

Wyciąganie wniosków z danych o słuchaczach

Te wywołania zwrotne odbiorcy można połączyć z potokiem analitycznym. Przekazując te zdarzenia do silnika analitycznego, możesz uzyskać odpowiedzi na kluczowe pytania, takie jak:

  • Jaki jest wskaźnik sukcesu wstępnego wczytania? (stosunek zdarzeń onCompleted do łącznej liczby prób wstępnego wczytania)
  • Które sieci CDN lub formaty wideo wykazują najwyższy odsetek błędów? (przez analizowanie wyjątków z funkcji onError)
  • Jaki jest nasz współczynnik błędów wstępnego wczytywania? (stosunek zdarzeń onError do łącznej liczby prób wstępnego ładowania)

Te dane mogą dostarczyć Ci ilościowych informacji zwrotnych na temat strategii wstępnego wczytywania, umożliwiając przeprowadzanie testów A/B i wprowadzanie ulepszeń opartych na danych, które zwiększą wygodę użytkowników. Te dane mogą Ci pomóc w inteligentnym dostosowywaniu czasu wstępnego buforowania oraz liczby filmów, które chcesz wstępnie buforować, a także przydzielanych buforów.

Nie tylko debugowanie: używanie funkcji onError do płynnego przełączania interfejsu

Nieudane wstępne wczytanie jest silnym wskaźnikiem zbliżającego się buforowania u użytkownika. Wywołanie zwrotne onError umożliwia reagowanie na bieżąco. Zamiast tylko rejestrować błąd, możesz dostosować interfejs. Jeśli na przykład nie uda się wstępnie wczytać następnego filmu, aplikacja może wyłączyć autoodtwarzanie przy następnym przesunięciu, wymagając od użytkownika kliknięcia, aby rozpocząć odtwarzanie.

Dodatkowo, sprawdzając typ PreloadException, możesz zdefiniować bardziej inteligentną strategię ponawiania. Aplikacja może natychmiast usunąć ze swojego menedżera źródło, które nie działa, na podstawie komunikatu o błędzie lub kodu stanu HTTP. Element należy usunąć ze strumienia interfejsu, aby problemy z wczytywaniem nie wpływały na wrażenia użytkownika. Możesz też uzyskać bardziej szczegółowe dane z wyjątku PreloadException, np. HttpDataSourceException, aby dokładniej zbadać błędy. Dowiedz się więcej o rozwiązywaniu problemów z ExoPlayerem.

System pomocniczy: dlaczego udostępnianie komponentów ExoPlayera jest konieczne?

Klasy DefaultPreloadManager i ExoPlayer zostały zaprojektowane tak, aby ze sobą współpracować. Aby zapewnić stabilność i wydajność, muszą one współdzielić kilka podstawowych komponentów. Jeśli działają one z oddzielnymi, nieskoordynowanymi komponentami, może to wpłynąć na bezpieczeństwo wątków i użyteczność wstępnie wczytanych ścieżek w odtwarzaczu, ponieważ musimy mieć pewność, że wstępnie wczytane ścieżki będą odtwarzane w odpowiednim odtwarzaczu. Poszczególne komponenty mogą też konkurować o ograniczone zasoby, takie jak przepustowość sieci i pamięć, co może prowadzić do pogorszenia wydajności. Ważną częścią cyklu życia jest odpowiednie usuwanie. Zalecana kolejność usuwania to najpierw PreloadManager, a potem ExoPlayer.

Klasa DefaultPreloadManager.Builder została zaprojektowana, aby ułatwić udostępnianie, i zawiera interfejsy API do tworzenia instancji zarówno klasy PreloadManager, jak i połączonej instancji odtwarzacza. Zobaczmy, dlaczego komponenty takie jak BandwidthMeter, LoadControl, TrackSelector czy Looper muszą być udostępniane. Sprawdź wizualizację interakcji tych komponentów z odtwarzaniem w ExoPlayer.

preloadManager2.png

Zapobieganie konfliktom przepustowości w przypadku udostępnionego narzędzia BandwidthMeter

BandwidthMeter szacuje dostępną przepustowość sieci na podstawie historycznych szybkości przesyłania. Jeśli PreloadManager i odtwarzacz używają oddzielnych instancji, nie są świadome aktywności sieciowej drugiej instancji, co może prowadzić do niepowodzeń. Rozważmy na przykład sytuację, w której użytkownik ogląda film, jego połączenie sieciowe ulega pogorszeniu, a wstępne wczytywanie MediaSource jednocześnie rozpoczyna agresywne pobieranie przyszłego filmu. Aktywność wstępnego wczytywania MediaSource zużywałaby przepustowość potrzebną aktywnemu odtwarzaczowi, co powodowałoby zatrzymanie bieżącego filmu. Zatrzymanie odtwarzania to poważny problem, który negatywnie wpływa na wrażenia użytkowników.

Udostępniając pojedynczy obiekt BandwidthMeter, TrackSelector może wybierać ścieżki o najwyższej jakości, biorąc pod uwagę bieżące warunki sieciowe i stan bufora podczas wstępnego wczytywania lub odtwarzania. Dzięki temu może podejmować inteligentne decyzje, aby chronić aktywną sesję odtwarzania i zapewnić płynne działanie.

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

Zapewnianie spójności ze wspólnymi komponentami LoadControl, TrackSelector i Renderer ExoPlayera

  • LoadControl: ten komponent określa zasady buforowania, np. ile danych należy buforować przed rozpoczęciem odtwarzania oraz kiedy rozpocząć lub zatrzymać wczytywanie większej ilości danych. Udostępnianie LoadControl zapewnia, że zużycie pamięci przez odtwarzacz i klasę PreloadManager jest regulowane przez jedną, skoordynowaną strategię buforowania zarówno w przypadku wstępnie załadowanych, jak i aktywnie odtwarzanych multimediów, co zapobiega konfliktom zasobów. Aby zapewnić spójność, musisz inteligentnie przydzielać rozmiar bufora, uwzględniając liczbę wstępnie wczytywanych elementów i czas ich trwania. W przypadku konfliktu odtwarzacz będzie traktować priorytetowo odtwarzanie bieżącego elementu wyświetlanego na ekranie. W przypadku wspólnego obiektu LoadControl menedżer wstępnego wczytywania będzie kontynuować wstępne wczytywanie, dopóki docelowa liczba bajtów bufora przydzielonych na wstępne wczytywanie nie osiągnie górnego limitu. Nie będzie czekać, aż wczytywanie do odtwarzania się zakończy.

Uwaga: udostępnianie LoadControl w najnowszej wersji Media3 (1.8) zapewnia, że jego Allocator może być prawidłowo udostępniany PreloadManagerowi i odtwarzaczowi. Skuteczne kontrolowanie wstępnego wczytywania za pomocą komponentu LoadControl to funkcja, która będzie dostępna w najbliższej wersji Media3 1.9.

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector: ten komponent odpowiada za wybieranie ścieżek (np. wideo o określonej rozdzielczości, audio w konkretnym języku) do załadowania i odtworzenia. Udostępnianie gwarantuje, że ścieżki wybrane podczas wstępnego wczytywania będą takie same jak te, których użyje odtwarzacz. Pozwala to uniknąć sytuacji, w której wstępnie wczytywana jest ścieżka wideo w rozdzielczości 480p, a odtwarzacz natychmiast ją odrzuca i pobiera ścieżkę w rozdzielczości 720p. Menedżer wstępnego wczytywania NIE powinien udostępniać odtwarzaczowi tej samej instancji selektora ścieżek. Zamiast tego powinni używać innej instancji TrackSelector, ale o tej samej implementacji. Dlatego w klasie DefaultPreloadManager.Builder ustawiamy TrackSelectorFactory, a nie TrackSelector.

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • Renderer: ten komponent odpowiada za rozpoznawanie możliwości odtwarzacza bez tworzenia pełnych rendererów. Sprawdza ten plan, aby dowiedzieć się, które formaty wideo, audio i tekstu będzie obsługiwał odtwarzacz końcowy. Dzięki temu może on inteligentnie wybierać i pobierać tylko zgodną ścieżkę multimedialną, co zapobiega marnowaniu przepustowości na treści, których odtwarzacz nie może odtworzyć.

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

Dowiedz się więcej o komponentach Exoplayera.

Złota zasada: jeden wspólny odtwarzacz pętli

Wątek, w którym można uzyskać dostęp do instancji ExoPlayera, można wyraźnie określić, przekazując Looper podczas tworzenia odtwarzacza. Pętlę wątku, z którego należy uzyskać dostęp do odtwarzacza, można sprawdzić za pomocą metody Player.getApplicationLooper. Dzięki utrzymywaniu wspólnego obiektu Looper między odtwarzaczem a klasą PreloadManager masz pewność, że wszystkie operacje na tych wspólnych obiektach multimedialnych są serializowane w kolejce komunikatów jednego wątku. Może to zmniejszyć liczbę błędów związanych z równoczesnością.

Wszystkie interakcje między menedżerem wstępnego wczytywania a odtwarzaczem ze źródłami multimediów, które mają zostać wczytane lub wstępnie wczytane, muszą odbywać się w tym samym wątku odtwarzania. Udostępnianie obiektu Looper jest niezbędne do zapewnienia bezpieczeństwa wątków, dlatego musimy udostępniać obiekt PlaybackLooper między klasami PreloadManager i player.

Klasa PreloadManager przygotowuje w tle obiekt MediaSource z informacjami o stanie. Gdy kod interfejsu wywołuje funkcję player.setMediaSource(mediaSource), przekazujesz ten złożony obiekt z informacjami o stanie z wstępnie wczytanego obiektu MediaSource do odtwarzacza. W tym scenariuszu cały element PreloadMediaSource jest przenoszony z menedżera do odtwarzacza. Wszystkie te interakcje i przekazywanie powinny odbywać się w ramach tego samego obiektu PlaybackLooper.

Jeśli menedżer wstępnego wczytywania i ExoPlayer działały na różnych wątkach, mogło dojść do sytuacji wyścigu. Wątek PreloadManager może modyfikować stan wewnętrzny MediaSource (np.zapisywać nowe dane w buforze) w momencie, gdy wątek odtwarzacza próbuje z niego odczytać dane. Prowadzi to do nieprzewidywalnego zachowania i trudnego do debugowania wyjątku IllegalStateException.

preloadManagerBuilder.setPreloadLooper(playbackLooper)

Zobaczmy, jak możesz udostępnić wszystkie powyższe komponenty między ExoPlayerem a klasą DefaultPreloadManager w ramach konfiguracji.

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

Wskazówka: jeśli używasz domyślnych komponentów w ExoPlayerze, takich jak DefaultLoadControl itp., nie musisz ich wyraźnie udostępniać w DefaultPreloadManager. Gdy tworzysz instancję ExoPlayera za pomocą metody buildExoPlayer klasy DefaultPreloadManager.Builder, te komponenty są automatycznie powiązane ze sobą, jeśli używasz domyślnych implementacji z domyślnymi konfiguracjami. Jeśli jednak używasz komponentów niestandardowych lub konfiguracji niestandardowych, musisz wyraźnie powiadomić o nich klasę DefaultPreloadManager za pomocą powyższych interfejsów API.

Wstępne wczytywanie gotowe do wykorzystania w środowisku produkcyjnym: wzorzec okna przesuwnego

W przypadku dynamicznego kanału użytkownik może przewijać praktycznie nieskończoną ilość treści. Jeśli będziesz stale dodawać filmy do klasy DefaultPreloadManager bez odpowiedniej strategii usuwania, nieuchronnie spowodujesz błąd OutOfMemoryError. Każde wstępnie wczytane źródło multimediów zawiera SampleQueue, które przydziela bufory pamięci. Gdy się one gromadzą, mogą wyczerpać miejsce na stercie aplikacji. Rozwiązaniem jest algorytm, który być może już znasz, zwany oknem przesuwnym. Wzorzec okna przesuwnego utrzymuje w pamięci niewielki, łatwy w zarządzaniu zestaw elementów, które są logicznie sąsiadujące z bieżącą pozycją użytkownika w pliku danych. Gdy użytkownik przewija stronę, to „okno” zarządzanych elementów przesuwa się wraz z nim, dodając nowe elementy, które pojawiają się w widoku, i usuwając te, które są już daleko.

slidingwindow.png

Wdrażanie wzorca okna przesuwnego

Pamiętaj, że klasa PreloadManager nie udostępnia wbudowanej metody setWindowSize(). Okno przesuwne to wzorzec projektowy, który deweloper musi zaimplementować za pomocą podstawowych metod add() i remove(). Logika aplikacji musi łączyć zdarzenia interfejsu, takie jak przewijanie lub zmiana strony, z tymi wywołaniami interfejsu API. Jeśli chcesz zobaczyć kod, w przykładowym projekcie socialite zaimplementowaliśmy wzorzec okna przesuwnego, który zawiera też klasę PreloadManagerWrapper imitującą okno przesuwne.

Nie zapomnij dodać w implementacji funkcji preloadManager.remove(mediaItem), gdy element prawdopodobnie nie pojawi się wkrótce w widoku użytkownika. Główną przyczyną problemów z pamięcią w przypadku implementacji wstępnego wczytywania jest brak możliwości usunięcia elementów, które nie znajdują się już w pobliżu użytkownika. Wywołanie remove() zapewnia zwolnienie zasobów, co pomaga utrzymać ograniczone i stabilne wykorzystanie pamięci przez aplikację.

Dostosowywanie strategii wstępnego wczytywania podzielonego na kategorie za pomocą parametru TargetPreloadStatusControl

Teraz, gdy wiemy już, co wstępnie wczytać (elementy w naszym oknie), możemy zastosować dobrze zdefiniowaną strategię określającą, ile danych wstępnie wczytać dla każdego elementu. W części 1 pokazaliśmy już, jak uzyskać taką szczegółowość za pomocą konfiguracji TargetPreloadStatusControl.

Przypomnijmy, że element na pozycji +/- 1 może mieć większe prawdopodobieństwo odtworzenia niż element na pozycji +/- 4. Możesz przydzielić więcej zasobów (sieci, procesora, pamięci) elementom, które użytkownik najprawdopodobniej wyświetli w następnej kolejności. W ten sposób tworzysz strategię „wstępnego wczytywania” opartą na bliskości, która jest kluczem do równoważenia natychmiastowego odtwarzania z efektywnym wykorzystaniem zasobów.

Aby określić strategię czasu wstępnego wczytywania, możesz użyć danych analitycznych za pomocą interfejsu PreloadManagerListener, jak opisano w poprzednich sekcjach.

Podsumowanie i dalsze kroki

Masz teraz zaawansowaną wiedzę, która pozwoli Ci tworzyć szybkie, stabilne i wydajne pod względem zasobów pliki danych o mediach za pomocą klasy DefaultPreloadManager w Media3.

Podsumujmy najważniejsze informacje:

  • Używaj PreloadManagerListener do zbierania statystyk i wdrażania niezawodnej obsługi błędów.
  • Aby mieć pewność, że ważne komponenty są udostępniane, zawsze używaj jednego obiektu DefaultPreloadManager.Builder do tworzenia instancji menedżera i odtwarzacza.
  • Zaimplementuj wzorzec okna przesuwnego, aktywnie zarządzając wywołaniami add() i remove(), aby zapobiec błędowi OutOfMemoryError.
  • Użyj TargetPreloadStatusControl, aby utworzyć inteligentną, wielopoziomową strategię wstępnego wczytywania, która równoważy wydajność i zużycie zasobów.

Dalsze kroki w części 3: buforowanie wstępnie załadowanych multimediów

Wstępne wczytywanie danych do pamięci zapewnia natychmiastowy wzrost wydajności, ale może wiązać się z kompromisami. Po zamknięciu aplikacji lub usunięciu wstępnie załadowanych multimediów z menedżera dane znikają. Aby uzyskać trwalszy poziom optymalizacji, możemy połączyć wstępne wczytywanie z pamięcią podręczną na dysku. Ta funkcja jest w trakcie opracowywania i będzie dostępna w ciągu kilku miesięcy.

Czy chcesz podzielić się opinią? Czekamy na Twoją odpowiedź.

Bądź na bieżąco i przyspiesz odtwarzanie filmów. 🚀

Autorzy:
Czytaj dalej