91 lines
3.7 KiB
Rust
Raw Normal View History

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-29 17:24:19 -05:00
use gstreamer_video::glib::Type;
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 fullscreen when LESAVKA_FULLSCREEN=1
2025-06-29 12:28:25 -05:00
let fullscreen = std::env::var("LESAVKA_FULLSCREEN").is_ok();
if let Some(sink) = pipeline.by_name("sink") {
// glimagesink: title / fullscreen / forceaspectratio
let title = format!("Lesavkaeye{_id}");
2025-06-29 17:24:19 -05:00
if sink.has_property("title", None::<Type>) {
sink.set_property("title", &title);
}
if sink.has_property("window-title", None::<Type>) {
sink.set_property("window-title", &title);
}
if sink.has_property("fullscreen", None::<Type>) && fullscreen {
let _ = sink.set_property("fullscreen", &true);
2025-06-29 03:46:34 -05:00
}
2025-06-29 12:28:25 -05:00
let _ = sink.set_property("force-aspect-ratio", &true);
2025-06-29 03:46:34 -05:00
}
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!("Lesavkaeye{_id}");
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
}
}