summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bin/sshcamera.rs59
-rw-r--r--src/camera.rs5
-rw-r--r--src/gtk.rs78
-rw-r--r--src/io.rs37
-rw-r--r--src/lib.rs2
-rw-r--r--src/main.rs20
-rw-r--r--src/v4l2.rs45
-rw-r--r--src/v4l2abst.rs103
-rw-r--r--src/v4l2cairo.rs121
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{
+}
diff --git a/src/gtk.rs b/src/gtk.rs
index 94fb5f1..c72b524 100644
--- a/src/gtk.rs
+++ b/src/gtk.rs
@@ -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: &gtk::Application, apps: Arc<AppState<Attr>>){
+fn activate<Attr: Overlay>(app: &gtk::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: &gtk::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: &gtk::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;
+}
diff --git a/src/lib.rs b/src/lib.rs
index 06d6a0b..61a1e4f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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: &gtk::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: &gtk::Label) -> Result<()>{
+ widget.set_label(
+ &self.timestamp.format("%Y/%m/%d %H:%M:%S%.3f").to_string());
+ Ok(())
+ }
+}
+