2025-06-08 22:24:14 -05:00
|
|
|
|
// client/src/input/camera.rs
|
2025-07-03 08:19:59 -05:00
|
|
|
|
#![forbid(unsafe_code)]
|
2025-06-08 22:24:14 -05:00
|
|
|
|
|
|
|
|
|
|
use anyhow::Result;
|
2025-07-03 09:24:57 -05:00
|
|
|
|
use anyhow::Context;
|
2025-07-03 08:19:59 -05:00
|
|
|
|
use gstreamer as gst;
|
|
|
|
|
|
use gstreamer_app as gst_app;
|
|
|
|
|
|
use gst::prelude::*;
|
|
|
|
|
|
use lesavka_common::lesavka::VideoPacket;
|
2025-06-08 22:24:14 -05:00
|
|
|
|
|
|
|
|
|
|
pub struct CameraCapture {
|
2025-07-03 08:19:59 -05:00
|
|
|
|
pipeline: gst::Pipeline,
|
|
|
|
|
|
sink: gst_app::AppSink,
|
2025-06-08 22:24:14 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl CameraCapture {
|
2025-07-03 08:19:59 -05:00
|
|
|
|
pub fn new(device_fragment: Option<&str>) -> anyhow::Result<Self> {
|
|
|
|
|
|
gst::init().ok();
|
|
|
|
|
|
|
|
|
|
|
|
// Pick device
|
|
|
|
|
|
let dev = device_fragment
|
|
|
|
|
|
.and_then(Self::find_device)
|
|
|
|
|
|
.unwrap_or_else(|| "/dev/video0".into());
|
|
|
|
|
|
|
2025-07-03 15:22:30 -05:00
|
|
|
|
// let (enc, raw_caps) = Self::pick_encoder();
|
|
|
|
|
|
// (NVIDIA → VA-API → software x264).
|
|
|
|
|
|
let (enc, kf_prop, kf_val) = Self::choose_encoder();
|
|
|
|
|
|
tracing::info!("📸 using encoder element: {enc}");
|
2025-07-04 01:56:59 -05:00
|
|
|
|
let (src_caps, preenc) = match enc {
|
|
|
|
|
|
"nvh264enc" => (
|
|
|
|
|
|
"video/x-raw(memory:NVMM),format=NV12,width=1280,height=720",
|
|
|
|
|
|
"nvvidconv !"
|
|
|
|
|
|
),
|
|
|
|
|
|
"vaapih264enc" => (
|
|
|
|
|
|
"video/x-raw,format=NV12,width=1280,height=720",
|
|
|
|
|
|
"videoconvert !"
|
|
|
|
|
|
),
|
|
|
|
|
|
_ => ("video/x-raw,width=1280,height=720", "videoconvert !"),
|
|
|
|
|
|
};
|
2025-07-03 15:22:30 -05:00
|
|
|
|
|
|
|
|
|
|
// let desc = format!(
|
|
|
|
|
|
// "v4l2src device={dev} do-timestamp=true ! {raw_caps},width=1280,height=720 ! \
|
|
|
|
|
|
// videoconvert ! {enc} key-int-max=30 ! \
|
|
|
|
|
|
// h264parse config-interval=-1 ! \
|
|
|
|
|
|
// appsink name=asink emit-signals=true max-buffers=60 drop=true"
|
|
|
|
|
|
// );
|
|
|
|
|
|
// tracing::debug!(%desc, "📸 pipeline-desc");
|
|
|
|
|
|
// Build a pipeline that works for any of the three encoders.
|
|
|
|
|
|
// * nvh264enc needs NVMM memory caps;
|
|
|
|
|
|
// * vaapih264enc wants system-memory caps;
|
|
|
|
|
|
// * x264enc needs the usual raw caps.
|
2025-07-03 08:19:59 -05:00
|
|
|
|
let desc = format!(
|
2025-07-04 01:56:59 -05:00
|
|
|
|
"v4l2src device={dev} do-timestamp=true ! {src_caps} ! \
|
|
|
|
|
|
{preenc} {enc} {kf_prop}={kf_val} ! \
|
|
|
|
|
|
h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,alignment=au ! \
|
|
|
|
|
|
queue max-size-buffers=30 leaky=downstream ! \
|
2025-07-03 15:22:30 -05:00
|
|
|
|
appsink name=asink emit-signals=true max-buffers=60 drop=true"
|
2025-07-03 08:19:59 -05:00
|
|
|
|
);
|
2025-07-03 15:22:30 -05:00
|
|
|
|
tracing::info!(%enc, ?desc, "📸 using encoder element");
|
2025-07-03 08:19:59 -05:00
|
|
|
|
|
2025-07-03 09:24:57 -05:00
|
|
|
|
let pipeline: gst::Pipeline = gst::parse::launch(&desc)
|
|
|
|
|
|
.context("gst parse_launch(cam)")?
|
2025-07-03 08:19:59 -05:00
|
|
|
|
.downcast::<gst::Pipeline>()
|
|
|
|
|
|
.expect("not a pipeline");
|
|
|
|
|
|
|
2025-07-03 09:24:57 -05:00
|
|
|
|
tracing::debug!("📸 pipeline built OK – setting PLAYING…");
|
2025-07-03 08:19:59 -05:00
|
|
|
|
let sink: gst_app::AppSink = pipeline
|
|
|
|
|
|
.by_name("asink")
|
|
|
|
|
|
.expect("appsink element not found")
|
|
|
|
|
|
.downcast::<gst_app::AppSink>()
|
|
|
|
|
|
.expect("appsink down‑cast");
|
|
|
|
|
|
|
|
|
|
|
|
pipeline.set_state(gst::State::Playing)?;
|
2025-07-03 09:24:57 -05:00
|
|
|
|
tracing::info!("📸 webcam pipeline ▶️ device={dev}");
|
2025-07-03 08:19:59 -05:00
|
|
|
|
|
|
|
|
|
|
Ok(Self { pipeline, sink })
|
2025-06-08 22:24:14 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-03 08:19:59 -05:00
|
|
|
|
pub fn pull(&self) -> Option<VideoPacket> {
|
|
|
|
|
|
let sample = self.sink.pull_sample().ok()?;
|
|
|
|
|
|
let buf = sample.buffer()?;
|
|
|
|
|
|
let map = buf.map_readable().ok()?;
|
|
|
|
|
|
let pts = buf.pts().unwrap_or(gst::ClockTime::ZERO).nseconds() / 1_000;
|
2025-07-04 03:41:39 -05:00
|
|
|
|
Some(VideoPacket { id: 2, pts, data: map.as_slice().to_vec() })
|
2025-07-03 08:19:59 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Fuzzy‑match devices under `/dev/v4l/by-id`
|
|
|
|
|
|
fn find_device(substr: &str) -> Option<String> {
|
|
|
|
|
|
let dir = std::fs::read_dir("/dev/v4l/by-id").ok()?;
|
|
|
|
|
|
for e in dir.flatten() {
|
|
|
|
|
|
let p = e.path();
|
|
|
|
|
|
if p.file_name()?.to_string_lossy().contains(substr) {
|
|
|
|
|
|
if let Ok(target) = std::fs::read_link(&p) {
|
|
|
|
|
|
return Some(format!("/dev/{}", target.file_name()?.to_string_lossy()));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Cheap stub used when the web‑cam is disabled
|
|
|
|
|
|
pub fn new_stub() -> Self {
|
|
|
|
|
|
let pipeline = gst::Pipeline::new();
|
|
|
|
|
|
let sink: gst_app::AppSink = gst::ElementFactory::make("appsink")
|
|
|
|
|
|
.build()
|
2025-07-03 09:24:57 -05:00
|
|
|
|
.expect("appsink")
|
2025-07-03 08:19:59 -05:00
|
|
|
|
.downcast::<gst_app::AppSink>()
|
2025-07-03 09:24:57 -05:00
|
|
|
|
.unwrap();
|
2025-07-03 08:19:59 -05:00
|
|
|
|
Self { pipeline, sink }
|
2025-06-08 22:24:14 -05:00
|
|
|
|
}
|
2025-07-03 15:22:30 -05:00
|
|
|
|
|
2025-07-04 01:56:59 -05:00
|
|
|
|
#[allow(dead_code)] // helper kept for future heuristics
|
2025-07-03 15:22:30 -05:00
|
|
|
|
fn pick_encoder() -> (&'static str, &'static str) {
|
|
|
|
|
|
let encoders = &[
|
|
|
|
|
|
("nvh264enc", "video/x-raw(memory:NVMM),format=NV12"),
|
|
|
|
|
|
("vaapih264enc","video/x-raw,format=NV12"),
|
|
|
|
|
|
("v4l2h264enc", "video/x-raw"), // RPi, Jetson, etc.
|
|
|
|
|
|
("x264enc", "video/x-raw"), // software
|
|
|
|
|
|
];
|
|
|
|
|
|
for (name, caps) in encoders {
|
|
|
|
|
|
if gst::ElementFactory::find(name).is_some() {
|
|
|
|
|
|
return (name, caps);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// last resort – software
|
|
|
|
|
|
("x264enc", "video/x-raw")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn choose_encoder() -> (&'static str, &'static str, &'static str) {
|
2025-07-04 01:56:59 -05:00
|
|
|
|
match () {
|
|
|
|
|
|
_ if gst::ElementFactory::find("nvh264enc").is_some() =>
|
|
|
|
|
|
("nvh264enc", "gop-size", "30"),
|
|
|
|
|
|
_ if gst::ElementFactory::find("vaapih264enc").is_some() =>
|
|
|
|
|
|
("vaapih264enc","keyframe-period","30"),
|
|
|
|
|
|
_ if gst::ElementFactory::find("v4l2h264enc").is_some() =>
|
|
|
|
|
|
("v4l2h264enc","idrcount", "30"),
|
|
|
|
|
|
_ => ("x264enc", "key-int-max", "30"),
|
2025-07-03 15:22:30 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-08 22:24:14 -05:00
|
|
|
|
}
|