diff --git a/Cargo.toml b/Cargo.toml index c585248..52b7197 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nokhwa" -version = "0.2.0" +version = "0.3.0" authors = ["l1npengtul "] edition = "2018" description = "A Simple-to-use, cross-platform Rust Webcam Capture Library" @@ -18,15 +18,18 @@ input-v4l = ["v4l"] input-opencv = ["opencv", "opencv/clang-runtime"] input-ipcam = ["input-opencv"] input-msmf = ["nokhwa-bindings-windows"] -input-ffmpeg = ["rusty_ffmpeg"] +input-gst = ["gstreamer", "glib", "gstreamer-app"] output-wgpu = ["wgpu"] -docs-only = ["input-uvc", "input-v4l", "input-opencv", "input-ipcam", "input-ffmpeg", "output-wgpu"] +output-threaded = ["parking_lot"] +docs-only = ["input-uvc", "input-v4l", "input-opencv", "input-ipcam", "input-gst", "output-wgpu", "output-threaded"] [dependencies] thiserror = "1.0.24" image = "0.23.14" mozjpeg = "0.8.24" flume = "0.10.5" +usb_enumeration = "0.1.2" +paste = "1.0.5" [dependencies.v4l] version = "0.12.1" @@ -53,8 +56,20 @@ optional = true version = "0.1.0" optional = true -[dependencies.rusty_ffmpeg] -version = "0.7.0" +[dependencies.gstreamer] +version = "0.16.7" +optional = true + +[dependencies.gstreamer-app] +version = "0.16.5" +optional = true + +[dependencies.glib] +version = "0.14.0" +optional = true + +[dependencies.parking_lot] +version = "0.11.1" optional = true [package.metadata.docs.rs] diff --git a/README.md b/README.md index 5afd408..7bef212 100644 --- a/README.md +++ b/README.md @@ -34,14 +34,38 @@ loop { ``` They can be found in the `examples` folder. +## API Support +The table below lists current Nokhwa API support. +- The `Backend` column signifies the backend. +- The `Input` column signifies reading frames from the camera +- The `Query` column signifies system device list support +- The `Query-Device` column signifies reading device capabilities +- The `OS` column signifies what OS this is availible on. + +| Backend | Input | Query | Query-Device | OS | +|--------------------------------|--------------------|--------------------|--------------------|---------------------| +| Video4Linux(`input-v4l`) | :white_check_mark: | :white_check_mark: | :white_check_mark: | Linux | +| libuvc(`input-uvc`) | :white_check_mark: | :white_check_mark: | :white_check_mark: | Linux, Windows, Mac | +| OpenCV(`input-opencv`) | :white_check_mark: | :x: | :x: | Linux, Windows, Mac | +| IPCamera(`input-ipcam`/OpenCV) | :white_check_mark: | :x: | :x: | Linux, Windows, Mac | +| GStreamer(`input-gst`) | :white_check_mark: | :white_check_mark: | :white_check_mark: | Linux, Windows, Mac | +| FFMpeg | * | * | * | Linux, Windows, Mac | +| AVFoundation | * | * | * | Mac | +| MSMF | * | * | * | Windows | +| JS/WASM | * | * | * | Web | + +:white_check_mark: : Working, :warning: : Experimental, :x: : Not Supported, *: Planned + ## Feature The default feature includes nothing. Anything starting with `input-*` is a feature that enables the specific backend. +As a general rule of thumb, you would want to keep at least `input-uvc` or other backend that has querying enabled so you can get device information from `nokhwa`. `input-*` features: - `input-v4l`: Enables the `Video4Linux` backend (linux) - `input-uvc`: Enables the `libuvc` backend (cross-platform, libuvc statically-linked) - `input-opencv`: Enables the `opencv` backend (cross-platform) - `input-ipcam`: Enables the use of IP Cameras, please see the `NetworkCamera` struct. Note that this relies on `opencv`, so it will automatically enable the `input-opencv` feature. + - `input-gst`: Enables the `gstreamer` backend (cross-platform). Conversely, anything that starts with `output-*` controls a feature that controls the output of something (usually a frame from the camera) diff --git a/UPDATE_CHECKLIST.md b/UPDATE_CHECKLIST.md new file mode 100644 index 0000000..58da081 --- /dev/null +++ b/UPDATE_CHECKLIST.md @@ -0,0 +1,5 @@ +- run the example program with all backends, make sure no errors or warnings +- run on windows +- ensure updated documentation by running `cargo doc` and reading the README +- update the version number in Cargo.toml +- do this one more time \ No newline at end of file diff --git a/build.rs b/build.rs deleted file mode 100644 index dfcd030..0000000 --- a/build.rs +++ /dev/null @@ -1,5 +0,0 @@ -fn main() { - if cfg!(feature = "input-ffmpeg") && option_env!("FFMPEG_PKG_CONFIG_PATH").is_none() { - std::env::set_var("FFMPEG_PKG_CONFIG_PATH", ""); - } -} diff --git a/examples/capture/Cargo.toml b/examples/capture/Cargo.toml index f4d03ce..8dfec2b 100644 --- a/examples/capture/Cargo.toml +++ b/examples/capture/Cargo.toml @@ -7,7 +7,12 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap = "2.33.3" +glium = "0.30.0" +glutin = "0.27.0" +flume = "0.10.7" +# Use these as you need [dependencies.nokhwa] path = "../../../nokhwa" -features = ["input-uvc", "input-v4l"] \ No newline at end of file +features = ["input-uvc", "input-v4l", "input-ipcam", "input-opencv", "input-gst"] diff --git a/examples/capture/src/main.rs b/examples/capture/src/main.rs index 6fe32e2..0598602 100644 --- a/examples/capture/src/main.rs +++ b/examples/capture/src/main.rs @@ -1,17 +1,307 @@ -use nokhwa::{Camera, CameraFormat, CaptureAPIBackend, FrameFormat}; +use clap::{App, Arg}; +use glium::{ + implement_vertex, index::PrimitiveType, program, texture::RawImage2d, uniform, Display, + IndexBuffer, Surface, Texture2d, VertexBuffer, +}; +use glutin::{event_loop::EventLoop, window::WindowBuilder, ContextBuilder}; +use nokhwa::{query_devices, Camera, CaptureAPIBackend, FrameFormat, NetworkCamera}; +use std::time::Instant; + +#[derive(Copy, Clone)] +pub struct Vertex { + position: [f32; 2], + tex_coords: [f32; 2], +} fn main() { - // set up the Camera - let mut camera = Camera::new( - 0, - Some(CameraFormat::new_from(640, 480, FrameFormat::MJPEG, 30)), - CaptureAPIBackend::Auto, - ) - .unwrap(); - // open stream - camera.open_stream().unwrap(); - loop { - let frame = camera.get_frame().unwrap(); - println!("{}, {}", frame.width(), frame.height()); + let matches = App::new("nokhwa-example") + .version("0.1.0") + .author("l1npengtul and the Nokhwa Contributers") + .about("Example program using Nokhwa") + .arg(Arg::with_name("query") + .short("q") + .long("query") + .value_name("BACKEND") + // TODO: Update as new backends are added! + .help("Query the system? Pass AUTO for automatic backend, UVC to query using UVC, V4L to query using Video4Linux, GST to query using Gstreamer.. Will post the list of availible devices.") + .takes_value(true)) + .arg(Arg::with_name("capture") + .short("c") + .long("capture") + .value_name("LOCATION") + .help("Capture from device? Pass the device index or string. Defaults to 0. If the input is not a number, it will be assumed an IPCamera") + .default_value("0") + .takes_value(true)) + .arg(Arg::with_name("width") + .short("w") + .long("width") + .value_name("WIDTH") + .help("Set width of capture. Does nothing if -c flag is not set.") + .default_value("640") + .takes_value(true)) + .arg(Arg::with_name("height") + .short("h") + .long("height") + .value_name("HEIGHT") + .help("Set height of capture. Does nothing if -c flag is not set.") + .default_value("480") + .takes_value(true)) + .arg(Arg::with_name("framerate") + .short("rate") + .long("framerate") + .value_name("FRAMES_PER_SECOND") + .help("Set FPS of capture. Does nothing if -c flag is not set.") + .default_value("15") + .takes_value(true)) + .arg(Arg::with_name("format") + .short("l") + .long("format") + .value_name("FORMAT") + .help("Set format of capture. Does nothing if -c flag is not set. Possible values are MJPG and YUYV. Will be ignored if not either. Ignored by GStreamer backend.") + .default_value("MJPG") + .takes_value(true)) + .arg(Arg::with_name("capture-backend") + .short("b") + .long("backend") + .value_name("BACKEND") + .help("Set the capture backend. Pass AUTO for automatic backend, UVC to query using UVC, V4L to query using Video4Linux, GST to query using Gstreamer, OPENCV to use OpenCV.") + .default_value("AUTO") + .takes_value(true)) + .arg(Arg::with_name("display") + .short("d") + .long("display") + .help("Pass to open a window and display.") + .takes_value(false)).get_matches(); + + // Query example + if matches.is_present("query") { + let backend_value = matches.value_of("query").unwrap(); + let mut use_backend = CaptureAPIBackend::Auto; + // AUTO + if backend_value == "AUTO" { + use_backend = CaptureAPIBackend::Auto; + } else if backend_value == "UVC" { + use_backend = CaptureAPIBackend::UniversalVideoClass; + } else if backend_value == "GST" { + use_backend = CaptureAPIBackend::GStreamer; + } else if backend_value == "V4L" { + use_backend = CaptureAPIBackend::Video4Linux; + } + + match query_devices(use_backend) { + Ok(devs) => { + for (idx, camera) in devs.iter().enumerate() { + println!("Device at index {}: {}", idx, camera) + } + } + Err(why) => { + println!("Failed to query: {}", why.to_string()) + } + } + } + + if matches.is_present("capture") { + let backend_value = { + match matches.value_of("capture-backend").unwrap() { + "UVC" => CaptureAPIBackend::UniversalVideoClass, + "GST" => CaptureAPIBackend::GStreamer, + "V4L" => CaptureAPIBackend::Video4Linux, + "OPENCV" => CaptureAPIBackend::OpenCv, + _ => CaptureAPIBackend::Auto, + } + }; + let width = matches + .value_of("width") + .unwrap() + .trim() + .parse::() + .expect("Width must be a u32!"); + let height = matches + .value_of("height") + .unwrap() + .trim() + .parse::() + .expect("Height must be a u32!"); + let fps = matches + .value_of("framerate") + .unwrap() + .trim() + .parse::() + .expect("Framerate must be a u32!"); + let format = { + match matches.value_of("format").unwrap() { + "YUYV" => FrameFormat::YUYV, + _ => FrameFormat::MJPEG, + } + }; + + let matches_clone = matches.clone(); + + let (send, recv) = flume::unbounded(); + // spawn a thread for capture + std::thread::spawn(move || { + // Index camera + if let Ok(index) = matches_clone + .value_of("capture") + .unwrap() + .trim() + .parse::() + { + let mut camera = + Camera::new_with(index, width, height, fps, format, backend_value).unwrap(); + // open stream + camera.open_stream().unwrap(); + loop { + let frame = camera.get_frame().unwrap(); + send.send(frame).unwrap() + } + } + // IP Camera + else { + let ip_camera = + NetworkCamera::new(matches_clone.value_of("capture").unwrap().to_string()) + .expect("Invalid IP!"); + ip_camera.open_stream().unwrap(); + loop { + let frame = ip_camera.get_frame().unwrap(); + send.send(frame).unwrap(); + } + } + }); + + // run glium + if matches.is_present("display") { + let gl_event_loop = EventLoop::new(); + let window_builder = WindowBuilder::new(); + let context_builder = ContextBuilder::new().with_vsync(true); + let gl_display = Display::new(window_builder, context_builder, &gl_event_loop).unwrap(); + + implement_vertex!(Vertex, position, tex_coords); + + let vert_buffer = VertexBuffer::new( + &gl_display, + &[ + Vertex { + position: [-1.0, -1.0], + tex_coords: [0.0, 0.0], + }, + Vertex { + position: [-1.0, 1.0], + tex_coords: [0.0, 1.0], + }, + Vertex { + position: [1.0, 1.0], + tex_coords: [1.0, 1.0], + }, + Vertex { + position: [1.0, -1.0], + tex_coords: [1.0, 0.0], + }, + ], + ) + .unwrap(); + + let idx_buf = IndexBuffer::new( + &gl_display, + PrimitiveType::TriangleStrip, + &[1 as u16, 2, 0, 3], + ) + .unwrap(); + + let program = program!(&gl_display, + 140 => { + vertex: " + #version 140 + uniform mat4 matrix; + in vec2 position; + in vec2 tex_coords; + out vec2 v_tex_coords; + void main() { + gl_Position = matrix * vec4(position, 0.0, 1.0); + v_tex_coords = tex_coords; + } + ", + + fragment: " + #version 140 + uniform sampler2D tex; + in vec2 v_tex_coords; + out vec4 f_color; + void main() { + f_color = texture(tex, v_tex_coords); + } + " + }, + ) + .unwrap(); + + // run the event loop + + gl_event_loop.run(move |event, _window, ctrl| { + let before_capture = Instant::now(); + let frame = recv.recv().unwrap(); + let after_capture = Instant::now(); + + let width = &frame.width(); + let height = &frame.height(); + + let raw_data = RawImage2d::from_raw_rgb(frame.into_raw(), (*width, *height)); + let gl_texture = Texture2d::new(&gl_display, raw_data).unwrap(); + + let uniforms = uniform! { + matrix: [ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0f32] + ], + tex: &gl_texture + }; + + let mut target = gl_display.draw(); + target.clear_color(0.0, 0.0, 0.0, 0.0); + target + .draw( + &vert_buffer, + &idx_buf, + &program, + &uniforms, + &Default::default(), + ) + .unwrap(); + target.finish().unwrap(); + + match event { + glutin::event::Event::WindowEvent { event, .. } => match event { + glutin::event::WindowEvent::CloseRequested => { + *ctrl = glutin::event_loop::ControlFlow::Exit; + } + _ => {} + }, + _ => {} + } + + println!( + "Took {}ms to capture", + after_capture.duration_since(before_capture).as_millis() + ) + }) + } + // dont + else { + loop { + if let Ok(frame) = recv.recv() { + println!( + "Frame width {} height {} size {}", + frame.width(), + frame.height(), + frame.len() + ); + } else { + println!("Thread terminated, closing!"); + break; + } + } + } } } diff --git a/src/backends/capture/gst_backend.rs b/src/backends/capture/gst_backend.rs new file mode 100644 index 0000000..2961ad3 --- /dev/null +++ b/src/backends/capture/gst_backend.rs @@ -0,0 +1,384 @@ +use crate::{ + CameraFormat, CameraInfo, CaptureAPIBackend, CaptureBackendTrait, FrameFormat, NokhwaError, + Resolution, +}; +use gstreamer::{ + glib::Cast, Caps, ClockTime, DeviceExt, DeviceMonitor, DeviceMonitorExt, + DeviceMonitorExtManual, Element, ElementExtManual, GstBinExt, State, +}; +use gstreamer_app::AppSink; +use image::{ImageBuffer, Rgb}; +use std::{collections::HashMap, str::FromStr}; + +/// The backend struct that interfaces with GStreamer. +/// To see what this does, please see [`CaptureBackendTrait`]. +/// # Quirks +/// - [`FrameFormat`]s are **not** respected. +/// - `Drop`-ing this may cause a `panic`. +pub struct GStreamerCaptureDevice { + pipeline: Element, + app_sink: AppSink, + camera_format: CameraFormat, + camera_info: CameraInfo, +} + +impl GStreamerCaptureDevice { + pub fn new(index: usize, cam_fmt: Option) -> Result { + let camera_format = match cam_fmt { + Some(fmt) => fmt, + None => CameraFormat::default(), + }; + + if let Err(why) = gstreamer::init() { + return Err(NokhwaError::CouldntOpenDevice(format!( + "Failed to initialize GStreamer: {}", + why.to_string() + ))); + } + + let camera_info = { + let device_monitor = DeviceMonitor::new(); + let video_caps = match Caps::from_str("video/x-raw") { + Ok(cap) => cap, + Err(why) => { + return Err(NokhwaError::GeneralError(format!( + "Failed to generate caps: {}", + why.to_string() + ))) + } + }; + let _video_filter_id = match device_monitor + .add_filter(Some("Video/Source"), Some(&video_caps)) + { + Some(id) => id, + None => return Err(NokhwaError::CouldntOpenDevice( + "Failed to generate Device Monitor Filter ID with video/x-raw and Video/Source" + .to_string(), + )), + }; + if let Err(why) = device_monitor.start() { + return Err(NokhwaError::CouldntOpenDevice(format!( + "Failed to start device monitor: {}", + why.to_string() + ))); + } + let device = match device_monitor.get_devices().get(index) { + Some(dev) => dev.clone(), + None => { + return Err(NokhwaError::CouldntOpenDevice(format!( + "Failed to find device at index {}", + index + ))) + } + }; + device_monitor.stop(); + + CameraInfo::new( + DeviceExt::get_display_name(&device).to_string(), + DeviceExt::get_device_class(&device).to_string(), + "".to_string(), + index, + ) + }; + + let pipeline = match gstreamer::parse_launch(&*webcam_pipeline( + camera_format.width(), + camera_format.height(), + camera_format.framerate(), + format!("{}", index).as_str(), + )) { + Ok(pl) => pl, + Err(why) => { + return Err(NokhwaError::CouldntOpenDevice(format!( + "Failed to initialize GStreamer Pipeline with string {} : {}", + webcam_pipeline( + camera_format.width(), + camera_format.height(), + camera_format.framerate(), + format!("{}", index).as_str() + ), + why.to_string(), + ))); + } + }; + + let sink = { + let bin = match pipeline.clone().dynamic_cast::() { + Ok(bn) => bn, + Err(_) => { + return Err(NokhwaError::CouldntOpenDevice( + "Failed to cast Element to Bin".to_string(), + )); + } + }; + + let snk = match bin.get_by_name("appsink") { + Some(sk) => sk, + None => { + return Err(NokhwaError::CouldntOpenDevice( + "Failed to cast get appsink from pipeline!".to_string(), + )); + } + }; + snk + }; + let app_sink = match sink.clone().dynamic_cast::() { + Ok(ap_sk) => ap_sk, + Err(_) => { + return Err(NokhwaError::CouldntOpenDevice( + "Failed to cast Element to AppSink".to_string(), + )); + } + }; + + Ok(GStreamerCaptureDevice { + pipeline, + app_sink, + camera_format, + camera_info, + }) + } + + pub fn new_with(index: usize, 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)) + } + + /// Regenerates the GStreamer Pipeline. Mostly used by internal functions, although made available for your convenience. + /// Equivalent to calling [`GStreamerCaptureDevice::new`] but it sets on the current backend object. + /// # Errors + /// If the GStreamer fails to capture the object or + pub fn regenerate_pipeline(&mut self) -> Result<(), NokhwaError> { + let pipeline = match gstreamer::parse_launch(&*webcam_pipeline( + self.camera_format.width(), + self.camera_format.height(), + self.camera_format.framerate(), + format!("{}", self.camera_info.index()).as_str(), + )) { + Ok(pl) => pl, + Err(why) => { + return Err(NokhwaError::CouldntOpenDevice(format!( + "Failed to initialize GStreamer Pipeline with string {} : {}", + webcam_pipeline( + self.camera_format.width(), + self.camera_format.height(), + self.camera_format.framerate(), + format!("{}", self.camera_info.index()).as_str() + ), + why.to_string(), + ))); + } + }; + + let sink = { + let bin = match pipeline.clone().dynamic_cast::() { + Ok(bn) => bn, + Err(_) => { + return Err(NokhwaError::CouldntOpenDevice( + "Failed to cast Element to Bin".to_string(), + )); + } + }; + + let snk = match bin.get_by_name("appsink") { + Some(sk) => sk, + None => { + return Err(NokhwaError::CouldntOpenDevice( + "Failed to cast get appsink from pipeline!".to_string(), + )); + } + }; + snk + }; + let app_sink = match sink.clone().dynamic_cast::() { + Ok(ap_sk) => ap_sk, + Err(_) => { + return Err(NokhwaError::CouldntOpenDevice( + "Failed to cast Element to AppSink".to_string(), + )); + } + }; + + self.app_sink = app_sink; + self.pipeline = pipeline; + + Ok(()) + } +} + +impl CaptureBackendTrait for GStreamerCaptureDevice { + fn camera_info(&self) -> CameraInfo { + self.camera_info.clone() + } + + fn camera_format(&self) -> CameraFormat { + self.camera_format + } + + fn set_camera_format(&mut self, new_fmt: CameraFormat) -> Result<(), NokhwaError> { + self.camera_format = new_fmt; + self.regenerate_pipeline()?; + if self.is_stream_open() { + self.open_stream()?; + } + Ok(()) + } + + fn get_compatible_list_by_resolution( + &self, + _fourcc: FrameFormat, + ) -> Result>, NokhwaError> { + Err(NokhwaError::UnsupportedOperation( + CaptureAPIBackend::GStreamer, + )) + } + + fn get_compatible_fourcc(&mut self) -> Result, NokhwaError> { + Err(NokhwaError::UnsupportedOperation( + CaptureAPIBackend::GStreamer, + )) + } + + fn resolution(&self) -> Resolution { + self.camera_format.resolution() + } + + fn set_resolution(&mut self, new_res: Resolution) -> Result<(), NokhwaError> { + let mut new_fmt = self.camera_format; + new_fmt.set_resolution(new_res); + self.set_camera_format(new_fmt) + } + + fn frame_rate(&self) -> u32 { + self.camera_format.framerate() + } + + fn set_framerate(&mut self, new_fps: u32) -> Result<(), NokhwaError> { + let mut new_fmt = self.camera_format; + new_fmt.set_framerate(new_fps); + self.set_camera_format(new_fmt) + } + + fn frameformat(&self) -> FrameFormat { + self.camera_format.format() + } + + fn set_frameformat(&mut self, _fourcc: FrameFormat) -> Result<(), NokhwaError> { + Err(NokhwaError::UnsupportedOperation( + CaptureAPIBackend::GStreamer, + )) + } + + fn open_stream(&mut self) -> Result<(), NokhwaError> { + if let Err(why) = self.pipeline.set_state(State::Playing) { + return Err(NokhwaError::CouldntOpenStream(format!( + "Failed to set appsink to playing: {}", + why.to_string() + ))); + } + Ok(()) + } + + // TODO: someone validate this + fn is_stream_open(&self) -> bool { + let (res, state_from, state_to) = self.pipeline.get_state(ClockTime::none()); + return match res { + Ok(_) => { + if state_to == State::Playing { + return true; + } + false + } + Err(_) => { + if state_from == State::Playing { + return true; + } + false + } + }; + } + + fn get_frame(&mut self) -> Result, Vec>, NokhwaError> { + let image_data = self.get_frame_raw()?; + let cam_fmt = self.camera_format; + let imagebuf = + match ImageBuffer::from_vec(cam_fmt.width(), cam_fmt.height(), image_data) { + Some(buf) => { + let rgbbuf: ImageBuffer, Vec> = buf; + rgbbuf + } + None => return Err(NokhwaError::CouldntCaptureFrame( + "Imagebuffer is not large enough! This is probably a bug, please report it!" + .to_string(), + )), + }; + Ok(imagebuf) + } + + fn get_frame_raw(&mut self) -> Result, NokhwaError> { + if self.is_stream_open() { + return Err(NokhwaError::CouldntCaptureFrame( + "Please call `open_stream` first!".to_string(), + )); + } + if self.app_sink.is_eos() { + return Err(NokhwaError::CouldntCaptureFrame( + "Stream is EOS!".to_string(), + )); + } + match self.app_sink.pull_sample() { + Ok(sample) => match sample.get_buffer_owned() { + Some(buf) => match buf.into_mapped_buffer_readable() { + Ok(readable) => Ok(readable.as_slice().to_vec()), + Err(_) => { + return Err(NokhwaError::CouldntCaptureFrame( + "Sample Buffer get mapped readable fail!".to_string(), + )) + } + }, + None => { + return Err(NokhwaError::CouldntCaptureFrame( + "Sample Buffer get fail!".to_string(), + )) + } + }, + Err(why) => { + return Err(NokhwaError::CouldntCaptureFrame(format!( + "Failed to pull sample from appsink: {}", + why.to_string() + ))) + } + } + } + + fn stop_stream(&mut self) -> Result<(), NokhwaError> { + if let Err(why) = self.pipeline.set_state(State::Null) { + return Err(NokhwaError::CouldntStopStream(format!( + "Could not change state: {}", + why.to_string() + ))); + } + Ok(()) + } +} + +impl Drop for GStreamerCaptureDevice { + fn drop(&mut self) { + self.pipeline.set_state(State::Null).unwrap(); + } +} + +#[cfg(target_os = "linux")] +fn webcam_pipeline(width: u32, height: u32, fps: u32, camera_location: &str) -> String { + format!("v4l2src device=/dev/video{} ! video/x-raw,width={},height={},format=RGB,framerate={}/1 ! appsink name=appsink async=false sync=false", camera_location, width, height, fps) +} + +#[cfg(target_os = "windows")] +fn webcam_pipeline(width: u32, height: u32, fps: u32, camera_location: &str) -> String { + format!("ksvideosrc device_index={} ! video/x-raw,width={},height={},format=RGB,framerate={}/1 ! appsink name=appsink async=false sync=false", camera_location, width, height, fps) +} + +#[cfg(target_os = "macos")] +fn webcam_pipeline(width: u32, height: u32, fps: u32, camera_location: &str) -> String { + format!("autovideosrc location=/dev/video{} ! video/x-raw,width={},height={},format=RGB,framerate={}/1 ! appsink name=appsink async=false sync=false", camera_location, width, height, fps) +} diff --git a/src/backends/capture/mod.rs b/src/backends/capture/mod.rs index 25751d8..c96133a 100644 --- a/src/backends/capture/mod.rs +++ b/src/backends/capture/mod.rs @@ -10,6 +10,10 @@ pub use uvc_backend::UVCCaptureDevice; mod msmf; #[cfg(feature = "input-msmf")] pub use msmf::MediaFoundationCaptureDevice; +#[cfg(feature = "input-gst")] +mod gst_backend; +#[cfg(feature = "input-gst")] +pub use gst_backend::GStreamerCaptureDevice; #[cfg(feature = "input-opencv")] mod opencv_backend; #[cfg(feature = "input-opencv")] diff --git a/src/backends/capture/opencv_backend.rs b/src/backends/capture/opencv_backend.rs index 608be7e..fd1ea11 100644 --- a/src/backends/capture/opencv_backend.rs +++ b/src/backends/capture/opencv_backend.rs @@ -3,13 +3,14 @@ use crate::{ NokhwaError, Resolution, }; use image::{ImageBuffer, Rgb}; -use opencv::videoio::{CAP_AVFOUNDATION, CAP_MSMF}; + use opencv::{ core::{ToInputArray, ToOutputArray, Vector}, imgproc::{cvt_color, ColorConversionCodes}, types::VectorOfu8, videoio::{ - VideoCapture, VideoCaptureTrait, VideoWriter, CAP_ANY, CAP_DSHOW, CAP_PROP_FOURCC, CAP_V4L2, + VideoCapture, VideoCaptureTrait, VideoWriter, CAP_ANY, CAP_AVFOUNDATION, CAP_DSHOW, + CAP_PROP_FOURCC, CAP_V4L2, }, videoio::{CAP_PROP_FPS, CAP_PROP_FRAME_HEIGHT, CAP_PROP_FRAME_WIDTH}, }; @@ -142,6 +143,17 @@ impl OpenCvCaptureDevice { 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. + /// # 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) + } + /// Gets weather said capture device is an `IPCamera`. pub fn is_ip_camera(&self) -> bool { match self.camera_location { diff --git a/src/backends/capture/uvc_backend.rs b/src/backends/capture/uvc_backend.rs index e66c170..712070a 100644 --- a/src/backends/capture/uvc_backend.rs +++ b/src/backends/capture/uvc_backend.rs @@ -44,6 +44,7 @@ impl From for StreamFormat { /// - The [create()](UVCCaptureDevice::create()) method will open the device twice. /// - 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()). /// - [`get_frame_raw()`](CaptureBackendTrait::get_frame_raw()) returns the same raw data as [`get_frame()`](CaptureBackendTrait::get_frame()), a.k.a. no custom decoding required, all data is automatically RGB +/// - The [`get_frame_raw()`](CaptureBackendTrait::get_frame_raw()) and by extension [`get_frame()`](CaptureBackendTrait::get_frame()) functions block. /// This backend, once stream is open, will constantly collect frames. When you call [`get_frame()`](CaptureBackendTrait::get_frame()) or one of its variants, it will only give you the latest frame. /// # Safety /// This backend requires use of `unsafe` due to the self-referencing structs involved. @@ -85,7 +86,7 @@ impl<'a> UVCCaptureDevice<'a> { Err(why) => return Err(NokhwaError::CouldntOpenDevice(why.to_string())), }; - let (camera_info, frame_receiver, frame_sender, vendor_id, product_id, serial) = { + let (camera_info, frame_receiver, frame_sender) = { let device_list = match context.devices() { Ok(device_list) => device_list, Err(why) => return Err(NokhwaError::CouldntOpenDevice(why.to_string())), @@ -129,24 +130,11 @@ impl<'a> UVCCaptureDevice<'a> { index, ); - let (vendor_id, product_id, serial) = ( - Some(i32::from(device_desc.product_id)), - Some(i32::from(device_desc.vendor_id)), - device_desc.serial_number, - ); - let (frame_sender, frame_receiver) = { let (a, b) = flume::unbounded::>(); (Box::new(a), Box::new(b)) }; - ( - camera_info, - frame_receiver, - frame_sender, - vendor_id, - product_id, - serial, - ) + (camera_info, frame_receiver, frame_sender) }; let camera_format = match cam_fmt { @@ -165,7 +153,10 @@ impl<'a> UVCCaptureDevice<'a> { device_builder: |context_builder| { Box::new( context_builder - .find_device(vendor_id, product_id, serial.as_deref()) + .devices() + .unwrap() + .into_iter() + .nth(index) .unwrap(), ) }, @@ -402,6 +393,7 @@ impl<'a> CaptureBackendTrait for UVCCaptureDevice<'a> { if ret_2.is_err() { return ret_2; } + self.borrow_active_stream_init().set(true); Ok(()) } @@ -444,7 +436,15 @@ impl<'a> CaptureBackendTrait for UVCCaptureDevice<'a> { let messages_iter = f_recv.drain(); match messages_iter.last() { Some(msg) => Ok(msg), - None => Err(NokhwaError::CouldntCaptureFrame("Too fast!".to_string())), + None => match f_recv.recv() { + Ok(msg) => Ok(msg), + Err(why) => { + return Err(NokhwaError::CouldntCaptureFrame(format!( + "All sender dropped: {}", + why.to_string() + ))) + } + }, } } diff --git a/src/camera.rs b/src/camera.rs index ef29b94..7919708 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,13 +1,9 @@ -#[cfg(feature = "input-uvc")] -use crate::backends::capture::UVCCaptureDevice; -#[cfg(feature = "input-v4l")] -use crate::backends::capture::V4LCaptureDevice; use crate::{ - CameraFormat, CameraInfo, CaptureAPIBackend, CaptureBackendTrait, FrameFormat, NokhwaError, - Resolution, + cap_impl_fn, cap_impl_matches, CameraFormat, CameraInfo, CaptureAPIBackend, + CaptureBackendTrait, FrameFormat, NokhwaError, Resolution, }; use image::{buffer::ConvertBuffer, ImageBuffer, Rgb, RgbaImage}; -use std::{cell::RefCell, collections::HashMap, convert::TryFrom, num::NonZeroU32}; +use std::{cell::RefCell, collections::HashMap}; #[cfg(feature = "output-wgpu")] use wgpu::{ Device as WgpuDevice, Extent3d, ImageCopyTexture, ImageDataLayout, Queue as WgpuQueue, @@ -31,75 +27,18 @@ impl Camera { format: Option, backend: CaptureAPIBackend, ) -> Result { - let platform = std::env::consts::OS; - let use_backend = match backend { - CaptureAPIBackend::Auto => { - let mut cap = CaptureAPIBackend::Auto; - if cfg!(feature = "input-v4l") && platform == "linux" { - cap = CaptureAPIBackend::Video4Linux - } else if cfg!(feature = "input-uvc") { - cap = CaptureAPIBackend::UniversalVideoClass; - } - if cap == CaptureAPIBackend::Auto { - return Err(NokhwaError::NotImplemented( - "Platform requirements not satisfied.".to_string(), - )); - } - cap - } - CaptureAPIBackend::Video4Linux => { - if !(cfg!(feature = "input-v4l") && platform == "linux") { - return Err(NokhwaError::NotImplemented( - "V4L Requirements: Linux and `input-v4l`.".to_string(), - )); - } - CaptureAPIBackend::Video4Linux - } - CaptureAPIBackend::UniversalVideoClass => { - if !(cfg!(feature = "input-uvc")) { - return Err(NokhwaError::NotImplemented( - "UVC Requirements: `input-uvc`.".to_string(), - )); - } - CaptureAPIBackend::UniversalVideoClass - } - _ => return Err(NokhwaError::NotImplemented(backend.to_string())), - }; - - let capture_backend = match use_backend { - CaptureAPIBackend::Video4Linux => match init_v4l(index, format) { - Some(capture) => match capture { - Ok(cap_back) => cap_back, - Err(why) => return Err(why), - }, - None => { - return Err(NokhwaError::NotImplemented( - "Platform requirements not satisfied.".to_string(), - )); - } - }, - CaptureAPIBackend::UniversalVideoClass => match init_uvc(index, format) { - Some(capture) => match capture { - Ok(cap_back) => cap_back, - Err(why) => return Err(why), - }, - None => { - return Err(NokhwaError::NotImplemented( - "Platform requirements not satisfied.".to_string(), - )); - } - }, - _ => { - return Err(NokhwaError::NotImplemented( - "Platform requirements not satisfied.".to_string(), - )); - } + let camera_backend = cap_impl_matches! { + backend, index, format, + ("input-v4l", Video4Linux, init_v4l), + ("input-uvc", UniversalVideoClass, init_uvc), + ("input-gst", GStreamer, init_gst), + ("input-opencv", OpenCv, init_opencv) }; Ok(Camera { idx: index, - backend: RefCell::new(capture_backend), - backend_api: use_backend, + backend: RefCell::new(camera_backend), + backend_api: backend, }) } @@ -262,9 +201,7 @@ impl Camera { buffer.copy_from_slice(&frame_data); Ok(bytes) } - /// Will drop the stream. - /// # Errors - /// Please check the `Quirks` section of each backend. + #[cfg(feature = "output-wgpu")] /// Directly copies a frame to a Wgpu texture. This will automatically convert the frame into a RGBA frame. /// # Errors @@ -330,44 +267,32 @@ impl Camera { } } -#[cfg(feature = "input-v4l")] -#[allow(clippy::unnecessary_wraps)] -fn init_v4l( - idx: usize, - setting: Option, -) -> Option, NokhwaError>> { - match V4LCaptureDevice::new(idx, setting) { - Ok(cap) => Some(Ok(Box::new(cap))), - Err(why) => Some(Err(why)), +// TODO: Update as we go +fn figure_out_auto() -> Option { + let platform = std::env::consts::OS; + let mut cap = CaptureAPIBackend::Auto; + if cfg!(feature = "input-v4l") && platform == "linux" { + cap = CaptureAPIBackend::Video4Linux + } else if cfg!(feature = "input-msmf") && platform == "windows" { + cap = CaptureAPIBackend::Windows + } else if cfg!(feature = "input-avfoundationn") && platform == "mac" { + cap = CaptureAPIBackend::AVFoundation + } else if cfg!(feature = "input-uvc") { + cap = CaptureAPIBackend::UniversalVideoClass; + } else if cfg!(feature = "input-gst") { + cap = CaptureAPIBackend::GStreamer; + } else if cfg!(feature = "input-opencv") { + cap = CaptureAPIBackend::OpenCv; } -} - -#[cfg(not(feature = "input-v4l"))] -#[allow(clippy::unnecessary_wraps)] -fn init_v4l( - _idx: usize, - _setting: Option, -) -> Option, NokhwaError>> { - None -} - -#[cfg(feature = "uvc")] -#[allow(clippy::unnecessary_wraps)] -fn init_uvc( - idx: usize, - setting: Option, -) -> Option, NokhwaError>> { - match UVCCaptureDevice::create(idx, setting) { - Ok(cap) => Some(Ok(Box::new(cap))), - Err(why) => Some(Err(why)), + if cap == CaptureAPIBackend::Auto { + return None; } + Some(cap) } -#[cfg(not(feature = "input-uvc"))] -#[allow(clippy::unnecessary_wraps)] -fn init_uvc( - _idx: usize, - _setting: Option, -) -> Option, NokhwaError>> { - None +cap_impl_fn! { + (GStreamerCaptureDevice, new, "input-gst", gst), + (OpenCvCaptureDevice, new_autopref, "input-opencv", opencv), + (V4LCaptureDevice, new, "input-v4l", v4l), + (UVCCaptureDevice, create, "input-uvc", uvc) } diff --git a/src/camera_traits.rs b/src/camera_traits.rs index 2ec124d..25a8415 100644 --- a/src/camera_traits.rs +++ b/src/camera_traits.rs @@ -3,7 +3,7 @@ use crate::{ utils::{CameraFormat, CameraInfo, FrameFormat, Resolution}, }; use image::{buffer::ConvertBuffer, ImageBuffer, Rgb, RgbaImage}; -use std::{collections::HashMap, convert::TryFrom, num::NonZeroU32}; +use std::collections::HashMap; #[cfg(feature = "output-wgpu")] use wgpu::{ diff --git a/src/lib.rs b/src/lib.rs index 1b25cb8..a609060 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,56 +1,82 @@ #![deny(clippy::pedantic)] #![warn(clippy::all)] +#![allow(clippy::must_use_candidate)] -//! # nokhwa -//! Nokhwa(녹화): Korean word meaning "to record". -//! -//! A Simple-to-use, cross-platform Rust Webcam Capture Library -//! -//! ## Using nokhwa -//! You will need the latest stable Rust and Cargo. -//! -//! Nokhwa can be added to your crate by adding it to your `Cargo.toml`: -//! ```.ignore -//! [dependencies.nokhwa] -//! // TODO: replace the "*" with the latest version of `nokhwa` -//! version = "*" -//! // TODO: add some features -//! features = [""] -//! ``` -//! -//! Most likely, you will only use functionality provided by the `Camera` struct. If you need lower-level access, you may instead opt to use the raw capture backends found at `nokhwa::backends::capture::*`. -//! ## Feature -//! The default feature includes nothing. Anything starting with `input-*` is a feature that enables the specific backend. -//! -//! `input-*` features: -//! - `input-v4l`: Enables the `Video4Linux` backend (linux) -//! - `input-uvc`: Enables the `libuvc` backend (cross-platform, libuvc statically-linked) -//! - `input-opencv`: Enables the `opencv` backend (cross-platform) -//! - `input-ipcam`: Enables the use of IP Cameras, please see the `NetworkCamera` struct. Note that this relies on `opencv`, so it will automatically enable the `input-opencv` feature. -//! -//! Conversely, anything that starts with `output-*` controls a feature that controls the output of something (usually a frame from the camera) -//! -//! `output-*` features: -//! - `output-wgpu`: Enables the API to copy a frame directly into a `wgpu` texture. -//! -//! You many want to pick and choose to reduce bloat. -//! ## Example -//! ```rust -//! // set up the Camera -//! let mut camera = Camera::new( -//! 0, // index -//! Some(CameraFormat::new_from(640, 480, FrameFormat::MJPEG, 30)), // format -//! CaptureAPIBackend::AUTO, // what backend to use (let nokhwa decide for itself) -//! ) -//! .unwrap(); -//! // open stream -//! camera.open_stream().unwrap(); -//! loop { -//! let frame = camera.get_frame().unwrap(); -//! println!("{}, {}", frame.width(), frame.height()); -//! } -//! ``` -//! They can be found in the `examples` folder. +#[allow(clippy::doc_markdown)] +/// # nokhwa +/// Nokhwa(녹화): Korean word meaning "to record". +/// +/// A Simple-to-use, cross-platform Rust Webcam Capture Library +/// +/// ## Using nokhwa +/// You will need the latest stable Rust and Cargo. +/// +/// Nokhwa can be added to your crate by adding it to your `Cargo.toml`: +/// ```.ignore +/// [dependencies.nokhwa] +/// // TODO: replace the "*" with the latest version of `nokhwa` +/// version = "*" +/// // TODO: add some features +/// features = [""] +/// ``` +/// +/// Most likely, you will only use functionality provided by the `Camera` struct. If you need lower-level access, you may instead opt to use the raw capture backends found at `nokhwa::backends::capture::*`. +/// ## API Support +/// The table below lists current Nokhwa API support. +/// - The `Backend` column signifies the backend. +/// - The `Input` column signifies reading frames from the camera +/// - The `Query` column signifies system device list support +/// - The `Query-Device` column signifies reading device capabilities +/// - The `OS` column signifies what OS this is availible on. +/// +/// | Backend | Input | Query | Query-Device | OS | +/// |--------------------------------|--------------------|--------------------|--------------------|---------------------| +/// | Video4Linux(`input-v4l`) | :white_check_mark: | :white_check_mark: | :white_check_mark: | Linux | +/// | libuvc(`input-uvc`) | :white_check_mark: | :white_check_mark: | :white_check_mark: | Linux, Windows, Mac | +/// | OpenCV(`input-opencv`) | :white_check_mark: | :x: | :x: | Linux, Windows, Mac | +/// | IPCamera(`input-ipcam`/OpenCV) | :white_check_mark: | :x: | :x: | Linux, Windows, Mac | +/// | GStreamer(`input-gst`) | :white_check_mark: | :white_check_mark: | :white_check_mark: | Linux, Windows, Mac | +/// | FFMpeg | * | * | * | Linux, Windows, Mac | +/// | AVFoundation | * | * | * | Mac | +/// | MSMF | * | * | * | Windows | +/// | JS/WASM | * | * | * | Web | +/// +/// :white_check_mark: : Working, :warning: : Experimental, :x: : Not Supported, *: Planned +/// +/// ## Feature +/// The default feature includes nothing. Anything starting with `input-*` is a feature that enables the specific backend. +/// As a general rule of thumb, you would want to keep at least `input-uvc` or other backend that has querying enabled so you can get device information from `nokhwa`. +/// +/// `input-*` features: +/// - `input-v4l`: Enables the `Video4Linux` backend (linux) +/// - `input-uvc`: Enables the `libuvc` backend (cross-platform, libuvc statically-linked) +/// - `input-opencv`: Enables the `opencv` backend (cross-platform) +/// - `input-ipcam`: Enables the use of IP Cameras, please see the `NetworkCamera` struct. Note that this relies on `opencv`, so it will automatically enable the `input-opencv` feature. +/// - `input-gst`: Enables the `gstreamer` backend (cross-platform). +/// +/// Conversely, anything that starts with `output-*` controls a feature that controls the output of something (usually a frame from the camera) +/// +/// `output-*` features: +/// - `output-wgpu`: Enables the API to copy a frame directly into a `wgpu` texture. +/// +/// You many want to pick and choose to reduce bloat. +/// ## Example +/// ```rust +/// // set up the Camera +/// let mut camera = Camera::new( +/// 0, // index +/// Some(CameraFormat::new_from(640, 480, FrameFormat::MJPEG, 30)), // format +/// CaptureAPIBackend::AUTO, // what backend to use (let nokhwa decide for itself) +/// ) +/// .unwrap(); +/// // open stream +/// camera.open_stream().unwrap(); +/// loop { +/// let frame = camera.get_frame().unwrap(); +/// println!("{}, {}", frame.width(), frame.height()); +/// } +/// ``` +/// They can be found in the `examples` folder. /// Raw access to each of Nokhwa's backends. pub mod backends; @@ -63,10 +89,15 @@ mod query; mod utils; #[macro_use] mod macros; +#[cfg(feature = "output-threaded")] +mod threaded_camera; pub use camera::Camera; pub use camera_traits::*; pub use error::NokhwaError; #[cfg(feature = "input-ipcam")] pub use network_camera::NetworkCamera; +pub use query::query_devices; +#[cfg(feature = "output-threaded")] +pub use threaded_camera::ThreadedCamera; pub use utils::*; diff --git a/src/macros.rs b/src/macros.rs index 4c67f86..da6fbbc 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -20,3 +20,111 @@ macro_rules! tryinto_num { } }}; } + +/// Makes `init-*` functions for you. To be used in `camera.rs` +/// Example usage: check `camera.rs` +#[macro_export] +macro_rules! cap_impl_fn { + { + $( ($backend:ty, $init_fn:ident, $feature:expr, $backend_name:ident) ),+ + } => { + $( + paste::paste! { + #[cfg(feature = $feature)] + fn [< init_ $backend_name>](idx: usize, setting: Option) -> Option, NokhwaError>> { + use crate::backends::capture::$backend; + match <$backend>::$init_fn(idx, setting) { + Ok(cap) => Some(Ok(Box::new(cap))), + Err(why) => Some(Err(why)), + } + } + #[cfg(not(feature = $feature))] + fn [< init_ $backend_name>](_idx: usize, _setting: Option) -> Option, NokhwaError>> { + None + } + } + )+ + }; +} + +#[macro_export] +macro_rules! cap_impl_matches { + { + $use_backend: expr, $index:expr, $setting:expr, + $( ($feature:expr, $backend:ident, $fn:ident) ),+ + } => { + { + let i = $index; + let s = $setting; + match $use_backend { + CaptureAPIBackend::Auto => match figure_out_auto() { + Some(cap) => match cap { + $( + CaptureAPIBackend::$backend => { + match cfg!(feature = $feature) { + true => { + match $fn(i,s) { + Some(cap) => match cap { + Ok(c) => c, + Err(why) => return Err(why), + } + None => { + return Err(NokhwaError::NotImplemented( + "Platform requirements not satisfied.".to_string(), + )); + } + } + } + false => { + return Err(NokhwaError::NotImplemented( + "Platform requirements not satisfied.".to_string(), + )); + } + } + } + )+ + _ => { + return Err(NokhwaError::NotImplemented( + "Platform requirements not satisfied.".to_string(), + )); + } + } + None => { + return Err(NokhwaError::NotImplemented( + "Platform requirements not satisfied.".to_string(), + )); + } + } + $( + CaptureAPIBackend::$backend => { + match cfg!(feature = $feature) { + true => { + match $fn(i,s) { + Some(cap) => match cap { + Ok(c) => c, + Err(why) => return Err(why), + } + None => { + return Err(NokhwaError::NotImplemented( + "Platform requirements not satisfied.".to_string(), + )); + } + } + } + false => { + return Err(NokhwaError::NotImplemented( + "Platform requirements not satisfied.".to_string(), + )); + } + } + } + )+ + _ => { + return Err(NokhwaError::NotImplemented( + "Platform requirements not satisfied.".to_string(), + )); + } + } + }; + } +} diff --git a/src/network_camera.rs b/src/network_camera.rs index 30895b0..fcadc39 100644 --- a/src/network_camera.rs +++ b/src/network_camera.rs @@ -1,6 +1,6 @@ use crate::{backends::capture::OpenCvCaptureDevice, CaptureBackendTrait, NokhwaError}; use image::{buffer::ConvertBuffer, ImageBuffer, Rgb, RgbaImage}; -use std::{cell::RefCell, convert::TryFrom, num::NonZeroU32}; +use std::cell::RefCell; #[cfg(feature = "output-wgpu")] use wgpu::{ Device as WgpuDevice, Extent3d, ImageCopyTexture, ImageDataLayout, Queue as WgpuQueue, diff --git a/src/query.rs b/src/query.rs index f89c7f3..f476b1e 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,2 +1,237 @@ -// TODO: Build a cross-platform query struct (preferably using native APIs) -// pub struct Query {} +use crate::{CameraInfo, CaptureAPIBackend, NokhwaError}; +use uvc::Device; + +// TODO: Update as this goes +/// Query the system for a list of available devices. Please refer to the API Backends that support `Query`)
+/// Currently, these are V4L, UVC, and GST.
+/// Usually the order goes Native -> UVC -> Gstreamer. +/// # 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. +#[allow(clippy::module_name_repetitions)] +pub fn query_devices(api: CaptureAPIBackend) -> Result, NokhwaError> { + match api { + CaptureAPIBackend::Auto => { + // determine platform + match std::env::consts::OS { + "linux" => { + if cfg!(feature = "input-v4l") { + query_devices(CaptureAPIBackend::Video4Linux) + } else if cfg!(feature = "input-uvc") { + query_devices(CaptureAPIBackend::UniversalVideoClass) + } else if cfg!(feature = "input-ffmpeg") { + query_devices(CaptureAPIBackend::Ffmpeg) + } else if cfg!(feature = "input-gstreamer") { + query_devices(CaptureAPIBackend::GStreamer) + } else { + Err(NokhwaError::UnsupportedOperation(CaptureAPIBackend::Auto)) + } + } + "windows" => { + if cfg!(feature = "input-msmf") { + query_devices(CaptureAPIBackend::Windows) + } else if cfg!(feature = "input-uvc") { + query_devices(CaptureAPIBackend::UniversalVideoClass) + } else if cfg!(feature = "input-ffmpeg") { + query_devices(CaptureAPIBackend::Ffmpeg) + } else if cfg!(feature = "input-gstreamer") { + query_devices(CaptureAPIBackend::GStreamer) + } else { + Err(NokhwaError::UnsupportedOperation(CaptureAPIBackend::Auto)) + } + } + "macos" => { + if cfg!(feature = "input-avfoundation") { + query_devices(CaptureAPIBackend::AVFoundation) + } else if cfg!(feature = "input-uvc") { + query_devices(CaptureAPIBackend::UniversalVideoClass) + } else if cfg!(feature = "input-ffmpeg") { + query_devices(CaptureAPIBackend::Ffmpeg) + } else if cfg!(feature = "input-gstreamer") { + query_devices(CaptureAPIBackend::GStreamer) + } else { + Err(NokhwaError::UnsupportedOperation(CaptureAPIBackend::Auto)) + } + } + _ => Err(NokhwaError::UnsupportedOperation(CaptureAPIBackend::Auto)), + } + } + CaptureAPIBackend::Video4Linux => query_v4l(), + CaptureAPIBackend::UniversalVideoClass => query_uvc(), + CaptureAPIBackend::Windows => Err(NokhwaError::UnsupportedOperation( + CaptureAPIBackend::Windows, + )), + CaptureAPIBackend::Ffmpeg => { + Err(NokhwaError::UnsupportedOperation(CaptureAPIBackend::Ffmpeg)) + } + CaptureAPIBackend::GStreamer => query_gstreamer(), + _ => Err(NokhwaError::UnsupportedOperation(api)), + } +} + +// TODO: More + +#[cfg(feature = "input-v4l")] +fn query_v4l() -> Result, NokhwaError> { + return Ok({ + let camera_info: Vec = v4l::context::enum_devices() + .iter() + .map(|node| { + CameraInfo::new( + node.name() + .unwrap_or(format!("{}", node.path().to_string_lossy())), + format!("Video4Linux Device @ {}", node.path().to_string_lossy()), + "".to_string(), + node.index(), + ) + }) + .collect(); + camera_info + }); +} + +#[cfg(not(feature = "input-v4l"))] +fn query_v4l() -> Result, NokhwaError> { + Err(NokhwaError::UnsupportedOperation( + CaptureAPIBackend::Video4Linux, + )) +} + +#[cfg(feature = "input-uvc")] +fn query_uvc() -> Result, NokhwaError> { + let context = match uvc::Context::new() { + Ok(ctx) => ctx, + Err(why) => { + return Err(NokhwaError::GeneralError(format!( + "UVC Context failure: {}", + why.to_string() + ))) + } + }; + + let usb_devices = usb_enumeration::enumerate(None, None); + let uvc_devices = match context.devices() { + Ok(devs) => { + let device_vec: Vec = devs.map(|d| d).collect(); + device_vec + } + Err(why) => { + return Err(NokhwaError::GeneralError(format!( + "UVC Context Devicelist failure: {}", + why.to_string() + ))) + } + }; + + let mut camera_info_vec = vec![]; + let mut counter = 0_usize; + + // Optimize this O(n*m) algorithem + for usb_dev in usb_devices.iter() { + for uvc_dev in uvc_devices.iter() { + if let Ok(desc) = uvc_dev.description() { + if desc.product_id == usb_dev.product_id && desc.vendor_id == usb_dev.vendor_id { + let name = usb_dev + .description + .as_ref() + .unwrap_or(&format!( + "{}:{} {} {}", + desc.vendor_id, + desc.product_id, + desc.manufacturer.unwrap_or("Generic".to_string()), + desc.product.unwrap_or("Camera".to_string()) + )) + .clone(); + + camera_info_vec.push(CameraInfo::new( + name.clone(), + usb_dev + .description + .as_ref() + .unwrap_or(&"".to_string()) + .clone(), + format!( + "{}:{} {}", + desc.vendor_id, + desc.product_id, + desc.serial_number.unwrap_or("".to_string()) + ), + counter, + )); + counter += 1; + } + } + } + } + Ok(camera_info_vec) +} + +#[cfg(not(feature = "input-uvc"))] +fn query_uvc() -> Result, NokhwaError> { + Err(NokhwaError::UnsupportedOperation( + CaptureAPIBackend::UniversalVideoClass, + )) +} + +#[cfg(feature = "input-gst")] +fn query_gstreamer() -> Result, NokhwaError> { + use gstreamer::{Caps, DeviceExt, DeviceMonitor, DeviceMonitorExt, DeviceMonitorExtManual}; + use std::str::FromStr; + if let Err(why) = gstreamer::init() { + return Err(NokhwaError::GeneralError(format!( + "Failed to init gstreamer: {}", + why + ))); + } + let device_monitor = DeviceMonitor::new(); + let video_caps = match Caps::from_str("video/x-raw") { + Ok(cap) => cap, + Err(why) => { + return Err(NokhwaError::GeneralError(format!( + "Failed to generate caps: {}", + why.to_string() + ))) + } + }; + let _video_filter_id = match device_monitor.add_filter(Some("Video/Source"), Some(&video_caps)) + { + Some(id) => id, + None => { + return Err(NokhwaError::CouldntOpenDevice( + "Failed to generate Device Monitor Filter ID with video/x-raw and Video/Source" + .to_string(), + )) + } + }; + if let Err(why) = device_monitor.start() { + return Err(NokhwaError::GeneralError(format!( + "Failed to start device monitor: {}", + why.to_string() + ))); + } + let mut counter = 0; + let devices: Vec = device_monitor + .get_devices() + .iter_mut() + .map(|gst_dev| { + let name = DeviceExt::get_display_name(gst_dev); + let class = DeviceExt::get_device_class(gst_dev); + counter += 1; + CameraInfo::new( + name.to_string(), + class.to_string(), + "".to_string(), + counter - 1, + ) + }) + .collect(); + device_monitor.stop(); + Ok(devices) +} + +#[cfg(not(feature = "input-gst"))] +fn query_gstreamer() -> Result, NokhwaError> { + Err(NokhwaError::UnsupportedOperation( + CaptureAPIBackend::GStreamer, + )) +} diff --git a/src/threaded_camera.rs b/src/threaded_camera.rs new file mode 100644 index 0000000..9466057 --- /dev/null +++ b/src/threaded_camera.rs @@ -0,0 +1,16 @@ +use crate::{Camera, NokhwaError}; +use flume::{Receiver, Sender}; +use std::cell::RefCell; +use std::sync::{Arc, Mutex}; +use std::thread::JoinHandle; + +pub struct ThreadedCamera { + camera: Arc>, + thread_handle: JoinHandle<_>, + receiver: Receiver, NokhwaError>>, + sender: Sender, NokhwaError>>, +} + +impl ThreadedCamera {} + +fn capture(camera: Arc>) {} diff --git a/src/utils.rs b/src/utils.rs index c10c58e..b1f16eb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -265,19 +265,23 @@ impl Display for CameraInfo { /// The list of known capture backends to the library.
/// **Note: Only V4L2 and UVC (and by extension AUTO) is implemented so far.** /// - AUTO is special - it tells the Camera struct to automatically choose a backend most suited for the current platform. +/// - `AVFoundation` - Uses `AVFoundation` on Mac **Not Implemted** /// - 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. -/// - Windows - Directshow, Windows only +/// - Windows - MSMF, Windows only, **Not Implemted** /// - `OpenCV` - Uses `OpenCV` to capture. Platform agnostic. -/// - FFMPEG - Uses FFMPEG (libavdevice) to capture. Platform agnostic. +/// - FFMPEG - Uses FFMPEG (libavdevice) to capture. Platform agnostic. **Not Implemted** +/// - `GStreamer` - Uses `GStreamer` RTP to capture. Platform agnostic. #[derive(Clone, Copy, Debug, PartialEq)] pub enum CaptureAPIBackend { Auto, + AVFoundation, Video4Linux, UniversalVideoClass, Windows, OpenCv, Ffmpeg, + GStreamer, } impl Display for CaptureAPIBackend {