Skip to main content

ccap/
provider.rs

1//! Camera provider for synchronous camera capture operations
2
3use crate::{error::*, frame::*, sys, types::*};
4use std::ffi::{CStr, CString};
5use std::ptr;
6use std::sync::Mutex;
7
8/// A wrapper around a raw pointer that can be safely shared between threads.
9/// This is used for storing callback pointers that we know are safe to share
10/// because the callback itself is `Send + Sync`.
11struct SendSyncPtr(*mut std::ffi::c_void);
12
13// SAFETY: The pointer stored here always points to a `Box<ErrorCallbackBox>`
14// where `ErrorCallbackBox = Box<dyn Fn(i32, &str) + Send + Sync>`.
15// The underlying callback is `Send + Sync`, so the pointer is safe to share.
16unsafe impl Send for SendSyncPtr {}
17unsafe impl Sync for SendSyncPtr {}
18
19// Global error callback storage - must be at module level to be shared between functions
20static GLOBAL_ERROR_CALLBACK: Mutex<Option<SendSyncPtr>> = Mutex::new(None);
21
22fn optional_c_string(value: Option<&str>, parameter_name: &str) -> Result<Option<CString>> {
23    value
24        .map(|text| {
25            CString::new(text).map_err(|_| {
26                CcapError::InvalidParameter(format!("{} contains null byte", parameter_name))
27            })
28        })
29        .transpose()
30}
31
32/// Type alias for the global error callback
33///
34/// # Thread Safety
35///
36/// `Provider` implements `Send` to allow moving the provider between threads.
37/// However, the underlying C++ implementation is **NOT thread-safe**.
38///
39/// **Important**: You must ensure that:
40/// - Only one thread accesses the `Provider` at a time
41/// - Use `Arc<Mutex<Provider>>` or similar synchronization if sharing between threads
42/// - If you need to integrate with an async runtime, wrap the `Provider` yourself (e.g. with a mutex and a dedicated worker thread)
43///
44/// # Example (Safe Multi-threaded Usage)
45///
46/// ```ignore
47/// use std::sync::{Arc, Mutex};
48/// use ccap::Provider;
49///
50/// let provider = Arc::new(Mutex::new(Provider::new()?));
51/// let provider_clone = Arc::clone(&provider);
52///
53/// std::thread::spawn(move || {
54///     let mut guard = provider_clone.lock().unwrap();
55///     // Safe: mutex ensures exclusive access
56///     guard.grab_frame(1000).ok();
57/// });
58/// ```
59pub struct Provider {
60    handle: *mut sys::CcapProvider,
61    is_opened: bool,
62    callback_ptr: Option<*mut std::ffi::c_void>,
63}
64
65// SAFETY: Provider is Send because:
66// 1. The handle is a raw pointer to C++ Provider, which can be safely moved between threads
67// 2. The callback_ptr ownership is properly tracked and cleaned up
68// 3. We document that users MUST synchronize access externally
69//
70// WARNING: The underlying C++ Provider is NOT thread-safe. Moving the Provider
71// to another thread is safe, but concurrent access from multiple threads is NOT.
72// Users must use external synchronization (e.g., Mutex) for multi-threaded access.
73unsafe impl Send for Provider {}
74
75impl Provider {
76    /// Create a new camera provider
77    pub fn new() -> Result<Self> {
78        let handle = unsafe { sys::ccap_provider_create() };
79        if handle.is_null() {
80            return Err(CcapError::DeviceOpenFailed);
81        }
82
83        Ok(Provider {
84            handle,
85            is_opened: false,
86            callback_ptr: None,
87        })
88    }
89
90    /// Create a provider with a specific device index
91    pub fn with_device(device_index: i32) -> Result<Self> {
92        Self::with_device_and_extra_info(device_index, None)
93    }
94
95    /// Create a provider with a specific device index and optional extra info.
96    ///
97    /// On Windows, `extra_info` can be used to force backend selection with values like
98    /// `"auto"`, `"msmf"`, `"dshow"`, or `"backend=<value>"`.
99    pub fn with_device_and_extra_info(device_index: i32, extra_info: Option<&str>) -> Result<Self> {
100        let extra_info = optional_c_string(extra_info, "extra info")?;
101        let handle = unsafe {
102            sys::ccap_provider_create_with_index(
103                device_index,
104                extra_info
105                    .as_ref()
106                    .map_or(ptr::null(), |value| value.as_ptr()),
107            )
108        };
109        if handle.is_null() {
110            return Err(CcapError::InvalidDevice(format!(
111                "device index {}",
112                device_index
113            )));
114        }
115
116        Ok(Provider {
117            handle,
118            // ccap C API contract: create_with_index opens the device.
119            // See `include/ccap_c.h`: "Create a camera provider and open device by index".
120            is_opened: true,
121            callback_ptr: None,
122        })
123    }
124
125    /// Create a provider with a specific device name
126    pub fn with_device_name<S: AsRef<str>>(device_name: S) -> Result<Self> {
127        Self::with_device_name_and_extra_info(device_name, None)
128    }
129
130    /// Create a provider with a specific device name and optional extra info.
131    ///
132    /// On Windows, `extra_info` can be used to force backend selection with values like
133    /// `"auto"`, `"msmf"`, `"dshow"`, or `"backend=<value>"`.
134    pub fn with_device_name_and_extra_info<S: AsRef<str>>(
135        device_name: S,
136        extra_info: Option<&str>,
137    ) -> Result<Self> {
138        let c_name = CString::new(device_name.as_ref()).map_err(|_| {
139            CcapError::InvalidParameter("device name contains null byte".to_string())
140        })?;
141        let extra_info = optional_c_string(extra_info, "extra info")?;
142
143        let handle = unsafe {
144            sys::ccap_provider_create_with_device(
145                c_name.as_ptr(),
146                extra_info
147                    .as_ref()
148                    .map_or(ptr::null(), |value| value.as_ptr()),
149            )
150        };
151        if handle.is_null() {
152            return Err(CcapError::InvalidDevice(device_name.as_ref().to_string()));
153        }
154
155        Ok(Provider {
156            handle,
157            // ccap C API contract: create_with_device opens the device.
158            // See `include/ccap_c.h`: "Create a camera provider and open specified device".
159            is_opened: true,
160            callback_ptr: None,
161        })
162    }
163
164    /// Get available camera devices
165    pub fn get_devices() -> Result<Vec<DeviceInfo>> {
166        // Create a temporary provider to query devices
167        let provider = Self::new()?;
168        let mut device_names_list = sys::CcapDeviceNamesList::default();
169
170        let success = unsafe {
171            sys::ccap_provider_find_device_names_list(provider.handle, &mut device_names_list)
172        };
173
174        if !success {
175            return Ok(Vec::new());
176        }
177
178        let mut devices = Vec::new();
179        for i in 0..device_names_list.deviceCount {
180            let name_bytes = &device_names_list.deviceNames[i];
181            let name = unsafe {
182                let cstr = CStr::from_ptr(name_bytes.as_ptr());
183                cstr.to_string_lossy().to_string()
184            };
185
186            // Try to get device info by creating provider with this device
187            if let Ok(device_provider) = Self::with_device_name(&name) {
188                if let Ok(device_info) = device_provider.get_device_info_direct() {
189                    devices.push(device_info);
190                } else {
191                    // Fallback: create minimal device info from just the name
192                    devices.push(DeviceInfo {
193                        name,
194                        supported_pixel_formats: Vec::new(),
195                        supported_resolutions: Vec::new(),
196                    });
197                }
198            }
199        }
200
201        Ok(devices)
202    }
203
204    /// Get device info directly from current provider
205    fn get_device_info_direct(&self) -> Result<DeviceInfo> {
206        let mut device_info = sys::CcapDeviceInfo::default();
207
208        let success = unsafe { sys::ccap_provider_get_device_info(self.handle, &mut device_info) };
209
210        if !success {
211            return Err(CcapError::DeviceOpenFailed);
212        }
213
214        let name = unsafe {
215            let cstr = CStr::from_ptr(device_info.deviceName.as_ptr());
216            cstr.to_string_lossy().to_string()
217        };
218
219        let mut formats = Vec::new();
220        for i in 0..device_info.pixelFormatCount {
221            if i < device_info.supportedPixelFormats.len() {
222                formats.push(PixelFormat::from(device_info.supportedPixelFormats[i]));
223            }
224        }
225
226        let mut resolutions = Vec::new();
227        for i in 0..device_info.resolutionCount {
228            if i < device_info.supportedResolutions.len() {
229                let res = &device_info.supportedResolutions[i];
230                resolutions.push(Resolution {
231                    width: res.width,
232                    height: res.height,
233                });
234            }
235        }
236
237        Ok(DeviceInfo {
238            name,
239            supported_pixel_formats: formats,
240            supported_resolutions: resolutions,
241        })
242    }
243
244    /// Open the camera device
245    pub fn open(&mut self) -> Result<()> {
246        if self.is_opened {
247            return Ok(());
248        }
249
250        let result = unsafe { sys::ccap_provider_open_by_index(self.handle, -1, false) };
251        if !result {
252            return Err(CcapError::DeviceOpenFailed);
253        }
254
255        self.is_opened = true;
256        Ok(())
257    }
258
259    /// Open device with optional device name and auto start
260    pub fn open_device(&mut self, device_name: Option<&str>, auto_start: bool) -> Result<()> {
261        self.open_device_with_extra_info(device_name, None, auto_start)
262    }
263
264    /// Open a device with optional device name, optional extra info, and optional auto start.
265    ///
266    /// On Windows, `extra_info` can be used to force backend selection with values like
267    /// `"auto"`, `"msmf"`, `"dshow"`, or `"backend=<value>"`.
268    pub fn open_device_with_extra_info(
269        &mut self,
270        device_name: Option<&str>,
271        extra_info: Option<&str>,
272        auto_start: bool,
273    ) -> Result<()> {
274        if let Some(name) = device_name {
275            let c_name = CString::new(name).map_err(|_| {
276                CcapError::InvalidParameter("device name contains null byte".to_string())
277            })?;
278            let extra_info = optional_c_string(extra_info, "extra info")?;
279
280            // Recreate provider with specific device
281            if !self.handle.is_null() {
282                // If the previous provider was running, stop it and detach callbacks
283                // before destroying the underlying handle.
284                let _ = self.stop_capture();
285                let _ = self.remove_new_frame_callback();
286                self.cleanup_callback();
287                unsafe {
288                    sys::ccap_provider_destroy(self.handle);
289                }
290                self.handle = ptr::null_mut();
291                self.is_opened = false;
292            } else {
293                self.cleanup_callback();
294            }
295
296            self.handle = unsafe {
297                sys::ccap_provider_create_with_device(
298                    c_name.as_ptr(),
299                    extra_info
300                        .as_ref()
301                        .map_or(ptr::null(), |value| value.as_ptr()),
302                )
303            };
304            if self.handle.is_null() {
305                return Err(CcapError::InvalidDevice(name.to_string()));
306            }
307            self.is_opened = true;
308            if !auto_start {
309                self.stop_capture()?;
310            }
311        } else if extra_info.is_some() {
312            return self.open_with_index_and_extra_info(-1, extra_info, auto_start);
313        } else {
314            self.open()?;
315        }
316        if auto_start {
317            self.start_capture()?;
318        }
319        Ok(())
320    }
321
322    /// Get device info for the current provider
323    pub fn device_info(&self) -> Result<DeviceInfo> {
324        self.get_device_info_direct()
325    }
326
327    /// Check if capture is started
328    pub fn is_started(&self) -> bool {
329        unsafe { sys::ccap_provider_is_started(self.handle) }
330    }
331
332    /// Start capture (alias for start_capture)
333    pub fn start(&mut self) -> Result<()> {
334        self.start_capture()
335    }
336
337    /// Stop capture (alias for stop_capture)  
338    pub fn stop(&mut self) -> Result<()> {
339        self.stop_capture()
340    }
341
342    /// Check if the camera is opened
343    pub fn is_opened(&self) -> bool {
344        self.is_opened
345    }
346
347    /// Set camera property
348    pub fn set_property(&mut self, property: PropertyName, value: f64) -> Result<()> {
349        let property_id: sys::CcapPropertyName = property.into();
350        let success = unsafe { sys::ccap_provider_set_property(self.handle, property_id, value) };
351
352        if !success {
353            return Err(CcapError::InvalidParameter(format!(
354                "property {:?}",
355                property
356            )));
357        }
358
359        Ok(())
360    }
361
362    /// Get camera property
363    pub fn get_property(&self, property: PropertyName) -> Result<f64> {
364        let property_id: sys::CcapPropertyName = property.into();
365        let value = unsafe { sys::ccap_provider_get_property(self.handle, property_id) };
366
367        Ok(value)
368    }
369
370    /// Set camera resolution
371    pub fn set_resolution(&mut self, width: u32, height: u32) -> Result<()> {
372        // Avoid leaving the device in a partially-updated state if only one property update
373        // succeeds (e.g. width succeeds but height fails).
374        let (old_w, old_h) = self.resolution()?;
375
376        self.set_property(PropertyName::Width, width as f64)?;
377        if let Err(e) = self.set_property(PropertyName::Height, height as f64) {
378            // Best-effort rollback.
379            let _ = self.set_property(PropertyName::Width, old_w as f64);
380            let _ = self.set_property(PropertyName::Height, old_h as f64);
381            return Err(e);
382        }
383
384        Ok(())
385    }
386
387    /// Set camera frame rate
388    pub fn set_frame_rate(&mut self, fps: f64) -> Result<()> {
389        self.set_property(PropertyName::FrameRate, fps)
390    }
391
392    /// Set pixel format
393    pub fn set_pixel_format(&mut self, format: PixelFormat) -> Result<()> {
394        self.set_property(PropertyName::PixelFormatOutput, format.to_c_enum() as f64)
395    }
396
397    /// Grab a single frame with timeout
398    pub fn grab_frame(&mut self, timeout_ms: u32) -> Result<Option<VideoFrame>> {
399        if !self.is_opened {
400            return Err(CcapError::DeviceNotOpened);
401        }
402
403        let frame = unsafe { sys::ccap_provider_grab(self.handle, timeout_ms) };
404        if frame.is_null() {
405            return Ok(None);
406        }
407
408        Ok(Some(VideoFrame::from_c_ptr(frame)))
409    }
410
411    /// Start continuous capture
412    pub fn start_capture(&mut self) -> Result<()> {
413        if !self.is_opened {
414            return Err(CcapError::DeviceNotOpened);
415        }
416
417        let result = unsafe { sys::ccap_provider_start(self.handle) };
418        if !result {
419            return Err(CcapError::CaptureStartFailed);
420        }
421
422        Ok(())
423    }
424
425    /// Stop continuous capture
426    pub fn stop_capture(&mut self) -> Result<()> {
427        unsafe { sys::ccap_provider_stop(self.handle) };
428        Ok(())
429    }
430
431    /// Get library version
432    pub fn version() -> Result<String> {
433        let version_ptr = unsafe { sys::ccap_get_version() };
434        if version_ptr.is_null() {
435            return Err(CcapError::Unknown { code: -1 });
436        }
437
438        let version_cstr = unsafe { CStr::from_ptr(version_ptr) };
439        version_cstr
440            .to_str()
441            .map(|s| s.to_string())
442            .map_err(|_| CcapError::Unknown { code: -2 })
443    }
444
445    /// List device names (simple string list)
446    pub fn list_devices(&self) -> Result<Vec<String>> {
447        let device_infos = Self::get_devices()?;
448        Ok(device_infos.into_iter().map(|info| info.name).collect())
449    }
450
451    /// Find device names (alias for list_devices)
452    pub fn find_device_names(&self) -> Result<Vec<String>> {
453        self.list_devices()
454    }
455
456    /// Get current resolution (convenience getter)
457    pub fn resolution(&self) -> Result<(u32, u32)> {
458        let width = self.get_property(PropertyName::Width)? as u32;
459        let height = self.get_property(PropertyName::Height)? as u32;
460        Ok((width, height))
461    }
462
463    /// Get current pixel format (convenience getter)
464    pub fn pixel_format(&self) -> Result<PixelFormat> {
465        let format_val = self.get_property(PropertyName::PixelFormatOutput)? as u32;
466        Ok(PixelFormat::from_c_enum(format_val as sys::CcapPixelFormat))
467    }
468
469    /// Get current frame rate (convenience getter)
470    pub fn frame_rate(&self) -> Result<f64> {
471        self.get_property(PropertyName::FrameRate)
472    }
473
474    /// Set error callback for camera errors
475    ///
476    /// # Memory Safety
477    ///
478    /// This is a **global** callback that persists until replaced or cleared.
479    /// Calling this function multiple times will properly clean up the previous callback.
480    ///
481    /// **Important**: this callback is process-global (shared by all `Provider` instances).
482    /// The last one set wins.
483    ///
484    /// # Thread Safety
485    ///
486    /// The callback will be invoked from the camera capture thread. Ensure your
487    /// callback is thread-safe (`Send + Sync`).
488    ///
489    /// # Example
490    ///
491    /// ```ignore
492    /// Provider::set_error_callback(|code, desc| {
493    ///     eprintln!("Camera error {}: {}", code, desc);
494    /// });
495    /// ```
496    pub fn set_error_callback<F>(callback: F)
497    where
498        F: Fn(i32, &str) + Send + Sync + 'static,
499    {
500        use std::os::raw::c_char;
501
502        type ErrorCallbackBox = Box<dyn Fn(i32, &str) + Send + Sync>;
503
504        unsafe extern "C" fn error_callback_wrapper(
505            error_code: sys::CcapErrorCode,
506            description: *const c_char,
507            user_data: *mut std::ffi::c_void,
508        ) {
509            if user_data.is_null() || description.is_null() {
510                return;
511            }
512
513            // SAFETY: user_data points to Box<ErrorCallbackBox> created below
514            let callback = &**(user_data as *const ErrorCallbackBox);
515            let desc_cstr = std::ffi::CStr::from_ptr(description);
516            if let Ok(desc_str) = desc_cstr.to_str() {
517                callback(error_code as i32, desc_str);
518            }
519        }
520
521        // Clean up old callback if exists (use module-level GLOBAL_ERROR_CALLBACK)
522        if let Ok(mut guard) = GLOBAL_ERROR_CALLBACK.lock() {
523            if let Some(SendSyncPtr(old_ptr)) = guard.take() {
524                unsafe {
525                    let _ = Box::from_raw(old_ptr as *mut ErrorCallbackBox);
526                }
527            }
528
529            // Store new callback - double box for stable pointer
530            let callback_box: ErrorCallbackBox = Box::new(callback);
531            let callback_ptr = Box::into_raw(Box::new(callback_box));
532
533            unsafe {
534                sys::ccap_set_error_callback(
535                    Some(error_callback_wrapper),
536                    callback_ptr as *mut std::ffi::c_void,
537                );
538            }
539
540            *guard = Some(SendSyncPtr(callback_ptr as *mut std::ffi::c_void));
541        }
542    }
543
544    /// Set the **global** error callback.
545    ///
546    /// This is an alias for [`Provider::set_error_callback`] to make the global scope explicit.
547    pub fn set_global_error_callback<F>(callback: F)
548    where
549        F: Fn(i32, &str) + Send + Sync + 'static,
550    {
551        Self::set_error_callback(callback)
552    }
553
554    /// Clear the global error callback
555    ///
556    /// This removes the error callback and frees associated memory.
557    pub fn clear_error_callback() {
558        type ErrorCallbackBox = Box<dyn Fn(i32, &str) + Send + Sync>;
559
560        // Use module-level GLOBAL_ERROR_CALLBACK (same as set_error_callback)
561        if let Ok(mut guard) = GLOBAL_ERROR_CALLBACK.lock() {
562            // Always clear the C-side callback even if we don't have a stored Rust callback.
563            unsafe {
564                sys::ccap_set_error_callback(None, ptr::null_mut());
565            }
566            if let Some(SendSyncPtr(old_ptr)) = guard.take() {
567                unsafe {
568                    let _ = Box::from_raw(old_ptr as *mut ErrorCallbackBox);
569                }
570            }
571        }
572    }
573
574    /// Clear the **global** error callback.
575    ///
576    /// This is an alias for [`Provider::clear_error_callback`] to make the global scope explicit.
577    pub fn clear_global_error_callback() {
578        Self::clear_error_callback()
579    }
580
581    /// Open device with index and auto start
582    pub fn open_with_index(&mut self, device_index: i32, auto_start: bool) -> Result<()> {
583        self.open_with_index_and_extra_info(device_index, None, auto_start)
584    }
585
586    /// Open device with index, optional extra info, and optional auto start.
587    ///
588    /// On Windows, `extra_info` can be used to force backend selection with values like
589    /// `"auto"`, `"msmf"`, `"dshow"`, or `"backend=<value>"`.
590    pub fn open_with_index_and_extra_info(
591        &mut self,
592        device_index: i32,
593        extra_info: Option<&str>,
594        auto_start: bool,
595    ) -> Result<()> {
596        let extra_info = optional_c_string(extra_info, "extra info")?;
597
598        // If the previous provider was running, stop it and detach callbacks
599        // before destroying the underlying handle.
600        if !self.handle.is_null() {
601            let _ = self.stop_capture();
602            let _ = self.remove_new_frame_callback();
603            self.cleanup_callback();
604            unsafe {
605                sys::ccap_provider_destroy(self.handle);
606            }
607            self.handle = ptr::null_mut();
608            self.is_opened = false;
609        } else {
610            // Clean up any stale callback allocation even if handle is null.
611            self.cleanup_callback();
612        }
613
614        // Create a new provider with the specified device index
615        self.handle = unsafe {
616            sys::ccap_provider_create_with_index(
617                device_index,
618                extra_info
619                    .as_ref()
620                    .map_or(ptr::null(), |value| value.as_ptr()),
621            )
622        };
623
624        if self.handle.is_null() {
625            return Err(CcapError::InvalidDevice(format!(
626                "device index {}",
627                device_index
628            )));
629        }
630
631        // ccap C API contract: create_with_index opens the device.
632        self.is_opened = true;
633        if !auto_start {
634            self.stop_capture()?;
635        }
636        if auto_start {
637            self.start_capture()?;
638        }
639        Ok(())
640    }
641
642    /// Set a callback for new frame notifications
643    ///
644    /// The callback receives a reference to the captured frame and returns `true`
645    /// to continue capturing or `false` to stop.
646    ///
647    /// # Thread Safety
648    ///
649    /// The callback will be invoked from the camera capture thread. Ensure your
650    /// callback is thread-safe (`Send + Sync`).
651    ///
652    /// # Example
653    ///
654    /// ```ignore
655    /// provider.set_new_frame_callback(|frame| {
656    ///     println!("Got frame: {}x{}", frame.width(), frame.height());
657    ///     true // continue capturing
658    /// })?;
659    /// ```
660    pub fn set_new_frame_callback<F>(&mut self, callback: F) -> Result<()>
661    where
662        F: Fn(&VideoFrame) -> bool + Send + Sync + 'static,
663    {
664        use std::os::raw::c_void;
665
666        // Type alias for the boxed callback to ensure consistency
667        type CallbackBox = Box<dyn Fn(&VideoFrame) -> bool + Send + Sync>;
668
669        // Clean up old callback if exists
670        self.cleanup_callback();
671
672        unsafe extern "C" fn new_frame_callback_wrapper(
673            frame: *const sys::CcapVideoFrame,
674            user_data: *mut c_void,
675        ) -> bool {
676            if user_data.is_null() || frame.is_null() {
677                return false;
678            }
679
680            // SAFETY: user_data points to a Box<CallbackBox> that we created below
681            let callback = &**(user_data as *const CallbackBox);
682
683            // Create a temporary VideoFrame wrapper that doesn't own the frame
684            let video_frame = VideoFrame::from_c_ptr_ref(frame as *mut sys::CcapVideoFrame);
685            callback(&video_frame)
686        }
687
688        // Box the callback as a trait object, then box again to get a thin pointer
689        // This ensures we can safely convert to/from *mut c_void
690        let callback_box: CallbackBox = Box::new(callback);
691        let callback_ptr = Box::into_raw(Box::new(callback_box));
692
693        let success = unsafe {
694            sys::ccap_provider_set_new_frame_callback(
695                self.handle,
696                Some(new_frame_callback_wrapper),
697                callback_ptr as *mut c_void,
698            )
699        };
700
701        if success {
702            self.callback_ptr = Some(callback_ptr as *mut c_void);
703            Ok(())
704        } else {
705            // Clean up on failure
706            unsafe {
707                let _ = Box::from_raw(callback_ptr);
708            }
709            Err(CcapError::InvalidParameter(
710                "Failed to set frame callback".to_string(),
711            ))
712        }
713    }
714
715    /// Remove frame callback
716    pub fn remove_new_frame_callback(&mut self) -> Result<()> {
717        let success = unsafe {
718            sys::ccap_provider_set_new_frame_callback(self.handle, None, ptr::null_mut())
719        };
720
721        if success {
722            self.cleanup_callback();
723            Ok(())
724        } else {
725            Err(CcapError::CaptureStopFailed)
726        }
727    }
728
729    /// Clean up callback pointer
730    fn cleanup_callback(&mut self) {
731        // Type alias must match what we used in set_new_frame_callback
732        type CallbackBox = Box<dyn Fn(&VideoFrame) -> bool + Send + Sync>;
733
734        if let Some(callback_ptr) = self.callback_ptr.take() {
735            unsafe {
736                // SAFETY: callback_ptr was created with Box::into_raw(Box::new(callback_box))
737                // where callback_box is a CallbackBox
738                let _ = Box::from_raw(callback_ptr as *mut CallbackBox);
739            }
740        }
741    }
742}
743
744impl Drop for Provider {
745    fn drop(&mut self) {
746        // Clean up callback first
747        self.cleanup_callback();
748
749        if !self.handle.is_null() {
750            unsafe {
751                sys::ccap_provider_destroy(self.handle);
752            }
753            self.handle = ptr::null_mut();
754        }
755    }
756}