screencapturekit/
recording_output.rs

1//! `SCRecordingOutput` - Direct video file recording
2//!
3//! Available on macOS 15.0+.
4//! Provides direct encoding of screen capture to video files with hardware acceleration.
5//!
6//! Requires the `macos_15_0` feature flag to be enabled.
7//!
8//! ## When to Use
9//!
10//! Use `SCRecordingOutput` when you need:
11//! - Direct recording to MP4/MOV files without manual encoding
12//! - Hardware-accelerated H.264 or HEVC encoding
13//! - Recording with automatic file management
14//!
15//! For custom processing of frames, use [`SCStream`](crate::stream::SCStream) with
16//! output handlers instead.
17//!
18//! ## Example
19//!
20//! ```no_run
21//! use screencapturekit::recording_output::{
22//!     SCRecordingOutput, SCRecordingOutputConfiguration, SCRecordingOutputCodec
23//! };
24//! use screencapturekit::prelude::*;
25//! use std::path::Path;
26//!
27//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
28//! let content = SCShareableContent::get()?;
29//! let display = &content.displays()[0];
30//! let filter = SCContentFilter::create().with_display(display).with_excluding_windows(&[]).build();
31//! let config = SCStreamConfiguration::new()
32//!     .with_width(1920)
33//!     .with_height(1080);
34//!
35//! // Configure recording output
36//! let rec_config = SCRecordingOutputConfiguration::new()
37//!     .with_output_url(Path::new("/tmp/recording.mp4"))
38//!     .with_video_codec(SCRecordingOutputCodec::HEVC);
39//!
40//! let recording = SCRecordingOutput::new(&rec_config).ok_or("Failed to create recording")?;
41//!
42//! // Add to stream and start
43//! let mut stream = SCStream::new(&filter, &config);
44//! stream.add_recording_output(&recording)?;
45//! stream.start_capture()?;
46//!
47//! // ... record for desired duration ...
48//!
49//! stream.stop_capture()?;
50//! stream.remove_recording_output(&recording)?;
51//! # Ok(())
52//! # }
53//! ```
54
55use std::collections::HashMap;
56use std::ffi::c_void;
57use std::path::Path;
58use std::sync::atomic::{AtomicUsize, Ordering};
59use std::sync::Mutex;
60
61use crate::cm::CMTime;
62
63/// Global registry for recording delegates - maps unique ID to delegate entry
64static RECORDING_DELEGATE_REGISTRY: Mutex<Option<HashMap<usize, RecordingDelegateEntry>>> =
65    Mutex::new(None);
66
67/// Counter for generating unique delegate IDs
68static NEXT_DELEGATE_ID: AtomicUsize = AtomicUsize::new(1);
69
70struct RecordingDelegateEntry {
71    delegate: Box<dyn SCRecordingOutputDelegate>,
72    ref_count: usize,
73}
74
75/// Video codec for recording
76#[repr(i32)]
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
78pub enum SCRecordingOutputCodec {
79    /// H.264 codec
80    #[default]
81    H264 = 0,
82    /// H.265/HEVC codec
83    HEVC = 1,
84}
85
86/// Output file type for recording
87#[repr(i32)]
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
89pub enum SCRecordingOutputFileType {
90    /// MPEG-4 file (.mp4)
91    #[default]
92    MP4 = 0,
93    /// `QuickTime` movie (.mov)
94    MOV = 1,
95}
96
97/// Configuration for recording output
98pub struct SCRecordingOutputConfiguration {
99    ptr: *const c_void,
100}
101
102impl SCRecordingOutputConfiguration {
103    /// Create a new recording output configuration
104    #[must_use]
105    pub fn new() -> Self {
106        let ptr = unsafe { crate::ffi::sc_recording_output_configuration_create() };
107        Self { ptr }
108    }
109
110    /// Set the output file URL
111    #[must_use]
112    pub fn with_output_url(self, path: &Path) -> Self {
113        if let Some(path_str) = path.to_str() {
114            if let Ok(c_path) = std::ffi::CString::new(path_str) {
115                unsafe {
116                    crate::ffi::sc_recording_output_configuration_set_output_url(
117                        self.ptr,
118                        c_path.as_ptr(),
119                    );
120                }
121            }
122        }
123        self
124    }
125
126    /// Set the video codec
127    #[must_use]
128    pub fn with_video_codec(self, codec: SCRecordingOutputCodec) -> Self {
129        unsafe {
130            crate::ffi::sc_recording_output_configuration_set_video_codec(self.ptr, codec as i32);
131        }
132        self
133    }
134
135    /// Get the video codec
136    pub fn video_codec(&self) -> SCRecordingOutputCodec {
137        let value =
138            unsafe { crate::ffi::sc_recording_output_configuration_get_video_codec(self.ptr) };
139        match value {
140            1 => SCRecordingOutputCodec::HEVC,
141            _ => SCRecordingOutputCodec::H264,
142        }
143    }
144
145    /// Set the output file type
146    #[must_use]
147    pub fn with_output_file_type(self, file_type: SCRecordingOutputFileType) -> Self {
148        unsafe {
149            crate::ffi::sc_recording_output_configuration_set_output_file_type(
150                self.ptr,
151                file_type as i32,
152            );
153        }
154        self
155    }
156
157    /// Get the output file type
158    pub fn output_file_type(&self) -> SCRecordingOutputFileType {
159        let value =
160            unsafe { crate::ffi::sc_recording_output_configuration_get_output_file_type(self.ptr) };
161        match value {
162            1 => SCRecordingOutputFileType::MOV,
163            _ => SCRecordingOutputFileType::MP4,
164        }
165    }
166
167    /// Get the number of available video codecs
168    pub fn available_video_codecs_count(&self) -> usize {
169        let count = unsafe {
170            crate::ffi::sc_recording_output_configuration_get_available_video_codecs_count(self.ptr)
171        };
172        #[allow(clippy::cast_sign_loss)]
173        if count > 0 {
174            count as usize
175        } else {
176            0
177        }
178    }
179
180    /// Get all available video codecs
181    ///
182    /// Returns a vector of all video codecs that can be used for recording.
183    pub fn available_video_codecs(&self) -> Vec<SCRecordingOutputCodec> {
184        let count = self.available_video_codecs_count();
185        let mut codecs = Vec::with_capacity(count);
186        for i in 0..count {
187            #[allow(clippy::cast_possible_wrap)]
188            let codec_value = unsafe {
189                crate::ffi::sc_recording_output_configuration_get_available_video_codec_at(
190                    self.ptr, i as isize,
191                )
192            };
193            match codec_value {
194                0 => codecs.push(SCRecordingOutputCodec::H264),
195                1 => codecs.push(SCRecordingOutputCodec::HEVC),
196                _ => {}
197            }
198        }
199        codecs
200    }
201
202    /// Get the number of available output file types
203    pub fn available_output_file_types_count(&self) -> usize {
204        let count = unsafe {
205            crate::ffi::sc_recording_output_configuration_get_available_output_file_types_count(
206                self.ptr,
207            )
208        };
209        #[allow(clippy::cast_sign_loss)]
210        if count > 0 {
211            count as usize
212        } else {
213            0
214        }
215    }
216
217    /// Get all available output file types
218    ///
219    /// Returns a vector of all file types that can be used for recording output.
220    pub fn available_output_file_types(&self) -> Vec<SCRecordingOutputFileType> {
221        let count = self.available_output_file_types_count();
222        let mut file_types = Vec::with_capacity(count);
223        for i in 0..count {
224            #[allow(clippy::cast_possible_wrap)]
225            let file_type_value = unsafe {
226                crate::ffi::sc_recording_output_configuration_get_available_output_file_type_at(
227                    self.ptr, i as isize,
228                )
229            };
230            match file_type_value {
231                0 => file_types.push(SCRecordingOutputFileType::MP4),
232                1 => file_types.push(SCRecordingOutputFileType::MOV),
233                _ => {}
234            }
235        }
236        file_types
237    }
238
239    #[must_use]
240    pub fn as_ptr(&self) -> *const c_void {
241        self.ptr
242    }
243}
244
245impl Default for SCRecordingOutputConfiguration {
246    fn default() -> Self {
247        Self::new()
248    }
249}
250
251impl Clone for SCRecordingOutputConfiguration {
252    fn clone(&self) -> Self {
253        unsafe {
254            Self {
255                ptr: crate::ffi::sc_recording_output_configuration_retain(self.ptr),
256            }
257        }
258    }
259}
260
261impl Drop for SCRecordingOutputConfiguration {
262    fn drop(&mut self) {
263        if !self.ptr.is_null() {
264            unsafe {
265                crate::ffi::sc_recording_output_configuration_release(self.ptr);
266            }
267        }
268    }
269}
270
271impl std::fmt::Debug for SCRecordingOutputConfiguration {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        f.debug_struct("SCRecordingOutputConfiguration")
274            .field("video_codec", &self.video_codec())
275            .field("file_type", &self.output_file_type())
276            .finish()
277    }
278}
279
280/// Delegate for recording output events
281///
282/// Implement this trait to receive notifications about recording lifecycle events.
283///
284/// # Examples
285///
286/// ## Using a struct
287///
288/// ```
289/// use screencapturekit::recording_output::SCRecordingOutputDelegate;
290///
291/// struct MyRecordingDelegate;
292///
293/// impl SCRecordingOutputDelegate for MyRecordingDelegate {
294///     fn recording_did_start(&self) {
295///         println!("Recording started!");
296///     }
297///     fn recording_did_fail(&self, error: String) {
298///         eprintln!("Recording failed: {}", error);
299///     }
300///     fn recording_did_finish(&self) {
301///         println!("Recording finished!");
302///     }
303/// }
304/// ```
305///
306/// ## Using closures
307///
308/// Use [`RecordingCallbacks`] to create a delegate from closures:
309///
310/// ```rust,no_run
311/// use screencapturekit::recording_output::{
312///     SCRecordingOutput, SCRecordingOutputConfiguration, RecordingCallbacks
313/// };
314/// use std::path::Path;
315///
316/// let config = SCRecordingOutputConfiguration::new()
317///     .with_output_url(Path::new("/tmp/recording.mp4"));
318///
319/// let delegate = RecordingCallbacks::new()
320///     .on_start(|| println!("Started!"))
321///     .on_finish(|| println!("Finished!"))
322///     .on_fail(|e| eprintln!("Error: {}", e));
323///
324/// let recording = SCRecordingOutput::new_with_delegate(&config, delegate);
325/// ```
326pub trait SCRecordingOutputDelegate: Send + 'static {
327    /// Called when recording starts successfully
328    fn recording_did_start(&self) {}
329    /// Called when recording fails with an error
330    fn recording_did_fail(&self, _error: String) {}
331    /// Called when recording finishes successfully
332    fn recording_did_finish(&self) {}
333}
334
335/// Builder for closure-based recording delegate
336///
337/// Provides a convenient way to create a recording delegate using closures
338/// instead of implementing the [`SCRecordingOutputDelegate`] trait.
339///
340/// # Examples
341///
342/// ```rust,no_run
343/// use screencapturekit::recording_output::{
344///     SCRecordingOutput, SCRecordingOutputConfiguration, RecordingCallbacks
345/// };
346/// use std::path::Path;
347///
348/// let config = SCRecordingOutputConfiguration::new()
349///     .with_output_url(Path::new("/tmp/recording.mp4"));
350///
351/// // Create delegate with all callbacks
352/// let delegate = RecordingCallbacks::new()
353///     .on_start(|| println!("Recording started!"))
354///     .on_finish(|| println!("Recording finished!"))
355///     .on_fail(|error| eprintln!("Recording failed: {}", error));
356///
357/// let recording = SCRecordingOutput::new_with_delegate(&config, delegate);
358///
359/// // Or just handle specific events
360/// let delegate = RecordingCallbacks::new()
361///     .on_fail(|error| eprintln!("Error: {}", error));
362/// ```
363#[allow(clippy::struct_field_names)]
364pub struct RecordingCallbacks {
365    on_start: Option<Box<dyn Fn() + Send + 'static>>,
366    on_fail: Option<Box<dyn Fn(String) + Send + 'static>>,
367    on_finish: Option<Box<dyn Fn() + Send + 'static>>,
368}
369
370impl RecordingCallbacks {
371    /// Create a new empty callbacks builder
372    #[must_use]
373    pub fn new() -> Self {
374        Self {
375            on_start: None,
376            on_fail: None,
377            on_finish: None,
378        }
379    }
380
381    /// Set the callback for when recording starts
382    #[must_use]
383    pub fn on_start<F>(mut self, f: F) -> Self
384    where
385        F: Fn() + Send + 'static,
386    {
387        self.on_start = Some(Box::new(f));
388        self
389    }
390
391    /// Set the callback for when recording fails
392    #[must_use]
393    pub fn on_fail<F>(mut self, f: F) -> Self
394    where
395        F: Fn(String) + Send + 'static,
396    {
397        self.on_fail = Some(Box::new(f));
398        self
399    }
400
401    /// Set the callback for when recording finishes
402    #[must_use]
403    pub fn on_finish<F>(mut self, f: F) -> Self
404    where
405        F: Fn() + Send + 'static,
406    {
407        self.on_finish = Some(Box::new(f));
408        self
409    }
410}
411
412impl Default for RecordingCallbacks {
413    fn default() -> Self {
414        Self::new()
415    }
416}
417
418impl std::fmt::Debug for RecordingCallbacks {
419    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420        f.debug_struct("RecordingCallbacks")
421            .field("on_start", &self.on_start.is_some())
422            .field("on_fail", &self.on_fail.is_some())
423            .field("on_finish", &self.on_finish.is_some())
424            .finish()
425    }
426}
427
428impl SCRecordingOutputDelegate for RecordingCallbacks {
429    fn recording_did_start(&self) {
430        if let Some(ref f) = self.on_start {
431            f();
432        }
433    }
434
435    fn recording_did_fail(&self, error: String) {
436        if let Some(ref f) = self.on_fail {
437            f(error);
438        }
439    }
440
441    fn recording_did_finish(&self) {
442        if let Some(ref f) = self.on_finish {
443            f();
444        }
445    }
446}
447
448/// Recording output for direct video file encoding
449///
450/// Available on macOS 15.0+
451pub struct SCRecordingOutput {
452    ptr: *const c_void,
453    /// ID into the delegate registry, if a delegate was set
454    delegate_id: Option<usize>,
455}
456
457// C callback trampolines for delegate - ctx is the recording ptr as usize
458extern "C" fn recording_started_callback(ctx: *mut c_void) {
459    let key = ctx as usize;
460    if let Ok(registry) = RECORDING_DELEGATE_REGISTRY.lock() {
461        if let Some(ref delegates) = *registry {
462            if let Some(entry) = delegates.get(&key) {
463                entry.delegate.recording_did_start();
464            }
465        }
466    }
467}
468
469extern "C" fn recording_failed_callback(ctx: *mut c_void, error_code: i32, error: *const i8) {
470    let key = ctx as usize;
471    let error_str = if error.is_null() {
472        String::from("Unknown error")
473    } else {
474        unsafe { std::ffi::CStr::from_ptr(error) }
475            .to_string_lossy()
476            .into_owned()
477    };
478
479    // Include error code in the message if it's a known SCStreamError
480    let full_error = if error_code != 0 {
481        crate::error::SCStreamErrorCode::from_raw(error_code).map_or_else(
482            || format!("{error_str} (code: {error_code})"),
483            |code| format!("{error_str} ({code})"),
484        )
485    } else {
486        error_str
487    };
488
489    if let Ok(registry) = RECORDING_DELEGATE_REGISTRY.lock() {
490        if let Some(ref delegates) = *registry {
491            if let Some(entry) = delegates.get(&key) {
492                entry.delegate.recording_did_fail(full_error);
493            }
494        }
495    }
496}
497
498extern "C" fn recording_finished_callback(ctx: *mut c_void) {
499    let key = ctx as usize;
500    if let Ok(registry) = RECORDING_DELEGATE_REGISTRY.lock() {
501        if let Some(ref delegates) = *registry {
502            if let Some(entry) = delegates.get(&key) {
503                entry.delegate.recording_did_finish();
504            }
505        }
506    }
507}
508
509impl SCRecordingOutput {
510    /// Create a new recording output with configuration
511    ///
512    /// # Errors
513    /// Returns None if the system is not macOS 15.0+ or creation fails
514    pub fn new(config: &SCRecordingOutputConfiguration) -> Option<Self> {
515        let ptr = unsafe { crate::ffi::sc_recording_output_create(config.as_ptr()) };
516        if ptr.is_null() {
517            None
518        } else {
519            Some(Self {
520                ptr,
521                delegate_id: None,
522            })
523        }
524    }
525
526    /// Create a new recording output with configuration and delegate
527    ///
528    /// The delegate receives callbacks for recording lifecycle events:
529    /// - `recording_did_start` - Called when recording begins
530    /// - `recording_did_fail` - Called if recording fails with an error
531    /// - `recording_did_finish` - Called when recording completes successfully
532    ///
533    /// # Errors
534    /// Returns None if the system is not macOS 15.0+ or creation fails
535    ///
536    /// # Panics
537    /// Panics if the delegate registry mutex is poisoned
538    pub fn new_with_delegate<D: SCRecordingOutputDelegate>(
539        config: &SCRecordingOutputConfiguration,
540        delegate: D,
541    ) -> Option<Self> {
542        // Generate a unique ID for this delegate
543        let delegate_id = NEXT_DELEGATE_ID.fetch_add(1, Ordering::Relaxed);
544
545        // Store delegate in registry before creating recording output
546        {
547            let mut registry = RECORDING_DELEGATE_REGISTRY.lock().unwrap();
548            if registry.is_none() {
549                *registry = Some(HashMap::new());
550            }
551            if let Some(ref mut delegates) = *registry {
552                delegates.insert(
553                    delegate_id,
554                    RecordingDelegateEntry {
555                        delegate: Box::new(delegate),
556                        ref_count: 1,
557                    },
558                );
559            }
560        }
561
562        // Use delegate_id as context
563        let ctx = delegate_id as *mut c_void;
564
565        let ptr = unsafe {
566            crate::ffi::sc_recording_output_create_with_delegate(
567                config.as_ptr(),
568                Some(recording_started_callback),
569                Some(recording_failed_callback),
570                Some(recording_finished_callback),
571                ctx,
572            )
573        };
574
575        if ptr.is_null() {
576            // Clean up delegate from registry on failure
577            if let Ok(mut registry) = RECORDING_DELEGATE_REGISTRY.lock() {
578                if let Some(ref mut delegates) = *registry {
579                    delegates.remove(&delegate_id);
580                }
581            }
582            None
583        } else {
584            Some(Self {
585                ptr,
586                delegate_id: Some(delegate_id),
587            })
588        }
589    }
590
591    /// Get the current recorded duration
592    pub fn recorded_duration(&self) -> CMTime {
593        let mut value: i64 = 0;
594        let mut timescale: i32 = 0;
595        unsafe {
596            crate::ffi::sc_recording_output_get_recorded_duration(
597                self.ptr,
598                &mut value,
599                &mut timescale,
600            );
601        }
602        CMTime {
603            value,
604            timescale,
605            flags: 0,
606            epoch: 0,
607        }
608    }
609
610    /// Get the current recorded file size in bytes
611    pub fn recorded_file_size(&self) -> i64 {
612        unsafe { crate::ffi::sc_recording_output_get_recorded_file_size(self.ptr) }
613    }
614
615    #[must_use]
616    pub fn as_ptr(&self) -> *const c_void {
617        self.ptr
618    }
619}
620
621impl Clone for SCRecordingOutput {
622    fn clone(&self) -> Self {
623        // Increment delegate ref count if one exists for this recording
624        if let Some(delegate_id) = self.delegate_id {
625            if let Ok(mut registry) = RECORDING_DELEGATE_REGISTRY.lock() {
626                if let Some(ref mut delegates) = *registry {
627                    if let Some(entry) = delegates.get_mut(&delegate_id) {
628                        entry.ref_count += 1;
629                    }
630                }
631            }
632        }
633
634        unsafe {
635            Self {
636                ptr: crate::ffi::sc_recording_output_retain(self.ptr),
637                delegate_id: self.delegate_id,
638            }
639        }
640    }
641}
642
643impl std::fmt::Debug for SCRecordingOutput {
644    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
645        f.debug_struct("SCRecordingOutput")
646            .field("recorded_duration", &self.recorded_duration())
647            .field("recorded_file_size", &self.recorded_file_size())
648            .field("has_delegate", &self.delegate_id.is_some())
649            .finish_non_exhaustive()
650    }
651}
652
653impl Drop for SCRecordingOutput {
654    fn drop(&mut self) {
655        // Decrement delegate ref count and clean up if this is the last reference
656        if let Some(delegate_id) = self.delegate_id {
657            let mut should_remove = false;
658            if let Ok(mut registry) = RECORDING_DELEGATE_REGISTRY.lock() {
659                if let Some(ref mut delegates) = *registry {
660                    if let Some(entry) = delegates.get_mut(&delegate_id) {
661                        entry.ref_count -= 1;
662                        if entry.ref_count == 0 {
663                            should_remove = true;
664                        }
665                    }
666                    if should_remove {
667                        delegates.remove(&delegate_id);
668                    }
669                }
670            }
671        }
672
673        if !self.ptr.is_null() {
674            unsafe {
675                crate::ffi::sc_recording_output_release(self.ptr);
676            }
677        }
678    }
679}
680
681// Safety: SCRecordingOutput wraps an Objective-C object that is thread-safe
682unsafe impl Send for SCRecordingOutput {}
683unsafe impl Sync for SCRecordingOutput {}
684
685// Safety: SCRecordingOutputConfiguration wraps an Objective-C object that is thread-safe
686unsafe impl Send for SCRecordingOutputConfiguration {}
687unsafe impl Sync for SCRecordingOutputConfiguration {}