summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authordyknon dyknonr5fjp2025-02-20 22:31:21 +0900
committerdyknon dyknonr5fjp2025-02-20 22:31:21 +0900
commit5633cf1b5fb1d07c2ae0cf4749bef3d08dde260a (patch)
treec9b942756d04668782c284a25164e6df93c82f91 /src/main.rs
simple v4l2 application now...
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs197
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: &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 = 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(&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())
+}