use anyhow::{Context, Result, bail}; use shell_escape::escape; use std::borrow::Cow; use std::process::{Child, Command}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DeviceTestKind { Camera, Microphone, Speaker, } #[derive(Default)] pub struct DeviceTestController { camera: Option, microphone: Option, speaker: Option, } impl DeviceTestController { pub fn new() -> Self { Self::default() } pub fn is_running(&mut self, kind: DeviceTestKind) -> bool { self.cleanup_finished(); self.slot(kind).is_some() } pub fn toggle_camera(&mut self, camera: Option<&str>) -> Result { self.toggle(DeviceTestKind::Camera, build_camera_test(camera)) } pub fn toggle_microphone(&mut self, source: Option<&str>, sink: Option<&str>) -> Result { self.toggle( DeviceTestKind::Microphone, build_microphone_test(source, sink), ) } pub fn toggle_speaker(&mut self, sink: Option<&str>) -> Result { self.toggle(DeviceTestKind::Speaker, build_speaker_test(sink)) } pub fn stop_all(&mut self) { for kind in [ DeviceTestKind::Camera, DeviceTestKind::Microphone, DeviceTestKind::Speaker, ] { self.stop(kind); } } fn toggle(&mut self, kind: DeviceTestKind, command: Result) -> Result { self.cleanup_finished(); if self.slot(kind).is_some() { self.stop(kind); return Ok(false); } let child = command? .spawn() .with_context(|| format!("starting {kind:?} test"))?; *self.slot_mut(kind) = Some(child); Ok(true) } fn stop(&mut self, kind: DeviceTestKind) { if let Some(mut child) = self.slot_mut(kind).take() { let _ = child.kill(); let _ = child.wait(); } } fn cleanup_finished(&mut self) { for kind in [ DeviceTestKind::Camera, DeviceTestKind::Microphone, DeviceTestKind::Speaker, ] { let finished = self .slot_mut(kind) .as_mut() .and_then(|child| child.try_wait().ok()) .flatten() .is_some(); if finished { let _ = self.slot_mut(kind).take(); } } } fn slot(&self, kind: DeviceTestKind) -> &Option { match kind { DeviceTestKind::Camera => &self.camera, DeviceTestKind::Microphone => &self.microphone, DeviceTestKind::Speaker => &self.speaker, } } fn slot_mut(&mut self, kind: DeviceTestKind) -> &mut Option { match kind { DeviceTestKind::Camera => &mut self.camera, DeviceTestKind::Microphone => &mut self.microphone, DeviceTestKind::Speaker => &mut self.speaker, } } } fn build_camera_test(camera: Option<&str>) -> Result { let Some(camera) = camera.filter(|value| !value.trim().is_empty()) else { bail!("select a camera before running the local camera test"); }; let device = format!("/dev/v4l/by-id/{camera}"); Ok(shell_command(format!( "gst-launch-1.0 -q v4l2src device={} ! videoconvert ! queue ! autovideosink sync=false", quote(device) ))) } fn build_microphone_test(source: Option<&str>, sink: Option<&str>) -> Result { let source = source .filter(|value| !value.trim().is_empty()) .ok_or_else(|| anyhow::anyhow!("select a microphone before starting a monitor test"))?; let sink = sink.filter(|value| !value.trim().is_empty()); let sink_prop = sink .map(|value| format!("device={}", quote(value))) .unwrap_or_default(); Ok(shell_command(format!( "gst-launch-1.0 -q pulsesrc device={} ! audioconvert ! audioresample ! queue ! pulsesink {}", quote(source), sink_prop ))) } fn build_speaker_test(sink: Option<&str>) -> Result { let sink_prop = sink .filter(|value| !value.trim().is_empty()) .map(|value| format!("device={}", quote(value))) .unwrap_or_default(); Ok(shell_command(format!( "gst-launch-1.0 -q audiotestsrc is-live=true wave=sine freq=880 volume=0.25 ! audioconvert ! audioresample ! queue ! pulsesink {}", sink_prop ))) } fn shell_command(command: String) -> Command { let mut child = Command::new("bash"); child.args(["-lc", &command]); child } fn quote(value: impl Into) -> String { escape(Cow::Owned(value.into())).into_owned() }