19. Dec 2022
AndroidJetpack 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.
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 🙂