use std::collections::VecDeque;
use std::ffi::c_void;
use std::ptr::NonNull;
use std::sync::{Arc, Mutex, OnceLock};
use crate::{EncodeError, VideoEncoder, VideoEncoderConfig, VideoInputPreference};
use mediaway_common::{
Bytes, CodecKind, ColorRange, GpuBufferHandle, Packet, PixelFormat, StreamInfo, VideoFrame,
VideoFrameStorage, VideoGeometry,
};
use objc2_core_foundation::{
CFArray, CFBoolean, CFDictionary, CFNumber, CFNumberType, CFRetained, CFString, CFType,
kCFBooleanFalse, kCFBooleanTrue,
};
use objc2_core_media::{CMSampleBuffer, CMTime, kCMSampleAttachmentKey_NotSync, kCMTimeIndefinite};
use objc2_core_video::{
CVPixelBuffer, CVPixelBufferCreateWithPlanarBytes, CVPixelBufferGetHeight,
CVPixelBufferGetWidth, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
};
use objc2_video_toolbox::{
VTCompressionSession, VTEncodeInfoFlags, VTSessionSetProperty,
kVTCompressionPropertyKey_AllowFrameReordering, kVTCompressionPropertyKey_AverageBitRate,
kVTCompressionPropertyKey_ExpectedFrameRate, kVTCompressionPropertyKey_MaxKeyFrameInterval,
kVTCompressionPropertyKey_ProfileLevel, kVTCompressionPropertyKey_RealTime,
kVTProfileLevel_H264_ConstrainedBaseline_AutoLevel, kVTProfileLevel_HEVC_Main_AutoLevel,
};
use super::codec::codec_type;
use super::extradata;
const NO_ERROR: i32 = 0;
struct SharedState {
pending: Mutex<VecDeque<Packet>>,
finalized_info: OnceLock<StreamInfo>,
base_info: StreamInfo,
time_base_den: u32,
codec: CodecKind,
}
pub(crate) struct VideoToolboxVideoEncoder {
session: CFRetained<VTCompressionSession>,
shared: Arc<SharedState>,
refcon_ptr: *const SharedState,
input: VideoInputPreference,
width: u32,
height: u32,
yuv420_bytes: usize,
color_range: ColorRange,
flushed: bool,
}
#[allow(
clippy::non_send_fields_in_send_ty,
reason = "CFRetained<VTCompressionSession> is a Core Foundation object — see the SAFETY comment above"
)]
unsafe impl Send for VideoToolboxVideoEncoder {}
impl VideoToolboxVideoEncoder {
pub(crate) fn open(config: &VideoEncoderConfig) -> Result<Self, EncodeError> {
validate(config)?;
match config.input {
VideoInputPreference::CpuUploadOk | VideoInputPreference::ZeroCopyGpu => {
Self::open_session(config)
}
}
}
fn open_session(config: &VideoEncoderConfig) -> Result<Self, EncodeError> {
let codec = codec_type(config.codec)?;
let width = i32::try_from(config.width).map_err(|_| EncodeError::InvalidInput)?;
let height = i32::try_from(config.height).map_err(|_| EncodeError::InvalidInput)?;
let shared = Arc::new(SharedState {
pending: Mutex::new(VecDeque::new()),
finalized_info: OnceLock::new(),
base_info: stream_info_from(config),
time_base_den: config.time_base.den,
codec: config.codec,
});
let refcon_ptr = Arc::into_raw(Arc::clone(&shared));
let mut session_ptr: *mut VTCompressionSession = std::ptr::null_mut();
let status = unsafe {
VTCompressionSession::create(
None,
width,
height,
codec,
None,
None,
None,
Some(compression_output_callback),
refcon_ptr.cast::<c_void>().cast_mut(),
NonNull::from(&mut session_ptr),
)
};
if status != NO_ERROR {
drop(unsafe { Arc::from_raw(refcon_ptr) });
return Err(EncodeError::Backend);
}
let Some(session_ptr) = NonNull::new(session_ptr) else {
drop(unsafe { Arc::from_raw(refcon_ptr) });
return Err(EncodeError::Backend);
};
let session = unsafe { CFRetained::from_raw(session_ptr) };
if let Err(e) = configure_properties(&session, config) {
unsafe { session.invalidate() };
drop(unsafe { Arc::from_raw(refcon_ptr) });
return Err(e);
}
let yuv420_bytes = yuv420_size(config.width, config.height)?;
Ok(Self {
session,
shared,
refcon_ptr,
input: config.input,
width: config.width,
height: config.height,
yuv420_bytes,
color_range: config.color_range,
flushed: false,
})
}
}
enum PixelBufferRef<'a> {
Owned(CFRetained<CVPixelBuffer>),
Borrowed(&'a CVPixelBuffer),
}
impl AsRef<CVPixelBuffer> for PixelBufferRef<'_> {
fn as_ref(&self) -> &CVPixelBuffer {
match self {
Self::Owned(buffer) => buffer,
Self::Borrowed(buffer) => buffer,
}
}
}
impl VideoEncoder for VideoToolboxVideoEncoder {
fn stream_info(&self) -> &StreamInfo {
self.shared
.finalized_info
.get()
.unwrap_or(&self.shared.base_info)
}
fn push_frame(&mut self, frame: &VideoFrame) -> Result<(), EncodeError> {
if self.flushed {
return Err(EncodeError::Closed);
}
if frame.width != self.width || frame.height != self.height {
return Err(EncodeError::InvalidInput);
}
let pixel_buffer = match &frame.storage {
VideoFrameStorage::Cpu { data } => {
if self.input != VideoInputPreference::CpuUploadOk {
return Err(EncodeError::InvalidInput);
}
if data.len() < self.yuv420_bytes {
return Err(EncodeError::InvalidInput);
}
PixelBufferRef::Owned(upload_cpu_nv12(
data,
self.width,
self.height,
self.color_range,
)?)
}
VideoFrameStorage::Gpu(GpuBufferHandle::Metal { buffer }) => {
if self.input != VideoInputPreference::ZeroCopyGpu {
return Err(EncodeError::InvalidInput);
}
let ptr = NonNull::new(buffer.get() as *mut CVPixelBuffer)
.ok_or(EncodeError::InvalidInput)?;
let pixel_buffer = unsafe { ptr.as_ref() };
if CVPixelBufferGetWidth(pixel_buffer) != self.width as usize
|| CVPixelBufferGetHeight(pixel_buffer) != self.height as usize
{
return Err(EncodeError::InvalidInput);
}
PixelBufferRef::Borrowed(pixel_buffer)
}
VideoFrameStorage::Gpu(_) | &_ => return Err(EncodeError::Unsupported),
};
let pts = cmtime_from_pts(frame.pts, self.shared.time_base_den);
let status = unsafe {
self.session.encode_frame(
pixel_buffer.as_ref(),
pts,
kCMTimeIndefinite,
None,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
if status != NO_ERROR {
return Err(EncodeError::Backend);
}
Ok(())
}
fn poll_packet(&mut self) -> Result<Option<Packet>, EncodeError> {
let mut pending = self
.shared
.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Ok(pending.pop_front())
}
fn flush(&mut self) -> Result<(), EncodeError> {
if self.flushed {
return Ok(());
}
let status = unsafe { self.session.complete_frames(kCMTimeIndefinite) };
self.flushed = true;
if status != NO_ERROR {
return Err(EncodeError::Backend);
}
Ok(())
}
}
impl Drop for VideoToolboxVideoEncoder {
fn drop(&mut self) {
if !self.flushed {
let _ = unsafe { self.session.complete_frames(kCMTimeIndefinite) };
}
unsafe { self.session.invalidate() };
drop(unsafe { Arc::from_raw(self.refcon_ptr) });
}
}
unsafe extern "C-unwind" fn compression_output_callback(
output_callback_ref_con: *mut c_void,
_source_frame_ref_con: *mut c_void,
status: i32,
_info_flags: VTEncodeInfoFlags,
sample_buffer: *mut CMSampleBuffer,
) {
if status != NO_ERROR || sample_buffer.is_null() {
return;
}
let shared = unsafe { &*(output_callback_ref_con.cast::<SharedState>()) };
let sample_buffer = unsafe { &*sample_buffer };
handle_output(shared, sample_buffer);
}
fn handle_output(shared: &SharedState, sample_buffer: &CMSampleBuffer) {
if shared.finalized_info.get().is_none() {
if super::codec::is_prores(shared.codec) {
let _ = shared.finalized_info.set(shared.base_info.clone());
} else {
let extracted = unsafe { sample_buffer.format_description() }.and_then(|format_desc| {
match shared.codec {
CodecKind::Hevc => extradata::extract_hevc(&format_desc),
CodecKind::H264 => extradata::extract_h264(&format_desc),
_ => None,
}
});
if let Some(extra_data) = extracted {
let mut info = shared.base_info.clone();
if let StreamInfo::Video { extra_data: ed, .. } = &mut info {
*ed = extra_data;
}
let _ = shared.finalized_info.set(info);
}
}
}
let Some(block_buffer) = (unsafe { sample_buffer.data_buffer() }) else {
return;
};
let len = unsafe { block_buffer.data_length() };
if len == 0 {
return;
}
let mut payload = vec![0u8; len];
let Some(dest) = NonNull::new(payload.as_mut_ptr().cast::<c_void>()) else {
return;
};
let status = unsafe { block_buffer.copy_data_bytes(0, len, dest) };
if status != NO_ERROR {
return;
}
let pts_cmtime = unsafe { sample_buffer.presentation_time_stamp() };
let pts = cmtime_to_pts(pts_cmtime, shared.time_base_den);
let is_keyframe = is_sync_sample(sample_buffer);
let mut pending = shared
.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
pending.push_back(Packet {
stream_id: 0,
pts,
dts: pts,
duration: 0,
is_keyframe,
is_discard: false,
payload: Bytes::from(payload),
});
}
fn is_sync_sample(sample_buffer: &CMSampleBuffer) -> bool {
let Some(attachments) = (unsafe { sample_buffer.sample_attachments_array(false) }) else {
return true;
};
let attachments: &CFArray<CFDictionary<CFString, CFType>> =
unsafe { attachments.cast_unchecked() };
let Some(dict) = attachments.get(0) else {
return true;
};
let key = unsafe { kCMSampleAttachmentKey_NotSync };
let Some(not_sync) = dict.get(key) else {
return true;
};
!not_sync
.downcast_ref::<CFBoolean>()
.is_some_and(CFBoolean::as_bool)
}
fn upload_cpu_nv12(
data: &[u8],
width: u32,
height: u32,
color_range: ColorRange,
) -> Result<CFRetained<CVPixelBuffer>, EncodeError> {
let owned: Box<Vec<u8>> = Box::new(data.to_vec());
let y_len = (width as usize) * (height as usize);
let base_ptr = owned.as_ptr();
let y_plane = base_ptr.cast_mut().cast::<c_void>();
let uv_plane = unsafe { base_ptr.add(y_len) }.cast_mut().cast::<c_void>();
let mut plane_base_address = [y_plane, uv_plane];
let mut plane_width = [width as usize, (width / 2) as usize];
let mut plane_height = [height as usize, (height / 2) as usize];
let mut plane_bytes_per_row = [width as usize, width as usize];
let pixel_format_type = match color_range {
ColorRange::Video => kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
ColorRange::Full => kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
_ => return Err(EncodeError::Unsupported),
};
let release_ref_con = Box::into_raw(owned).cast::<c_void>();
let mut pixel_buffer_ptr: *mut CVPixelBuffer = std::ptr::null_mut();
let Some(plane_base_address_ptr) = NonNull::new(plane_base_address.as_mut_ptr()) else {
drop(unsafe { Box::from_raw(release_ref_con.cast::<Vec<u8>>()) });
return Err(EncodeError::Backend);
};
let Some(plane_width_ptr) = NonNull::new(plane_width.as_mut_ptr()) else {
drop(unsafe { Box::from_raw(release_ref_con.cast::<Vec<u8>>()) });
return Err(EncodeError::Backend);
};
let Some(plane_height_ptr) = NonNull::new(plane_height.as_mut_ptr()) else {
drop(unsafe { Box::from_raw(release_ref_con.cast::<Vec<u8>>()) });
return Err(EncodeError::Backend);
};
let Some(plane_bytes_per_row_ptr) = NonNull::new(plane_bytes_per_row.as_mut_ptr()) else {
drop(unsafe { Box::from_raw(release_ref_con.cast::<Vec<u8>>()) });
return Err(EncodeError::Backend);
};
let cv_return = unsafe {
CVPixelBufferCreateWithPlanarBytes(
None,
width as usize,
height as usize,
pixel_format_type,
std::ptr::null_mut(),
0,
2,
plane_base_address_ptr,
plane_width_ptr,
plane_height_ptr,
plane_bytes_per_row_ptr,
Some(release_planar_bytes),
release_ref_con,
None,
NonNull::from(&mut pixel_buffer_ptr),
)
};
if let (NO_ERROR, Some(pixel_buffer_ptr)) = (cv_return, NonNull::new(pixel_buffer_ptr)) {
Ok(unsafe { CFRetained::from_raw(pixel_buffer_ptr) })
} else {
drop(unsafe { Box::from_raw(release_ref_con.cast::<Vec<u8>>()) });
Err(EncodeError::Backend)
}
}
unsafe extern "C-unwind" fn release_planar_bytes(
release_ref_con: *mut c_void,
_data_ptr: *const c_void,
_data_size: usize,
_number_of_planes: usize,
_plane_addresses: *mut *const c_void,
) {
drop(unsafe { Box::from_raw(release_ref_con.cast::<Vec<u8>>()) });
}
fn configure_properties(
session: &VTCompressionSession,
config: &VideoEncoderConfig,
) -> Result<(), EncodeError> {
unsafe {
set_bool_property(session, kVTCompressionPropertyKey_RealTime, true)?;
set_bool_property(
session,
kVTCompressionPropertyKey_AllowFrameReordering,
false,
)?;
let frame_rate = frame_rate_hint(config.time_base);
set_i32_property(
session,
kVTCompressionPropertyKey_ExpectedFrameRate,
frame_rate,
)?;
if !super::codec::is_prores(config.codec) {
let profile_level = match config.codec {
CodecKind::Hevc => kVTProfileLevel_HEVC_Main_AutoLevel,
_ => kVTProfileLevel_H264_ConstrainedBaseline_AutoLevel,
};
set_string_property(
session,
kVTCompressionPropertyKey_ProfileLevel,
profile_level,
)?;
let max_key_frame_interval = i32::try_from(config.gop_size.max(1)).unwrap_or(1);
set_i32_property(
session,
kVTCompressionPropertyKey_MaxKeyFrameInterval,
max_key_frame_interval,
)?;
if config.bitrate_bps > 0 {
let bitrate = i32::try_from(config.bitrate_bps).unwrap_or(i32::MAX);
set_i32_property(session, kVTCompressionPropertyKey_AverageBitRate, bitrate)?;
}
}
}
Ok(())
}
unsafe fn set_i32_property(
session: &VTCompressionSession,
key: &CFString,
value: i32,
) -> Result<(), EncodeError> {
let number =
unsafe { CFNumber::new(None, CFNumberType::SInt32Type, (&raw const value).cast()) };
let number = number.ok_or(EncodeError::Backend)?;
let status = unsafe { VTSessionSetProperty(session, key, Some(&number)) };
if status == NO_ERROR {
Ok(())
} else {
Err(EncodeError::Backend)
}
}
unsafe fn set_bool_property(
session: &VTCompressionSession,
key: &CFString,
value: bool,
) -> Result<(), EncodeError> {
let cf_bool = unsafe {
if value {
kCFBooleanTrue
} else {
kCFBooleanFalse
}
}
.map(|b: &CFBoolean| -> &CFType { b });
let status = unsafe { VTSessionSetProperty(session, key, cf_bool) };
if status == NO_ERROR {
Ok(())
} else {
Err(EncodeError::Backend)
}
}
unsafe fn set_string_property(
session: &VTCompressionSession,
key: &CFString,
value: &CFString,
) -> Result<(), EncodeError> {
let status = unsafe { VTSessionSetProperty(session, key, Some(value)) };
if status == NO_ERROR {
Ok(())
} else {
Err(EncodeError::Backend)
}
}
fn frame_rate_hint(time_base: mediaway_common::Rational) -> i32 {
if time_base.num == 0 {
return 30;
}
i32::try_from(u64::from(time_base.den) / time_base.num)
.unwrap_or(30)
.max(1)
}
fn cmtime_from_pts(pts: i64, time_base_den: u32) -> CMTime {
CMTime {
value: pts,
timescale: i32::try_from(time_base_den).unwrap_or(i32::MAX),
flags: objc2_core_media::CMTimeFlags::Valid,
epoch: 0,
}
}
fn cmtime_to_pts(time: CMTime, time_base_den: u32) -> i64 {
if time.timescale == 0 {
return 0;
}
(time.value.saturating_mul(i64::from(time_base_den))) / i64::from(time.timescale)
}
fn validate(config: &VideoEncoderConfig) -> Result<(), EncodeError> {
if !super::codec::is_supported_video_codec(config.codec) {
return Err(EncodeError::Unsupported);
}
if config.width == 0 || config.height == 0 {
return Err(EncodeError::InvalidInput);
}
if config.pixel_format != PixelFormat::Nv12 {
return Err(EncodeError::Unsupported);
}
if config.time_base.den == 0 {
return Err(EncodeError::InvalidInput);
}
Ok(())
}
fn yuv420_size(width: u32, height: u32) -> Result<usize, EncodeError> {
let w = usize::try_from(width).map_err(|_| EncodeError::InvalidInput)?;
let h = usize::try_from(height).map_err(|_| EncodeError::InvalidInput)?;
w.checked_mul(h)
.and_then(|y| y.checked_mul(3))
.and_then(|v| v.checked_div(2))
.ok_or(EncodeError::InvalidInput)
}
#[allow(clippy::missing_const_for_fn, reason = "StreamInfo holds Bytes")]
fn stream_info_from(config: &VideoEncoderConfig) -> StreamInfo {
StreamInfo::Video {
id: 0,
codec: config.codec,
time_base: config.time_base,
geometry: VideoGeometry {
width: config.width,
height: config.height,
},
extra_data: Bytes::new(),
}
}
#[cfg(test)]
#[path = "video_tests.rs"]
mod tests;