Skip to main content

oxidant/api/
webcam.rs

1//! Webcam live-inference API; frames go straight to the texture, only detections cross the bridge.
2
3use crate::frb_generated::StreamSink;
4
5pub struct CameraDesc {
6    pub index: u32,
7    pub name: String,
8}
9
10/// A detection in source-frame pixels; a field-accessible mirror of the opaque core `Detection`.
11pub struct DetectionBox {
12    pub x1: f32,
13    pub y1: f32,
14    pub x2: f32,
15    pub y2: f32,
16    pub score: f32,
17    pub class_id: i64,
18}
19
20#[cfg(all(feature = "gstreamer", onnx))]
21impl From<crate::Detection> for DetectionBox {
22    fn from(d: crate::Detection) -> Self {
23        Self {
24            x1: d.x1,
25            y1: d.y1,
26            x2: d.x2,
27            y2: d.y2,
28            score: d.score,
29            class_id: d.class_id,
30        }
31    }
32}
33
34pub struct WebcamStreamConfig {
35    /// `videotestsrc` instead of a real camera (containers/CI have none).
36    pub use_test_source: bool,
37    /// Camera enumeration index; `None` = first available.
38    pub device_index: Option<u32>,
39    pub width: u32,
40    pub height: u32,
41    pub framerate: u32,
42    pub model_path: String,
43    pub score_threshold: f32,
44    /// Texture id from the native plugin's `create` call; `0` = headless.
45    pub texture_id: i64,
46    /// Override for the DLL exporting `knt_push_frame`.
47    pub texture_library: Option<String>,
48}
49
50pub struct DetectionEvent {
51    pub detections: Vec<DetectionBox>,
52    pub frame_sequence: u64,
53    pub pts_ms: Option<u64>,
54    pub inference_ms: f64,
55    pub fps: f32,
56}
57
58/// Whether this build can run the webcam pipeline: a compile-time answer, cheap enough for a widget's build.
59#[flutter_rust_bridge::frb(sync)]
60pub fn webcam_inference_available() -> bool {
61    cfg!(all(feature = "gstreamer", onnx))
62}
63
64/// Not sync on purpose: the device monitor starts libcamera, about a second the UI thread must not wait out.
65pub fn list_cameras() -> Result<Vec<CameraDesc>, String> {
66    #[cfg(feature = "gstreamer")]
67    {
68        kataglyphis_media::list_cameras()
69            .map(|cams| {
70                cams.into_iter()
71                    .map(|c| CameraDesc {
72                        index: c.index,
73                        name: c.display_name,
74                    })
75                    .collect()
76            })
77            .map_err(|e| format!("camera enumeration failed: {e:#}"))
78    }
79    #[cfg(not(feature = "gstreamer"))]
80    {
81        Err("webcam capture is disabled. Build with --features gstreamer".into())
82    }
83}
84
85/// Streams capture + inference events to `sink` until [`stop_webcam_inference`]; errors if running.
86pub fn start_webcam_inference(
87    config: WebcamStreamConfig,
88    sink: StreamSink<DetectionEvent>,
89) -> Result<(), String> {
90    #[cfg(all(feature = "gstreamer", onnx))]
91    {
92        enabled::start(config, sink).map_err(|e| {
93            log::error!("start_webcam_inference failed: {e:#}");
94            format!("start_webcam_inference failed: {e:#}")
95        })
96    }
97    #[cfg(not(all(feature = "gstreamer", onnx)))]
98    {
99        let (_, _) = (config, sink);
100        Err("webcam inference is disabled. Build with --features gstreamer,onnxruntime*".into())
101    }
102}
103
104/// Stops the running session (no-op when idle).
105#[flutter_rust_bridge::frb(sync)]
106pub fn stop_webcam_inference() {
107    #[cfg(all(feature = "gstreamer", onnx))]
108    enabled::stop();
109}
110
111#[cfg(all(feature = "gstreamer", onnx))]
112mod enabled {
113    use std::sync::{Mutex, OnceLock};
114
115    use anyhow::Result;
116    use kataglyphis_media::CameraSource;
117
118    use super::{DetectionEvent, StreamSink, WebcamStreamConfig};
119    use crate::webcam_engine::{EngineConfig, TextureTarget, WebcamEngine};
120
121    static ENGINE: OnceLock<Mutex<Option<WebcamEngine>>> = OnceLock::new();
122
123    fn engine_slot() -> &'static Mutex<Option<WebcamEngine>> {
124        ENGINE.get_or_init(|| Mutex::new(None))
125    }
126
127    fn lock() -> std::sync::MutexGuard<'static, Option<WebcamEngine>> {
128        engine_slot()
129            .lock()
130            .unwrap_or_else(|poisoned| poisoned.into_inner())
131    }
132
133    pub(super) fn start(
134        config: WebcamStreamConfig,
135        sink: StreamSink<DetectionEvent>,
136    ) -> Result<()> {
137        let mut slot = lock();
138        if slot.is_some() {
139            anyhow::bail!("webcam inference is already running; stop it first");
140        }
141
142        let source = if config.use_test_source {
143            CameraSource::Test
144        } else {
145            match config.device_index {
146                Some(index) => CameraSource::Device(index),
147                None => CameraSource::Auto,
148            }
149        };
150
151        let texture = (config.texture_id != 0).then(|| TextureTarget {
152            library: config
153                .texture_library
154                .clone()
155                .unwrap_or_else(default_texture_library),
156            texture_id: config.texture_id,
157        });
158
159        let engine = WebcamEngine::start(
160            EngineConfig {
161                source,
162                width: config.width,
163                height: config.height,
164                framerate: config.framerate,
165                model_path: config.model_path,
166                score_threshold: config.score_threshold,
167                texture,
168            },
169            Box::new(move |event| {
170                let _ = sink.add(DetectionEvent {
171                    detections: event.detections.into_iter().map(Into::into).collect(),
172                    frame_sequence: event.frame_sequence,
173                    pts_ms: event.pts_ms,
174                    inference_ms: event.inference_ms,
175                    fps: event.fps,
176                });
177            }),
178        )?;
179
180        *slot = Some(engine);
181        Ok(())
182    }
183
184    pub(super) fn stop() {
185        let engine = lock().take();
186        if let Some(mut engine) = engine {
187            engine.stop();
188        }
189    }
190
191    pub(super) fn default_texture_library() -> String {
192        if cfg!(windows) {
193            "kataglyphis_native_inference_plugin.dll".into()
194        } else {
195            "libkataglyphis_native_inference_plugin.so".into()
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    #[test]
203    fn the_probe_answers_what_this_build_was_compiled_with() {
204        assert_eq!(
205            super::webcam_inference_available(),
206            cfg!(all(feature = "gstreamer", onnx))
207        );
208    }
209
210    #[cfg(all(feature = "gstreamer", onnx))]
211    #[test]
212    fn the_default_texture_library_is_the_native_plugin() {
213        let name = super::enabled::default_texture_library();
214        assert!(
215            name.contains("kataglyphis_native_inference_plugin"),
216            "{name}"
217        );
218    }
219}