Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

basic V4l2 support #4

Merged
merged 12 commits into from
Jul 28, 2023
30 changes: 29 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,32 @@ jobs:
with:
command: clippy
args: -- -D warnings

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
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
29 changes: 22 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
```
```

## 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
13 changes: 13 additions & 0 deletions src/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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<Self::Frame>;
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
157 changes: 157 additions & 0 deletions src/linux_v4l2/mod.rs
Original file line number Diff line number Diff line change
@@ -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<v4l::Device>,
device_name: String,
stream: RwLock<Option<v4l::io::mmap::Stream<'static>>>,
}

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<Frame> {
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<u8>,
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<u8>,
_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<u8> {
use ffimage::color::Rgb;
use ffimage::packed::{ImageBuffer, ImageView};
use ffimage::traits::Convert;
use ffimage_yuv::{yuv::Yuv, yuyv::Yuyv};

let yuv422 = ImageView::<Yuyv<u8>>::from_buf(buf, w, h).unwrap();
let mut yuv444 = ImageBuffer::<Yuv<u8>>::new(w, h, 0u8);
let mut rgb = ImageBuffer::<Rgb<u8>>::new(w, h, 0u8);
let mut rgba = ImageBuffer::<Bgra<u8>>::new(w, h, 0u8);
yuv422.convert(&mut yuv444);
yuv444.convert(&mut rgb);
rgb.convert(&mut rgba);

rgba.into_buf()
}
1 change: 1 addition & 0 deletions src/mac_avf/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::*;
use objc2::rc::Id;
use std::sync::Arc;

#[derive(Debug)]
pub struct Camera {
_device: Id<AVCaptureDevice>,
_input: Id<AVCaptureDeviceInput>,
Expand Down
1 change: 1 addition & 0 deletions src/win_mf/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::sync::mpsc::*;
use windows::Win32::Media::MediaFoundation::*;

#[allow(unused)]
#[derive(Debug)]
pub struct Camera {
engine: IMFCaptureEngine,
device: IMFActivate,
Expand Down
5 changes: 4 additions & 1 deletion tests/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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();
Expand Down
Loading