pub mod decoder; pub mod output; use std::sync::{ atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering}, Arc, }; use std::time::Duration; use crate::api::TrackDto; use decoder::NextAction; #[derive(Clone)] pub enum PlayerCommand { Play(TrackInfo), QueueNext(TrackInfo), Pause, Resume, Stop, SetVolume(u8), } #[derive(Clone)] pub struct TrackInfo { pub track: TrackDto, pub url: String, pub n_segments: u32, pub encryption_key: Option, pub replaygain_db: Option, pub prefetch_data: Option, } #[derive(Debug, Clone, PartialEq)] pub enum PlayerState { Idle, Playing, Paused, Error(String), } #[derive(Clone)] pub struct PlayerStatus { pub state: Arc>, pub position_secs: Arc, pub duration_secs: Arc, pub volume: Arc, pub current_track: Arc>>, pub track_finished: Arc, pub track_transitioned: Arc, pub seek_requested: Arc, pub seek_target_secs: Arc, pub replaygain_gain: Arc>, pub gapless: Arc, } impl PlayerStatus { pub fn new() -> Self { Self { state: Arc::new(std::sync::Mutex::new(PlayerState::Idle)), position_secs: Arc::new(AtomicU64::new(0)), duration_secs: Arc::new(AtomicU64::new(0)), volume: Arc::new(AtomicU8::new(80)), current_track: Arc::new(std::sync::Mutex::new(None)), track_finished: Arc::new(AtomicBool::new(false)), track_transitioned: Arc::new(AtomicBool::new(false)), seek_requested: Arc::new(AtomicBool::new(false)), seek_target_secs: Arc::new(AtomicU64::new(0)), replaygain_gain: Arc::new(std::sync::Mutex::new(1.0)), gapless: Arc::new(AtomicBool::new(false)), } } pub fn get_state(&self) -> PlayerState { self.state.lock().unwrap().clone() } pub fn get_position(&self) -> u64 { self.position_secs.load(Ordering::Relaxed) } pub fn get_duration(&self) -> u64 { self.duration_secs.load(Ordering::Relaxed) } pub fn get_volume(&self) -> u8 { self.volume.load(Ordering::Relaxed) } } pub struct Player { pub cmd_tx: std::sync::mpsc::SyncSender, pub status: PlayerStatus, } impl Player { pub fn new() -> Self { let (cmd_tx, cmd_rx) = std::sync::mpsc::sync_channel(32); let status = PlayerStatus::new(); let status_clone = status.clone(); std::thread::spawn(move || { player_loop(cmd_rx, status_clone); }); Self { cmd_tx, status } } pub fn send(&self, cmd: PlayerCommand) { self.cmd_tx.send(cmd).ok(); } pub fn pause(&self) { self.send(PlayerCommand::Pause); } pub fn resume(&self) { self.send(PlayerCommand::Resume); } pub fn stop(&self) { self.send(PlayerCommand::Stop); } pub fn set_volume(&self, vol: u8) { self.status.volume.store(vol, Ordering::Relaxed); self.send(PlayerCommand::SetVolume(vol)); } pub fn seek(&self, secs: u64) { self.status.seek_target_secs.store(secs, Ordering::Relaxed); self.status.seek_requested.store(true, Ordering::SeqCst); } } fn player_loop(rx: std::sync::mpsc::Receiver, status: PlayerStatus) { use std::sync::mpsc::RecvTimeoutError; let mut audio_output: Option = None; let paused = Arc::new(AtomicBool::new(false)); let mut pending_action: Option = None; 'outer: loop { let info = if let Some(action) = pending_action.take() { match action { NextAction::Play(info) | NextAction::Transition(info) => info, } } else { loop { match rx.recv_timeout(Duration::from_millis(100)) { Ok(PlayerCommand::Play(info)) => break info, Ok(PlayerCommand::QueueNext(info)) => { // If we are completely idle and get QueueNext, treat as Play break info; } Ok(PlayerCommand::Stop) => { audio_output = None; paused.store(false, Ordering::SeqCst); *status.state.lock().unwrap() = PlayerState::Idle; *status.current_track.lock().unwrap() = None; status.position_secs.store(0, Ordering::Relaxed); status.duration_secs.store(0, Ordering::Relaxed); } Ok(PlayerCommand::SetVolume(v)) => { status.volume.store(v, Ordering::Relaxed); } Ok(_) => {} // Ignore Pause/Resume when idle Err(RecvTimeoutError::Timeout) => {} Err(RecvTimeoutError::Disconnected) => break 'outer, } } }; let rg_factor = info .replaygain_db .map(|db| 10f32.powf(db as f32 / 20.0)) .unwrap_or(1.0); *status.replaygain_gain.lock().unwrap() = rg_factor; *status.state.lock().unwrap() = PlayerState::Playing; *status.current_track.lock().unwrap() = Some(info.track.clone()); if let Some(dur) = info.track.duration { status.duration_secs.store(dur as u64, Ordering::Relaxed); } status.position_secs.store(0, Ordering::Relaxed); paused.store(false, Ordering::SeqCst); // TrackInfo now directly passes the prefetch_data (if it exists) to the decoder match decoder::play_track_inline(info, &status, &paused, &mut audio_output, &rx) { Ok(Some(NextAction::Play(next_track))) => { pending_action = Some(NextAction::Play(next_track)); // Interrupted by a manual play, no need to tell C++ to advance the queue } Ok(Some(NextAction::Transition(next_track))) => { pending_action = Some(NextAction::Play(next_track)); status.track_transitioned.store(true, Ordering::SeqCst); } Ok(None) => { if !status.gapless.load(Ordering::Relaxed) { audio_output = None; } *status.state.lock().unwrap() = PlayerState::Idle; status.track_finished.store(true, Ordering::SeqCst); } Err(e) => { eprintln!("playback error: {e}"); *status.state.lock().unwrap() = PlayerState::Error(e.to_string()); status.track_finished.store(true, Ordering::SeqCst); } } } }