kataglyphis_rustprojecttemplate/api/
webcam.rs

1//! Webcam live-inference API: camera enumeration + a detection event stream.
2//!
3//! Video frames never cross the bridge — they go straight from the capture
4//! pipeline into the Flutter texture via the native plugin's C ABI. Only
5//! detection metadata streams to Dart, where the UI overlays boxes.
6
7use crate::frb_generated::StreamSink;
8
9pub struct CameraDesc {
10    pub index: u32,
11    pub name: String,
12}
13
14/// A detection in source-frame pixel coordinates. Field-accessible Dart
15/// mirror of `kataglyphis_core::detection::Detection` (which the bridge
16/// would otherwise expose as an opaque type).
17pub struct DetectionBox {
18    pub x1: f32,
19    pub y1: f32,
20    pub x2: f32,
21    pub y2: f32,
22    pub score: f32,
23    pub class_id: i64,
24}
25
26#[cfg(all(feature = "gstreamer", onnx))]
27impl From<crate::Detection> for DetectionBox {
28    fn from(d: crate::Detection) -> Self {
29        Self {
30            x1: d.x1,
31            y1: d.y1,
32            x2: d.x2,
33            y2: d.y2,
34            score: d.score,
35            class_id: d.class_id,
36        }
37    }
38}
39
40pub struct WebcamStreamConfig {
41    /// `videotestsrc` instead of a real camera (containers/CI have none).
42    pub use_test_source: bool,
43    /// Camera enumeration index; `None` = first available.
44    pub device_index: Option<u32>,
45    pub width: u32,
46    pub height: u32,
47    pub framerate: u32,
48    pub model_path: String,
49    pub score_threshold: f32,
50    /// Texture id from the native plugin's `create` call; `0` = headless.
51    pub texture_id: i64,
52    /// Override for the DLL exporting `knt_push_frame`.
53    pub texture_library: Option<String>,
54}
55
56pub struct DetectionEvent {
57    pub detections: Vec<DetectionBox>,
58    pub frame_sequence: u64,
59    pub pts_ms: Option<u64>,
60    pub inference_ms: f64,
61    pub fps: f32,
62}
63
64#[flutter_rust_bridge::frb(sync)]
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/// Starts capture + inference; events arrive on `sink` until
86/// [`stop_webcam_inference`] is called. Errors if already running.
87pub fn start_webcam_inference(
88    config: WebcamStreamConfig,
89    sink: StreamSink<DetectionEvent>,
90) -> Result<(), String> {
91    #[cfg(all(feature = "gstreamer", onnx))]
92    {
93        enabled::start(config, sink).map_err(|e| {
94            log::error!("start_webcam_inference failed: {e:#}");
95            format!("start_webcam_inference failed: {e:#}")
96        })
97    }
98    #[cfg(not(all(feature = "gstreamer", onnx)))]
99    {
100        let (_, _) = (config, sink);
101        Err("webcam inference is disabled. Build with --features gstreamer,onnxruntime*".into())
102    }
103}
104
105/// Stops the running session (no-op when idle).
106#[flutter_rust_bridge::frb(sync)]
107pub fn stop_webcam_inference() {
108    #[cfg(all(feature = "gstreamer", onnx))]
109    enabled::stop();
110}
111
112#[cfg(all(feature = "gstreamer", onnx))]
113mod enabled {
114    use std::sync::{Mutex, OnceLock};
115
116    use anyhow::Result;
117    use kataglyphis_media::CameraSource;
118
119    use super::{DetectionEvent, StreamSink, WebcamStreamConfig};
120    use crate::webcam_engine::{EngineConfig, TextureTarget, WebcamEngine};
121
122    static ENGINE: OnceLock<Mutex<Option<WebcamEngine>>> = OnceLock::new();
123
124    fn engine_slot() -> &'static Mutex<Option<WebcamEngine>> {
125        ENGINE.get_or_init(|| Mutex::new(None))
126    }
127
128    fn lock() -> std::sync::MutexGuard<'static, Option<WebcamEngine>> {
129        engine_slot()
130            .lock()
131            .unwrap_or_else(|poisoned| poisoned.into_inner())
132    }
133
134    pub(super) fn start(
135        config: WebcamStreamConfig,
136        sink: StreamSink<DetectionEvent>,
137    ) -> Result<()> {
138        let mut slot = lock();
139        if slot.is_some() {
140            anyhow::bail!("webcam inference is already running; stop it first");
141        }
142
143        let source = if config.use_test_source {
144            CameraSource::Test
145        } else {
146            match config.device_index {
147                Some(index) => CameraSource::Device(index),
148                None => CameraSource::Auto,
149            }
150        };
151
152        let texture = (config.texture_id != 0).then(|| TextureTarget {
153            library: config
154                .texture_library
155                .clone()
156                .unwrap_or_else(default_texture_library),
157            texture_id: config.texture_id,
158        });
159
160        let engine = WebcamEngine::start(
161            EngineConfig {
162                source,
163                width: config.width,
164                height: config.height,
165                framerate: config.framerate,
166                model_path: config.model_path,
167                score_threshold: config.score_threshold,
168                texture,
169            },
170            Box::new(move |event| {
171                let _ = sink.add(DetectionEvent {
172                    detections: event.detections.into_iter().map(Into::into).collect(),
173                    frame_sequence: event.frame_sequence,
174                    pts_ms: event.pts_ms,
175                    inference_ms: event.inference_ms,
176                    fps: event.fps,
177                });
178            }),
179        )?;
180
181        *slot = Some(engine);
182        Ok(())
183    }
184
185    pub(super) fn stop() {
186        let engine = lock().take();
187        if let Some(mut engine) = engine {
188            engine.stop();
189        }
190    }
191
192    fn default_texture_library() -> String {
193        if cfg!(windows) {
194            "kataglyphis_native_inference_plugin.dll".into()
195        } else {
196            "libkataglyphis_native_inference_plugin.so".into()
197        }
198    }
199}