cameras/controls.rs
1//! Runtime camera controls — focus, exposure, white-balance, PTZ, and related
2//! image adjustments.
3//!
4//! The types here describe "what a camera can do" ([`ControlCapabilities`],
5//! [`ControlRange`]), "what the caller wants to set" ([`Controls`]), and a
6//! stable enumeration of every control ([`ControlKind`]). The three free
7//! functions at the bottom ([`control_capabilities`], [`read_controls`],
8//! [`apply_controls`]) dispatch through the active platform backend.
9//!
10//! Every item in this module is gated on the `controls` Cargo feature.
11
12use crate::ActiveBackend;
13use crate::backend::BackendControls;
14use crate::error::Error;
15use crate::types::Device;
16
17/// AC mains frequency choice for cameras that support power-line-frequency filtering.
18///
19/// Supported on Linux via `V4L2_CID_POWER_LINE_FREQUENCY` and on Windows via
20/// `IAMVideoProcAmp`'s `VideoProcAmp_PowerLineFrequency` property (id `10`).
21/// macOS reports [`None`] for this capability — AVFoundation does not expose it.
22#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
23#[non_exhaustive]
24pub enum PowerLineFrequency {
25 /// Flicker-suppression disabled.
26 Disabled,
27 /// 50 Hz mains.
28 Hz50,
29 /// 60 Hz mains.
30 Hz60,
31 /// Hardware auto-detects mains frequency.
32 Auto,
33}
34
35/// Requested tweaks to a device's runtime controls.
36///
37/// Each field uses [`Option::None`] to mean "leave the current value alone"
38/// and [`Option::Some`] to mean "apply this value." Values are in each
39/// platform's native range; consult [`ControlCapabilities`] for the exact
40/// endpoints before writing.
41///
42/// Platforms reject out-of-range or unsupported writes with
43/// [`crate::Error::Unsupported`].
44#[derive(Clone, Debug, Default, PartialEq)]
45pub struct Controls {
46 /// Manual focus position. See [`ControlCapabilities::focus`] for range semantics.
47 pub focus: Option<f32>,
48 /// Enable (`true`) or disable (`false`) continuous auto-focus.
49 pub auto_focus: Option<bool>,
50 /// Manual exposure value in each platform's native unit (seconds on macOS, microseconds on Linux).
51 pub exposure: Option<f32>,
52 /// Enable (`true`) or disable (`false`) auto-exposure. Read-back collapses V4L2 priority modes (shutter/aperture priority) into `Some(true)`; write-back of `Some(true)` applies full AUTO (value 0).
53 pub auto_exposure: Option<bool>,
54 /// Manual white-balance temperature (Kelvin on Linux, synthesized via gains round-trip on macOS).
55 pub white_balance_temperature: Option<f32>,
56 /// Enable (`true`) or disable (`false`) auto white balance.
57 pub auto_white_balance: Option<bool>,
58 /// Image brightness in native units.
59 pub brightness: Option<f32>,
60 /// Image contrast in native units.
61 pub contrast: Option<f32>,
62 /// Image saturation in native units.
63 pub saturation: Option<f32>,
64 /// Image sharpness in native units.
65 pub sharpness: Option<f32>,
66 /// Sensor gain in native units (ISO on macOS).
67 pub gain: Option<f32>,
68 /// Backlight compensation in native units.
69 pub backlight_compensation: Option<f32>,
70 /// AC power-line frequency for flicker suppression.
71 pub power_line_frequency: Option<PowerLineFrequency>,
72 /// Pan axis in native units. PTZ-capable devices only.
73 pub pan: Option<f32>,
74 /// Tilt axis in native units. PTZ-capable devices only.
75 pub tilt: Option<f32>,
76 /// Zoom factor in native units. PTZ-capable devices only.
77 pub zoom: Option<f32>,
78}
79
80/// Reported range for one numeric camera control.
81///
82/// All fields are in the platform's native unit for the control — do not
83/// assume a normalized 0..1 scale. Read endpoints from this struct before
84/// constructing [`Controls`] values.
85#[derive(Copy, Clone, Debug, PartialEq)]
86#[non_exhaustive]
87pub struct ControlRange {
88 /// Minimum accepted value, inclusive.
89 pub min: f32,
90 /// Maximum accepted value, inclusive.
91 pub max: f32,
92 /// Smallest step between accepted values. `0.0` means continuous.
93 pub step: f32,
94 /// Factory default value.
95 pub default: f32,
96}
97
98/// Power-line-frequency capability detail on devices that expose it.
99#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
100#[non_exhaustive]
101pub struct PowerLineFrequencyCapability {
102 /// `true` if 50 Hz filtering is selectable on this device.
103 pub hz50: bool,
104 /// `true` if 60 Hz filtering is selectable on this device.
105 pub hz60: bool,
106 /// `true` if the "off" mode is selectable on this device.
107 pub disabled: bool,
108 /// `true` if hardware auto-detect mode is selectable on this device.
109 pub auto: bool,
110 /// Factory default mode.
111 pub default: PowerLineFrequency,
112}
113
114/// Identifier for every control field on [`Controls`] and [`ControlCapabilities`].
115///
116/// Useful for UI iteration, config serialization, and fetching platform-scoped
117/// caveats via [`ControlKind::caveat`]. Iterate [`ControlKind::ALL`] to visit
118/// every control in a stable order.
119#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
120#[non_exhaustive]
121pub enum ControlKind {
122 /// Manual focus position.
123 Focus,
124 /// Auto-focus toggle.
125 AutoFocus,
126 /// Manual exposure value.
127 Exposure,
128 /// Auto-exposure toggle.
129 AutoExposure,
130 /// Manual white-balance temperature.
131 WhiteBalanceTemperature,
132 /// Auto-white-balance toggle.
133 AutoWhiteBalance,
134 /// Image brightness.
135 Brightness,
136 /// Image contrast.
137 Contrast,
138 /// Image saturation.
139 Saturation,
140 /// Image sharpness.
141 Sharpness,
142 /// Sensor gain (ISO on macOS).
143 Gain,
144 /// Backlight compensation.
145 BacklightCompensation,
146 /// AC mains frequency filtering.
147 PowerLineFrequency,
148 /// Pan axis (PTZ-capable devices only).
149 Pan,
150 /// Tilt axis (PTZ-capable devices only).
151 Tilt,
152 /// Zoom factor (PTZ-capable devices only).
153 Zoom,
154}
155
156impl ControlKind {
157 /// Every [`ControlKind`] variant in declaration order.
158 pub const ALL: [ControlKind; 16] = [
159 ControlKind::Focus,
160 ControlKind::AutoFocus,
161 ControlKind::Exposure,
162 ControlKind::AutoExposure,
163 ControlKind::WhiteBalanceTemperature,
164 ControlKind::AutoWhiteBalance,
165 ControlKind::Brightness,
166 ControlKind::Contrast,
167 ControlKind::Saturation,
168 ControlKind::Sharpness,
169 ControlKind::Gain,
170 ControlKind::BacklightCompensation,
171 ControlKind::PowerLineFrequency,
172 ControlKind::Pan,
173 ControlKind::Tilt,
174 ControlKind::Zoom,
175 ];
176
177 /// Snake_case name matching the corresponding field on [`Controls`].
178 pub fn label(&self) -> &'static str {
179 match self {
180 ControlKind::Focus => "focus",
181 ControlKind::AutoFocus => "auto_focus",
182 ControlKind::Exposure => "exposure",
183 ControlKind::AutoExposure => "auto_exposure",
184 ControlKind::WhiteBalanceTemperature => "white_balance_temperature",
185 ControlKind::AutoWhiteBalance => "auto_white_balance",
186 ControlKind::Brightness => "brightness",
187 ControlKind::Contrast => "contrast",
188 ControlKind::Saturation => "saturation",
189 ControlKind::Sharpness => "sharpness",
190 ControlKind::Gain => "gain",
191 ControlKind::BacklightCompensation => "backlight_compensation",
192 ControlKind::PowerLineFrequency => "power_line_frequency",
193 ControlKind::Pan => "pan",
194 ControlKind::Tilt => "tilt",
195 ControlKind::Zoom => "zoom",
196 }
197 }
198
199 /// Platform-specific caveat for this control on the current target, if any.
200 ///
201 /// Returns `Some` only when the current target cannot expose the control
202 /// regardless of device — useful as UI tooltip text explaining why a
203 /// capability row is marked unsupported. Currently populated for macOS
204 /// controls that AVFoundation does not surface.
205 pub fn caveat(&self) -> Option<&'static str> {
206 #[cfg(target_os = "macos")]
207 {
208 match self {
209 ControlKind::Brightness
210 | ControlKind::Contrast
211 | ControlKind::Saturation
212 | ControlKind::Sharpness
213 | ControlKind::BacklightCompensation => Some(
214 "macOS: AVFoundation doesn't expose per-channel image-processing controls. \
215 Apply CPU/GPU post-processing (shaders, color matrices) over the Frame \
216 bytes in your app. The library is capture-only.",
217 ),
218 ControlKind::PowerLineFrequency => {
219 Some("macOS: AVFoundation doesn't expose AC mains frequency filtering.")
220 }
221 ControlKind::Pan | ControlKind::Tilt => Some(
222 "macOS: AVFoundation doesn't expose pan/tilt controls for built-in or UVC cameras.",
223 ),
224 ControlKind::Focus
225 | ControlKind::AutoFocus
226 | ControlKind::Exposure
227 | ControlKind::AutoExposure
228 | ControlKind::WhiteBalanceTemperature
229 | ControlKind::AutoWhiteBalance
230 | ControlKind::Gain
231 | ControlKind::Zoom => None,
232 }
233 }
234 #[cfg(not(target_os = "macos"))]
235 {
236 None
237 }
238 }
239}
240
241/// What a device reports it can do, per control.
242///
243/// Each field is [`Some`] when the platform exposes the control on this
244/// device and [`None`] when it does not. For numeric controls, `Some` carries
245/// the native [`ControlRange`]. For auto toggles, `Some(true)` means the
246/// device supports auto, `Some(false)` means manual-only, `None` means no
247/// auto control.
248#[derive(Clone, Debug, Default, PartialEq)]
249#[non_exhaustive]
250pub struct ControlCapabilities {
251 /// Focus-position capability.
252 pub focus: Option<ControlRange>,
253 /// Auto-focus toggle capability.
254 pub auto_focus: Option<bool>,
255 /// Exposure-value capability.
256 pub exposure: Option<ControlRange>,
257 /// Auto-exposure toggle capability.
258 pub auto_exposure: Option<bool>,
259 /// White-balance-temperature capability.
260 pub white_balance_temperature: Option<ControlRange>,
261 /// Auto-white-balance toggle capability.
262 pub auto_white_balance: Option<bool>,
263 /// Brightness capability.
264 pub brightness: Option<ControlRange>,
265 /// Contrast capability.
266 pub contrast: Option<ControlRange>,
267 /// Saturation capability.
268 pub saturation: Option<ControlRange>,
269 /// Sharpness capability.
270 pub sharpness: Option<ControlRange>,
271 /// Gain capability.
272 pub gain: Option<ControlRange>,
273 /// Backlight-compensation capability.
274 pub backlight_compensation: Option<ControlRange>,
275 /// Power-line-frequency capability.
276 pub power_line_frequency: Option<PowerLineFrequencyCapability>,
277 /// Pan capability.
278 pub pan: Option<ControlRange>,
279 /// Tilt capability.
280 pub tilt: Option<ControlRange>,
281 /// Zoom capability.
282 pub zoom: Option<ControlRange>,
283}
284
285/// Report which runtime controls the given device exposes and their native ranges.
286///
287/// Fields on the returned [`ControlCapabilities`] are `None` for controls the
288/// platform / device does not expose. Ranges are in each platform's native
289/// unit — do not assume a normalized scale.
290pub fn control_capabilities(device: &Device) -> Result<ControlCapabilities, Error> {
291 <ActiveBackend as BackendControls>::control_capabilities(&device.id)
292}
293
294/// Read the current value of every exposed control on `device`.
295///
296/// Fields are `None` for controls the device does not expose. Read-back of
297/// `auto_exposure` collapses V4L2 priority modes into `Some(true)`.
298pub fn read_controls(device: &Device) -> Result<Controls, Error> {
299 <ActiveBackend as BackendControls>::read_controls(&device.id)
300}
301
302/// Apply every [`Some`]-valued field in `controls` to `device`.
303///
304/// `None` fields are left at their current value. Returns the first platform
305/// failure encountered; does not preflight against [`control_capabilities`].
306pub fn apply_controls(device: &Device, controls: &Controls) -> Result<(), Error> {
307 <ActiveBackend as BackendControls>::apply_controls(&device.id, controls)
308}
309
310/// Build a [`Controls`] that, when applied, returns every exposed control to
311/// a sensible "factory" state.
312///
313/// For axes that have an auto mode (focus, exposure, white-balance
314/// temperature), prefers enabling auto over writing a manual default: on
315/// most UVC devices writing a manual value implicitly disables auto, so
316/// leaving the numeric field `None` lets the camera's own AE / AF / AWB
317/// algorithms converge instead of pinning a stale value. When auto is not
318/// available on an axis, the numeric field falls back to
319/// [`ControlRange::default`].
320///
321/// Orphan numeric fields (brightness, contrast, saturation, sharpness, gain,
322/// backlight_compensation, pan, tilt, zoom, power_line_frequency) always
323/// carry their platform-reported default, because UVC has no auto mode for
324/// image-adjustment knobs.
325///
326/// Fields the device does not expose stay `None`.
327///
328/// Platform caveats apply: V4L2 reports genuine driver defaults; Media
329/// Foundation reports UVC-populated defaults which most drivers honor;
330/// AVFoundation synthesizes defaults from current-state reads rather than
331/// tracking true factory values.
332pub fn default_controls(capabilities: &ControlCapabilities) -> Controls {
333 let auto_toggle = |supported: Option<bool>| match supported {
334 Some(true) => Some(true),
335 _ => None,
336 };
337 let numeric_with_auto_fallback = |range: Option<&ControlRange>, auto: Option<bool>| {
338 if auto == Some(true) {
339 None
340 } else {
341 range.map(|range| range.default)
342 }
343 };
344 Controls {
345 focus: numeric_with_auto_fallback(capabilities.focus.as_ref(), capabilities.auto_focus),
346 auto_focus: auto_toggle(capabilities.auto_focus),
347 exposure: numeric_with_auto_fallback(
348 capabilities.exposure.as_ref(),
349 capabilities.auto_exposure,
350 ),
351 auto_exposure: auto_toggle(capabilities.auto_exposure),
352 white_balance_temperature: numeric_with_auto_fallback(
353 capabilities.white_balance_temperature.as_ref(),
354 capabilities.auto_white_balance,
355 ),
356 auto_white_balance: auto_toggle(capabilities.auto_white_balance),
357 brightness: capabilities.brightness.as_ref().map(|range| range.default),
358 contrast: capabilities.contrast.as_ref().map(|range| range.default),
359 saturation: capabilities.saturation.as_ref().map(|range| range.default),
360 sharpness: capabilities.sharpness.as_ref().map(|range| range.default),
361 gain: capabilities.gain.as_ref().map(|range| range.default),
362 backlight_compensation: capabilities
363 .backlight_compensation
364 .as_ref()
365 .map(|range| range.default),
366 power_line_frequency: capabilities
367 .power_line_frequency
368 .as_ref()
369 .map(|capability| capability.default),
370 pan: capabilities.pan.as_ref().map(|range| range.default),
371 tilt: capabilities.tilt.as_ref().map(|range| range.default),
372 zoom: capabilities.zoom.as_ref().map(|range| range.default),
373 }
374}
375
376/// Probe the device's capabilities, build a defaults [`Controls`] via
377/// [`default_controls`], and apply it in one call.
378///
379/// Intended as a "the camera looks wrong, start over" escape hatch for UIs.
380/// See [`default_controls`] for the platform-specific meaning of "default."
381pub fn reset_to_defaults(device: &Device) -> Result<(), Error> {
382 let capabilities = control_capabilities(device)?;
383 let controls = default_controls(&capabilities);
384 apply_controls(device, &controls)
385}