Skip to main content

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}