diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae65e60..afc7d6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,4 +62,32 @@ jobs: with: command: clippy args: -- -D warnings - \ No newline at end of file + linux: + name: linux + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + - run: rustup component add clippy + - name: check + uses: actions-rs/cargo@v1 + with: + command: check + - name: test + uses: actions-rs/cargo@v1 + with: + command: test + args: --no-run + - name: clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 64ad200..fcbf22a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,13 @@ windows = { version = "0.43", features = [ "implement", ] } +[target.'cfg(target_os="linux")'.dependencies] +v4l = "*" + [dev-dependencies] softbuffer = "0.3.0" winit = "0.27.5" + +[dependencies] +ffimage = "0.9.0" +ffimage_yuv = "0.9.0" diff --git a/README.md b/README.md index 19743d4..7c59691 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,15 @@ Camera API with a reduced feature set for basic usecases and learning. -* ❌ Linux, Web, Android, iOS and various embedded support is not existent yet. -* 🚧 Mac support is based on AVFoundation and is not behind the Camera API yet. - * its good to review test print outs too `cargo t -- --nocapture --test-threads=1` -* 🚧 Windows support is based on MediaFoundation. - * tests need to run with a single thread `cargo t -- --test-threads=1` -* ❌ CI is manual running tests on Mac and Windows with various camera devices. +* 🚧 Mac support is based on AVFoundation +* 🚧 Windows support is based on MediaFoundation +* 🚧 Linux support is based on V4L2 + +* ❌ tests need to run with a single thread `cargo t -- --test-threads=1 --nocapture` + and it is good to review the output of the test cases + +* ✔️ PR check is manual running tests on Mac, Windows and Linux laptop internal camera device and a Logitech external camera +* ✔️ CI runs checks, formatting and clippy for main and PRs ```rust use kamera::Camera; @@ -26,4 +29,16 @@ frame.data().data_u32() // use this buffer, per default in ARGB format // for real use cases processing or displaying frames can get more complicated when trying to be most efficient camera.stop() // or drop it -``` \ No newline at end of file +``` + +## Linux system dependecies + +On a Debian like system (MX Linux for example) I needed to install these system dependencies to build all crates: + +```sh +sudo apt install build-essentials cmake libfontconfig1-dev clang +``` + +* TODO winit depends on sctk-adwaita which transitively also is depending on freetype for font rendering. I think + * winit probably does not have a real need to do font rendering in a default or minimal configuration + * basic font rendering could also be done by a pure rust dependency \ No newline at end of file diff --git a/src/camera.rs b/src/camera.rs index 5ae9b77..0527ecc 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -4,6 +4,10 @@ use super::mac_avf as backend; #[cfg(target_os = "windows")] use super::win_mf as backend; +#[cfg(target_os = "linux")] +use super::linux_v4l2 as backend; + +#[derive(Debug)] pub struct Camera { inner: backend::Camera, } @@ -54,3 +58,12 @@ impl<'a> FrameData<'a> { self.inner.data_u32() } } + +pub(crate) trait InnerCamera: std::fmt::Debug { + type Frame; + + fn new_default_device() -> Self; + fn start(&self); + fn stop(&self); + fn wait_for_frame(&self) -> Option; +} diff --git a/src/lib.rs b/src/lib.rs index 7239524..a448519 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,3 +6,6 @@ pub(crate) mod mac_avf; #[cfg(target_os = "windows")] pub(crate) mod win_mf; + +#[cfg(target_os = "linux")] +pub(crate) mod linux_v4l2; diff --git a/src/linux_v4l2/mod.rs b/src/linux_v4l2/mod.rs new file mode 100644 index 0000000..f7d82b7 --- /dev/null +++ b/src/linux_v4l2/mod.rs @@ -0,0 +1,157 @@ +use ffimage::color::Bgra; + +use v4l::io::traits::CaptureStream; + +use v4l::video::Capture; +use v4l::*; + +use std::marker::PhantomData; +use std::sync::RwLock; + +use crate::InnerCamera; + +pub struct Camera { + device: RwLock, + device_name: String, + stream: RwLock>>, +} + +impl InnerCamera for Camera { + type Frame = Frame; + + fn new_default_device() -> Self { + let device_node = v4l::context::enum_devices().into_iter().next().unwrap(); + let device_name = + device_node.name().unwrap_or_else(|| device_node.path().to_string_lossy().to_string()); + + println!( + "Node {{ index: {}, name: {:?}, path: {:?} }}", + device_node.index(), + device_node.name(), + device_node.path() + ); + + let device = v4l::Device::new(0).unwrap(); + + for fmt in device.enum_formats().unwrap() { + println!("{:?}", fmt); + + for size in device.enum_framesizes(fmt.fourcc).unwrap() { + println!("{:?}", size); + } + } + + let _rgb = FourCC::new(b"RGB3"); + let mut fmt = device.format().unwrap(); + let size = device + .enum_framesizes(fmt.fourcc) + .unwrap() + .into_iter() + .next() + .unwrap() + .size + .to_discrete() + .into_iter() + .last() + .unwrap(); + fmt.width = size.width; + fmt.height = size.height; + + if let Err(error) = device.set_format(&fmt) { + eprintln!("Device.set_format: {}", error); + } + + Self { device: RwLock::new(device), device_name, stream: RwLock::new(None) } + } + + fn start(&self) { + if self.stream.read().unwrap().is_none() { + let device = self.device.write().unwrap(); + let stream = + v4l::io::mmap::Stream::with_buffers(&device, v4l::buffer::Type::VideoCapture, 4) + .expect("Failed to create buffer stream"); + let _ = self.stream.write().unwrap().insert(stream); + } + } + + fn stop(&self) { + let _ = self.stream.write().unwrap().take(); + } + + fn wait_for_frame(&self) -> Option { + let format = self.device.read().unwrap().format().unwrap(); + let size = (format.width, format.height); + if let Ok((buf, _meta)) = self.stream.write().unwrap().as_mut().unwrap().next() { + let data = match &format.fourcc.repr { + b"RGB3" => buf.to_vec(), + b"YUYV" => yuyv_to_rgb32(buf, size.0, size.1), + b"MJPG" => todo!("NJPG not implemented"), + _ => panic!("invalid buffer pixelformat"), + }; + + Some(Frame { data, size }) + } else { + None + } + } +} + +impl std::fmt::Debug for Camera { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Camera").field("device", &self.device_name).finish() + } +} + +pub struct Frame { + data: Vec, + size: (u32, u32), +} + +impl Frame { + pub fn data(&self) -> FrameData { + FrameData { data: self.data.clone(), _phantom: PhantomData } + } + + pub fn size_u32(&self) -> (u32, u32) { + self.size + } +} + +impl std::fmt::Debug for Frame { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Frame").field("data", &self.data.len()).finish() + } +} + +#[derive(Debug)] +pub struct FrameData<'a> { + data: Vec, + _phantom: PhantomData<&'a ()>, +} + +impl<'a> FrameData<'a> { + pub fn data_u8(&self) -> &[u8] { + &self.data + } + + pub fn data_u32(&self) -> &[u32] { + unsafe { self.data.align_to().1 } + } +} + +fn yuyv_to_rgb32(buf: &[u8], w: u32, h: u32) -> Vec { + use ffimage::color::Rgb; + use ffimage::packed::{ImageBuffer, ImageView}; + use ffimage::traits::Convert; + use ffimage_yuv::{yuv::Yuv, yuyv::Yuyv}; + + let yuv422 = ImageView::>::from_buf(buf, w, h).unwrap(); + let mut yuv444 = ImageBuffer::>::new(w, h, 0u8); + let mut rgb = ImageBuffer::>::new(w, h, 0u8); + let mut rgba = ImageBuffer::>::new(w, h, 0u8); + yuv422.convert(&mut yuv444); + yuv444.convert(&mut rgb); + rgb.convert(&mut rgba); + + rgba.into_buf() +} diff --git a/src/mac_avf/camera.rs b/src/mac_avf/camera.rs index 10fd592..976ad83 100644 --- a/src/mac_avf/camera.rs +++ b/src/mac_avf/camera.rs @@ -2,6 +2,7 @@ use super::*; use objc2::rc::Id; use std::sync::Arc; +#[derive(Debug)] pub struct Camera { _device: Id, _input: Id, diff --git a/src/win_mf/camera.rs b/src/win_mf/camera.rs index a02b949..64dc51d 100644 --- a/src/win_mf/camera.rs +++ b/src/win_mf/camera.rs @@ -10,6 +10,7 @@ use std::sync::mpsc::*; use windows::Win32::Media::MediaFoundation::*; #[allow(unused)] +#[derive(Debug)] pub struct Camera { engine: IMFCaptureEngine, device: IMFActivate, diff --git a/tests/camera.rs b/tests/camera.rs index 39ef818..9dec184 100644 --- a/tests/camera.rs +++ b/tests/camera.rs @@ -2,7 +2,8 @@ use kamera::Camera; #[test] fn new_default_device() { - Camera::new_default_device(); + let camera = Camera::new_default_device(); + println!("{:?}", camera); } #[test] @@ -73,6 +74,8 @@ fn frame_data() { assert_eq!(a, b); } +#[cfg(not(target_os = "linux"))] +// linux_v4l2: ioctl VIDIOC_REQBUFS fails with Device Busy, Chromium also fails in this case, no alternative on this level #[test] fn two_cameras_start_and_wait_for_frames() { let camera1 = Camera::new_default_device();