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
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) |
|
} |
|
} |
|
}
|
|
|