Wiadomości o usługach

Ulepszanie odtwarzania multimediów: szczegółowe omówienie narzędzia PreloadManager w 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ą 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ą Media3 zawierała podstawowe informacje. Omówiliśmy różnicę między PreloadConfiguration w przypadku prostych playlist a bardziej zaawansowanym narzędziem DefaultPreloadManager w przypadku dynamicznych interfejsów użytkownika. Dowiedzieliśmy się, jak zaimplementować podstawowy cykl życia interfejsu API: dodawanie multimediów za pomocą add(), pobieranie przygotowanego MediaSource za pomocą getMediaSource(), zarządzanie priorytetami za pomocą setCurrentPlayingIndex() i invalidate() oraz zwalnianie zasobów za pomocą remove() i release().
  • Część 2. (ten post): w tym artykule omówimy zaawansowane funkcje DefaultPreloadManager. Dowiesz się, jak uzyskiwać informacje za pomocą PreloadManagerListener, wdrażać sprawdzone metody gotowe do użycia w środowisku produkcyjnym, takie jak udostępnianie podstawowych komponentów 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 PreloadManager z trwałą pamięcią podręczną dysku, co pozwoli Ci zmniejszyć zużycie danych dzięki zarządzaniu zasobami i zapewnić użytkownikom płynne działanie.

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

Słuchanie: pobieranie statystyk za pomocą PreloadManagerListener

Gdy chcesz wprowadzić funkcję w środowisku produkcyjnym, jako deweloper aplikacji chcesz też zrozumieć i rejestrować statystyki dotyczące tej funkcji. Jak możesz mieć pewność, że Twoja 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.

