From 4f62be91b8d51c4116d9e23c3e2ef628e448495c Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 2 May 2026 14:05:47 +0530 Subject: [PATCH] fix: ios player not recreating after switching source/episode --- .../app/features/player/PlayerEngine.ios.kt | 244 +++++++++--------- 1 file changed, 123 insertions(+), 121 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 a0286b9f..2877b04c 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 @@ -34,180 +34,182 @@ actual fun PlatformPlayerSurface( onError: (String?) -> Unit, ) { sanitizePlaybackResponseHeaders(sourceResponseHeaders) + val latestOnControllerReady = rememberUpdatedState(onControllerReady) val latestOnSnapshot = rememberUpdatedState(onSnapshot) val latestOnError = rememberUpdatedState(onError) - val bridge = remember(sourceUrl) { + val bridge = remember { NuvioPlayerBridgeFactory.create() } if (bridge == null) { LaunchedEffect(Unit) { - onError("MPV player engine not available. Please rebuild the app.") + latestOnError.value("MPV player engine not available. Please rebuild the app.") } return } - // Create controller - LaunchedEffect(bridge) { - onControllerReady( - object : PlayerEngineController { - override fun play() { - bridge.play() - } + val controller = remember(bridge) { + object : PlayerEngineController { + override fun play() { + bridge.play() + } - override fun pause() { - bridge.pause() - } + override fun pause() { + bridge.pause() + } - override fun seekTo(positionMs: Long) { - bridge.seekTo(positionMs) - } + override fun seekTo(positionMs: Long) { + bridge.seekTo(positionMs) + } - override fun seekBy(offsetMs: Long) { - bridge.seekBy(offsetMs) - } + override fun seekBy(offsetMs: Long) { + bridge.seekBy(offsetMs) + } - override fun retry() { - bridge.retry() - } + override fun retry() { + bridge.retry() + } - override fun setPlaybackSpeed(speed: Float) { - bridge.setPlaybackSpeed(speed) - } + override fun setPlaybackSpeed(speed: Float) { + bridge.setPlaybackSpeed(speed) + } - override fun getAudioTracks(): List { - val count = bridge.getAudioTrackCount() - return (0 until count).map { i -> - AudioTrack( - index = bridge.getAudioTrackIndex(i), - id = bridge.getAudioTrackId(i), - label = bridge.getAudioTrackLabel(i), - language = bridge.getAudioTrackLang(i), - isSelected = bridge.isAudioTrackSelected(i), - ) - } + override fun getAudioTracks(): List { + val count = bridge.getAudioTrackCount() + return (0 until count).map { i -> + AudioTrack( + index = bridge.getAudioTrackIndex(i), + id = bridge.getAudioTrackId(i), + label = bridge.getAudioTrackLabel(i), + language = bridge.getAudioTrackLang(i), + isSelected = bridge.isAudioTrackSelected(i), + ) } + } - override fun getSubtitleTracks(): List { - val count = bridge.getSubtitleTrackCount() - val tracks = (0 until count).map { i -> - val trackId = bridge.getSubtitleTrackId(i) - val trackLabel = bridge.getSubtitleTrackLabel(i) - val trackLanguage = bridge.getSubtitleTrackLang(i) - SubtitleTrack( - index = bridge.getSubtitleTrackIndex(i), - id = trackId, + override fun getSubtitleTracks(): List { + val count = bridge.getSubtitleTrackCount() + val tracks = (0 until count).map { i -> + val trackId = bridge.getSubtitleTrackId(i) + val trackLabel = bridge.getSubtitleTrackLabel(i) + val trackLanguage = bridge.getSubtitleTrackLang(i) + SubtitleTrack( + index = bridge.getSubtitleTrackIndex(i), + id = trackId, + label = trackLabel, + language = trackLanguage, + isSelected = bridge.isSubtitleTrackSelected(i), + isForced = inferForcedSubtitleTrack( label = trackLabel, language = trackLanguage, - isSelected = bridge.isSubtitleTrackSelected(i), - isForced = inferForcedSubtitleTrack( - label = trackLabel, - language = trackLanguage, - trackId = trackId, - ), - ) - } - Logger.d(TAG) { "getSubtitleTracks: found ${tracks.size} tracks" } - return tracks + trackId = trackId, + ), + ) } + Logger.d(TAG) { "getSubtitleTracks: found ${tracks.size} tracks" } + return tracks + } - override fun selectAudioTrack(index: Int) { - // Convert from logical track index to mpv track id - val count = bridge.getAudioTrackCount() + override fun selectAudioTrack(index: Int) { + // Convert from logical track index to mpv track id + val count = bridge.getAudioTrackCount() + if (count <= 0) return + + val trackId = (0 until count) + .firstNotNullOfOrNull { at -> + if (bridge.getAudioTrackIndex(at) == index) { + bridge.getAudioTrackId(at).toIntOrNull() + } else { + null + } + } + ?: if (index in 0 until count) { + bridge.getAudioTrackId(index).toIntOrNull() ?: (index + 1) + } else { + null + } + + if (trackId != null) { + bridge.selectAudioTrack(trackId) + } + } + + override fun selectSubtitleTrack(index: Int) { + if (index < 0) { + bridge.selectSubtitleTrack(-1) // disable + } else { + val count = bridge.getSubtitleTrackCount() if (count <= 0) return val trackId = (0 until count) .firstNotNullOfOrNull { at -> - if (bridge.getAudioTrackIndex(at) == index) { - bridge.getAudioTrackId(at).toIntOrNull() + if (bridge.getSubtitleTrackIndex(at) == index) { + bridge.getSubtitleTrackId(at).toIntOrNull() } else { null } } ?: if (index in 0 until count) { - bridge.getAudioTrackId(index).toIntOrNull() ?: (index + 1) + bridge.getSubtitleTrackId(index).toIntOrNull() ?: (index + 1) } else { null } if (trackId != null) { - bridge.selectAudioTrack(trackId) + bridge.selectSubtitleTrack(trackId) } } + } - override fun selectSubtitleTrack(index: Int) { - if (index < 0) { - bridge.selectSubtitleTrack(-1) // disable + override fun setSubtitleUri(url: String) { + Logger.d(TAG) { "setSubtitleUri: $url" } + bridge.setSubtitleUrl(url) + } + + override fun clearExternalSubtitle() { + bridge.clearExternalSubtitle() + } + + override fun clearExternalSubtitleAndSelect(trackIndex: Int) { + val trackId = if (trackIndex < 0) { + -1 + } else { + val count = bridge.getSubtitleTrackCount() + if (count <= 0) { + trackIndex + 1 } else { - val count = bridge.getSubtitleTrackCount() - if (count <= 0) return - - val trackId = (0 until count) + (0 until count) .firstNotNullOfOrNull { at -> - if (bridge.getSubtitleTrackIndex(at) == index) { + if (bridge.getSubtitleTrackIndex(at) == trackIndex) { bridge.getSubtitleTrackId(at).toIntOrNull() } else { null } } - ?: if (index in 0 until count) { - bridge.getSubtitleTrackId(index).toIntOrNull() ?: (index + 1) + ?: if (trackIndex in 0 until count) { + bridge.getSubtitleTrackId(trackIndex).toIntOrNull() ?: (trackIndex + 1) } else { - null + trackIndex + 1 } - - if (trackId != null) { - bridge.selectSubtitleTrack(trackId) - } } } - - override fun setSubtitleUri(url: String) { - Logger.d(TAG) { "setSubtitleUri: $url" } - bridge.setSubtitleUrl(url) - } - - override fun clearExternalSubtitle() { - bridge.clearExternalSubtitle() - } - - override fun clearExternalSubtitleAndSelect(trackIndex: Int) { - val trackId = if (trackIndex < 0) { - -1 - } else { - val count = bridge.getSubtitleTrackCount() - if (count <= 0) { - trackIndex + 1 - } else { - (0 until count) - .firstNotNullOfOrNull { at -> - if (bridge.getSubtitleTrackIndex(at) == trackIndex) { - bridge.getSubtitleTrackId(at).toIntOrNull() - } else { - null - } - } - ?: if (trackIndex in 0 until count) { - bridge.getSubtitleTrackId(trackIndex).toIntOrNull() ?: (trackIndex + 1) - } else { - trackIndex + 1 - } - } - } - bridge.clearExternalSubtitleAndSelect(trackId) - } - - override fun applySubtitleStyle(style: SubtitleStyleState) { - bridge.applySubtitleStyle( - textColor = style.textColor.toMpvColorString(), - outlineSize = if (style.outlineEnabled) 1.65f else 0f, - fontSize = style.toMpvSubtitleFontSize(), - subPos = style.toMpvSubtitlePosition(), - ) - } + bridge.clearExternalSubtitleAndSelect(trackId) } - ) + + override fun applySubtitleStyle(style: SubtitleStyleState) { + bridge.applySubtitleStyle( + textColor = style.textColor.toMpvColorString(), + outlineSize = if (style.outlineEnabled) 1.65f else 0f, + fontSize = style.toMpvSubtitleFontSize(), + subPos = style.toMpvSubtitlePosition(), + ) + } + } + } + + LaunchedEffect(controller, sourceUrl, sourceAudioUrl, sourceHeaders, sourceResponseHeaders) { + latestOnControllerReady.value(controller) } // Load file and set initial state