Inżynier ds. relacji z deweloperami
Konsumpcja multimediów stanowi dużą część czasu spędzanego przez użytkowników na urządzeniach mobilnych, co wymaga, aby odtwarzanie było bezproblemowe, bardzo wydajne i nie obciążało baterii. Użytkownicy oczekują też, że te funkcje będą płynnie działać na różnych urządzeniach, od telefonów po telewizory w salonie i nie tylko.
Dowiedz się, jak aktualizacje architektury w Media3 i ExoPlayerze umożliwiają płynne odtwarzanie na różnych formatach. Dyskusja koncentruje się na tworzeniu elastycznych interfejsów za pomocą zestawu narzędzi Media3 Compose UI, eliminowaniu buforowania w kanałach za pomocą nowego narzędzia PreloadManager oraz rozszerzaniu aplikacji na ekrany Living Room i Androida XR za pomocą CastPlayer i dźwięku przestrzennego.
Transkrypcja
OK.
Witam wszystkich.
Mam na imię Toni.
Jestem inżynierem w zespole ds. multimediów na Androidzie.
Razem z Kristiną i Donovanem opowiemy, jak Jetpack Media3 może pomóc w obsłudze multimediów na Androidzie.
Media3 to biblioteka Androida obsługująca wszystko, co jest związane z multimediami, niezależnie od tego, czy chodzi o odtwarzanie za pomocą ExoPlayera, udostępnianie multimediów za pomocą sesji multimedialnej i Cast, edytowanie multimediów za pomocą transformatora czy sprawdzanie multimediów, np. wyodrębnianie klatek lub metadanych.
Media3 obejmuje cały ten obszar w jednej grupie bibliotek.
Jest to biblioteka pomocnicza, która jest dołączona do aplikacji.
Dzięki temu możemy zapewnić spójność, czyli ten sam kod działający na wszystkich wersjach systemu operacyjnego i wszystkich urządzeniach.
Ułatwia też aktualizowanie.
Każda poprawka błędów, którą wprowadzamy, dotyczy interfejsu API 24 i nie jest powiązana z żadnym cyklem wydawania systemu operacyjnego.
Jako biblioteka pomocnicza możemy udostępniać bardzo bogate opcje dostosowywania, które nie byłyby możliwe w ramach Androida.
Jako biblioteka na GitHubie jesteśmy przede wszystkim rozwiązaniem open source.
Oznacza to, że każda wprowadzona przez nas zmiana jest natychmiast dostępna na GitHubie.
Mamy aktywne narzędzie Issue Tracker i akceptujemy żądania pull.
Najważniejsze jest to, że we wszystkich tych przypadkach użycia dbamy o wszystkie poziomy szczegółowości – od elementów interfejsu wysokiego poziomu po proste, gotowe interfejsy API, ale też zaawansowane opcje dostosowywania, szeroką obsługę różnych formatów i standardów oraz wydajność i skuteczność, które stanowią podstawę tego wszystkiego.
Media3 obejmuje cały ten obszar.
Dziś chcemy bardziej szczegółowo omówić niektóre z tych tematów.
Omówimy nowy moduł interfejsu Compose, nowy CastPlayer, który jeszcze bardziej ułatwia udostępnianie sesji multimedialnej, odnowiony proces edycji z nowymi zaawansowanymi efektami, nowe narzędzia do pobierania metadanych i klatek, nową technologię strumieniowania, w szczególności wideo i audio Eclipsa, oraz szereg narzędzi poprawiających jakość, które umożliwiają wstępne wczytywanie, przewijanie i oszczędzanie baterii.
Przejdźmy teraz do pierwszego tematu, czyli tego, jak Media3 może pomóc w tworzeniu elementów interfejsu do multimediów.
Niedawno opublikowaliśmy 2 nowe moduły: Media3 UI Compose i Media3 UI Compose Material 3.
Tutaj możesz zobaczyć element kompozycyjny odtwarzacza wysokiego poziomu, który został uruchomiony w Media3 1.10 bez dalszych modyfikacji.
@Composable
fun videoScreen() {
Player(viewModel.exoPlayer)
}Działa od razu po wyjęciu z pudełka.
Wymaga tylko odtwarzacza Media3.
W tym przykładzie jest to ExoPlayer, który zwykle znajduje się w modelu widoku.
Może to być jednak coś innego, np. CastPlayer, kontroler multimediów lub dowolny inny odtwarzacz w aplikacji.
Warto podkreślić, że ExoPlayer jest tutaj źródłem informacji.
Zawiera on wszystkie informacje, a funkcja kompozycyjna odtwarzacza zajmuje się renderowaniem interfejsu i łączeniem wszystkiego.
Co jednak, jeśli chcesz dostosować ten interfejs, dodać nowe przyciski, zmienić ich pozycje, style lub dodać nakładki tekstowe?
Komponent odtwarzacza składa się z elementów składowych.
Możesz je zastąpić.
Możesz dostosować każdy blok lub użyć go samodzielnie w innym miejscu w aplikacji.
Możesz na przykład osobno dostosować górę, środek i bottomControls.
Możesz też dostosować wygląd przesłony, która wyświetla się, gdy filmy nie są odtwarzane, lub nakładki błędu.
To jednak dopiero pierwsza warstwa.
Domyślny środek i bottomControls mają też bardziej szczegółowe miejsca, które możesz dostosować.
Możesz na przykład zamienić środkowy przycisk, pozostawiając wszystko inne bez zmian, lub dodać elementy nad suwakiem lub pod nim.
Media3 Compose udostępnia wszystkie elementy, których potrzebujesz, aby to zrobić.
Na przykład mamy kilka przycisków, takich jak przycisk odtwarzania/wstrzymywania, tryb powtarzania, tryb odtwarzania losowego, przycisk wyciszania i inne.
Następnie są wskaźniki postępu, takie jak suwak i tekst pozycji wyświetlany na ekranie.
Dostępnych jest też kilka kontenerów i nakładek.
Pokazana przeze mnie funkcja kompozycyjna odtwarzacza wysokiego poziomu to tylko jeden z przykładów. Jest ich znacznie więcej, np. nakładki błędów czy napisy.
Oprócz tego mamy inne przydatne komponenty, takie jak ten minikontroler.
Możesz umieścić je w planszy dolnej, gdy użytkownik porusza się po aplikacji.
To jeszcze nie koniec.
Stale rozbudowujemy tę bibliotekę, na przykład o nowe elementy sterujące edycją, takie jak suwak przycinania, który pojawi się już wkrótce.
Zobaczmy teraz, jak działa ta personalizacja i jak wykorzystywane są te elementy.
Player(viewModel.exoPlayer)Załóżmy, że chcemy przenieść tekst dotyczący pozycji i czasu trwania, który obecnie znajduje się obok suwaka, nad suwak.
Player(
player = viewModel.exoPlayer,
bottomControls = {
PlayerDefaults.BottomControls(
player,
left = {}, right = {},
above = {
PositionAndDurationText(
player, separator = " • ")
},
)
},
)Możemy więc zastąpić te bottomControls.
Nadal możemy używać komponentu domyślnego, ale ustawiamy lewy i prawy slot jako puste, aby zniknęły.
Zamiast tego umieszczamy tekst dotyczący pozycji i czasu trwania nad nim w miejscu powyżej z niestandardowym separatorem, aby wyglądał bardziej interesująco.
Załóżmy teraz, że chcemy całkowicie usunąć nakładkę centerControls i zamiast tego umieścić przycisk odtwarzania tuż nad suwakiem z regulacją prędkości i przyciskiem wyciszania.
Player(
player = viewModel.exoPlayer,
centerControls = {},
bottomControls = { ...
above = {
Row(Modifier.fillMaxWidth()) {
PositionAndDurationText(...)
Spacer(Modifier.weight(1f))
PlaybackSpeedToggleButton(player)
MuteButton(player)
PlayPauseButton(player)
}
},
)
}To dokładnie ten sam pomysł.
Możemy ustawić centerControls jako pusty, aby zniknął, i rozszerzyć boks nad suwakiem, dodając trochę miejsca i 3 przyciski, które chcemy wyświetlić.
Pamiętaj, że te przyciski pochodzą z samej biblioteki, więc nie musisz dodawać do niej żadnego dodatkowego kodu.
To wszystko, czego potrzebujesz.
Możesz też kontynuować stylizowanie tych elementów.
Jeśli chcesz na przykład zmienić ikonę przycisku wyciszania, ale zachować jego funkcjonalność, działa to w ten sam sposób.
Player(
...
bottomControls = { ...
above = {
...
MuteButton(player,
painter = { painterResource(
if (showMuted)
R.drawable.muted
else
R.drawable.unmuted )})
}
},
)Jak widać, możesz zdefiniować niestandardowy obiekt rysujący dla przycisku i samodzielnie wybrać ikony.
Komponent udostępnia pewne metadane, więc wystarczy, że sprawdzisz stan, np. showMuted w tym przykładzie, aby wybrać odpowiednią ikonę.
To jest główna zaleta Media3 Compose.
Możesz skupić się na wyglądzie i działaniu aplikacji, a Media3 zajmie się resztą.
Odczytuje stan odtwarzacza, wchodzi z nim w interakcję i obsługuje wszystkie przejścia oraz animacje.
Przejdźmy teraz od interfejsu do udostępniania multimediów.
Możesz udostępniać stan odtwarzania innym aplikacjom na tym samym urządzeniu lub połączonym z urządzeniami zdalnymi.
Głównym sposobem publikowania bieżącego odtwarzania jest MediaSession.
Udostępnia stan i metadane odtwarzania innym osobom i umożliwia im sterowanie odtwarzaniem.
W ten sposób Gemini, powiadomienie o multimediach w interfejsie systemu, Cast, Wear OS lub aplikacje towarzyszące Android Auto komunikują się z Twoją aplikacją.
MediaSession opakowuje odtwarzacz Media3 i jest bardzo łatwy do utworzenia, gdy masz już odtwarzacz.
MediaSession.Builder(context, player).build()Może to być ExoPlayer lub dowolny inny odtwarzacz, który masz w aplikacji, tak jak w przypadku interfejsu Compose.
Jeśli odtwarzasz w tle, zwykle hostujesz to w MediaSessionService lub MediaLibraryService, który automatycznie obsługuje cały cykl życia, a także takie elementy jak powiadomienia.
class PlaybackService : MediaSessionService() {
private lateinit var mediaSession:
MediaSession
override fun onGetSession(
info: ControllerInfo) : MediaSession?
= mediaSession
}Jeśli jednak masz aplikację do multimediów, prawdopodobnie już o tym wiesz.
Chcę Ci pokazać nasz nowy, przepisany odtwarzacz CastPlayer.
Google Cast umożliwia przeniesienie lokalnego odtwarzania na urządzenie zdalne, takie jak Android TV, Chromecast czy inteligentny głośnik.
Wcześniej publikowaliśmy oddzielny pakiet Cast SDK, który jest teraz w trybie konserwacji.
Zamiast tego Media3 Cast całkowicie go zastępuje, ponieważ zapewnia znacznie prostszą i bardziej usprawnioną integrację.
Najważniejszym elementem jest CastPlayer, który implementuje ten sam standardowy interfejs odtwarzacza co ExoPlayer.
ExoPlayer służy do odtwarzania lokalnego, a CastPlayer to instancja delegująca, która może odtwarzać lokalnie za pomocą ExoPlayera lub zdalnie za pomocą protokołu Cast.
I zajmuje się wszystkim.
Wykrywa zmiany urządzenia wyjściowego, nawiązuje połączenie, synchronizuje stan i w razie potrzeby przenosi odtwarzanie.
Dzięki temu zintegrowanie CastPlayera z aplikacją jest bardzo proste.
val player = ExoPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()Zamiast tworzyć ExoPlayera i udostępniać go MediaSession, używasz CastPlayera. To wszystko.
Cast.getSingletonInstance(context).initialize()
val player = CastPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()Jedyną rzeczą, którą musisz zrobić, jest zainicjowanie systemu Cast, zanim zostanie on użyty w aplikacji.
Pamiętaj, że ten CastPlayer wewnętrznie tworzy ExoPlayera do odtwarzania lokalnego, więc aby dodatkowo dostosować odtwarzacz lokalny, możesz też utworzyć własnego ExoPlayera i przekazać go jako odtwarzacz lokalny do tego CastPlayera.
Cast.getSingletonInstance(context).initialize()
val exoPlayer = ExoPlayer.Builder(context).build()
val player = CastPlayer.Builder(context)
.setLocalPlayer(exoPlayer).build()
mediaSession = MediaSession.Builder(context,player)
.build()To ta sama zasada Media3.
Zapewniamy proste, gotowe do użycia elementy interfejsu API, ale też wszystkie opcje dostosowywania, których potrzebujesz, aby dostosować go do swoich potrzeb.
Możesz się zastanawiać, jak użytkownik przełącza się między odtwarzaniem lokalnym a zdalnym w tej konfiguracji.
Działa od razu po wyjęciu z pudełka z oknem dialogowym przełącznika wyjścia systemu w powiadomieniu o multimediach.
To ten mały przycisk w rogu.
A co, jeśli chcesz wyświetlić przycisk Cast na ekranie odtwarzania, aby był bardziej widoczny?
Pamiętasz ten dostosowany interfejs tworzenia wiadomości, który utworzyliśmy kilka minut temu?
Player(
exoPlayer,
centerControls = {}
bottomControls = { /* customized */ },
)Media3 Cast udostępnia też gotowy przycisk, który możesz tam umieścić.
Wystarczy dodać gotowy MediaRouteButton do topControls i nie trzeba konfigurować żadnych dodatkowych parametrów. To wszystko.
Player(
castPlayer,
centerControls = {}
bottomControls = { /* customized */ },
topControls = {
PlayerDefaults.TopControls(castPlayer) {
MediaRouteButton(
Modifier.align(Alignment.TopEnd))
}
},
)Kliknięcie tego przycisku otwiera to samo okno przełącznika wyjścia systemowego, a następnie CastPlayer przenosi odtwarzanie.
Zauważ też, że połączyłem CastPlayer z interfejsem, aby mieć pewność, że wszystko, co dzieje się zdalnie, jest odzwierciedlane w lokalnym interfejsie.
Teraz przekażę głos Kristinie, która opowie o przypadkach użycia edycji.
Dziękujemy.
Cześć, przejdźmy teraz do edycji i efektów zaawansowanych.
Interfejsy API Media3 składają się z kilku komponentów.
Po pierwsze, Transformer jest głównym punktem wejścia dla naszych interfejsów API do edycji.
Transformer przetwarza, transkoduje i eksportuje multimedia do pliku.
Kolejny element to CompositionPlayer, który renderuje podglądy kompozycji multimedialnych w czasie rzeczywistym.
Dzięki temu możesz wyświetlić podgląd i wprowadzić poprawki przed ostatecznym wyeksportowaniem.
Kolejny element to efekty.
Zapewnia pełną kontrolę nad przetwarzaniem klatka po klatce podczas stosowania prostych efektów wizualnych lub dodawania nakładek graficznych.
Ostatni element to multiplekser.
Jest to ostatni etap przetwarzania w naszym potoku, na którym przetworzone ścieżki audio, wideo i metadanych są zapisywane w standardowych kontenerach MP4.
Przez ostatni rok pracowaliśmy nad przebudową naszego procesu edycji filmów i dziś z przyjemnością przedstawiamy najważniejsze ulepszenia.
Dlaczego przebudowaliśmy potok?
Starsza architektura wiązała się z kilkoma wyzwaniami.
Po pierwsze, nie mógł propagować metadanych we wszystkich krokach przetwarzania.
Był on ściśle powiązany z OpenGL, co ograniczało elastyczność nowoczesnych narzędzi graficznych.
Złożony interfejs API utrudniał deweloperom dostosowywanie i utrzymywanie aplikacji.
Aby rozwiązać te problemy, stworzyliśmy zupełnie nowy potok przetwarzania.
Po pierwsze, tworzenie złożonych kompozycji składających się z wielu komponentów jest teraz znacznie prostsze.
Po drugie, dzięki odłączeniu od OpenGL masz pełną swobodę korzystania z nowoczesnych platform graficznych, takich jak WebGPU.
Na koniec warto wspomnieć, że Media3 zarządza teraz bezpośrednio dekoderem i koderem, co zapewnia większą spójność i wydajność na wszystkich urządzeniach.
Przyjrzyjmy się, jak działa ten potok.
Dekodery wysyłają klatki bezpośrednio do buforów sprzętowych zarządzanych przez Media3.
Agregator klatek koordynuje klatki w wielu sekwencjach, dając niestandardowemu procesorowi klatek bezpośredni dostęp do odczytu i przetwarzania tych buforów przed wysłaniem ich do kodera.
Interfejs FrameProcessor służy jako główny punkt rozszerzenia do przetwarzania na poziomie aplikacji.
public interface FrameProcessor
extends AutoCloseable {
interface Listener {
void onWakeup();
void onError(
VideoFrameProcessingException exception);
void onFrameProcessed(
Frame frame,
@Nullable SyncFenceWrapper onCompleteFence);
}
interface Factory {
FrameProcessor create(
FrameWriter output,
Executor listenerExecutor,
Listener listener);
}
boolean queue(List<AsyncFrame> frames);
void signalEndOfStream();
}Za pomocą interfejsu detektora aplikacja powiadamia Media3 o każdym przetworzeniu klatki, przekazując bariery synchronizacji, dzięki czemu Media3 dokładnie wie, kiedy można bezpiecznie ponownie użyć buforów.
Uwaga dotycząca ujednoliconej pamięci: dzięki temu, że Media3 przejmuje kontrolę nad buforami sprzętowymi, aplikacja może odczytywać i modyfikować dane bezpośrednio na miejscu.
Takie podejście bez kopiowania zmniejsza wykorzystanie pamięci i zapewnia bardzo szybkie przetwarzanie.
Jednym z deweloperów, którzy korzystają z nowych interfejsów API do edycji, jest Adobe.
Zespół Adobe Premiere wykorzystał interfejsy Media3 Editing API jako podstawę infrastruktury do edycji multimediów, aby zapewnić większą zgodność z ekosystemem Androida, zachowując jednocześnie elastyczność potrzebną do korzystania z własnego niestandardowego potoku i WebGPU.
Z przyjemnością informujemy, że w ramach Media3 1.12 wprowadzamy nowe efekty.
Zaczynamy od ulepszeń filmów.
Nasza architektura opiera się na konfiguracji modułowej.
Twoja aplikacja integruje się z usługami Google Play efektów Media3, które współpracują z modułem ulepszeń usług Google Play.
Moduł ulepszania filmów obejmuje funkcje mapowania tonów, odszumiania, usuwania rozmycia i superrozdzielczości.
Integracja ulepszania filmów z potokiem wymaga tylko kilku wierszy kodu.
val frameProcessorFactory = EnhancementSessionDecorator.Builder(/* context = */ this, baseGlFactory)
.setTonemappingEnabled(true)
.setDeblurAndDenoiseEnabled(true)
.setUpscaleVideoEnabled(true)
.build()
val transformerBuilder = Transformer.Builder(/* context = */ this)
.setFrameProcessorFactory(frameProcessorFactory)Za pomocą EnhancementSessionDecorator.Builder możesz selektywnie włączać funkcje takie jak mapowanie tonów, usuwanie rozmycia czy skalowanie, a następnie przekazywać wynikowe frameProcessorFactory i pomocnicze bufory sprzętowe do Transformer.Builder.
Kolejnym ciekawym efektem, który pojawi się w Media3 1.12, jest dźwięk studyjny.
Dźwięk studyjny automatycznie analizuje ścieżkę audio filmu i dzieli ją na poszczególne elementy, takie jak mowa, szum otoczenia i muzyka.
Twórcy mogą łatwo wyciszyć szum wiatru lub hałas tłumu, aby na pierwszym planie były wyraźne dialogi i muzyka.
Aby zintegrować dźwięk studyjny, przekaż procesor dźwięku studyjnego w obiekcie efektu.
val editedMediaItem =
EditedMediaItem.Builder(MediaItem.fromUri(media.uri))
.setEffects(
Effects(
listOf(
soundSeparationAudioProcessor
),
emptyList(), // video effects
)
)
.build()Spowoduje to dołączenie procesora dźwięku do elementu multimedialnego.
Podczas tworzenia procesora dźwięku Studio Sound dołącz wywołanie zwrotne odbiorcy, aby otrzymywać mapowanie przeanalizowanego kanału audio za pomocą onResult.
val soundSeparationAudioProcessor =
SoundSeparationAudioProcessor(
getApplication(),
mainExecutor,
object : SoundSeparationAudioProcessor.Listener {
override fun onResult(channelToAudioType: SparseIntArray) {
for (i in 0 until channelToAudioType.size()) {
val channelIndex = channelToAudioType.keyAt(i)
val audioType = channelToAudioType.valueAt(i)
val label = toAudioTypeName(audioType)
detectedChannelVolumes.add(ChannelVolume(channelIndex, audioType, label))
}
}
},
)Przejrzyj każdy kanał, aby wyodrębnić indeks kanału, zidentyfikowany typ dźwięku i czytelną etykietę, gdy wynik operacji jest dostępny.
Posłuchajmy, jak działa dźwięk studyjny.
Pierwszy efekt. Kochanie, nie płacz.
Kroimy składniki na kolację.
W tle słychać płacz dziecka, a teraz zastosowano efekt.
Jak słychać, dzięki efektowi dźwięku studyjnego w tle nie było słychać płaczu dziecka.
Przejdźmy teraz do pobierania metadanych.
Przyjrzymy się, jak Media3 ułatwia odczytywanie metadanych kontenera i wyodrębnianie klatek.
Od wersji MetadataRetriever płynnie integruje się z procedurami Kotlin, co pozwala na tworzenie przejrzystego, nieblokującego kodu.
Ma przejrzystą, mniej podatną na błędy konstrukcję, która wykorzystuje 1 niezmienny moduł pobierania na element multimedialny.
Działa on spójnie we wszystkich wersjach interfejsu API i na wszystkich urządzeniach.
Obsługuje też szeroką gamę formatów multimedialnych.
Oto praktyczny przykład użycia właściwości MetadataRetriever.
MetadataRetriever.Builder(context, mediaItem).build().use { retriever ->
// 1. Duration (in microseconds)
val durationUs = retriever.retrieveDurationUs().await()
// 2. Track formats & container metadata (reuses the same preparation pass)
val trackGroups = retriever.retrieveTrackGroups().await()
for (i in 0 until trackGroups.length) {
val format = trackGroups[i].getFormat(0)
// Video/Audio properties: format.width, format.height, format.colorInfo …
// Rich metadata: SlowMotionData, MotionPhotoMetadata, Mp4TimestampData …
val metadata = format.metadata
}
// 3. Stream structure & manifest info
val timeline = retriever.retrieveTimeline().await()
}Dzięki współprogramom Kotlin możesz asynchronicznie pobierać czas trwania, sprawdzać formaty ścieżek i metadane – wszystko w jednym, przejrzystym bloku.
Porozmawiajmy o FrameExtractor.
Umożliwia szybkie i wydajne pobieranie zdekodowanych klatek wideo z elementu multimedialnego.
Automatycznie zarządza zasobami, takimi jak dekodery, umożliwia stosowanie efektów wizualnych lub mapowania tonów HDR oraz zapewnia precyzyjną kontrolę nad wyszukiwaniem, dzięki czemu możesz uzyskać dokładnie taką klatkę, jakiej potrzebujesz.
Oto jak wygląda wyodrębnianie klatek w kodzie.
FrameExtractor.Builder(context, mediaItem)
.setSeekParameters(SeekParameters.CLOSEST_SYNC) // Fast keyframe seeking
.setEffects(listOf(Presentation.createForHeight(360))) // GPU downscaling
.setExtractHdrFrames(true) // Optional: HLG HDR or UltraHDR
.build()
.use { extractor ->
// Representative thumbnail
val thumbnail = extractor.getThumbnail().await()
// Frame at specific timestamp (bitmap + actual presentationTimeMs)
val frame = extractor.getFrame(positionMs = 5_000L).await()
}Tworzysz instancję z opcjami takimi jak parametry wyszukiwania, zmniejszanie skali GPU, efekty i obsługa HDR, a następnie asynchronicznie czekasz na miniatury i konkretne klatki, używając czystych wywołań zawieszenia.
A teraz Donovan.
Dziękuję, Kristina.
Przejdźmy teraz do technologii strumieniowania, które dodaliśmy w Media3, w tym obsługi formatów takich jak Eclipsa Video i Eclipsa Audio.
Dlatego głównym celem Media3 jest wyprzedzanie standardów strumieniowania, aby nie trzeba było przepisywać potoku multimediów za każdym razem, gdy pojawia się nowa specyfikacja.
Oto kilka przykładów funkcji, które niedawno wprowadziliśmy.
Mamy funkcję kierowania treści, która umożliwia dynamiczne przełączanie ścieżek CDN w backendzie w trakcie transmisji.
Obsługujemy wstawianie reklam kierowane przez serwer, co umożliwia płynne łączenie reklam spersonalizowanych.
Wprowadziliśmy też obsługę AV1 w wielu punktach wejścia, w tym pełną obsługę AV1 HDR.
Zapewniamy obsługę profesjonalnych filmów na Androidzie, co umożliwia nagrywanie i edytowanie w wysokiej jakości.
Dodajemy też obsługę zdjęć ruchomych w formatach JPEG i HEIC.
Dodajemy też obsługę Eclipsa Video i Eclipsa Audio, o których opowiem więcej za chwilę.
Dobra wiadomość jest taka, że wszystkie te funkcje są obsługiwane automatycznie, gdy tylko zostanie wprowadzona obsługa urządzenia.
Przyjrzyjmy się teraz kolejnemu etapowi rozwoju HDR, czyli Eclipsa Video, i głównemu problemowi, który ma rozwiązać.
Poprzednie formaty HDR dostarczały opisowe metadane, które określały, jak powinna wyglądać scena, ale nie informowały wyświetlacza, jak ją wyrenderować.
Najpierw określmy podstawy.
W różnych środowiskach zalecamy różne ustawienia średniej jasności ekranu, aby zapewnić wyraźny i wygodny widok treści.
W ciemnym pomieszczeniu zakres ten wynosi od 100 do 300 nitów.
Oczywiście, gdy otoczenie staje się coraz jaśniejsze, ekran musi się do niego dostosować i zwiększyć jasność, aby treści były wyraźnie widoczne. Gdy korzystasz z telefonu w pełnym słońcu, ekran może potrzebować jasności 1000, 2000 lub nawet więcej nitów, aby treści były widoczne.
Tradycyjny SDR działa w zakresie od 100 do 300.
Oczywiście HDR nie ma na celu rozjaśnienia całego ekranu.
Chcesz rozszerzyć zakres dynamiki, aby uzyskać zlokalizowane podświetlenia takich elementów jak ogień i światło słoneczne.
Smartfon wprowadza jednak pewne komplikacje, ponieważ zabieramy go ze sobą wszędzie – do ciemnego pokoju w domu lub na słoneczną plażę.
W połączeniu z faktem, że wiele aplikacji miesza elementy interfejsu SDR z treściami SDR i HDR w mieszanych kanałach, może to prowadzić do nieprzyjemnego, oślepiającego efektu, zwłaszcza podczas przewijania późno w nocy.
Teraz widzimy, dlaczego warto korzystać z HDR.
Dzięki temu nasze treści wyglądają świetnie w większej liczbie środowisk.
Zauważyliśmy nawet, że po włączeniu HDR zaangażowanie użytkowników wzrasta.
Problem polega na tym, że istniejące formaty HDR nie uwzględniają tych środowisk.
W tym właśnie Eclipsa Video sprawdza się znakomicie.
Pauza na śmiech.
Dziękujemy.
Dziękujemy.
Dlatego zamiast opisowych metadanych Eclipsa Video dostarcza metadane nakazowe.
Opiera się ona na 3 głównych filarach.
Pierwszy filar to punkt odniesienia dla bieli.
Dzięki temu standardowa jasność jest blokowana na poziomie punktu bieli SDR dostosowanego do oświetlenia otoczenia, co eliminuje nagłe skoki jasności.
Drugi filar to podstawowy zakres dynamiki (SDR) twórcy.
Dzięki temu zachowywana jest dokładna krzywa mapowania tonów twórcy z powrotem do SDR.
Po trzecie, mamy krzywe wzmocnienia adaptacyjnego zapasu, które deterministycznie skalują jasne partie obrazu, aby dopasować je do zapasu HDR wyświetlacza w czasie rzeczywistym, dzięki czemu treści pasują do środowiska, w którym są wyświetlane.
Ta funkcja jest dostępna w ExoPlayerze w Media3 1.11 na urządzeniach z Androidem 17 lub nowszym.
Eclipsa Video opiera się na otwartych standardach, co zapewnia zgodność między branżami.
Dlatego właśnie teraz jest najlepszy moment na obsługę HDR.
Jeśli chodzi o dźwięk, mamy Eclipsa Audio, czyli bezpłatny format dźwięku przestrzennego 3D typu open source.
Jest on oparty na specyfikacji IAMF organizacji Alliance for Open Media.
Łączy on dźwięk ambisoniczny oparty na scenie z dźwiękiem opartym na kanale w jeden strumień z wieloma mieszanymi prezentacjami.
Umożliwia to manipulowanie obiektami, na przykład ulepszanie dialogów, i działa na wszystkich urządzeniach, od słuchawek stereo po systemy dźwięku przestrzennego.
W Media3 są 2 ścieżki odtwarzania.
W przypadku Androida 17 i nowszych ExoPlayer i Android Audio Framework będą automatycznie renderować Eclipsa Audio.
W przypadku Androida 16 i starszych wersji nadal masz opcję.
Rozszerzenie IAMF możesz włączyć za pomocą opcjonalnej biblioteki dodatków Media3.
Jeśli chodzi o tworzenie, możesz zapisywać ścieżki audio IAMF za pomocą multiplekserów Media3.
Jednak obsługa formatów wysokiej jakości to tylko połowa sukcesu.
Musimy dostarczać tę jakość w wydajny sposób.
Teraz chciałbym omówić, jak Media3 przyspiesza i usprawnia nasze potoki.
Podobnie jak w przypadku wielu innych rzeczy, o których dziś mówiliśmy, większość tych ulepszeń wydajności jest dostępna od razu po zaktualizowaniu wersji Media3.
Jedną z dostępnych funkcji jest odciążanie dźwięku PCM, które może przekierowywać odtwarzanie dźwięku do procesora o niskim poborze mocy.
Z naszych wstępnych analiz wynika, że podczas odtwarzania dźwięku przy wyłączonym ekranie można zaoszczędzić do 20% energii.
Oprócz odciążania dźwięku PCM mamy dynamiczne planowanie, które dostosowuje wybudzenia wewnętrznej pętli renderowania ExoPlayera do VSYNC i dostępności bufora dekodera.
Mamy też wstępne rozgrzewanie renderera, które umożliwia wstępne rozgrzanie dodatkowego renderera wideo, co pozwala na natychmiastowe przejścia na playliście i łatwiejsze wstawianie reklam.
Mamy też funkcję wykrywania zablokowanego odtwarzacza i wstępnego buforowania, która automatycznie wykrywa zablokowane bufory i renderery oraz precyzyjnie ogranicza pamięć wstępnie buforowaną.
Innym optymalnym trybem jest tryb przewijania.
Dotyczy to aplikacji, w których użytkownicy często przewijają treści.
Gdy włączysz tryb przewijania, przeciąganie paska postępu będzie znacznie bardziej płynne i będzie szybciej reagować, jak widać tutaj.
Przy okazji, tryb przewijania jest wyłączony.
Po prawej stronie widzimy włączony tryb przewijania.
Dziękujemy.
Dziękujemy.
OK.
Porozmawiajmy teraz o Android System Health.
Musimy szanować nasze wspólne zasoby pamięci RAM, procesora i baterii, więc gdy Android zaostrza zasady dotyczące wzmacniania dźwięku i usług działających na pierwszym planie, aplikacje, które zarządzają własnymi usługami i bitmapami, mogą często napotykać błędy.
Mogą się pojawić wyjątki dotyczące niedozwolonego uruchomienia usługi działającej na pierwszym planie lub wyjątki dotyczące braku pamięci w binderze.
Dlatego na różne sposoby wzmocniliśmy sesję usługi multimedialnej i menedżera powiadomień o multimediach, a także nasze potoki bitmap, m.in. zapobiegamy nieaktualnym intencjom usługi działającej na pierwszym planie i ograniczamy pamięć grafiki.
Jeśli jednak używasz Media3 i postępujesz zgodnie ze standardowymi wskazówkami, wszystkie te ograniczenia dotyczące stanu systemu są obsługiwane bez wpływu na działanie aplikacji.
Na koniec chciałbym omówić funkcję, która wkrótce pojawi się w Media3.
Gdy przychodzi czas na kodowanie lub eksportowanie filmu, różnorodność sprzętu może być jednym z największych wyzwań.
Wiele aplikacji używa uniwersalnej konfiguracji kodeków, co prowadzi do nieoptymalnej jakości i wydajności.
Dlatego stworzyliśmy CodecDB.
CodecDB opiera się na obszernych pomiarach rzeczywistych filmów przy użyciu standardowych danych dotyczących jakości i testów porównawczych sprzętu na urządzeniach z Androidem.
Zobaczmy, jak to działa.
Po lewej stronie mamy kadr z filmu nagranego telefonem, w którym użyliśmy bardzo ogólnej konfiguracji wideo, którą można zastosować na wielu urządzeniach.
Po prawej stronie użyliśmy optymalnej konfiguracji na poziomie urządzenia, którą uzyskaliśmy z CodecDB.
Gdzie więc są różnice?
Największą różnicę w jakości widać w obszarach o wysokim poziomie szczegółowości.
Zwróć uwagę na ostrość gałęzi drzew i krzewów oraz krawędzi budynku.
Dzięki CodecDB uzyskujemy znacznie lepsze wrażenia podczas oglądania, które wykorzystują możliwości konkretnego urządzenia.
Używanie CodecDB w kodzie jest dość proste.
val config = CodecDb.onDevice(context)
.forEncodingFallingBackTo {
constraints -> createFallbackConfig(constraints)
}
.findOptimalSettings()
.forEditing()
.withResolution(1920, 1080, /* frameRate= */ 30.0)
.withAnyCodec()
.targetingQuality(VideoEncoder.QUALITY_LEVEL_VERY_GOOD)
.get()Użyjesz wzorca płynnego kreatora etapów, który przeprowadzi Cię przez wszystkie wymagane parametry.
Określisz więc przypadek użycia.
W tym przypadku używamy go do edycji.
Informuje to CodecDB, że używasz potoku eksportu lub transkodowania Media3.
Inne przypadki użycia, które możesz ustawić, to na przykład przechwytywanie obrazu z kamery, jeśli używasz tej funkcji.
Przekażesz też docelową rozdzielczość i liczbę klatek na sekundę.
Zamiast zgadywać, czy film w rozdzielczości 1080p potrzebuje 8 czy 15 megabitów na sekundę, informujesz CodecDB, którego kodeka chcesz użyć, i prosisz o docelową jakość postrzeganą.
W tym przypadku używamy bardzo dobrego poziomu jakości.
W tle CodecDB wyszukuje empiryczne krzywe testów porównawczych dla danego SOC i zwraca idealne parametry kodeka i kodera.
Spodziewamy się, że CodecDB zostanie wkrótce uruchomiony w Media3 i będzie używać natywnych typów Media3.
Dlatego integracja z istniejącym kodem Media3 powinna być bardzo prosta.
OK.
Dzisiaj poznaliśmy wiele niesamowitych funkcji Media3. Podsumujmy je.
Dzisiaj dowiedzieliście się o naszych nowych modułach interfejsu Compose, nowym CastPlayerze, ulepszonym procesie edycji i wielu nowych efektach, których możecie używać w Media3.
Pojawiły się też nowe narzędzia do pobierania metadanych i klatek.
Omówiliśmy też obsługę nowej technologii strumieniowania oraz ulepszenia i narzędzia zwiększające wydajność.
Media3 to biblioteka open source, więc możesz zobaczyć, jak zaimplementowaliśmy wszystkie te funkcje w naszym repozytorium GitHub.
Obecnie najnowszą stabilną wersją Media3 jest 1.11.1.
Możesz też sprawdzić wersję 1.12.0, która jest obecnie w wersji alfa.
Kliknij ten krótki link, który przekieruje Cię do dokumentacji dla deweloperów dotyczącej Media3.
Dziękuję i życzę udanego udziału w pozostałej części ADS.
@Composable
fun videoScreen() {
Player(viewModel.exoPlayer)
}Player(viewModel.exoPlayer)Player(
player = viewModel.exoPlayer,
bottomControls = {
PlayerDefaults.BottomControls(
player,
left = {}, right = {},
above = {
PositionAndDurationText(
player, separator = " • ")
},
)
},
)Player(
player = viewModel.exoPlayer,
centerControls = {},
bottomControls = { ...
above = {
Row(Modifier.fillMaxWidth()) {
PositionAndDurationText(...)
Spacer(Modifier.weight(1f))
PlaybackSpeedToggleButton(player)
MuteButton(player)
PlayPauseButton(player)
}
},
)
}Player(
...
bottomControls = { ...
above = {
...
MuteButton(player,
painter = { painterResource(
if (showMuted)
R.drawable.muted
else
R.drawable.unmuted )})
}
},
)MediaSession.Builder(context, player).build()class PlaybackService : MediaSessionService() {
private lateinit var mediaSession:
MediaSession
override fun onGetSession(
info: ControllerInfo) : MediaSession?
= mediaSession
}val player = ExoPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()Cast.getSingletonInstance(context).initialize()
val player = CastPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()Cast.getSingletonInstance(context).initialize()
val exoPlayer = ExoPlayer.Builder(context).build()
val player = CastPlayer.Builder(context)
.setLocalPlayer(exoPlayer).build()
mediaSession = MediaSession.Builder(context,player)
.build()Player(
exoPlayer,
centerControls = {}
bottomControls = { /* customized */ },
)Player(
castPlayer,
centerControls = {}
bottomControls = { /* customized */ },
topControls = {
PlayerDefaults.TopControls(castPlayer) {
MediaRouteButton(
Modifier.align(Alignment.TopEnd))
}
},
)public interface FrameProcessor
extends AutoCloseable {
interface Listener {
void onWakeup();
void onError(
VideoFrameProcessingException exception);
void onFrameProcessed(
Frame frame,
@Nullable SyncFenceWrapper onCompleteFence);
}
interface Factory {
FrameProcessor create(
FrameWriter output,
Executor listenerExecutor,
Listener listener);
}
boolean queue(List<AsyncFrame> frames);
void signalEndOfStream();
}val frameProcessorFactory = EnhancementSessionDecorator.Builder(/* context = */ this, baseGlFactory)
.setTonemappingEnabled(true)
.setDeblurAndDenoiseEnabled(true)
.setUpscaleVideoEnabled(true)
.build()
val transformerBuilder = Transformer.Builder(/* context = */ this)
.setFrameProcessorFactory(frameProcessorFactory)val editedMediaItem =
EditedMediaItem.Builder(MediaItem.fromUri(media.uri))
.setEffects(
Effects(
listOf(
soundSeparationAudioProcessor
),
emptyList(), // video effects
)
)
.build()val soundSeparationAudioProcessor =
SoundSeparationAudioProcessor(
getApplication(),
mainExecutor,
object : SoundSeparationAudioProcessor.Listener {
override fun onResult(channelToAudioType: SparseIntArray) {
for (i in 0 until channelToAudioType.size()) {
val channelIndex = channelToAudioType.keyAt(i)
val audioType = channelToAudioType.valueAt(i)
val label = toAudioTypeName(audioType)
detectedChannelVolumes.add(ChannelVolume(channelIndex, audioType, label))
}
}
},
)MetadataRetriever.Builder(context, mediaItem).build().use { retriever ->
// 1. Duration (in microseconds)
val durationUs = retriever.retrieveDurationUs().await()
// 2. Track formats & container metadata (reuses the same preparation pass)
val trackGroups = retriever.retrieveTrackGroups().await()
for (i in 0 until trackGroups.length) {
val format = trackGroups[i].getFormat(0)
// Video/Audio properties: format.width, format.height, format.colorInfo …
// Rich metadata: SlowMotionData, MotionPhotoMetadata, Mp4TimestampData …
val metadata = format.metadata
}
// 3. Stream structure & manifest info
val timeline = retriever.retrieveTimeline().await()
}FrameExtractor.Builder(context, mediaItem)
.setSeekParameters(SeekParameters.CLOSEST_SYNC) // Fast keyframe seeking
.setEffects(listOf(Presentation.createForHeight(360))) // GPU downscaling
.setExtractHdrFrames(true) // Optional: HLG HDR or UltraHDR
.build()
.use { extractor ->
// Representative thumbnail
val thumbnail = extractor.getThumbnail().await()
// Frame at specific timestamp (bitmap + actual presentationTimeMs)
val frame = extractor.getFrame(positionMs = 5_000L).await()
}val config = CodecDb.onDevice(context)
.forEncodingFallingBackTo {
constraints -> createFallbackConfig(constraints)
}
.findOptimalSettings()
.forEditing()
.withResolution(1920, 1080, /* frameRate= */ 30.0)
.withAnyCodec()
.targetingQuality(VideoEncoder.QUALITY_LEVEL_VERY_GOOD)
.get()