diff --git a/examples/boilerplate-react-buildless/index.html b/examples/boilerplate-react-buildless/index.html index 2fe70fab..ca823438 100644 --- a/examples/boilerplate-react-buildless/index.html +++ b/examples/boilerplate-react-buildless/index.html @@ -36,6 +36,7 @@ battery: { type: 'battery' }, memory: { type: 'memory' }, weather: { type: 'weather' }, + media: { type: 'media' }, }); createRoot(document.getElementById('root')).render(); @@ -49,6 +50,10 @@ return (
+
+ Media: {output.media?.session?.title} - + {output.media?.session?.artist} +
CPU usage: {output.cpu?.usage}
Battery charge: {output.battery?.chargePercent} diff --git a/examples/boilerplate-solid-ts/src/index.tsx b/examples/boilerplate-solid-ts/src/index.tsx index 3c3c64dd..5448a27b 100644 --- a/examples/boilerplate-solid-ts/src/index.tsx +++ b/examples/boilerplate-solid-ts/src/index.tsx @@ -21,7 +21,10 @@ function App() { return (
-
Media: {output.media?.artist}
+
+ Media: {output.media?.session?.title} - + {output.media?.session?.artist} +
CPU usage: {output.cpu?.usage}
Battery charge: {output.battery?.chargePercent} 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 7ae68159..0398bbad 100644 --- a/packages/client-api/src/providers/media/create-media-provider.ts +++ b/packages/client-api/src/providers/media/create-media-provider.ts @@ -9,7 +9,6 @@ import type { const mediaProviderConfigSchema = z.object({ type: z.literal('media'), - refreshInterval: z.coerce.number().default(5 * 1000), }); export function createMediaProvider( 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 77ecd6a2..b6bb1695 100644 --- a/packages/client-api/src/providers/media/media-provider-types.ts +++ b/packages/client-api/src/providers/media/media-provider-types.ts @@ -5,10 +5,14 @@ export interface MediaProviderConfig { } export interface MediaOutput { + session: MediaSession | null; +} + +export interface MediaSession { title: string; - artist: string; - albumTitle: string; - albumArtist: string; + artist: string | null; + albumTitle: string | null; + albumArtist: string | null; trackNumber: number; startTime: number; endTime: number; diff --git a/packages/desktop/src/providers/media/media_provider.rs b/packages/desktop/src/providers/media/media_provider.rs index 4719a5c7..e3f80974 100644 --- a/packages/desktop/src/providers/media/media_provider.rs +++ b/packages/desktop/src/providers/media/media_provider.rs @@ -5,14 +5,14 @@ use std::{ use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc::Sender; -use tracing::debug; +use tokio::{sync::mpsc::Sender, task}; +use tracing::{debug, error}; use windows::{ Foundation::{EventRegistrationToken, TypedEventHandler}, Media::Control::{ - GlobalSystemMediaTransportControlsSession as MediaSession, - GlobalSystemMediaTransportControlsSessionManager as MediaManager, - GlobalSystemMediaTransportControlsSessionPlaybackStatus as MediaPlaybackStatus, + GlobalSystemMediaTransportControlsSession as GsmtcSession, + GlobalSystemMediaTransportControlsSessionManager as GsmtcManager, + GlobalSystemMediaTransportControlsSessionPlaybackStatus as GsmtcPlaybackStatus, }, }; @@ -25,10 +25,16 @@ pub struct MediaProviderConfig {} #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MediaOutput { + pub session: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MediaSession { pub title: String, - pub artist: String, - pub album_title: String, - pub album_artist: String, + pub artist: Option, + pub album_title: Option, + pub album_artist: Option, pub track_number: u32, pub start_time: u64, pub end_time: u64, @@ -45,36 +51,58 @@ struct EventTokens { pub struct MediaProvider { _config: MediaProviderConfig, - current_session: Arc>>, - event_tokens: Arc>>, } impl MediaProvider { pub fn new(config: MediaProviderConfig) -> MediaProvider { - MediaProvider { - _config: config, - current_session: Arc::new(Mutex::new(None)), - event_tokens: Arc::new(Mutex::new(None)), - } + MediaProvider { _config: config } } fn emit_media_info( - session: &MediaSession, + session: Option<&GsmtcSession>, emit_result_tx: Sender, ) { - if let Ok(media_output) = Self::media_output(session) { - let _ = emit_result_tx - .try_send(Ok(ProviderOutput::Media(media_output)).into()); - } + let _ = match Self::media_output(session) { + Ok(media_output) => emit_result_tx + .blocking_send(Ok(ProviderOutput::Media(media_output)).into()), + Err(err) => { + error!("Error retrieving media output: {:?}", err); + emit_result_tx.blocking_send(Err(err).into()) + } + }; } - fn media_output(session: &MediaSession) -> anyhow::Result { + 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 is_playing = - playback_info.PlaybackStatus()? == MediaPlaybackStatus::Playing; + playback_info.PlaybackStatus()? == GsmtcPlaybackStatus::Playing; let start_time = timeline_properties.StartTime()?.Duration as u64 / 10_000_000; let end_time = @@ -82,68 +110,79 @@ impl MediaProvider { let position = timeline_properties.Position()?.Duration as u64 / 10_000_000; - Ok(MediaOutput { - title: media_properties.Title()?.to_string(), - artist: media_properties.Artist()?.to_string(), - album_title: media_properties.AlbumTitle()?.to_string(), - album_artist: media_properties.AlbumArtist()?.to_string(), + 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, - }) + })) } fn create_session_manager( - &self, emit_result_tx: Sender, ) -> anyhow::Result<()> { - // Find the current GSMTC session & add listeners. - let session_manager = MediaManager::RequestAsync()?.get()?; - let current_session = session_manager.GetCurrentSession()?; - let event_tokens = Self::add_session_listeners( - ¤t_session, - emit_result_tx.clone(), - )?; + debug!("Creating media session manager."); - debug!("Media session manager obtained."); + // Find the current GSMTC session & add listeners. + let session_manager = GsmtcManager::RequestAsync()?.get()?; + let current_session = session_manager.GetCurrentSession().ok(); + + let event_tokens = match ¤t_session { + Some(session) => Some(Self::add_session_listeners( + session, + emit_result_tx.clone(), + )?), + None => None, + }; // Emit initial media info. - Self::emit_media_info(¤t_session, emit_result_tx.clone()); + Self::emit_media_info( + current_session.as_ref(), + emit_result_tx.clone(), + ); - *self.current_session.lock().unwrap() = Some(current_session); - *self.event_tokens.lock().unwrap() = Some(event_tokens); + let current_session = Arc::new(Mutex::new(current_session)); + let event_tokens = Arc::new(Mutex::new(event_tokens)); // Clean up & rebind listeners when session changes. - let current_session = self.current_session.clone(); - let event_tokens = self.event_tokens.clone(); let session_changed_handler = TypedEventHandler::new( - move |session_manager: &Option, _| { + move |session_manager: &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 Err(err) = Self::remove_session_listeners( - ¤t_session.as_ref().unwrap(), - event_tokens.as_ref().unwrap(), - ) { - debug!("Error removing media session listeners: {:?}", err); + 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); + } } // Set up new session. let new_session = - MediaManager::RequestAsync()?.get()?.GetCurrentSession()?; + GsmtcManager::RequestAsync()?.get()?.GetCurrentSession()?; let tokens = Self::add_session_listeners( &new_session, emit_result_tx.clone(), + )?; + + Self::emit_media_info( + Some(&new_session), + emit_result_tx.clone(), ); - *event_tokens = tokens.ok(); - Self::emit_media_info(&new_session, emit_result_tx.clone()); *current_session = Some(new_session); + *event_tokens = Some(tokens); } Ok(()) @@ -158,7 +197,7 @@ impl MediaProvider { } fn remove_session_listeners( - session: &MediaSession, + session: &GsmtcSession, tokens: &EventTokens, ) -> anyhow::Result<()> { session.RemoveMediaPropertiesChanged( @@ -176,17 +215,19 @@ impl MediaProvider { } fn add_session_listeners( - session: &MediaSession, + session: &GsmtcSession, emit_result_tx: Sender, - ) -> anyhow::Result { + ) -> windows::core::Result { + debug!("Adding session listeners."); + let media_properties_changed_handler = { let emit_result_tx = emit_result_tx.clone(); - TypedEventHandler::new(move |session: &Option, _| { + TypedEventHandler::new(move |session: &Option, _| { debug!("Media properties changed event triggered."); if let Some(session) = session { - Self::emit_media_info(session, emit_result_tx.clone()); + Self::emit_media_info(Some(session), emit_result_tx.clone()); } Ok(()) @@ -196,11 +237,11 @@ impl MediaProvider { let playback_info_changed_handler = { let emit_result_tx = emit_result_tx.clone(); - TypedEventHandler::new(move |session: &Option, _| { + TypedEventHandler::new(move |session: &Option, _| { debug!("Playback info changed event triggered."); if let Some(session) = session { - Self::emit_media_info(session, emit_result_tx.clone()); + Self::emit_media_info(Some(session), emit_result_tx.clone()); } Ok(()) @@ -210,11 +251,11 @@ impl MediaProvider { let timeline_properties_changed_handler = { let emit_result_tx = emit_result_tx.clone(); - TypedEventHandler::new(move |session: &Option, _| { + TypedEventHandler::new(move |session: &Option, _| { debug!("Timeline properties changed event triggered."); if let Some(session) = session { - Self::emit_media_info(session, emit_result_tx.clone()); + Self::emit_media_info(Some(session), emit_result_tx.clone()); } Ok(()) @@ -241,8 +282,12 @@ impl MediaProvider { #[async_trait] impl Provider for MediaProvider { async fn run(&self, emit_result_tx: Sender) { - if let Err(err) = self.create_session_manager(emit_result_tx.clone()) { - let _ = emit_result_tx.send(Err(err).into()).await; - } + task::spawn_blocking(move || { + if let Err(err) = + Self::create_session_manager(emit_result_tx.clone()) + { + let _ = emit_result_tx.blocking_send(Err(err).into()); + } + }); } }