pref: 完善视频播放器
This commit is contained in:
parent
3a5d601fd2
commit
4acd2d1852
|
|
@ -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)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<VideoPlayerState>
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String, String> = mapOf()) :
|
||||
VideoPlayerSource()
|
||||
|
||||
@Immutable
|
||||
data class Network(
|
||||
val url: String,
|
||||
val headers: Map<String, String> = mapOf()
|
||||
) : VideoPlayerSource()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -676,4 +676,5 @@
|
|||
<string name="tip_blocked_post">由于你的屏蔽设置,第 %d 楼已被屏蔽</string>
|
||||
<string name="settings_block_video">屏蔽视频贴子</string>
|
||||
<string name="btn_full_screen">全屏</string>
|
||||
<string name="btn_play">播放</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Reference in New Issue