diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/common/PbContentRender.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/common/PbContentRender.kt index e0b1195a..0e81adfd 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/common/PbContentRender.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/common/PbContentRender.kt @@ -1,14 +1,10 @@ package com.huanchengfly.tieba.post.ui.common -import android.content.pm.ActivityInfo -import android.util.Log import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.waitForUpOrCancellation -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.InlineTextContent @@ -16,11 +12,8 @@ import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -42,23 +35,16 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.github.panpf.sketch.compose.AsyncImage -import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.activities.UserActivity import com.huanchengfly.tieba.post.activities.WebViewActivity import com.huanchengfly.tieba.post.arch.BaseComposeActivity.Companion.LocalWindowSizeClass import com.huanchengfly.tieba.post.arch.ImmutableHolder -import com.huanchengfly.tieba.post.findActivity import com.huanchengfly.tieba.post.models.protos.PhotoViewData import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass import com.huanchengfly.tieba.post.ui.widgets.compose.EmoticonText -import com.huanchengfly.tieba.post.ui.widgets.compose.FullScreen import com.huanchengfly.tieba.post.ui.widgets.compose.NetworkImage import com.huanchengfly.tieba.post.ui.widgets.compose.VoicePlayer -import com.huanchengfly.tieba.post.ui.widgets.compose.video.OnFullScreenModeChangedListener -import com.huanchengfly.tieba.post.ui.widgets.compose.video.VideoPlayer -import com.huanchengfly.tieba.post.ui.widgets.compose.video.VideoPlayerSource -import com.huanchengfly.tieba.post.ui.widgets.compose.video.rememberVideoPlayerController import com.huanchengfly.tieba.post.utils.appPreferences import com.huanchengfly.tieba.post.utils.launchUrl @@ -179,72 +165,22 @@ data class VideoContentRender( val context = LocalContext.current if (picUrl.isNotBlank()) { - if (videoUrl.isNotBlank()) { - var fullScreen by remember { mutableStateOf(false) } - val systemUiController = rememberSystemUiController() - val videoPlayerController = rememberVideoPlayerController( - source = VideoPlayerSource.Network(videoUrl), - fullScreenModeChangedListener = object : OnFullScreenModeChangedListener { - override fun onFullScreenModeChanged(isFullScreen: Boolean) { - Log.i("VideoPlayer", "onFullScreenModeChanged $isFullScreen") - fullScreen = isFullScreen - systemUiController.isStatusBarVisible = !isFullScreen - systemUiController.isNavigationBarVisible = !isFullScreen - if (isFullScreen) { - context.findActivity()?.requestedOrientation = - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - } else { - context.findActivity()?.requestedOrientation = - ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT - } - } - } - ) -// DisposableEffect(Unit) { -// onDispose { -// Log.i("VideoContentRender", "onDispose") -// videoPlayerController.release() -// } -// } - val videoPlayerContent = - movableContentOf { isFullScreen: Boolean, modifier: Modifier -> - VideoPlayer( - videoPlayerController = videoPlayerController, - modifier = modifier, - backgroundColor = if (isFullScreen) Color.Black else Color.Transparent - ) - } + val picModifier = Modifier + .clip(RoundedCornerShape(context.appPreferences.radius.dp)) + .fillMaxWidth(widthFraction) + .aspectRatio(width * 1f / height) - if (fullScreen) { - Spacer( - modifier = Modifier - .clip(RoundedCornerShape(context.appPreferences.radius.dp)) - .fillMaxWidth(widthFraction) - .aspectRatio(width * 1f / height) - ) - FullScreen { - videoPlayerContent( - true, - Modifier.fillMaxSize() - ) - } - } else { - videoPlayerContent( - false, - Modifier - .clip(RoundedCornerShape(context.appPreferences.radius.dp)) - .fillMaxWidth(widthFraction) - .aspectRatio(width * 1f / height) - ) - } + if (videoUrl.isNotBlank()) { + com.huanchengfly.tieba.post.ui.widgets.compose.VideoPlayer( + videoUrl = videoUrl, + thumbnailUrl = picUrl, + modifier = picModifier + ) } else { AsyncImage( imageUri = picUrl, contentDescription = stringResource(id = R.string.desc_video), - modifier = Modifier - .clip(RoundedCornerShape(context.appPreferences.radius.dp)) - .fillMaxWidth(widthFraction) - .aspectRatio(width * 1f / height) + modifier = picModifier .clickable { WebViewActivity.launch(context, webUrl) }, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt index 4f92f1c9..90d8a604 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt @@ -1,5 +1,7 @@ package com.huanchengfly.tieba.post.ui.widgets.compose +import android.content.pm.ActivityInfo +import android.util.Log import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -13,6 +15,7 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -30,8 +33,8 @@ import androidx.compose.material.icons.rounded.FavoriteBorder import androidx.compose.material.icons.rounded.PhotoSizeSelectActual import androidx.compose.material.icons.rounded.SwapCalls import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.movableContentOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -51,12 +54,10 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.AndroidView -import cn.jzvd.Jzvd -import com.github.panpf.sketch.displayImage import com.google.accompanist.placeholder.PlaceholderHighlight import com.google.accompanist.placeholder.material.fade import com.google.accompanist.placeholder.material.placeholder +import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.activities.UserActivity import com.huanchengfly.tieba.post.api.abstractText @@ -65,9 +66,13 @@ import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo import com.huanchengfly.tieba.post.api.models.protos.User import com.huanchengfly.tieba.post.arch.ImmutableHolder import com.huanchengfly.tieba.post.arch.wrapImmutable +import com.huanchengfly.tieba.post.findActivity import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.utils.getImmutablePhotoViewData -import com.huanchengfly.tieba.post.ui.widgets.VideoPlayerStandard +import com.huanchengfly.tieba.post.ui.widgets.compose.video.DefaultVideoPlayerController +import com.huanchengfly.tieba.post.ui.widgets.compose.video.OnFullScreenModeChangedListener +import com.huanchengfly.tieba.post.ui.widgets.compose.video.VideoPlayerSource +import com.huanchengfly.tieba.post.ui.widgets.compose.video.rememberVideoPlayerController import com.huanchengfly.tieba.post.utils.DateTimeUtils import com.huanchengfly.tieba.post.utils.EmoticonUtil.emoticonString import com.huanchengfly.tieba.post.utils.ImageUtil @@ -593,19 +598,51 @@ fun VideoPlayer( modifier: Modifier = Modifier, title: String = "" ) { - AndroidView( - factory = { context -> - VideoPlayerStandard(context) - }, - modifier = modifier - ) { - it.setUp(videoUrl, title) - it.posterImageView.displayImage(thumbnailUrl) - } - DisposableEffect(videoUrl) { - onDispose { - Jzvd.releaseAllVideos() + val context = LocalContext.current + val systemUiController = rememberSystemUiController() + val videoPlayerController = rememberVideoPlayerController( + source = VideoPlayerSource.Network(videoUrl), + thumbnailUrl = thumbnailUrl, + fullScreenModeChangedListener = object : OnFullScreenModeChangedListener { + override fun onFullScreenModeChanged(isFullScreen: Boolean) { + Log.i("VideoPlayer", "onFullScreenModeChanged $isFullScreen") + systemUiController.isStatusBarVisible = !isFullScreen + systemUiController.isNavigationBarVisible = !isFullScreen + if (isFullScreen) { + context.findActivity()?.requestedOrientation = + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } else { + context.findActivity()?.requestedOrientation = + ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT + } + } } + ) + val fullScreen by (videoPlayerController as DefaultVideoPlayerController).collect { isFullScreen } + val videoPlayerContent = + movableContentOf { isFullScreen: Boolean, playerModifier: Modifier -> + com.huanchengfly.tieba.post.ui.widgets.compose.video.VideoPlayer( + videoPlayerController = videoPlayerController, + modifier = playerModifier, + backgroundColor = if (isFullScreen) Color.Black else Color.Transparent + ) + } + + if (fullScreen) { + Spacer( + modifier = modifier + ) + FullScreen { + videoPlayerContent( + true, + Modifier.fillMaxSize() + ) + } + } else { + videoPlayerContent( + false, + modifier + ) } } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/DefaultVideoPlayerController.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/DefaultVideoPlayerController.kt index 1f00147e..68200603 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/DefaultVideoPlayerController.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/DefaultVideoPlayerController.kt @@ -2,6 +2,7 @@ package com.huanchengfly.tieba.post.ui.widgets.compose.video import android.annotation.SuppressLint import android.content.Context +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState @@ -9,6 +10,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.common.Player.STATE_ENDED +import androidx.media3.common.Player.STATE_IDLE +import androidx.media3.common.Player.STATE_READY import androidx.media3.common.VideoSize import androidx.media3.datasource.DataSource import androidx.media3.datasource.DefaultDataSource @@ -29,7 +33,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger interface OnFullScreenModeChangedListener { fun onFullScreenModeChanged(isFullScreen: Boolean) @@ -42,7 +45,6 @@ internal class DefaultVideoPlayerController( private val fullScreenModeChangedListener: OnFullScreenModeChangedListener? = null ) : VideoPlayerController { private val released = AtomicBoolean(false) - val releaseCounter = AtomicInteger(0) private val _state = MutableStateFlow(initialState) override val state: StateFlow @@ -86,6 +88,7 @@ internal class DefaultVideoPlayerController( private var playerView: PlayerView? = null private var updateDurationAndPositionJob: Job? = null + private var autoHideControllerJob: Job? = null private val playerListener = object : Player.Listener { @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @@ -107,7 +110,8 @@ internal class DefaultVideoPlayerController( _state.set { copy( - playbackState = PlaybackState.of(playbackState) + playbackState = PlaybackState.of(playbackState), + startedPlay = playbackState != STATE_IDLE ) } } @@ -175,7 +179,18 @@ internal class DefaultVideoPlayerController( } fun initialize() { - releaseCounter.incrementAndGet() + Log.i("VideoPlayerController", "$this initialize") + val currentState = _state.value + exoPlayer.playWhenReady = currentState.isPlaying + initialStateRunner = { + exoPlayer.seekTo(currentState.currentPosition) + } + if (this::source.isInitialized) { + setSource(source) + } + if (playerView != null) { + playerViewAvailable(playerView!!) + } } /** @@ -185,17 +200,19 @@ internal class DefaultVideoPlayerController( private val waitPlayerViewToPrepare = AtomicBoolean(false) override fun play() { - if (exoPlayer.playbackState == Player.STATE_ENDED) { + _state.set { copy(startedPlay = true) } + if (exoPlayer.playbackState == STATE_ENDED) { exoPlayer.seekTo(0) } exoPlayer.playWhenReady = true + autoHideControls() } override fun pause() { exoPlayer.playWhenReady = false } - override fun playPauseToggle() { + override fun togglePlaying() { if (exoPlayer.isPlaying) pause() else play() } @@ -228,9 +245,6 @@ internal class DefaultVideoPlayerController( } override fun setSource(source: VideoPlayerSource) { - if (released.get()) { - initialize() - } this.source = source if (playerView == null) { waitPlayerViewToPrepare.set(true) @@ -247,8 +261,27 @@ internal class DefaultVideoPlayerController( _state.set { copy(controlsEnabled = enabled) } } - fun showControls() { + fun showControls(autoHide: Boolean = true) { _state.set { copy(controlsVisible = true) } + if (autoHide) { + autoHideControls() + } else { + cancelAutoHideControls() + } + } + + private fun cancelAutoHideControls() { + Log.i("VideoPlayerController", "cancelAutoHideControls") + autoHideControllerJob?.cancel() + } + + private fun autoHideControls() { + cancelAutoHideControls() + Log.i("VideoPlayerController", "autoHideControls") + autoHideControllerJob = coroutineScope.launch { + delay(5000) + hideControls() + } } fun hideControls() { @@ -264,12 +297,15 @@ internal class DefaultVideoPlayerController( } private fun updateDurationAndPosition() { - _state.set { - copy( - duration = exoPlayer.duration.coerceAtLeast(0), - currentPosition = exoPlayer.currentPosition.coerceAtLeast(0), - secondaryProgress = exoPlayer.bufferedPosition.coerceAtLeast(0) - ) + if (exoPlayer.playbackState == STATE_READY || exoPlayer.playbackState == STATE_ENDED) { + _state.set { + copy( + duration = exoPlayer.duration.coerceAtLeast(0), + currentPosition = exoPlayer.currentPosition.coerceAtLeast(0), + secondaryProgress = exoPlayer.bufferedPosition.coerceAtLeast(0), + isPlaying = exoPlayer.isPlaying + ) + } } } @@ -334,12 +370,12 @@ internal class DefaultVideoPlayerController( } override fun release() { - if (releaseCounter.decrementAndGet() <= 0 && released.compareAndSet(false, true)) { + Log.i("VideoPlayerController", "$this release") + if (released.compareAndSet(false, true)) { exoPlayer.release() previewExoPlayer.release() _exoPlayer = null _previewExoPlayer = null - playerView = null } } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/MediaContaonButtons.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/MediaContaonButtons.kt index 0ea522d9..38c29444 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/MediaContaonButtons.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/MediaContaonButtons.kt @@ -92,7 +92,7 @@ fun PlayPauseButton(modifier: Modifier = Modifier) { val playbackState by controller.collect { playbackState } IconButton( - onClick = { controller.playPauseToggle() }, + onClick = { controller.togglePlaying() }, modifier = modifier ) { if (isPlaying) { diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/MediaControlGestures.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/MediaControlGestures.kt index 22fb72ff..448f9f34 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/MediaControlGestures.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/MediaControlGestures.kt @@ -163,6 +163,21 @@ suspend fun PointerInputScope.detectMediaPlayerGesture( onDrag: (Float) -> Unit ) { coroutineScope { +// launch { +// detectVerticalDragGestures( +// onDragStart = { +// Log.i("MediaControlGestures", "Vertical onDragStart $it") +// }, +// onDragEnd = { +// Log.i("MediaControlGestures", "Vertical onDragEnd") +// }, +// onVerticalDrag = { change, dragAmount -> +// Log.i("MediaControlGestures", "Vertical onVerticalDrag $change $dragAmount") +// if (change.positionChange() != Offset.Zero) change.consume() +// }, +// ) +// } + launch { detectHorizontalDragGestures( onDragStart = onDragStart, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/ProgressIndicator.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/ProgressIndicator.kt index 34910c35..e7071109 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/ProgressIndicator.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/ProgressIndicator.kt @@ -23,9 +23,11 @@ fun ProgressIndicator( max = duration, enabled = controlsVisible && controlsEnabled, onSeek = { + controller.showControls(autoHide = false) controller.previewSeekTo(it) }, onSeekStopped = { + controller.showControls(autoHide = true) controller.seekTo(it) }, secondaryProgress = secondaryProgress, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayer.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayer.kt index c8f2a013..8eca969f 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayer.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayer.kt @@ -4,8 +4,9 @@ import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -14,12 +15,17 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.LocalContentColor import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Fullscreen import androidx.compose.material.icons.rounded.FullscreenExit +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect @@ -36,10 +42,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp +import com.github.panpf.sketch.compose.AsyncImage import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.ui.widgets.compose.video.util.getDurationString @@ -49,6 +57,7 @@ internal val LocalVideoPlayerController = @Composable fun rememberVideoPlayerController( source: VideoPlayerSource? = null, + thumbnailUrl: String? = null, fullScreenModeChangedListener: OnFullScreenModeChangedListener? = null, playWhenReady: Boolean = false, ): VideoPlayerController { @@ -62,8 +71,11 @@ fun rememberVideoPlayerController( return DefaultVideoPlayerController( context = context, initialState = value, - coroutineScope = coroutineScope - ) + coroutineScope = coroutineScope, + fullScreenModeChangedListener = fullScreenModeChangedListener + ).apply { + source?.let { setSource(it) } + } } override fun SaverScope.save(value: DefaultVideoPlayerController): VideoPlayerState { @@ -73,7 +85,10 @@ fun rememberVideoPlayerController( init = { DefaultVideoPlayerController( context = context, - initialState = VideoPlayerState(isPlaying = playWhenReady), + initialState = VideoPlayerState( + thumbnailUrl = thumbnailUrl, + isPlaying = playWhenReady + ), coroutineScope = coroutineScope, fullScreenModeChangedListener = fullScreenModeChangedListener ).apply { @@ -101,10 +116,18 @@ fun VideoPlayer( videoPlayerController.enableGestures(gesturesEnabled) } + DisposableEffect(Unit) { + videoPlayerController.initialize() + onDispose { + videoPlayerController.release() + } + } + CompositionLocalProvider( LocalContentColor provides Color.White, LocalVideoPlayerController provides videoPlayerController ) { + val startedPlay by videoPlayerController.collect { startedPlay || playbackState != PlaybackState.IDLE } val aspectRatio by videoPlayerController.collect { videoSize.first / videoSize.second } val supportFullScreen = remember(videoPlayerController) { videoPlayerController.supportFullScreen() } @@ -124,72 +147,98 @@ fun VideoPlayer( .fillMaxSize() .then(modifier) ) { - PlayerSurface( - modifier = Modifier - .aspectRatio(aspectRatio.takeUnless { it.isNaN() || it == 0f } ?: 2f) - .align(Alignment.Center) - ) { - videoPlayerController.playerViewAvailable(it) - } - - DisposableEffect(Unit) { - videoPlayerController.initialize() - Log.i( - "VideoPlayer", - "${videoPlayerController.releaseCounter.get()} init $videoPlayerController" - ) - - onDispose { - videoPlayerController.release() - Log.i( - "VideoPlayer", - "${videoPlayerController.releaseCounter.get()} release $videoPlayerController" - ) - } - } - - MediaControlGestures(modifier = Modifier.matchParentSize()) - MediaControlButtons( - modifier = Modifier.matchParentSize() - ) - - val controlsVisible by videoPlayerController.collect { controlsVisible } - - if (controlsVisible) { - Column( + if (startedPlay) { + PlayerSurface( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) - .align(Alignment.BottomCenter), - verticalArrangement = Arrangement.spacedBy(8.dp) + .aspectRatio(aspectRatio.takeUnless { it.isNaN() || it == 0f } ?: 2f) + .align(Alignment.Center) ) { - ProgressIndicator( - modifier = Modifier.fillMaxWidth() - ) - - Row(verticalAlignment = Alignment.CenterVertically) { - PositionAndDuration() - Spacer(modifier = Modifier.weight(1f)) - if (videoPlayerController.supportFullScreen()) { - FullScreenButton() - } - } + videoPlayerController.playerViewAvailable(it) } + + MediaController() } else { + val thumbnailUrl by videoPlayerController.collect { thumbnailUrl } + Box( modifier = Modifier - .align(Alignment.BottomCenter) - .offset(y = 12.dp) + .fillMaxSize() + .align(Alignment.Center), + contentAlignment = Alignment.Center ) { - ProgressIndicator( - modifier = Modifier.fillMaxWidth() - ) + if (thumbnailUrl != null) { + AsyncImage( + imageUri = thumbnailUrl, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } + + IconButton(onClick = { videoPlayerController.play() }) { + Icon( + imageVector = Icons.Rounded.PlayArrow, + contentDescription = stringResource(id = R.string.btn_play), + modifier = Modifier.size(48.dp) + ) + } } } } } } +@Composable +fun BoxScope.MediaController() { + val videoPlayerController = LocalVideoPlayerController.current + + MediaControlButtons( + modifier = Modifier.matchParentSize() + ) + + val controlsVisible by videoPlayerController.collect { controlsVisible } + val isFullScreen by videoPlayerController.collect { isFullScreen } + + if (controlsVisible) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .align(Alignment.BottomCenter), + ) { + if (isFullScreen) { + ProgressIndicator( + modifier = Modifier.fillMaxWidth() + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(16.dp)) + PositionAndDuration() + Spacer(modifier = Modifier.weight(1f)) + if (videoPlayerController.supportFullScreen()) { + FullScreenButton() + } + } + } + } + if (!isFullScreen || !controlsVisible) { + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .offset(y = 12.dp) + ) { + ProgressIndicator( + modifier = Modifier.fillMaxWidth() + ) + } + } + + MediaControlGestures(modifier = Modifier.matchParentSize()) +} + @Composable fun PositionAndDuration( modifier: Modifier = Modifier @@ -225,9 +274,14 @@ private fun FullScreenButton() { Icons.Rounded.Fullscreen } Box( - modifier = Modifier.clickable { - videoPlayerController.toggleFullScreen() - } + modifier = Modifier + .padding(8.dp) + .clickable( + indication = rememberRipple(bounded = false), + interactionSource = remember { MutableInteractionSource() } + ) { + videoPlayerController.toggleFullScreen() + } ) { Icon( imageVector = icon, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayerController.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayerController.kt index 924af4e6..4e5ef2f9 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayerController.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayerController.kt @@ -1,7 +1,9 @@ package com.huanchengfly.tieba.post.ui.widgets.compose.video +import androidx.compose.runtime.Stable import kotlinx.coroutines.flow.StateFlow +@Stable interface VideoPlayerController { fun setSource(source: VideoPlayerSource) @@ -9,7 +11,7 @@ interface VideoPlayerController { fun pause() - fun playPauseToggle() + fun togglePlaying() fun quickSeekForward() diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayerSource.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayerSource.kt index ad0e3960..f32375b9 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayerSource.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayerSource.kt @@ -1,9 +1,19 @@ package com.huanchengfly.tieba.post.ui.widgets.compose.video +import android.os.Parcelable import androidx.annotation.RawRes +import androidx.compose.runtime.Immutable +import kotlinx.parcelize.Parcelize -sealed class VideoPlayerSource { +@Immutable +@Parcelize +sealed class VideoPlayerSource : Parcelable { + @Immutable data class Raw(@RawRes val resId: Int) : VideoPlayerSource() - data class Network(val url: String, val headers: Map = mapOf()) : - VideoPlayerSource() + + @Immutable + data class Network( + val url: String, + val headers: Map = mapOf() + ) : VideoPlayerSource() } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayerState.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayerState.kt index 0e25126b..573da6fb 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayerState.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/video/VideoPlayerState.kt @@ -5,7 +5,9 @@ import kotlinx.parcelize.Parcelize @Parcelize data class VideoPlayerState( - val isPlaying: Boolean = true, + val thumbnailUrl: String? = null, + val startedPlay: Boolean = false, + val isPlaying: Boolean = false, val controlsVisible: Boolean = true, val controlsEnabled: Boolean = true, val gesturesEnabled: Boolean = true, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9381149c..dce2c9e6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -676,4 +676,5 @@ 由于你的屏蔽设置,第 %d 楼已被屏蔽 屏蔽视频贴子 全屏 + 播放