use core::ffi::{c_char, c_void};
use core::ptr;
use apple_cf::cf::{CFDictionary, CFNumber, CFString};
use crate::ffi;
use crate::session::Codec;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VideoEncoder {
pub codec_type: u32,
pub encoder_id: String,
pub codec_name: String,
pub encoder_name: String,
pub display_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VideoEncoderDetails {
pub base: VideoEncoder,
pub gpu_registry_id: Option<u64>,
pub supported_selection_properties: Option<CFDictionary>,
pub performance_rating: Option<i64>,
pub quality_rating: Option<i64>,
pub instance_limit: Option<bool>,
pub is_hardware_accelerated: Option<bool>,
pub supports_frame_reordering: Option<bool>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct VideoEncoderListOptions {
include_standard_definition_dv_encoders: bool,
}
impl VideoEncoderListOptions {
#[must_use]
pub const fn new() -> Self {
Self {
include_standard_definition_dv_encoders: false,
}
}
#[must_use]
pub const fn with_include_standard_definition_dv_encoders(mut self, include: bool) -> Self {
self.include_standard_definition_dv_encoders = include;
self
}
fn to_cf_dictionary(self) -> Option<CFDictionary> {
if !self.include_standard_definition_dv_encoders {
return None;
}
Some(dictionary_with_boolean(
unsafe { ffi::kVTVideoEncoderListOption_IncludeStandardDefinitionDVEncoders },
true,
))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncoderSupportedProperties {
pub encoder_id: Option<String>,
pub supported_properties: Option<CFDictionary>,
}
pub fn available_video_encoders() -> Result<Vec<VideoEncoder>, i32> {
available_video_encoder_details().map(|encoders| {
encoders
.into_iter()
.map(|details| details.base)
.collect::<Vec<_>>()
})
}
pub fn available_video_encoder_details() -> Result<Vec<VideoEncoderDetails>, i32> {
available_video_encoder_details_with_options(&VideoEncoderListOptions::default())
}
pub fn available_video_encoder_details_with_options(
options: &VideoEncoderListOptions,
) -> Result<Vec<VideoEncoderDetails>, i32> {
let options_dict = options.to_cf_dictionary();
let mut arr: ffi::CFArrayRef = ptr::null();
let status = unsafe {
ffi::VTCopyVideoEncoderList(
options_dict
.as_ref()
.map_or(ptr::null(), |dict| dict.as_ptr().cast_const().cast()),
&mut arr,
)
};
if status != 0 {
return Err(status);
}
if arr.is_null() {
return Ok(Vec::new());
}
let count = unsafe { ffi::CFArrayGetCount(arr) };
let mut encoders = Vec::with_capacity(usize::try_from(count).unwrap_or(0));
for index in 0..count {
let dict: ffi::CFDictionaryRef = unsafe { ffi::CFArrayGetValueAtIndex(arr, index) }.cast();
if !dict.is_null() {
encoders.push(unsafe { parse_video_encoder_details(dict) });
}
}
unsafe { ffi::CFRelease(arr.cast()) };
Ok(encoders)
}
pub fn supported_property_dictionary_for_encoder(
width: i32,
height: i32,
codec: Codec,
encoder_id: Option<&str>,
) -> Result<EncoderSupportedProperties, i32> {
let encoder_specification = encoder_id.map(dictionary_with_encoder_id);
let mut encoder_id_out: ffi::CFStringRef = ptr::null();
let mut supported_properties_out: ffi::CFDictionaryRef = ptr::null();
let status = unsafe {
ffi::VTCopySupportedPropertyDictionaryForEncoder(
width,
height,
codec.as_cm_codec_type(),
encoder_specification
.as_ref()
.map_or(ptr::null(), |dict| dict.as_ptr().cast_const().cast()),
&mut encoder_id_out,
&mut supported_properties_out,
)
};
if status != 0 {
return Err(status);
}
let encoder_id =
CFString::from_raw(encoder_id_out.cast_mut().cast()).map(|string| string.to_string_lossy());
let supported_properties = CFDictionary::from_raw(supported_properties_out.cast_mut().cast());
Ok(EncoderSupportedProperties {
encoder_id,
supported_properties,
})
}
unsafe fn parse_video_encoder_details(dict: ffi::CFDictionaryRef) -> VideoEncoderDetails {
let codec_type = unsafe {
let number = ffi::CFDictionaryGetValue(dict, ffi::kVTVideoEncoderList_CodecType.cast());
cf_number_u32(number).unwrap_or(0)
};
let encoder_id = unsafe {
let string = ffi::CFDictionaryGetValue(dict, ffi::kVTVideoEncoderList_EncoderID.cast());
cf_string_to_rust(string.cast())
};
let codec_name = unsafe {
let string = ffi::CFDictionaryGetValue(dict, ffi::kVTVideoEncoderList_CodecName.cast());
cf_string_to_rust(string.cast())
};
let encoder_name = unsafe {
let string = ffi::CFDictionaryGetValue(dict, ffi::kVTVideoEncoderList_EncoderName.cast());
cf_string_to_rust(string.cast())
};
let display_name = unsafe {
let string = ffi::CFDictionaryGetValue(dict, ffi::kVTVideoEncoderList_DisplayName.cast());
cf_string_to_rust(string.cast())
};
VideoEncoderDetails {
base: VideoEncoder {
codec_type,
encoder_id,
codec_name,
encoder_name,
display_name,
},
gpu_registry_id: unsafe {
let value =
ffi::CFDictionaryGetValue(dict, ffi::kVTVideoEncoderList_GPURegistryID.cast());
cf_number_u64(value)
},
supported_selection_properties: unsafe {
let value = ffi::CFDictionaryGetValue(
dict,
ffi::kVTVideoEncoderList_SupportedSelectionProperties.cast(),
);
cf_dictionary(value)
},
performance_rating: unsafe {
let value =
ffi::CFDictionaryGetValue(dict, ffi::kVTVideoEncoderList_PerformanceRating.cast());
cf_number_i64(value)
},
quality_rating: unsafe {
let value =
ffi::CFDictionaryGetValue(dict, ffi::kVTVideoEncoderList_QualityRating.cast());
cf_number_i64(value)
},
instance_limit: unsafe {
let value =
ffi::CFDictionaryGetValue(dict, ffi::kVTVideoEncoderList_InstanceLimit.cast());
cf_bool(value)
},
is_hardware_accelerated: unsafe {
let value = ffi::CFDictionaryGetValue(
dict,
ffi::kVTVideoEncoderList_IsHardwareAccelerated.cast(),
);
cf_bool(value)
},
supports_frame_reordering: unsafe {
let value = ffi::CFDictionaryGetValue(
dict,
ffi::kVTVideoEncoderList_SupportsFrameReordering.cast(),
);
cf_bool(value)
},
}
}
fn dictionary_with_boolean(key: ffi::CFStringRef, value: bool) -> CFDictionary {
let dict = unsafe {
ffi::CFDictionaryCreateMutable(
ffi::kCFAllocatorDefault,
1,
(&raw const ffi::kCFTypeDictionaryKeyCallBacks).cast(),
(&raw const ffi::kCFTypeDictionaryValueCallBacks).cast(),
)
};
unsafe {
ffi::CFDictionarySetValue(
dict,
key.cast(),
if value {
ffi::kCFBooleanTrue.cast()
} else {
ffi::kCFBooleanFalse.cast()
},
);
}
CFDictionary::from_raw(dict.cast()).expect("CFDictionaryCreateMutable returned NULL")
}
fn dictionary_with_encoder_id(encoder_id: &str) -> CFDictionary {
let dict = unsafe {
ffi::CFDictionaryCreateMutable(
ffi::kCFAllocatorDefault,
1,
(&raw const ffi::kCFTypeDictionaryKeyCallBacks).cast(),
(&raw const ffi::kCFTypeDictionaryValueCallBacks).cast(),
)
};
let encoder_id = CFString::new(encoder_id);
unsafe {
ffi::CFDictionarySetValue(
dict,
ffi::kVTVideoEncoderSpecification_EncoderID.cast(),
encoder_id.as_ptr().cast(),
);
}
CFDictionary::from_raw(dict.cast()).expect("CFDictionaryCreateMutable returned NULL")
}
#[allow(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap
)]
unsafe fn cf_string_to_rust(s: ffi::CFStringRef) -> String {
if s.is_null() {
return String::new();
}
let len = unsafe { ffi::CFStringGetLength(s) };
let buf_len = usize::try_from(len * 4 + 1).unwrap_or(0);
let mut buf = vec![0u8; buf_len];
let ok = unsafe {
ffi::CFStringGetCString(
s,
buf.as_mut_ptr().cast::<c_char>(),
buf_len as isize,
0x0800_0100,
)
};
if !ok {
return String::new();
}
if let Some(end) = buf.iter().position(|&b| b == 0) {
buf.truncate(end);
}
String::from_utf8_lossy(&buf).into_owned()
}
#[allow(clippy::cast_sign_loss)]
unsafe fn cf_number_u32(n: *const c_void) -> Option<u32> {
if n.is_null() {
return None;
}
let mut v: i32 = 0;
let ok = unsafe {
ffi::CFNumberGetValue(
n.cast(),
ffi::kCFNumberSInt32Type,
(&raw mut v).cast::<c_void>(),
)
};
ok.then_some(v as u32)
}
unsafe fn cf_number_i64(n: *const c_void) -> Option<i64> {
unsafe { CFNumber::from_raw_retained(n.cast_mut()) }.and_then(|number| number.to_i64())
}
unsafe fn cf_number_u64(n: *const c_void) -> Option<u64> {
unsafe { CFNumber::from_raw_retained(n.cast_mut()) }.and_then(|number| number.to_u64())
}
unsafe fn cf_dictionary(value: *const c_void) -> Option<CFDictionary> {
unsafe { CFDictionary::from_raw_retained(value.cast_mut()) }
}
unsafe fn cf_bool(value: *const c_void) -> Option<bool> {
if value.is_null() {
None
} else if value == unsafe { ffi::kCFBooleanTrue.cast() } {
Some(true)
} else if value == unsafe { ffi::kCFBooleanFalse.cast() } {
Some(false)
} else {
None
}
}