diff options
| -rw-r--r-- | Cargo.lock | 71 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | src/gtk.rs | 171 | ||||
| -rw-r--r-- | src/lib.rs | 5 | ||||
| -rw-r--r-- | src/main.rs | 206 | ||||
| -rw-r--r-- | src/sync.rs | 30 | ||||
| -rw-r--r-- | src/v4l2.rs | 17 | ||||
| -rw-r--r-- | src/v4l2cairo.rs | 88 |
8 files changed, 333 insertions, 257 deletions
@@ -109,31 +109,6 @@ dependencies = [ ] [[package]] -name = "crossbeam-deque" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "Could not get crate checksum" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "Could not get crate checksum" - -[[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -503,15 +478,6 @@ dependencies = [ ] [[package]] -name = "jpeg-decoder" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "Could not get crate checksum" -dependencies = [ - "rayon", -] - -[[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -643,26 +609,6 @@ dependencies = [ ] [[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "Could not get crate checksum" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "Could not get crate checksum" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -768,9 +714,9 @@ version = "0.1.0" dependencies = [ "anyhow", "gtk4", - "jpeg-decoder", "libc", "v4l2-sys", + "zune-jpeg", ] [[package]] @@ -865,3 +811,18 @@ checksum = "Could not get crate checksum" dependencies = [ "memchr", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "Could not get crate checksum" +dependencies = [ + "zune-core", +] @@ -8,4 +8,4 @@ libc = "0.2" v4l2-sys = { version = "1", path = "v4l2-sys/" } anyhow = "1" gtk4 = "0.9" -jpeg-decoder = "0.3" +zune-jpeg = "0.4" diff --git a/src/gtk.rs b/src/gtk.rs new file mode 100644 index 0000000..94fb5f1 --- /dev/null +++ b/src/gtk.rs @@ -0,0 +1,171 @@ +use anyhow::{anyhow, Result}; +use gtk4::{self as gtk, glib, cairo}; +use gtk4::prelude::*; +use glib::{clone, spawn_future_local}; +use std::thread; +use std::sync::{Arc, Mutex}; +use crate::sync::Signal; +use std::future::poll_fn; +use std::task::Poll; + +pub struct FbPool{ + size: usize, + pool: Vec<cairo::ImageSurfaceDataOwned>, +} +impl FbPool{ + pub fn new(size: usize) -> Self{ + FbPool{ + size, + pool: Vec::with_capacity(size), + } + } + pub fn put(&mut self, buf: cairo::ImageSurfaceDataOwned){ + if self.pool.len() < self.size{ + self.pool.push(buf); + } + } + pub fn get(&mut self, w: usize, h: usize) -> Result<cairo::ImageSurface>{ + while let Some(i) = self.pool.pop(){ + let i = i.into_inner(); + if i.width() as usize == w && i.height() as usize == h{ + return Ok(i); + } + } + Ok(cairo::ImageSurface::create( + cairo::Format::Rgb24, + w.try_into()?, h.try_into()?)?) + } +} +pub trait FbSourceOnce{ + fn get(self, w: usize, h: usize) -> Result<cairo::ImageSurface>; +} +impl FbSourceOnce for &Mutex<FbPool>{ + fn get(self, w: usize, h: usize) -> Result<cairo::ImageSurface>{ + self.lock().map_err(|e| anyhow!("{}", e))?.get(w, h) + } +} + +pub struct Packet<T>{ + pub image: cairo::ImageSurfaceDataOwned, + pub attr: T, +} +pub trait Overray: Send + 'static{} +pub trait Source: Send + 'static{ + type Attr: Overray; + fn next(&mut self, fbpool: impl FbSourceOnce) -> Result<Packet<Self::Attr>>; +} +impl Overray for (){} + +struct AppState<T>{ + next: Mutex<Option<Packet<T>>>, + update: Mutex<Signal>, + abort: Mutex<Signal>, + fbpool: Mutex<FbPool>, +} + +fn sourcing_loop<Attr: Overray>( + apps: &AppState<Attr>, + src: &mut impl Source<Attr=Attr> +) -> Result<()>{ + loop{ + let p = src.next(&apps.fbpool)?; + let old = apps.next.lock() + .map_err(|e| anyhow!("{}", e))? + .replace(p); + apps.update.lock().map_err(|e| anyhow!("{}", e))?.wake(); + if let Some(old) = old{ + apps.fbpool.lock() + .map_err(|e| anyhow!("{}", e))? + .put(old.image); + } + } +} + +fn activate<Attr: Overray>(app: >k::Application, apps: Arc<AppState<Attr>>){ + let draw = gtk::DrawingArea::new(); + let mut frame_cache: Option<cairo::ImageSurface> = None; + draw.set_draw_func(clone!{ + #[strong] apps, + 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(){ + 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)); + ctx.scale(scale, scale); + ctx.set_source(&ipat).unwrap(); + ctx.paint().unwrap(); + } + } + }); + spawn_future_local(poll_fn(clone!{ + #[strong] apps, + #[strong] draw, + move |ctx|{ + loop{ + match apps.update.lock().unwrap().poll(ctx){ + Poll::Ready(_) => { + draw.queue_draw(); + }, + pending => return pending, + } + } + } + })); + spawn_future_local(poll_fn(clone!{ + #[strong] apps, + #[strong] app, + move |ctx|{ + loop{ + match apps.abort.lock().unwrap().poll(ctx){ + Poll::Ready(_) => { + app.quit(); + }, + pending => return pending, + } + } + } + })); + + let win = gtk::ApplicationWindow::builder() + .application(app) + .child(&draw) + .build(); + win.present(); +} + +pub fn main(src: impl Source + 'static) -> Result<glib::ExitCode>{ + let apps = Arc::new(AppState{ + next: Mutex::new(None), + update: Mutex::new(Signal::new()), + abort: Mutex::new(Signal::new()), + fbpool: Mutex::new(FbPool::new(4)), + }); + + thread::spawn(clone!{ + #[strong] apps, + move ||{ + let mut src = src; + let res = sourcing_loop(&apps, &mut src); + apps.abort.lock().unwrap().wake(); + res.unwrap(); + } + }); + + let app = gtk::Application::builder() + .build(); + app.connect_activate(clone!{ + #[strong] apps, + move |app| activate(app, apps.clone()) + }); + Ok(app.run()) +} @@ -1,2 +1,5 @@ -pub mod v4l2; pub mod color; +pub mod sync; +pub mod v4l2; +pub mod v4l2cairo; +pub mod gtk; diff --git a/src/main.rs b/src/main.rs index ab54508..5e9ae13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,56 +1,10 @@ -#![allow(unused)] - -use anyhow::{anyhow, Result}; -use std::sync::{Arc, Mutex}; -use std::future::poll_fn; -use std::task::{Context, Poll, Waker}; -use std::thread; - -use gtk4::{self as gtk, glib, cairo}; -use gtk4::prelude::*; -use glib::{clone, spawn_future_local}; - +use anyhow::Result; use sshcamera::v4l2::{Device as V4l2, Field}; -use sshcamera::color::yuv2rgb; -use jpeg_decoder::{self as jpeg, Decoder as JpegDec}; - -struct SignalChannel{ - waker: Option<Waker>, - active: bool, -} -impl SignalChannel{ - fn new() -> Self{ - Self{ - waker: None, - active: false, - } - } - fn wake(&mut self){ - self.active = true; - if let Some(w) = self.waker.take(){ - w.wake(); - } - } - fn poll(this: &Arc<Mutex<Self>>, ctx: &mut Context<'_>) -> Poll<()>{ - let mut l = this.lock().unwrap(); - if l.active{ - l.active = false; - Poll::Ready(()) - }else{ - l.waker = Some(ctx.waker().clone()); - Poll::Pending - } - } -} +use sshcamera::v4l2cairo::V4l2Cairo; +use sshcamera::gtk; +use gtk4::glib::ExitCode; -#[derive(Clone)] -struct AppState{ - frame_buf: Arc<Mutex<Option<cairo::ImageSurfaceDataOwned>>>, - notify: Arc<Mutex<SignalChannel>>, - fbpool: Arc<Mutex<Vec<cairo::ImageSurfaceDataOwned>>>, -} - -fn videothread(apps: AppState) -> Result<()>{ +fn main() -> Result<ExitCode>{ let v = V4l2::open("/dev/video0")?; // TODO: It should be better. @@ -61,152 +15,6 @@ fn videothread(apps: AppState) -> Result<()>{ .build()?; assert!(["YUYV", "MJPG"].contains(&c.pixelformat().as_str())); assert!(c.field() == Field::None); - - let (w, h) = (c.width(), c.height()); - let s = c.bytesperline(); - loop{ - let img: Result<cairo::ImageSurface> = c.next(|frame, _|{ - let mut img = None; - let mut fbpool = apps.fbpool.lock().unwrap(); - while let Some(i) = fbpool.pop(){ - let i = i.into_inner(); - if i.width() as usize == w && i.height() as usize == h{ - img = Some(i); - break; - } - } - drop(fbpool); - let mut img = match img{ - Some(i) => i, - None => { - cairo::ImageSurface::create( - cairo::Format::Rgb24, - w.try_into()?, h.try_into()?)? - }, - }; - let stride: usize = img.stride().try_into()?; - let mut imgslice = img.data()?; - match c.pixelformat().as_str(){ - "YUYV" => { - for (x, y) in (0..h).map( - |y| (0..w).map(move |x|(x, y))).flatten(){ - let p = s*y + x*2; - let (r, g, b) = yuv2rgb( - frame[p], frame[p/4*4 + 1], frame[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) - }, - "MJPG" => { - // 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) - .next() - .ok_or(anyhow!("jpeg not found"))?; - - let mut jpeg = JpegDec::new(&frame[jindex..]); - let b = jpeg.decode()?; - let info = jpeg.info().unwrap(); - - assert!((info.width as usize, info.height as usize) - == (w, h)); - for (x, y) in (0..h).map( - |y| (0..w).map(move |x|(x, y))).flatten(){ - imgslice[stride*y + x*4 + 0] = b[(y*w+x)*3 + 2]; - imgslice[stride*y + x*4 + 1] = b[(y*w+x)*3 + 1]; - imgslice[stride*y + x*4 + 2] = b[(y*w+x)*3 + 0]; - imgslice[stride*y + x*4 + 3] = 0; - } - - drop(imgslice); - Ok(img) - }, - _ => unreachable!(), - } - }).unwrap_or_else(|e| Err(e.into())); - - match img{ - Ok(img) => { - *apps.frame_buf.lock().unwrap() = Some(img.take_data().unwrap()); - apps.notify.lock().unwrap().wake(); - }, - Err(err) => { - println!("Skipping erroneous frame: {:?}", err); - }, - } - } -} - -fn gtkmain(app: >k::Application){ - let apps = AppState{ - frame_buf: Arc::new(Mutex::new(None)), - notify: Arc::new(Mutex::new(SignalChannel::new())), - fbpool: Arc::new(Mutex::new(Vec::new())), - }; - - thread::spawn(clone!{ - #[strong] apps, - move || videothread(apps).unwrap() - }); - - let draw = gtk::DrawingArea::new(); - let mut frame_cache: Option<cairo::ImageSurface> = None; - draw.set_draw_func(clone!{ - #[strong(rename_to=frame_buf)] apps.frame_buf, - #[strong(rename_to=fbpool)] apps.fbpool, - move |_draw, ctx, canvas_w, canvas_h|{ - ctx.set_source_rgb(0., 0., 0.); - ctx.paint().unwrap(); - - if let Some(newfb) = frame_buf.lock().unwrap().take(){ - if let Some(mut lastframe) = frame_cache.take(){ - let mut fbpool = fbpool.lock().unwrap(); - if fbpool.len() < 8{ - fbpool.push(lastframe.take_data().unwrap()); - } - } - frame_cache = Some(newfb.into_inner()); - } - if let Some(image) = frame_cache.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)); - ctx.scale(scale, scale); - ctx.set_source(&ipat).unwrap(); - ctx.paint().unwrap(); - } - } - }); - spawn_future_local(poll_fn(clone!{ - #[strong(rename_to=notify)] apps.notify, - #[strong] draw, - move |ctx|{ - loop{ - match SignalChannel::poll(¬ify, ctx){ - Poll::Ready(_) => { - draw.queue_draw(); - }, - pending => return pending, - } - } - } - })); - - let win = gtk::ApplicationWindow::builder() - .application(app) - .child(&draw) - .build(); - win.present(); -} - -fn main() -> Result<glib::ExitCode>{ - let app = gtk::Application::builder() - .build(); - app.connect_activate(gtkmain); - Ok(app.run()) + let v2c = V4l2Cairo::new(c); + gtk::main(v2c) } diff --git a/src/sync.rs b/src/sync.rs new file mode 100644 index 0000000..08bcce3 --- /dev/null +++ b/src/sync.rs @@ -0,0 +1,30 @@ +use std::task::{Context, Poll, Waker}; + +pub struct Signal{ + waker: Option<Waker>, + active: bool, +} +impl Signal{ + pub fn new() -> Self{ + Self{ + waker: None, + active: false, + } + } + pub fn wake(&mut self){ + self.active = true; + if let Some(w) = self.waker.take(){ + w.wake(); + } + } + pub fn poll(&mut self, ctx: &mut Context<'_>) -> Poll<()>{ + if self.active{ + self.active = false; + Poll::Ready(()) + }else{ + self.waker = Some(ctx.waker().clone()); + Poll::Pending + } + } +} + diff --git a/src/v4l2.rs b/src/v4l2.rs index 96cc356..0f22aba 100644 --- a/src/v4l2.rs +++ b/src/v4l2.rs @@ -148,6 +148,16 @@ impl Debug for ImageFormat{ if self.be(){ ":be" }else{ "" }) } } +impl PartialEq<str> for ImageFormat{ + fn eq(&self, other: &str) -> bool{ + Into::<ImageFormat>::into(other) == *self + } +} +impl PartialEq<ImageFormat> for str{ + fn eq(&self, other: &ImageFormat) -> bool{ + Into::<ImageFormat>::into(self) == *other + } +} #[derive(Copy, Clone)] pub struct BufAttrs{ @@ -194,6 +204,8 @@ pub struct Device{ cap: v4l2::v4l2_capability, io_capture: IoMethod, } +// Safe to Send while *mut u8 in IoMethod is never be copied. +unsafe impl Send for Device{} macro_rules! /* unsafe */ mk_ioctl_getter{ ($name:ident, $type:ty, $op:expr; $($qn:ident: $qt:ty),*) => { @@ -325,7 +337,10 @@ impl Device{ unsafe{ Self::unmap_bufs(bufs) }; panic!("mmap: {:?}", IoError::last_os_error()); } - bufs.push(MmappedBuffer{ptr: ptr as *mut u8, length: bufr.length}); + bufs.push(MmappedBuffer{ + ptr: ptr as *mut u8, + length: bufr.length, + }); } Ok(IoMethod::Mmap(bufs)) diff --git a/src/v4l2cairo.rs b/src/v4l2cairo.rs new file mode 100644 index 0000000..322a14b --- /dev/null +++ b/src/v4l2cairo.rs @@ -0,0 +1,88 @@ +use anyhow::{anyhow, Result}; +use crate::gtk; +use crate::v4l2; +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; + +pub struct V4l2Cairo(v4l2::CaptStream); +impl V4l2Cairo{ + pub fn new(inner: v4l2::CaptStream) -> Self{ + V4l2Cairo(inner) + } +} +impl gtk::Source for V4l2Cairo{ + type Attr = (); + fn next(&mut self, fbpool: impl gtk::FbSourceOnce) + -> Result<gtk::Packet<()>>{ + 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 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; + let (r, g, b) = yuv2rgb( + frame[p], frame[p/4*4 + 1], frame[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"{ + // 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) + .next() + .ok_or(anyhow!("jpeg not found"))?; + + let mut jpeg = JpegDec::new_with_options( + &frame[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{ + return Err(anyhow!("invalid size of jpeg")); + } + let mut img = fbpool.take().unwrap().get(w, h)?; + 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]); + } + drop(imgslice); + Ok(img) + }else{ + unimplemented!() + } + })?; + + if let Ok(img) = img{ + return Ok(gtk::Packet{ + image: img.take_data()?, + attr: (), + }); + } + } + } +} + |
