1use base64::Engine;
2use souvlaki::{MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, PlatformConfig};
3use std::sync::mpsc::{self, Receiver};
4use termusiclib::{
5 library_db::const_unknown::{UNKNOWN_ARTIST, UNKNOWN_TITLE},
6 track::Track,
7};
8
9use crate::{
10 GeneralPlayer, PlayerCmd, PlayerProgress, PlayerTimeUnit, PlayerTrait, Status, Volume,
11};
12
13pub struct Mpris {
14 controls: MediaControls,
15 pub rx: Receiver<MediaControlEvent>,
16}
17
18impl Mpris {
19 pub fn new(cmd_tx: crate::PlayerCmdSender) -> Self {
20 #[cfg(not(target_os = "windows"))]
32 let hwnd = None;
33
34 #[cfg(target_os = "windows")]
35 let (hwnd, _dummy_window) = {
36 let dummy_window = windows::DummyWindow::new().unwrap();
37 let handle = Some(dummy_window.handle.0);
38 (handle, dummy_window)
39 };
40
41 let config = PlatformConfig {
42 dbus_name: "termusic",
43 display_name: "Termusic in Rust",
44 hwnd,
45 };
46
47 let mut controls = MediaControls::new(config).unwrap();
48
49 let (tx, rx) = mpsc::sync_channel(32);
50 controls
52 .attach(move |event: MediaControlEvent| {
53 tx.send(event).ok();
54 cmd_tx.send(PlayerCmd::Tick).ok();
57 })
58 .ok();
59
60 Self { controls, rx }
61 }
62}
63
64impl Mpris {
65 pub fn add_and_play(&mut self, track: &Track) {
66 std::thread::sleep(std::time::Duration::from_millis(100));
68 self.controls
69 .set_playback(MediaPlayback::Playing { progress: None })
70 .ok();
71
72 let cover_art = track.picture().map(|picture| {
73 format!(
74 "data:{};base64,{}",
75 picture.mime_type().map_or_else(
76 || {
77 error!(
78 "Unknown mimetype for picture of track {}",
79 track.file().unwrap_or("<unknown file>")
80 );
81 "application/octet-stream"
82 },
83 |v| v.as_str()
84 ),
85 base64::engine::general_purpose::STANDARD_NO_PAD.encode(picture.data())
86 )
87 });
88
89 self.controls
90 .set_metadata(MediaMetadata {
91 title: Some(track.title().unwrap_or(UNKNOWN_TITLE)),
92 artist: Some(track.artist().unwrap_or(UNKNOWN_ARTIST)),
93 album: Some(track.album().unwrap_or("")),
94 cover_url: cover_art.as_deref(),
95 duration: Some(track.duration()),
96 })
97 .ok();
98 }
99
100 pub fn pause(&mut self) {
101 self.controls
102 .set_playback(MediaPlayback::Paused { progress: None })
103 .ok();
104 }
105 pub fn resume(&mut self) {
106 self.controls
107 .set_playback(MediaPlayback::Playing { progress: None })
108 .ok();
109 }
110
111 pub fn update_progress(&mut self, position: Option<PlayerTimeUnit>, playlist_status: Status) {
113 if let Some(position) = position {
114 match playlist_status {
115 Status::Running => self
116 .controls
117 .set_playback(MediaPlayback::Playing {
118 progress: Some(souvlaki::MediaPosition(position)),
119 })
120 .ok(),
121 Status::Paused | Status::Stopped => self
122 .controls
123 .set_playback(MediaPlayback::Paused {
124 progress: Some(souvlaki::MediaPosition(position)),
125 })
126 .ok(),
127 };
128 }
129 }
130
131 #[allow(unused_variables, clippy::unused_self)] pub fn update_volume(&mut self, volume: Volume) {
136 #[cfg(target_os = "linux")]
138 {
139 let vol = f64::from(volume) / 100.0;
141 let _ = self.controls.set_volume(vol);
142 }
143 }
144}
145
146impl GeneralPlayer {
147 pub fn mpris_handler(&mut self, e: MediaControlEvent) {
148 match e {
149 MediaControlEvent::Next => {
150 self.next();
151 }
152 MediaControlEvent::Previous => {
153 self.previous();
154 }
155 MediaControlEvent::Pause => {
156 self.pause();
157 }
158 MediaControlEvent::Toggle => {
159 self.toggle_pause();
160 }
161 MediaControlEvent::Play => {
162 self.play();
163 }
164 MediaControlEvent::Seek(direction) => {
166 let cmd = match direction {
167 souvlaki::SeekDirection::Forward => PlayerCmd::SeekForward,
168 souvlaki::SeekDirection::Backward => PlayerCmd::SeekBackward,
169 };
170
171 self.cmd_tx.send(cmd).ok();
173 }
174 MediaControlEvent::SetPosition(position) => {
175 self.seek_to(position.0);
176 }
177 MediaControlEvent::OpenUri(_uri) => {
178 info!("Unimplemented Event: OpenUri");
185 }
186 MediaControlEvent::SeekBy(direction, duration) => {
187 #[allow(clippy::cast_possible_wrap)]
188 let as_secs = duration.as_secs().min(i64::MAX as u64) as i64;
189
190 if as_secs == 0 {
192 warn!("can only seek in seconds, got less than 0 seconds");
193 return;
194 }
195
196 let offset = match direction {
197 souvlaki::SeekDirection::Forward => as_secs,
198 souvlaki::SeekDirection::Backward => -as_secs,
199 };
200
201 let _ = self.seek(offset);
204 }
205 MediaControlEvent::SetVolume(volume) => {
206 debug!("got souvlaki SetVolume: {:#}", volume);
207 if volume > 1.0 {
210 error!("SetVolume above 1.0 will be clamped to 1.0!");
211 }
212 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
215 let uvol = (volume.clamp(0.0, 1.0) * 100.0) as u16;
216 self.set_volume(uvol);
217 }
218 MediaControlEvent::Quit => {
219 self.cmd_tx.send(PlayerCmd::Quit).ok();
221 }
222 MediaControlEvent::Stop => {
223 info!("Unimplemented Event: Stop");
225 }
226 MediaControlEvent::Raise => {}
228 }
229 }
230
231 pub fn mpris_handle_events(&mut self) {
233 if let Some(ref mut mpris) = self.mpris {
234 if let Ok(m) = mpris.rx.try_recv() {
235 self.mpris_handler(m);
236 }
237 }
238 }
239
240 #[inline]
242 pub fn mpris_update_progress(&mut self, progress: &PlayerProgress) {
243 if let Some(ref mut mpris) = self.mpris {
244 mpris.update_progress(progress.position, self.playlist.status());
245 }
246 }
247
248 #[inline]
250 pub fn mpris_volume_update(&mut self) {
251 let volume = self.volume();
252 if let Some(ref mut mpris) = self.mpris {
253 mpris.update_volume(volume);
254 }
255 }
256}
257
258#[cfg(target_os = "windows")]
261#[allow(clippy::cast_possible_truncation, unsafe_code)]
262mod windows {
263 use std::io::Error;
264 use std::mem;
265
266 use windows::core::w;
267 use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
269 use windows::Win32::System::LibraryLoader::GetModuleHandleW;
270 use windows::Win32::UI::WindowsAndMessaging::{
271 CreateWindowExW, DefWindowProcW, DestroyWindow, RegisterClassExW, WINDOW_EX_STYLE,
272 WINDOW_STYLE, WNDCLASSEXW,
273 };
274
275 pub struct DummyWindow {
276 pub handle: HWND,
277 }
278
279 impl DummyWindow {
280 pub fn new() -> Result<DummyWindow, String> {
281 let class_name = w!("SimpleTray");
282
283 let handle_result = unsafe {
284 let instance = GetModuleHandleW(None)
285 .map_err(|e| (format!("Getting module handle failed: {e}")))?;
286
287 let wnd_class = WNDCLASSEXW {
288 cbSize: mem::size_of::<WNDCLASSEXW>() as u32,
289 hInstance: instance.into(),
290 lpszClassName: class_name,
291 lpfnWndProc: Some(Self::wnd_proc),
292 ..Default::default()
293 };
294
295 if RegisterClassExW(&wnd_class) == 0 {
296 return Err(format!(
297 "Registering class failed: {}",
298 Error::last_os_error()
299 ));
300 }
301
302 let handle = match CreateWindowExW(
303 WINDOW_EX_STYLE::default(),
304 class_name,
305 w!(""),
306 WINDOW_STYLE::default(),
307 0,
308 0,
309 0,
310 0,
311 None,
312 None,
313 instance,
314 None,
315 ) {
316 Ok(v) => v,
317 Err(err) => {
318 return Err(format!("{err}"));
319 }
320 };
321
322 if handle.is_invalid() {
323 Err(format!(
324 "Message only window creation failed: {}",
325 Error::last_os_error()
326 ))
327 } else {
328 Ok(handle)
329 }
330 };
331
332 handle_result.map(|handle| DummyWindow { handle })
333 }
334 extern "system" fn wnd_proc(
335 hwnd: HWND,
336 msg: u32,
337 wparam: WPARAM,
338 lparam: LPARAM,
339 ) -> LRESULT {
340 unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
341 }
342 }
343
344 impl Drop for DummyWindow {
345 fn drop(&mut self) {
346 unsafe {
347 DestroyWindow(self.handle).unwrap();
348 }
349 }
350 }
351
352 }