#![allow(unsafe_code)]
use std::collections::VecDeque;
use std::ffi::c_void;
use std::ptr::NonNull;
use std::sync::{Arc, Mutex};
use crate::{DecodeError, VideoDecoder, VideoDecoderConfig, VideoOutputPreference};
use mediaway_common::{
Bytes, CodecKind, GpuBufferHandle, NativeHandle, Packet, PixelFormat, Rational, StreamInfo,
VideoFrame, VideoFrameStorage, VideoGeometry,
};
use iso_bmff::bitstream::avc::parse_avc_decoder_config;
use iso_bmff::bitstream::avc::to_avcc;
use iso_bmff::bitstream::hevc::{parse_hevc_decoder_config, to_hvcc};
use objc2_core_foundation::{CFDictionary, CFNumber, CFRetained, CFString, CFType};
use objc2_core_media::{
CMBlockBuffer, CMBlockBufferCustomBlockSource, CMSampleBuffer, CMSampleTimingInfo, CMTime,
CMTimeFlags, CMVideoFormatDescription, kCMBlockBufferCustomBlockSourceVersion,
};
use objc2_core_video::{
CVImageBuffer, CVPixelBuffer, CVPixelBufferGetBaseAddressOfPlane,
CVPixelBufferGetBytesPerRowOfPlane, CVPixelBufferGetHeight, CVPixelBufferGetHeightOfPlane,
CVPixelBufferGetPixelFormatType, CVPixelBufferGetWidth, CVPixelBufferGetWidthOfPlane,
CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags, CVPixelBufferUnlockBaseAddress,
kCVPixelBufferPixelFormatTypeKey, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
};
use objc2_video_toolbox::{
VTDecodeFrameFlags, VTDecodeInfoFlags, VTDecompressionOutputCallbackRecord,
VTDecompressionSession,
};
use super::codec::{
cmtime_value_from_ticks, copy_nv12_planes, duration_ticks_from_cmtime_value, is_prores,
is_supported_video_codec, raw_atom_key, requires_extra_data_at_open, ticks_from_cmtime_value,
validate_hevc_parameter_sets, validate_parameter_sets,
};
use super::format_desc;
const NO_ERROR: i32 = 0;
struct PendingFrame {
frame: VideoFrame,
zero_copy_retain: Option<CFRetained<CVPixelBuffer>>,
}
struct SharedState {
pending: Mutex<VecDeque<PendingFrame>>,
time_base: Rational,
output: VideoOutputPreference,
}
pub(crate) struct VideoToolboxVideoDecoder {
session: Option<CFRetained<VTDecompressionSession>>,
video_format_desc: Option<CFRetained<CMVideoFormatDescription>>,
shared: Arc<SharedState>,
refcon_ptr: Option<*const SharedState>,
codec: CodecKind,
last_zero_copy_retain: Option<CFRetained<CVPixelBuffer>>,
info: StreamInfo,
flushed: bool,
}
#[allow(
clippy::non_send_fields_in_send_ty,
reason = "CFRetained<VTDecompressionSession> is a Core Foundation object — see the SAFETY comment above"
)]
unsafe impl Send for VideoToolboxVideoDecoder {}
impl VideoToolboxVideoDecoder {
pub(crate) fn open(config: &VideoDecoderConfig) -> Result<Self, DecodeError> {
validate(config)?;
let shared = Arc::new(SharedState {
pending: Mutex::new(VecDeque::new()),
time_base: config.time_base,
output: config.output,
});
let mut decoder = Self {
session: None,
video_format_desc: None,
shared,
refcon_ptr: None,
codec: config.codec,
last_zero_copy_retain: None,
info: stream_info_from(config),
flushed: false,
};
if requires_extra_data_at_open(config.codec) {
if config.extra_data.is_empty() {
return Err(DecodeError::Unsupported);
}
if config.width == 0 || config.height == 0 {
return Err(DecodeError::InvalidInput);
}
let atom_key = raw_atom_key(config.codec).ok_or(DecodeError::Unsupported)?;
let codec_type =
format_desc::raw_codec_type(config.codec).ok_or(DecodeError::Unsupported)?;
let width = i32::try_from(config.width).map_err(|_| DecodeError::InvalidInput)?;
let height = i32::try_from(config.height).map_err(|_| DecodeError::InvalidInput)?;
let fd =
format_desc::create_raw(codec_type, width, height, atom_key, &config.extra_data)?;
decoder.ensure_session(fd)?;
} else if is_prores(config.codec) {
if config.width == 0 || config.height == 0 {
return Err(DecodeError::InvalidInput);
}
let codec_type =
format_desc::raw_codec_type(config.codec).ok_or(DecodeError::Unsupported)?;
let width = i32::try_from(config.width).map_err(|_| DecodeError::InvalidInput)?;
let height = i32::try_from(config.height).map_err(|_| DecodeError::InvalidInput)?;
let fd = format_desc::create_raw_no_extension(codec_type, width, height)?;
decoder.ensure_session(fd)?;
} else if !config.extra_data.is_empty() {
match config.codec {
CodecKind::H264 => {
let avcc_config = parse_avc_decoder_config(&config.extra_data)
.ok_or(DecodeError::InvalidInput)?;
validate_parameter_sets(&avcc_config)?;
let fd = format_desc::create_h264(&avcc_config.sps[0], &avcc_config.pps[0])?;
decoder.ensure_session(fd)?;
}
CodecKind::Hevc => {
let hvcc_config = parse_hevc_decoder_config(&config.extra_data)
.ok_or(DecodeError::InvalidInput)?;
validate_hevc_parameter_sets(&hvcc_config)?;
let fd = format_desc::create_hevc(
&hvcc_config.vps[0],
&hvcc_config.sps[0],
&hvcc_config.pps[0],
)?;
decoder.ensure_session(fd)?;
}
_ => return Err(DecodeError::Unsupported),
}
}
Ok(decoder)
}
fn ensure_session(
&mut self,
video_format_desc: CFRetained<CMVideoFormatDescription>,
) -> Result<(), DecodeError> {
if self.session.is_some() {
return Ok(());
}
let dest_attrs = destination_pixel_buffer_attributes();
let refcon_ptr = Arc::into_raw(Arc::clone(&self.shared));
let callback_record = VTDecompressionOutputCallbackRecord {
decompressionOutputCallback: Some(decompression_output_callback),
decompressionOutputRefCon: refcon_ptr.cast::<c_void>().cast_mut(),
};
let mut session_out: *mut VTDecompressionSession = std::ptr::null_mut();
let status = unsafe {
VTDecompressionSession::create(
None,
&video_format_desc,
None,
Some(&dest_attrs),
std::ptr::from_ref(&callback_record),
NonNull::from(&mut session_out),
)
};
if status != NO_ERROR {
drop(unsafe { Arc::from_raw(refcon_ptr) });
return Err(DecodeError::Backend);
}
let Some(session_ptr) = NonNull::new(session_out) else {
drop(unsafe { Arc::from_raw(refcon_ptr) });
return Err(DecodeError::Backend);
};
let session: CFRetained<VTDecompressionSession> =
unsafe { CFRetained::from_raw(session_ptr) };
self.video_format_desc = Some(video_format_desc);
self.session = Some(session);
self.refcon_ptr = Some(refcon_ptr);
Ok(())
}
}
impl VideoDecoder for VideoToolboxVideoDecoder {
fn stream_info(&self) -> &StreamInfo {
&self.info
}
fn push_packet(&mut self, packet: &Packet) -> Result<(), DecodeError> {
self.last_zero_copy_retain = None;
if self.flushed {
return Err(DecodeError::Closed);
}
if packet.is_discard {
return Ok(());
}
let payload = match self.codec {
CodecKind::H264 => {
let avcc_out = to_avcc(&packet.payload);
if self.session.is_none() {
let avcc_bytes = avcc_out.avcc.as_ref().ok_or(DecodeError::InvalidInput)?;
let avcc_config =
parse_avc_decoder_config(avcc_bytes).ok_or(DecodeError::InvalidInput)?;
validate_parameter_sets(&avcc_config)?;
let fd = format_desc::create_h264(&avcc_config.sps[0], &avcc_config.pps[0])?;
self.ensure_session(fd)?;
}
avcc_out.payload
}
CodecKind::Hevc => {
let hvcc_out = to_hvcc(&packet.payload);
if self.session.is_none() {
let hvcc_bytes = hvcc_out.hvcc.as_ref().ok_or(DecodeError::InvalidInput)?;
let hvcc_config =
parse_hevc_decoder_config(hvcc_bytes).ok_or(DecodeError::InvalidInput)?;
validate_hevc_parameter_sets(&hvcc_config)?;
let fd = format_desc::create_hevc(
&hvcc_config.vps[0],
&hvcc_config.sps[0],
&hvcc_config.pps[0],
)?;
self.ensure_session(fd)?;
}
hvcc_out.payload
}
CodecKind::Vp9 | CodecKind::Av1 => {
if self.session.is_none() {
return Err(DecodeError::Backend);
}
Bytes::copy_from_slice(&packet.payload)
}
CodecKind::ProRes422Proxy
| CodecKind::ProRes422Lt
| CodecKind::ProRes422
| CodecKind::ProRes422Hq
| CodecKind::ProRes4444
| CodecKind::ProRes4444Xq => {
if self.session.is_none() {
return Err(DecodeError::Backend);
}
Bytes::copy_from_slice(&packet.payload)
}
_ => return Err(DecodeError::Unsupported),
};
let session = self.session.as_ref().ok_or(DecodeError::Backend)?;
let format_desc = self
.video_format_desc
.as_ref()
.ok_or(DecodeError::Backend)?;
let block_buffer = create_block_buffer(&payload)?;
let timing = build_timing_info(packet, self.shared.time_base);
let sample_buffer = create_sample_buffer(&block_buffer, format_desc, &timing)?;
let flags = VTDecodeFrameFlags::Frame_EnableAsynchronousDecompression
| VTDecodeFrameFlags::Frame_EnableTemporalProcessing;
let status = unsafe {
session.decode_frame(
&sample_buffer,
flags,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
if status != NO_ERROR {
return Err(DecodeError::Backend);
}
Ok(())
}
fn poll_frame(&mut self) -> Result<Option<VideoFrame>, DecodeError> {
self.last_zero_copy_retain = None;
let popped = {
let mut pending = self
.shared
.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
pending.pop_front()
};
let Some(popped) = popped else {
return Ok(None);
};
self.last_zero_copy_retain = popped.zero_copy_retain;
Ok(Some(popped.frame))
}
fn flush(&mut self) -> Result<(), DecodeError> {
self.last_zero_copy_retain = None;
if self.flushed {
return Ok(());
}
self.flushed = true;
let Some(session) = self.session.as_ref() else {
return Ok(());
};
let status = unsafe { session.wait_for_asynchronous_frames() };
if status != NO_ERROR {
return Err(DecodeError::Backend);
}
Ok(())
}
}
impl Drop for VideoToolboxVideoDecoder {
fn drop(&mut self) {
if let Some(session) = self.session.as_ref() {
let _ = unsafe { session.wait_for_asynchronous_frames() };
unsafe { session.invalidate() };
}
if let Some(refcon_ptr) = self.refcon_ptr {
drop(unsafe { Arc::from_raw(refcon_ptr) });
}
}
}
unsafe extern "C-unwind" fn decompression_output_callback(
decompression_output_ref_con: *mut c_void,
_source_frame_ref_con: *mut c_void,
status: i32,
_info_flags: VTDecodeInfoFlags,
image_buffer: *mut CVImageBuffer,
presentation_time_stamp: CMTime,
presentation_duration: CMTime,
) {
if status != NO_ERROR || image_buffer.is_null() {
return;
}
let shared = unsafe { &*(decompression_output_ref_con.cast::<SharedState>()) };
let image_buffer = unsafe { &*image_buffer };
let pixel_buffer: &CVPixelBuffer = image_buffer;
if CVPixelBufferGetPixelFormatType(pixel_buffer)
!= kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
{
return;
}
let pending_frame = if shared.output == VideoOutputPreference::ZeroCopyGpu {
build_zero_copy_frame(
pixel_buffer,
presentation_time_stamp,
presentation_duration,
shared.time_base,
)
} else {
if unsafe { CVPixelBufferLockBaseAddress(pixel_buffer, CVPixelBufferLockFlags::ReadOnly) }
!= NO_ERROR
{
return;
}
let frame = build_frame(
CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 0),
CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 0),
CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 1),
CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 1),
CVPixelBufferGetHeightOfPlane(pixel_buffer, 1),
CVPixelBufferGetWidthOfPlane(pixel_buffer, 0),
CVPixelBufferGetHeightOfPlane(pixel_buffer, 0),
presentation_time_stamp,
presentation_duration,
shared.time_base,
);
let _ = unsafe {
CVPixelBufferUnlockBaseAddress(pixel_buffer, CVPixelBufferLockFlags::ReadOnly)
};
frame.map(|frame| PendingFrame {
frame,
zero_copy_retain: None,
})
};
let Some(pending_frame) = pending_frame else {
return;
};
let mut pending = shared
.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
pending.push_back(pending_frame);
}
fn build_zero_copy_frame(
pixel_buffer: &CVPixelBuffer,
presentation_time_stamp: CMTime,
presentation_duration: CMTime,
time_base: Rational,
) -> Option<PendingFrame> {
let width = u32::try_from(CVPixelBufferGetWidth(pixel_buffer)).ok()?;
let height = u32::try_from(CVPixelBufferGetHeight(pixel_buffer)).ok()?;
if width == 0 || height == 0 {
return None;
}
let retained = unsafe { CFRetained::retain(NonNull::from(pixel_buffer)) };
let bits = CFRetained::as_ptr(&retained).as_ptr() as usize;
let buffer = NativeHandle::new(bits)?;
let pts = ticks_from_cmtime_value(
presentation_time_stamp.value,
presentation_time_stamp.timescale,
time_base,
);
let duration = duration_ticks_from_cmtime_value(
presentation_duration.value,
presentation_duration.timescale,
time_base,
);
Some(PendingFrame {
frame: VideoFrame {
pts,
duration,
width,
height,
format: PixelFormat::Nv12,
storage: VideoFrameStorage::Gpu(GpuBufferHandle::Metal { buffer }),
},
zero_copy_retain: Some(retained),
})
}
#[allow(
clippy::too_many_arguments,
reason = "raw CVPixelBuffer plane accessors read individually by the one call site above; grouping them into a struct would not simplify anything"
)]
fn build_frame(
y_base: *mut c_void,
y_stride: usize,
uv_base: *mut c_void,
uv_stride: usize,
uv_height: usize,
width: usize,
height: usize,
presentation_time_stamp: CMTime,
presentation_duration: CMTime,
time_base: Rational,
) -> Option<VideoFrame> {
let width_u32 = u32::try_from(width).ok()?;
let height_u32 = u32::try_from(height).ok()?;
if width_u32 == 0 || height_u32 == 0 || y_base.is_null() || uv_base.is_null() {
return None;
}
let y_plane = unsafe { std::slice::from_raw_parts(y_base.cast::<u8>(), y_stride * height) };
let uv_plane =
unsafe { std::slice::from_raw_parts(uv_base.cast::<u8>(), uv_stride * uv_height) };
let data = copy_nv12_planes(
y_plane, y_stride, uv_plane, uv_stride, width_u32, height_u32,
);
let pts = ticks_from_cmtime_value(
presentation_time_stamp.value,
presentation_time_stamp.timescale,
time_base,
);
let duration = duration_ticks_from_cmtime_value(
presentation_duration.value,
presentation_duration.timescale,
time_base,
);
Some(VideoFrame {
pts,
duration,
width: width_u32,
height: height_u32,
format: PixelFormat::Nv12,
storage: VideoFrameStorage::Cpu { data },
})
}
fn destination_pixel_buffer_attributes() -> CFRetained<CFDictionary> {
let value = i32::from_ne_bytes(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange.to_ne_bytes());
let number = CFNumber::new_i32(value);
let number_ct: &CFType = &number;
let key = unsafe { kCVPixelBufferPixelFormatTypeKey };
let dict = CFDictionary::<CFString, CFType>::from_slices(&[key], &[number_ct]);
unsafe { CFRetained::cast_unchecked(dict) }
}
fn build_timing_info(packet: &Packet, time_base: Rational) -> CMSampleTimingInfo {
let (pts_value, timescale) = cmtime_value_from_ticks(packet.pts, time_base);
let (dts_value, _) = cmtime_value_from_ticks(packet.dts, time_base);
let duration_ticks = i64::try_from(packet.duration).unwrap_or(i64::MAX);
let (duration_value, _) = cmtime_value_from_ticks(duration_ticks, time_base);
let cmtime = |value: i64| CMTime {
value,
timescale,
flags: CMTimeFlags::Valid,
epoch: 0,
};
CMSampleTimingInfo {
duration: cmtime(duration_value),
presentationTimeStamp: cmtime(pts_value),
decodeTimeStamp: cmtime(dts_value),
}
}
fn create_block_buffer(payload: &Bytes) -> Result<CFRetained<CMBlockBuffer>, DecodeError> {
let owned: Box<Vec<u8>> = Box::new(payload.to_vec());
let len = owned.len();
if len == 0 {
return Err(DecodeError::InvalidInput);
}
let data_ptr = owned.as_ptr().cast_mut().cast::<c_void>();
let refcon_ptr = Box::into_raw(owned).cast::<c_void>();
let custom_block_source = CMBlockBufferCustomBlockSource {
version: kCMBlockBufferCustomBlockSourceVersion,
AllocateBlock: None,
FreeBlock: Some(free_avcc_block),
refCon: refcon_ptr,
};
let mut block_buffer_out: *mut CMBlockBuffer = std::ptr::null_mut();
let status = unsafe {
CMBlockBuffer::create_with_memory_block(
None,
data_ptr,
len,
None,
std::ptr::from_ref(&custom_block_source),
0,
len,
0,
NonNull::from(&mut block_buffer_out),
)
};
if status != NO_ERROR {
drop(unsafe { Box::from_raw(refcon_ptr.cast::<Vec<u8>>()) });
return Err(DecodeError::Backend);
}
let Some(block_buffer_ptr) = NonNull::new(block_buffer_out) else {
drop(unsafe { Box::from_raw(refcon_ptr.cast::<Vec<u8>>()) });
return Err(DecodeError::Backend);
};
Ok(unsafe { CFRetained::from_raw(block_buffer_ptr) })
}
unsafe extern "C-unwind" fn free_avcc_block(
ref_con: *mut c_void,
_mem: NonNull<c_void>,
_size: usize,
) {
drop(unsafe { Box::from_raw(ref_con.cast::<Vec<u8>>()) });
}
fn create_sample_buffer(
block_buffer: &CMBlockBuffer,
format_desc: &CMVideoFormatDescription,
timing: &CMSampleTimingInfo,
) -> Result<CFRetained<CMSampleBuffer>, DecodeError> {
let mut sample_buffer_out: *mut CMSampleBuffer = std::ptr::null_mut();
let status = unsafe {
CMSampleBuffer::create(
None,
Some(block_buffer),
true,
None,
std::ptr::null_mut(),
Some(format_desc),
1,
1,
std::ptr::from_ref(timing),
0,
std::ptr::null(),
NonNull::from(&mut sample_buffer_out),
)
};
if status != NO_ERROR {
return Err(DecodeError::Backend);
}
let Some(sample_buffer_ptr) = NonNull::new(sample_buffer_out) else {
return Err(DecodeError::Backend);
};
Ok(unsafe { CFRetained::from_raw(sample_buffer_ptr) })
}
fn validate(config: &VideoDecoderConfig) -> Result<(), DecodeError> {
if !is_supported_video_codec(config.codec) {
return Err(DecodeError::Unsupported);
}
if !matches!(
config.output,
VideoOutputPreference::CpuFramesOk | VideoOutputPreference::ZeroCopyGpu
) {
return Err(DecodeError::Unsupported);
}
if config.pixel_format != PixelFormat::Nv12 {
return Err(DecodeError::Unsupported);
}
if config.time_base.den == 0 {
return Err(DecodeError::InvalidInput);
}
Ok(())
}
#[allow(clippy::missing_const_for_fn, reason = "StreamInfo holds Bytes")]
fn stream_info_from(config: &VideoDecoderConfig) -> StreamInfo {
StreamInfo::Video {
id: 0,
codec: config.codec,
time_base: config.time_base,
geometry: VideoGeometry {
width: config.width,
height: config.height,
},
extra_data: config.extra_data.clone(), }
}