19. Dec 2022Android

Jetpack Compose Basics - Ako na prehrávanie videa pomocou knižnice Exoplayer

Jednou z veľmi častých požiadaviek, naprieč rôznymi Android aplikáciami, je prehrávanie videa. Knižnica Exoplayer je jedna z najpopulárnejších knižníc určených pre splnenie tejto úlohy. V tomto článku sa pozrieme na to ako ju použiť a implementovať v Jetpack Compose.

Paulina SlavikováAndroid developer

Prečo práve Exoplayer?

Možno niektorí z Vás krútia hlavou nad tým, že potrebujeme použiť na takúto bežnú úlohu nejakú externú knižnicu. Nuž, Android nám síce dáva k dispozícií triedu MediaPlayer, avšak jej možnosti nie sú vo väčšine prípadov postačujúce.

Exoplayer je open-source knižnica od Google, ktorá je na rozdiel od MediaPlayer stabilnejšia, oveľa viac prispôsobiteľná a jej použitie je jednoduchšie.

Jetpack Compose a Exoplayer

Prvý krok, ktorý potrebujeme urobiť je pridanie novej knižnice už do existujúceho projektu. Aktuálne je posledná verzia knižnice 2.18.1 . Najaktuálnejšiu releasovú verziu si môžete skontrolovať tu https://github.com/google/ExoPlayer/releases .

implementation 'com.google.android.exoplayer:exoplayer:2.18.1'

Ďalším krokom je vytvorenie @Composable funkcie, ktorá bude roztiahnutá na celú plochu obrazovky a bude predstavovať priestor pre umiestnenie prehrávača. V tejto composable vytvoríme objekt Exoplayer pomocou volania ExoPlayer.Builder, do ktorého potrebujeme poslať context danej composable. Následne ho dodatočne upravíme:

  • Najdôležitejším krokom je zavolať funkciu setMediaItem, ktorá pomocou funkcie fromUri vytvorí zo stringovej hodnoty videoURL objekt MediaItem, ktorý dokáže prehrávač prehrať. Volanie tejto funkcie vymaže akýkoľvek predošle nastavený playlist a resetne pozíciu prehrávača na pôvodný stav. Nesmieme zabudnúť pridať do súboru manifest toto povolenie <uses-permission android:name="android.permission.INTERNET"/>
  • Nastavením playWhenReady  na true prehrávač spustí video automaticky.
  • Zavolaním prepare funkcie, prehrávač začne načítať médiá a získavať zdroje potrebné na prehrávanie.
