From c74577414e668b3a71fefb09bf3b6554b8a6c12f Mon Sep 17 00:00:00 2001 From: Sabeeh Ul Hussnain Date: Mon, 5 Oct 2026 23:27:39 +0500 Subject: [PATCH] feat: configure macOS video dimensions timeout --- .github/workflows/build-test.yml | 34 +++++ README_VIDEO.MD | 21 +++ mediaplayer/build.gradle.kts | 12 ++ .../VideoPlayerState.jvm.kt | 36 ++++- .../mac/MacVideoDimensionsTimeout.kt | 24 ++++ .../mac/MacVideoPlayerState.kt | 45 ++++--- .../MacVideoDimensionsTimeoutNativeTest.kt | 97 ++++++++++++++ .../mac/MacVideoDimensionsTimeoutTest.kt | 123 ++++++++++++++++++ .../resources/startup/metadata-startup.mp4 | Bin 0 -> 20006 bytes 9 files changed, 372 insertions(+), 20 deletions(-) create mode 100644 mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoDimensionsTimeout.kt create mode 100644 mediaplayer/src/jvmTest/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoDimensionsTimeoutNativeTest.kt create mode 100644 mediaplayer/src/jvmTest/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoDimensionsTimeoutTest.kt create mode 100644 mediaplayer/src/jvmTest/resources/startup/metadata-startup.mp4 diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 243855f5..f75e9a4d 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -59,6 +59,40 @@ jobs: name: test-reports-jvm path: '**/build/reports/tests/' + macos-video-dimensions-timeout: + needs: build-natives + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Download macOS native libraries + uses: actions/download-artifact@v8 + with: + name: native-macos + path: mediaplayer/src/jvmMain/resources/composemediaplayer/native/ + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Validate configurable macOS video dimensions timeout + run: | + ./gradlew :mediaplayer:compileKotlinJvm :mediaplayer:jvmTest \ + --tests '*MacVideoDimensionsTimeout*Test' --no-daemon --no-configuration-cache + env: + COMPOSE_MEDIA_PLAYER_RUN_NATIVE_TIMEOUT_TEST: '1' + + - name: Upload test reports + uses: actions/upload-artifact@v7 + if: always() + with: + name: test-reports-macos-video-dimensions-timeout + path: '**/build/reports/tests/' + android: runs-on: ubuntu-latest steps: diff --git a/README_VIDEO.MD b/README_VIDEO.MD index 4dbe8560..d1396c1d 100644 --- a/README_VIDEO.MD +++ b/README_VIDEO.MD @@ -128,6 +128,27 @@ Before using Compose Media Player, you need to create a state for the video play val playerState = rememberVideoPlayerState() ``` +### macOS Video Dimensions Timeout (Desktop JVM) + +The macOS backend polls AVFoundation for video dimensions for up to **5 seconds** by default. +For remote sources whose metadata takes longer to arrive, desktop JVM code can opt into a +longer window: + +```kotlin +val playerState = rememberVideoPlayerState(macVideoDimensionsTimeoutMillis = 30_000L) +``` + +The desktop overload manages disposal automatically. Changing the timeout recreates the player +state. The value must be positive and is ignored by the Windows and Linux backends. Existing +`rememberVideoPlayerState()` calls retain the five-second window. + +For manually managed states, use `DefaultVideoPlayerState(macVideoDimensionsTimeoutMillis = 30_000L)` +or, on macOS, `MacVideoPlayerState(videoDimensionsTimeoutMillis = 30_000L)`. Call `dispose()` when a +manually managed state is no longer needed. Existing no-argument constructors remain available. + +This setting controls dimension polling only. Native initialization and the separate duration +polling window retain their existing timeouts; it does not limit the total media-open time. + ### Displaying the Video Surface After initializing the player state, you can display the surface of the video using `VideoPlayerSurface`: diff --git a/mediaplayer/build.gradle.kts b/mediaplayer/build.gradle.kts index cd719b08..97542bc7 100644 --- a/mediaplayer/build.gradle.kts +++ b/mediaplayer/build.gradle.kts @@ -109,6 +109,8 @@ kotlin { implementation(kotlin("test")) implementation(kotlin("test-junit")) implementation(libs.kotlinx.coroutines.test) + // Real AVFoundation tests render frames through Skia. + runtimeOnly(compose.desktop.currentOs) } if (Os.isFamily(Os.FAMILY_MAC)) { @@ -267,3 +269,13 @@ mavenPublishing { } } + +// Enabling native timeout regressions must invalidate an earlier skipped test run. +tasks.withType().configureEach { + if (name == "jvmTest") { + inputs.property( + "nativeVideoDimensionsTimeoutTest", + providers.environmentVariable("COMPOSE_MEDIA_PLAYER_RUN_NATIVE_TIMEOUT_TEST").orElse("0"), + ) + } +} diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.jvm.kt b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.jvm.kt index e0323d2e..7956a442 100644 --- a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.jvm.kt +++ b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.jvm.kt @@ -1,9 +1,13 @@ package io.github.kdroidfilter.composemediaplayer +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import io.github.kdroidfilter.composemediaplayer.linux.LinuxVideoPlayerState +import io.github.kdroidfilter.composemediaplayer.mac.DEFAULT_MAC_VIDEO_DIMENSIONS_TIMEOUT_MILLIS import io.github.kdroidfilter.composemediaplayer.mac.MacVideoPlayerState import io.github.kdroidfilter.composemediaplayer.util.CurrentPlatform import io.github.kdroidfilter.composemediaplayer.windows.WindowsVideoPlayerState @@ -14,6 +18,25 @@ actual fun createVideoPlayerState( cacheConfig: CacheConfig, ): VideoPlayerState = DefaultVideoPlayerState() +/** + * Desktop overload with a configurable macOS video-dimensions polling window. + * + * [macVideoDimensionsTimeoutMillis] must be positive. Windows and Linux keep their existing + * behavior. Changing the value creates a new state and disposes the previous state. + * Use the common no-argument overload to retain the default five-second macOS window. + */ +@Composable +fun rememberVideoPlayerState(macVideoDimensionsTimeoutMillis: Long): VideoPlayerState { + val playerState = + remember(macVideoDimensionsTimeoutMillis) { + DefaultVideoPlayerState(macVideoDimensionsTimeoutMillis) + } + DisposableEffect(playerState) { + onDispose { playerState.dispose() } + } + return playerState +} + /** * Represents the state and behavior of a video player. This class provides properties * and methods to control video playback, manage the playback state, and interact with @@ -35,13 +58,22 @@ actual fun createVideoPlayerState( * - `stop()`: Stops playback and resets the player state. * - `seekTo(value: Float)`: Seeks to a specific playback position based on the provided normalized value. * - `dispose()`: Releases resources used by the video player and disposes of the state. + * + * @param macVideoDimensionsTimeoutMillis Maximum macOS video-dimensions polling time in + * milliseconds. Must be positive; defaults to 5,000 ms. Ignored by Windows and Linux backends. */ @Stable -open class DefaultVideoPlayerState : VideoPlayerState { +open class DefaultVideoPlayerState( + macVideoDimensionsTimeoutMillis: Long = DEFAULT_MAC_VIDEO_DIMENSIONS_TIMEOUT_MILLIS, +) : VideoPlayerState { + init { + require(macVideoDimensionsTimeoutMillis > 0) { "Video dimensions timeout must be positive." } + } + val delegate: VideoPlayerState = when (CurrentPlatform.os) { CurrentPlatform.OS.WINDOWS -> WindowsVideoPlayerState() - CurrentPlatform.OS.MAC -> MacVideoPlayerState() + CurrentPlatform.OS.MAC -> MacVideoPlayerState(macVideoDimensionsTimeoutMillis) CurrentPlatform.OS.LINUX -> LinuxVideoPlayerState() } diff --git a/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoDimensionsTimeout.kt b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoDimensionsTimeout.kt new file mode 100644 index 00000000..2a07ceb0 --- /dev/null +++ b/mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoDimensionsTimeout.kt @@ -0,0 +1,24 @@ +package io.github.kdroidfilter.composemediaplayer.mac + +import kotlinx.coroutines.delay +import kotlinx.coroutines.withTimeoutOrNull + +internal const val DEFAULT_MAC_VIDEO_DIMENSIONS_TIMEOUT_MILLIS = 5_000L +private const val DIMENSION_POLL_INTERVAL_MILLIS = 250L + +/** Wait for asynchronous AVFoundation dimensions while the current open remains active. */ +internal suspend fun awaitMacVideoDimensions( + timeoutMillis: Long, + isCurrentOpen: () -> Boolean, + readDimensions: () -> Pair, +): Boolean { + require(timeoutMillis > 0) { "Video dimensions timeout must be positive." } + return withTimeoutOrNull(timeoutMillis) { + while (isCurrentOpen()) { + val (width, height) = readDimensions() + if (width > 0 && height > 0) return@withTimeoutOrNull true + delay(DIMENSION_POLL_INTERVAL_MILLIS) + } + false + } ?: false +} 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 035445b6..35cab6a9 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 @@ -74,8 +74,18 @@ private const val POSITION_POLL_INTERVAL_MS = 200L * separate polling loop. The one place macOS deliberately diverges is the aspect ratio: it uses * AVFoundation's display aspect ratio (pixel-aspect-ratio / clean-aperture corrected) so * anamorphic content renders correctly — see [displayAspectRatio]. + * + * @param videoDimensionsTimeoutMillis Maximum time to poll for video dimensions after opening + * media. Must be positive; defaults to the existing five-second polling window. The separate + * duration polling and native-player initialization timeouts are unchanged. */ -class MacVideoPlayerState : VideoPlayerState { +class MacVideoPlayerState( + private val videoDimensionsTimeoutMillis: Long = DEFAULT_MAC_VIDEO_DIMENSIONS_TIMEOUT_MILLIS, +) : VideoPlayerState { + init { + require(videoDimensionsTimeoutMillis > 0) { "Video dimensions timeout must be positive." } + } + /** Native AVFoundation player. AtomicLong allows lock-free reads from the frame hot path. */ private val playerPtrAtomic = AtomicLong(0L) private val playerPtr: Long get() = playerPtrAtomic.get() @@ -408,7 +418,8 @@ class MacVideoPlayerState : VideoPlayerState { // Phase 2 (UNLOCKED): AVFoundation loads asynchronously, so these polls can take // several seconds. Running them outside mediaOperationMutex keeps volume/other // operations responsive during a slow open. - pollDimensionsUntilReady(ptr) + pollDimensionsUntilReady(ptr, myToken) + if (isDisposing.get() || myToken != openToken.get()) return@launch val resolvedDuration = pollDurationUntilReady(ptr) if (isDisposing.get() || myToken != openToken.get()) return@launch @@ -475,30 +486,28 @@ class MacVideoPlayerState : VideoPlayerState { _hasMedia = false } finally { // Only clear the flag if a newer open hasn't taken over. - if (myToken == openToken.get()) isOpening.set(false) - if (!_hasMedia) isLoading = false + if (myToken == openToken.get()) { + isOpening.set(false) + if (!_hasMedia) isLoading = false + } } } } - /** - * Loops several times (every 250 ms) until width/height are no longer zero, or until - * [maxAttempts] is reached. - */ + /** Polls video dimensions without holding [mediaOperationMutex] during asynchronous loading. */ private suspend fun pollDimensionsUntilReady( ptr: Long, - maxAttempts: Int = 20, + token: Long, ) { - for (attempt in 1..maxAttempts) { - val width = MacNativeBridge.nGetFrameWidth(ptr) - val height = MacNativeBridge.nGetFrameHeight(ptr) - if (width > 0 && height > 0) { - macLogger.d { "Dimensions validated (w=$width, h=$height) after $attempt attempts" } - return - } - delay(250) + val ready = + awaitMacVideoDimensions( + timeoutMillis = videoDimensionsTimeoutMillis, + isCurrentOpen = { !isDisposing.get() && token == openToken.get() && ptr == playerPtr }, + readDimensions = { MacNativeBridge.nGetFrameWidth(ptr) to MacNativeBridge.nGetFrameHeight(ptr) }, + ) + if (!ready && !isDisposing.get() && token == openToken.get()) { + macLogger.e { "Unable to retrieve valid dimensions within $videoDimensionsTimeoutMillis ms" } } - macLogger.e { "Unable to retrieve valid dimensions after $maxAttempts attempts" } } /** diff --git a/mediaplayer/src/jvmTest/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoDimensionsTimeoutNativeTest.kt b/mediaplayer/src/jvmTest/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoDimensionsTimeoutNativeTest.kt new file mode 100644 index 00000000..7e249daa --- /dev/null +++ b/mediaplayer/src/jvmTest/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoDimensionsTimeoutNativeTest.kt @@ -0,0 +1,97 @@ +package io.github.kdroidfilter.composemediaplayer.mac + +import com.sun.net.httpserver.HttpServer +import io.github.kdroidfilter.composemediaplayer.DefaultVideoPlayerState +import io.github.kdroidfilter.composemediaplayer.VideoPlayerError +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.junit.Assume.assumeTrue +import java.net.InetSocketAddress +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicLong +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** AVFoundation integration against a synthetic MP4 served by a delayed local HTTP server. */ +class MacVideoDimensionsTimeoutNativeTest { + @Test + fun defaultWindowStillRejectsDelayedDimensions() = verifySlowStartup("video.mp4", null) + + @Test + fun configuredWindowAllowsDelayedMp4Playback() = verifySlowStartup("video.mp4", 30_000L) + + @Test + fun configuredWindowAllowsDelayedOpaqueUriPlayback() = verifySlowStartup("media", 30_000L) + + private fun verifySlowStartup( + path: String, + timeoutMillis: Long?, + ) = runBlocking { + assumeTrue(System.getenv("COMPOSE_MEDIA_PLAYER_RUN_NATIVE_TIMEOUT_TEST") == "1") + assumeTrue(System.getProperty("os.name").startsWith("Mac", ignoreCase = true)) + val media = checkNotNull(javaClass.getResourceAsStream("/startup/metadata-startup.mp4")).use { it.readBytes() } + val firstRequestAt = AtomicLong(0L) + val server = HttpServer.create(InetSocketAddress("127.0.0.1", 0), 0) + val executor = Executors.newCachedThreadPool() + server.executor = executor + server.createContext("/$path") { exchange -> + try { + firstRequestAt.compareAndSet(0L, System.nanoTime()) + // Every request waits for the same deadline, including AVFoundation range requests. + while (System.nanoTime() - firstRequestAt.get() < METADATA_DELAY_NANOS) Thread.sleep(50) + val range = exchange.requestHeaders.getFirst("Range") + val bounds = range?.removePrefix("bytes=")?.split("-") + val start = bounds?.getOrNull(0)?.toIntOrNull() ?: 0 + val end = (bounds?.getOrNull(1)?.toIntOrNull() ?: media.lastIndex).coerceAtMost(media.lastIndex) + exchange.responseHeaders.set("Content-Type", "video/mp4") + exchange.responseHeaders.set("Accept-Ranges", "bytes") + if (range != null) exchange.responseHeaders.set("Content-Range", "bytes $start-$end/${media.size}") + exchange.sendResponseHeaders(if (range == null) 200 else 206, (end - start + 1).toLong()) + exchange.responseBody.use { it.write(media, start, end - start + 1) } + } catch (_: InterruptedException) { + Thread.currentThread().interrupt() + } finally { + exchange.close() + } + } + server.start() + val player = if (timeoutMillis == null) DefaultVideoPlayerState() else DefaultVideoPlayerState(timeoutMillis) + try { + assertIs(player.delegate) + player.openUri("http://127.0.0.1:${server.address.port}/$path") + if (timeoutMillis == null) { + withTimeout(12_000L) { + while (player.error == null) delay(50) + } + assertEquals( + "Failed to retrieve video size", + assertIs(player.error).message, + ) + assertFalse(player.hasMedia) + assertTrue(System.nanoTime() - firstRequestAt.get() < METADATA_DELAY_NANOS) + } else { + withTimeout(40_000L) { + while (!player.hasMedia || player.currentTime < 0.5) { + assertNull(player.error) + delay(50) + } + } + assertTrue(player.hasMedia) + assertTrue(player.isPlaying) + assertTrue(player.duration > 0) + assertTrue(System.nanoTime() - firstRequestAt.get() >= METADATA_DELAY_NANOS) + } + } finally { + player.dispose() + server.stop(0) + executor.shutdownNow() + } + } +} + +private const val METADATA_DELAY_NANOS = 16_000_000_000L diff --git a/mediaplayer/src/jvmTest/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoDimensionsTimeoutTest.kt b/mediaplayer/src/jvmTest/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoDimensionsTimeoutTest.kt new file mode 100644 index 00000000..f0e3ace7 --- /dev/null +++ b/mediaplayer/src/jvmTest/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoDimensionsTimeoutTest.kt @@ -0,0 +1,123 @@ +package io.github.kdroidfilter.composemediaplayer.mac + +import io.github.kdroidfilter.composemediaplayer.DefaultVideoPlayerState +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.currentTime +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class MacVideoDimensionsTimeoutTest { + @Test + fun noArgumentJvmConstructorsRemainAvailable() { + assertNotNull(MacVideoPlayerState::class.java.getConstructor()) + assertNotNull(DefaultVideoPlayerState::class.java.getConstructor()) + } + + @Test + fun defaultRetainsFiveSecondPollingWindow() = + runTest { + var reads = 0 + assertFalse( + awaitMacVideoDimensions(DEFAULT_MAC_VIDEO_DIMENSIONS_TIMEOUT_MILLIS, { true }) { + reads++ + 0 to 0 + }, + ) + assertEquals(5_000L, currentTime) + assertEquals(20, reads) + } + + @Test + fun readyDimensionsReturnImmediately() = + runTest { + assertTrue(awaitMacVideoDimensions(30_000L, { true }) { 1920 to 1080 }) + assertEquals(0L, currentTime) + } + + @Test + fun longerWindowAllowsDelayedDimensions() = + runTest { + assertTrue( + awaitMacVideoDimensions(30_000L, { true }) { + if (currentTime >= 16_000L) 1920 to 1080 else 0 to 0 + }, + ) + assertEquals(16_000L, currentTime) + } + + @Test + fun customWindowStillHasABoundedDeadline() = + runTest { + assertFalse(awaitMacVideoDimensions(30_000L, { true }) { 0 to 0 }) + assertEquals(30_000L, currentTime) + } + + @Test + fun shorterWindowUsesConfiguredDeadline() = + runTest { + assertFalse(awaitMacVideoDimensions(600L, { true }) { 0 to 0 }) + assertEquals(600L, currentTime) + } + + @Test + fun bothDimensionsMustBePositive() = + runTest { + assertFalse(awaitMacVideoDimensions(1_000L, { true }) { 1920 to 0 }) + } + + @Test + fun supersededOpenStopsPolling() = + runTest { + var current = true + var reads = 0 + val wait = + async { + awaitMacVideoDimensions(30_000L, { current }) { + reads++ + 0 to 0 + } + } + advanceTimeBy(1_000L) + current = false + assertFalse(wait.await()) + assertEquals(4, reads) + assertEquals(1_000L, currentTime) + } + + @Test + fun cancellationStopsPolling() = + runTest { + var reads = 0 + val wait = + async { + awaitMacVideoDimensions(30_000L, { true }) { + reads++ + 0 to 0 + } + } + advanceTimeBy(1_000L) + wait.cancel() + wait.join() + advanceTimeBy(30_000L) + assertTrue(wait.isCancelled) + assertEquals(4, reads) + } + + @Test + fun nonPositiveTimeoutIsRejectedBeforeNativeInitialization() = + runTest { + for (timeout in listOf(0L, -1L)) { + assertFailsWith { awaitMacVideoDimensions(timeout, { true }) { 0 to 0 } } + assertFailsWith { MacVideoPlayerState(timeout) } + assertFailsWith { DefaultVideoPlayerState(timeout) } + } + } +} diff --git a/mediaplayer/src/jvmTest/resources/startup/metadata-startup.mp4 b/mediaplayer/src/jvmTest/resources/startup/metadata-startup.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..4e387c62adae5d3e601fbda6555743d978e41e8a GIT binary patch literal 20006 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 JVBV|n%|VrLLaqP+ literal 0 HcmV?d00001