1extern crate machine_vision_formats as formats;
29
30use std::sync::OnceLock;
31
32use nokhwa::{
33 Camera,
34 pixel_format::{LumaFormat, RgbFormat},
35 utils::{CameraIndex, RequestedFormat, RequestedFormatType},
36};
37
38use ci2::{
39 AcquisitionMode, AutoMode, DynamicFrameWithInfo, HostTimingInfo, TriggerMode, TriggerSelector,
40};
41use formats::PixFmt;
42use strand_dynamic_frame::DynamicFrameOwned;
43use tracing::info;
44
45fn nokhwa_err(e: nokhwa::NokhwaError) -> ci2::Error {
47 ci2::Error::BackendError(anyhow::Error::new(e))
48}
49
50fn ensure_nokhwa_initialized() -> ci2::Result<()> {
54 static GRANTED: OnceLock<bool> = OnceLock::new();
55
56 let granted = *GRANTED.get_or_init(|| {
57 let (tx, rx) = std::sync::mpsc::channel();
58 nokhwa::nokhwa_initialize(move |granted| {
59 let _ = tx.send(granted);
60 });
61 rx.recv().unwrap_or(false)
62 });
63
64 if granted {
65 Ok(())
66 } else {
67 Err(ci2::Error::from(
68 "nokhwa initialization failed or camera access was not granted",
69 ))
70 }
71}
72
73fn enumerate() -> ci2::Result<Vec<nokhwa::utils::CameraInfo>> {
75 ensure_nokhwa_initialized()?;
76 let backend = nokhwa::native_api_backend()
77 .ok_or_else(|| ci2::Error::from("no native nokhwa backend available"))?;
78 nokhwa::query(backend).map_err(nokhwa_err)
79}
80
81fn device_name(info: &nokhwa::utils::CameraInfo) -> String {
86 info.human_name()
87}
88
89pub struct WrappedModule {}
90
91pub fn new_module() -> ci2::Result<WrappedModule> {
92 Ok(WrappedModule {})
93}
94
95pub struct WebcamTerminateGuard {}
98
99pub fn make_singleton_guard(
100 _module: &dyn ci2::CameraModule<CameraType = WrappedCamera, Guard = WebcamTerminateGuard>,
101) -> ci2::Result<WebcamTerminateGuard> {
102 Ok(WebcamTerminateGuard {})
103}
104
105impl<'a> ci2::CameraModule for &'a WrappedModule {
106 type CameraType = WrappedCamera;
107 type Guard = WebcamTerminateGuard;
108
109 fn name(self: &&'a WrappedModule) -> &'static str {
110 "webcam"
111 }
112
113 fn camera_infos(self: &&'a WrappedModule) -> ci2::Result<Vec<Box<dyn ci2::CameraInfo>>> {
114 let infos = enumerate()?
115 .iter()
116 .map(|info| {
117 let ci: Box<dyn ci2::CameraInfo> = Box::new(WebcamCameraInfo::from_nokhwa(info));
118 ci
119 })
120 .collect();
121 Ok(infos)
122 }
123
124 fn camera(self: &mut &'a WrappedModule, name: &str) -> ci2::Result<Self::CameraType> {
125 WrappedCamera::new(name)
126 }
127
128 fn settings_file_extension(&self) -> &str {
129 "txt"
131 }
132}
133
134#[derive(Debug, Clone)]
135struct WebcamCameraInfo {
136 name: String,
137 serial: String,
138 model: String,
139 vendor: String,
140}
141
142impl WebcamCameraInfo {
143 fn from_nokhwa(info: &nokhwa::utils::CameraInfo) -> Self {
144 Self {
147 name: device_name(info),
148 serial: index_to_string(info.index()),
149 model: info.human_name(),
150 vendor: info.description().to_string(),
151 }
152 }
153}
154
155impl ci2::CameraInfo for WebcamCameraInfo {
156 fn name(&self) -> &str {
157 &self.name
158 }
159 fn serial(&self) -> &str {
160 &self.serial
161 }
162 fn model(&self) -> &str {
163 &self.model
164 }
165 fn vendor(&self) -> &str {
166 &self.vendor
167 }
168}
169
170fn index_to_string(index: &CameraIndex) -> String {
171 match index {
172 CameraIndex::Index(i) => i.to_string(),
173 CameraIndex::String(s) => s.clone(),
174 }
175}
176
177pub struct WrappedCamera {
178 cam: Camera,
179 info: WebcamCameraInfo,
180 store_fno: usize,
181 pixel_format: PixFmt,
185}
186
187fn _test_camera_is_send() {
188 fn implements<T: Send>() {}
190 implements::<WrappedCamera>();
191}
192
193impl WrappedCamera {
194 fn new(name: &str) -> ci2::Result<Self> {
195 let devices = enumerate()?;
196 if devices.is_empty() {
197 return Err(ci2::Error::from("no webcams found"));
198 }
199
200 for (i, device) in devices.iter().enumerate() {
201 info!("webcam #{i}: {}", device.human_name());
202 }
203
204 let device = if name.is_empty() {
207 &devices[0]
208 } else {
209 devices
210 .iter()
211 .find(|d| device_name(d) == name || index_to_string(d.index()) == name)
212 .ok_or_else(|| ci2::Error::from(format!("could not find webcam \"{name}\"")))?
213 };
214
215 let info = WebcamCameraInfo::from_nokhwa(device);
216
217 let requested =
220 RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate);
221 let cam = Camera::new(device.index().clone(), requested).map_err(nokhwa_err)?;
222
223 info!(
224 "opened webcam \"{}\" with format {}",
225 info.name,
226 cam.camera_format()
227 );
228
229 Ok(Self {
230 cam,
231 info,
232 store_fno: 0,
233 pixel_format: PixFmt::RGB8,
234 })
235 }
236}
237
238impl ci2::CameraInfo for WrappedCamera {
239 fn name(&self) -> &str {
240 &self.info.name
241 }
242 fn serial(&self) -> &str {
243 &self.info.serial
244 }
245 fn model(&self) -> &str {
246 &self.info.model
247 }
248 fn vendor(&self) -> &str {
249 &self.info.vendor
250 }
251}
252
253impl ci2::Camera for WrappedCamera {
254 fn command_execute(&self, _name: &str, _verify: bool) -> ci2::Result<()> {
259 Err(ci2::Error::FeatureNotPresent())
260 }
261 fn feature_bool(&self, _name: &str) -> ci2::Result<bool> {
262 Err(ci2::Error::FeatureNotPresent())
263 }
264 fn feature_bool_set(&self, _name: &str, _value: bool) -> ci2::Result<()> {
265 Err(ci2::Error::FeatureNotPresent())
266 }
267 fn feature_enum(&self, _name: &str) -> ci2::Result<String> {
268 Err(ci2::Error::FeatureNotPresent())
269 }
270 fn feature_enum_set(&self, _name: &str, _value: &str) -> ci2::Result<()> {
271 Err(ci2::Error::FeatureNotPresent())
272 }
273 fn feature_float(&self, _name: &str) -> ci2::Result<f64> {
274 Err(ci2::Error::FeatureNotPresent())
275 }
276 fn feature_float_set(&self, _name: &str, _value: f64) -> ci2::Result<()> {
277 Err(ci2::Error::FeatureNotPresent())
278 }
279 fn feature_int(&self, _name: &str) -> ci2::Result<i64> {
280 Err(ci2::Error::FeatureNotPresent())
281 }
282 fn feature_int_set(&self, _name: &str, _value: i64) -> ci2::Result<()> {
283 Err(ci2::Error::FeatureNotPresent())
284 }
285
286 fn node_map_load(&self, _settings: &str) -> ci2::Result<()> {
289 Err(ci2::Error::FeatureNotPresent())
290 }
291 fn node_map_save(&self) -> ci2::Result<String> {
292 Err(ci2::Error::FeatureNotPresent())
293 }
294
295 fn width(&self) -> ci2::Result<u32> {
296 Ok(self.cam.resolution().width())
297 }
298 fn height(&self) -> ci2::Result<u32> {
299 Ok(self.cam.resolution().height())
300 }
301
302 fn pixel_format(&self) -> ci2::Result<PixFmt> {
303 Ok(self.pixel_format)
304 }
305 fn possible_pixel_formats(&self) -> ci2::Result<Vec<PixFmt>> {
306 Ok(vec![PixFmt::RGB8, PixFmt::Mono8])
307 }
308 fn set_pixel_format(&mut self, pixel_format: PixFmt) -> ci2::Result<()> {
309 match pixel_format {
310 PixFmt::RGB8 | PixFmt::Mono8 => {
311 self.pixel_format = pixel_format;
312 Ok(())
313 }
314 other => Err(ci2::Error::from(format!(
315 "webcam backend does not support pixel format {other}"
316 ))),
317 }
318 }
319
320 fn exposure_time(&self) -> ci2::Result<f64> {
321 Err(ci2::Error::FeatureNotPresent())
322 }
323 fn exposure_time_range(&self) -> ci2::Result<(f64, f64)> {
324 Err(ci2::Error::FeatureNotPresent())
325 }
326 fn set_exposure_time(&mut self, _: f64) -> ci2::Result<()> {
327 Err(ci2::Error::FeatureNotPresent())
328 }
329
330 fn exposure_auto(&self) -> ci2::Result<AutoMode> {
331 Err(ci2::Error::FeatureNotPresent())
332 }
333 fn set_exposure_auto(&mut self, _: AutoMode) -> ci2::Result<()> {
334 Err(ci2::Error::FeatureNotPresent())
335 }
336
337 fn gain(&self) -> ci2::Result<f64> {
338 Err(ci2::Error::FeatureNotPresent())
339 }
340 fn gain_range(&self) -> ci2::Result<(f64, f64)> {
341 Err(ci2::Error::FeatureNotPresent())
342 }
343 fn set_gain(&mut self, _: f64) -> ci2::Result<()> {
344 Err(ci2::Error::FeatureNotPresent())
345 }
346
347 fn gain_auto(&self) -> ci2::Result<AutoMode> {
348 Err(ci2::Error::FeatureNotPresent())
349 }
350 fn set_gain_auto(&mut self, _: AutoMode) -> ci2::Result<()> {
351 Err(ci2::Error::FeatureNotPresent())
352 }
353
354 fn trigger_mode(&self) -> ci2::Result<TriggerMode> {
355 Err(ci2::Error::FeatureNotPresent())
356 }
357 fn set_trigger_mode(&mut self, _: TriggerMode) -> ci2::Result<()> {
358 Err(ci2::Error::FeatureNotPresent())
359 }
360
361 fn acquisition_frame_rate_enable(&self) -> ci2::Result<bool> {
362 Err(ci2::Error::FeatureNotPresent())
363 }
364 fn set_acquisition_frame_rate_enable(&mut self, _value: bool) -> ci2::Result<()> {
365 Err(ci2::Error::FeatureNotPresent())
366 }
367
368 fn acquisition_frame_rate(&self) -> ci2::Result<f64> {
369 Err(ci2::Error::FeatureNotPresent())
370 }
371 fn acquisition_frame_rate_range(&self) -> ci2::Result<(f64, f64)> {
372 Err(ci2::Error::FeatureNotPresent())
373 }
374 fn set_acquisition_frame_rate(&mut self, _value: f64) -> ci2::Result<()> {
375 Err(ci2::Error::FeatureNotPresent())
376 }
377
378 fn trigger_selector(&self) -> ci2::Result<TriggerSelector> {
379 Err(ci2::Error::FeatureNotPresent())
380 }
381 fn set_trigger_selector(&mut self, _: TriggerSelector) -> ci2::Result<()> {
382 Err(ci2::Error::FeatureNotPresent())
383 }
384
385 fn acquisition_mode(&self) -> ci2::Result<AcquisitionMode> {
386 Err(ci2::Error::FeatureNotPresent())
387 }
388 fn set_acquisition_mode(&mut self, _: AcquisitionMode) -> ci2::Result<()> {
389 Err(ci2::Error::FeatureNotPresent())
390 }
391
392 fn acquisition_start(&mut self) -> ci2::Result<()> {
393 self.store_fno = 0;
394 self.cam.open_stream().map_err(nokhwa_err)
395 }
396 fn acquisition_stop(&mut self) -> ci2::Result<()> {
397 self.cam.stop_stream().map_err(nokhwa_err)
398 }
399
400 fn next_frame(&mut self) -> ci2::Result<DynamicFrameWithInfo> {
401 let buffer = self.cam.frame().map_err(nokhwa_err)?;
403 let datetime = chrono::Utc::now();
404
405 let image = match self.pixel_format {
406 PixFmt::Mono8 => {
407 let decoded = buffer.decode_image::<LumaFormat>().map_err(nokhwa_err)?;
408 let (width, height) = (decoded.width(), decoded.height());
409 let stride = width as usize;
410 DynamicFrameOwned::from_buf(
411 width,
412 height,
413 stride,
414 decoded.into_raw(),
415 PixFmt::Mono8,
416 )
417 }
418 _ => {
421 let decoded = buffer.decode_image::<RgbFormat>().map_err(nokhwa_err)?;
422 let (width, height) = (decoded.width(), decoded.height());
423 let stride = width as usize * 3;
424 DynamicFrameOwned::from_buf(width, height, stride, decoded.into_raw(), PixFmt::RGB8)
425 }
426 }
427 .ok_or_else(|| ci2::Error::SingleFrameError("decoded frame had invalid layout".into()))?;
428
429 let fno = self.store_fno;
430 self.store_fno += 1;
431
432 Ok(DynamicFrameWithInfo {
433 image: std::sync::Arc::new(image),
434 host_timing: HostTimingInfo { fno, datetime },
435 backend_data: None,
436 })
437 }
438}