mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
chore(repo): prune dead code and duplicate abstractions (#3413)
This commit is contained in:
1 parent
c17091768c
commit
c9fdf8fead
2937 files changed
+42233
-176793
No files matched your search
@@ -1,596 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use std::cell::UnsafeCell;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||
|
||||
pub const MAC_AUDIO_POOL_CAP: usize = 16;
|
||||
pub const MAX_FRAME_BYTES_PER_SLOT: usize = 8192;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MacAudioError {
|
||||
ZeroCapacity,
|
||||
ZeroBytesPerSlot,
|
||||
BytesPerSlotTooLarge(usize),
|
||||
PayloadTooLarge { offered: usize, capacity: usize },
|
||||
}
|
||||
|
||||
impl fmt::Display for MacAudioError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::ZeroCapacity => write!(f, "MacAudioFramePool capacity must be > 0"),
|
||||
Self::ZeroBytesPerSlot => write!(f, "MacAudioFramePool bytes_per_slot must be > 0"),
|
||||
Self::BytesPerSlotTooLarge(n) => write!(
|
||||
f,
|
||||
"MacAudioFramePool bytes_per_slot {n} exceeds MAX_FRAME_BYTES_PER_SLOT={MAX_FRAME_BYTES_PER_SLOT}"
|
||||
),
|
||||
Self::PayloadTooLarge { offered, capacity } => write!(
|
||||
f,
|
||||
"MacAudioFramePool payload {offered} bytes exceeds slot capacity {capacity}"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MacAudioError {}
|
||||
|
||||
struct PoolSlotCell {
|
||||
inner: UnsafeCell<Box<[u8]>>,
|
||||
}
|
||||
|
||||
unsafe impl Send for PoolSlotCell {}
|
||||
unsafe impl Sync for PoolSlotCell {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct MacAudioPoolStats {
|
||||
pub acquired: u64,
|
||||
pub released: u64,
|
||||
pub dropped: u64,
|
||||
pub in_flight: u32,
|
||||
}
|
||||
|
||||
pub(crate) struct MacAudioFramePoolInner {
|
||||
slots: Vec<PoolSlotCell>,
|
||||
free: Mutex<Vec<usize>>,
|
||||
capacity: u32,
|
||||
bytes_per_slot: u32,
|
||||
acquired_total: AtomicU64,
|
||||
released_total: AtomicU64,
|
||||
dropped_total: AtomicU64,
|
||||
in_flight: AtomicU32,
|
||||
}
|
||||
|
||||
pub struct MacAudioFramePool {
|
||||
inner: Arc<MacAudioFramePoolInner>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for MacAudioFramePool {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let stats = self.stats();
|
||||
f.debug_struct("MacAudioFramePool")
|
||||
.field("capacity", &self.inner.capacity)
|
||||
.field("bytes_per_slot", &self.inner.bytes_per_slot)
|
||||
.field("stats", &stats)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl MacAudioFramePool {
|
||||
pub fn new(capacity: usize, bytes_per_slot: usize) -> Result<Self, MacAudioError> {
|
||||
if capacity == 0 {
|
||||
return Err(MacAudioError::ZeroCapacity);
|
||||
}
|
||||
if bytes_per_slot == 0 {
|
||||
return Err(MacAudioError::ZeroBytesPerSlot);
|
||||
}
|
||||
if bytes_per_slot > MAX_FRAME_BYTES_PER_SLOT {
|
||||
return Err(MacAudioError::BytesPerSlotTooLarge(bytes_per_slot));
|
||||
}
|
||||
assert!(capacity > 0);
|
||||
assert!(bytes_per_slot > 0);
|
||||
assert!(bytes_per_slot <= MAX_FRAME_BYTES_PER_SLOT);
|
||||
|
||||
let mut slots: Vec<PoolSlotCell> = Vec::with_capacity(capacity);
|
||||
for _ in 0..capacity {
|
||||
let buf: Box<[u8]> = vec![0u8; bytes_per_slot].into_boxed_slice();
|
||||
assert_eq!(buf.len(), bytes_per_slot);
|
||||
slots.push(PoolSlotCell {
|
||||
inner: UnsafeCell::new(buf),
|
||||
});
|
||||
}
|
||||
assert_eq!(slots.len(), capacity);
|
||||
|
||||
let mut free: Vec<usize> = Vec::with_capacity(capacity);
|
||||
for index in 0..capacity {
|
||||
free.push(index);
|
||||
}
|
||||
assert_eq!(free.len(), capacity);
|
||||
|
||||
let cap_u32 = u32::try_from(capacity).map_err(|_| MacAudioError::ZeroCapacity)?;
|
||||
let bps_u32 = u32::try_from(bytes_per_slot).map_err(|_| MacAudioError::ZeroBytesPerSlot)?;
|
||||
let inner = MacAudioFramePoolInner {
|
||||
slots,
|
||||
free: Mutex::new(free),
|
||||
capacity: cap_u32,
|
||||
bytes_per_slot: bps_u32,
|
||||
acquired_total: AtomicU64::new(0),
|
||||
released_total: AtomicU64::new(0),
|
||||
dropped_total: AtomicU64::new(0),
|
||||
in_flight: AtomicU32::new(0),
|
||||
};
|
||||
Ok(Self {
|
||||
inner: Arc::new(inner),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn try_acquire(&self) -> Option<PooledMacAudioFrame> {
|
||||
assert!(self.inner.capacity > 0);
|
||||
assert!(self.inner.bytes_per_slot > 0);
|
||||
|
||||
let mut free = self.inner.free.lock();
|
||||
assert!(free.len() <= self.inner.capacity as usize);
|
||||
let index = match free.pop() {
|
||||
Some(idx) => idx,
|
||||
None => {
|
||||
drop(free);
|
||||
self.inner.dropped_total.fetch_add(1, Ordering::Relaxed);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
assert!(index < self.inner.capacity as usize);
|
||||
self.inner.acquired_total.fetch_add(1, Ordering::Relaxed);
|
||||
let after = self.inner.in_flight.fetch_add(1, Ordering::AcqRel) + 1;
|
||||
assert!(after <= self.inner.capacity);
|
||||
drop(free);
|
||||
|
||||
Some(PooledMacAudioFrame {
|
||||
slot_index: index,
|
||||
filled_len: 0,
|
||||
pool: Arc::clone(&self.inner),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn capacity(&self) -> u32 {
|
||||
let cap = self.inner.capacity;
|
||||
assert!(cap > 0);
|
||||
assert!(cap as usize == self.inner.slots.len());
|
||||
cap
|
||||
}
|
||||
|
||||
pub fn bytes_per_slot(&self) -> u32 {
|
||||
let bps = self.inner.bytes_per_slot;
|
||||
assert!(bps > 0);
|
||||
assert!(bps as usize <= MAX_FRAME_BYTES_PER_SLOT);
|
||||
bps
|
||||
}
|
||||
|
||||
pub fn stats(&self) -> MacAudioPoolStats {
|
||||
assert!(self.inner.capacity > 0);
|
||||
let in_flight = self.inner.in_flight.load(Ordering::Acquire);
|
||||
assert!(in_flight <= self.inner.capacity);
|
||||
let acquired = self.inner.acquired_total.load(Ordering::Relaxed);
|
||||
let released = self.inner.released_total.load(Ordering::Relaxed);
|
||||
let dropped = self.inner.dropped_total.load(Ordering::Relaxed);
|
||||
assert!(released <= acquired);
|
||||
MacAudioPoolStats {
|
||||
acquired,
|
||||
released,
|
||||
dropped,
|
||||
in_flight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for MacAudioFramePool {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: Arc::clone(&self.inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PooledMacAudioFrame {
|
||||
slot_index: usize,
|
||||
filled_len: usize,
|
||||
pool: Arc<MacAudioFramePoolInner>,
|
||||
}
|
||||
|
||||
impl PooledMacAudioFrame {
|
||||
pub fn write(&mut self, samples: &[u8]) -> Result<(), MacAudioError> {
|
||||
assert!(self.slot_index < self.pool.capacity as usize);
|
||||
let cap = self.pool.bytes_per_slot as usize;
|
||||
if samples.len() > cap {
|
||||
return Err(MacAudioError::PayloadTooLarge {
|
||||
offered: samples.len(),
|
||||
capacity: cap,
|
||||
});
|
||||
}
|
||||
assert!(samples.len() <= cap);
|
||||
|
||||
let cell = &self.pool.slots[self.slot_index];
|
||||
let buf: &mut [u8] = unsafe { &mut *cell.inner.get() };
|
||||
assert_eq!(buf.len(), cap);
|
||||
if !samples.is_empty() {
|
||||
buf[..samples.len()].copy_from_slice(samples);
|
||||
}
|
||||
self.filled_len = samples.len();
|
||||
assert!(self.filled_len <= cap);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn append(&mut self, samples: &[u8]) -> Result<(), MacAudioError> {
|
||||
assert!(self.slot_index < self.pool.capacity as usize);
|
||||
let cap = self.pool.bytes_per_slot as usize;
|
||||
let new_len =
|
||||
self.filled_len
|
||||
.checked_add(samples.len())
|
||||
.ok_or(MacAudioError::PayloadTooLarge {
|
||||
offered: usize::MAX,
|
||||
capacity: cap,
|
||||
})?;
|
||||
if new_len > cap {
|
||||
return Err(MacAudioError::PayloadTooLarge {
|
||||
offered: new_len,
|
||||
capacity: cap,
|
||||
});
|
||||
}
|
||||
assert!(new_len <= cap);
|
||||
|
||||
let cell = &self.pool.slots[self.slot_index];
|
||||
let buf: &mut [u8] = unsafe { &mut *cell.inner.get() };
|
||||
assert_eq!(buf.len(), cap);
|
||||
if !samples.is_empty() {
|
||||
buf[self.filled_len..new_len].copy_from_slice(samples);
|
||||
}
|
||||
self.filled_len = new_len;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn data_slice(&self) -> &[u8] {
|
||||
assert!(self.slot_index < self.pool.capacity as usize);
|
||||
assert!(self.filled_len <= self.pool.bytes_per_slot as usize);
|
||||
let cell = &self.pool.slots[self.slot_index];
|
||||
let buf: &[u8] = unsafe { &*cell.inner.get() };
|
||||
&buf[..self.filled_len]
|
||||
}
|
||||
|
||||
pub fn filled_len(&self) -> usize {
|
||||
assert!(self.slot_index < self.pool.capacity as usize);
|
||||
assert!(self.filled_len <= self.pool.bytes_per_slot as usize);
|
||||
self.filled_len
|
||||
}
|
||||
|
||||
pub fn capacity(&self) -> usize {
|
||||
let cap = self.pool.bytes_per_slot as usize;
|
||||
assert!(cap > 0);
|
||||
assert!(cap <= MAX_FRAME_BYTES_PER_SLOT);
|
||||
cap
|
||||
}
|
||||
|
||||
pub fn slot_index(&self) -> usize {
|
||||
assert!(self.slot_index < self.pool.capacity as usize);
|
||||
self.slot_index
|
||||
}
|
||||
|
||||
pub fn as_mut_ptr(&mut self) -> *mut u8 {
|
||||
assert!(self.slot_index < self.pool.capacity as usize);
|
||||
assert!(self.filled_len <= self.pool.bytes_per_slot as usize);
|
||||
let cell = &self.pool.slots[self.slot_index];
|
||||
let buf: &mut [u8] = unsafe { &mut *cell.inner.get() };
|
||||
assert_eq!(buf.len(), self.pool.bytes_per_slot as usize);
|
||||
buf.as_mut_ptr()
|
||||
}
|
||||
|
||||
pub fn into_external_parts(mut self) -> (*mut u8, usize, Self) {
|
||||
assert!(self.slot_index < self.pool.capacity as usize);
|
||||
assert!(self.filled_len <= self.pool.bytes_per_slot as usize);
|
||||
let len = self.filled_len;
|
||||
let ptr = self.as_mut_ptr();
|
||||
assert!(!ptr.is_null());
|
||||
(ptr, len, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PooledMacAudioFrame {
|
||||
fn drop(&mut self) {
|
||||
assert!(self.slot_index < self.pool.capacity as usize);
|
||||
let mut free = self.pool.free.lock();
|
||||
assert!(free.len() < self.pool.capacity as usize);
|
||||
free.push(self.slot_index);
|
||||
let before = self.pool.in_flight.fetch_sub(1, Ordering::AcqRel);
|
||||
assert!(before >= 1);
|
||||
self.pool.released_total.fetch_add(1, Ordering::Relaxed);
|
||||
drop(free);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Barrier;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const SLOT_BYTES: usize = 3840;
|
||||
|
||||
fn default_pool() -> MacAudioFramePool {
|
||||
MacAudioFramePool::new(MAC_AUDIO_POOL_CAP, SLOT_BYTES).expect("default pool builds")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_zero_capacity() {
|
||||
let err = MacAudioFramePool::new(0, SLOT_BYTES).unwrap_err();
|
||||
assert_eq!(err, MacAudioError::ZeroCapacity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_zero_bytes_per_slot() {
|
||||
let err = MacAudioFramePool::new(4, 0).unwrap_err();
|
||||
assert_eq!(err, MacAudioError::ZeroBytesPerSlot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_bytes_per_slot_above_max() {
|
||||
let err = MacAudioFramePool::new(4, MAX_FRAME_BYTES_PER_SLOT + 1).unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
MacAudioError::BytesPerSlotTooLarge(MAX_FRAME_BYTES_PER_SLOT + 1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acquire_release_cycle_increments_counters() {
|
||||
let pool = MacAudioFramePool::new(4, SLOT_BYTES).expect("pool");
|
||||
let stats_before = pool.stats();
|
||||
assert_eq!(stats_before.acquired, 0);
|
||||
assert_eq!(stats_before.released, 0);
|
||||
assert_eq!(stats_before.in_flight, 0);
|
||||
{
|
||||
let _slot = pool.try_acquire().expect("slot");
|
||||
let stats_held = pool.stats();
|
||||
assert_eq!(stats_held.acquired, 1);
|
||||
assert_eq!(stats_held.in_flight, 1);
|
||||
}
|
||||
let stats_after = pool.stats();
|
||||
assert_eq!(stats_after.acquired, 1);
|
||||
assert_eq!(stats_after.released, 1);
|
||||
assert_eq!(stats_after.in_flight, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_exhausts_at_cap_seventeenth_acquire_returns_none() {
|
||||
let pool = default_pool();
|
||||
let mut held = Vec::with_capacity(MAC_AUDIO_POOL_CAP);
|
||||
for _ in 0..MAC_AUDIO_POOL_CAP {
|
||||
held.push(pool.try_acquire().expect("slot in capacity"));
|
||||
}
|
||||
assert!(pool.try_acquire().is_none());
|
||||
let stats = pool.stats();
|
||||
assert_eq!(stats.dropped, 1);
|
||||
assert_eq!(stats.acquired as usize, MAC_AUDIO_POOL_CAP);
|
||||
assert_eq!(stats.in_flight as usize, MAC_AUDIO_POOL_CAP);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_returns_slot_to_free_list() {
|
||||
let pool = MacAudioFramePool::new(2, SLOT_BYTES).expect("pool");
|
||||
let first = pool.try_acquire().expect("first");
|
||||
let second = pool.try_acquire().expect("second");
|
||||
assert!(pool.try_acquire().is_none());
|
||||
drop(first);
|
||||
let revived = pool.try_acquire().expect("revived");
|
||||
assert_eq!(pool.stats().in_flight, 2);
|
||||
drop(second);
|
||||
drop(revived);
|
||||
assert_eq!(pool.stats().in_flight, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_then_data_slice_matches_payload() {
|
||||
let pool = MacAudioFramePool::new(2, 64).expect("pool");
|
||||
let mut slot = pool.try_acquire().expect("slot");
|
||||
let payload = [0xAB_u8; 32];
|
||||
slot.write(&payload).expect("payload fits");
|
||||
assert_eq!(slot.data_slice(), &payload[..]);
|
||||
assert_eq!(slot.filled_len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_rejects_payload_larger_than_slot() {
|
||||
let pool = MacAudioFramePool::new(2, 64).expect("pool");
|
||||
let mut slot = pool.try_acquire().expect("slot");
|
||||
let big = [0u8; 128];
|
||||
let err = slot.write(&big).unwrap_err();
|
||||
assert!(matches!(err, MacAudioError::PayloadTooLarge { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_concatenates_planar_to_interleaved_like_payload() {
|
||||
let pool = MacAudioFramePool::new(1, 32).expect("pool");
|
||||
let mut slot = pool.try_acquire().expect("slot");
|
||||
let left = [1_u8, 2, 3, 4];
|
||||
let right = [5_u8, 6, 7, 8];
|
||||
slot.append(&left).expect("left fits");
|
||||
slot.append(&right).expect("right fits");
|
||||
assert_eq!(slot.data_slice(), &[1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
assert_eq!(slot.filled_len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_rejects_overflow() {
|
||||
let pool = MacAudioFramePool::new(1, 4).expect("pool");
|
||||
let mut slot = pool.try_acquire().expect("slot");
|
||||
slot.append(&[1, 2, 3]).expect("3 of 4 fits");
|
||||
let err = slot.append(&[4, 5]).unwrap_err();
|
||||
assert!(matches!(err, MacAudioError::PayloadTooLarge { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recycled_slot_persists_buffer_storage_until_overwritten() {
|
||||
let pool = MacAudioFramePool::new(1, 16).expect("pool");
|
||||
let mut slot = pool.try_acquire().expect("first");
|
||||
let payload = [0x42_u8; 8];
|
||||
slot.write(&payload).expect("write");
|
||||
let observed_first = slot.data_slice().to_vec();
|
||||
drop(slot);
|
||||
let slot2 = pool.try_acquire().expect("recycled");
|
||||
assert_eq!(slot2.filled_len(), 0);
|
||||
assert_eq!(observed_first, payload.to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_thread_acquire_release_stress_8_threads_1000_ops_no_deadlock() {
|
||||
const THREADS: usize = 8;
|
||||
const OPS_PER_THREAD: usize = 1000;
|
||||
const POOL_CAP: usize = 4;
|
||||
|
||||
let pool = Arc::new(MacAudioFramePool::new(POOL_CAP, 64).expect("pool"));
|
||||
let barrier = Arc::new(Barrier::new(THREADS));
|
||||
let acquired_obs = Arc::new(AtomicUsize::new(0));
|
||||
let dropped_obs = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let mut handles = Vec::with_capacity(THREADS);
|
||||
for _ in 0..THREADS {
|
||||
let pool = Arc::clone(&pool);
|
||||
let barrier = Arc::clone(&barrier);
|
||||
let acquired_obs = Arc::clone(&acquired_obs);
|
||||
let dropped_obs = Arc::clone(&dropped_obs);
|
||||
handles.push(thread::spawn(move || {
|
||||
barrier.wait();
|
||||
let mut local_acq: usize = 0;
|
||||
let mut local_drop: usize = 0;
|
||||
for _ in 0..OPS_PER_THREAD {
|
||||
match pool.try_acquire() {
|
||||
Some(mut slot) => {
|
||||
local_acq += 1;
|
||||
let _ = slot.write(&[0xAA; 16]);
|
||||
drop(slot);
|
||||
}
|
||||
None => {
|
||||
local_drop += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
acquired_obs.fetch_add(local_acq, Ordering::Relaxed);
|
||||
dropped_obs.fetch_add(local_drop, Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(30);
|
||||
for h in handles {
|
||||
assert!(Instant::now() < deadline, "stress test exceeded 30s budget");
|
||||
h.join().expect("worker panicked");
|
||||
}
|
||||
|
||||
let stats = pool.stats();
|
||||
let total = stats.acquired + stats.dropped;
|
||||
assert_eq!(total as usize, THREADS * OPS_PER_THREAD);
|
||||
assert_eq!(
|
||||
stats.acquired as usize,
|
||||
acquired_obs.load(Ordering::Relaxed)
|
||||
);
|
||||
assert_eq!(stats.dropped as usize, dropped_obs.load(Ordering::Relaxed));
|
||||
assert_eq!(stats.in_flight, 0);
|
||||
assert_eq!(stats.acquired, stats.released);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_pool_dimensions_match_constants() {
|
||||
let pool = default_pool();
|
||||
assert_eq!(pool.capacity() as usize, MAC_AUDIO_POOL_CAP);
|
||||
assert_eq!(pool.bytes_per_slot() as usize, SLOT_BYTES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stats_in_flight_matches_simultaneous_holders() {
|
||||
let pool = MacAudioFramePool::new(8, 64).expect("pool");
|
||||
let a = pool.try_acquire().expect("a");
|
||||
let b = pool.try_acquire().expect("b");
|
||||
let c = pool.try_acquire().expect("c");
|
||||
assert_eq!(pool.stats().in_flight, 3);
|
||||
drop(b);
|
||||
assert_eq!(pool.stats().in_flight, 2);
|
||||
drop(a);
|
||||
drop(c);
|
||||
assert_eq!(pool.stats().in_flight, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_external_parts_exposes_filled_pointer_and_length() {
|
||||
let pool = MacAudioFramePool::new(2, 64).expect("pool");
|
||||
let mut slot = pool.try_acquire().expect("slot");
|
||||
let payload = [0x5A_u8; 16];
|
||||
slot.write(&payload).expect("write fits");
|
||||
let (ptr, len, owned) = slot.into_external_parts();
|
||||
assert!(!ptr.is_null());
|
||||
assert_eq!(len, 16);
|
||||
let observed = unsafe { core::slice::from_raw_parts(ptr, len) };
|
||||
assert_eq!(observed, &payload[..]);
|
||||
assert_eq!(pool.stats().in_flight, 1);
|
||||
drop(owned);
|
||||
assert_eq!(pool.stats().in_flight, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_external_parts_drop_returns_slot_to_pool() {
|
||||
let pool = MacAudioFramePool::new(1, 32).expect("pool");
|
||||
let mut slot = pool.try_acquire().expect("slot");
|
||||
slot.write(&[0xCC_u8; 8]).expect("write");
|
||||
let (_ptr, _len, owned) = slot.into_external_parts();
|
||||
assert!(pool.try_acquire().is_none());
|
||||
drop(owned);
|
||||
let revived = pool.try_acquire().expect("revived");
|
||||
assert_eq!(revived.filled_len(), 0);
|
||||
drop(revived);
|
||||
assert_eq!(pool.stats().in_flight, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_external_parts_holds_slot_across_send() {
|
||||
let pool = MacAudioFramePool::new(2, 64).expect("pool");
|
||||
let mut slot = pool.try_acquire().expect("slot");
|
||||
slot.write(&[0x77_u8; 32]).expect("write");
|
||||
let (ptr, len, owned) = slot.into_external_parts();
|
||||
let ptr_addr = ptr as usize;
|
||||
let handle = thread::spawn(move || {
|
||||
let owned = owned;
|
||||
assert_eq!(owned.filled_len(), 32);
|
||||
let observed = unsafe { core::slice::from_raw_parts(ptr_addr as *const u8, len) };
|
||||
assert_eq!(observed[0], 0x77);
|
||||
drop(owned);
|
||||
});
|
||||
handle.join().expect("worker");
|
||||
assert_eq!(pool.stats().in_flight, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_mut_ptr_returns_slot_base_address() {
|
||||
let pool = MacAudioFramePool::new(1, 64).expect("pool");
|
||||
let mut slot = pool.try_acquire().expect("slot");
|
||||
let payload = [0x11_u8; 8];
|
||||
slot.write(&payload).expect("write");
|
||||
let ptr = slot.as_mut_ptr();
|
||||
assert!(!ptr.is_null());
|
||||
unsafe { ptr.add(0).write(0x22) };
|
||||
unsafe { ptr.add(1).write(0x33) };
|
||||
assert_eq!(&slot.data_slice()[..2], &[0x22, 0x33]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capacity_one_pool_round_trips() {
|
||||
let pool = MacAudioFramePool::new(1, 32).expect("pool");
|
||||
for _ in 0..5 {
|
||||
let mut slot = pool.try_acquire().expect("slot");
|
||||
slot.write(&[1, 2, 3]).expect("write");
|
||||
assert_eq!(slot.data_slice(), &[1, 2, 3]);
|
||||
drop(slot);
|
||||
}
|
||||
let stats = pool.stats();
|
||||
assert_eq!(stats.acquired, 5);
|
||||
assert_eq!(stats.released, 5);
|
||||
assert_eq!(stats.in_flight, 0);
|
||||
}
|
||||
}
|
||||
@@ -1,467 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::fmt;
|
||||
|
||||
pub const FPS_MIN: u32 = 1;
|
||||
pub const FPS_MAX: u32 = 120;
|
||||
pub const MAX_OUTPUT_WIDTH_DEFAULT: u32 = 3840;
|
||||
pub const MAX_OUTPUT_HEIGHT_DEFAULT: u32 = 2160;
|
||||
pub const OUTPUT_DIMENSION_MIN: u32 = 2;
|
||||
pub const QUEUE_DEPTH_MIN: u32 = 1;
|
||||
pub const QUEUE_DEPTH_MAX: u32 = 16;
|
||||
pub const QUEUE_DEPTH_DEFAULT: u32 = 8;
|
||||
pub const FPS_DEFAULT: u32 = 30;
|
||||
pub const FRAME_INTERVAL_FACTOR_NUM: u64 = 9;
|
||||
pub const FRAME_INTERVAL_FACTOR_DEN: u64 = 10;
|
||||
|
||||
pub const PIXEL_FORMAT_BGRA_FOURCC: u32 = u32::from_be_bytes(*b"BGRA");
|
||||
pub const PIXEL_FORMAT_L10R_FOURCC: u32 = u32::from_be_bytes(*b"l10r");
|
||||
pub const PIXEL_FORMAT_420V_FOURCC: u32 = u32::from_be_bytes(*b"420v");
|
||||
pub const PIXEL_FORMAT_420F_FOURCC: u32 = u32::from_be_bytes(*b"420f");
|
||||
|
||||
pub const AUDIO_SAMPLE_RATE_DEFAULT_HZ: u32 = 48_000;
|
||||
pub const AUDIO_CHANNEL_COUNT_DEFAULT: u32 = 2;
|
||||
pub const AUDIO_SAMPLE_RATE_MIN_HZ: u32 = 8_000;
|
||||
pub const AUDIO_SAMPLE_RATE_MAX_HZ: u32 = 192_000;
|
||||
pub const AUDIO_CHANNEL_COUNT_MIN: u32 = 1;
|
||||
pub const AUDIO_CHANNEL_COUNT_MAX: u32 = 8;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SckPixelFormat {
|
||||
Bgra8,
|
||||
L10rHdr,
|
||||
Nv12VideoRange,
|
||||
Nv12FullRange,
|
||||
}
|
||||
|
||||
impl SckPixelFormat {
|
||||
pub fn as_fourcc(self) -> u32 {
|
||||
let value = match self {
|
||||
SckPixelFormat::Bgra8 => PIXEL_FORMAT_BGRA_FOURCC,
|
||||
SckPixelFormat::L10rHdr => PIXEL_FORMAT_L10R_FOURCC,
|
||||
SckPixelFormat::Nv12VideoRange => PIXEL_FORMAT_420V_FOURCC,
|
||||
SckPixelFormat::Nv12FullRange => PIXEL_FORMAT_420F_FOURCC,
|
||||
};
|
||||
assert!(value != 0);
|
||||
value
|
||||
}
|
||||
|
||||
pub fn is_hdr(self) -> bool {
|
||||
matches!(self, SckPixelFormat::L10rHdr)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SckColorSpace {
|
||||
DisplayP3,
|
||||
SrgbBt709,
|
||||
}
|
||||
|
||||
impl SckColorSpace {
|
||||
pub fn as_cf_name(self) -> &'static str {
|
||||
let name = match self {
|
||||
SckColorSpace::DisplayP3 => "kCGColorSpaceDisplayP3",
|
||||
SckColorSpace::SrgbBt709 => "kCGColorSpaceSRGB",
|
||||
};
|
||||
assert!(!name.is_empty());
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SckError {
|
||||
InvalidFps(u32),
|
||||
InvalidQueueDepth(u32),
|
||||
HdrRequiresWideColorSpace,
|
||||
InvalidAudioSampleRate(u32),
|
||||
InvalidAudioChannelCount(u32),
|
||||
}
|
||||
|
||||
impl fmt::Display for SckError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
SckError::InvalidFps(v) => write!(
|
||||
f,
|
||||
"SckCaptureConfig: target_fps={v} out of range [{FPS_MIN}..={FPS_MAX}]"
|
||||
),
|
||||
SckError::InvalidQueueDepth(v) => write!(
|
||||
f,
|
||||
"SckCaptureConfig: queue_depth={v} out of range [{QUEUE_DEPTH_MIN}..={QUEUE_DEPTH_MAX}]"
|
||||
),
|
||||
SckError::HdrRequiresWideColorSpace => write!(
|
||||
f,
|
||||
"SckCaptureConfig: l10r HDR pixel format requires DisplayP3 color space"
|
||||
),
|
||||
SckError::InvalidAudioSampleRate(v) => write!(
|
||||
f,
|
||||
"SckCaptureConfig: audio_sample_rate_hz={v} out of range [{AUDIO_SAMPLE_RATE_MIN_HZ}..={AUDIO_SAMPLE_RATE_MAX_HZ}]"
|
||||
),
|
||||
SckError::InvalidAudioChannelCount(v) => write!(
|
||||
f,
|
||||
"SckCaptureConfig: audio_channels={v} out of range [{AUDIO_CHANNEL_COUNT_MIN}..={AUDIO_CHANNEL_COUNT_MAX}]"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SckError {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SckCaptureConfig {
|
||||
target_fps: u32,
|
||||
queue_depth: u32,
|
||||
pixel_format: SckPixelFormat,
|
||||
color_space: SckColorSpace,
|
||||
captures_audio: bool,
|
||||
audio_sample_rate_hz: u32,
|
||||
audio_channels: u32,
|
||||
}
|
||||
|
||||
impl SckCaptureConfig {
|
||||
pub fn new(
|
||||
target_fps: u32,
|
||||
queue_depth: u32,
|
||||
pixel_format: SckPixelFormat,
|
||||
color_space: SckColorSpace,
|
||||
) -> Result<Self, SckError> {
|
||||
Self::new_with_audio(
|
||||
target_fps,
|
||||
queue_depth,
|
||||
pixel_format,
|
||||
color_space,
|
||||
false,
|
||||
AUDIO_SAMPLE_RATE_DEFAULT_HZ,
|
||||
AUDIO_CHANNEL_COUNT_DEFAULT,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_with_audio(
|
||||
target_fps: u32,
|
||||
queue_depth: u32,
|
||||
pixel_format: SckPixelFormat,
|
||||
color_space: SckColorSpace,
|
||||
captures_audio: bool,
|
||||
audio_sample_rate_hz: u32,
|
||||
audio_channels: u32,
|
||||
) -> Result<Self, SckError> {
|
||||
if !(FPS_MIN..=FPS_MAX).contains(&target_fps) {
|
||||
return Err(SckError::InvalidFps(target_fps));
|
||||
}
|
||||
if !(QUEUE_DEPTH_MIN..=QUEUE_DEPTH_MAX).contains(&queue_depth) {
|
||||
return Err(SckError::InvalidQueueDepth(queue_depth));
|
||||
}
|
||||
if pixel_format.is_hdr() && color_space != SckColorSpace::DisplayP3 {
|
||||
return Err(SckError::HdrRequiresWideColorSpace);
|
||||
}
|
||||
if !(AUDIO_SAMPLE_RATE_MIN_HZ..=AUDIO_SAMPLE_RATE_MAX_HZ).contains(&audio_sample_rate_hz) {
|
||||
return Err(SckError::InvalidAudioSampleRate(audio_sample_rate_hz));
|
||||
}
|
||||
if !(AUDIO_CHANNEL_COUNT_MIN..=AUDIO_CHANNEL_COUNT_MAX).contains(&audio_channels) {
|
||||
return Err(SckError::InvalidAudioChannelCount(audio_channels));
|
||||
}
|
||||
let cfg = Self {
|
||||
target_fps,
|
||||
queue_depth,
|
||||
pixel_format,
|
||||
color_space,
|
||||
captures_audio,
|
||||
audio_sample_rate_hz,
|
||||
audio_channels,
|
||||
};
|
||||
assert!(cfg.target_fps >= FPS_MIN);
|
||||
assert!(cfg.target_fps <= FPS_MAX);
|
||||
assert!(cfg.queue_depth >= QUEUE_DEPTH_MIN);
|
||||
assert!(cfg.queue_depth <= QUEUE_DEPTH_MAX);
|
||||
assert!(cfg.audio_sample_rate_hz >= AUDIO_SAMPLE_RATE_MIN_HZ);
|
||||
assert!(cfg.audio_channels >= AUDIO_CHANNEL_COUNT_MIN);
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn builder() -> SckCaptureConfigBuilder {
|
||||
SckCaptureConfigBuilder::default()
|
||||
}
|
||||
|
||||
pub fn target_fps(&self) -> u32 {
|
||||
assert!(self.target_fps >= FPS_MIN);
|
||||
assert!(self.target_fps <= FPS_MAX);
|
||||
self.target_fps
|
||||
}
|
||||
|
||||
pub fn queue_depth(&self) -> u32 {
|
||||
assert!(self.queue_depth >= QUEUE_DEPTH_MIN);
|
||||
assert!(self.queue_depth <= QUEUE_DEPTH_MAX);
|
||||
self.queue_depth
|
||||
}
|
||||
|
||||
pub fn pixel_format(&self) -> SckPixelFormat {
|
||||
let pf = self.pixel_format;
|
||||
assert!(pf.as_fourcc() != 0);
|
||||
pf
|
||||
}
|
||||
|
||||
pub fn color_space(&self) -> SckColorSpace {
|
||||
let cs = self.color_space;
|
||||
assert!(!cs.as_cf_name().is_empty());
|
||||
cs
|
||||
}
|
||||
|
||||
pub fn minimum_frame_interval_ns(&self) -> u64 {
|
||||
assert!(self.target_fps >= FPS_MIN);
|
||||
assert!(self.target_fps <= FPS_MAX);
|
||||
let base_ns: u64 = 1_000_000_000 / (self.target_fps as u64);
|
||||
let scaled = base_ns * FRAME_INTERVAL_FACTOR_NUM / FRAME_INTERVAL_FACTOR_DEN;
|
||||
assert!(scaled > 0);
|
||||
assert!(scaled <= 1_000_000_000);
|
||||
scaled
|
||||
}
|
||||
|
||||
pub fn captures_audio(&self) -> bool {
|
||||
self.captures_audio
|
||||
}
|
||||
|
||||
pub fn audio_sample_rate_hz(&self) -> u32 {
|
||||
assert!(self.audio_sample_rate_hz >= AUDIO_SAMPLE_RATE_MIN_HZ);
|
||||
assert!(self.audio_sample_rate_hz <= AUDIO_SAMPLE_RATE_MAX_HZ);
|
||||
self.audio_sample_rate_hz
|
||||
}
|
||||
|
||||
pub fn audio_channels(&self) -> u32 {
|
||||
assert!(self.audio_channels >= AUDIO_CHANNEL_COUNT_MIN);
|
||||
assert!(self.audio_channels <= AUDIO_CHANNEL_COUNT_MAX);
|
||||
self.audio_channels
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SckCaptureConfig {
|
||||
fn default() -> Self {
|
||||
let cfg = Self::new_with_audio(
|
||||
FPS_DEFAULT,
|
||||
QUEUE_DEPTH_DEFAULT,
|
||||
SckPixelFormat::Nv12VideoRange,
|
||||
SckColorSpace::SrgbBt709,
|
||||
false,
|
||||
AUDIO_SAMPLE_RATE_DEFAULT_HZ,
|
||||
AUDIO_CHANNEL_COUNT_DEFAULT,
|
||||
);
|
||||
match cfg {
|
||||
Ok(c) => c,
|
||||
Err(_) => unreachable!("default SckCaptureConfig must validate"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SckCaptureConfigBuilder {
|
||||
target_fps: u32,
|
||||
queue_depth: u32,
|
||||
pixel_format: SckPixelFormat,
|
||||
color_space: SckColorSpace,
|
||||
captures_audio: bool,
|
||||
audio_sample_rate_hz: u32,
|
||||
audio_channels: u32,
|
||||
}
|
||||
|
||||
impl Default for SckCaptureConfigBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
target_fps: FPS_DEFAULT,
|
||||
queue_depth: QUEUE_DEPTH_DEFAULT,
|
||||
pixel_format: SckPixelFormat::Nv12VideoRange,
|
||||
color_space: SckColorSpace::SrgbBt709,
|
||||
captures_audio: false,
|
||||
audio_sample_rate_hz: AUDIO_SAMPLE_RATE_DEFAULT_HZ,
|
||||
audio_channels: AUDIO_CHANNEL_COUNT_DEFAULT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SckCaptureConfigBuilder {
|
||||
pub fn target_fps(mut self, target_fps: u32) -> Self {
|
||||
self.target_fps = target_fps;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn queue_depth(mut self, queue_depth: u32) -> Self {
|
||||
self.queue_depth = queue_depth;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pixel_format(mut self, pixel_format: SckPixelFormat) -> Self {
|
||||
self.pixel_format = pixel_format;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color_space(mut self, color_space: SckColorSpace) -> Self {
|
||||
self.color_space = color_space;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn captures_audio(mut self, captures_audio: bool) -> Self {
|
||||
self.captures_audio = captures_audio;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn audio_sample_rate_hz(mut self, audio_sample_rate_hz: u32) -> Self {
|
||||
self.audio_sample_rate_hz = audio_sample_rate_hz;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn audio_channels(mut self, audio_channels: u32) -> Self {
|
||||
self.audio_channels = audio_channels;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<SckCaptureConfig, SckError> {
|
||||
SckCaptureConfig::new_with_audio(
|
||||
self.target_fps,
|
||||
self.queue_depth,
|
||||
self.pixel_format,
|
||||
self.color_space,
|
||||
self.captures_audio,
|
||||
self.audio_sample_rate_hz,
|
||||
self.audio_channels,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SckCaptureFailure {
|
||||
StreamStoppedWithError(String),
|
||||
StreamStartFailed(String),
|
||||
SystemDeniedAccess,
|
||||
DisplayDisconnected,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl SckCaptureFailure {
|
||||
pub fn reason(&self) -> &str {
|
||||
match self {
|
||||
SckCaptureFailure::StreamStoppedWithError(m) => m.as_str(),
|
||||
SckCaptureFailure::StreamStartFailed(m) => m.as_str(),
|
||||
SckCaptureFailure::SystemDeniedAccess => "screen recording permission denied",
|
||||
SckCaptureFailure::DisplayDisconnected => "captured display was disconnected",
|
||||
SckCaptureFailure::Unknown(m) => m.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CaptureFailureSurface: Send + Sync {
|
||||
fn on_failure(&self, reason: SckCaptureFailure);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
pub enum AudioSampleFormat {
|
||||
F32Planar = 0,
|
||||
F32Interleaved = 1,
|
||||
I16Interleaved = 2,
|
||||
Unknown = 3,
|
||||
}
|
||||
|
||||
pub const AUDIO_SAMPLE_FORMAT_CODE_MAX: u32 = 3;
|
||||
|
||||
impl AudioSampleFormat {
|
||||
pub fn code(self) -> u32 {
|
||||
let code = match self {
|
||||
AudioSampleFormat::F32Planar => 0,
|
||||
AudioSampleFormat::F32Interleaved => 1,
|
||||
AudioSampleFormat::I16Interleaved => 2,
|
||||
AudioSampleFormat::Unknown => 3,
|
||||
};
|
||||
assert!(code <= AUDIO_SAMPLE_FORMAT_CODE_MAX);
|
||||
assert_eq!(code, self as u32);
|
||||
code
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
let s = match self {
|
||||
AudioSampleFormat::F32Planar => "f32_planar",
|
||||
AudioSampleFormat::F32Interleaved => "f32_interleaved",
|
||||
AudioSampleFormat::I16Interleaved => "i16_interleaved",
|
||||
AudioSampleFormat::Unknown => "unknown",
|
||||
};
|
||||
assert!(!s.is_empty());
|
||||
s
|
||||
}
|
||||
|
||||
pub fn bytes_per_sample(self) -> u32 {
|
||||
let bytes = match self {
|
||||
AudioSampleFormat::F32Planar => 4,
|
||||
AudioSampleFormat::F32Interleaved => 4,
|
||||
AudioSampleFormat::I16Interleaved => 2,
|
||||
AudioSampleFormat::Unknown => 0,
|
||||
};
|
||||
assert!(bytes <= 4);
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MacScreenShareAudioFrame {
|
||||
pub sample_rate_hz: u32,
|
||||
pub channels: u32,
|
||||
pub num_samples_per_channel: u32,
|
||||
pub pts_us: i64,
|
||||
}
|
||||
|
||||
impl MacScreenShareAudioFrame {
|
||||
pub fn new(
|
||||
sample_rate_hz: u32,
|
||||
channels: u32,
|
||||
num_samples_per_channel: u32,
|
||||
pts_us: i64,
|
||||
) -> Result<Self, SckError> {
|
||||
if !(AUDIO_SAMPLE_RATE_MIN_HZ..=AUDIO_SAMPLE_RATE_MAX_HZ).contains(&sample_rate_hz) {
|
||||
return Err(SckError::InvalidAudioSampleRate(sample_rate_hz));
|
||||
}
|
||||
if !(AUDIO_CHANNEL_COUNT_MIN..=AUDIO_CHANNEL_COUNT_MAX).contains(&channels) {
|
||||
return Err(SckError::InvalidAudioChannelCount(channels));
|
||||
}
|
||||
assert!(sample_rate_hz >= AUDIO_SAMPLE_RATE_MIN_HZ);
|
||||
assert!(channels >= AUDIO_CHANNEL_COUNT_MIN);
|
||||
Ok(Self {
|
||||
sample_rate_hz,
|
||||
channels,
|
||||
num_samples_per_channel,
|
||||
pts_us,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MacScreenShareAudioFrameWithBytes {
|
||||
pub sample_rate_hz: u32,
|
||||
pub channels: u32,
|
||||
pub num_samples_per_channel: u32,
|
||||
pub pts_us: i64,
|
||||
pub format: AudioSampleFormat,
|
||||
pub samples: Vec<u8>,
|
||||
}
|
||||
|
||||
impl MacScreenShareAudioFrameWithBytes {
|
||||
pub fn new(
|
||||
sample_rate_hz: u32,
|
||||
channels: u32,
|
||||
num_samples_per_channel: u32,
|
||||
pts_us: i64,
|
||||
format: AudioSampleFormat,
|
||||
samples: Vec<u8>,
|
||||
) -> Result<Self, SckError> {
|
||||
if !(AUDIO_SAMPLE_RATE_MIN_HZ..=AUDIO_SAMPLE_RATE_MAX_HZ).contains(&sample_rate_hz) {
|
||||
return Err(SckError::InvalidAudioSampleRate(sample_rate_hz));
|
||||
}
|
||||
if !(AUDIO_CHANNEL_COUNT_MIN..=AUDIO_CHANNEL_COUNT_MAX).contains(&channels) {
|
||||
return Err(SckError::InvalidAudioChannelCount(channels));
|
||||
}
|
||||
assert!(sample_rate_hz >= AUDIO_SAMPLE_RATE_MIN_HZ);
|
||||
assert!(channels >= AUDIO_CHANNEL_COUNT_MIN);
|
||||
Ok(Self {
|
||||
sample_rate_hz,
|
||||
channels,
|
||||
num_samples_per_channel,
|
||||
pts_us,
|
||||
format,
|
||||
samples,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,846 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use parking_lot::{Condvar, Mutex};
|
||||
|
||||
pub const CAPTURE_PTS_MAP_CAP: usize = RING_SIZE;
|
||||
pub const EXTERNAL_SURFACE_QUEUE_CAP: usize = RING_SIZE;
|
||||
pub const READY_WAIT_TIMEOUT_US_MAX: u64 = 16_667;
|
||||
|
||||
use fluxer_encoder_ring::{
|
||||
EncoderFrameRate, EncoderInputRing, EncoderReady, IoSurfaceSlotHandle,
|
||||
MetalSharedTextureBackend, RING_SIZE, RingError, TextureFormat,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use fluxer_encoder_ring::FillReservation;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use fluxer_encoder_ring::VtPixelTransfer;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::iosurface_pair::{IoSurfaceRaw, iosurface_decrement_use_count};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EncoderAttachError {
|
||||
AlreadyAttached,
|
||||
InvalidDimensions { width: u32, height: u32 },
|
||||
RingInitFailed,
|
||||
NotAttached,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EncoderAttachError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::AlreadyAttached => write!(f, "encoder ring already attached"),
|
||||
Self::InvalidDimensions { width, height } => {
|
||||
write!(f, "encoder ring invalid dimensions {width}x{height}")
|
||||
}
|
||||
Self::RingInitFailed => write!(f, "encoder ring initialise failed"),
|
||||
Self::NotAttached => write!(f, "encoder ring not attached"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for EncoderAttachError {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct EncoderAttachStats {
|
||||
pub frames_submitted: u64,
|
||||
pub frames_dropped: u64,
|
||||
pub ring_full_events: u64,
|
||||
pub failed_blits: u64,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub struct ExternalSurfaceFrame {
|
||||
surface: IoSurfaceRaw,
|
||||
sequence: u64,
|
||||
capture_pts_us: i64,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe impl Send for ExternalSurfaceFrame {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl ExternalSurfaceFrame {
|
||||
pub fn surface(&self) -> IoSurfaceRaw {
|
||||
assert!(self.sequence > 0, "external frame sequence starts at one");
|
||||
assert!(self.surface.as_ptr() as usize != 0, "surface ptr non-null");
|
||||
self.surface
|
||||
}
|
||||
|
||||
pub fn sequence(&self) -> u64 {
|
||||
assert!(self.sequence > 0, "external frame sequence starts at one");
|
||||
assert!(self.surface.as_ptr() as usize != 0, "surface ptr non-null");
|
||||
self.sequence
|
||||
}
|
||||
|
||||
pub fn capture_pts_us(&self) -> i64 {
|
||||
assert!(self.sequence > 0, "external frame sequence starts at one");
|
||||
assert!(self.surface.as_ptr() as usize != 0, "surface ptr non-null");
|
||||
self.capture_pts_us
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl Drop for ExternalSurfaceFrame {
|
||||
fn drop(&mut self) {
|
||||
unsafe { iosurface_decrement_use_count(self.surface) };
|
||||
unsafe { CFRelease(self.surface.as_ptr()) };
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
struct BlitDestination {
|
||||
pixel_buffer: *mut core::ffi::c_void,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe impl Send for BlitDestination {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe impl Sync for BlitDestination {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl Drop for BlitDestination {
|
||||
fn drop(&mut self) {
|
||||
unsafe { release_pixel_buffer(self.pixel_buffer) };
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EncoderAttachment {
|
||||
ring: Mutex<EncoderInputRing<MetalSharedTextureBackend>>,
|
||||
ready_condvar: Condvar,
|
||||
attached: AtomicBool,
|
||||
frames_submitted: AtomicU64,
|
||||
frames_dropped: AtomicU64,
|
||||
ring_full_events: AtomicU64,
|
||||
failed_blits: AtomicU64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
frame_rate: EncoderFrameRate,
|
||||
capture_pts_by_sequence: Mutex<VecDeque<(u64, i64)>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
pixel_transfer: Mutex<Option<VtPixelTransfer>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
blit_destinations: Vec<BlitDestination>,
|
||||
#[cfg(target_os = "macos")]
|
||||
external_surfaces: Mutex<VecDeque<ExternalSurfaceFrame>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
external_sequence: AtomicU64,
|
||||
}
|
||||
|
||||
impl EncoderAttachment {
|
||||
pub fn try_new(width: u32, height: u32) -> Result<Arc<Self>, EncoderAttachError> {
|
||||
Self::try_new_with_frame_rate(width, height, EncoderFrameRate::default())
|
||||
}
|
||||
|
||||
pub fn try_new_with_frame_rate(
|
||||
width: u32,
|
||||
height: u32,
|
||||
frame_rate: EncoderFrameRate,
|
||||
) -> Result<Arc<Self>, EncoderAttachError> {
|
||||
if width == 0 || height == 0 {
|
||||
return Err(EncoderAttachError::InvalidDimensions { width, height });
|
||||
}
|
||||
assert!(frame_rate.numerator > 0, "frame rate numerator positive");
|
||||
assert!(
|
||||
frame_rate.denominator > 0,
|
||||
"frame rate denominator positive"
|
||||
);
|
||||
let mut ring = EncoderInputRing::new(MetalSharedTextureBackend::new());
|
||||
ring.initialise(width, height, TextureFormat::Nv12)
|
||||
.map_err(|_| EncoderAttachError::RingInitFailed)?;
|
||||
#[cfg(target_os = "macos")]
|
||||
let blit_destinations = build_blit_destinations(&mut ring, width, height)?;
|
||||
#[cfg(target_os = "macos")]
|
||||
let pixel_transfer = VtPixelTransfer::new().ok();
|
||||
let attachment = Self {
|
||||
ring: Mutex::new(ring),
|
||||
ready_condvar: Condvar::new(),
|
||||
attached: AtomicBool::new(true),
|
||||
frames_submitted: AtomicU64::new(0),
|
||||
frames_dropped: AtomicU64::new(0),
|
||||
ring_full_events: AtomicU64::new(0),
|
||||
failed_blits: AtomicU64::new(0),
|
||||
width,
|
||||
height,
|
||||
frame_rate,
|
||||
capture_pts_by_sequence: Mutex::new(VecDeque::with_capacity(CAPTURE_PTS_MAP_CAP)),
|
||||
#[cfg(target_os = "macos")]
|
||||
pixel_transfer: Mutex::new(pixel_transfer),
|
||||
#[cfg(target_os = "macos")]
|
||||
blit_destinations,
|
||||
#[cfg(target_os = "macos")]
|
||||
external_surfaces: Mutex::new(VecDeque::with_capacity(EXTERNAL_SURFACE_QUEUE_CAP)),
|
||||
#[cfg(target_os = "macos")]
|
||||
external_sequence: AtomicU64::new(0),
|
||||
};
|
||||
assert!(
|
||||
attachment.attached.load(Ordering::Acquire),
|
||||
"attachment is attached"
|
||||
);
|
||||
assert!(attachment.width > 0, "attachment width positive");
|
||||
assert!(
|
||||
attachment.frame_rate.numerator > 0,
|
||||
"attachment fps positive"
|
||||
);
|
||||
Ok(Arc::new(attachment))
|
||||
}
|
||||
|
||||
pub fn width(&self) -> u32 {
|
||||
let w = self.width;
|
||||
assert!(w > 0, "attachment width positive");
|
||||
assert!(self.height > 0, "attachment height positive");
|
||||
w
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u32 {
|
||||
let h = self.height;
|
||||
assert!(h > 0, "attachment height positive");
|
||||
assert!(self.width > 0, "attachment width positive");
|
||||
h
|
||||
}
|
||||
|
||||
pub fn frame_rate(&self) -> EncoderFrameRate {
|
||||
let rate = self.frame_rate;
|
||||
assert!(rate.numerator > 0, "attachment fps numerator positive");
|
||||
assert!(rate.denominator > 0, "attachment fps denominator positive");
|
||||
rate
|
||||
}
|
||||
|
||||
pub fn is_attached(&self) -> bool {
|
||||
let a = self.attached.load(Ordering::Acquire);
|
||||
assert!(self.width > 0, "width intact while reading attached");
|
||||
assert!(self.height > 0, "height intact while reading attached");
|
||||
a
|
||||
}
|
||||
|
||||
pub fn detach(&self) {
|
||||
self.attached.store(false, Ordering::Release);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let drained = core::mem::take(&mut *self.external_surfaces.lock());
|
||||
assert!(
|
||||
drained.len() <= EXTERNAL_SURFACE_QUEUE_CAP,
|
||||
"external queue bounded at detach"
|
||||
);
|
||||
drop(drained);
|
||||
}
|
||||
self.ready_condvar.notify_all();
|
||||
}
|
||||
|
||||
pub fn capacity(&self) -> usize {
|
||||
let cap = RING_SIZE;
|
||||
assert!(cap > 0, "ring capacity positive");
|
||||
assert_eq!(cap, 8, "ring capacity matches RING_SIZE");
|
||||
cap
|
||||
}
|
||||
|
||||
pub fn submit_iosurface_frame(&self, _iosurface_handle: u64) -> Result<(), EncoderAttachError> {
|
||||
if !self.attached.load(Ordering::Acquire) {
|
||||
return Err(EncoderAttachError::NotAttached);
|
||||
}
|
||||
let mut ring = self.ring.lock();
|
||||
let result: Result<(), RingError> = ring.submit_skip_oldest(|_handle| {});
|
||||
drop(ring);
|
||||
match result {
|
||||
Ok(()) => {
|
||||
self.frames_submitted.fetch_add(1, Ordering::Relaxed);
|
||||
self.ready_condvar.notify_one();
|
||||
Ok(())
|
||||
}
|
||||
Err(RingError::FullDropped { .. }) => {
|
||||
self.frames_dropped.fetch_add(1, Ordering::Relaxed);
|
||||
self.ring_full_events.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(EncoderAttachError::RingInitFailed),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn submit_with_blit(
|
||||
&self,
|
||||
source_pixel_buffer: *mut core::ffi::c_void,
|
||||
capture_pts_us: i64,
|
||||
) -> Result<(), EncoderAttachError> {
|
||||
assert!(
|
||||
!source_pixel_buffer.is_null(),
|
||||
"blit source must be non-null"
|
||||
);
|
||||
assert!(self.width > 0, "attachment width positive");
|
||||
if !self.attached.load(Ordering::Acquire) {
|
||||
return Err(EncoderAttachError::NotAttached);
|
||||
}
|
||||
let transfer_guard = self.pixel_transfer.lock();
|
||||
let Some(transfer) = transfer_guard.as_ref() else {
|
||||
self.failed_blits.fetch_add(1, Ordering::Relaxed);
|
||||
return Err(EncoderAttachError::RingInitFailed);
|
||||
};
|
||||
let Some(reservation) = self.reserve_blit_slot()? else {
|
||||
return Ok(());
|
||||
};
|
||||
let slot_index = reservation.slot_index() as usize;
|
||||
assert!(
|
||||
slot_index < self.blit_destinations.len(),
|
||||
"slot index within cached destinations"
|
||||
);
|
||||
let dest_pb = self.blit_destinations[slot_index].pixel_buffer;
|
||||
assert!(
|
||||
!dest_pb.is_null(),
|
||||
"cached destination pixel buffer non-null"
|
||||
);
|
||||
let blit_ok = unsafe { transfer.transfer(source_pixel_buffer, dest_pb) }.is_ok();
|
||||
if blit_ok {
|
||||
self.commit_blit_slot(reservation, capture_pts_us)
|
||||
} else {
|
||||
let cancelled = self.ring.lock().cancel(reservation);
|
||||
assert!(cancelled.is_ok(), "cancel of filling reservation succeeds");
|
||||
self.failed_blits.fetch_add(1, Ordering::Relaxed);
|
||||
Err(EncoderAttachError::RingInitFailed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn reserve_blit_slot(
|
||||
&self,
|
||||
) -> Result<Option<FillReservation<IoSurfaceSlotHandle>>, EncoderAttachError> {
|
||||
assert!(self.width > 0, "attachment width positive");
|
||||
assert!(self.height > 0, "attachment height positive");
|
||||
let mut ring = self.ring.lock();
|
||||
match ring.reserve_skip_oldest() {
|
||||
Ok(reservation) => {
|
||||
assert!(
|
||||
(reservation.slot_index() as usize) < RING_SIZE,
|
||||
"reserved slot within ring"
|
||||
);
|
||||
Ok(Some(reservation))
|
||||
}
|
||||
Err(RingError::FullDropped { .. }) => {
|
||||
self.frames_dropped.fetch_add(1, Ordering::Relaxed);
|
||||
self.ring_full_events.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(None)
|
||||
}
|
||||
Err(_) => Err(EncoderAttachError::RingInitFailed),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn commit_blit_slot(
|
||||
&self,
|
||||
reservation: FillReservation<IoSurfaceSlotHandle>,
|
||||
capture_pts_us: i64,
|
||||
) -> Result<(), EncoderAttachError> {
|
||||
assert!(
|
||||
(reservation.slot_index() as usize) < RING_SIZE,
|
||||
"slot within ring"
|
||||
);
|
||||
let sequence = {
|
||||
let mut ring = self.ring.lock();
|
||||
let sequence = match ring.commit(reservation) {
|
||||
Ok(sequence) => sequence,
|
||||
Err(_) => return Err(EncoderAttachError::RingInitFailed),
|
||||
};
|
||||
self.record_capture_pts(sequence, capture_pts_us);
|
||||
sequence
|
||||
};
|
||||
assert!(sequence > 0, "committed sequence positive");
|
||||
self.frames_submitted.fetch_add(1, Ordering::Relaxed);
|
||||
self.ready_condvar.notify_one();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn wait_next_ready(&self, timeout: Duration) -> Option<EncoderReady<IoSurfaceSlotHandle>> {
|
||||
assert!(self.width > 0, "attachment width positive");
|
||||
let timeout_bound = Duration::from_micros(READY_WAIT_TIMEOUT_US_MAX);
|
||||
let bounded_timeout = timeout.min(timeout_bound);
|
||||
assert!(
|
||||
bounded_timeout <= timeout_bound,
|
||||
"wait bounded to one frame interval"
|
||||
);
|
||||
let mut ring = self.ring.lock();
|
||||
if let Some(ready) = ring.poll_next_ready() {
|
||||
return Some(ready);
|
||||
}
|
||||
if !self.attached.load(Ordering::Acquire) {
|
||||
return None;
|
||||
}
|
||||
let _ = self.ready_condvar.wait_for(&mut ring, bounded_timeout);
|
||||
ring.poll_next_ready()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub unsafe fn submit_external_surface(
|
||||
&self,
|
||||
surface: IoSurfaceRaw,
|
||||
capture_pts_us: i64,
|
||||
) -> Result<(), EncoderAttachError> {
|
||||
assert!(surface.as_ptr() as usize != 0, "external surface non-null");
|
||||
assert!(self.width > 0, "attachment width positive");
|
||||
if !self.attached.load(Ordering::Acquire) {
|
||||
unsafe { CFRelease(surface.as_ptr()) };
|
||||
return Err(EncoderAttachError::NotAttached);
|
||||
}
|
||||
unsafe { crate::iosurface_pair::iosurface_increment_use_count(surface) };
|
||||
let sequence = self.external_sequence.fetch_add(1, Ordering::AcqRel) + 1;
|
||||
assert!(sequence > 0, "external sequence starts at one");
|
||||
let frame = ExternalSurfaceFrame {
|
||||
surface,
|
||||
sequence,
|
||||
capture_pts_us,
|
||||
};
|
||||
let evicted = {
|
||||
let mut queue = self.external_surfaces.lock();
|
||||
let evicted = if queue.len() >= EXTERNAL_SURFACE_QUEUE_CAP {
|
||||
queue.pop_front()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
queue.push_back(frame);
|
||||
assert!(
|
||||
queue.len() <= EXTERNAL_SURFACE_QUEUE_CAP,
|
||||
"external queue bounded"
|
||||
);
|
||||
evicted
|
||||
};
|
||||
if let Some(oldest) = evicted {
|
||||
drop(oldest);
|
||||
self.frames_dropped.fetch_add(1, Ordering::Relaxed);
|
||||
self.ring_full_events.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
self.frames_submitted.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn take_external_surface(&self) -> Option<ExternalSurfaceFrame> {
|
||||
let frame = {
|
||||
let mut queue = self.external_surfaces.lock();
|
||||
assert!(
|
||||
queue.len() <= EXTERNAL_SURFACE_QUEUE_CAP,
|
||||
"external queue bounded"
|
||||
);
|
||||
queue.pop_front()
|
||||
};
|
||||
if let Some(ref taken) = frame {
|
||||
assert!(taken.sequence > 0, "popped frame has real sequence");
|
||||
}
|
||||
frame
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn external_surface_queue_len(&self) -> usize {
|
||||
let len = self.external_surfaces.lock().len();
|
||||
assert!(len <= EXTERNAL_SURFACE_QUEUE_CAP, "external queue bounded");
|
||||
assert!(self.width > 0, "attachment width intact");
|
||||
len
|
||||
}
|
||||
|
||||
fn record_capture_pts(&self, sequence: u64, capture_pts_us: i64) {
|
||||
assert!(sequence > 0, "ring sequences start at one");
|
||||
let mut map = self.capture_pts_by_sequence.lock();
|
||||
while map.len() >= CAPTURE_PTS_MAP_CAP {
|
||||
map.pop_front();
|
||||
}
|
||||
map.push_back((sequence, capture_pts_us));
|
||||
assert!(map.len() <= CAPTURE_PTS_MAP_CAP, "pts map bounded");
|
||||
}
|
||||
|
||||
pub fn capture_pts_us_for_sequence(&self, sequence: u64) -> Option<i64> {
|
||||
let map = self.capture_pts_by_sequence.lock();
|
||||
assert!(map.len() <= CAPTURE_PTS_MAP_CAP, "pts map bounded");
|
||||
map.iter()
|
||||
.find(|(seq, _)| *seq == sequence)
|
||||
.map(|(_, pts)| *pts)
|
||||
}
|
||||
|
||||
pub fn note_ring_full(&self) {
|
||||
self.ring_full_events.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn stats(&self) -> EncoderAttachStats {
|
||||
let stats = EncoderAttachStats {
|
||||
frames_submitted: self.frames_submitted.load(Ordering::Relaxed),
|
||||
frames_dropped: self.frames_dropped.load(Ordering::Relaxed),
|
||||
ring_full_events: self.ring_full_events.load(Ordering::Relaxed),
|
||||
failed_blits: self.failed_blits.load(Ordering::Relaxed),
|
||||
};
|
||||
assert!(
|
||||
stats.frames_dropped <= stats.ring_full_events + stats.frames_dropped,
|
||||
"drop counter consistent"
|
||||
);
|
||||
assert!(
|
||||
stats.frames_submitted <= u64::MAX / 2,
|
||||
"submitted within plausible bound"
|
||||
);
|
||||
stats
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn build_blit_destinations(
|
||||
ring: &mut EncoderInputRing<MetalSharedTextureBackend>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<Vec<BlitDestination>, EncoderAttachError> {
|
||||
assert!(width > 0, "blit destination width positive");
|
||||
assert!(height > 0, "blit destination height positive");
|
||||
let mut destinations: Vec<BlitDestination> = Vec::with_capacity(RING_SIZE);
|
||||
for slot_index in 0..RING_SIZE {
|
||||
let surface_ptr = ring
|
||||
.backend_mut()
|
||||
.slot_iosurface_ptr(slot_index as u32)
|
||||
.ok_or(EncoderAttachError::RingInitFailed)?;
|
||||
assert!(!surface_ptr.is_null(), "slot iosurface ptr non-null");
|
||||
let pixel_buffer = unsafe { VtPixelTransfer::wrap_iosurface(surface_ptr, width, height) }
|
||||
.map_err(|_| EncoderAttachError::RingInitFailed)?;
|
||||
destinations.push(BlitDestination { pixel_buffer });
|
||||
}
|
||||
assert_eq!(destinations.len(), RING_SIZE, "one destination per slot");
|
||||
Ok(destinations)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[link(name = "CoreVideo", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
fn CVPixelBufferRelease(buffer: *mut core::ffi::c_void);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[link(name = "CoreFoundation", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
fn CFRelease(cf: *const core::ffi::c_void);
|
||||
fn CFRetain(cf: *const core::ffi::c_void) -> *const core::ffi::c_void;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
unsafe fn release_pixel_buffer(pb: *mut core::ffi::c_void) {
|
||||
if !pb.is_null() {
|
||||
unsafe { CVPixelBufferRelease(pb) };
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EncoderAttachment {
|
||||
fn drop(&mut self) {
|
||||
self.detach();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_dimensions() {
|
||||
let err = EncoderAttachment::try_new(0, 720).err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Some(EncoderAttachError::InvalidDimensions { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn macos_attach_then_detach() {
|
||||
let attach = EncoderAttachment::try_new(640, 480).expect("attach ok");
|
||||
assert!(attach.is_attached());
|
||||
assert_eq!(attach.width(), 640);
|
||||
assert_eq!(attach.height(), 480);
|
||||
assert_eq!(attach.capacity(), 8);
|
||||
attach.detach();
|
||||
assert!(!attach.is_attached());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn macos_submit_records_stats() {
|
||||
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
|
||||
for _ in 0..3 {
|
||||
attach
|
||||
.submit_iosurface_frame(0xdead_beef)
|
||||
.expect("submit ok");
|
||||
}
|
||||
let stats = attach.stats();
|
||||
assert_eq!(stats.frames_submitted, 3);
|
||||
assert_eq!(stats.frames_dropped, 0);
|
||||
assert_eq!(stats.ring_full_events, 0);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn wait_next_ready_returns_submitted_frame() {
|
||||
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
|
||||
attach
|
||||
.submit_iosurface_frame(0xdead_beef)
|
||||
.expect("submit ok");
|
||||
let ready = attach
|
||||
.wait_next_ready(Duration::from_millis(5))
|
||||
.expect("frame ready");
|
||||
assert_eq!(ready.sequence, 1);
|
||||
assert_eq!(ready.duplicate_count, 0);
|
||||
assert!(attach.wait_next_ready(Duration::from_millis(1)).is_none());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn wait_next_ready_timeout_is_capped_at_frame_interval() {
|
||||
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
|
||||
let start = std::time::Instant::now();
|
||||
let ready = attach.wait_next_ready(Duration::from_secs(60));
|
||||
assert!(ready.is_none());
|
||||
assert!(
|
||||
start.elapsed() < Duration::from_millis(500),
|
||||
"wait returned within the named frame-interval bound"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn wait_next_ready_wakes_for_concurrent_submit() {
|
||||
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
|
||||
let waiter = {
|
||||
let attach = Arc::clone(&attach);
|
||||
std::thread::spawn(move || attach.wait_next_ready(Duration::from_millis(15)))
|
||||
};
|
||||
std::thread::sleep(Duration::from_millis(3));
|
||||
attach
|
||||
.submit_iosurface_frame(0xdead_beef)
|
||||
.expect("submit ok");
|
||||
let ready = waiter.join().expect("waiter joins");
|
||||
let sequence = ready.map(|r| r.sequence);
|
||||
assert!(sequence == Some(1) || sequence.is_none());
|
||||
if sequence.is_none() {
|
||||
let retry = attach.wait_next_ready(Duration::from_millis(5));
|
||||
assert_eq!(retry.map(|r| r.sequence), Some(1));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn wait_next_ready_returns_none_after_detach() {
|
||||
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
|
||||
attach.detach();
|
||||
assert!(!attach.is_attached());
|
||||
assert!(attach.wait_next_ready(Duration::from_millis(5)).is_none());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn macos_submit_rejected_when_detached() {
|
||||
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
|
||||
attach.detach();
|
||||
let err = attach.submit_iosurface_frame(0xdead_beef).err();
|
||||
assert!(matches!(err, Some(EncoderAttachError::NotAttached)));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn submit_encoder_ring_frame_blits_iosurface_to_slot() {
|
||||
use core::ptr::NonNull;
|
||||
use objc2_core_foundation::{CFDictionary, CFRetained};
|
||||
use objc2_core_video::{
|
||||
CVPixelBuffer, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
|
||||
CVPixelBufferGetHeightOfPlane, CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags,
|
||||
CVPixelBufferUnlockBaseAddress, kCVPixelBufferIOSurfacePropertiesKey,
|
||||
};
|
||||
|
||||
let attach = EncoderAttachment::try_new(256, 256).expect("attach ok");
|
||||
let mut empty_keys: [*const core::ffi::c_void; 0] = [];
|
||||
let mut empty_vals: [*const core::ffi::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 { kCVPixelBufferIOSurfacePropertiesKey };
|
||||
let key_ptr: *const core::ffi::c_void = key_ref as *const _ as *const core::ffi::c_void;
|
||||
let val_ptr: *const core::ffi::c_void =
|
||||
&*iosurf_dict as *const _ as *const core::ffi::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 nv12: u32 = u32::from_be_bytes(*b"420v");
|
||||
let mut pb_out: *mut CVPixelBuffer = core::ptr::null_mut();
|
||||
let status = unsafe {
|
||||
objc2_core_video::CVPixelBufferCreate(
|
||||
None,
|
||||
256,
|
||||
256,
|
||||
nv12,
|
||||
Some(&attrs),
|
||||
NonNull::new(&mut pb_out).expect("pb_out non-null"),
|
||||
)
|
||||
};
|
||||
assert_eq!(status, 0, "CVPixelBufferCreate ok");
|
||||
let source_pb: CFRetained<CVPixelBuffer> =
|
||||
unsafe { CFRetained::from_raw(NonNull::new(pb_out).expect("pb non-null")) };
|
||||
let lock_flags = CVPixelBufferLockFlags(0);
|
||||
let lock_st = unsafe { CVPixelBufferLockBaseAddress(&source_pb, lock_flags) };
|
||||
assert_eq!(lock_st, 0, "lock ok");
|
||||
let y_ptr = CVPixelBufferGetBaseAddressOfPlane(&source_pb, 0);
|
||||
let y_stride = CVPixelBufferGetBytesPerRowOfPlane(&source_pb, 0);
|
||||
let y_h = CVPixelBufferGetHeightOfPlane(&source_pb, 0);
|
||||
assert!(!y_ptr.is_null(), "Y plane base non-null");
|
||||
assert!(y_stride > 0, "Y stride positive");
|
||||
for row in 0..y_h {
|
||||
for col in 0..y_stride {
|
||||
unsafe {
|
||||
(y_ptr as *mut u8).add(row * y_stride + col).write(0x55);
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = unsafe { CVPixelBufferUnlockBaseAddress(&source_pb, lock_flags) };
|
||||
let src_ptr = &*source_pb as *const CVPixelBuffer as *mut core::ffi::c_void;
|
||||
attach
|
||||
.submit_with_blit(src_ptr, 41_500)
|
||||
.expect("submit_with_blit ok");
|
||||
let stats = attach.stats();
|
||||
assert_eq!(stats.frames_submitted, 1, "one frame submitted");
|
||||
assert_eq!(stats.failed_blits, 0, "no failed blits");
|
||||
assert_eq!(
|
||||
attach.capture_pts_us_for_sequence(1),
|
||||
Some(41_500),
|
||||
"capture pts recorded for ring sequence"
|
||||
);
|
||||
assert_eq!(attach.capture_pts_us_for_sequence(2), None);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn external_surface_submit_take_round_trip() {
|
||||
use core::ptr::NonNull;
|
||||
use fluxer_encoder_ring::metal_iosurface_macos::OwnedIoSurface;
|
||||
|
||||
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
|
||||
let owned = OwnedIoSurface::create_nv12(64, 64).expect("surface");
|
||||
let raw = NonNull::new(owned.as_ptr()).expect("non-null surface");
|
||||
let use_before = unsafe { crate::iosurface_pair::iosurface_use_count(raw) };
|
||||
unsafe { CFRetain(raw.as_ptr()) };
|
||||
unsafe { attach.submit_external_surface(raw, 41_500) }.expect("submit ok");
|
||||
assert_eq!(attach.external_surface_queue_len(), 1);
|
||||
let use_during = unsafe { crate::iosurface_pair::iosurface_use_count(raw) };
|
||||
assert_eq!(use_during, use_before + 1);
|
||||
let stats = attach.stats();
|
||||
assert_eq!(stats.frames_submitted, 1);
|
||||
assert_eq!(stats.frames_dropped, 0);
|
||||
assert_eq!(stats.ring_full_events, 0);
|
||||
let frame = attach.take_external_surface().expect("frame queued");
|
||||
assert_eq!(frame.surface(), raw);
|
||||
assert_eq!(frame.sequence(), 1);
|
||||
assert_eq!(frame.capture_pts_us(), 41_500);
|
||||
drop(frame);
|
||||
let use_after = unsafe { crate::iosurface_pair::iosurface_use_count(raw) };
|
||||
assert_eq!(use_after, use_before);
|
||||
assert_eq!(attach.external_surface_queue_len(), 0);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn external_surface_queue_evicts_oldest_when_full() {
|
||||
use core::ptr::NonNull;
|
||||
use fluxer_encoder_ring::metal_iosurface_macos::OwnedIoSurface;
|
||||
|
||||
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
|
||||
let total = EXTERNAL_SURFACE_QUEUE_CAP + 2;
|
||||
let mut owned: Vec<OwnedIoSurface> = Vec::with_capacity(total);
|
||||
for i in 0..total {
|
||||
let surface = OwnedIoSurface::create_nv12(64, 64).expect("surface");
|
||||
let raw = NonNull::new(surface.as_ptr()).expect("non-null surface");
|
||||
unsafe { CFRetain(raw.as_ptr()) };
|
||||
unsafe { attach.submit_external_surface(raw, (i as i64) * 1_000) }.expect("submit ok");
|
||||
owned.push(surface);
|
||||
}
|
||||
assert_eq!(
|
||||
attach.external_surface_queue_len(),
|
||||
EXTERNAL_SURFACE_QUEUE_CAP
|
||||
);
|
||||
let stats = attach.stats();
|
||||
assert_eq!(stats.frames_submitted, total as u64);
|
||||
assert_eq!(stats.frames_dropped, 2);
|
||||
assert_eq!(stats.ring_full_events, 2);
|
||||
let oldest_remaining = attach.take_external_surface().expect("frame queued");
|
||||
assert_eq!(oldest_remaining.sequence(), 3);
|
||||
assert_eq!(oldest_remaining.capture_pts_us(), 2_000);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn detach_drains_external_surface_queue_and_rejects_submit() {
|
||||
use core::ptr::NonNull;
|
||||
use fluxer_encoder_ring::metal_iosurface_macos::OwnedIoSurface;
|
||||
|
||||
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
|
||||
let owned = OwnedIoSurface::create_nv12(64, 64).expect("surface");
|
||||
let raw = NonNull::new(owned.as_ptr()).expect("non-null surface");
|
||||
let use_before = unsafe { crate::iosurface_pair::iosurface_use_count(raw) };
|
||||
unsafe { CFRetain(raw.as_ptr()) };
|
||||
unsafe { attach.submit_external_surface(raw, 7) }.expect("submit ok");
|
||||
assert_eq!(attach.external_surface_queue_len(), 1);
|
||||
attach.detach();
|
||||
assert_eq!(attach.external_surface_queue_len(), 0);
|
||||
assert_eq!(
|
||||
unsafe { crate::iosurface_pair::iosurface_use_count(raw) },
|
||||
use_before
|
||||
);
|
||||
unsafe { CFRetain(raw.as_ptr()) };
|
||||
let err = unsafe { attach.submit_external_surface(raw, 8) }.err();
|
||||
assert!(matches!(err, Some(EncoderAttachError::NotAttached)));
|
||||
assert_eq!(attach.external_surface_queue_len(), 0);
|
||||
assert_eq!(
|
||||
unsafe { crate::iosurface_pair::iosurface_use_count(raw) },
|
||||
use_before
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn capture_pts_map_stays_bounded_and_evicts_oldest() {
|
||||
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
|
||||
let total = (CAPTURE_PTS_MAP_CAP as u64) + 4;
|
||||
for sequence in 1..=total {
|
||||
attach.record_capture_pts(sequence, (sequence as i64) * 1_000);
|
||||
}
|
||||
assert_eq!(
|
||||
attach.capture_pts_us_for_sequence(1),
|
||||
None,
|
||||
"oldest evicted"
|
||||
);
|
||||
assert_eq!(
|
||||
attach.capture_pts_us_for_sequence(4),
|
||||
None,
|
||||
"oldest evicted"
|
||||
);
|
||||
assert_eq!(
|
||||
attach.capture_pts_us_for_sequence(5),
|
||||
Some(5_000),
|
||||
"newest retained"
|
||||
);
|
||||
assert_eq!(
|
||||
attach.capture_pts_us_for_sequence(total),
|
||||
Some((total as i64) * 1_000)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use objc2_foundation::{NSError, NSProcessInfo};
|
||||
|
||||
pub fn operating_system_version_string() -> String {
|
||||
let info = NSProcessInfo::processInfo();
|
||||
info.operatingSystemVersionString().to_string()
|
||||
}
|
||||
|
||||
pub fn ns_error_localized_description(err: &NSError) -> String {
|
||||
err.localizedDescription().to_string()
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use core::ffi::c_void;
|
||||
use core::ptr::NonNull;
|
||||
|
||||
pub type IoSurfaceRaw = NonNull<c_void>;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[link(name = "IOSurface", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
fn IOSurfaceIncrementUseCount(buffer: *mut c_void);
|
||||
fn IOSurfaceDecrementUseCount(buffer: *mut c_void);
|
||||
fn IOSurfaceGetUseCount(buffer: *mut c_void) -> i32;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub unsafe fn iosurface_increment_use_count(surface: IoSurfaceRaw) {
|
||||
unsafe { IOSurfaceIncrementUseCount(surface.as_ptr()) };
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub unsafe fn iosurface_decrement_use_count(surface: IoSurfaceRaw) {
|
||||
unsafe { IOSurfaceDecrementUseCount(surface.as_ptr()) };
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub unsafe fn iosurface_use_count(surface: IoSurfaceRaw) -> i32 {
|
||||
unsafe { IOSurfaceGetUseCount(surface.as_ptr()) }
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub unsafe fn iosurface_increment_use_count(_surface: IoSurfaceRaw) {}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub unsafe fn iosurface_decrement_use_count(_surface: IoSurfaceRaw) {}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub unsafe fn iosurface_use_count(_surface: IoSurfaceRaw) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
pub struct IoSurfacePair {
|
||||
current: Option<IoSurfaceRaw>,
|
||||
prev: Option<IoSurfaceRaw>,
|
||||
}
|
||||
|
||||
unsafe impl Send for IoSurfacePair {}
|
||||
|
||||
impl IoSurfacePair {
|
||||
pub fn new() -> Self {
|
||||
let pair = Self {
|
||||
current: None,
|
||||
prev: None,
|
||||
};
|
||||
assert!(pair.current.is_none());
|
||||
assert!(pair.prev.is_none());
|
||||
pair
|
||||
}
|
||||
|
||||
pub fn has_current(&self) -> bool {
|
||||
let has = self.current.is_some();
|
||||
assert!(has == self.current.is_some());
|
||||
has
|
||||
}
|
||||
|
||||
pub fn has_prev(&self) -> bool {
|
||||
let has = self.prev.is_some();
|
||||
assert!(has == self.prev.is_some());
|
||||
has
|
||||
}
|
||||
|
||||
pub unsafe fn push(&mut self, new: IoSurfaceRaw) {
|
||||
unsafe { iosurface_increment_use_count(new) };
|
||||
let evicted = self.prev.take();
|
||||
let rotated = self.current.take();
|
||||
self.prev = rotated;
|
||||
self.current = Some(new);
|
||||
assert!(self.current.is_some());
|
||||
if let Some(old) = evicted {
|
||||
unsafe { iosurface_decrement_use_count(old) };
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_current(&mut self) -> Option<IoSurfaceRaw> {
|
||||
let taken = self.current.take();
|
||||
assert!(self.current.is_none());
|
||||
taken
|
||||
}
|
||||
|
||||
pub fn peek_current(&self) -> Option<IoSurfaceRaw> {
|
||||
self.current
|
||||
}
|
||||
|
||||
pub fn peek_prev(&self) -> Option<IoSurfaceRaw> {
|
||||
self.prev
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
let cur = self.current.take();
|
||||
let prev = self.prev.take();
|
||||
assert!(self.current.is_none());
|
||||
assert!(self.prev.is_none());
|
||||
if let Some(s) = cur {
|
||||
unsafe { iosurface_decrement_use_count(s) };
|
||||
}
|
||||
if let Some(s) = prev {
|
||||
unsafe { iosurface_decrement_use_count(s) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IoSurfacePair {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for IoSurfacePair {
|
||||
fn drop(&mut self) {
|
||||
let cur = self.current.take();
|
||||
let prev = self.prev.take();
|
||||
if let Some(s) = cur {
|
||||
unsafe { iosurface_decrement_use_count(s) };
|
||||
}
|
||||
if let Some(s) = prev {
|
||||
unsafe { iosurface_decrement_use_count(s) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core::ptr::NonNull;
|
||||
|
||||
fn fake_surface(addr: usize) -> IoSurfaceRaw {
|
||||
assert!(addr != 0);
|
||||
NonNull::new(addr as *mut c_void).expect("non-null fake surface")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_pair_is_empty() {
|
||||
let pair = IoSurfacePair::new();
|
||||
assert!(!pair.has_current());
|
||||
assert!(!pair.has_prev());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_current_on_empty_returns_none() {
|
||||
let mut pair = IoSurfacePair::new();
|
||||
assert!(pair.take_current().is_none());
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[test]
|
||||
fn push_rotates_current_to_prev_offplatform() {
|
||||
let mut pair = IoSurfacePair::new();
|
||||
let a = fake_surface(0x1);
|
||||
let b = fake_surface(0x2);
|
||||
unsafe {
|
||||
pair.push(a);
|
||||
}
|
||||
assert_eq!(pair.peek_current(), Some(a));
|
||||
assert!(pair.peek_prev().is_none());
|
||||
unsafe {
|
||||
pair.push(b);
|
||||
}
|
||||
assert_eq!(pair.peek_current(), Some(b));
|
||||
assert_eq!(pair.peek_prev(), Some(a));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[test]
|
||||
fn take_current_clears_slot_offplatform() {
|
||||
let mut pair = IoSurfacePair::new();
|
||||
let a = fake_surface(0x10);
|
||||
unsafe {
|
||||
pair.push(a);
|
||||
}
|
||||
let taken = pair.take_current().expect("current is set");
|
||||
assert_eq!(taken, a);
|
||||
assert!(pair.peek_current().is_none());
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[test]
|
||||
fn drop_pair_is_safe_offplatform() {
|
||||
let mut pair = IoSurfacePair::new();
|
||||
unsafe {
|
||||
pair.push(fake_surface(0x100));
|
||||
pair.push(fake_surface(0x200));
|
||||
}
|
||||
drop(pair);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "macos"))]
|
||||
mod macos_tests {
|
||||
use super::*;
|
||||
use core::ffi::c_void;
|
||||
use core::ptr::NonNull;
|
||||
use objc2_core_foundation::{
|
||||
CFDictionary, CFNumber, CFRetained, CFString, kCFAllocatorDefault,
|
||||
kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks,
|
||||
};
|
||||
|
||||
#[link(name = "IOSurface", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
fn IOSurfaceCreate(properties: *const CFDictionary) -> *mut c_void;
|
||||
}
|
||||
|
||||
fn cf_number_i32(v: i32) -> CFRetained<CFNumber> {
|
||||
CFNumber::new_i32(v)
|
||||
}
|
||||
|
||||
fn cf_string(s: &'static str) -> CFRetained<CFString> {
|
||||
CFString::from_static_str(s)
|
||||
}
|
||||
|
||||
fn make_iosurface() -> IoSurfaceRaw {
|
||||
let width_key = cf_string("IOSurfaceWidth");
|
||||
let height_key = cf_string("IOSurfaceHeight");
|
||||
let bpe_key = cf_string("IOSurfaceBytesPerElement");
|
||||
let pf_key = cf_string("IOSurfacePixelFormat");
|
||||
let width_val = cf_number_i32(32);
|
||||
let height_val = cf_number_i32(32);
|
||||
let bpe_val = cf_number_i32(4);
|
||||
let pf_val = cf_number_i32(i32::from_be_bytes(*b"BGRA"));
|
||||
let keys: [*const c_void; 4] = [
|
||||
CFRetained::as_ptr(&width_key).as_ptr() as *const c_void,
|
||||
CFRetained::as_ptr(&height_key).as_ptr() as *const c_void,
|
||||
CFRetained::as_ptr(&bpe_key).as_ptr() as *const c_void,
|
||||
CFRetained::as_ptr(&pf_key).as_ptr() as *const c_void,
|
||||
];
|
||||
let vals: [*const c_void; 4] = [
|
||||
CFRetained::as_ptr(&width_val).as_ptr() as *const c_void,
|
||||
CFRetained::as_ptr(&height_val).as_ptr() as *const c_void,
|
||||
CFRetained::as_ptr(&bpe_val).as_ptr() as *const c_void,
|
||||
CFRetained::as_ptr(&pf_val).as_ptr() as *const c_void,
|
||||
];
|
||||
let dict_opt = unsafe {
|
||||
CFDictionary::new(
|
||||
kCFAllocatorDefault,
|
||||
keys.as_ptr() as *mut *const c_void,
|
||||
vals.as_ptr() as *mut *const c_void,
|
||||
4,
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks,
|
||||
)
|
||||
};
|
||||
let dict = dict_opt.expect("CFDictionary::new returned non-null");
|
||||
let dict_ptr = CFRetained::as_ptr(&dict).as_ptr() as *const CFDictionary;
|
||||
let raw = unsafe { IOSurfaceCreate(dict_ptr) };
|
||||
assert!(!raw.is_null(), "IOSurfaceCreate must produce a surface");
|
||||
NonNull::new(raw).expect("non-null IOSurface")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_increments_use_count_and_take_current_does_not_release() {
|
||||
let a = make_iosurface();
|
||||
let before = unsafe { iosurface_use_count(a) };
|
||||
let mut pair = IoSurfacePair::new();
|
||||
unsafe {
|
||||
pair.push(a);
|
||||
}
|
||||
let after_push = unsafe { iosurface_use_count(a) };
|
||||
assert_eq!(after_push, before + 1);
|
||||
let taken = pair.take_current().expect("current set");
|
||||
assert_eq!(taken, a);
|
||||
let after_take = unsafe { iosurface_use_count(a) };
|
||||
assert_eq!(after_take, before + 1);
|
||||
unsafe { iosurface_decrement_use_count(a) };
|
||||
let after_balance = unsafe { iosurface_use_count(a) };
|
||||
assert_eq!(after_balance, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_rotates_and_drop_releases_both_slots() {
|
||||
let a = make_iosurface();
|
||||
let b = make_iosurface();
|
||||
let before_a = unsafe { iosurface_use_count(a) };
|
||||
let before_b = unsafe { iosurface_use_count(b) };
|
||||
let mut pair = IoSurfacePair::new();
|
||||
unsafe {
|
||||
pair.push(a);
|
||||
pair.push(b);
|
||||
}
|
||||
assert_eq!(pair.peek_current(), Some(b));
|
||||
assert_eq!(pair.peek_prev(), Some(a));
|
||||
assert_eq!(unsafe { iosurface_use_count(a) }, before_a + 1);
|
||||
assert_eq!(unsafe { iosurface_use_count(b) }, before_b + 1);
|
||||
drop(pair);
|
||||
assert_eq!(unsafe { iosurface_use_count(a) }, before_a);
|
||||
assert_eq!(unsafe { iosurface_use_count(b) }, before_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_push_evicts_oldest_and_releases_it() {
|
||||
let a = make_iosurface();
|
||||
let b = make_iosurface();
|
||||
let c = make_iosurface();
|
||||
let before_a = unsafe { iosurface_use_count(a) };
|
||||
let mut pair = IoSurfacePair::new();
|
||||
unsafe {
|
||||
pair.push(a);
|
||||
pair.push(b);
|
||||
pair.push(c);
|
||||
}
|
||||
assert_eq!(unsafe { iosurface_use_count(a) }, before_a);
|
||||
assert_eq!(pair.peek_current(), Some(c));
|
||||
assert_eq!(pair.peek_prev(), Some(b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_releases_both_slots() {
|
||||
let a = make_iosurface();
|
||||
let b = make_iosurface();
|
||||
let before_a = unsafe { iosurface_use_count(a) };
|
||||
let before_b = unsafe { iosurface_use_count(b) };
|
||||
let mut pair = IoSurfacePair::new();
|
||||
unsafe {
|
||||
pair.push(a);
|
||||
pair.push(b);
|
||||
}
|
||||
pair.clear();
|
||||
assert_eq!(unsafe { iosurface_use_count(a) }, before_a);
|
||||
assert_eq!(unsafe { iosurface_use_count(b) }, before_b);
|
||||
assert!(pair.peek_current().is_none());
|
||||
assert!(pair.peek_prev().is_none());
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,10 @@
|
||||
#![deny(clippy::all)]
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
#![allow(dead_code)]
|
||||
#![allow(clippy::missing_safety_doc)]
|
||||
#![allow(clippy::collapsible_if)]
|
||||
#![allow(clippy::manual_is_multiple_of)]
|
||||
#![allow(clippy::manual_slice_size_calculation)]
|
||||
#![allow(clippy::unnecessary_cast)]
|
||||
#![allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
#![allow(clippy::missing_transmute_annotations)]
|
||||
#![allow(clippy::missing_const_for_thread_local)]
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
pub mod audio_pool;
|
||||
pub mod config;
|
||||
pub mod encoder_attach;
|
||||
pub mod iosurface_pair;
|
||||
pub mod os_version;
|
||||
pub mod permission_probe;
|
||||
|
||||
pub use audio_pool::{
|
||||
MAC_AUDIO_POOL_CAP, MAX_FRAME_BYTES_PER_SLOT, MacAudioError, MacAudioFramePool,
|
||||
MacAudioPoolStats, PooledMacAudioFrame,
|
||||
};
|
||||
pub use config::{
|
||||
AUDIO_CHANNEL_COUNT_DEFAULT, AUDIO_CHANNEL_COUNT_MAX, AUDIO_CHANNEL_COUNT_MIN,
|
||||
AUDIO_SAMPLE_RATE_DEFAULT_HZ, AUDIO_SAMPLE_RATE_MAX_HZ, AUDIO_SAMPLE_RATE_MIN_HZ,
|
||||
AudioSampleFormat, CaptureFailureSurface, FPS_DEFAULT, FPS_MAX, FPS_MIN,
|
||||
MacScreenShareAudioFrame, MacScreenShareAudioFrameWithBytes, QUEUE_DEPTH_DEFAULT,
|
||||
QUEUE_DEPTH_MAX, QUEUE_DEPTH_MIN, SckCaptureConfig, SckCaptureConfigBuilder, SckCaptureFailure,
|
||||
SckColorSpace, SckError, SckPixelFormat,
|
||||
};
|
||||
pub use encoder_attach::{EncoderAttachError, EncoderAttachStats, EncoderAttachment};
|
||||
pub use iosurface_pair::{IoSurfacePair, IoSurfaceRaw};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod foundation;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod sck;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(all(target_os = "macos", not(test)))]
|
||||
mod napi_surface_macos;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(all(not(target_os = "macos"), not(test)))]
|
||||
mod napi_surface_stub;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,87 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use napi::bindgen_prelude::{Error, Result, Status};
|
||||
use napi_derive::napi;
|
||||
|
||||
fn unsupported() -> Error {
|
||||
Error::new(
|
||||
Status::GenericFailure,
|
||||
"@fluxer/mac-screen-capture is only supported on macOS",
|
||||
)
|
||||
}
|
||||
|
||||
#[napi(object, js_name = "MacScreenCaptureSource")]
|
||||
pub struct MacScreenCaptureSource {
|
||||
pub kind: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub app_name: Option<String>,
|
||||
pub bundle_id: Option<String>,
|
||||
pub target_pid: Option<i32>,
|
||||
}
|
||||
|
||||
#[napi(js_name = "listSources")]
|
||||
pub fn list_sources() -> Result<Vec<MacScreenCaptureSource>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
#[napi(object, js_name = "MacScreenCaptureBackendSckAvailability")]
|
||||
pub struct SckAvailability {
|
||||
pub supported: bool,
|
||||
pub macos_version: Option<String>,
|
||||
}
|
||||
|
||||
#[napi(object, js_name = "MacScreenCaptureBackendAvailability")]
|
||||
pub struct BackendAvailability {
|
||||
pub sck: SckAvailability,
|
||||
pub screen_permission: String,
|
||||
}
|
||||
|
||||
#[napi(js_name = "getBackendAvailability")]
|
||||
pub fn get_backend_availability() -> Result<BackendAvailability> {
|
||||
Err(unsupported())
|
||||
}
|
||||
|
||||
#[napi(js_name = "probeScreenRecordingAccess")]
|
||||
pub fn probe_screen_recording_access() -> String {
|
||||
crate::permission_probe::PROBE_UNSUPPORTED.to_owned()
|
||||
}
|
||||
|
||||
#[napi(object, js_name = "MacScreenCaptureBackendInfo")]
|
||||
pub struct MacScreenCaptureBackendInfo {
|
||||
pub backend: String,
|
||||
pub supported: bool,
|
||||
pub reason: String,
|
||||
#[napi(js_name = "minMacosVersion")]
|
||||
pub min_macos_version: String,
|
||||
#[napi(js_name = "detectedMacosVersion")]
|
||||
pub detected_macos_version: Option<String>,
|
||||
#[napi(js_name = "sckAvailable")]
|
||||
pub sck_available: bool,
|
||||
}
|
||||
|
||||
#[napi(js_name = "getBackendInfo")]
|
||||
pub fn get_backend_info() -> MacScreenCaptureBackendInfo {
|
||||
use crate::os_version::{SCK_MIN_MACOS, format_version};
|
||||
MacScreenCaptureBackendInfo {
|
||||
backend: "mac-screen-capture".to_owned(),
|
||||
supported: false,
|
||||
reason: "@fluxer/mac-screen-capture is only supported on macOS".to_owned(),
|
||||
min_macos_version: format_version(SCK_MIN_MACOS),
|
||||
detected_macos_version: None,
|
||||
sck_available: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct ScreenCapture;
|
||||
|
||||
#[napi]
|
||||
impl ScreenCapture {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Result<Self> {
|
||||
Err(unsupported())
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub const SCK_MIN_MACOS: (i64, i64, i64) = (12, 3, 0);
|
||||
|
||||
pub fn meets_floor(version: (i64, i64, i64), floor: (i64, i64, i64)) -> bool {
|
||||
if version.0 != floor.0 {
|
||||
return version.0 > floor.0;
|
||||
}
|
||||
if version.1 != floor.1 {
|
||||
return version.1 > floor.1;
|
||||
}
|
||||
version.2 >= floor.2
|
||||
}
|
||||
|
||||
pub fn format_version(version: (i64, i64, i64)) -> String {
|
||||
if version.2 == 0 {
|
||||
format!("{}.{}", version.0, version.1)
|
||||
} else {
|
||||
format!("{}.{}.{}", version.0, version.1, version.2)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn current_macos_version() -> Option<(i64, i64, i64)> {
|
||||
use objc2_foundation::NSProcessInfo;
|
||||
let info = NSProcessInfo::processInfo();
|
||||
let v = info.operatingSystemVersion();
|
||||
Some((
|
||||
v.majorVersion as i64,
|
||||
v.minorVersion as i64,
|
||||
v.patchVersion as i64,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn current_macos_version() -> Option<(i64, i64, i64)> {
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SupportClassification {
|
||||
pub supported: bool,
|
||||
pub sck_available: bool,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
pub fn classify_support(detected: Option<(i64, i64, i64)>) -> SupportClassification {
|
||||
let min_sck = format_version(SCK_MIN_MACOS);
|
||||
match detected {
|
||||
None => SupportClassification {
|
||||
supported: false,
|
||||
sck_available: false,
|
||||
reason: "mac-screen-capture could not detect the running macOS version. \
|
||||
Native screen capture unavailable."
|
||||
.to_owned(),
|
||||
},
|
||||
Some(v) => {
|
||||
let detected_str = format_version(v);
|
||||
let sck_ok = meets_floor(v, SCK_MIN_MACOS);
|
||||
let reason = if sck_ok {
|
||||
format!(
|
||||
"mac-screen-capture supported on macOS {detected_str} \
|
||||
(ScreenCaptureKit, requires macOS {min_sck}+)."
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"mac-screen-capture requires macOS {min_sck}+ (ScreenCaptureKit). \
|
||||
This Mac is running macOS {detected_str}. Native cursor-hidden \
|
||||
screen capture unavailable; fall back to getDisplayMedia."
|
||||
)
|
||||
};
|
||||
SupportClassification {
|
||||
supported: sck_ok,
|
||||
sck_available: sck_ok,
|
||||
reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use objc2::rc::Retained;
|
||||
use objc2_core_foundation::{CFString, CGFloat, CGRect};
|
||||
use objc2_core_media::{CMTime, CMTimeFlags};
|
||||
use objc2_foundation::NSString;
|
||||
use objc2_screen_capture_kit::{
|
||||
SCCaptureDynamicRange, SCContentFilter, SCDisplay, SCRunningApplication, SCStream,
|
||||
SCStreamConfiguration, SCWindow,
|
||||
};
|
||||
|
||||
use crate::config::{SckCaptureConfig, SckColorSpace, SckPixelFormat};
|
||||
|
||||
pub use objc2_screen_capture_kit::SCStreamOutputType;
|
||||
|
||||
pub fn cmtime_seconds(value: i64, timescale: i32) -> CMTime {
|
||||
CMTime {
|
||||
value,
|
||||
timescale,
|
||||
flags: CMTimeFlags(1),
|
||||
epoch: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sc_running_application_process_id(app: &SCRunningApplication) -> i32 {
|
||||
unsafe { app.processID() }
|
||||
}
|
||||
|
||||
pub fn sc_running_application_bundle_identifier(app: &SCRunningApplication) -> Retained<NSString> {
|
||||
unsafe { app.bundleIdentifier() }
|
||||
}
|
||||
|
||||
pub fn sc_running_application_name(app: &SCRunningApplication) -> Retained<NSString> {
|
||||
unsafe { app.applicationName() }
|
||||
}
|
||||
|
||||
pub fn sc_display_frame(display: &SCDisplay) -> CGRect {
|
||||
unsafe { display.frame() }
|
||||
}
|
||||
|
||||
pub fn sc_display_display_id(display: &SCDisplay) -> u32 {
|
||||
unsafe { display.displayID() }
|
||||
}
|
||||
|
||||
pub fn sc_display_width(display: &SCDisplay) -> isize {
|
||||
unsafe { display.width() }
|
||||
}
|
||||
|
||||
pub fn sc_display_height(display: &SCDisplay) -> isize {
|
||||
unsafe { display.height() }
|
||||
}
|
||||
|
||||
pub fn sc_window_window_id(win: &SCWindow) -> u32 {
|
||||
unsafe { win.windowID() }
|
||||
}
|
||||
|
||||
pub fn sc_window_owning_application(win: &SCWindow) -> Option<Retained<SCRunningApplication>> {
|
||||
unsafe { win.owningApplication() }
|
||||
}
|
||||
|
||||
pub fn sc_window_frame(win: &SCWindow) -> CGRect {
|
||||
unsafe { win.frame() }
|
||||
}
|
||||
|
||||
pub fn sc_window_title(win: &SCWindow) -> Option<Retained<NSString>> {
|
||||
unsafe { win.title() }
|
||||
}
|
||||
|
||||
pub fn sc_window_is_on_screen(win: &SCWindow) -> bool {
|
||||
unsafe { win.isOnScreen() }
|
||||
}
|
||||
|
||||
pub fn filter_content_rect_if_available(filter: &SCContentFilter) -> Option<CGRect> {
|
||||
use objc2::runtime::NSObjectProtocol;
|
||||
use objc2::{msg_send, sel};
|
||||
let obj: &objc2::runtime::NSObject = filter.as_ref();
|
||||
if !obj.respondsToSelector(sel!(contentRect)) {
|
||||
return None;
|
||||
}
|
||||
let rect: CGRect = unsafe { msg_send![obj, contentRect] };
|
||||
if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
Some(rect)
|
||||
}
|
||||
|
||||
pub fn filter_point_pixel_scale_if_available(filter: &SCContentFilter) -> Option<f32> {
|
||||
use objc2::runtime::NSObjectProtocol;
|
||||
use objc2::{msg_send, sel};
|
||||
let obj: &objc2::runtime::NSObject = filter.as_ref();
|
||||
if !obj.respondsToSelector(sel!(pointPixelScale)) {
|
||||
return None;
|
||||
}
|
||||
let scale: CGFloat = unsafe { msg_send![obj, pointPixelScale] };
|
||||
if !scale.is_finite() || scale <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
Some(scale as f32)
|
||||
}
|
||||
|
||||
pub fn cfg_set_scales_to_fit_if_available(cfg: &SCStreamConfiguration, scales_to_fit: bool) {
|
||||
use objc2::runtime::NSObjectProtocol;
|
||||
use objc2::{msg_send, sel};
|
||||
let obj: &objc2::runtime::NSObject = cfg.as_ref();
|
||||
if obj.respondsToSelector(sel!(setScalesToFit:)) {
|
||||
unsafe {
|
||||
let _: () = msg_send![obj, setScalesToFit: scales_to_fit];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cfg_set_width(cfg: &SCStreamConfiguration, v: usize) {
|
||||
unsafe { cfg.setWidth(v) }
|
||||
}
|
||||
pub fn cfg_set_height(cfg: &SCStreamConfiguration, v: usize) {
|
||||
unsafe { cfg.setHeight(v) }
|
||||
}
|
||||
pub fn cfg_set_queue_depth(cfg: &SCStreamConfiguration, v: isize) {
|
||||
unsafe { cfg.setQueueDepth(v) }
|
||||
}
|
||||
pub fn cfg_set_shows_cursor(cfg: &SCStreamConfiguration, v: bool) {
|
||||
unsafe { cfg.setShowsCursor(v) }
|
||||
}
|
||||
pub fn cfg_set_minimum_frame_interval(cfg: &SCStreamConfiguration, t: CMTime) {
|
||||
unsafe { cfg.setMinimumFrameInterval(t) }
|
||||
}
|
||||
pub fn cfg_set_pixel_format(cfg: &SCStreamConfiguration, format: u32) {
|
||||
unsafe { cfg.setPixelFormat(format) }
|
||||
}
|
||||
|
||||
pub fn cfg_set_source_rect_if_available(cfg: &SCStreamConfiguration, rect: CGRect) -> bool {
|
||||
use objc2::runtime::NSObjectProtocol;
|
||||
use objc2::{msg_send, sel};
|
||||
assert!(rect.size.width > 0.0);
|
||||
assert!(rect.size.height > 0.0);
|
||||
let obj: &objc2::runtime::NSObject = cfg.as_ref();
|
||||
if !obj.respondsToSelector(sel!(setSourceRect:)) {
|
||||
return false;
|
||||
}
|
||||
unsafe {
|
||||
let _: () = msg_send![obj, setSourceRect: rect];
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn cfg_set_capture_dynamic_range_sdr_if_available(cfg: &SCStreamConfiguration) {
|
||||
use objc2::runtime::NSObjectProtocol;
|
||||
use objc2::sel;
|
||||
let obj: &objc2::runtime::NSObject = cfg.as_ref();
|
||||
if obj.respondsToSelector(sel!(setCaptureDynamicRange:)) {
|
||||
unsafe {
|
||||
cfg.setCaptureDynamicRange(SCCaptureDynamicRange::SDR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cfg_set_capture_dynamic_range_hdr_if_available(cfg: &SCStreamConfiguration) {
|
||||
use objc2::runtime::NSObjectProtocol;
|
||||
use objc2::sel;
|
||||
let obj: &objc2::runtime::NSObject = cfg.as_ref();
|
||||
if obj.respondsToSelector(sel!(setCaptureDynamicRange:)) {
|
||||
unsafe {
|
||||
cfg.setCaptureDynamicRange(SCCaptureDynamicRange::HDRLocalDisplay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cfg_set_color_space_name_if_available(cfg: &SCStreamConfiguration, name: &str) {
|
||||
use objc2::runtime::NSObjectProtocol;
|
||||
use objc2::sel;
|
||||
assert!(!name.is_empty());
|
||||
let obj: &objc2::runtime::NSObject = cfg.as_ref();
|
||||
if obj.respondsToSelector(sel!(setColorSpaceName:)) {
|
||||
let cf = CFString::from_str(name);
|
||||
unsafe {
|
||||
cfg.setColorSpaceName(&cf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn obs_minimum_frame_interval(target_fps: u32) -> CMTime {
|
||||
assert!(target_fps >= crate::config::FPS_MIN);
|
||||
assert!(target_fps <= crate::config::FPS_MAX);
|
||||
CMTime {
|
||||
value: crate::config::FRAME_INTERVAL_FACTOR_NUM as i64,
|
||||
timescale: (crate::config::FRAME_INTERVAL_FACTOR_DEN as i32)
|
||||
.saturating_mul(target_fps as i32),
|
||||
flags: CMTimeFlags(1),
|
||||
epoch: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_capture_config(cfg: &SCStreamConfiguration, capture: &SckCaptureConfig) {
|
||||
assert!(capture.target_fps() >= crate::config::FPS_MIN);
|
||||
assert!(capture.queue_depth() >= crate::config::QUEUE_DEPTH_MIN);
|
||||
cfg_set_queue_depth(cfg, capture.queue_depth() as isize);
|
||||
cfg_set_pixel_format(cfg, capture.pixel_format().as_fourcc());
|
||||
cfg_set_minimum_frame_interval(cfg, obs_minimum_frame_interval(capture.target_fps()));
|
||||
cfg_set_color_space_name_if_available(cfg, capture.color_space().as_cf_name());
|
||||
match capture.pixel_format() {
|
||||
SckPixelFormat::L10rHdr => {
|
||||
assert!(capture.color_space() == SckColorSpace::DisplayP3);
|
||||
cfg_set_capture_dynamic_range_hdr_if_available(cfg);
|
||||
}
|
||||
SckPixelFormat::Bgra8 | SckPixelFormat::Nv12VideoRange | SckPixelFormat::Nv12FullRange => {
|
||||
cfg_set_capture_dynamic_range_sdr_if_available(cfg);
|
||||
}
|
||||
}
|
||||
if capture.captures_audio() {
|
||||
cfg_set_captures_audio_if_available(cfg, true);
|
||||
cfg_set_audio_sample_rate_if_available(cfg, capture.audio_sample_rate_hz());
|
||||
cfg_set_audio_channel_count_if_available(cfg, capture.audio_channels());
|
||||
cfg_set_excludes_current_process_audio_if_available(cfg, true);
|
||||
} else {
|
||||
cfg_set_captures_audio_if_available(cfg, false);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cfg_set_captures_audio_if_available(cfg: &SCStreamConfiguration, captures_audio: bool) {
|
||||
use objc2::runtime::NSObjectProtocol;
|
||||
use objc2::sel;
|
||||
let obj: &objc2::runtime::NSObject = cfg.as_ref();
|
||||
if obj.respondsToSelector(sel!(setCapturesAudio:)) {
|
||||
unsafe {
|
||||
cfg.setCapturesAudio(captures_audio);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cfg_set_audio_sample_rate_if_available(cfg: &SCStreamConfiguration, sample_rate_hz: u32) {
|
||||
use objc2::runtime::NSObjectProtocol;
|
||||
use objc2::sel;
|
||||
assert!(sample_rate_hz >= crate::config::AUDIO_SAMPLE_RATE_MIN_HZ);
|
||||
assert!(sample_rate_hz <= crate::config::AUDIO_SAMPLE_RATE_MAX_HZ);
|
||||
let obj: &objc2::runtime::NSObject = cfg.as_ref();
|
||||
if obj.respondsToSelector(sel!(setSampleRate:)) {
|
||||
unsafe {
|
||||
cfg.setSampleRate(sample_rate_hz as isize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cfg_set_audio_channel_count_if_available(cfg: &SCStreamConfiguration, channels: u32) {
|
||||
use objc2::runtime::NSObjectProtocol;
|
||||
use objc2::sel;
|
||||
assert!(channels >= crate::config::AUDIO_CHANNEL_COUNT_MIN);
|
||||
assert!(channels <= crate::config::AUDIO_CHANNEL_COUNT_MAX);
|
||||
let obj: &objc2::runtime::NSObject = cfg.as_ref();
|
||||
if obj.respondsToSelector(sel!(setChannelCount:)) {
|
||||
unsafe {
|
||||
cfg.setChannelCount(channels as isize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cfg_set_excludes_current_process_audio_if_available(
|
||||
cfg: &SCStreamConfiguration,
|
||||
excludes_self: bool,
|
||||
) {
|
||||
use objc2::runtime::NSObjectProtocol;
|
||||
use objc2::sel;
|
||||
let obj: &objc2::runtime::NSObject = cfg.as_ref();
|
||||
if obj.respondsToSelector(sel!(setExcludesCurrentProcessAudio:)) {
|
||||
unsafe {
|
||||
cfg.setExcludesCurrentProcessAudio(excludes_self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cfg_set_stream_name_if_available(cfg: &SCStreamConfiguration, name: &NSString) {
|
||||
use objc2::runtime::NSObjectProtocol;
|
||||
use objc2::sel;
|
||||
let obj: &objc2::runtime::NSObject = cfg.as_ref();
|
||||
if obj.respondsToSelector(sel!(setStreamName:)) {
|
||||
unsafe {
|
||||
cfg.setStreamName(Some(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sc_stream_add_stream_output(
|
||||
stream: &SCStream,
|
||||
output: &objc2::runtime::ProtocolObject<dyn objc2_screen_capture_kit::SCStreamOutput>,
|
||||
kind: SCStreamOutputType,
|
||||
queue: Option<&dispatch2::DispatchQueue>,
|
||||
) -> Result<(), Retained<objc2_foundation::NSError>> {
|
||||
unsafe { stream.addStreamOutput_type_sampleHandlerQueue_error(output, kind, queue) }
|
||||
}
|
||||
Reference in new issue
Block a user