#![allow(clippy::doc_markdown)]
pub mod capture;
pub mod gesture;
#[cfg(test)]
mod tests;
pub use capture::{capture_frame, list_cameras};
pub use gesture::{capture_and_detect, detect_gestures, gesture_listen};
use std::ffi::c_void;
use std::time::Duration;
use base64::Engine as _;
use serde::{Deserialize, Serialize};
use tracing::debug;
#[derive(Debug, thiserror::Error)]
pub enum CameraError {
#[error("camera_denied: Camera permission denied — open System Settings > Privacy & Security > Camera and grant access")]
PermissionDenied,
#[error("device_not_found: Camera device '{0}' not found — call ax_camera_devices to list available cameras")]
DeviceNotFound(String),
#[error("duration_exceeded: Requested duration {0:.1}s exceeds the maximum of 60s")]
DurationExceeded(f64),
#[error("unknown_gesture: '{0}' is not a recognised gesture name — valid values: thumbs_up, thumbs_down, wave, stop, point, nod, shake")]
UnknownGesture(String),
#[error("capture_failed: {0}")]
CaptureFailed(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CameraPosition {
Front,
Back,
External,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CameraDevice {
pub id: String,
pub name: String,
pub position: CameraPosition,
pub is_default: bool,
}
#[derive(Debug, Clone)]
pub struct ImageData {
pub width: u32,
pub height: u32,
pub jpeg_data: Vec<u8>,
}
impl ImageData {
#[must_use]
pub fn base64_jpeg(&self) -> String {
base64::engine::general_purpose::STANDARD.encode(&self.jpeg_data)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Gesture {
ThumbsUp,
ThumbsDown,
Wave,
Stop,
Point,
Nod,
Shake,
}
impl Gesture {
pub fn from_name(name: &str) -> Result<Self, CameraError> {
match name {
"thumbs_up" => Ok(Self::ThumbsUp),
"thumbs_down" => Ok(Self::ThumbsDown),
"wave" => Ok(Self::Wave),
"stop" => Ok(Self::Stop),
"point" => Ok(Self::Point),
"nod" => Ok(Self::Nod),
"shake" => Ok(Self::Shake),
other => Err(CameraError::UnknownGesture(other.to_string())),
}
}
#[must_use]
pub fn as_name(&self) -> &'static str {
match self {
Self::ThumbsUp => "thumbs_up",
Self::ThumbsDown => "thumbs_down",
Self::Wave => "wave",
Self::Stop => "stop",
Self::Point => "point",
Self::Nod => "nod",
Self::Shake => "shake",
}
}
#[must_use]
pub fn all_names() -> &'static [&'static str] {
&[
"thumbs_up",
"thumbs_down",
"wave",
"stop",
"point",
"nod",
"shake",
]
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Hand {
Left,
Right,
Face,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GestureDetection {
pub gesture: Gesture,
pub confidence: f32,
pub hand: Hand,
}
pub const MAX_GESTURE_DURATION_SECS: f64 = 60.0;
pub fn validate_duration(duration_secs: f64) -> Result<Duration, CameraError> {
if duration_secs > MAX_GESTURE_DURATION_SECS {
return Err(CameraError::DurationExceeded(duration_secs));
}
Ok(Duration::from_secs_f64(duration_secs.max(0.0)))
}
pub fn validate_gesture_names(names: &[&str]) -> Result<Vec<Gesture>, CameraError> {
names.iter().map(|n| Gesture::from_name(n)).collect()
}
#[must_use]
pub fn check_camera_permission() -> bool {
let status = unsafe { av_camera_authorization_status() };
if status == AV_AUTH_AUTHORIZED {
return true;
}
if status == AV_AUTH_NOT_DETERMINED {
debug!("Camera permission not determined, requesting access");
let granted = unsafe { av_request_camera_access() };
return granted == 1;
}
false
}
pub(crate) type CChar = std::os::raw::c_char;
pub(crate) const AV_AUTH_AUTHORIZED: i32 = 3;
const AV_AUTH_NOT_DETERMINED: i32 = 0;
#[repr(C)]
pub(crate) struct CDeviceInfo {
pub unique_id: *const CChar,
pub localized_name: *const CChar,
pub position: i32,
pub is_default: i32,
}
#[repr(C)]
pub(crate) struct CFrameResult {
pub jpeg_data: *mut c_void,
pub jpeg_len: usize,
pub width: u32,
pub height: u32,
pub error_msg: *const CChar,
}
#[repr(C)]
pub(crate) struct CGestureItem {
pub gesture_name: *const CChar,
pub confidence: f32,
pub hand_code: i32,
}
#[repr(C)]
pub(crate) struct CGestureList {
pub items: *mut CGestureItem,
pub count: usize,
pub error_msg: *const CChar,
}
extern "C" {
pub(crate) fn av_camera_authorization_status() -> i32;
pub(crate) fn av_request_camera_access() -> i32;
pub(crate) fn av_list_cameras(count: *mut usize) -> *mut CDeviceInfo;
pub(crate) fn av_free_camera_list(ptr: *mut CDeviceInfo, count: usize);
pub(crate) fn av_capture_frame(device_id: *const CChar, result: *mut CFrameResult) -> bool;
pub(crate) fn av_free_frame_result(result: *mut CFrameResult);
pub(crate) fn vn_detect_gestures(
jpeg_data: *const u8,
jpeg_len: usize,
list: *mut CGestureList,
) -> bool;
pub(crate) fn vn_free_gesture_list(list: *mut CGestureList);
}