//! The state websocket, with reconnect-and-reseed. //! //! Ported from `web/src/hooks/usePlayerState.ts`. The backend pushes and never listens, //! so this is a read loop and nothing else. Two details are load-bearing: //! //! * **Reseed on open, not just on connect.** A reconnect can have missed every change //! between the drop and the new socket, and the server only sends a snapshot to a //! client it already considers connected. So each successful open is followed by a //! `GET /api/state`. //! * **Position is not on the event bus.** The server polls its player on a 0.5 s timer //! and sends `position` frames only while something is playing - so the absence of //! them is not a stall, and the UI interpolates between them rather than waiting. use std::sync::mpsc::Sender; use std::time::Duration; use crate::api::{Client, PlayerState, ServerMessage}; /// What the UI thread hears from this connection. Wider than `ServerMessage` because /// connectivity is itself a thing the screen shows. pub enum Update { Connected, Disconnected, State(Box), Position { position: f64, duration: f64, }, /// The library was rescanned: re-fetch it, and drop the cover thumbnails, since a /// rescan is exactly when the art behind an unchanged URL gets rewritten. LibraryChanged, } /// Matches the web client's retry interval. Short enough that the screen comes back /// before a child gives up on it, long enough not to spin while the backend restarts. const RETRY: Duration = Duration::from_millis(1500); pub fn spawn(client: Client, updates: Sender) -> std::thread::JoinHandle<()> { std::thread::Builder::new() .name("ws".into()) .spawn(move || loop { match connect_once(&client, &updates) { Ok(()) | Err(()) => {} } // `send` failing means the UI is gone, which is the one reason to stop. if updates.send(Update::Disconnected).is_err() { return; } std::thread::sleep(RETRY); }) .expect("spawning the websocket thread") } fn connect_once(client: &Client, updates: &Sender) -> Result<(), ()> { let (mut socket, _) = tungstenite::connect(client.websocket_url()).map_err(|_| ())?; if updates.send(Update::Connected).is_err() { return Err(()); } // Reseed: see the note at the top of this module. if let Ok(state) = client.state() { if updates.send(Update::State(Box::new(state))).is_err() { return Err(()); } } loop { let message = match socket.read() { Ok(message) => message, Err(_) => return Err(()), }; let text = match message { tungstenite::Message::Text(text) => text, tungstenite::Message::Close(_) => return Ok(()), // Ping/Pong are answered by tungstenite itself on the next write; binary // frames are not part of this protocol. _ => continue, }; let parsed: ServerMessage = match serde_json::from_str(&text) { Ok(parsed) => parsed, // A frame this build cannot read is not a reason to drop the connection - // the next one is probably fine. Err(_) => continue, }; let update = match parsed { ServerMessage::State { state } => Update::State(Box::new(state)), ServerMessage::Position { position, duration } => { Update::Position { position, duration } } ServerMessage::Library => Update::LibraryChanged, ServerMessage::Unknown => continue, }; if updates.send(update).is_err() { return Err(()); } } }