camera core add
This commit is contained in:
parent
f96bb0f8c2
commit
1c095a95d1
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,21 +1,85 @@
|
||||
// client/src/input/camera.rs
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use anyhow::Result;
|
||||
use gstreamer as gst;
|
||||
use gstreamer_app as gst_app;
|
||||
use gst::prelude::*;
|
||||
use lesavka_common::lesavka::VideoPacket;
|
||||
|
||||
/// A stub camera aggregator or capture
|
||||
pub struct CameraCapture {
|
||||
// no real fields yet
|
||||
pipeline: gst::Pipeline,
|
||||
sink: gst_app::AppSink,
|
||||
}
|
||||
|
||||
impl CameraCapture {
|
||||
pub fn new_stub() -> Self {
|
||||
// in real code: open /dev/video0, set formats, etc
|
||||
CameraCapture {}
|
||||
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 down‑cast 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 down‑cast");
|
||||
|
||||
pipeline.set_state(gst::State::Playing)?;
|
||||
|
||||
Ok(Self { pipeline, sink })
|
||||
}
|
||||
|
||||
/// Called regularly to capture frames or do nothing
|
||||
pub fn process_frames(&mut self) -> Result<()> {
|
||||
// no-op
|
||||
Ok(())
|
||||
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() })
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
// --- 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 down‑cast");
|
||||
|
||||
Self { pipeline, sink }
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,8 +92,11 @@ impl InputAggregator {
|
||||
bail!("No suitable keyboard/mouse devices found or none grabbed.");
|
||||
}
|
||||
|
||||
// Stubs for camera / mic:
|
||||
self.camera = Some(CameraCapture::new_stub());
|
||||
// Auto‑select webcam (may be toggled later through magic chord)
|
||||
self.camera = match CameraCapture::new(std::env::var("LESAVKA_CAM_SOURCE").ok().as_deref()) {
|
||||
Ok(cam) => { info!("📸 webcam enabled"); Some(cam) }
|
||||
Err(e) => { warn!("📸 webcam disabled: {e}"); None }
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -17,6 +17,8 @@ service Relay {
|
||||
rpc StreamMouse (stream MouseReport) returns (stream MouseReport);
|
||||
rpc CaptureVideo (MonitorRequest) returns (stream VideoPacket);
|
||||
rpc CaptureAudio (MonitorRequest) returns (stream AudioPacket);
|
||||
rpc StreamMicrophone (stream AudioPacket) returns (stream Empty) {}
|
||||
rpc StreamMicrophone (stream AudioPacket) returns (stream Empty);
|
||||
rpc StreamCamera (stream VideoPacket) returns (stream Empty);
|
||||
|
||||
rpc ResetUsb (Empty) returns (ResetUsbReply);
|
||||
}
|
||||
|
||||
@ -112,6 +112,14 @@ echo 2 >"$U/c_ssize"
|
||||
# Optional: allocate a few extra request buffers
|
||||
echo 32 >"$U/req_number" 2>/dev/null || true
|
||||
|
||||
# ----------------------- UVC function (usb‑video) ------------------
|
||||
mkdir -p "$G/functions/uvc.usb0"
|
||||
# Mandatory control interface strings
|
||||
mkdir -p "$G/functions/uvc.usb0/control/strings/0x409"
|
||||
echo "Lesavka UVC" >"$G/functions/uvc.usb0/control/strings/0x409/label"
|
||||
# Simple 720p MJPEG + 720p H.264 alt‑setting
|
||||
printf '\x50\x00\x00\x00' >"$G/functions/uvc.usb0/control/header/h_video"
|
||||
|
||||
# ----------------------- configuration -----------------------------
|
||||
mkdir -p "$G/configs/c.1/strings/0x409"
|
||||
echo 500 > "$G/configs/c.1/MaxPower"
|
||||
@ -120,7 +128,8 @@ echo "Config 1: HID + UAC2" >"$G/configs/c.1/strings/0x409/configuration"
|
||||
|
||||
ln -s $G/functions/hid.usb0 $G/configs/c.1/
|
||||
ln -s $G/functions/hid.usb1 $G/configs/c.1/
|
||||
ln -s "$U" "$G/configs/c.1/"
|
||||
ln -s $U $G/configs/c.1/
|
||||
ln -s $G/functions/uvc.usb0 $G/configs/c.1/
|
||||
|
||||
# mkdir -p $G/functions/hid.usb0/os_desc
|
||||
# mkdir -p $G/functions/hid.usb1/os_desc
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -133,6 +133,7 @@ impl Relay for Handler {
|
||||
type CaptureVideoStream = Pin<Box<dyn Stream<Item=Result<VideoPacket,Status>> + Send>>;
|
||||
type CaptureAudioStream = Pin<Box<dyn Stream<Item=Result<AudioPacket,Status>> + Send>>;
|
||||
type StreamMicrophoneStream = ReceiverStream<Result<Empty, Status>>;
|
||||
type StreamCameraStream = ReceiverStream<Result<Empty, Status>>;
|
||||
|
||||
async fn stream_keyboard(
|
||||
&self,
|
||||
@ -209,6 +210,16 @@ impl Relay for Handler {
|
||||
Ok(Response::new(ReceiverStream::new(rx)))
|
||||
}
|
||||
|
||||
async fn stream_camera(
|
||||
&self,
|
||||
_req: Request<tonic::Streaming<VideoPacket>>,
|
||||
) -> Result<Response<Self::StreamCameraStream>, Status> {
|
||||
// Simply consume the inbound stream and discard; echo back a single Empty.
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(1);
|
||||
tx.send(Ok(Empty {})).await.ok();
|
||||
Ok(Response::new(ReceiverStream::new(rx)))
|
||||
}
|
||||
|
||||
async fn capture_video(
|
||||
&self,
|
||||
req: Request<MonitorRequest>,
|
||||
|
||||
@ -214,3 +214,48 @@ pub async fn eye_ball(
|
||||
}
|
||||
Ok(VideoStream { _pipeline: pipeline, inner: ReceiverStream::new(rx) })
|
||||
}
|
||||
|
||||
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()?;
|
||||
|
||||
// Up‑cast 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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user