Wiadomości o usługach

Ulepszanie odtwarzania multimediów: szczegółowe omówienie klasy PreloadManager w bibliotece Media3 – część 2

Czas czytania: 9 min
Mayuri Khinvasara Khabya
Inżynier ds. relacji z deweloperami

Zapraszamy do drugiej części 3-częściowego cyklu artykułów o wstępnym wczytywaniu multimediów za pomocą biblioteki Media3. Ten cykl ma pomóc Ci w tworzeniu w aplikacjach na Androida środowisk multimedialnych o wysokiej responsywności i niskim opóźnieniu.

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

Jeśli dopiero zaczynasz korzystać z wstępnego wczytywania w bibliotece Media3, przed kontynuowaniem lektury przeczytaj część 1. Jeśli chcesz wyjść poza podstawy, dowiedz się, jak ulepszyć implementację odtwarzania multimediów.

Słuchanie: pobieranie statystyk za pomocą interfejsu PreloadManagerListener

Gdy chcesz wprowadzić funkcję w środowisku produkcyjnym, jako deweloper aplikacji chcesz też poznać i rejestrować statystyki dotyczące tej funkcji. Jak możesz mieć pewność, że strategia wstępnego wczytywania jest skuteczna w rzeczywistym środowisku? Aby odpowiedzieć na to pytanie, potrzebujesz danych o współczynnikach powodzenia, błędach i skuteczności. Interfejs PreloadManagerListener to podstawowy mechanizm zbierania tych danych.

Interfejs PreloadManagerListener udostępnia 2 podstawowe wywołania zwrotne, które zawierają ważne informacje 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 definicją w interfejsie TargetPreloadStatusControl.
  • onError(PreloadException error): to wywołanie zwrotne może być przydatne do debugowania i monitorowania. Jest wywoływane, gdy wstępne wczytywanie się nie powiedzie, i zawiera powiązany wyjątek.

Możesz zarejestrować odbiornik za pomocą pojedynczego wywołania metody, jak pokazano w tym przykładowym kodzie:

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)

Wyodrębnianie statystyk z odbiornika

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

  • Jaki jest nasz współczynnik powodzenia wstępnego wczytywania? (stosunek zdarzeń onCompleted do łącznej liczby prób wstępnego wczytywania)
  • Które sieci CDN lub formaty wideo wykazują najwyższe współczynniki błędów? (analizując wyjątki z wywołania onError)
  • Jaki jest nasz współczynnik błędów wstępnego wczytywania? (stosunek zdarzeń onError do łącznej liczby prób wstępnego wczytywania)

Te dane mogą dostarczyć Ci ilościowych informacji zwrotnych na temat strategii wstępnego wczytywania, co umożliwi przeprowadzanie testów A/B i wprowadzanie ulepszeń opartych na danych w celu poprawy wrażeń użytkowników. Te dane mogą też pomóc Ci w inteligentnym dostosowaniu czasu wstępnego wczytywania i liczby filmów, które chcesz wstępnie wczytać, oraz przydzielonych buforów.

Poza debugowaniem: używanie wywołania onError do płynnego przełączania interfejsu

Nieudane wstępne wczytywanie jest silnym wskaźnikiem zbliżającego się zdarzenia buforowania dla użytkownika. Wywołanie zwrotne onError umożliwia reakcję. 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, co będzie wymagać kliknięcia użytkownika, aby rozpocząć odtwarzanie.

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

System partnerski: dlaczego konieczne jest udostępnianie komponentów bibliotece ExoPlayer?

Klasy DefaultPreloadManager i ExoPlayer zostały zaprojektowane tak, aby ze sobą współpracowały. Aby zapewnić stabilność i wydajność, muszą one udostępniać kilka podstawowych komponentów. Jeśli działają 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. Oddzielne 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 zwolnienie klasy PreloadManager, a potem klasy ExoPlayer.

Klasa DefaultPreloadManager.Builder została zaprojektowana tak, aby ułatwić to udostępnianie, i zawiera interfejsy API do tworzenia instancji klasy PreloadManager i powiązanej instancji odtwarzacza. Zobaczmy, dlaczego należy udostępniać komponenty takie jak BandwidthMeter, LoadControl, TrackSelector i Looper. Sprawdź wizualizację interakcji tych komponentów z odtwarzaniem w bibliotece ExoPlayer.

preloadManager2.png

Zapobieganie konfliktom przepustowości dzięki udostępnianiu klasy BandwidthMeter

Klasa BandwidthMeter szacuje dostępną przepustowość sieci na podstawie historycznych szybkości przesyłania. Jeśli klasy PreloadManager i odtwarzacz używają oddzielnych instancji, nie wiedzą o 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ępnie wczytujące źródło MediaSource jednocześnie rozpoczyna agresywne pobieranie przyszłego filmu. Aktywność wstępnie wczytującego źródła MediaSource zużywa przepustowość potrzebną aktywnemu odtwarzaczowi, co powoduje zatrzymanie bieżącego filmu. Zatrzymanie podczas odtwarzania to poważny problem z wrażeniami użytkownika.

