diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt index 0604cd03..5c71f4a1 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt @@ -130,11 +130,24 @@ class PlayerController @Inject constructor( // ── Transport (no-op until the controller is connected) ────────────── - fun play() { mediaController?.play() } - fun pause() { mediaController?.pause() } - fun seekTo(positionMs: Long) { mediaController?.seekTo(positionMs) } - fun skipToNext() { mediaController?.seekToNextMediaItem() } - fun skipToPrevious() { mediaController?.seekToPreviousMediaItem() } + // Each transport call must run on the MediaController's + // applicationLooper; calling from a background coroutine throws + // IllegalStateException (see PlayerController.setQueue's note). + // UI tap handlers are already on Main so the in-place branch hits; + // the background path only fires for cross-thread callers like + // OutputPickerController.selectUpnp (which calls pause() after + // handing playback off to a UPnP renderer). + fun play() { mediaController?.let { runOnControllerThread(it) { it.play() } } } + fun pause() { mediaController?.let { runOnControllerThread(it) { it.pause() } } } + fun seekTo(positionMs: Long) { + mediaController?.let { runOnControllerThread(it) { it.seekTo(positionMs) } } + } + fun skipToNext() { + mediaController?.let { runOnControllerThread(it) { it.seekToNextMediaItem() } } + } + fun skipToPrevious() { + mediaController?.let { runOnControllerThread(it) { it.seekToPreviousMediaItem() } } + } /** Flip shuffle on/off. Media3 emits onEvents → uiState reflects. */ fun toggleShuffle() {