summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock71
-rw-r--r--Cargo.toml2
-rw-r--r--src/gtk.rs171
-rw-r--r--src/lib.rs5
-rw-r--r--src/main.rs206
-rw-r--r--src/sync.rs30
-rw-r--r--src/v4l2.rs17
-rw-r--r--src/v4l2cairo.rs88
8 files changed, 333 insertions, 257 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 644fe9e..b31a57f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
+]
diff --git a/Cargo.toml b/Cargo.toml
index a3c4b23..9d7ba5b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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: &gtk::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())
+}
diff --git a/src/lib.rs b/src/lib.rs
index cf29039..06d6a0b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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: &gtk::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(&notify, 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: (),
+ });
+ }
+ }
+ }
+}
+