2025-06-29 03:46:34 -05:00
|
|
|
|
// client/src/output/audio.rs
|
|
|
|
|
|
|
|
|
|
|
|
use gstreamer as gst;
|
|
|
|
|
|
use gstreamer_app as gst_app;
|
|
|
|
|
|
use lesavka_common::lesavka::AudioPacket;
|
|
|
|
|
|
use gst::prelude::*;
|
2025-06-29 11:44:16 -05:00
|
|
|
|
use tracing::{error, info, warn, debug};
|
2025-06-29 03:46:34 -05:00
|
|
|
|
|
|
|
|
|
|
pub struct AudioOut {
|
|
|
|
|
|
src: gst_app::AppSrc,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl AudioOut {
|
|
|
|
|
|
pub fn new() -> anyhow::Result<Self> {
|
|
|
|
|
|
gst::init()?;
|
|
|
|
|
|
|
2025-06-29 22:39:17 -05:00
|
|
|
|
// Auto-audiosink picks PipeWire / Pulse etc.
|
2025-06-29 03:46:34 -05:00
|
|
|
|
const PIPE: &str =
|
|
|
|
|
|
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! \
|
|
|
|
|
|
queue leaky=downstream ! aacparse ! avdec_aac ! audioresample ! autoaudiosink";
|
|
|
|
|
|
|
2025-06-29 22:39:17 -05:00
|
|
|
|
// `parse_launch()` returns `gst::Element`; down-cast manually and
|
2025-06-29 03:46:34 -05:00
|
|
|
|
// map the error into anyhow ourselves (no `?` on the downcast).
|
|
|
|
|
|
let pipeline: gst::Pipeline = gst::parse::launch(PIPE)?
|
|
|
|
|
|
.downcast::<gst::Pipeline>()
|
|
|
|
|
|
.expect("not a pipeline");
|
|
|
|
|
|
|
|
|
|
|
|
let src: gst_app::AppSrc = pipeline
|
|
|
|
|
|
.by_name("src")
|
|
|
|
|
|
.expect("no src element")
|
|
|
|
|
|
.downcast::<gst_app::AppSrc>()
|
|
|
|
|
|
.expect("src not an AppSrc");
|
2025-06-30 02:42:20 -05:00
|
|
|
|
|
|
|
|
|
|
src.set_caps(Some(&gst::Caps::builder("audio/mpeg") // mpeg‑4 AAC
|
|
|
|
|
|
.field("mpegversion", &4i32)
|
|
|
|
|
|
.field("stream-format", &"adts")
|
|
|
|
|
|
.build()));
|
2025-06-29 03:46:34 -05:00
|
|
|
|
src.set_format(gst::Format::Time);
|
2025-06-30 02:42:20 -05:00
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
let bus = pipeline.bus().expect("bus");
|
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
|
use gst::MessageView::*;
|
|
|
|
|
|
for msg in bus.iter_timed(gst::ClockTime::NONE) {
|
|
|
|
|
|
if let Error(e) = msg.view() {
|
|
|
|
|
|
error!("💥 client‑audio: {} ({})",
|
|
|
|
|
|
e.error(), e.debug().unwrap_or_default());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-29 03:46:34 -05:00
|
|
|
|
pipeline.set_state(gst::State::Playing)?;
|
|
|
|
|
|
|
|
|
|
|
|
Ok(Self { src })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub fn push(&self, pkt: AudioPacket) {
|
2025-06-29 11:44:16 -05:00
|
|
|
|
static CNT : std::sync::atomic::AtomicU64 =
|
|
|
|
|
|
std::sync::atomic::AtomicU64::new(0);
|
|
|
|
|
|
let n = CNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
|
|
|
|
if n % 300 == 0 || n < 10 {
|
|
|
|
|
|
debug!(bytes = pkt.data.len(), pts = pkt.pts, "⬇️ received audio AU");
|
|
|
|
|
|
}
|
2025-06-29 03:46:34 -05:00
|
|
|
|
let mut buf = gst::Buffer::from_slice(pkt.data);
|
|
|
|
|
|
buf.get_mut()
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
.set_pts(Some(gst::ClockTime::from_useconds(pkt.pts)));
|
|
|
|
|
|
let _ = self.src.push_buffer(buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|