1use 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
63static RECORDING_DELEGATE_REGISTRY: Mutex<Option<HashMap<usize, RecordingDelegateEntry>>> =
65 Mutex::new(None);
66
67static NEXT_DELEGATE_ID: AtomicUsize = AtomicUsize::new(1);
69
70struct RecordingDelegateEntry {
71 delegate: Box<dyn SCRecordingOutputDelegate>,
72 ref_count: usize,
73}
74
75#[repr(i32)]
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
78pub enum SCRecordingOutputCodec {
79 #[default]
81 H264 = 0,
82 HEVC = 1,
84}
85
86#[repr(i32)]
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
89pub enum SCRecordingOutputFileType {
90 #[default]
92 MP4 = 0,
93 MOV = 1,
95}
96
97pub struct SCRecordingOutputConfiguration {
99 ptr: *const c_void,
100}
101
102impl SCRecordingOutputConfiguration {
103 #[must_use]
105 pub fn new() -> Self {
106 let ptr = unsafe { crate::ffi::sc_recording_output_configuration_create() };
107 Self { ptr }
108 }
109
110 #[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 #[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 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 #[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 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 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 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 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 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
280pub trait SCRecordingOutputDelegate: Send + 'static {
327 fn recording_did_start(&self) {}
329 fn recording_did_fail(&self, _error: String) {}
331 fn recording_did_finish(&self) {}
333}
334
335#[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 #[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 #[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 #[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 #[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
448pub struct SCRecordingOutput {
452 ptr: *const c_void,
453 delegate_id: Option<usize>,
455}
456
457extern "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 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 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 pub fn new_with_delegate<D: SCRecordingOutputDelegate>(
548 config: &SCRecordingOutputConfiguration,
549 delegate: D,
550 ) -> Option<Self> {
551 let delegate_id = NEXT_DELEGATE_ID.fetch_add(1, Ordering::Relaxed);
553
554 {
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 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 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 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 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 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 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
690unsafe impl Send for SCRecordingOutput {}
692unsafe impl Sync for SCRecordingOutput {}
693
694unsafe impl Send for SCRecordingOutputConfiguration {}
696unsafe impl Sync for SCRecordingOutputConfiguration {}