diff --git a/Cargo.toml b/Cargo.toml index ce91aef..6ae3bf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ input_opencv = ["opencv", "opencv/clang-runtime"] input_msmf = ["windows"] input_v4l = ["v4l"] input_gstreamer = ["gstreamer"] +input_ffmpeg = [] +docs-only = ["input_uvc", "input_opencv", "input_msmf", "input_v4l", "input_gstreamer", "input_ffmpeg"] [dependencies] thiserror = "1.0.24" @@ -44,3 +46,7 @@ optional = true [dev-dependencies.windows] version = "0.10.0" + +[package.metadata.docs.rs] +no-default-features = true +features = ["docs-only"] \ No newline at end of file diff --git a/src/backends/capture/v4l2.rs b/src/backends/capture/v4l2.rs index 104d94b..cc14f99 100644 --- a/src/backends/capture/v4l2.rs +++ b/src/backends/capture/v4l2.rs @@ -1,30 +1,62 @@ -use crate::{error::NokhwaError, utils::{CameraFormat, CameraInfo}}; +use crate::{ + error::NokhwaError, + utils::{CameraFormat, CameraInfo}, + CaptureBackendTrait, FrameFormat, Resolution, +}; use v4l::prelude::*; +use v4l::{ + buffer::Type, + io::traits::CaptureStream, + video::{capture::Parameters, Capture}, + Format, FourCC, +}; -pub struct V4LCaptureDevice { +#[cfg(feature = "input_v4l")] +impl From for Format { + fn from(cam_fmt: CameraFormat) -> Self { + let pxfmt = match cam_fmt.format() { + FrameFormat::MJPEG => FourCC::new(b"MJPG"), + FrameFormat::YUYV => FourCC::new(b"YUYV"), + }; + + Format::new(cam_fmt.width(), cam_fmt.height(), pxfmt) + } +} + +/// The backend struct that interfaces with V4L2. +/// To see what this does, please see [`CaptureBackendTrait`] +/// # Quirks +/// Calling [`set_resolution()`](CaptureBackendTrait::set_resolution), [`set_framerate()`](CaptureBackendTrait::set_framerate), or [`set_frameformat()`](CaptureBackendTrait::set_frameformat) +/// each internally calls [`set_camera_format()`](CaptureBackendTrait::set_camera_format). +pub struct V4LCaptureDevice<'a> { camera_format: Option, camera_info: CameraInfo, device: Device, + stream_handle: Option>, } -impl V4LCaptureDevice { - /// Creates a new capture device using the V4L2 backend +impl<'a> V4LCaptureDevice<'a> { + /// Creates a new capture device using the V4L2 backend. Indexes are gives to devices by the OS, and usually numbered by order of discovery. /// # Errors - /// This function will error if the camera is currently busy or if V4L2 can't read device information. + /// This function will error if the camera is currently busy or if V4L2 can't read device information. pub fn new(index: usize) -> Result { let device = match Device::new(index) { Ok(dev) => dev, Err(why) => { - return Err(NokhwaError::CouldntOpenDevice(format!("V4L2 Error: {}", why.to_string()))) + return Err(NokhwaError::CouldntOpenDevice(format!( + "V4L2 Error: {}", + why.to_string() + ))) } }; let camera_info = match device.query_caps() { - Ok(caps) => { - CameraInfo::new(caps.card, "".to_string(), caps.driver, index) - } + Ok(caps) => CameraInfo::new(caps.card, "".to_string(), caps.driver, index), Err(why) => { - return Err(NokhwaError::CouldntQueryDevice{ property: "Capabilities".to_string(), error: why.to_string() }) + return Err(NokhwaError::CouldntQueryDevice { + property: "Capabilities".to_string(), + error: why.to_string(), + }) } }; @@ -32,7 +64,168 @@ impl V4LCaptureDevice { camera_format: None, camera_info, device, + stream_handle: None, }) - + } +} + +impl<'a> CaptureBackendTrait for V4LCaptureDevice<'a> { + fn get_info(&self) -> CameraInfo { + self.camera_info.clone() + } + + fn get_camera_format(&self) -> Option { + self.camera_format + } + + #[allow(clippy::option_if_let_else)] + fn init_camera_format_default(&mut self, overwrite: bool) -> Result<(), NokhwaError> { + match self.camera_format { + Some(_) => { + if overwrite { + return self.set_camera_format(CameraFormat::default()); + } + Ok(()) + } + None => self.set_camera_format(CameraFormat::default()), + } + } + + fn set_camera_format(&mut self, new_fmt: CameraFormat) -> Result<(), NokhwaError> { + let prev_format = match self.device.format() { + Ok(fmt) => fmt, + Err(why) => { + return Err(NokhwaError::CouldntQueryDevice { + property: "Resolution, FrameFormat".to_string(), + error: why.to_string(), + }) + } + }; + let prev_fps = match self.device.params() { + Ok(fps) => fps, + Err(why) => { + return Err(NokhwaError::CouldntQueryDevice { + property: "Framerate".to_string(), + error: why.to_string(), + }) + } + }; + + let format: Format = new_fmt.into(); + let framerate = Parameters::with_fps(new_fmt.framerate()); + + if let Err(why) = self.device.set_format(&format) { + return Err(NokhwaError::CouldntSetProperty { + property: "Resolution, FrameFormat".to_string(), + value: format.to_string(), + error: why.to_string(), + }); + } + if let Err(why) = self.device.set_params(&framerate) { + return Err(NokhwaError::CouldntSetProperty { + property: "Framerate".to_string(), + value: framerate.to_string(), + error: why.to_string(), + }); + } + + if self.stream_handle.is_some() { + self.stream_handle = Some({ + match MmapStream::new(&self.device, Type::VideoCapture) { + Ok(stream) => stream, + Err(why) => { + // undo + if let Err(why) = self.device.set_format(&prev_format) { + return Err(NokhwaError::CouldntSetProperty { + property: "Attempt undo due to stream acquisition failure. Resolution, FrameFormat".to_string(), + value: prev_format.to_string(), + error: why.to_string(), + }); + } + if let Err(why) = self.device.set_params(&prev_fps) { + return Err(NokhwaError::CouldntSetProperty { + property: + "Attempt undo due to stream acquisition failure. Framerate" + .to_string(), + value: prev_fps.to_string(), + error: why.to_string(), + }); + } + + return Err(NokhwaError::CouldntOpenStream(why.to_string())); + } + } + }) + } + self.camera_format = Some(new_fmt); + Ok(()) + } + + fn get_resolution(&self) -> Option { + self.camera_format.map(|fmt| fmt.resoltuion()) + } + + #[allow(clippy::option_if_let_else)] + fn set_resolution(&mut self, new_res: Resolution) -> Result<(), NokhwaError> { + if let Some(fmt) = self.camera_format { + let mut new_fmt = fmt; + new_fmt.set_resolution(new_res); + self.set_camera_format(new_fmt) + } else { + self.camera_format = Some(CameraFormat::new(new_res, FrameFormat::MJPEG, 0)); + Ok(()) + } + } + + fn get_framerate(&self) -> Option { + self.camera_format.map(|fmt| fmt.framerate()) + } + + #[allow(clippy::option_if_let_else)] + fn set_framerate(&mut self, new_fps: u32) -> Result<(), NokhwaError> { + if let Some(fmt) = self.camera_format { + let mut new_fmt = fmt; + new_fmt.set_framerate(new_fps); + self.set_camera_format(new_fmt) + } else { + self.camera_format = Some(CameraFormat::new( + Resolution::new(0, 0), + FrameFormat::MJPEG, + new_fps, + )); + Ok(()) + } + } + + fn get_frameformat(&self) -> Option { + self.camera_format.map(|fmt| fmt.format()) + } + + #[allow(clippy::option_if_let_else)] + fn set_frameformat(&mut self, fourcc: FrameFormat) -> Result<(), NokhwaError> { + if let Some(fmt) = self.camera_format { + let mut new_fmt = fmt; + new_fmt.set_format(fourcc); + self.set_camera_format(new_fmt) + } else { + self.camera_format = Some(CameraFormat::new(Resolution::new(0, 0), fourcc, 0)); + Ok(()) + } + } + + fn open_stream(&mut self) -> Result<(), NokhwaError> { + todo!() + } + + fn is_stream_open(&self) -> bool { + self.stream_handle.is_some() + } + + fn get_frame(&self) -> Result, Vec>, NokhwaError> { + todo!() + } + + fn get_frame_raw(&self) -> Vec { + todo!() } } diff --git a/src/camera.rs b/src/camera.rs index c70d729..1f620b6 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,3 +1 @@ -pub struct Capture { - -} \ No newline at end of file +pub struct Capture {} diff --git a/src/camera_traits.rs b/src/camera_traits.rs index 7024652..39f1bd1 100644 --- a/src/camera_traits.rs +++ b/src/camera_traits.rs @@ -1,12 +1,75 @@ +use std::collections::HashMap; + use image::{ImageBuffer, Rgb}; -use crate::{error::NokhwaError, utils::{CameraFormat, CameraInfo}}; +use crate::{ + error::NokhwaError, + utils::{CameraFormat, CameraInfo, FrameFormat, Resolution}, +}; +/// This trait is for any backend that allows you to grab and take frames from a camera. pub trait CaptureBackendTrait { - fn info(&self) -> CameraInfo; - fn set_camera_format(&self, new_fmt: CameraFormat) -> Result<(), NokhwaError>; - fn open_stream(&self) -> Result<(), NokhwaError>; + /// Gets the camera information such as Name and Index as a [`CameraInfo`]. + fn get_info(&self) -> CameraInfo; + /// Assigns a sensible default to the backend's `CameraFormat`. Usually 640x480 @ 15 FPS + MJPEG. + /// If there is already a value assigned to the `CameraFormat`, it will only be overwritten if `overwrite` is set to `true`. + /// If false, this function will do nothing (NO-OP). + /// This will reset the current stream if used while stream is opened. + /// # Errors + /// If you started the stream and the camera rejects the new camera format, this will return an error. + fn init_camera_format_default(&mut self, overwrite: bool) -> Result<(), NokhwaError>; + /// Gets the current [`CameraFormat`]. Will return none if no format has been set yet. + fn get_camera_format(&self) -> Option; + /// Will set the current [`CameraFormat`] + /// This will reset the current stream if used while stream is opened. + /// # Errors + /// If you started the stream and the camera rejects the new camera format, this will return an error. + fn set_camera_format(&mut self, new_fmt: CameraFormat) -> Result<(), NokhwaError>; + /// Gets the current camera resolution (See: [`Resolution`]). Will return none if no resolution has been set yet. + fn get_resolution(&self) -> Option; + /// Will set the current [`Resolution`] + /// This will reset the current stream if used while stream is opened. + /// # Errors + /// If you started the stream and the camera rejects the new resolution, this will return an error. + fn set_resolution(&mut self, new_res: Resolution) -> Result<(), NokhwaError>; + /// Gets the current camera framerate. Will return none if no framerate has been set yet. + fn get_framerate(&self) -> Option; + /// Will set the current framerate + /// This will reset the current stream if used while stream is opened. + /// # Errors + /// If you started the stream and the camera rejects the new framerate, this will return an error. + fn set_framerate(&mut self, new_fps: u32) -> Result<(), NokhwaError>; + /// Gets the current camera's frame format (See: [`FrameFormat`]). Will return none if no frame format has been set yet. + fn get_frameformat(&self) -> Option; + /// Will set the current [`FrameFormat`] + /// This will reset the current stream if used while stream is opened. + /// # Errors + /// If you started the stream and the camera rejects the new frame foramt, this will return an error. + fn set_frameformat(&mut self, fourcc: FrameFormat) -> Result<(), NokhwaError>; + /// Will open the camera stream with set parameters. This will be called internally if you try and call [`CaptureBackendTrait::get_frame()`] before you call [`CaptureBackendTrait::open_stream()`]. + /// # Errors + /// If the specific backend fails to open the camera (e.g. already taken, busy, doesn't exist anymore) this will error. + fn open_stream(&mut self) -> Result<(), NokhwaError>; + /// Checks if stream if open. If it is, it will return true. + fn is_stream_open(&self) -> bool; + /// Will get a frame from the camera as a Raw RGB image buffer. This will call [`CaptureBackendTrait::open_stream()`] first if you haven't already. + /// # Errors + /// If the backend fails to get the frame (e.g. already taken, busy, doesn't exist anymore) or the decoding fails (e.g. MJPEG -> u8), this will error. fn get_frame(&self) -> Result, Vec>, NokhwaError>; + /// Will get a frame from the camera **without** any processing applied, meaning you will usually get a frame you need to decode yourself. + /// # Errors + /// If the backend fails to get the frame (e.g. already taken, busy, doesn't exist anymore), this will error. + fn get_frame_raw(&self) -> Vec; +} + +/// This is for any backend that allows you to query a camera for its compatible resolutions/fourcc/framerates. +pub trait QueryBackendTrait: CaptureBackendTrait { + /// A hashmap of Framerates mapped to [`Resolution`]s. + fn get_compatible_by_framerate(&self, fourcc: FrameFormat) -> HashMap>; + /// A hashmap of [`Resolution`]s mapped to framerates + fn get_compatible_by_resolution(&self, fourcc: FrameFormat) -> HashMap>; + /// Gets the supported camera formats. + fn get_compatible_fourcc(&self, resolution: Resolution, framerate: u32) -> Option; } pub trait VirtualBackendTrait {} diff --git a/src/error.rs b/src/error.rs index 0c40c37..e2d2275 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,13 +1,19 @@ use thiserror::Error; -#[allow(clippy::clippy::module_name_repetitions)] +#[allow(clippy::module_name_repetitions)] +#[allow(clippy::pub_enum_variant_names)] #[derive(Error, Debug, Clone)] pub enum NokhwaError { #[error("Could not open device: {0}")] CouldntOpenDevice(String), #[error("Could not query device property {property}: {error}")] - CouldntQueryDevice { + CouldntQueryDevice { property: String, error: String }, + #[error("Could not set device property {property} with value {value}: {error}")] + CouldntSetProperty { property: String, + value: String, error: String, - } -} \ No newline at end of file + }, + #[error("Could not open device stream: {0}")] + CouldntOpenStream(String), +} diff --git a/src/lib.rs b/src/lib.rs index 3a81d50..2e3ad44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,15 @@ #![deny(clippy::pedantic)] #![warn(clippy::all)] #![allow(clippy::upper_case_acronyms)] +#![allow(clippy::must_use_candidate)] -mod camera_traits; pub mod backends; -mod utils; mod camera; +mod camera_traits; mod error; -mod query; \ No newline at end of file +mod query; +mod utils; + +pub use camera_traits::*; +pub use error::NokhwaError; +pub use utils::*; diff --git a/src/query.rs b/src/query.rs index e69de29..8b13789 100644 --- a/src/query.rs +++ b/src/query.rs @@ -0,0 +1 @@ + diff --git a/src/utils.rs b/src/utils.rs index 3921909..d7318d8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,14 +1,19 @@ -use std::fmt::Display; -use std::cmp::Ordering; +use std::{cmp::Ordering, fmt::Display}; +/// Describes a frame format (i.e. how the bytes themselves are encoded). Often called `FourCC` + +/// YUYV is a mathmatical color space. You can read more [here.](https://en.wikipedia.org/wiki/YCbCr) + +/// MJPEG is a motion-jpeg compressed frame, it allows for high frame rates. #[derive(Copy, Clone, Debug, PartialEq, Hash)] pub enum FrameFormat { MJPEG, YUYV, } -/// Describes a Resolution. +/// Describes a Resolution. /// This struct consists of a Width and a Height value (x,y). +/// Note that the [`Ord`] implementation of this struct is flipped from highest to lowest. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct Resolution { pub width_x: u32, @@ -58,8 +63,6 @@ impl PartialOrd for Resolution { } impl Ord for Resolution { - // Flip around the order to make it seem the way the user would expect. - // The user would expect a descending list of resolutions (aka highest -> lowest) fn cmp(&self, other: &Self) -> Ordering { match self.x().cmp(&other.x()) { Ordering::Less => Ordering::Less, @@ -69,6 +72,8 @@ impl Ord for Resolution { } } +/// This is a conveinence struct that holds all information about the format of a webcam stream. +/// It consists of a Resolution, `FrameFormat`, and a framerate. #[derive(Copy, Clone, Debug, Hash, PartialEq)] pub struct CameraFormat { resolution: Resolution, @@ -77,6 +82,7 @@ pub struct CameraFormat { } impl CameraFormat { + /// Construct a new [`CameraFormat`] pub fn new(resolution: Resolution, format: FrameFormat, framerate: u32) -> Self { CameraFormat { resolution, @@ -85,6 +91,7 @@ impl CameraFormat { } } + /// [`CameraFormat::new()`], but raw. pub fn new_from(res_x: u32, res_y: u32, format: FrameFormat, fps: u32) -> Self { CameraFormat { resolution: Resolution { @@ -96,27 +103,60 @@ impl CameraFormat { } } - pub fn res(&self) -> Resolution { + /// Get the resolution of the current [`CameraFormat`] + pub fn resoltuion(&self) -> Resolution { self.resolution } + /// Get the width of the resolution of the current [`CameraFormat`] pub fn width(&self) -> u32 { self.resolution.width() } + /// Get the height of the resolution of the current [`CameraFormat`] pub fn height(&self) -> u32 { self.resolution.height() } - pub fn frame_format(&self) -> FrameFormat { - self.format + /// Set the [`CameraFormat`]'s resolution. + pub fn set_resolution(&mut self, resolution: Resolution) { + self.resolution = resolution; } + /// Get the framerate of the current [`CameraFormat`] pub fn framerate(&self) -> u32 { self.framerate } + + /// Set the [`CameraFormat`]'s framerate. + pub fn set_framerate(&mut self, framerate: u32) { + self.framerate = framerate; + } + + /// Get the [`CameraFormat`]'s format. + pub fn format(&self) -> FrameFormat { + self.format + } + + /// Set the [`CameraFormat`]'s format. + pub fn set_format(&mut self, format: FrameFormat) { + self.format = format; + } } +impl Default for CameraFormat { + fn default() -> Self { + CameraFormat { + resolution: Resolution::new(640, 480), + format: FrameFormat::MJPEG, + framerate: 15, + } + } +} + +/// Information about a Camera e.g. its name. +/// `description` amd `misc` may contain backend-specific information. +/// `index` is a camera's index given to it by (usually) the OS usually in the order it is known to the system. #[derive(Clone, Debug, Default, Hash, PartialEq, Eq)] pub struct CameraInfo { human_name: String, @@ -126,13 +166,13 @@ pub struct CameraInfo { } impl CameraInfo { + /// Create a new [`CameraInfo`]. pub fn new(human_name: String, description: String, misc: String, index: usize) -> Self { CameraInfo { human_name, description, misc, index, - } } @@ -189,6 +229,19 @@ impl Ord for CameraInfo { } } +/// The list of known capture backends to the library. +/// +/// AUTO is special - it tells the Camera struct to automatically choose a backend most suited for the current platform. +/// +/// V4L2 - `Video4Linux2`, a linux specific backend. +/// +/// UVC - Universal Video Class (please check [libuvc](https://github.com/libuvc/libuvc)). Platform agnostic, although on linux it needs `sudo` permissions or similar to use. +/// +/// MSMF - Microsoft Media Foundation, Winsows only (replacement for `DirectShow`) +/// +/// `OpenCV` - Uses `OpenCV` to capture. Platform agnostic. +/// +/// FFMPEG - Uses FFMPEG (libavdevice) to capture. Platform agnostic. #[derive(Clone, Copy, Debug, PartialEq)] pub enum CaptureAPIBackend { AUTO,