diff --git a/README.md b/README.md index 5e36b90..ae17483 100644 --- a/README.md +++ b/README.md @@ -20,17 +20,7 @@ Most likely, you will only use functionality provided by the `Camera` struct. If ```rust // set up the Camera -let mut camera = Camera::new( - 0, // index - Some(CameraFormat::new_from(640, 480, FrameFormat::MJPEG, 30)), // format -) -.unwrap(); -// open stream -camera.open_stream().unwrap(); -loop { - let frame = camera.frame().unwrap(); - println!("{}, {}", frame.width(), frame.height()); -} + ``` A command line app made with `nokhwa` can be found in the `examples` folder. @@ -42,20 +32,19 @@ The table below lists current Nokhwa API support. - The `Query-Device` column signifies reading device capabilities - The `Platform` column signifies what Platform this is availible on. - | Backend | Input | Query | Query-Device | Platform | - |----------------------------------------|--------------------|-------------------|--------------------|---------------------| - | Video4Linux(`input-v4l`) | ✅ | ✅ | ✅ | Linux | - | MSMF(`input-msmf`) | ✅ | ✅ | ✅ | Windows | - | AVFoundation(`input-avfoundation`)^^ | ✅ | ✅ | ✅ | Mac | - | libuvc(`input-uvc`) (**DEPRECATED**)^^^| ❌ | ✅ | ❌ | Linux, Windows, Mac | + | Backend | Input | Query | Query-Device | Platform | +-----------------------------------------|----------------------------------------|--------------------|-------------------|--------------------|---------------------| + | Video4Linux(`input-v4l`) | ✅ | ✅ | ✅ | Linux | + | MSMF(`input-msmf`) | ✅ | ✅ | ✅ | Windows | + | AVFoundation(`input-avfoundation`)^^ | ✅ | ✅ | ✅ | Mac | + | libuvc(`input-uvc`) (**DEPRECATED**)^^^ | ❌ | ✅ | ❌ | Linux, Windows, Mac | | OpenCV(`input-opencv`)^ | ✅ | ❌ | ❌ | Linux, Windows, Mac | - | IPCamera(`input-ipcam`/OpenCV)^ | ✅ | ❌ | ❌ | Linux, Windows, Mac | - | GStreamer(`input-gst`)(**DEPRECATED**) | ✅ | ✅ | ✅ | Linux, Windows, Mac | - | JS/WASM(`input-wasm`) | ✅ | ✅ | ✅ | Browser(Web) | + | GStreamer(`input-gst`)(**DEPRECATED**) | ✅ | ✅ | ✅ | Linux, Windows, Mac | + | JS/WASM(`input-wasm`) | ✅ | ✅ | ✅ | Browser(Web) | ✅: Working, 🔮 : Experimental, ❌ : Not Supported, 🚧: Planned/WIP - ^ = No CameraFormat setting support. + ^ = May be bugged. Also supports IP Cameras. ^^ = No FPS setting support. diff --git a/examples/capture/Cargo.toml b/examples/capture/Cargo.toml index e1016f9..906ba41 100644 --- a/examples/capture/Cargo.toml +++ b/examples/capture/Cargo.toml @@ -1,23 +1,24 @@ [package] -name = "capture" -version = "0.1.0" -authors = ["l1npengtul "] +name = "nokhwactl" +version = "0.10.0" +authors = ["l1npengtul ", "The Nokhwa Contributors"] edition = "2018" - +about = "An example CLI program to show off Nokhwa's capabilities." # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] default = ["nokhwa/default"] -input-msmf = ["nokhwa/input-msmf"] -input-v4l = ["nokhwa/input-v4l"] -input-opencv = ["nokhwa/input-opencv"] -input-avfoundation = ["nokhwa/input-avfoundation"] +native = ["nokhwa/input-native"] [dependencies] -clap = "4.0.19" -glium = "0.32.1" -glutin = "0.30.0" -flume = "0.10.9" +ggez = "0.8.1" +flume = "0.10.14" +once_cell = "1.16.0" +image = "0.24.4" + +[dependencies.clap] +version = "4.0.20" +features = ["derive"] # Use these as you need [dependencies.nokhwa] diff --git a/examples/capture/Info.plist b/examples/capture/Info.plist index 94e8d11..4da6056 100644 --- a/examples/capture/Info.plist +++ b/examples/capture/Info.plist @@ -23,7 +23,7 @@ LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright - Copywight © :3 2021 w1npengtuw, the x3 Nyokhwa Contwibutews. Wicensed undew the x3 MPL-2.0. *looks at you* You shouwd have weceived a copy with this softwawe, but if nyot i-it c-can be found at You c-can obtain onye at https://mozilla.org/MPL/2.0/. *huggles tightly* + Copywight © :3 2022 w1npengtuw, the x3 Nyokhwa Contwibutews. Wicensed undew the x3 APL-2.0. *looks at you* You shouwd have weceived a copy with this softwawe, but if nyot i-it c-can be found at You c-can obtain onye at https://www.apache.org/licenses/LICENSE-2.0.html. *huggles tightly* NSCameraUsageDescription we *notices buldge* wiww s-s-steaw aww youw webcam and pewsonyaw infowmation undew the x3 guise of b-being a "nyokhwa exampwe c-captuwe app" and a "devewopew toow" <3 :3 *nyuzzwes yuwu* NSMainNibFile diff --git a/examples/capture/src/main.rs b/examples/capture/src/main.rs index 37062b4..9e2060b 100644 --- a/examples/capture/src/main.rs +++ b/examples/capture/src/main.rs @@ -16,4 +16,339 @@ // Some assembly required. For developers 7 and up. -fn main() {} +use clap::{Parser, Subcommand}; +use flume::{Receiver, Sender}; +use ggez::{ + event::{run, EventHandler}, + graphics::{Canvas, Image}, + Context, ContextBuilder, GameError, +}; +use nokhwa::{ + native_api_backend, + pixel_format::RgbFormat, + query, + utils::{ + frame_formats, CameraFormat, CameraIndex, FrameFormat, RequestedFormat, + RequestedFormatType, Resolution, + }, + Buffer, CallbackCamera, Camera, +}; +use std::sync::Arc; + +struct CaptureState { + sender: Arc>, + receiver: Arc>, + buffer: Vec, + camera: CallbackCamera, +} + +impl EventHandler for CaptureState { + fn update(&mut self, _ctx: &mut Context) -> Result<(), GameError> { + Ok(()) + } + + fn draw(&mut self, ctx: &mut Context) -> Result<(), GameError> { + self.receiver + .recv() + .map_err(|why| GameError::RenderError(why.to_string()))? + .decode_image_to_buffer(&mut self.buffer)?; + let image = Image::from_bytes(ctx, &self.buffer)?; + let canvas = Canvas::from_image(ctx, image, None); + canvas.finish(ctx)?; + Ok(()) + } +} + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Option, +} + +enum IndexKind { + String(String), + Index(u32), +} + +#[derive(Subcommand)] +enum Commands { + ListDevices, + ListProperties { + device: Option, + kind: PropertyKind, + }, + Stream { + device: Option, + display: bool, + requested: Option, + }, + Single { + device: Option, + save: Option, + requested: Option, + }, +} + +struct RequestedCliFormat { + format_type: String, + format_option: Option, +} + +enum PropertyKind { + All, + Controls, + CompatibleFormats, +} + +fn main() { + nokhwa::nokhwa_initialize(|_| { + println!("Nokhwa Initalized."); + nokhwa_main() + }) +} + +fn nokhwa_main() { + let cli = Cli::parse(); + + let cmd = match &cli.command { + Some(cmd) => cmd, + None => { + println!("Unknown command \"\". Do --help for info.") + } + }; + + match cmd { + Commands::ListDevices => { + let backend = native_api_backend().unwrap(); + let devices = query(backend).unwrap(); + println!("There are {} available cameras.", devices.len()); + for device in devices { + println!("{device}"); + } + } + Commands::ListProperties { device, kind } => { + let index = match device.unwrap_or(IndexKind::Index(0)) { + IndexKind::String(s) => CameraIndex::String(s), + IndexKind::Index(i) => CameraIndex::Index(i), + }; + let mut camera = Camera::new( + index, + RequestedFormat::new::(RequestedFormatType::None), + ) + .unwrap(); + match kind { + PropertyKind::All => { + camera_print_controls(&camera); + camera_compatible_formats(&mut camera); + } + PropertyKind::Controls => { + camera_print_controls(&camera); + } + PropertyKind::CompatibleFormats => { + camera_compatible_formats(&mut camera); + } + } + } + Commands::Stream { + device, + display, + requested, + } => { + let requested = match requested { + Some(req) => match req.format_type.as_str() { + "HighestResolutionAbs" => { + RequestedFormat::new::(RequestedFormatType::HighestResolutionAbs) + } + "HighestFrameRateAbs" => { + RequestedFormat::new::(RequestedFormatType::HighestFrameRateAbs) + } + "HighestResolution" => { + let values = req.format_option.unwrap().split(",").collect::>(); + let x = values[0].parse::().unwrap(); + let y = values[1].parse::().unwrap(); + let resolution = Resolution::new(x, y); + + RequestedFormat::new::(RequestedFormatType::HighestResolution( + resolution, + )) + } + "HighestFrameRate" => { + let fps = req.format_option.unwrap().parse::().unwrap(); + + RequestedFormat::new::(RequestedFormatType::HighestFrameRate( + fps, + )) + } + "Exact" => { + let values = req.format_option.unwrap().split(",").collect::>(); + let x = values[0].parse::().unwrap(); + let y = values[1].parse::().unwrap(); + let fps = values[2].parse::().unwrap(); + let fourcc = values[3].parse::().unwrap(); + + let resolution = Resolution::new(x, y); + let camera_format = CameraFormat::new(resolution, fourcc, fps); + RequestedFormat::new::(RequestedFormatType::Exact(camera_format)) + } + "Closest" => { + let values = req.format_option.unwrap().split(",").collect::>(); + let x = values[0].parse::().unwrap(); + let y = values[1].parse::().unwrap(); + let fps = values[2].parse::().unwrap(); + let fourcc = values[3].parse::().unwrap(); + + let resolution = Resolution::new(x, y); + let camera_format = CameraFormat::new(resolution, fourcc, fps); + RequestedFormat::new::(RequestedFormatType::Closest( + camera_format, + )) + } + "None" => RequestedFormat::new::(RequestedFormatType::None), + _ => { + println!("Expected HighestResolutionAbs, HighestFrameRateAbs, HighestResolution, HighestFrameRate, Exact, Closest, or None"); + return; + } + }, + None => RequestedFormat::new::(RequestedFormatType::None), + }; + + let index = match device.unwrap_or(IndexKind::Index(0)) { + IndexKind::String(s) => CameraIndex::String(s), + IndexKind::Index(i) => CameraIndex::Index(i), + }; + + if display { + let (sender, receiver) = flume::unbounded(); + let (sender, receiver) = (Arc::new(sender), Arc::new(receiver)); + let sender_clone = sender.clone(); + + let mut camera = CallbackCamera::new(index, requested, move |buf| { + sender_clone.send(buf).expect("Error sending frame!!!!"); + }) + .unwrap(); + + let camera_info = camera.info().unwrap().clone(); + + camera.open_stream().unwrap(); + + let state = CaptureState { + sender, + receiver, + buffer: Vec::with_capacity(3840 * 2160 * 3), + camera, + }; + + let cb = ContextBuilder::new(&camera_info.human_name(), "Nokhwa"); + let (ctx, el) = cb.build().unwrap(); + run(ctx, el, state) + } else { + let mut cb = CallbackCamera::new(index, requested, |buf| { + println!("Captured frame of size {}", buf.buffer().len()); + }) + .unwrap(); + + cb.open_stream().unwrap(); + } + } + Commands::Single { + device, + save, + requested, + } => { + let index = match device.unwrap_or(IndexKind::Index(0)) { + IndexKind::String(s) => CameraIndex::String(s), + IndexKind::Index(i) => CameraIndex::Index(i), + }; + let requested = match requested { + Some(req) => match req.format_type.as_str() { + "HighestResolutionAbs" => { + RequestedFormat::new::(RequestedFormatType::HighestResolutionAbs) + } + "HighestFrameRateAbs" => { + RequestedFormat::new::(RequestedFormatType::HighestFrameRateAbs) + } + "HighestResolution" => { + let values = req.format_option.unwrap().split(",").collect::>(); + let x = values[0].parse::().unwrap(); + let y = values[1].parse::().unwrap(); + let resolution = Resolution::new(x, y); + + RequestedFormat::new::(RequestedFormatType::HighestResolution( + resolution, + )) + } + "HighestFrameRate" => { + let fps = req.format_option.unwrap().parse::().unwrap(); + + RequestedFormat::new::(RequestedFormatType::HighestFrameRate( + fps, + )) + } + "Exact" => { + let values = req.format_option.unwrap().split(",").collect::>(); + let x = values[0].parse::().unwrap(); + let y = values[1].parse::().unwrap(); + let fps = values[2].parse::().unwrap(); + let fourcc = values[3].parse::().unwrap(); + + let resolution = Resolution::new(x, y); + let camera_format = CameraFormat::new(resolution, fourcc, fps); + RequestedFormat::new::(RequestedFormatType::Exact(camera_format)) + } + "Closest" => { + let values = req.format_option.unwrap().split(",").collect::>(); + let x = values[0].parse::().unwrap(); + let y = values[1].parse::().unwrap(); + let fps = values[2].parse::().unwrap(); + let fourcc = values[3].parse::().unwrap(); + + let resolution = Resolution::new(x, y); + let camera_format = CameraFormat::new(resolution, fourcc, fps); + RequestedFormat::new::(RequestedFormatType::Closest( + camera_format, + )) + } + "None" => RequestedFormat::new::(RequestedFormatType::None), + _ => { + println!("Expected HighestResolutionAbs, HighestFrameRateAbs, HighestResolution, HighestFrameRate, Exact, Closest, or None"); + return; + } + }, + None => RequestedFormat::new::(RequestedFormatType::None), + }; + + let mut camera = Camera::new(index, requested).unwrap(); + + let frame = camera.frame().unwrap(); + println!("Captured Single Frame of {}", frame.buffer().len()); + let decoded = frame.decode_image::().unwrap(); + println!("Decoded Frame of {}", decoded.len()); + + if let Some(path) = save { + println!("Saving to {path}"); + decoded.save(path).unwrap(); + } + } + } +} + +fn camera_print_controls(cam: &Camera) { + let ctrls = cam.camera_controls().unwrap(); + let index = cam.index(); + println!("Controls for camera {index}"); + for ctrl in ctrls { + println!("{ctrl}") + } +} + +fn camera_compatible_formats(cam: &mut Camera) { + for ffmt in frame_formats() { + if let Ok(compatible) = cam.compatible_list_by_resolution(*ffmt) { + for (resolution, fps) in compatible { + println!("{ffmt}:"); + println!(" {resolution}: {fps:?}"); + } + } + } +} diff --git a/nokhwa-core/src/types.rs b/nokhwa-core/src/types.rs index f0060bb..2389b9e 100644 --- a/nokhwa-core/src/types.rs +++ b/nokhwa-core/src/types.rs @@ -1,11 +1,11 @@ -use crate::error::NokhwaError; -use crate::pixel_format::FormatDecoder; +use crate::{error::NokhwaError, pixel_format::FormatDecoder}; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; use std::{ borrow::Borrow, cmp::Ordering, fmt::{Display, Formatter}, + str::FromStr, }; /// Tells the init function what camera format to pick. @@ -21,8 +21,8 @@ use std::{ pub enum RequestedFormatType { HighestResolutionAbs, HighestFrameRateAbs, - HighestResolution(u32), - HighestFrameRate(Resolution), + HighestResolution(Resolution), + HighestFrameRate(u32), Exact(CameraFormat), Closest(CameraFormat), None, @@ -112,23 +112,7 @@ impl RequestedFormat<'_> { format_framerates.sort_by_key(CameraFormat::resolution); format_framerates.last().copied() } - RequestedFormatType::HighestResolution(fps) => { - let mut formats = all_formats - .iter() - .filter(|x| x.frame_rate == fps) - .copied() - .collect::>(); - formats.sort_by(|a, b| a.resolution.cmp(&b.resolution)); - let highest_res = match formats.last() { - Some(cf) => cf.resolution, - None => return None, - }; - formats - .into_iter() - .filter(|x| x.resolution() == highest_res) - .last() - } - RequestedFormatType::HighestFrameRate(res) => { + RequestedFormatType::HighestResolution(res) => { let mut formats = all_formats .iter() .filter(|x| x.resolution == res) @@ -144,6 +128,22 @@ impl RequestedFormat<'_> { .filter(|x| x.frame_rate == highest_fps) .last() } + RequestedFormatType::HighestFrameRate(fps) => { + let mut formats = all_formats + .iter() + .filter(|x| x.frame_rate == fps) + .copied() + .collect::>(); + formats.sort_by(|a, b| a.resolution.cmp(&b.resolution)); + let highest_res = match formats.last() { + Some(cf) => cf.resolution, + None => return None, + }; + formats + .into_iter() + .filter(|x| x.resolution() == highest_res) + .last() + } RequestedFormatType::Exact(fmt) => { if self.wanted_decoder.contains(&fmt.format()) { Some(fmt) @@ -305,7 +305,7 @@ impl Display for FrameFormat { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { FrameFormat::MJPEG => { - write!(f, "MJPG") + write!(f, "MJPEG") } FrameFormat::YUYV => { write!(f, "YUYV") @@ -322,7 +322,23 @@ impl Display for FrameFormat { } } } +impl FromStr for FrameFormat { + type Err = NokhwaError; + fn from_str(s: &str) -> Result { + match s.as_ref() { + "MJPEG" => Ok(FrameFormat::MJPEG), + "YUYV" => Ok(FrameFormat::YUYV), + "GRAY" => Ok(FrameFormat::GRAY), + "RAWRGB" => Ok(FrameFormat::RAWRGB), + "NV12" => Ok(FrameFormat::NV12), + _ => Err(NokhwaError::StructureError { + structure: "FrameFormat".to_string(), + error: format!("No match for {s}"), + }), + } + } +} #[must_use] pub const fn frame_formats() -> &'static [FrameFormat] { &[ diff --git a/src/query.rs b/src/query.rs index f1e85be..f95d58f 100644 --- a/src/query.rs +++ b/src/query.rs @@ -19,6 +19,16 @@ use nokhwa_core::{ types::{ApiBackend, CameraInfo}, }; +/// Gets the native [`ApiBackend`] +pub fn native_api_backend() -> Option { + match std::env::consts::OS { + "linux" => Some(ApiBackend::Video4Linux), + "macos" | "ios" => Some(ApiBackend::AVFoundation), + "windows" => Some(ApiBackend::MediaFoundation), + _ => None, + } +} + // TODO: Update as this goes /// Query the system for a list of available devices. Please refer to the API Backends that support `Query`)
/// Usually the order goes Native -> UVC -> Gstreamer. @@ -42,7 +52,7 @@ pub fn query(api: ApiBackend) -> Result, NokhwaError> { } else if cfg!(feature = "input-opencv") { query(ApiBackend::OpenCv) } else { - dbg!("Error: No suitable Backends availible. Perhaps you meant to enable one of the backends such as `input-v4l`? (Please read the docs.)"); + dbg!("Error: No suitable Backends available. Perhaps you meant to enable one of the backends such as `input-v4l`? (Please read the docs.)"); Err(NokhwaError::UnsupportedOperationError(ApiBackend::Auto)) } } @@ -52,7 +62,7 @@ pub fn query(api: ApiBackend) -> Result, NokhwaError> { } else if cfg!(feature = "input-opencv") { query(ApiBackend::OpenCv) } else { - dbg!("Error: No suitable Backends availible. Perhaps you meant to enable one of the backends such as `input-msmf`? (Please read the docs.)"); + dbg!("Error: No suitable Backends available. Perhaps you meant to enable one of the backends such as `input-msmf`? (Please read the docs.)"); Err(NokhwaError::UnsupportedOperationError(ApiBackend::Auto)) } } @@ -62,7 +72,7 @@ pub fn query(api: ApiBackend) -> Result, NokhwaError> { } else if cfg!(feature = "input-opencv") { query(ApiBackend::OpenCv) } else { - dbg!("Error: No suitable Backends availible. Perhaps you meant to enable one of the backends such as `input-avfoundation`? (Please read the docs.)"); + dbg!("Error: No suitable Backends available. Perhaps you meant to enable one of the backends such as `input-avfoundation`? (Please read the docs.)"); Err(NokhwaError::UnsupportedOperationError(ApiBackend::Auto)) } } @@ -70,12 +80,12 @@ pub fn query(api: ApiBackend) -> Result, NokhwaError> { if cfg!(feature = "input-avfoundation") { query(ApiBackend::AVFoundation) } else { - dbg!("Error: No suitable Backends availible. Perhaps you meant to enable one of the backends such as `input-avfoundation`? (Please read the docs.)"); + dbg!("Error: No suitable Backends available. Perhaps you meant to enable one of the backends such as `input-avfoundation`? (Please read the docs.)"); Err(NokhwaError::UnsupportedOperationError(ApiBackend::Auto)) } } _ => { - dbg!("Error: No suitable Backends availible. You are on an unsupported platform."); + dbg!("Error: No suitable Backends available. You are on an unsupported platform."); Err(NokhwaError::NotImplementedError("Bad Platform".to_string())) } } @@ -100,6 +110,7 @@ pub fn query(api: ApiBackend) -> Result, NokhwaError> { #[allow(clippy::unnecessary_wraps)] #[allow(clippy::cast_possible_truncation)] fn query_v4l() -> Result, NokhwaError> { + use nokhwa_core::types::CameraIndex; Ok({ let camera_info: Vec = v4l::context::enum_devices() .iter()