use std::collections::{BTreeMap, VecDeque};
use std::ffi::{c_int, c_void};
use std::ptr;
use std::sync::{Mutex, PoisonError};
use std::time::Duration;
use crate::bitstream::AvcConfig;
use crate::color::{ColorSpace, Range};
use crate::decode::{Decoder, DecoderConfig, Hardware};
use crate::error::{Error, Result};
use crate::frame::{Frame, PixelFormat, Plane};
use crate::identify::Encoding;
use crate::media::Packet;
#[allow(non_upper_case_globals, non_snake_case, dead_code)]
mod sys {
use std::ffi::{c_int, c_void};
use std::marker::{PhantomData, PhantomPinned};
pub type OSStatus = i32;
pub type CFTypeRef = *const c_void;
pub type CFAllocatorRef = *const c_void;
pub type CFDictionaryRef = *const c_void;
pub type CFStringRef = *const c_void;
pub type CFNumberRef = *const c_void;
pub type CFBooleanRef = *const c_void;
pub type CFIndex = isize;
pub type CMFormatDescriptionRef = *const c_void;
pub type CMBlockBufferRef = *const c_void;
pub type CMSampleBufferRef = *const c_void;
pub type CVPixelBufferRef = *const c_void;
pub type VTDecompressionSessionRef = *const c_void;
pub const DEFAULT_ALLOCATOR: CFAllocatorRef = std::ptr::null();
pub const kCFNumberSInt32Type: CFIndex = 3;
pub const kCMTimeFlags_Valid: u32 = 1 << 0;
pub const kCMBlockBufferAssureMemoryNowFlag: u32 = 1 << 0;
pub const kCVPixelBufferLock_ReadOnly: u64 = 1 << 0;
pub const kVTDecodeInfo_FrameDropped: u32 = 1 << 1;
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct CMTime {
pub value: i64,
pub timescale: i32,
pub flags: u32,
pub epoch: i64,
}
pub const INVALID_TIME: CMTime = CMTime {
value: 0,
timescale: 0,
flags: 0,
epoch: 0,
};
#[repr(C)]
pub struct CMSampleTimingInfo {
pub duration: CMTime,
pub presentation_time_stamp: CMTime,
pub decode_time_stamp: CMTime,
}
pub type VTDecompressionOutputCallback = unsafe extern "C" fn(
output_ref_con: *mut c_void,
source_frame_ref_con: *mut c_void,
status: OSStatus,
info_flags: u32,
image_buffer: CVPixelBufferRef,
presentation_time_stamp: CMTime,
presentation_duration: CMTime,
);
#[repr(C)]
pub struct VTDecompressionOutputCallbackRecord {
pub callback: VTDecompressionOutputCallback,
pub ref_con: *mut c_void,
}
#[repr(C)]
pub struct CFDictionaryCallBacks {
_opaque: [u8; 0],
_marker: PhantomData<(*mut u8, PhantomPinned)>,
}
#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {
pub static kCFTypeDictionaryKeyCallBacks: CFDictionaryCallBacks;
pub static kCFTypeDictionaryValueCallBacks: CFDictionaryCallBacks;
pub static kCFBooleanTrue: CFBooleanRef;
pub static kCFBooleanFalse: CFBooleanRef;
pub fn CFDictionaryCreate(
allocator: CFAllocatorRef,
keys: *const *const c_void,
values: *const *const c_void,
count: CFIndex,
key_callbacks: *const CFDictionaryCallBacks,
value_callbacks: *const CFDictionaryCallBacks,
) -> CFDictionaryRef;
pub fn CFNumberCreate(
allocator: CFAllocatorRef,
number_type: CFIndex,
value: *const c_void,
) -> CFNumberRef;
pub fn CFBooleanGetValue(boolean: CFBooleanRef) -> u8;
pub fn CFRelease(object: CFTypeRef);
}
#[link(name = "CoreMedia", kind = "framework")]
extern "C" {
pub fn CMVideoFormatDescriptionCreateFromH264ParameterSets(
allocator: CFAllocatorRef,
parameter_set_count: usize,
parameter_set_pointers: *const *const u8,
parameter_set_sizes: *const usize,
nal_unit_header_length: c_int,
format_description_out: *mut CMFormatDescriptionRef,
) -> OSStatus;
pub fn CMBlockBufferCreateWithMemoryBlock(
structure_allocator: CFAllocatorRef,
memory_block: *mut c_void,
block_length: usize,
block_allocator: CFAllocatorRef,
custom_block_source: *const c_void,
offset_to_data: usize,
data_length: usize,
flags: u32,
block_buffer_out: *mut CMBlockBufferRef,
) -> OSStatus;
pub fn CMBlockBufferReplaceDataBytes(
source_bytes: *const c_void,
destination_buffer: CMBlockBufferRef,
offset_into_destination: usize,
data_length: usize,
) -> OSStatus;
pub fn CMSampleBufferCreateReady(
allocator: CFAllocatorRef,
data_buffer: CMBlockBufferRef,
format_description: CMFormatDescriptionRef,
num_samples: CFIndex,
num_sample_timing_entries: CFIndex,
sample_timing_array: *const CMSampleTimingInfo,
num_sample_size_entries: CFIndex,
sample_size_array: *const usize,
sample_buffer_out: *mut CMSampleBufferRef,
) -> OSStatus;
}
#[link(name = "CoreVideo", kind = "framework")]
extern "C" {
pub static kCVPixelBufferPixelFormatTypeKey: CFStringRef;
pub fn CVPixelBufferLockBaseAddress(buffer: CVPixelBufferRef, flags: u64) -> i32;
pub fn CVPixelBufferUnlockBaseAddress(buffer: CVPixelBufferRef, flags: u64) -> i32;
pub fn CVPixelBufferGetPixelFormatType(buffer: CVPixelBufferRef) -> u32;
pub fn CVPixelBufferGetWidth(buffer: CVPixelBufferRef) -> usize;
pub fn CVPixelBufferGetHeight(buffer: CVPixelBufferRef) -> usize;
pub fn CVPixelBufferGetPlaneCount(buffer: CVPixelBufferRef) -> usize;
pub fn CVPixelBufferGetBaseAddressOfPlane(buffer: CVPixelBufferRef, plane: usize)
-> *mut c_void;
pub fn CVPixelBufferGetBytesPerRowOfPlane(buffer: CVPixelBufferRef, plane: usize) -> usize;
pub fn CVPixelBufferGetHeightOfPlane(buffer: CVPixelBufferRef, plane: usize) -> usize;
}
#[link(name = "VideoToolbox", kind = "framework")]
extern "C" {
#[cfg(target_os = "macos")]
pub static kVTDecompressionPropertyKey_UsingHardwareAcceleratedVideoDecoder: CFStringRef;
#[cfg(target_os = "macos")]
pub static kVTVideoDecoderSpecification_EnableHardwareAcceleratedVideoDecoder: CFStringRef;
#[cfg(target_os = "macos")]
pub static kVTVideoDecoderSpecification_RequireHardwareAcceleratedVideoDecoder: CFStringRef;
pub fn VTDecompressionSessionCreate(
allocator: CFAllocatorRef,
video_format_description: CMFormatDescriptionRef,
video_decoder_specification: CFDictionaryRef,
destination_image_buffer_attributes: CFDictionaryRef,
output_callback: *const VTDecompressionOutputCallbackRecord,
decompression_session_out: *mut VTDecompressionSessionRef,
) -> OSStatus;
pub fn VTDecompressionSessionDecodeFrame(
session: VTDecompressionSessionRef,
sample_buffer: CMSampleBufferRef,
decode_flags: u32,
source_frame_ref_con: *mut c_void,
info_flags_out: *mut u32,
) -> OSStatus;
pub fn VTDecompressionSessionFinishDelayedFrames(session: VTDecompressionSessionRef)
-> OSStatus;
pub fn VTDecompressionSessionWaitForAsynchronousFrames(
session: VTDecompressionSessionRef,
) -> OSStatus;
pub fn VTDecompressionSessionInvalidate(session: VTDecompressionSessionRef);
pub fn VTSessionCopyProperty(
session: CFTypeRef,
key: CFStringRef,
allocator: CFAllocatorRef,
value_out: *mut c_void,
) -> OSStatus;
}
}
const NV12_VIDEO_RANGE: u32 = u32::from_be_bytes(*b"420v");
const NV12_FULL_RANGE: u32 = u32::from_be_bytes(*b"420f");
const REORDER_WINDOW: usize = 4;
struct Retained(sys::CFTypeRef);
impl Retained {
unsafe fn adopt(object: sys::CFTypeRef) -> Option<Self> {
(!object.is_null()).then_some(Retained(object))
}
fn get(&self) -> sys::CFTypeRef {
self.0
}
}
impl Drop for Retained {
fn drop(&mut self) {
unsafe { sys::CFRelease(self.0) }
}
}
struct Output {
color: ColorSpace,
frames: Vec<Frame>,
failure: Option<String>,
}
pub struct VideoToolboxDecoder {
session: Retained,
format: Retained,
output: Box<Mutex<Output>>,
reorder: Reorder,
hardware: bool,
}
unsafe impl Send for VideoToolboxDecoder {}
impl VideoToolboxDecoder {
pub fn new(config: &DecoderConfig) -> Result<Self> {
VideoToolboxDecoder::with_hardware(config, Hardware::Prefer)
}
pub fn with_hardware(config: &DecoderConfig, hardware: Hardware) -> Result<Self> {
if config.encoding != Encoding::H264 {
let remedy = match config.encoding {
Encoding::Av1 => "vtome drives VideoToolbox for H.264 only so far; AV1 in \
hardware needs an av1C path to a format description \
(planning/TODO.md §2), and until then AV1 is decode-av1's"
.to_string(),
other => format!(
"vtome decodes H.264 and AV1 and nothing else; {other} is outside its scope"
),
};
return Err(Error::NoDecoder {
encoding: config.encoding,
remedy,
});
}
if config.extra_data.is_empty() {
return Err(decode_error(
"the track carries no avcC record, and VideoToolbox cannot start without \
the SPS and PPS inside it",
));
}
let avc = AvcConfig::parse(&config.extra_data)?;
let format = format_description(&avc)?;
let output = Box::new(Mutex::new(Output {
color: config.color,
frames: Vec::new(),
failure: None,
}));
let specification = decoder_specification(hardware)?;
let session = open_session(&format, &output, specification.as_ref())?;
let on_hardware = uses_hardware(&session);
if hardware == Hardware::Require && !on_hardware {
return Err(Error::NoDecoder {
encoding: Encoding::H264,
remedy: "hardware decoding was required, and VideoToolbox opened a software \
session — this machine has no H.264 decoder in silicon free for it"
.to_string(),
});
}
Ok(VideoToolboxDecoder {
session,
format,
output,
reorder: Reorder::default(),
hardware: on_hardware,
})
}
fn sample_buffer(&self, packet: &Packet) -> Result<Retained> {
let length = packet.data.len();
let mut block = ptr::null();
let status = unsafe {
sys::CMBlockBufferCreateWithMemoryBlock(
sys::DEFAULT_ALLOCATOR,
ptr::null_mut(),
length,
sys::DEFAULT_ALLOCATOR,
ptr::null(),
0,
length,
sys::kCMBlockBufferAssureMemoryNowFlag,
&mut block,
)
};
check(status, "allocating a block buffer")?;
let block = unsafe { Retained::adopt(block) }
.ok_or_else(|| decode_error("CoreMedia allocated no block buffer"))?;
let status = unsafe {
sys::CMBlockBufferReplaceDataBytes(packet.data.as_ptr().cast(), block.get(), 0, length)
};
check(status, "copying a packet into its block buffer")?;
let timing = sys::CMSampleTimingInfo {
duration: sys::INVALID_TIME,
presentation_time_stamp: to_cm_time(packet.pts),
decode_time_stamp: to_cm_time(packet.dts),
};
let mut sample = ptr::null();
let status = unsafe {
sys::CMSampleBufferCreateReady(
sys::DEFAULT_ALLOCATOR,
block.get(),
self.format.get(),
1,
1,
&timing,
1,
&length,
&mut sample,
)
};
check(status, "wrapping a packet as a sample buffer")?;
unsafe { Retained::adopt(sample) }
.ok_or_else(|| decode_error("CoreMedia made no sample buffer"))
}
fn collect(&mut self, status: sys::OSStatus) -> Result<()> {
let (frames, failure) = {
let mut output = self.output.lock().unwrap_or_else(PoisonError::into_inner);
(std::mem::take(&mut output.frames), output.failure.take())
};
for frame in frames {
self.reorder.push(frame);
}
check(status, "decoding a packet")?;
match failure {
Some(reason) => Err(decode_error(format!("decoding a packet: {reason}"))),
None => Ok(()),
}
}
}
impl Decoder for VideoToolboxDecoder {
fn encoding(&self) -> Encoding {
Encoding::H264
}
fn decode(&mut self, packet: &Packet) -> Result<Option<Frame>> {
if !packet.is_empty() {
let sample = self.sample_buffer(packet)?;
let mut info = 0_u32;
let status = unsafe {
sys::VTDecompressionSessionDecodeFrame(
self.session.get(),
sample.get(),
0,
ptr::null_mut(),
&mut info,
)
};
self.collect(status)?;
}
self.reorder.fed(packet);
self.reorder.release();
Ok(self.reorder.next())
}
fn flush(&mut self) -> Result<Vec<Frame>> {
let status = unsafe {
let finished = sys::VTDecompressionSessionFinishDelayedFrames(self.session.get());
let waited = sys::VTDecompressionSessionWaitForAsynchronousFrames(self.session.get());
if finished != 0 {
finished
} else {
waited
}
};
self.collect(status)?;
Ok(self.reorder.drain())
}
fn reset(&mut self) -> Result<()> {
unsafe {
sys::VTDecompressionSessionFinishDelayedFrames(self.session.get());
sys::VTDecompressionSessionWaitForAsynchronousFrames(self.session.get());
}
let mut output = self.output.lock().unwrap_or_else(PoisonError::into_inner);
output.frames.clear();
output.failure = None;
drop(output);
self.reorder = Reorder::default();
Ok(())
}
fn is_hardware(&self) -> bool {
self.hardware
}
}
impl Drop for VideoToolboxDecoder {
fn drop(&mut self) {
unsafe { sys::VTDecompressionSessionInvalidate(self.session.get()) }
}
}
fn format_description(avc: &AvcConfig) -> Result<Retained> {
if avc.sequence_parameter_sets.is_empty() || avc.picture_parameter_sets.is_empty() {
return Err(decode_error(format!(
"the avcC record holds {} SPS and {} PPS; VideoToolbox needs at least one of each",
avc.sequence_parameter_sets.len(),
avc.picture_parameter_sets.len()
)));
}
let sets: Vec<&[u8]> = avc
.sequence_parameter_sets
.iter()
.chain(&avc.picture_parameter_sets)
.map(Vec::as_slice)
.collect();
let pointers: Vec<*const u8> = sets.iter().map(|set| set.as_ptr()).collect();
let sizes: Vec<usize> = sets.iter().map(|set| set.len()).collect();
let mut format = ptr::null();
let status = unsafe {
sys::CMVideoFormatDescriptionCreateFromH264ParameterSets(
sys::DEFAULT_ALLOCATOR,
sets.len(),
pointers.as_ptr(),
sizes.as_ptr(),
avc.nal_length_size as c_int,
&mut format,
)
};
check(status, "reading the SPS and PPS")?;
unsafe { Retained::adopt(format) }
.ok_or_else(|| decode_error("CoreMedia made no format description from the SPS and PPS"))
}
fn decoder_specification(hardware: Hardware) -> Result<Option<Retained>> {
#[cfg(target_os = "macos")]
{
let (key, value) = unsafe {
match hardware {
Hardware::Prefer => return Ok(None),
Hardware::Require => (
sys::kVTVideoDecoderSpecification_RequireHardwareAcceleratedVideoDecoder,
sys::kCFBooleanTrue,
),
Hardware::Off => (
sys::kVTVideoDecoderSpecification_EnableHardwareAcceleratedVideoDecoder,
sys::kCFBooleanFalse,
),
}
};
let keys = [key];
let values = [value];
let dictionary = unsafe {
sys::CFDictionaryCreate(
sys::DEFAULT_ALLOCATOR,
keys.as_ptr(),
values.as_ptr(),
1,
ptr::addr_of!(sys::kCFTypeDictionaryKeyCallBacks),
ptr::addr_of!(sys::kCFTypeDictionaryValueCallBacks),
)
};
unsafe { Retained::adopt(dictionary) }
.map(Some)
.ok_or_else(|| decode_error("CoreFoundation made no dictionary"))
}
#[cfg(not(target_os = "macos"))]
match hardware {
Hardware::Off => Err(Error::NoDecoder {
encoding: Encoding::H264,
remedy: "this platform decodes H.264 only in hardware; software decoding is a \
macOS choice"
.to_string(),
}),
_ => Ok(None),
}
}
fn open_session(
format: &Retained,
output: &Mutex<Output>,
specification: Option<&Retained>,
) -> Result<Retained> {
let attributes = nv12_attributes()?;
let callback = sys::VTDecompressionOutputCallbackRecord {
callback: on_decoded,
ref_con: ptr::from_ref(output).cast_mut().cast(),
};
let mut session = ptr::null();
let status = unsafe {
sys::VTDecompressionSessionCreate(
sys::DEFAULT_ALLOCATOR,
format.get(),
specification.map_or(ptr::null(), Retained::get),
attributes.get(),
&callback,
&mut session,
)
};
check(status, "opening a decompression session")?;
unsafe { Retained::adopt(session) }
.ok_or_else(|| decode_error("VideoToolbox opened no session"))
}
fn nv12_attributes() -> Result<Retained> {
let format = NV12_VIDEO_RANGE as i32;
let number = unsafe {
Retained::adopt(sys::CFNumberCreate(
sys::DEFAULT_ALLOCATOR,
sys::kCFNumberSInt32Type,
ptr::from_ref(&format).cast(),
))
}
.ok_or_else(|| decode_error("CoreFoundation made no number"))?;
let keys = [unsafe { sys::kCVPixelBufferPixelFormatTypeKey }];
let values = [number.get()];
let dictionary = unsafe {
sys::CFDictionaryCreate(
sys::DEFAULT_ALLOCATOR,
keys.as_ptr(),
values.as_ptr(),
1,
ptr::addr_of!(sys::kCFTypeDictionaryKeyCallBacks),
ptr::addr_of!(sys::kCFTypeDictionaryValueCallBacks),
)
};
unsafe { Retained::adopt(dictionary) }
.ok_or_else(|| decode_error("CoreFoundation made no dictionary"))
}
#[cfg(target_os = "macos")]
fn uses_hardware(session: &Retained) -> bool {
let mut value: sys::CFBooleanRef = ptr::null();
let status = unsafe {
sys::VTSessionCopyProperty(
session.get(),
sys::kVTDecompressionPropertyKey_UsingHardwareAcceleratedVideoDecoder,
sys::DEFAULT_ALLOCATOR,
ptr::from_mut(&mut value).cast(),
)
};
if status != 0 {
return false;
}
let Some(value) = (unsafe { Retained::adopt(value) }) else {
return false;
};
unsafe { sys::CFBooleanGetValue(value.get()) != 0 }
}
#[cfg(not(target_os = "macos"))]
fn uses_hardware(_session: &Retained) -> bool {
true
}
unsafe extern "C" fn on_decoded(
output: *mut c_void,
_source_frame: *mut c_void,
status: sys::OSStatus,
info_flags: u32,
image: sys::CVPixelBufferRef,
pts: sys::CMTime,
_duration: sys::CMTime,
) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let output = unsafe { &*output.cast_const().cast::<Mutex<Output>>() };
let mut output = output.lock().unwrap_or_else(PoisonError::into_inner);
if status != 0 {
output.failure = Some(describe(status));
return;
}
if image.is_null() || info_flags & sys::kVTDecodeInfo_FrameDropped != 0 {
return;
}
let color = output.color;
match unsafe { copy_out(image, to_duration(pts), color) } {
Ok(frame) => output.frames.push(frame),
Err(error) => output.failure = Some(error.to_string()),
}
}));
}
unsafe fn copy_out(image: sys::CVPixelBufferRef, pts: Duration, color: ColorSpace) -> Result<Frame> {
let range = match sys::CVPixelBufferGetPixelFormatType(image) {
NV12_VIDEO_RANGE => Range::Limited,
NV12_FULL_RANGE => Range::Full,
other => {
return Err(decode_error(format!(
"VideoToolbox returned pixel format '{}' rather than the NV12 asked for",
String::from_utf8_lossy(&other.to_be_bytes())
)))
}
};
if sys::CVPixelBufferGetPlaneCount(image) != 2 {
return Err(decode_error("an NV12 buffer arrived without two planes"));
}
if sys::CVPixelBufferLockBaseAddress(image, sys::kCVPixelBufferLock_ReadOnly) != 0 {
return Err(decode_error("the decoded picture could not be locked for reading"));
}
struct Unlock(sys::CVPixelBufferRef);
impl Drop for Unlock {
fn drop(&mut self) {
unsafe {
sys::CVPixelBufferUnlockBaseAddress(self.0, sys::kCVPixelBufferLock_ReadOnly);
}
}
}
let _unlock = Unlock(image);
let too_big = || decode_error("the decoded picture is larger than a u32 can describe");
let width = u32::try_from(sys::CVPixelBufferGetWidth(image)).map_err(|_| too_big())?;
let height = u32::try_from(sys::CVPixelBufferGetHeight(image)).map_err(|_| too_big())?;
let layout: Vec<(*const u8, usize, usize)> = (0..2)
.map(|plane| {
(
sys::CVPixelBufferGetBaseAddressOfPlane(image, plane)
.cast_const()
.cast::<u8>(),
sys::CVPixelBufferGetBytesPerRowOfPlane(image, plane),
sys::CVPixelBufferGetHeightOfPlane(image, plane),
)
})
.collect();
let total = layout
.iter()
.try_fold(0_usize, |sum, (_, stride, rows)| {
stride.checked_mul(*rows).and_then(|size| sum.checked_add(size))
})
.ok_or_else(too_big)?;
let mut data = Vec::with_capacity(total);
let mut planes = Vec::with_capacity(2);
for (base, stride, rows) in layout {
if base.is_null() {
return Err(decode_error("a locked plane had no address"));
}
planes.push(Plane {
offset: data.len(),
stride,
});
data.extend_from_slice(std::slice::from_raw_parts(base, stride * rows));
}
Frame::with_planes(
width,
height,
PixelFormat::Nv12,
ColorSpace { range, ..color },
pts,
planes,
data,
)
}
#[derive(Default)]
struct Reorder {
waiting: BTreeMap<(Duration, u64), Frame>,
ready: VecDeque<Frame>,
arrivals: u64,
decode_times_are_real: bool,
decoded_to: Duration,
lead: Duration,
}
impl Reorder {
fn fed(&mut self, packet: &Packet) {
if packet.dts != packet.pts {
self.decode_times_are_real = true;
}
self.lead = self.lead.max(packet.dts.saturating_sub(packet.pts));
self.decoded_to = packet.dts;
}
fn push(&mut self, frame: Frame) {
self.waiting.insert((frame.pts(), self.arrivals), frame);
self.arrivals += 1;
}
fn release(&mut self) {
loop {
let waiting = self.waiting.len();
let Some(entry) = self.waiting.first_entry() else {
break;
};
let (pts, _) = *entry.key();
let due = if self.decode_times_are_real {
pts + self.lead <= self.decoded_to
} else {
waiting > REORDER_WINDOW
};
if !due {
break;
}
self.ready.push_back(entry.remove());
}
}
fn next(&mut self) -> Option<Frame> {
self.ready.pop_front()
}
fn drain(&mut self) -> Vec<Frame> {
let mut frames: Vec<Frame> = self.ready.drain(..).collect();
frames.extend(std::mem::take(&mut self.waiting).into_values());
frames
}
}
fn decode_error(reason: impl Into<String>) -> Error {
Error::Decode {
encoding: Encoding::H264,
reason: reason.into(),
}
}
fn check(status: sys::OSStatus, doing: &str) -> Result<()> {
if status == 0 {
Ok(())
} else {
Err(decode_error(format!("{doing}: {}", describe(status))))
}
}
fn describe(status: sys::OSStatus) -> String {
let meaning = match status {
-12902 => "kVTParameterErr",
-12903 => "kVTInvalidSessionErr — the session was torn down, which sleep or a GPU switch can do",
-12904 => "kVTAllocationFailedErr",
-12906 => "kVTCouldNotFindVideoDecoderErr — nothing on this machine decodes this stream",
-12909 => "kVTVideoDecoderBadDataErr — the bytes are corrupt, or not what the avcC record describes",
-12910 => "kVTVideoDecoderUnsupportedDataFormatErr — a profile or feature this decoder does not take",
-12911 => "kVTVideoDecoderMalfunctionErr",
-12913 => "kVTVideoDecoderNotAvailableNowErr — the hardware decoder is busy or unavailable",
_ => return format!("OSStatus {status}"),
};
format!("OSStatus {status} ({meaning})")
}
fn to_cm_time(time: Duration) -> sys::CMTime {
sys::CMTime {
value: i64::try_from(time.as_nanos()).unwrap_or(i64::MAX),
timescale: 1_000_000_000,
flags: sys::kCMTimeFlags_Valid,
epoch: 0,
}
}
fn to_duration(time: sys::CMTime) -> Duration {
if time.flags & sys::kCMTimeFlags_Valid == 0 || time.timescale <= 0 || time.value < 0 {
return Duration::ZERO;
}
let nanoseconds = i128::from(time.value) * 1_000_000_000 / i128::from(time.timescale);
Duration::from_nanos(u64::try_from(nanoseconds).unwrap_or(u64::MAX))
}
#[cfg(test)]
mod tests {
use super::*;
fn frame_at(milliseconds: u64) -> Frame {
Frame::packed(
2,
2,
PixelFormat::Nv12,
ColorSpace::default(),
Duration::from_millis(milliseconds),
vec![0; 6],
)
.unwrap()
}
fn packet(pts: u64, dts: u64) -> Packet {
Packet {
track_id: 1,
data: vec![1],
pts: Duration::from_millis(pts),
dts: Duration::from_millis(dts),
is_keyframe: false,
}
}
fn order(reorder: &mut Reorder, arrivals: &[(u64, u64)]) -> Vec<u64> {
let mut shown = Vec::new();
for &(pts, dts) in arrivals {
reorder.push(frame_at(pts));
reorder.fed(&packet(pts, dts));
reorder.release();
while let Some(frame) = reorder.next() {
shown.push(frame.pts().as_millis() as u64);
}
}
shown.extend(reorder.drain().iter().map(|frame| frame.pts().as_millis() as u64));
shown
}
#[test]
fn mp4_decode_times_put_b_frames_back_in_display_order() {
let arrivals = [(2, 0), (5, 1), (3, 2), (4, 3), (8, 4), (6, 5), (7, 6)];
assert_eq!(order(&mut Reorder::default(), &arrivals), [2, 3, 4, 5, 6, 7, 8]);
}
#[test]
fn mp4_decode_times_release_without_waiting_for_a_window() {
let mut reorder = Reorder::default();
reorder.push(frame_at(2));
reorder.fed(&packet(2, 0));
reorder.release();
assert!(reorder.next().is_none(), "a B-frame could still come before it");
reorder.push(frame_at(5));
reorder.fed(&packet(5, 1));
reorder.push(frame_at(3));
reorder.fed(&packet(3, 2));
reorder.release();
assert_eq!(reorder.next().map(|frame| frame.pts()), Some(Duration::from_millis(2)));
assert!(reorder.next().is_none());
}
#[test]
fn negative_composition_offsets_still_come_out_in_order() {
let arrivals = [(2, 2), (5, 3), (3, 4), (4, 5), (8, 6), (6, 7), (7, 8)];
assert_eq!(order(&mut Reorder::default(), &arrivals), [2, 3, 4, 5, 6, 7, 8]);
}
#[test]
fn without_decode_times_the_window_keeps_order() {
let arrivals = [(0, 0), (3, 3), (1, 1), (2, 2), (6, 6), (4, 4), (5, 5)];
assert_eq!(order(&mut Reorder::default(), &arrivals), [0, 1, 2, 3, 4, 5, 6]);
}
#[test]
fn times_survive_the_trip_through_cm_time() {
let time = Duration::from_nanos(41_708_333);
assert_eq!(to_duration(to_cm_time(time)), time);
assert_eq!(to_duration(sys::INVALID_TIME), Duration::ZERO);
}
#[test]
fn only_h264_is_attempted_and_the_rest_are_refused_by_name() {
let config = |encoding| DecoderConfig {
encoding,
width: 64,
height: 64,
bit_depth: 8,
color: ColorSpace::default(),
extra_data: vec![1, 2, 3],
};
let Err(Error::NoDecoder { remedy, .. }) = VideoToolboxDecoder::new(&config(Encoding::H265))
else {
panic!("HEVC is outside vtome's scope");
};
assert!(remedy.contains("scope"), "{remedy}");
let Err(Error::NoDecoder { remedy, .. }) = VideoToolboxDecoder::new(&config(Encoding::Av1))
else {
panic!("AV1 through VideoToolbox is not wired up yet");
};
assert!(remedy.contains("decode-av1"), "{remedy}");
}
#[test]
fn a_track_without_parameter_sets_is_refused_before_touching_videotoolbox() {
let config = DecoderConfig {
encoding: Encoding::H264,
width: 64,
height: 64,
bit_depth: 8,
color: ColorSpace::default(),
extra_data: Vec::new(),
};
let Err(Error::Decode { reason, .. }) = VideoToolboxDecoder::new(&config) else {
panic!("no avcC, no decoder");
};
assert!(reason.contains("avcC"), "{reason}");
}
#[test]
fn named_statuses_say_what_they_mean() {
assert!(describe(-12909).contains("BadData"));
assert_eq!(describe(-1), "OSStatus -1");
}
}