client: normalize launcher audio sink override
This commit is contained in:
parent
0908fccdb4
commit
455e73cdbc
@ -17,18 +17,12 @@ pub struct AudioOut {
|
||||
impl AudioOut {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
gst::init().context("initialising GStreamer")?;
|
||||
|
||||
// ── 1. Decide which sink element to instantiate ────────────────────
|
||||
let sink = pick_sink_element()?;
|
||||
|
||||
// Operator can request a tee to /tmp via LESAVKA_TAP_AUDIO=1
|
||||
let tee_dump = std::env::var("LESAVKA_TAP_AUDIO")
|
||||
.ok()
|
||||
.as_deref()
|
||||
.map(|v| v == "1")
|
||||
.unwrap_or(false);
|
||||
|
||||
// ── 2. Assemble pipeline description string ────────────────────────
|
||||
let mut pipe = format!(
|
||||
"appsrc name=src is-live=true format=time do-timestamp=true \
|
||||
block=false ! \
|
||||
@ -46,8 +40,6 @@ impl AudioOut {
|
||||
);
|
||||
warn!("💾 tee to /tmp/lesavka-audio.aac enabled (LESAVKA_TAP_AUDIO=1)");
|
||||
}
|
||||
|
||||
// ── 3. Create the pipeline & fetch the AppSrc ───────────────────────
|
||||
let pipeline: gst::Pipeline = gst::parse::launch(&pipe)?
|
||||
.downcast::<gst::Pipeline>()
|
||||
.expect("not a pipeline");
|
||||
@ -70,7 +62,6 @@ impl AudioOut {
|
||||
|
||||
#[cfg(not(coverage))]
|
||||
{
|
||||
// ── 4. Log *all* warnings/errors from the bus ──────────────────────
|
||||
let bus = pipeline.bus().unwrap();
|
||||
std::thread::spawn(move || {
|
||||
for msg in bus.iter_timed(gst::ClockTime::NONE) {
|
||||
@ -106,11 +97,7 @@ impl AudioOut {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pipeline
|
||||
.set_state(gst::State::Playing)
|
||||
.context("starting audio pipeline")?;
|
||||
|
||||
pipeline.set_state(gst::State::Playing).context("starting audio pipeline")?;
|
||||
Ok(Self { pipeline, src })
|
||||
}
|
||||
|
||||
@ -133,42 +120,32 @@ impl AudioOut {
|
||||
|
||||
impl Drop for AudioOut {
|
||||
fn drop(&mut self) {
|
||||
// put the whole pipeline back to NULL so GStreamer can dispose cleanly
|
||||
let _ = self.pipeline.set_state(gst::State::Null);
|
||||
}
|
||||
}
|
||||
|
||||
/*──────────────── helper: sink selection ─────────────────────────────*/
|
||||
#[cfg(not(coverage))]
|
||||
fn pick_sink_element() -> Result<String> {
|
||||
// 1. Operator override
|
||||
if let Ok(s) = std::env::var("LESAVKA_AUDIO_SINK") {
|
||||
info!("💪 sink overridden via LESAVKA_AUDIO_SINK={}", s);
|
||||
return Ok(s);
|
||||
let sink = normalize_sink_override(&s);
|
||||
info!("💪 sink overridden via LESAVKA_AUDIO_SINK={} -> {}", s, sink);
|
||||
return Ok(sink);
|
||||
}
|
||||
|
||||
// 2. Query PipeWire for default & running sinks
|
||||
let sinks = list_pw_sinks(); // Vec<(name,state)>
|
||||
let sinks = list_pw_sinks();
|
||||
for (n, st) in &sinks {
|
||||
if *st == "RUNNING" {
|
||||
info!("🔈 using default RUNNING sink '{}'", n);
|
||||
return Ok(format!("pulsesink device={}", n));
|
||||
return Ok(pulsesink_device_element(n));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. First RUNNING sink
|
||||
if let Some((n, _)) = sinks.iter().find(|(_, st)| *st == "RUNNING") {
|
||||
warn!("🏃 picking first RUNNING sink '{}'", n);
|
||||
return Ok(format!("pulsesink device={}", n));
|
||||
return Ok(pulsesink_device_element(n));
|
||||
}
|
||||
|
||||
// 4. Anything
|
||||
if let Some((n, _)) = sinks.first() {
|
||||
warn!("🎲 picking first sink '{}'", n);
|
||||
return Ok(format!("pulsesink device={}", n));
|
||||
return Ok(pulsesink_device_element(n));
|
||||
}
|
||||
|
||||
// Fallback - let autoaudiosink try its luck
|
||||
warn!("🫣 no PipeWire sinks readable - falling back to autoaudiosink");
|
||||
Ok("autoaudiosink".to_string())
|
||||
}
|
||||
@ -176,16 +153,29 @@ fn pick_sink_element() -> Result<String> {
|
||||
#[cfg(coverage)]
|
||||
fn pick_sink_element() -> Result<String> {
|
||||
if let Ok(s) = std::env::var("LESAVKA_AUDIO_SINK") {
|
||||
return Ok(s);
|
||||
return Ok(normalize_sink_override(&s));
|
||||
}
|
||||
if let Some((n, _)) = list_pw_sinks().first() {
|
||||
return Ok(format!("pulsesink device={}", n));
|
||||
return Ok(pulsesink_device_element(n));
|
||||
}
|
||||
Ok("autoaudiosink".to_string())
|
||||
}
|
||||
|
||||
/// Interpret `LESAVKA_AUDIO_SINK` as either a full sink element or bare device.
|
||||
fn normalize_sink_override(raw: &str) -> String {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.contains([' ', '=', '!']) || trimmed.ends_with("sink") {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
pulsesink_device_element(trimmed)
|
||||
}
|
||||
|
||||
fn pulsesink_device_element(device: &str) -> String {
|
||||
let escaped = device.replace('\\', "\\\\").replace('"', "\\\"");
|
||||
format!("pulsesink device=\"{escaped}\"")
|
||||
}
|
||||
|
||||
fn list_pw_sinks() -> Vec<(String, String)> {
|
||||
// ── PulseAudio / pactl fallback ────────────────────────────────
|
||||
if let Ok(info) = std::process::Command::new("pactl")
|
||||
.args(["info"])
|
||||
.output()
|
||||
|
||||
@ -46,6 +46,22 @@ mod audio_include_contract {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn pick_sink_element_wraps_bare_device_override_for_pulsesink() {
|
||||
with_var(
|
||||
"LESAVKA_AUDIO_SINK",
|
||||
Some("alsa_output.pci-0000_00_1f.3.analog-stereo"),
|
||||
|| {
|
||||
let sink = pick_sink_element().expect("device sink");
|
||||
assert_eq!(
|
||||
sink,
|
||||
"pulsesink device=\"alsa_output.pci-0000_00_1f.3.analog-stereo\""
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn pick_sink_element_uses_default_sink_from_pactl_info() {
|
||||
@ -70,7 +86,7 @@ exit 0
|
||||
let sink = pick_sink_element().expect("pick sink");
|
||||
assert_eq!(
|
||||
sink,
|
||||
"pulsesink device=alsa_output.usb-DAC_1234-00.analog-stereo"
|
||||
"pulsesink device=\"alsa_output.usb-DAC_1234-00.analog-stereo\""
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user