Dzięki udostępnianiu pojedynczej klasy BandwidthMeter klasa TrackSelector może wybierać ścieżki o najwyższej jakości w danych warunkach sieciowych i stanie bufora podczas wstępnego wczytywania lub odtwarzania. Może wtedy podejmować inteligentne decyzje, aby chronić aktywną sesję odtwarzania i zapewnić płynne działanie.

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

Zapewnianie spójności dzięki udostępnianiu komponentów LoadControl, TrackSelector i Renderer biblioteki ExoPlayer

  • 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 klasy 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 wczytanych, jak i aktywnie odtwarzanych multimediów, co zapobiega konfliktom zasobów. Aby zapewnić spójność, musisz inteligentnie przydzielać rozmiar bufora, koordynując go z liczbą wstępnie wczytywanych elementów i ich czasem trwania. W przypadku konfliktu odtwarzacz będzie traktować priorytetowo odtwarzanie bieżącego elementu wyświetlanego na ekranie. Dzięki udostępnianiu klasy 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ć na zakończenie wczytywania do odtwarzania.

Uwaga: udostępnianie klasy LoadControl w najnowszej wersji biblioteki Media3 (1.8) zapewnia, że jej alokator może być prawidłowo udostępniany klasie PreloadManager i odtwarzaczowi. Funkcja skutecznego kontrolowania wstępnego wczytywania za pomocą klasy LoadControl będzie dostępna w nadchodzącej wersji Media3 1.9.

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector: ten komponent odpowiada za wybieranie ścieżek (np. wideo o określonej rozdzielczości, dźwięku w określonym języku) do wczytania i odtworzenia. Udostępnianie zapewnia, że ścieżki wybrane podczas wstępnego wczytywania będą takie same jak te, których będzie używać odtwarzacz. Pozwala to uniknąć marnowania zasobów w sytuacji, gdy wstępnie wczytana jest ścieżka wideo w rozdzielczości 480p, a odtwarzacz natychmiast ją odrzuca i pobiera ścieżkę w rozdzielczości 720p podczas odtwarzania.< br /> Menedżer wstępnego wczytywania NIE powinien udostępniać odtwarzaczowi tej samej instancji klasy TrackSelector. Zamiast tego powinny używać różnych instancji klasy TrackSelector, ale tej samej implementacji. Dlatego w klasie DefaultPreloadManager.Builder ustawiamy klasę TrackSelectorFactory, a nie TrackSelector.

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • Renderer: ten komponent odpowiada za rozpoznawanie możliwości odtwarzacza bez tworzenia pełnych modułów renderowania. Sprawdza ten plan, aby zobaczyć, które formaty wideo, audio i tekstowe będą obsługiwane przez ostateczny odtwarzacz. Dzięki temu może inteligentnie wybierać i pobierać tylko zgodne ścieżki multimedialne oraz zapobiegać marnowaniu przepustowości na treści, których odtwarzacz nie może odtworzyć.

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

Więcej informacji o komponentach biblioteki ExoPlayer

Złota zasada: wspólna pętla odtwarzania

Wątek, w którym można uzyskać dostęp do instancji klasy ExoPlayer, można wyraźnie określić, przekazując klasę Looper podczas tworzenia odtwarzacza. Klasę Looper wątku, z którego należy uzyskać dostęp do odtwarzacza, można sprawdzić za pomocą metody Player.getApplicationLooper. Dzięki utrzymywaniu wspólnej klasy Looper między odtwarzaczem a klasą PreloadManager gwarantujemy, że wszystkie operacje na tych udostępnionych obiektach multimedialnych są serializowane w kolejce komunikatów pojedynczego wątku. Może to zmniejszyć liczbę błędów współbieżności.

Wszystkie interakcje między klasą PreloadManager 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 klasy Looper jest konieczne do zapewnienia bezpieczeństwa wątków, dlatego musimy udostępniać klasę PlaybackLooper między klasą PreloadManager a odtwarzaczem.

Klasa PreloadManager przygotowuje w tle obiekt MediaSource z zachowaniem stanu. Gdy kod interfejsu wywołuje metodę player.setMediaSource(mediaSource), przekazujesz ten złożony obiekt z zachowaniem stanu ze wstępnie wczytującego źródła MediaSource do odtwarzacza. W tym przypadku całe źródło PreloadMediaSource jest przenoszone z menedżera do odtwarzacza. Wszystkie te interakcje i przekazywanie powinny odbywać się w tej samej klasie PlaybackLooper.

Jeśli klasy PreloadManager i ExoPlayer działałyby w różnych wątkach, mogłaby wystąpić sytuacja wyścigu. Wątek klasy PreloadManager mógłby modyfikować stan wewnętrzny źródła MediaSource (np.zapisywać nowe dane w buforze) dokładnie w momencie, gdy wątek odtwarzacza próbuje z niego odczytać dane. Prowadzi to do nieprzewidywalnego zachowania i wyjątku IllegalStateException, który trudno debugować.

