From 42a14a715a28c86d1d694546f340de8460c887c1 Mon Sep 17 00:00:00 2001 From: holby Date: Sat, 9 Nov 2024 20:07:17 -0500 Subject: [PATCH 01/22] WIP --- examples/boilerplate-solid-ts/dist/index.html | 10 +- .../providers/audio/audio-provider-types.ts | 13 ++ .../providers/audio/create-audio-provider.ts | 31 ++++ .../src/providers/create-provider.ts | 9 ++ packages/desktop/Cargo.toml | 3 + .../src/providers/audio/audio_provider.rs | 133 ++++++++++++++++++ packages/desktop/src/providers/audio/mod.rs | 4 + packages/desktop/src/providers/mod.rs | 2 + .../desktop/src/providers/provider_output.rs | 5 +- 9 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 packages/client-api/src/providers/audio/audio-provider-types.ts create mode 100644 packages/client-api/src/providers/audio/create-audio-provider.ts create mode 100644 packages/desktop/src/providers/audio/audio_provider.rs create mode 100644 packages/desktop/src/providers/audio/mod.rs diff --git a/examples/boilerplate-solid-ts/dist/index.html b/examples/boilerplate-solid-ts/dist/index.html index 5c6c9a50..e838057c 100644 --- a/examples/boilerplate-solid-ts/dist/index.html +++ b/examples/boilerplate-solid-ts/dist/index.html @@ -3,13 +3,13 @@ + Zebar + + -

- Boilerplate for SolidJS with TypeScript. Run npm i and - npm run dev in the solid-ts directory to - run this example. -

