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-27 14:01:29 -05:00
|
|
|
|
use gst::log;
|
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-26 17:12:59 -05:00
|
|
|
|
use tracing::{debug, enabled, Level};
|
2025-06-21 05:21:57 -05:00
|
|
|
|
|
2025-06-27 14:01:29 -05:00
|
|
|
|
static START: std::sync::OnceLock<gst::ClockTime> = std::sync::OnceLock::new();
|
|
|
|
|
|
|
2025-06-21 05:21:57 -05:00
|
|
|
|
pub async fn spawn_camera(
|
|
|
|
|
|
dev: &str,
|
|
|
|
|
|
id: u32,
|
2025-06-25 16:52:26 -05:00
|
|
|
|
_max_bitrate_kbit: u32,
|
2025-06-21 05:21:57 -05:00
|
|
|
|
) -> anyhow::Result<ReceiverStream<Result<VideoPacket, Status>>> {
|
|
|
|
|
|
gst::init().context("gst init")?;
|
|
|
|
|
|
|
2025-06-26 16:17:31 -05:00
|
|
|
|
// IMPORTANT: keep one AU per buffer, include regular SPS/PPS
|
2025-06-21 05:21:57 -05:00
|
|
|
|
let desc = format!(
|
2025-06-27 14:01:29 -05:00
|
|
|
|
"v4l2src device={dev} io-mode=mmap ! \
|
|
|
|
|
|
video/x-h264,stream-format=byte-stream,alignment=au ! \
|
2025-06-26 16:17:31 -05:00
|
|
|
|
h264parse config-interval=1 ! \
|
2025-06-27 14:01:29 -05:00
|
|
|
|
appsink name=sink emit-signals=true drop=true sync=false"
|
|
|
|
|
|
);
|
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>()
|
|
|
|
|
|
.expect("pipeline down-cast");
|
2025-06-21 05:21:57 -05:00
|
|
|
|
|
2025-06-23 20:57:18 -05:00
|
|
|
|
let sink = pipeline
|
|
|
|
|
|
.by_name("sink")
|
|
|
|
|
|
.expect("appsink")
|
|
|
|
|
|
.dynamic_cast::<gst_app::AppSink>()
|
|
|
|
|
|
.expect("appsink downcast");
|
|
|
|
|
|
|
2025-06-21 05:21:57 -05:00
|
|
|
|
let (tx, rx) = tokio::sync::mpsc::channel(256);
|
|
|
|
|
|
|
|
|
|
|
|
sink.set_callbacks(
|
|
|
|
|
|
gst_app::AppSinkCallbacks::builder()
|
2025-06-23 20:45:21 -05:00
|
|
|
|
// .new_sample(move |sink| {
|
|
|
|
|
|
// let sample = sink.pull_sample().map_err(|_| gst::FlowError::Eos)?;
|
|
|
|
|
|
// let buffer = sample.buffer().ok_or(gst::FlowError::Error)?;
|
|
|
|
|
|
|
|
|
|
|
|
// let map = buffer.map_readable().map_err(|_| gst::FlowError::Error)?;
|
|
|
|
|
|
// let pkt = VideoPacket {
|
|
|
|
|
|
// id,
|
|
|
|
|
|
// pts: buffer.pts().nseconds() / 1_000, // → µs
|
|
|
|
|
|
// data: map.as_slice().to_vec(),
|
|
|
|
|
|
// };
|
|
|
|
|
|
// // ignore back‑pressure: drop oldest if channel is full
|
|
|
|
|
|
// let _ = tx.try_send(Ok(pkt));
|
|
|
|
|
|
// Ok(gst::FlowSuccess::Ok)
|
|
|
|
|
|
// })
|
|
|
|
|
|
// .build(),
|
2025-06-21 05:21:57 -05:00
|
|
|
|
.new_sample(move |sink| {
|
|
|
|
|
|
let sample = sink.pull_sample().map_err(|_| gst::FlowError::Eos)?;
|
|
|
|
|
|
let buffer = sample.buffer().ok_or(gst::FlowError::Error)?;
|
2025-06-27 14:01:29 -05:00
|
|
|
|
let origin = *START.get_or_init(|| buffer.pts().unwrap_or(gst::ClockTime::ZERO));
|
2025-06-21 05:21:57 -05:00
|
|
|
|
|
2025-06-23 20:45:21 -05:00
|
|
|
|
let pts_us = buffer
|
|
|
|
|
|
.pts()
|
2025-06-27 14:01:29 -05:00
|
|
|
|
.unwrap_or(gst::ClockTime::ZERO)
|
|
|
|
|
|
.saturating_sub(origin)
|
|
|
|
|
|
.nseconds() / 1_000;
|
2025-06-23 20:45:21 -05:00
|
|
|
|
|
2025-06-21 05:21:57 -05:00
|
|
|
|
let map = buffer.map_readable().map_err(|_| gst::FlowError::Error)?;
|
2025-06-23 20:45:21 -05:00
|
|
|
|
|
2025-06-26 17:12:59 -05:00
|
|
|
|
if enabled!(Level::DEBUG) {
|
|
|
|
|
|
if let Some(slice) = map.as_slice().get(0..5) {
|
|
|
|
|
|
match slice[4] & 0b1_1111 {
|
|
|
|
|
|
// 0x07 = SPS, 0x05 = IDR (Annex-B “byte-stream”)
|
|
|
|
|
|
0x07 | 0x05 => {
|
|
|
|
|
|
debug!(
|
|
|
|
|
|
"🎞️ monitor {id}: got NAL {:02X}, {} bytes",
|
|
|
|
|
|
slice[4],
|
|
|
|
|
|
map.as_slice().len()
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-21 05:21:57 -05:00
|
|
|
|
let pkt = VideoPacket {
|
|
|
|
|
|
id,
|
2025-06-23 20:45:21 -05:00
|
|
|
|
pts: pts_us,
|
2025-06-21 05:21:57 -05:00
|
|
|
|
data: map.as_slice().to_vec(),
|
|
|
|
|
|
};
|
2025-06-23 20:45:21 -05:00
|
|
|
|
let _ = tx.try_send(Ok(pkt)); // drop on overflow
|
2025-06-21 05:21:57 -05:00
|
|
|
|
Ok(gst::FlowSuccess::Ok)
|
|
|
|
|
|
})
|
|
|
|
|
|
.build(),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-06-27 14:01:29 -05:00
|
|
|
|
// gst::debug_remove_default_log_function();
|
|
|
|
|
|
// gst::debug_add_default_log_function(|lvl, cat, msg| {
|
|
|
|
|
|
// println!("[GST] {lvl:?} {cat}: {msg}");
|
|
|
|
|
|
// });
|
|
|
|
|
|
// std::env::set_var("GST_DEBUG", "v4l2src:4,h264parse:3");
|
|
|
|
|
|
|
2025-06-21 05:21:57 -05:00
|
|
|
|
pipeline.set_state(gst::State::Playing)?;
|
|
|
|
|
|
|
|
|
|
|
|
Ok(ReceiverStream::new(rx))
|
|
|
|
|
|
}
|