lesavka/server/src/video.rs

313 lines
12 KiB
Rust
Raw Normal View History

2025-06-21 05:21:57 -05:00
// server/src/video.rs
use anyhow::Context;
use gstreamer as gst;
use gstreamer_app as gst_app;
use gst::prelude::*;
2025-06-28 15:45:11 -05:00
use gst::{log, MessageView};
2025-06-30 14:20:07 -05:00
use gst::MessageView::*;
2025-06-23 07:18:26 -05:00
use lesavka_common::lesavka::VideoPacket;
2025-06-21 05:21:57 -05:00
use tokio_stream::wrappers::ReceiverStream;
use tonic::Status;
2025-06-29 12:28:25 -05:00
use tracing::{debug, warn, error, info, enabled, trace, Level};
2025-06-28 19:40:48 -05:00
use futures_util::Stream;
2025-06-21 05:21:57 -05:00
2025-06-28 03:34:48 -05:00
const EYE_ID: [&str; 2] = ["l", "r"];
2025-06-27 14:01:29 -05:00
static START: std::sync::OnceLock<gst::ClockTime> = std::sync::OnceLock::new();
2025-06-28 19:40:48 -05:00
pub struct VideoStream {
_pipeline: gst::Pipeline,
inner: ReceiverStream<Result<VideoPacket, Status>>,
}
impl Stream for VideoStream {
type Item = Result<VideoPacket, Status>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
Stream::poll_next(std::pin::Pin::new(&mut self.inner), cx)
}
}
impl Drop for VideoStream {
fn drop(&mut self) {
2025-06-30 15:45:37 -05:00
// shut down nicely - avoids the “dispose element … READY/PLAYING …” spam
2025-06-28 19:40:48 -05:00
let _ = self._pipeline.set_state(gst::State::Null);
}
}
2025-06-27 22:51:50 -05:00
pub async fn eye_ball(
2025-06-21 05:21:57 -05:00
dev: &str,
id: u32,
2025-06-25 16:52:26 -05:00
_max_bitrate_kbit: u32,
2025-06-28 19:40:48 -05:00
) -> anyhow::Result<VideoStream> {
2025-06-28 03:34:48 -05:00
let eye = EYE_ID[id as usize];
2025-06-21 05:21:57 -05:00
gst::init().context("gst init")?;
2025-06-29 04:17:44 -05:00
let desc = format!(
2025-06-29 17:53:25 -05:00
"v4l2src name=cam_{eye} device=\"{dev}\" io-mode=mmap do-timestamp=true ! \
2025-06-29 17:24:19 -05:00
queue ! \
2025-06-29 10:58:42 -05:00
h264parse disable-passthrough=true config-interval=-1 ! \
video/x-h264,stream-format=byte-stream,alignment=au ! \
appsink name=sink emit-signals=true max-buffers=32 drop=true"
2025-06-29 04:17:44 -05:00
);
2025-06-28 15:45:11 -05:00
// let desc = format!(
// "v4l2src device={dev} io-mode=mmap ! \
2025-06-29 04:17:44 -05:00
// queue max-size-buffers=0 max-size-bytes=0 max-size-time=0 ! tsdemux name=d ! \
// video/x-h264,stream-format=byte-stream,alignment=au,profile=high ! tsdemux name=d ! \
// d. ! h264parse config-interval=1 ! queue ! appsink name=vsink emit-signals=true \
// d. ! aacparse ! queue ! h264parse config-interval=1 ! appsink name=sink \
// emit-signals=true drop=false sync=false"
2025-06-28 15:45:11 -05:00
// );
2025-06-21 05:21:57 -05:00
2025-06-23 20:51:52 -05:00
let pipeline = gst::parse::launch(&desc)?
.downcast::<gst::Pipeline>()
2025-06-29 04:17:44 -05:00
.expect("not a pipeline");
2025-06-21 05:21:57 -05:00
2025-06-29 05:13:01 -05:00
// let pipeline: gst::Pipeline = gst::parse_launch(&desc)?
// .downcast()
// .expect("not a pipeline");
2025-06-23 20:57:18 -05:00
let sink = pipeline
.by_name("sink")
.expect("appsink")
.dynamic_cast::<gst_app::AppSink>()
2025-06-28 15:45:11 -05:00
.expect("appsink down-cast");
2025-06-23 20:57:18 -05:00
2025-06-28 15:45:11 -05:00
let (tx, rx) = tokio::sync::mpsc::channel(8192);
2025-06-21 05:21:57 -05:00
2025-06-29 12:28:25 -05:00
/* ----- BUS WATCH: show errors & warnings immediately --------------- */
let bus = pipeline.bus().expect("bus");
2025-06-29 17:53:25 -05:00
if let Some(src_pad) = pipeline.by_name(&format!("cam_{eye}"))
.and_then(|e| e.static_pad("src")) {
src_pad.add_probe(gst::PadProbeType::EVENT_DOWNSTREAM, |pad, info| {
if let Some(gst::PadProbeData::Event(ref ev)) = info.data {
if let gst::EventView::Caps(c) = ev.view() {
trace!(target:"lesavka_server::video",
?c, "🔍 new caps on {}", pad.name());
}
2025-06-29 17:24:19 -05:00
}
2025-06-29 17:53:25 -05:00
gst::PadProbeReturn::Ok
});
} else {
warn!(target:"lesavka_server::video",
eye = %eye,
2025-06-30 15:45:37 -05:00
"🍪 cam_{eye} not found - skipping pad-probe");
2025-06-29 17:53:25 -05:00
}
2025-06-29 17:24:19 -05:00
2025-06-29 12:28:25 -05:00
let eye_clone = eye.to_owned();
std::thread::spawn(move || {
for msg in bus.iter_timed(gst::ClockTime::NONE) {
match msg.view() {
Error(err) => {
2025-06-29 17:24:19 -05:00
error!(target:"lesavka_server::video",
2025-06-29 12:28:25 -05:00
eye = %eye_clone,
"💥 pipeline error: {} ({})",
err.error(), err.debug().unwrap_or_default());
}
Warning(w) => {
2025-06-29 17:24:19 -05:00
warn!(target:"lesavka_server::video",
2025-06-29 12:28:25 -05:00
eye = %eye_clone,
"⚠️ pipeline warning: {} ({})",
w.error(), w.debug().unwrap_or_default());
}
Info(i) => {
2025-06-29 17:24:19 -05:00
info!(target:"lesavka_server::video",
2025-06-29 12:28:25 -05:00
eye = %eye_clone,
"📌 pipeline info: {} ({})",
i.error(), i.debug().unwrap_or_default());
}
StateChanged(s) if s.current() == gst::State::Playing => {
2025-06-30 02:03:01 -05:00
debug!(target:"lesavka_server::video",
2025-06-29 12:28:25 -05:00
eye = %eye_clone,
"🎬 pipeline PLAYING");
}
_ => {}
}
}
});
2025-06-21 05:21:57 -05:00
sink.set_callbacks(
gst_app::AppSinkCallbacks::builder()
.new_sample(move |sink| {
2025-06-27 22:51:50 -05:00
/* -------- pull frame ---------- */
2025-06-21 05:21:57 -05:00
let sample = sink.pull_sample().map_err(|_| gst::FlowError::Eos)?;
let buffer = sample.buffer().ok_or(gst::FlowError::Error)?;
2025-06-28 01:08:57 -05:00
/* -------- map once, reuse ----- */
let map = buffer.map_readable().map_err(|_| gst::FlowError::Error)?;
2025-06-27 22:51:50 -05:00
/* -------- basic counters ------ */
static FRAME: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
let n = FRAME.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if n % 120 == 0 {
2025-06-28 03:34:48 -05:00
trace!(target: "lesavka_server::video", "eye-{eye}: delivered {n} frames");
2025-06-28 01:08:57 -05:00
if enabled!(Level::TRACE) {
2025-06-28 03:34:48 -05:00
let path = format!("/tmp/eye-{eye}-srv-{:05}.h264", n);
2025-06-28 01:08:57 -05:00
std::fs::write(&path, map.as_slice()).ok();
}
2025-06-29 11:44:16 -05:00
} else if n < 10 {
debug!(target: "lesavka_server::video",
eye = eye, frame = n, bytes = map.len(),
pts = ?buffer.pts(), "⬆️ pushed video sample eye-{eye}");
2025-06-27 22:51:50 -05:00
}
2025-06-23 20:45:21 -05:00
2025-06-27 22:51:50 -05:00
/* -------- detect SPS / IDR ---- */
2025-06-26 17:12:59 -05:00
if enabled!(Level::DEBUG) {
2025-06-27 22:51:50 -05:00
if let Some(&nal) = map.as_slice().get(4) {
if (nal & 0x1F) == 0x05 /* IDR */ {
2025-06-28 03:34:48 -05:00
debug!("eye-{eye}: IDR");
2025-06-26 17:12:59 -05:00
}
}
}
2025-06-27 22:51:50 -05:00
/* -------- timestamps ---------- */
let origin = *START.get_or_init(|| buffer.pts().unwrap_or(gst::ClockTime::ZERO));
let pts_us = buffer
.pts()
.unwrap_or(gst::ClockTime::ZERO)
.saturating_sub(origin)
.nseconds()
/ 1_000;
/* -------- ship over gRPC ----- */
2025-06-29 12:28:25 -05:00
let data = map.as_slice().to_vec();
let size = data.len();
let pkt = VideoPacket { id, pts: pts_us, data };
match tx.try_send(Ok(pkt)) {
Ok(_) => {
trace!(target:"lesavka_server::video",
eye = %eye,
size = size,
2025-07-03 16:08:30 -05:00
"🎥📤 sent");
2025-06-29 12:28:25 -05:00
}
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
static DROP_CNT: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
let c = DROP_CNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if c % 120 == 0 {
debug!(target:"lesavka_server::video",
eye = %eye,
dropped = c,
2025-07-03 16:08:30 -05:00
"🎥⏳ channel full - dropping frames");
2025-06-29 12:28:25 -05:00
}
}
Err(e) => error!("mpsc send err: {e}"),
}
2025-06-27 22:51:50 -05:00
2025-06-21 05:21:57 -05:00
Ok(gst::FlowSuccess::Ok)
})
.build(),
);
2025-06-29 11:44:16 -05:00
pipeline.set_state(gst::State::Playing).context("🎥 starting video pipeline eye-{eye}")?;
2025-06-28 15:45:11 -05:00
let bus = pipeline.bus().unwrap();
loop {
match bus.timed_pop(gst::ClockTime::NONE) {
Some(msg) if matches!(msg.view(), MessageView::StateChanged(s)
if s.current() == gst::State::Playing) => break,
Some(_) => continue,
None => continue,
}
}
2025-06-28 19:40:48 -05:00
Ok(VideoStream { _pipeline: pipeline, inner: ReceiverStream::new(rx) })
2025-06-21 05:21:57 -05:00
}
2025-07-03 08:19:59 -05:00
pub struct WebcamSink {
appsrc: gst_app::AppSrc,
_pipe: gst::Pipeline,
}
impl WebcamSink {
pub fn new(uvc_dev: &str) -> anyhow::Result<Self> {
gst::init()?;
let pipeline = gst::Pipeline::new();
let src = gst::ElementFactory::make("appsrc")
.build()?
.downcast::<gst_app::AppSrc>()
.expect("appsrc");
src.set_is_live(true);
src.set_format(gst::Format::Time);
let h264parse = gst::ElementFactory::make("h264parse").build()?;
let decoder = gst::ElementFactory::make("v4l2h264dec").build()?;
let convert = gst::ElementFactory::make("videoconvert").build()?;
let sink = gst::ElementFactory::make("v4l2sink")
.property("device", &uvc_dev)
.property("sync", &false)
.build()?;
// Upcast to &gst::Element for the collection macros
pipeline.add_many(&[
src.upcast_ref(), &h264parse, &decoder, &convert, &sink
])?;
gst::Element::link_many(&[
src.upcast_ref(), &h264parse, &decoder, &convert, &sink
])?;
pipeline.set_state(gst::State::Playing)?;
Ok(Self { appsrc: src, _pipe: pipeline })
}
pub fn push(&self, pkt: VideoPacket) {
let mut buf = gst::Buffer::from_slice(pkt.data);
buf.get_mut().unwrap()
.set_pts(Some(gst::ClockTime::from_useconds(pkt.pts)));
let _ = self.appsrc.push_buffer(buf);
}
}
2025-07-03 09:24:57 -05:00
/*─────────────────────────────────*/
/* gRPC → WebcamSink relay */
/*─────────────────────────────────*/
pub struct CameraRelay {
sink: WebcamSink, // the v4l2sink pipeline (or stub)
id: u32, // gRPC “id” (for future multicam)
frames: std::sync::atomic::AtomicU64,
}
impl CameraRelay {
pub fn new(id: u32, uvc_dev: &str) -> anyhow::Result<Self> {
Ok(Self {
sink: WebcamSink::new(uvc_dev)?,
id,
frames: std::sync::atomic::AtomicU64::new(0),
})
}
/// Push one VideoPacket coming from the client
pub fn feed(&self, pkt: VideoPacket) {
let n = self.frames.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2025-07-03 15:22:30 -05:00
if n < 10 || n % 60 == 0 {
2025-07-03 09:24:57 -05:00
tracing::debug!(target:"lesavka_server::video",
cam_id = self.id,
frame = n,
bytes = pkt.data.len(),
pts = pkt.pts,
"📸 srv webcam frame");
2025-07-03 15:22:30 -05:00
} else if n % 10 == 0 {
tracing::trace!(target:"lesavka_server::video",
cam_id = self.id,
bytes = pkt.data.len(),
2025-07-03 16:08:30 -05:00
"📸📥 srv pkt");
2025-07-03 09:24:57 -05:00
}
2025-07-04 01:56:59 -05:00
if cfg!(debug_assertions) || tracing::enabled!(tracing::Level::TRACE) {
if n % 120 == 0 {
let path = format!("/tmp/eye3-cli-{n:05}.h264");
if let Err(e) = std::fs::write(&path, &pkt.data) {
tracing::warn!("📸💾 dump failed: {e}");
} else {
tracing::debug!("📸💾 wrote {}", path);
}
}
}
2025-07-03 09:24:57 -05:00
self.sink.push(pkt);
}
}