+ +
diff --git a/packages/client-api/src/providers/audio/audio-provider-types.ts b/packages/client-api/src/providers/audio/audio-provider-types.ts new file mode 100644 index 00000000..f1a9d229 --- /dev/null +++ b/packages/client-api/src/providers/audio/audio-provider-types.ts @@ -0,0 +1,13 @@ +import type { Provider } from '../create-base-provider'; + +export interface AudioProviderConfig { + type: 'audio'; +} + +export type AudioProvider = Provider; + +export interface AudioOutput { + volume: number; + currentDevice: string; +} + diff --git a/packages/client-api/src/providers/audio/create-audio-provider.ts b/packages/client-api/src/providers/audio/create-audio-provider.ts new file mode 100644 index 00000000..27ef0d0d --- /dev/null +++ b/packages/client-api/src/providers/audio/create-audio-provider.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; + +import { createBaseProvider } from '../create-base-provider'; +import { onProviderEmit } from '~/desktop'; +import type { + AudioOutput, + AudioProvider, + AudioProviderConfig +} from './audio-provider-types'; + +const audioProviderConfigSchema = z.object({ + type: z.literal('audio'), + refreshInterval: z.coerce.number().default(60 * 1000), +}); + +export function createAudioProvider( + config: AudioProviderConfig, +): AudioProvider { + const mergedConfig = audioProviderConfigSchema.parse(config); + + return createBaseProvider(mergedConfig, async queue => { + return onProviderEmit(mergedConfig, ({ result }) => { + if ('error' in result) { + queue.error(result.error); + } else { + queue.output(result.output); + } + }); + }); +} + diff --git a/packages/client-api/src/providers/create-provider.ts b/packages/client-api/src/providers/create-provider.ts index 5a9adeaa..1aebc531 100644 --- a/packages/client-api/src/providers/create-provider.ts +++ b/packages/client-api/src/providers/create-provider.ts @@ -1,3 +1,8 @@ +import { createAudioProvider } from './audio/create-audio-provider'; +import type { + AudioProviderConfig, + AudioProvider, +} from './audio/audio-provider-types'; import { createBatteryProvider } from './battery/create-battery-provider'; import type { BatteryProviderConfig, @@ -57,6 +62,7 @@ import type { } from './disk/disk-provider-types'; export interface ProviderConfigMap { + audio: AudioProviderConfig; battery: BatteryProviderConfig; cpu: CpuProviderConfig; date: DateProviderConfig; @@ -72,6 +78,7 @@ export interface ProviderConfigMap { } export interface ProviderMap { + audio: AudioProvider; battery: BatteryProvider; cpu: CpuProvider; date: DateProvider; @@ -107,6 +114,8 @@ export function createProvider( config: T, ): ProviderMap[T['type']] { switch (config.type) { + case 'audio': + return createAudioProvider(config) as any; case 'battery': return createBatteryProvider(config) as any; case 'cpu': diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 1b56d76b..61d53996 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -41,6 +41,9 @@ regex = "1" komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.28" } windows = { version = "0.58", features = [ "Win32_Globalization", + "Win32_Media", + "Win32_Media_Audio", + "Win32_Media_Audio_Endpoints", "Win32_System_Console", "Win32_System_SystemServices", "Win32_UI_WindowsAndMessaging", diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs new file mode 100644 index 00000000..c4f82479 --- /dev/null +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -0,0 +1,133 @@ +use windows::{ + core::*, + Win32::{ + Media::Audio::{ + eConsole, eRender, IMMDevice, IMMDeviceEnumerator, + IMMNotificationClient, MMDeviceEnumerator, + }, + System::Com::{ + CoCreateInstance, CoInitializeEx, CoUninitialize, + StructuredStorage::PropVariantClear, CLSCTX_ALL, + COINIT_MULTITHREADED, + }, + }, +}; + +fn main() -> Result<()> { + // Initialize COM for the current thread + unsafe { CoInitializeEx(std::ptr::null_mut(), COINIT_MULTITHREADED)? }; + + // Create the device enumerator + let device_enumerator: IMMDeviceEnumerator = + unsafe { CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)? }; + + // Register the notification callback + let callback = AudioDeviceNotificationCallback::new(); + let callback_interface: IMMNotificationClient = callback.into(); + + unsafe { + device_enumerator + .RegisterEndpointNotificationCallback(&callback_interface)? + }; + + println!("Listening for audio device changes... Press Ctrl+C to exit."); + + // Keep the application running to listen for events + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + } + + // Normally, we would also unregister the callback and uninitialize COM + // here, but this loop will run indefinitely, so those steps are + // omitted. +} + +struct AudioDeviceNotificationCallback; + +impl AudioDeviceNotificationCallback { + fn new() -> Self { + Self {} + } + + fn print_default_device_name(&self) -> Result<()> { + // Create a new instance of IMMDeviceEnumerator to get the default + // audio device + let device_enumerator: IMMDeviceEnumerator = + unsafe { CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)? }; + let default_device = unsafe { + device_enumerator.GetDefaultAudioEndpoint(eRender, eConsole)? + }; + + // Retrieve the friendly name from the device's property store + let property_store = unsafe { + default_device + .OpenPropertyStore(windows::Win32::System::Com::STGM_READ)? + }; + let mut prop_value = PROPVARIANT::default(); + unsafe { + property_store + .GetValue(&PKEY_Device_FriendlyName, &mut prop_value)? + }; + + // Convert the PROPVARIANT to a Rust string and print it + if let Some(friendly_name_ptr) = + unsafe { prop_value.Anonymous.Anonymous.pwszVal.0.as_ref() } + { + let friendly_name = + unsafe { widestring::U16CStr::from_ptr_str(friendly_name_ptr) } + .to_string_lossy(); + println!( + "Current default audio device changed to: {}", + friendly_name + ); + } + + // Clear the PROPVARIANT to prevent memory leaks + unsafe { PropVariantClear(&mut prop_value)? }; + + Ok(()) + } +} + +impl IMMNotificationClient_Impl for AudioDeviceNotificationCallback { + fn OnDefaultDeviceChanged( + &self, + _flow: windows::Win32::Media::Audio::EDataFlow, + _role: windows::Win32::Media::Audio::ERole, + _pwstr_device_id: &windows::core::PCWSTR, + ) -> windows::Win32::Foundation::HRESULT { + self + .print_default_device_name() + .unwrap_or_else(|e| eprintln!("Error: {:?}", e)); + windows::Win32::Foundation::S_OK + } + + // Implement other methods to complete the interface, but leave them + // empty if they’re not needed + fn OnDeviceStateChanged( + &self, + _: &windows::core::PCWSTR, + _: u32, + ) -> windows::Win32::Foundation::HRESULT { + windows::Win32::Foundation::S_OK + } + fn OnDeviceAdded( + &self, + _: &windows::core::PCWSTR, + ) -> windows::Win32::Foundation::HRESULT { + windows::Win32::Foundation::S_OK + } + fn OnDeviceRemoved( + &self, + _: &windows::core::PCWSTR, + ) -> windows::Win32::Foundation::HRESULT { + windows::Win32::Foundation::S_OK + } + fn OnPropertyValueChanged( + &self, + _: &windows::core::PCWSTR, + _: &windows::Win32::UI::Shell::PropertiesSystem::PROPERTYKEY, + ) -> windows::Win32::Foundation::HRESULT { + windows::Win32::Foundation::S_OK + } +} diff --git a/packages/desktop/src/providers/audio/mod.rs b/packages/desktop/src/providers/audio/mod.rs new file mode 100644 index 00000000..cd9acb94 --- /dev/null +++ b/packages/desktop/src/providers/audio/mod.rs @@ -0,0 +1,4 @@ + +mod audio_provider; + +pub use audio_provider::*; diff --git a/packages/desktop/src/providers/mod.rs b/packages/desktop/src/providers/mod.rs index dce7a9cc..79f4f5ea 100644 --- a/packages/desktop/src/providers/mod.rs +++ b/packages/desktop/src/providers/mod.rs @@ -14,6 +14,8 @@ mod provider_config; mod provider_manager; mod provider_output; mod provider_ref; +#[cfg(windows)] +mod audio; mod weather; pub use provider::*; diff --git a/packages/desktop/src/providers/provider_output.rs b/packages/desktop/src/providers/provider_output.rs index 4ef23ab7..89248194 100644 --- a/packages/desktop/src/providers/provider_output.rs +++ b/packages/desktop/src/providers/provider_output.rs @@ -1,8 +1,8 @@ use serde::Serialize; use super::{ - battery::BatteryOutput, cpu::CpuOutput, disk::DiskOutput, - host::HostOutput, ip::IpOutput, memory::MemoryOutput, + audio::AudioOutput, battery::BatteryOutput, cpu::CpuOutput, + disk::DiskOutput, host::HostOutput, ip::IpOutput, memory::MemoryOutput, network::NetworkOutput, weather::WeatherOutput, }; #[cfg(windows)] @@ -11,6 +11,7 @@ use super::{keyboard::KeyboardOutput, komorebi::KomorebiOutput}; #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(untagged)] pub enum ProviderOutput { + Audio(AudioOutput), Battery(BatteryOutput), Cpu(CpuOutput), Host(HostOutput), From f12bd0bd198511a253554bcf5d8d46c74742add5 Mon Sep 17 00:00:00 2001 From: holby Date: Mon, 11 Nov 2024 18:02:15 -0500 Subject: [PATCH 02/22] compiles but does not work --- Cargo.lock | 3 +- examples/boilerplate-solid-ts/dist/index.html | 2 +- examples/boilerplate-solid-ts/src/index.tsx | 2 + packages/desktop/Cargo.toml | 2 + .../src/providers/audio/audio_provider.rs | 234 ++++++++++-------- .../desktop/src/providers/provider_config.rs | 7 +- .../desktop/src/providers/provider_ref.rs | 13 +- 7 files changed, 144 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 29afaccf..a6b5b0af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -6809,6 +6809,7 @@ dependencies = [ "tracing", "tracing-subscriber", "windows 0.58.0", + "windows-core 0.58.0", ] [[package]] diff --git a/examples/boilerplate-solid-ts/dist/index.html b/examples/boilerplate-solid-ts/dist/index.html index e838057c..53d75ec6 100644 --- a/examples/boilerplate-solid-ts/dist/index.html +++ b/examples/boilerplate-solid-ts/dist/index.html @@ -5,7 +5,7 @@ Zebar - + diff --git a/examples/boilerplate-solid-ts/src/index.tsx b/examples/boilerplate-solid-ts/src/index.tsx index f86d90b5..ecd2eea4 100644 --- a/examples/boilerplate-solid-ts/src/index.tsx +++ b/examples/boilerplate-solid-ts/src/index.tsx @@ -5,6 +5,7 @@ import { createStore } from 'solid-js/store'; import * as zebar from 'zebar'; const providers = zebar.createProviderGroup({ + audio: { type: 'audio' }, cpu: { type: 'cpu' }, battery: { type: 'battery' }, memory: { type: 'memory' }, @@ -20,6 +21,7 @@ function App() { return (
+
Audio volume: {output.audio?.volume}
CPU usage: {output.cpu?.usage}
Battery charge: {output.battery?.chargePercent} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 61d53996..b80c0464 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -39,7 +39,9 @@ regex = "1" [target.'cfg(target_os = "windows")'.dependencies] komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.28" } +windows-core = "0.58" windows = { version = "0.58", features = [ + "implement", "Win32_Globalization", "Win32_Media", "Win32_Media_Audio", diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index c4f82479..0602f84d 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -1,133 +1,149 @@ -use windows::{ - core::*, - Win32::{ - Media::Audio::{ - eConsole, eRender, IMMDevice, IMMDeviceEnumerator, - IMMNotificationClient, MMDeviceEnumerator, - }, - System::Com::{ - CoCreateInstance, CoInitializeEx, CoUninitialize, - StructuredStorage::PropVariantClear, CLSCTX_ALL, - COINIT_MULTITHREADED, +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc::Sender; +use windows::Win32::{ + Media::Audio::{ + eMultimedia, eRender, + Endpoints::{ + IAudioEndpointVolume, IAudioEndpointVolumeCallback, + IAudioEndpointVolumeCallback_Impl, }, + IAudioSessionControl, IAudioSessionNotification, + IAudioSessionNotification_Impl, IMMDeviceEnumerator, + MMDeviceEnumerator, + }, + System::Com::{ + CoCreateInstance, CoInitializeEx, CLSCTX_ALL, COINIT_MULTITHREADED, }, }; -fn main() -> Result<()> { - // Initialize COM for the current thread - unsafe { CoInitializeEx(std::ptr::null_mut(), COINIT_MULTITHREADED)? }; - - // Create the device enumerator - let device_enumerator: IMMDeviceEnumerator = - unsafe { CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)? }; - - // Register the notification callback - let callback = AudioDeviceNotificationCallback::new(); - let callback_interface: IMMNotificationClient = callback.into(); +use crate::providers::{Provider, ProviderOutput, ProviderResult}; - unsafe { - device_enumerator - .RegisterEndpointNotificationCallback(&callback_interface)? - }; +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AudioProviderConfig {} - println!("Listening for audio device changes... Press Ctrl+C to exit."); - - // Keep the application running to listen for events - loop { - std::thread::sleep(std::time::Duration::from_secs(1)); - } - - // Normally, we would also unregister the callback and uninitialize COM - // here, but this loop will run indefinitely, so those steps are - // omitted. +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AudioOutput { + pub device: String, + pub volume: u8, } -struct AudioDeviceNotificationCallback; +pub struct AudioProvider { + _config: AudioProviderConfig, +} -impl AudioDeviceNotificationCallback { - fn new() -> Self { - Self {} +impl AudioProvider { + pub fn new(config: AudioProviderConfig) -> Self { + Self { _config: config } } - fn print_default_device_name(&self) -> Result<()> { - // Create a new instance of IMMDeviceEnumerator to get the default - // audio device - let device_enumerator: IMMDeviceEnumerator = - unsafe { CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)? }; - let default_device = unsafe { - device_enumerator.GetDefaultAudioEndpoint(eRender, eConsole)? - }; - - // Retrieve the friendly name from the device's property store - let property_store = unsafe { - default_device - .OpenPropertyStore(windows::Win32::System::Com::STGM_READ)? - }; - let mut prop_value = PROPVARIANT::default(); + fn create_audio_manager( + &self, + emit_result_tx: Sender, + ) -> anyhow::Result<()> { unsafe { - property_store - .GetValue(&PKEY_Device_FriendlyName, &mut prop_value)? - }; - - // Convert the PROPVARIANT to a Rust string and print it - if let Some(friendly_name_ptr) = - unsafe { prop_value.Anonymous.Anonymous.pwszVal.0.as_ref() } - { - let friendly_name = - unsafe { widestring::U16CStr::from_ptr_str(friendly_name_ptr) } - .to_string_lossy(); - println!( - "Current default audio device changed to: {}", - friendly_name - ); + // Initialize COM library + let _ = + CoInitializeEx(Some(std::ptr::null_mut()), COINIT_MULTITHREADED); + + // Get the audio endpoint volume interface + let enumerator: IMMDeviceEnumerator = + CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)?; + let default_device = + enumerator.GetDefaultAudioEndpoint(eRender, eMultimedia)?; + let endpoint_volume: IAudioEndpointVolume = + default_device.Activate(CLSCTX_ALL, None)?; + + let device_id = default_device.GetId()?.to_string()?; + println!("Default audio render device: {}", device_id.clone()); + + // Register the volume change callback + let device_volume_callback = + IAudioEndpointVolumeCallback::from(MediaDeviceEventHandler {}); + + endpoint_volume + .RegisterControlChangeNotify(&device_volume_callback)?; } - // Clear the PROPVARIANT to prevent memory leaks - unsafe { PropVariantClear(&mut prop_value)? }; - - Ok(()) + loop { + // tx randomized data to test + let output = AudioOutput { + device: "default".to_string(), + volume: 50, + }; + emit_result_tx.try_send(Ok(ProviderOutput::Audio(output)).into())?; + std::thread::sleep(std::time::Duration::from_secs(1)); + } } } -impl IMMNotificationClient_Impl for AudioDeviceNotificationCallback { - fn OnDefaultDeviceChanged( - &self, - _flow: windows::Win32::Media::Audio::EDataFlow, - _role: windows::Win32::Media::Audio::ERole, - _pwstr_device_id: &windows::core::PCWSTR, - ) -> windows::Win32::Foundation::HRESULT { - self - .print_default_device_name() - .unwrap_or_else(|e| eprintln!("Error: {:?}", e)); - windows::Win32::Foundation::S_OK +#[async_trait] +impl Provider for AudioProvider { + async fn run(&self, emit_result_tx: Sender) { + if let Err(err) = self.create_audio_manager(emit_result_tx.clone()) { + emit_result_tx + .send(Err(err).into()) + .await + .expect("Erroring emitting media provider err."); + } } +} - // Implement other methods to complete the interface, but leave them - // empty if they’re not needed - fn OnDeviceStateChanged( - &self, - _: &windows::core::PCWSTR, - _: u32, - ) -> windows::Win32::Foundation::HRESULT { - windows::Win32::Foundation::S_OK - } - fn OnDeviceAdded( - &self, - _: &windows::core::PCWSTR, - ) -> windows::Win32::Foundation::HRESULT { - windows::Win32::Foundation::S_OK - } - fn OnDeviceRemoved( +#[windows::core::implement( + IAudioEndpointVolumeCallback, + IAudioSessionNotification +)] +struct MediaDeviceEventHandler {} + +impl IAudioEndpointVolumeCallback_Impl for MediaDeviceEventHandler_Impl { + fn OnNotify( &self, - _: &windows::core::PCWSTR, - ) -> windows::Win32::Foundation::HRESULT { - windows::Win32::Foundation::S_OK + data: *mut windows::Win32::Media::Audio::AUDIO_VOLUME_NOTIFICATION_DATA, + ) -> windows_core::Result<()> { + println!("Volume notification"); + if let Some(data) = unsafe { data.as_ref() } { + println!("Volume notification: {}", data.fMasterVolume,); + } + Ok(()) } - fn OnPropertyValueChanged( +} + +impl IAudioSessionNotification_Impl for MediaDeviceEventHandler_Impl { + fn OnSessionCreated( &self, - _: &windows::core::PCWSTR, - _: &windows::Win32::UI::Shell::PropertiesSystem::PROPERTYKEY, - ) -> windows::Win32::Foundation::HRESULT { - windows::Win32::Foundation::S_OK + _new_session: Option<&IAudioSessionControl>, + ) -> windows::core::Result<()> { + println!("SESSION CREATED!"); + let name = unsafe { + _new_session + .unwrap() + .GetDisplayName() + .unwrap() + .to_string() + .unwrap() + }; + println!("New session created: {}", name); + Ok(()) } } + +// this works for current device changes +// but typedeventhandlers cant be used for volume updates +// ----------------------------------------------------------- +// let handler = TypedEventHandler::::new( move |_: +// &Option, args: +// &Option| { println!(" +// Default audio render device changed"); let device_id = +// args.as_ref().unwrap().Id().unwrap(); let device_info = +// DeviceInformation::CreateFromIdAsync(&device_id) .unwrap() +// .get() +// .unwrap(); +// let device_name = device_info.Name().unwrap(); +// println!("New default audio render device: {}", device_name); +// Ok(()) +// }, +// ); +// MediaDevice::DefaultAudioRenderDeviceChanged(&handler)?; diff --git a/packages/desktop/src/providers/provider_config.rs b/packages/desktop/src/providers/provider_config.rs index 4410bccc..57ef8f19 100644 --- a/packages/desktop/src/providers/provider_config.rs +++ b/packages/desktop/src/providers/provider_config.rs @@ -1,10 +1,7 @@ use serde::Deserialize; use super::{ - battery::BatteryProviderConfig, cpu::CpuProviderConfig, - disk::DiskProviderConfig, host::HostProviderConfig, - ip::IpProviderConfig, memory::MemoryProviderConfig, - network::NetworkProviderConfig, weather::WeatherProviderConfig, + audio::AudioProviderConfig, battery::BatteryProviderConfig, cpu::CpuProviderConfig, disk::DiskProviderConfig, host::HostProviderConfig, ip::IpProviderConfig, memory::MemoryProviderConfig, network::NetworkProviderConfig, weather::WeatherProviderConfig }; #[cfg(windows)] use super::{ @@ -14,6 +11,8 @@ use super::{ #[derive(Deserialize, Debug)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ProviderConfig { + #[cfg(windows)] + Audio(AudioProviderConfig), Battery(BatteryProviderConfig), Cpu(CpuProviderConfig), Host(HostProviderConfig), diff --git a/packages/desktop/src/providers/provider_ref.rs b/packages/desktop/src/providers/provider_ref.rs index 9c72f062..7a5ec438 100644 --- a/packages/desktop/src/providers/provider_ref.rs +++ b/packages/desktop/src/providers/provider_ref.rs @@ -11,10 +11,11 @@ use tokio::{ use tracing::{info, warn}; use super::{ - battery::BatteryProvider, cpu::CpuProvider, disk::DiskProvider, - host::HostProvider, ip::IpProvider, memory::MemoryProvider, - network::NetworkProvider, weather::WeatherProvider, Provider, - ProviderConfig, ProviderOutput, SharedProviderState, + audio::AudioProvider, battery::BatteryProvider, cpu::CpuProvider, + disk::DiskProvider, host::HostProvider, ip::IpProvider, + memory::MemoryProvider, network::NetworkProvider, + weather::WeatherProvider, Provider, ProviderConfig, ProviderOutput, + SharedProviderState, }; #[cfg(windows)] use super::{keyboard::KeyboardProvider, komorebi::KomorebiProvider}; @@ -161,6 +162,10 @@ impl ProviderRef { shared_state: SharedProviderState, ) -> anyhow::Result> { let provider: Box = match config { + #[cfg(windows)] + ProviderConfig::Audio(config) => { + Box::new(AudioProvider::new(config)) + } ProviderConfig::Battery(config) => { Box::new(BatteryProvider::new(config)) } From fedd5014d82663463408e24c1df332c31fc7bf08 Mon Sep 17 00:00:00 2001 From: holby Date: Mon, 11 Nov 2024 21:49:49 -0500 Subject: [PATCH 03/22] functional event handlers. emit not working yet --- .../src/providers/audio/audio_provider.rs | 84 ++++++++++--------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index 0602f84d..6a742b41 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -1,6 +1,8 @@ +use std::sync::OnceLock; + use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc::Sender; +use tokio::sync::mpsc::{self, Sender}; use windows::Win32::{ Media::Audio::{ eMultimedia, eRender, @@ -19,6 +21,11 @@ use windows::Win32::{ use crate::providers::{Provider, ProviderOutput, ProviderResult}; +static PROVIDER_TX: OnceLock> = + OnceLock::new(); + +static AUDIO_STATUS: OnceLock = OnceLock::new(); + #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct AudioProviderConfig {} @@ -27,7 +34,7 @@ pub struct AudioProviderConfig {} #[serde(rename_all = "camelCase")] pub struct AudioOutput { pub device: String, - pub volume: u8, + pub volume: f32, } pub struct AudioProvider { @@ -39,14 +46,30 @@ impl AudioProvider { Self { _config: config } } + fn emit_volume() { + let tx = PROVIDER_TX.get().expect("Error getting provider tx"); + let output = AUDIO_STATUS.get().expect("Error getting audio status"); + tx.try_send(Ok(ProviderOutput::Audio(output.clone())).into()) + .expect("Error sending audio status"); + } + fn create_audio_manager( &self, emit_result_tx: Sender, ) -> anyhow::Result<()> { + PROVIDER_TX + .set(emit_result_tx.clone()) + .expect("Error setting provider tx in focused window provider"); + + // TODO is this the best way to initialize this + let _ = AUDIO_STATUS.set(AudioOutput { + device: "n/a".to_string(), + volume: 0.0, + }); + unsafe { // Initialize COM library - let _ = - CoInitializeEx(Some(std::ptr::null_mut()), COINIT_MULTITHREADED); + let _ = CoInitializeEx(None, COINIT_MULTITHREADED); // Get the audio endpoint volume interface let enumerator: IMMDeviceEnumerator = @@ -56,25 +79,15 @@ impl AudioProvider { let endpoint_volume: IAudioEndpointVolume = default_device.Activate(CLSCTX_ALL, None)?; - let device_id = default_device.GetId()?.to_string()?; - println!("Default audio render device: {}", device_id.clone()); - // Register the volume change callback let device_volume_callback = IAudioEndpointVolumeCallback::from(MediaDeviceEventHandler {}); - endpoint_volume .RegisterControlChangeNotify(&device_volume_callback)?; - } - loop { - // tx randomized data to test - let output = AudioOutput { - device: "default".to_string(), - volume: 50, - }; - emit_result_tx.try_send(Ok(ProviderOutput::Audio(output)).into())?; - std::thread::sleep(std::time::Duration::from_secs(1)); + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + } } } } @@ -104,7 +117,15 @@ impl IAudioEndpointVolumeCallback_Impl for MediaDeviceEventHandler_Impl { ) -> windows_core::Result<()> { println!("Volume notification"); if let Some(data) = unsafe { data.as_ref() } { - println!("Volume notification: {}", data.fMasterVolume,); + // TODO: surely theres a better way to do this without the clone + AUDIO_STATUS + .set(AudioOutput { + device: AUDIO_STATUS.get().expect("msg").device.clone(), + volume: data.fMasterVolume, + }) + .expect("Error setting audio status"); + println!("Volume update: {}", data.fMasterVolume,); + AudioProvider::emit_volume(); } Ok(()) } @@ -115,7 +136,6 @@ impl IAudioSessionNotification_Impl for MediaDeviceEventHandler_Impl { &self, _new_session: Option<&IAudioSessionControl>, ) -> windows::core::Result<()> { - println!("SESSION CREATED!"); let name = unsafe { _new_session .unwrap() @@ -124,26 +144,14 @@ impl IAudioSessionNotification_Impl for MediaDeviceEventHandler_Impl { .to_string() .unwrap() }; + AUDIO_STATUS + .set(AudioOutput { + device: name.clone(), + volume: AUDIO_STATUS.get().expect("msg").volume, + }) + .expect("Error setting audio status"); println!("New session created: {}", name); + AudioProvider::emit_volume(); Ok(()) } } - -// this works for current device changes -// but typedeventhandlers cant be used for volume updates -// ----------------------------------------------------------- -// let handler = TypedEventHandler::::new( move |_: -// &Option, args: -// &Option| { println!(" -// Default audio render device changed"); let device_id = -// args.as_ref().unwrap().Id().unwrap(); let device_info = -// DeviceInformation::CreateFromIdAsync(&device_id) .unwrap() -// .get() -// .unwrap(); -// let device_name = device_info.Name().unwrap(); -// println!("New default audio render device: {}", device_name); -// Ok(()) -// }, -// ); -// MediaDevice::DefaultAudioRenderDeviceChanged(&handler)?; From dc4c6ecf7c47965bab0f646ffb50a5ba5308e13e Mon Sep 17 00:00:00 2001 From: holby Date: Thu, 14 Nov 2024 18:02:24 -0500 Subject: [PATCH 04/22] functional but messy --- examples/boilerplate-solid-ts/dist/index.html | 2 +- examples/boilerplate-solid-ts/src/index.tsx | 4 +- packages/desktop/Cargo.toml | 4 +- .../src/providers/audio/audio_provider.rs | 237 ++++++++++++------ 4 files changed, 174 insertions(+), 73 deletions(-) diff --git a/examples/boilerplate-solid-ts/dist/index.html b/examples/boilerplate-solid-ts/dist/index.html index 53d75ec6..78026713 100644 --- a/examples/boilerplate-solid-ts/dist/index.html +++ b/examples/boilerplate-solid-ts/dist/index.html @@ -5,7 +5,7 @@ Zebar - + diff --git a/examples/boilerplate-solid-ts/src/index.tsx b/examples/boilerplate-solid-ts/src/index.tsx index ecd2eea4..233df7ae 100644 --- a/examples/boilerplate-solid-ts/src/index.tsx +++ b/examples/boilerplate-solid-ts/src/index.tsx @@ -21,7 +21,9 @@ function App() { return (
-
Audio volume: {output.audio?.volume}
+
+ vol: {output.audio?.volume} dev: {output.audio?.currentDevice} +
CPU usage: {output.cpu?.usage}
Battery charge: {output.battery?.chargePercent} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index b80c0464..e90d71e2 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -42,15 +42,17 @@ komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.28" windows-core = "0.58" windows = { version = "0.58", features = [ "implement", + "Win32_Devices_FunctionDiscovery", "Win32_Globalization", "Win32_Media", "Win32_Media_Audio", "Win32_Media_Audio_Endpoints", "Win32_System_Console", "Win32_System_SystemServices", - "Win32_UI_WindowsAndMessaging", "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Shell_PropertiesSystem", "Win32_UI_TextServices", + "Win32_UI_WindowsAndMessaging", "Win32_NetworkManagement_WiFi", ] } diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index 6a742b41..a66f060a 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -1,30 +1,32 @@ -use std::sync::OnceLock; +use std::sync::{Arc, Mutex, OnceLock}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::{self, Sender}; use windows::Win32::{ + Devices::FunctionDiscovery::PKEY_Device_FriendlyName, Media::Audio::{ - eMultimedia, eRender, + eMultimedia, eRender, EDataFlow, ERole, Endpoints::{ IAudioEndpointVolume, IAudioEndpointVolumeCallback, IAudioEndpointVolumeCallback_Impl, }, - IAudioSessionControl, IAudioSessionNotification, - IAudioSessionNotification_Impl, IMMDeviceEnumerator, - MMDeviceEnumerator, + IMMDevice, IMMDeviceEnumerator, IMMNotificationClient, + IMMNotificationClient_Impl, MMDeviceEnumerator, DEVICE_STATE, }, System::Com::{ CoCreateInstance, CoInitializeEx, CLSCTX_ALL, COINIT_MULTITHREADED, + STGM_READ, }, + UI::Shell::PropertiesSystem::IPropertyStore, }; +use windows_core::PCWSTR; use crate::providers::{Provider, ProviderOutput, ProviderResult}; static PROVIDER_TX: OnceLock> = OnceLock::new(); - -static AUDIO_STATUS: OnceLock = OnceLock::new(); +static AUDIO_STATE: OnceLock>> = OnceLock::new(); #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -33,7 +35,7 @@ pub struct AudioProviderConfig {} #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AudioOutput { - pub device: String, + pub current_device: String, pub volume: f32, } @@ -41,16 +43,28 @@ pub struct AudioProvider { _config: AudioProviderConfig, } +#[async_trait] +impl Provider for AudioProvider { + async fn run(&self, emit_result_tx: Sender) { + if let Err(err) = self.create_audio_manager(emit_result_tx.clone()) { + emit_result_tx + .send(Err(err).into()) + .await + .expect("Error emitting media provider err."); + } + } +} + impl AudioProvider { pub fn new(config: AudioProviderConfig) -> Self { Self { _config: config } } fn emit_volume() { - let tx = PROVIDER_TX.get().expect("Error getting provider tx"); - let output = AUDIO_STATUS.get().expect("Error getting audio status"); - tx.try_send(Ok(ProviderOutput::Audio(output.clone())).into()) - .expect("Error sending audio status"); + if let Some(tx) = PROVIDER_TX.get() { + let output = AUDIO_STATE.get().unwrap().lock().unwrap().clone(); + let _ = tx.try_send(Ok(ProviderOutput::Audio(output)).into()); + } } fn create_audio_manager( @@ -61,29 +75,37 @@ impl AudioProvider { .set(emit_result_tx.clone()) .expect("Error setting provider tx in focused window provider"); - // TODO is this the best way to initialize this - let _ = AUDIO_STATUS.set(AudioOutput { - device: "n/a".to_string(), - volume: 0.0, - }); - unsafe { - // Initialize COM library let _ = CoInitializeEx(None, COINIT_MULTITHREADED); - // Get the audio endpoint volume interface let enumerator: IMMDeviceEnumerator = CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)?; + + let handler = MediaDeviceEventHandler::new(enumerator.clone()); + let device_notification_callback = + IMMNotificationClient::from(handler.clone()); + let default_device = enumerator.GetDefaultAudioEndpoint(eRender, eMultimedia)?; - let endpoint_volume: IAudioEndpointVolume = - default_device.Activate(CLSCTX_ALL, None)?; + if let Ok(name) = + MediaDeviceEventHandler::get_device_name(&default_device) + { + println!("Default audio render device: {}", name); + } - // Register the volume change callback - let device_volume_callback = - IAudioEndpointVolumeCallback::from(MediaDeviceEventHandler {}); - endpoint_volume - .RegisterControlChangeNotify(&device_volume_callback)?; + let initial_device = + MediaDeviceEventHandler::get_device_name(&default_device)?; + let initial_volume = + handler.setup_volume_monitoring(&default_device)?; + AUDIO_STATE.set(Arc::new(Mutex::new(AudioOutput { + current_device: initial_device, + volume: initial_volume, + }))).expect("Error setting initial device volume"); + Self::emit_volume(); + + enumerator.RegisterEndpointNotificationCallback( + &device_notification_callback, + )?; loop { std::thread::sleep(std::time::Duration::from_secs(1)); @@ -92,66 +114,141 @@ impl AudioProvider { } } -#[async_trait] -impl Provider for AudioProvider { - async fn run(&self, emit_result_tx: Sender) { - if let Err(err) = self.create_audio_manager(emit_result_tx.clone()) { - emit_result_tx - .send(Err(err).into()) - .await - .expect("Erroring emitting media provider err."); - } - } +struct DeviceState { + volume_callbacks: + Arc>>, } +#[derive(Clone)] #[windows::core::implement( - IAudioEndpointVolumeCallback, - IAudioSessionNotification + IMMNotificationClient, + IAudioEndpointVolumeCallback )] -struct MediaDeviceEventHandler {} +struct MediaDeviceEventHandler { + enumerator: IMMDeviceEnumerator, + device_state: Arc, +} + +impl MediaDeviceEventHandler { + fn new(enumerator: IMMDeviceEnumerator) -> Self { + Self { + enumerator, + device_state: Arc::new(DeviceState { + volume_callbacks: Arc::new(Mutex::new(Vec::new())), + }), + } + } + + fn get_device_name(device: &IMMDevice) -> windows_core::Result { + unsafe { + let store: IPropertyStore = device.OpenPropertyStore(STGM_READ)?; + let value = store.GetValue(&PKEY_Device_FriendlyName)?; + Ok(value.to_string()) + } + } + + fn setup_volume_monitoring( + &self, + device: &IMMDevice, + ) -> windows_core::Result { + unsafe { + let endpoint_volume: IAudioEndpointVolume = + device.Activate(CLSCTX_ALL, None)?; + let handler = MediaDeviceEventHandler::new(self.enumerator.clone()); + let volume_callback = IAudioEndpointVolumeCallback::from(handler); + let inital_volume = endpoint_volume.GetMasterVolumeLevelScalar()?; + endpoint_volume.RegisterControlChangeNotify(&volume_callback)?; + self + .device_state + .volume_callbacks + .lock() + .unwrap() + .push((volume_callback, endpoint_volume)); + Ok(inital_volume) + } + } +} impl IAudioEndpointVolumeCallback_Impl for MediaDeviceEventHandler_Impl { fn OnNotify( &self, data: *mut windows::Win32::Media::Audio::AUDIO_VOLUME_NOTIFICATION_DATA, ) -> windows_core::Result<()> { - println!("Volume notification"); if let Some(data) = unsafe { data.as_ref() } { - // TODO: surely theres a better way to do this without the clone - AUDIO_STATUS - .set(AudioOutput { - device: AUDIO_STATUS.get().expect("msg").device.clone(), - volume: data.fMasterVolume, - }) - .expect("Error setting audio status"); - println!("Volume update: {}", data.fMasterVolume,); - AudioProvider::emit_volume(); + if let Some(state) = AUDIO_STATE.get() { + if let Ok(mut output) = state.lock() { + output.volume = data.fMasterVolume; + println!("Volume update: {}", data.fMasterVolume); + } + AudioProvider::emit_volume(); + } } Ok(()) } } -impl IAudioSessionNotification_Impl for MediaDeviceEventHandler_Impl { - fn OnSessionCreated( +impl IMMNotificationClient_Impl for MediaDeviceEventHandler_Impl { + fn OnDeviceStateChanged( &self, - _new_session: Option<&IAudioSessionControl>, - ) -> windows::core::Result<()> { - let name = unsafe { - _new_session - .unwrap() - .GetDisplayName() - .unwrap() - .to_string() - .unwrap() - }; - AUDIO_STATUS - .set(AudioOutput { - device: name.clone(), - volume: AUDIO_STATUS.get().expect("msg").volume, - }) - .expect("Error setting audio status"); - println!("New session created: {}", name); - AudioProvider::emit_volume(); + pwstrDeviceId: &PCWSTR, + dwNewState: DEVICE_STATE, + ) -> windows_core::Result<()> { + let device_id = unsafe { pwstrDeviceId.to_string()? }; + println!( + "Device state changed: {} - State: {:?}", + device_id, dwNewState + ); + Ok(()) + } + + fn OnDeviceAdded( + &self, + pwstrDeviceId: &PCWSTR, + ) -> windows_core::Result<()> { + let device_id = unsafe { pwstrDeviceId.to_string()? }; + println!("Device added: {}", device_id); + Ok(()) + } + + fn OnDeviceRemoved( + &self, + pwstrDeviceId: &PCWSTR, + ) -> windows_core::Result<()> { + let device_id = unsafe { pwstrDeviceId.to_string()? }; + println!("Device removed: {}", device_id); + Ok(()) + } + + fn OnDefaultDeviceChanged( + &self, + flow: EDataFlow, + role: ERole, + pwstrDefaultDeviceId: &PCWSTR, + ) -> windows_core::Result<()> { + unsafe { + if flow == eRender && role == eMultimedia { + let device = self.enumerator.GetDevice(*pwstrDefaultDeviceId)?; + if let Ok(name) = MediaDeviceEventHandler::get_device_name(&device) + { + println!("Default device changed to: {}", name); + self.setup_volume_monitoring(&device)?; + if let Ok(mut output) = AUDIO_STATE.get().unwrap().lock() { + output.current_device = name; + } + AudioProvider::emit_volume(); + } + } + } + Ok(()) + } + + fn OnPropertyValueChanged( + &self, + pwstrDeviceId: &PCWSTR, + key: &windows::Win32::UI::Shell::PropertiesSystem::PROPERTYKEY, + ) -> windows_core::Result<()> { + let device_id = unsafe { pwstrDeviceId.to_string()? }; + println!("Property changed: {} - Key: {:?}", device_id, key); Ok(()) } } From 3ae65378ec9b0cc9b6909902324654dd7361af99 Mon Sep 17 00:00:00 2001 From: holby Date: Thu, 14 Nov 2024 18:08:00 -0500 Subject: [PATCH 05/22] actually functional, no initial values yet --- .../src/providers/audio/audio_provider.rs | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index a66f060a..ca9baeee 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -75,6 +75,14 @@ impl AudioProvider { .set(emit_result_tx.clone()) .expect("Error setting provider tx in focused window provider"); + // todo do this at initialization + AUDIO_STATE + .set(Arc::new(Mutex::new(AudioOutput { + current_device: "n/a".to_string(), + volume: 0.0, + }))) + .expect("Error setting initial state"); + unsafe { let _ = CoInitializeEx(None, COINIT_MULTITHREADED); @@ -93,16 +101,8 @@ impl AudioProvider { println!("Default audio render device: {}", name); } - let initial_device = - MediaDeviceEventHandler::get_device_name(&default_device)?; - let initial_volume = - handler.setup_volume_monitoring(&default_device)?; - AUDIO_STATE.set(Arc::new(Mutex::new(AudioOutput { - current_device: initial_device, - volume: initial_volume, - }))).expect("Error setting initial device volume"); - Self::emit_volume(); - + // Set up initial volume monitoring + handler.setup_volume_monitoring(&default_device)?; enumerator.RegisterEndpointNotificationCallback( &device_notification_callback, )?; @@ -150,13 +150,12 @@ impl MediaDeviceEventHandler { fn setup_volume_monitoring( &self, device: &IMMDevice, - ) -> windows_core::Result { + ) -> windows_core::Result<()> { unsafe { let endpoint_volume: IAudioEndpointVolume = device.Activate(CLSCTX_ALL, None)?; let handler = MediaDeviceEventHandler::new(self.enumerator.clone()); let volume_callback = IAudioEndpointVolumeCallback::from(handler); - let inital_volume = endpoint_volume.GetMasterVolumeLevelScalar()?; endpoint_volume.RegisterControlChangeNotify(&volume_callback)?; self .device_state @@ -164,8 +163,8 @@ impl MediaDeviceEventHandler { .lock() .unwrap() .push((volume_callback, endpoint_volume)); - Ok(inital_volume) } + Ok(()) } } From a8c0bd7710889ecf9e6dc4e8e04132b65fabb80e Mon Sep 17 00:00:00 2001 From: holby Date: Thu, 14 Nov 2024 19:53:20 -0500 Subject: [PATCH 06/22] why does this break everything --- .../src/providers/audio/audio_provider.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index ca9baeee..cdf5e44f 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -126,16 +126,15 @@ struct DeviceState { )] struct MediaDeviceEventHandler { enumerator: IMMDeviceEnumerator, - device_state: Arc, + device_state: + Arc>>, } impl MediaDeviceEventHandler { fn new(enumerator: IMMDeviceEnumerator) -> Self { Self { enumerator, - device_state: Arc::new(DeviceState { - volume_callbacks: Arc::new(Mutex::new(Vec::new())), - }), + device_state: Arc::new(Mutex::new(None)), } } @@ -152,17 +151,16 @@ impl MediaDeviceEventHandler { device: &IMMDevice, ) -> windows_core::Result<()> { unsafe { + // unbind previous volume callback + if let Some((prev_callback, prev_endpoint)) = self.device_state.lock().unwrap().take() { + prev_endpoint.UnregisterControlChangeNotify(&prev_callback)?; + } let endpoint_volume: IAudioEndpointVolume = device.Activate(CLSCTX_ALL, None)?; let handler = MediaDeviceEventHandler::new(self.enumerator.clone()); let volume_callback = IAudioEndpointVolumeCallback::from(handler); endpoint_volume.RegisterControlChangeNotify(&volume_callback)?; - self - .device_state - .volume_callbacks - .lock() - .unwrap() - .push((volume_callback, endpoint_volume)); + self.device_state.lock().unwrap().replace((volume_callback, endpoint_volume)); } Ok(()) } From e581df1ad8c08f3e3d42c0667722a776d2356385 Mon Sep 17 00:00:00 2001 From: holby Date: Fri, 15 Nov 2024 10:27:19 -0500 Subject: [PATCH 07/22] Revert "why does this break everything" This reverts commit a8c0bd7710889ecf9e6dc4e8e04132b65fabb80e. --- .../src/providers/audio/audio_provider.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index cdf5e44f..ca9baeee 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -126,15 +126,16 @@ struct DeviceState { )] struct MediaDeviceEventHandler { enumerator: IMMDeviceEnumerator, - device_state: - Arc>>, + device_state: Arc, } impl MediaDeviceEventHandler { fn new(enumerator: IMMDeviceEnumerator) -> Self { Self { enumerator, - device_state: Arc::new(Mutex::new(None)), + device_state: Arc::new(DeviceState { + volume_callbacks: Arc::new(Mutex::new(Vec::new())), + }), } } @@ -151,16 +152,17 @@ impl MediaDeviceEventHandler { device: &IMMDevice, ) -> windows_core::Result<()> { unsafe { - // unbind previous volume callback - if let Some((prev_callback, prev_endpoint)) = self.device_state.lock().unwrap().take() { - prev_endpoint.UnregisterControlChangeNotify(&prev_callback)?; - } let endpoint_volume: IAudioEndpointVolume = device.Activate(CLSCTX_ALL, None)?; let handler = MediaDeviceEventHandler::new(self.enumerator.clone()); let volume_callback = IAudioEndpointVolumeCallback::from(handler); endpoint_volume.RegisterControlChangeNotify(&volume_callback)?; - self.device_state.lock().unwrap().replace((volume_callback, endpoint_volume)); + self + .device_state + .volume_callbacks + .lock() + .unwrap() + .push((volume_callback, endpoint_volume)); } Ok(()) } From 78d482d0910da13ac4d7d14a85787dcbbfc43a2f Mon Sep 17 00:00:00 2001 From: holby Date: Fri, 15 Nov 2024 10:40:06 -0500 Subject: [PATCH 08/22] fix threading --- .../src/providers/audio/audio_provider.rs | 107 ++++++++---------- 1 file changed, 50 insertions(+), 57 deletions(-) diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index ca9baeee..7570a178 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -2,7 +2,10 @@ use std::sync::{Arc, Mutex, OnceLock}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc::{self, Sender}; +use tokio::{ + sync::mpsc::{self, Sender}, + task, +}; use windows::Win32::{ Devices::FunctionDiscovery::PKEY_Device_FriendlyName, Media::Audio::{ @@ -46,12 +49,24 @@ pub struct AudioProvider { #[async_trait] impl Provider for AudioProvider { async fn run(&self, emit_result_tx: Sender) { - if let Err(err) = self.create_audio_manager(emit_result_tx.clone()) { - emit_result_tx - .send(Err(err).into()) - .await - .expect("Error emitting media provider err."); - } + PROVIDER_TX + .set(emit_result_tx.clone()) + .expect("Error setting provider tx in audio provider"); + + AUDIO_STATE + .set(Arc::new(Mutex::new(AudioOutput { + current_device: "n/a".to_string(), + volume: 0.0, + }))) + .expect("Error setting initial audio state"); + + task::spawn_blocking(move || { + if let Err(err) = Self::create_audio_manager() { + emit_result_tx + .blocking_send(Err(err).into()) + .expect("Error with media provider"); + } + }); } } @@ -67,22 +82,7 @@ impl AudioProvider { } } - fn create_audio_manager( - &self, - emit_result_tx: Sender, - ) -> anyhow::Result<()> { - PROVIDER_TX - .set(emit_result_tx.clone()) - .expect("Error setting provider tx in focused window provider"); - - // todo do this at initialization - AUDIO_STATE - .set(Arc::new(Mutex::new(AudioOutput { - current_device: "n/a".to_string(), - volume: 0.0, - }))) - .expect("Error setting initial state"); - + fn create_audio_manager() -> anyhow::Result<()> { unsafe { let _ = CoInitializeEx(None, COINIT_MULTITHREADED); @@ -114,11 +114,6 @@ impl AudioProvider { } } -struct DeviceState { - volume_callbacks: - Arc>>, -} - #[derive(Clone)] #[windows::core::implement( IMMNotificationClient, @@ -126,16 +121,15 @@ struct DeviceState { )] struct MediaDeviceEventHandler { enumerator: IMMDeviceEnumerator, - device_state: Arc, + device_state: + Arc>>, } impl MediaDeviceEventHandler { fn new(enumerator: IMMDeviceEnumerator) -> Self { Self { enumerator, - device_state: Arc::new(DeviceState { - volume_callbacks: Arc::new(Mutex::new(Vec::new())), - }), + device_state: Arc::new(Mutex::new(Vec::new())), } } @@ -159,7 +153,6 @@ impl MediaDeviceEventHandler { endpoint_volume.RegisterControlChangeNotify(&volume_callback)?; self .device_state - .volume_callbacks .lock() .unwrap() .push((volume_callback, endpoint_volume)); @@ -187,6 +180,30 @@ impl IAudioEndpointVolumeCallback_Impl for MediaDeviceEventHandler_Impl { } impl IMMNotificationClient_Impl for MediaDeviceEventHandler_Impl { + fn OnDefaultDeviceChanged( + &self, + flow: EDataFlow, + role: ERole, + pwstrDefaultDeviceId: &PCWSTR, + ) -> windows_core::Result<()> { + unsafe { + if flow == eRender && role == eMultimedia { + let device = self.enumerator.GetDevice(*pwstrDefaultDeviceId)?; + if let Ok(name) = MediaDeviceEventHandler::get_device_name(&device) + { + println!("Default device changed to: {}", name); + self.setup_volume_monitoring(&device)?; + if let Ok(mut output) = AUDIO_STATE.get().unwrap().lock() { + output.current_device = name; + } + AudioProvider::emit_volume(); + } + } + } + Ok(()) + } + + // Unused fns, required for IMMNotificationClient_Impl fn OnDeviceStateChanged( &self, pwstrDeviceId: &PCWSTR, @@ -218,36 +235,12 @@ impl IMMNotificationClient_Impl for MediaDeviceEventHandler_Impl { Ok(()) } - fn OnDefaultDeviceChanged( - &self, - flow: EDataFlow, - role: ERole, - pwstrDefaultDeviceId: &PCWSTR, - ) -> windows_core::Result<()> { - unsafe { - if flow == eRender && role == eMultimedia { - let device = self.enumerator.GetDevice(*pwstrDefaultDeviceId)?; - if let Ok(name) = MediaDeviceEventHandler::get_device_name(&device) - { - println!("Default device changed to: {}", name); - self.setup_volume_monitoring(&device)?; - if let Ok(mut output) = AUDIO_STATE.get().unwrap().lock() { - output.current_device = name; - } - AudioProvider::emit_volume(); - } - } - } - Ok(()) - } - fn OnPropertyValueChanged( &self, pwstrDeviceId: &PCWSTR, key: &windows::Win32::UI::Shell::PropertiesSystem::PROPERTYKEY, ) -> windows_core::Result<()> { let device_id = unsafe { pwstrDeviceId.to_string()? }; - println!("Property changed: {} - Key: {:?}", device_id, key); Ok(()) } } From df99c68411f36982939785c9d790a10ea9d6e94e Mon Sep 17 00:00:00 2001 From: holby Date: Fri, 15 Nov 2024 23:56:46 -0500 Subject: [PATCH 09/22] outputs all devices. still has bugs --- examples/boilerplate-solid-ts/dist/index.html | 2 +- examples/boilerplate-solid-ts/src/index.tsx | 3 +- .../providers/audio/audio-provider-types.ts | 11 +- .../src/providers/audio/audio_provider.rs | 346 +++++++++++------- 4 files changed, 231 insertions(+), 131 deletions(-) diff --git a/examples/boilerplate-solid-ts/dist/index.html b/examples/boilerplate-solid-ts/dist/index.html index 78026713..40b6af96 100644 --- a/examples/boilerplate-solid-ts/dist/index.html +++ b/examples/boilerplate-solid-ts/dist/index.html @@ -5,7 +5,7 @@ Zebar - + diff --git a/examples/boilerplate-solid-ts/src/index.tsx b/examples/boilerplate-solid-ts/src/index.tsx index 233df7ae..6b800925 100644 --- a/examples/boilerplate-solid-ts/src/index.tsx +++ b/examples/boilerplate-solid-ts/src/index.tsx @@ -22,7 +22,8 @@ function App() { return (
- vol: {output.audio?.volume} dev: {output.audio?.currentDevice} + {output.audio?.devices[output.audio?.defaultDevice!].name} : + {output.audio?.devices[output.audio?.defaultDevice!].volume}
CPU usage: {output.cpu?.usage}
diff --git a/packages/client-api/src/providers/audio/audio-provider-types.ts b/packages/client-api/src/providers/audio/audio-provider-types.ts index f1a9d229..6c170e61 100644 --- a/packages/client-api/src/providers/audio/audio-provider-types.ts +++ b/packages/client-api/src/providers/audio/audio-provider-types.ts @@ -6,8 +6,15 @@ export interface AudioProviderConfig { export type AudioProvider = Provider; -export interface AudioOutput { +export interface AudioDeviceInfo { + deviceId: string; + name: string; volume: number; - currentDevice: string; + isDefault: boolean; +} + +export interface AudioOutput { + devices: Record; + defaultDevice: string | null; } diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index 7570a178..8f54e09e 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -1,4 +1,7 @@ -use std::sync::{Arc, Mutex, OnceLock}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex, OnceLock}, +}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; @@ -16,12 +19,13 @@ use windows::Win32::{ }, IMMDevice, IMMDeviceEnumerator, IMMNotificationClient, IMMNotificationClient_Impl, MMDeviceEnumerator, DEVICE_STATE, + DEVICE_STATE_ACTIVE, }, System::Com::{ CoCreateInstance, CoInitializeEx, CLSCTX_ALL, COINIT_MULTITHREADED, STGM_READ, }, - UI::Shell::PropertiesSystem::IPropertyStore, + UI::Shell::PropertiesSystem::{IPropertyStore, PROPERTYKEY}, }; use windows_core::PCWSTR; @@ -37,81 +41,32 @@ pub struct AudioProviderConfig {} #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] -pub struct AudioOutput { - pub current_device: String, +pub struct AudioDeviceInfo { + pub name: String, pub volume: f32, + pub is_default: bool, } -pub struct AudioProvider { - _config: AudioProviderConfig, -} - -#[async_trait] -impl Provider for AudioProvider { - async fn run(&self, emit_result_tx: Sender) { - PROVIDER_TX - .set(emit_result_tx.clone()) - .expect("Error setting provider tx in audio provider"); - - AUDIO_STATE - .set(Arc::new(Mutex::new(AudioOutput { - current_device: "n/a".to_string(), - volume: 0.0, - }))) - .expect("Error setting initial audio state"); - - task::spawn_blocking(move || { - if let Err(err) = Self::create_audio_manager() { - emit_result_tx - .blocking_send(Err(err).into()) - .expect("Error with media provider"); - } - }); - } +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AudioOutput { + pub devices: HashMap, + pub default_device: Option, } -impl AudioProvider { - pub fn new(config: AudioProviderConfig) -> Self { - Self { _config: config } - } - - fn emit_volume() { - if let Some(tx) = PROVIDER_TX.get() { - let output = AUDIO_STATE.get().unwrap().lock().unwrap().clone(); - let _ = tx.try_send(Ok(ProviderOutput::Audio(output)).into()); +impl AudioOutput { + fn new() -> Self { + Self { + devices: HashMap::new(), + default_device: None, } } +} - fn create_audio_manager() -> anyhow::Result<()> { - unsafe { - let _ = CoInitializeEx(None, COINIT_MULTITHREADED); - - let enumerator: IMMDeviceEnumerator = - CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)?; - - let handler = MediaDeviceEventHandler::new(enumerator.clone()); - let device_notification_callback = - IMMNotificationClient::from(handler.clone()); - - let default_device = - enumerator.GetDefaultAudioEndpoint(eRender, eMultimedia)?; - if let Ok(name) = - MediaDeviceEventHandler::get_device_name(&default_device) - { - println!("Default audio render device: {}", name); - } - - // Set up initial volume monitoring - handler.setup_volume_monitoring(&default_device)?; - enumerator.RegisterEndpointNotificationCallback( - &device_notification_callback, - )?; - - loop { - std::thread::sleep(std::time::Duration::from_secs(1)); - } - } - } +#[derive(Clone)] +struct DeviceInfo { + name: String, + endpoint_volume: IAudioEndpointVolume, } #[derive(Clone)] @@ -121,19 +76,20 @@ impl AudioProvider { )] struct MediaDeviceEventHandler { enumerator: IMMDeviceEnumerator, - device_state: - Arc>>, + device_state: Arc>>, + current_device: String, } impl MediaDeviceEventHandler { fn new(enumerator: IMMDeviceEnumerator) -> Self { Self { enumerator, - device_state: Arc::new(Mutex::new(Vec::new())), + device_state: Arc::new(Mutex::new(HashMap::new())), + current_device: String::new(), } } - fn get_device_name(device: &IMMDevice) -> windows_core::Result { + fn get_device_name(device: &IMMDevice) -> windows::core::Result { unsafe { let store: IPropertyStore = device.OpenPropertyStore(STGM_READ)?; let value = store.GetValue(&PKEY_Device_FriendlyName)?; @@ -144,38 +100,136 @@ impl MediaDeviceEventHandler { fn setup_volume_monitoring( &self, device: &IMMDevice, - ) -> windows_core::Result<()> { + ) -> windows::core::Result<(String, String)> { + unsafe { + let device_id = device.GetId()?.to_string()?; + let device_name = Self::get_device_name(device)?; + let mut device_state = self.device_state.lock().unwrap(); + + if !device_state.contains_key(&device_id) { + let endpoint_volume: IAudioEndpointVolume = + device.Activate(CLSCTX_ALL, None)?; + let mut handler = self.clone(); + handler.current_device = device_id.clone(); + let callback = IAudioEndpointVolumeCallback::from(handler); + endpoint_volume.RegisterControlChangeNotify(&callback)?; + device_state.insert( + device_id.clone(), + DeviceInfo { + name: device_name.clone(), + endpoint_volume, + }, + ); + } + + Ok((device_id, device_name)) + } + } + + fn enumerate_devices(&self) -> windows::core::Result<()> { unsafe { - let endpoint_volume: IAudioEndpointVolume = - device.Activate(CLSCTX_ALL, None)?; - let handler = MediaDeviceEventHandler::new(self.enumerator.clone()); - let volume_callback = IAudioEndpointVolumeCallback::from(handler); - endpoint_volume.RegisterControlChangeNotify(&volume_callback)?; + let collection = self + .enumerator + .EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE)?; + let mut audio_state = AUDIO_STATE.get().unwrap().lock().unwrap(); + let mut devices = HashMap::new(); + + for i in 0..collection.GetCount()? { + if let Ok(device) = collection.Item(i) { + let (device_id, device_name) = + self.setup_volume_monitoring(&device)?; + let volume = self + .device_state + .lock() + .unwrap() + .get(&device_id) + .map(|d| { + d.endpoint_volume + .GetMasterVolumeLevelScalar() + .unwrap_or(0.0) + }) + .unwrap_or(0.0); + let is_default = self + .enumerator + .GetDefaultAudioEndpoint(eRender, eMultimedia) + .ok() + .and_then(|d| d.GetId().ok()) + .and_then(|id| id.to_string().ok()) + .as_ref() + .map(|id| id == &device_id) + .unwrap_or(false); + + devices.insert( + device_id, + AudioDeviceInfo { + name: device_name, + volume, + is_default, + }, + ); + } + } + self .device_state .lock() .unwrap() - .push((volume_callback, endpoint_volume)); + .retain(|id, _| devices.contains_key(id)); + audio_state.devices = devices; + audio_state.default_device = self + .enumerator + .GetDefaultAudioEndpoint(eRender, eMultimedia) + .ok() + .and_then(|d| d.GetId().ok()) + .and_then(|id| id.to_string().ok()); } + + AudioProvider::emit_volume(); Ok(()) } } +impl Drop for MediaDeviceEventHandler { + fn drop(&mut self) { + unsafe { + let mut device_state = self.device_state.lock().unwrap(); + for (device_id, device_info) in device_state.iter() { + device_info + .endpoint_volume + .UnregisterControlChangeNotify( + &IAudioEndpointVolumeCallback::from(self.clone()), + ) + .expect("Failed to unregister volume callback"); + } + } + } +} + impl IAudioEndpointVolumeCallback_Impl for MediaDeviceEventHandler_Impl { fn OnNotify( &self, data: *mut windows::Win32::Media::Audio::AUDIO_VOLUME_NOTIFICATION_DATA, - ) -> windows_core::Result<()> { - if let Some(data) = unsafe { data.as_ref() } { - if let Some(state) = AUDIO_STATE.get() { - if let Ok(mut output) = state.lock() { - output.volume = data.fMasterVolume; - println!("Volume update: {}", data.fMasterVolume); + ) -> windows::core::Result<()> { + unsafe { + if let Some(data) = data.as_ref() { + let device_id = &*self.current_device; + println!("Got notification for device: {}", device_id); + + if let Some(state) = AUDIO_STATE.get() { + let mut output = state.lock().unwrap(); + if let Some(device) = output.devices.get_mut(device_id) { + device.volume = data.fMasterVolume; + println!( + "Volume update for {} (ID: {}): {}", + device.name, device_id, data.fMasterVolume + ); + drop(output); + AudioProvider::emit_volume(); + } } - AudioProvider::emit_volume(); } + Ok(()) } - Ok(()) } } @@ -184,63 +238,101 @@ impl IMMNotificationClient_Impl for MediaDeviceEventHandler_Impl { &self, flow: EDataFlow, role: ERole, - pwstrDefaultDeviceId: &PCWSTR, - ) -> windows_core::Result<()> { - unsafe { - if flow == eRender && role == eMultimedia { - let device = self.enumerator.GetDevice(*pwstrDefaultDeviceId)?; - if let Ok(name) = MediaDeviceEventHandler::get_device_name(&device) - { - println!("Default device changed to: {}", name); - self.setup_volume_monitoring(&device)?; - if let Ok(mut output) = AUDIO_STATE.get().unwrap().lock() { - output.current_device = name; - } - AudioProvider::emit_volume(); - } - } + _pwstrDefaultDeviceId: &PCWSTR, + ) -> windows::core::Result<()> { + if flow == eRender && role == eMultimedia { + self.enumerate_devices()?; } Ok(()) } - // Unused fns, required for IMMNotificationClient_Impl fn OnDeviceStateChanged( &self, - pwstrDeviceId: &PCWSTR, - dwNewState: DEVICE_STATE, - ) -> windows_core::Result<()> { - let device_id = unsafe { pwstrDeviceId.to_string()? }; - println!( - "Device state changed: {} - State: {:?}", - device_id, dwNewState - ); - Ok(()) + _pwstrDeviceId: &PCWSTR, + _dwNewState: DEVICE_STATE, + ) -> windows::core::Result<()> { + self.enumerate_devices() } fn OnDeviceAdded( &self, - pwstrDeviceId: &PCWSTR, - ) -> windows_core::Result<()> { - let device_id = unsafe { pwstrDeviceId.to_string()? }; - println!("Device added: {}", device_id); - Ok(()) + _pwstrDeviceId: &PCWSTR, + ) -> windows::core::Result<()> { + self.enumerate_devices() } fn OnDeviceRemoved( &self, - pwstrDeviceId: &PCWSTR, - ) -> windows_core::Result<()> { - let device_id = unsafe { pwstrDeviceId.to_string()? }; - println!("Device removed: {}", device_id); - Ok(()) + _pwstrDeviceId: &PCWSTR, + ) -> windows::core::Result<()> { + self.enumerate_devices() } fn OnPropertyValueChanged( &self, - pwstrDeviceId: &PCWSTR, - key: &windows::Win32::UI::Shell::PropertiesSystem::PROPERTYKEY, - ) -> windows_core::Result<()> { - let device_id = unsafe { pwstrDeviceId.to_string()? }; + _pwstrDeviceId: &PCWSTR, + _key: &PROPERTYKEY, + ) -> windows::core::Result<()> { Ok(()) } } + +pub struct AudioProvider { + _config: AudioProviderConfig, +} + +impl AudioProvider { + pub fn new(config: AudioProviderConfig) -> Self { + Self { _config: config } + } + + fn emit_volume() { + if let Some(tx) = PROVIDER_TX.get() { + let output = AUDIO_STATE.get().unwrap().lock().unwrap().clone(); + println!("Emitting audio output: {:#?}", output); + let _ = tx.try_send(Ok(ProviderOutput::Audio(output)).into()); + } + } + + fn create_audio_manager() -> anyhow::Result<()> { + unsafe { + let _ = CoInitializeEx(None, COINIT_MULTITHREADED); + let enumerator: IMMDeviceEnumerator = + CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)?; + let handler = MediaDeviceEventHandler::new(enumerator.clone()); + + handler.enumerate_devices()?; + + let device_notification_callback = + IMMNotificationClient::from(handler.clone()); + enumerator.RegisterEndpointNotificationCallback( + &device_notification_callback, + )?; + + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + } + } + } +} + +#[async_trait] +impl Provider for AudioProvider { + async fn run(&self, emit_result_tx: Sender) { + PROVIDER_TX + .set(emit_result_tx.clone()) + .expect("Error setting provider tx in audio provider"); + + AUDIO_STATE + .set(Arc::new(Mutex::new(AudioOutput::new()))) + .expect("Error setting initial audio state"); + + task::spawn_blocking(move || { + if let Err(err) = Self::create_audio_manager() { + emit_result_tx + .blocking_send(Err(err).into()) + .expect("Error with media provider"); + } + }); + } +} From 2910d4b88c29f20012081f13b8404745844e5a96 Mon Sep 17 00:00:00 2001 From: holby Date: Sat, 16 Nov 2024 00:12:20 -0500 Subject: [PATCH 10/22] improvement --- .../src/providers/audio/audio_provider.rs | 74 +++++++------------ 1 file changed, 26 insertions(+), 48 deletions(-) diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index 8f54e09e..5cc6b05f 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -97,35 +97,6 @@ impl MediaDeviceEventHandler { } } - fn setup_volume_monitoring( - &self, - device: &IMMDevice, - ) -> windows::core::Result<(String, String)> { - unsafe { - let device_id = device.GetId()?.to_string()?; - let device_name = Self::get_device_name(device)?; - let mut device_state = self.device_state.lock().unwrap(); - - if !device_state.contains_key(&device_id) { - let endpoint_volume: IAudioEndpointVolume = - device.Activate(CLSCTX_ALL, None)?; - let mut handler = self.clone(); - handler.current_device = device_id.clone(); - let callback = IAudioEndpointVolumeCallback::from(handler); - endpoint_volume.RegisterControlChangeNotify(&callback)?; - device_state.insert( - device_id.clone(), - DeviceInfo { - name: device_name.clone(), - endpoint_volume, - }, - ); - } - - Ok((device_id, device_name)) - } - } - fn enumerate_devices(&self) -> windows::core::Result<()> { unsafe { let collection = self @@ -133,21 +104,34 @@ impl MediaDeviceEventHandler { .EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE)?; let mut audio_state = AUDIO_STATE.get().unwrap().lock().unwrap(); let mut devices = HashMap::new(); + let mut device_state = self.device_state.lock().unwrap(); for i in 0..collection.GetCount()? { if let Ok(device) = collection.Item(i) { - let (device_id, device_name) = - self.setup_volume_monitoring(&device)?; - let volume = self - .device_state - .lock() - .unwrap() - .get(&device_id) - .map(|d| { - d.endpoint_volume - .GetMasterVolumeLevelScalar() - .unwrap_or(0.0) - }) + let device_id = device.GetId()?.to_string()?; + + // Check if the device is already being monitored + if !device_state.contains_key(&device_id) { + let device_name = Self::get_device_name(&device)?; + let endpoint_volume: IAudioEndpointVolume = + device.Activate(CLSCTX_ALL, None)?; + let mut handler = self.clone(); + handler.current_device = device_id.clone(); + let callback = IAudioEndpointVolumeCallback::from(handler); + endpoint_volume.RegisterControlChangeNotify(&callback)?; + device_state.insert( + device_id.clone(), + DeviceInfo { + name: device_name.clone(), + endpoint_volume, + }, + ); + } + + let device_info = device_state.get(&device_id).unwrap(); + let volume = device_info + .endpoint_volume + .GetMasterVolumeLevelScalar() .unwrap_or(0.0); let is_default = self .enumerator @@ -162,7 +146,7 @@ impl MediaDeviceEventHandler { devices.insert( device_id, AudioDeviceInfo { - name: device_name, + name: device_info.name.clone(), volume, is_default, }, @@ -170,11 +154,6 @@ impl MediaDeviceEventHandler { } } - self - .device_state - .lock() - .unwrap() - .retain(|id, _| devices.contains_key(id)); audio_state.devices = devices; audio_state.default_device = self .enumerator @@ -223,7 +202,6 @@ impl IAudioEndpointVolumeCallback_Impl for MediaDeviceEventHandler_Impl { "Volume update for {} (ID: {}): {}", device.name, device_id, data.fMasterVolume ); - drop(output); AudioProvider::emit_volume(); } } From deee9290a7c7a801c1f2b57e7ad088393520caf8 Mon Sep 17 00:00:00 2001 From: holby Date: Sat, 16 Nov 2024 00:15:23 -0500 Subject: [PATCH 11/22] undo accidental delete --- packages/desktop/src/providers/audio/audio_provider.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index 5cc6b05f..cef5ff9e 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -202,6 +202,7 @@ impl IAudioEndpointVolumeCallback_Impl for MediaDeviceEventHandler_Impl { "Volume update for {} (ID: {}): {}", device.name, device_id, data.fMasterVolume ); + drop(output); AudioProvider::emit_volume(); } } From cb41df5b468a73d62817bff8a6d8aaf542e57189 Mon Sep 17 00:00:00 2001 From: holby Date: Sat, 16 Nov 2024 18:00:48 -0500 Subject: [PATCH 12/22] debouncer --- .../src/providers/audio/audio_provider.rs | 93 +++++++++++++------ 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index cef5ff9e..caf89ebf 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -1,6 +1,7 @@ use std::{ collections::HashMap, sync::{Arc, Mutex, OnceLock}, + time::Duration, }; use async_trait::async_trait; @@ -8,6 +9,7 @@ use serde::{Deserialize, Serialize}; use tokio::{ sync::mpsc::{self, Sender}, task, + time::sleep, }; use windows::Win32::{ Devices::FunctionDiscovery::PKEY_Device_FriendlyName, @@ -78,14 +80,19 @@ struct MediaDeviceEventHandler { enumerator: IMMDeviceEnumerator, device_state: Arc>>, current_device: String, + update_tx: mpsc::Sender<(String, f32)>, } impl MediaDeviceEventHandler { - fn new(enumerator: IMMDeviceEnumerator) -> Self { + fn new( + enumerator: IMMDeviceEnumerator, + update_tx: mpsc::Sender<(String, f32)>, + ) -> Self { Self { enumerator, device_state: Arc::new(Mutex::new(HashMap::new())), current_device: String::new(), + update_tx, } } @@ -171,14 +178,11 @@ impl MediaDeviceEventHandler { impl Drop for MediaDeviceEventHandler { fn drop(&mut self) { unsafe { - let mut device_state = self.device_state.lock().unwrap(); - for (device_id, device_info) in device_state.iter() { - device_info - .endpoint_volume - .UnregisterControlChangeNotify( - &IAudioEndpointVolumeCallback::from(self.clone()), - ) - .expect("Failed to unregister volume callback"); + let device_state = self.device_state.lock().unwrap(); + for (_, device_info) in device_state.iter() { + let _ = device_info.endpoint_volume.UnregisterControlChangeNotify( + &IAudioEndpointVolumeCallback::from(self.clone()), + ); } } } @@ -191,21 +195,9 @@ impl IAudioEndpointVolumeCallback_Impl for MediaDeviceEventHandler_Impl { ) -> windows::core::Result<()> { unsafe { if let Some(data) = data.as_ref() { - let device_id = &*self.current_device; - println!("Got notification for device: {}", device_id); - - if let Some(state) = AUDIO_STATE.get() { - let mut output = state.lock().unwrap(); - if let Some(device) = output.devices.get_mut(device_id) { - device.volume = data.fMasterVolume; - println!( - "Volume update for {} (ID: {}): {}", - device.name, device_id, data.fMasterVolume - ); - drop(output); - AudioProvider::emit_volume(); - } - } + let device_id = self.current_device.clone(); + let volume = data.fMasterVolume; + let _ = self.update_tx.blocking_send((device_id, volume)); } Ok(()) } @@ -268,17 +260,48 @@ impl AudioProvider { fn emit_volume() { if let Some(tx) = PROVIDER_TX.get() { let output = AUDIO_STATE.get().unwrap().lock().unwrap().clone(); - println!("Emitting audio output: {:#?}", output); let _ = tx.try_send(Ok(ProviderOutput::Audio(output)).into()); } } - fn create_audio_manager() -> anyhow::Result<()> { + async fn handle_volume_updates(mut rx: mpsc::Receiver<(String, f32)>) { + const PROCESS_DELAY: Duration = Duration::from_millis(50); + let mut latest_updates: HashMap = HashMap::new(); + + while let Some((device_id, volume)) = rx.recv().await { + latest_updates.insert(device_id, volume); + + // Collect any additional pending updates without waiting + while let Ok((device_id, volume)) = rx.try_recv() { + latest_updates.insert(device_id, volume); + } + + // Brief delay to collect more potential updates + sleep(PROCESS_DELAY).await; + + // Process all collected updates + if let Some(state) = AUDIO_STATE.get() { + let mut output = state.lock().unwrap(); + for (device_id, volume) in latest_updates.drain() { + if let Some(device) = output.devices.get_mut(&device_id) { + device.volume = volume; + } + } + drop(output); + Self::emit_volume(); + } + } + } + + fn create_audio_manager( + update_tx: mpsc::Sender<(String, f32)>, + ) -> anyhow::Result<()> { unsafe { let _ = CoInitializeEx(None, COINIT_MULTITHREADED); let enumerator: IMMDeviceEnumerator = CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)?; - let handler = MediaDeviceEventHandler::new(enumerator.clone()); + let handler = + MediaDeviceEventHandler::new(enumerator.clone(), update_tx); handler.enumerate_devices()?; @@ -306,12 +329,24 @@ impl Provider for AudioProvider { .set(Arc::new(Mutex::new(AudioOutput::new()))) .expect("Error setting initial audio state"); - task::spawn_blocking(move || { - if let Err(err) = Self::create_audio_manager() { + // Create a channel for volume updates + let (update_tx, update_rx) = mpsc::channel(100); + + // Spawn both tasks + let update_handler = + task::spawn(Self::handle_volume_updates(update_rx)); + let manager = task::spawn_blocking(move || { + if let Err(err) = Self::create_audio_manager(update_tx) { emit_result_tx .blocking_send(Err(err).into()) .expect("Error with media provider"); } }); + + // Wait for either task to complete (though they should run forever) + tokio::select! { + _ = manager => println!("Audio manager stopped unexpectedly"), + _ = update_handler => println!("Update handler stopped unexpectedly"), + } } } From 1df141c4c34177adc9d3baeb091f539b19a312f4 Mon Sep 17 00:00:00 2001 From: holby Date: Sat, 16 Nov 2024 19:00:07 -0500 Subject: [PATCH 13/22] cleanup --- .../src/providers/audio/audio_provider.rs | 133 +++++++++++------- 1 file changed, 79 insertions(+), 54 deletions(-) diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index caf89ebf..749454bf 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -104,74 +104,99 @@ impl MediaDeviceEventHandler { } } + fn get_device_info( + &self, + device: &IMMDevice, + ) -> windows::core::Result<(String, AudioDeviceInfo)> { + unsafe { + let device_id = device.GetId()?.to_string()?; + let mut device_state = self.device_state.lock().unwrap(); + + if !device_state.contains_key(&device_id) { + let new_device = self.register_new_device(device)?; + device_state.insert(device_id.clone(), new_device); + } + + let device_info = device_state.get(&device_id).unwrap(); + Ok(( + device_id.clone(), + AudioDeviceInfo { + name: device_info.name.clone(), + volume: device_info + .endpoint_volume + .GetMasterVolumeLevelScalar() + .unwrap_or(0.0), + is_default: self.is_default_device(&device_id)?, + }, + )) + } + } + + fn register_new_device( + &self, + device: &IMMDevice, + ) -> windows::core::Result { + unsafe { + let device_name = Self::get_device_name(device)?; + let endpoint_volume: IAudioEndpointVolume = + device.Activate(CLSCTX_ALL, None)?; + + let mut handler = self.clone(); + handler.current_device = device.GetId()?.to_string()?; + endpoint_volume.RegisterControlChangeNotify( + &IAudioEndpointVolumeCallback::from(handler), + )?; + + Ok(DeviceInfo { + name: device_name, + endpoint_volume, + }) + } + } + + fn is_default_device( + &self, + device_id: &str, + ) -> windows::core::Result { + unsafe { + let default = self + .enumerator + .GetDefaultAudioEndpoint(eRender, eMultimedia)?; + let default_id = default.GetId()?.to_string()?; + Ok(default_id == device_id) + } + } + fn enumerate_devices(&self) -> windows::core::Result<()> { unsafe { let collection = self .enumerator .EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE)?; - let mut audio_state = AUDIO_STATE.get().unwrap().lock().unwrap(); + let mut devices = HashMap::new(); - let mut device_state = self.device_state.lock().unwrap(); + let mut default_device = None; + // Get info for all active devices for i in 0..collection.GetCount()? { if let Ok(device) = collection.Item(i) { - let device_id = device.GetId()?.to_string()?; - - // Check if the device is already being monitored - if !device_state.contains_key(&device_id) { - let device_name = Self::get_device_name(&device)?; - let endpoint_volume: IAudioEndpointVolume = - device.Activate(CLSCTX_ALL, None)?; - let mut handler = self.clone(); - handler.current_device = device_id.clone(); - let callback = IAudioEndpointVolumeCallback::from(handler); - endpoint_volume.RegisterControlChangeNotify(&callback)?; - device_state.insert( - device_id.clone(), - DeviceInfo { - name: device_name.clone(), - endpoint_volume, - }, - ); + let (id, info) = self.get_device_info(&device)?; + if info.is_default { + default_device = Some(id.clone()); } - - let device_info = device_state.get(&device_id).unwrap(); - let volume = device_info - .endpoint_volume - .GetMasterVolumeLevelScalar() - .unwrap_or(0.0); - let is_default = self - .enumerator - .GetDefaultAudioEndpoint(eRender, eMultimedia) - .ok() - .and_then(|d| d.GetId().ok()) - .and_then(|id| id.to_string().ok()) - .as_ref() - .map(|id| id == &device_id) - .unwrap_or(false); - - devices.insert( - device_id, - AudioDeviceInfo { - name: device_info.name.clone(), - volume, - is_default, - }, - ); + devices.insert(id, info); } } - audio_state.devices = devices; - audio_state.default_device = self - .enumerator - .GetDefaultAudioEndpoint(eRender, eMultimedia) - .ok() - .and_then(|d| d.GetId().ok()) - .and_then(|id| id.to_string().ok()); - } + // Update global state once + if let Some(state) = AUDIO_STATE.get() { + let mut audio_state = state.lock().unwrap(); + audio_state.devices = devices; + audio_state.default_device = default_device; + } - AudioProvider::emit_volume(); - Ok(()) + AudioProvider::emit_volume(); + Ok(()) + } } } From 8371c96ec1cd2bbdfe38761efedad3cf5e4f0163 Mon Sep 17 00:00:00 2001 From: holby Date: Sat, 16 Nov 2024 19:18:22 -0500 Subject: [PATCH 14/22] prettier --- .../client-api/src/providers/audio/audio-provider-types.ts | 1 - .../src/providers/audio/create-audio-provider.ts | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/client-api/src/providers/audio/audio-provider-types.ts b/packages/client-api/src/providers/audio/audio-provider-types.ts index 6c170e61..ab615aed 100644 --- a/packages/client-api/src/providers/audio/audio-provider-types.ts +++ b/packages/client-api/src/providers/audio/audio-provider-types.ts @@ -17,4 +17,3 @@ export interface AudioOutput { devices: Record; defaultDevice: string | null; } - diff --git a/packages/client-api/src/providers/audio/create-audio-provider.ts b/packages/client-api/src/providers/audio/create-audio-provider.ts index 27ef0d0d..5c422e72 100644 --- a/packages/client-api/src/providers/audio/create-audio-provider.ts +++ b/packages/client-api/src/providers/audio/create-audio-provider.ts @@ -3,9 +3,9 @@ import { z } from 'zod'; import { createBaseProvider } from '../create-base-provider'; import { onProviderEmit } from '~/desktop'; import type { - AudioOutput, - AudioProvider, - AudioProviderConfig + AudioOutput, + AudioProvider, + AudioProviderConfig, } from './audio-provider-types'; const audioProviderConfigSchema = z.object({ @@ -28,4 +28,3 @@ export function createAudioProvider( }); }); } - From 9512ae85b2d661ab5dd9277a165bf3ec1d61bff9 Mon Sep 17 00:00:00 2001 From: holby Date: Sat, 16 Nov 2024 19:30:45 -0500 Subject: [PATCH 15/22] fmt --- examples/boilerplate-solid-ts/dist/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/boilerplate-solid-ts/dist/index.html b/examples/boilerplate-solid-ts/dist/index.html index 40b6af96..aeff5811 100644 --- a/examples/boilerplate-solid-ts/dist/index.html +++ b/examples/boilerplate-solid-ts/dist/index.html @@ -5,7 +5,7 @@ Zebar - + From a03f540840ed65c9f912ba5242376cc7c8ac264b Mon Sep 17 00:00:00 2001 From: holby Date: Sat, 16 Nov 2024 19:32:32 -0500 Subject: [PATCH 16/22] fmt --- packages/desktop/src/providers/audio/mod.rs | 1 - packages/desktop/src/providers/mod.rs | 4 ++-- packages/desktop/src/providers/provider_config.rs | 6 +++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/providers/audio/mod.rs b/packages/desktop/src/providers/audio/mod.rs index cd9acb94..f59b57a3 100644 --- a/packages/desktop/src/providers/audio/mod.rs +++ b/packages/desktop/src/providers/audio/mod.rs @@ -1,4 +1,3 @@ - mod audio_provider; pub use audio_provider::*; diff --git a/packages/desktop/src/providers/mod.rs b/packages/desktop/src/providers/mod.rs index c32ae277..bad74540 100644 --- a/packages/desktop/src/providers/mod.rs +++ b/packages/desktop/src/providers/mod.rs @@ -1,3 +1,5 @@ +#[cfg(windows)] +mod audio; mod battery; mod cpu; mod disk; @@ -16,8 +18,6 @@ mod provider_config; mod provider_manager; mod provider_output; mod provider_ref; -#[cfg(windows)] -mod audio; mod weather; pub use provider::*; diff --git a/packages/desktop/src/providers/provider_config.rs b/packages/desktop/src/providers/provider_config.rs index 236d2641..ba04a0cc 100644 --- a/packages/desktop/src/providers/provider_config.rs +++ b/packages/desktop/src/providers/provider_config.rs @@ -1,7 +1,11 @@ use serde::Deserialize; use super::{ - audio::AudioProviderConfig, battery::BatteryProviderConfig, cpu::CpuProviderConfig, disk::DiskProviderConfig, host::HostProviderConfig, ip::IpProviderConfig, memory::MemoryProviderConfig, network::NetworkProviderConfig, weather::WeatherProviderConfig + audio::AudioProviderConfig, battery::BatteryProviderConfig, + cpu::CpuProviderConfig, disk::DiskProviderConfig, + host::HostProviderConfig, ip::IpProviderConfig, + memory::MemoryProviderConfig, network::NetworkProviderConfig, + weather::WeatherProviderConfig, }; #[cfg(windows)] use super::{ From a4f5c1b2221b179195b567b42d088984c8f9086c Mon Sep 17 00:00:00 2001 From: holby Date: Sat, 16 Nov 2024 20:18:17 -0500 Subject: [PATCH 17/22] build check fix --- packages/desktop/src/providers/provider_output.rs | 1 + packages/desktop/src/providers/provider_ref.rs | 15 +++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/desktop/src/providers/provider_output.rs b/packages/desktop/src/providers/provider_output.rs index 479bc762..3afe315a 100644 --- a/packages/desktop/src/providers/provider_output.rs +++ b/packages/desktop/src/providers/provider_output.rs @@ -13,6 +13,7 @@ use super::{ #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(untagged)] pub enum ProviderOutput { + #[cfg(windows)] Audio(AudioOutput), Battery(BatteryOutput), Cpu(CpuOutput), diff --git a/packages/desktop/src/providers/provider_ref.rs b/packages/desktop/src/providers/provider_ref.rs index 7af8d656..b8b94cae 100644 --- a/packages/desktop/src/providers/provider_ref.rs +++ b/packages/desktop/src/providers/provider_ref.rs @@ -10,17 +10,16 @@ use tokio::{ }; use tracing::{info, warn}; +#[cfg(windows)] use super::{ - audio::AudioProvider, battery::BatteryProvider, cpu::CpuProvider, - disk::DiskProvider, host::HostProvider, ip::IpProvider, - memory::MemoryProvider, network::NetworkProvider, - weather::WeatherProvider, Provider, ProviderConfig, ProviderOutput, - SharedProviderState, + audio::AudioProvider, keyboard::KeyboardProvider, + komorebi::KomorebiProvider, media::MediaProvider, }; -#[cfg(windows)] use super::{ - keyboard::KeyboardProvider, komorebi::KomorebiProvider, - media::MediaProvider, + battery::BatteryProvider, cpu::CpuProvider, disk::DiskProvider, + host::HostProvider, ip::IpProvider, memory::MemoryProvider, + network::NetworkProvider, weather::WeatherProvider, Provider, + ProviderConfig, ProviderOutput, SharedProviderState, }; /// Reference to an active provider. From 7aa0f47b236daeebfe05010d5f0acb906c4da37f Mon Sep 17 00:00:00 2001 From: holby Date: Sat, 16 Nov 2024 20:23:47 -0500 Subject: [PATCH 18/22] more build fix --- .../desktop/src/providers/audio/audio_provider.rs | 1 - packages/desktop/src/providers/provider_config.rs | 15 +++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index 749454bf..16c1b7d9 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -187,7 +187,6 @@ impl MediaDeviceEventHandler { } } - // Update global state once if let Some(state) = AUDIO_STATE.get() { let mut audio_state = state.lock().unwrap(); audio_state.devices = devices; diff --git a/packages/desktop/src/providers/provider_config.rs b/packages/desktop/src/providers/provider_config.rs index ba04a0cc..2a35a1f5 100644 --- a/packages/desktop/src/providers/provider_config.rs +++ b/packages/desktop/src/providers/provider_config.rs @@ -1,16 +1,15 @@ use serde::Deserialize; +#[cfg(windows)] use super::{ - audio::AudioProviderConfig, battery::BatteryProviderConfig, - cpu::CpuProviderConfig, disk::DiskProviderConfig, - host::HostProviderConfig, ip::IpProviderConfig, - memory::MemoryProviderConfig, network::NetworkProviderConfig, - weather::WeatherProviderConfig, + audio::AudioProviderConfig, keyboard::KeyboardProviderConfig, + komorebi::KomorebiProviderConfig, media::MediaProviderConfig, }; -#[cfg(windows)] use super::{ - keyboard::KeyboardProviderConfig, komorebi::KomorebiProviderConfig, - media::MediaProviderConfig, + battery::BatteryProviderConfig, cpu::CpuProviderConfig, + disk::DiskProviderConfig, host::HostProviderConfig, + ip::IpProviderConfig, memory::MemoryProviderConfig, + network::NetworkProviderConfig, weather::WeatherProviderConfig, }; #[derive(Deserialize, Debug)] From 2da6ba20ababde7c5cf556cdf911b09e16954bac Mon Sep 17 00:00:00 2001 From: holby Date: Sat, 16 Nov 2024 20:28:19 -0500 Subject: [PATCH 19/22] even more --- packages/desktop/src/providers/provider_output.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/providers/provider_output.rs b/packages/desktop/src/providers/provider_output.rs index 3afe315a..c922fdd8 100644 --- a/packages/desktop/src/providers/provider_output.rs +++ b/packages/desktop/src/providers/provider_output.rs @@ -1,13 +1,14 @@ use serde::Serialize; +#[cfg(windows)] use super::{ - audio::AudioOutput, battery::BatteryOutput, cpu::CpuOutput, - disk::DiskOutput, host::HostOutput, ip::IpOutput, memory::MemoryOutput, - network::NetworkOutput, weather::WeatherOutput, + audio::AudioOutput, keyboard::KeyboardOutput, komorebi::KomorebiOutput, + media::MediaOutput, }; -#[cfg(windows)] use super::{ - keyboard::KeyboardOutput, komorebi::KomorebiOutput, media::MediaOutput, + battery::BatteryOutput, cpu::CpuOutput, disk::DiskOutput, + host::HostOutput, ip::IpOutput, memory::MemoryOutput, + network::NetworkOutput, weather::WeatherOutput, }; #[derive(Debug, Clone, PartialEq, Serialize)] From 7c72f6e017d171cf273edbdccf69396c5e0ffdcf Mon Sep 17 00:00:00 2001 From: holby Date: Sun, 17 Nov 2024 14:40:13 -0500 Subject: [PATCH 20/22] update readme --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 8c439247..65eaddd7 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ In some cases, updating to the latest Microsoft Webview2 version is needed ([sta Through the `zebar` NPM package, Zebar exposes various system information via reactive "providers". Providers are a collection of functions and variables that can change over time. +- [audio](#Audio) - [battery](#Battery) - [cpu](#CPU) - [date](#Date) @@ -72,6 +73,30 @@ Through the `zebar` NPM package, Zebar exposes various system information via re - [network](#Network) - [weather](#Weather) +### Media + +#### Config + +No config options. + +#### Outputs + +| Variable | Description | Return type | Supported OS | +| ------------------- | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `defaultDevice` | ID of default audio output device. | `string \| null` | microsoft icon | +| `devices` | All devices. | `Record` | microsoft icon | + +#### Return types + +#### `AudioDeviceInfo` + +| Variable | Description | Return type | +| ------------------ | ----------------------------- | ----------------------- | +| `deviceId` | Device ID. | `string` | +| `name` | Display name. | `string ` | +| `volume` | Volume as a % of maximum volume. Returned value is between `0` and `100`. | `string ` | +| `isDefault` | `True` if the device is selected as the default output device.| `string \| null` | + ### Battery #### Config From 9f716834e2859ff110ffa0efbed936cfe1cdf344 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 20 Nov 2024 20:15:37 +0800 Subject: [PATCH 21/22] feat: volume to whole number; change `devices` to array; change `defaultDevice` to object --- README.md | 14 +- examples/boilerplate-solid-ts/dist/index.html | 10 +- examples/boilerplate-solid-ts/src/index.tsx | 4 +- .../providers/audio/audio-provider-types.ts | 12 +- .../providers/audio/create-audio-provider.ts | 1 - .../src/providers/audio/audio_provider.rs | 134 +++++++++++------- 6 files changed, 99 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 65eaddd7..74898cd3 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Through the `zebar` NPM package, Zebar exposes various system information via re - [network](#Network) - [weather](#Weather) -### Media +### Audio #### Config @@ -83,19 +83,19 @@ No config options. | Variable | Description | Return type | Supported OS | | ------------------- | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `defaultDevice` | ID of default audio output device. | `string \| null` | microsoft icon | -| `devices` | All devices. | `Record` | microsoft icon | +| `defaultDevice` | Default audio output device. | `AudioDevice \| null` | microsoft icon | +| `devices` | All devices. | `AudioDevice[]` | microsoft icon | #### Return types -#### `AudioDeviceInfo` +#### `AudioDevice` | Variable | Description | Return type | | ------------------ | ----------------------------- | ----------------------- | | `deviceId` | Device ID. | `string` | -| `name` | Display name. | `string ` | -| `volume` | Volume as a % of maximum volume. Returned value is between `0` and `100`. | `string ` | -| `isDefault` | `True` if the device is selected as the default output device.| `string \| null` | +| `name` | Friendly display name. | `string` | +| `volume` | Volume as a % of maximum volume. Returned value is between `0` and `100`. | `number` | +| `isDefault` | `true` if the device is selected as the default output device.| `boolean` | ### Battery diff --git a/examples/boilerplate-solid-ts/dist/index.html b/examples/boilerplate-solid-ts/dist/index.html index aeff5811..5c6c9a50 100644 --- a/examples/boilerplate-solid-ts/dist/index.html +++ b/examples/boilerplate-solid-ts/dist/index.html @@ -3,13 +3,13 @@ - Zebar - - - -
+

+ Boilerplate for SolidJS with TypeScript. Run npm i and + npm run dev in the solid-ts directory to + run this example. +

diff --git a/examples/boilerplate-solid-ts/src/index.tsx b/examples/boilerplate-solid-ts/src/index.tsx index a4d27ddd..bdbc228c 100644 --- a/examples/boilerplate-solid-ts/src/index.tsx +++ b/examples/boilerplate-solid-ts/src/index.tsx @@ -23,8 +23,8 @@ function App() { return (
- {output.audio?.devices[output.audio?.defaultDevice!].name} : - {output.audio?.devices[output.audio?.defaultDevice!].volume} + {output.audio?.defaultDevice?.name} - + {output.audio?.defaultDevice?.volume}
Media: {output.media?.session?.title} - diff --git a/packages/client-api/src/providers/audio/audio-provider-types.ts b/packages/client-api/src/providers/audio/audio-provider-types.ts index ab615aed..bfccac52 100644 --- a/packages/client-api/src/providers/audio/audio-provider-types.ts +++ b/packages/client-api/src/providers/audio/audio-provider-types.ts @@ -6,14 +6,14 @@ export interface AudioProviderConfig { export type AudioProvider = Provider; -export interface AudioDeviceInfo { +export interface AudioOutput { + defaultDevice: AudioDevice; + devices: AudioDevice[]; +} + +export interface AudioDevice { deviceId: string; name: string; volume: number; isDefault: boolean; } - -export interface AudioOutput { - devices: Record; - defaultDevice: string | null; -} diff --git a/packages/client-api/src/providers/audio/create-audio-provider.ts b/packages/client-api/src/providers/audio/create-audio-provider.ts index 5c422e72..a96af3c7 100644 --- a/packages/client-api/src/providers/audio/create-audio-provider.ts +++ b/packages/client-api/src/providers/audio/create-audio-provider.ts @@ -10,7 +10,6 @@ import type { const audioProviderConfigSchema = z.object({ type: z.literal('audio'), - refreshInterval: z.coerce.number().default(60 * 1000), }); export function createAudioProvider( diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index 16c1b7d9..af760c75 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + ops::Mul, sync::{Arc, Mutex, OnceLock}, time::Duration, }; @@ -11,6 +12,7 @@ use tokio::{ task, time::sleep, }; +use tracing::debug; use windows::Win32::{ Devices::FunctionDiscovery::PKEY_Device_FriendlyName, Media::Audio::{ @@ -35,6 +37,7 @@ use crate::providers::{Provider, ProviderOutput, ProviderResult}; static PROVIDER_TX: OnceLock> = OnceLock::new(); + static AUDIO_STATE: OnceLock>> = OnceLock::new(); #[derive(Deserialize, Debug)] @@ -43,23 +46,24 @@ pub struct AudioProviderConfig {} #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] -pub struct AudioDeviceInfo { +pub struct AudioDevice { pub name: String, - pub volume: f32, + pub device_id: String, + pub volume: u32, pub is_default: bool, } #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AudioOutput { - pub devices: HashMap, - pub default_device: Option, + pub devices: Vec, + pub default_device: Option, } impl AudioOutput { fn new() -> Self { Self { - devices: HashMap::new(), + devices: Vec::new(), default_device: None, } } @@ -80,13 +84,13 @@ struct MediaDeviceEventHandler { enumerator: IMMDeviceEnumerator, device_state: Arc>>, current_device: String, - update_tx: mpsc::Sender<(String, f32)>, + update_tx: mpsc::Sender<(String, u32)>, } impl MediaDeviceEventHandler { fn new( enumerator: IMMDeviceEnumerator, - update_tx: mpsc::Sender<(String, f32)>, + update_tx: mpsc::Sender<(String, u32)>, ) -> Self { Self { enumerator, @@ -107,7 +111,7 @@ impl MediaDeviceEventHandler { fn get_device_info( &self, device: &IMMDevice, - ) -> windows::core::Result<(String, AudioDeviceInfo)> { + ) -> windows::core::Result { unsafe { let device_id = device.GetId()?.to_string()?; let mut device_state = self.device_state.lock().unwrap(); @@ -118,17 +122,20 @@ impl MediaDeviceEventHandler { } let device_info = device_state.get(&device_id).unwrap(); - Ok(( - device_id.clone(), - AudioDeviceInfo { - name: device_info.name.clone(), - volume: device_info - .endpoint_volume - .GetMasterVolumeLevelScalar() - .unwrap_or(0.0), - is_default: self.is_default_device(&device_id)?, - }, - )) + let is_default = self.is_default_device(&device_id)?; + let volume = device_info + .endpoint_volume + .GetMasterVolumeLevelScalar() + .unwrap_or(0.0) + .mul(100.0) + .round() as u32; + + Ok(AudioDevice { + device_id, + name: device_info.name.clone(), + volume, + is_default, + }) } } @@ -173,17 +180,17 @@ impl MediaDeviceEventHandler { .enumerator .EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE)?; - let mut devices = HashMap::new(); + let mut devices = Vec::new(); let mut default_device = None; - // Get info for all active devices + // Get info for all active devices. for i in 0..collection.GetCount()? { if let Ok(device) = collection.Item(i) { - let (id, info) = self.get_device_info(&device)?; + let info = self.get_device_info(&device)?; if info.is_default { - default_device = Some(id.clone()); + default_device = Some(info.clone()); } - devices.insert(id, info); + devices.push(info); } } @@ -217,25 +224,24 @@ impl IAudioEndpointVolumeCallback_Impl for MediaDeviceEventHandler_Impl { &self, data: *mut windows::Win32::Media::Audio::AUDIO_VOLUME_NOTIFICATION_DATA, ) -> windows::core::Result<()> { - unsafe { - if let Some(data) = data.as_ref() { - let device_id = self.current_device.clone(); - let volume = data.fMasterVolume; - let _ = self.update_tx.blocking_send((device_id, volume)); - } - Ok(()) + if let Some(data) = unsafe { data.as_ref() } { + let device_id = self.current_device.clone(); + let volume = data.fMasterVolume.mul(100.0).round() as u32; + + let _ = self.update_tx.blocking_send((device_id, volume)); } + Ok(()) } } impl IMMNotificationClient_Impl for MediaDeviceEventHandler_Impl { fn OnDefaultDeviceChanged( &self, - flow: EDataFlow, + data_flow: EDataFlow, role: ERole, - _pwstrDefaultDeviceId: &PCWSTR, + _default_device_id: &PCWSTR, ) -> windows::core::Result<()> { - if flow == eRender && role == eMultimedia { + if data_flow == eRender && role == eMultimedia { self.enumerate_devices()?; } Ok(()) @@ -243,29 +249,29 @@ impl IMMNotificationClient_Impl for MediaDeviceEventHandler_Impl { fn OnDeviceStateChanged( &self, - _pwstrDeviceId: &PCWSTR, - _dwNewState: DEVICE_STATE, + _device_id: &PCWSTR, + _new_state: DEVICE_STATE, ) -> windows::core::Result<()> { self.enumerate_devices() } fn OnDeviceAdded( &self, - _pwstrDeviceId: &PCWSTR, + _device_id: &PCWSTR, ) -> windows::core::Result<()> { self.enumerate_devices() } fn OnDeviceRemoved( &self, - _pwstrDeviceId: &PCWSTR, + _device_id: &PCWSTR, ) -> windows::core::Result<()> { self.enumerate_devices() } fn OnPropertyValueChanged( &self, - _pwstrDeviceId: &PCWSTR, + _device_id: &PCWSTR, _key: &PROPERTYKEY, ) -> windows::core::Result<()> { Ok(()) @@ -288,37 +294,54 @@ impl AudioProvider { } } - async fn handle_volume_updates(mut rx: mpsc::Receiver<(String, f32)>) { + async fn handle_volume_updates(mut rx: mpsc::Receiver<(String, u32)>) { const PROCESS_DELAY: Duration = Duration::from_millis(50); - let mut latest_updates: HashMap = HashMap::new(); + let mut latest_updates = HashMap::new(); while let Some((device_id, volume)) = rx.recv().await { latest_updates.insert(device_id, volume); - // Collect any additional pending updates without waiting + // Collect any additional pending updates without waiting. while let Ok((device_id, volume)) = rx.try_recv() { latest_updates.insert(device_id, volume); } - // Brief delay to collect more potential updates + // Brief delay to collect more potential updates. sleep(PROCESS_DELAY).await; - // Process all collected updates + // Process all collected updates. if let Some(state) = AUDIO_STATE.get() { - let mut output = state.lock().unwrap(); - for (device_id, volume) in latest_updates.drain() { - if let Some(device) = output.devices.get_mut(&device_id) { - device.volume = volume; + { + let mut output = state.lock().unwrap(); + for (device_id, volume) in latest_updates.drain() { + debug!( + "Updating volume to {} for device: {}", + volume, device_id + ); + + // Update device in the devices list. + if let Some(device) = + output.devices.iter_mut().find(|d| d.device_id == device_id) + { + device.volume = volume; + } + + // Update default device if it matches. + if let Some(default_device) = &mut output.default_device { + if default_device.device_id == device_id { + default_device.volume = volume; + } + } } } - drop(output); + Self::emit_volume(); } } } fn create_audio_manager( - update_tx: mpsc::Sender<(String, f32)>, + update_tx: mpsc::Sender<(String, u32)>, ) -> anyhow::Result<()> { unsafe { let _ = CoInitializeEx(None, COINIT_MULTITHREADED); @@ -353,12 +376,13 @@ impl Provider for AudioProvider { .set(Arc::new(Mutex::new(AudioOutput::new()))) .expect("Error setting initial audio state"); - // Create a channel for volume updates + // Create a channel for volume updates. let (update_tx, update_rx) = mpsc::channel(100); - // Spawn both tasks + // Spawn both tasks. let update_handler = task::spawn(Self::handle_volume_updates(update_rx)); + let manager = task::spawn_blocking(move || { if let Err(err) = Self::create_audio_manager(update_tx) { emit_result_tx @@ -367,10 +391,10 @@ impl Provider for AudioProvider { } }); - // Wait for either task to complete (though they should run forever) + // Wait for either task to complete (though they should run forever). tokio::select! { - _ = manager => println!("Audio manager stopped unexpectedly"), - _ = update_handler => println!("Update handler stopped unexpectedly"), + _ = manager => debug!("Audio manager stopped unexpectedly"), + _ = update_handler => debug!("Update handler stopped unexpectedly"), } } } From 2b6655ea40cb2819c1d7d93a2a6fd9e15838f31a Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Wed, 20 Nov 2024 21:06:47 +0800 Subject: [PATCH 22/22] refactor: rename `defaultDevice` -> `defaultPlaybackDevice`; rename `devices` -> `playbackDevices` --- README.md | 8 +++---- examples/boilerplate-solid-ts/src/index.tsx | 4 ++-- .../providers/audio/audio-provider-types.ts | 4 ++-- .../src/providers/audio/audio_provider.rs | 22 +++++++++++-------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 74898cd3..8abe1b12 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,8 @@ No config options. | Variable | Description | Return type | Supported OS | | ------------------- | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `defaultDevice` | Default audio output device. | `AudioDevice \| null` | microsoft icon | -| `devices` | All devices. | `AudioDevice[]` | microsoft icon | +| `defaultPlaybackDevice` | Default audio playback device. | `AudioDevice \| null` | microsoft icon | +| `playbackDevices` | All audio playback devices. | `AudioDevice[]` | microsoft icon | #### Return types @@ -93,9 +93,9 @@ No config options. | Variable | Description | Return type | | ------------------ | ----------------------------- | ----------------------- | | `deviceId` | Device ID. | `string` | -| `name` | Friendly display name. | `string` | +| `name` | Friendly display name of device. | `string` | | `volume` | Volume as a % of maximum volume. Returned value is between `0` and `100`. | `number` | -| `isDefault` | `true` if the device is selected as the default output device.| `boolean` | +| `isDefault` | `true` if the device is selected as the default playback device.| `boolean` | ### Battery diff --git a/examples/boilerplate-solid-ts/src/index.tsx b/examples/boilerplate-solid-ts/src/index.tsx index bdbc228c..1a6ce453 100644 --- a/examples/boilerplate-solid-ts/src/index.tsx +++ b/examples/boilerplate-solid-ts/src/index.tsx @@ -23,8 +23,8 @@ function App() { return (
- {output.audio?.defaultDevice?.name} - - {output.audio?.defaultDevice?.volume} + {output.audio?.defaultPlaybackDevice?.name} - + {output.audio?.defaultPlaybackDevice?.volume}
Media: {output.media?.session?.title} - diff --git a/packages/client-api/src/providers/audio/audio-provider-types.ts b/packages/client-api/src/providers/audio/audio-provider-types.ts index bfccac52..dab6d41d 100644 --- a/packages/client-api/src/providers/audio/audio-provider-types.ts +++ b/packages/client-api/src/providers/audio/audio-provider-types.ts @@ -7,8 +7,8 @@ export interface AudioProviderConfig { export type AudioProvider = Provider; export interface AudioOutput { - defaultDevice: AudioDevice; - devices: AudioDevice[]; + defaultPlaybackDevice: AudioDevice; + playbackDevices: AudioDevice[]; } export interface AudioDevice { diff --git a/packages/desktop/src/providers/audio/audio_provider.rs b/packages/desktop/src/providers/audio/audio_provider.rs index af760c75..3c71e1c4 100644 --- a/packages/desktop/src/providers/audio/audio_provider.rs +++ b/packages/desktop/src/providers/audio/audio_provider.rs @@ -56,15 +56,15 @@ pub struct AudioDevice { #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AudioOutput { - pub devices: Vec, - pub default_device: Option, + pub playback_devices: Vec, + pub default_playback_device: Option, } impl AudioOutput { fn new() -> Self { Self { - devices: Vec::new(), - default_device: None, + playback_devices: Vec::new(), + default_playback_device: None, } } } @@ -196,8 +196,8 @@ impl MediaDeviceEventHandler { if let Some(state) = AUDIO_STATE.get() { let mut audio_state = state.lock().unwrap(); - audio_state.devices = devices; - audio_state.default_device = default_device; + audio_state.playback_devices = devices; + audio_state.default_playback_device = default_device; } AudioProvider::emit_volume(); @@ -320,14 +320,18 @@ impl AudioProvider { ); // Update device in the devices list. - if let Some(device) = - output.devices.iter_mut().find(|d| d.device_id == device_id) + if let Some(device) = output + .playback_devices + .iter_mut() + .find(|d| d.device_id == device_id) { device.volume = volume; } // Update default device if it matches. - if let Some(default_device) = &mut output.default_device { + if let Some(default_device) = + &mut output.default_playback_device + { if default_device.device_id == device_id { default_device.volume = volume; }