diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 197 |
1 files changed, 197 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b1a4404 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,197 @@ +#![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 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 + } + } +} + +#[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<()>{ + 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 (w, h) = (c.width(), c.height()); + let s = c.bytesperline(); + loop{ + let img: Result<cairo::ImageSurface> = c.next(|frame, _|{ + let mut img = 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; + } + 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 + 0]; + imgslice[stride*y + x*4 + 1] = b[(y*w+x)*3 + 1]; + imgslice[stride*y + x*4 + 2] = b[(y*w+x)*3 + 2]; + } + 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 = None; + draw.set_draw_func(clone!{ + #[strong(rename_to=frame_buf)] apps.frame_buf, + 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(){ + frame_cache = Some(newfb.into_inner()); + } + if let Some(image) = frame_cache.clone(){ + //{ + // let stride: usize = isurface.stride().try_into().unwrap(); + // let mut isdata = isurface.data().unwrap(); + // for y in 0..image.height{ + // for x in 0..image.width{ + // for c in 0..4{ + // isdata[stride*y + x*4 + c] + // = image.buf[(image.width*y+x)*4 + c]; + // } + // } + // } + //} + 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()) +} |
