diff options
| author | dyknon dyknonr5fjp | 2025-02-24 18:58:32 +0900 |
|---|---|---|
| committer | dyknon dyknonr5fjp | 2025-02-24 18:58:32 +0900 |
| commit | 6ecbf0d55695335f52d6fcf2b6a22ed45f5e4d99 (patch) | |
| tree | e17bfc8a9b55d5afbfe2f884322a96a54340f25d /src | |
| parent | 31b60ae28e6aff6d23b378cd77e288c96c7db148 (diff) | |
Remote camera capability.
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/sshcamera.rs | 59 | ||||
| -rw-r--r-- | src/camera.rs | 5 | ||||
| -rw-r--r-- | src/gtk.rs | 78 | ||||
| -rw-r--r-- | src/io.rs | 37 | ||||
| -rw-r--r-- | src/lib.rs | 2 | ||||
| -rw-r--r-- | src/main.rs | 20 | ||||
| -rw-r--r-- | src/v4l2.rs | 45 | ||||
| -rw-r--r-- | src/v4l2abst.rs | 103 | ||||
| -rw-r--r-- | src/v4l2cairo.rs | 121 |
9 files changed, 391 insertions, 79 deletions
diff --git a/src/bin/sshcamera.rs b/src/bin/sshcamera.rs new file mode 100644 index 0000000..02c8778 --- /dev/null +++ b/src/bin/sshcamera.rs @@ -0,0 +1,59 @@ +use anyhow::{anyhow, Result}; +use sshcamera::v4l2::{Device as V4l2, Field}; +use sshcamera::v4l2cairo::V4l2Cairo; +use sshcamera::gtk; +use sshcamera::v4l2abst::{CaptStream, RemoteCam}; +use sshcamera::io::RWBundle; +use gtk4::glib::ExitCode; +use std::env; +use std::io::{self, Read as _, Write as _}; +use std::process::{Command, Stdio}; + +fn main() -> Result<ExitCode>{ + let mut args = env::args(); + if args.next() == None{ + return Err(anyhow!("arg0 is not present??")); + } + let Some(arg1) = args.next() else{ + return Err(anyhow!("Give me args")); + }; + if arg1.contains('/'){ + if args.next() != None{ + return Err(anyhow!("too many args")); + } + + let v = V4l2::open(arg1)?; + + // TODO: It should be better. + let mut c = v.captstream_builder()? + .set_pixelformat("MJPG".into()) + //.set_pixelformat("YUYV".into()) + .set_field(Field::None) + .build()?; + assert!(["YUYV", "MJPG"].contains(&c.pixelformat().as_str())); + assert!(c.field() == Field::None); + + let mut io = RWBundle(io::stdin(), io::stdout()); + loop{ + CaptStream::next(&mut c, |frame|{ + frame.serialize(&mut io)?; + io.flush()?; + let mut rb = [0]; + io.read_exact(&mut rb)?; + if rb[0] != 0x2e{ + return Err(anyhow!("protocol error")); + } + Ok(()) + })??; + } + }else{ + let child = Command::new(arg1) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + let io = RWBundle(child.stdout.unwrap(), child.stdin.unwrap()); + let v2c = V4l2Cairo::new(RemoteCam::new(io)); + gtk::main(v2c) + } +} diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..53acf76 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,5 @@ +use crate::v4l2; +use chrono::{DateTime, Local}; + +trait Camera{ +} @@ -1,9 +1,11 @@ use anyhow::{anyhow, Result}; -use gtk4::{self as gtk, glib, cairo}; +use gtk4::{self as gtk, glib, cairo, gio}; use gtk4::prelude::*; use glib::{clone, spawn_future_local}; use std::thread; use std::sync::{Arc, Mutex}; +use std::rc::Rc; +use std::cell::RefCell; use crate::sync::Signal; use std::future::poll_fn; use std::task::Poll; @@ -49,12 +51,30 @@ pub struct Packet<T>{ pub image: cairo::ImageSurfaceDataOwned, pub attr: T, } -pub trait Overray: Send + 'static{} +pub trait Overlay: Send + 'static{ + type Widget: glib::object::IsA<gtk::Widget>; + fn empty() -> Result<Self::Widget>; + fn activate(_widget: &Self::Widget) -> Result<()>{ + Ok(()) + } + fn update(&self, _widget: &Self::Widget) -> Result<()>{ + Ok(()) + } +} pub trait Source: Send + 'static{ - type Attr: Overray; + type Attr: Overlay; fn next(&mut self, fbpool: impl FbSourceOnce) -> Result<Packet<Self::Attr>>; } -impl Overray for (){} +impl Overlay for (){ + type Widget = gtk::Box; + fn empty() -> Result<gtk::Box>{ + Ok(gtk::Box::builder() + .halign(gtk::Align::Start) + .valign(gtk::Align::Start) + .visible(false) + .build()) + } +} struct AppState<T>{ next: Mutex<Option<Packet<T>>>, @@ -63,7 +83,7 @@ struct AppState<T>{ fbpool: Mutex<FbPool>, } -fn sourcing_loop<Attr: Overray>( +fn sourcing_loop<Attr: Overlay>( apps: &AppState<Attr>, src: &mut impl Source<Attr=Attr> ) -> Result<()>{ @@ -81,23 +101,19 @@ fn sourcing_loop<Attr: Overray>( } } -fn activate<Attr: Overray>(app: >k::Application, apps: Arc<AppState<Attr>>){ +fn activate<Attr: Overlay>(app: >k::Application, apps: Arc<AppState<Attr>>){ let draw = gtk::DrawingArea::new(); - let mut frame_cache: Option<cairo::ImageSurface> = None; + let overlay = Attr::empty().unwrap(); + + let frame_cache: Rc<RefCell<Option<cairo::ImageSurface>>> + = Rc::new(RefCell::new(None)); draw.set_draw_func(clone!{ - #[strong] apps, + #[strong] frame_cache, move |_draw, ctx, canvas_w, canvas_h|{ ctx.set_source_rgb(0., 0., 0.); ctx.paint().unwrap(); - if let Some(newfb) = apps.next.lock().unwrap().take(){ - if let Some(lastframe) = frame_cache.take(){ - apps.fbpool.lock().unwrap() - .put(lastframe.take_data().unwrap()); - } - frame_cache = Some(newfb.image.into_inner()); - } - if let Some(image) = frame_cache.clone(){ + if let Some(image) = frame_cache.borrow_mut().clone(){ let ipat = cairo::SurfacePattern::create(&image); let scale = ((canvas_w as f64) / (image.width() as f64)).min( (canvas_h as f64) / (image.height() as f64)); @@ -110,10 +126,21 @@ fn activate<Attr: Overray>(app: >k::Application, apps: Arc<AppState<Attr>>){ spawn_future_local(poll_fn(clone!{ #[strong] apps, #[strong] draw, + #[strong] frame_cache, + #[strong] overlay, move |ctx|{ loop{ match apps.update.lock().unwrap().poll(ctx){ Poll::Ready(_) => { + if let Some(newfb) = apps.next.lock().unwrap().take(){ + let mut frame_cache = frame_cache.borrow_mut(); + if let Some(lastframe) = frame_cache.take(){ + apps.fbpool.lock().unwrap() + .put(lastframe.take_data().unwrap()); + } + *frame_cache = Some(newfb.image.into_inner()); + newfb.attr.update(&overlay).unwrap(); + } draw.queue_draw(); }, pending => return pending, @@ -136,11 +163,16 @@ fn activate<Attr: Overray>(app: >k::Application, apps: Arc<AppState<Attr>>){ } })); + let olcontainer = gtk::Overlay::builder() + .child(&draw) + .build(); + olcontainer.add_overlay(&overlay); let win = gtk::ApplicationWindow::builder() .application(app) - .child(&draw) + .child(&olcontainer) .build(); win.present(); + Attr::activate(&overlay).unwrap(); } pub fn main(src: impl Source + 'static) -> Result<glib::ExitCode>{ @@ -162,10 +194,18 @@ pub fn main(src: impl Source + 'static) -> Result<glib::ExitCode>{ }); let app = gtk::Application::builder() + .flags(gio::ApplicationFlags::HANDLES_COMMAND_LINE) .build(); - app.connect_activate(clone!{ + app.connect_command_line(clone!{ #[strong] apps, - move |app| activate(app, apps.clone()) + move |app, _| { + activate(app, apps.clone()); + 0 + } }); + //app.connect_activate(clone!{ + // #[strong] apps, + // move |app| activate(app, apps.clone()) + //}); Ok(app.run()) } diff --git a/src/io.rs b/src/io.rs new file mode 100644 index 0000000..ec47e1d --- /dev/null +++ b/src/io.rs @@ -0,0 +1,37 @@ +use std::io::{Read, Write, Result}; + +pub struct RWBundle<R: Read, W: Write>(pub R, pub W); +impl<R: Read, W: Write> Read for RWBundle<R, W>{ + fn read(&mut self, buf: &mut [u8]) -> Result<usize>{ + self.0.read(buf) + } + //fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> Result<usize>; + //fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize>; + //fn read_to_string(&mut self, buf: &mut String) -> Result<usize>; + fn read_exact(&mut self, buf: &mut [u8]) -> Result<()>{ + self.0.read_exact(buf) + } + //fn by_ref(&mut self) -> &mut Self + // where Self: Sized; + //fn bytes(self) -> Bytes<Self> + // where Self: Sized; + //fn chain<R: Read>(self, next: R) -> Chain<Self, R> + // where Self: Sized; + //fn take(self, limit: u64) -> Take<Self> + // where Self: Sized; +} +impl<R: Read, W: Write> Write for RWBundle<R, W>{ + fn write(&mut self, buf: &[u8]) -> Result<usize>{ + self.1.write(buf) + } + fn flush(&mut self) -> Result<()>{ + self.1.flush() + } + //fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result<usize>; + fn write_all(&mut self, buf: &[u8]) -> Result<()>{ + self.1.write_all(buf) + } + //fn write_fmt(&mut self, fmt: Arguments<'_>) -> Result<()>; + //fn by_ref(&mut self) -> &mut Self + // where Self: Sized; +} @@ -1,5 +1,7 @@ pub mod color; pub mod sync; +pub mod io; pub mod v4l2; +pub mod v4l2abst; pub mod v4l2cairo; pub mod gtk; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 5e9ae13..0000000 --- a/src/main.rs +++ /dev/null @@ -1,20 +0,0 @@ -use anyhow::Result; -use sshcamera::v4l2::{Device as V4l2, Field}; -use sshcamera::v4l2cairo::V4l2Cairo; -use sshcamera::gtk; -use gtk4::glib::ExitCode; - -fn main() -> Result<ExitCode>{ - let v = V4l2::open("/dev/video0")?; - - // TODO: It should be better. - let c = v.captstream_builder()? - .set_pixelformat("MJPG".into()) - //.set_pixelformat("YUYV".into()) - .set_field(Field::None) - .build()?; - assert!(["YUYV", "MJPG"].contains(&c.pixelformat().as_str())); - assert!(c.field() == Field::None); - let v2c = V4l2Cairo::new(c); - gtk::main(v2c) -} diff --git a/src/v4l2.rs b/src/v4l2.rs index 0f22aba..a8aff0c 100644 --- a/src/v4l2.rs +++ b/src/v4l2.rs @@ -9,11 +9,13 @@ use std::io::Error as IoError; use std::fmt::{Display, Debug, Formatter, Error as FmtError}; use std::error::Error as ErrorTrait; use std::{str, iter, array}; +use std::time::Duration; +use chrono::{DateTime, Local}; macro_rules! define_flagset{ ($tname:ident: $ctype:ty; $($name:ident = $mask:expr),+) => { #[derive(Copy, Clone, Debug, PartialEq, Eq)] - pub struct $tname{ $($name: bool),+ } + pub struct $tname{ $(pub $name: bool),+ } impl $tname{ pub fn zero() -> Self{ Self{ $($name: false),+ } @@ -166,6 +168,47 @@ pub struct BufAttrs{ pub timestamp: c::timeval, pub sequence: u32, } +impl BufAttrs{ + // XXX: It is correct only when V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC + pub fn get_datetime(&self) -> Option<DateTime<Local>>{ + let now_monotonic = unsafe{ + let mut buf = MaybeUninit::uninit(); + assert!(c::clock_gettime(c::CLOCK_MONOTONIC, buf.as_mut_ptr()) + == 0); + buf.assume_init() + }; + let now_wallclock = Local::now(); + let mut timestamp = c::timespec{ + tv_sec: self.timestamp.tv_sec as _, + tv_nsec: self.timestamp.tv_usec as _, + }; + timestamp.tv_nsec *= 1000; + let (mut so, mut nso, future); + if now_monotonic.tv_sec < timestamp.tv_sec + || now_monotonic.tv_sec == timestamp.tv_sec + && now_monotonic.tv_nsec < timestamp.tv_nsec + { + future = true; + so = timestamp.tv_sec - now_monotonic.tv_sec; + nso = timestamp.tv_nsec - now_monotonic.tv_nsec; + }else{ + future = false; + so = now_monotonic.tv_sec - timestamp.tv_sec; + nso = now_monotonic.tv_nsec - timestamp.tv_nsec; + } + if nso < 0{ + nso += 1000_000_000; + so -= 1; + } + assert!(0 <= nso && nso < 1000_000_000); + let to = Duration::new(so as u64, nso as u32); + if future{ + Some(now_wallclock + to) + }else{ + Some(now_wallclock - to) + } + } +} impl Debug for BufAttrs{ fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError>{ #[derive(Debug)] diff --git a/src/v4l2abst.rs b/src/v4l2abst.rs new file mode 100644 index 0000000..baefea0 --- /dev/null +++ b/src/v4l2abst.rs @@ -0,0 +1,103 @@ +use anyhow::{anyhow, Result}; +use crate::v4l2; +use chrono::{DateTime, Local}; +use std::io::{Read, Write}; +use std::mem::size_of; + +pub struct Frame<'a>{ + pub format: v4l2::ImageFormat, + pub width: usize, + pub height: usize, + pub stride: usize, + pub buf: &'a [u8], + pub timestamp: DateTime<Local>, +} +impl<'a> Frame<'a>{ + pub fn serialize(&self, dst: &mut impl Write) -> Result<()>{ + dst.write_all(&u32::to_be_bytes(self.format.into()))?; + dst.write_all(&self.timestamp.timestamp_subsec_nanos().to_be_bytes())?; + dst.write_all(&self.timestamp.timestamp().to_be_bytes())?; + dst.write_all(&self.width.to_be_bytes())?; + dst.write_all(&self.height.to_be_bytes())?; + dst.write_all(&self.stride.to_be_bytes())?; + dst.write_all(&self.buf.len().to_be_bytes())?; + dst.write_all(self.buf)?; + Ok(()) + } + pub fn deserialize<'b>(src: &'b mut impl Read, buf: &'a mut Vec<u8>) + -> Result<Self>{ + struct Rh<'c>(&'c mut dyn Read); + impl<'c> Rh<'c>{ + fn u32(&mut self) -> Result<u32>{ + let mut tmp: [u8; 4] = Default::default(); + self.0.read_exact(&mut tmp)?; + Ok(u32::from_be_bytes(tmp)) + } + fn i64(&mut self) -> Result<i64>{ + let mut tmp: [u8; 8] = Default::default(); + self.0.read_exact(&mut tmp)?; + Ok(i64::from_be_bytes(tmp)) + } + fn usize(&mut self) -> Result<usize>{ + let mut tmp: [u8; size_of::<usize>()] = Default::default(); + self.0.read_exact(&mut tmp)?; + Ok(usize::from_be_bytes(tmp)) + } + } + let mut src = Rh(src); + let format = src.u32()?.into(); + let (ns, s) = (src.u32()?, src.i64()?); + let timestamp = DateTime::from_timestamp(s, ns) + .ok_or(anyhow!("Invalid DateTime"))?.into(); + let width = src.usize()?; + let height = src.usize()?; + let stride = src.usize()?; + let len = src.usize()?; + buf.resize(len, 0); + src.0.read_exact(buf)?; + Ok(Frame{ format, width, height, stride, buf, timestamp }) + } +} + +pub trait CaptStream{ + fn next<R>(&mut self, cb: impl FnOnce(Frame) -> R) -> Result<R>; +} + +impl CaptStream for v4l2::CaptStream{ + fn next<R>(&mut self, cb: impl FnOnce(Frame) -> R) -> Result<R>{ + let (width, height) = (self.width(), self.height()); + let stride = self.bytesperline(); + let format = self.pixelformat(); + let mut cb = Some(cb); + loop{ + if let Some(ret) = v4l2::CaptStream::next(self, |buf, attr|{ + if attr.flags.error{ + return None; + } + let timestamp = attr.get_datetime().unwrap(); + Some(cb.take().unwrap()(Frame{ + format, width, height, stride, buf, timestamp + })) + })?{ + return Ok(ret); + } + } + } +} + +pub struct RemoteCam<I: Read + Write>(I); +impl<I: Read + Write> RemoteCam<I>{ + pub fn new(inner: I) -> Self{ + Self(inner) + } +} +impl<I: Read + Write> CaptStream for RemoteCam<I>{ + fn next<R>(&mut self, cb: impl FnOnce(Frame) -> R) -> Result<R>{ + let ack: [u8; 1] = [0x2e]; + let mut buf = Vec::new(); + let frame = Frame::deserialize(&mut self.0, &mut buf)?; + self.0.write_all(&ack)?; + self.0.flush()?; + Ok(cb(frame)) + } +} diff --git a/src/v4l2cairo.rs b/src/v4l2cairo.rs index 322a14b..83be3b1 100644 --- a/src/v4l2cairo.rs +++ b/src/v4l2cairo.rs @@ -1,88 +1,131 @@ use anyhow::{anyhow, Result}; -use crate::gtk; -use crate::v4l2; +use gtk4 as gtk; +use gtk::prelude::*; +use crate::gtk as cgtk; +use crate::v4l2abst; use crate::color::yuv2rgb; use zune_jpeg::JpegDecoder as JpegDec; use zune_jpeg::zune_core::options::DecoderOptions as JpegOptions; use zune_jpeg::zune_core::colorspace::ColorSpace as JpegColorSpace; +use chrono::{DateTime, Local}; -pub struct V4l2Cairo(v4l2::CaptStream); -impl V4l2Cairo{ - pub fn new(inner: v4l2::CaptStream) -> Self{ +pub struct V4l2Cairo<T: v4l2abst::CaptStream>(T); +impl<T: v4l2abst::CaptStream> V4l2Cairo<T>{ + pub fn new(inner: T) -> Self{ V4l2Cairo(inner) } } -impl gtk::Source for V4l2Cairo{ - type Attr = (); - fn next(&mut self, fbpool: impl gtk::FbSourceOnce) - -> Result<gtk::Packet<()>>{ +impl<T: v4l2abst::CaptStream + Send + 'static> cgtk::Source for V4l2Cairo<T>{ + type Attr = Overlay; + fn next(&mut self, fbpool: impl cgtk::FbSourceOnce) + -> Result<cgtk::Packet<Overlay>>{ let mut fbpool = Some(fbpool); - let (w, h) = (self.0.width(), self.0.height()); - let s = self.0.bytesperline(); - let pixelformat = self.0.pixelformat(); loop{ - let img = self.0.next(|frame, _|{ - if &pixelformat == "YUYV"{ - if w % 2 != 0{ - return Err(anyhow!("invalid width of YUYV")); - } - if frame.len() < w*h*2{ - return Err(anyhow!("invalid size of YUYV")); - } - let mut img = fbpool.take().unwrap().get(w, h)?; + let img = self.0.next(|frame|{ + let v4l2abst::Frame{ + format, width, height, stride: sstride, buf, timestamp + } = frame; + if &format == "YUYV"{ + assert!(width % 2 == 0); + assert!(width * 2 <= sstride); + assert!(buf.len() >= sstride * height); + let mut img = fbpool.take().unwrap().get(width, height)?; let stride: usize = img.stride().try_into()?; let mut imgslice = img.data()?; - for (x, y) in (0..h).map( - |y| (0..w).map(move |x|(x, y))).flatten(){ - let p = s*y + x*2; + for (x, y) in (0..height).map( + |y| (0..width).map(move |x|(x, y))).flatten(){ + let p = sstride*y + x*2; let (r, g, b) = yuv2rgb( - frame[p], frame[p/4*4 + 1], frame[p/4*4 + 3]); + buf[p], buf[p/4*4 + 1], buf[p/4*4 + 3]); imgslice[stride*y + x*4 + 0] = b; imgslice[stride*y + x*4 + 1] = g; imgslice[stride*y + x*4 + 2] = r; imgslice[stride*y + x*4 + 3] = 0; } drop(imgslice); - Ok(img) - }else if &pixelformat == "MJPG" || &pixelformat == "JPEG"{ + Ok((img, timestamp)) + }else if &format == "MJPG" || &format == "JPEG"{ // Jpeg is not placed in start of slice in some situation. // It is even possible that there are no Jpeg data. - let jindex = (0..frame.len()-1) - .filter(|i| frame[*i] == 0xff && frame[i+1] == 0xd8) + let jindex = (0..buf.len()-1) + .filter(|i| buf[*i] == 0xff && buf[i+1] == 0xd8) .next() .ok_or(anyhow!("jpeg not found"))?; let mut jpeg = JpegDec::new_with_options( - &frame[jindex..], + &buf[jindex..], JpegOptions::new_fast() .jpeg_set_out_colorspace(JpegColorSpace::BGRA)); let b = jpeg.decode()?; let info = jpeg.info().unwrap(); - if info.width as usize != w || info.height as usize != h{ + if info.width as usize != width + || info.height as usize != height + { return Err(anyhow!("invalid size of jpeg")); } - let mut img = fbpool.take().unwrap().get(w, h)?; + let mut img = fbpool.take().unwrap().get(width, height)?; let stride: usize = img.stride().try_into()?; let mut imgslice = img.data()?; - for y in 0..h{ - imgslice[stride*y..stride*y+w*4] - .copy_from_slice(&b[y*w*4..((y+1)*w)*4]); + for y in 0..height{ + imgslice[stride*y..stride*y+width*4] + .copy_from_slice(&b[y*width*4..((y+1)*width)*4]); } drop(imgslice); - Ok(img) + Ok((img, timestamp)) }else{ unimplemented!() } })?; - if let Ok(img) = img{ - return Ok(gtk::Packet{ + if let Ok((img, timestamp)) = img{ + return Ok(cgtk::Packet{ image: img.take_data()?, - attr: (), + attr: Overlay{ timestamp }, }); } } } } +pub struct Overlay{ + timestamp: DateTime<Local>, +} +impl cgtk::Overlay for Overlay{ + type Widget = gtk::Label; + fn empty() -> Result<gtk::Label>{ + Ok(gtk::Label::builder() + .label("...") + .valign(gtk::Align::End) + .halign(gtk::Align::Start) + .build()) + } + fn activate(widget: >k::Label) -> Result<()>{ + let disp = widget.display(); + let style = gtk::CssProvider::new(); + style.load_from_string(r#" + .v4l2cairo_label{ + color: black; + text-shadow: + white 1px 1px, + white 1px 0, + white 1px -1px, + white 0 1px, + white 0 -1px, + white -1px 1px, + white -1px 0, + white -1px -1px; + } + "#); + gtk::style_context_add_provider_for_display( + &disp, &style, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION); + widget.add_css_class("v4l2cairo_label"); + Ok(()) + } + fn update(&self, widget: >k::Label) -> Result<()>{ + widget.set_label( + &self.timestamp.format("%Y/%m/%d %H:%M:%S%.3f").to_string()); + Ok(()) + } +} + |
