lesavka/client/src/input/camera.rs

86 lines
2.9 KiB
Rust
Raw Normal View History

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 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());
let desc = format!(
"v4l2src device={dev} io-mode=dmabuf-do-timestamp=true ! \
videoconvert ! videoscale ! video/x-raw,width=1280,height=720 ! \
v4l2h264enc key-int-max=30 \
extra-controls=\"encode,frame_level_rate_control_enable=1,h264_profile=4\" ! \
h264parse config-interval=-1 ! \
appsink name=asink emit-signals=true max-buffers=60 drop=true"
);
// --- NEW: propagate the Result and downcast with `?`
let pipeline: gst::Pipeline = gst::parse::launch(&desc)?
.downcast::<gst::Pipeline>()
.expect("not a pipeline");
// --- NEW: the lookup already returns the concrete AppSink → just unwrap with `?`
let sink: gst_app::AppSink = pipeline
.by_name("asink")
.expect("appsink element not found")
.downcast::<gst_app::AppSink>()
.expect("appsink downcast");
pipeline.set_state(gst::State::Playing)?;
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;
Some(VideoPacket { id: 100, pts, data: map.as_slice().to_vec() })
}
/// Fuzzymatch 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 webcam is disabled
pub fn new_stub() -> Self {
let pipeline = gst::Pipeline::new();
// --- NEW: AppSink factory helper (AppSink::new() does not exist)
let sink: gst_app::AppSink = gst::ElementFactory::make("appsink")
.build()
.expect("make appsink")
.downcast::<gst_app::AppSink>()
.expect("appsink downcast");
Self { pipeline, sink }
2025-06-08 22:24:14 -05:00
}
}