first step

This commit is contained in:
2026-01-11 10:30:54 +01:00
parent f9b9ce0994
commit 34c3f0dc89
7 changed files with 1105 additions and 30 deletions

View File

@@ -9,6 +9,9 @@ use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri_plugin_shell::ShellExt;
use reqwest;
mod player;
use player::{PlayerCommand, PlayerController, PlayerShared, PlayerState};
struct SidecarState {
child: Mutex<Option<CommandChild>>,
}
@@ -17,6 +20,81 @@ struct AppState {
known_devices: Mutex<HashMap<String, String>>,
}
// Native (non-WebView) audio player state.
// Step 1: state machine + command interface only (no decoding/output yet).
struct PlayerRuntime {
shared: &'static PlayerShared,
controller: PlayerController,
}
fn clamp01(v: f32) -> f32 {
if v.is_nan() {
0.0
} else if v < 0.0 {
0.0
} else if v > 1.0 {
1.0
} else {
v
}
}
#[tauri::command]
async fn player_get_state(player: State<'_, PlayerRuntime>) -> Result<PlayerState, String> {
Ok(player.shared.snapshot())
}
#[tauri::command]
async fn player_set_volume(
player: State<'_, PlayerRuntime>,
volume: f32,
) -> Result<(), String> {
let volume = clamp01(volume);
{
let mut s = player.shared.state.lock().unwrap();
s.volume = volume;
}
player
.controller
.tx
.send(PlayerCommand::SetVolume { volume })
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
async fn player_play(player: State<'_, PlayerRuntime>, url: String) -> Result<(), String> {
{
let mut s = player.shared.state.lock().unwrap();
s.error = None;
s.url = Some(url.clone());
// Step 1: report buffering immediately; the engine thread will progress.
s.status = player::PlayerStatus::Buffering;
}
player
.controller
.tx
.send(PlayerCommand::Play { url })
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
async fn player_stop(player: State<'_, PlayerRuntime>) -> Result<(), String> {
{
let mut s = player.shared.state.lock().unwrap();
s.error = None;
s.status = player::PlayerStatus::Stopped;
}
player
.controller
.tx
.send(PlayerCommand::Stop)
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
async fn list_cast_devices(state: State<'_, AppState>) -> Result<Vec<String>, String> {
let devices = state.known_devices.lock().unwrap();
@@ -139,6 +217,14 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init())
.on_window_event(|window, event| {
// Ensure native audio shuts down on app close.
// We do not prevent the close; this is best-effort cleanup.
if matches!(event, tauri::WindowEvent::CloseRequested { .. }) {
let player = window.app_handle().state::<PlayerRuntime>();
let _ = player.controller.tx.send(PlayerCommand::Shutdown);
}
})
.setup(|app| {
app.manage(AppState {
known_devices: Mutex::new(HashMap::new()),
@@ -147,6 +233,15 @@ pub fn run() {
child: Mutex::new(None),
});
// Player scaffolding: leak shared state to get a 'static reference for the
// long-running thread without complex lifetime plumbing.
// Later refactors can move this to Arc<...> when the engine grows.
let shared: &'static PlayerShared = Box::leak(Box::new(PlayerShared {
state: Mutex::new(PlayerState::default()),
}));
let controller = player::spawn_player_thread(shared);
app.manage(PlayerRuntime { shared, controller });
let handle = app.handle().clone();
thread::spawn(move || {
let mdns = ServiceDaemon::new().expect("Failed to create daemon");
@@ -189,7 +284,12 @@ pub fn run() {
cast_stop,
cast_set_volume,
// allow frontend to request arbitrary URLs via backend (bypass CORS)
fetch_url
fetch_url,
// native player commands (step 1 scaffold)
player_play,
player_stop,
player_set_volume,
player_get_state
])
.run(tauri::generate_context!())
.expect("error while running tauri application");