1use crate::VideoError;
2
3#[cfg(feature = "native-camera")]
4use std::fmt;
5#[cfg(feature = "native-camera")]
6use std::time::Instant;
7
8#[cfg(feature = "native-camera")]
9use nokhwa::Camera;
10#[cfg(feature = "native-camera")]
11use nokhwa::pixel_format::RgbFormat;
12#[cfg(feature = "native-camera")]
13use nokhwa::utils::{ApiBackend, CameraIndex, RequestedFormat, RequestedFormatType, Resolution};
14
15#[cfg(feature = "native-camera")]
16use super::convert::{micros_to_u64, rgb8_bytes_to_frame};
17use super::frame::{Frame, Rgb8Frame};
18use super::source::FrameSource;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct CameraConfig {
22 pub device_index: u32,
23 pub width: u32,
24 pub height: u32,
25 pub fps: u32,
26}
27
28impl Default for CameraConfig {
29 fn default() -> Self {
30 Self {
31 device_index: 0,
32 width: 640,
33 height: 480,
34 fps: 30,
35 }
36 }
37}
38
39impl CameraConfig {
40 pub fn validate(&self) -> Result<(), VideoError> {
41 if self.width == 0 || self.height == 0 {
42 return Err(VideoError::InvalidCameraResolution {
43 width: self.width,
44 height: self.height,
45 });
46 }
47 if self.fps == 0 {
48 return Err(VideoError::InvalidCameraFps { fps: self.fps });
49 }
50 Ok(())
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct CameraDeviceInfo {
56 pub index: u32,
57 pub label: String,
58}
59
60#[cfg(feature = "native-camera")]
61pub fn list_camera_devices() -> Result<Vec<CameraDeviceInfo>, VideoError> {
62 let backend = preferred_camera_backend();
63 let cameras = match nokhwa::query(backend) {
64 Ok(cameras) => cameras,
65 Err(primary_err) if backend != ApiBackend::Auto => {
66 nokhwa::query(ApiBackend::Auto).map_err(|fallback_err| {
67 VideoError::Source(format!(
68 "failed to enumerate cameras with preferred backend {backend:?}: {primary_err}; \
69 auto fallback failed: {fallback_err}"
70 ))
71 })?
72 }
73 Err(err) => {
74 return Err(VideoError::Source(format!(
75 "failed to enumerate cameras with backend {backend:?}: {err}"
76 )));
77 }
78 };
79 let mut devices = Vec::with_capacity(cameras.len());
80 for (idx, info) in cameras.into_iter().enumerate() {
81 let fallback_index = u32::try_from(idx).unwrap_or(u32::MAX);
82 let index = match info.index() {
83 CameraIndex::Index(value) => *value,
84 CameraIndex::String(value) => value.parse::<u32>().unwrap_or(fallback_index),
85 };
86 let human_name = info.human_name().trim().to_string();
87 let description = info.description().trim().to_string();
88 let label = if description.is_empty() {
89 human_name
90 } else {
91 format!("{human_name} ({description})")
92 };
93 devices.push(CameraDeviceInfo { index, label });
94 }
95 devices.sort_by(|left, right| {
96 left.index
97 .cmp(&right.index)
98 .then_with(|| left.label.cmp(&right.label))
99 });
100 devices.dedup_by(|left, right| left.index == right.index && left.label == right.label);
101 Ok(devices)
102}
103
104#[cfg(not(feature = "native-camera"))]
105pub fn list_camera_devices() -> Result<Vec<CameraDeviceInfo>, VideoError> {
106 Err(VideoError::CameraBackendDisabled)
107}
108
109pub fn resolve_camera_device(query: &str) -> Result<CameraDeviceInfo, VideoError> {
110 let devices = list_camera_devices()?;
111 select_camera_device(&devices, query)
112}
113
114pub fn resolve_camera_device_index(query: &str) -> Result<u32, VideoError> {
115 Ok(resolve_camera_device(query)?.index)
116}
117
118pub fn query_camera_devices(query: &str) -> Result<Vec<CameraDeviceInfo>, VideoError> {
119 let devices = list_camera_devices()?;
120 filter_camera_devices(&devices, query)
121}
122
123pub fn filter_camera_devices(
124 devices: &[CameraDeviceInfo],
125 query: &str,
126) -> Result<Vec<CameraDeviceInfo>, VideoError> {
127 let normalized_query = query.trim();
128 if normalized_query.is_empty() {
129 return Err(VideoError::InvalidCameraDeviceQuery {
130 query: query.to_string(),
131 });
132 }
133
134 let index_query = normalized_query.parse::<u32>().ok();
135 let query_lc = normalized_query.to_lowercase();
136 let mut matches = devices
137 .iter()
138 .filter(|device| {
139 if let Some(index_query) = index_query
140 && device.index == index_query
141 {
142 return true;
143 }
144 device.label.to_lowercase().contains(&query_lc)
145 })
146 .cloned()
147 .collect::<Vec<_>>();
148
149 matches.sort_by(|left, right| {
150 left.index
151 .cmp(&right.index)
152 .then_with(|| left.label.cmp(&right.label))
153 });
154 matches.dedup_by(|left, right| left.index == right.index && left.label == right.label);
155 Ok(matches)
156}
157
158pub(crate) fn select_camera_device(
159 devices: &[CameraDeviceInfo],
160 query: &str,
161) -> Result<CameraDeviceInfo, VideoError> {
162 let normalized_query = query.trim();
163 if normalized_query.is_empty() {
164 return Err(VideoError::InvalidCameraDeviceQuery {
165 query: query.to_string(),
166 });
167 }
168
169 if let Ok(index_query) = normalized_query.parse::<u32>() {
170 let by_index = devices
171 .iter()
172 .filter(|device| device.index == index_query)
173 .cloned()
174 .collect::<Vec<_>>();
175 if let Some(device) = unique_match(&by_index) {
176 return Ok(device);
177 }
178 if by_index.len() > 1 {
179 return Err(VideoError::CameraDeviceAmbiguous {
180 query: normalized_query.to_string(),
181 matches: format_device_matches(&by_index),
182 });
183 }
184 }
185
186 let query_lc = normalized_query.to_lowercase();
187
188 let mut exact = Vec::new();
189 let mut partial = Vec::new();
190 for device in devices {
191 let label_lc = device.label.to_lowercase();
192 if label_lc == query_lc {
193 exact.push(device.clone());
194 continue;
195 }
196 if label_lc.contains(&query_lc) {
197 partial.push(device.clone());
198 }
199 }
200
201 if let Some(device) = unique_match(&exact) {
202 return Ok(device);
203 }
204 if exact.len() > 1 {
205 return Err(VideoError::CameraDeviceAmbiguous {
206 query: normalized_query.to_string(),
207 matches: format_device_matches(&exact),
208 });
209 }
210 if let Some(device) = unique_match(&partial) {
211 return Ok(device);
212 }
213 if partial.len() > 1 {
214 return Err(VideoError::CameraDeviceAmbiguous {
215 query: normalized_query.to_string(),
216 matches: format_device_matches(&partial),
217 });
218 }
219 Err(VideoError::CameraDeviceNotFound {
220 query: normalized_query.to_string(),
221 })
222}
223
224fn unique_match(matches: &[CameraDeviceInfo]) -> Option<CameraDeviceInfo> {
225 if matches.len() == 1 {
226 Some(matches[0].clone())
227 } else {
228 None
229 }
230}
231
232fn format_device_matches(matches: &[CameraDeviceInfo]) -> Vec<String> {
233 let mut values = matches
234 .iter()
235 .map(|device| format!("{}: {}", device.index, device.label))
236 .collect::<Vec<_>>();
237 values.sort();
238 values
239}
240
241#[cfg(feature = "native-camera")]
242pub struct CameraFrameSource {
243 camera: Camera,
244 next_index: u64,
245 started_at: Instant,
246}
247
248#[cfg(feature = "native-camera")]
249impl fmt::Debug for CameraFrameSource {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 f.debug_struct("CameraFrameSource")
252 .field("next_index", &self.next_index)
253 .finish_non_exhaustive()
254 }
255}
256
257#[cfg(not(feature = "native-camera"))]
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub struct CameraFrameSource;
260
261#[cfg(feature = "native-camera")]
262impl CameraFrameSource {
263 pub fn open(config: CameraConfig) -> Result<Self, VideoError> {
264 config.validate()?;
265 let backend = preferred_camera_backend();
266 let mut camera =
267 open_camera_with_backend(config.device_index, backend).or_else(|primary| {
268 if backend == ApiBackend::Auto {
269 return Err(primary);
270 }
271 open_camera_with_backend(config.device_index, ApiBackend::Auto).map_err(|fallback| {
272 VideoError::Source(format!(
273 "failed to create camera source with preferred backend {backend:?}: {primary}; \
274 auto fallback failed: {fallback}"
275 ))
276 })
277 })?;
278 camera
279 .set_resolution(Resolution::new(config.width, config.height))
280 .map_err(|err| VideoError::Source(format!("failed to set camera resolution: {err}")))?;
281 camera
282 .set_frame_rate(config.fps)
283 .map_err(|err| VideoError::Source(format!("failed to set camera frame rate: {err}")))?;
284 camera
285 .open_stream()
286 .map_err(|err| VideoError::Source(format!("failed to open camera stream: {err}")))?;
287
288 Ok(Self {
289 camera,
290 next_index: 0,
291 started_at: Instant::now(),
292 })
293 }
294
295 pub fn next_rgb8_frame(&mut self) -> Result<Option<Rgb8Frame>, VideoError> {
296 let captured = self
297 .camera
298 .frame()
299 .map_err(|err| VideoError::Source(format!("failed to read camera frame: {err}")))?;
300 let resolution = captured.resolution();
301 let width = usize::try_from(resolution.width()).map_err(|err| {
302 VideoError::Source(format!("failed to convert frame width to usize: {err}"))
303 })?;
304 let mut height = usize::try_from(resolution.height()).map_err(|err| {
305 VideoError::Source(format!("failed to convert frame height to usize: {err}"))
306 })?;
307 let buf = captured.buffer_bytes();
308 let expected = width.saturating_mul(height).saturating_mul(3);
313 if buf.len() != expected && width > 0 {
314 let actual_pixels = buf.len() / 3;
315 if actual_pixels > 0 && actual_pixels % width == 0 {
316 height = actual_pixels / width;
317 }
318 }
319 let timestamp_us = micros_to_u64(self.started_at.elapsed().as_micros());
320 let frame = Rgb8Frame::from_bytes(self.next_index, timestamp_us, width, height, buf)?;
321 self.next_index += 1;
322 Ok(Some(frame))
323 }
324}
325
326#[cfg(feature = "native-camera")]
327fn preferred_camera_backend() -> ApiBackend {
328 if cfg!(target_os = "linux") {
329 ApiBackend::Video4Linux
330 } else if cfg!(target_os = "windows") {
331 ApiBackend::MediaFoundation
332 } else if cfg!(target_os = "macos") {
333 ApiBackend::AVFoundation
334 } else {
335 ApiBackend::Auto
336 }
337}
338
339#[cfg(feature = "native-camera")]
340fn open_camera_with_backend(device_index: u32, backend: ApiBackend) -> Result<Camera, VideoError> {
341 let requested =
342 RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate);
343 Camera::with_backend(CameraIndex::Index(device_index), requested, backend).map_err(|err| {
344 VideoError::Source(format!(
345 "failed to create camera source with backend {backend:?}: {err}"
346 ))
347 })
348}
349
350#[cfg(not(feature = "native-camera"))]
351impl CameraFrameSource {
352 pub fn open(config: CameraConfig) -> Result<Self, VideoError> {
353 config.validate()?;
354 Err(VideoError::CameraBackendDisabled)
355 }
356
357 pub fn next_rgb8_frame(&mut self) -> Result<Option<Rgb8Frame>, VideoError> {
358 Err(VideoError::CameraBackendDisabled)
359 }
360}
361
362#[cfg(feature = "native-camera")]
363impl FrameSource for CameraFrameSource {
364 fn next_frame(&mut self) -> Result<Option<Frame>, VideoError> {
365 let Some(raw_frame) = self.next_rgb8_frame()? else {
366 return Ok(None);
367 };
368 let frame = rgb8_bytes_to_frame(
369 raw_frame.index(),
370 raw_frame.timestamp_us(),
371 raw_frame.width(),
372 raw_frame.height(),
373 raw_frame.data(),
374 )?;
375 Ok(Some(frame))
376 }
377}
378
379#[cfg(not(feature = "native-camera"))]
380impl FrameSource for CameraFrameSource {
381 fn next_frame(&mut self) -> Result<Option<Frame>, VideoError> {
382 Err(VideoError::CameraBackendDisabled)
383 }
384}