1use std::borrow::Cow;
56use std::collections::HashMap;
57use std::ffi::c_void;
58use std::path::{Path, PathBuf};
59use std::sync::atomic::{AtomicUsize, Ordering};
60use std::sync::{Arc, Mutex, PoisonError};
61
62use crate::cm::CMTime;
63
64static RECORDING_DELEGATE_REGISTRY: Mutex<Option<HashMap<usize, RecordingDelegateEntry>>> =
66 Mutex::new(None);
67
68static NEXT_DELEGATE_ID: AtomicUsize = AtomicUsize::new(1);
70
71struct RecordingDelegateEntry {
77 delegate: Arc<dyn SCRecordingOutputDelegate>,
78}
79
80fn lookup_delegate(key: usize) -> Option<Arc<dyn SCRecordingOutputDelegate>> {
82 let registry = RECORDING_DELEGATE_REGISTRY
83 .lock()
84 .unwrap_or_else(PoisonError::into_inner);
85 registry
86 .as_ref()?
87 .get(&key)
88 .map(|entry| Arc::clone(&entry.delegate))
89}
90
91fn remove_delegate(key: usize) -> Option<RecordingDelegateEntry> {
92 let mut registry = RECORDING_DELEGATE_REGISTRY
93 .lock()
94 .unwrap_or_else(PoisonError::into_inner);
95 registry
96 .as_mut()
97 .and_then(|delegates| delegates.remove(&key))
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Hash)]
105pub struct SCRecordingOutputCodec(Cow<'static, str>);
106
107impl SCRecordingOutputCodec {
108 pub const H264: Self = Self(Cow::Borrowed("avc1"));
110 pub const HEVC: Self = Self(Cow::Borrowed("hvc1"));
112 pub const JPEG: Self = Self(Cow::Borrowed("jpeg"));
114 pub const PRO_RES_422: Self = Self(Cow::Borrowed("apcn"));
116 pub const PRO_RES_4444: Self = Self(Cow::Borrowed("ap4h"));
118 pub const HEVC_WITH_ALPHA: Self = Self(Cow::Borrowed("muxa"));
120 pub const PRO_RES_422_HQ: Self = Self(Cow::Borrowed("apch"));
122 pub const PRO_RES_422_LT: Self = Self(Cow::Borrowed("apcs"));
124 pub const PRO_RES_422_PROXY: Self = Self(Cow::Borrowed("apco"));
126
127 pub fn from_identifier(
134 identifier: impl Into<String>,
135 ) -> Result<Self, InvalidRecordingIdentifier> {
136 let identifier = identifier.into();
137 if identifier.as_bytes().contains(&0) {
138 Err(InvalidRecordingIdentifier)
139 } else {
140 Ok(Self(Cow::Owned(identifier)))
141 }
142 }
143
144 #[must_use]
146 pub fn identifier(&self) -> &str {
147 self.0.as_ref()
148 }
149}
150
151impl Default for SCRecordingOutputCodec {
152 fn default() -> Self {
153 Self::H264
154 }
155}
156
157impl std::fmt::Display for SCRecordingOutputCodec {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 match self.identifier() {
160 "avc1" => f.write_str("H.264"),
161 "hvc1" => f.write_str("HEVC"),
162 "jpeg" => f.write_str("JPEG"),
163 "apcn" => f.write_str("ProRes 422"),
164 "ap4h" => f.write_str("ProRes 4444"),
165 "muxa" => f.write_str("HEVC with alpha"),
166 "apch" => f.write_str("ProRes 422 HQ"),
167 "apcs" => f.write_str("ProRes 422 LT"),
168 "apco" => f.write_str("ProRes 422 Proxy"),
169 other => write!(f, "codec {other}"),
170 }
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Hash)]
176pub struct SCRecordingOutputFileType(Cow<'static, str>);
177
178impl SCRecordingOutputFileType {
179 pub const MP4: Self = Self(Cow::Borrowed("public.mpeg-4"));
181 pub const MOV: Self = Self(Cow::Borrowed("com.apple.quicktime-movie"));
183 pub const M4V: Self = Self(Cow::Borrowed("com.apple.m4v-video"));
185 pub const M4A: Self = Self(Cow::Borrowed("com.apple.m4a-audio"));
187 pub const MOBILE_3GPP: Self = Self(Cow::Borrowed("public.3gpp"));
189
190 pub fn from_identifier(
197 identifier: impl Into<String>,
198 ) -> Result<Self, InvalidRecordingIdentifier> {
199 let identifier = identifier.into();
200 if identifier.as_bytes().contains(&0) {
201 Err(InvalidRecordingIdentifier)
202 } else {
203 Ok(Self(Cow::Owned(identifier)))
204 }
205 }
206
207 #[must_use]
209 pub fn identifier(&self) -> &str {
210 self.0.as_ref()
211 }
212
213 #[must_use]
215 pub fn extension(&self) -> Option<&'static str> {
216 match self.identifier() {
217 "public.mpeg-4" => Some("mp4"),
218 "com.apple.quicktime-movie" => Some("mov"),
219 "com.apple.m4v-video" => Some("m4v"),
220 "com.apple.m4a-audio" => Some("m4a"),
221 "public.3gpp" => Some("3gp"),
222 _ => None,
223 }
224 }
225}
226
227impl Default for SCRecordingOutputFileType {
228 fn default() -> Self {
229 Self::MP4
230 }
231}
232
233impl std::fmt::Display for SCRecordingOutputFileType {
234 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235 match self.identifier() {
236 "public.mpeg-4" => f.write_str("MP4"),
237 "com.apple.quicktime-movie" => f.write_str("MOV"),
238 "com.apple.m4v-video" => f.write_str("M4V"),
239 "com.apple.m4a-audio" => f.write_str("M4A"),
240 "public.3gpp" => f.write_str("3GPP"),
241 other => write!(f, "file type {other}"),
242 }
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
248pub struct InvalidRecordingIdentifier;
249
250impl std::fmt::Display for InvalidRecordingIdentifier {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 f.write_str("recording identifier contains an interior NUL byte")
253 }
254}
255
256impl std::error::Error for InvalidRecordingIdentifier {}
257
258pub struct SCRecordingOutputConfiguration {
260 ptr: *const c_void,
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
265pub enum InvalidOutputPath {
266 NotUtf8,
268 InteriorNul,
270}
271
272impl std::fmt::Display for InvalidOutputPath {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 match self {
275 Self::NotUtf8 => f.write_str("output path is not valid UTF-8"),
276 Self::InteriorNul => f.write_str("output path contains an interior NUL byte"),
277 }
278 }
279}
280
281impl std::error::Error for InvalidOutputPath {}
282
283impl SCRecordingOutputConfiguration {
284 #[must_use]
291 pub fn new() -> Self {
292 Self::try_new().expect("SCRecordingOutput requires macOS 15.0 or later")
293 }
294
295 #[must_use]
297 pub fn try_new() -> Option<Self> {
298 if !SCRecordingOutput::is_available() {
299 return None;
300 }
301 let ptr = unsafe { crate::ffi::sc_recording_output_configuration_create() };
302 (!ptr.is_null()).then_some(Self { ptr })
303 }
304
305 #[must_use]
311 pub fn with_output_url(self, path: &Path) -> Self {
312 match self.try_with_output_url(path) {
313 Ok(config) => config,
314 Err((config, error)) => {
315 eprintln!("SCRecordingOutputConfiguration: {error}; output URL was not changed");
316 config
317 }
318 }
319 }
320
321 pub fn try_with_output_url(self, path: &Path) -> Result<Self, (Self, InvalidOutputPath)> {
330 let Some(path) = path.to_str() else {
331 return Err((self, InvalidOutputPath::NotUtf8));
332 };
333 let Ok(c_path) = std::ffi::CString::new(path) else {
334 return Err((self, InvalidOutputPath::InteriorNul));
335 };
336 unsafe {
337 crate::ffi::sc_recording_output_configuration_set_output_url(self.ptr, c_path.as_ptr());
338 }
339 Ok(self)
340 }
341
342 pub fn output_url(&self) -> Option<PathBuf> {
344 use std::os::unix::ffi::OsStringExt;
345
346 unsafe {
347 let path =
348 crate::ffi::sc_recording_output_configuration_get_output_path_owned(self.ptr);
349 if path.is_null() {
350 return None;
351 }
352 let bytes = std::ffi::CStr::from_ptr(path).to_bytes().to_vec();
353 crate::ffi::sc_free_string(path);
354 Some(PathBuf::from(std::ffi::OsString::from_vec(bytes)))
355 }
356 }
357
358 #[must_use]
360 #[allow(clippy::needless_pass_by_value)]
361 pub fn with_video_codec(self, codec: SCRecordingOutputCodec) -> Self {
362 let codec = unsafe {
365 std::ffi::CString::from_vec_unchecked(codec.identifier().as_bytes().to_vec())
366 };
367 unsafe {
368 crate::ffi::sc_recording_output_configuration_set_video_codec_identifier(
369 self.ptr,
370 codec.as_ptr(),
371 );
372 }
373 self
374 }
375
376 pub fn video_codec(&self) -> SCRecordingOutputCodec {
378 let identifier = unsafe {
379 crate::utils::ffi_string::ffi_string_owned(|| {
380 crate::ffi::sc_recording_output_configuration_get_video_codec_identifier_owned(
381 self.ptr,
382 )
383 })
384 }
385 .unwrap_or_else(|| SCRecordingOutputCodec::H264.identifier().to_string());
386 SCRecordingOutputCodec(Cow::Owned(identifier))
387 }
388
389 #[must_use]
391 #[allow(clippy::needless_pass_by_value)]
392 pub fn with_output_file_type(self, file_type: SCRecordingOutputFileType) -> Self {
393 let file_type = unsafe {
396 std::ffi::CString::from_vec_unchecked(file_type.identifier().as_bytes().to_vec())
397 };
398 unsafe {
399 crate::ffi::sc_recording_output_configuration_set_output_file_type_identifier(
400 self.ptr,
401 file_type.as_ptr(),
402 );
403 }
404 self
405 }
406
407 pub fn output_file_type(&self) -> SCRecordingOutputFileType {
409 let identifier = unsafe {
410 crate::utils::ffi_string::ffi_string_owned(|| {
411 crate::ffi::sc_recording_output_configuration_get_output_file_type_identifier_owned(
412 self.ptr,
413 )
414 })
415 }
416 .unwrap_or_else(|| SCRecordingOutputFileType::MP4.identifier().to_string());
417 SCRecordingOutputFileType(Cow::Owned(identifier))
418 }
419
420 pub fn available_video_codecs_count(&self) -> usize {
422 let count = unsafe {
423 crate::ffi::sc_recording_output_configuration_get_available_video_codecs_count(self.ptr)
424 };
425 usize::try_from(count).unwrap_or(0)
426 }
427
428 pub fn available_video_codecs(&self) -> Vec<SCRecordingOutputCodec> {
435 let count = self.available_video_codecs_count();
436 let mut codecs = Vec::with_capacity(count);
437 for i in 0..count {
438 let Ok(index) = isize::try_from(i) else { break };
439 let identifier = unsafe {
440 crate::utils::ffi_string::ffi_string_owned(|| {
441 crate::ffi::sc_recording_output_configuration_get_available_video_codec_identifier_at_owned(
442 self.ptr,
443 index,
444 )
445 })
446 };
447 if let Some(identifier) = identifier {
448 codecs.push(SCRecordingOutputCodec(Cow::Owned(identifier)));
449 }
450 }
451 codecs
452 }
453
454 pub fn available_output_file_types_count(&self) -> usize {
456 let count = unsafe {
457 crate::ffi::sc_recording_output_configuration_get_available_output_file_types_count(
458 self.ptr,
459 )
460 };
461 usize::try_from(count).unwrap_or(0)
462 }
463
464 pub fn available_output_file_types(&self) -> Vec<SCRecordingOutputFileType> {
470 let count = self.available_output_file_types_count();
471 let mut file_types = Vec::with_capacity(count);
472 for i in 0..count {
473 let Ok(index) = isize::try_from(i) else { break };
474 let identifier = unsafe {
475 crate::utils::ffi_string::ffi_string_owned(|| {
476 crate::ffi::sc_recording_output_configuration_get_available_output_file_type_identifier_at_owned(
477 self.ptr,
478 index,
479 )
480 })
481 };
482 if let Some(identifier) = identifier {
483 file_types.push(SCRecordingOutputFileType(Cow::Owned(identifier)));
484 }
485 }
486 file_types
487 }
488
489 #[must_use]
490 pub fn as_ptr(&self) -> *const c_void {
491 self.ptr
492 }
493}
494
495impl Default for SCRecordingOutputConfiguration {
496 fn default() -> Self {
497 Self::new()
498 }
499}
500
501crate::utils::retained::sc_retained!(
502 SCRecordingOutputConfiguration,
503 field = ptr,
504 release = crate::ffi::sc_recording_output_configuration_release,
505);
506
507impl Clone for SCRecordingOutputConfiguration {
508 fn clone(&self) -> Self {
516 Self {
517 ptr: unsafe { crate::ffi::sc_recording_output_configuration_copy(self.ptr) },
518 }
519 }
520}
521
522impl std::fmt::Debug for SCRecordingOutputConfiguration {
523 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
524 f.debug_struct("SCRecordingOutputConfiguration")
525 .field("video_codec", &format_args!("{}", self.video_codec()))
526 .field("file_type", &format_args!("{}", self.output_file_type()))
527 .finish()
528 }
529}
530
531pub trait SCRecordingOutputDelegate: Send + Sync + 'static {
580 fn recording_did_start(&self) {}
582 fn recording_did_fail(&self, _error: String) {}
584 fn recording_did_finish(&self) {}
586}
587
588#[allow(clippy::struct_field_names)]
617pub struct RecordingCallbacks {
618 on_start: Option<Box<dyn Fn() + Send + Sync + 'static>>,
619 on_fail: Option<Box<dyn Fn(String) + Send + Sync + 'static>>,
620 on_finish: Option<Box<dyn Fn() + Send + Sync + 'static>>,
621}
622
623impl RecordingCallbacks {
624 #[must_use]
626 pub fn new() -> Self {
627 Self {
628 on_start: None,
629 on_fail: None,
630 on_finish: None,
631 }
632 }
633
634 #[must_use]
636 pub fn on_start<F>(mut self, f: F) -> Self
637 where
638 F: Fn() + Send + Sync + 'static,
639 {
640 self.on_start = Some(Box::new(f));
641 self
642 }
643
644 #[must_use]
646 pub fn on_fail<F>(mut self, f: F) -> Self
647 where
648 F: Fn(String) + Send + Sync + 'static,
649 {
650 self.on_fail = Some(Box::new(f));
651 self
652 }
653
654 #[must_use]
656 pub fn on_finish<F>(mut self, f: F) -> Self
657 where
658 F: Fn() + Send + Sync + 'static,
659 {
660 self.on_finish = Some(Box::new(f));
661 self
662 }
663}
664
665impl Default for RecordingCallbacks {
666 fn default() -> Self {
667 Self::new()
668 }
669}
670
671impl std::fmt::Debug for RecordingCallbacks {
672 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
673 f.debug_struct("RecordingCallbacks")
674 .field("on_start", &self.on_start.is_some())
675 .field("on_fail", &self.on_fail.is_some())
676 .field("on_finish", &self.on_finish.is_some())
677 .finish()
678 }
679}
680
681impl SCRecordingOutputDelegate for RecordingCallbacks {
682 fn recording_did_start(&self) {
683 if let Some(ref f) = self.on_start {
684 f();
685 }
686 }
687
688 fn recording_did_fail(&self, error: String) {
689 if let Some(ref f) = self.on_fail {
690 f(error);
691 }
692 }
693
694 fn recording_did_finish(&self) {
695 if let Some(ref f) = self.on_finish {
696 f();
697 }
698 }
699}
700
701pub struct SCRecordingOutput {
705 ptr: *const c_void,
706 delegate_id: Option<usize>,
708}
709
710extern "C" fn recording_started_callback(ctx: *mut c_void) {
718 crate::utils::panic_safe::catch_user_panic(
719 "SCRecordingOutputDelegate::recording_did_start",
720 || {
721 if let Some(delegate) = lookup_delegate(ctx as usize) {
722 delegate.recording_did_start();
723 }
724 },
725 );
726}
727
728extern "C" fn recording_failed_callback(ctx: *mut c_void, error_code: i32, error: *const i8) {
729 crate::utils::panic_safe::catch_user_panic(
730 "SCRecordingOutputDelegate::recording_did_fail",
731 || {
732 let error_str = if error.is_null() {
733 String::from("Unknown error")
734 } else {
735 unsafe { std::ffi::CStr::from_ptr(error) }
736 .to_string_lossy()
737 .into_owned()
738 };
739
740 let full_error = if error_code == 0 {
742 error_str
743 } else {
744 crate::error::SCStreamErrorCode::from_raw(error_code).map_or_else(
745 || format!("{error_str} (code: {error_code})"),
746 |code| format!("{error_str} ({code})"),
747 )
748 };
749 if let Some(delegate) = lookup_delegate(ctx as usize) {
750 delegate.recording_did_fail(full_error);
751 }
752 },
753 );
754}
755
756extern "C" fn recording_finished_callback(ctx: *mut c_void) {
757 crate::utils::panic_safe::catch_user_panic(
758 "SCRecordingOutputDelegate::recording_did_finish",
759 || {
760 if let Some(delegate) = lookup_delegate(ctx as usize) {
761 delegate.recording_did_finish();
762 }
763 },
764 );
765}
766
767extern "C" fn recording_context_release_callback(ctx: *mut c_void) {
768 crate::utils::panic_safe::catch_user_panic("SCRecordingOutputDelegate::release", || {
769 drop(remove_delegate(ctx as usize));
770 });
771}
772
773impl SCRecordingOutput {
774 #[must_use]
776 pub fn is_available() -> bool {
777 unsafe { crate::ffi::sc_recording_output_is_available() }
778 }
779
780 pub fn new(config: &SCRecordingOutputConfiguration) -> Option<Self> {
785 if !Self::is_available() {
786 return None;
787 }
788 let ptr = unsafe { crate::ffi::sc_recording_output_create(config.as_ptr()) };
789 if ptr.is_null() {
790 None
791 } else {
792 Some(Self {
793 ptr,
794 delegate_id: None,
795 })
796 }
797 }
798
799 pub fn new_with_delegate<D: SCRecordingOutputDelegate>(
809 config: &SCRecordingOutputConfiguration,
810 delegate: D,
811 ) -> Option<Self> {
812 if !Self::is_available() {
813 return None;
814 }
815 let entry = RecordingDelegateEntry {
816 delegate: Arc::new(delegate),
817 };
818 let delegate_id = {
819 let mut registry = RECORDING_DELEGATE_REGISTRY
820 .lock()
821 .unwrap_or_else(PoisonError::into_inner);
822 let delegates = registry.get_or_insert_with(HashMap::new);
823 loop {
824 let id = NEXT_DELEGATE_ID.fetch_add(1, Ordering::Relaxed);
825 if id != 0 && !delegates.contains_key(&id) {
826 delegates.insert(id, entry);
827 drop(registry);
828 break id;
829 }
830 }
831 };
832
833 let ctx = delegate_id as *mut c_void;
835
836 let ptr = unsafe {
837 crate::ffi::sc_recording_output_create_with_delegate(
838 config.as_ptr(),
839 Some(recording_started_callback),
840 Some(recording_failed_callback),
841 Some(recording_finished_callback),
842 Some(recording_context_release_callback),
843 ctx,
844 )
845 };
846
847 if ptr.is_null() {
848 drop(remove_delegate(delegate_id));
849 None
850 } else {
851 Some(Self {
852 ptr,
853 delegate_id: Some(delegate_id),
854 })
855 }
856 }
857
858 pub fn recorded_duration(&self) -> CMTime {
860 let mut value: i64 = 0;
861 let mut timescale: i32 = 0;
862 unsafe {
863 crate::ffi::sc_recording_output_get_recorded_duration(
864 self.ptr,
865 &mut value,
866 &mut timescale,
867 );
868 }
869 CMTime::new(value, timescale)
870 }
871
872 pub fn recorded_file_size(&self) -> i64 {
874 unsafe { crate::ffi::sc_recording_output_get_recorded_file_size(self.ptr) }
875 }
876
877 #[must_use]
878 pub fn as_ptr(&self) -> *const c_void {
879 self.ptr
880 }
881}
882
883impl Clone for SCRecordingOutput {
884 fn clone(&self) -> Self {
885 unsafe {
886 Self {
887 ptr: crate::ffi::sc_recording_output_retain(self.ptr),
888 delegate_id: self.delegate_id,
889 }
890 }
891 }
892}
893
894impl std::fmt::Debug for SCRecordingOutput {
895 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
896 f.debug_struct("SCRecordingOutput")
897 .field("recorded_duration", &self.recorded_duration())
898 .field("recorded_file_size", &self.recorded_file_size())
899 .field("has_delegate", &self.delegate_id.is_some())
900 .finish_non_exhaustive()
901 }
902}
903
904impl Drop for SCRecordingOutput {
905 fn drop(&mut self) {
906 if !self.ptr.is_null() {
907 unsafe {
908 crate::ffi::sc_recording_output_release(self.ptr);
909 }
910 }
911 }
912}
913
914unsafe impl Send for SCRecordingOutput {}
916unsafe impl Sync for SCRecordingOutput {}
917
918unsafe impl Send for SCRecordingOutputConfiguration {}
920unsafe impl Sync for SCRecordingOutputConfiguration {}