preloadManagerBuilder.setPreloadLooper(playbackLooper)

Zobaczmy, jak możesz udostępniać wszystkie powyższe komponenty między biblioteką ExoPlayer a klasą DefaultPreloadManager w samej 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 biblioteki ExoPlayer, takich jak DefaultLoadControlitp., nie musisz ich wyraźnie udostępniać klasie DefaultPreloadManager. Gdy tworzysz instancję klasy ExoPlayer 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 poinformować o nich klasę DefaultPreloadManager za pomocą powyższych interfejsów API.

Wstępne wczytywanie gotowe do użycia w środowisku produkcyjnym: wzorzec okna przesuwnego

W dynamicznym pliku danych 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 MediaSource zawiera kolejkę SampleQueue, która przydziela bufory pamięci. Gdy się one gromadzą, mogą wyczerpać miejsce w stercie aplikacji. Rozwiązaniem jest algorytm, który być może już znasz, zwany oknem przesuwnym. Wzorzec okna przesuwnego utrzymuje w pamięci mały, ł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, to „okno” zarządzanych elementów przesuwa się wraz z nim, dodając nowe elementy, które pojawiają się w widoku, i usuwając elementy, które są teraz daleko.

slidingwindow.png

Implementowanie wzorca okna przesuwnego

Pamiętaj, że klasa PreloadManager nie udostępnia wbudowanej metody setWindowSize(). Okno przesuwne to wzorzec projektowy, który musisz 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ć odniesienie do kodu, ten wzorzec okna przesuwnego jest zaimplementowany w przykładzie socialite, który zawiera też klasę PreloadManagerWrapper imitującą okno przesuwne.

Nie zapomnij dodać w implementacji metody preloadManager.remove(mediaItem), gdy element prawdopodobnie nie będzie już wkrótce wyświetlany. Nieusuwanie elementów, które nie są już blisko użytkownika, jest główną przyczyną problemów z pamięcią w implementacjach wstępnego wczytywania. Wywołanie metody remove() zapewnia zwolnienie zasobów, co pomaga utrzymać ograniczone i stabilne wykorzystanie pamięci przez aplikację.

Dostosowywanie strategii wstępnego wczytywania podzielonej na kategorie za pomocą interfejsu TargetPreloadStatusControl

Teraz, gdy zdefiniowaliśmy, co należy wstępnie wczytać (elementy w naszym oknie), możemy zastosować dobrze zdefiniowaną strategię dotyczącą tego, ile należy wstępnie wczytać dla każdego elementu. W części 1 pokazaliśmy już, jak osiągnąć tę szczegółowość za pomocą konfiguracji interfejsu 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. Tworzy to strategię „wstępnego wczytywania” opartą na bliskości, która jest kluczem do równoważenia natychmiastowego odtwarzania z efektywnym wykorzystaniem zasobów.

Możesz używać danych statystycznych za pomocą interfejsu PreloadManagerListener, jak opisano w poprzednich sekcjach, aby określić strategię czasu wstępnego wczytywania.

Podsumowanie i dalsze kroki

Masz teraz zaawansowaną wiedzę, która pozwoli Ci tworzyć szybkie, stabilne i oszczędne w zasoby pliki danych multimediów za pomocą klasy DefaultPreloadManager biblioteki Media3.

Podsumujmy najważniejsze informacje:

  • Używaj interfejsu PreloadManagerListener do zbierania statystyk i wdrażania niezawodnej obsługi błędów.
  • Zawsze używaj pojedynczej klasy DefaultPreloadManager.Builder do tworzenia instancji menedżera i odtwarzacza, aby zapewnić udostępnianie ważnych komponentów.
  • Zaimplementuj wzorzec okna przesuwnego, aktywnie zarządzając wywołaniami metod add() i remove(), aby zapobiec błędowi OutOfMemoryError.
  • Używaj interfejsu TargetPreloadStatusControl, aby utworzyć inteligentną, warstwową strategię wstępnego wczytywania, która równoważy wydajność i zużycie zasobów.

Co dalej w części 3: buforowanie wstępnie wczytanych multimediów

Wstępne wczytywanie danych do pamięci zapewnia natychmiastową poprawę wydajności, ale może wiązać się z kompromisami. Po zamknięciu aplikacji lub usunięciu wstępnie wczytanych multimediów z menedżera dane znikają. Aby osiągnąć bardziej trwały poziom optymalizacji, możemy połączyć wstępne wczytywanie z buforowaniem na dysku. Ta funkcja jest w trakcie opracowywania i będzie dostępna w ciągu kilku najbliższych miesięcy.

Masz jakieś uwagi do udostępnienia? Chętnie je poznamy.

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

Autor:

Czytaj dalej