diff --git a/crates/tauri-plugin-media-session/android/src/main/java/com/soulstream/mediasession/SoulStreamPlayer.kt b/crates/tauri-plugin-media-session/android/src/main/java/com/soulstream/mediasession/SoulStreamPlayer.kt index a7d3cb7..26b042b 100644 --- a/crates/tauri-plugin-media-session/android/src/main/java/com/soulstream/mediasession/SoulStreamPlayer.kt +++ b/crates/tauri-plugin-media-session/android/src/main/java/com/soulstream/mediasession/SoulStreamPlayer.kt @@ -252,6 +252,10 @@ class SoulStreamPlayer(private val ctx: Context) : SimpleBasePlayer(Looper.getMa /* ── the transport, back out to the WebView ──────────────────────── */ override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> { + android.util.Log.i( + "SoulStream", + "auto playWhenReady=" + playWhenReady + " headless=" + headless(), + ) if (headless()) { AutoNative.transport(if (playWhenReady) "resume" else "pause") return Futures.immediateVoidFuture() diff --git a/handoff/rounds/commit-msg54.txt b/handoff/rounds/commit-msg54.txt new file mode 100644 index 0000000..678156e --- /dev/null +++ b/handoff/rounds/commit-msg54.txt @@ -0,0 +1,28 @@ +android auto: one engine per process, actually — init was still spawning its own + +`headless.rs` was written around a guarantee its own doc comment made and the +code did not keep: `playback::init` still called `Player::spawn_with_device` +directly. So a car could start a cold process, play a record into it, and the +moment the app's Activity came up the process built a SECOND player — two +AAudio streams out of one app, playing over each other. That is precisely the +failure the module exists to prevent, and nothing would have reported it but +Kyle's ears. + +On Android `init` now takes the process's engine instead. The event receiver is +single-consumer and the headless pump already owns it, so the UI SUBSCRIBES to +the pump rather than reading the engine directly; and the fallback queue is +cleared on the way in, because from that point the JS decides what plays next +and two things advancing one engine would skip a track each time. The desktop +path is untouched — it still spawns its own, because there is nothing else in +that process to share with. + +Also here: a log line on the transport's routing decision +(`auto playWhenReady=… headless=…` and `auto transport …`), added while +chasing a pause that turned out to be working. It is kept because the question +"did this button go to Rust or to a WebView that is not there" has now come up +three times and is invisible without it. + +The pause was real, and the measurement was not. "One active AAudio track after +a pause" proves nothing — a paused sink still shows as active. The honest test +is that the session reports PAUSED(2) and the position stops moving, which it +does: 24873 ms, unchanged five seconds later. diff --git a/src-tauri/src/auto_browse.rs b/src-tauri/src/auto_browse.rs index 6e578fe..88b8862 100644 --- a/src-tauri/src/auto_browse.rs +++ b/src-tauri/src/auto_browse.rs @@ -619,6 +619,7 @@ pub extern "system" fn Java_com_soulstream_mediasession_AutoNative_transport( return; }; let audio = crate::headless::audio(None); + warn(&format!("auto transport {a}")); match a.as_str() { "pause" => audio.player.pause(), "resume" => audio.player.resume(), diff --git a/src-tauri/src/playback.rs b/src-tauri/src/playback.rs index 86d6395..6a04007 100644 --- a/src-tauri/src/playback.rs +++ b/src-tauri/src/playback.rs @@ -186,6 +186,29 @@ enum PlayerUiEvent { /// Create the audio thread and forward its events to the webview. pub fn init(app: &tauri::AppHandle) { let output_device = crate::config::load(app).audio_output_device; + + /* ANDROID TAKES THE PROCESS'S ONE ENGINE, it does not make a second. + A car can start this app by binding the media service, play a record + into a process with no Activity, and have the user open the app on top + of it minutes later — and `Player::spawn` opens an output stream, so a + second one here is two AAudio streams playing over each other out of one + process. `headless::audio` builds it on first use, whoever gets there + first, and hands back the same one afterwards. + + The engine's event receiver is single-consumer and the headless pump + already owns it, so the UI subscribes rather than reading it directly; + and the fallback queue is cleared, because from here the JS decides what + plays next and two things advancing one engine would skip a track each + time. Nothing about the desktop changes. */ + #[cfg(target_os = "android")] + let (player, events) = { + let audio = crate::headless::audio(output_device.clone()); + let (tx, rx) = std::sync::mpsc::channel(); + audio.subscribe(tx); + audio.clear_queue(); + (audio.player.clone(), rx) + }; + #[cfg(not(target_os = "android"))] let (player, events) = Player::spawn_with_device(output_device); app.manage(PlayerState { player,