Skip to content

Commit

Permalink
feat: add media provider (#136)
Browse files Browse the repository at this point in the history
  • Loading branch information
HolbyFPV authored Nov 13, 2024
1 parent c5fe429 commit 485b3bc
Show file tree
Hide file tree
Showing 12 changed files with 331 additions and 2 deletions.
2 changes: 2 additions & 0 deletions examples/boilerplate-solid-ts/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const providers = zebar.createProviderGroup({
battery: { type: 'battery' },
memory: { type: 'memory' },
weather: { type: 'weather' },
media: { type: 'media' },
});

render(() => <App />, document.getElementById('root')!);
Expand All @@ -20,6 +21,7 @@ function App() {

return (
<div class="app">
<div class="chip">Media: {output.media?.artist}</div>
<div class="chip">CPU usage: {output.cpu?.usage}</div>
<div class="chip">
Battery charge: {output.battery?.chargePercent}
Expand Down
9 changes: 9 additions & 0 deletions packages/client-api/src/providers/create-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ import type {
KomorebiProviderConfig,
KomorebiProvider,
} from './komorebi/komorebi-provider-types';
import type {
MediaProviderConfig,
MediaProvider,
} from './media/media-provider-types';
import { createMediaProvider } from './media/create-media-provider';
import { createMemoryProvider } from './memory/create-memory-provider';
import type {
MemoryProviderConfig,
Expand Down Expand Up @@ -64,6 +69,7 @@ export interface ProviderConfigMap {
host: HostProviderConfig;
ip: IpProviderConfig;
komorebi: KomorebiProviderConfig;
media: MediaProviderConfig;
memory: MemoryProviderConfig;
network: NetworkProviderConfig;
weather: WeatherProviderConfig;
Expand All @@ -79,6 +85,7 @@ export interface ProviderMap {
host: HostProvider;
ip: IpProvider;
komorebi: KomorebiProvider;
media: MediaProvider;
memory: MemoryProvider;
network: NetworkProvider;
weather: WeatherProvider;
Expand Down Expand Up @@ -121,6 +128,8 @@ export function createProvider<T extends ProviderConfig>(
return createIpProvider(config) as any;
case 'komorebi':
return createKomorebiProvider(config) as any;
case 'media':
return createMediaProvider(config) as any;
case 'memory':
return createMemoryProvider(config) as any;
case 'network':
Expand Down
1 change: 1 addition & 0 deletions packages/client-api/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './ip/ip-provider-types';
export * from './keyboard/keyboard-provider-types';
export * from './disk/disk-provider-types';
export * from './komorebi/komorebi-provider-types';
export * from './media/media-provider-types';
export * from './memory/memory-provider-types';
export * from './network/network-provider-types';
export * from './weather/weather-provider-types';
Expand Down
29 changes: 29 additions & 0 deletions packages/client-api/src/providers/media/create-media-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { z } from 'zod';
import { createBaseProvider } from '../create-base-provider';
import { onProviderEmit } from '~/desktop';
import type {
MediaOutput,
MediaProvider,
MediaProviderConfig,
} from './media-provider-types';

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

export function createMediaProvider(
config: MediaProviderConfig,
): MediaProvider {
const mergedConfig = mediaProviderConfigSchema.parse(config);

return createBaseProvider(mergedConfig, async queue => {
return onProviderEmit<MediaOutput>(mergedConfig, ({ result }) => {
if ('error' in result) {
queue.error(result.error);
} else {
queue.output(result.output);
}
});
});
}
19 changes: 19 additions & 0 deletions packages/client-api/src/providers/media/media-provider-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Provider } from '../create-base-provider';

export interface MediaProviderConfig {
type: 'media';
}

export interface MediaOutput {
title: string;
artist: string;
albumTitle: string;
albumArtist: string;
trackNumber: number;
startTime: number;
endTime: number;
position: number;
isPlaying: boolean;
}

export type MediaProvider = Provider<MediaProviderConfig, MediaOutput>;
2 changes: 2 additions & 0 deletions packages/desktop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ regex = "1"
[target.'cfg(target_os = "windows")'.dependencies]
komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.28" }
windows = { version = "0.58", features = [
"Foundation",
"Media_Control",
"Win32_Globalization",
"Win32_System_Console",
"Win32_System_SystemServices",
Expand Down
248 changes: 248 additions & 0 deletions packages/desktop/src/providers/media/media_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
use std::{
sync::{Arc, Mutex},
time,
};

use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::Sender;
use tracing::debug;
use windows::{
Foundation::{EventRegistrationToken, TypedEventHandler},
Media::Control::{
GlobalSystemMediaTransportControlsSession as MediaSession,
GlobalSystemMediaTransportControlsSessionManager as MediaManager,
GlobalSystemMediaTransportControlsSessionPlaybackStatus as MediaPlaybackStatus,
},
};

use crate::providers::{Provider, ProviderOutput, ProviderResult};

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MediaProviderConfig {}

#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaOutput {
pub title: String,
pub artist: String,
pub album_title: String,
pub album_artist: String,
pub track_number: u32,
pub start_time: u64,
pub end_time: u64,
pub position: u64,
pub is_playing: bool,
}

#[derive(Clone, Debug)]
struct EventTokens {
playback_info_changed_token: EventRegistrationToken,
media_properties_changed_token: EventRegistrationToken,
timeline_properties_changed_token: EventRegistrationToken,
}

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)),
}
}

fn emit_media_info(
session: &MediaSession,
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());
}
}

fn media_output(session: &MediaSession) -> anyhow::Result<MediaOutput> {
let media_properties = session.TryGetMediaPropertiesAsync()?.get()?;
let timeline_properties = session.GetTimelineProperties()?;
let playback_info = session.GetPlaybackInfo()?;

let is_playing =
playback_info.PlaybackStatus()? == MediaPlaybackStatus::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(),
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!("Media session manager obtained.");

// Emit initial media info.
Self::emit_media_info(&current_session, emit_result_tx.clone());

*self.current_session.lock().unwrap() = Some(current_session);
*self.event_tokens.lock().unwrap() = Some(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>, _| {
{
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);
}

// Set up new session.
let new_session =
MediaManager::RequestAsync()?.get()?.GetCurrentSession()?;

let tokens = Self::add_session_listeners(
&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);
}

Ok(())
},
);

