use std::borrow::Cow;
use std::collections::HashMap;
use std::ffi::c_void;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use crate::cm::CMTime;
static RECORDING_DELEGATE_REGISTRY: Mutex<Option<HashMap<usize, RecordingDelegateEntry>>> =
Mutex::new(None);
static NEXT_DELEGATE_ID: AtomicUsize = AtomicUsize::new(1);
struct RecordingDelegateEntry {
delegate: Arc<dyn SCRecordingOutputDelegate>,
}
fn lookup_delegate(key: usize) -> Option<Arc<dyn SCRecordingOutputDelegate>> {
let registry = RECORDING_DELEGATE_REGISTRY
.lock()
.unwrap_or_else(PoisonError::into_inner);
registry
.as_ref()?
.get(&key)
.map(|entry| Arc::clone(&entry.delegate))
}
fn remove_delegate(key: usize) -> Option<RecordingDelegateEntry> {
let mut registry = RECORDING_DELEGATE_REGISTRY
.lock()
.unwrap_or_else(PoisonError::into_inner);
registry
.as_mut()
.and_then(|delegates| delegates.remove(&key))
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SCRecordingOutputCodec(Cow<'static, str>);
impl SCRecordingOutputCodec {
pub const H264: Self = Self(Cow::Borrowed("avc1"));
pub const HEVC: Self = Self(Cow::Borrowed("hvc1"));
pub const JPEG: Self = Self(Cow::Borrowed("jpeg"));
pub const PRO_RES_422: Self = Self(Cow::Borrowed("apcn"));
pub const PRO_RES_4444: Self = Self(Cow::Borrowed("ap4h"));
pub const HEVC_WITH_ALPHA: Self = Self(Cow::Borrowed("muxa"));
pub const PRO_RES_422_HQ: Self = Self(Cow::Borrowed("apch"));
pub const PRO_RES_422_LT: Self = Self(Cow::Borrowed("apcs"));
pub const PRO_RES_422_PROXY: Self = Self(Cow::Borrowed("apco"));
pub fn from_identifier(
identifier: impl Into<String>,
) -> Result<Self, InvalidRecordingIdentifier> {
let identifier = identifier.into();
if identifier.as_bytes().contains(&0) {
Err(InvalidRecordingIdentifier)
} else {
Ok(Self(Cow::Owned(identifier)))
}
}
#[must_use]
pub fn identifier(&self) -> &str {
self.0.as_ref()
}
}
impl Default for SCRecordingOutputCodec {
fn default() -> Self {
Self::H264
}
}
impl std::fmt::Display for SCRecordingOutputCodec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.identifier() {
"avc1" => f.write_str("H.264"),
"hvc1" => f.write_str("HEVC"),
"jpeg" => f.write_str("JPEG"),
"apcn" => f.write_str("ProRes 422"),
"ap4h" => f.write_str("ProRes 4444"),
"muxa" => f.write_str("HEVC with alpha"),
"apch" => f.write_str("ProRes 422 HQ"),
"apcs" => f.write_str("ProRes 422 LT"),
"apco" => f.write_str("ProRes 422 Proxy"),
other => write!(f, "codec {other}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SCRecordingOutputFileType(Cow<'static, str>);
impl SCRecordingOutputFileType {
pub const MP4: Self = Self(Cow::Borrowed("public.mpeg-4"));
pub const MOV: Self = Self(Cow::Borrowed("com.apple.quicktime-movie"));
pub const M4V: Self = Self(Cow::Borrowed("com.apple.m4v-video"));
pub const M4A: Self = Self(Cow::Borrowed("com.apple.m4a-audio"));
pub const MOBILE_3GPP: Self = Self(Cow::Borrowed("public.3gpp"));
pub fn from_identifier(
identifier: impl Into<String>,
) -> Result<Self, InvalidRecordingIdentifier> {
let identifier = identifier.into();
if identifier.as_bytes().contains(&0) {
Err(InvalidRecordingIdentifier)
} else {
Ok(Self(Cow::Owned(identifier)))
}
}
#[must_use]
pub fn identifier(&self) -> &str {
self.0.as_ref()
}
#[must_use]
pub fn extension(&self) -> Option<&'static str> {
match self.identifier() {
"public.mpeg-4" => Some("mp4"),
"com.apple.quicktime-movie" => Some("mov"),
"com.apple.m4v-video" => Some("m4v"),
"com.apple.m4a-audio" => Some("m4a"),
"public.3gpp" => Some("3gp"),
_ => None,
}
}
}
impl Default for SCRecordingOutputFileType {
fn default() -> Self {
Self::MP4
}
}
impl std::fmt::Display for SCRecordingOutputFileType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.identifier() {
"public.mpeg-4" => f.write_str("MP4"),
"com.apple.quicktime-movie" => f.write_str("MOV"),
"com.apple.m4v-video" => f.write_str("M4V"),
"com.apple.m4a-audio" => f.write_str("M4A"),
"public.3gpp" => f.write_str("3GPP"),
other => write!(f, "file type {other}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct InvalidRecordingIdentifier;
impl std::fmt::Display for InvalidRecordingIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("recording identifier contains an interior NUL byte")
}
}
impl std::error::Error for InvalidRecordingIdentifier {}
pub struct SCRecordingOutputConfiguration {
ptr: *const c_void,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InvalidOutputPath {
NotUtf8,
InteriorNul,
}
impl std::fmt::Display for InvalidOutputPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotUtf8 => f.write_str("output path is not valid UTF-8"),
Self::InteriorNul => f.write_str("output path contains an interior NUL byte"),
}
}
}
impl std::error::Error for InvalidOutputPath {}
impl SCRecordingOutputConfiguration {
#[must_use]
pub fn new() -> Self {
Self::try_new().expect("SCRecordingOutput requires macOS 15.0 or later")
}
#[must_use]
pub fn try_new() -> Option<Self> {
if !SCRecordingOutput::is_available() {
return None;
}
let ptr = unsafe { crate::ffi::sc_recording_output_configuration_create() };
(!ptr.is_null()).then_some(Self { ptr })
}
#[must_use]
pub fn with_output_url(self, path: &Path) -> Self {
match self.try_with_output_url(path) {
Ok(config) => config,
Err((config, error)) => {
eprintln!("SCRecordingOutputConfiguration: {error}; output URL was not changed");
config
}
}
}
pub fn try_with_output_url(self, path: &Path) -> Result<Self, (Self, InvalidOutputPath)> {
let Some(path) = path.to_str() else {
return Err((self, InvalidOutputPath::NotUtf8));
};
let Ok(c_path) = std::ffi::CString::new(path) else {
return Err((self, InvalidOutputPath::InteriorNul));
};
unsafe {
crate::ffi::sc_recording_output_configuration_set_output_url(self.ptr, c_path.as_ptr());
}
Ok(self)
}
pub fn output_url(&self) -> Option<PathBuf> {
use std::os::unix::ffi::OsStringExt;
unsafe {
let path =
crate::ffi::sc_recording_output_configuration_get_output_path_owned(self.ptr);
if path.is_null() {
return None;
}
let bytes = std::ffi::CStr::from_ptr(path).to_bytes().to_vec();
crate::ffi::sc_free_string(path);
Some(PathBuf::from(std::ffi::OsString::from_vec(bytes)))
}
}
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn with_video_codec(self, codec: SCRecordingOutputCodec) -> Self {
let codec = unsafe {
std::ffi::CString::from_vec_unchecked(codec.identifier().as_bytes().to_vec())
};
unsafe {
crate::ffi::sc_recording_output_configuration_set_video_codec_identifier(
self.ptr,
codec.as_ptr(),
);
}
self
}
pub fn video_codec(&self) -> SCRecordingOutputCodec {
let identifier = unsafe {
crate::utils::ffi_string::ffi_string_owned(|| {
crate::ffi::sc_recording_output_configuration_get_video_codec_identifier_owned(
self.ptr,
)
})
}
.unwrap_or_else(|| SCRecordingOutputCodec::H264.identifier().to_string());
SCRecordingOutputCodec(Cow::Owned(identifier))
}
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn with_output_file_type(self, file_type: SCRecordingOutputFileType) -> Self {
let file_type = unsafe {
std::ffi::CString::from_vec_unchecked(file_type.identifier().as_bytes().to_vec())
};
unsafe {
crate::ffi::sc_recording_output_configuration_set_output_file_type_identifier(
self.ptr,
file_type.as_ptr(),
);
}
self
}
pub fn output_file_type(&self) -> SCRecordingOutputFileType {
let identifier = unsafe {
crate::utils::ffi_string::ffi_string_owned(|| {
crate::ffi::sc_recording_output_configuration_get_output_file_type_identifier_owned(
self.ptr,
)
})
}
.unwrap_or_else(|| SCRecordingOutputFileType::MP4.identifier().to_string());
SCRecordingOutputFileType(Cow::Owned(identifier))
}
pub fn available_video_codecs_count(&self) -> usize {
let count = unsafe {
crate::ffi::sc_recording_output_configuration_get_available_video_codecs_count(self.ptr)
};
usize::try_from(count).unwrap_or(0)
}
pub fn available_video_codecs(&self) -> Vec<SCRecordingOutputCodec> {
let count = self.available_video_codecs_count();
let mut codecs = Vec::with_capacity(count);
for i in 0..count {
let Ok(index) = isize::try_from(i) else { break };
let identifier = unsafe {
crate::utils::ffi_string::ffi_string_owned(|| {
crate::ffi::sc_recording_output_configuration_get_available_video_codec_identifier_at_owned(
self.ptr,
index,
)
})
};
if let Some(identifier) = identifier {
codecs.push(SCRecordingOutputCodec(Cow::Owned(identifier)));
}
}
codecs
}
pub fn available_output_file_types_count(&self) -> usize {
let count = unsafe {
crate::ffi::sc_recording_output_configuration_get_available_output_file_types_count(
self.ptr,
)
};
usize::try_from(count).unwrap_or(0)
}
pub fn available_output_file_types(&self) -> Vec<SCRecordingOutputFileType> {
let count = self.available_output_file_types_count();
let mut file_types = Vec::with_capacity(count);
for i in 0..count {
let Ok(index) = isize::try_from(i) else { break };
let identifier = unsafe {
crate::utils::ffi_string::ffi_string_owned(|| {
crate::ffi::sc_recording_output_configuration_get_available_output_file_type_identifier_at_owned(
self.ptr,
index,
)
})
};
if let Some(identifier) = identifier {
file_types.push(SCRecordingOutputFileType(Cow::Owned(identifier)));
}
}
file_types
}
#[must_use]
pub fn as_ptr(&self) -> *const c_void {
self.ptr
}
}
impl Default for SCRecordingOutputConfiguration {
fn default() -> Self {
Self::new()
}
}
crate::utils::retained::sc_retained!(
SCRecordingOutputConfiguration,
field = ptr,
release = crate::ffi::sc_recording_output_configuration_release,
);
impl Clone for SCRecordingOutputConfiguration {
fn clone(&self) -> Self {
Self {
ptr: unsafe { crate::ffi::sc_recording_output_configuration_copy(self.ptr) },
}
}
}
impl std::fmt::Debug for SCRecordingOutputConfiguration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SCRecordingOutputConfiguration")
.field("video_codec", &format_args!("{}", self.video_codec()))
.field("file_type", &format_args!("{}", self.output_file_type()))
.finish()
}
}
pub trait SCRecordingOutputDelegate: Send + Sync + 'static {
fn recording_did_start(&self) {}
fn recording_did_fail(&self, _error: String) {}
fn recording_did_finish(&self) {}
}
#[allow(clippy::struct_field_names)]
pub struct RecordingCallbacks {
on_start: Option<Box<dyn Fn() + Send + Sync + 'static>>,
on_fail: Option<Box<dyn Fn(String) + Send + Sync + 'static>>,
on_finish: Option<Box<dyn Fn() + Send + Sync + 'static>>,
}
impl RecordingCallbacks {
#[must_use]
pub fn new() -> Self {
Self {
on_start: None,
on_fail: None,
on_finish: None,
}
}
#[must_use]
pub fn on_start<F>(mut self, f: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_start = Some(Box::new(f));
self
}
#[must_use]
pub fn on_fail<F>(mut self, f: F) -> Self
where
F: Fn(String) + Send + Sync + 'static,
{
self.on_fail = Some(Box::new(f));
self
}
#[must_use]
pub fn on_finish<F>(mut self, f: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_finish = Some(Box::new(f));
self
}
}
impl Default for RecordingCallbacks {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for RecordingCallbacks {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RecordingCallbacks")
.field("on_start", &self.on_start.is_some())
.field("on_fail", &self.on_fail.is_some())
.field("on_finish", &self.on_finish.is_some())
.finish()
}
}
impl SCRecordingOutputDelegate for RecordingCallbacks {
fn recording_did_start(&self) {
if let Some(ref f) = self.on_start {
f();
}
}
fn recording_did_fail(&self, error: String) {
if let Some(ref f) = self.on_fail {
f(error);
}
}
fn recording_did_finish(&self) {
if let Some(ref f) = self.on_finish {
f();
}
}
}
pub struct SCRecordingOutput {
ptr: *const c_void,
delegate_id: Option<usize>,
}
extern "C" fn recording_started_callback(ctx: *mut c_void) {
crate::utils::panic_safe::catch_user_panic(
"SCRecordingOutputDelegate::recording_did_start",
|| {
if let Some(delegate) = lookup_delegate(ctx as usize) {
delegate.recording_did_start();
}
},
);
}
extern "C" fn recording_failed_callback(ctx: *mut c_void, error_code: i32, error: *const i8) {
crate::utils::panic_safe::catch_user_panic(
"SCRecordingOutputDelegate::recording_did_fail",
|| {
let error_str = if error.is_null() {
String::from("Unknown error")
} else {
unsafe { std::ffi::CStr::from_ptr(error) }
.to_string_lossy()
.into_owned()
};
let full_error = if error_code == 0 {
error_str
} else {
crate::error::SCStreamErrorCode::from_raw(error_code).map_or_else(
|| format!("{error_str} (code: {error_code})"),
|code| format!("{error_str} ({code})"),
)
};
if let Some(delegate) = lookup_delegate(ctx as usize) {
delegate.recording_did_fail(full_error);
}
},
);
}
extern "C" fn recording_finished_callback(ctx: *mut c_void) {
crate::utils::panic_safe::catch_user_panic(
"SCRecordingOutputDelegate::recording_did_finish",
|| {
if let Some(delegate) = lookup_delegate(ctx as usize) {
delegate.recording_did_finish();
}
},
);
}
extern "C" fn recording_context_release_callback(ctx: *mut c_void) {
crate::utils::panic_safe::catch_user_panic("SCRecordingOutputDelegate::release", || {
drop(remove_delegate(ctx as usize));
});
}
impl SCRecordingOutput {
#[must_use]
pub fn is_available() -> bool {
unsafe { crate::ffi::sc_recording_output_is_available() }
}
pub fn new(config: &SCRecordingOutputConfiguration) -> Option<Self> {
if !Self::is_available() {
return None;
}
let ptr = unsafe { crate::ffi::sc_recording_output_create(config.as_ptr()) };
if ptr.is_null() {
None
} else {
Some(Self {
ptr,
delegate_id: None,
})
}
}
pub fn new_with_delegate<D: SCRecordingOutputDelegate>(
config: &SCRecordingOutputConfiguration,
delegate: D,
) -> Option<Self> {
if !Self::is_available() {
return None;
}
let entry = RecordingDelegateEntry {
delegate: Arc::new(delegate),
};
let delegate_id = {
let mut registry = RECORDING_DELEGATE_REGISTRY
.lock()
.unwrap_or_else(PoisonError::into_inner);
let delegates = registry.get_or_insert_with(HashMap::new);
loop {
let id = NEXT_DELEGATE_ID.fetch_add(1, Ordering::Relaxed);
if id != 0 && !delegates.contains_key(&id) {
delegates.insert(id, entry);
drop(registry);
break id;
}
}
};
let ctx = delegate_id as *mut c_void;
let ptr = unsafe {
crate::ffi::sc_recording_output_create_with_delegate(
config.as_ptr(),
Some(recording_started_callback),
Some(recording_failed_callback),
Some(recording_finished_callback),
Some(recording_context_release_callback),
ctx,
)
};
if ptr.is_null() {
drop(remove_delegate(delegate_id));
None
} else {
Some(Self {
ptr,
delegate_id: Some(delegate_id),
})
}
}
pub fn recorded_duration(&self) -> CMTime {
let mut value: i64 = 0;
let mut timescale: i32 = 0;
unsafe {
crate::ffi::sc_recording_output_get_recorded_duration(
self.ptr,
&mut value,
&mut timescale,
);
}
CMTime::new(value, timescale)
}
pub fn recorded_file_size(&self) -> i64 {
unsafe { crate::ffi::sc_recording_output_get_recorded_file_size(self.ptr) }
}
#[must_use]
pub fn as_ptr(&self) -> *const c_void {
self.ptr
}
}
impl Clone for SCRecordingOutput {
fn clone(&self) -> Self {
unsafe {
Self {
ptr: crate::ffi::sc_recording_output_retain(self.ptr),
delegate_id: self.delegate_id,
}
}
}
}
impl std::fmt::Debug for SCRecordingOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SCRecordingOutput")
.field("recorded_duration", &self.recorded_duration())
.field("recorded_file_size", &self.recorded_file_size())
.field("has_delegate", &self.delegate_id.is_some())
.finish_non_exhaustive()
}
}
impl Drop for SCRecordingOutput {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe {
crate::ffi::sc_recording_output_release(self.ptr);
}
}
}
}
unsafe impl Send for SCRecordingOutput {}
unsafe impl Sync for SCRecordingOutput {}
unsafe impl Send for SCRecordingOutputConfiguration {}
unsafe impl Sync for SCRecordingOutputConfiguration {}