A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 447 lines 16 kB view raw
1use anyhow::Error; 2use souvlaki::{ 3 MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, MediaPosition, PlatformConfig, 4}; 5use std::env; 6use std::sync::atomic::{AtomicBool, Ordering}; 7use std::sync::{mpsc, Arc}; 8use std::time::Duration; 9use tonic::transport::Channel; 10use winit::application::ApplicationHandler; 11use winit::event::WindowEvent; 12use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; 13use winit::window::WindowId; 14 15use crate::api::rockbox::v1alpha1::playback_service_client::PlaybackServiceClient; 16use crate::api::rockbox::v1alpha1::playlist_service_client::PlaylistServiceClient; 17use crate::api::rockbox::v1alpha1::system_service_client::SystemServiceClient; 18use crate::api::rockbox::v1alpha1::{ 19 GetGlobalStatusRequest, NextRequest, PauseRequest, PlayRequest, PreviousRequest, ResumeRequest, 20 ResumeTrackRequest, StatusRequest, StreamCurrentTrackRequest, StreamStatusRequest, 21}; 22 23pub mod api { 24 #[path = ""] 25 pub mod rockbox { 26 27 #[path = "rockbox.v1alpha1.rs"] 28 pub mod v1alpha1; 29 } 30} 31 32// Commands to send to the media controls from async tasks 33#[derive(Debug)] 34enum MediaCommand { 35 SetMetadata { 36 title: String, 37 artist: String, 38 album: String, 39 duration: Duration, 40 cover_url: Option<String>, 41 }, 42 Play, 43 Pause, 44 Next, 45 Previous, 46 SetMediaPosition((MediaPosition, bool)), 47} 48 49struct App { 50 controls: MediaControls, 51 command_receiver: mpsc::Receiver<MediaCommand>, 52 _media_event_sender: tokio::sync::mpsc::UnboundedSender<MediaControlEvent>, 53} 54 55impl ApplicationHandler for App { 56 fn resumed(&mut self, _event_loop: &ActiveEventLoop) { 57 println!("App resumed, media controls ready!"); 58 } 59 60 fn window_event(&mut self, _event_loop: &ActiveEventLoop, _id: WindowId, _event: WindowEvent) {} 61 62 fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { 63 // Process all pending commands from async tasks 64 while let Ok(cmd) = self.command_receiver.try_recv() { 65 match cmd { 66 MediaCommand::SetMetadata { 67 title, 68 artist, 69 album, 70 duration, 71 cover_url, 72 } => { 73 // Set playback state first 74 if let Err(e) = self 75 .controls 76 .set_playback(MediaPlayback::Playing { progress: None }) 77 { 78 eprintln!("Failed to set playback state: {}", e); 79 } 80 81 // Then set metadata 82 if let Err(e) = self.controls.set_metadata(MediaMetadata { 83 title: Some(&title), 84 artist: Some(&artist), 85 album: Some(&album), 86 duration: Some(duration), 87 cover_url: cover_url.as_deref(), 88 }) { 89 eprintln!("Failed to set metadata: {}", e); 90 } 91 } 92 MediaCommand::Play => { 93 if let Err(e) = self 94 .controls 95 .set_playback(MediaPlayback::Playing { progress: None }) 96 { 97 eprintln!("Failed to set playback state: {}", e); 98 } 99 } 100 MediaCommand::Pause => { 101 if let Err(e) = self 102 .controls 103 .set_playback(MediaPlayback::Paused { progress: None }) 104 { 105 eprintln!("Failed to set playback state: {}", e); 106 } 107 } 108 MediaCommand::SetMediaPosition((position, playing)) => { 109 if let Err(e) = self.controls.set_playback(match playing { 110 true => MediaPlayback::Playing { 111 progress: Some(position), 112 }, 113 false => MediaPlayback::Paused { 114 progress: Some(position), 115 }, 116 }) { 117 eprintln!("Failed to set playback state: {}", e); 118 } 119 } 120 _ => {} 121 } 122 } 123 } 124} 125 126/// Start the media controls system. 127/// This function blocks and runs the event loop on the main thread. 128pub fn run_media_controls() -> Result<(), Box<dyn std::error::Error>> { 129 // Channel for sending commands TO the media controls (from async tasks) 130 let (command_sender, command_receiver) = mpsc::channel::<MediaCommand>(); 131 132 // Channel for receiving events FROM the media controls (to async tasks) 133 let (media_event_sender, media_event_receiver) = 134 tokio::sync::mpsc::unbounded_channel::<MediaControlEvent>(); 135 136 // Shared playing state between status and metadata tasks 137 let is_playing = Arc::new(AtomicBool::new(false)); 138 139 let sender = command_sender.clone(); 140 let playing_state = Arc::clone(&is_playing); 141 std::thread::spawn(move || { 142 let runtime = tokio::runtime::Runtime::new().unwrap(); 143 match runtime.block_on(spawn_metadata_update_task(sender.clone(), playing_state)) { 144 Ok(_) => println!("Metadata update task completed"), 145 Err(e) => eprintln!("Metadata update task failed: {}", e), 146 } 147 }); 148 149 let sender = command_sender.clone(); 150 std::thread::spawn(move || { 151 let runtime = tokio::runtime::Runtime::new().unwrap(); 152 match runtime.block_on(spawn_event_handler_task( 153 sender.clone(), 154 media_event_receiver, 155 )) { 156 Ok(()) => println!("Event handler task completed"), 157 Err(e) => eprintln!("Event handler task failed: {}", e), 158 } 159 }); 160 161 let sender = command_sender.clone(); 162 let playing_state = Arc::clone(&is_playing); 163 std::thread::spawn(move || { 164 let runtime = tokio::runtime::Runtime::new().unwrap(); 165 match runtime.block_on(spawn_status_update_task(sender.clone(), playing_state)) { 166 Ok(_) => println!("Status update task completed"), 167 Err(e) => eprintln!("Status update task failed: {}", e), 168 } 169 }); 170 171 // Run the event loop on the main thread (required for macOS) 172 let event_loop = EventLoop::new()?; 173 event_loop.set_control_flow(ControlFlow::Poll); 174 175 let mut controls = MediaControls::new(PlatformConfig { 176 display_name: "Rockbox", 177 dbus_name: "tsirysndr.rockbox", 178 hwnd: None, 179 })?; 180 181 // Attach event handler - forward events to async task 182 let event_sender = media_event_sender.clone(); 183 controls.attach(move |event| { 184 let _ = event_sender.send(event); 185 })?; 186 187 let mut app = App { 188 controls, 189 command_receiver, 190 _media_event_sender: media_event_sender, 191 }; 192 193 event_loop.run_app(&mut app)?; 194 195 Ok(()) 196} 197 198async fn build_client() -> Result<PlaybackServiceClient<Channel>, tonic::transport::Error> { 199 let host = env::var("ROCKBOX_HOST").unwrap_or_else(|_| "localhost".to_string()); 200 let port = env::var("ROCKBOX_PORT").unwrap_or_else(|_| "6061".to_string()); 201 202 let url = format!("tcp://{}:{}", host, port); 203 204 PlaybackServiceClient::connect(url).await 205} 206 207async fn build_system_client() -> Result<SystemServiceClient<Channel>, tonic::transport::Error> { 208 let host = env::var("ROCKBOX_HOST").unwrap_or_else(|_| "localhost".to_string()); 209 let port = env::var("ROCKBOX_PORT").unwrap_or_else(|_| "6061".to_string()); 210 211 let url = format!("tcp://{}:{}", host, port); 212 213 SystemServiceClient::connect(url).await 214} 215 216async fn build_playlist_client() -> Result<PlaylistServiceClient<Channel>, tonic::transport::Error> 217{ 218 let host = env::var("ROCKBOX_HOST").unwrap_or_else(|_| "localhost".to_string()); 219 let port = env::var("ROCKBOX_PORT").unwrap_or_else(|_| "6061".to_string()); 220 221 let url = format!("tcp://{}:{}", host, port); 222 223 PlaylistServiceClient::connect(url).await 224} 225 226async fn spawn_metadata_update_task( 227 command_sender: mpsc::Sender<MediaCommand>, 228 is_playing: Arc<AtomicBool>, 229) -> Result<(), Error> { 230 let mut client = build_client().await?; 231 let mut stream = client 232 .stream_current_track(StreamCurrentTrackRequest {}) 233 .await? 234 .into_inner(); 235 236 let asset_host = env::var("ROCKBOX_GRAPHQL_HOST").unwrap_or_else(|_| "localhost".to_string()); 237 let asset_port = env::var("ROCKBOX_GRAPHQL_PORT").unwrap_or_else(|_| "6062".to_string()); 238 239 let mut previous_album_art: Option<String> = None; 240 let mut current_cover_url: Option<String> = None; 241 242 // Track previous track information to detect changes 243 let mut previous_title: Option<String> = None; 244 let mut previous_artist: Option<String> = None; 245 let mut previous_album: Option<String> = None; 246 let mut previous_length: Option<u64> = None; 247 248 // Track previous playing state 249 let mut previous_playing: Option<bool> = None; 250 251 while let Some(track) = stream.message().await? { 252 // Check if track has changed 253 let track_changed = previous_title.as_ref() != Some(&track.title) 254 || previous_artist.as_ref() != Some(&track.artist) 255 || previous_album.as_ref() != Some(&track.album) 256 || previous_length != Some(track.length); 257 258 // Only update cover_url if album_art has changed 259 if track.album_art != previous_album_art { 260 previous_album_art.clone_from(&track.album_art); 261 current_cover_url = match &track.album_art { 262 Some(album_art) => match album_art.starts_with("http") { 263 true => Some(album_art.clone()), 264 false => Some(format!( 265 "http://{}:{}/covers/{}", 266 asset_host, asset_port, album_art 267 )), 268 }, 269 None => None, 270 }; 271 } 272 273 // Only send metadata if track changed 274 if track_changed { 275 let cmd = MediaCommand::SetMetadata { 276 title: track.title.clone(), 277 artist: track.artist.clone(), 278 album: track.album.clone(), 279 duration: Duration::from_millis(track.length), 280 cover_url: current_cover_url.clone(), 281 }; 282 283 command_sender.send(cmd)?; 284 285 // Update previous track info 286 previous_title = Some(track.title); 287 previous_artist = Some(track.artist); 288 previous_album = Some(track.album); 289 previous_length = Some(track.length); 290 } 291 292 // Get current playing state 293 let playing = is_playing.load(Ordering::Relaxed); 294 let status_changed = previous_playing != Some(playing); 295 296 // Only send position update if track changed or status changed 297 if track_changed || status_changed { 298 command_sender.send(MediaCommand::SetMediaPosition(( 299 MediaPosition(Duration::from_millis(track.elapsed)), 300 playing, 301 )))?; 302 303 previous_playing = Some(playing); 304 } 305 } 306 307 Ok(()) 308} 309 310async fn spawn_event_handler_task( 311 command_sender: std::sync::mpsc::Sender<MediaCommand>, 312 mut receiver: tokio::sync::mpsc::UnboundedReceiver<MediaControlEvent>, 313) -> Result<(), Error> { 314 let mut client = build_client().await?; 315 let mut system = build_system_client().await?; 316 let mut playlist = build_playlist_client().await?; 317 318 while let Some(event) = receiver.recv().await { 319 match event { 320 MediaControlEvent::Play => { 321 println!("[MediaControl] Play"); 322 command_sender.send(MediaCommand::Play)?; 323 324 let status_resp = client.status(StatusRequest {}).await?.into_inner(); 325 326 let global_status = system 327 .get_global_status(GetGlobalStatusRequest {}) 328 .await? 329 .into_inner(); 330 331 if global_status.resume_index > -1 && status_resp.status == 0 { 332 playlist 333 .resume_track(ResumeTrackRequest { 334 ..Default::default() 335 }) 336 .await?; 337 } else { 338 client.resume(ResumeRequest {}).await?; 339 } 340 } 341 MediaControlEvent::Pause => { 342 println!("[MediaControl] Pause"); 343 command_sender.send(MediaCommand::Pause)?; 344 client.pause(PauseRequest {}).await?; 345 } 346 MediaControlEvent::Next => { 347 println!("[MediaControl] Next"); 348 command_sender.send(MediaCommand::Next)?; 349 command_sender.send(MediaCommand::SetMediaPosition(( 350 MediaPosition(Duration::from_millis(0)), 351 true, 352 )))?; 353 client.next(NextRequest {}).await?; 354 } 355 MediaControlEvent::Previous => { 356 println!("[MediaControl] Previous"); 357 command_sender.send(MediaCommand::Previous)?; 358 command_sender.send(MediaCommand::SetMediaPosition(( 359 MediaPosition(Duration::from_millis(0)), 360 true, 361 )))?; 362 client.previous(PreviousRequest {}).await?; 363 } 364 MediaControlEvent::Seek(_) => { 365 println!("[MediaControl] Seek"); 366 } 367 MediaControlEvent::Toggle => { 368 println!("[MediaControl] Toggle"); 369 } 370 MediaControlEvent::Stop => { 371 println!("[MediaControl] Stop"); 372 } 373 MediaControlEvent::SeekBy(seek_direction, duration) => { 374 println!("[MediaControl] SeekBy {:?} {:?}", seek_direction, duration); 375 } 376 MediaControlEvent::SetPosition(media_position) => { 377 println!("[MediaControl] SetPosition {:?}", media_position); 378 client 379 .play(PlayRequest { 380 elapsed: media_position.0.as_millis() as i64, 381 offset: 0, 382 }) 383 .await?; 384 } 385 MediaControlEvent::SetVolume(volume) => { 386 println!("[MediaControl] SetVolume {}", volume); 387 } 388 MediaControlEvent::OpenUri(uri) => { 389 println!("[MediaControl] OpenUri {}", uri); 390 } 391 MediaControlEvent::Raise => { 392 println!("[MediaControl] Raise"); 393 } 394 MediaControlEvent::Quit => { 395 println!("[MediaControl] Quit"); 396 } 397 } 398 } 399 400 println!(">> Event receiver closed"); 401 402 Ok(()) 403} 404 405async fn spawn_status_update_task( 406 command_sender: mpsc::Sender<MediaCommand>, 407 is_playing: Arc<AtomicBool>, 408) -> Result<(), Error> { 409 let host = env::var("ROCKBOX_HOST").unwrap_or_else(|_| "localhost".to_string()); 410 let port = env::var("ROCKBOX_PORT").unwrap_or_else(|_| "6061".to_string()); 411 412 let url = format!("tcp://{}:{}", host, port); 413 414 let mut client = PlaybackServiceClient::connect(url).await?; 415 416 let mut stream = client 417 .stream_status(StreamStatusRequest {}) 418 .await? 419 .into_inner(); 420 421 // Track previous status to only send updates when it changes 422 let mut previous_status: Option<i32> = None; 423 424 while let Some(response) = stream.message().await? { 425 // Only send command if status actually changed 426 if previous_status != Some(response.status) { 427 match response.status { 428 1 => { 429 is_playing.store(true, Ordering::Relaxed); 430 command_sender.send(MediaCommand::Play)?; 431 } 432 3 => { 433 is_playing.store(false, Ordering::Relaxed); 434 command_sender.send(MediaCommand::Pause)?; 435 } 436 _ => { 437 is_playing.store(false, Ordering::Relaxed); 438 command_sender.send(MediaCommand::Pause)?; 439 } 440 }; 441 442 previous_status = Some(response.status); 443 } 444 } 445 446 Ok(()) 447}