lesavka/client/src/launcher/device_test.rs

156 lines
4.6 KiB
Rust
Raw Normal View History

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<Child>,
microphone: Option<Child>,
speaker: Option<Child>,
}
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<bool> {
self.toggle(DeviceTestKind::Camera, build_camera_test(camera))
}
pub fn toggle_microphone(&mut self, source: Option<&str>, sink: Option<&str>) -> Result<bool> {
self.toggle(
DeviceTestKind::Microphone,
build_microphone_test(source, sink),
)
}
pub fn toggle_speaker(&mut self, sink: Option<&str>) -> Result<bool> {
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<Command>) -> Result<bool> {
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<Child> {
match kind {
DeviceTestKind::Camera => &self.camera,
DeviceTestKind::Microphone => &self.microphone,
DeviceTestKind::Speaker => &self.speaker,
}
}
fn slot_mut(&mut self, kind: DeviceTestKind) -> &mut Option<Child> {
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<Command> {
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<Command> {
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<Command> {
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>) -> String {
escape(Cow::Owned(value.into())).into_owned()
}