PreloadManagerListener udostępnia 2 podstawowe wywołania zwrotne, które zawierają kluczowe 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ą 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 informacji 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? (przez analizowanie wyjątków z 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 trwania wstępnego wczytywania i liczby filmów, które chcesz wstępnie wczytać, oraz przydzielonych buforów.

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

Nieudane wstępne wczytanie jest silnym wskaźnikiem zbliżającego się zdarzenia buforowania dla użytkownika. Wywołanie zwrotne onError umożliwia reaktywne reagowanie. 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 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 PreloadException, np. HttpDataSourceException, aby dokładniej zbadać błędy. Więcej informacji o rozwiązywaniu problemów z ExoPlayer

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

DefaultPreloadManager i ExoPlayer zostały zaprojektowane do współpracy. 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 PreloadManager, a potem ExoPlayer.

DefaultPreloadManager.Builder został zaprojektowany, aby ułatwić to udostępnianie, i ma interfejsy API do tworzenia instancji PreloadManager i połączonej instancji odtwarzacza. Zobaczmy, dlaczego należy udostępniać komponenty takie jak BandwidthMeter, LoadControl, TrackSelector i Looper. Sprawdź wizualizację interakcji tych komponentów z odtwarzaniem ExoPlayer.

preloadManager2.png

Zapobieganie konfliktom przepustowości dzięki udostępnianemu BandwidthMeter

BandwidthMeter szacuje dostępną przepustowość sieci na podstawie historycznych szybkości przesyłania. Jeśli PreloadManager i odtwarzacz używają oddzielnych instancji, nie wiedzą o aktywności sieciowej drugiego, co może prowadzić do niepowodzeń. Rozważmy na przykład sytuację, w której użytkownik ogląda film, jego połączenie sieciowe się pogarsza, a wstępnie wczytujące MediaSource jednocześnie rozpoczyna agresywne pobieranie przyszłego filmu. Aktywność wstępnie wczytującego 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 pojedynczego BandwidthMeter 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ępnianym komponentom LoadControl, TrackSelector i Renderer 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 LoadControl zapewnia, że zużycie pamięci przez odtwarzacz i 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ępnianemu 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 czeka, aż wczytywanie do odtwarzania się zakończy.

Uwaga: udostępnianie LoadControl w najnowszej wersji Media3 (1.8) zapewnia, że jego alokator może być prawidłowo udostępniany PreloadManagerowi i odtwarzaczowi. Używanie LoadControl do skutecznego kontrolowania wstępnego wczytywania to funkcja, która 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ęk 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ąć marnotrawstwa, gdy wstępnie wczytana zostanie ścieżka wideo w rozdzielczości 480p, a odtwarzacz natychmiast ją odrzuci i pobierze ścieżkę w rozdzielczości 720p.< br /> Menedżer wstępnego wczytywania NIE powinien udostępniać odtwarzaczowi tej samej instancji TrackSelector. Zamiast tego powinny używać różnych instancji TrackSelector, ale tej samej implementacji. Dlatego w 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 zobaczyć, które formaty wideo, audio i tekstowe będą obsługiwane przez 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 ExoPlayer

Złota zasada: wspólny Looper odtwarzania

Wątek, w którym można uzyskać dostęp do instancji ExoPlayer, można wyraźnie określić, przekazując Looper podczas tworzenia odtwarzacza. Looper wątku, z którego należy uzyskać dostęp do odtwarzacza, można sprawdzić za pomocą Player.getApplicationLooper. Dzięki utrzymywaniu wspólnego looper między odtwarzaczem a PreloadManagerem masz pewność, ż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 PreloadManagerem 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 Looper jest konieczne do zapewnienia bezpieczeństwa wątków, dlatego musimy udostępniać PlaybackLooper między PreloadManagerem a odtwarzaczem.

PreloadManager przygotowuje w tle obiekt MediaSource z zachowaniem stanu. Gdy kod interfejsu wywołuje player.setMediaSource(mediaSource), przekazujesz ten złożony obiekt z zachowaniem stanu z wstępnie wczytującego MediaSource do odtwarzacza. W tym przypadku cały PreloadMediaSource jest przenoszony z menedżera do odtwarzacza. Wszystkie te interakcje i przekazywanie powinny odbywać się w tym samym PlaybackLooper.

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

preloadManagerBuilder.setPreloadLooper(playbackLooper)

Zobaczmy, jak możesz udostępniać wszystkie powyższe komponenty między ExoPlayerem a DefaultPreloadManagerem 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 w ExoPlayer, takich jak DefaultLoadControlitp., nie musisz ich wyraźnie udostępniać DefaultPreloadManagerowi. Gdy tworzysz instancję ExoPlayer za pomocą buildExoPlayer w 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 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 DefaultPreloadManager bez odpowiedniej strategii usuwania, nieuchronnie spowodujesz błąd OutOfMemoryError. Każdy wstępnie wczytany MediaSource zawiera SampleQueue, który 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 PreloadManager nie udostępnia wbudowanej metody setWindowSize(). Okno przesuwne to wzorzec projektowy, który musisz zaimplementować jako deweloper 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 uzyskać odniesienie do kodu, ten wzorzec okna przesuwnego jest zaimplementowany w socialite przykładzie, który zawiera też PreloadManagerWrapper imitujący okno przesuwne.

Nie zapomnij dodać preloadManager.remove(mediaItem) w implementacji, gdy element prawdopodobnie nie pojawi się wkrótce w widoku użytkownika. 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 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ą TargetPreloadStatusControl

Teraz, gdy zdefiniowaliśmy, co wstępnie wczytać (elementy w naszym oknie), możemy zastosować dobrze zdefiniowaną strategię dotyczącą tego, ile wstępnie wczytać dla każdego elementu. W części 1 pokazaliśmy już, jak osiągnąć tę 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. 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ą PreloadManagerListener, jak opisano w poprzednich sekcjach, aby określić strategię czasu trwania wstępnego wczytywania.

Podsumowanie i dalsze kroki

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

Podsumujmy najważniejsze informacje:

  • Używaj PreloadManagerListener do zbierania informacji statystycznych i wdrażania niezawodnej obsługi błędów.
  • Zawsze używaj pojedynczego 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 add() i remove(), aby zapobiec błędowi OutOfMemoryError.
  • Używaj 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. 🚀

Autorzy:

Czytaj dalej