156 lines
4.6 KiB
Rust
156 lines
4.6 KiB
Rust
|
|
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()
|
||
|
|
}
|