media_pp\elements\source\capture\windows/wasapi_capture_source.rs
1use std::{
2 ffi::c_void,
3 ptr,
4 sync::Arc,
5 thread,
6 time::{Duration, Instant},
7};
8
9use crate::pp_log::{PpLog, pp_error, pp_info};
10use ffmpeg_next as ffmpeg;
11use thiserror::Error as ThisError;
12use windows::Win32::{
13 Media::Audio::{
14 AUDCLNT_BUFFERFLAGS_SILENT, AUDCLNT_E_DEVICE_INVALIDATED, AUDCLNT_SHAREMODE_SHARED,
15 AUDCLNT_STREAMFLAGS_LOOPBACK, IAudioCaptureClient, IAudioClient,
16 },
17 System::Com::{CLSCTX_ALL, CoTaskMemFree},
18};
19
20use crate::{
21 buffer::MediaBuffer,
22 bus::{Bus, BusEvent},
23 control::{self, ControlMsg, ControlOutcome, ControlReceiver, RequestKind},
24 element::{Element, ElementType, Source, SourceElement, element_pp_log},
25 error::Result,
26 pad::SrcPad,
27 platform::windows::wasapi::{
28 ComApartment, WasapiDevice, WasapiDeviceKind, list_devices as enumerate_wasapi_devices,
29 open_device, resolve_mix_format,
30 },
31 schedule::ActiveTimeline,
32};
33
34/// How long [`WasapiCaptureSource::run`] sleeps between checks of
35/// `GetNextPacketSize` — also bounds `Stop` latency, same reasoning as
36/// [`crate::elements::DxgiCaptureSource`]'s own `POLL_GRANULARITY`. Plain
37/// polling rather than `IAudioClient::SetEventHandle` + `WaitForSingleObject`:
38/// event-driven signaling is well documented as unreliable specifically
39/// for loopback capture (Microsoft's own WASAPILoopbackCapture sample
40/// polls for exactly this reason, rather than using
41/// `AUDCLNT_STREAMFLAGS_EVENTCALLBACK`). Using the same poll loop for
42/// `AudioCaptureMode::Microphone` too keeps one code path instead of
43/// branching between event-driven and polled just for a latency
44/// difference that doesn't matter at these timescales.
45const POLL_INTERVAL: Duration = Duration::from_millis(20);
46
47/// WASAPI shared-mode buffer size, in 100ns units (200ms) — comfortably
48/// larger than `POLL_INTERVAL` so this element's own wakeup cadence, not
49/// the device's ring buffer, is what bounds latency.
50const BUFFER_DURATION_100NS: i64 = 200 * 10_000;
51
52/// Errors specific to `WasapiCaptureSource`. Converts into the crate-wide
53/// `Error` via `?` (see [`crate::error::Error`]).
54#[derive(Debug, ThisError)]
55pub enum WasapiCaptureSourceError {
56 #[error("windows error: {0}")]
57 Windows(#[from] windows::core::Error),
58
59 /// `AUDCLNT_E_DEVICE_INVALIDATED` specifically, broken out of the
60 /// generic [`WasapiCaptureSourceError::Windows`] variant for the same
61 /// reason [`crate::elements::DxgiCaptureSourceError::AccessLost`] is:
62 /// the single most common *recoverable* failure (default device
63 /// changed, device unplugged, format changed) surfaces this way. Same
64 /// "fail fast, caller rebuilds a fresh one" contract
65 /// [`crate::elements::RtspSource`]/[`crate::elements::DxgiCaptureSource`]
66 /// already document: this element doesn't retry internally.
67 #[error("AUDCLNT_E_DEVICE_INVALIDATED — audio device needs to be reopened")]
68 DeviceInvalidated,
69
70 #[error("WasapiCaptureSource doesn't support seeking a live capture")]
71 SeekUnsupported,
72
73 #[error("unsupported WASAPI mix format: format_tag={format_tag}, bits_per_sample={bits}")]
74 UnsupportedMixFormat { format_tag: u32, bits: u16 },
75}
76
77/// Construction-time options for [`WasapiCaptureSource::open`].
78#[derive(Debug, Clone)]
79pub struct WasapiCaptureOptions {
80 /// Which endpoint to capture from — one entry out of
81 /// [`WasapiCaptureSource::list_devices`] (or hand-built, if the caller
82 /// already knows a device's id/kind some other way).
83 pub device: WasapiDevice,
84}
85
86/// Captures audio via WASAPI (`IAudioClient`/`IAudioCaptureClient`) —
87/// GStreamer's `wasapi2src` equivalent. One src pad, pushing
88/// `MediaBuffer::Audio` frames in the captured device's own native mix
89/// format/rate/channel count — no resampling. Same division of labor as
90/// [`crate::elements::DxgiCaptureSource`] emitting raw `Pixel::BGRA` and
91/// leaving conversion to a downstream [`crate::elements::Scaler`]: if
92/// something downstream needs a fixed sample rate/format, use
93/// [`crate::elements::AudioResampler`] rather than hiding conversion in
94/// this element.
95///
96/// Polls `IAudioCaptureClient::GetNextPacketSize` on a short fixed
97/// interval (`POLL_INTERVAL`) rather than waiting on a WASAPI-signaled
98/// event — see that constant's own docs on why event-driven mode isn't
99/// used here.
100///
101/// Emits continuously from the moment `run` starts, `pts` always in
102/// lockstep with wall-clock time — backed by real WASAPI data when it's
103/// available and synthesized silence otherwise (see
104/// `WasapiCaptureSource::fill_silence_gap`), since WASAPI itself
105/// delivers literally nothing whenever the render engine has no active
106/// session at all (e.g. nothing currently playing, for
107/// [`WasapiDeviceKind::Render`]). Without this, a quiet period would be a
108/// real gap in the audio timeline rather than silence, which would leave
109/// a downstream muxer/encoder with no way to keep audio and video in
110/// sync across it.
111///
112/// Every WASAPI object here is created by [`WasapiCaptureSource::open`] on
113/// its caller's thread, then actually driven by [`SourceElement::run`] on
114/// whichever thread [`crate::pipeline::Pipeline`] spawns for this source
115/// — a different thread in the normal case. COM requires every thread
116/// that touches an interface to have joined an apartment itself (even
117/// though the interfaces here are free-threaded/agile and can be handed
118/// across threads freely), so `run` makes its own `CoInitializeEx` call
119/// before touching anything, paired with `CoUninitialize` when it returns
120/// — the same two-`CoInitializeEx`-calls-per-object-lifetime pattern
121/// `cpal`'s own WASAPI backend uses. `open` also joins its caller's COM
122/// apartment while it creates the agile WASAPI interfaces, then balances
123/// that call before returning; the pipeline worker joins its own apartment
124/// independently in `run`.
125///
126/// Deliberately does **not** retry internally on
127/// `AUDCLNT_E_DEVICE_INVALIDATED` (default device changed, unplugged,
128/// format changed) — same "fail fast, caller rebuilds" contract as
129/// `DxgiCaptureSource`/`RtspSource`; watch for
130/// [`WasapiCaptureSourceError::DeviceInvalidated`] and call
131/// [`WasapiCaptureSource::open`] again.
132///
133/// Runs until `Stop` — never reaches `Eos` on its own, same as every other
134/// live source in this crate.
135pub struct WasapiCaptureSource {
136 pp_log: PpLog,
137 name: Arc<str>,
138 audio_client: IAudioClient,
139 capture_client: IAudioCaptureClient,
140 sample_rate: u32,
141 format: ffmpeg::format::Sample,
142 channel_layout: ffmpeg::ChannelLayout,
143 /// Cumulative sample count across every emitted frame — this
144 /// element's `pts` unit (see [`WasapiCaptureSource::time_base`]), same
145 /// "integer tick counter" convention every other source in this crate
146 /// uses.
147 samples_emitted: i64,
148 pad: SrcPad,
149}
150
151// SAFETY: every WASAPI/COM handle here is a `windows-rs` COM interface
152// wrapper — thread-safe to hand off (refcounting is interlocked, and
153// these specific interfaces are documented free-threaded/agile).
154// `&mut self` on every method that touches them already rules out
155// concurrent access from multiple threads — same reasoning
156// `DxgiCaptureSource` documents for its own `unsafe impl Send`.
157unsafe impl Send for WasapiCaptureSource {}
158
159impl WasapiCaptureSource {
160 /// Enumerates every currently-active audio endpoint — both `Render`
161 /// (playback) and `Capture` (recording) — as an [`WasapiDevice`] list a
162 /// caller can show in a picker UI and index/search into, then hand the
163 /// chosen entry straight to [`WasapiCaptureOptions::device`]. No
164 /// concept of "mode" to reason about beforehand: the picked device's
165 /// own [`WasapiDeviceKind`] is what tells `open` whether to use
166 /// loopback.
167 pub fn list_devices() -> std::result::Result<Vec<WasapiDevice>, WasapiCaptureSourceError> {
168 Ok(enumerate_wasapi_devices(None)?)
169 }
170
171 /// Opens `options.device` and starts a shared-mode WASAPI capture
172 /// session. Returns the element alongside the captured stream's
173 /// actual `(sample_rate, channels)` — what a caller needs to build a
174 /// matching downstream encoder/muxer, same pattern as
175 /// [`crate::elements::DxgiCaptureSource::open`] returning
176 /// `(width, height)`.
177 pub fn open(
178 name: impl Into<String>,
179 options: WasapiCaptureOptions,
180 ) -> std::result::Result<(Self, u32, u16), WasapiCaptureSourceError> {
181 let name: Arc<str> = name.into().into();
182 let pp_log = element_pp_log(ElementType::WasapiCaptureSource, &name, None);
183 let _apartment = ComApartment::new()?;
184
185 unsafe {
186 let device = open_device(&options.device.id)?;
187 let audio_client: IAudioClient = device.Activate(CLSCTX_ALL, None)?;
188
189 let mix_format = audio_client.GetMixFormat()?;
190 let audio_format = resolve_mix_format(mix_format).map_err(|error| {
191 WasapiCaptureSourceError::UnsupportedMixFormat {
192 format_tag: error.format_tag,
193 bits: error.bits,
194 }
195 })?;
196
197 let stream_flags = match options.device.kind {
198 WasapiDeviceKind::Render => AUDCLNT_STREAMFLAGS_LOOPBACK,
199 WasapiDeviceKind::Capture => 0,
200 };
201 let init_result = audio_client.Initialize(
202 AUDCLNT_SHAREMODE_SHARED,
203 stream_flags,
204 BUFFER_DURATION_100NS,
205 0,
206 mix_format,
207 None,
208 );
209 CoTaskMemFree(Some(mix_format as *const c_void));
210 init_result?;
211
212 let capture_client: IAudioCaptureClient = audio_client.GetService()?;
213
214 pp_info!(
215 pp_log: &pp_log,
216 "opened: device={:?} ({:?}), {}Hz, {} channel(s), format={:?}",
217 options.device.name,
218 options.device.kind,
219 audio_format.sample_rate,
220 audio_format.channels,
221 audio_format.sample_format
222 );
223 let pad = SrcPad::new(format!("{name}_src"));
224
225 Ok((
226 Self {
227 name,
228 pp_log,
229 audio_client,
230 capture_client,
231 sample_rate: audio_format.sample_rate,
232 format: audio_format.sample_format,
233 channel_layout: audio_format.channel_layout(),
234 samples_emitted: 0,
235 pad,
236 },
237 audio_format.sample_rate,
238 audio_format.channels,
239 ))
240 }
241 }
242
243 /// The unit each emitted frame's `pts` is expressed in.
244 pub fn time_base(&self) -> ffmpeg::Rational {
245 ffmpeg::Rational::new(1, self.sample_rate as i32)
246 }
247
248 fn classify_error(&self, error: windows::core::Error) -> WasapiCaptureSourceError {
249 if error.code() == AUDCLNT_E_DEVICE_INVALIDATED {
250 WasapiCaptureSourceError::DeviceInvalidated
251 } else {
252 WasapiCaptureSourceError::Windows(error)
253 }
254 }
255
256 /// Wraps one WASAPI packet (`data`/`frames`/`flags` straight out of
257 /// [`IAudioCaptureClient::GetBuffer`]) into a fresh `ffmpeg::frame::Audio`
258 /// and stamps its `pts`. `AUDCLNT_BUFFERFLAGS_SILENT` (the device has
259 /// nothing real to report this tick, e.g. right after `Start`) or a
260 /// null `data` pointer both mean "emit silence" rather than reading
261 /// past the end of nothing.
262 fn build_frame(&mut self, data: *mut u8, frames: u32, flags: u32) -> ffmpeg::frame::Audio {
263 let mut frame =
264 ffmpeg::frame::Audio::new(self.format, frames as usize, self.channel_layout);
265 frame.set_rate(self.sample_rate);
266 // `frame.data_mut(0)`'s length is FFmpeg's own padded linesize,
267 // not necessarily `frames * channels * format.bytes()` exactly —
268 // only ever touch that tight amount (the same bound
269 // `frame.plane::<T>()` itself reads via `samples()`), never the
270 // destination's full length, or a WASAPI buffer exactly `frames`
271 // frames long could get read past its end.
272 let tight_bytes =
273 frames as usize * self.channel_layout.channels() as usize * self.format.bytes();
274 if data.is_null() || flags & (AUDCLNT_BUFFERFLAGS_SILENT.0 as u32) != 0 {
275 frame.data_mut(0)[..tight_bytes].fill(0);
276 } else {
277 unsafe {
278 ptr::copy_nonoverlapping(data, frame.data_mut(0).as_mut_ptr(), tight_bytes);
279 }
280 }
281 frame.set_pts(Some(self.samples_emitted));
282 self.samples_emitted += frames as i64;
283 frame
284 }
285
286 /// Pushes `frame` downstream, reporting (rather than dying on) a
287 /// failing `Sink` — same "drop this one buffer, keep going" contract
288 /// [`crate::elements::DxgiCaptureSource::run`]/[`crate::elements::TestVideoSource::run`]
289 /// give their own pushes.
290 fn push_frame(&mut self, frame: ffmpeg::frame::Audio, bus: &Bus) {
291 if let Err(error) = self.pad.push(MediaBuffer::Audio(Arc::new(frame))) {
292 bus.post(
293 &self.pp_log,
294 BusEvent::Error {
295 element_type: ElementType::WasapiCaptureSource,
296 name: self.name.clone(),
297 error,
298 },
299 );
300 }
301 }
302
303 /// Synthesizes and pushes one silence frame covering however many
304 /// samples real WASAPI delivery has fallen behind `elapsed` — active
305 /// wall-clock time since `run_captured` started, already excluding
306 /// time spent frozen inside `Pause` (see
307 /// [`crate::schedule::ActiveTimeline`]; a raw `Instant::elapsed()`
308 /// would count a pause as a deficit to fill, making `Resume`
309 /// synthesize one giant silence frame covering the whole pause) — a
310 /// no-op (`deficit <= 0`) whenever real packets have kept up. WASAPI
311 /// delivers **zero** packets
312 /// whenever the render engine has no active session at all (as
313 /// opposed to an active-but-quiet session, which still delivers
314 /// `AUDCLNT_BUFFERFLAGS_SILENT`-flagged packets `build_frame` already
315 /// turns into silence) — without this, nothing plays on the system
316 /// would mean nothing at all comes out of this source, leaving a real
317 /// gap in the audio timeline exactly when a downstream muxer/encoder
318 /// needs `pts` to keep advancing to stay in sync with video. Backing
319 /// every gap with synthesized silence (rather than, say, stretching
320 /// the next real frame's `pts`) keeps `pts` a plain, always-accurate
321 /// sample count no matter which samples were real.
322 fn fill_silence_gap(&mut self, elapsed: Duration, bus: &Bus) {
323 let expected = (elapsed.as_secs_f64() * self.sample_rate as f64) as i64;
324 let deficit = expected - self.samples_emitted;
325 if deficit <= 0 {
326 return;
327 }
328 let mut frame =
329 ffmpeg::frame::Audio::new(self.format, deficit as usize, self.channel_layout);
330 frame.set_rate(self.sample_rate);
331 frame.data_mut(0).fill(0);
332 frame.set_pts(Some(self.samples_emitted));
333 self.samples_emitted += deficit;
334 self.push_frame(frame, bus);
335 }
336
337 /// Like [`crate::control::drain_control`], but drives the raw control
338 /// receiver and [`crate::control::apply_one`] directly (same reason
339 /// [`crate::elements::AppSource`] does — see `drain_control`'s own
340 /// docs) so it can bracket the blocking `Pause` wait with
341 /// `IAudioClient::Stop`/`Reset`/`Start`: without this, the capture
342 /// session keeps running the whole time this source is paused, with
343 /// nothing ever draining `IAudioCaptureClient::GetBuffer` to read it —
344 /// harmless in that WASAPI's shared-mode ring buffer just silently
345 /// overwrites whatever it can't hold (see `BUFFER_DURATION_100NS`), but
346 /// there's no reason to keep the device (and whatever driver-side work
347 /// backs it) actively capturing audio nobody is ever going to read.
348 /// `Reset` also flushes data already pending at the pause boundary, so
349 /// Resume cannot emit stale pre-pause packets as a short burst. These
350 /// device transitions happen before their synchronous control request
351 /// is acknowledged; failure is fatal because correct capture state can
352 /// no longer be guaranteed.
353 fn handle_control(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<ControlOutcome> {
354 let mut paused_for = Duration::ZERO;
355 while let Some((request, ack)) = control.try_recv() {
356 let RequestKind::Control(msg) = request else {
357 control::apply_finish(self, bus, &ack);
358 return Ok(ControlOutcome {
359 stopped: true,
360 paused_for,
361 });
362 };
363 if msg != ControlMsg::Pause {
364 if control::apply_one(self, bus, msg, &ack)? {
365 return Ok(ControlOutcome {
366 stopped: true,
367 paused_for,
368 });
369 }
370 continue;
371 }
372
373 // Include device shutdown and both synchronous downstream
374 // cascades in the frozen interval: this source produces no
375 // media during any of them.
376 let pause_start = Instant::now();
377 unsafe { self.audio_client.Stop() }.map_err(|error| self.classify_error(error))?;
378 // Stop freezes the stream but does not discard packets that
379 // were already pending. Reset flushes those packets so Resume
380 // cannot dump stale pre-pause audio in a short burst.
381 unsafe { self.audio_client.Reset() }.map_err(|error| self.classify_error(error))?;
382 control::apply_one(self, bus, msg, &ack)?;
383
384 loop {
385 let Some((paused_msg, paused_ack)) = control.recv() else {
386 paused_for += pause_start.elapsed();
387 return Ok(ControlOutcome {
388 stopped: true,
389 paused_for,
390 });
391 };
392
393 let RequestKind::Control(paused_msg) = paused_msg else {
394 control::apply_finish(self, bus, &paused_ack);
395 paused_for += pause_start.elapsed();
396 return Ok(ControlOutcome {
397 stopped: true,
398 paused_for,
399 });
400 };
401
402 if paused_msg == ControlMsg::Resume {
403 // Keep capture stopped until every downstream element
404 // has resumed. Start the device and only then ack, so
405 // the synchronous caller cannot observe a half-resumed
406 // source and no audio accumulates during a slow cascade.
407 control::apply_one_unacked(self, bus, paused_msg)?;
408 unsafe { self.audio_client.Start() }
409 .map_err(|error| self.classify_error(error))?;
410 let _ = paused_ack.send(());
411 paused_for += pause_start.elapsed();
412 break;
413 }
414
415 if control::apply_one(self, bus, paused_msg, &paused_ack)? {
416 paused_for += pause_start.elapsed();
417 return Ok(ControlOutcome {
418 stopped: true,
419 paused_for,
420 });
421 }
422 // A redundant Pause (or another one-shot control) was
423 // forwarded and acknowledged; remain frozen until Resume.
424 }
425 }
426 Ok(ControlOutcome {
427 stopped: false,
428 paused_for,
429 })
430 }
431
432 /// The main capture loop, run once COM has joined this thread's
433 /// apartment (see [`SourceElement::run`]) and the audio client has been
434 /// started. Drains every buffer WASAPI has ready on each
435 /// `POLL_INTERVAL` tick (`GetNextPacketSize` returning `0` means
436 /// caught up), pushing one `MediaBuffer::Audio` per packet, then tops
437 /// up with synthesized silence (see [`WasapiCaptureSource::fill_silence_gap`])
438 /// so `pts` keeps advancing with wall-clock time even across a tick
439 /// where WASAPI delivered nothing at all.
440 fn run_captured(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<()> {
441 let mut timeline = ActiveTimeline::new(Instant::now());
442 loop {
443 let outcome = self.handle_control(control, bus)?;
444 if outcome.stopped {
445 pp_info!(self, "stopped");
446 return Ok(());
447 }
448 timeline.account_pause(outcome.paused_for);
449
450 thread::sleep(POLL_INTERVAL);
451
452 loop {
453 let packet_size = match unsafe { self.capture_client.GetNextPacketSize() } {
454 Ok(size) => size,
455 Err(error) => return Err(self.classify_error(error).into()),
456 };
457 if packet_size == 0 {
458 break;
459 }
460
461 let mut data: *mut u8 = ptr::null_mut();
462 let mut frames_available = 0u32;
463 let mut flags = 0u32;
464 if let Err(error) = unsafe {
465 self.capture_client.GetBuffer(
466 &mut data,
467 &mut frames_available,
468 &mut flags,
469 None,
470 None,
471 )
472 } {
473 return Err(self.classify_error(error).into());
474 }
475
476 let frame = self.build_frame(data, frames_available, flags);
477 if let Err(error) = unsafe { self.capture_client.ReleaseBuffer(frames_available) } {
478 return Err(self.classify_error(error).into());
479 }
480
481 self.push_frame(frame, bus);
482 }
483
484 self.fill_silence_gap(timeline.elapsed(Instant::now()), bus);
485 }
486 }
487}
488
489impl Element for WasapiCaptureSource {
490 fn name(&self) -> Arc<str> {
491 self.name.clone()
492 }
493
494 fn element_type(&self) -> ElementType {
495 ElementType::WasapiCaptureSource
496 }
497
498 fn pp_log(&self) -> &PpLog {
499 &self.pp_log
500 }
501
502 fn pp_log_mut(&mut self) -> &mut PpLog {
503 &mut self.pp_log
504 }
505}
506
507impl Source for WasapiCaptureSource {
508 fn src_pads(&mut self) -> &mut [SrcPad] {
509 std::slice::from_mut(&mut self.pad)
510 }
511}
512
513impl SourceElement for WasapiCaptureSource {
514 fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<()> {
515 pp_info!(self, "started");
516
517 let _apartment = ComApartment::new().map_err(WasapiCaptureSourceError::from)?;
518
519 if let Err(error) = unsafe { self.audio_client.Start() } {
520 return Err(self.classify_error(error).into());
521 }
522
523 let result = self.run_captured(control, bus);
524
525 if let Err(error) = unsafe { self.audio_client.Stop() } {
526 pp_error!(self, "Stop failed: {error}");
527 }
528 result
529 }
530
531 fn seek(&mut self, _target: std::time::Duration) -> Result<std::time::Duration> {
532 Err(WasapiCaptureSourceError::SeekUnsupported.into())
533 }
534}