Skip to content

Commit 0483240

Browse files
committed
refactor: consolidate frame polling and cleanup across all platforms
1 parent c30746d commit 0483240

3 files changed

Lines changed: 80 additions & 91 deletions

File tree

mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/linux/LinuxVideoPlayerState.kt

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -718,26 +718,32 @@ class LinuxVideoPlayerState : VideoPlayerState {
718718
uiUpdateJob?.cancel()
719719
playerScope.cancel()
720720

721-
// Dispose the native player synchronously to guarantee cleanup before
722-
// ioScope is cancelled — otherwise GStreamer keeps running (audio leak).
721+
// Clear the pointer atomically so no background task can use it
723722
val ptrToDispose = playerPtrAtomic.getAndSet(0L)
724723

725-
skiaBitmapA?.close()
726-
skiaBitmapB?.close()
727-
skiaBitmapA = null
728-
skiaBitmapB = null
729-
skiaBitmapWidth = 0
730-
skiaBitmapHeight = 0
731-
nextSkiaBitmapA = true
732-
733-
if (ptrToDispose != 0L) {
724+
// Native cleanup on a background thread to avoid blocking the UI.
725+
Thread {
734726
try {
735-
LinuxNativeBridge.nDisposePlayer(ptrToDispose)
727+
skiaBitmapA?.close()
728+
skiaBitmapB?.close()
729+
skiaBitmapA = null
730+
skiaBitmapB = null
731+
skiaBitmapWidth = 0
732+
skiaBitmapHeight = 0
733+
nextSkiaBitmapA = true
736734
} catch (e: Exception) {
737-
if (e is CancellationException) throw e
738-
linuxLogger.e { "Error disposing player: ${e.message}" }
735+
linuxLogger.e { "Error releasing bitmaps: ${e.message}" }
739736
}
740-
}
737+
738+
if (ptrToDispose != 0L) {
739+
try {
740+
LinuxNativeBridge.nDisposePlayer(ptrToDispose)
741+
} catch (e: Exception) {
742+
if (e is CancellationException) throw e
743+
linuxLogger.e { "Error disposing player: ${e.message}" }
744+
}
745+
}
746+
}.start()
741747

742748
ioScope.cancel()
743749
}

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

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -861,32 +861,37 @@ class MacVideoPlayerState : VideoPlayerState {
861861
uiUpdateJob?.cancel()
862862
playerScope.cancel()
863863

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-
}
864+
// Clear the pointer atomically so no background task can use it
865+
val ptrToDispose = playerPtrAtomic.getAndSet(0L)
880866

881-
if (ptrToDispose != 0L) {
882-
macLogger.d { "dispose() - Disposing native player" }
867+
// Release bitmaps on the frame dispatcher (rendering accesses them there)
868+
// then dispose the native player — all on a background thread to avoid
869+
// blocking the main/UI thread.
870+
Thread {
883871
try {
884-
MacNativeBridge.nDisposePlayer(ptrToDispose)
872+
// Close bitmaps (not thread-safe with rendering, but frame updates
873+
// are already cancelled above and playerPtr is zeroed)
874+
skiaBitmapA?.close()
875+
skiaBitmapB?.close()
876+
skiaBitmapA = null
877+
skiaBitmapB = null
878+
skiaBitmapWidth = 0
879+
skiaBitmapHeight = 0
880+
nextSkiaBitmapA = true
885881
} catch (e: Exception) {
886-
if (e is CancellationException) throw e
887-
macLogger.e { "Error disposing player: ${e.message}" }
882+
macLogger.e { "Error releasing bitmaps: ${e.message}" }
888883
}
889-
}
884+
885+
if (ptrToDispose != 0L) {
886+
macLogger.d { "dispose() - Disposing native player" }
887+
try {
888+
MacNativeBridge.nDisposePlayer(ptrToDispose)
889+
} catch (e: Exception) {
890+
if (e is CancellationException) throw e
891+
macLogger.e { "Error disposing player: ${e.message}" }
892+
}
893+
}
894+
}.start()
890895

891896
ioScope.cancel()
892897
}

mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/windows/WindowsVideoPlayerState.kt

Lines changed: 32 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -297,65 +297,43 @@ class WindowsVideoPlayerState : VideoPlayerState {
297297
return // Already disposing
298298
}
299299

300-
// Cancel the scope immediately to stop all coroutines
301-
scope.cancel()
302-
303-
// Use runBlocking to ensure resources are cleaned up synchronously
304-
runBlocking {
305-
try {
306-
// Cancel all jobs with immediate effect
307-
videoJob?.cancel()
308-
resizeJob?.cancel()
309-
310-
// Wait a bit for coroutines to cancel
311-
delay(50)
312-
313-
mediaOperationMutex.withLock {
314-
// Stop playing if active
315-
_isPlaying = false
316-
val instance = videoPlayerInstance
317-
if (instance != 0L) {
318-
try {
319-
// Stop playback before releasing resources
320-
val hr = player.SetPlaybackState(instance, false, true)
321-
if (hr < 0) {
322-
windowsLogger.e { "Error stopping playback (hr=0x${hr.toString(16)})" }
323-
}
324-
} catch (e: Exception) {
325-
windowsLogger.e { "Exception stopping playback: ${e.message}" }
326-
}
327-
328-
// Close the media
329-
try {
330-
player.CloseMedia(instance)
331-
} catch (e: Exception) {
332-
windowsLogger.e { "Exception closing media: ${e.message}" }
333-
}
334-
335-
// Remove volume setting for this instance
336-
instanceVolumes.remove(instance)
300+
// Stop coroutines first — non-blocking
301+
videoJob?.cancel()
302+
resizeJob?.cancel()
303+
_isPlaying = false
304+
_hasMedia = false
337305

338-
// Destroy the player instance
339-
try {
340-
WindowsNativeBridge.destroyInstance(instance)
341-
} catch (e: Exception) {
342-
windowsLogger.e { "Exception destroying instance: ${e.message}" }
343-
}
306+
// Release Kotlin-side resources immediately (bitmaps, channel)
307+
releaseAllResources()
344308

345-
videoPlayerInstance = 0L
346-
}
309+
// Native cleanup on a background thread so dispose() never blocks the UI.
310+
// scope is about to be cancelled, so use a detached thread.
311+
val instance = videoPlayerInstance
312+
videoPlayerInstance = 0L
313+
lastUri = null
347314

348-
// Clear all resources
349-
clearAllResourcesSync()
315+
if (instance != 0L) {
316+
Thread {
317+
try {
318+
player.SetPlaybackState(instance, false, true)
319+
} catch (e: Exception) {
320+
windowsLogger.e { "Exception stopping playback: ${e.message}" }
350321
}
351-
} catch (e: Exception) {
352-
windowsLogger.e { "Error during dispose: ${e.message}" }
353-
} finally {
354-
// Mark player as uninitialized
355-
_hasMedia = false
356-
lastUri = null
357-
}
322+
try {
323+
player.CloseMedia(instance)
324+
} catch (e: Exception) {
325+
windowsLogger.e { "Exception closing media: ${e.message}" }
326+
}
327+
instanceVolumes.remove(instance)
328+
try {
329+
WindowsNativeBridge.destroyInstance(instance)
330+
} catch (e: Exception) {
331+
windowsLogger.e { "Exception destroying instance: ${e.message}" }
332+
}
333+
}.start()
358334
}
335+
336+
scope.cancel()
359337
}
360338

361339
private fun clearAllResourcesSync() {

0 commit comments

Comments
 (0)