@Composablefun ExoPlayerComp() {    Surface(        modifier = Modifier.fillMaxSize(),        color = Color.Black    ) {        val videoURL = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"        val context = LocalContext.current        val exoPlayer = ExoPlayer.Builder(context)            .build()            .apply {                setMediaItem(fromUri(videoURL))                playWhenReady = true                prepare()           }    }}

Keď máme objekt exoplayer správne nastavený, tak musíme vytvoriť nejakú @Composable, ktorá bude obsahovať UI prehrávaného videa a jeho ovládacie prvky. V súčasnosti knižnica Exoplayer ešte nie je prispôsobená pre Compose, no dokážeme si ju prispôsobiť  sami 🙂 a to pomocou @Composable AndroidView, do ktorej vieme vložiť klasické view aké by sme použili v prípade použitia xml, v našom prípade StyledPlayerView.

AndroidView(    modifier = Modifier.fillMaxSize(),    factory = {        StyledPlayerView(context).apply {           resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT           player = exoPlayer    }})

Pomocou funkcie apply vieme tomuto "composed view" nastaviť rovnaké parametre, aké by sme vedeli nastaviť pre view v xml súbore. Podľa parametra resizeMode vieme nastaviť rozpätie videa. V tomto prípade sa veľkosť videa prispôsobuje rozmeru obrazovky so zachovaním pôvodného pomeru strán. Do parametra player vložíme objekt exoplayer, ktorý sme vytvorili v predchádzajúcom kroku. Teraz máme všetko hotové a video môžeme spustiť!

… lenže …

… ak sa vrátite na predchádzajúcu obrazovku, vidíte že video sa stále prehráva na pozadí, aj keď obrazovka už nie je súčasťou kompozície. Je to preto, lebo prehrávač nebol správne releasnutý a neuvoľnil prostriedky, ktoré používal. Tento problém vieme vyriešiť pomocou DisposableEffect efektu, ktorý nám poskytuje Compose API. Pre naše použitie nám postačuje o tomto efekte vedieť len to, že telo efektu sa vykoná vždy keď dôjde ku zmene jeho parametru key1, a že callback metóda onDispose{} sa vykoná vždy, keď composable opustí kompozíciu. A táto funkcia je pre nás kľúčová. Práve v tejto časti vieme zavolať exoPlayer.release().

DisposableEffect(    key1 = AndroidView(        modifier = Modifier.fillMaxSize(),        factory = {            StyledPlayerView(context).apply {                resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT                player = exoPlayer            }         }),    effect = {        onDispose {            exoPlayer.release()         }     })

Teraz keď sa vrátime na predchádzajúcu obrazovku, video prestane hrať, avšak ak aplikáciu dáme len do pozadia, video sa bude prehrávať naďalej, pretože je stále súčasťou kompozície a callback onDispose{} nebol zavolaný. Na vyriešenie tohto problému potrebujeme získať inštanciu triedy LocalLifecycleOwner, ktorú potrebujeme na počvanie zmien životného cyklu aktivity.

val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)

Následne vytvoríme v DisposableEffect implementáciu LifecycleEventObserver, ktorý prehrávač buď stopne alebo spustí, podľa toho či je aplikácia na popredí alebo v pozadí. Tento observer musíme naviazať na životný cyklus aktivity a takisto ho musíme odstrániť pri opustení kompozície, opäť v callbacku onDispose{}.

Celý kód po úprave je tu 🙂

import android.util.Logimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.material.Surfaceimport androidx.compose.runtime.*import androidx.compose.ui.Modifierimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.platform.LocalLifecycleOwnerimport androidx.compose.ui.viewinterop.AndroidViewimport androidx.lifecycle.Lifecycleimport androidx.lifecycle.LifecycleEventObserverimport com.google.android.exoplayer2.ExoPlayerimport com.google.android.exoplayer2.MediaItem.fromUriimport com.google.android.exoplayer2.ui.AspectRatioFrameLayoutimport com.google.android.exoplayer2.ui.StyledPlayerView@Composablefun ExoPlayerComp() {    Surface(        modifier = Modifier.fillMaxSize(),        color = Color.Black    ) {        val videoURL       = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"        val context        = LocalContext.current        val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)        val exoPlayer = ExoPlayer.Builder(context)            .build()            .apply {                setMediaItem(fromUri(videoURL))                playWhenReady    = true                prepare()            }        DisposableEffect(            key1 = AndroidView(                modifier = Modifier.fillMaxSize(),                factory = {                    StyledPlayerView(context).apply {                        resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT                        player = exoPlayer                    }                }),            effect = {                val observer = LifecycleEventObserver { _, event ->                    when (event) {                        Lifecycle.Event.ON_RESUME -> {                            Log.e("LIFECYCLE", "resumed")                            exoPlayer.play()                        }                        Lifecycle.Event.ON_PAUSE  -> {                            Log.e("LIFECYCLE", "paused")                            exoPlayer.stop()                        }                    }                }                val lifecycle = lifecycleOwner.value.lifecycle                lifecycle.addObserver(observer)                onDispose {                    exoPlayer.release()                    lifecycle.removeObserver(observer)                }            }        )    }}

To je všetko. Implementácia nie je úplne triviálna, no dúfam že vám pomôže vo vašom projekte 🙂

Zdroje:

Pridaj sa k nám

Jozef Knažko – Head of Android

"GoodRequest tvorí tím ľudí, ktorí nehľadajú výhovorky, ale riešenia. Máme spoločné ciele a nepozeráme sa len na seba, ale na to, čo môžeme spoločne dosiahnuť."

Paulina SlavikováAndroid developer