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 entry.delegate.recording_did_start();
464 }
465 }
466 }
467}
468
469extern "C" fn recording_failed_callback(ctx: *mut c_void, error_code: i32, error: *const i8) {
470 let key = ctx as usize;
471 let error_str = if error.is_null() {
472 String::from("Unknown error")
473 } else {
474 unsafe { std::ffi::CStr::from_ptr(error) }
475 .to_string_lossy()
476 .into_owned()
477 };
478
479 let full_error = if error_code != 0 {
481 crate::error::SCStreamErrorCode::from_raw(error_code).map_or_else(
482 || format!("{error_str} (code: {error_code})"),
483 |code| format!("{error_str} ({code})"),
484 )
485 } else {
486 error_str
487 };
488
489 if let Ok(registry) = RECORDING_DELEGATE_REGISTRY.lock() {
490 if let Some(ref delegates) = *registry {
491 if let Some(entry) = delegates.get(&key) {
492 entry.delegate.recording_did_fail(full_error);
493 }
494 }
495 }
496}
497
498extern "C" fn recording_finished_callback(ctx: *mut c_void) {
499 let key = ctx as usize;
500 if let Ok(registry) = RECORDING_DELEGATE_REGISTRY.lock() {
501 if let Some(ref delegates) = *registry {
502 if let Some(entry) = delegates.get(&key) {
503 entry.delegate.recording_did_finish();
504 }
505 }
506 }
507}
508
509impl SCRecordingOutput {
510 pub fn new(config: &SCRecordingOutputConfiguration) -> Option<Self> {
515 let ptr = unsafe { crate::ffi::sc_recording_output_create(config.as_ptr()) };
516 if ptr.is_null() {
517 None
518 } else {
519 Some(Self {
520 ptr,
521 delegate_id: None,
522 })
523 }
524 }
525
526 pub fn new_with_delegate<D: SCRecordingOutputDelegate>(
539 config: &SCRecordingOutputConfiguration,
540 delegate: D,
541 ) -> Option<Self> {
542 let delegate_id = NEXT_DELEGATE_ID.fetch_add(1, Ordering::Relaxed);
544
545 {
547 let mut registry = RECORDING_DELEGATE_REGISTRY.lock().unwrap();
548 if registry.is_none() {
549 *registry = Some(HashMap::new());
550 }
551 if let Some(ref mut delegates) = *registry {
552 delegates.insert(
553 delegate_id,
554 RecordingDelegateEntry {
555 delegate: Box::new(delegate),
556 ref_count: 1,
557 },
558 );
559 }
560 }
561
562 let ctx = delegate_id as *mut c_void;
564
565 let ptr = unsafe {
566 crate::ffi::sc_recording_output_create_with_delegate(
567 config.as_ptr(),
568 Some(recording_started_callback),
569 Some(recording_failed_callback),
570 Some(recording_finished_callback),
571 ctx,
572 )
573 };
574
575 if ptr.is_null() {
576 if let Ok(mut registry) = RECORDING_DELEGATE_REGISTRY.lock() {
578 if let Some(ref mut delegates) = *registry {
579 delegates.remove(&delegate_id);
580 }
581 }
582 None
583 } else {
584 Some(Self {
585 ptr,
586 delegate_id: Some(delegate_id),
587 })
588 }
589 }
590
591 pub fn recorded_duration(&self) -> CMTime {
593 let mut value: i64 = 0;
594 let mut timescale: i32 = 0;
595 unsafe {
596 crate::ffi::sc_recording_output_get_recorded_duration(
597 self.ptr,
598 &mut value,
599 &mut timescale,
600 );
601 }
602 CMTime {
603 value,
604 timescale,
605 flags: 0,
606 epoch: 0,
607 }
608 }
609
610 pub fn recorded_file_size(&self) -> i64 {
612 unsafe { crate::ffi::sc_recording_output_get_recorded_file_size(self.ptr) }
613 }
614
615 #[must_use]
616 pub fn as_ptr(&self) -> *const c_void {
617 self.ptr
618 }
619}
620
621impl Clone for SCRecordingOutput {
622 fn clone(&self) -> Self {
623 if let Some(delegate_id) = self.delegate_id {
625 if let Ok(mut registry) = RECORDING_DELEGATE_REGISTRY.lock() {
626 if let Some(ref mut delegates) = *registry {
627 if let Some(entry) = delegates.get_mut(&delegate_id) {
628 entry.ref_count += 1;
629 }
630 }
631 }
632 }
633
634 unsafe {
635 Self {
636 ptr: crate::ffi::sc_recording_output_retain(self.ptr),
637 delegate_id: self.delegate_id,
638 }
639 }
640 }
641}
642
643impl std::fmt::Debug for SCRecordingOutput {
644 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
645 f.debug_struct("SCRecordingOutput")
646 .field("recorded_duration", &self.recorded_duration())
647 .field("recorded_file_size", &self.recorded_file_size())
648 .field("has_delegate", &self.delegate_id.is_some())
649 .finish_non_exhaustive()
650 }
651}
652
653impl Drop for SCRecordingOutput {
654 fn drop(&mut self) {
655 if let Some(delegate_id) = self.delegate_id {
657 let mut should_remove = false;
658 if let Ok(mut registry) = RECORDING_DELEGATE_REGISTRY.lock() {
659 if let Some(ref mut delegates) = *registry {
660 if let Some(entry) = delegates.get_mut(&delegate_id) {
661 entry.ref_count -= 1;
662 if entry.ref_count == 0 {
663 should_remove = true;
664 }
665 }
666 if should_remove {
667 delegates.remove(&delegate_id);
668 }
669 }
670 }
671 }
672
673 if !self.ptr.is_null() {
674 unsafe {
675 crate::ffi::sc_recording_output_release(self.ptr);
676 }
677 }
678 }
679}
680
681unsafe impl Send for SCRecordingOutput {}
683unsafe impl Sync for SCRecordingOutput {}
684
685unsafe impl Send for SCRecordingOutputConfiguration {}
687unsafe impl Sync for SCRecordingOutputConfiguration {}