diff --git a/.gitignore b/.gitignore index b0dd1ce..b5226e7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ nokhwa.iml .vscode -/pkg \ No newline at end of file +/pkg + +target \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index b30426f..a6f3b6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ version = "0.2.0" optional = true [dependencies.ouroboros] -version = "0.10.0" +version = "0.11.1" optional = true [dependencies.uvc] diff --git a/README.md b/README.md index 51ac4cc..2bf2438 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@ Most likely, you will only use functionality provided by the `Camera` struct. If 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 diff --git a/examples/capture/Cargo.toml b/examples/capture/Cargo.toml index b75ce61..f2b4593 100644 --- a/examples/capture/Cargo.toml +++ b/examples/capture/Cargo.toml @@ -19,7 +19,7 @@ input-avfoundation = ["nokhwa/input-avfoundation"] clap = "2.33.3" glium = "0.30.0" glutin = "0.27.0" -flume = "0.10.7" +flume = "0.10.9" # Use these as you need [dependencies.nokhwa] diff --git a/examples/capture/src/main.rs b/examples/capture/src/main.rs index 56c3cb8..6e89421 100644 --- a/examples/capture/src/main.rs +++ b/examples/capture/src/main.rs @@ -4,6 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +// Some assembly required. For developers 7 and up. + use clap::{App, Arg}; use glium::{ implement_vertex, index::PrimitiveType, program, texture::RawImage2d, uniform, Display, diff --git a/examples/threaded-capture/Cargo.toml b/examples/threaded-capture/Cargo.toml new file mode 100644 index 0000000..e729a6c --- /dev/null +++ b/examples/threaded-capture/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "threaded-capture" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[dependencies.image] +version = "0.23.14" +no-default-features = true + +[dependencies.nokhwa] +path = "../../../nokhwa" +# EDIT THIS! +features = ["input-v4l", "output-threaded"] \ No newline at end of file diff --git a/examples/threaded-capture/src/main.rs b/examples/threaded-capture/src/main.rs new file mode 100644 index 0000000..64b6d34 --- /dev/null +++ b/examples/threaded-capture/src/main.rs @@ -0,0 +1,16 @@ +use image::{ImageBuffer, Rgb}; +use nokhwa::{query_devices, CaptureAPIBackend, ThreadedCamera}; + +fn main() { + let cameras = query_devices(CaptureAPIBackend::Auto).unwrap(); + cameras.iter().for_each(|cam| println!("{:?}", cam)); + + let mut threaded = ThreadedCamera::new(0, None).unwrap(); + threaded.open_stream(callback).unwrap(); + #[allow(clippy::empty_loop)] // keep it running + loop {} +} + +fn callback(image: ImageBuffer, Vec>) { + println!("{}x{} {}", image.width(), image.height(), image.len()); +} diff --git a/nokhwa-bindings-macos/Cargo.toml b/nokhwa-bindings-macos/Cargo.toml index 9554193..c162f3e 100644 --- a/nokhwa-bindings-macos/Cargo.toml +++ b/nokhwa-bindings-macos/Cargo.toml @@ -6,13 +6,13 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -thiserror = "1.0.29" -lazy_static = "1.4.0" +thiserror = "1.0.26" +flume = "0.10.9" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] core-media-sys = "0.1.2" cocoa-foundation = "0.1.0" objc = { version = "0.2.7", features = ["exception"] } block = "0.1.6" -flume = "0.10.9" -dashmap = "4.0.2" \ No newline at end of file +dashmap = "4.0.2" +lazy_static = "1.4.0" \ No newline at end of file diff --git a/nokhwa-bindings-macos/src/lib.rs b/nokhwa-bindings-macos/src/lib.rs index e946b58..069a096 100644 --- a/nokhwa-bindings-macos/src/lib.rs +++ b/nokhwa-bindings-macos/src/lib.rs @@ -14,8 +14,10 @@ )] #![allow(clippy::not_unsafe_ptr_arg_deref)] -#[cfg_attr(any(target_os = "macos", target_os = "ios"), macro_use)] +#[cfg(any(target_os = "macos", target_os = "ios"))] +#[macro_use] extern crate objc; +#[cfg(any(target_os = "macos", target_os = "ios"))] #[macro_use] extern crate lazy_static; @@ -1225,6 +1227,8 @@ pub mod avfoundation { #[cfg(not(any(target_os = "macos", target_os = "ios")))] pub mod avfoundation { use crate::AVFError; + use flume::{Receiver, Sender}; + use std::borrow::Cow; pub type CompressionData<'a> = (Cow<'a, [u8]>, AVFourCC); pub type DataPipe<'a> = (Sender>, Receiver>); @@ -1411,11 +1415,11 @@ pub mod avfoundation { vec![] } - pub fn new(index: usize) -> Result { + pub fn new(_: usize) -> Result { Err(AVFError::NotSupported) } - pub fn from_id(id: &str) -> Result { + pub fn from_id(_: &str) -> Result { Err(AVFError::NotSupported) } diff --git a/src/backends/capture/mod.rs b/src/backends/capture/mod.rs index 92808e5..1c80290 100644 --- a/src/backends/capture/mod.rs +++ b/src/backends/capture/mod.rs @@ -5,21 +5,42 @@ */ #[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; #[cfg(all(feature = "input-v4l", target_os = "linux"))] pub use v4l2::V4LCaptureDevice; -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] mod msmf; -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] pub use msmf::MediaFoundationCaptureDevice; -#[cfg(all( - feature = "input-avfoundation", - any(target_os = "macos", target_os = "ios") +#[cfg(any( + all( + feature = "input-avfoundation", + any(target_os = "macos", target_os = "ios") + ), + all( + feature = "docs-only", + feature = "docs-nolink", + feature = "input-avfoundation" + ) ))] mod avfoundation; -#[cfg(all( - feature = "input-avfoundation", - any(target_os = "macos", target_os = "ios") +#[cfg(any( + all( + feature = "input-avfoundation", + any(target_os = "macos", target_os = "ios") + ), + all( + feature = "docs-only", + feature = "docs-nolink", + feature = "input-avfoundation" + ) ))] pub use avfoundation::AVFoundationCaptureDevice; #[cfg(feature = "input-uvc")] diff --git a/src/camera.rs b/src/camera.rs index 01297f8..e5c85c1 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -26,12 +26,22 @@ pub struct Camera { #[allow(clippy::nonminimal_bool)] impl Camera { - /// Create a new camera from an `index`, `format`, and `backend`. `format` can be `None`. + /// 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 { + Camera::with_backend(index, format, CaptureAPIBackend::Auto) + } + + /// Create a new camera from an `index`, `format`, and `backend`. `format` can be `None`. + /// # 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, + format: Option, backend: CaptureAPIBackend, ) -> Result { let camera_backend = init_camera(index, format, backend)?; @@ -55,7 +65,7 @@ impl Camera { backend: CaptureAPIBackend, ) -> Result { let camera_format = CameraFormat::new_from(width, height, fourcc, fps); - Camera::new(index, Some(camera_format), backend) + Camera::with_backend(index, Some(camera_format), backend) } /// Gets the current Camera's index. diff --git a/src/error.rs b/src/error.rs index 1b3c7b9..c5446e2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -53,10 +53,16 @@ pub enum NokhwaError { NotImplementedError(String), } -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] use nokhwa_bindings_windows::BindingError; -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] impl From for NokhwaError { fn from(err: BindingError) -> Self { match err { @@ -95,15 +101,29 @@ impl From for NokhwaError { } } -#[cfg(all( - feature = "input-avfoundation", - any(target_os = "macos", target_os = "ios") +#[cfg(any( + all( + feature = "input-avfoundation", + any(target_os = "macos", target_os = "ios") + ), + all( + feature = "docs-only", + feature = "docs-nolink", + feature = "input-avfoundation" + ) ))] use nokhwa_bindings_macos::AVFError; -#[cfg(all( - feature = "input-avfoundation", - any(target_os = "macos", target_os = "ios") +#[cfg(any( + all( + feature = "input-avfoundation", + any(target_os = "macos", target_os = "ios") + ), + all( + feature = "docs-only", + feature = "docs-nolink", + feature = "input-avfoundation" + ) ))] impl From for NokhwaError { fn from(avf_error: AVFError) -> Self { diff --git a/src/lib.rs b/src/lib.rs index 5ae4212..bc16c3f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,9 +4,9 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #![cfg_attr(feature = "test-fail-warning", deny(warnings))] -#![doc = include_str!("../README.md")] -#![deny(clippy::pedantic)] -#![warn(clippy::all)] + +//! # nokhwa +//! A #[cfg(feature = "small-wasm")] #[global_allocator] @@ -27,14 +27,14 @@ pub mod network_camera; mod query; /// A camera that runs in a different thread and can call your code based on callbacks. #[cfg(feature = "output-threaded")] -mod threaded_camera; +mod threaded; mod utils; pub use camera::Camera; pub use camera_traits::*; pub use error::NokhwaError; pub use init::*; -pub use query::query_devices; +pub use query::*; #[cfg(feature = "output-threaded")] -pub use threaded_camera::ThreadedCamera; +pub use threaded::ThreadedCamera; pub use utils::*; diff --git a/src/query.rs b/src/query.rs index 7af72f7..0f53c73 100644 --- a/src/query.rs +++ b/src/query.rs @@ -6,6 +6,20 @@ use crate::{CameraInfo, CaptureAPIBackend, NokhwaError}; +/// Query the system for a list of available devices. +/// Usually the order goes Native -> UVC -> Gstreamer. +/// # Quirks +/// - Media Foundation: The symbolic link for the device is listed in the `misc` attribute of the [`CameraInfo`]. +/// - Media Foundation: The names may contain invalid characters since they were converted from UTF16. +/// - AVFoundation: The ID of the device is stored in the `misc` attribute of the [`CameraInfo`]. +/// - AVFoundation: There is lots of miscellaneous info in the `desc` attribute. +/// # 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> { + query_devices(CaptureAPIBackend::Auto) +} + // 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`, `MediaFoundation`, `AVFoundation`, `UVC`, and `GST`.
@@ -13,6 +27,8 @@ use crate::{CameraInfo, CaptureAPIBackend, NokhwaError}; /// # Quirks /// - Media Foundation: The symbolic link for the device is listed in the `misc` attribute of the [`CameraInfo`]. /// - Media Foundation: The names may contain invalid characters since they were converted from UTF16. +/// - AVFoundation: The ID of the device is stored in the `misc` attribute of the [`CameraInfo`]. +/// - AVFoundation: There is lots of miscellaneous info in the `desc` attribute. /// # 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. diff --git a/src/threaded_camera.rs b/src/threaded.rs similarity index 75% rename from src/threaded_camera.rs rename to src/threaded.rs index b48d93b..4ce4e5b 100644 --- a/src/threaded_camera.rs +++ b/src/threaded.rs @@ -9,33 +9,68 @@ use crate::{ }; use image::{ImageBuffer, Rgb}; use parking_lot::FairMutex; -use std::{collections::HashMap, sync::Arc, thread::JoinHandle}; +use std::{ + collections::HashMap, + ops::Deref, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; +/// 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. +/// 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 +/// complete before a new frame is available. If you need to do heavy image processing, it may be +/// beneficial to directly pipe the data to a new thread to process it there. +/// +/// Note that this does not have `WGPU` capabilities. However, it should be easy to implement. +/// # SAFETY +/// The `Mutex` guarantees exclusive access to the underlying camera struct. They should be safe to +/// impl `Send` on. +#[derive(Clone)] pub struct ThreadedCamera { camera: Arc>, - thread_handle: JoinHandle<()>, frame_callback: Arc, Vec>)>>>, + die_bool: Arc, } impl ThreadedCamera { - /// Create a new camera from an `index`, `format`, and `backend`. `format` can be `None`. + /// Create a new camera from an `index` and `format`. `format` can be `None`. /// # 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 { + ThreadedCamera::with_backend(index, format, CaptureAPIBackend::Auto) + } + + /// Create a new camera from an `index`, `format`, and `backend`. `format` can be `None`. + /// # 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, + format: Option, backend: CaptureAPIBackend, ) -> Result { - let camera = Arc::new(FairMutex::new(Camera::new(index, format, backend)?)); + let camera = Arc::new(FairMutex::new(Camera::with_backend(index, format, backend)?)); let frame_callback = Arc::new(FairMutex::new(None)); + let die_bool = Arc::new(AtomicBool::new(false)); - let thread_handle = - std::thread::spawn(|| camera_frame_thread_loop(camera.clone(), frame_callback.clone())); + let die_clone = die_bool.clone(); + let camera_clone = camera.clone(); + let callback_clone = frame_callback.clone(); + std::thread::spawn(move || { + camera_frame_thread_loop(camera_clone, callback_clone, die_clone) + }); Ok(ThreadedCamera { camera, - thread_handle, frame_callback, + die_bool, }) } @@ -51,7 +86,7 @@ impl ThreadedCamera { backend: CaptureAPIBackend, ) -> Result { let camera_format = CameraFormat::new_from(width, height, fourcc, fps); - ThreadedCamera::new(index, Some(camera_format), backend) + ThreadedCamera::with_backend(index, Some(camera_format), backend) } /// Gets the current Camera's index. @@ -82,8 +117,8 @@ impl ThreadedCamera { /// Gets the camera information such as Name and Index as a [`CameraInfo`]. #[must_use] - pub fn info(&self) -> &CameraInfo { - self.camera.lock().info() + pub fn info(&self) -> CameraInfo { + self.camera.lock().info().clone() } /// Gets the current [`CameraFormat`]. @@ -188,15 +223,26 @@ impl ThreadedCamera { } } +impl Drop for ThreadedCamera { + fn drop(&mut self) { + let _ = self.stop_stream(); + self.die_bool.store(true, Ordering::SeqCst); + } +} + fn camera_frame_thread_loop( camera: Arc>, callback: Arc, Vec>)>>>, + die_bool: Arc, ) { loop { if let Ok(img) = camera.lock().frame() { - if let Some(cb) = callback.lock() { + if let Some(cb) = callback.lock().deref() { cb(img) } } + if die_bool.load(Ordering::SeqCst) { + break; + } } } diff --git a/src/utils.rs b/src/utils.rs index 49409ec..7b63bf9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,14 +6,24 @@ use std::{ #[cfg(feature = "output-wasm")] use wasm_bindgen::prelude::wasm_bindgen; -#[cfg(all( - feature = "input-avfoundation", - any(target_os = "macos", target_os = "ios") +#[cfg(any( + all( + feature = "input-avfoundation", + any(target_os = "macos", target_os = "ios") + ), + all( + feature = "docs-only", + feature = "docs-nolink", + feature = "input-avfoundation" + ) ))] use nokhwa_bindings_macos::avfoundation::{ AVCaptureDeviceDescriptor, AVFourCC, AVVideoResolution, CaptureDeviceFormatDescriptor, }; -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] use nokhwa_bindings_windows::{ MFCameraFormat, MFControl, MFFrameFormat, MFResolution, MediaFoundationControls, MediaFoundationDeviceDescriptor, @@ -57,7 +67,10 @@ impl From for uvc::FrameFormat { } } -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] impl From for FrameFormat { fn from(mf_ff: MFFrameFormat) -> Self { match mf_ff { @@ -67,7 +80,10 @@ impl From for FrameFormat { } } -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] impl From for MFFrameFormat { fn from(ff: FrameFormat) -> Self { match ff { @@ -77,9 +93,16 @@ impl From for MFFrameFormat { } } -#[cfg(all( - feature = "input-avfoundation", - any(target_os = "macos", target_os = "ios") +#[cfg(any( + all( + feature = "input-avfoundation", + any(target_os = "macos", target_os = "ios") + ), + all( + feature = "docs-only", + feature = "docs-nolink", + feature = "input-avfoundation" + ) ))] impl From for FrameFormat { fn from(av_fcc: AVFourCC) -> Self { @@ -90,9 +113,16 @@ impl From for FrameFormat { } } -#[cfg(all( - feature = "input-avfoundation", - any(target_os = "macos", target_os = "ios") +#[cfg(any( + all( + feature = "input-avfoundation", + any(target_os = "macos", target_os = "ios") + ), + all( + feature = "docs-only", + feature = "docs-nolink", + feature = "input-avfoundation" + ) ))] impl From for AVFourCC { fn from(ff: FrameFormat) -> Self { @@ -184,7 +214,10 @@ impl Ord for Resolution { } } -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] impl From for Resolution { fn from(mf_res: MFResolution) -> Self { Resolution { @@ -194,7 +227,10 @@ impl From for Resolution { } } -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] impl From for MFResolution { fn from(res: Resolution) -> Self { MFResolution { @@ -204,9 +240,16 @@ impl From for MFResolution { } } -#[cfg(all( - feature = "input-avfoundation", - any(target_os = "macos", target_os = "ios") +#[cfg(any( + all( + feature = "input-avfoundation", + any(target_os = "macos", target_os = "ios") + ), + all( + feature = "docs-only", + feature = "docs-nolink", + feature = "input-avfoundation" + ) ))] #[allow(clippy::cast_sign_loss)] impl From for Resolution { @@ -329,7 +372,10 @@ impl Display for CameraFormat { } } -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] impl From for CameraFormat { fn from(mf_cam_fmt: MFCameraFormat) -> Self { CameraFormat { @@ -340,7 +386,10 @@ impl From for CameraFormat { } } -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] impl From for MFCameraFormat { fn from(cf: CameraFormat) -> Self { MFCameraFormat::new(cf.resolution.into(), cf.format.into(), cf.frame_rate) @@ -359,9 +408,16 @@ impl From for Format { } } -#[cfg(all( - feature = "input-avfoundation", - any(target_os = "macos", target_os = "ios") +#[cfg(any( + all( + feature = "input-avfoundation", + any(target_os = "macos", target_os = "ios") + ), + all( + feature = "docs-only", + feature = "docs-nolink", + feature = "input-avfoundation" + ) ))] #[allow(clippy::cast_possible_wrap)] #[allow(clippy::cast_lossless)] @@ -505,7 +561,10 @@ impl Display for CameraInfo { } } -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] impl From> for CameraInfo { fn from(dev_desc: MediaFoundationDeviceDescriptor<'_>) -> Self { CameraInfo { @@ -517,9 +576,16 @@ impl From> for CameraInfo { } } -#[cfg(all( - feature = "input-avfoundation", - any(target_os = "macos", target_os = "ios") +#[cfg(any( + all( + feature = "input-avfoundation", + any(target_os = "macos", target_os = "ios") + ), + all( + feature = "docs-only", + feature = "docs-nolink", + feature = "input-avfoundation" + ) ))] #[allow(clippy::cast_possible_truncation)] impl From for CameraInfo { @@ -586,7 +652,10 @@ impl Display for KnownCameraControls { } } -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] impl From for KnownCameraControls { fn from(mf_c: MediaFoundationControls) -> Self { match mf_c { @@ -611,7 +680,10 @@ impl From for KnownCameraControls { } } -#[cfg(all(feature = "input-msmf", target_os = "windows"))] +#[cfg(any( + all(feature = "input-msmf", target_os = "windows"), + all(feature = "docs-only", feature = "docs-nolink", feature = "input-msmf") +))] impl From for KnownCameraControls { fn from(mf_cc: MFControl) -> Self { mf_cc.control().into()