#![allow(unsafe_code)]
#![allow(
clippy::inline_always,
clippy::ref_as_ptr,
clippy::redundant_pub_crate,
reason = "windows `#[implement]` expansion + private module visibility (same as wasapi_process.rs)"
)]
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use crate::{CaptureError, DeviceEvent, DeviceHotplug, DeviceId, DeviceKind};
use windows::Win32::Foundation::PROPERTYKEY;
use windows::Win32::Media::Audio::{
DEVICE_STATE, EDataFlow, ERole, IMMDeviceEnumerator, IMMEndpoint, IMMNotificationClient,
IMMNotificationClient_Impl, MMDeviceEnumerator, eCapture, eConsole, eRender,
};
use windows::Win32::System::Com::{
CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx,
};
use windows_core::{Interface, PCWSTR, implement};
use crate::windows_audio::ComGuard;
const HOTPLUG_QUEUE_CAP: usize = 64;
struct HotplugQueue {
events: Mutex<VecDeque<DeviceEvent>>,
}
fn push_bounded(queue: &HotplugQueue, event: DeviceEvent) {
let Ok(mut events) = queue.events.lock() else {
return;
};
if events.len() >= HOTPLUG_QUEUE_CAP {
let _ = events.pop_front();
}
events.push_back(event);
}
fn map_dataflow_to_kind(flow: EDataFlow) -> Option<DeviceKind> {
if flow == eCapture {
Some(DeviceKind::Microphone)
} else if flow == eRender {
Some(DeviceKind::Loopback)
} else {
None
}
}
fn map_default_changed_kind(flow: EDataFlow, role: ERole) -> Option<DeviceKind> {
if role == eConsole {
map_dataflow_to_kind(flow)
} else {
None
}
}
fn pcwstr_to_owned_string(raw: PCWSTR) -> Option<String> {
if raw.is_null() {
return None;
}
unsafe { raw.to_string() }.ok()
}
fn lookup_endpoint_kind(id: &str) -> Option<DeviceKind> {
let hr = unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) };
if hr.is_err() {
return None;
}
let _com = ComGuard;
let enumerator: IMMDeviceEnumerator =
unsafe { CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_INPROC_SERVER) }.ok()?;
let wide: Vec<u16> = id.encode_utf16().chain(std::iter::once(0)).collect();
let id_pcwstr = PCWSTR::from_raw(wide.as_ptr());
let device = unsafe { enumerator.GetDevice(id_pcwstr) }.ok()?;
let endpoint: IMMEndpoint = device.cast().ok()?;
let flow = unsafe { endpoint.GetDataFlow() }.ok()?;
map_dataflow_to_kind(flow)
}
#[implement(IMMNotificationClient)]
struct NotificationSink {
queue: Arc<HotplugQueue>,
kinds: Vec<DeviceKind>,
}
impl NotificationSink {
fn push_endpoint_event(
&self,
raw_id: PCWSTR,
build: impl FnOnce(DeviceId, DeviceKind) -> DeviceEvent,
) {
let Some(id_string) = pcwstr_to_owned_string(raw_id) else {
return;
};
let Some(kind) = lookup_endpoint_kind(&id_string).filter(|k| self.kinds.contains(k)) else {
return;
};
push_bounded(
&self.queue,
build(DeviceId::from_wasapi_endpoint_id(id_string), kind),
);
}
}
impl IMMNotificationClient_Impl for NotificationSink_Impl {
fn OnDeviceStateChanged(
&self,
pwstrdeviceid: &PCWSTR,
_dwnewstate: DEVICE_STATE,
) -> windows_core::Result<()> {
self.push_endpoint_event(*pwstrdeviceid, |id, kind| DeviceEvent::StateChanged {
id,
kind,
});
Ok(())
}
fn OnDeviceAdded(&self, pwstrdeviceid: &PCWSTR) -> windows_core::Result<()> {
self.push_endpoint_event(*pwstrdeviceid, |id, kind| DeviceEvent::Added { id, kind });
Ok(())
}
fn OnDeviceRemoved(&self, pwstrdeviceid: &PCWSTR) -> windows_core::Result<()> {
self.push_endpoint_event(*pwstrdeviceid, |id, kind| DeviceEvent::Removed { id, kind });
Ok(())
}
fn OnDefaultDeviceChanged(
&self,
flow: EDataFlow,
role: ERole,
pwstrdefaultdeviceid: &PCWSTR,
) -> windows_core::Result<()> {
let Some(kind) = map_default_changed_kind(flow, role) else {
return Ok(());
};
if !self.kinds.contains(&kind) {
return Ok(());
}
let id =
pcwstr_to_owned_string(*pwstrdefaultdeviceid).map(DeviceId::from_wasapi_endpoint_id);
push_bounded(&self.queue, DeviceEvent::DefaultChanged { kind, id });
Ok(())
}
fn OnPropertyValueChanged(
&self,
_pwstrdeviceid: &PCWSTR,
_key: &PROPERTYKEY,
) -> windows_core::Result<()> {
Ok(())
}
}
pub struct WindowsDeviceHotplug {
inner: Option<HotplugSession>,
}
struct HotplugSession {
enumerator: IMMDeviceEnumerator,
client: IMMNotificationClient,
queue: Arc<HotplugQueue>,
_com: ComGuard,
}
impl WindowsDeviceHotplug {
pub fn open(kinds: &[DeviceKind]) -> Result<Self, CaptureError> {
if kinds.is_empty() {
return Err(CaptureError::InvalidInput);
}
let mut watched = Vec::with_capacity(kinds.len());
for &kind in kinds {
match kind {
DeviceKind::Microphone | DeviceKind::Loopback => watched.push(kind),
_ => return Err(CaptureError::Unsupported),
}
}
let hr = unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) };
if hr.is_err() {
return Err(CaptureError::Backend);
}
let com = ComGuard;
let enumerator: IMMDeviceEnumerator =
unsafe { CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_INPROC_SERVER) }
.map_err(|_| CaptureError::Backend)?;
let queue = Arc::new(HotplugQueue {
events: Mutex::new(VecDeque::new()),
});
let sink_queue = Arc::clone(&queue);
let client: IMMNotificationClient = NotificationSink {
queue: sink_queue,
kinds: watched,
}
.into();
unsafe { enumerator.RegisterEndpointNotificationCallback(&client) }
.map_err(|_| CaptureError::Backend)?;
Ok(Self {
inner: Some(HotplugSession {
enumerator,
client,
queue,
_com: com,
}),
})
}
}
impl DeviceHotplug for WindowsDeviceHotplug {
fn poll_event(&mut self) -> Result<Option<DeviceEvent>, CaptureError> {
let Some(session) = self.inner.as_ref() else {
return Err(CaptureError::Closed);
};
let mut events = session
.queue
.events
.lock()
.map_err(|_| CaptureError::Backend)?;
Ok(events.pop_front())
}
fn close(&mut self) -> Result<(), CaptureError> {
let Some(session) = self.inner.take() else {
return Ok(());
};
unsafe {
session
.enumerator
.UnregisterEndpointNotificationCallback(&session.client)
}
.map_err(|_| CaptureError::Backend)?;
Ok(())
}
}
impl Drop for WindowsDeviceHotplug {
fn drop(&mut self) {
let _ = self.close();
}
}
#[cfg(test)]
#[path = "hotplug_tests.rs"]
mod tests;