Skip to content

Commit 95b5a1e

Browse files
kdroidFilterclaude
andcommitted
fix(mac): Fix NaN crash in slider and synchronize native player disposal
Guard slider position calculation against division by zero to prevent NaN crashes in Material3 Slider semantics. Make native player disposal synchronous (matching Linux behavior) to ensure cleanup completes before ioScope cancellation, preventing audio leaks. Use runBlocking(frameDispatcher) to safely close bitmaps on the same dispatcher that rendering uses, avoiding use-after-free race conditions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 22ec8ae commit 95b5a1e

1 file changed

Lines changed: 24 additions & 29 deletions

File tree

mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/MacVideoPlayerState.kt

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ class MacVideoPlayerState : VideoPlayerState {
666666
}
667667
} else {
668668
// Update slider position, batched with other UI updates to reduce main thread calls
669-
val newSliderPos = (current / duration * 1000).toFloat().coerceIn(0f, 1000f)
669+
val newSliderPos = if (duration > 0) (current / duration * 1000).toFloat().coerceIn(0f, 1000f) else 0f
670670
withContext(Dispatchers.Main) {
671671
sliderPos = newSliderPos
672672
}
@@ -861,38 +861,33 @@ class MacVideoPlayerState : VideoPlayerState {
861861
uiUpdateJob?.cancel()
862862
playerScope.cancel()
863863

864-
ioScope.launch {
865-
// Get player pointer and clear cached bitmaps while frame updates are paused.
866-
val ptrToDispose =
867-
withContext(frameDispatcher) {
868-
val ptrToDispose = playerPtrAtomic.getAndSet(0L)
869-
870-
skiaBitmapA?.close()
871-
skiaBitmapB?.close()
872-
skiaBitmapA = null
873-
skiaBitmapB = null
874-
skiaBitmapWidth = 0
875-
skiaBitmapHeight = 0
876-
nextSkiaBitmapA = true
877-
878-
ptrToDispose
879-
}
864+
// Dispose synchronously to guarantee cleanup before ioScope is cancelled —
865+
// otherwise AVPlayer keeps running (audio leak).
866+
// Use frameDispatcher to safely close bitmaps (rendering accesses them there).
867+
val ptrToDispose = runBlocking(frameDispatcher) {
868+
val ptr = playerPtrAtomic.getAndSet(0L)
869+
870+
skiaBitmapA?.close()
871+
skiaBitmapB?.close()
872+
skiaBitmapA = null
873+
skiaBitmapB = null
874+
skiaBitmapWidth = 0
875+
skiaBitmapHeight = 0
876+
nextSkiaBitmapA = true
877+
878+
ptr
879+
}
880880

881-
// Dispose native resources outside the mutex lock
882-
if (ptrToDispose != 0L) {
883-
macLogger.d { "dispose() - Disposing native player" }
884-
try {
885-
MacNativeBridge.nDisposePlayer(ptrToDispose)
886-
} catch (e: Exception) {
887-
if (e is CancellationException) throw e
888-
macLogger.e { "Error disposing player: ${e.message}" }
889-
}
881+
if (ptrToDispose != 0L) {
882+
macLogger.d { "dispose() - Disposing native player" }
883+
try {
884+
MacNativeBridge.nDisposePlayer(ptrToDispose)
885+
} catch (e: Exception) {
886+
if (e is CancellationException) throw e
887+
macLogger.e { "Error disposing player: ${e.message}" }
890888
}
891-
892-
resetState()
893889
}
894890

895-
// Cancel ioScope last to ensure cleanup completes
896891
ioScope.cancel()
897892
}
898893

0 commit comments

Comments
 (0)