fix: preserve GPUI lifetime and avoid WASAPI silence warnings
This commit is contained in:
+112
-10
@@ -153,6 +153,8 @@ unsafe extern "C" fn log_to_stderr(
|
||||
if let Some(state) = pipewire_state(&text) {
|
||||
PIPEWIRE_STATE.store(state as u8, Ordering::Relaxed);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
record_wasapi_event(&text);
|
||||
eprintln!("obs {label}: {text}");
|
||||
}
|
||||
// SAFETY: frees the buffer dstr_vprintf allocated (null is allowed).
|
||||
@@ -612,9 +614,89 @@ pub(crate) fn segment_index(path: &str) -> Option<u64> {
|
||||
stem.strip_prefix("segment-")?.parse().ok()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
static AUDIO_FAULTS: Mutex<Vec<(String, std::sync::Weak<std::sync::atomic::AtomicBool>)>> =
|
||||
Mutex::new(Vec::new());
|
||||
|
||||
#[cfg(any(windows, test))]
|
||||
fn wasapi_event(message: &str) -> Option<(&str, bool)> {
|
||||
let (message, source) = message.rsplit_once(" (source: ")?;
|
||||
let source = source.strip_suffix(')')?;
|
||||
if !source.starts_with("clipbin-") {
|
||||
return None;
|
||||
}
|
||||
if message.starts_with("WASAPI: Device '") && message.contains(" initialized") {
|
||||
Some((source, false))
|
||||
} else if (message.starts_with("WASAPI: Device '") && message.contains(" failed to start"))
|
||||
|| (message.starts_with("Device '") && message.contains(" invalidated. Retrying"))
|
||||
{
|
||||
Some((source, true))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn record_wasapi_event(message: &str) {
|
||||
if let Some((source, failed)) = wasapi_event(message)
|
||||
&& let Ok(faults) = AUDIO_FAULTS.lock()
|
||||
&& let Some(flag) = faults
|
||||
.iter()
|
||||
.find(|(name, _)| name == source)
|
||||
.and_then(|(_, flag)| flag.upgrade())
|
||||
{
|
||||
flag.store(failed, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
struct AudioClock {
|
||||
started: Instant,
|
||||
last_millis: AtomicU64,
|
||||
#[cfg(windows)]
|
||||
source_name: String,
|
||||
#[cfg(windows)]
|
||||
failed: Arc<std::sync::atomic::AtomicBool>,
|
||||
}
|
||||
impl AudioClock {
|
||||
fn new(source_name: &str) -> Self {
|
||||
#[cfg(windows)]
|
||||
let failed = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
#[cfg(windows)]
|
||||
AUDIO_FAULTS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((source_name.into(), Arc::downgrade(&failed)));
|
||||
#[cfg(not(windows))]
|
||||
let _ = source_name;
|
||||
Self {
|
||||
started: Instant::now(),
|
||||
last_millis: AtomicU64::new(0),
|
||||
#[cfg(windows)]
|
||||
source_name: source_name.into(),
|
||||
#[cfg(windows)]
|
||||
failed,
|
||||
}
|
||||
}
|
||||
fn lost(&self) -> bool {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
self.failed.load(Ordering::Relaxed)
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
(self.started.elapsed().as_millis() as u64)
|
||||
.saturating_sub(self.last_millis.load(Ordering::Relaxed))
|
||||
>= 5_000
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
impl Drop for AudioClock {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(mut faults) = AUDIO_FAULTS.lock() {
|
||||
faults.retain(|(name, _)| name != &self.source_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn audio_captured(
|
||||
@@ -626,6 +708,8 @@ unsafe extern "C" fn audio_captured(
|
||||
// SAFETY: each callback holds the stable AudioClock allocation until removal;
|
||||
// OBS serialises removal against callbacks with source->audio_cb_mutex.
|
||||
let clock = unsafe { &*param.cast::<AudioClock>() };
|
||||
#[cfg(windows)]
|
||||
clock.failed.store(false, Ordering::Relaxed);
|
||||
clock.last_millis.store(
|
||||
clock.started.elapsed().as_millis() as u64,
|
||||
Ordering::Relaxed,
|
||||
@@ -693,12 +777,10 @@ impl Pipeline {
|
||||
(&capture.microphone, MICROPHONE_CHANNEL, MICROPHONE_TRACK),
|
||||
] {
|
||||
if let Some(source_spec) = source_spec {
|
||||
let source = create_source(source_spec, &name(&format!("audio-{channel}")))?;
|
||||
let source_name = format!("{name_prefix}-audio-{channel}");
|
||||
let mut clock = Box::new(AudioClock::new(&source_name));
|
||||
let source = create_source(source_spec, &cstring(&source_name))?;
|
||||
pipeline.sources.push((Some(channel), source));
|
||||
let mut clock = Box::new(AudioClock {
|
||||
started: Instant::now(),
|
||||
last_millis: AtomicU64::new(0),
|
||||
});
|
||||
sys::obs_source_add_audio_capture_callback(
|
||||
source,
|
||||
Some(audio_captured),
|
||||
@@ -802,11 +884,7 @@ impl Pipeline {
|
||||
lagged_frames: unsafe { sys::obs_get_lagged_frames() } as u64,
|
||||
written_bytes: 0,
|
||||
disk_available_bytes: None,
|
||||
audio_lost: self.audio_clocks.iter().any(|(_, clock)| {
|
||||
(clock.started.elapsed().as_millis() as u64)
|
||||
.saturating_sub(clock.last_millis.load(Ordering::Relaxed))
|
||||
>= 5_000
|
||||
}),
|
||||
audio_lost: self.audio_clocks.iter().any(|(_, clock)| clock.lost()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1033,6 +1111,30 @@ unsafe fn create_source(spec: &SourceSpec, name: &CStr) -> Result<*mut sys::obs_
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wasapi_events_are_scoped_and_distinguish_failure_from_silence() {
|
||||
assert_eq!(
|
||||
wasapi_event(
|
||||
"WASAPI: Device 'Speakers' [48000 Hz] initialized (source: clipbin-7-audio-1)"
|
||||
),
|
||||
Some(("clipbin-7-audio-1", false))
|
||||
);
|
||||
assert_eq!(
|
||||
wasapi_event("Device 'Speakers' invalidated. Retrying (source: clipbin-7-audio-1)"),
|
||||
Some(("clipbin-7-audio-1", true))
|
||||
);
|
||||
assert_eq!(
|
||||
wasapi_event("WASAPI: Device 'default' failed to start (source: clipbin-7-audio-3)"),
|
||||
Some(("clipbin-7-audio-3", true))
|
||||
);
|
||||
assert_eq!(
|
||||
wasapi_event("Device 'Speakers' invalidated. Retrying (source: another-app)"),
|
||||
None
|
||||
);
|
||||
assert_eq!(wasapi_event("WASAPI: Device 'Speakers' Terminated"), None);
|
||||
assert_eq!(wasapi_event("WASAPI: Default output device changed"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipewire_state_is_read_from_the_plugin_log() {
|
||||
let state = |line| pipewire_state(line);
|
||||
|
||||
@@ -304,7 +304,7 @@ pub struct Health {
|
||||
/// PipeWire capture's portal and stream state. `None` for other sources,
|
||||
/// which report loss through their size, and before the portal answers.
|
||||
pub pipewire: Option<PipeWireState>,
|
||||
/// A configured capture source has delivered no audio for five seconds.
|
||||
/// A configured capture source lost delivery (Linux) or reported a device error (Windows).
|
||||
pub audio_lost: bool,
|
||||
pub output_active: bool,
|
||||
pub total_frames: u64,
|
||||
|
||||
Reference in New Issue
Block a user