2025-06-29 03:46:34 -05:00
|
|
|
|
// client/src/output/video.rs
|
|
|
|
|
|
|
|
|
|
|
|
use anyhow::Context;
|
2025-06-21 05:21:57 -05:00
|
|
|
|
use gstreamer as gst;
|
|
|
|
|
|
use gstreamer_app as gst_app;
|
|
|
|
|
|
use gst::prelude::*;
|
2025-06-23 07:18:26 -05:00
|
|
|
|
use lesavka_common::lesavka::VideoPacket;
|
2025-06-29 11:44:16 -05:00
|
|
|
|
use tracing::{error, info, warn, debug};
|
2025-06-21 05:21:57 -05:00
|
|
|
|
|
2025-06-29 03:46:34 -05:00
|
|
|
|
/* ---------- pipeline ----------------------------------------------------
|
|
|
|
|
|
* ┌────────────┐ H.264/AU ┌─────────┐ Decoded ┌─────────────┐
|
|
|
|
|
|
* │ AppSrc │────────────► decodebin ├──────────► glimagesink │
|
|
|
|
|
|
* └────────────┘ (autoplug) (overlay) |
|
|
|
|
|
|
* ----------------------------------------------------------------------*/
|
|
|
|
|
|
const PIPELINE_DESC: &str = concat!(
|
2025-06-27 19:31:46 -05:00
|
|
|
|
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! ",
|
2025-06-29 03:46:34 -05:00
|
|
|
|
"queue leaky=downstream ! ",
|
2025-06-27 19:31:46 -05:00
|
|
|
|
"capsfilter caps=video/x-h264,stream-format=byte-stream,alignment=au ! ",
|
2025-06-29 03:46:34 -05:00
|
|
|
|
"h264parse disable-passthrough=true ! decodebin ! videoconvert ! ",
|
|
|
|
|
|
"glimagesink name=sink sync=false"
|
2025-06-27 19:31:46 -05:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-06-21 05:21:57 -05:00
|
|
|
|
pub struct MonitorWindow {
|
2025-06-29 03:46:34 -05:00
|
|
|
|
_pipeline: gst::Pipeline,
|
|
|
|
|
|
src: gst_app::AppSrc,
|
2025-06-21 05:21:57 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl MonitorWindow {
|
2025-06-29 03:46:34 -05:00
|
|
|
|
pub fn new(_id: u32) -> anyhow::Result<Self> {
|
|
|
|
|
|
gst::init().context("initialising GStreamer")?;
|
2025-06-26 16:17:31 -05:00
|
|
|
|
|
2025-06-29 03:46:34 -05:00
|
|
|
|
// --- Build pipeline ------------------------------------------------
|
|
|
|
|
|
let pipeline: gst::Pipeline = gst::parse::launch(PIPELINE_DESC)?
|
|
|
|
|
|
.downcast::<gst::Pipeline>()
|
|
|
|
|
|
.expect("not a pipeline");
|
2025-06-26 16:17:31 -05:00
|
|
|
|
|
2025-06-29 03:46:34 -05:00
|
|
|
|
// Optional: turn the sink full‑screen when LESAVKA_FULLSCREEN=1
|
|
|
|
|
|
if std::env::var("LESAVKA_FULLSCREEN").is_ok() {
|
|
|
|
|
|
if let Some(sink) = pipeline.by_name("sink") {
|
|
|
|
|
|
sink.set_property_from_str("fullscreen", "true");
|
|
|
|
|
|
sink.set_property("force-aspect-ratio", &true);
|
|
|
|
|
|
// Wayland & GL sinks accept it :contentReference[oaicite:1]{index=1}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-24 23:48:06 -05:00
|
|
|
|
|
2025-06-29 03:46:34 -05:00
|
|
|
|
/* ---------- AppSrc ------------------------------------------------- */
|
|
|
|
|
|
let src: gst_app::AppSrc = pipeline
|
|
|
|
|
|
.by_name("src")
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
.downcast::<gst_app::AppSrc>()
|
|
|
|
|
|
.unwrap();
|
2025-06-26 16:17:31 -05:00
|
|
|
|
|
2025-06-29 03:46:34 -05:00
|
|
|
|
src.set_caps(Some(&gst::Caps::builder("video/x-h264")
|
|
|
|
|
|
.field("stream-format", &"byte-stream")
|
|
|
|
|
|
.field("alignment", &"au")
|
|
|
|
|
|
.build()));
|
|
|
|
|
|
src.set_format(gst::Format::Time);
|
2025-06-21 05:21:57 -05:00
|
|
|
|
pipeline.set_state(gst::State::Playing)?;
|
2025-06-29 11:44:16 -05:00
|
|
|
|
if let Some(sink) = pipeline.by_name("sink") {
|
|
|
|
|
|
let title = format!("Lesavka‑eye‑{_id}");
|
|
|
|
|
|
sink.set_property_from_str("window-title", &title);
|
|
|
|
|
|
sink.set_property("force-aspect-ratio", &true);
|
|
|
|
|
|
}
|
2025-06-24 23:48:06 -05:00
|
|
|
|
|
2025-06-29 03:46:34 -05:00
|
|
|
|
Ok(Self { _pipeline: pipeline, src })
|
2025-06-21 05:21:57 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-29 03:46:34 -05:00
|
|
|
|
/// Feed one access-unit to the decoder.
|
2025-06-21 05:21:57 -05:00
|
|
|
|
pub fn push_packet(&self, pkt: VideoPacket) {
|
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 % 150 == 0 || n < 10 {
|
|
|
|
|
|
debug!(eye = pkt.id, bytes = pkt.data.len(), pts = pkt.pts, "⬇️ received video AU");
|
|
|
|
|
|
}
|
2025-06-27 22:51:50 -05:00
|
|
|
|
let mut buf = gst::Buffer::from_slice(pkt.data);
|
2025-06-29 03:46:34 -05:00
|
|
|
|
buf.get_mut()
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
.set_pts(Some(gst::ClockTime::from_useconds(pkt.pts)));
|
|
|
|
|
|
let _ = self.src.push_buffer(buf); // ignore Eos/flushing
|
2025-06-21 05:21:57 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|