Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle null session in media provider #153

Merged
merged 5 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/boilerplate-react-buildless/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
battery: { type: 'battery' },
memory: { type: 'memory' },
weather: { type: 'weather' },
media: { type: 'media' },
});

createRoot(document.getElementById('root')).render(<App />);
Expand All @@ -49,6 +50,10 @@

return (
<div className="app">
<div class="chip">
Media: {output.media?.session?.title} -
{output.media?.session?.artist}
</div>
<div className="chip">CPU usage: {output.cpu?.usage}</div>
<div className="chip">
Battery charge: {output.battery?.chargePercent}
Expand Down
5 changes: 4 additions & 1 deletion examples/boilerplate-solid-ts/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ function App() {

return (
<div class="app">
<div class="chip">Media: {output.media?.artist}</div>
<div class="chip">
Media: {output.media?.session?.title} -
{output.media?.session?.artist}
</div>
<div class="chip">CPU usage: {output.cpu?.usage}</div>
<div class="chip">
Battery charge: {output.battery?.chargePercent}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import type {

const mediaProviderConfigSchema = z.object({
type: z.literal('media'),
refreshInterval: z.coerce.number().default(5 * 1000),
});

export function createMediaProvider(
Expand Down
10 changes: 7 additions & 3 deletions packages/client-api/src/providers/media/media-provider-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
171 changes: 108 additions & 63 deletions packages/desktop/src/providers/media/media_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};

Expand All @@ -25,10 +25,16 @@ pub struct MediaProviderConfig {}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaOutput {
pub session: Option<MediaSession>,
}

#[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<String>,
pub album_title: Option<String>,
pub album_artist: Option<String>,
pub track_number: u32,
pub start_time: u64,
pub end_time: u64,
Expand All @@ -45,105 +51,138 @@ struct EventTokens {

pub struct MediaProvider {
_config: MediaProviderConfig,
current_session: Arc<Mutex<Option<MediaSession>>>,
event_tokens: Arc<Mutex<Option<EventTokens>>>,
}

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<ProviderResult>,
) {
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<MediaOutput> {
fn media_output(
session: Option<&GsmtcSession>,
) -> anyhow::Result<MediaOutput> {
Ok(MediaOutput {
session: match session {
Some(session) => Self::media_session(session)?,
None => None,
},
})
}

fn media_session(
session: &GsmtcSession,
) -> anyhow::Result<Option<MediaSession>> {
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 =
timeline_properties.EndTime()?.Duration as u64 / 10_000_000;
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<ProviderResult>,
) -> 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(
&current_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 &current_session {
Some(session) => Some(Self::add_session_listeners(
session,
emit_result_tx.clone(),
)?),
None => None,
};

// Emit initial media info.
Self::emit_media_info(&current_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<MediaManager>, _| {
move |session_manager: &Option<GsmtcManager>, _| {
{
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(
&current_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(())
Expand All @@ -158,7 +197,7 @@ impl MediaProvider {
}

fn remove_session_listeners(
session: &MediaSession,
session: &GsmtcSession,
tokens: &EventTokens,
) -> anyhow::Result<()> {
session.RemoveMediaPropertiesChanged(
Expand All @@ -176,17 +215,19 @@ impl MediaProvider {
}

fn add_session_listeners(
session: &MediaSession,
session: &GsmtcSession,
emit_result_tx: Sender<ProviderResult>,
) -> anyhow::Result<EventTokens> {
) -> windows::core::Result<EventTokens> {
debug!("Adding session listeners.");

let media_properties_changed_handler = {
let emit_result_tx = emit_result_tx.clone();

TypedEventHandler::new(move |session: &Option<MediaSession>, _| {
TypedEventHandler::new(move |session: &Option<GsmtcSession>, _| {
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(())
Expand All @@ -196,11 +237,11 @@ impl MediaProvider {
let playback_info_changed_handler = {
let emit_result_tx = emit_result_tx.clone();

TypedEventHandler::new(move |session: &Option<MediaSession>, _| {
TypedEventHandler::new(move |session: &Option<GsmtcSession>, _| {
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(())
Expand All @@ -210,11 +251,11 @@ impl MediaProvider {
let timeline_properties_changed_handler = {
let emit_result_tx = emit_result_tx.clone();

TypedEventHandler::new(move |session: &Option<MediaSession>, _| {
TypedEventHandler::new(move |session: &Option<GsmtcSession>, _| {
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(())
Expand All @@ -241,8 +282,12 @@ impl MediaProvider {
#[async_trait]
impl Provider for MediaProvider {
async fn run(&self, emit_result_tx: Sender<ProviderResult>) {
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());
}
});
}
}