"""macOS UVC controller — backed by pyuvc (libuvc bindings).""" from __future__ import annotations import logging from app.camera.uvc.base import UvcControllerBase, UvcParam, UvcParamInfo from app.camera.uvc.stub import NullUvcController logger = logging.getLogger(__name__) # pyuvc provides access to UVC controls via libuvc. # Install: pip install pyuvc (requires libusb + libjpeg-turbo via brew) # # If pyuvc is not installed, fall back silently to NullUvcController. try: import uvc # type: ignore[import-untyped] _PYUVC_AVAILABLE = True except ImportError: _PYUVC_AVAILABLE = False logger.debug("pyuvc not available — macOS UVC controls disabled") # Map UvcParam → pyuvc control name string _CONTROL_NAME: dict[UvcParam, str] = { UvcParam.BRIGHTNESS: "Brightness", UvcParam.CONTRAST: "Contrast", UvcParam.SATURATION: "Saturation", UvcParam.HUE: "Hue", UvcParam.SHARPNESS: "Sharpness", UvcParam.GAMMA: "Gamma", UvcParam.WHITE_BALANCE: "White Balance temperature", UvcParam.BACKLIGHT_COMPENSATION: "Backlight Compensation", UvcParam.EXPOSURE: "Absolute Exposure Time", } class MacUvcController(UvcControllerBase): """ UVC camera controls on macOS via pyuvc / libuvc. pyuvc: https://github.com/pupil-labs/pyuvc Install: pip install pyuvc (requires: brew install libusb jpeg-turbo) """ def __init__(self, device_name: str) -> None: if not _PYUVC_AVAILABLE: raise ImportError("pyuvc not installed") self._device_name = device_name self._cap: object | None = None self._controls: dict[str, object] = {} # ------------------------------------------------------------------ # Lifecycle # ------------------------------------------------------------------ def open(self, device_name: str) -> bool: self.close() if not _PYUVC_AVAILABLE: return False try: device_list = uvc.device_list() target = None for d in device_list: if device_name.lower() in d.get("name", "").lower(): target = d break if target is None and device_list: target = device_list[0] logger.warning( "UVC: camera '%s' not found by name, using '%s'", device_name, target.get("name"), ) if target is None: logger.warning("UVC: no UVC devices found on macOS") return False self._cap = uvc.Capture(target["uid"]) # Index controls by name for fast lookup self._controls = {c.display_name: c for c in self._cap.controls} logger.info( "UVC: opened '%s', controls: %s", target.get("name"), list(self._controls.keys()), ) return True except Exception as exc: logger.warning("UVC macOS open failed: %s", exc) self._cap = None return False def close(self) -> None: if self._cap is not None: try: self._cap.close() except Exception: pass self._cap = None self._controls.clear() def is_open(self) -> bool: return self._cap is not None # ------------------------------------------------------------------ # Query # ------------------------------------------------------------------ def get_param_info(self, param: UvcParam) -> UvcParamInfo: ctrl_name = _CONTROL_NAME.get(param) if ctrl_name is None or ctrl_name not in self._controls: return UvcParamInfo(param=param, supported=False) try: ctrl = self._controls[ctrl_name] auto_supported = hasattr(ctrl, "auto_mode") auto_enabled = bool(ctrl.auto_mode) if auto_supported else False return UvcParamInfo( param=param, supported=True, minimum=int(ctrl.min_val), maximum=int(ctrl.max_val), default=int(ctrl.def_val), current=int(ctrl.value), step=int(getattr(ctrl, "step", 1)), auto_supported=auto_supported, auto_enabled=auto_enabled, ) except Exception as exc: logger.debug("UVC get_param_info(%s): %s", param.name, exc) return UvcParamInfo(param=param, supported=False) def get_all_params(self) -> list[UvcParamInfo]: return [self.get_param_info(p) for p in UvcParam] # ------------------------------------------------------------------ # Set # ------------------------------------------------------------------ def set_value(self, param: UvcParam, value: int) -> bool: ctrl_name = _CONTROL_NAME.get(param) if ctrl_name is None or ctrl_name not in self._controls: return False try: self._controls[ctrl_name].value = value logger.debug("UVC set %s = %d", param.name, value) return True except Exception as exc: logger.warning("UVC set_value(%s, %d): %s", param.name, value, exc) return False def set_auto(self, param: UvcParam, enabled: bool) -> bool: ctrl_name = _CONTROL_NAME.get(param) if ctrl_name is None or ctrl_name not in self._controls: return False try: ctrl = self._controls[ctrl_name] if hasattr(ctrl, "auto_mode"): ctrl.auto_mode = enabled logger.debug("UVC auto %s = %s", param.name, enabled) return True return False except Exception as exc: logger.warning("UVC set_auto(%s, %s): %s", param.name, enabled, exc) return False def make_mac_uvc_controller(device_name: str) -> UvcControllerBase: """Factory: returns MacUvcController or NullUvcController if pyuvc absent.""" if not _PYUVC_AVAILABLE: return NullUvcController() ctrl = MacUvcController(device_name) if ctrl.open(device_name): return ctrl return NullUvcController()