From 76bbef1545bdb7dfef0c142b39f8a171a1d0bce4 Mon Sep 17 00:00:00 2001 From: Ilia Bogdanovich Date: Thu, 9 Oct 2025 11:16:14 +0200 Subject: [PATCH] Fixed opening `file:` URIs without authority on macOS This patch addresses an important case when the URI has the form of `file:/Users/.../file.mp4` where authority (//) is not provided. This problem specifically shows up when opening files from Compose Multiplatform resources, acquired by `Res.getUri()` method, which in turn calls java's `ClassLoader.getResource()` method that returns a URI without authority. --- .../mac/MacVideoPlayerState.kt | 47 ++++++----- .../mac/MacVideoPlayerStateTest.kt | 80 ++++++++++++++++++- .../src/jvmTest/resources/existing_file.mp4 | 0 3 files changed, 107 insertions(+), 20 deletions(-) create mode 100644 mediaplayer/src/jvmTest/resources/existing_file.mp4 diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerState.kt b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerState.kt index b606ae24..936fb4b3 100644 --- a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerState.kt +++ b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerState.kt @@ -18,6 +18,8 @@ import io.github.kdroidfilter.composemediaplayer.SubtitleTrack import io.github.kdroidfilter.composemediaplayer.VideoMetadata import io.github.kdroidfilter.composemediaplayer.VideoPlayerError import io.github.kdroidfilter.composemediaplayer.util.formatTime +import io.github.vinceglb.filekit.utils.toFile +import io.github.vinceglb.filekit.utils.toPath import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.debounce @@ -25,6 +27,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.awt.image.BufferedImage import java.awt.image.DataBufferInt +import java.net.URI import kotlin.math.abs import kotlin.math.log10 @@ -218,21 +221,33 @@ class MacVideoPlayerState : PlatformVideoPlayerState { } } + // Check if this is a local file that doesn't exist + // This handles both URIs with a "file:" scheme and simple filenames without a scheme, with or without authority. + private fun checkExistsIfLocalFile(uri: String): Boolean { + val javaUri = try { + URI.create(uri) + } catch (e: IllegalArgumentException) { + macLogger.e(e) { "URI object is malformed: $uri" } + return false + } + return if (javaUri.scheme == "file" || javaUri.scheme == null) { + val file = javaUri.path?.toPath()?.toFile() + file?.exists() == true + } else { + true + } + } + override fun openUri(uri: String, initializeplayerState: InitialPlayerState) { macLogger.d { "openUri() - Opening URI: $uri, initializeplayerState: $initializeplayerState" } lastUri = uri // Check if this is a local file that doesn't exist - // This handles both URIs with file:// scheme and simple filenames without a scheme - if (uri.startsWith("file://") || !uri.contains("://") || !uri.matches("^[a-zA-Z]+://.*".toRegex())) { - val filePath = uri.replace("file://", "") - val file = java.io.File(filePath) - if (!file.exists()) { - macLogger.e { "File does not exist: $filePath" } - setPlayerError(VideoPlayerError.SourceError("File not found: $filePath")) - return - } + if (!checkExistsIfLocalFile(uri)) { + macLogger.e { "File does not exist: $uri" } + setPlayerError(VideoPlayerError.SourceError("File not found: $uri")) + return } // Update UI state first @@ -352,15 +367,11 @@ class MacVideoPlayerState : PlatformVideoPlayerState { // Check if file exists (for local files) // This handles both URIs with file:// scheme and simple filenames without a scheme - if (uri.startsWith("file://") || !uri.contains("://") || !uri.matches("^[a-zA-Z]+://.*".toRegex())) { - val filePath = uri.replace("file://", "") - val file = java.io.File(filePath) - if (!file.exists()) { - macLogger.e { "File does not exist: $filePath" } - // Use setPlayerError to ensure the error is set synchronously - setPlayerError(VideoPlayerError.SourceError("File not found: $filePath")) - return false - } + if (!checkExistsIfLocalFile(uri)) { + macLogger.e { "File does not exist: $uri" } + // Use setPlayerError to ensure the error is set synchronously + setPlayerError(VideoPlayerError.SourceError("File not found: $uri")) + return false } return try { diff --git a/mediaplayer/src/jvmTest/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerStateTest.kt b/mediaplayer/src/jvmTest/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerStateTest.kt index 184b7d1d..da2de80b 100644 --- a/mediaplayer/src/jvmTest/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerStateTest.kt +++ b/mediaplayer/src/jvmTest/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerStateTest.kt @@ -1,7 +1,5 @@ package io.github.kdroidfilter.composemediaplayer.mac -import io.github.kdroidfilter.composemediaplayer.PlatformVideoPlayerState -import io.github.kdroidfilter.composemediaplayer.VideoPlayerError import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -168,4 +166,82 @@ class MacVideoPlayerStateTest { // Clean up playerState.dispose() } + + private fun testOpenLocalFile(file: String) { + // Skip test if not running on Mac + if (!Platform.isMac()) { + println("Skipping Mac-specific test on non-Mac platform") + return + } + + val playerState = MacVideoPlayerState() + + // Initially there should be no error + assertNull(playerState.error) + + // Test opening a non-existent file (should cause an error) + runBlocking { + playerState.openUri(file) + delay(500) // Give some time for the error to be set + } + + // There should be no error + assertNull(playerState.error) + + // Clean up + playerState.dispose() + } + + @Test + fun testOpenLocalFile() { + val path = assertNotNull(javaClass.classLoader.getResource("existing_file.mp4")).toURI().path + testOpenLocalFile(path) + } + + @Test + fun testOpenLocalFileWithScheme() { + val path = assertNotNull(javaClass.classLoader.getResource("existing_file.mp4")).toURI().path + testOpenLocalFile("file:$path") + } + + @Test + fun testOpenLocalFileWithSchemeWithAuthority() { + val path = assertNotNull(javaClass.classLoader.getResource("existing_file.mp4")).toURI().path + testOpenLocalFile("file://$path") + } + + private fun testMalformedUri(uri: String) { + // Skip test if not running on Mac + if (!Platform.isMac()) { + println("Skipping Mac-specific test on non-Mac platform") + return + } + + val playerState = MacVideoPlayerState() + + // Initially there should be no error + assertNull(playerState.error) + + // Test opening a non-existent file (should cause an error) + runBlocking { + playerState.openUri(uri) + delay(500) // Give some time for the error to be set + } + + // There should be an error now + assertNotNull(playerState.error) + + // Test clearing the error + playerState.clearError() + assertNull(playerState.error) + + // Clean up + playerState.dispose() + } + + @Test + fun testMalformedUri() { + val path = assertNotNull(javaClass.classLoader.getResource("existing_file.mp4")).toURI().path + testMalformedUri("file:${path.removePrefix("/")}") + } } \ No newline at end of file diff --git a/mediaplayer/src/jvmTest/resources/existing_file.mp4 b/mediaplayer/src/jvmTest/resources/existing_file.mp4 new file mode 100644 index 00000000..e69de29b