Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 + tools/ci) with a "Connect to a Different Server" menu item and popout that lets the desktop app switch to any self-hosted Fluxer instance, plus fixes for well-known discovery on single-domain self-hosted deployments and a false-positive ERR_ABORTED on same-origin client redirects during the switch. Defaults to chat.fluxr.chat and uses an isolated userData directory from the official build.
1127 lines
38 KiB
Rust
1127 lines
38 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use core::ffi::c_void;
|
|
use core::ptr::{self, NonNull};
|
|
use std::collections::HashMap;
|
|
use std::collections::VecDeque;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use crate::encoder_handoff::{
|
|
EncodedBitstream, EncoderDims, EncoderError, EncoderFrameRate, EncoderSubmission, HandoffSlot,
|
|
MAX_BITSTREAM_BYTES, PicParams, VideoToolboxHandoff,
|
|
};
|
|
use crate::ring::RingError;
|
|
|
|
type CfDictionaryRef = *const c_void;
|
|
type CfStringRef = *const c_void;
|
|
type CfNumberRef = *const c_void;
|
|
type CfBooleanRef = *const c_void;
|
|
type CfAllocatorRef = *const c_void;
|
|
type CmTime = OsCmTime;
|
|
pub(crate) type CvPixelBufferRef = *mut c_void;
|
|
type CmSampleBufferRef = *mut c_void;
|
|
type CmBlockBufferRef = *mut c_void;
|
|
type VtCompressionSessionRef = *mut c_void;
|
|
type VtPixelTransferSessionRef = *mut c_void;
|
|
type OsStatus = i32;
|
|
pub(crate) type IoSurfaceRef = *mut c_void;
|
|
|
|
const KCM_VIDEO_CODEC_TYPE_H264: u32 = u32::from_be_bytes(*b"avc1");
|
|
#[cfg(test)]
|
|
const KCV_PIXEL_FORMAT_TYPE_420_YPCBCR8_BIPLANAR_VIDEO: u32 = u32::from_be_bytes(*b"420v");
|
|
const NOERR: OsStatus = 0;
|
|
const KCM_TIME_FLAGS_VALID: u32 = 1;
|
|
const KCF_NUMBER_SINT32_TYPE: i32 = 3;
|
|
const COMPLETION_RING_CAPACITY: usize = 16;
|
|
const KVT_INVALID_SESSION_ERR: OsStatus = -12903;
|
|
const VT_SESSION_REBUILD_MAX: u64 = 3;
|
|
const VT_ENCODE_ATTEMPTS_PER_FRAME_MAX: usize = 2;
|
|
|
|
#[repr(C)]
|
|
#[derive(Copy, Clone)]
|
|
struct OsCmTime {
|
|
value: i64,
|
|
timescale: i32,
|
|
flags: u32,
|
|
epoch: i64,
|
|
}
|
|
|
|
#[link(name = "CoreFoundation", kind = "framework")]
|
|
unsafe extern "C" {
|
|
static kCFAllocatorDefault: CfAllocatorRef;
|
|
static kCFBooleanTrue: CfBooleanRef;
|
|
fn CFNumberCreate(
|
|
allocator: CfAllocatorRef,
|
|
the_type: i32,
|
|
value_ptr: *const c_void,
|
|
) -> CfNumberRef;
|
|
fn CFRelease(cf: *const c_void);
|
|
fn CFRetain(cf: *const c_void) -> *const c_void;
|
|
}
|
|
|
|
#[link(name = "CoreVideo", kind = "framework")]
|
|
unsafe extern "C" {
|
|
fn CVPixelBufferCreateWithIOSurface(
|
|
allocator: CfAllocatorRef,
|
|
surface: IoSurfaceRef,
|
|
pixel_buffer_attributes: CfDictionaryRef,
|
|
pixel_buffer_out: *mut CvPixelBufferRef,
|
|
) -> i32;
|
|
fn CVPixelBufferRelease(buffer: CvPixelBufferRef);
|
|
}
|
|
|
|
#[link(name = "CoreMedia", kind = "framework")]
|
|
unsafe extern "C" {
|
|
fn CMSampleBufferGetDataBuffer(sample_buffer: CmSampleBufferRef) -> CmBlockBufferRef;
|
|
fn CMBlockBufferGetDataLength(block_buffer: CmBlockBufferRef) -> usize;
|
|
fn CMBlockBufferCopyDataBytes(
|
|
block_buffer: CmBlockBufferRef,
|
|
offset_to_data: usize,
|
|
data_length: usize,
|
|
destination: *mut c_void,
|
|
) -> OsStatus;
|
|
}
|
|
|
|
#[link(name = "VideoToolbox", kind = "framework")]
|
|
unsafe extern "C" {
|
|
static kVTCompressionPropertyKey_RealTime: CfStringRef;
|
|
static kVTCompressionPropertyKey_ProfileLevel: CfStringRef;
|
|
static kVTProfileLevel_H264_Baseline_AutoLevel: CfStringRef;
|
|
static kVTCompressionPropertyKey_AverageBitRate: CfStringRef;
|
|
static kVTCompressionPropertyKey_ExpectedFrameRate: CfStringRef;
|
|
static kVTCompressionPropertyKey_AllowFrameReordering: CfStringRef;
|
|
fn VTCompressionSessionCreate(
|
|
allocator: CfAllocatorRef,
|
|
width: i32,
|
|
height: i32,
|
|
codec_type: u32,
|
|
encoder_specification: CfDictionaryRef,
|
|
source_image_buffer_attributes: CfDictionaryRef,
|
|
compressed_data_allocator: CfAllocatorRef,
|
|
output_callback: Option<VtCompressionOutputCallback>,
|
|
output_callback_refcon: *mut c_void,
|
|
session_out: *mut VtCompressionSessionRef,
|
|
) -> OsStatus;
|
|
fn VTCompressionSessionEncodeFrame(
|
|
session: VtCompressionSessionRef,
|
|
image_buffer: CvPixelBufferRef,
|
|
presentation_time_stamp: CmTime,
|
|
duration: CmTime,
|
|
frame_properties: CfDictionaryRef,
|
|
source_frame_refcon: *mut c_void,
|
|
info_flags_out: *mut u32,
|
|
) -> OsStatus;
|
|
fn VTCompressionSessionCompleteFrames(
|
|
session: VtCompressionSessionRef,
|
|
complete_until: CmTime,
|
|
) -> OsStatus;
|
|
fn VTCompressionSessionInvalidate(session: VtCompressionSessionRef);
|
|
fn VTSessionSetProperty(
|
|
session: *mut c_void,
|
|
property_key: CfStringRef,
|
|
property_value: *const c_void,
|
|
) -> OsStatus;
|
|
fn VTPixelTransferSessionCreate(
|
|
allocator: CfAllocatorRef,
|
|
session_out: *mut VtPixelTransferSessionRef,
|
|
) -> OsStatus;
|
|
fn VTPixelTransferSessionTransferImage(
|
|
session: VtPixelTransferSessionRef,
|
|
source: CvPixelBufferRef,
|
|
destination: CvPixelBufferRef,
|
|
) -> OsStatus;
|
|
fn VTPixelTransferSessionInvalidate(session: VtPixelTransferSessionRef);
|
|
}
|
|
|
|
type VtCompressionOutputCallback = unsafe extern "C" fn(
|
|
output_callback_refcon: *mut c_void,
|
|
source_frame_refcon: *mut c_void,
|
|
status: OsStatus,
|
|
info_flags: u32,
|
|
sample_buffer: CmSampleBufferRef,
|
|
);
|
|
|
|
fn is_invalid_session_error(err: &EncoderError) -> bool {
|
|
match err {
|
|
EncoderError::EncodeFailed { vendor, status } => {
|
|
assert!(!vendor.is_empty(), "encode error vendor non-empty");
|
|
*vendor == "vt-compression-encode" && *status == KVT_INVALID_SESSION_ERR as i64
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn cf_num_i32(v: i32) -> CfNumberRef {
|
|
let ptr: *const i32 = &v;
|
|
unsafe {
|
|
CFNumberCreate(
|
|
kCFAllocatorDefault,
|
|
KCF_NUMBER_SINT32_TYPE,
|
|
ptr as *const c_void,
|
|
)
|
|
}
|
|
}
|
|
|
|
pub struct VtPixelTransfer {
|
|
session: NonNull<c_void>,
|
|
}
|
|
|
|
unsafe impl Send for VtPixelTransfer {}
|
|
|
|
impl VtPixelTransfer {
|
|
pub fn new() -> Result<Self, EncoderError> {
|
|
let mut raw: VtPixelTransferSessionRef = ptr::null_mut();
|
|
let status = unsafe { VTPixelTransferSessionCreate(kCFAllocatorDefault, &mut raw) };
|
|
if status != NOERR || raw.is_null() {
|
|
return Err(EncoderError::SessionInitFailed {
|
|
vendor: "vt-pixel-transfer",
|
|
status: status as i64,
|
|
});
|
|
}
|
|
let session = NonNull::new(raw).ok_or(EncoderError::SessionInitFailed {
|
|
vendor: "vt-pixel-transfer",
|
|
status: -1,
|
|
})?;
|
|
assert!(
|
|
session.as_ptr() as usize != 0,
|
|
"transfer session non-null after create"
|
|
);
|
|
Ok(Self { session })
|
|
}
|
|
|
|
#[allow(clippy::missing_safety_doc)]
|
|
pub unsafe fn transfer(
|
|
&self,
|
|
source: CvPixelBufferRef,
|
|
destination: CvPixelBufferRef,
|
|
) -> Result<(), EncoderError> {
|
|
assert!(!source.is_null(), "source pixel buffer non-null");
|
|
assert!(!destination.is_null(), "destination pixel buffer non-null");
|
|
let status = unsafe {
|
|
VTPixelTransferSessionTransferImage(self.session.as_ptr(), source, destination)
|
|
};
|
|
if status != NOERR {
|
|
return Err(EncoderError::EncodeFailed {
|
|
vendor: "vt-pixel-transfer",
|
|
status: status as i64,
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(clippy::missing_safety_doc)]
|
|
pub unsafe fn wrap_iosurface(
|
|
surface: IoSurfaceRef,
|
|
width: u32,
|
|
height: u32,
|
|
) -> Result<CvPixelBufferRef, EncoderError> {
|
|
assert!(!surface.is_null(), "wrap source IOSurface non-null");
|
|
assert!(width > 0 && height > 0, "wrap dims positive");
|
|
let mut pb: CvPixelBufferRef = ptr::null_mut();
|
|
let status = unsafe {
|
|
CVPixelBufferCreateWithIOSurface(kCFAllocatorDefault, surface, ptr::null(), &mut pb)
|
|
};
|
|
if status != 0 || pb.is_null() {
|
|
return Err(EncoderError::EncodeFailed {
|
|
vendor: "cv-create-iosurface",
|
|
status: status as i64,
|
|
});
|
|
}
|
|
assert!(!pb.is_null(), "wrap pb non-null after create");
|
|
Ok(pb)
|
|
}
|
|
}
|
|
|
|
impl Drop for VtPixelTransfer {
|
|
fn drop(&mut self) {
|
|
let p = self.session.as_ptr();
|
|
if !p.is_null() {
|
|
unsafe { VTPixelTransferSessionInvalidate(p) };
|
|
unsafe { CFRelease(p) };
|
|
}
|
|
}
|
|
}
|
|
|
|
struct CompletionRing {
|
|
buffer: VecDeque<EncodedBitstream>,
|
|
}
|
|
|
|
impl CompletionRing {
|
|
fn new() -> Self {
|
|
Self {
|
|
buffer: VecDeque::with_capacity(COMPLETION_RING_CAPACITY),
|
|
}
|
|
}
|
|
|
|
fn push(&mut self, item: EncodedBitstream) {
|
|
if self.buffer.len() >= COMPLETION_RING_CAPACITY {
|
|
self.buffer.pop_front();
|
|
}
|
|
self.buffer.push_back(item);
|
|
assert!(
|
|
self.buffer.len() <= COMPLETION_RING_CAPACITY,
|
|
"completion ring bounded"
|
|
);
|
|
}
|
|
|
|
fn pop(&mut self) -> Option<EncodedBitstream> {
|
|
let item = self.buffer.pop_front();
|
|
assert!(
|
|
self.buffer.len() <= COMPLETION_RING_CAPACITY,
|
|
"completion ring bounded after pop"
|
|
);
|
|
item
|
|
}
|
|
|
|
fn len(&self) -> usize {
|
|
let n = self.buffer.len();
|
|
assert!(n <= COMPLETION_RING_CAPACITY, "completion ring len bounded");
|
|
n
|
|
}
|
|
}
|
|
|
|
struct SharedState {
|
|
completed: CompletionRing,
|
|
accepted: u64,
|
|
last_pts_us: u64,
|
|
}
|
|
|
|
struct VtCallbackContext {
|
|
state: Arc<Mutex<SharedState>>,
|
|
failed_completions: AtomicU64,
|
|
}
|
|
|
|
unsafe extern "C" fn vt_compression_output(
|
|
output_callback_refcon: *mut c_void,
|
|
source_frame_refcon: *mut c_void,
|
|
status: OsStatus,
|
|
_info_flags: u32,
|
|
sample_buffer: CmSampleBufferRef,
|
|
) {
|
|
if output_callback_refcon.is_null() {
|
|
return;
|
|
}
|
|
let ctx = unsafe { &*(output_callback_refcon as *const VtCallbackContext) };
|
|
if status != NOERR {
|
|
ctx.failed_completions.fetch_add(1, Ordering::Relaxed);
|
|
return;
|
|
}
|
|
if sample_buffer.is_null() {
|
|
return;
|
|
}
|
|
let block = unsafe { CMSampleBufferGetDataBuffer(sample_buffer) };
|
|
if block.is_null() {
|
|
return;
|
|
}
|
|
let len = unsafe { CMBlockBufferGetDataLength(block) };
|
|
if len == 0 || len > MAX_BITSTREAM_BYTES {
|
|
return;
|
|
}
|
|
let mut bytes: Vec<u8> = vec![0u8; len];
|
|
let copy_status =
|
|
unsafe { CMBlockBufferCopyDataBytes(block, 0, len, bytes.as_mut_ptr() as *mut c_void) };
|
|
if copy_status != NOERR {
|
|
return;
|
|
}
|
|
let pts_us = source_frame_refcon as usize as u64;
|
|
let payload =
|
|
match std::panic::catch_unwind(|| EncodedBitstream::new(bytes, pts_us, pts_us, true)) {
|
|
Ok(p) => p,
|
|
Err(_) => return,
|
|
};
|
|
if let Ok(mut guard) = ctx.state.lock() {
|
|
guard.completed.push(payload);
|
|
}
|
|
}
|
|
|
|
struct SlotSurface {
|
|
surface: IoSurfaceRef,
|
|
pixel_buffer: CvPixelBufferRef,
|
|
}
|
|
|
|
pub struct VtCompressionHandoff {
|
|
session: NonNull<c_void>,
|
|
callback_context: Box<VtCallbackContext>,
|
|
shared: Arc<Mutex<SharedState>>,
|
|
surfaces: HashMap<u32, SlotSurface>,
|
|
dims: EncoderDims,
|
|
next_slot: u32,
|
|
frame_interval_us: u64,
|
|
frame_rate: EncoderFrameRate,
|
|
session_rebuilds: u64,
|
|
}
|
|
|
|
unsafe impl Send for VtCompressionHandoff {}
|
|
|
|
impl VtCompressionHandoff {
|
|
pub fn try_new(dims: EncoderDims) -> Result<Self, EncoderError> {
|
|
Self::try_new_with_frame_rate(dims, EncoderFrameRate::from_fps(30))
|
|
}
|
|
|
|
pub fn try_new_with_frame_rate(
|
|
dims: EncoderDims,
|
|
frame_rate: EncoderFrameRate,
|
|
) -> Result<Self, EncoderError> {
|
|
assert!(
|
|
dims.width > 0 && dims.height > 0,
|
|
"compression dims positive"
|
|
);
|
|
assert!(frame_rate.numerator > 0, "frame rate numerator positive");
|
|
assert!(
|
|
frame_rate.denominator > 0,
|
|
"frame rate denominator positive"
|
|
);
|
|
if dims.width > 7680 || dims.height > 4320 {
|
|
return Err(EncoderError::DimensionsOutOfRange {
|
|
width: dims.width,
|
|
height: dims.height,
|
|
});
|
|
}
|
|
let shared = Arc::new(Mutex::new(SharedState {
|
|
completed: CompletionRing::new(),
|
|
accepted: 0,
|
|
last_pts_us: 0,
|
|
}));
|
|
let mut callback_context = Box::new(VtCallbackContext {
|
|
state: shared.clone(),
|
|
failed_completions: AtomicU64::new(0),
|
|
});
|
|
let cb_ctx_ptr = callback_context.as_mut() as *mut VtCallbackContext as *mut c_void;
|
|
let session = Self::create_session(dims, cb_ctx_ptr, frame_rate)?;
|
|
let handoff = Self {
|
|
session,
|
|
callback_context,
|
|
shared,
|
|
surfaces: HashMap::new(),
|
|
dims,
|
|
next_slot: 0,
|
|
frame_interval_us: frame_rate.frame_interval_us(),
|
|
frame_rate,
|
|
session_rebuilds: 0,
|
|
};
|
|
assert!(
|
|
handoff.session.as_ptr() as usize != 0,
|
|
"session non-null after init"
|
|
);
|
|
assert_eq!(handoff.next_slot, 0, "next slot fresh");
|
|
Ok(handoff)
|
|
}
|
|
|
|
fn create_session(
|
|
dims: EncoderDims,
|
|
cb_ctx_ptr: *mut c_void,
|
|
frame_rate: EncoderFrameRate,
|
|
) -> Result<NonNull<c_void>, EncoderError> {
|
|
assert!(dims.width > 0, "session dims width positive");
|
|
assert!(!cb_ctx_ptr.is_null(), "session callback context non-null");
|
|
assert!(frame_rate.numerator > 0, "frame rate numerator positive");
|
|
let mut raw: VtCompressionSessionRef = ptr::null_mut();
|
|
let status = unsafe {
|
|
VTCompressionSessionCreate(
|
|
kCFAllocatorDefault,
|
|
dims.width as i32,
|
|
dims.height as i32,
|
|
KCM_VIDEO_CODEC_TYPE_H264,
|
|
ptr::null(),
|
|
ptr::null(),
|
|
kCFAllocatorDefault,
|
|
Some(vt_compression_output),
|
|
cb_ctx_ptr,
|
|
&mut raw,
|
|
)
|
|
};
|
|
if status != NOERR || raw.is_null() {
|
|
return Err(EncoderError::SessionInitFailed {
|
|
vendor: "vt-compression",
|
|
status: status as i64,
|
|
});
|
|
}
|
|
let session = NonNull::new(raw).ok_or(EncoderError::SessionInitFailed {
|
|
vendor: "vt-compression",
|
|
status: -1,
|
|
})?;
|
|
Self::configure(session.as_ptr(), frame_rate)?;
|
|
Ok(session)
|
|
}
|
|
|
|
fn try_rebuild_session(&mut self) -> bool {
|
|
assert!(
|
|
self.session_rebuilds <= VT_SESSION_REBUILD_MAX,
|
|
"rebuild count within cap"
|
|
);
|
|
if self.session_rebuilds >= VT_SESSION_REBUILD_MAX {
|
|
return false;
|
|
}
|
|
let cb_ctx_ptr = self.callback_context.as_mut() as *mut VtCallbackContext as *mut c_void;
|
|
let Ok(new_session) = Self::create_session(self.dims, cb_ctx_ptr, self.frame_rate) else {
|
|
return false;
|
|
};
|
|
let old = self.session.as_ptr();
|
|
assert!(!old.is_null(), "old session non-null before replace");
|
|
unsafe {
|
|
VTCompressionSessionInvalidate(old);
|
|
CFRelease(old);
|
|
}
|
|
self.session = new_session;
|
|
self.session_rebuilds = self.session_rebuilds.saturating_add(1);
|
|
assert!(
|
|
self.session_rebuilds <= VT_SESSION_REBUILD_MAX,
|
|
"rebuild count stays within cap"
|
|
);
|
|
true
|
|
}
|
|
|
|
fn configure(session: *mut c_void, frame_rate: EncoderFrameRate) -> Result<(), EncoderError> {
|
|
assert!(!session.is_null(), "configure session non-null");
|
|
assert!(frame_rate.numerator > 0, "frame rate numerator positive");
|
|
let real_time_true = unsafe { kCFBooleanTrue };
|
|
let st1 = unsafe {
|
|
VTSessionSetProperty(session, kVTCompressionPropertyKey_RealTime, real_time_true)
|
|
};
|
|
if st1 != NOERR {
|
|
return Err(EncoderError::SessionInitFailed {
|
|
vendor: "vt-compression-realtime",
|
|
status: st1 as i64,
|
|
});
|
|
}
|
|
let st2 = unsafe {
|
|
VTSessionSetProperty(
|
|
session,
|
|
kVTCompressionPropertyKey_ProfileLevel,
|
|
kVTProfileLevel_H264_Baseline_AutoLevel,
|
|
)
|
|
};
|
|
if st2 != NOERR {
|
|
return Err(EncoderError::SessionInitFailed {
|
|
vendor: "vt-compression-profile",
|
|
status: st2 as i64,
|
|
});
|
|
}
|
|
let bitrate = cf_num_i32(2_000_000);
|
|
let st3 = unsafe {
|
|
VTSessionSetProperty(session, kVTCompressionPropertyKey_AverageBitRate, bitrate)
|
|
};
|
|
unsafe { CFRelease(bitrate) };
|
|
if st3 != NOERR {
|
|
return Err(EncoderError::SessionInitFailed {
|
|
vendor: "vt-compression-bitrate",
|
|
status: st3 as i64,
|
|
});
|
|
}
|
|
let fps_value = frame_rate.gop_pic_size() as i32;
|
|
let fps = cf_num_i32(fps_value);
|
|
let st4 = unsafe {
|
|
VTSessionSetProperty(session, kVTCompressionPropertyKey_ExpectedFrameRate, fps)
|
|
};
|
|
unsafe { CFRelease(fps) };
|
|
if st4 != NOERR {
|
|
return Err(EncoderError::SessionInitFailed {
|
|
vendor: "vt-compression-fps",
|
|
status: st4 as i64,
|
|
});
|
|
}
|
|
let no_reorder = unsafe { CFRetain(kCFBooleanTrue) };
|
|
let st5 = unsafe {
|
|
VTSessionSetProperty(
|
|
session,
|
|
kVTCompressionPropertyKey_AllowFrameReordering,
|
|
no_reorder,
|
|
)
|
|
};
|
|
unsafe { CFRelease(no_reorder) };
|
|
if st5 != NOERR {
|
|
return Err(EncoderError::SessionInitFailed {
|
|
vendor: "vt-compression-reorder",
|
|
status: st5 as i64,
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn pending_completion(&self) -> usize {
|
|
let guard = match self.shared.lock() {
|
|
Ok(g) => g,
|
|
Err(_) => return 0,
|
|
};
|
|
let n = guard.completed.len();
|
|
assert!(n <= COMPLETION_RING_CAPACITY, "pending count bounded");
|
|
n
|
|
}
|
|
|
|
pub fn accepted_count(&self) -> u64 {
|
|
let guard = match self.shared.lock() {
|
|
Ok(g) => g,
|
|
Err(_) => return 0,
|
|
};
|
|
let n = guard.accepted;
|
|
assert!(
|
|
self.surfaces.len() as u64 <= u32::MAX as u64,
|
|
"surfaces table bounded"
|
|
);
|
|
n
|
|
}
|
|
|
|
pub fn failed_completion_count(&self) -> u64 {
|
|
let n = self
|
|
.callback_context
|
|
.failed_completions
|
|
.load(Ordering::Relaxed);
|
|
assert!(
|
|
self.session_rebuilds <= VT_SESSION_REBUILD_MAX,
|
|
"rebuild count within cap"
|
|
);
|
|
n
|
|
}
|
|
|
|
pub fn session_rebuild_count(&self) -> u64 {
|
|
let n = self.session_rebuilds;
|
|
assert!(n <= VT_SESSION_REBUILD_MAX, "rebuild count within cap");
|
|
n
|
|
}
|
|
|
|
pub fn wait_for_completion(&self) -> Result<(), EncoderError> {
|
|
let until = CmTime {
|
|
value: 0,
|
|
timescale: 0,
|
|
flags: 0,
|
|
epoch: 0,
|
|
};
|
|
let status = unsafe { VTCompressionSessionCompleteFrames(self.session.as_ptr(), until) };
|
|
if status != NOERR {
|
|
return Err(EncoderError::EncodeFailed {
|
|
vendor: "vt-compression-complete",
|
|
status: status as i64,
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn encode_frame_once(
|
|
&self,
|
|
pixel_buffer: CvPixelBufferRef,
|
|
pic_params: PicParams,
|
|
) -> Result<(), EncoderError> {
|
|
assert!(!pixel_buffer.is_null(), "encode pixel buffer non-null");
|
|
assert!(self.frame_interval_us > 0, "frame interval positive");
|
|
let pts = CmTime {
|
|
value: pic_params.pts_us as i64,
|
|
timescale: 1_000_000,
|
|
flags: KCM_TIME_FLAGS_VALID,
|
|
epoch: 0,
|
|
};
|
|
let dur = CmTime {
|
|
value: self.frame_interval_us as i64,
|
|
timescale: 1_000_000,
|
|
flags: KCM_TIME_FLAGS_VALID,
|
|
epoch: 0,
|
|
};
|
|
let mut flags_out: u32 = 0;
|
|
let refcon = pic_params.pts_us as usize as *mut c_void;
|
|
let enc_status = unsafe {
|
|
VTCompressionSessionEncodeFrame(
|
|
self.session.as_ptr(),
|
|
pixel_buffer,
|
|
pts,
|
|
dur,
|
|
ptr::null(),
|
|
refcon,
|
|
&mut flags_out,
|
|
)
|
|
};
|
|
if enc_status != NOERR {
|
|
return Err(EncoderError::EncodeFailed {
|
|
vendor: "vt-compression-encode",
|
|
status: enc_status as i64,
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn encode_one(&mut self, slot: HandoffSlot, pic_params: PicParams) -> Result<(), EncoderError> {
|
|
assert!(slot.shared_handle != 0, "encode slot handle non-zero");
|
|
let pixel_buffer = match self.surfaces.get(&slot.slot_index) {
|
|
Some(entry) => {
|
|
assert!(!entry.surface.is_null(), "cached slot surface non-null");
|
|
assert!(
|
|
!entry.pixel_buffer.is_null(),
|
|
"cached pixel buffer non-null"
|
|
);
|
|
entry.pixel_buffer
|
|
}
|
|
None => {
|
|
return Err(EncoderError::SlotUnknown {
|
|
slot_index: slot.slot_index,
|
|
});
|
|
}
|
|
};
|
|
let mut attempts: usize = 0;
|
|
while attempts < VT_ENCODE_ATTEMPTS_PER_FRAME_MAX {
|
|
attempts += 1;
|
|
assert!(
|
|
attempts <= VT_ENCODE_ATTEMPTS_PER_FRAME_MAX,
|
|
"encode attempts bounded"
|
|
);
|
|
match self.encode_frame_once(pixel_buffer, pic_params) {
|
|
Ok(()) => {
|
|
if let Ok(mut guard) = self.shared.lock() {
|
|
guard.accepted = guard.accepted.saturating_add(1);
|
|
guard.last_pts_us = pic_params.pts_us;
|
|
}
|
|
return Ok(());
|
|
}
|
|
Err(err) => {
|
|
if !is_invalid_session_error(&err) {
|
|
return Err(err);
|
|
}
|
|
if !self.try_rebuild_session() {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(EncoderError::EncodeFailed {
|
|
vendor: "vt-compression-invalid-session",
|
|
status: KVT_INVALID_SESSION_ERR as i64,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Drop for VtCompressionHandoff {
|
|
fn drop(&mut self) {
|
|
for (_, entry) in self.surfaces.drain() {
|
|
if !entry.pixel_buffer.is_null() {
|
|
unsafe { CVPixelBufferRelease(entry.pixel_buffer) };
|
|
}
|
|
}
|
|
let p = self.session.as_ptr();
|
|
if !p.is_null() {
|
|
unsafe { VTCompressionSessionInvalidate(p) };
|
|
unsafe { CFRelease(p) };
|
|
}
|
|
let _ = &self.callback_context;
|
|
}
|
|
}
|
|
|
|
impl VideoToolboxHandoff for VtCompressionHandoff {
|
|
fn register_slot(
|
|
&mut self,
|
|
iosurface_handle: u64,
|
|
_key: u64,
|
|
dims: EncoderDims,
|
|
) -> Result<HandoffSlot, EncoderError> {
|
|
if iosurface_handle == 0 {
|
|
return Err(EncoderError::SlotUnknown {
|
|
slot_index: u32::MAX,
|
|
});
|
|
}
|
|
if dims.width == 0 || dims.height == 0 {
|
|
return Err(EncoderError::DimensionsOutOfRange {
|
|
width: dims.width,
|
|
height: dims.height,
|
|
});
|
|
}
|
|
let idx = self.next_slot;
|
|
let surface = iosurface_handle as IoSurfaceRef;
|
|
if surface.is_null() {
|
|
return Err(EncoderError::SlotUnknown { slot_index: idx });
|
|
}
|
|
let mut pixel_buffer: CvPixelBufferRef = ptr::null_mut();
|
|
let status = unsafe {
|
|
CVPixelBufferCreateWithIOSurface(
|
|
kCFAllocatorDefault,
|
|
surface,
|
|
ptr::null(),
|
|
&mut pixel_buffer,
|
|
)
|
|
};
|
|
if status != 0 || pixel_buffer.is_null() {
|
|
return Err(EncoderError::RegisterFailed {
|
|
vendor: "vt-cv-create",
|
|
status: status as i64,
|
|
});
|
|
}
|
|
self.surfaces.insert(
|
|
idx,
|
|
SlotSurface {
|
|
surface,
|
|
pixel_buffer,
|
|
},
|
|
);
|
|
self.next_slot = self.next_slot.saturating_add(1);
|
|
let slot = HandoffSlot::new(idx, iosurface_handle);
|
|
assert_eq!(
|
|
slot.shared_handle, iosurface_handle,
|
|
"slot handle preserved"
|
|
);
|
|
assert!(self.next_slot > idx, "slot counter advanced");
|
|
Ok(slot)
|
|
}
|
|
|
|
fn encode_shared_async(
|
|
&mut self,
|
|
slot: HandoffSlot,
|
|
_key: u64,
|
|
dims: EncoderDims,
|
|
pic_params: PicParams,
|
|
) -> Result<(), EncoderError> {
|
|
if dims.width == 0 || dims.height == 0 {
|
|
return Err(EncoderError::DimensionsOutOfRange {
|
|
width: dims.width,
|
|
height: dims.height,
|
|
});
|
|
}
|
|
self.encode_one(slot, pic_params)
|
|
}
|
|
|
|
fn poll_completed(&mut self, _slot: HandoffSlot) -> Option<EncodedBitstream> {
|
|
let mut guard = self.shared.lock().ok()?;
|
|
let item = guard.completed.pop();
|
|
if let Some(ref payload) = item {
|
|
assert!(!payload.data.is_empty(), "polled payload non-empty");
|
|
assert!(
|
|
payload.data.len() <= MAX_BITSTREAM_BYTES,
|
|
"polled payload bounded"
|
|
);
|
|
}
|
|
item
|
|
}
|
|
|
|
fn unregister_slot(&mut self, slot: HandoffSlot) {
|
|
assert!(slot.shared_handle != 0, "unregister handle non-zero");
|
|
if let Some(entry) = self.surfaces.remove(&slot.slot_index) {
|
|
assert!(
|
|
!entry.pixel_buffer.is_null(),
|
|
"cached pixel buffer non-null on unregister"
|
|
);
|
|
unsafe { CVPixelBufferRelease(entry.pixel_buffer) };
|
|
}
|
|
}
|
|
|
|
fn encode_shared(
|
|
&mut self,
|
|
submission: EncoderSubmission,
|
|
callback: &mut dyn crate::encoder_handoff::EncoderCompletionCallback,
|
|
) -> Result<(), RingError> {
|
|
if submission.shared_handle == 0 {
|
|
return Err(RingError::UnknownSlot);
|
|
}
|
|
if submission.dims.width == 0 || submission.dims.height == 0 {
|
|
return Err(RingError::BackendFailed {
|
|
source: crate::backend::BackendError::DimensionsOutOfRange {
|
|
width: submission.dims.width,
|
|
height: submission.dims.height,
|
|
},
|
|
});
|
|
}
|
|
let slot = HandoffSlot::new(
|
|
(submission.sequence & 0x3f) as u32,
|
|
submission.shared_handle,
|
|
);
|
|
let pts_us = match submission.capture_pts_us {
|
|
Some(capture_pts_us) => capture_pts_us,
|
|
None => submission.sequence.saturating_mul(self.frame_interval_us),
|
|
};
|
|
let params = PicParams::new(pts_us, false);
|
|
let encoded_bytes_estimate = self.pending_completion() as u32;
|
|
if let Err(e) = self.encode_one(slot, params) {
|
|
return Err(RingError::BackendFailed {
|
|
source: crate::backend::BackendError::PlatformUnsupported {
|
|
reason: match e {
|
|
EncoderError::SlotUnknown { .. } => "vt slot unknown",
|
|
EncoderError::EncodeFailed { .. } => "vt encode failed",
|
|
_ => "vt session error",
|
|
},
|
|
},
|
|
});
|
|
}
|
|
callback.on_complete(submission.sequence, encoded_bytes_estimate);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn make_cv_nv12_pixel_buffer(
|
|
width: u32,
|
|
height: u32,
|
|
) -> objc2_core_foundation::CFRetained<objc2_core_video::CVPixelBuffer> {
|
|
use core::ptr::NonNull;
|
|
use objc2_core_foundation::{CFDictionary, CFRetained};
|
|
use objc2_core_video::CVPixelBuffer;
|
|
|
|
let mut empty_keys: [*const c_void; 0] = [];
|
|
let mut empty_vals: [*const c_void; 0] = [];
|
|
let iosurf_dict: CFRetained<CFDictionary> = unsafe {
|
|
CFDictionary::new(
|
|
None,
|
|
empty_keys.as_mut_ptr(),
|
|
empty_vals.as_mut_ptr(),
|
|
0,
|
|
&objc2_core_foundation::kCFTypeDictionaryKeyCallBacks,
|
|
&objc2_core_foundation::kCFTypeDictionaryValueCallBacks,
|
|
)
|
|
.expect("iosurf empty dict")
|
|
};
|
|
let key_ref: &objc2_core_foundation::CFString =
|
|
unsafe { objc2_core_video::kCVPixelBufferIOSurfacePropertiesKey };
|
|
let key_ptr: *const c_void = key_ref as *const _ as *const c_void;
|
|
let val_ptr: *const c_void = &*iosurf_dict as *const _ as *const c_void;
|
|
let mut keys = [key_ptr];
|
|
let mut vals = [val_ptr];
|
|
let attrs: CFRetained<CFDictionary> = unsafe {
|
|
CFDictionary::new(
|
|
None,
|
|
keys.as_mut_ptr(),
|
|
vals.as_mut_ptr(),
|
|
1,
|
|
&objc2_core_foundation::kCFTypeDictionaryKeyCallBacks,
|
|
&objc2_core_foundation::kCFTypeDictionaryValueCallBacks,
|
|
)
|
|
.expect("attrs dict")
|
|
};
|
|
let mut out: *mut CVPixelBuffer = std::ptr::null_mut();
|
|
let status = unsafe {
|
|
objc2_core_video::CVPixelBufferCreate(
|
|
None,
|
|
width as usize,
|
|
height as usize,
|
|
KCV_PIXEL_FORMAT_TYPE_420_YPCBCR8_BIPLANAR_VIDEO,
|
|
Some(&attrs),
|
|
NonNull::new(&mut out).expect("out pointer non-null"),
|
|
)
|
|
};
|
|
assert_eq!(status, 0, "CVPixelBufferCreate succeeded");
|
|
assert!(!out.is_null(), "CVPixelBufferCreate produced buffer");
|
|
unsafe { CFRetained::from_raw(NonNull::new_unchecked(out)) }
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn make_handoff() -> VtCompressionHandoff {
|
|
VtCompressionHandoff::try_new(EncoderDims::new(1280, 720)).expect("create handoff")
|
|
}
|
|
|
|
pub(super) fn iosurface_ptr_from_pb(pb: &objc2_core_video::CVPixelBuffer) -> *mut c_void {
|
|
let s = objc2_core_video::CVPixelBufferGetIOSurface(Some(pb));
|
|
let surface = s.expect("pixel buffer must be IOSurface-backed");
|
|
let raw: *const objc2_io_surface::IOSurfaceRef = &*surface;
|
|
raw as *mut c_void
|
|
}
|
|
|
|
#[test]
|
|
fn vt_compression_handoff_encodes_one_frame() {
|
|
let mut handoff = make_handoff();
|
|
let pb = make_cv_nv12_pixel_buffer(1280, 720);
|
|
let surface = iosurface_ptr_from_pb(&pb);
|
|
let slot = handoff
|
|
.register_slot(surface as u64, 0, EncoderDims::new(1280, 720))
|
|
.expect("register slot");
|
|
let params = PicParams::new(0, false);
|
|
handoff
|
|
.encode_shared_async(slot, 0, EncoderDims::new(1280, 720), params)
|
|
.expect("encode async");
|
|
handoff.wait_for_completion().expect("complete frames");
|
|
let payload = handoff.poll_completed(slot).expect("polled bitstream");
|
|
assert!(!payload.data.is_empty(), "encoded payload non-empty");
|
|
assert!(
|
|
payload.data.len() <= MAX_BITSTREAM_BYTES,
|
|
"encoded payload bounded"
|
|
);
|
|
}
|
|
|
|
struct RecordingCallback {
|
|
seen: Vec<(u64, u32)>,
|
|
}
|
|
|
|
impl crate::encoder_handoff::EncoderCompletionCallback for RecordingCallback {
|
|
fn on_complete(&mut self, sequence: u64, encoded_bytes: u32) {
|
|
self.seen.push((sequence, encoded_bytes));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn output_callback_counts_failed_completions() {
|
|
let ctx = VtCallbackContext {
|
|
state: Arc::new(Mutex::new(SharedState {
|
|
completed: CompletionRing::new(),
|
|
accepted: 0,
|
|
last_pts_us: 0,
|
|
})),
|
|
failed_completions: AtomicU64::new(0),
|
|
};
|
|
let ctx_ptr = &ctx as *const VtCallbackContext as *mut c_void;
|
|
unsafe {
|
|
vt_compression_output(
|
|
ctx_ptr,
|
|
ptr::null_mut(),
|
|
KVT_INVALID_SESSION_ERR,
|
|
0,
|
|
ptr::null_mut(),
|
|
);
|
|
vt_compression_output(ctx_ptr, ptr::null_mut(), -1, 0, ptr::null_mut());
|
|
}
|
|
assert_eq!(ctx.failed_completions.load(Ordering::Relaxed), 2);
|
|
let guard = ctx.state.lock().expect("state lock");
|
|
assert_eq!(
|
|
guard.completed.len(),
|
|
0,
|
|
"failed completions enqueue nothing"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn failed_completion_count_starts_at_zero_and_survives_encode() {
|
|
let mut handoff = make_handoff();
|
|
assert_eq!(handoff.failed_completion_count(), 0);
|
|
let pb = make_cv_nv12_pixel_buffer(1280, 720);
|
|
let surface = iosurface_ptr_from_pb(&pb);
|
|
let slot = handoff
|
|
.register_slot(surface as u64, 0, EncoderDims::new(1280, 720))
|
|
.expect("register slot");
|
|
handoff
|
|
.encode_shared_async(
|
|
slot,
|
|
0,
|
|
EncoderDims::new(1280, 720),
|
|
PicParams::new(0, false),
|
|
)
|
|
.expect("encode async");
|
|
handoff.wait_for_completion().expect("complete frames");
|
|
assert_eq!(handoff.failed_completion_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn session_rebuild_bounded_by_named_cap() {
|
|
let mut handoff = make_handoff();
|
|
assert_eq!(handoff.session_rebuild_count(), 0);
|
|
for expected in 1..=VT_SESSION_REBUILD_MAX {
|
|
assert!(handoff.try_rebuild_session(), "rebuild within cap succeeds");
|
|
assert_eq!(handoff.session_rebuild_count(), expected);
|
|
}
|
|
assert!(
|
|
!handoff.try_rebuild_session(),
|
|
"rebuild beyond cap rejected"
|
|
);
|
|
assert_eq!(handoff.session_rebuild_count(), VT_SESSION_REBUILD_MAX);
|
|
}
|
|
|
|
#[test]
|
|
fn session_still_encodes_after_rebuild() {
|
|
let mut handoff = make_handoff();
|
|
assert!(handoff.try_rebuild_session(), "first rebuild succeeds");
|
|
let pb = make_cv_nv12_pixel_buffer(1280, 720);
|
|
let surface = iosurface_ptr_from_pb(&pb);
|
|
let slot = handoff
|
|
.register_slot(surface as u64, 0, EncoderDims::new(1280, 720))
|
|
.expect("register slot");
|
|
handoff
|
|
.encode_shared_async(
|
|
slot,
|
|
0,
|
|
EncoderDims::new(1280, 720),
|
|
PicParams::new(0, false),
|
|
)
|
|
.expect("encode async after rebuild");
|
|
handoff.wait_for_completion().expect("complete frames");
|
|
let payload = handoff.poll_completed(slot).expect("polled bitstream");
|
|
assert!(!payload.data.is_empty(), "encoded payload non-empty");
|
|
assert_eq!(handoff.session_rebuild_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn encode_shared_uses_capture_pts_when_present() {
|
|
let mut handoff = make_handoff();
|
|
let pb = make_cv_nv12_pixel_buffer(1280, 720);
|
|
let surface = iosurface_ptr_from_pb(&pb);
|
|
let slot = handoff
|
|
.register_slot(surface as u64, 0, EncoderDims::new(1280, 720))
|
|
.expect("register slot");
|
|
assert_eq!(slot.slot_index, 0);
|
|
let dims = EncoderDims::new(1280, 720);
|
|
let submission =
|
|
EncoderSubmission::new(surface as u64, 0, dims, 0).with_capture_pts_us(987_654);
|
|
let mut cb = RecordingCallback { seen: Vec::new() };
|
|
VideoToolboxHandoff::encode_shared(&mut handoff, submission, &mut cb)
|
|
.expect("encode shared with capture pts");
|
|
handoff.wait_for_completion().expect("complete frames");
|
|
let payload = handoff.poll_completed(slot).expect("polled bitstream");
|
|
assert_eq!(payload.pts_us, 987_654, "real capture pts threaded through");
|
|
assert_eq!(cb.seen.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn encode_shared_synthesizes_pts_when_capture_pts_absent() {
|
|
let mut handoff = make_handoff();
|
|
let pb = make_cv_nv12_pixel_buffer(1280, 720);
|
|
let surface = iosurface_ptr_from_pb(&pb);
|
|
let slot = handoff
|
|
.register_slot(surface as u64, 0, EncoderDims::new(1280, 720))
|
|
.expect("register slot");
|
|
assert_eq!(slot.slot_index, 0);
|
|
let dims = EncoderDims::new(1280, 720);
|
|
let submission = EncoderSubmission::new(surface as u64, 0, dims, 0);
|
|
assert_eq!(submission.capture_pts_us, None);
|
|
let mut cb = RecordingCallback { seen: Vec::new() };
|
|
VideoToolboxHandoff::encode_shared(&mut handoff, submission, &mut cb)
|
|
.expect("encode shared without capture pts");
|
|
handoff.wait_for_completion().expect("complete frames");
|
|
let payload = handoff.poll_completed(slot).expect("polled bitstream");
|
|
assert_eq!(payload.pts_us, 0, "sequence 0 synthesizes pts 0");
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_session_error_detection_is_exact() {
|
|
let invalid = EncoderError::EncodeFailed {
|
|
vendor: "vt-compression-encode",
|
|
status: KVT_INVALID_SESSION_ERR as i64,
|
|
};
|
|
assert!(is_invalid_session_error(&invalid));
|
|
let other_status = EncoderError::EncodeFailed {
|
|
vendor: "vt-compression-encode",
|
|
status: -1,
|
|
};
|
|
assert!(!is_invalid_session_error(&other_status));
|
|
let other_vendor = EncoderError::EncodeFailed {
|
|
vendor: "vt-cv-create",
|
|
status: KVT_INVALID_SESSION_ERR as i64,
|
|
};
|
|
assert!(!is_invalid_session_error(&other_vendor));
|
|
}
|
|
|
|
#[test]
|
|
fn vt_compression_handoff_handles_back_pressure() {
|
|
let mut handoff = make_handoff();
|
|
let pb = make_cv_nv12_pixel_buffer(1280, 720);
|
|
let surface = iosurface_ptr_from_pb(&pb);
|
|
let slot = handoff
|
|
.register_slot(surface as u64, 0, EncoderDims::new(1280, 720))
|
|
.expect("register slot");
|
|
let total: u64 = (COMPLETION_RING_CAPACITY as u64) + 8;
|
|
for i in 0..total {
|
|
let params = PicParams::new(i * 33_333, false);
|
|
handoff
|
|
.encode_shared_async(slot, 0, EncoderDims::new(1280, 720), params)
|
|
.expect("encode async");
|
|
}
|
|
handoff.wait_for_completion().expect("complete frames");
|
|
let pending = handoff.pending_completion();
|
|
assert!(
|
|
pending <= COMPLETION_RING_CAPACITY,
|
|
"completion ring stays bounded"
|
|
);
|
|
let mut drained = 0usize;
|
|
while handoff.poll_completed(slot).is_some() {
|
|
drained += 1;
|
|
if drained > COMPLETION_RING_CAPACITY * 2 {
|
|
break;
|
|
}
|
|
}
|
|
assert!(
|
|
drained <= COMPLETION_RING_CAPACITY,
|
|
"drained respects completion bound"
|
|
);
|
|
}
|
|
}
|