From 4db3231c2a17abb8db8f9b6b05144b8a1a20a9e5 Mon Sep 17 00:00:00 2001 From: darkflame91 Date: Sun, 12 Jul 2026 17:27:36 +0530 Subject: [PATCH] fix(android): fix issues with PiP window --- .../kotlin/com/nuvio/app/MainActivity.kt | 9 ++ .../features/player/PlayerEngine.android.kt | 74 +++++++++++-- .../PlayerPictureInPictureManager.android.kt | 104 +++++++++++++++--- .../player/PlayerPlatformEffects.android.kt | 29 ++++- .../nuvio/app/features/player/PlayerModels.kt | 2 + .../features/player/PlayerPlatformEffects.kt | 5 +- .../features/player/PlayerScreenContent.kt | 6 +- .../features/player/PlayerScreenRuntimeUi.kt | 4 +- .../player/PlayerPlatformEffects.ios.kt | 5 +- 9 files changed, 210 insertions(+), 28 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt index 1c54fd0d..645c9b26 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt @@ -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() } diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt index 1c10a237..6826571a 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt @@ -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(null) } var currentSubtitleStyle by remember { mutableStateOf(SubtitleStyleState.DEFAULT) } var subtitleSelectionJob by remember { mutableStateOf(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 { + 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) } } diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerPictureInPictureManager.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerPictureInPictureManager.android.kt index 33a05be4..70c9467d 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerPictureInPictureManager.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerPictureInPictureManager.android.kt @@ -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 { + 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 \ No newline at end of file +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 + } + } +} diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerPlatformEffects.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerPlatformEffects.android.kt index c02f9f6a..8f5c9704 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerPlatformEffects.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerPlatformEffects.android.kt @@ -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 { info -> + pipState = info.isInPictureInPictureMode + } + componentActivity.addOnPictureInPictureModeChangedListener(listener) + onDispose { + componentActivity.removeOnPictureInPictureModeChangedListener(listener) + } + } + return pipState +} + @Composable actual fun rememberPlayerGestureController(): PlayerGestureController? { val context = LocalContext.current diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt index 09d2bd08..f4c21143 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerModels.kt @@ -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( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerPlatformEffects.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerPlatformEffects.kt index 024b0e50..1b8754ce 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerPlatformEffects.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerPlatformEffects.kt @@ -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? diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenContent.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenContent.kt index bc1363e5..af2cab91 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenContent.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenContent.kt @@ -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() diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt index 717ca541..44ac5c17 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt @@ -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(), ) { diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerPlatformEffects.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerPlatformEffects.ios.kt index 90575e4d..212d5f1d 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerPlatformEffects.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerPlatformEffects.ios.kt @@ -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() }