From f7c2942ec69759f388b60b5cde6ce8f2976a0f9c Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 21 Nov 2024 01:29:09 +0800 Subject: [PATCH 01/17] wip refactor --- .../src/providers/media/media_provider.rs | 430 +++++++++++------- 1 file changed, 257 insertions(+), 173 deletions(-) diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index 008858ed..b2754480 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -1,23 +1,19 @@ -use std::{ - sync::{Arc, Mutex}, - time, -}; - use async_trait::async_trait; +use crossbeam::channel::{unbounded, Receiver, Sender}; use serde::{Deserialize, Serialize}; -use tracing::{debug, error}; +use tracing::debug; use windows::{ Foundation::{EventRegistrationToken, TypedEventHandler}, Media::Control::{ GlobalSystemMediaTransportControlsSession as GsmtcSession, GlobalSystemMediaTransportControlsSessionManager as GsmtcManager, + GlobalSystemMediaTransportControlsSessionMediaProperties as GsmtcMediaProperties, GlobalSystemMediaTransportControlsSessionPlaybackStatus as GsmtcPlaybackStatus, + GlobalSystemMediaTransportControlsSessionTimelineProperties as GsmtcTimelineProperties, }, }; -use crate::providers::{ - CommonProviderState, Provider, ProviderEmitter, RuntimeType, -}; +use crate::providers::{CommonProviderState, Provider, RuntimeType}; #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -43,15 +39,31 @@ pub struct MediaSession { pub is_playing: bool, } -#[derive(Clone, Debug)] +/// Events that can be emitted from media session state changes. +#[derive(Debug)] +enum MediaSessionEvent { + PlaybackInfoChanged, + MediaPropertiesChanged, + TimelinePropertiesChanged, + SessionChanged, +} + +/// Holds event registration tokens for media session callbacks. +/// +/// These need to be cleaned up when the session changes. +#[derive(Clone)] struct EventTokens { - playback_info_changed_token: EventRegistrationToken, - media_properties_changed_token: EventRegistrationToken, - timeline_properties_changed_token: EventRegistrationToken, + playback: EventRegistrationToken, + properties: EventRegistrationToken, + timeline: EventRegistrationToken, } pub struct MediaProvider { common: CommonProviderState, + session: Option, + event_tokens: Option, + event_sender: Sender, + event_receiver: Receiver, } impl MediaProvider { @@ -59,204 +71,276 @@ impl MediaProvider { _config: MediaProviderConfig, common: CommonProviderState, ) -> MediaProvider { - MediaProvider { common } - } - - fn emit_media_info( - session: Option<&GsmtcSession>, - emitter: &ProviderEmitter, - ) { - let media_output = Self::media_output(session); - emitter.emit_output(media_output); - } - - fn media_output( - session: Option<&GsmtcSession>, - ) -> anyhow::Result { - Ok(MediaOutput { - session: match session { - Some(session) => Self::media_session(session)?, - None => None, - }, - }) - } - - 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); + let (event_sender, event_receiver) = unbounded(); + + Self { + common, + session: None, + event_tokens: None, + event_sender, + event_receiver, } - - 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, - })) } + /// Main entry point that sets up the media session manager and runs the + /// event loop. This method: + /// 1. Creates the Windows media session manager + /// 2. Registers for session change notifications + /// 3. Sets up the initial session if one exists + /// 4. Runs the main event loop to handle media state changes fn create_session_manager(&mut self) -> anyhow::Result<()> { debug!("Creating media session manager."); + let manager = GsmtcManager::RequestAsync()?.get()?; + self.register_session_changed_handler(&manager)?; + self.create_session()?; - // Find the current GSMTC session & add listeners. - let session_manager = GsmtcManager::RequestAsync()?.get()?; - let current_session = session_manager.GetCurrentSession().ok(); + while let Ok(event) = self.event_receiver.recv() { + debug!("Got media session event: {:?}", event); + self.handle_event(event)?; + } + + Ok(()) + } - let event_tokens = match ¤t_session { - Some(session) => { - Some(Self::add_session_listeners(session, &self.common.emitter)?) + /// Registers a callback with the session manager to detect when the + /// active media session changes (e.g. when switching between media + /// players). + fn register_session_changed_handler( + &self, + manager: &GsmtcManager, + ) -> anyhow::Result<()> { + let handler = TypedEventHandler::new({ + let sender = self.event_sender.clone(); + move |_, _| { + sender.send(MediaSessionEvent::SessionChanged).unwrap(); + Ok(()) } - None => None, - }; + }); + manager.CurrentSessionChanged(&handler)?; + Ok(()) + } - // 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); + /// Central event handler that processes all media session events. + /// Routes events to appropriate update methods based on event type. + fn handle_event( + &mut self, + event: MediaSessionEvent, + ) -> anyhow::Result<()> { + match event { + MediaSessionEvent::SessionChanged => self.create_session()?, + _ => { + if let Some(session) = &self.session { + match event { + MediaSessionEvent::PlaybackInfoChanged => { + self.update_playback(session)? } + MediaSessionEvent::MediaPropertiesChanged => { + self.update_properties(session)? + } + MediaSessionEvent::TimelinePropertiesChanged => { + self.update_timeline(session)? + } + _ => {} } + } + } + } + Ok(()) + } - // Set up new session. - let new_session = - GsmtcManager::RequestAsync()?.get()?.GetCurrentSession()?; - - let tokens = - Self::add_session_listeners(&new_session, &emitter)?; - - Self::emit_media_info(Some(&new_session), &emitter); + /// Sets up a new media session when one becomes available. + /// This includes: + /// 1. Cleaning up any existing session listeners + /// 2. Getting the current session from Windows + /// 3. Setting up new event listeners + /// 4. Emitting initial state + fn create_session(&mut self) -> anyhow::Result<()> { + self.remove_session_listeners(); + let manager = GsmtcManager::RequestAsync()?.get()?; + let session = manager.GetCurrentSession().ok(); + + if let Some(session) = &session { + self.event_tokens = Some(self.setup_session_listeners(session)?); + self.emit_full_state(session)?; + } else { + self.emit_empty_state(); + } - *current_session = Some(new_session); - *event_tokens = Some(tokens); - } + self.session = session; + Ok(()) + } + /// Creates event listeners for all media session state changes. + /// Returns tokens needed for cleanup when the session ends. + fn setup_session_listeners( + &self, + session: &GsmtcSession, + ) -> anyhow::Result { + let create_handler = |event| { + let sender = self.event_sender.clone(); + TypedEventHandler::new(move |_, _| { + sender.send(event).unwrap(); Ok(()) - }); + }) + }; - session_manager.CurrentSessionChanged(&session_changed_handler)?; + Ok(EventTokens { + playback: session.PlaybackInfoChanged(&create_handler( + MediaSessionEvent::PlaybackInfoChanged, + ))?, + properties: session.MediaPropertiesChanged(&create_handler( + MediaSessionEvent::MediaPropertiesChanged, + ))?, + timeline: session.TimelinePropertiesChanged(&create_handler( + MediaSessionEvent::TimelinePropertiesChanged, + ))?, + }) + } - loop { - std::thread::sleep(time::Duration::from_secs(1)); + /// Cleans up event listeners from the current session. + fn remove_session_listeners(&mut self) { + if let (Some(session), Some(tokens)) = + (&self.session, &self.event_tokens) + { + let _ = session.RemovePlaybackInfoChanged(tokens.playback); + let _ = session.RemoveMediaPropertiesChanged(tokens.properties); + let _ = session.RemoveTimelinePropertiesChanged(tokens.timeline); } + self.event_tokens = None; } - fn remove_session_listeners( - session: &GsmtcSession, - tokens: &EventTokens, - ) -> anyhow::Result<()> { - session.RemoveMediaPropertiesChanged( - tokens.media_properties_changed_token, - )?; - - session - .RemovePlaybackInfoChanged(tokens.playback_info_changed_token)?; + /// Emits an empty state when no media session is active. + fn emit_empty_state(&self) { + self + .common + .emitter + .emit_output(Ok(MediaOutput { session: None })); + } - session.RemoveTimelinePropertiesChanged( - tokens.timeline_properties_changed_token, - )?; + /// Emits a complete state update for all media session properties. + fn emit_full_state(&self, session: &GsmtcSession) -> anyhow::Result<()> { + let output = Self::create_media_output(session)?; + self.common.emitter.emit_output(Ok(output)); + Ok(()) + } + /// Updates and emits only playback state changes (playing/paused). + fn update_playback(&self, session: &GsmtcSession) -> anyhow::Result<()> { + if let Some(mut media_session) = Self::create_media_session(session)? { + let info = session.GetPlaybackInfo()?; + media_session.is_playing = + info.PlaybackStatus()? == GsmtcPlaybackStatus::Playing; + self.emit_session(media_session); + } Ok(()) } - fn add_session_listeners( + /// Updates and emits only media property changes (title, artist, etc). + fn update_properties( + &self, session: &GsmtcSession, - emitter: &ProviderEmitter, - ) -> windows::core::Result { - debug!("Adding session listeners."); - - let media_properties_changed_handler = { - let emitter = emitter.clone(); + ) -> anyhow::Result<()> { + if let Some(mut media_session) = Self::create_media_session(session)? { + let props = session.TryGetMediaPropertiesAsync()?.get()?; + Self::update_media_properties(&mut media_session, &props)?; + self.emit_session(media_session); + } + Ok(()) + } - TypedEventHandler::new(move |session: &Option, _| { - debug!("Media properties changed event triggered."); + /// Updates and emits only timeline property changes (position/duration). + fn update_timeline(&self, session: &GsmtcSession) -> anyhow::Result<()> { + if let Some(mut media_session) = Self::create_media_session(session)? { + let props = session.GetTimelineProperties()?; + Self::update_timeline_properties(&mut media_session, &props)?; + self.emit_session(media_session); + } + Ok(()) + } - if let Some(session) = session { - Self::emit_media_info(Some(session), &emitter); - } + /// Helper to emit a media session update through the provider's emitter. + fn emit_session(&self, session: MediaSession) { + self.common.emitter.emit_output(Ok(MediaOutput { + session: Some(session), + })); + } - Ok(()) - }) - }; + /// Creates a complete MediaOutput struct from a Windows media session. + fn create_media_output( + session: &GsmtcSession, + ) -> anyhow::Result { + Ok(MediaOutput { + session: Self::create_media_session(session)?, + }) + } - let playback_info_changed_handler = { - let emitter = emitter.clone(); + /// Creates our MediaSession struct from a Windows media session. + /// Returns None if the session has no title (indicating invalid/empty + /// state). + fn create_media_session( + session: &GsmtcSession, + ) -> anyhow::Result> { + let props = session.TryGetMediaPropertiesAsync()?.get()?; + let title = props.Title()?.to_string(); + if title.is_empty() { + return Ok(None); + } - TypedEventHandler::new(move |session: &Option, _| { - debug!("Playback info changed event triggered."); + // Create base session with title and default values + let mut media_session = MediaSession { + title, + artist: None, + album_title: None, + album_artist: None, + track_number: 0, + start_time: 0, + end_time: 0, + position: 0, + is_playing: false, + }; - if let Some(session) = session { - Self::emit_media_info(Some(session), &emitter); - } + // Update with current state + Self::update_media_properties(&mut media_session, &props)?; + Self::update_timeline_properties( + &mut media_session, + &session.GetTimelineProperties()?, + )?; - Ok(()) - }) - }; + let info = session.GetPlaybackInfo()?; + media_session.is_playing = + info.PlaybackStatus()? == GsmtcPlaybackStatus::Playing; - let timeline_properties_changed_handler = { - let emitter = emitter.clone(); + Ok(Some(media_session)) + } - TypedEventHandler::new(move |session: &Option, _| { - debug!("Timeline properties changed event triggered."); + /// Updates media metadata properties in a MediaSession struct. + fn update_media_properties( + session: &mut MediaSession, + props: &GsmtcMediaProperties, + ) -> anyhow::Result<()> { + let artist = props.Artist()?.to_string(); + let album_title = props.AlbumTitle()?.to_string(); + let album_artist = props.AlbumArtist()?.to_string(); - if let Some(session) = session { - Self::emit_media_info(Some(session), &emitter); - } + session.artist = (!artist.is_empty()).then_some(artist); + session.album_title = (!album_title.is_empty()).then_some(album_title); + session.album_artist = + (!album_artist.is_empty()).then_some(album_artist); + session.track_number = props.TrackNumber()? as u32; - 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)?, - }) + /// Updates timeline properties (position/duration) in a MediaSession + /// struct. + fn update_timeline_properties( + session: &mut MediaSession, + props: &GsmtcTimelineProperties, + ) -> anyhow::Result<()> { + session.start_time = props.StartTime()?.Duration as u64 / 10_000_000; + session.end_time = props.EndTime()?.Duration as u64 / 10_000_000; + session.position = props.Position()?.Duration as u64 / 10_000_000; + Ok(()) } } From 001c49c42c0667b20d805a64806ea2225ea216be Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 21 Nov 2024 15:32:04 +0800 Subject: [PATCH 02/17] fix: type fix; fn renaming --- .../src/providers/media/media_provider.rs | 68 ++++++++++++------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index b2754480..f806aef2 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use crossbeam::channel::{unbounded, Receiver, Sender}; use serde::{Deserialize, Serialize}; use tracing::debug; @@ -176,24 +175,36 @@ impl MediaProvider { &self, session: &GsmtcSession, ) -> anyhow::Result { - let create_handler = |event| { - let sender = self.event_sender.clone(); - TypedEventHandler::new(move |_, _| { - sender.send(event).unwrap(); - Ok(()) - }) - }; - Ok(EventTokens { - playback: session.PlaybackInfoChanged(&create_handler( - MediaSessionEvent::PlaybackInfoChanged, - ))?, - properties: session.MediaPropertiesChanged(&create_handler( - MediaSessionEvent::MediaPropertiesChanged, - ))?, - timeline: session.TimelinePropertiesChanged(&create_handler( - MediaSessionEvent::TimelinePropertiesChanged, - ))?, + playback: session.PlaybackInfoChanged(&TypedEventHandler::new({ + let sender = self.event_sender.clone(); + move |_, _| { + sender.send(MediaSessionEvent::PlaybackInfoChanged).unwrap(); + Ok(()) + } + }))?, + properties: session.MediaPropertiesChanged( + &TypedEventHandler::new({ + let sender = self.event_sender.clone(); + move |_, _| { + sender + .send(MediaSessionEvent::MediaPropertiesChanged) + .unwrap(); + Ok(()) + } + }), + )?, + timeline: session.TimelinePropertiesChanged( + &TypedEventHandler::new({ + let sender = self.event_sender.clone(); + move |_, _| { + sender + .send(MediaSessionEvent::TimelinePropertiesChanged) + .unwrap(); + Ok(()) + } + }), + )?, }) } @@ -219,14 +230,16 @@ impl MediaProvider { /// Emits a complete state update for all media session properties. fn emit_full_state(&self, session: &GsmtcSession) -> anyhow::Result<()> { - let output = Self::create_media_output(session)?; + let output = Self::to_media_output(session)?; self.common.emitter.emit_output(Ok(output)); Ok(()) } /// Updates and emits only playback state changes (playing/paused). fn update_playback(&self, session: &GsmtcSession) -> anyhow::Result<()> { - if let Some(mut media_session) = Self::create_media_session(session)? { + if let Some(mut media_session) = + Self::to_media_session_output(session)? + { let info = session.GetPlaybackInfo()?; media_session.is_playing = info.PlaybackStatus()? == GsmtcPlaybackStatus::Playing; @@ -240,7 +253,9 @@ impl MediaProvider { &self, session: &GsmtcSession, ) -> anyhow::Result<()> { - if let Some(mut media_session) = Self::create_media_session(session)? { + if let Some(mut media_session) = + Self::to_media_session_output(session)? + { let props = session.TryGetMediaPropertiesAsync()?.get()?; Self::update_media_properties(&mut media_session, &props)?; self.emit_session(media_session); @@ -250,7 +265,9 @@ impl MediaProvider { /// Updates and emits only timeline property changes (position/duration). fn update_timeline(&self, session: &GsmtcSession) -> anyhow::Result<()> { - if let Some(mut media_session) = Self::create_media_session(session)? { + if let Some(mut media_session) = + Self::to_media_session_output(session)? + { let props = session.GetTimelineProperties()?; Self::update_timeline_properties(&mut media_session, &props)?; self.emit_session(media_session); @@ -266,18 +283,18 @@ impl MediaProvider { } /// Creates a complete MediaOutput struct from a Windows media session. - fn create_media_output( + fn to_media_output( session: &GsmtcSession, ) -> anyhow::Result { Ok(MediaOutput { - session: Self::create_media_session(session)?, + session: Self::to_media_session_output(session)?, }) } /// Creates our MediaSession struct from a Windows media session. /// Returns None if the session has no title (indicating invalid/empty /// state). - fn create_media_session( + fn to_media_session_output( session: &GsmtcSession, ) -> anyhow::Result> { let props = session.TryGetMediaPropertiesAsync()?.get()?; @@ -344,7 +361,6 @@ impl MediaProvider { } } -#[async_trait] impl Provider for MediaProvider { fn runtime_type(&self) -> RuntimeType { RuntimeType::Sync From e641f1ab8fe4d3a1c083047e73710c98fbb2a8d0 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 21 Nov 2024 17:34:49 +0800 Subject: [PATCH 03/17] feat: add function support to media provider --- examples/boilerplate-solid-ts/src/index.tsx | 7 +++ .../src/desktop/desktop-commands.ts | 15 +++++ .../providers/media/create-media-provider.ts | 51 ++++++++++++++--- .../providers/media/media-provider-types.ts | 5 ++ .../src/providers/media/media_provider.rs | 56 +++++++++++++++++-- .../src/providers/provider_function.rs | 6 +- .../desktop/src/providers/provider_manager.rs | 5 ++ 7 files changed, 132 insertions(+), 13 deletions(-) diff --git a/examples/boilerplate-solid-ts/src/index.tsx b/examples/boilerplate-solid-ts/src/index.tsx index 1a6ce453..a893add2 100644 --- a/examples/boilerplate-solid-ts/src/index.tsx +++ b/examples/boilerplate-solid-ts/src/index.tsx @@ -29,6 +29,13 @@ function App() {
Media: {output.media?.session?.title} - {output.media?.session?.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..4b4628e3 100644 --- a/packages/client-api/src/desktop/desktop-commands.ts +++ b/packages/client-api/src/desktop/desktop-commands.ts @@ -14,10 +14,18 @@ export const desktopCommands = { startPreset, listenProvider, unlistenProvider, + callProviderFunction, setAlwaysOnTop, setSkipTaskbar, }; +export type ProviderFunction = MediaFunction; + +export interface MediaFunction { + type: 'media'; + function: 'play' | 'pause' | 'toggle_play_pause' | 'next' | 'previous'; +} + function startWidget( configPath: string, placement: WidgetPlacement, @@ -43,6 +51,13 @@ function unlistenProvider(configHash: string): Promise { return invoke('unlisten_provider', { configHash }); } +function callProviderFunction(args: { + configHash: string; + function: ProviderFunction; +}): Promise { + return invoke('call_provider_function', args); +} + 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..a2899a2f 100644 --- a/packages/client-api/src/providers/media/create-media-provider.ts +++ b/packages/client-api/src/providers/media/create-media-provider.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { createBaseProvider } from '../create-base-provider'; -import { onProviderEmit } from '~/desktop'; +import { desktopCommands, onProviderEmit } from '~/desktop'; import type { MediaOutput, MediaProvider, @@ -17,12 +17,47 @@ 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, + play: () => { + desktopCommands.callProviderFunction({ + configHash, + function: { type: 'media', function: 'play' }, + }); + }, + pause: () => { + desktopCommands.callProviderFunction({ + configHash, + function: { type: 'media', function: 'pause' }, + }); + }, + togglePlayPause: () => { + desktopCommands.callProviderFunction({ + configHash, + function: { type: 'media', function: 'toggle_play_pause' }, + }); + }, + next: () => { + desktopCommands.callProviderFunction({ + configHash, + function: { type: 'media', function: 'next' }, + }); + }, + previous: () => { + desktopCommands.callProviderFunction({ + configHash, + function: { type: 'media', function: 'previous' }, + }); + }, + }); + } + }, + ); }); } 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..7936ba58 100644 --- a/packages/client-api/src/providers/media/media-provider-types.ts +++ b/packages/client-api/src/providers/media/media-provider-types.ts @@ -6,6 +6,11 @@ export interface MediaProviderConfig { export interface MediaOutput { session: MediaSession | null; + play(): void; + pause(): void; + togglePlayPause(): void; + next(): void; + previous(): void; } export interface MediaSession { diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index f806aef2..edcb78f3 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -12,7 +12,11 @@ use windows::{ }, }; -use crate::providers::{CommonProviderState, Provider, RuntimeType}; +use crate::providers::{ + CommonProviderState, MediaFunction, Provider, ProviderFunction, + ProviderFunctionResponse, ProviderFunctionResult, ProviderInputMsg, + RuntimeType, +}; #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -93,9 +97,53 @@ impl MediaProvider { self.register_session_changed_handler(&manager)?; self.create_session()?; - while let Ok(event) = self.event_receiver.recv() { - debug!("Got media session event: {:?}", event); - self.handle_event(event)?; + loop { + crossbeam::select! { + recv(self.event_receiver) -> event => { + if let Ok(event) = event { + debug!("Got media session event: {:?}", event); + self.handle_event(event)?; + } + } + recv(self.common.input.sync_rx) -> input => { + match input { + Ok(ProviderInputMsg::Stop) => { + break; + } + Ok(ProviderInputMsg::Function( + ProviderFunction::Media(media_function), + sender, + )) => { + if let Some(session) = &self.session { + let result = match media_function { + MediaFunction::Play => { + session.TryPlayAsync()?.get()?; + Ok(ProviderFunctionResponse::Null) + } + MediaFunction::Pause => { + session.TryPauseAsync()?.get()?; + Ok(ProviderFunctionResponse::Null) + } + MediaFunction::TogglePlayPause => { + session.TryTogglePlayPauseAsync()?.get()?; + Ok(ProviderFunctionResponse::Null) + } + MediaFunction::Next => { + session.TrySkipNextAsync()?.get()?; + Ok(ProviderFunctionResponse::Null) + } + MediaFunction::Previous => { + session.TrySkipPreviousAsync()?.get()?; + Ok(ProviderFunctionResponse::Null) + } + }; + sender.send(result).unwrap(); + } + } + _ => {} + } + } + } } Ok(()) diff --git a/packages/desktop/src/providers/provider_function.rs b/packages/desktop/src/providers/provider_function.rs index 8a6378c8..3873a05a 100644 --- a/packages/desktop/src/providers/provider_function.rs +++ b/packages/desktop/src/providers/provider_function.rs @@ -1,13 +1,17 @@ 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(rename_all = "snake_case")] pub enum MediaFunction { - PlayPause, + Play, + Pause, + TogglePlayPause, Next, Previous, } 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) From ee83c321c8d41085a4a0d163f33b4ef35fd75522 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 21 Nov 2024 18:22:00 +0800 Subject: [PATCH 04/17] feat: improve media function handling --- .../src/providers/media/media_provider.rs | 66 ++++++++++--------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index edcb78f3..92ec3ef9 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use crossbeam::channel::{unbounded, Receiver, Sender}; use serde::{Deserialize, Serialize}; use tracing::debug; @@ -14,8 +15,7 @@ use windows::{ use crate::providers::{ CommonProviderState, MediaFunction, Provider, ProviderFunction, - ProviderFunctionResponse, ProviderFunctionResult, ProviderInputMsg, - RuntimeType, + ProviderFunctionResponse, ProviderInputMsg, RuntimeType, }; #[derive(Deserialize, Debug)] @@ -92,8 +92,9 @@ impl MediaProvider { /// 3. Sets up the initial session if one exists /// 4. Runs the main event loop to handle media state changes fn create_session_manager(&mut self) -> anyhow::Result<()> { - debug!("Creating media session manager."); + debug!("Getting media session manager."); let manager = GsmtcManager::RequestAsync()?.get()?; + self.register_session_changed_handler(&manager)?; self.create_session()?; @@ -114,31 +115,8 @@ impl MediaProvider { ProviderFunction::Media(media_function), sender, )) => { - if let Some(session) = &self.session { - let result = match media_function { - MediaFunction::Play => { - session.TryPlayAsync()?.get()?; - Ok(ProviderFunctionResponse::Null) - } - MediaFunction::Pause => { - session.TryPauseAsync()?.get()?; - Ok(ProviderFunctionResponse::Null) - } - MediaFunction::TogglePlayPause => { - session.TryTogglePlayPauseAsync()?.get()?; - Ok(ProviderFunctionResponse::Null) - } - MediaFunction::Next => { - session.TrySkipNextAsync()?.get()?; - Ok(ProviderFunctionResponse::Null) - } - MediaFunction::Previous => { - session.TrySkipPreviousAsync()?.get()?; - Ok(ProviderFunctionResponse::Null) - } - }; - sender.send(result).unwrap(); - } + let function_res = self.handle_function(media_function).map_err(|err| err.to_string()); + sender.send(function_res).unwrap(); } _ => {} } @@ -167,8 +145,7 @@ impl MediaProvider { Ok(()) } - /// Central event handler that processes all media session events. - /// Routes events to appropriate update methods based on event type. + /// Handles a media session event. fn handle_event( &mut self, event: MediaSessionEvent, @@ -195,6 +172,35 @@ impl MediaProvider { Ok(()) } + /// Handles an incoming media provider function call. + fn handle_function( + &mut self, + function: MediaFunction, + ) -> anyhow::Result { + let session = + self.session.as_ref().context("No active media session.")?; + + match function { + MediaFunction::Play => { + session.TryPlayAsync()?.get()?; + } + MediaFunction::Pause => { + session.TryPauseAsync()?.get()?; + } + MediaFunction::TogglePlayPause => { + session.TryTogglePlayPauseAsync()?.get()?; + } + MediaFunction::Next => { + session.TrySkipNextAsync()?.get()?; + } + MediaFunction::Previous => { + session.TrySkipPreviousAsync()?.get()?; + } + }; + + Ok(ProviderFunctionResponse::Null) + } + /// Sets up a new media session when one becomes available. /// This includes: /// 1. Cleaning up any existing session listeners From 0c17f5e2c12d1084d87a83f03e32a202f4d0ce1c Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 21 Nov 2024 19:17:41 +0800 Subject: [PATCH 05/17] wip --- .../src/providers/media/media_provider.rs | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index 92ec3ef9..3d4662d6 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -64,6 +64,7 @@ struct EventTokens { pub struct MediaProvider { common: CommonProviderState, session: Option, + session_output: Option, event_tokens: Option, event_sender: Sender, event_receiver: Receiver, @@ -79,6 +80,7 @@ impl MediaProvider { Self { common, session: None, + session_output: None, event_tokens: None, event_sender, event_receiver, @@ -86,17 +88,13 @@ impl MediaProvider { } /// Main entry point that sets up the media session manager and runs the - /// event loop. This method: - /// 1. Creates the Windows media session manager - /// 2. Registers for session change notifications - /// 3. Sets up the initial session if one exists - /// 4. Runs the main event loop to handle media state changes + /// event loop. fn create_session_manager(&mut self) -> anyhow::Result<()> { debug!("Getting media session manager."); let manager = GsmtcManager::RequestAsync()?.get()?; self.register_session_changed_handler(&manager)?; - self.create_session()?; + self.create_session(&manager)?; loop { crossbeam::select! { @@ -127,21 +125,21 @@ impl MediaProvider { Ok(()) } - /// Registers a callback with the session manager to detect when the - /// active media session changes (e.g. when switching between media - /// players). + /// Registers a callback with the session manager for when the active + /// media session changes (e.g. when switching between media players). fn register_session_changed_handler( &self, manager: &GsmtcManager, ) -> anyhow::Result<()> { - let handler = TypedEventHandler::new({ - let sender = self.event_sender.clone(); + let sender = self.event_sender.clone(); + + manager.CurrentSessionChanged(&TypedEventHandler::new( move |_, _| { sender.send(MediaSessionEvent::SessionChanged).unwrap(); Ok(()) - } - }); - manager.CurrentSessionChanged(&handler)?; + }, + ))?; + Ok(()) } @@ -169,6 +167,8 @@ impl MediaProvider { } } } + + self.emit_session(media_session); Ok(()) } @@ -202,14 +202,14 @@ impl MediaProvider { } /// Sets up a new media session when one becomes available. - /// This includes: - /// 1. Cleaning up any existing session listeners - /// 2. Getting the current session from Windows - /// 3. Setting up new event listeners - /// 4. Emitting initial state - fn create_session(&mut self) -> anyhow::Result<()> { + fn create_session( + &mut self, + manager: &GsmtcManager, + ) -> anyhow::Result<()> { + // Remove any existing session listeners. self.remove_session_listeners(); - let manager = GsmtcManager::RequestAsync()?.get()?; + + // Get the updated session. let session = manager.GetCurrentSession().ok(); if let Some(session) = &session { @@ -224,6 +224,7 @@ impl MediaProvider { } /// Creates event listeners for all media session state changes. + /// /// Returns tokens needed for cleanup when the session ends. fn setup_session_listeners( &self, @@ -329,7 +330,7 @@ impl MediaProvider { Ok(()) } - /// Helper to emit a media session update through the provider's emitter. + /// Emits a `MediaSession` update through the provider's emitter. fn emit_session(&self, session: MediaSession) { self.common.emitter.emit_output(Ok(MediaOutput { session: Some(session), @@ -384,33 +385,34 @@ impl MediaProvider { Ok(Some(media_session)) } - /// Updates media metadata properties in a MediaSession struct. + /// Updates media metadata properties in a `MediaSession`. fn update_media_properties( session: &mut MediaSession, - props: &GsmtcMediaProperties, + properties: &GsmtcMediaProperties, ) -> anyhow::Result<()> { - let artist = props.Artist()?.to_string(); - let album_title = props.AlbumTitle()?.to_string(); - let album_artist = props.AlbumArtist()?.to_string(); + let artist = properties.Artist()?.to_string(); + let album_title = properties.AlbumTitle()?.to_string(); + let album_artist = properties.AlbumArtist()?.to_string(); session.artist = (!artist.is_empty()).then_some(artist); session.album_title = (!album_title.is_empty()).then_some(album_title); session.album_artist = (!album_artist.is_empty()).then_some(album_artist); - session.track_number = props.TrackNumber()? as u32; + session.track_number = properties.TrackNumber()? as u32; Ok(()) } - /// Updates timeline properties (position/duration) in a MediaSession - /// struct. + /// Updates timeline properties (position/duration) in a `MediaSession`. fn update_timeline_properties( session: &mut MediaSession, - props: &GsmtcTimelineProperties, + properties: &GsmtcTimelineProperties, ) -> anyhow::Result<()> { - session.start_time = props.StartTime()?.Duration as u64 / 10_000_000; - session.end_time = props.EndTime()?.Duration as u64 / 10_000_000; - session.position = props.Position()?.Duration as u64 / 10_000_000; + session.start_time = + properties.StartTime()?.Duration as u64 / 10_000_000; + session.end_time = properties.EndTime()?.Duration as u64 / 10_000_000; + session.position = properties.Position()?.Duration as u64 / 10_000_000; + Ok(()) } } From aae6d83de03f09a98a2ba0a5d1e87cc6d6b21e61 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 21 Nov 2024 19:39:02 +0800 Subject: [PATCH 06/17] wip add all sessions --- packages/desktop/Cargo.toml | 3 +- .../src/providers/media/media_provider.rs | 65 +++++++++++++------ 2 files changed, 46 insertions(+), 22 deletions(-) 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 3d4662d6..d85f5d7a 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use anyhow::Context; use crossbeam::channel::{unbounded, Receiver, Sender}; use serde::{Deserialize, Serialize}; @@ -25,12 +27,14 @@ 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 session_id: String, pub title: String, pub artist: Option, pub album_title: Option, @@ -40,15 +44,18 @@ pub struct MediaSession { pub end_time: u64, pub position: u64, pub is_playing: bool, + // TODO + // pub is_current_session: bool, } /// Events that can be emitted from media session state changes. #[derive(Debug)] enum MediaSessionEvent { - PlaybackInfoChanged, - MediaPropertiesChanged, - TimelinePropertiesChanged, - SessionChanged, + CurrentSessionChanged, + SessionListChanged, + PlaybackInfoChanged(String), + MediaPropertiesChanged(String), + TimelinePropertiesChanged(String), } /// Holds event registration tokens for media session callbacks. @@ -63,7 +70,8 @@ struct EventTokens { pub struct MediaProvider { common: CommonProviderState, - session: Option, + current_session_id: Option, + sessions: HashMap, session_output: Option, event_tokens: Option, event_sender: Sender, @@ -79,7 +87,8 @@ impl MediaProvider { Self { common, - session: None, + current_session_id: None, + sessions: HashMap::new(), session_output: None, event_tokens: None, event_sender, @@ -125,20 +134,35 @@ impl MediaProvider { Ok(()) } - /// Registers a callback with the session manager for when the active - /// media session changes (e.g. when switching between media players). + /// Registers callbacks with the session manager. + /// + /// - `CurrentSessionChanged`: for when the active media session changes + /// (e.g. when switching between media players). + /// - `SessionListChanged`: for when the list of available media sessions + /// changes (e.g. when a media player is opened or closed). fn register_session_changed_handler( &self, manager: &GsmtcManager, ) -> anyhow::Result<()> { - let sender = self.event_sender.clone(); + // Handler for current session changes. + manager.CurrentSessionChanged(&TypedEventHandler::new({ + let sender = self.event_sender.clone(); + move |_, _| { + sender + .send(MediaSessionEvent::CurrentSessionChanged) + .unwrap(); + Ok(()) + } + }))?; - manager.CurrentSessionChanged(&TypedEventHandler::new( + // Handler for session list changes. + manager.SessionsChanged(&TypedEventHandler::new({ + let sender = self.event_sender.clone(); move |_, _| { - sender.send(MediaSessionEvent::SessionChanged).unwrap(); + sender.send(MediaSessionEvent::SessionListChanged).unwrap(); Ok(()) - }, - ))?; + } + }))?; Ok(()) } @@ -149,7 +173,7 @@ impl MediaProvider { event: MediaSessionEvent, ) -> anyhow::Result<()> { match event { - MediaSessionEvent::SessionChanged => self.create_session()?, + MediaSessionEvent::CurrentSessionChanged => self.create_session()?, _ => { if let Some(session) = &self.session { match event { @@ -277,10 +301,9 @@ impl MediaProvider { /// Emits an empty state when no media session is active. fn emit_empty_state(&self) { - self - .common - .emitter - .emit_output(Ok(MediaOutput { session: None })); + self.common.emitter.emit_output(Ok(MediaOutput { + current_session: None, + })); } /// Emits a complete state update for all media session properties. @@ -333,7 +356,7 @@ impl MediaProvider { /// Emits a `MediaSession` update through the provider's emitter. fn emit_session(&self, session: MediaSession) { self.common.emitter.emit_output(Ok(MediaOutput { - session: Some(session), + current_session: Some(session), })); } @@ -342,7 +365,7 @@ impl MediaProvider { session: &GsmtcSession, ) -> anyhow::Result { Ok(MediaOutput { - session: Self::to_media_session_output(session)?, + current_session: Self::to_media_session_output(session)?, }) } From eeb2f2aabb324386493a66ad56ec9a7c42df5edc Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 21 Nov 2024 19:39:08 +0800 Subject: [PATCH 07/17] wip add all sessions --- .../src/providers/media/media_provider.rs | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index d85f5d7a..61a8844d 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -44,15 +44,14 @@ pub struct MediaSession { pub end_time: u64, pub position: u64, pub is_playing: bool, - // TODO - // pub is_current_session: bool, + pub is_current_session: bool, } /// Events that can be emitted from media session state changes. #[derive(Debug)] enum MediaSessionEvent { - CurrentSessionChanged, SessionListChanged, + CurrentSessionChanged(Option), PlaybackInfoChanged(String), MediaPropertiesChanged(String), TimelinePropertiesChanged(String), @@ -72,7 +71,7 @@ pub struct MediaProvider { common: CommonProviderState, current_session_id: Option, sessions: HashMap, - session_output: Option, + session_outputs: HashMap, event_tokens: Option, event_sender: Sender, event_receiver: Receiver, @@ -89,7 +88,7 @@ impl MediaProvider { common, current_session_id: None, sessions: HashMap::new(), - session_output: None, + session_outputs: HashMap::new(), event_tokens: None, event_sender, event_receiver, @@ -147,10 +146,17 @@ impl MediaProvider { // Handler for current session changes. manager.CurrentSessionChanged(&TypedEventHandler::new({ let sender = self.event_sender.clone(); - move |_, _| { + move |manager: &Option, _| { + let session_id = manager + .as_ref() + .and_then(|manager| manager.GetCurrentSession().ok()) + .and_then(|session| session.SourceAppUserModelId().ok()) + .map(|id| id.to_string()); + sender - .send(MediaSessionEvent::CurrentSessionChanged) + .send(MediaSessionEvent::CurrentSessionChanged(session_id)) .unwrap(); + Ok(()) } }))?; @@ -173,26 +179,33 @@ impl MediaProvider { event: MediaSessionEvent, ) -> anyhow::Result<()> { match event { - MediaSessionEvent::CurrentSessionChanged => self.create_session()?, - _ => { - if let Some(session) = &self.session { - match event { - MediaSessionEvent::PlaybackInfoChanged => { - self.update_playback(session)? - } - MediaSessionEvent::MediaPropertiesChanged => { - self.update_properties(session)? - } - MediaSessionEvent::TimelinePropertiesChanged => { - self.update_timeline(session)? - } - _ => {} - } + MediaSessionEvent::CurrentSessionChanged(id) => { + // TODO: Update `is_current_session` for all sessions. + self.current_session_id = id; + } + MediaSessionEvent::SessionListChanged => { + let manager = GsmtcManager::RequestAsync()?.get()?; + self.update_all_sessions(&manager)?; + } + MediaSessionEvent::PlaybackInfoChanged(id) => { + if let Some((session, _)) = self.sessions.get(&id) { + self.update_session_playback(session, &id)?; + } + } + MediaSessionEvent::MediaPropertiesChanged(id) => { + if let Some((session, _)) = self.sessions.get(&id) { + self.update_session_properties(session, &id)?; + } + } + MediaSessionEvent::TimelinePropertiesChanged(id) => { + if let Some((session, _)) = self.sessions.get(&id) { + self.update_session_timeline(session, &id)?; } } } - self.emit_session(media_session); + self.emit_output(); + Ok(()) } @@ -321,7 +334,7 @@ impl MediaProvider { let info = session.GetPlaybackInfo()?; media_session.is_playing = info.PlaybackStatus()? == GsmtcPlaybackStatus::Playing; - self.emit_session(media_session); + self.emit_output(media_session); } Ok(()) } @@ -336,7 +349,7 @@ impl MediaProvider { { let props = session.TryGetMediaPropertiesAsync()?.get()?; Self::update_media_properties(&mut media_session, &props)?; - self.emit_session(media_session); + self.emit_output(media_session); } Ok(()) } @@ -348,13 +361,13 @@ impl MediaProvider { { let props = session.GetTimelineProperties()?; Self::update_timeline_properties(&mut media_session, &props)?; - self.emit_session(media_session); + self.emit_output(media_session); } Ok(()) } /// Emits a `MediaSession` update through the provider's emitter. - fn emit_session(&self, session: MediaSession) { + fn emit_output(&self, session: MediaSession) { self.common.emitter.emit_output(Ok(MediaOutput { current_session: Some(session), })); From faf412b28e7f08c5551f2a0b588ab81afc5fb58b Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 21 Nov 2024 19:58:38 +0800 Subject: [PATCH 08/17] feat: change client-side for calling media functions --- .../src/desktop/desktop-commands.ts | 22 ++++++--- .../providers/media/create-media-provider.ts | 46 +++++++++++++------ .../providers/media/media-provider-types.ts | 4 ++ .../src/providers/media/media_provider.rs | 31 +++++++++---- .../src/providers/provider_function.rs | 17 ++++--- 5 files changed, 85 insertions(+), 35 deletions(-) diff --git a/packages/client-api/src/desktop/desktop-commands.ts b/packages/client-api/src/desktop/desktop-commands.ts index 4b4628e3..d95a272c 100644 --- a/packages/client-api/src/desktop/desktop-commands.ts +++ b/packages/client-api/src/desktop/desktop-commands.ts @@ -23,7 +23,14 @@ export type ProviderFunction = MediaFunction; export interface MediaFunction { type: 'media'; - function: 'play' | 'pause' | 'toggle_play_pause' | 'next' | 'previous'; + function: { + name: 'play' | 'pause' | 'toggle_play_pause' | 'next' | 'previous'; + args: MediaControlArgs; + }; +} + +export interface MediaControlArgs { + sessionId: string | null; } function startWidget( @@ -51,11 +58,14 @@ function unlistenProvider(configHash: string): Promise { return invoke('unlisten_provider', { configHash }); } -function callProviderFunction(args: { - configHash: string; - function: ProviderFunction; -}): Promise { - return invoke('call_provider_function', args); +function callProviderFunction( + configHash: string, + fn: ProviderFunction, +): Promise { + return invoke('call_provider_function', { + configHash, + function: fn, + }); } function setAlwaysOnTop(): Promise { 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 a2899a2f..34f87d68 100644 --- a/packages/client-api/src/providers/media/create-media-provider.ts +++ b/packages/client-api/src/providers/media/create-media-provider.ts @@ -25,34 +25,50 @@ export function createMediaProvider( } else { queue.output({ ...result.output, + session: result.output.currentSession, play: () => { - desktopCommands.callProviderFunction({ - configHash, - function: { type: 'media', function: 'play' }, + desktopCommands.callProviderFunction(configHash, { + type: 'media', + function: { + name: 'play', + args: { sessionId: null }, + }, }); }, pause: () => { - desktopCommands.callProviderFunction({ - configHash, - function: { type: 'media', function: 'pause' }, + desktopCommands.callProviderFunction(configHash, { + type: 'media', + function: { + name: 'pause', + args: { sessionId: null }, + }, }); }, togglePlayPause: () => { - desktopCommands.callProviderFunction({ - configHash, - function: { type: 'media', function: 'toggle_play_pause' }, + desktopCommands.callProviderFunction(configHash, { + type: 'media', + function: { + name: 'toggle_play_pause', + args: { sessionId: null }, + }, }); }, next: () => { - desktopCommands.callProviderFunction({ - configHash, - function: { type: 'media', function: 'next' }, + desktopCommands.callProviderFunction(configHash, { + type: 'media', + function: { + name: 'next', + args: { sessionId: null }, + }, }); }, previous: () => { - desktopCommands.callProviderFunction({ - configHash, - function: { type: 'media', function: 'previous' }, + desktopCommands.callProviderFunction(configHash, { + type: 'media', + function: { + name: 'previous', + args: { sessionId: null }, + }, }); }, }); 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 7936ba58..0d527d21 100644 --- a/packages/client-api/src/providers/media/media-provider-types.ts +++ b/packages/client-api/src/providers/media/media-provider-types.ts @@ -5,7 +5,10 @@ export interface MediaProviderConfig { } export interface MediaOutput { + /** @deprecated Use {@link currentSession} instead */ session: MediaSession | null; + currentSession: MediaSession | null; + allSessions: MediaSession[]; play(): void; pause(): void; togglePlayPause(): void; @@ -23,6 +26,7 @@ export interface MediaSession { endTime: number; position: number; isPlaying: boolean; + isCurrentSession: boolean; } export type MediaProvider = Provider; diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index 61a8844d..b667a3e7 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -70,8 +70,7 @@ struct EventTokens { pub struct MediaProvider { common: CommonProviderState, current_session_id: Option, - sessions: HashMap, - session_outputs: HashMap, + sessions: HashMap, event_tokens: Option, event_sender: Sender, event_receiver: Receiver, @@ -88,7 +87,6 @@ impl MediaProvider { common, current_session_id: None, sessions: HashMap::new(), - session_outputs: HashMap::new(), event_tokens: None, event_sender, event_receiver, @@ -214,8 +212,11 @@ impl MediaProvider { &mut self, function: MediaFunction, ) -> anyhow::Result { - let session = - self.session.as_ref().context("No active media session.")?; + let session = self + .current_session_id + .as_ref() + .and_then(|id| self.sessions.get(id)) + .context("No active media session.")?; match function { MediaFunction::Play => { @@ -266,21 +267,32 @@ impl MediaProvider { fn setup_session_listeners( &self, session: &GsmtcSession, + session_id: &str, ) -> anyhow::Result { + let session_id = session_id.to_string(); + Ok(EventTokens { playback: session.PlaybackInfoChanged(&TypedEventHandler::new({ let sender = self.event_sender.clone(); + let session_id = session_id.clone(); move |_, _| { - sender.send(MediaSessionEvent::PlaybackInfoChanged).unwrap(); + 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.clone(); move |_, _| { sender - .send(MediaSessionEvent::MediaPropertiesChanged) + .send(MediaSessionEvent::MediaPropertiesChanged( + session_id.clone(), + )) .unwrap(); Ok(()) } @@ -289,9 +301,12 @@ impl MediaProvider { timeline: session.TimelinePropertiesChanged( &TypedEventHandler::new({ let sender = self.event_sender.clone(); + let session_id = session_id.clone(); move |_, _| { sender - .send(MediaSessionEvent::TimelinePropertiesChanged) + .send(MediaSessionEvent::TimelinePropertiesChanged( + session_id.clone(), + )) .unwrap(); Ok(()) } diff --git a/packages/desktop/src/providers/provider_function.rs b/packages/desktop/src/providers/provider_function.rs index 3873a05a..f95f88ad 100644 --- a/packages/desktop/src/providers/provider_function.rs +++ b/packages/desktop/src/providers/provider_function.rs @@ -7,13 +7,18 @@ pub enum ProviderFunction { } #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] +#[serde(tag = "name", content = "args", rename_all = "snake_case")] pub enum MediaFunction { - Play, - Pause, - TogglePlayPause, - 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; From b1583f5893df1808dc4285682a84884dbb5d37ed Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 21 Nov 2024 21:37:02 +0800 Subject: [PATCH 09/17] wip --- README.md | 23 +- .../providers/media/create-media-provider.ts | 21 +- .../providers/media/media-provider-types.ts | 15 +- .../src/providers/media/media_provider.rs | 253 ++++++++---------- 4 files changed, 151 insertions(+), 161 deletions(-) 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/packages/client-api/src/providers/media/create-media-provider.ts b/packages/client-api/src/providers/media/create-media-provider.ts index 34f87d68..58becf35 100644 --- a/packages/client-api/src/providers/media/create-media-provider.ts +++ b/packages/client-api/src/providers/media/create-media-provider.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { createBaseProvider } from '../create-base-provider'; import { desktopCommands, onProviderEmit } from '~/desktop'; import type { + MediaControlArgs, MediaOutput, MediaProvider, MediaProviderConfig, @@ -26,48 +27,48 @@ export function createMediaProvider( queue.output({ ...result.output, session: result.output.currentSession, - play: () => { + play: (args?: MediaControlArgs) => { desktopCommands.callProviderFunction(configHash, { type: 'media', function: { name: 'play', - args: { sessionId: null }, + args: args ?? { sessionId: null }, }, }); }, - pause: () => { + pause: (args?: MediaControlArgs) => { desktopCommands.callProviderFunction(configHash, { type: 'media', function: { name: 'pause', - args: { sessionId: null }, + args: args ?? { sessionId: null }, }, }); }, - togglePlayPause: () => { + togglePlayPause: (args?: MediaControlArgs) => { desktopCommands.callProviderFunction(configHash, { type: 'media', function: { name: 'toggle_play_pause', - args: { sessionId: null }, + args: args ?? { sessionId: null }, }, }); }, - next: () => { + next: (args?: MediaControlArgs) => { desktopCommands.callProviderFunction(configHash, { type: 'media', function: { name: 'next', - args: { sessionId: null }, + args: args ?? { sessionId: null }, }, }); }, - previous: () => { + previous: (args?: MediaControlArgs) => { desktopCommands.callProviderFunction(configHash, { type: 'media', function: { name: 'previous', - args: { sessionId: null }, + args: args ?? { sessionId: null }, }, }); }, 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 0d527d21..05362cb9 100644 --- a/packages/client-api/src/providers/media/media-provider-types.ts +++ b/packages/client-api/src/providers/media/media-provider-types.ts @@ -9,14 +9,19 @@ export interface MediaOutput { session: MediaSession | null; currentSession: MediaSession | null; allSessions: MediaSession[]; - play(): void; - pause(): void; - togglePlayPause(): void; - next(): void; - previous(): void; + 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 { + sessionId: string; title: string; artist: string | null; albumTitle: string | null; diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index b667a3e7..8211734d 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -10,6 +10,7 @@ use windows::{ GlobalSystemMediaTransportControlsSession as GsmtcSession, GlobalSystemMediaTransportControlsSessionManager as GsmtcManager, GlobalSystemMediaTransportControlsSessionMediaProperties as GsmtcMediaProperties, + GlobalSystemMediaTransportControlsSessionPlaybackInfo as GsmtcPlaybackInfo, GlobalSystemMediaTransportControlsSessionPlaybackStatus as GsmtcPlaybackStatus, GlobalSystemMediaTransportControlsSessionTimelineProperties as GsmtcTimelineProperties, }, @@ -47,10 +48,28 @@ pub struct MediaSession { pub is_current_session: bool, } +impl Default for MediaSession { + fn default() -> Self { + Self { + session_id: "".to_string(), + title: "".to_string(), + 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 { - SessionListChanged, + SessionAddOrRemove, CurrentSessionChanged(Option), PlaybackInfoChanged(String), MediaPropertiesChanged(String), @@ -60,17 +79,25 @@ enum MediaSessionEvent { /// Holds event registration tokens for media session callbacks. /// /// These need to be cleaned up when the session changes. -#[derive(Clone)] +#[derive(Debug)] struct EventTokens { 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, - sessions: HashMap, + session_states: HashMap, event_tokens: Option, event_sender: Sender, event_receiver: Receiver, @@ -86,7 +113,7 @@ impl MediaProvider { Self { common, current_session_id: None, - sessions: HashMap::new(), + session_states: HashMap::new(), event_tokens: None, event_sender, event_receiver, @@ -135,7 +162,7 @@ impl MediaProvider { /// /// - `CurrentSessionChanged`: for when the active media session changes /// (e.g. when switching between media players). - /// - `SessionListChanged`: for when the list of available media sessions + /// - `SessionAddOrRemove`: for when the list of available media sessions /// changes (e.g. when a media player is opened or closed). fn register_session_changed_handler( &self, @@ -159,11 +186,11 @@ impl MediaProvider { } }))?; - // Handler for session list changes. + // Handler for a session is added or removed. manager.SessionsChanged(&TypedEventHandler::new({ let sender = self.event_sender.clone(); move |_, _| { - sender.send(MediaSessionEvent::SessionListChanged).unwrap(); + sender.send(MediaSessionEvent::SessionAddOrRemove).unwrap(); Ok(()) } }))?; @@ -181,22 +208,22 @@ impl MediaProvider { // TODO: Update `is_current_session` for all sessions. self.current_session_id = id; } - MediaSessionEvent::SessionListChanged => { + MediaSessionEvent::SessionAddOrRemove => { let manager = GsmtcManager::RequestAsync()?.get()?; self.update_all_sessions(&manager)?; } MediaSessionEvent::PlaybackInfoChanged(id) => { - if let Some((session, _)) = self.sessions.get(&id) { + if let Some((session, _)) = self.session_states.get(&id) { self.update_session_playback(session, &id)?; } } MediaSessionEvent::MediaPropertiesChanged(id) => { - if let Some((session, _)) = self.sessions.get(&id) { + if let Some((session, _)) = self.session_states.get(&id) { self.update_session_properties(session, &id)?; } } MediaSessionEvent::TimelinePropertiesChanged(id) => { - if let Some((session, _)) = self.sessions.get(&id) { + if let Some((session, _)) = self.session_states.get(&id) { self.update_session_timeline(session, &id)?; } } @@ -212,27 +239,27 @@ impl MediaProvider { &mut self, function: MediaFunction, ) -> anyhow::Result { - let session = self + let session_state = self .current_session_id .as_ref() - .and_then(|id| self.sessions.get(id)) + .and_then(|id| self.session_states.get(id)) .context("No active media session.")?; match function { - MediaFunction::Play => { - session.TryPlayAsync()?.get()?; + MediaFunction::Play(args) => { + session_state.session.TryPlayAsync()?.get()?; } - MediaFunction::Pause => { - session.TryPauseAsync()?.get()?; + MediaFunction::Pause(args) => { + session_state.session.TryPauseAsync()?.get()?; } - MediaFunction::TogglePlayPause => { - session.TryTogglePlayPauseAsync()?.get()?; + MediaFunction::TogglePlayPause(args) => { + session_state.session.TryTogglePlayPauseAsync()?.get()?; } - MediaFunction::Next => { - session.TrySkipNextAsync()?.get()?; + MediaFunction::Next(args) => { + session_state.session.TrySkipNextAsync()?.get()?; } - MediaFunction::Previous => { - session.TrySkipPreviousAsync()?.get()?; + MediaFunction::Previous(args) => { + session_state.session.TrySkipPreviousAsync()?.get()?; } }; @@ -315,154 +342,108 @@ impl MediaProvider { }) } - /// Cleans up event listeners from the current session. - fn remove_session_listeners(&mut self) { - if let (Some(session), Some(tokens)) = - (&self.session, &self.event_tokens) - { - let _ = session.RemovePlaybackInfoChanged(tokens.playback); - let _ = session.RemoveMediaPropertiesChanged(tokens.properties); - let _ = session.RemoveTimelinePropertiesChanged(tokens.timeline); - } - self.event_tokens = None; - } - - /// Emits an empty state when no media session is active. - fn emit_empty_state(&self) { - self.common.emitter.emit_output(Ok(MediaOutput { - current_session: None, - })); - } - - /// Emits a complete state update for all media session properties. - fn emit_full_state(&self, session: &GsmtcSession) -> anyhow::Result<()> { - let output = Self::to_media_output(session)?; - self.common.emitter.emit_output(Ok(output)); - Ok(()) - } - - /// Updates and emits only playback state changes (playing/paused). - fn update_playback(&self, session: &GsmtcSession) -> anyhow::Result<()> { - if let Some(mut media_session) = - Self::to_media_session_output(session)? - { - let info = session.GetPlaybackInfo()?; - media_session.is_playing = - info.PlaybackStatus()? == GsmtcPlaybackStatus::Playing; - self.emit_output(media_session); - } - Ok(()) - } - - /// Updates and emits only media property changes (title, artist, etc). - fn update_properties( - &self, + /// Cleans up event listeners from the given session. + fn remove_session_listeners( session: &GsmtcSession, - ) -> anyhow::Result<()> { - if let Some(mut media_session) = - Self::to_media_session_output(session)? - { - let props = session.TryGetMediaPropertiesAsync()?.get()?; - Self::update_media_properties(&mut media_session, &props)?; - self.emit_output(media_session); - } - Ok(()) + tokens: &EventTokens, + ) { + let _ = session.RemovePlaybackInfoChanged(tokens.playback); + let _ = session.RemoveMediaPropertiesChanged(tokens.properties); + let _ = session.RemoveTimelinePropertiesChanged(tokens.timeline); } - /// Updates and emits only timeline property changes (position/duration). - fn update_timeline(&self, session: &GsmtcSession) -> anyhow::Result<()> { - if let Some(mut media_session) = - Self::to_media_session_output(session)? - { - let props = session.GetTimelineProperties()?; - Self::update_timeline_properties(&mut media_session, &props)?; - self.emit_output(media_session); - } - Ok(()) - } + /// Emits a `MediaOutput` update through the provider's emitter. + fn emit_output(&self) { + // At times, GSMTC can have a valid session, but return empty string + // for all media properties. Check that we at least have a valid + // title, otherwise, return `None`. + let current_session = self + .current_session_id + .as_ref() + .and_then(|id| { + self + .session_states + .get(id) + .filter(|state| !state.output.title.is_empty()) + }) + .map(|state| state.output.clone()); + + let all_sessions = self + .session_states + .values() + .filter(|state| !state.output.title.is_empty()) + .map(|state| state.output.clone()) + .collect(); - /// Emits a `MediaSession` update through the provider's emitter. - fn emit_output(&self, session: MediaSession) { self.common.emitter.emit_output(Ok(MediaOutput { - current_session: Some(session), + current_session, + all_sessions, })); } - /// Creates a complete MediaOutput struct from a Windows media session. - fn to_media_output( - session: &GsmtcSession, - ) -> anyhow::Result { - Ok(MediaOutput { - current_session: Self::to_media_session_output(session)?, - }) - } - - /// Creates our MediaSession struct from a Windows media session. - /// Returns None if the session has no title (indicating invalid/empty - /// state). + /// Creates a `MediaSession` from a Windows media session. fn to_media_session_output( session: &GsmtcSession, - ) -> anyhow::Result> { - let props = session.TryGetMediaPropertiesAsync()?.get()?; - let title = props.Title()?.to_string(); - if title.is_empty() { - return Ok(None); - } + ) -> anyhow::Result { + let mut session_output = MediaSession::default(); - // Create base session with title and default values - let mut media_session = MediaSession { - title, - artist: None, - album_title: None, - album_artist: None, - track_number: 0, - start_time: 0, - end_time: 0, - position: 0, - is_playing: false, - }; - - // Update with current state - Self::update_media_properties(&mut media_session, &props)?; + Self::update_media_properties( + &mut session_output, + &session.TryGetMediaPropertiesAsync()?.get()?, + )?; Self::update_timeline_properties( - &mut media_session, + &mut session_output, &session.GetTimelineProperties()?, )?; + Self::update_playback_info( + &mut session_output, + &session.GetPlaybackInfo()?, + )?; - let info = session.GetPlaybackInfo()?; - media_session.is_playing = - info.PlaybackStatus()? == GsmtcPlaybackStatus::Playing; - - Ok(Some(media_session)) + Ok(session_output) } /// Updates media metadata properties in a `MediaSession`. fn update_media_properties( - session: &mut MediaSession, + session_output: &mut MediaSession, properties: &GsmtcMediaProperties, ) -> anyhow::Result<()> { let artist = properties.Artist()?.to_string(); let album_title = properties.AlbumTitle()?.to_string(); let album_artist = properties.AlbumArtist()?.to_string(); - session.artist = (!artist.is_empty()).then_some(artist); - session.album_title = (!album_title.is_empty()).then_some(album_title); - session.album_artist = + 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.track_number = properties.TrackNumber()? as u32; + session_output.track_number = properties.TrackNumber()? as u32; Ok(()) } /// Updates timeline properties (position/duration) in a `MediaSession`. fn update_timeline_properties( - session: &mut MediaSession, + session_output: &mut MediaSession, properties: &GsmtcTimelineProperties, ) -> anyhow::Result<()> { - session.start_time = + session_output.start_time = properties.StartTime()?.Duration as u64 / 10_000_000; - session.end_time = properties.EndTime()?.Duration as u64 / 10_000_000; - session.position = properties.Position()?.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; + + Ok(()) + } + + /// Updates playback info in a `MediaSession`. + fn update_playback_info( + session_output: &mut MediaSession, + playback_info: &GsmtcPlaybackInfo, + ) -> anyhow::Result<()> { + session_output.is_playing = + playback_info.PlaybackStatus()? == GsmtcPlaybackStatus::Playing; Ok(()) } From 6db2cfbc0112c6ba5b03fc7e2d370f99ec673b77 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 21 Nov 2024 23:05:30 +0800 Subject: [PATCH 10/17] wip --- .../src/providers/media/media_provider.rs | 119 +++++++++++------- 1 file changed, 76 insertions(+), 43 deletions(-) diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index 8211734d..4e96806c 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use anyhow::Context; use crossbeam::channel::{unbounded, Receiver, Sender}; @@ -126,8 +126,8 @@ impl MediaProvider { debug!("Getting media session manager."); let manager = GsmtcManager::RequestAsync()?.get()?; - self.register_session_changed_handler(&manager)?; - self.create_session(&manager)?; + self.register_session_change_callbacks(&manager)?; + self.update_session_states(&manager)?; loop { crossbeam::select! { @@ -158,13 +158,13 @@ impl MediaProvider { Ok(()) } - /// Registers callbacks with the session manager. + /// 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_changed_handler( + fn register_session_change_callbacks( &self, manager: &GsmtcManager, ) -> anyhow::Result<()> { @@ -210,21 +210,30 @@ impl MediaProvider { } MediaSessionEvent::SessionAddOrRemove => { let manager = GsmtcManager::RequestAsync()?.get()?; - self.update_all_sessions(&manager)?; + self.update_session_states(&manager)?; } MediaSessionEvent::PlaybackInfoChanged(id) => { - if let Some((session, _)) = self.session_states.get(&id) { - self.update_session_playback(session, &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, _)) = self.session_states.get(&id) { - self.update_session_properties(session, &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, _)) = self.session_states.get(&id) { - self.update_session_timeline(session, &id)?; + if let Some(session_state) = self.session_states.get_mut(&id) { + Self::update_timeline_properties( + &mut session_state.output, + &session_state.session, + )?; } } } @@ -266,32 +275,59 @@ impl MediaProvider { Ok(ProviderFunctionResponse::Null) } - /// Sets up a new media session when one becomes available. - fn create_session( + /// Updates the state of all media sessions. + fn update_session_states( &mut self, manager: &GsmtcManager, ) -> anyhow::Result<()> { - // Remove any existing session listeners. - self.remove_session_listeners(); + let sessions = manager.GetSessions()?; + let active_session = manager.GetCurrentSession(); + + // Track existing sessions to detect removals. + let mut existing_ids: HashSet = HashSet::new(); + + for session in sessions { + let id = session.SourceAppUserModelId()?.to_string(); + existing_ids.insert(id.clone()); + + // Handle new sessions. + if !self.session_states.contains_key(&id) { + let tokens = self.register_session_callbacks(&session, &id)?; + let output = Self::to_media_session_output(&session)?; + self.session_states.insert( + id.clone(), + SessionState { + session, + tokens, + output, + }, + ); + } + } - // Get the updated session. - let session = manager.GetCurrentSession().ok(); + // Remove sessions that no longer exist. + self.session_states.retain(|id, state| { + if !existing_ids.contains(id) { + Self::remove_session_listeners(&state.session, &state.tokens); + false + } else { + true + } + }); - if let Some(session) = &session { - self.event_tokens = Some(self.setup_session_listeners(session)?); - self.emit_full_state(session)?; - } else { - self.emit_empty_state(); - } + // Update active session. + self.current_session_id = active_session + .as_ref() + .and_then(|session| session.SourceAppUserModelId().ok()) + .map(|id| id.to_string()); - self.session = session; Ok(()) } - /// Creates event listeners for all media session state changes. + /// Registers event callbacks for media session state changes. /// /// Returns tokens needed for cleanup when the session ends. - fn setup_session_listeners( + fn register_session_callbacks( &self, session: &GsmtcSession, session_id: &str, @@ -387,18 +423,9 @@ impl MediaProvider { ) -> anyhow::Result { let mut session_output = MediaSession::default(); - Self::update_media_properties( - &mut session_output, - &session.TryGetMediaPropertiesAsync()?.get()?, - )?; - Self::update_timeline_properties( - &mut session_output, - &session.GetTimelineProperties()?, - )?; - Self::update_playback_info( - &mut session_output, - &session.GetPlaybackInfo()?, - )?; + Self::update_media_properties(&mut session_output, &session)?; + Self::update_timeline_properties(&mut session_output, &session)?; + Self::update_playback_info(&mut session_output, &session)?; Ok(session_output) } @@ -406,8 +433,10 @@ impl MediaProvider { /// Updates media metadata properties in a `MediaSession`. fn update_media_properties( session_output: &mut MediaSession, - properties: &GsmtcMediaProperties, + session: &GsmtcSession, ) -> anyhow::Result<()> { + let properties = session.TryGetMediaPropertiesAsync()?.get()?; + let artist = properties.Artist()?.to_string(); let album_title = properties.AlbumTitle()?.to_string(); let album_artist = properties.AlbumArtist()?.to_string(); @@ -425,8 +454,10 @@ impl MediaProvider { /// Updates timeline properties (position/duration) in a `MediaSession`. fn update_timeline_properties( session_output: &mut MediaSession, - properties: &GsmtcTimelineProperties, + session: &GsmtcSession, ) -> anyhow::Result<()> { + let properties = session.GetTimelineProperties()?; + session_output.start_time = properties.StartTime()?.Duration as u64 / 10_000_000; session_output.end_time = @@ -440,10 +471,12 @@ impl MediaProvider { /// Updates playback info in a `MediaSession`. fn update_playback_info( session_output: &mut MediaSession, - playback_info: &GsmtcPlaybackInfo, + session: &GsmtcSession, ) -> anyhow::Result<()> { + let info = session.GetPlaybackInfo()?; + session_output.is_playing = - playback_info.PlaybackStatus()? == GsmtcPlaybackStatus::Playing; + info.PlaybackStatus()? == GsmtcPlaybackStatus::Playing; Ok(()) } From b9a858eae3d766659a61ac4ebc55c3a5b70645a1 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Thu, 21 Nov 2024 23:29:20 +0800 Subject: [PATCH 11/17] add getter for current session id --- .../src/providers/media/media_provider.rs | 126 ++++++++++-------- 1 file changed, 67 insertions(+), 59 deletions(-) diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index 4e96806c..4c98299f 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -129,6 +129,9 @@ impl MediaProvider { self.register_session_change_callbacks(&manager)?; self.update_session_states(&manager)?; + // Emit initial output. + self.emit_output(); + loop { crossbeam::select! { recv(self.event_receiver) -> event => { @@ -146,8 +149,8 @@ impl MediaProvider { ProviderFunction::Media(media_function), sender, )) => { - let function_res = self.handle_function(media_function).map_err(|err| err.to_string()); - sender.send(function_res).unwrap(); + let res = self.handle_function(media_function).map_err(|err| err.to_string()); + sender.send(res).unwrap(); } _ => {} } @@ -158,46 +161,6 @@ impl MediaProvider { Ok(()) } - /// 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 |manager: &Option, _| { - let session_id = manager - .as_ref() - .and_then(|manager| manager.GetCurrentSession().ok()) - .and_then(|session| session.SourceAppUserModelId().ok()) - .map(|id| id.to_string()); - - sender - .send(MediaSessionEvent::CurrentSessionChanged(session_id)) - .unwrap(); - - Ok(()) - } - }))?; - - // 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(()) - } - }))?; - - Ok(()) - } - /// Handles a media session event. fn handle_event( &mut self, @@ -238,6 +201,7 @@ impl MediaProvider { } } + // Emit new output after handling the event. self.emit_output(); Ok(()) @@ -275,27 +239,67 @@ impl MediaProvider { Ok(ProviderFunctionResponse::Null) } + /// 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 |manager: &Option, _| { + let session_id = manager + .as_ref() + .and_then(|manager| Self::current_session_id(manager)); + + sender + .send(MediaSessionEvent::CurrentSessionChanged(session_id)) + .unwrap(); + + Ok(()) + } + }))?; + + // 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(()) + } + }))?; + + Ok(()) + } + /// Updates the state of all media sessions. fn update_session_states( &mut self, manager: &GsmtcManager, ) -> anyhow::Result<()> { let sessions = manager.GetSessions()?; - let active_session = manager.GetCurrentSession(); // Track existing sessions to detect removals. - let mut existing_ids: HashSet = HashSet::new(); + let mut found_ids: HashSet = HashSet::new(); for session in sessions { - let id = session.SourceAppUserModelId()?.to_string(); - existing_ids.insert(id.clone()); + let session_id = session.SourceAppUserModelId()?.to_string(); + found_ids.insert(session_id.clone()); // Handle new sessions. - if !self.session_states.contains_key(&id) { - let tokens = self.register_session_callbacks(&session, &id)?; + if !self.session_states.contains_key(&session_id) { + let tokens = + self.register_session_callbacks(&session, &session_id)?; + let output = Self::to_media_session_output(&session)?; + self.session_states.insert( - id.clone(), + session_id, SessionState { session, tokens, @@ -307,7 +311,7 @@ impl MediaProvider { // Remove sessions that no longer exist. self.session_states.retain(|id, state| { - if !existing_ids.contains(id) { + if !found_ids.contains(id) { Self::remove_session_listeners(&state.session, &state.tokens); false } else { @@ -316,10 +320,7 @@ impl MediaProvider { }); // Update active session. - self.current_session_id = active_session - .as_ref() - .and_then(|session| session.SourceAppUserModelId().ok()) - .map(|id| id.to_string()); + self.current_session_id = Self::current_session_id(manager); Ok(()) } @@ -332,12 +333,10 @@ impl MediaProvider { session: &GsmtcSession, session_id: &str, ) -> anyhow::Result { - let session_id = session_id.to_string(); - Ok(EventTokens { playback: session.PlaybackInfoChanged(&TypedEventHandler::new({ let sender = self.event_sender.clone(); - let session_id = session_id.clone(); + let session_id = session_id.to_string(); move |_, _| { sender .send(MediaSessionEvent::PlaybackInfoChanged( @@ -350,7 +349,7 @@ impl MediaProvider { properties: session.MediaPropertiesChanged( &TypedEventHandler::new({ let sender = self.event_sender.clone(); - let session_id = session_id.clone(); + let session_id = session_id.to_string(); move |_, _| { sender .send(MediaSessionEvent::MediaPropertiesChanged( @@ -364,7 +363,7 @@ impl MediaProvider { timeline: session.TimelinePropertiesChanged( &TypedEventHandler::new({ let sender = self.event_sender.clone(); - let session_id = session_id.clone(); + let session_id = session_id.to_string(); move |_, _| { sender .send(MediaSessionEvent::TimelinePropertiesChanged( @@ -378,6 +377,15 @@ impl MediaProvider { }) } + /// Gets the ID of the current media session. + fn current_session_id(manager: &GsmtcManager) -> Option { + manager + .GetCurrentSession() + .ok() + .and_then(|session| session.SourceAppUserModelId().ok()) + .map(|id| id.to_string()) + } + /// Cleans up event listeners from the given session. fn remove_session_listeners( session: &GsmtcSession, From c81cb21686fe7ed38c9881fdd8a890ec05151b31 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 22 Nov 2024 02:24:14 +0800 Subject: [PATCH 12/17] working --- .../src/providers/media/media_provider.rs | 117 ++++++++++-------- 1 file changed, 65 insertions(+), 52 deletions(-) diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index 4c98299f..63f12489 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -3,16 +3,13 @@ use std::collections::{HashMap, HashSet}; use anyhow::Context; use crossbeam::channel::{unbounded, Receiver, Sender}; use serde::{Deserialize, Serialize}; -use tracing::debug; +use tracing::{debug, warn}; use windows::{ Foundation::{EventRegistrationToken, TypedEventHandler}, Media::Control::{ GlobalSystemMediaTransportControlsSession as GsmtcSession, GlobalSystemMediaTransportControlsSessionManager as GsmtcManager, - GlobalSystemMediaTransportControlsSessionMediaProperties as GsmtcMediaProperties, - GlobalSystemMediaTransportControlsSessionPlaybackInfo as GsmtcPlaybackInfo, GlobalSystemMediaTransportControlsSessionPlaybackStatus as GsmtcPlaybackStatus, - GlobalSystemMediaTransportControlsSessionTimelineProperties as GsmtcTimelineProperties, }, }; @@ -70,7 +67,7 @@ impl Default for MediaSession { #[derive(Debug)] enum MediaSessionEvent { SessionAddOrRemove, - CurrentSessionChanged(Option), + CurrentSessionChanged, PlaybackInfoChanged(String), MediaPropertiesChanged(String), TimelinePropertiesChanged(String), @@ -98,7 +95,6 @@ pub struct MediaProvider { common: CommonProviderState, current_session_id: Option, session_states: HashMap, - event_tokens: Option, event_sender: Sender, event_receiver: Receiver, } @@ -114,7 +110,6 @@ impl MediaProvider { common, current_session_id: None, session_states: HashMap::new(), - event_tokens: None, event_sender, event_receiver, } @@ -128,6 +123,7 @@ impl MediaProvider { self.register_session_change_callbacks(&manager)?; self.update_session_states(&manager)?; + self.update_current_session(&manager)?; // Emit initial output. self.emit_output(); @@ -137,7 +133,10 @@ impl MediaProvider { recv(self.event_receiver) -> event => { if let Ok(event) = event { debug!("Got media session event: {:?}", event); - self.handle_event(event)?; + + if let Err(err) = self.handle_event(event) { + warn!("Error handling media session event: {}", err); + } } } recv(self.common.input.sync_rx) -> input => { @@ -167,9 +166,9 @@ impl MediaProvider { event: MediaSessionEvent, ) -> anyhow::Result<()> { match event { - MediaSessionEvent::CurrentSessionChanged(id) => { - // TODO: Update `is_current_session` for all sessions. - self.current_session_id = id; + MediaSessionEvent::CurrentSessionChanged => { + let manager = GsmtcManager::RequestAsync()?.get()?; + self.update_current_session(&manager)?; } MediaSessionEvent::SessionAddOrRemove => { let manager = GsmtcManager::RequestAsync()?.get()?; @@ -252,15 +251,10 @@ impl MediaProvider { // Handler for current session changes. manager.CurrentSessionChanged(&TypedEventHandler::new({ let sender = self.event_sender.clone(); - move |manager: &Option, _| { - let session_id = manager - .as_ref() - .and_then(|manager| Self::current_session_id(manager)); - + move |_, _| { sender - .send(MediaSessionEvent::CurrentSessionChanged(session_id)) + .send(MediaSessionEvent::CurrentSessionChanged) .unwrap(); - Ok(()) } }))?; @@ -283,44 +277,64 @@ impl MediaProvider { manager: &GsmtcManager, ) -> anyhow::Result<()> { let sessions = manager.GetSessions()?; - - // Track existing sessions to detect removals. let mut found_ids: HashSet = HashSet::new(); + // 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()); - // Handle new sessions. if !self.session_states.contains_key(&session_id) { - let tokens = - self.register_session_callbacks(&session, &session_id)?; - - let output = Self::to_media_session_output(&session)?; - - self.session_states.insert( - session_id, - SessionState { - session, - tokens, - output, - }, - ); + debug!("New media session detected: {}", session_id); + + 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. - self.session_states.retain(|id, state| { - if !found_ids.contains(id) { - Self::remove_session_listeners(&state.session, &state.tokens); - false - } else { - true + 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(()) + } - // Update active session. - self.current_session_id = Self::current_session_id(manager); + /// 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<()> { + 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(); + } Ok(()) } @@ -343,6 +357,7 @@ impl MediaProvider { session_id.clone(), )) .unwrap(); + Ok(()) } }))?, @@ -356,6 +371,7 @@ impl MediaProvider { session_id.clone(), )) .unwrap(); + Ok(()) } }), @@ -370,6 +386,7 @@ impl MediaProvider { session_id.clone(), )) .unwrap(); + Ok(()) } }), @@ -377,15 +394,6 @@ impl MediaProvider { }) } - /// Gets the ID of the current media session. - fn current_session_id(manager: &GsmtcManager) -> Option { - manager - .GetCurrentSession() - .ok() - .and_then(|session| session.SourceAppUserModelId().ok()) - .map(|id| id.to_string()) - } - /// Cleans up event listeners from the given session. fn remove_session_listeners( session: &GsmtcSession, @@ -398,6 +406,7 @@ impl MediaProvider { /// Emits a `MediaOutput` update through the provider's emitter. fn emit_output(&self) { + println!("Emitting output {:?}", self.session_states); // At times, GSMTC can have a valid session, but return empty string // for all media properties. Check that we at least have a valid // title, otherwise, return `None`. @@ -428,9 +437,11 @@ impl MediaProvider { /// Creates a `MediaSession` from a Windows media session. fn to_media_session_output( session: &GsmtcSession, + session_id: &str, ) -> anyhow::Result { let mut session_output = MediaSession::default(); + 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)?; @@ -445,10 +456,12 @@ impl MediaProvider { ) -> anyhow::Result<()> { let properties = session.TryGetMediaPropertiesAsync()?.get()?; + 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(); + session_output.title = title; session_output.artist = (!artist.is_empty()).then_some(artist); session_output.album_title = (!album_title.is_empty()).then_some(album_title); From 4c02616511caebcc09028404704580371440effb Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 22 Nov 2024 02:31:46 +0800 Subject: [PATCH 13/17] implement drop --- .../desktop/src/providers/media/media_provider.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index 63f12489..443a4c79 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -503,6 +503,17 @@ impl MediaProvider { } } +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, + ); + } + } +} + impl Provider for MediaProvider { fn runtime_type(&self) -> RuntimeType { RuntimeType::Sync From a5f1cbdeaf8c63965bf09257498383b003b28274 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 22 Nov 2024 14:34:34 +0800 Subject: [PATCH 14/17] feat: handle functions with specified id --- .../src/providers/media/media_provider.rs | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index 443a4c79..1496d019 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -211,26 +211,42 @@ impl MediaProvider { &mut self, function: MediaFunction, ) -> anyhow::Result { - let session_state = self - .current_session_id - .as_ref() - .and_then(|id| self.session_states.get(id)) - .context("No active media session.")?; + let args = match &function { + MediaFunction::Play(args) + | MediaFunction::Pause(args) + | MediaFunction::TogglePlayPause(args) + | MediaFunction::Next(args) + | MediaFunction::Previous(args) => args, + }; + + // 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.")? + }; match function { - MediaFunction::Play(args) => { + MediaFunction::Play(_) => { session_state.session.TryPlayAsync()?.get()?; } - MediaFunction::Pause(args) => { + MediaFunction::Pause(_) => { session_state.session.TryPauseAsync()?.get()?; } - MediaFunction::TogglePlayPause(args) => { + MediaFunction::TogglePlayPause(_) => { session_state.session.TryTogglePlayPauseAsync()?.get()?; } - MediaFunction::Next(args) => { + MediaFunction::Next(_) => { session_state.session.TrySkipNextAsync()?.get()?; } - MediaFunction::Previous(args) => { + MediaFunction::Previous(_) => { session_state.session.TrySkipPreviousAsync()?.get()?; } }; From f6d1b04e7b3ff75f084132b758f2192e2cbd0346 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 22 Nov 2024 14:39:15 +0800 Subject: [PATCH 15/17] feat: change title to nullable --- .../providers/media/media-provider-types.ts | 2 +- .../src/providers/media/media_provider.rs | 21 +++++++------------ 2 files changed, 8 insertions(+), 15 deletions(-) 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 05362cb9..3ac946e3 100644 --- a/packages/client-api/src/providers/media/media-provider-types.ts +++ b/packages/client-api/src/providers/media/media-provider-types.ts @@ -22,7 +22,7 @@ export interface MediaControlArgs { export interface MediaSession { sessionId: string; - title: string; + title: string | null; artist: string | null; albumTitle: string | null; albumArtist: string | null; diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index 1496d019..707dbfea 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -33,7 +33,7 @@ pub struct MediaOutput { #[serde(rename_all = "camelCase")] pub struct MediaSession { pub session_id: String, - pub title: String, + pub title: Option, pub artist: Option, pub album_title: Option, pub album_artist: Option, @@ -49,7 +49,7 @@ impl Default for MediaSession { fn default() -> Self { Self { session_id: "".to_string(), - title: "".to_string(), + title: None, artist: None, album_title: None, album_artist: None, @@ -421,26 +421,19 @@ impl MediaProvider { } /// 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) { - println!("Emitting output {:?}", self.session_states); - // At times, GSMTC can have a valid session, but return empty string - // for all media properties. Check that we at least have a valid - // title, otherwise, return `None`. let current_session = self .current_session_id .as_ref() - .and_then(|id| { - self - .session_states - .get(id) - .filter(|state| !state.output.title.is_empty()) - }) + .and_then(|id| self.session_states.get(id)) .map(|state| state.output.clone()); let all_sessions = self .session_states .values() - .filter(|state| !state.output.title.is_empty()) .map(|state| state.output.clone()) .collect(); @@ -477,7 +470,7 @@ impl MediaProvider { let album_title = properties.AlbumTitle()?.to_string(); let album_artist = properties.AlbumArtist()?.to_string(); - session_output.title = title; + 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); From 6a683b649d3421d55fed92c669a02f1c6951e77b Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 22 Nov 2024 14:55:07 +0800 Subject: [PATCH 16/17] final changes --- examples/boilerplate-react-buildless/index.html | 7 +++++-- examples/boilerplate-solid-ts/src/index.tsx | 10 +++------- packages/client-api/src/desktop/desktop-commands.ts | 2 +- .../src/providers/media/create-media-provider.ts | 10 +++++----- .../src/providers/media/media-provider-types.ts | 2 +- 5 files changed, 15 insertions(+), 16 deletions(-) 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 a893add2..f7e79652 100644 --- a/examples/boilerplate-solid-ts/src/index.tsx +++ b/examples/boilerplate-solid-ts/src/index.tsx @@ -27,15 +27,11 @@ 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 d95a272c..14474228 100644 --- a/packages/client-api/src/desktop/desktop-commands.ts +++ b/packages/client-api/src/desktop/desktop-commands.ts @@ -30,7 +30,7 @@ export interface MediaFunction { } export interface MediaControlArgs { - sessionId: string | null; + sessionId?: string; } function startWidget( 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 58becf35..eb95f65d 100644 --- a/packages/client-api/src/providers/media/create-media-provider.ts +++ b/packages/client-api/src/providers/media/create-media-provider.ts @@ -32,7 +32,7 @@ export function createMediaProvider( type: 'media', function: { name: 'play', - args: args ?? { sessionId: null }, + args: args ?? {}, }, }); }, @@ -41,7 +41,7 @@ export function createMediaProvider( type: 'media', function: { name: 'pause', - args: args ?? { sessionId: null }, + args: args ?? {}, }, }); }, @@ -50,7 +50,7 @@ export function createMediaProvider( type: 'media', function: { name: 'toggle_play_pause', - args: args ?? { sessionId: null }, + args: args ?? {}, }, }); }, @@ -59,7 +59,7 @@ export function createMediaProvider( type: 'media', function: { name: 'next', - args: args ?? { sessionId: null }, + args: args ?? {}, }, }); }, @@ -68,7 +68,7 @@ export function createMediaProvider( type: 'media', function: { name: 'previous', - args: args ?? { sessionId: null }, + 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 3ac946e3..b1973cfd 100644 --- a/packages/client-api/src/providers/media/media-provider-types.ts +++ b/packages/client-api/src/providers/media/media-provider-types.ts @@ -17,7 +17,7 @@ export interface MediaOutput { } export interface MediaControlArgs { - sessionId: string; + sessionId?: string; } export interface MediaSession { From df7123e3f2423921a292bca201f5a6339b4fe901 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Fri, 22 Nov 2024 14:58:55 +0800 Subject: [PATCH 17/17] lint fix --- examples/boilerplate-solid-ts/src/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/boilerplate-solid-ts/src/index.tsx b/examples/boilerplate-solid-ts/src/index.tsx index f7e79652..03c0aa35 100644 --- a/examples/boilerplate-solid-ts/src/index.tsx +++ b/examples/boilerplate-solid-ts/src/index.tsx @@ -29,9 +29,7 @@ function App() {
Media: {output.media?.currentSession?.title} - {output.media?.currentSession?.artist} - +
CPU usage: {output.cpu?.usage}