diff --git a/Cargo.toml b/Cargo.toml index 27cc9b5..270aed1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,10 @@ [package] name = "nokhwa" -version = "0.9.1" +version = "0.10.0" authors = ["l1npengtul "] -edition = "2018" +edition = "2021" description = "A Simple-to-use, cross-platform Rust Webcam Capture Library" keywords = ["camera", "webcam", "capture", "cross-platform"] -resolver = "2" license = "Apache-2.0" repository = "https://github.com/l1npengtul/nokhwa" @@ -15,8 +14,7 @@ repository = "https://github.com/l1npengtul/nokhwa" crate-type = ["cdylib", "rlib"] [features] -default = ["decoding", "flume"] -decoding = ["mozjpeg"] +default = ["flume"] input-v4l = ["v4l", "v4l2-sys-mit"] input-msmf = ["nokhwa-bindings-windows"] input-avfoundation = ["nokhwa-bindings-macos"] @@ -24,7 +22,7 @@ input-uvc = ["uvc", "uvc/vendor", "ouroboros", "usb_enumeration"] input-opencv = ["opencv", "opencv/clang-runtime"] input-ipcam = ["input-opencv"] input-gst = ["gstreamer", "glib", "gstreamer-app", "gstreamer-video", "regex"] -input-jscam = ["web-sys", "js-sys", "wasm-bindgen-futures", "wasm-bindgen"] +input-jscam = ["web-sys", "js-sys", "wasm-bindgen-futures", "wasm-bindgen", "wasm-rs-async-executor"] output-wgpu = ["wgpu"] output-wasm = ["input-jscam"] output-threaded = ["parking_lot"] @@ -37,15 +35,12 @@ test-fail-warning = [] [dependencies] thiserror = "1.0.26" paste = "1.0.5" +mozjpeg = "0.8.24" [dependencies.flume] version = "0.10.8" optional = true -[target.'cfg(not(target_family = "wasm"))'.dependencies.mozjpeg] -version = "0.8.24" -optional = true - [dependencies.image] version = "^0.23" default-features = false @@ -140,6 +135,10 @@ optional = true version = "^0.4" optional = true +[dependencies.wasm-rs-async-executor] +version = "^0.9" +optional = true + [dependencies.wee_alloc] version = "0.4.5" optional = true diff --git a/nokhwa-bindings-windows/Cargo.toml b/nokhwa-bindings-windows/Cargo.toml index 5e73387..588bf86 100644 --- a/nokhwa-bindings-windows/Cargo.toml +++ b/nokhwa-bindings-windows/Cargo.toml @@ -11,11 +11,11 @@ keywords = ["media-foundation", "windows", "capture", "webcam"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -thiserror = "1.0.26" +thiserror = "^1.0" [target.'cfg(all(target_os = "windows", windows))'.dependencies] -windows = "0.21.1" +windows = "^0.28" lazy_static = "1.4.0" [target.'cfg(all(target_os = "windows", windows))'.build-dependencies.windows] -version = "0.21.1" +version = "^0.28" diff --git a/src/backends/capture/avfoundation.rs b/src/backends/capture/avfoundation.rs index 1374a6f..6d75819 100644 --- a/src/backends/capture/avfoundation.rs +++ b/src/backends/capture/avfoundation.rs @@ -15,15 +15,16 @@ */ use crate::{ - mjpeg_to_rgb888, yuyv422_to_rgb888, CameraControl, CameraFormat, CameraInfo, CaptureAPIBackend, - CaptureBackendTrait, FrameFormat, KnownCameraControls, NokhwaError, Resolution, + mjpeg_to_rgb888, yuyv422_to_rgb888, CameraControl, CameraFormat, CameraIndex, CameraInfo, + CaptureAPIBackend, CaptureBackendTrait, FrameFormat, KnownCameraControls, NokhwaError, + Resolution, }; use image::{ImageBuffer, Rgb}; use nokhwa_bindings_macos::avfoundation::{ query_avfoundation, AVCaptureDevice, AVCaptureDeviceInput, AVCaptureSession, AVCaptureVideoCallback, AVCaptureVideoDataOutput, AVFourCC, }; -use std::{any::Any, borrow::Cow, collections::HashMap}; +use std::{any::Any, borrow::Borrow, borrow::Cow, collections::HashMap, ops::Deref}; /// The backend struct that interfaces with V4L2. /// To see what this does, please see [`CaptureBackendTrait`]. @@ -33,28 +34,33 @@ use std::{any::Any, borrow::Cow, collections::HashMap}; /// - This only works on 64 bit platforms. /// - FPS adjustment does not work. #[cfg_attr(feature = "docs-features", doc(cfg(feature = "input-avfoundation")))] -pub struct AVFoundationCaptureDevice { +pub struct AVFoundationCaptureDevice<'a> { device: AVCaptureDevice, dev_input: Option, session: Option, data_out: Option, data_collect: Option, - info: CameraInfo, + info: CameraInfo<'a>, format: CameraFormat, } -impl AVFoundationCaptureDevice { +impl<'a> AVFoundationCaptureDevice<'a> { /// Creates a new capture device using the `AVFoundation` backend. Indexes are gives to devices by the OS, and usually numbered by order of discovery. /// /// If `camera_format` is `None`, it will be spawned with with 640x480@15 FPS, MJPEG [`CameraFormat`] default. /// # Errors - /// This function will error if the camera is currently busy or if `AVFoundation` can't read device information, or permission was not given by the user. - pub fn new(index: usize, camera_format: Option) -> Result { + /// This function will error if the camera is currently busy or if `AVFoundation` can't read device information, or permission was not given by the user. This will also error if the index is a [`CameraIndex::String`] that cannot be parsed into a `usize`. + pub fn new( + index: CameraIndex<'a>, + camera_format: Option, + ) -> Result { let camera_format = match camera_format { Some(fmt) => fmt, None => CameraFormat::default(), }; + let index = index.index_num()? as usize; + let device_descriptor: CameraInfo = match query_avfoundation()?.into_iter().nth(index) { Some(descriptor) => descriptor.into(), None => { @@ -85,7 +91,7 @@ impl AVFoundationCaptureDevice { /// # Errors /// This function will error if the camera is currently busy or if `AVFoundation` can't read device information, or permission was not given by the user. pub fn new_with( - index: usize, + index: CameraIndex<'a>, width: u32, height: u32, fps: u32, @@ -223,7 +229,7 @@ impl CaptureBackendTrait for AVFoundationCaptureDevice { let session = AVCaptureSession::new(); session.begin_configuration(); session.add_input(&input)?; - let callback = AVCaptureVideoCallback::new(self.info.index()); + let callback = AVCaptureVideoCallback::new(self.info.index_num()? as usize); let output = AVCaptureVideoDataOutput::new(); output.add_delegate(&callback)?; session.add_output(&output)?; @@ -293,8 +299,8 @@ impl CaptureBackendTrait for AVFoundationCaptureDevice { Some(collector) => { let data = collector.frame_to_slice()?; let data = match data.1 { - AVFourCC::YUV2 => Cow::from(yuyv422_to_rgb888(&data.0)?), - AVFourCC::MJPEG => Cow::from(mjpeg_to_rgb888(&data.0)?), + AVFourCC::YUV2 => Cow::from(yuyv422_to_rgb888(data.0.borrow())), + AVFourCC::MJPEG => Cow::from(mjpeg_to_rgb888(data.0.borrow())), }; Ok(data) } diff --git a/src/backends/capture/browser_backend.rs b/src/backends/capture/browser_backend.rs new file mode 100644 index 0000000..08db00b --- /dev/null +++ b/src/backends/capture/browser_backend.rs @@ -0,0 +1,266 @@ +use crate::{ + js_camera::JSCameraResizeMode, + js_camera::{query_js_cameras, JSCameraConstraintsBuilder}, + CameraControl, CameraFormat, CameraIndex, CameraInfo, CaptureAPIBackend, CaptureBackendTrait, + FrameFormat, JSCamera, KnownCameraControls, NokhwaError, Resolution, +}; +use image::{ImageBuffer, Rgb}; +use std::{any::Any, borrow::Cow, collections::HashMap}; + +/// Captures using the Browser API. This internally wraps [`JSCamera`]. +/// +/// # Quirks +/// - FourCC setting is ignored +/// - Cannot get compatible resolution(s). +/// - CameraControl(s) are not supported. +/// - All frame capture is done by creating (then destorying) a canvas on the DOM. +/// - Many methods are blocking on user input. +pub struct BrowserCaptureDevice<'a> { + camera: JSCamera, + info: CameraInfo<'a>, +} + +impl<'a> BrowserCaptureDevice<'a> { + // WARN: blocking on pass integer for index + /// Creates a new camera from an [`CameraIndex`]. It can take [`CameraIndex::Index`] or [`CameraIndex::String`] (NOTE: blocks on [`CameraIndex::Index`]) + /// + /// # Errors + /// If the device is not found, browser not supported, or camera is over-constrained this will error. + pub fn new(index: CameraIndex<'a>, cam_fmt: Option) -> Result { + let (group_id, device_id) = match &index { + CameraIndex::Index(i) => { + let query_devices = + wasm_rs_async_executor::single_threaded::block_on(query_js_cameras())?; + match query_devices.into_iter().nth(*i as usize) { + Some(info) => { + let ids = info + .to_string() + .split(" ") + .map(ToString::to_string) + .collect::>(); + match (ids.get(0), ids.get(1)) { + (Some(group_id), Some(device_id)) => { + (group_id.clone(), device_id.clone()) + } + (_, _) => { + return Err(NokhwaError::OpenDeviceError( + "Invalid Index".to_string(), + index.to_string(), + )) + } + } + } + None => { + return Err(NokhwaError::OpenDeviceError( + "Device not found".to_string(), + index.to_string(), + )) + } + } + } + CameraIndex::String(id) => { + let ids = id + .to_string() + .split(" ") + .map(ToString::to_string) + .collect::>(); + match (ids.get(0), ids.get(1)) { + (Some(group_id), Some(device_id)) => (group_id.clone(), device_id.clone()), + (_, _) => { + return Err(NokhwaError::OpenDeviceError( + "Invalid Index".to_string(), + index.to_string(), + )) + } + } + } + }; + + let camera_format = cam_fmt.unwrap_or_default(); + + let constraints = JSCameraConstraintsBuilder::new() + .frame_rate(camera_format.frame_rate()) + .resolution(camera_format.resolution()) + .aspect_ratio(camera_format.width() as f64 / camera_format.height() as f64) + .group_id(&group_id) + .group_id_exact(true) + .device_id(&device_id) + .device_id_exact(true) + .resize_mode(JSCameraResizeMode::Any) + .resize_mode(JSCameraResizeMode::Any) + .build(); + + let camera = wasm_rs_async_executor::single_threaded::block_on(JSCamera::new(constraints))?; + + let info = (|| { + let cameras = wasm_rs_async_executor::single_threaded::block_on(query_js_cameras())?; + let giddid = format!("{} {}", group_id, device_id); + for cam in cameras { + if cam.misc() == giddid { + return Ok(cam); + } + } + Ok(CameraInfo::new( + "".to_string(), + "videoinput".to_string(), + giddid, + index, + )) + })()?; + Ok(BrowserCaptureDevice { camera, info }) + } + + /// Creates a new camera from an [`CameraIndex`] and raw parts. It can take [`CameraIndex::Index`] or [`CameraIndex::String`] (NOTE: blocks on [`CameraIndex::Index`]) + /// + /// # Errors + /// If the device is not found, browser not supported, or camera is over-constrained this will error. + pub fn new_with( + index: CameraIndex<'a>, + width: u32, + height: u32, + fps: u32, + fourcc: FrameFormat, + ) -> Result { + Self::new( + index, + Some(CameraFormat::new( + Resolution::new(width, height), + fourcc, + fps, + )), + ) + } +} + +impl<'a> CaptureBackendTrait for BrowserCaptureDevice<'a> { + fn backend(&self) -> CaptureAPIBackend { + CaptureAPIBackend::Browser + } + + fn camera_info(&self) -> &CameraInfo { + &self.info + } + + fn camera_format(&self) -> CameraFormat { + CameraFormat::new( + self.camera.resolution(), + FrameFormat::MJPEG, + self.camera.constraints().frame_rate(), + ) + } + + fn set_camera_format(&mut self, new_fmt: CameraFormat) -> Result<(), NokhwaError> { + let current_constraints = self.camera.constraints(); + + let new_constraints = JSCameraConstraintsBuilder::new() + .resolution(new_fmt.resolution()) + .aspect_ratio(new_fmt.width() as f64 / new_fmt.height() as f64) + .frame_rate(new_fmt.frame_rate()) + .group_id(¤t_constraints.group_id()) + .device_id(¤t_constraints.device_id()) + .resize_mode(JSCameraResizeMode::Any) + .build(); + + let _ = self.camera.set_constraints(new_constraints); + match self.camera.apply_constraints() { + Ok(_) => Ok(()), + Err(why) => { + let _ = self.camera.set_constraints(current_constraints); // swallow errors - revert + Err(why) + } + } + } + + fn compatible_list_by_resolution( + &mut self, + _: FrameFormat, + ) -> Result>, NokhwaError> { + Err(NokhwaError::NotImplementedError( + "Not Implemented".to_string(), + )) + } + + fn compatible_fourcc(&mut self) -> Result, NokhwaError> { + Ok(vec![FrameFormat::MJPEG, FrameFormat::YUYV]) + } + + fn resolution(&self) -> Resolution { + self.camera.resolution() + } + + fn set_resolution(&mut self, new_res: Resolution) -> Result<(), NokhwaError> { + let mut current_format = self.camera_format(); + current_format.set_resolution(new_res); + self.set_camera_format(current_format) + } + + fn frame_rate(&self) -> u32 { + self.camera.constraints().frame_rate() + } + + fn set_frame_rate(&mut self, new_fps: u32) -> Result<(), NokhwaError> { + let mut current_format = self.camera_format(); + current_format.set_frame_rate(new_fps); + self.set_camera_format(current_format) + } + + fn frame_format(&self) -> FrameFormat { + FrameFormat::MJPEG + } + + fn set_frame_format(&mut self, _: FrameFormat) -> Result<(), NokhwaError> { + Ok(()) + } + + fn supported_camera_controls(&self) -> Result, NokhwaError> { + Ok(vec![]) + } + + fn camera_control(&self, _: KnownCameraControls) -> Result { + Err(NokhwaError::NotImplementedError( + "Not Implemented".to_string(), + )) + } + + fn set_camera_control(&mut self, _: CameraControl) -> Result<(), NokhwaError> { + Err(NokhwaError::NotImplementedError( + "Not Implemented".to_string(), + )) + } + + fn raw_supported_camera_controls(&self) -> Result>, NokhwaError> { + Ok(vec![]) + } + + fn raw_camera_control(&self, _: &dyn Any) -> Result, NokhwaError> { + Err(NokhwaError::NotImplementedError( + "Not Implemented".to_string(), + )) + } + + fn set_raw_camera_control(&mut self, _: &dyn Any, _: &dyn Any) -> Result<(), NokhwaError> { + Err(NokhwaError::NotImplementedError( + "Not Implemented".to_string(), + )) + } + + fn open_stream(&mut self) -> Result<(), NokhwaError> { + Ok(()) + } + + fn is_stream_open(&self) -> bool { + self.camera.is_open() + } + + fn frame(&mut self) -> Result, Vec>, NokhwaError> { + self.camera.frame() + } + + fn frame_raw(&mut self) -> Result, NokhwaError> { + self.camera.frame_raw() + } + + fn stop_stream(&mut self) -> Result<(), NokhwaError> { + self.camera.stop_all() + } +} diff --git a/src/backends/capture/gst_backend.rs b/src/backends/capture/gst_backend.rs index 591d582..4c44e6d 100644 --- a/src/backends/capture/gst_backend.rs +++ b/src/backends/capture/gst_backend.rs @@ -15,8 +15,9 @@ */ use crate::{ - mjpeg_to_rgb888, yuyv422_to_rgb888, CameraControl, CameraFormat, CameraInfo, CaptureAPIBackend, - CaptureBackendTrait, FrameFormat, KnownCameraControls, NokhwaError, Resolution, + mjpeg_to_rgb888, yuyv422_to_rgb888, CameraControl, CameraFormat, CameraIndex, CameraInfo, + CaptureAPIBackend, CaptureBackendTrait, FrameFormat, KnownCameraControls, NokhwaError, + Resolution, }; use flume::Receiver; use glib::Quark; @@ -42,29 +43,31 @@ type PipelineGenRet = (Element, AppSink, Receiver, Vec>> /// - `Drop`-ing this may cause a `panic`. /// - Setting controls is not supported. #[cfg_attr(feature = "docs-features", doc(cfg(feature = "input-gst")))] -pub struct GStreamerCaptureDevice { +pub struct GStreamerCaptureDevice<'a> { pipeline: Element, app_sink: AppSink, camera_format: CameraFormat, - camera_info: CameraInfo, + camera_info: CameraInfo<'a>, receiver: Receiver, Vec>>, caps: Option, } -impl GStreamerCaptureDevice { +impl<'a> GStreamerCaptureDevice<'a> { /// Creates a new capture device using the `GStreamer` backend. Indexes are gives to devices by the OS, and usually numbered by order of discovery. /// /// `GStreamer` uses `v4l2src` on linux, `ksvideosrc` on windows, and `autovideosrc` on mac. /// /// If `camera_format` is `None`, it will be spawned with with 640x480@15 FPS, MJPEG [`CameraFormat`] default. /// # Errors - /// This function will error if the camera is currently busy or if `GStreamer` can't read device information. - pub fn new(index: usize, cam_fmt: Option) -> Result { + /// This function will error if the camera is currently busy or if `GStreamer` can't read device information. This will also error if the index is a [`CameraIndex::String`] that cannot be parsed into a `usize`. + pub fn new(index: CameraIndex<'a>, cam_fmt: Option) -> Result { let camera_format = match cam_fmt { Some(fmt) => fmt, None => CameraFormat::default(), }; + let index = index.as_index()?; + if let Err(why) = gstreamer::init() { return Err(NokhwaError::InitializeError { backend: CaptureAPIBackend::GStreamer, @@ -99,7 +102,7 @@ impl GStreamerCaptureDevice { error: format!("Not started, {}", why), }); } - let device = match device_monitor.devices().get(index) { + let device = match device_monitor.devices().get(index as usize) { Some(dev) => dev.clone(), None => { return Err(NokhwaError::OpenDeviceError( @@ -115,13 +118,13 @@ impl GStreamerCaptureDevice { DeviceExt::display_name(&device).to_string(), DeviceExt::device_class(&device).to_string(), "".to_string(), - index, + CameraIndex::Index(index), ), caps, ) }; - let (pipeline, app_sink, receiver) = generate_pipeline(camera_format, index)?; + let (pipeline, app_sink, receiver) = generate_pipeline(camera_format, index as usize)?; Ok(GStreamerCaptureDevice { pipeline, @@ -138,13 +141,18 @@ impl GStreamerCaptureDevice { /// `GStreamer` uses `v4l2src` on linux, `ksvideosrc` on windows, and `autovideosrc` on mac. /// # Errors /// This function will error if the camera is currently busy or if `GStreamer` can't read device information. - pub fn new_with(index: usize, width: u32, height: u32, fps: u32) -> Result { + pub fn new_with( + index: CameraIndex<'a>, + width: u32, + height: u32, + fps: u32, + ) -> Result { let cam_fmt = CameraFormat::new(Resolution::new(width, height), FrameFormat::MJPEG, fps); GStreamerCaptureDevice::new(index, Some(cam_fmt)) } } -impl CaptureBackendTrait for GStreamerCaptureDevice { +impl<'a> CaptureBackendTrait for GStreamerCaptureDevice<'a> { fn backend(&self) -> CaptureAPIBackend { CaptureAPIBackend::GStreamer } @@ -163,7 +171,8 @@ impl CaptureBackendTrait for GStreamerCaptureDevice { self.stop_stream()?; reopen = true; } - let (pipeline, app_sink, receiver) = generate_pipeline(new_fmt, self.camera_info.index())?; + let (pipeline, app_sink, receiver) = + generate_pipeline(new_fmt, self.camera_info.index_num()? as usize)?; self.pipeline = pipeline; self.app_sink = app_sink; self.receiver = receiver; @@ -562,9 +571,9 @@ impl CaptureBackendTrait for GStreamerCaptureDevice { } } -impl Drop for GStreamerCaptureDevice { +impl<'a> Drop for GStreamerCaptureDevice<'a> { fn drop(&mut self) { - self.pipeline.set_state(State::Null).unwrap(); + let _ = self.pipeline.set_state(State::Null); } } diff --git a/src/backends/capture/mod.rs b/src/backends/capture/mod.rs index 18188bf..fb083dd 100644 --- a/src/backends/capture/mod.rs +++ b/src/backends/capture/mod.rs @@ -16,21 +16,21 @@ #[cfg(all(feature = "input-v4l", target_os = "linux"))] // I'm too lazy to set up a skeleton facade for V4L so here it will stay -mod v4l2; +mod v4l2_backend; #[cfg(all(feature = "input-v4l", target_os = "linux"))] #[cfg_attr(feature = "docs-features", doc(cfg(feature = "input-v4l")))] -pub use v4l2::V4LCaptureDevice; +pub use v4l2_backend::V4LCaptureDevice; #[cfg(any( all(feature = "input-msmf", target_os = "windows"), all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") ))] -mod msmf; +mod msmf_backend; #[cfg(any( all(feature = "input-msmf", target_os = "windows"), all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") ))] #[cfg_attr(feature = "docs-features", doc(cfg(feature = "input-msmf")))] -pub use msmf::MediaFoundationCaptureDevice; +pub use msmf_backend::MediaFoundationCaptureDevice; #[cfg(any( all( feature = "input-avfoundation", @@ -67,6 +67,11 @@ mod gst_backend; #[cfg(feature = "input-gst")] #[cfg_attr(feature = "docs-features", doc(cfg(feature = "input-gst")))] pub use gst_backend::GStreamerCaptureDevice; +#[cfg(feature = "input-jscam")] +mod browser_backend; +#[cfg(feature = "input-jscam")] +#[cfg_attr(feature = "docs-features", doc(cfg(feature = "input-jscam")))] +pub use browser_backend::BrowserCaptureDevice; #[cfg(feature = "input-opencv")] mod opencv_backend; #[cfg(feature = "input-opencv")] diff --git a/src/backends/capture/msmf.rs b/src/backends/capture/msmf_backend.rs similarity index 95% rename from src/backends/capture/msmf.rs rename to src/backends/capture/msmf_backend.rs index 2e6f92f..198c4e2 100644 --- a/src/backends/capture/msmf.rs +++ b/src/backends/capture/msmf_backend.rs @@ -16,8 +16,8 @@ use crate::{ all_known_camera_controls, mjpeg_to_rgb888, yuyv422_to_rgb888, CameraControl, CameraFormat, - CameraInfo, CaptureAPIBackend, CaptureBackendTrait, FrameFormat, KnownCameraControlFlag, - KnownCameraControls, NokhwaError, Resolution, + CameraIndex, CameraInfo, CaptureAPIBackend, CaptureBackendTrait, FrameFormat, + KnownCameraControlFlag, KnownCameraControls, NokhwaError, Resolution, }; use image::{ImageBuffer, Rgb}; use nokhwa_bindings_windows::{wmf::MediaFoundationDevice, MFControl, MediaFoundationControls}; @@ -34,10 +34,11 @@ use std::{any::Any, borrow::Cow, collections::HashMap}; /// - The symbolic link for the device is listed in the `misc` attribute of the [`CameraInfo`]. /// - The names may contain invalid characters since they were converted from UTF16. /// - When you call new or drop the struct, `initialize`/`de_initialize` will automatically be called. +// TODO: Allow CameraIndex to contain a device string. #[cfg_attr(feature = "docs-features", doc(cfg(feature = "input-msmf")))] pub struct MediaFoundationCaptureDevice<'a> { inner: MediaFoundationDevice<'a>, - info: CameraInfo, + info: CameraInfo<'a>, } impl<'a> MediaFoundationCaptureDevice<'a> { @@ -45,9 +46,13 @@ impl<'a> MediaFoundationCaptureDevice<'a> { /// /// If `camera_format` is `None`, it will be spawned with with 640x480@15 FPS, MJPEG [`CameraFormat`] default. /// # Errors - /// This function will error if Media Foundation fails to get the device. - pub fn new(index: usize, camera_fmt: Option) -> Result { - let mut mf_device = MediaFoundationDevice::new(index)?; + /// This function will error if Media Foundation fails to get the device. This will also error if the index is a [`CameraIndex::String`] that cannot be parsed into a `usize`. + pub fn new( + index: CameraIndex<'a>, + camera_fmt: Option, + ) -> Result { + let index = index.index_num()?; + let mut mf_device = MediaFoundationDevice::new(index as usize)?; if let Some(fmt) = camera_fmt { mf_device.set_format(fmt.into())?; } @@ -56,7 +61,7 @@ impl<'a> MediaFoundationCaptureDevice<'a> { mf_device.name(), "MediaFoundation Camera Device".to_string(), mf_device.symlink(), - mf_device.index(), + CameraIndex::Index(index), ); Ok(MediaFoundationCaptureDevice { @@ -67,9 +72,9 @@ impl<'a> MediaFoundationCaptureDevice<'a> { /// Create a new Media Foundation Device with desired settings. /// # Errors - /// This function will error if Media Foundation fails to get the device. + /// This function will error if Media Foundation fails to get the device. This will also error if the index is a [`CameraIndex::String`] that cannot be parsed into a `usize`. pub fn new_with( - index: usize, + index: CameraIndex<'a>, width: u32, height: u32, fps: u32, diff --git a/src/backends/capture/opencv_backend.rs b/src/backends/capture/opencv_backend.rs index 2963432..b4cc1c0 100644 --- a/src/backends/capture/opencv_backend.rs +++ b/src/backends/capture/opencv_backend.rs @@ -15,8 +15,8 @@ */ use crate::{ - CameraControl, CameraFormat, CameraIndexType, CameraInfo, CaptureAPIBackend, - CaptureBackendTrait, FrameFormat, KnownCameraControls, NokhwaError, Resolution, + CameraControl, CameraFormat, CameraIndex, CameraInfo, CaptureAPIBackend, CaptureBackendTrait, + FrameFormat, KnownCameraControls, NokhwaError, Resolution, }; use image::{ImageBuffer, Rgb}; use opencv::{ @@ -26,6 +26,7 @@ use opencv::{ CAP_MSMF, CAP_PROP_FPS, CAP_PROP_FRAME_HEIGHT, CAP_PROP_FRAME_WIDTH, CAP_V4L2, }, }; +use std::ops::Deref; use std::{any::Any, borrow::Cow, collections::HashMap}; /// Converts $from into $to @@ -69,16 +70,16 @@ macro_rules! tryinto_num { /// - The `Any` type for [`raw_camera_control()`](CaptureBackendTrait::raw_camera_control) is [`i32`], and its return `Any` is a [`f64`]. Please check [`OpenCV Documentation Constants`](https://docs.rs/opencv/0.53.1/opencv/videoio/index.html) for more. /// - The `Any` type for `control` for [`set_raw_camera_control()`](CaptureBackendTrait::set_raw_camera_control) is [`i32`] and [`f64`]. Please check [`OpenCV Documentation Constants`](https://docs.rs/opencv/0.53.1/opencv/videoio/index.html) for more. #[cfg_attr(feature = "docs-features", doc(cfg(feature = "input-opencv")))] -pub struct OpenCvCaptureDevice { +pub struct OpenCvCaptureDevice<'a> { camera_format: CameraFormat, - camera_location: CameraIndexType, - camera_info: CameraInfo, + camera_location: CameraIndex<'a>, + camera_info: CameraInfo<'a>, api_preference: i32, video_capture: VideoCapture, } #[allow(clippy::must_use_candidate)] -impl OpenCvCaptureDevice { +impl<'a> OpenCvCaptureDevice<'a> { /// Creates a new capture device using the `OpenCV` backend. You can either use an [`Index`](CameraIndexType::Index) or [`IPCamera`](CameraIndexType::IPCamera). /// /// Indexes are gives to devices by the OS, and usually numbered by order of discovery. @@ -89,13 +90,14 @@ impl OpenCvCaptureDevice { /// ``` /// , but please refer to the manufacturer for the actual IP format. /// - /// If `camera_format` is `None`, it will be spawned with with 640x480@15 FPS, MJPEG [`CameraFormat`] default if it is a index camera. + /// If `camera_format` is `None`, it will be spawned with with 640x480@30 FPS, MJPEG [`CameraFormat`] default if it is a index camera. /// # Errors /// If the backend fails to open the camera (e.g. Device does not exist at specified index/ip), Camera does not support specified [`CameraFormat`], and/or other `OpenCV` Error, this will error. + /// [`CameraIndex::Index`] will open a local camera, and [`CameraIndex::String`] **requires** an IP. /// # Panics /// If the API u32 -> i32 fails this will error pub fn new( - camera_location: CameraIndexType, + camera_location: CameraIndex<'a>, cfmt: Option, api_pref: Option, ) -> Result { @@ -113,7 +115,7 @@ impl OpenCvCaptureDevice { }; let mut video_capture = match camera_location.clone() { - CameraIndexType::Index(idx) => { + CameraIndex::Index(idx) => { let vid_cap = match VideoCapture::new(tryinto_num!(i32, idx), api) { Ok(vc) => { index = idx; @@ -128,9 +130,14 @@ impl OpenCvCaptureDevice { }; vid_cap } - CameraIndexType::IPCamera(ip) => match VideoCapture::from_file(&*ip, CAP_ANY) { + CameraIndex::String(ip) => match VideoCapture::from_file(ip.as_ref(), CAP_ANY) { Ok(vc) => vc, - Err(why) => return Err(NokhwaError::OpenDeviceError(ip, why.to_string())), + Err(why) => { + return Err(NokhwaError::OpenDeviceError( + ip.to_string(), + why.to_string(), + )) + } }, }; @@ -140,7 +147,7 @@ impl OpenCvCaptureDevice { format!("OpenCV Capture Device {}", camera_location), camera_location.to_string(), "".to_string(), - index as usize, + CameraIndex::Index(index), ); Ok(OpenCvCaptureDevice { @@ -160,18 +167,18 @@ impl OpenCvCaptureDevice { /// ``` /// , but please refer to the manufacturer for the actual IP format. /// - /// If `camera_format` is `None`, it will be spawned with with 640x480@15 FPS, MJPEG [`CameraFormat`] default if it is a index camera. + /// If `camera_format` is `None`, it will be spawned with with 640x480@30 FPS, MJPEG [`CameraFormat`] default if it is a index camera. /// # Errors /// If the backend fails to open the camera (e.g. Device does not exist at specified index/ip), Camera does not support specified [`CameraFormat`], and/or other `OpenCV` Error, this will error. - pub fn new_ip_camera(ip: String) -> Result { - let camera_location = CameraIndexType::IPCamera(ip); + pub fn new_ip_camera(ip: impl Deref + 'a) -> Result { + let camera_location = CameraIndex::String(Cow::from(ip.to_string())); OpenCvCaptureDevice::new(camera_location, None, None) } /// Creates a new capture device using the `OpenCV` backend. /// Indexes are gives to devices by the OS, and usually numbered by order of discovery. /// - /// If `camera_format` is `None`, it will be spawned with with 640x480@15 FPS, MJPEG [`CameraFormat`] default if it is a index camera. + /// If `camera_format` is `None`, it will be spawned with with 640x480@30 FPS, MJPEG [`CameraFormat`] default if it is a index camera. /// # Errors /// If the backend fails to open the camera (e.g. Device does not exist at specified index/ip), Camera does not support specified [`CameraFormat`], and/or other `OpenCV` Error, this will error. pub fn new_index_camera( @@ -179,39 +186,41 @@ impl OpenCvCaptureDevice { cfmt: Option, api_pref: Option, ) -> Result { - let camera_location = CameraIndexType::Index(tryinto_num!(u32, index)); + let camera_location = CameraIndex::Index(tryinto_num!(u32, index)); OpenCvCaptureDevice::new(camera_location, cfmt, api_pref) } /// Creates a new capture device using the `OpenCV` backend. /// Indexes are gives to devices by the OS, and usually numbered by order of discovery. /// - /// If `camera_format` is `None`, it will be spawned with with 640x480@15 FPS, MJPEG [`CameraFormat`] default if it is a index camera. + /// If `camera_format` is `None`, it will be spawned with with 640x480@30 FPS, MJPEG [`CameraFormat`] default if it is a index camera. /// # Errors /// If the backend fails to open the camera (e.g. Device does not exist at specified index/ip), Camera does not support specified [`CameraFormat`], and/or other `OpenCV` Error, this will error. - pub fn new_autopref(index: usize, cfmt: Option) -> Result { - let camera_location = CameraIndexType::Index(tryinto_num!(u32, index)); - OpenCvCaptureDevice::new(camera_location, cfmt, None) + pub fn new_autopref( + index: CameraIndex<'a>, + cfmt: Option, + ) -> Result { + OpenCvCaptureDevice::new(index, cfmt, None) } /// Gets weather said capture device is an `IPCamera`. pub fn is_ip_camera(&self) -> bool { match self.camera_location { - CameraIndexType::Index(_) => false, - CameraIndexType::IPCamera(_) => true, + CameraIndex::Index(_) => false, + CameraIndex::String(_) => true, } } /// Gets weather said capture device is an OS-based indexed camera. pub fn is_index_camera(&self) -> bool { match self.camera_location { - CameraIndexType::Index(_) => true, - CameraIndexType::IPCamera(_) => false, + CameraIndex::Index(_) => true, + CameraIndex::String(_) => false, } } /// Gets the camera location - pub fn camera_location(&self) -> CameraIndexType { + pub fn camera_location(&self) -> CameraIndex { self.camera_location.clone() } @@ -338,7 +347,7 @@ impl OpenCvCaptureDevice { } } -impl CaptureBackendTrait for OpenCvCaptureDevice { +impl<'a> CaptureBackendTrait for OpenCvCaptureDevice<'a> { fn backend(&self) -> CaptureAPIBackend { CaptureAPIBackend::OpenCv } @@ -520,7 +529,7 @@ impl CaptureBackendTrait for OpenCvCaptureDevice { #[allow(clippy::cast_possible_wrap)] fn open_stream(&mut self) -> Result<(), NokhwaError> { match self.camera_location.clone() { - CameraIndexType::Index(idx) => { + CameraIndex::Index(idx) => { match self .video_capture .open_1(idx as i32, get_api_pref_int() as i32) @@ -534,15 +543,15 @@ impl CaptureBackendTrait for OpenCvCaptureDevice { } } } - CameraIndexType::IPCamera(ip) => { + CameraIndex::String(ip) => { match self .video_capture - .open_file(&*ip, get_api_pref_int() as i32) + .open_file(ip.as_ref(), get_api_pref_int() as i32) { Ok(_) => {} Err(why) => { return Err(NokhwaError::OpenDeviceError( - ip, + ip.to_string(), format!("Failed to open device: {}", why), )) } @@ -627,7 +636,7 @@ fn get_api_pref_int() -> u32 { fn set_properties( _vc: &mut VideoCapture, _camera_format: CameraFormat, - _camera_location: &CameraIndexType, + _camera_location: &CameraIndex, ) -> Result<(), NokhwaError> { Ok(()) } diff --git a/src/backends/capture/v4l2.rs b/src/backends/capture/v4l2_backend.rs similarity index 97% rename from src/backends/capture/v4l2.rs rename to src/backends/capture/v4l2_backend.rs index 0fd31b8..8c0f4e2 100644 --- a/src/backends/capture/v4l2.rs +++ b/src/backends/capture/v4l2_backend.rs @@ -17,7 +17,7 @@ use crate::{ error::NokhwaError, mjpeg_to_rgb888, - utils::{CameraFormat, CameraInfo}, + utils::{CameraFormat, CameraIndex, CameraInfo}, yuyv422_to_rgb888, CameraControl, CaptureAPIBackend, CaptureBackendTrait, FrameFormat, KnownCameraControlFlag, KnownCameraControls, Resolution, }; @@ -171,7 +171,7 @@ fn clone_control(ctrl: &Control) -> Control { #[cfg_attr(feature = "docs-features", doc(cfg(feature = "input-v4l")))] pub struct V4LCaptureDevice<'a> { camera_format: CameraFormat, - camera_info: CameraInfo, + camera_info: CameraInfo<'a>, device: Device, stream_handle: Option>, } @@ -181,9 +181,10 @@ impl<'a> V4LCaptureDevice<'a> { /// /// If `camera_format` is `None`, it will be spawned with with 640x480@15 FPS, MJPEG [`CameraFormat`] default. /// # Errors - /// This function will error if the camera is currently busy or if `V4L2` can't read device information. - pub fn new(index: usize, cam_fmt: Option) -> Result { - let device = match Device::new(index) { + /// This function will error if the camera is currently busy or if `V4L2` can't read device information. This will also error if the index is a [`CameraIndex::String`] that cannot be parsed into a `usize`. + pub fn new(index: CameraIndex<'a>, cam_fmt: Option) -> Result { + let index = index.as_index()?; + let device = match Device::new(index as usize) { Ok(dev) => dev, Err(why) => { return Err(NokhwaError::OpenDeviceError( @@ -194,7 +195,12 @@ impl<'a> V4LCaptureDevice<'a> { }; 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, + CameraIndex::Index(index), + ), Err(why) => { return Err(NokhwaError::GetPropertyError { property: "Capabilities".to_string(), @@ -269,7 +275,7 @@ impl<'a> V4LCaptureDevice<'a> { /// # Errors /// This function will error if the camera is currently busy or if `V4L2` can't read device information. pub fn new_with( - index: usize, + index: CameraIndex<'a>, width: u32, height: u32, fps: u32, diff --git a/src/camera.rs b/src/camera.rs index 17bad4d..5526b2b 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -15,12 +15,11 @@ */ use crate::{ - CameraControl, CameraFormat, CameraInfo, CaptureAPIBackend, CaptureBackendTrait, FrameFormat, - KnownCameraControls, NokhwaError, Resolution, + CameraControl, CameraFormat, CameraIndex, CameraInfo, CaptureAPIBackend, CaptureBackendTrait, + FrameFormat, KnownCameraControls, NokhwaError, Resolution, }; use image::{buffer::ConvertBuffer, ImageBuffer, Rgb, RgbaImage}; -use std::any::Any; -use std::{borrow::Cow, collections::HashMap}; +use std::{any::Any, borrow::Cow, collections::HashMap}; #[cfg(feature = "output-wgpu")] use wgpu::{ Device as WgpuDevice, Extent3d, ImageCopyTexture, ImageDataLayout, Queue as WgpuQueue, @@ -29,18 +28,18 @@ use wgpu::{ }; /// The main `Camera` struct. This is the struct that abstracts over all the backends, providing a simplified interface for use. -pub struct Camera { - idx: usize, - backend: Box, +pub struct Camera<'a> { + idx: CameraIndex<'a>, + backend: Box, backend_api: CaptureAPIBackend, } #[allow(clippy::nonminimal_bool)] -impl Camera { +impl<'a> Camera<'a> { /// Create a new camera from an `index` and `format` /// # Errors /// This will error if you either have a bad platform configuration (e.g. `input-v4l` but not on linux) or the backend cannot create the camera (e.g. permission denied). - pub fn new(index: usize, format: Option) -> Result { + pub fn new(index: CameraIndex<'a>, format: Option) -> Result { Camera::with_backend(index, format, CaptureAPIBackend::Auto) } @@ -48,11 +47,12 @@ impl Camera { /// # Errors /// This will error if you either have a bad platform configuration (e.g. `input-v4l` but not on linux) or the backend cannot create the camera (e.g. permission denied). pub fn with_backend( - index: usize, + index: CameraIndex<'a>, format: Option, backend: CaptureAPIBackend, ) -> Result { - let camera_backend = init_camera(index, format, backend)?; + let camera_backend: Box = + init_camera(index.clone(), format, backend)?; Ok(Camera { idx: index, @@ -65,7 +65,7 @@ impl Camera { /// # Errors /// This will error if you either have a bad platform configuration (e.g. `input-v4l` but not on linux) or the backend cannot create the camera (e.g. permission denied). pub fn new_with( - index: usize, + index: CameraIndex<'a>, width: u32, height: u32, fps: u32, @@ -78,19 +78,20 @@ impl Camera { /// Gets the current Camera's index. #[must_use] - pub fn index(&self) -> usize { - self.idx + pub fn index(&self) -> &CameraIndex<'a> { + &self.idx } /// Sets the current Camera's index. Note that this re-initializes the camera. /// # Errors /// The Backend may fail to initialize. - pub fn set_index(&mut self, new_idx: usize) -> Result<(), NokhwaError> { + pub fn set_index(&mut self, new_idx: CameraIndex<'a>) -> Result<(), NokhwaError> { { self.backend.stop_stream()?; } let new_camera_format = self.backend.camera_format(); - let new_camera = init_camera(new_idx, Some(new_camera_format), self.backend_api)?; + let new_camera: Box = + init_camera(new_idx, Some(new_camera_format), self.backend_api)?; self.backend = new_camera; Ok(()) } @@ -109,7 +110,8 @@ impl Camera { self.backend.stop_stream()?; } let new_camera_format = self.backend.camera_format(); - let new_camera = init_camera(self.idx, Some(new_camera_format), new_backend)?; + let new_camera: Box = + init_camera((&self.idx).clone(), Some(new_camera_format), new_backend)?; self.backend = new_camera; Ok(()) } @@ -389,13 +391,13 @@ impl Camera { /// Directly copies a frame to a Wgpu texture. This will automatically convert the frame into a RGBA frame. /// # Errors /// If the frame cannot be captured or the resolution is 0 on any axis, this will error. - pub fn frame_texture<'a>( + pub fn frame_texture( &mut self, device: &WgpuDevice, queue: &WgpuQueue, label: Option<&'a str>, ) -> Result { - use std::{convert::TryFrom, num::NonZeroU32}; + use std::num::NonZeroU32; let frame = self.frame()?; let rgba_frame: RgbaImage = frame.convert(); @@ -452,9 +454,9 @@ impl Camera { } } -impl Drop for Camera { +impl<'a> Drop for Camera<'a> { fn drop(&mut self) { - self.stop_stream().unwrap(); + let _ = self.stop_stream(); } } @@ -475,6 +477,8 @@ fn figure_out_auto() -> Option { cap = CaptureAPIBackend::GStreamer; } else if cfg!(feature = "input-opencv") { cap = CaptureAPIBackend::OpenCv; + } else if cfg!(feature = "input-jscam") { + cap = CaptureAPIBackend::Browser; } if cap == CaptureAPIBackend::Auto { return None; @@ -489,7 +493,7 @@ macro_rules! cap_impl_fn { $( paste::paste! { #[cfg ($cfg) ] - fn [< init_ $backend_name>](idx: usize, setting: Option) -> Option, NokhwaError>> { + fn [< init_ $backend_name>](idx: CameraIndex<'_>, setting: Option) -> Option, NokhwaError>> { use crate::backends::capture::$backend; match <$backend>::$init_fn(idx, setting) { Ok(cap) => Some(Ok(Box::new(cap))), @@ -497,7 +501,7 @@ macro_rules! cap_impl_fn { } } #[cfg(not( $cfg ))] - fn [< init_ $backend_name>](_idx: usize, _setting: Option) -> Option, NokhwaError>> { + fn [< init_ $backend_name>](_idx: CameraIndex<'_>, _setting: Option) -> Option, NokhwaError>> { None } } @@ -591,16 +595,17 @@ cap_impl_fn! { (GStreamerCaptureDevice, new, feature = "input-gst", gst), (OpenCvCaptureDevice, new_autopref, feature = "input-opencv", opencv), // (UVCCaptureDevice, create, feature = "input-uvc", uvc), + (BrowserCaptureDevice, new, feature = "input-jscam", browser), (V4LCaptureDevice, new, all(feature = "input-v4l", target_os = "linux"), v4l), (MediaFoundationCaptureDevice, new, all(feature = "input-msmf", target_os = "windows"), msmf), (AVFoundationCaptureDevice, new, all(feature = "input-avfoundation", any(target_os = "macos", target_os = "ios")), avfoundation) } -fn init_camera( - index: usize, +fn init_camera<'a>( + index: CameraIndex<'a>, format: Option, backend: CaptureAPIBackend, -) -> Result, NokhwaError> { +) -> Result, NokhwaError> { let camera_backend = cap_impl_matches! { backend, index, format, ("input-v4l", Video4Linux, init_v4l), @@ -608,10 +613,12 @@ fn init_camera( ("input-avfoundation", AVFoundation, init_avfoundation), // ("input-uvc", UniversalVideoClass, init_uvc), ("input-gst", GStreamer, init_gst), - ("input-opencv", OpenCv, init_opencv) + ("input-opencv", OpenCv, init_opencv), + ("input-jscam", Browser, init_browser) }; Ok(camera_backend) } #[cfg(feature = "output-threaded")] -unsafe impl Send for Camera {} +#[cfg_attr(feature = "docs-features", doc(cfg(feature = "output-threaded")))] +unsafe impl<'a> Send for Camera<'a> {} diff --git a/src/js_camera.rs b/src/js_camera.rs index e7d8eaa..591790e 100644 --- a/src/js_camera.rs +++ b/src/js_camera.rs @@ -20,9 +20,10 @@ //! //! This assumes that you are running a modern browser on the desktop. -use crate::{CameraInfo, NokhwaError, Resolution}; +use crate::{CameraIndex, CameraInfo, NokhwaError, Resolution}; use image::{buffer::ConvertBuffer, ImageBuffer, Rgb, RgbImage, Rgba}; use js_sys::{Array, JsString, Map, Object, Promise}; +use std::borrow::Borrow; use std::{ borrow::Cow, convert::TryFrom, @@ -243,7 +244,7 @@ pub async fn js_request_permission() -> Result<(), JsValue> { /// Queries Cameras using [`MediaDevices::enumerate_devices()`](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.MediaDevices.html#method.enumerate_devices) [MDN](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices) /// # Errors /// This will error if there is no valid web context or the web API is not supported -pub async fn query_js_cameras() -> Result, NokhwaError> { +pub async fn query_js_cameras<'a>() -> Result>, NokhwaError> { let window: Window = window()?; let navigator = window.navigator(); let media_devices = media_devices(&navigator)?; @@ -294,7 +295,11 @@ pub async fn query_js_cameras() -> Result, NokhwaError> { media_device_info.group_id(), media_device_info.device_id() ), - idx_device as usize, + CameraIndex::String(Cow::from(format!( + "{} {}", + media_device_info.group_id(), + media_device_info.device_id() + ))), )); tracks .iter() @@ -314,7 +319,11 @@ pub async fn query_js_cameras() -> Result, NokhwaError> { media_device_info.group_id(), media_device_info.device_id() ), - idx_device as usize, + CameraIndex::String(Cow::from(format!( + "{} {}", + media_device_info.group_id(), + media_device_info.device_id() + ))), )); } } @@ -2544,7 +2553,7 @@ impl JSCamera { let resolution = self.resolution(); let frame = self.frame_raw()?; if convert_rgba { - buffer.copy_from_slice(&frame); + buffer.copy_from_slice(frame.borrow()); return Ok(frame.len()); } let image = match ImageBuffer::from_raw(resolution.width(), resolution.height(), frame) { @@ -2611,7 +2620,7 @@ impl JSCamera { origin: wgpu::Origin3d::ZERO, aspect: TextureAspect::All, }, - &frame, + frame.borrow(), ImageDataLayout { offset: 0, bytes_per_row: width_nonzero, diff --git a/src/lib.rs b/src/lib.rs index 4773890..149e40e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,10 @@ */ #![cfg_attr(feature = "test-fail-warning", deny(warnings))] #![cfg_attr(feature = "docs-features", feature(doc_cfg))] - +#![deny(clippy::pedantic)] +#![deny(clippy::missing_errors_doc)] +#![deny(clippy::missing_safety_doc)] +#![warn(clippy::all)] //! # nokhwa //! A Simple-to-use, cross-platform Rust Webcam Capture Library //! diff --git a/src/network_camera.rs b/src/network_camera.rs index 3d1a1a8..cbf6449 100644 --- a/src/network_camera.rs +++ b/src/network_camera.rs @@ -26,12 +26,12 @@ use wgpu::{ /// A struct that supports IP Cameras via the `OpenCV` backend. #[cfg_attr(feature = "docs-features", doc(cfg(feature = "input-ipcam")))] -pub struct NetworkCamera { +pub struct NetworkCamera<'a> { ip: String, - opencv_backend: RefCell, + opencv_backend: RefCell>, } -impl NetworkCamera { +impl<'a> NetworkCamera<'a> { /// Creates a new [`NetworkCamera`] from an IP. /// # Errors /// If the IP is invalid or `OpenCV` fails to open the IP, this will error @@ -108,7 +108,7 @@ impl NetworkCamera { queue: &WgpuQueue, label: Option<&'a str>, ) -> Result { - use std::{convert::TryFrom, num::NonZeroU32}; + use std::num::NonZeroU32; let frame = self.frame()?; let rgba_frame: RgbaImage = frame.convert(); @@ -165,8 +165,8 @@ impl NetworkCamera { } } -impl Drop for NetworkCamera { +impl<'a> Drop for NetworkCamera<'a> { fn drop(&mut self) { - self.stop_stream().unwrap(); + let _ = self.stop_stream(); } } diff --git a/src/query.rs b/src/query.rs index 3a8abd5..2df1aab 100644 --- a/src/query.rs +++ b/src/query.rs @@ -14,7 +14,7 @@ * limitations under the License. */ -use crate::{CameraInfo, CaptureAPIBackend, NokhwaError}; +use crate::{CameraIndex, CameraInfo, CaptureAPIBackend, NokhwaError}; /// Query the system for a list of available devices. /// Usually the order goes Native -> UVC -> Gstreamer. @@ -26,7 +26,7 @@ use crate::{CameraInfo, CaptureAPIBackend, NokhwaError}; /// # Errors /// If you use an unsupported API (check the README or crate root for more info), incompatible backend for current platform, incompatible platform, or insufficient permissions, etc /// this will error. -pub fn query() -> Result, NokhwaError> { +pub fn query<'a>() -> Result>, NokhwaError> { query_devices(CaptureAPIBackend::Auto) } @@ -43,7 +43,7 @@ pub fn query() -> Result, NokhwaError> { /// If you use an unsupported API (check the README or crate root for more info), incompatible backend for current platform, incompatible platform, or insufficient permissions, etc /// this will error. #[allow(clippy::module_name_repetitions)] -pub fn query_devices(api: CaptureAPIBackend) -> Result, NokhwaError> { +pub fn query_devices<'a>(api: CaptureAPIBackend) -> Result>, NokhwaError> { match api { CaptureAPIBackend::Auto => { // determine platform @@ -113,6 +113,8 @@ pub fn query_devices(api: CaptureAPIBackend) -> Result, NokhwaEr CaptureAPIBackend::MediaFoundation => query_msmf(), CaptureAPIBackend::GStreamer => query_gstreamer(), CaptureAPIBackend::OpenCv => Err(NokhwaError::UnsupportedOperationError(api)), + CaptureAPIBackend::Network => Err(NokhwaError::UnsupportedOperationError(api)), + CaptureAPIBackend::Browser => query_wasm(), } } @@ -120,7 +122,7 @@ pub fn query_devices(api: CaptureAPIBackend) -> Result, NokhwaEr #[cfg(all(feature = "input-v4l", target_os = "linux"))] #[allow(clippy::unnecessary_wraps)] -fn query_v4l() -> Result, NokhwaError> { +fn query_v4l<'a>() -> Result>, NokhwaError> { Ok({ let camera_info: Vec = v4l::context::enum_devices() .iter() @@ -130,7 +132,7 @@ fn query_v4l() -> Result, NokhwaError> { .unwrap_or(format!("{}", node.path().to_string_lossy())), format!("Video4Linux Device @ {}", node.path().to_string_lossy()), "".to_string(), - node.index(), + CameraIndex::Index(node.index() as u32), ) }) .collect(); @@ -139,14 +141,14 @@ fn query_v4l() -> Result, NokhwaError> { } #[cfg(any(not(feature = "input-v4l"), not(target_os = "linux")))] -fn query_v4l() -> Result, NokhwaError> { +fn query_v4l<'a>() -> Result>, NokhwaError> { Err(NokhwaError::UnsupportedOperationError( CaptureAPIBackend::Video4Linux, )) } #[cfg(feature = "input-uvc")] -fn query_uvc() -> Result, NokhwaError> { +fn query_uvc<'a>() -> Result>, NokhwaError> { use uvc::Device; let context = match uvc::Context::new() { Ok(ctx) => ctx, @@ -205,7 +207,7 @@ fn query_uvc() -> Result, NokhwaError> { desc.product_id, desc.serial_number.unwrap_or_else(|| "".to_string()) ), - counter, + CameraIndex::Index(counter as u32), )); counter += 1; } @@ -216,14 +218,14 @@ fn query_uvc() -> Result, NokhwaError> { } #[cfg(not(feature = "input-uvc"))] -fn query_uvc() -> Result, NokhwaError> { +fn query_uvc<'a>() -> Result>, NokhwaError> { Err(NokhwaError::UnsupportedOperationError( CaptureAPIBackend::UniversalVideoClass, )) } #[cfg(feature = "input-gst")] -fn query_gstreamer() -> Result, NokhwaError> { +fn query_gstreamer<'a>() -> Result>, NokhwaError> { use gstreamer::{ prelude::{DeviceExt, DeviceMonitorExt, DeviceMonitorExtManual}, Caps, DeviceMonitor, @@ -273,7 +275,7 @@ fn query_gstreamer() -> Result, NokhwaError> { name.to_string(), class.to_string(), "".to_string(), - counter - 1, + CameraIndex::Index(counter - 1), ) }) .collect(); @@ -282,7 +284,7 @@ fn query_gstreamer() -> Result, NokhwaError> { } #[cfg(not(feature = "input-gst"))] -fn query_gstreamer() -> Result, NokhwaError> { +fn query_gstreamer<'a>() -> Result>, NokhwaError> { Err(NokhwaError::UnsupportedOperationError( CaptureAPIBackend::GStreamer, )) @@ -290,7 +292,7 @@ fn query_gstreamer() -> Result, NokhwaError> { // please refer to https://docs.microsoft.com/en-us/windows/win32/medfound/enumerating-video-capture-devices #[cfg(all(feature = "input-msmf", target_os = "windows"))] -fn query_msmf() -> Result, NokhwaError> { +fn query_msmf<'a>() -> Result>, NokhwaError> { let list: Vec = match nokhwa_bindings_windows::wmf::query_media_foundation_descriptors() { Ok(l) => l @@ -306,7 +308,7 @@ fn query_msmf() -> Result, NokhwaError> { } #[cfg(any(not(feature = "input-msmf"), not(target_os = "windows")))] -fn query_msmf() -> Result, NokhwaError> { +fn query_msmf<'a>() -> Result>, NokhwaError> { Err(NokhwaError::UnsupportedOperationError( CaptureAPIBackend::MediaFoundation, )) @@ -316,7 +318,7 @@ fn query_msmf() -> Result, NokhwaError> { feature = "input-avfoundation", any(target_os = "macos", target_os = "ios") ))] -fn query_avfoundation() -> Result, NokhwaError> { +fn query_avfoundation<'a>() -> Result>, NokhwaError> { use nokhwa_bindings_macos::avfoundation::query_avfoundation as q_avf; Ok(q_avf()? @@ -329,8 +331,23 @@ fn query_avfoundation() -> Result, NokhwaError> { feature = "input-avfoundation", any(target_os = "macos", target_os = "ios") )))] -fn query_avfoundation() -> Result, NokhwaError> { +fn query_avfoundation<'a>() -> Result>, NokhwaError> { Err(NokhwaError::UnsupportedOperationError( CaptureAPIBackend::AVFoundation, )) } + +#[cfg(feature = "input-jscam")] +fn query_wasm<'a>() -> Result>, NokhwaError> { + use crate::js_camera::query_js_cameras; + use wasm_rs_async_executor::single_threaded::block_on; + + block_on(query_js_cameras()) +} + +#[cfg(not(feature = "input-jscam"))] +fn query_wasm<'a>() -> Result>, NokhwaError> { + Err(NokhwaError::UnsupportedOperationError( + CaptureAPIBackend::Browser, + )) +} diff --git a/src/threaded.rs b/src/threaded.rs index 2046e32..823a4ee 100644 --- a/src/threaded.rs +++ b/src/threaded.rs @@ -19,7 +19,7 @@ use crate::{ KnownCameraControls, NokhwaError, Resolution, }; use image::{ImageBuffer, Rgb}; -use parking_lot::FairMutex; +use parking_lot::Mutex; use std::{ any::Any, collections::HashMap, @@ -31,7 +31,7 @@ use std::{ }; /// Creates a camera that runs in a different thread that you can use a callback to access the frames of. -/// It uses a `Arc` and a `FairMutex` to ensure that this feels like a normal camera, but callback based. +/// It uses a `Arc` and a `Mutex` to ensure that this feels like a normal camera, but callback based. /// See [`Camera`] for more details on the camera itself. /// /// Your function is called every time there is a new frame. In order to avoid frame loss, it should @@ -45,9 +45,9 @@ use std::{ #[cfg_attr(feature = "docs-features", doc(cfg(feature = "output-threaded")))] #[derive(Clone)] pub struct ThreadedCamera { - camera: Arc>, - frame_callback: Arc, Vec>)>>>, - last_frame_captured: Arc, Vec>>>, + camera: Arc>, + frame_callback: Arc, Vec>)>>>, + last_frame_captured: Arc, Vec>>>, die_bool: Arc, } @@ -98,23 +98,21 @@ impl ThreadedCamera { backend: CaptureAPIBackend, func: Option< fn( - _: Arc>, - _: Arc, Vec>)>>>, - _: Arc, Vec>>>, + _: Arc>, + _: Arc, Vec>)>>>, + _: Arc, Vec>>>, _: Arc, ), >, ) -> Result { - let camera = Arc::new(FairMutex::new(Camera::with_backend( - index, format, backend, - )?)); + let camera = Arc::new(Mutex::new(Camera::with_backend(index, format, backend)?)); let format = match format { Some(fmt) => fmt, None => CameraFormat::default(), }; - let frame_callback = Arc::new(FairMutex::new(None)); + let frame_callback = Arc::new(Mutex::new(None)); let die_bool = Arc::new(AtomicBool::new(false)); - let holding_cell = Arc::new(FairMutex::new(ImageBuffer::new( + let holding_cell = Arc::new(Mutex::new(ImageBuffer::new( format.width(), format.height(), ))); @@ -414,9 +412,9 @@ impl Drop for ThreadedCamera { } fn camera_frame_thread_loop( - camera: Arc>, - callback: Arc, Vec>)>>>, - holding_cell: Arc, Vec>>>, + camera: Arc>, + callback: Arc, Vec>)>>>, + holding_cell: Arc, Vec>>>, die_bool: Arc, ) { loop { diff --git a/src/utils.rs b/src/utils.rs index f91d727..cd1c7bc 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -14,24 +14,9 @@ * limitations under the License. */ -/* - * Copyright 2021 l1npengtul / The Nokhwa Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - use crate::NokhwaError; use std::{ + borrow::{Borrow, Cow}, cmp::Ordering, fmt::{Display, Formatter}, }; @@ -70,7 +55,7 @@ use v4l::{control::Description, Format, FourCC}; /// - MJPEG is a motion-jpeg compressed frame, it allows for high frame rates. /// # JS-WASM /// This is exported as `FrameFormat` -#[derive(Copy, Clone, Debug, PartialEq, Hash, PartialOrd, Ord, Eq)] +#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] pub enum FrameFormat { MJPEG, YUYV, @@ -171,7 +156,7 @@ impl From for AVFourCC { /// # JS-WASM /// This is exported as `JSResolution` #[cfg_attr(feature = "output-wasm", wasm_bindgen(js_name = JSResolution))] -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Debug, Default, Hash, Eq, PartialEq)] pub struct Resolution { pub width_x: u32, pub height_y: u32, @@ -295,7 +280,7 @@ impl From for Resolution { /// This is a convenience struct that holds all information about the format of a webcam stream. /// It consists of a [`Resolution`], [`FrameFormat`], and a frame rate(u8). -#[derive(Copy, Clone, Debug, Hash, PartialEq)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, PartialOrd)] pub struct CameraFormat { resolution: Resolution, format: FrameFormat, @@ -472,26 +457,31 @@ impl From for CaptureDeviceFormatDescriptor { /// # JS-WASM /// This is exported as a `JSCameraInfo`. #[cfg_attr(feature = "output-wasm", wasm_bindgen(js_name = JSCameraInfo))] -#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)] -pub struct CameraInfo { - human_name: String, - description: String, - misc: String, - index: usize, +#[derive(Clone, Debug, Hash, PartialEq, PartialOrd)] +pub struct CameraInfo<'a> { + human_name: Cow<'a, str>, + description: Cow<'a, str>, + misc: Cow<'a, str>, + index: CameraIndex<'a>, } #[cfg_attr(feature = "output-wasm", wasm_bindgen(js_class = JSCameraInfo))] -impl CameraInfo { +impl<'a> CameraInfo<'a> { /// Create a new [`CameraInfo`]. /// # JS-WASM /// This is exported as a constructor for [`CameraInfo`]. #[must_use] #[cfg_attr(feature = "output-wasm", wasm_bindgen(constructor))] - pub fn new(human_name: String, description: String, misc: String, index: usize) -> Self { + pub fn new( + human_name: S, + description: S, + misc: S, + index: CameraIndex<'a>, + ) -> Self { CameraInfo { - human_name, - description, - misc, + human_name: Cow::from(human_name.to_string()), + description: Cow::from(description.to_string()), + misc: Cow::from(misc.to_string()), index, } } @@ -504,8 +494,8 @@ impl CameraInfo { feature = "output-wasm", wasm_bindgen(getter = HumanReadableName) )] - pub fn human_name(&self) -> String { - self.human_name.clone() + pub fn human_name(&self) -> &'_ str { + &self.human_name.borrow() } /// Set the device info's human name. @@ -515,8 +505,8 @@ impl CameraInfo { feature = "output-wasm", wasm_bindgen(setter = HumanReadableName) )] - pub fn set_human_name(&mut self, human_name: String) { - self.human_name = human_name; + pub fn set_human_name>(&mut self, human_name: S) { + self.human_name = Cow::from(human_name.as_ref().to_string()); } /// Get a reference to the device info's description. @@ -524,16 +514,16 @@ impl CameraInfo { /// This is exported as a `get_Description`. #[must_use] #[cfg_attr(feature = "output-wasm", wasm_bindgen(getter = Description))] - pub fn description(&self) -> String { - self.description.clone() + pub fn description(&self) -> &'_ str { + &self.description.borrow() } /// Set the device info's description. /// # JS-WASM /// This is exported as a `set_Description`. #[cfg_attr(feature = "output-wasm", wasm_bindgen(setter = Description))] - pub fn set_description(&mut self, description: String) { - self.description = description; + pub fn set_description>(&mut self, description: S) { + self.description = Cow::from(description.as_ref().to_string()); } /// Get a reference to the device info's misc. @@ -541,16 +531,16 @@ impl CameraInfo { /// This is exported as a `get_MiscString`. #[must_use] #[cfg_attr(feature = "output-wasm", wasm_bindgen(getter = MiscString))] - pub fn misc(&self) -> String { - self.misc.clone() + pub fn misc(&self) -> &'_ str { + &self.misc.borrow() } /// Set the device info's misc. /// # JS-WASM /// This is exported as a `set_MiscString`. #[cfg_attr(feature = "output-wasm", wasm_bindgen(setter = MiscString))] - pub fn set_misc(&mut self, misc: String) { - self.misc = misc; + pub fn set_misc>(&mut self, misc: S) { + self.misc = Cow::from(misc.as_ref().to_string()); } /// Get a reference to the device info's index. @@ -558,32 +548,37 @@ impl CameraInfo { /// This is exported as a `get_Index`. #[must_use] #[cfg_attr(feature = "output-wasm", wasm_bindgen(getter = Index))] - pub fn index(&self) -> usize { - self.index + pub fn index(&self) -> &CameraIndex<'a> { + &self.index } /// Set the device info's index. /// # JS-WASM /// This is exported as a `set_Index`. #[cfg_attr(feature = "output-wasm", wasm_bindgen(setter = Index))] - pub fn set_index(&mut self, index: usize) { + pub fn set_index(&mut self, index: CameraIndex<'a>) { self.index = index; } -} -impl PartialOrd for CameraInfo { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + /// Gets the device info's index as an `usize`. + /// # JS-WASM + /// This is exported as `get_Index_Int` + #[cfg_attr(feature = "output-wasm", wasm_bindgen(getter = Index_Int))] + pub fn index_num(&self) -> Result { + match &self.index { + CameraIndex::Index(i) => Ok(*i), + CameraIndex::String(s) => match s.parse::() { + Ok(p) => Ok(p), + Err(why) => Err(NokhwaError::GetPropertyError { + property: "index-int".to_string(), + error: why.to_string(), + }), + }, + } } } -impl Ord for CameraInfo { - fn cmp(&self, other: &Self) -> Ordering { - self.index.cmp(&other.index) - } -} - -impl Display for CameraInfo { +impl<'a> Display for CameraInfo<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, @@ -1003,7 +998,9 @@ impl Ord for CameraControl { /// - `MediaFoundation` - Microsoft Media Foundation, Windows only, /// - `OpenCV` - Uses `OpenCV` to capture. Platform agnostic. /// - `GStreamer` - Uses `GStreamer` RTP to capture. Platform agnostic. -#[derive(Clone, Copy, Debug, PartialEq)] +/// - `Network` - Uses `OpenCV` to capture from an IP. +/// - `Browser` - Uses browser APIs to capture from a webcam. +#[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] pub enum CaptureAPIBackend { Auto, AVFoundation, @@ -1012,6 +1009,8 @@ pub enum CaptureAPIBackend { MediaFoundation, OpenCv, GStreamer, + Network, + Browser, } impl Display for CaptureAPIBackend { @@ -1021,40 +1020,74 @@ impl Display for CaptureAPIBackend { } } -/// The `OpenCV` backend supports both native cameras and IP Cameras, so this is an enum to differentiate them -/// The `IPCamera`'s string follows the pattern -/// ```.ignore -/// ://:/ -/// ``` -/// but please consult the manufacturer's specification for more details. -/// The index is a standard webcam index. -#[derive(Clone, Debug, PartialEq)] -pub enum CameraIndexType { +/// A webcam index that supports both strings and integers. Most backends take an int, but `IPCamera`s take a URL (string). +#[derive(Clone, Debug, Hash, PartialEq, PartialOrd)] +pub enum CameraIndex<'a> { Index(u32), - IPCamera(String), + String(Cow<'a, str>), } -impl Display for CameraIndexType { +impl<'a> CameraIndex<'a> { + pub fn as_index(&self) -> Result { + match self { + CameraIndex::Index(i) => Ok(*i), + CameraIndex::String(s) => match s.parse::() { + Ok(p) => Ok(p), + Err(why) => Err(NokhwaError::GetPropertyError { + property: "index-int".to_string(), + error: why.to_string(), + }), + }, + } + } +} + +impl<'a> Display for CameraIndex<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - CameraIndexType::Index(idx) => { + CameraIndex::Index(idx) => { write!(f, "{}", idx) } - CameraIndexType::IPCamera(ip) => { + CameraIndex::String(ip) => { write!(f, "{}", ip) } } } } +impl<'a> From for CameraIndex<'a> { + fn from(v: u32) -> Self { + CameraIndex::Index(v) + } +} + +/// Trait for strings that can be converted to [`CameraIndex`]es. +pub trait ValidString: AsRef {} + +impl ValidString for String {} +impl<'a> ValidString for &'a String {} +impl<'a> ValidString for &'a mut String {} +impl<'a> ValidString for Cow<'a, str> {} +impl<'a> ValidString for &'a Cow<'a, str> {} +impl<'a> ValidString for &'a mut Cow<'a, str> {} +impl<'a> ValidString for &'a str {} +impl<'a> ValidString for &'a mut str {} + +impl<'a, T> From for CameraIndex<'a> +where + T: ValidString + 'a, +{ + fn from(v: T) -> Self { + CameraIndex::String(Cow::from(v.as_ref().to_string())) + } +} + /// Converts a MJPEG stream of [u8] into a Vec of RGB888. (R,G,B,R,G,B,...) /// # Errors /// If `mozjpeg` fails to read scanlines or setup the decompressor, this will error. /// # Safety /// This function uses `unsafe`. The caller must ensure that: /// - The input data is of the right size, does not exceed bounds, and/or the final size matches with the initial size. -#[cfg(feature = "decoding")] -#[cfg_attr(feature = "docs-features", doc(cfg(feature = "decoding")))] pub fn mjpeg_to_rgb888(data: &[u8]) -> Result, NokhwaError> { use mozjpeg::Decompress; @@ -1103,12 +1136,8 @@ pub fn mjpeg_to_rgb888(data: &[u8]) -> Result, NokhwaError> { /// Converts a YUYV 4:2:2 datastream to a RGB888 Stream. [For further reading](https://en.wikipedia.org/wiki/YUV#Converting_between_Y%E2%80%B2UV_and_RGB) /// # Errors /// This may error when the data stream size is not divisible by 4, a i32 -> u8 conversion fails, or it fails to read from a certain index. -#[cfg(any(not(target_family = "wasm"), feature = "decoding"))] -#[cfg_attr(feature = "docs-features", doc(cfg(feature = "decoding")))] #[inline] pub fn yuyv422_to_rgb888(data: &[u8]) -> Result, NokhwaError> { - use std::convert::TryFrom; - let mut rgb_vec: Vec = vec![]; if data.len() % 4 == 0 { for px_idx in (0..data.len()).step_by(4) { @@ -1210,8 +1239,6 @@ pub fn yuyv422_to_rgb888(data: &[u8]) -> Result, NokhwaError> { #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] #[must_use] -#[cfg(any(not(target_family = "wasm"), feature = "decoding"))] -#[cfg_attr(feature = "docs-features", doc(cfg(feature = "decoding")))] #[inline] pub fn yuyv444_to_rgb888(y: i32, u: i32, v: i32) -> [u8; 3] { let c298 = (y - 16) * 298;