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;
63use crate::error::SCError;
64
65static RECORDING_DELEGATE_REGISTRY: Mutex<Option<HashMap<usize, RecordingDelegateEntry>>> =
67 Mutex::new(None);
68
69static NEXT_DELEGATE_ID: AtomicUsize = AtomicUsize::new(1);
71
72struct RecordingDelegateEntry {
78 delegate: Arc<dyn SCRecordingOutputDelegate>,
79}
80
81fn lookup_delegate(key: usize) -> Option<Arc<dyn SCRecordingOutputDelegate>> {
83 let registry = RECORDING_DELEGATE_REGISTRY
84 .lock()
85 .unwrap_or_else(PoisonError::into_inner);
86 registry
87 .as_ref()?
88 .get(&key)
89 .map(|entry| Arc::clone(&entry.delegate))
90}
91
92fn remove_delegate(key: usize) -> Option<RecordingDelegateEntry> {
93 let mut registry = RECORDING_DELEGATE_REGISTRY
94 .lock()
95 .unwrap_or_else(PoisonError::into_inner);
96 registry
97 .as_mut()
98 .and_then(|delegates| delegates.remove(&key))
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Hash)]
106pub struct SCRecordingOutputCodec(Cow<'static, str>);
107
108impl SCRecordingOutputCodec {
109 pub const H264: Self = Self(Cow::Borrowed("avc1"));
111 pub const HEVC: Self = Self(Cow::Borrowed("hvc1"));
113 pub const JPEG: Self = Self(Cow::Borrowed("jpeg"));
115 pub const PRO_RES_422: Self = Self(Cow::Borrowed("apcn"));
117 pub const PRO_RES_4444: Self = Self(Cow::Borrowed("ap4h"));
119 pub const HEVC_WITH_ALPHA: Self = Self(Cow::Borrowed("muxa"));
121 pub const PRO_RES_422_HQ: Self = Self(Cow::Borrowed("apch"));
123 pub const PRO_RES_422_LT: Self = Self(Cow::Borrowed("apcs"));
125 pub const PRO_RES_422_PROXY: Self = Self(Cow::Borrowed("apco"));
127
128 pub fn from_identifier(
135 identifier: impl Into<String>,
136 ) -> Result<Self, InvalidRecordingIdentifier> {
137 let identifier = identifier.into();
138 if identifier.as_bytes().contains(&0) {
139 Err(InvalidRecordingIdentifier)
140 } else {
141 Ok(Self(Cow::Owned(identifier)))
142 }
143 }
144
145 #[must_use]
147 pub fn identifier(&self) -> &str {
148 self.0.as_ref()
149 }
150}
151
152impl Default for SCRecordingOutputCodec {
153 fn default() -> Self {
154 Self::H264
155 }
156}
157
158impl std::fmt::Display for SCRecordingOutputCodec {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 match self.identifier() {
161 "avc1" => f.write_str("H.264"),
162 "hvc1" => f.write_str("HEVC"),
163 "jpeg" => f.write_str("JPEG"),
164 "apcn" => f.write_str("ProRes 422"),
165 "ap4h" => f.write_str("ProRes 4444"),
166 "muxa" => f.write_str("HEVC with alpha"),
167 "apch" => f.write_str("ProRes 422 HQ"),
168 "apcs" => f.write_str("ProRes 422 LT"),
169 "apco" => f.write_str("ProRes 422 Proxy"),
170 other => write!(f, "codec {other}"),
171 }
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Hash)]
177pub struct SCRecordingOutputFileType(Cow<'static, str>);
178
179impl SCRecordingOutputFileType {
180 pub const MP4: Self = Self(Cow::Borrowed("public.mpeg-4"));
182 pub const MOV: Self = Self(Cow::Borrowed("com.apple.quicktime-movie"));
184 pub const M4V: Self = Self(Cow::Borrowed("com.apple.m4v-video"));
186 pub const M4A: Self = Self(Cow::Borrowed("com.apple.m4a-audio"));
188 pub const MOBILE_3GPP: Self = Self(Cow::Borrowed("public.3gpp"));
190
191 pub fn from_identifier(
198 identifier: impl Into<String>,
199 ) -> Result<Self, InvalidRecordingIdentifier> {
200 let identifier = identifier.into();
201 if identifier.as_bytes().contains(&0) {
202 Err(InvalidRecordingIdentifier)
203 } else {
204 Ok(Self(Cow::Owned(identifier)))
205 }
206 }
207
208 #[must_use]
210 pub fn identifier(&self) -> &str {
211 self.0.as_ref()
212 }
213
214 #[must_use]
216 pub fn extension(&self) -> Option<&'static str> {
217 match self.identifier() {
218 "public.mpeg-4" => Some("mp4"),
219 "com.apple.quicktime-movie" => Some("mov"),
220 "com.apple.m4v-video" => Some("m4v"),
221 "com.apple.m4a-audio" => Some("m4a"),
222 "public.3gpp" => Some("3gp"),
223 _ => None,
224 }
225 }
226}
227
228impl Default for SCRecordingOutputFileType {
229 fn default() -> Self {
230 Self::MP4
231 }
232}
233
234impl std::fmt::Display for SCRecordingOutputFileType {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 match self.identifier() {
237 "public.mpeg-4" => f.write_str("MP4"),
238 "com.apple.quicktime-movie" => f.write_str("MOV"),
239 "com.apple.m4v-video" => f.write_str("M4V"),
240 "com.apple.m4a-audio" => f.write_str("M4A"),
241 "public.3gpp" => f.write_str("3GPP"),
242 other => write!(f, "file type {other}"),
243 }
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249pub struct InvalidRecordingIdentifier;
250
251impl std::fmt::Display for InvalidRecordingIdentifier {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 f.write_str("recording identifier contains an interior NUL byte")
254 }
255}
256
257impl std::error::Error for InvalidRecordingIdentifier {}
258
259pub struct SCRecordingOutputConfiguration {
261 ptr: *const c_void,
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
266pub enum InvalidOutputPath {
267 NotUtf8,
269 InteriorNul,
271}
272
273impl std::fmt::Display for InvalidOutputPath {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 match self {
276 Self::NotUtf8 => f.write_str("output path is not valid UTF-8"),
277 Self::InteriorNul => f.write_str("output path contains an interior NUL byte"),
278 }
279 }
280}
281
282impl std::error::Error for InvalidOutputPath {}
283
284impl SCRecordingOutputConfiguration {
285 pub fn new() -> Result<Self, SCError> {
292 if !SCRecordingOutput::is_available() {
293 return Err(SCError::feature_not_available("SCRecordingOutput", "15.0"));
294 }
295 let ptr = unsafe { crate::ffi::sc_recording_output_configuration_create() };
296 if ptr.is_null() {
297 return Err(SCError::null_pointer("SCRecordingOutputConfiguration"));
298 }
299 Ok(Self { ptr })
300 }
301
302 pub fn with_output_url(self, path: &Path) -> Result<Self, InvalidOutputPath> {
309 let path = path.to_str().ok_or(InvalidOutputPath::NotUtf8)?;
310 let c_path = std::ffi::CString::new(path).map_err(|_| InvalidOutputPath::InteriorNul)?;
311 unsafe {
312 crate::ffi::sc_recording_output_configuration_set_output_url(self.ptr, c_path.as_ptr());
313 }
314 Ok(self)
315 }
316
317 pub fn output_url(&self) -> Option<PathBuf> {
319 use std::os::unix::ffi::OsStringExt;
320
321 unsafe {
322 let path =
323 crate::ffi::sc_recording_output_configuration_get_output_path_owned(self.ptr);
324 if path.is_null() {
325 return None;
326 }
327 let bytes = std::ffi::CStr::from_ptr(path).to_bytes().to_vec();
328 crate::ffi::sc_free_string(path);
329 Some(PathBuf::from(std::ffi::OsString::from_vec(bytes)))
330 }
331 }
332
333 #[must_use]
335 #[allow(clippy::needless_pass_by_value)]
336 pub fn with_video_codec(self, codec: SCRecordingOutputCodec) -> Self {
337 let codec = unsafe {
340 std::ffi::CString::from_vec_unchecked(codec.identifier().as_bytes().to_vec())
341 };
342 unsafe {
343 crate::ffi::sc_recording_output_configuration_set_video_codec_identifier(
344 self.ptr,
345 codec.as_ptr(),
346 );
347 }
348 self
349 }
350
351 pub fn video_codec(&self) -> SCRecordingOutputCodec {
353 let identifier = unsafe {
354 crate::utils::ffi_string::ffi_string_owned(|| {
355 crate::ffi::sc_recording_output_configuration_get_video_codec_identifier_owned(
356 self.ptr,
357 )
358 })
359 }
360 .unwrap_or_else(|| SCRecordingOutputCodec::H264.identifier().to_string());
361 SCRecordingOutputCodec(Cow::Owned(identifier))
362 }
363
364 #[must_use]
366 #[allow(clippy::needless_pass_by_value)]
367 pub fn with_output_file_type(self, file_type: SCRecordingOutputFileType) -> Self {
368 let file_type = unsafe {
371 std::ffi::CString::from_vec_unchecked(file_type.identifier().as_bytes().to_vec())
372 };
373 unsafe {
374 crate::ffi::sc_recording_output_configuration_set_output_file_type_identifier(
375 self.ptr,
376 file_type.as_ptr(),
377 );
378 }
379 self
380 }
381
382 pub fn output_file_type(&self) -> SCRecordingOutputFileType {
384 let identifier = unsafe {
385 crate::utils::ffi_string::ffi_string_owned(|| {
386 crate::ffi::sc_recording_output_configuration_get_output_file_type_identifier_owned(
387 self.ptr,
388 )
389 })
390 }
391 .unwrap_or_else(|| SCRecordingOutputFileType::MP4.identifier().to_string());
392 SCRecordingOutputFileType(Cow::Owned(identifier))
393 }
394
395 pub fn available_video_codecs_count(&self) -> usize {
397 let count = unsafe {
398 crate::ffi::sc_recording_output_configuration_get_available_video_codecs_count(self.ptr)
399 };
400 usize::try_from(count).unwrap_or(0)
401 }
402
403 pub fn available_video_codecs(&self) -> Vec<SCRecordingOutputCodec> {
410 let count = self.available_video_codecs_count();
411 let mut codecs = Vec::with_capacity(count);
412 for i in 0..count {
413 let Ok(index) = isize::try_from(i) else { break };
414 let identifier = unsafe {
415 crate::utils::ffi_string::ffi_string_owned(|| {
416 crate::ffi::sc_recording_output_configuration_get_available_video_codec_identifier_at_owned(
417 self.ptr,
418 index,
419 )
420 })
421 };
422 if let Some(identifier) = identifier {
423 codecs.push(SCRecordingOutputCodec(Cow::Owned(identifier)));
424 }
425 }
426 codecs
427 }
428
429 pub fn available_output_file_types_count(&self) -> usize {
431 let count = unsafe {
432 crate::ffi::sc_recording_output_configuration_get_available_output_file_types_count(
433 self.ptr,
434 )
435 };
436 usize::try_from(count).unwrap_or(0)
437 }
438
439 pub fn available_output_file_types(&self) -> Vec<SCRecordingOutputFileType> {
445 let count = self.available_output_file_types_count();
446 let mut file_types = Vec::with_capacity(count);
447 for i in 0..count {
448 let Ok(index) = isize::try_from(i) else { break };
449 let identifier = unsafe {
450 crate::utils::ffi_string::ffi_string_owned(|| {
451 crate::ffi::sc_recording_output_configuration_get_available_output_file_type_identifier_at_owned(
452 self.ptr,
453 index,
454 )
455 })
456 };
457 if let Some(identifier) = identifier {
458 file_types.push(SCRecordingOutputFileType(Cow::Owned(identifier)));
459 }
460 }
461 file_types
462 }
463
464 #[must_use]
465 pub fn as_ptr(&self) -> *const c_void {
466 self.ptr
467 }
468}
469
470crate::utils::retained::sc_retained!(
471 SCRecordingOutputConfiguration,
472 field = ptr,
473 release = crate::ffi::sc_recording_output_configuration_release,
474);
475
476impl Clone for SCRecordingOutputConfiguration {
477 fn clone(&self) -> Self {
485 Self {
486 ptr: unsafe { crate::ffi::sc_recording_output_configuration_copy(self.ptr) },
487 }
488 }
489}
490
491impl std::fmt::Debug for SCRecordingOutputConfiguration {
492 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493 f.debug_struct("SCRecordingOutputConfiguration")
494 .field("video_codec", &format_args!("{}", self.video_codec()))
495 .field("file_type", &format_args!("{}", self.output_file_type()))
496 .finish()
497 }
498}
499
500pub trait SCRecordingOutputDelegate: Send + Sync + 'static {
549 fn recording_did_start(&self) {}
551 fn recording_did_fail(&self, _error: String) {}
553 fn recording_did_finish(&self) {}
555}
556
557#[allow(clippy::struct_field_names)]
586pub struct RecordingCallbacks {
587 on_start: Option<Box<dyn Fn() + Send + Sync + 'static>>,
588 on_fail: Option<Box<dyn Fn(String) + Send + Sync + 'static>>,
589 on_finish: Option<Box<dyn Fn() + Send + Sync + 'static>>,
590}
591
592impl RecordingCallbacks {
593 #[must_use]
595 pub fn new() -> Self {
596 Self {
597 on_start: None,
598 on_fail: None,
599 on_finish: None,
600 }
601 }
602
603 #[must_use]
605 pub fn on_start<F>(mut self, f: F) -> Self
606 where
607 F: Fn() + Send + Sync + 'static,
608 {
609 self.on_start = Some(Box::new(f));
610 self
611 }
612
613 #[must_use]
615 pub fn on_fail<F>(mut self, f: F) -> Self
616 where
617 F: Fn(String) + Send + Sync + 'static,
618 {
619 self.on_fail = Some(Box::new(f));
620 self
621 }
622
623 #[must_use]
625 pub fn on_finish<F>(mut self, f: F) -> Self
626 where
627 F: Fn() + Send + Sync + 'static,
628 {
629 self.on_finish = Some(Box::new(f));
630 self
631 }
632}
633
634impl Default for RecordingCallbacks {
635 fn default() -> Self {
636 Self::new()
637 }
638}
639
640impl std::fmt::Debug for RecordingCallbacks {
641 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642 f.debug_struct("RecordingCallbacks")
643 .field("on_start", &self.on_start.is_some())
644 .field("on_fail", &self.on_fail.is_some())
645 .field("on_finish", &self.on_finish.is_some())
646 .finish()
647 }
648}
649
650impl SCRecordingOutputDelegate for RecordingCallbacks {
651 fn recording_did_start(&self) {
652 if let Some(ref f) = self.on_start {
653 f();
654 }
655 }
656
657 fn recording_did_fail(&self, error: String) {
658 if let Some(ref f) = self.on_fail {
659 f(error);
660 }
661 }
662
663 fn recording_did_finish(&self) {
664 if let Some(ref f) = self.on_finish {
665 f();
666 }
667 }
668}
669
670pub struct SCRecordingOutput {
674 ptr: *const c_void,
675 delegate_id: Option<usize>,
677}
678
679extern "C" fn recording_started_callback(ctx: *mut c_void) {
687 crate::utils::panic_safe::catch_user_panic(
688 "SCRecordingOutputDelegate::recording_did_start",
689 || {
690 if let Some(delegate) = lookup_delegate(ctx as usize) {
691 delegate.recording_did_start();
692 }
693 },
694 );
695}
696
697extern "C" fn recording_failed_callback(ctx: *mut c_void, error_code: i32, error: *const i8) {
698 crate::utils::panic_safe::catch_user_panic(
699 "SCRecordingOutputDelegate::recording_did_fail",
700 || {
701 let error_str = if error.is_null() {
702 String::from("Unknown error")
703 } else {
704 unsafe { std::ffi::CStr::from_ptr(error) }
705 .to_string_lossy()
706 .into_owned()
707 };
708
709 let full_error = if error_code == 0 {
711 error_str
712 } else {
713 crate::error::SCStreamErrorCode::from_raw(error_code).map_or_else(
714 || format!("{error_str} (code: {error_code})"),
715 |code| format!("{error_str} ({code})"),
716 )
717 };
718 if let Some(delegate) = lookup_delegate(ctx as usize) {
719 delegate.recording_did_fail(full_error);
720 }
721 },
722 );
723}
724
725extern "C" fn recording_finished_callback(ctx: *mut c_void) {
726 crate::utils::panic_safe::catch_user_panic(
727 "SCRecordingOutputDelegate::recording_did_finish",
728 || {
729 if let Some(delegate) = lookup_delegate(ctx as usize) {
730 delegate.recording_did_finish();
731 }
732 },
733 );
734}
735
736extern "C" fn recording_context_release_callback(ctx: *mut c_void) {
737 crate::utils::panic_safe::catch_user_panic("SCRecordingOutputDelegate::release", || {
738 drop(remove_delegate(ctx as usize));
739 });
740}
741
742impl SCRecordingOutput {
743 #[must_use]
745 pub fn is_available() -> bool {
746 unsafe { crate::ffi::sc_recording_output_is_available() }
747 }
748
749 pub fn new(config: &SCRecordingOutputConfiguration) -> Option<Self> {
754 if !Self::is_available() {
755 return None;
756 }
757 let config = config.clone();
758 let ptr = unsafe { crate::ffi::sc_recording_output_create(config.as_ptr()) };
759 if ptr.is_null() {
760 None
761 } else {
762 Some(Self {
763 ptr,
764 delegate_id: None,
765 })
766 }
767 }
768
769 pub fn new_with_delegate<D: SCRecordingOutputDelegate>(
779 config: &SCRecordingOutputConfiguration,
780 delegate: D,
781 ) -> Option<Self> {
782 if !Self::is_available() {
783 return None;
784 }
785 let entry = RecordingDelegateEntry {
786 delegate: Arc::new(delegate),
787 };
788 let delegate_id = {
789 let mut registry = RECORDING_DELEGATE_REGISTRY
790 .lock()
791 .unwrap_or_else(PoisonError::into_inner);
792 let delegates = registry.get_or_insert_with(HashMap::new);
793 loop {
794 let id = NEXT_DELEGATE_ID.fetch_add(1, Ordering::Relaxed);
795 if id != 0 && !delegates.contains_key(&id) {
796 delegates.insert(id, entry);
797 drop(registry);
798 break id;
799 }
800 }
801 };
802
803 let ctx = delegate_id as *mut c_void;
805 let config = config.clone();
806
807 let ptr = unsafe {
808 crate::ffi::sc_recording_output_create_with_delegate(
809 config.as_ptr(),
810 Some(recording_started_callback),
811 Some(recording_failed_callback),
812 Some(recording_finished_callback),
813 Some(recording_context_release_callback),
814 ctx,
815 )
816 };
817
818 if ptr.is_null() {
819 drop(remove_delegate(delegate_id));
820 None
821 } else {
822 Some(Self {
823 ptr,
824 delegate_id: Some(delegate_id),
825 })
826 }
827 }
828
829 pub fn recorded_duration(&self) -> CMTime {
831 let mut value: i64 = 0;
832 let mut timescale: i32 = 0;
833 unsafe {
834 crate::ffi::sc_recording_output_get_recorded_duration(
835 self.ptr,
836 &raw mut value,
837 &raw mut timescale,
838 );
839 }
840 CMTime::new(value, timescale)
841 }
842
843 pub fn recorded_file_size(&self) -> i64 {
845 unsafe { crate::ffi::sc_recording_output_get_recorded_file_size(self.ptr) }
846 }
847
848 #[must_use]
849 pub fn as_ptr(&self) -> *const c_void {
850 self.ptr
851 }
852}
853
854impl Clone for SCRecordingOutput {
855 fn clone(&self) -> Self {
856 unsafe {
857 Self {
858 ptr: crate::ffi::sc_recording_output_retain(self.ptr),
859 delegate_id: self.delegate_id,
860 }
861 }
862 }
863}
864
865impl std::fmt::Debug for SCRecordingOutput {
866 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
867 f.debug_struct("SCRecordingOutput")
868 .field("recorded_duration", &self.recorded_duration())
869 .field("recorded_file_size", &self.recorded_file_size())
870 .field("has_delegate", &self.delegate_id.is_some())
871 .finish_non_exhaustive()
872 }
873}
874
875impl Drop for SCRecordingOutput {
876 fn drop(&mut self) {
877 if !self.ptr.is_null() {
878 unsafe {
879 crate::ffi::sc_recording_output_release(self.ptr);
880 }
881 }
882 }
883}
884
885unsafe impl Send for SCRecordingOutput {}
887unsafe impl Sync for SCRecordingOutput {}
888
889unsafe impl Send for SCRecordingOutputConfiguration {}
891unsafe impl Sync for SCRecordingOutputConfiguration {}