1use crate::frb_generated::StreamSink;
4
5pub struct CameraDesc {
6 pub index: u32,
7 pub name: String,
8}
9
10pub 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 pub use_test_source: bool,
37 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 pub texture_id: i64,
46 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#[flutter_rust_bridge::frb(sync)]
60pub fn webcam_inference_available() -> bool {
61 cfg!(all(feature = "gstreamer", onnx))
62}
63
64pub 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
85pub 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#[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}