You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

135 lines
4.7 KiB

package org.jetbrains.compose.videoplayer
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import uk.co.caprica.vlcj.factory.MediaPlayerFactory
import uk.co.caprica.vlcj.player.base.MediaPlayer
import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter
import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer
import kotlin.math.roundToInt
// Same as MediaPlayerComponentDefaults.EMBEDDED_MEDIA_PLAYER_ARGS
private val PLAYER_ARGS = listOf(
"--video-title=vlcj video output",
"--no-snapshot-preview",
"--quiet",
"--intf=dummy"
)
@Composable
internal actual fun VideoPlayerImpl(
url: String,
isResumed: Boolean,
volume: Float,
speed: Float,
seek: Float,
isFullscreen: Boolean,
progressState: MutableState<Progress>,
modifier: Modifier,
onFinish: (() -> Unit)?
) {
val mediaPlayerFactory = remember { MediaPlayerFactory(PLAYER_ARGS) }
val mediaPlayer = remember {
mediaPlayerFactory
.mediaPlayers()
.newEmbeddedMediaPlayer()
}
val surface = remember {
SkiaBitmapVideoSurface().also {
mediaPlayer.videoSurface().set(it)
}
}
mediaPlayer.emitProgressTo(progressState)
mediaPlayer.setupVideoFinishHandler(onFinish)
LaunchedEffect(url) { mediaPlayer.media().play/*OR .start*/(url) }
LaunchedEffect(seek) { mediaPlayer.controls().setPosition(seek) }
LaunchedEffect(speed) { mediaPlayer.controls().setRate(speed) }
LaunchedEffect(volume) { mediaPlayer.audio().setVolume(volume.toPercentage()) }
LaunchedEffect(isResumed) { mediaPlayer.controls().setPause(!isResumed) }
LaunchedEffect(isFullscreen) {
if (mediaPlayer is EmbeddedMediaPlayer) {
/*
* To be able to access window in the commented code below,
* extend the player composable function from WindowScope.
* See https://github.com/JetBrains/compose-jb/issues/176#issuecomment-812514936
* and its subsequent comments.
*
* We could also just fullscreen the whole window:
* `window.placement = WindowPlacement.Fullscreen`
* See https://github.com/JetBrains/compose-multiplatform/issues/1489
*/
// mediaPlayer.fullScreen().strategy(ExclusiveModeFullScreenStrategy(window))
mediaPlayer.fullScreen().toggle()
}
}
DisposableEffect(Unit) {
onDispose {
mediaPlayer.release()
mediaPlayerFactory.release()
}
}
Box(modifier = modifier) {
surface.bitmap.value?.let { bitmap ->
Image(
bitmap,
modifier = Modifier
.background(Color.Transparent)
.fillMaxSize(),
contentDescription = null,
contentScale = ContentScale.Fit,
alignment = Alignment.Center,
)
}
}
}
private fun Float.toPercentage(): Int = (this * 100).roundToInt()
/**
* We play the video again on finish (so the player is kind of idempotent),
* unless the [onFinish] callback stops the playback.
* Using `mediaPlayer.controls().repeat = true` did not work as expected.
*/
@Composable
private fun MediaPlayer.setupVideoFinishHandler(onFinish: (() -> Unit)?) {
DisposableEffect(onFinish) {
val listener = object : MediaPlayerEventAdapter() {
override fun stopped(mediaPlayer: MediaPlayer) {
onFinish?.invoke()
mediaPlayer.controls().play()
}
}
events().addMediaPlayerEventListener(listener)
onDispose { events().removeMediaPlayerEventListener(listener) }
}
}
/**
* Checks for and emits video progress every 50 milliseconds.
* Note that it seems vlcj updates the progress only every 250 milliseconds or so.
*
* Instead of using `Unit` as the `key1` for [LaunchedEffect],
* we could use `media().info()?.mrl()` if it's needed to re-launch
* the effect (for whatever reason) when the url (aka video) changes.
*/
@Composable
private fun MediaPlayer.emitProgressTo(state: MutableState<Progress>) {
LaunchedEffect(key1 = Unit) {
while (isActive) {
val fraction = status().position()
val time = status().time()
state.value = Progress(fraction, time)
delay(50)
}
}
}