diff --git a/gradle.properties b/gradle.properties index 9cabf573..a63f8781 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,9 +12,12 @@ kotlin.daemon.jvmargs=-Xmx4G kotlin.mpp.enableCInteropCommonization=true kotlin.native.ignoreDisabledTargets=true kotlin.native.enableKlibsCrossCompilation=false +## Incremental compilation breaks Wasm tests, see https://youtrack.jetbrains.com/projects/KT/issues/KT-84610/Wasm-Failed-to-compile-klibs-in-IC-mode +kotlin.incremental.js=false +kotlin.incremental.js.klib=false #Android android.useAndroidX=true android.nonTransitiveRClass=true -org.jetbrains.compose.experimental.macos.enabled=true \ No newline at end of file +org.jetbrains.compose.experimental.macos.enabled=true diff --git a/mediaplayer/src/jsMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.js.kt b/mediaplayer/src/jsMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.js.kt index 785a0ad3..949367a2 100644 --- a/mediaplayer/src/jsMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.js.kt +++ b/mediaplayer/src/jsMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.js.kt @@ -26,7 +26,6 @@ actual fun VideoPlayerSurface( // State for CORS mode changes var lastPosition by remember { mutableStateOf(0.0) } var wasPlaying by remember { mutableStateOf(false) } - var lastPlaybackSpeed by remember { mutableStateOf(1.0f) } // Shared effects VideoPlayerEffects( @@ -36,10 +35,8 @@ actual fun VideoPlayerSurface( useCors = useCors, onLastPositionChange = { lastPosition = it }, onWasPlayingChange = { wasPlaying = it }, - onLastPlaybackSpeedChange = { lastPlaybackSpeed = it }, lastPosition = lastPosition, wasPlaying = wasPlaying, - lastPlaybackSpeed = lastPlaybackSpeed, ) VideoVolumeAndSpeedEffects( diff --git a/mediaplayer/src/wasmJsMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.wasm.kt b/mediaplayer/src/wasmJsMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.wasm.kt index 785a0ad3..949367a2 100644 --- a/mediaplayer/src/wasmJsMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.wasm.kt +++ b/mediaplayer/src/wasmJsMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurface.wasm.kt @@ -26,7 +26,6 @@ actual fun VideoPlayerSurface( // State for CORS mode changes var lastPosition by remember { mutableStateOf(0.0) } var wasPlaying by remember { mutableStateOf(false) } - var lastPlaybackSpeed by remember { mutableStateOf(1.0f) } // Shared effects VideoPlayerEffects( @@ -36,10 +35,8 @@ actual fun VideoPlayerSurface( useCors = useCors, onLastPositionChange = { lastPosition = it }, onWasPlayingChange = { wasPlaying = it }, - onLastPlaybackSpeedChange = { lastPlaybackSpeed = it }, lastPosition = lastPosition, wasPlaying = wasPlaying, - lastPlaybackSpeed = lastPlaybackSpeed, ) VideoVolumeAndSpeedEffects( diff --git a/mediaplayer/src/wasmJsTest/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerStateTest.kt b/mediaplayer/src/wasmJsTest/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerStateTest.kt index f3efa03e..a5b8208f 100644 --- a/mediaplayer/src/wasmJsTest/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerStateTest.kt +++ b/mediaplayer/src/wasmJsTest/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerStateTest.kt @@ -23,6 +23,7 @@ class VideoPlayerStateTest { assertFalse(playerState.isPlaying) assertEquals(0f, playerState.sliderPos) assertEquals(1f, playerState.volume) + assertEquals(1f, playerState.playbackSpeed) assertFalse(playerState.loop) assertEquals("00:00", playerState.positionText) assertEquals("00:00", playerState.durationText) @@ -57,6 +58,28 @@ class VideoPlayerStateTest { playerState.dispose() } + @Test + fun `testPlaybackSpeedControl`() { + val playerState = createVideoPlayerState() + + // Test initial speed + assertEquals(1f, playerState.playbackSpeed) + + // Test setting speed + playerState.playbackSpeed = 0.5f + assertEquals(0.5f, playerState.playbackSpeed) + + // Test speed bounds + playerState.playbackSpeed = -1f + assertEquals(0.25f, playerState.playbackSpeed, "Playback speed should be clamped to 0.25") + + playerState.playbackSpeed = 10f + assertEquals(2f, playerState.playbackSpeed, "Playback speed should be clamped to 2") + + // Clean up + playerState.dispose() + } + /** * Test loop setting */ @@ -185,4 +208,14 @@ class VideoPlayerStateTest { // Clean up playerState.dispose() } + + @Test + fun testLoadingVideoDoNotResetPlaybackSpeed() { + val playerState = createVideoPlayerState() + playerState.playbackSpeed = 2f + + playerState.openUri("file:///path/to/file") + + assertEquals(2f, playerState.playbackSpeed) + } } diff --git a/mediaplayer/src/webMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.web.kt b/mediaplayer/src/webMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.web.kt index 6a88271d..8c3e94b5 100644 --- a/mediaplayer/src/webMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.web.kt +++ b/mediaplayer/src/webMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerState.web.kt @@ -246,7 +246,6 @@ open class DefaultVideoPlayerState : VideoPlayerState { _isLoading = true // Set initial loading state _error = null _isPlaying = false - _playbackSpeed = 1.0f // Don't set isLoading to false here - let the video events handle it playerScope.launch { diff --git a/mediaplayer/src/webMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurfaceImpl.kt b/mediaplayer/src/webMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurfaceImpl.kt index ee0b44f1..48a7be9b 100644 --- a/mediaplayer/src/webMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurfaceImpl.kt +++ b/mediaplayer/src/webMain/kotlin/io/github/kdroidfilter/composemediaplayer/VideoPlayerSurfaceImpl.kt @@ -518,10 +518,8 @@ internal fun VideoPlayerEffects( useCors: Boolean, onLastPositionChange: (Double) -> Unit, onWasPlayingChange: (Boolean) -> Unit, - onLastPlaybackSpeedChange: (Float) -> Unit, lastPosition: Double, wasPlaying: Boolean, - lastPlaybackSpeed: Float, ) { // Handle fullscreen LaunchedEffect(playerState.isFullscreen) { @@ -593,7 +591,6 @@ internal fun VideoPlayerEffects( videoElement?.let { onLastPositionChange(it.currentTime) onWasPlayingChange(playerState.isPlaying) - onLastPlaybackSpeedChange(playerState.playbackSpeed) } } @@ -605,11 +602,6 @@ internal fun VideoPlayerEffects( onLastPositionChange(0.0) } - if (lastPlaybackSpeed != 1.0f) { - video.safeSetPlaybackRate(lastPlaybackSpeed) - onLastPlaybackSpeedChange(1.0f) - } - if (wasPlaying) { video.safePlay() onWasPlayingChange(false) @@ -666,61 +658,46 @@ internal fun VideoVolumeAndSpeedEffects( playerState: VideoPlayerState, videoElement: HTMLVideoElement?, ) { - var pendingVolumeChange by remember { mutableStateOf(null) } - var pendingPlaybackSpeedChange by remember { mutableStateOf(null) } - - if (playerState is DefaultVideoPlayerState) { - DisposableEffect(videoElement) { - val video = videoElement ?: return@DisposableEffect onDispose {} + if (playerState !is DefaultVideoPlayerState) { + webVideoLogger.e { + """ + Expected ${DefaultVideoPlayerState::class} but found ${playerState::class}. + Volume and speed settings won't work. + """.trimIndent() + } + return + } - playerState.applyVolumeCallback = { value -> - if (playerState._isLoading) { - pendingVolumeChange = value.toDouble() - } else { - video.volume = value.toDouble() - pendingVolumeChange = null - } - } + DisposableEffect(videoElement) { + val video = videoElement ?: return@DisposableEffect onDispose {} - if (!playerState._isLoading) { - video.volume = playerState.volume.toDouble() - } else { - pendingVolumeChange = playerState.volume.toDouble() - } + // Init video element state when video is loaded + // Volume could be changed any time but playbackRate require video to be loaded + val videoLoadedListener: (Event) -> Unit = { + video.volume = playerState.volume.toDouble() + video.safeSetPlaybackRate(playerState.playbackSpeed) + } - playerState.applyPlaybackSpeedCallback = { value -> - if (playerState._isLoading) { - pendingPlaybackSpeedChange = value - } else { - video.safeSetPlaybackRate(value) - pendingPlaybackSpeedChange = null - } - } + video.addEventListener("canplay", videoLoadedListener) - if (!playerState._isLoading) { - video.safeSetPlaybackRate(playerState.playbackSpeed) - } else { - pendingPlaybackSpeedChange = playerState.playbackSpeed + // They should change only `playerState` while the video is loading + // The video element will be updated in `videoLoadedListener` + playerState.applyVolumeCallback = { value -> + if (!playerState.isLoading) { + video.volume = value.toDouble() } + } - val seekedListener: (Event) -> Unit = { - pendingVolumeChange?.let { volume -> - video.volume = volume - pendingVolumeChange = null - } - pendingPlaybackSpeedChange?.let { speed -> - video.safeSetPlaybackRate(speed) - pendingPlaybackSpeedChange = null - } + playerState.applyPlaybackSpeedCallback = { value -> + if (!playerState.isLoading) { + video.safeSetPlaybackRate(value) } + } - video.addEventListener("seeked", seekedListener) - - onDispose { - video.removeEventListener("seeked", seekedListener) - playerState.applyVolumeCallback = null - playerState.applyPlaybackSpeedCallback = null - } + onDispose { + video.removeEventListener("canplay", videoLoadedListener) + playerState.applyVolumeCallback = null + playerState.applyPlaybackSpeedCallback = null } } }