Podstawy aplikacji

Optymalizowanie odtwarzania multimediów i maksymalizowanie zasięgu dzięki interfejsom Jetpack Media3 API

Głośniki

Informacje

Informacje o wystąpieniu

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.

Podsumowanie

  • Media3 UI Compose udostępnia gotowe komponenty odtwarzacza i konfigurowalne elementy składowe. Możesz zastąpić określone miejsca na elementy sterujące, pozwalając bibliotece zarządzać stanem odtwarzacza i przejściami.
  • Nowo napisany CastPlayer zastępuje samodzielny pakiet SDK Cast. Implementuje standardowy interfejs odtwarzacza i automatycznie przekazuje odtwarzanie między lokalnymi instancjami ExoPlayera a zdalnymi urządzeniami Cast.
  • Przebudowany proces edycji jest odłączony od OpenGL i bezpośrednio zarządza buforami sprzętowymi. Takie podejście bez kopiowania zmniejsza obciążenie pamięci, a rozdzielenie pozwala deweloperom integrować nowoczesne platformy graficzne, takie jak WebGPU.
  • Media3 dodaje gotową obsługę Eclipsa Video i Eclipsa Audio na Androidzie 17 i nowszych. Preskryptywne metadane HDR dostosowują jasne obszary do jasności otoczenia, a dźwięk przestrzenny obsługuje wiele mieszanych prezentacji.
  • Wbudowane optymalizacje, takie jak odciążanie dźwięku PCM, zmniejszają zużycie energii przy wyłączonym ekranie nawet o 20%. Nadchodząca baza danych CodecDB wykorzystuje dane analizy porównawczej do wybierania optymalnych ustawień kodera dla urządzenia.

Zasoby

Gemini Notebook

Tematy

  • jetpack-media3
  • exoplayer
  • jetpack-compose
  • google-cast
  • edytowanie filmów
  • audio
  • wydajność

Transkrypcja

Pobierz transkrypcję

Wprowadzenie do Jetpack Media3

  • 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.

Interfejs multimediów w Jetpack Compose

  • 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.

Udostępnianie odtwarzania za pomocą MediaSession

  • 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.

Interfejsy API Media3 do 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.

Standardy transmisji strumieniowej i nowe formaty multimedialne

  • 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.

Optymalizacje wydajności i baterii

  • 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.

Podsumowanie i wnioski

  • 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.

Kod

Fragmenty kodu

Fragment kodu w języku Kotlin w filmie „Media UI with Jetpack Compose” (Interfejs multimedialny w Jetpack Compose) od 02:31
@Composable
fun videoScreen() {
  Player(viewModel.exoPlayer)
}
Fragment kodu w języku Kotlin w 04:44 w filmie „Interfejs multimedialny w Jetpack Compose”
Player(viewModel.exoPlayer)
Fragment kodu w Kotlinie w 04:48 w filmie „Interfejs multimediów w Jetpack Compose”
Player(
  player = viewModel.exoPlayer,
  bottomControls = {
    PlayerDefaults.BottomControls(
      player,
      left = {}, right = {},
      above = {
        PositionAndDurationText(
          player, separator = " • ")
      },
    )
  },
)
Fragment kodu w języku Kotlin w 05:13 w filmie „Interfejs multimediów w Jetpack Compose”
Player(
  player = viewModel.exoPlayer,
  centerControls = {},
  bottomControls = { ...
      above = {
        Row(Modifier.fillMaxWidth()) {
          PositionAndDurationText(...)
          Spacer(Modifier.weight(1f))
          PlaybackSpeedToggleButton(player)
          MuteButton(player)
          PlayPauseButton(player)
        }
      },
    )
  }
Fragment kodu w języku Kotlin w 05:51 w filmie „Media UI with Jetpack Compose”
Player(
  ...
  bottomControls = { ...
      above = {
          ...
          MuteButton(player,
            painter = { painterResource(
              if (showMuted)
                R.drawable.muted
              else
                R.drawable.unmuted )})
      }
  },
)
Fragment kodu w Kotlinie w 07:01 w filmie „Udostępnianie odtwarzania za pomocą MediaSession”
MediaSession.Builder(context, player).build()
Fragment kodu w Kotlinie w 07:17 w filmie „Udostępnianie odtwarzania za pomocą MediaSession”
class PlaybackService : MediaSessionService() {

  private lateinit var mediaSession:
        MediaSession

  override fun onGetSession(
        info: ControllerInfo) : MediaSession?
    = mediaSession
}
Fragment kodu w Kotlinie w 08:25 w filmie „Udostępnianie odtwarzania za pomocą MediaSession”
val player = ExoPlayer.Builder(context).build()

mediaSession = MediaSession.Builder(context,player)
.build()
Fragment kodu w języku Kotlin w 08:29 w filmie „Udostępnianie odtwarzania za pomocą MediaSession”
Cast.getSingletonInstance(context).initialize()

val player = CastPlayer.Builder(context).build()

mediaSession = MediaSession.Builder(context,player)
.build()
Fragment kodu w języku Kotlin w 08:45 w filmie „Udostępnianie odtwarzania za pomocą MediaSession”
Cast.getSingletonInstance(context).initialize()
val exoPlayer = ExoPlayer.Builder(context).build()
val player = CastPlayer.Builder(context)
                .setLocalPlayer(exoPlayer).build()
mediaSession = MediaSession.Builder(context,player)
.build()
Fragment kodu w języku Kotlin w 09:32 w filmie „Udostępnianie odtwarzania za pomocą MediaSession”
Player(
  exoPlayer,
  centerControls = {}
  bottomControls = { /* customized */ },
)
Fragment kodu w języku Kotlin w 09:40 w filmie „Udostępnianie odtwarzania za pomocą MediaSession”
Player(
  castPlayer,
  centerControls = {}
  bottomControls = { /* customized */ },
  topControls = {
    PlayerDefaults.TopControls(castPlayer) {
      MediaRouteButton(
        Modifier.align(Alignment.TopEnd))
    }
  },
)
Fragment kodu w języku Java w 13:02 w filmie „Interfejsy API Media3 do edycji”
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();
}
Fragment kodu w języku Kotlin w 14:45 w filmie „Interfejsy API Media3 do edycji”
val frameProcessorFactory = EnhancementSessionDecorator.Builder(/* context = */ this, baseGlFactory)
    .setTonemappingEnabled(true)
    .setDeblurAndDenoiseEnabled(true)
    .setUpscaleVideoEnabled(true)
    .build()

val transformerBuilder = Transformer.Builder(/* context = */ this)
    .setFrameProcessorFactory(frameProcessorFactory)
Fragment kodu w Kotlinie w 15:36 w filmie „Interfejsy API Media3 do edycji”
val editedMediaItem =
     EditedMediaItem.Builder(MediaItem.fromUri(media.uri))
       .setEffects(
         Effects(
           listOf(
             soundSeparationAudioProcessor
           ),
           emptyList(), // video effects
         )
       )
       .build()
Fragment kodu w Kotlinie w 15:50 w filmie „Interfejsy API Media3 do edycji”
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))
            }
          }
        },
      )
Fragment kodu w języku Kotlin w filmie „Interfejsy API Media3 do edycji” (17:29)
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()
}
Fragment kodu w języku Kotlin w 18:06 w filmie „Interfejsy API Media3 do edycji”
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()
    }
Fragment kodu w Kotlinie w 28:26 w filmie „Optymalizacja wydajności i baterii”
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()