From af5459d5e8786a1fc2cbccbc4eef289d36c09c62 Mon Sep 17 00:00:00 2001 From: Luqman Fadlli <75630095+luqmanfadlli@users.noreply.github.com> Date: Sat, 6 Jun 2026 03:09:29 +0700 Subject: [PATCH 1/3] fix: trailer popup player sizing The previous popup layout only applied fillMaxWidth() to PlatformPlayerSurface, which could leave the underlying native UIView with zero-height bounds on iOS. As a result, the MPV bridge queued the trailer load but never issued the loadfile command because the player view was not ready. This changes ensure the trailer popup player surface fills its 16:9 container on iOS and Android. --- .../app/features/details/components/TrailerPlayerPopup.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/TrailerPlayerPopup.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/TrailerPlayerPopup.kt index c12ead6b..09976eba 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/TrailerPlayerPopup.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/TrailerPlayerPopup.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape @@ -173,7 +174,7 @@ fun TrailerPlayerPopup( sourceUrl = playbackSource.videoUrl, sourceAudioUrl = playbackSource.audioUrl, useYoutubeChunkedPlayback = true, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxSize(), playWhenReady = true, resizeMode = PlayerResizeMode.Fit, useNativeController = true, From 108d6cfb23eedbe2f1473d2f71b065d29f5a8772 Mon Sep 17 00:00:00 2001 From: Luqman Fadlli <75630095+luqmanfadlli@users.noreply.github.com> Date: Sat, 6 Jun 2026 03:12:08 +0700 Subject: [PATCH 2/3] feat(ios): implement native controller polyfill for MPV player MPV lacks a built-in native playback controller on iOS. Previously, passing `useNativeController = true` like being used in `TrailerPlayerPopup.kt` resulted in a bare video surface with no user interaction. This commit injects a custom Compose touch overlay (play/pause control) directly into `PlayerEngine.ios.kt` when native controls are requested. By isolating this polyfill to the iOS source set, we prevent touch event interception issues on Android, ensuring ExoPlayer's robust native UI remains fully functional and unobstructed. --- .../app/features/player/PlayerEngine.ios.kt | 72 +++++++++++++++++-- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt index 5fa4ffe7..e5f13406 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt @@ -4,9 +4,21 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material3.Icon +import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp import androidx.compose.ui.Modifier import androidx.compose.ui.interop.UIKitViewController import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -87,6 +99,23 @@ actual fun PlatformPlayerSurface( bridge.setPlaybackSpeed(speed) } + override fun currentPlayerVolume(): PlayerAudioLevel { + val current = bridge.getVolume().coerceIn(0f, 2f) + return PlayerAudioLevel( + fraction = current, + isMuted = current <= 0.001f, + ) + } + + override fun setPlayerVolume(level: Float): PlayerAudioLevel { + val target = level.coerceIn(0f, 2f) + bridge.setVolume(target) + return PlayerAudioLevel( + fraction = target, + isMuted = target <= 0.001f, + ) + } + override fun setMuted(muted: Boolean) { bridge.setMuted(muted) } @@ -295,17 +324,50 @@ actual fun PlatformPlayerSurface( // Cleanup DisposableEffect(bridge) { + IosPictureInPictureSession.registerBridge(bridge) onDispose { + IosPictureInPictureSession.unregisterBridge(bridge) bridge.destroy() } } // Render the player view - UIKitViewController( - factory = { bridge.createPlayerViewController() }, - modifier = modifier, - interactive = false, - ) + Box(modifier = modifier) { + UIKitViewController( + factory = { bridge.createPlayerViewController() }, + modifier = Modifier.fillMaxSize(), + interactive = false, + ) + + if (useNativeController) { + var isPlayingLocal by remember { mutableStateOf(playWhenReady) } + + Box( + modifier = Modifier + .fillMaxSize() + .background(if (!isPlayingLocal) Color.Black.copy(alpha = 0.4f) else Color.Transparent) + .clickable { + if (isPlayingLocal) { + bridge.pause() + isPlayingLocal = false + } else { + bridge.play() + isPlayingLocal = true + } + }, + contentAlignment = Alignment.Center + ) { + if (!isPlayingLocal) { + Icon( + imageVector = Icons.Rounded.PlayArrow, + contentDescription = "Play", + tint = Color.White, + modifier = Modifier.size(64.dp) + ) + } + } + } + } } private fun NuvioPlayerBridge.applyIosVideoOutputSettings(settings: PlayerSettingsUiState) { From c23fff9c73bac84259b9bb3efe7c307fd1c00f52 Mon Sep 17 00:00:00 2001 From: Luqman Fadlli <75630095+luqmanfadlli@users.noreply.github.com> Date: Mon, 8 Jun 2026 02:09:22 +0700 Subject: [PATCH 3/3] remove orphaned IosPictureInPictureSession --- .../kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt index 7cf6a766..623b90ca 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerEngine.ios.kt @@ -324,9 +324,7 @@ actual fun PlatformPlayerSurface( // Cleanup DisposableEffect(bridge) { - IosPictureInPictureSession.registerBridge(bridge) onDispose { - IosPictureInPictureSession.unregisterBridge(bridge) bridge.destroy() } }