session_manager.CurrentSessionChanged(&session_changed_handler)?;

loop {
std::thread::sleep(time::Duration::from_secs(1));
}
}

fn remove_session_listeners(
session: &MediaSession,
tokens: &EventTokens,
) -> anyhow::Result<()> {
session.RemoveMediaPropertiesChanged(
tokens.media_properties_changed_token,
)?;

session
.RemovePlaybackInfoChanged(tokens.playback_info_changed_token)?;

session.RemoveTimelinePropertiesChanged(
tokens.timeline_properties_changed_token,
)?;

Ok(())
}

fn add_session_listeners(
session: &MediaSession,
emit_result_tx: Sender<ProviderResult>,
) -> anyhow::Result<EventTokens> {
let media_properties_changed_handler = {
let emit_result_tx = emit_result_tx.clone();

TypedEventHandler::new(move |session: &Option<MediaSession>, _| {
debug!("Media properties changed event triggered.");

if let Some(session) = session {
Self::emit_media_info(session, emit_result_tx.clone());
}

Ok(())
})
};

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

TypedEventHandler::new(move |session: &Option<MediaSession>, _| {
debug!("Playback info changed event triggered.");

if let Some(session) = session {
Self::emit_media_info(session, emit_result_tx.clone());
}

Ok(())
})
};

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

TypedEventHandler::new(move |session: &Option<MediaSession>, _| {
debug!("Timeline properties changed event triggered.");

if let Some(session) = session {
Self::emit_media_info(session, emit_result_tx.clone());
}

Ok(())
})
};

let timeline_token = session
.TimelinePropertiesChanged(&timeline_properties_changed_handler)?;
let playback_token =
session.PlaybackInfoChanged(&playback_info_changed_handler)?;
let media_token =
session.MediaPropertiesChanged(&media_properties_changed_handler)?;

Ok({
EventTokens {
playback_info_changed_token: playback_token,
media_properties_changed_token: media_token,
timeline_properties_changed_token: timeline_token,
}
})
}
}

#[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;
}
}
}
3 changes: 3 additions & 0 deletions packages/desktop/src/providers/media/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod media_provider;

pub use media_provider::*;
2 changes: 2 additions & 0 deletions packages/desktop/src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ mod ip;
mod keyboard;
#[cfg(windows)]
mod komorebi;
#[cfg(windows)]
mod media;
mod memory;
mod network;
mod provider;
Expand Down
3 changes: 3 additions & 0 deletions packages/desktop/src/providers/provider_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use super::{
#[cfg(windows)]
use super::{
keyboard::KeyboardProviderConfig, komorebi::KomorebiProviderConfig,
media::MediaProviderConfig,
};

#[derive(Deserialize, Debug)]
Expand All @@ -20,6 +21,8 @@ pub enum ProviderConfig {
Ip(IpProviderConfig),
#[cfg(windows)]
Komorebi(KomorebiProviderConfig),
#[cfg(windows)]
Media(MediaProviderConfig),
Memory(MemoryProviderConfig),
Disk(DiskProviderConfig),
Network(NetworkProviderConfig),
Expand Down
Loading

0 comments on commit 485b3bc

Please sign in to comment.