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"); + } + }); + } +}