Skip to content

Commit 3ea6341

Browse files
committed
fix(sample): dismiss bottom sheet before launching file picker on iOS
iOS cannot present a UIDocumentPickerViewController while a ModalBottomSheet is still visible (stacked modals not supported). Use a pendingPick flag + LaunchedEffect to launch the picker only after the sheet is fully dismissed. Also revert the unnecessary security-scoped resource access changes in VideoPlayerState.
1 parent 41ad9ec commit 3ea6341

2 files changed

Lines changed: 27 additions & 29 deletions

File tree

mediaplayer/src/iosMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.ios.kt

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import io.github.kdroidfilter.composemediaplayer.util.PipResult
1616
import io.github.kdroidfilter.composemediaplayer.util.TaggedLogger
1717
import io.github.kdroidfilter.composemediaplayer.util.formatTime
1818
import io.github.vinceglb.filekit.PlatformFile
19-
import io.github.vinceglb.filekit.startAccessingSecurityScopedResource
20-
import io.github.vinceglb.filekit.stopAccessingSecurityScopedResource
2119
import kotlinx.cinterop.COpaquePointer
2220
import kotlinx.cinterop.ExperimentalForeignApi
2321
import kotlinx.cinterop.useContents
@@ -172,8 +170,6 @@ open class DefaultVideoPlayerState(
172170
// Flag to track if the state has been disposed
173171
private var isDisposed = false
174172

175-
// Security-scoped file that needs to be released on cleanup
176-
private var securityScopedFile: PlatformFile? = null
177173

178174
init {
179175
if (cacheConfig.enabled) {
@@ -522,9 +518,6 @@ open class DefaultVideoPlayerState(
522518
player?.replaceCurrentItemWithPlayerItem(null)
523519
player = null
524520

525-
// Release security-scoped resource access from file picker
526-
securityScopedFile?.stopAccessingSecurityScopedResource()
527-
securityScopedFile = null
528521
}
529522

530523
/**
@@ -550,17 +543,7 @@ open class DefaultVideoPlayerState(
550543
iosLogger.d { "Failed to create NSURL from uri: $uri" }
551544
return
552545
}
553-
openNsUrl(nsUrl, initializeplayerState)
554-
}
555546

556-
/**
557-
* Core method to open media from an NSURL.
558-
* Both [openUri] and [openFile] delegate to this.
559-
*/
560-
private fun openNsUrl(
561-
nsUrl: NSURL,
562-
initializeplayerState: InitialPlayerState,
563-
) {
564547
_error = null
565548

566549
stopPositionUpdates()
@@ -735,16 +718,7 @@ open class DefaultVideoPlayerState(
735718
file: PlatformFile,
736719
initializeplayerState: InitialPlayerState,
737720
) {
738-
iosLogger.d { "openFile called with file: $file" }
739-
740-
// iOS requires security-scoped resource access for files picked via
741-
// UIDocumentPickerViewController. Without this, AVPlayer cannot read the file.
742-
val hasAccess = file.startAccessingSecurityScopedResource()
743-
iosLogger.d { "Security-scoped access: $hasAccess" }
744-
if (hasAccess) {
745-
securityScopedFile = file
746-
}
747-
721+
iosLogger.d { "openFile called with file: $file, initializeplayerState: $initializeplayerState" }
748722
val fileUrl = file.getUri()
749723
iosLogger.d { "Opening file with URL: $fileUrl" }
750724
openUri(fileUrl, initializeplayerState)

sample/composeApp/src/commonMain/kotlin/sample/app/player/PlayerScreen.kt

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ fun PlayerScreen(modifier: Modifier = Modifier) {
8888
var showSettingsSheet by remember { mutableStateOf(false) }
8989
var showSubtitleSheet by remember { mutableStateOf(false) }
9090

91+
// Flags to launch pickers after the bottom sheet is fully dismissed.
92+
// On iOS, presenting a file picker while a ModalBottomSheet is still
93+
// visible fails silently because iOS cannot stack two modals.
94+
var pendingPickVideo by remember { mutableStateOf(false) }
95+
var pendingPickSubtitle by remember { mutableStateOf(false) }
96+
9197
val videoFileLauncher = rememberFilePickerLauncher(type = FileKitType.Video) { file ->
9298
file?.let { playerState.openFile(it, initialPlayerState) }
9399
}
@@ -102,6 +108,21 @@ fun PlayerScreen(modifier: Modifier = Modifier) {
102108
}
103109
}
104110

111+
// Launch pickers only after the sheet is gone
112+
LaunchedEffect(pendingPickVideo, showSourceSheet) {
113+
if (pendingPickVideo && !showSourceSheet) {
114+
pendingPickVideo = false
115+
videoFileLauncher.launch()
116+
}
117+
}
118+
119+
LaunchedEffect(pendingPickSubtitle, showSubtitleSheet) {
120+
if (pendingPickSubtitle && !showSubtitleSheet) {
121+
pendingPickSubtitle = false
122+
subtitleFileLauncher.launch()
123+
}
124+
}
125+
105126
// Example: detect when playback reaches the end
106127
playerState.onPlaybackEnded = {
107128
println("Playback ended")
@@ -219,7 +240,7 @@ fun PlayerScreen(modifier: Modifier = Modifier) {
219240
showSourceSheet = false
220241
},
221242
onPickFile = {
222-
videoFileLauncher.launch()
243+
pendingPickVideo = true
223244
showSourceSheet = false
224245
},
225246
onSelectPreset = { url ->
@@ -252,7 +273,10 @@ fun PlayerScreen(modifier: Modifier = Modifier) {
252273
selectedSubtitleTrack = null
253274
playerState.disableSubtitles()
254275
},
255-
onPickFile = { subtitleFileLauncher.launch() },
276+
onPickFile = {
277+
pendingPickSubtitle = true
278+
showSubtitleSheet = false
279+
},
256280
onAddTrack = { track ->
257281
subtitleTracks.add(track)
258282
selectedSubtitleTrack = track

0 commit comments

Comments
 (0)