Skip to content

Commit 485b3bc

Browse files
authored
feat: add media provider (#136)
1 parent c5fe429 commit 485b3bc

File tree

12 files changed

+331
-2
lines changed

12 files changed

+331
-2
lines changed

examples/boilerplate-solid-ts/src/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const providers = zebar.createProviderGroup({
99
battery: { type: 'battery' },
1010
memory: { type: 'memory' },
1111
weather: { type: 'weather' },
12+
media: { type: 'media' },
1213
});
1314

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

2122
return (
2223
<div class="app">
24+
<div class="chip">Media: {output.media?.artist}</div>
2325
<div class="chip">CPU usage: {output.cpu?.usage}</div>
2426
<div class="chip">
2527
Battery charge: {output.battery?.chargePercent}

packages/client-api/src/providers/create-provider.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ import type {
3535
KomorebiProviderConfig,
3636
KomorebiProvider,
3737
} from './komorebi/komorebi-provider-types';
38+
import type {
39+
MediaProviderConfig,
40+
MediaProvider,
41+
} from './media/media-provider-types';
42+
import { createMediaProvider } from './media/create-media-provider';
3843
import { createMemoryProvider } from './memory/create-memory-provider';
3944
import type {
4045
MemoryProviderConfig,
@@ -64,6 +69,7 @@ export interface ProviderConfigMap {
6469
host: HostProviderConfig;
6570
ip: IpProviderConfig;
6671
komorebi: KomorebiProviderConfig;
72+
media: MediaProviderConfig;
6773
memory: MemoryProviderConfig;
6874
network: NetworkProviderConfig;
6975
weather: WeatherProviderConfig;
@@ -79,6 +85,7 @@ export interface ProviderMap {
7985
host: HostProvider;
8086
ip: IpProvider;
8187
komorebi: KomorebiProvider;
88+
media: MediaProvider;
8289
memory: MemoryProvider;
8390
network: NetworkProvider;
8491
weather: WeatherProvider;
@@ -121,6 +128,8 @@ export function createProvider<T extends ProviderConfig>(
121128
return createIpProvider(config) as any;
122129
case 'komorebi':
123130
return createKomorebiProvider(config) as any;
131+
case 'media':
132+
return createMediaProvider(config) as any;
124133
case 'memory':
125134
return createMemoryProvider(config) as any;
126135
case 'network':

packages/client-api/src/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './ip/ip-provider-types';
77
export * from './keyboard/keyboard-provider-types';
88
export * from './disk/disk-provider-types';
99
export * from './komorebi/komorebi-provider-types';
10+
export * from './media/media-provider-types';
1011
export * from './memory/memory-provider-types';
1112
export * from './network/network-provider-types';
1213
export * from './weather/weather-provider-types';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { z } from 'zod';
2+
import { createBaseProvider } from '../create-base-provider';
3+
import { onProviderEmit } from '~/desktop';
4+
import type {
5+
MediaOutput,
6+
MediaProvider,
7+
MediaProviderConfig,
8+
} from './media-provider-types';
9+
10+
const mediaProviderConfigSchema = z.object({
11+
type: z.literal('media'),
12+
refreshInterval: z.coerce.number().default(5 * 1000),
13+
});
14+
15+
export function createMediaProvider(
16+
config: MediaProviderConfig,
17+
): MediaProvider {
18+
const mergedConfig = mediaProviderConfigSchema.parse(config);
19+
20+
return createBaseProvider(mergedConfig, async queue => {
21+
return onProviderEmit<MediaOutput>(mergedConfig, ({ result }) => {
22+
if ('error' in result) {
23+
queue.error(result.error);
24+
} else {
25+
queue.output(result.output);
26+
}
27+
});
28+
});
29+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Provider } from '../create-base-provider';
2+
3+
export interface MediaProviderConfig {
4+
type: 'media';
5+
}
6+
7+
export interface MediaOutput {
8+
title: string;
9+
artist: string;
10+
albumTitle: string;
11+
albumArtist: string;
12+
trackNumber: number;
13+
startTime: number;
14+
endTime: number;
15+
position: number;
16+
isPlaying: boolean;
17+
}
18+
19+
export type MediaProvider = Provider<MediaProviderConfig, MediaOutput>;

packages/desktop/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ regex = "1"
4040
[target.'cfg(target_os = "windows")'.dependencies]
4141
komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.28" }
4242
windows = { version = "0.58", features = [
43+
"Foundation",
44+
"Media_Control",
4345
"Win32_Globalization",
4446
"Win32_System_Console",
4547
"Win32_System_SystemServices",
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
use std::{
2+
sync::{Arc, Mutex},
3+
time,
4+
};
5+
6+
use async_trait::async_trait;
7+
use serde::{Deserialize, Serialize};
8+
use tokio::sync::mpsc::Sender;
9+
use tracing::debug;
10+
use windows::{
11+
Foundation::{EventRegistrationToken, TypedEventHandler},
12+
Media::Control::{
13+
GlobalSystemMediaTransportControlsSession as MediaSession,
14+
GlobalSystemMediaTransportControlsSessionManager as MediaManager,
15+
GlobalSystemMediaTransportControlsSessionPlaybackStatus as MediaPlaybackStatus,
16+
},
17+
};
18+
19+
use crate::providers::{Provider, ProviderOutput, ProviderResult};
20+
21+
#[derive(Deserialize, Debug)]
22+
#[serde(rename_all = "camelCase")]
23+
pub struct MediaProviderConfig {}
24+
25+
#[derive(Debug, Clone, PartialEq, Serialize)]
26+
#[serde(rename_all = "camelCase")]
27+
pub struct MediaOutput {
28+
pub title: String,
29+
pub artist: String,
30+
pub album_title: String,
31+
pub album_artist: String,
32+
pub track_number: u32,
33+
pub start_time: u64,
34+
pub end_time: u64,
35+
pub position: u64,
36+
pub is_playing: bool,
37+
}
38+
39+
#[derive(Clone, Debug)]
40+
struct EventTokens {
41+
playback_info_changed_token: EventRegistrationToken,
42+
media_properties_changed_token: EventRegistrationToken,
43+
timeline_properties_changed_token: EventRegistrationToken,
44+
}
45+
46+
pub struct MediaProvider {
47+
_config: MediaProviderConfig,
48+
current_session: Arc<Mutex<Option<MediaSession>>>,
49+
event_tokens: Arc<Mutex<Option<EventTokens>>>,
50+
}
51+
52+
impl MediaProvider {
53+
pub fn new(config: MediaProviderConfig) -> MediaProvider {
54+
MediaProvider {
55+
_config: config,
56+
current_session: Arc::new(Mutex::new(None)),
57+
event_tokens: Arc::new(Mutex::new(None)),
58+
}
59+
}
60+
61+
fn emit_media_info(
62+
session: &MediaSession,
63+
emit_result_tx: Sender<ProviderResult>,
64+
) {
65+
if let Ok(media_output) = Self::media_output(session) {
66+
let _ = emit_result_tx
67+
.try_send(Ok(ProviderOutput::Media(media_output)).into());
68+
}
69+
}
70+
71+
fn media_output(session: &MediaSession) -> anyhow::Result<MediaOutput> {
72+
let media_properties = session.TryGetMediaPropertiesAsync()?.get()?;
73+
let timeline_properties = session.GetTimelineProperties()?;
74+
let playback_info = session.GetPlaybackInfo()?;
75+
76+
let is_playing =
77+
playback_info.PlaybackStatus()? == MediaPlaybackStatus::Playing;
78+
let start_time =
79+
timeline_properties.StartTime()?.Duration as u64 / 10_000_000;
80+
let end_time =
81+
timeline_properties.EndTime()?.Duration as u64 / 10_000_000;
82+
let position =
83+
timeline_properties.Position()?.Duration as u64 / 10_000_000;
84+
85+
Ok(MediaOutput {
86+
title: media_properties.Title()?.to_string(),
87+
artist: media_properties.Artist()?.to_string(),
88+
album_title: media_properties.AlbumTitle()?.to_string(),
89+
album_artist: media_properties.AlbumArtist()?.to_string(),
90+
track_number: media_properties.TrackNumber()? as u32,
91+
start_time,
92+
end_time,
93+
position,
94+
is_playing,
95+
})
96+
}
97+
98+
fn create_session_manager(
99+
&self,
100+
emit_result_tx: Sender<ProviderResult>,
101+
) -> anyhow::Result<()> {
102+
// Find the current GSMTC session & add listeners.
103+
let session_manager = MediaManager::RequestAsync()?.get()?;
104+
let current_session = session_manager.GetCurrentSession()?;
105+
let event_tokens = Self::add_session_listeners(
106+
&current_session,
107+
emit_result_tx.clone(),
108+
)?;
109+
110+
debug!("Media session manager obtained.");
111+
112+
// Emit initial media info.
113+
Self::emit_media_info(&current_session, emit_result_tx.clone());
114+
115+
*self.current_session.lock().unwrap() = Some(current_session);
116+
*self.event_tokens.lock().unwrap() = Some(event_tokens);
117+
118+
// Clean up & rebind listeners when session changes.
119+
let current_session = self.current_session.clone();
120+
let event_tokens = self.event_tokens.clone();
121+
let session_changed_handler = TypedEventHandler::new(
122+
move |session_manager: &Option<MediaManager>, _| {
123+
{
124+
let mut current_session = current_session.lock().unwrap();
125+
let mut event_tokens = event_tokens.lock().unwrap();
126+
127+
// Remove listeners from the previous session.
128+
if let Err(err) = Self::remove_session_listeners(
129+
&current_session.as_ref().unwrap(),
130+
event_tokens.as_ref().unwrap(),
131+
) {
132+
debug!("Error removing media session listeners: {:?}", err);
133+
}
134+
135+
// Set up new session.
136+
let new_session =
137+
MediaManager::RequestAsync()?.get()?.GetCurrentSession()?;
138+
139+
let tokens = Self::add_session_listeners(
140+
&new_session,
141+
emit_result_tx.clone(),
142+
);
143+
*event_tokens = tokens.ok();
144+
145+
Self::emit_media_info(&new_session, emit_result_tx.clone());
146+
*current_session = Some(new_session);
147+
}
148+
149+
Ok(())
150+
},
151+
);
152+
153+
session_manager.CurrentSessionChanged(&session_changed_handler)?;
154+
155+
loop {
156+
std::thread::sleep(time::Duration::from_secs(1));
157+
}
158+
}
159+
160+
fn remove_session_listeners(
161+
session: &MediaSession,
162+
tokens: &EventTokens,
163+
) -> anyhow::Result<()> {
164+
session.RemoveMediaPropertiesChanged(
165+
tokens.media_properties_changed_token,
166+
)?;
167+
168+
session
169+
.RemovePlaybackInfoChanged(tokens.playback_info_changed_token)?;
170+
171+
session.RemoveTimelinePropertiesChanged(
172+
tokens.timeline_properties_changed_token,
173+
)?;
174+
175+
Ok(())
176+
}
177+
178+
fn add_session_listeners(
179+
session: &MediaSession,
180+
emit_result_tx: Sender<ProviderResult>,
181+
) -> anyhow::Result<EventTokens> {
182+
let media_properties_changed_handler = {
183+
let emit_result_tx = emit_result_tx.clone();
184+
185+
TypedEventHandler::new(move |session: &Option<MediaSession>, _| {
186+
debug!("Media properties changed event triggered.");
187+
188+
if let Some(session) = session {
189+
Self::emit_media_info(session, emit_result_tx.clone());
190+
}
191+
192+
Ok(())
193+
})
194+
};
195+
196+
let playback_info_changed_handler = {
197+
let emit_result_tx = emit_result_tx.clone();
198+
199+
TypedEventHandler::new(move |session: &Option<MediaSession>, _| {
200+
debug!("Playback info changed event triggered.");
201+
202+
if let Some(session) = session {
203+
Self::emit_media_info(session, emit_result_tx.clone());
204+
}
205+
206+
Ok(())
207+
})
208+
};
209+
210+
let timeline_properties_changed_handler = {
211+
let emit_result_tx = emit_result_tx.clone();
212+
213+
TypedEventHandler::new(move |session: &Option<MediaSession>, _| {
214+
debug!("Timeline properties changed event triggered.");
215+
216+
if let Some(session) = session {
217+
Self::emit_media_info(session, emit_result_tx.clone());
218+
}
219+
220+
Ok(())
221+
})
222+
};
223+
224+
let timeline_token = session
225+
.TimelinePropertiesChanged(&timeline_properties_changed_handler)?;
226+
let playback_token =
227+
session.PlaybackInfoChanged(&playback_info_changed_handler)?;
228+
let media_token =
229+
session.MediaPropertiesChanged(&media_properties_changed_handler)?;
230+
231+
Ok({
232+
EventTokens {
233+
playback_info_changed_token: playback_token,
234+
media_properties_changed_token: media_token,
235+
timeline_properties_changed_token: timeline_token,
236+
}
237+
})
238+
}
239+
}
240+
241+
#[async_trait]
242+
impl Provider for MediaProvider {
243+
async fn run(&self, emit_result_tx: Sender<ProviderResult>) {
244+
if let Err(err) = self.create_session_manager(emit_result_tx.clone()) {
245+
let _ = emit_result_tx.send(Err(err).into()).await;
246+
}
247+
}
248+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mod media_provider;
2+
3+
pub use media_provider::*;

packages/desktop/src/providers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ mod ip;
77
mod keyboard;
88
#[cfg(windows)]
99
mod komorebi;
10+
#[cfg(windows)]
11+
mod media;
1012
mod memory;
1113
mod network;
1214
mod provider;

packages/desktop/src/providers/provider_config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use super::{
99
#[cfg(windows)]
1010
use super::{
1111
keyboard::KeyboardProviderConfig, komorebi::KomorebiProviderConfig,
12+
media::MediaProviderConfig,
1213
};
1314

1415
#[derive(Deserialize, Debug)]
@@ -20,6 +21,8 @@ pub enum ProviderConfig {
2021
Ip(IpProviderConfig),
2122
#[cfg(windows)]
2223
Komorebi(KomorebiProviderConfig),
24+
#[cfg(windows)]
25+
Media(MediaProviderConfig),
2326
Memory(MemoryProviderConfig),
2427
Disk(DiskProviderConfig),
2528
Network(NetworkProviderConfig),

0 commit comments

Comments
 (0)