From f9eff37972e375a3528bb6360dbb6a74f08e93ce Mon Sep 17 00:00:00 2001 From: DatLag Date: Tue, 21 Nov 2023 18:50:25 +0100 Subject: [PATCH] finished video player on android and desktop --- .../ui/screen/video/VideoScreen.android.kt | 3 +- .../burningseries/common/ExtendNumber.kt | 13 +++++++ .../initial/series/component/EpisodeItem.kt | 36 +++++++++++++---- .../ui/screen/video/VideoComponent.kt | 2 + .../ui/screen/video/VideoScreenComponent.kt | 12 ++++++ .../commonMain/resources/MR/base/strings.xml | 2 + .../ui/screen/video/VideoControls.kt | 15 ++++++- .../ui/screen/video/VideoPlayer.kt | 39 +++++++++++-------- .../burningseries/database/BurningSeries.sq | 3 ++ 9 files changed, 99 insertions(+), 26 deletions(-) create mode 100644 app/shared/src/commonMain/kotlin/dev/datlag/burningseries/common/ExtendNumber.kt diff --git a/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoScreen.android.kt b/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoScreen.android.kt index d6b6d43d..23891d17 100644 --- a/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoScreen.android.kt +++ b/app/shared/src/androidMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoScreen.android.kt @@ -75,6 +75,7 @@ actual fun VideoScreen(component: VideoComponent) { val mediaItem = remember(streamList, streamIndex, sourceIndex) { MediaItem.fromUri(streamList[streamIndex].list[sourceIndex]) } + val startingPos by component.startingPos.collectAsStateWithLifecycle() val castState by remember(castContext) { mutableStateOf(castContext?.castState) } val casting by remember(castState) { mutableStateOf(castState == CastState.CONNECTED || castState == CastState.CONNECTING) } @@ -221,7 +222,7 @@ actual fun VideoScreen(component: VideoComponent) { } else { mediaItem } - usingPlayer.setMediaItem(media) + usingPlayer.setMediaItem(media, startingPos) usingPlayer.prepare() withIOContext { diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/common/ExtendNumber.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/common/ExtendNumber.kt new file mode 100644 index 00000000..63312427 --- /dev/null +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/common/ExtendNumber.kt @@ -0,0 +1,13 @@ +package dev.datlag.burningseries.common + +fun Long.toDuration(): String { + val duration = this / 1000 + val hours = duration / 3600 + val minutes = (duration - hours * 3600) / 60 + val seconds = duration - (hours * 3600 + minutes * 60) + return if (hours > 0) { + "%02d:%02d:%02d".format(hours.toInt(), minutes.toInt(), seconds.toInt()) + } else { + "%02d:%02d".format(minutes.toInt(), seconds.toInt()) + } +} \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/component/EpisodeItem.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/component/EpisodeItem.kt index 3b3e486d..09530718 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/component/EpisodeItem.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/initial/series/component/EpisodeItem.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp @@ -32,18 +33,24 @@ import dev.datlag.burningseries.common.* import dev.datlag.burningseries.database.Episode import dev.datlag.burningseries.model.Series import dev.datlag.burningseries.ui.theme.TopLeftBottomRightRoundedShape +import dev.icerock.moko.resources.compose.stringResource import io.github.aakira.napier.Napier import kotlinx.coroutines.delay import kotlin.math.roundToInt +import dev.datlag.burningseries.SharedRes @Composable fun EpisodeItem(content: Series.Episode, dbEpisode: Episode?, isLoading: Boolean, onClick: () -> Unit) { val blurHash = remember(content.href) { BlurHash.random() } val enabled = content.hosters.isNotEmpty() - val isFinished = remember(dbEpisode) { - val length = dbEpisode?.length ?: 0L - val progress = dbEpisode?.progress ?: 0L + val length = remember(dbEpisode) { + dbEpisode?.length ?: 0L + } + val progress = remember(dbEpisode) { + dbEpisode?.progress ?: 0L + } + val isFinished = remember(length, progress) { if (length != 0L || progress != 0L) { Napier.e("Length: $length") Napier.e("Progress: $progress") @@ -110,9 +117,24 @@ fun EpisodeItem(content: Series.Episode, dbEpisode: Episode?, isLoading: Boolean CircularProgressIndicator() } } - Text( - text = content.episodeTitle, - maxLines = 3 - ) + Column( + modifier = Modifier.fillMaxHeight().weight(1F) + ) { + Box( + modifier = Modifier.fillMaxWidth().weight(1F), + contentAlignment = Alignment.CenterStart + ) { + Text( + text = content.episodeTitle, + maxLines = 3 + ) + } + if (length != 0L && progress != 0L) { + Text( + text = stringResource(SharedRes.strings.episode_progress, progress.toDuration(), length.toDuration()), + style = MaterialTheme.typography.labelSmall + ) + } + } } } \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoComponent.kt index de09c83a..d9069fa4 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoComponent.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoComponent.kt @@ -11,6 +11,8 @@ interface VideoComponent : Component { val episode: StateFlow val streams: List + val startingPos: StateFlow + fun back() fun ended() fun lengthUpdate(millis: Long) diff --git a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoScreenComponent.kt b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoScreenComponent.kt index 8336bc0c..5c6e2636 100644 --- a/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoScreenComponent.kt +++ b/app/shared/src/commonMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoScreenComponent.kt @@ -1,6 +1,8 @@ package dev.datlag.burningseries.ui.screen.video import androidx.compose.runtime.Composable +import app.cash.sqldelight.coroutines.asFlow +import app.cash.sqldelight.coroutines.mapToOneOrNull import com.arkivanov.decompose.ComponentContext import com.arkivanov.essenty.backhandler.BackCallback import dev.datlag.burningseries.common.ioScope @@ -12,6 +14,7 @@ import dev.datlag.burningseries.model.Stream import dev.datlag.burningseries.model.state.EpisodeState import dev.datlag.burningseries.network.state.EpisodeStateMachine import dev.datlag.burningseries.ui.theme.SchemeTheme +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import org.kodein.di.DI @@ -32,6 +35,15 @@ class VideoScreenComponent( override val episode: StateFlow = episodeStateMachine.state.mapNotNull { it as? EpisodeState.EpisodeHolder }.map { it.episode }.stateIn(ioScope(), SharingStarted.WhileSubscribed(), initialEpisode) private val database by di.instance() + private val dbEpisode = episode.transform { + return@transform emitAll(database.burningSeriesQueries.selectEpisodeByHref(it.href).asFlow().mapToOneOrNull( + currentCoroutineContext() + )) + }.stateIn(ioScope(), SharingStarted.Lazily, database.burningSeriesQueries.selectEpisodeByHref(episode.value.href).executeAsOneOrNull()) + + override val startingPos: StateFlow = dbEpisode.transform { + return@transform emit(it?.progress ?: 0L) + }.stateIn(ioScope(), SharingStarted.Lazily, dbEpisode.value?.progress ?: 0L) private val backPressCounter = MutableStateFlow(0) diff --git a/app/shared/src/commonMain/resources/MR/base/strings.xml b/app/shared/src/commonMain/resources/MR/base/strings.xml index 3b4c3878..882e058e 100644 --- a/app/shared/src/commonMain/resources/MR/base/strings.xml +++ b/app/shared/src/commonMain/resources/MR/base/strings.xml @@ -44,4 +44,6 @@ Pause Play Fullscreen + Exit Fullscreen + %s - %s \ No newline at end of file diff --git a/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoControls.kt b/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoControls.kt index 60e40ba6..4f136de7 100644 --- a/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoControls.kt +++ b/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoControls.kt @@ -9,10 +9,12 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowPlacement import dev.datlag.burningseries.LocalWindow import dev.datlag.burningseries.SharedRes +import dev.datlag.burningseries.common.toDuration import dev.icerock.moko.resources.compose.stringResource @Composable @@ -29,7 +31,7 @@ fun VideoControls( derivedStateOf { mediaPlayer.length.value } } val window = LocalWindow.current - val originalPlacement = remember(window) { window.placement } + var originalPlacement = remember(window) { window.placement } Row( modifier = Modifier.fillMaxWidth(), @@ -83,6 +85,11 @@ fun VideoControls( tint = Color.White ) } + Text( + text = time.toDuration(), + textAlign = TextAlign.Center, + color = Color.White + ) Slider( modifier = Modifier.weight(1F), value = time.toDouble().toFloat(), @@ -96,6 +103,11 @@ fun VideoControls( inactiveTrackColor = Color.White.copy(alpha = 0.2F) ) ) + Text( + text = length.toDuration(), + textAlign = TextAlign.Center, + color = Color.White + ) if (window.placement == WindowPlacement.Fullscreen) { IconButton( onClick = { @@ -111,6 +123,7 @@ fun VideoControls( } else { IconButton( onClick = { + originalPlacement = window.placement window.placement = WindowPlacement.Fullscreen } ) { diff --git a/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoPlayer.kt b/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoPlayer.kt index e542600f..e9fddd13 100644 --- a/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoPlayer.kt +++ b/app/shared/src/desktopMain/kotlin/dev/datlag/burningseries/ui/screen/video/VideoPlayer.kt @@ -1,14 +1,12 @@ package dev.datlag.burningseries.ui.screen.video -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.awt.SwingPanel import androidx.compose.ui.graphics.Color +import dev.datlag.burningseries.common.lifecycle.collectAsStateWithLifecycle import org.apache.commons.lang3.SystemUtils import uk.co.caprica.vlcj.factory.discovery.NativeDiscovery import uk.co.caprica.vlcj.player.base.MediaPlayer @@ -25,7 +23,7 @@ fun VideoPlayer( val foundVlc = NativeDiscovery().discover() if (foundVlc) { - val mediaPlayer = remember { + val mediaPlayerComponent = remember { if (SystemUtils.IS_OS_MAC) { CallbackMediaPlayerComponent() } else { @@ -40,7 +38,7 @@ fun VideoPlayer( background = Color.Black, modifier = Modifier.fillMaxSize(), factory = { - mediaPlayer + mediaPlayerComponent } ) } @@ -53,6 +51,7 @@ fun VideoPlayer( val headers = remember(streamIndex) { streamList[streamIndex].headers } + val startingPos by component.startingPos.collectAsStateWithLifecycle() val isPlaying = remember { mutableStateOf(false) } val length = remember { mutableLongStateOf(0) } @@ -100,46 +99,52 @@ fun VideoPlayer( isPlaying.value = false } + + override fun opening(mediaPlayer: MediaPlayer?) { + super.opening(mediaPlayer) + + (mediaPlayer ?: mediaPlayerComponent.mediaPlayer())?.controls()?.setTime(startingPos) + } } } - LaunchedEffect(mediaPlayer, eventListener) { - mediaPlayer.mediaPlayer()?.events()?.addMediaPlayerEventListener(eventListener) + LaunchedEffect(mediaPlayerComponent, eventListener) { + mediaPlayerComponent.mediaPlayer()?.events()?.addMediaPlayerEventListener(eventListener) } SideEffect { - applyHeaders(headers, mediaPlayer.mediaPlayer()) - mediaPlayer.mediaPlayer()?.media()?.play(url) + applyHeaders(headers, mediaPlayerComponent.mediaPlayer()) + mediaPlayerComponent.mediaPlayer()?.media()?.play(url) } - DisposableEffect(mediaPlayer) { + DisposableEffect(mediaPlayerComponent) { onDispose { - mediaPlayer.mediaPlayer()?.release() + mediaPlayerComponent.mediaPlayer()?.release() } } - return remember(mediaPlayer) { object : dev.datlag.burningseries.ui.screen.video.MediaPlayer { + return remember(mediaPlayerComponent) { object : dev.datlag.burningseries.ui.screen.video.MediaPlayer { override val isPlaying: MutableState = isPlaying override val length: MutableLongState = length override val time: MutableLongState = time override fun play() { - mediaPlayer.mediaPlayer()?.controls()?.play() + mediaPlayerComponent.mediaPlayer()?.controls()?.play() } override fun pause() { - mediaPlayer.mediaPlayer()?.controls()?.pause() + mediaPlayerComponent.mediaPlayer()?.controls()?.pause() } override fun rewind() { - mediaPlayer.mediaPlayer()?.controls()?.skipTime(-10000) + mediaPlayerComponent.mediaPlayer()?.controls()?.skipTime(-10000) } override fun forward() { - mediaPlayer.mediaPlayer()?.controls()?.skipTime(10000) + mediaPlayerComponent.mediaPlayer()?.controls()?.skipTime(10000) } override fun seekTo(millis: Long) { - mediaPlayer.mediaPlayer()?.controls()?.setTime(millis) + mediaPlayerComponent.mediaPlayer()?.controls()?.setTime(millis) } } } } diff --git a/database/src/commonMain/bs/dev/datlag/burningseries/database/BurningSeries.sq b/database/src/commonMain/bs/dev/datlag/burningseries/database/BurningSeries.sq index 429034e4..584e3899 100644 --- a/database/src/commonMain/bs/dev/datlag/burningseries/database/BurningSeries.sq +++ b/database/src/commonMain/bs/dev/datlag/burningseries/database/BurningSeries.sq @@ -49,5 +49,8 @@ updateEpisodeProgress { INSERT OR IGNORE INTO Episode (href, number, title, length, progress, seriesHref) VALUES (:href, :number, :title, :length, :progress, :seriesHref); } +selectEpisodeByHref: +SELECT * FROM Episode WHERE href = :href OR href LIKE :href; + selectEpisodesBySeriesHref: SELECT * FROM Episode WHERE seriesHref = :href OR seriesHref LIKE :href; \ No newline at end of file