diff --git a/README.md b/README.md index 8abe1b12..80a086ac 100644 --- a/README.md +++ b/README.md @@ -358,7 +358,8 @@ No config options. | Variable | Description | Return type | Supported OS | | ------------------- | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `session` | Currently playing media session. | `MediaSession \| null` | microsoft icon | +| `currentSession` | Currently playing media session. | `MediaSession \| null` | microsoft icon | +| `allSessions` | All active media sessions. | `MediaSession[]` | microsoft icon | #### Return types @@ -366,15 +367,17 @@ No config options. | Variable | Description | Return type | | ------------------ | ----------------------------- | ----------------------- | -| `title` | TODO | `string` | -| `artist` | TODO | `string \| null` | -| `albumTitle` | TODO | `string \| null` | -| `albumArtist` | TODO | `string \| null` | -| `trackNumber` | TODO | `number` | -| `startTime` | TODO | `number` | -| `endTime` | TODO | `number` | -| `position` | TODO | `number` | -| `isPlaying` | TODO | `boolean` | +| `sessionId` | ID of the media session. | `string` | +| `title` | Title of the media session. | `string` | +| `artist` | Artist of the media session. | `string \| null` | +| `albumTitle` | Album title of the media session. | `string \| null` | +| `albumArtist` | Album artist of the media session. | `string \| null` | +| `trackNumber` | Track number of the media session. | `number` | +| `startTime` | Start time of the media session. | `number` | +| `endTime` | End time of the media session. | `number` | +| `position` | Position of the media session. | `number` | +| `isPlaying` | Whether the media session is playing. | `boolean` | +| `isCurrentSession` | Whether this is the currently active session (i.e. `currentSession`). | `boolean` | #### `DataSizeMeasure` diff --git a/examples/boilerplate-react-buildless/index.html b/examples/boilerplate-react-buildless/index.html index ca823438..f76e9e1a 100644 --- a/examples/boilerplate-react-buildless/index.html +++ b/examples/boilerplate-react-buildless/index.html @@ -51,8 +51,11 @@ return (
- Media: {output.media?.session?.title} - - {output.media?.session?.artist} + Media: {output.media?.currentSession?.title} - + {output.media?.currentSession?.artist} +
CPU usage: {output.cpu?.usage}
diff --git a/examples/boilerplate-solid-ts/src/index.tsx b/examples/boilerplate-solid-ts/src/index.tsx index 1a6ce453..03c0aa35 100644 --- a/examples/boilerplate-solid-ts/src/index.tsx +++ b/examples/boilerplate-solid-ts/src/index.tsx @@ -27,8 +27,9 @@ function App() { {output.audio?.defaultPlaybackDevice?.volume}
- Media: {output.media?.session?.title} - - {output.media?.session?.artist} + Media: {output.media?.currentSession?.title} - + {output.media?.currentSession?.artist} +
CPU usage: {output.cpu?.usage}
diff --git a/packages/client-api/src/desktop/desktop-commands.ts b/packages/client-api/src/desktop/desktop-commands.ts index 4df3de77..14474228 100644 --- a/packages/client-api/src/desktop/desktop-commands.ts +++ b/packages/client-api/src/desktop/desktop-commands.ts @@ -14,10 +14,25 @@ export const desktopCommands = { startPreset, listenProvider, unlistenProvider, + callProviderFunction, setAlwaysOnTop, setSkipTaskbar, }; +export type ProviderFunction = MediaFunction; + +export interface MediaFunction { + type: 'media'; + function: { + name: 'play' | 'pause' | 'toggle_play_pause' | 'next' | 'previous'; + args: MediaControlArgs; + }; +} + +export interface MediaControlArgs { + sessionId?: string; +} + function startWidget( configPath: string, placement: WidgetPlacement, @@ -43,6 +58,16 @@ function unlistenProvider(configHash: string): Promise { return invoke('unlisten_provider', { configHash }); } +function callProviderFunction( + configHash: string, + fn: ProviderFunction, +): Promise { + return invoke('call_provider_function', { + configHash, + function: fn, + }); +} + function setAlwaysOnTop(): Promise { return invoke('set_always_on_top'); } diff --git a/packages/client-api/src/providers/media/create-media-provider.ts b/packages/client-api/src/providers/media/create-media-provider.ts index 0398bbad..eb95f65d 100644 --- a/packages/client-api/src/providers/media/create-media-provider.ts +++ b/packages/client-api/src/providers/media/create-media-provider.ts @@ -1,7 +1,8 @@ import { z } from 'zod'; import { createBaseProvider } from '../create-base-provider'; -import { onProviderEmit } from '~/desktop'; +import { desktopCommands, onProviderEmit } from '~/desktop'; import type { + MediaControlArgs, MediaOutput, MediaProvider, MediaProviderConfig, @@ -17,12 +18,63 @@ export function createMediaProvider( const mergedConfig = mediaProviderConfigSchema.parse(config); return createBaseProvider(mergedConfig, async queue => { - return onProviderEmit(mergedConfig, ({ result }) => { - if ('error' in result) { - queue.error(result.error); - } else { - queue.output(result.output); - } - }); + return onProviderEmit( + mergedConfig, + ({ result, configHash }) => { + if ('error' in result) { + queue.error(result.error); + } else { + queue.output({ + ...result.output, + session: result.output.currentSession, + play: (args?: MediaControlArgs) => { + desktopCommands.callProviderFunction(configHash, { + type: 'media', + function: { + name: 'play', + args: args ?? {}, + }, + }); + }, + pause: (args?: MediaControlArgs) => { + desktopCommands.callProviderFunction(configHash, { + type: 'media', + function: { + name: 'pause', + args: args ?? {}, + }, + }); + }, + togglePlayPause: (args?: MediaControlArgs) => { + desktopCommands.callProviderFunction(configHash, { + type: 'media', + function: { + name: 'toggle_play_pause', + args: args ?? {}, + }, + }); + }, + next: (args?: MediaControlArgs) => { + desktopCommands.callProviderFunction(configHash, { + type: 'media', + function: { + name: 'next', + args: args ?? {}, + }, + }); + }, + previous: (args?: MediaControlArgs) => { + desktopCommands.callProviderFunction(configHash, { + type: 'media', + function: { + name: 'previous', + args: args ?? {}, + }, + }); + }, + }); + } + }, + ); }); } diff --git a/packages/client-api/src/providers/media/media-provider-types.ts b/packages/client-api/src/providers/media/media-provider-types.ts index b6bb1695..b1973cfd 100644 --- a/packages/client-api/src/providers/media/media-provider-types.ts +++ b/packages/client-api/src/providers/media/media-provider-types.ts @@ -5,11 +5,24 @@ export interface MediaProviderConfig { } export interface MediaOutput { + /** @deprecated Use {@link currentSession} instead */ session: MediaSession | null; + currentSession: MediaSession | null; + allSessions: MediaSession[]; + play(args?: MediaControlArgs): void; + pause(args?: MediaControlArgs): void; + togglePlayPause(args?: MediaControlArgs): void; + next(args?: MediaControlArgs): void; + previous(args?: MediaControlArgs): void; +} + +export interface MediaControlArgs { + sessionId?: string; } export interface MediaSession { - title: string; + sessionId: string; + title: string | null; artist: string | null; albumTitle: string | null; albumArtist: string | null; @@ -18,6 +31,7 @@ export interface MediaSession { endTime: number; position: number; isPlaying: boolean; + isCurrentSession: boolean; } export type MediaProvider = Provider; diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 086fe7ad..01e0197d 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -43,9 +43,10 @@ komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.28" windows-core = "0.58" windows = { version = "0.58", features = [ "Foundation", + "Foundation_Collections", "implement", - "Win32_Devices_FunctionDiscovery", "Media_Control", + "Win32_Devices_FunctionDiscovery", "Win32_Globalization", "Win32_Media", "Win32_Media_Audio", diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index 008858ed..707dbfea 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -1,11 +1,9 @@ -use std::{ - sync::{Arc, Mutex}, - time, -}; +use std::collections::{HashMap, HashSet}; -use async_trait::async_trait; +use anyhow::Context; +use crossbeam::channel::{unbounded, Receiver, Sender}; use serde::{Deserialize, Serialize}; -use tracing::{debug, error}; +use tracing::{debug, warn}; use windows::{ Foundation::{EventRegistrationToken, TypedEventHandler}, Media::Control::{ @@ -16,7 +14,8 @@ use windows::{ }; use crate::providers::{ - CommonProviderState, Provider, ProviderEmitter, RuntimeType, + CommonProviderState, MediaFunction, Provider, ProviderFunction, + ProviderFunctionResponse, ProviderInputMsg, RuntimeType, }; #[derive(Deserialize, Debug)] @@ -26,13 +25,15 @@ pub struct MediaProviderConfig {} #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MediaOutput { - pub session: Option, + pub current_session: Option, + pub all_sessions: Vec, } #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MediaSession { - pub title: String, + pub session_id: String, + pub title: Option, pub artist: Option, pub album_title: Option, pub album_artist: Option, @@ -41,17 +42,61 @@ pub struct MediaSession { pub end_time: u64, pub position: u64, pub is_playing: bool, + pub is_current_session: bool, +} + +impl Default for MediaSession { + fn default() -> Self { + Self { + session_id: "".to_string(), + title: None, + artist: None, + album_title: None, + album_artist: None, + track_number: 0, + start_time: 0, + end_time: 0, + position: 0, + is_playing: false, + is_current_session: false, + } + } +} + +/// Events that can be emitted from media session state changes. +#[derive(Debug)] +enum MediaSessionEvent { + SessionAddOrRemove, + CurrentSessionChanged, + PlaybackInfoChanged(String), + MediaPropertiesChanged(String), + TimelinePropertiesChanged(String), } -#[derive(Clone, Debug)] +/// Holds event registration tokens for media session callbacks. +/// +/// These need to be cleaned up when the session changes. +#[derive(Debug)] struct EventTokens { - playback_info_changed_token: EventRegistrationToken, - media_properties_changed_token: EventRegistrationToken, - timeline_properties_changed_token: EventRegistrationToken, + playback: EventRegistrationToken, + properties: EventRegistrationToken, + timeline: EventRegistrationToken, +} + +/// Holds the state of a media session. +#[derive(Debug)] +struct SessionState { + session: GsmtcSession, + tokens: EventTokens, + output: MediaSession, } pub struct MediaProvider { common: CommonProviderState, + current_session_id: Option, + session_states: HashMap, + event_sender: Sender, + event_receiver: Receiver, } impl MediaProvider { @@ -59,208 +104,425 @@ impl MediaProvider { _config: MediaProviderConfig, common: CommonProviderState, ) -> MediaProvider { - MediaProvider { common } + let (event_sender, event_receiver) = unbounded(); + + Self { + common, + current_session_id: None, + session_states: HashMap::new(), + event_sender, + event_receiver, + } } - fn emit_media_info( - session: Option<&GsmtcSession>, - emitter: &ProviderEmitter, - ) { - let media_output = Self::media_output(session); - emitter.emit_output(media_output); - } + /// Main entry point that sets up the media session manager and runs the + /// event loop. + fn create_session_manager(&mut self) -> anyhow::Result<()> { + debug!("Getting media session manager."); + let manager = GsmtcManager::RequestAsync()?.get()?; - fn media_output( - session: Option<&GsmtcSession>, - ) -> anyhow::Result { - Ok(MediaOutput { - session: match session { - Some(session) => Self::media_session(session)?, - None => None, - }, - }) + self.register_session_change_callbacks(&manager)?; + self.update_session_states(&manager)?; + self.update_current_session(&manager)?; + + // Emit initial output. + self.emit_output(); + + loop { + crossbeam::select! { + recv(self.event_receiver) -> event => { + if let Ok(event) = event { + debug!("Got media session event: {:?}", event); + + if let Err(err) = self.handle_event(event) { + warn!("Error handling media session event: {}", err); + } + } + } + recv(self.common.input.sync_rx) -> input => { + match input { + Ok(ProviderInputMsg::Stop) => { + break; + } + Ok(ProviderInputMsg::Function( + ProviderFunction::Media(media_function), + sender, + )) => { + let res = self.handle_function(media_function).map_err(|err| err.to_string()); + sender.send(res).unwrap(); + } + _ => {} + } + } + } + } + + Ok(()) } - fn media_session( - session: &GsmtcSession, - ) -> anyhow::Result> { - let media_properties = session.TryGetMediaPropertiesAsync()?.get()?; - let timeline_properties = session.GetTimelineProperties()?; - let playback_info = session.GetPlaybackInfo()?; - - let title = media_properties.Title()?.to_string(); - let artist = media_properties.Artist()?.to_string(); - let album_title = media_properties.AlbumTitle()?.to_string(); - let album_artist = media_properties.AlbumArtist()?.to_string(); - - // GSMTC can have a valid session, but return empty string for all - // media properties. Check that we at least have a valid title. - if title.is_empty() { - return Ok(None); + /// Handles a media session event. + fn handle_event( + &mut self, + event: MediaSessionEvent, + ) -> anyhow::Result<()> { + match event { + MediaSessionEvent::CurrentSessionChanged => { + let manager = GsmtcManager::RequestAsync()?.get()?; + self.update_current_session(&manager)?; + } + MediaSessionEvent::SessionAddOrRemove => { + let manager = GsmtcManager::RequestAsync()?.get()?; + self.update_session_states(&manager)?; + } + MediaSessionEvent::PlaybackInfoChanged(id) => { + if let Some(session_state) = self.session_states.get_mut(&id) { + Self::update_playback_info( + &mut session_state.output, + &session_state.session, + )?; + } + } + MediaSessionEvent::MediaPropertiesChanged(id) => { + if let Some(session_state) = self.session_states.get_mut(&id) { + Self::update_media_properties( + &mut session_state.output, + &session_state.session, + )?; + } + } + MediaSessionEvent::TimelinePropertiesChanged(id) => { + if let Some(session_state) = self.session_states.get_mut(&id) { + Self::update_timeline_properties( + &mut session_state.output, + &session_state.session, + )?; + } + } } - let is_playing = - playback_info.PlaybackStatus()? == GsmtcPlaybackStatus::Playing; - let start_time = - timeline_properties.StartTime()?.Duration as u64 / 10_000_000; - let end_time = - timeline_properties.EndTime()?.Duration as u64 / 10_000_000; - let position = - timeline_properties.Position()?.Duration as u64 / 10_000_000; - - Ok(Some(MediaSession { - title, - artist: (!artist.is_empty()).then_some(artist), - album_title: (!album_title.is_empty()).then_some(album_title), - album_artist: (!album_artist.is_empty()).then_some(album_artist), - track_number: media_properties.TrackNumber()? as u32, - start_time, - end_time, - position, - is_playing, - })) + // Emit new output after handling the event. + self.emit_output(); + + Ok(()) } - fn create_session_manager(&mut self) -> anyhow::Result<()> { - debug!("Creating media session manager."); + /// Handles an incoming media provider function call. + fn handle_function( + &mut self, + function: MediaFunction, + ) -> anyhow::Result { + let args = match &function { + MediaFunction::Play(args) + | MediaFunction::Pause(args) + | MediaFunction::TogglePlayPause(args) + | MediaFunction::Next(args) + | MediaFunction::Previous(args) => args, + }; - // Find the current GSMTC session & add listeners. - let session_manager = GsmtcManager::RequestAsync()?.get()?; - let current_session = session_manager.GetCurrentSession().ok(); + // Get target session - use specified ID or current session. + let session_state = if let Some(id) = &args.session_id { + self + .session_states + .get(id) + .context("Specified session not found.")? + } else { + self + .current_session_id + .as_ref() + .and_then(|id| self.session_states.get(id)) + .context("No active session.")? + }; - let event_tokens = match ¤t_session { - Some(session) => { - Some(Self::add_session_listeners(session, &self.common.emitter)?) + match function { + MediaFunction::Play(_) => { + session_state.session.TryPlayAsync()?.get()?; + } + MediaFunction::Pause(_) => { + session_state.session.TryPauseAsync()?.get()?; + } + MediaFunction::TogglePlayPause(_) => { + session_state.session.TryTogglePlayPauseAsync()?.get()?; + } + MediaFunction::Next(_) => { + session_state.session.TrySkipNextAsync()?.get()?; + } + MediaFunction::Previous(_) => { + session_state.session.TrySkipPreviousAsync()?.get()?; } - None => None, }; - // Emit initial media info. - Self::emit_media_info(current_session.as_ref(), &self.common.emitter); - - let current_session = Arc::new(Mutex::new(current_session)); - let event_tokens = Arc::new(Mutex::new(event_tokens)); - let emitter = self.common.emitter.clone(); - - // Clean up & rebind listeners when session changes. - let session_changed_handler = - TypedEventHandler::new(move |_: &Option, _| { - { - let mut current_session = current_session.lock().unwrap(); - let mut event_tokens = event_tokens.lock().unwrap(); - - // Remove listeners from the previous session. - if let (Some(session), Some(token)) = - (current_session.as_ref(), event_tokens.as_ref()) - { - if let Err(err) = - Self::remove_session_listeners(session, token) - { - error!("Failed to remove session listeners: {}", err); - } - } + Ok(ProviderFunctionResponse::Null) + } - // Set up new session. - let new_session = - GsmtcManager::RequestAsync()?.get()?.GetCurrentSession()?; + /// Registers event callbacks with the session manager. + /// + /// - `CurrentSessionChanged`: for when the active media session changes + /// (e.g. when switching between media players). + /// - `SessionAddOrRemove`: for when the list of available media sessions + /// changes (e.g. when a media player is opened or closed). + fn register_session_change_callbacks( + &self, + manager: &GsmtcManager, + ) -> anyhow::Result<()> { + // Handler for current session changes. + manager.CurrentSessionChanged(&TypedEventHandler::new({ + let sender = self.event_sender.clone(); + move |_, _| { + sender + .send(MediaSessionEvent::CurrentSessionChanged) + .unwrap(); + Ok(()) + } + }))?; - let tokens = - Self::add_session_listeners(&new_session, &emitter)?; + // Handler for a session is added or removed. + manager.SessionsChanged(&TypedEventHandler::new({ + let sender = self.event_sender.clone(); + move |_, _| { + sender.send(MediaSessionEvent::SessionAddOrRemove).unwrap(); + Ok(()) + } + }))?; - Self::emit_media_info(Some(&new_session), &emitter); + Ok(()) + } - *current_session = Some(new_session); - *event_tokens = Some(tokens); - } + /// Updates the state of all media sessions. + fn update_session_states( + &mut self, + manager: &GsmtcManager, + ) -> anyhow::Result<()> { + let sessions = manager.GetSessions()?; + let mut found_ids: HashSet = HashSet::new(); - Ok(()) - }); + // Handle new sessions and track existing sessions to detect removals. + for session in sessions { + let session_id = session.SourceAppUserModelId()?.to_string(); + found_ids.insert(session_id.clone()); - session_manager.CurrentSessionChanged(&session_changed_handler)?; + if !self.session_states.contains_key(&session_id) { + debug!("New media session detected: {}", session_id); - loop { - std::thread::sleep(time::Duration::from_secs(1)); + let session_state = SessionState { + tokens: self + .register_session_callbacks(&session, &session_id)?, + output: Self::to_media_session_output(&session, &session_id)?, + session, + }; + + self.session_states.insert(session_id, session_state); + } } + + let removed_ids = self + .session_states + .keys() + .filter(|id| !found_ids.contains(*id)) + .cloned() + .collect::>(); + + // Remove sessions that no longer exist. + for session_id in &removed_ids { + if let Some(session_state) = self.session_states.remove(session_id) { + debug!("Media session ended: {}", session_id); + Self::remove_session_listeners( + &session_state.session, + &session_state.tokens, + ); + } + } + + Ok(()) } - fn remove_session_listeners( - session: &GsmtcSession, - tokens: &EventTokens, + /// Updates the current media session ID and marks the correct session as + /// the current one. + fn update_current_session( + &mut self, + manager: &GsmtcManager, ) -> anyhow::Result<()> { - session.RemoveMediaPropertiesChanged( - tokens.media_properties_changed_token, - )?; + self.current_session_id = manager + .GetCurrentSession() + .ok() + .and_then(|session| session.SourceAppUserModelId().ok()) + .map(|session_id| session_id.to_string()); + + for (session_id, state) in self.session_states.iter_mut() { + state.output.is_current_session = + Some(session_id) == self.current_session_id.as_ref(); + } - session - .RemovePlaybackInfoChanged(tokens.playback_info_changed_token)?; + Ok(()) + } - session.RemoveTimelinePropertiesChanged( - tokens.timeline_properties_changed_token, - )?; + /// Registers event callbacks for media session state changes. + /// + /// Returns tokens needed for cleanup when the session ends. + fn register_session_callbacks( + &self, + session: &GsmtcSession, + session_id: &str, + ) -> anyhow::Result { + Ok(EventTokens { + playback: session.PlaybackInfoChanged(&TypedEventHandler::new({ + let sender = self.event_sender.clone(); + let session_id = session_id.to_string(); + move |_, _| { + sender + .send(MediaSessionEvent::PlaybackInfoChanged( + session_id.clone(), + )) + .unwrap(); + + Ok(()) + } + }))?, + properties: session.MediaPropertiesChanged( + &TypedEventHandler::new({ + let sender = self.event_sender.clone(); + let session_id = session_id.to_string(); + move |_, _| { + sender + .send(MediaSessionEvent::MediaPropertiesChanged( + session_id.clone(), + )) + .unwrap(); + + Ok(()) + } + }), + )?, + timeline: session.TimelinePropertiesChanged( + &TypedEventHandler::new({ + let sender = self.event_sender.clone(); + let session_id = session_id.to_string(); + move |_, _| { + sender + .send(MediaSessionEvent::TimelinePropertiesChanged( + session_id.clone(), + )) + .unwrap(); + + Ok(()) + } + }), + )?, + }) + } - Ok(()) + /// Cleans up event listeners from the given session. + fn remove_session_listeners( + session: &GsmtcSession, + tokens: &EventTokens, + ) { + let _ = session.RemovePlaybackInfoChanged(tokens.playback); + let _ = session.RemoveMediaPropertiesChanged(tokens.properties); + let _ = session.RemoveTimelinePropertiesChanged(tokens.timeline); } - fn add_session_listeners( + /// Emits a `MediaOutput` update through the provider's emitter. + /// + /// Note that at times, GSMTC can have a valid session, but return empty + /// string for all media properties. + fn emit_output(&self) { + let current_session = self + .current_session_id + .as_ref() + .and_then(|id| self.session_states.get(id)) + .map(|state| state.output.clone()); + + let all_sessions = self + .session_states + .values() + .map(|state| state.output.clone()) + .collect(); + + self.common.emitter.emit_output(Ok(MediaOutput { + current_session, + all_sessions, + })); + } + + /// Creates a `MediaSession` from a Windows media session. + fn to_media_session_output( session: &GsmtcSession, - emitter: &ProviderEmitter, - ) -> windows::core::Result { - debug!("Adding session listeners."); + session_id: &str, + ) -> anyhow::Result { + let mut session_output = MediaSession::default(); - let media_properties_changed_handler = { - let emitter = emitter.clone(); + session_output.session_id = session_id.to_string(); + Self::update_media_properties(&mut session_output, &session)?; + Self::update_timeline_properties(&mut session_output, &session)?; + Self::update_playback_info(&mut session_output, &session)?; - TypedEventHandler::new(move |session: &Option, _| { - debug!("Media properties changed event triggered."); + Ok(session_output) + } - if let Some(session) = session { - Self::emit_media_info(Some(session), &emitter); - } + /// Updates media metadata properties in a `MediaSession`. + fn update_media_properties( + session_output: &mut MediaSession, + session: &GsmtcSession, + ) -> anyhow::Result<()> { + let properties = session.TryGetMediaPropertiesAsync()?.get()?; - Ok(()) - }) - }; + let title = properties.Title()?.to_string(); + let artist = properties.Artist()?.to_string(); + let album_title = properties.AlbumTitle()?.to_string(); + let album_artist = properties.AlbumArtist()?.to_string(); - let playback_info_changed_handler = { - let emitter = emitter.clone(); + session_output.title = (!title.is_empty()).then_some(title); + session_output.artist = (!artist.is_empty()).then_some(artist); + session_output.album_title = + (!album_title.is_empty()).then_some(album_title); + session_output.album_artist = + (!album_artist.is_empty()).then_some(album_artist); + session_output.track_number = properties.TrackNumber()? as u32; - TypedEventHandler::new(move |session: &Option, _| { - debug!("Playback info changed event triggered."); + Ok(()) + } - if let Some(session) = session { - Self::emit_media_info(Some(session), &emitter); - } + /// Updates timeline properties (position/duration) in a `MediaSession`. + fn update_timeline_properties( + session_output: &mut MediaSession, + session: &GsmtcSession, + ) -> anyhow::Result<()> { + let properties = session.GetTimelineProperties()?; - Ok(()) - }) - }; + session_output.start_time = + properties.StartTime()?.Duration as u64 / 10_000_000; + session_output.end_time = + properties.EndTime()?.Duration as u64 / 10_000_000; + session_output.position = + properties.Position()?.Duration as u64 / 10_000_000; - let timeline_properties_changed_handler = { - let emitter = emitter.clone(); + Ok(()) + } - TypedEventHandler::new(move |session: &Option, _| { - debug!("Timeline properties changed event triggered."); + /// Updates playback info in a `MediaSession`. + fn update_playback_info( + session_output: &mut MediaSession, + session: &GsmtcSession, + ) -> anyhow::Result<()> { + let info = session.GetPlaybackInfo()?; - if let Some(session) = session { - Self::emit_media_info(Some(session), &emitter); - } + session_output.is_playing = + info.PlaybackStatus()? == GsmtcPlaybackStatus::Playing; - Ok(()) - }) - }; + Ok(()) + } +} - Ok(EventTokens { - playback_info_changed_token: session - .PlaybackInfoChanged(&playback_info_changed_handler)?, - media_properties_changed_token: session - .MediaPropertiesChanged(&media_properties_changed_handler)?, - timeline_properties_changed_token: session - .TimelinePropertiesChanged(&timeline_properties_changed_handler)?, - }) +impl Drop for MediaProvider { + fn drop(&mut self) { + for (_, session_state) in &self.session_states { + Self::remove_session_listeners( + &session_state.session, + &session_state.tokens, + ); + } } } -#[async_trait] impl Provider for MediaProvider { fn runtime_type(&self) -> RuntimeType { RuntimeType::Sync diff --git a/packages/desktop/src/providers/provider_function.rs b/packages/desktop/src/providers/provider_function.rs index 8a6378c8..f95f88ad 100644 --- a/packages/desktop/src/providers/provider_function.rs +++ b/packages/desktop/src/providers/provider_function.rs @@ -1,15 +1,24 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "function", rename_all = "snake_case")] pub enum ProviderFunction { Media(MediaFunction), } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "name", content = "args", rename_all = "snake_case")] pub enum MediaFunction { - PlayPause, - Next, - Previous, + Play(MediaControlArgs), + Pause(MediaControlArgs), + TogglePlayPause(MediaControlArgs), + Next(MediaControlArgs), + Previous(MediaControlArgs), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MediaControlArgs { + pub session_id: Option, } pub type ProviderFunctionResult = Result; diff --git a/packages/desktop/src/providers/provider_manager.rs b/packages/desktop/src/providers/provider_manager.rs index 67aa541d..e742e007 100644 --- a/packages/desktop/src/providers/provider_manager.rs +++ b/packages/desktop/src/providers/provider_manager.rs @@ -286,6 +286,11 @@ impl ProviderManager { config_hash: String, function: ProviderFunction, ) -> anyhow::Result { + info!( + "Calling provider function: {:?} for: {}", + function, config_hash + ); + let provider_refs = self.provider_refs.lock().await; let provider_ref = provider_refs .get(&config_hash)