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.
@Composable
fun 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.Log
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem.fromUri
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.StyledPlayerView
@Composable
fun 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 🙂
Ďalšie články zo série Jetpack Compose Basics:
Zdroje: