Skip to content

Commit

Permalink
feat: screenshot by vlc video player in desktop (#620)
Browse files Browse the repository at this point in the history
* feat: add button for screenshot.

* feat: add screenshot methods for PlayerState

* style: adjust rhs bar in VideoScaffold

* feat: vlc player screenshot

* optimize: save screenshot file logic

* optimize: 抽取截图逻辑到vm层

* feat: add exo player screenshot methods.

* style: 调整截屏按钮只在Desktop显示

* optimize: 去掉截图文件名的当前时间戳

* optimize: 文件名的当前时间转换成时分秒毫秒字符串

* fix: build issue in VideoScaffold.android.kt

* optimize: adjust code

* optimize: adjust code

* optimize: adjust code

* optimize: adjust code

* optimize: adjust code
  • Loading branch information
ChiveHao authored Jul 27, 2024
1 parent acea6c6 commit 65d0738
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 5 deletions.
10 changes: 10 additions & 0 deletions app/shared/src/commonMain/kotlin/ui/subject/episode/EpisodePage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,16 @@ private fun EpisodeVideo(
},
onShowMediaSelector = { isMediaSelectorVisible = true },
onShowSelectEpisode = { isEpisodeSelectorVisible = true },
onClickScreenshot = {
val currentPositionMillis = vm.playerState.currentPositionMillis.value
val min = currentPositionMillis / 60000
val sec = (currentPositionMillis - (min * 60000)) / 1000
val ms = currentPositionMillis - (min * 60000) - (sec * 1000)
val currentPosition = "${min}m${sec}s${ms}ms"
// 条目ID-剧集序号-视频时间点.png
val filename = "${vm.subjectId}-${vm.episodePresentation.ep}-${currentPosition}.png"
vm.playerState.saveScreenshotFile(filename)
},
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import me.him188.ani.app.videoplayer.ui.VideoPlayer
import me.him188.ani.app.videoplayer.ui.VideoScaffold
import me.him188.ani.app.videoplayer.ui.guesture.GestureLock
import me.him188.ani.app.videoplayer.ui.guesture.LockableVideoGestureHost
import me.him188.ani.app.videoplayer.ui.guesture.ScreenshotButton
import me.him188.ani.app.videoplayer.ui.guesture.rememberGestureIndicatorState
import me.him188.ani.app.videoplayer.ui.guesture.rememberPlayerFastSkipState
import me.him188.ani.app.videoplayer.ui.guesture.rememberSwipeSeekerState
Expand Down Expand Up @@ -95,6 +96,7 @@ internal fun EpisodeVideoImpl(
sideSheets: @Composable () -> Unit,
onShowMediaSelector: () -> Unit,
onShowSelectEpisode: () -> Unit,
onClickScreenshot: () -> Unit,
modifier: Modifier = Modifier,
maintainAspectRatio: Boolean = !expanded,
) {
Expand Down Expand Up @@ -204,7 +206,14 @@ internal fun EpisodeVideoImpl(
)
}
},
rhsBar = {
rhsButtons = {
if (expanded && currentPlatform.isDesktop()) {
ScreenshotButton(
onClick = onClickScreenshot,
)
}
},
gestureLock = {
if (expanded) {
GestureLock(isLocked = isLocked, onClick = { isLocked = !isLocked })
}
Expand Down
3 changes: 3 additions & 0 deletions app/shared/video-player/android/PlayerState.android.kt
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,9 @@ internal class ExoPlayerState @UiThread constructor(
override val subtitleTracks: MutableTrackGroup<SubtitleTrack> = MutableTrackGroup()

override val audioTracks: MutableTrackGroup<AudioTrack> = MutableTrackGroup()
override fun saveScreenshotFile(filename: String) {
TODO("Not yet implemented")
}

override val currentPositionMillis: MutableStateFlow<Long> = MutableStateFlow(0)
override fun getExactCurrentPositionMillis(): Long = player.currentPosition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ private fun PreviewVideoScaffoldImpl(
},
onShowMediaSelector = { isMediaSelectorVisible = true },
onShowSelectEpisode = { isEpisodeSelectorVisible = true },
onClickScreenshot = {},
)

// VideoScaffold(
Expand Down
20 changes: 16 additions & 4 deletions app/shared/video-player/common/ui/VideoScaffold.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ fun VideoScaffold(
danmakuHost: @Composable BoxScope.() -> Unit = {},
gestureHost: @Composable BoxWithConstraintsScope.() -> Unit = {},
floatingMessage: @Composable BoxScope.() -> Unit = {},
rhsBar: @Composable ColumnScope.() -> Unit = {},
rhsButtons: @Composable ColumnScope.() -> Unit = {},
gestureLock: @Composable ColumnScope.() -> Unit = {},
bottomBar: @Composable RowScope.() -> Unit = {},
floatingBottomEnd: @Composable RowScope.() -> Unit = {},
rhsSheet: @Composable () -> Unit = {},
Expand Down Expand Up @@ -205,15 +206,26 @@ fun VideoScaffold(
}
}
Column(Modifier.fillMaxSize().background(Color.Transparent)) {
// Separate from controllers, to fix position when controllers are/aren't hidden
Box(Modifier.weight(1f, fill = true).fillMaxWidth()) {
Column(Modifier.padding(end = 16.dp).align(Alignment.CenterEnd)) {
Column(
Modifier.padding(end = 16.dp).align(Alignment.CenterEnd),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
AnimatedVisibility(
visible = controllersVisibleState && !gestureLockedState,
enter = fadeIn(),
exit = fadeOut(),
) {
rhsButtons()
}

// Separate from controllers, to fix position when controllers are/aren't hidden
AnimatedVisibility(
visible = controllersVisibleState,
enter = fadeIn(),
exit = fadeOut(),
) {
rhsBar()
gestureLock()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package me.him188.ani.app.videoplayer.ui.guesture

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import me.him188.ani.app.ui.foundation.theme.aniDarkColorTheme
import me.him188.ani.app.ui.foundation.theme.aniLightColorTheme
import me.him188.ani.app.ui.foundation.theme.slightlyWeaken

@Composable
fun PlayerFloatingButtonBox(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Surface(
modifier,
shape = RoundedCornerShape(16.dp),
color = aniDarkColorTheme().background.copy(0.05f),
contentColor = Color.White,
border = BorderStroke(0.5.dp, aniLightColorTheme().outline.slightlyWeaken()),
) {
content()
}
}
30 changes: 30 additions & 0 deletions app/shared/video-player/common/ui/guesture/ScreenshotButton.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package me.him188.ani.app.videoplayer.ui.guesture

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.PhotoCamera
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color

@Composable
fun ScreenshotButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
PlayerFloatingButtonBox(
modifier = modifier,
content = {
IconButton(onClick) {
val color = Color.White
CompositionLocalProvider(LocalContentColor provides color) {
Icon(Icons.Rounded.PhotoCamera, contentDescription = "Lock screen")
}
}
},
)
}

5 changes: 5 additions & 0 deletions app/shared/video-player/common/ui/state/PlayerState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ interface PlayerState {
val subtitleTracks: TrackGroup<SubtitleTrack>

val audioTracks: TrackGroup<AudioTrack>

fun saveScreenshotFile(filename: String)
}

fun PlayerState.togglePause() {
Expand Down Expand Up @@ -438,4 +440,7 @@ class DummyPlayerState : AbstractPlayerState<AbstractPlayerState.Data>(EmptyCoro

override val subtitleTracks: TrackGroup<SubtitleTrack> = emptyTrackGroup()
override val audioTracks: TrackGroup<AudioTrack> = emptyTrackGroup()

override fun saveScreenshotFile(filename: String) {
}
}
17 changes: 17 additions & 0 deletions app/shared/video-player/desktop/ui/VideoPlayer.desktop.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ import uk.co.caprica.vlcj.player.base.MediaPlayer
import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter
import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer
import java.io.IOException
import java.nio.file.Path
import java.util.Locale
import kotlin.coroutines.CoroutineContext
import kotlin.io.path.createDirectories
import kotlin.time.Duration.Companion.seconds


Expand Down Expand Up @@ -139,6 +142,20 @@ class VlcjVideoPlayerState(parentCoroutineContext: CoroutineContext) : PlayerSta
}
}

override fun saveScreenshotFile(filename: String) {
player.submit {
val screenshotPath: Path =
Path.of(System.getProperty("user.home")).resolve("Pictures").resolve("Ani")
try {
screenshotPath.createDirectories()
} catch (ex: IOException) {
logger.warn("Create ani pictures dir fail", ex);
}
val filePath = screenshotPath.resolve(filename)
player.snapshots().save(filePath.toFile())
}
}

class VlcjData(
override val videoSource: VideoSource<*>,
override val videoData: VideoData,
Expand Down

0 comments on commit 65d0738

Please sign in to comment.