first step
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user