Skip to main content

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                crate::utils::panic_safe::catch_user_panic(
464                    "SCRecordingOutputDelegate::recording_did_start",
465                    || entry.delegate.recording_did_start(),
466                );
467            }
468        }
469    }
470}
471
472extern "C" fn recording_failed_callback(ctx: *mut c_void, error_code: i32, error: *const i8) {
473    let key = ctx as usize;
474    let error_str = if error.is_null() {
475        String::from("Unknown error")
476    } else {
477        unsafe { std::ffi::CStr::from_ptr(error) }
478            .to_string_lossy()
479            .into_owned()
480    };
481
482    // Include error code in the message if it's a known SCStreamError
483    let full_error = if error_code != 0 {
484        crate::error::SCStreamErrorCode::from_raw(error_code).map_or_else(
485            || format!("{error_str} (code: {error_code})"),
486            |code| format!("{error_str} ({code})"),
487        )
488    } else {
489        error_str
490    };
491
492    if let Ok(registry) = RECORDING_DELEGATE_REGISTRY.lock() {
493        if let Some(ref delegates) = *registry {
494            if let Some(entry) = delegates.get(&key) {
495                crate::utils::panic_safe::catch_user_panic(
496                    "SCRecordingOutputDelegate::recording_did_fail",
497                    || entry.delegate.recording_did_fail(full_error),
498                );
499            }
500        }
501    }
502}
503
504extern "C" fn recording_finished_callback(ctx: *mut c_void) {
505    let key = ctx as usize;
506    if let Ok(registry) = RECORDING_DELEGATE_REGISTRY.lock() {
507        if let Some(ref delegates) = *registry {
508            if let Some(entry) = delegates.get(&key) {
509                crate::utils::panic_safe::catch_user_panic(
510                    "SCRecordingOutputDelegate::recording_did_finish",
511                    || entry.delegate.recording_did_finish(),
512                );
513            }
514        }
515    }
516}
517
518impl SCRecordingOutput {
519    /// Create a new recording output with configuration
520    ///
521    /// # Errors
522    /// Returns None if the system is not macOS 15.0+ or creation fails
523    pub fn new(config: &SCRecordingOutputConfiguration) -> Option<Self> {
524        let ptr = unsafe { crate::ffi::sc_recording_output_create(config.as_ptr()) };
525        if ptr.is_null() {
526            None
527        } else {
528            Some(Self {
529                ptr,
530                delegate_id: None,
531            })
532        }
533    }
534
535    /// Create a new recording output with configuration and delegate
536    ///
537    /// The delegate receives callbacks for recording lifecycle events:
538    /// - `recording_did_start` - Called when recording begins
539    /// - `recording_did_fail` - Called if recording fails with an error
540    /// - `recording_did_finish` - Called when recording completes successfully
541    ///
542    /// # Errors
543    /// Returns None if the system is not macOS 15.0+ or creation fails
544    ///
545    /// # Panics
546    /// Panics if the delegate registry mutex is poisoned
547    pub fn new_with_delegate<D: SCRecordingOutputDelegate>(
548        config: &SCRecordingOutputConfiguration,
549        delegate: D,
550    ) -> Option<Self> {
551        // Generate a unique ID for this delegate
552        let delegate_id = NEXT_DELEGATE_ID.fetch_add(1, Ordering::Relaxed);
553
554        // Store delegate in registry before creating recording output
555        {
556            let mut registry = RECORDING_DELEGATE_REGISTRY.lock().unwrap();
557            if registry.is_none() {
558                *registry = Some(HashMap::new());
559            }
560            if let Some(ref mut delegates) = *registry {
561                delegates.insert(
562                    delegate_id,
563                    RecordingDelegateEntry {
564                        delegate: Box::new(delegate),
565                        ref_count: 1,
566                    },
567                );
568            }
569        }
570
571        // Use delegate_id as context
572        let ctx = delegate_id as *mut c_void;
573
574        let ptr = unsafe {
575            crate::ffi::sc_recording_output_create_with_delegate(
576                config.as_ptr(),
577                Some(recording_started_callback),
578                Some(recording_failed_callback),
579                Some(recording_finished_callback),
580                ctx,
581            )
582        };
583
584        if ptr.is_null() {
585            // Clean up delegate from registry on failure
586            if let Ok(mut registry) = RECORDING_DELEGATE_REGISTRY.lock() {
587                if let Some(ref mut delegates) = *registry {
588                    delegates.remove(&delegate_id);
589                }
590            }
591            None
592        } else {
593            Some(Self {
594                ptr,
595                delegate_id: Some(delegate_id),
596            })
597        }
598    }
599
600    /// Get the current recorded duration
601    pub fn recorded_duration(&self) -> CMTime {
602        let mut value: i64 = 0;
603        let mut timescale: i32 = 0;
604        unsafe {
605            crate::ffi::sc_recording_output_get_recorded_duration(
606                self.ptr,
607                &mut value,
608                &mut timescale,
609            );
610        }
611        CMTime {
612            value,
613            timescale,
614            flags: 0,
615            epoch: 0,
616        }
617    }
618
619    /// Get the current recorded file size in bytes
620    pub fn recorded_file_size(&self) -> i64 {
621        unsafe { crate::ffi::sc_recording_output_get_recorded_file_size(self.ptr) }
622    }
623
624    #[must_use]
625    pub fn as_ptr(&self) -> *const c_void {
626        self.ptr
627    }
628}
629
630impl Clone for SCRecordingOutput {
631    fn clone(&self) -> Self {
632        // Increment delegate ref count if one exists for this recording
633        if let Some(delegate_id) = self.delegate_id {
634            if let Ok(mut registry) = RECORDING_DELEGATE_REGISTRY.lock() {
635                if let Some(ref mut delegates) = *registry {
636                    if let Some(entry) = delegates.get_mut(&delegate_id) {
637                        entry.ref_count += 1;
638                    }
639                }
640            }
641        }
642
643        unsafe {
644            Self {
645                ptr: crate::ffi::sc_recording_output_retain(self.ptr),
646                delegate_id: self.delegate_id,
647            }
648        }
649    }
650}
651
652impl std::fmt::Debug for SCRecordingOutput {
653    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
654        f.debug_struct("SCRecordingOutput")
655            .field("recorded_duration", &self.recorded_duration())
656            .field("recorded_file_size", &self.recorded_file_size())
657            .field("has_delegate", &self.delegate_id.is_some())
658            .finish_non_exhaustive()
659    }
660}
661
662impl Drop for SCRecordingOutput {
663    fn drop(&mut self) {
664        // Decrement delegate ref count and clean up if this is the last reference
665        if let Some(delegate_id) = self.delegate_id {
666            let mut should_remove = false;
667            if let Ok(mut registry) = RECORDING_DELEGATE_REGISTRY.lock() {
668                if let Some(ref mut delegates) = *registry {
669                    if let Some(entry) = delegates.get_mut(&delegate_id) {
670                        entry.ref_count -= 1;
671                        if entry.ref_count == 0 {
672                            should_remove = true;
673                        }
674                    }
675                    if should_remove {
676                        delegates.remove(&delegate_id);
677                    }
678                }
679            }
680        }
681
682        if !self.ptr.is_null() {
683            unsafe {
684                crate::ffi::sc_recording_output_release(self.ptr);
685            }
686        }
687    }
688}
689
690// Safety: SCRecordingOutput wraps an Objective-C object that is thread-safe
691unsafe impl Send for SCRecordingOutput {}
692unsafe impl Sync for SCRecordingOutput {}
693
694// Safety: SCRecordingOutputConfiguration wraps an Objective-C object that is thread-safe
695unsafe impl Send for SCRecordingOutputConfiguration {}
696unsafe impl Sync for SCRecordingOutputConfiguration {}