1use 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
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 {
72 delegate: Box<dyn SCRecordingOutputDelegate>,
73 ref_count: usize,
74}
75
76#[repr(i32)]
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
79pub enum SCRecordingOutputCodec {
80 #[default]
82 H264 = 0,
83 HEVC = 1,
85}
86
87#[repr(i32)]
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
90pub enum SCRecordingOutputFileType {
91 #[default]
93 MP4 = 0,
94 MOV = 1,
96}
97
98pub struct SCRecordingOutputConfiguration {
100 ptr: *const c_void,
101}
102
103impl SCRecordingOutputConfiguration {
104 #[must_use]
106 pub fn new() -> Self {
107 let ptr = unsafe { crate::ffi::sc_recording_output_configuration_create() };
108 Self { ptr }
109 }
110
111 #[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 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 #[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 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 #[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 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 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 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 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 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
278pub trait SCRecordingOutputDelegate: Send + 'static {
325 fn recording_did_start(&self) {}
327 fn recording_did_fail(&self, _error: String) {}
329 fn recording_did_finish(&self) {}
331}
332
333#[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 #[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 #[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 #[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 #[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
446pub struct SCRecordingOutput {
450 ptr: *const c_void,
451 delegate_id: Option<usize>,
453}
454
455extern "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 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 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 pub fn new_with_delegate<D: SCRecordingOutputDelegate>(
543 config: &SCRecordingOutputConfiguration,
544 delegate: D,
545 ) -> Option<Self> {
546 let delegate_id = NEXT_DELEGATE_ID.fetch_add(1, Ordering::Relaxed);
548
549 {
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 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 {
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 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 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 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 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
685unsafe impl Send for SCRecordingOutput {}
687unsafe impl Sync for SCRecordingOutput {}
688
689unsafe impl Send for SCRecordingOutputConfiguration {}
691unsafe impl Sync for SCRecordingOutputConfiguration {}