A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
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}