mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
fix(android): fix issues with PiP window
This commit is contained in:
parent
f19d3d1a08
commit
4db3231c2a
9 changed files with 210 additions and 28 deletions
|
|
@ -32,6 +32,7 @@ import com.nuvio.app.features.player.PlayerTrackPreferenceStorage
|
|||
import com.nuvio.app.features.player.ExternalPlayerPlatform
|
||||
import com.nuvio.app.features.player.SubtitleFileCache
|
||||
import com.nuvio.app.features.player.PlayerPictureInPictureManager
|
||||
import com.nuvio.app.features.player.PipRemoteActionReceiver
|
||||
import com.nuvio.app.features.p2p.P2pSettingsStorage
|
||||
import com.nuvio.app.features.p2p.P2pStreamingEngine
|
||||
import com.nuvio.app.features.plugins.PluginStorage
|
||||
|
|
@ -59,6 +60,8 @@ import com.nuvio.app.features.watchprogress.ResumePromptStorage
|
|||
import com.nuvio.app.features.watchprogress.WatchProgressStorage
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private var pipRemoteActionReceiver: PipRemoteActionReceiver? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
enableEdgeToEdge(
|
||||
|
|
@ -71,6 +74,7 @@ class MainActivity : AppCompatActivity() {
|
|||
SentryInitializer.start(application)
|
||||
super.onCreate(savedInstanceState)
|
||||
window.setBackgroundDrawableResource(R.color.nuvio_background)
|
||||
pipRemoteActionReceiver = PipRemoteActionReceiver.register(this)
|
||||
SyncClientIdentityStorage.initialize(applicationContext)
|
||||
AddonStorage.initialize(applicationContext)
|
||||
AuthStorage.initialize(applicationContext)
|
||||
|
|
@ -143,6 +147,11 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
override fun onDestroy() {
|
||||
EpisodeReleaseNotificationPlatform.unbindActivity(this)
|
||||
val receiver = pipRemoteActionReceiver
|
||||
if (receiver != null) {
|
||||
runCatching { unregisterReceiver(receiver) }
|
||||
pipRemoteActionReceiver = null
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
|
|
@ -69,6 +70,7 @@ import `is`.xyz.mpv.Utils
|
|||
import io.github.peerless2012.ass.media.widget.AssSubtitleView
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -371,6 +373,8 @@ private fun ExoPlayerSurface(
|
|||
var playerViewRef by remember { mutableStateOf<PlayerView?>(null) }
|
||||
var currentSubtitleStyle by remember { mutableStateOf(SubtitleStyleState.DEFAULT) }
|
||||
var subtitleSelectionJob by remember { mutableStateOf<Job?>(null) }
|
||||
val isInPip = rememberIsInPictureInPicture()
|
||||
val pipSubtitleScale by rememberUpdatedState(if (isInPip) 0.4f else 1.0f)
|
||||
|
||||
fun syncPlayerViewKeepScreenOn() {
|
||||
playerViewRef?.keepScreenOn = exoPlayer.shouldKeepPlayerScreenOn()
|
||||
|
|
@ -387,6 +391,16 @@ private fun ExoPlayerSurface(
|
|||
PlayerPictureInPictureManager.registerPausePlaybackCallback {
|
||||
exoPlayer.pause()
|
||||
}
|
||||
PlayerPictureInPictureManager.registerTogglePlaybackCallback {
|
||||
if (exoPlayer.isPlaying) {
|
||||
exoPlayer.pause()
|
||||
} else {
|
||||
if (exoPlayer.playbackState == androidx.media3.common.Player.STATE_ENDED) {
|
||||
exoPlayer.seekTo(0L)
|
||||
}
|
||||
exoPlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
fun reportPlayerError(error: PlaybackException) {
|
||||
if (
|
||||
|
|
@ -471,6 +485,10 @@ private fun ExoPlayerSurface(
|
|||
latestOnSnapshot.value(exoPlayer.snapshot())
|
||||
}
|
||||
|
||||
override fun onVideoSizeChanged(videoSize: androidx.media3.common.VideoSize) {
|
||||
latestOnSnapshot.value(exoPlayer.snapshot())
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
syncPlayerViewKeepScreenOn()
|
||||
latestOnSnapshot.value(exoPlayer.snapshot())
|
||||
|
|
@ -508,6 +526,7 @@ private fun ExoPlayerSurface(
|
|||
exoPlayer.addListener(listener)
|
||||
onDispose {
|
||||
PlayerPictureInPictureManager.registerPausePlaybackCallback(null)
|
||||
PlayerPictureInPictureManager.registerTogglePlaybackCallback(null)
|
||||
exoPlayer.removeListener(listener)
|
||||
playerViewRef?.keepScreenOn = false
|
||||
subtitleSelectionJob?.cancel()
|
||||
|
|
@ -688,7 +707,7 @@ private fun ExoPlayerSurface(
|
|||
|
||||
override fun applySubtitleStyle(style: SubtitleStyleState) {
|
||||
currentSubtitleStyle = style
|
||||
playerViewRef?.applySubtitleStyle(style)
|
||||
playerViewRef?.applySubtitleStyle(style, pipSubtitleScale)
|
||||
}
|
||||
|
||||
override fun setSubtitleDelayMs(delayMs: Int) {
|
||||
|
|
@ -721,7 +740,7 @@ private fun ExoPlayerSurface(
|
|||
enabled = useLibass,
|
||||
renderType = libassRenderType,
|
||||
)
|
||||
applySubtitleStyle(currentSubtitleStyle)
|
||||
applySubtitleStyle(currentSubtitleStyle, pipSubtitleScale)
|
||||
}
|
||||
},
|
||||
update = { playerView ->
|
||||
|
|
@ -735,7 +754,7 @@ private fun ExoPlayerSurface(
|
|||
enabled = useLibass,
|
||||
renderType = libassRenderType,
|
||||
)
|
||||
playerView.applySubtitleStyle(currentSubtitleStyle)
|
||||
playerView.applySubtitleStyle(currentSubtitleStyle, pipSubtitleScale)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -851,8 +870,20 @@ private fun LibmpvPlayerSurface(
|
|||
PlayerPictureInPictureManager.registerPausePlaybackCallback {
|
||||
view.setPaused(true)
|
||||
}
|
||||
PlayerPictureInPictureManager.registerTogglePlaybackCallback {
|
||||
val snapshot = view.snapshot()
|
||||
if (snapshot.isPlaying) {
|
||||
view.setPaused(true)
|
||||
} else {
|
||||
if (snapshot.isEnded) {
|
||||
view.seekToMs(0L)
|
||||
}
|
||||
view.setPaused(false)
|
||||
}
|
||||
}
|
||||
onDispose {
|
||||
PlayerPictureInPictureManager.registerPausePlaybackCallback(null)
|
||||
PlayerPictureInPictureManager.registerTogglePlaybackCallback(null)
|
||||
view.keepScreenOn = false
|
||||
}
|
||||
}
|
||||
|
|
@ -1025,6 +1056,12 @@ private class NuvioLibmpvView(
|
|||
runCatching { mpv.setPropertyBoolean("pause", paused) }
|
||||
}
|
||||
|
||||
fun seekToMs(positionMs: Long) {
|
||||
runCatching {
|
||||
mpv.command("seek", (positionMs.coerceAtLeast(0L) / 1000.0).toString(), "absolute")
|
||||
}
|
||||
}
|
||||
|
||||
fun snapshot(): PlayerPlaybackSnapshot {
|
||||
val paused = mpv.getPropertyBoolean("pause") ?: true
|
||||
val pausedForCache = mpv.getPropertyBoolean("paused-for-cache") ?: false
|
||||
|
|
@ -1038,6 +1075,12 @@ private class NuvioLibmpvView(
|
|||
val isCacheBuffering = cacheBufferingState != null && cacheBufferingState in 0 until 100
|
||||
val isLoading = pausedForCache ||
|
||||
(!paused && !ended && (seeking || isCacheBuffering || (idle && durationMs <= 0L)))
|
||||
val videoWidth = mpv.getPropertyInt("video-out-params/dw")
|
||||
?: mpv.getPropertyInt("video-params/dw")
|
||||
?: 0
|
||||
val videoHeight = mpv.getPropertyInt("video-out-params/dh")
|
||||
?: mpv.getPropertyInt("video-params/dh")
|
||||
?: 0
|
||||
return PlayerPlaybackSnapshot(
|
||||
isLoading = isLoading,
|
||||
isPlaying = !paused && !isLoading && !idle && !ended,
|
||||
|
|
@ -1046,6 +1089,8 @@ private class NuvioLibmpvView(
|
|||
positionMs = positionMs,
|
||||
bufferedPositionMs = maxOf(positionMs, cachePositionMs),
|
||||
playbackSpeed = (mpv.getPropertyDouble("speed") ?: 1.0).toFloat(),
|
||||
videoWidth = videoWidth,
|
||||
videoHeight = videoHeight,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1276,8 +1321,9 @@ private const val MPV_SUBTITLE_FONT_SIZE_MIN = 36
|
|||
private const val MPV_SUBTITLE_FONT_SIZE_MAX = 122
|
||||
private const val MPV_SUBTITLE_OUTLINE_SIZE_SCALE = 1.5
|
||||
|
||||
private fun ExoPlayer.snapshot(): PlayerPlaybackSnapshot =
|
||||
PlayerPlaybackSnapshot(
|
||||
private fun ExoPlayer.snapshot(): PlayerPlaybackSnapshot {
|
||||
val (videoWidth, videoHeight) = videoDimensions()
|
||||
return PlayerPlaybackSnapshot(
|
||||
isLoading = playbackState == Player.STATE_IDLE || playbackState == Player.STATE_BUFFERING,
|
||||
isPlaying = isPlaying,
|
||||
isEnded = playbackState == Player.STATE_ENDED,
|
||||
|
|
@ -1285,7 +1331,21 @@ private fun ExoPlayer.snapshot(): PlayerPlaybackSnapshot =
|
|||
positionMs = currentPosition.coerceAtLeast(0L),
|
||||
bufferedPositionMs = bufferedPosition.coerceAtLeast(0L),
|
||||
playbackSpeed = playbackParameters.speed,
|
||||
videoWidth = videoWidth,
|
||||
videoHeight = videoHeight,
|
||||
)
|
||||
}
|
||||
|
||||
private fun ExoPlayer.videoDimensions(): Pair<Int, Int> {
|
||||
val format = videoFormat ?: return videoSize.width to videoSize.height
|
||||
val hasCrop = format.decodedWidth != Format.NO_VALUE &&
|
||||
format.decodedHeight != Format.NO_VALUE &&
|
||||
(format.decodedWidth > format.width || format.decodedHeight > format.height)
|
||||
val baseWidth = if (hasCrop) format.width else (format.width.takeIf { it > 0 } ?: videoSize.width)
|
||||
val baseHeight = if (hasCrop) format.height else (format.height.takeIf { it > 0 } ?: videoSize.height)
|
||||
val ratio = format.pixelWidthHeightRatio
|
||||
return if (ratio != 1f) (baseWidth * ratio).roundToInt() to baseHeight else baseWidth to baseHeight
|
||||
}
|
||||
|
||||
private fun ExoPlayer.shouldKeepPlayerScreenOn(): Boolean =
|
||||
playerError == null &&
|
||||
|
|
@ -1448,7 +1508,7 @@ private fun android.widget.FrameLayout.removeAssOverlayChildren() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun PlayerView.applySubtitleStyle(style: SubtitleStyleState) {
|
||||
private fun PlayerView.applySubtitleStyle(style: SubtitleStyleState, pipScale: Float = 1.0f) {
|
||||
subtitleView?.apply {
|
||||
val baseBottomPaddingFraction = SubtitleView.DEFAULT_BOTTOM_PADDING_FRACTION * 2f / 3f
|
||||
val offsetFraction = (style.bottomOffset / 1000f).coerceIn(0f, 0.2f)
|
||||
|
|
@ -1467,7 +1527,7 @@ private fun PlayerView.applySubtitleStyle(style: SubtitleStyleState) {
|
|||
if (style.bold) Typeface.DEFAULT_BOLD else Typeface.DEFAULT,
|
||||
)
|
||||
)
|
||||
setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, style.fontSizeSp.toFloat())
|
||||
setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, style.fontSizeSp.toFloat() * pipScale)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,37 +2,52 @@ package com.nuvio.app.features.player
|
|||
|
||||
import android.app.Activity
|
||||
import android.app.PictureInPictureParams
|
||||
import android.app.RemoteAction
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Rational
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.nuvio.app.R
|
||||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
internal const val PIP_ACTION_TOGGLE_PLAY_PAUSE = "com.nuvio.app.action.PIP_TOGGLE_PLAY_PAUSE"
|
||||
|
||||
internal object PlayerPictureInPictureManager {
|
||||
private data class SessionState(
|
||||
val isActive: Boolean = false,
|
||||
val isPlaying: Boolean = false,
|
||||
val playerSize: IntSize = IntSize.Zero,
|
||||
val videoSize: IntSize = IntSize.Zero,
|
||||
)
|
||||
|
||||
private var sessionState = SessionState()
|
||||
private var lastAppliedSessionState: SessionState? = null
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private var wasInPictureInPictureMode = false
|
||||
private var pendingPictureInPictureExitCheck: Runnable? = null
|
||||
private var pausePlaybackCallback: (() -> Unit)? = null
|
||||
private var togglePlaybackCallback: (() -> Unit)? = null
|
||||
|
||||
fun updateSession(
|
||||
activity: Activity,
|
||||
isActive: Boolean,
|
||||
isPlaying: Boolean,
|
||||
playerSize: IntSize,
|
||||
videoSize: IntSize,
|
||||
) {
|
||||
sessionState = SessionState(
|
||||
isActive = isActive,
|
||||
isPlaying = isPlaying,
|
||||
playerSize = playerSize,
|
||||
videoSize = videoSize,
|
||||
)
|
||||
applyPictureInPictureParams(activity)
|
||||
}
|
||||
|
|
@ -51,6 +66,10 @@ internal object PlayerPictureInPictureManager {
|
|||
}
|
||||
}
|
||||
|
||||
fun registerTogglePlaybackCallback(callback: (() -> Unit)?) {
|
||||
togglePlaybackCallback = callback
|
||||
}
|
||||
|
||||
fun onUserLeaveHint(activity: Activity): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
return false
|
||||
|
|
@ -80,9 +99,20 @@ internal object PlayerPictureInPictureManager {
|
|||
mainHandler.postDelayed(exitCheck, 250L)
|
||||
}
|
||||
|
||||
fun toggleViaRemoteAction(context: Context) {
|
||||
val wasPlaying = sessionState.isPlaying
|
||||
togglePlaybackCallback?.invoke()
|
||||
sessionState = sessionState.copy(isPlaying = !wasPlaying)
|
||||
if (context is Activity) {
|
||||
applyPictureInPictureParams(context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyPictureInPictureParams(activity: Activity) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
activity.setPictureInPictureParams(buildParams())
|
||||
if (sessionState == lastAppliedSessionState) return
|
||||
lastAppliedSessionState = sessionState
|
||||
activity.setPictureInPictureParams(buildParams(activity))
|
||||
}
|
||||
|
||||
private fun enterIfEligible(activity: Activity): Boolean {
|
||||
|
|
@ -90,12 +120,13 @@ internal object PlayerPictureInPictureManager {
|
|||
if (!sessionState.isActive || !sessionState.isPlaying) return false
|
||||
if (activity.isFinishing) return false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInPictureInPictureMode) return false
|
||||
return activity.enterPictureInPictureMode(buildParams())
|
||||
return activity.enterPictureInPictureMode(buildParams(activity))
|
||||
}
|
||||
|
||||
private fun buildParams(): PictureInPictureParams {
|
||||
private fun buildParams(activity: Activity): PictureInPictureParams {
|
||||
val builder = PictureInPictureParams.Builder()
|
||||
buildAspectRatio(sessionState.playerSize)?.let(builder::setAspectRatio)
|
||||
buildAspectRatio(sessionState.videoSize)?.let(builder::setAspectRatio)
|
||||
builder.setActions(buildActions(activity))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setAutoEnterEnabled(sessionState.isActive && sessionState.isPlaying)
|
||||
builder.setSeamlessResizeEnabled(true)
|
||||
|
|
@ -103,25 +134,68 @@ internal object PlayerPictureInPictureManager {
|
|||
return builder.build()
|
||||
}
|
||||
|
||||
private fun buildAspectRatio(playerSize: IntSize): Rational? {
|
||||
if (playerSize.width <= 0 || playerSize.height <= 0) return null
|
||||
private fun buildAspectRatio(videoSize: IntSize): Rational? {
|
||||
if (videoSize.width <= 0 || videoSize.height <= 0) return null
|
||||
|
||||
val width = playerSize.width.coerceAtLeast(1)
|
||||
val height = playerSize.height.coerceAtLeast(1)
|
||||
val width = videoSize.width.coerceAtLeast(1)
|
||||
val height = videoSize.height.coerceAtLeast(1)
|
||||
val ratio = width.toDouble() / height.toDouble()
|
||||
|
||||
return when {
|
||||
ratio > MaxPictureInPictureAspectRatio -> Rational(239, 100)
|
||||
ratio < MinPictureInPictureAspectRatio -> Rational(100, 239)
|
||||
ratio > MaxPictureInPictureAspectRatio ->
|
||||
Rational((MaxPictureInPictureAspectRatio * 100).roundToInt(), 100)
|
||||
ratio < MinPictureInPictureAspectRatio ->
|
||||
Rational(100, (MaxPictureInPictureAspectRatio * 100).roundToInt())
|
||||
else -> Rational(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildActions(activity: Activity): List<RemoteAction> {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return emptyList()
|
||||
|
||||
val isPlaying = sessionState.isPlaying
|
||||
val iconRes = if (isPlaying) R.drawable.ic_player_pause else R.drawable.ic_player_play
|
||||
val title = if (isPlaying) pipPauseLabel else pipPlayLabel
|
||||
|
||||
val toggleIntent = Intent(PIP_ACTION_TOGGLE_PLAY_PAUSE).setPackage(activity.packageName)
|
||||
val flags = android.app.PendingIntent.FLAG_IMMUTABLE or android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
val pendingIntent = android.app.PendingIntent.getBroadcast(activity, REQUEST_CODE_TOGGLE, toggleIntent, flags)
|
||||
val icon = Icon.createWithResource(activity, iconRes)
|
||||
icon.setTint(android.graphics.Color.WHITE)
|
||||
return listOf(RemoteAction(icon, title, title, pendingIntent))
|
||||
}
|
||||
|
||||
private fun clearPendingPictureInPictureExitCheck() {
|
||||
pendingPictureInPictureExitCheck?.let(mainHandler::removeCallbacks)
|
||||
pendingPictureInPictureExitCheck = null
|
||||
}
|
||||
|
||||
private const val REQUEST_CODE_TOGGLE = 1
|
||||
private const val MaxPictureInPictureAspectRatio = 2.39
|
||||
private const val MinPictureInPictureAspectRatio = 1.0 / MaxPictureInPictureAspectRatio
|
||||
|
||||
private val pipPlayLabel: String by lazy { runBlocking { getString(Res.string.action_play) } }
|
||||
private val pipPauseLabel: String by lazy { runBlocking { getString(Res.string.compose_action_pause) } }
|
||||
}
|
||||
|
||||
private const val MaxPictureInPictureAspectRatio = 2.39
|
||||
private const val MinPictureInPictureAspectRatio = 1.0 / MaxPictureInPictureAspectRatio
|
||||
internal class PipRemoteActionReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (context == null) return
|
||||
if (intent?.action != PIP_ACTION_TOGGLE_PLAY_PAUSE) return
|
||||
PlayerPictureInPictureManager.toggleViaRemoteAction(context)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun register(context: Context): PipRemoteActionReceiver {
|
||||
val filter = IntentFilter(PIP_ACTION_TOGGLE_PLAY_PAUSE)
|
||||
val receiver = PipRemoteActionReceiver()
|
||||
androidx.core.content.ContextCompat.registerReceiver(
|
||||
context,
|
||||
receiver,
|
||||
filter,
|
||||
androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
return receiver
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,15 +5,21 @@ import android.content.Context
|
|||
import android.content.ContextWrapper
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.media.AudioManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.app.PictureInPictureModeChangedInfo
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
|
|
@ -57,7 +63,7 @@ actual fun EnterImmersivePlayerMode(keepScreenAwake: Boolean) {
|
|||
@Composable
|
||||
actual fun ManagePlayerPictureInPicture(
|
||||
isPlaying: Boolean,
|
||||
playerSize: IntSize,
|
||||
videoSize: IntSize,
|
||||
) {
|
||||
val activity = LocalContext.current.findActivity() ?: return
|
||||
|
||||
|
|
@ -72,11 +78,30 @@ actual fun ManagePlayerPictureInPicture(
|
|||
activity = activity,
|
||||
isActive = true,
|
||||
isPlaying = isPlaying,
|
||||
playerSize = playerSize,
|
||||
videoSize = videoSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun rememberIsInPictureInPicture(): Boolean {
|
||||
val context = LocalContext.current
|
||||
val activity = context.findActivity() ?: return false
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return false
|
||||
val componentActivity = activity as? ComponentActivity ?: return false
|
||||
var pipState by remember(activity) { mutableStateOf(componentActivity.isInPictureInPictureMode) }
|
||||
DisposableEffect(componentActivity) {
|
||||
val listener = Consumer<PictureInPictureModeChangedInfo> { info ->
|
||||
pipState = info.isInPictureInPictureMode
|
||||
}
|
||||
componentActivity.addOnPictureInPictureModeChangedListener(listener)
|
||||
onDispose {
|
||||
componentActivity.removeOnPictureInPictureModeChangedListener(listener)
|
||||
}
|
||||
}
|
||||
return pipState
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun rememberPlayerGestureController(): PlayerGestureController? {
|
||||
val context = LocalContext.current
|
||||
|
|
|
|||
|
|
@ -217,6 +217,8 @@ data class PlayerPlaybackSnapshot(
|
|||
val positionMs: Long = 0L,
|
||||
val bufferedPositionMs: Long = 0L,
|
||||
val playbackSpeed: Float = 1f,
|
||||
val videoWidth: Int = 0,
|
||||
val videoHeight: Int = 0,
|
||||
)
|
||||
|
||||
data class PlayerNowPlayingInfo(
|
||||
|
|
|
|||
|
|
@ -24,8 +24,11 @@ expect fun EnterImmersivePlayerMode(keepScreenAwake: Boolean)
|
|||
@Composable
|
||||
expect fun ManagePlayerPictureInPicture(
|
||||
isPlaying: Boolean,
|
||||
playerSize: IntSize,
|
||||
videoSize: IntSize,
|
||||
)
|
||||
|
||||
@Composable
|
||||
expect fun rememberIsInPictureInPicture(): Boolean
|
||||
|
||||
@Composable
|
||||
expect fun rememberPlayerGestureController(): PlayerGestureController?
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.nuvio.app.features.addons.AddonRepository
|
||||
import com.nuvio.app.features.details.MetaDetailsRepository
|
||||
|
|
@ -138,7 +139,10 @@ internal fun PlayerScreenContent(args: PlayerScreenArgs) {
|
|||
EnterImmersivePlayerMode(keepScreenAwake = keepScreenAwake)
|
||||
ManagePlayerPictureInPicture(
|
||||
isPlaying = runtime.playbackSnapshot.isPlaying,
|
||||
playerSize = runtime.layoutSize,
|
||||
videoSize = IntSize(
|
||||
runtime.playbackSnapshot.videoWidth,
|
||||
runtime.playbackSnapshot.videoHeight,
|
||||
),
|
||||
)
|
||||
runtime.BindPlayerRuntimeEffects()
|
||||
runtime.RenderPlayerRuntimeUi()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import nuvio.composeapp.generated.resources.*
|
|||
@Composable
|
||||
internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
|
||||
val runtime = this
|
||||
val isInPip = rememberIsInPictureInPicture()
|
||||
val displayedPositionMs = scrubbingPositionMs ?: playbackSnapshot.positionMs
|
||||
val isEpisode = activeSeasonNumber != null && activeEpisodeNumber != null
|
||||
val currentGestureFeedback = liveGestureFeedback ?: gestureFeedback
|
||||
|
|
@ -188,8 +189,9 @@ internal fun PlayerScreenRuntime.RenderPlayerRuntimeUi() {
|
|||
|
||||
@Composable
|
||||
private fun PlayerScreenRuntime.RenderPlayerControls(displayedPositionMs: Long, isEpisode: Boolean) {
|
||||
val isInPip = rememberIsInPictureInPicture()
|
||||
AnimatedVisibility(
|
||||
visible = (controlsVisible || showParentalGuide) && !playerControlsLocked,
|
||||
visible = (controlsVisible || showParentalGuide) && !playerControlsLocked && !isInPip,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -48,9 +48,12 @@ actual fun EnterImmersivePlayerMode(keepScreenAwake: Boolean) {
|
|||
@Composable
|
||||
actual fun ManagePlayerPictureInPicture(
|
||||
isPlaying: Boolean,
|
||||
playerSize: IntSize,
|
||||
videoSize: IntSize,
|
||||
) = Unit
|
||||
|
||||
@Composable
|
||||
actual fun rememberIsInPictureInPicture(): Boolean = false
|
||||
|
||||
@Composable
|
||||
actual fun rememberPlayerGestureController(): PlayerGestureController? {
|
||||
val controller = remember { IOSPlayerGestureController() }
|
||||
|
|
|
|||
Loading…
Reference in a new issue