//! The media session on the phone. //! //! Android shows a playing app on the lock screen and in the shade through a //! `MediaSession` carried by a foreground service, and it is that service //! that keeps the process alive — and un-frozen — once the app is in the //! background. The audio itself stays where it is, on the Rust audio thread; //! the Kotlin side only mirrors what is playing and hands the card's //! buttons, the headset's buttons and audio-focus changes back as events. //! //! Two commands, both fed by the phone UI (src/mobile/media.svelte.js): //! `update` with the track on the bar, `clear` when the bar empties. The //! session's actions arrive as the plugin event `action` //! (`addPluginListener("media-session", "action", …)`). //! //! On the desktop the plugin is registered too, so the permission set //! resolves on every platform, but its commands answer "not here": the //! desktop has its own card (src-tauri/src/smtc.rs). use serde::{Deserialize, Serialize}; use tauri::{ plugin::{Builder, TauriPlugin}, Runtime, }; /// What the card shows. `art` carries the cover's bytes (base64) only when /// `art_key` changed since the last update — the card keeps the bitmap. #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct NowPlaying { pub title: Option, pub artist: Option, pub album: Option, #[serde(default)] pub playing: bool, #[serde(default)] pub position: f64, pub duration: Option, pub art_key: Option, pub art: Option, #[serde(default = "yes")] pub has_next: bool, #[serde(default = "yes")] pub has_prev: bool, } fn yes() -> bool { true } #[cfg(mobile)] pub struct MediaSession(tauri::plugin::PluginHandle); #[tauri::command] fn update(app: tauri::AppHandle, state: NowPlaying) -> Result<(), String> { #[cfg(mobile)] { use tauri::Manager; let session = app.state::>(); session .0 .run_mobile_plugin::("update", state) .map(|_| ()) .map_err(|e| e.to_string()) } #[cfg(not(mobile))] { let _ = (app, state); Err("the media session is a phone feature".into()) } } #[tauri::command] fn clear(app: tauri::AppHandle) -> Result<(), String> { #[cfg(mobile)] { use tauri::Manager; let session = app.state::>(); session .0 .run_mobile_plugin::("clear", ()) .map(|_| ()) .map_err(|e| e.to_string()) } #[cfg(not(mobile))] { let _ = app; Err("the media session is a phone feature".into()) } } pub fn init() -> TauriPlugin { Builder::new("media-session") .invoke_handler(tauri::generate_handler![update, clear]) .setup(|app, api| { #[cfg(target_os = "android")] { use tauri::Manager; let handle = api.register_android_plugin("com.soulstream.mediasession", "MediaSessionPlugin")?; app.manage(MediaSession(handle)); } #[cfg(not(target_os = "android"))] { let _ = (app, api); } Ok(()) }) .build() }