11use log:: { error, info} ;
22use nokhwa:: Camera ;
33use nokhwa:: pixel_format:: RgbFormat ;
4- use nokhwa:: utils:: { CameraIndex , RequestedFormat , RequestedFormatType } ;
4+ use nokhwa:: utils:: {
5+ CameraFormat , CameraIndex , FrameFormat , RequestedFormat , RequestedFormatType , Resolution ,
6+ } ;
57use std:: path:: PathBuf ;
8+ use std:: sync:: mpsc:: { Receiver , SyncSender , TryRecvError } ;
69use std:: thread;
7- use std:: time:: { SystemTime , UNIX_EPOCH } ;
10+ use std:: time:: { Duration , Instant , SystemTime , UNIX_EPOCH } ;
811
912/// Captures a single frame from the default webcam and saves it as a JPEG
1013/// under `photos_dir`, running on a dedicated thread so it never blocks the UI.
@@ -19,7 +22,7 @@ pub fn capture_donation_photo(photos_dir: &str, username: &str) {
1922 } ) ;
2023}
2124
22- fn capture_and_save ( photos_dir : & str , username : & str ) -> Result < ( ) , String > {
25+ fn open_camera ( ) -> Result < Camera , String > {
2326 let index = CameraIndex :: Index ( 0 ) ;
2427 let requested =
2528 RequestedFormat :: new :: < RgbFormat > ( RequestedFormatType :: AbsoluteHighestFrameRate ) ;
@@ -31,6 +34,31 @@ fn capture_and_save(photos_dir: &str, username: &str) -> Result<(), String> {
3134 . open_stream ( )
3235 . map_err ( |e| format ! ( "failed to open webcam stream: {e}" ) ) ?;
3336
37+ Ok ( camera)
38+ }
39+
40+ /// Opens the camera for the live diagnostics preview. Decoding a full-resolution
41+ /// frame (e.g. 1920x1080, which many webcams default to) takes ~450ms — far too
42+ /// slow for a live view — so this asks for a modest 640x480 mode first, which
43+ /// virtually every UVC/AVFoundation webcam supports and decodes in under 100ms.
44+ /// Falls back to whatever the camera actually offers if that request is rejected.
45+ fn open_preview_camera ( ) -> Result < Camera , String > {
46+ let small = RequestedFormat :: new :: < RgbFormat > ( RequestedFormatType :: Exact ( CameraFormat :: new (
47+ Resolution :: new ( 640 , 480 ) ,
48+ FrameFormat :: MJPEG ,
49+ 30 ,
50+ ) ) ) ;
51+ if let Ok ( mut camera) = Camera :: new ( CameraIndex :: Index ( 0 ) , small)
52+ && camera. open_stream ( ) . is_ok ( )
53+ {
54+ return Ok ( camera) ;
55+ }
56+ open_camera ( )
57+ }
58+
59+ fn capture_and_save ( photos_dir : & str , username : & str ) -> Result < ( ) , String > {
60+ let mut camera = open_camera ( ) ?;
61+
3462 // Discard the first couple of frames to let auto-exposure/white-balance settle.
3563 for _ in 0 ..2 {
3664 let _ = camera. frame ( ) ;
@@ -69,3 +97,73 @@ fn capture_and_save(photos_dir: &str, username: &str) -> Result<(), String> {
6997 info ! ( "📷 Saved donation photo to {path:?}" ) ;
7098 Ok ( ( ) )
7199}
100+
101+ /// Commands accepted by the [`spawn_preview`] thread.
102+ pub enum PreviewCommand {
103+ Start ,
104+ Stop ,
105+ }
106+
107+ /// A single decoded RGB8 frame, ready to hand to `slint::SharedPixelBuffer`.
108+ pub struct PreviewFrame {
109+ pub rgb : Vec < u8 > ,
110+ pub width : u32 ,
111+ pub height : u32 ,
112+ }
113+
114+ const PREVIEW_FRAME_INTERVAL : Duration = Duration :: from_millis ( 100 ) ;
115+
116+ /// Spawns a thread that owns the webcam for as long as the diagnostics page's
117+ /// live preview is active. `Start`/`Stop` on `cmd_rx` open/close the device;
118+ /// while open, frames are pushed to `frame_tx` on a best-effort basis (a full
119+ /// channel just means the consumer hasn't caught up, so the frame is dropped).
120+ pub fn spawn_preview ( cmd_rx : Receiver < PreviewCommand > , frame_tx : SyncSender < PreviewFrame > ) {
121+ thread:: spawn ( move || {
122+ let mut camera: Option < Camera > = None ;
123+
124+ loop {
125+ let cmd = if camera. is_some ( ) {
126+ match cmd_rx. try_recv ( ) {
127+ Ok ( cmd) => Some ( cmd) ,
128+ Err ( TryRecvError :: Empty ) => None ,
129+ Err ( TryRecvError :: Disconnected ) => break ,
130+ }
131+ } else {
132+ match cmd_rx. recv ( ) {
133+ Ok ( cmd) => Some ( cmd) ,
134+ Err ( _) => break ,
135+ }
136+ } ;
137+
138+ match cmd {
139+ Some ( PreviewCommand :: Start ) if camera. is_none ( ) => match open_preview_camera ( ) {
140+ Ok ( cam) => camera = Some ( cam) ,
141+ Err ( e) => error ! ( "📷 Failed to start preview: {}" , e) ,
142+ } ,
143+ Some ( PreviewCommand :: Stop ) => camera = None ,
144+ _ => { }
145+ }
146+
147+ let Some ( cam) = camera. as_mut ( ) else {
148+ continue ;
149+ } ;
150+
151+ let frame_start = Instant :: now ( ) ;
152+ match cam. frame ( ) . and_then ( |f| f. decode_image :: < RgbFormat > ( ) ) {
153+ Ok ( image) => {
154+ let _ = frame_tx. try_send ( PreviewFrame {
155+ width : image. width ( ) ,
156+ height : image. height ( ) ,
157+ rgb : image. into_raw ( ) ,
158+ } ) ;
159+ }
160+ Err ( e) => error ! ( "📷 Preview frame capture failed: {}" , e) ,
161+ }
162+ // Capture+decode already ate into the budget; only sleep the remainder
163+ // so the preview holds close to its target cadence instead of drifting.
164+ if let Some ( remaining) = PREVIEW_FRAME_INTERVAL . checked_sub ( frame_start. elapsed ( ) ) {
165+ thread:: sleep ( remaining) ;
166+ }
167+ }
168+ } ) ;
169+ }
0 commit comments