From 9555763ce4fc4b6e81829cb361a64a35e1cd126b Mon Sep 17 00:00:00 2001 From: Corner Date: Sun, 23 Jun 2024 12:07:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=92=AD=E6=94=BE=E5=99=A8=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增视频信息(链接,分辨率) - 优化缓冲加载页面 - 优化播放文字提示 --- .../commonMain/kotlin/com/corner/ui/Player.kt | 83 ++++++++++++++----- .../component/DefaultDetailComponent.kt | 4 +- .../com/corner/ui/player/PlayerState.kt | 10 ++- .../corner/ui/player/frame/FrameContainer.kt | 28 +++---- .../corner/ui/player/vlcj/VlcjController.kt | 40 +++++++-- 5 files changed, 117 insertions(+), 48 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/corner/ui/Player.kt b/composeApp/src/commonMain/kotlin/com/corner/ui/Player.kt index d893317..11376b9 100644 --- a/composeApp/src/commonMain/kotlin/com/corner/ui/Player.kt +++ b/composeApp/src/commonMain/kotlin/com/corner/ui/Player.kt @@ -1,17 +1,14 @@ package com.corner.ui import androidx.compose.animation.* -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.onClick -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -25,6 +22,7 @@ import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp @@ -34,8 +32,10 @@ import com.corner.bean.SettingStore import com.corner.catvodcore.viewmodel.GlobalModel import com.corner.ui.decompose.DetailComponent import com.corner.ui.player.DefaultControls +import com.corner.ui.player.PlayerState import com.corner.ui.player.frame.FrameContainer import com.corner.ui.player.vlcj.VlcjFrameController +import com.corner.ui.scene.Dialog import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.update @@ -74,6 +74,7 @@ fun Player( val showTip = controller.showTip.collectAsState() val tip = controller.tip.collectAsState() val videoFullScreen = GlobalModel.videoFullScreen.subscribeAsState() + val showMediaInfoDialog = remember { mutableStateOf(false) } LaunchedEffect(Unit){ val volume = SettingStore.getCache("playerState") @@ -136,10 +137,22 @@ fun Player( delay(3000) showCursor.value = false } - }.onClick{ - showControllerBar.value = !showControllerBar.value }.pointerHoverIcon(PointerIcon(if (!showCursor.value) createEmptyCursor() else Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)))) { - FrameContainer(Modifier.fillMaxSize().focusTarget().focusable().focusRequester(focusRequester), controller) + FrameContainer(Modifier.fillMaxSize().focusTarget().focusable().focusRequester(focusRequester), controller){ + showControllerBar.value = !showControllerBar.value + } + AnimatedVisibility(showControllerBar.value, + modifier = Modifier.align(Alignment.TopEnd), + enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut()){ + Row (Modifier.height(40.dp).fillMaxWidth(), horizontalArrangement = Arrangement.End){ + IconButton(onClick = { + showMediaInfoDialog.value = true + }){ + Icon(Icons.Default.Info, contentDescription = "media info", tint = MaterialTheme.colorScheme.primary) + } + } + } AnimatedVisibility( showControllerBar.value, modifier = Modifier.align(Alignment.BottomEnd), @@ -170,9 +183,42 @@ fun Player( ) } } + MediaInfoDialog(Modifier.fillMaxWidth(0.5f).fillMaxHeight(0.4f), controller.state.value, showMediaInfoDialog.value){ + showMediaInfoDialog.value = false + } } } +@Composable +fun MediaInfoDialog(modifier: Modifier, playerState: PlayerState, show:Boolean, onClose:()->Unit){ + val mediaInfo = rememberUpdatedState(playerState.mediaInfo) + Dialog(modifier, showDialog = show, onClose = onClose){ + val scrollbar = rememberLazyListState(0) + LazyColumn(verticalArrangement = Arrangement.spacedBy(30.dp), modifier = Modifier.padding(30.dp), state = scrollbar, + horizontalAlignment = Alignment.CenterHorizontally) { + item { + SelectionContainer { + Text(text = AnnotatedString(mediaInfo.value?.url ?: "")) + } + } + item { + Text("${mediaInfo.value?.width ?: ""} * ${mediaInfo.value?.height ?: ""}") + } + } + VerticalScrollbar(rememberScrollbarAdapter(scrollbar)) + } +} + +//@Preview +//@Composable +//fun previewMediaInfoDialog(){ +// AppTheme { +// MediaInfoDialog(Modifier.fillMaxSize(), MediaInfo(800, 1200, "http://xxxxxx.com/dddd"), true){ +// +// } +// } +//} + private fun createEmptyCursor(): Cursor { return Toolkit.getDefaultToolkit().createCustomCursor( BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB), @@ -180,12 +226,3 @@ private fun createEmptyCursor(): Cursor { "Empty Cursor" ) } - -enum class PlayState{ - INIT, - LOADING, - PLAYING, - REFRESH, - CHANGEEP, - -} diff --git a/composeApp/src/commonMain/kotlin/com/corner/ui/decompose/component/DefaultDetailComponent.kt b/composeApp/src/commonMain/kotlin/com/corner/ui/decompose/component/DefaultDetailComponent.kt index 104ba26..c859d99 100644 --- a/composeApp/src/commonMain/kotlin/com/corner/ui/decompose/component/DefaultDetailComponent.kt +++ b/composeApp/src/commonMain/kotlin/com/corner/ui/decompose/component/DefaultDetailComponent.kt @@ -308,7 +308,9 @@ class DefaultDetailComponent(componentContext: ComponentContext) : DetailCompone detail.subEpisode?.parallelStream()?.forEach { it.activated = it == ep } - SnackBar.postMsg(if(internalPlayer) "开始播放" else "上次播放" + ": ${ep.name}") + if(!internalPlayer){ + SnackBar.postMsg("上次播放" + ": ${ep.name}") + } } override fun nextEP() { diff --git a/composeApp/src/commonMain/kotlin/com/corner/ui/player/PlayerState.kt b/composeApp/src/commonMain/kotlin/com/corner/ui/player/PlayerState.kt index 58ec759..d63283f 100644 --- a/composeApp/src/commonMain/kotlin/com/corner/ui/player/PlayerState.kt +++ b/composeApp/src/commonMain/kotlin/com/corner/ui/player/PlayerState.kt @@ -2,6 +2,7 @@ package com.corner.ui.player data class PlayerState( val isPlaying: Boolean = false, + val isBuffering:Boolean = false, val isMuted: Boolean = false, var isFullScreen: Boolean = false, val volume: Float = .5f, @@ -9,5 +10,12 @@ data class PlayerState( val duration: Long = 0L, val speed: Float = 1F, var opening:Long = -1, - val ending:Long = -1 + val ending:Long = -1, + val mediaInfo: MediaInfo? = null, +) + +data class MediaInfo( + val height:Int, + val width:Int, + val url:String ) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/corner/ui/player/frame/FrameContainer.kt b/composeApp/src/commonMain/kotlin/com/corner/ui/player/frame/FrameContainer.kt index 79f7293..a9bb2a6 100644 --- a/composeApp/src/commonMain/kotlin/com/corner/ui/player/frame/FrameContainer.kt +++ b/composeApp/src/commonMain/kotlin/com/corner/ui/player/frame/FrameContainer.kt @@ -8,10 +8,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.text.isTypedEvent -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -30,8 +27,9 @@ import uk.co.caprica.vlcj.player.base.State fun FrameContainer( modifier: Modifier = Modifier, controller: VlcjFrameController, + onClick:()->Unit ) { - val playerState = rememberUpdatedState(controller.getPlayer()?.status()?.state()) + val playerState = controller.state.collectAsState() val bitmap by remember { controller.imageBitmapState } val interactionSource = remember { MutableInteractionSource() } Box(modifier = modifier.background(Color.Black) @@ -44,6 +42,7 @@ fun FrameContainer( indication = null ) { // onClick + onClick() } .onPointerEvent(PointerEventType.Scroll) { e -> val y = e.changes.first().scrollDelta.y @@ -86,16 +85,15 @@ fun FrameContainer( } }else{ Box(modifier = Modifier.fillMaxSize().background(Color.Black)) { - when (playerState.value?.name ?: "") { - State.BUFFERING.name -> - androidx.compose.material3.CircularProgressIndicator(Modifier.align(Alignment.Center)) - else -> - Image( - modifier = Modifier.align(Alignment.Center), - painter = painterResource("/pic/TV-icon-x.png"), - contentDescription = "nothing here", - contentScale = ContentScale.Crop - ) + if(playerState.value.isBuffering){ + androidx.compose.material3.CircularProgressIndicator(Modifier.align(Alignment.Center)) + }else { + Image( + modifier = Modifier.align(Alignment.Center), + painter = painterResource("/pic/TV-icon-x.png"), + contentDescription = "nothing here", + contentScale = ContentScale.Crop + ) } } } diff --git a/composeApp/src/commonMain/kotlin/com/corner/ui/player/vlcj/VlcjController.kt b/composeApp/src/commonMain/kotlin/com/corner/ui/player/vlcj/VlcjController.kt index e359ec1..d501298 100644 --- a/composeApp/src/commonMain/kotlin/com/corner/ui/player/vlcj/VlcjController.kt +++ b/composeApp/src/commonMain/kotlin/com/corner/ui/player/vlcj/VlcjController.kt @@ -6,6 +6,7 @@ import com.corner.catvod.enum.bean.Vod import com.corner.catvodcore.viewmodel.GlobalModel import com.corner.database.History import com.corner.ui.decompose.DetailComponent +import com.corner.ui.player.MediaInfo import com.corner.ui.player.PlayerController import com.corner.ui.player.PlayerState import com.corner.ui.scene.SnackBar @@ -41,8 +42,19 @@ class VlcjController(val component: DetailComponent) : PlayerController { override var tip = MutableStateFlow("") override var history: MutableStateFlow = MutableStateFlow(null) var scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - - internal val factory by lazy { MediaPlayerFactory() } + private val vlcjArgs = listOf( + "--no-video-title-show", // 禁用视频标题显示 + "--no-snapshot-preview", // 禁用快照预览 + "--no-autoscale", // 禁用自动缩放 + "--no-disable-screensaver", // 禁用屏保 + "--avcodec-fast", // 使用快速解码模式 + "--network-caching=3000", // 设置网络缓存为 3000ms + "--file-caching=3000", // 设置文件缓存为 3000ms + "--live-caching=3000", // 设置直播缓存为 3000ms + "--sout-mux-caching=3000" // 设置输出缓存为 3000ms + ) + + internal val factory by lazy { MediaPlayerFactory(vlcjArgs) } override fun doWithMediaPlayer(block: (MediaPlayer) -> Unit) { player?.let { @@ -69,6 +81,23 @@ class VlcjController(val component: DetailComponent) : PlayerController { play() } + override fun videoOutput(mediaPlayer: MediaPlayer?, newCount: Int) { + + val trackInfo = mediaPlayer?.media()?.info()?.videoTracks()?.first() + if(trackInfo != null){ + _state.update { it.copy(mediaInfo = MediaInfo(url = mediaPlayer.media()?.info()?.mrl() ?: "", height = trackInfo.height(), width = trackInfo.width())) } + } + } + + override fun buffering(mediaPlayer: MediaPlayer?, newCache: Float) { + _state.update { it.copy(isBuffering = newCache != 100F) } + } + + override fun opening(mediaPlayer: MediaPlayer?) { + _state.update { it.copy(isBuffering = true) } + } + + override fun playing(mediaPlayer: MediaPlayer) { _state.update { it.copy(isPlaying = true) } } @@ -217,11 +246,6 @@ class VlcjController(val component: DetailComponent) : PlayerController { tip.emit(text) showTip.emit(true) } -// tipJob?.cancel() -// tipJob = scope.launch { -// delay(1000) -// showTip = false -// } } override fun stop() = catch { @@ -279,7 +303,7 @@ class VlcjController(val component: DetailComponent) : PlayerController { override fun toggleFullscreen() = catch { val videoFullScreen = GlobalModel.toggleVideoFullScreen() runBlocking { - if(videoFullScreen) showTips("[ESC]退出全屏") + if (videoFullScreen) showTips("[ESC]退出全屏") } }