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