Wiadomości o usługach
Ulepszone odtwarzanie multimediów: szczegółowe omówienie klasy PreloadManager w Media3 – część 2
Czas czytania: 9 minut
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 wysoce responsywnych multimediów o niskim opóźnieniu.
- Część 1. Wprowadzenie do wstępnego wczytywania za pomocą Media3 zawierała podstawowe informacje. Zbadaliśmy różnicę między PreloadConfiguration w przypadku prostych list odtwarzania a bardziej zaawansowanym DefaultPreloadManager w przypadku dynamicznych interfejsów. Wiesz już, 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 środowisku produkcyjnym, 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łą pamięcią podręczną dysku, co pozwoli Ci zmniejszyć zużycie danych dzięki zarządzaniu zasobami i zapewnić płynne działanie.
Jeśli dopiero zaczynasz korzystać z wstępnego wczytywania w Media3, przed przejściem dalej zdecydowanie zalecamy przeczytanie części 1. Jeśli chcesz wyjść poza podstawy, zobacz, jak ulepszyć implementację odtwarzania multimediów.
Odbieranie danych analitycznych za pomocą interfejsu PreloadManagerListener
Gdy deweloper aplikacji chce wprowadzić funkcję w wersji produkcyjnej, chce też poznać i zarejestrować dane analityczne dotyczące tej funkcji. Jak możesz mieć pewność, że strategia wstępnego wczytywania jest skuteczna w rzeczywistym środowisku? Odpowiedź na to pytanie wymaga danych o wskaźnikach sukcesu, niepowodzeniach 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ć odbiorcę za pomocą jednego 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 urządzenia nasłuchującego
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 odsetek udanych wstępnych załadowań? (stosunek zdarzeń onCompleted do łącznej liczby prób wstępnego wczytywania)
- Które sieci CDN lub formaty wideo wykazują najwyższy odsetek błędów? (przez analizowanie wyjątków z funkcji onError)
- Jaki jest odsetek 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, 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 wczytywania oraz liczby filmów, które chcesz wstępnie wczytać, 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 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ć kolejnego 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ąć z 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 osobnymi, 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 ExoPlayerze.
Zapobieganie konfliktom przepustowości w przypadku współdzielonego obiektu BandwidthMeter
BandwidthMeter szacuje dostępną przepustowość sieci na podstawie historycznych szybkości przesyłania. Jeśli PreloadManager i odtwarzacz używają oddzielnych instancji, nie mają informacji 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ępne wczytywanie MediaSource jednocześnie rozpoczyna agresywne pobieranie przyszłego filmu. Aktywność wstępnego wczytywania obiektu 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.
Dzięki udostępnianiu jednego obiektu BandwidthMeter obiekt TrackSelector może wybierać ścieżki o najwyższej jakości w zależności od bieżących warunków sieciowych i stanu 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)
Zapewnienie spójności ze wspólnymi komponentami LoadControl, TrackSelector i Renderer w 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 załadowanych, jak i aktywnie odtwarzanych multimediów, co zapobiega konfliktom o zasoby. 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 udostępnionego 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ć na zakończenie wczytywania na potrzeby odtwarzania.
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 sterowanie wstępnym wczytywaniem za pomocą 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, dźwięku 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 klasy TrackSelector. Zamiast tego powinni używać innej instancji TrackSelector, ale tej samej implementacji. Dlatego w klasie DefaultPreloadManager.Builder ustawiamy TrackSelectorFactory zamiast TrackSelector.
preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)
- Renderer: ten komponent odpowiada za rozpoznawanie możliwości odtwarzacza bez tworzenia pełnych rendererów. Sprawdza ten plan, aby określić, które formaty wideo, audio i tekstu będzie obsługiwał ostateczny odtwarzacz. 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 obiekt Looper podczas tworzenia odtwarzacza. Pętlę wątku, z którego musi być dostępny odtwarzacz, 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 udostępnionych 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 Loopera jest niezbędne dla bezpieczeństwa wątków, dlatego musimy udostępniać PlaybackLooper między PreloadManagerem a odtwarzaczem.
Klasa PreloadManager przygotowuje w tle obiekt MediaSource z informacjami o stanie. Gdy kod interfejsu wywołuje player.setMediaSource(mediaSource), przekazujesz ten złożony obiekt z informacjami o stanie z wstępnie wczytanego obiektu MediaSource do odtwarzacza. W tym przypadku 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 PreloadManager 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 trakcie 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 DefaultLoadControl itp., nie musisz ich wyraźnie udostępniać w klasie 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 menedżera DefaultPreloadManager za pomocą powyższych interfejsów API.
Wstępne wczytywanie gotowe do wykorzystania w środowisku produkcyjnym: wzorzec okna przesuwnego
W kanale dynamicznym 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 – okno przesuwne. 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 kanale. Podczas przewijania przez użytkownika 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.
Wdrażanie wzorca okna przesuwnego
Pamiętaj, że 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 uzyskać odniesienie do kodu, ten wzorzec okna przesuwnego jest zaimplementowany w przykładzie socialite, który zawiera też PreloadManagerWrapper imitujący 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() zwalnia zasoby, co pomaga utrzymać ograniczone i stabilne wykorzystanie pamięci przez aplikację.
Dostrajanie strategii wstępnego wczytywania podzielonego na kategorie za pomocą TargetPreloadStatusControl
Skoro wiemy już, co wstępnie wczytać (elementy w 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żywać danych analitycznych za pomocą interfejsu PreloadManagerListener, jak opisano w poprzednich sekcjach.
Podsumowanie i dalsze kroki
Masz już zaawansowaną wiedzę, która pozwoli Ci tworzyć szybkie, stabilne i wydajne pod względem wykorzystania 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 funkcji 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 z wstępnie załadowanymi multimediami
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 masz jakieś uwagi, którymi chcesz się podzielić? Czekamy na Twoją opinię.
Śledź nasze aktualności i przyspiesz odtwarzanie filmów. 🚀
Czytaj dalej
-
Wiadomości o usługach
W dzisiejszych aplikacjach multimedialnych zapewnienie płynnego, nieprzerwanego odtwarzania jest kluczowe dla pozytywnych wrażeń użytkowników. Użytkownicy oczekują, że filmy będą odtwarzane natychmiast i płynnie, bez przerw.
Mayuri Khinvasara Khabya • Czas czytania: 8 minut
-
Wiadomości o usługach
Android Studio Panda 4 jest już stabilny i możesz go używać w środowisku produkcyjnym. Wprowadziliśmy m.in. tryb planowania i przewidywanie kolejnych zmian, dzięki czemu tworzenie wysokiej jakości aplikacji na Androida jest jeszcze łatwiejsze.
Matt Dyor • Czas czytania: 5 minut
-
Wiadomości o usługach
Jeśli jesteś deweloperem aplikacji na Androida i chcesz wdrożyć w niej innowacyjne funkcje oparte na AI, niedawno udostępniliśmy nowe, zaawansowane aktualizacje.
Thomas Ezan • Czas czytania: 3 minuty
Bądź na bieżąco
Otrzymuj co tydzień najnowsze informacje o tworzeniu aplikacji na Androida na swoją skrzynkę odbiorczą.