use super::{BackendSnapshot, MediaSource, VideoError, VideoState};
use objc2::encode::{Encode, Encoding, RefEncode};
use objc2::{
class, msg_send,
rc::{Allocated, Retained},
runtime::AnyObject,
};
use objc2_foundation::{NSBundle, NSString, NSURL};
use std::ffi::c_void;
use std::path::Path;
use std::time::Duration;
#[repr(C)]
struct CVBuffer {
_private: [u8; 0],
}
unsafe impl RefEncode for CVBuffer {
const ENCODING_REF: Encoding = Encoding::Pointer(&Encoding::Struct("__CVBuffer", &[]));
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct CMTime {
value: i64,
timescale: i32,
flags: u32,
epoch: i64,
}
unsafe impl Encode for CMTime {
const ENCODING: Encoding = Encoding::Struct(
"?",
&[i64::ENCODING, i32::ENCODING, u32::ENCODING, i64::ENCODING],
);
}
unsafe impl RefEncode for CMTime {
const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
}
#[link(name = "AVFoundation", kind = "framework")]
unsafe extern "C" {}
#[link(name = "CoreMedia", kind = "framework")]
unsafe extern "C" {
fn CMTimeGetSeconds(time: CMTime) -> f64;
fn CMTimeMakeWithSeconds(seconds: f64, preferred_timescale: i32) -> CMTime;
}
#[link(name = "CoreVideo", kind = "framework")]
unsafe extern "C" {
#[link_name = "kCVPixelBufferPixelFormatTypeKey"]
static PIXEL_BUFFER_PIXEL_FORMAT_TYPE_KEY: *const AnyObject;
fn CVPixelBufferLockBaseAddress(pixel_buffer: *mut CVBuffer, flags: u64) -> i32;
fn CVPixelBufferUnlockBaseAddress(pixel_buffer: *mut CVBuffer, flags: u64) -> i32;
fn CVPixelBufferGetBaseAddress(pixel_buffer: *mut CVBuffer) -> *mut c_void;
fn CVPixelBufferGetBytesPerRow(pixel_buffer: *mut CVBuffer) -> usize;
fn CVPixelBufferGetWidth(pixel_buffer: *mut CVBuffer) -> usize;
fn CVPixelBufferGetHeight(pixel_buffer: *mut CVBuffer) -> usize;
fn CVPixelBufferRelease(pixel_buffer: *mut CVBuffer);
}
pub(crate) struct AppleVideoBackend {
player: Retained<AnyObject>,
item: Retained<AnyObject>,
output: Retained<AnyObject>,
width: Option<u32>,
height: Option<u32>,
duration: Option<Duration>,
play_requested: bool,
looping: bool,
muted: bool,
volume: f32,
}
impl AppleVideoBackend {
pub(crate) fn open(source: &MediaSource) -> Result<Self, VideoError> {
let path = match source {
MediaSource::Asset(asset) => asset.clone(),
_ => source.uri(),
};
let url = match source {
MediaSource::File(_) => NSURL::from_path(Path::new(&path), false, None),
MediaSource::Url(_) => NSURL::URLWithString(&NSString::from_str(&path)),
MediaSource::Asset(_) => bundle_asset_url(&path),
}
.ok_or_else(|| VideoError::Backend(format!("invalid Apple video URL: {path}")))?;
unsafe {
let asset: Option<Retained<AnyObject>> = msg_send![
class!(AVURLAsset),
URLAssetWithURL: &*url,
options: std::ptr::null::<AnyObject>(),
];
let asset = asset
.ok_or_else(|| VideoError::Backend("failed to create AVURLAsset".to_string()))?;
let item: Option<Retained<AnyObject>> =
msg_send![class!(AVPlayerItem), playerItemWithAsset: &*asset];
let item = item
.ok_or_else(|| VideoError::Backend("failed to create AVPlayerItem".to_string()))?;
let pixel_format: Option<Retained<AnyObject>> = msg_send![
class!(NSNumber),
numberWithUnsignedInt: u32::from_be_bytes(*b"BGRA"),
];
let pixel_format = pixel_format.ok_or_else(|| {
VideoError::Backend("failed to create Apple BGRA pixel format".to_string())
})?;
let pixel_attributes: Option<Retained<AnyObject>> = msg_send![
class!(NSDictionary),
dictionaryWithObject: &*pixel_format,
forKey: PIXEL_BUFFER_PIXEL_FORMAT_TYPE_KEY,
];
let pixel_attributes = pixel_attributes.ok_or_else(|| {
VideoError::Backend("failed to create Apple pixel buffer attributes".to_string())
})?;
let output_alloc: Allocated<AnyObject> =
msg_send![class!(AVPlayerItemVideoOutput), alloc];
let output: Option<Retained<AnyObject>> = msg_send![
output_alloc,
initWithPixelBufferAttributes: &*pixel_attributes,
];
let output = output.ok_or_else(|| {
VideoError::Backend("failed to initialize AVPlayerItemVideoOutput".to_string())
})?;
let _: () = msg_send![
&*item,
addOutput: &*output,
];
let player: Option<Retained<AnyObject>> = msg_send![
class!(AVPlayer),
playerWithPlayerItem: &*item,
];
let player = player
.ok_or_else(|| VideoError::Backend("failed to create AVPlayer".to_string()))?;
let _: () = msg_send![&*player, setMuted: false];
let _: () = msg_send![&*player, setVolume: 1.0_f32];
Ok(Self {
player,
item,
output,
width: None,
height: None,
duration: None,
play_requested: false,
looping: false,
muted: false,
volume: 1.0,
})
}
}
pub(crate) fn close(&mut self) {
unsafe {
let _: () = msg_send![&*self.player, cancelPendingPrerolls];
let _: () = msg_send![&*self.player, pause];
}
}
pub(crate) fn update(&mut self) -> Result<BackendSnapshot, VideoError> {
unsafe {
let item_status: isize = msg_send![&*self.item, status];
if item_status == 2 {
return Err(VideoError::Backend(
"Apple video item failed to become ready".to_string(),
));
}
let duration_time: CMTime = msg_send![&*self.item, duration];
let duration_seconds = CMTimeGetSeconds(duration_time);
if duration_seconds.is_finite() && duration_seconds > 0.0 {
self.duration = Some(Duration::from_secs_f64(duration_seconds));
}
let time: CMTime = msg_send![&*self.player, currentTime];
let position_seconds = CMTimeGetSeconds(time).max(0.0);
let mut rate: f32 = msg_send![&*self.player, rate];
let mut rgba = None;
let has_new_frame: bool = msg_send![&*self.output, hasNewPixelBufferForItemTime: time];
if has_new_frame {
let mut display_time = CMTime::default();
let pixel_buffer: *mut CVBuffer = msg_send![
&*self.output,
copyPixelBufferForItemTime: time,
itemTimeForDisplay: &mut display_time,
];
if !pixel_buffer.is_null() {
let (frame, width, height) = copy_pixel_buffer(pixel_buffer)?;
CVPixelBufferRelease(pixel_buffer);
self.width = Some(width);
self.height = Some(height);
rgba = Some(frame);
}
}
let ended = self
.duration
.map(|duration| {
position_seconds >= duration.as_secs_f64().max(0.001) - 0.08 && rate == 0.0
})
.unwrap_or(false);
if self.play_requested && item_status == 1 && rate == 0.0 && !ended {
let _: () = msg_send![&*self.player, play];
rate = msg_send![&*self.player, rate];
}
if ended && self.looping {
let zero = CMTimeMakeWithSeconds(0.0, 600);
let _: () = msg_send![&*self.player, seekToTime: zero];
let _: () = msg_send![&*self.player, play];
}
let state = if ended && !self.looping {
VideoState::Ended
} else if rate > 0.0 || (self.play_requested && item_status == 1) {
VideoState::Playing
} else if item_status == 0 {
VideoState::Opening
} else if position_seconds > 0.0 {
VideoState::Paused
} else {
VideoState::Ready
};
Ok(BackendSnapshot {
state,
duration: self.duration,
position: Duration::from_secs_f64(position_seconds),
dimensions: self.width.zip(self.height),
rgba,
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
external_video: None,
})
}
}
pub(crate) fn play(&mut self) -> Result<(), VideoError> {
self.play_requested = true;
unsafe {
let _: () = msg_send![&*self.player, setMuted: self.muted];
let effective_volume = if self.muted { 0.0 } else { self.volume };
let _: () = msg_send![&*self.player, setVolume: effective_volume];
let item_status: isize = msg_send![&*self.item, status];
if item_status == 1 {
let _: () = msg_send![&*self.player, play];
}
}
Ok(())
}
pub(crate) fn pause(&mut self) -> Result<(), VideoError> {
self.play_requested = false;
unsafe {
let _: () = msg_send![&*self.player, pause];
}
Ok(())
}
pub(crate) fn stop(&mut self) -> Result<(), VideoError> {
self.pause()?;
self.seek(Duration::ZERO)
}
pub(crate) fn seek(&mut self, position: Duration) -> Result<(), VideoError> {
unsafe {
let time = CMTimeMakeWithSeconds(position.as_secs_f64(), 600);
let _: () = msg_send![&*self.player, seekToTime: time];
}
Ok(())
}
pub(crate) fn set_loop(&mut self, looping: bool) -> Result<(), VideoError> {
self.looping = looping;
Ok(())
}
pub(crate) fn set_muted(&mut self, muted: bool) -> Result<(), VideoError> {
self.muted = muted;
self.set_volume(self.volume)
}
pub(crate) fn set_volume(&mut self, volume: f32) -> Result<(), VideoError> {
self.volume = volume.clamp(0.0, 1.0);
let effective = if self.muted { 0.0 } else { self.volume };
unsafe {
let _: () = msg_send![&*self.player, setVolume: effective];
}
Ok(())
}
pub(crate) fn set_playback_rate(&mut self, rate: f64) -> Result<(), VideoError> {
unsafe {
let _: () = msg_send![&*self.player, setRate: rate as f32];
}
Ok(())
}
}
fn bundle_asset_url(path: &str) -> Option<Retained<NSURL>> {
let path = path.strip_prefix("./").unwrap_or(path);
let path = path.strip_prefix("assets/").unwrap_or(path);
let resource_path = Path::new(path);
let name = resource_path.file_stem()?.to_str()?;
let extension = resource_path.extension().and_then(|value| value.to_str());
let name = NSString::from_str(name);
let extension = extension.map(NSString::from_str);
let subdirectory = resource_path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.and_then(|parent| parent.to_str())
.map(NSString::from_str);
let bundle = NSBundle::mainBundle();
bundle.URLForResource_withExtension_subdirectory(
Some(&name),
extension.as_deref(),
subdirectory.as_deref(),
)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn copy_pixel_buffer(
pixel_buffer: *mut CVBuffer,
) -> Result<(Vec<u8>, u32, u32), VideoError> {
if CVPixelBufferLockBaseAddress(pixel_buffer, 0) != 0 {
return Err(VideoError::Backend(
"failed to lock Apple video pixel buffer".to_string(),
));
}
let width = CVPixelBufferGetWidth(pixel_buffer);
let height = CVPixelBufferGetHeight(pixel_buffer);
let stride = CVPixelBufferGetBytesPerRow(pixel_buffer);
let base = CVPixelBufferGetBaseAddress(pixel_buffer).cast::<u8>();
if base.is_null() || width == 0 || height == 0 || stride < width * 4 {
let _ = CVPixelBufferUnlockBaseAddress(pixel_buffer, 0);
return Err(VideoError::Backend(
"Apple video pixel buffer has an unsupported layout".to_string(),
));
}
let mut rgba = vec![0; width * height * 4];
for row in 0..height {
let src = std::slice::from_raw_parts(base.add(row * stride), width * 4);
let dst = &mut rgba[row * width * 4..(row + 1) * width * 4];
for (source, target) in src.chunks_exact(4).zip(dst.chunks_exact_mut(4)) {
target.copy_from_slice(&[source[2], source[1], source[0], source[3]]);
}
}
let _ = CVPixelBufferUnlockBaseAddress(pixel_buffer, 0);
Ok((rgba, width as u32, height as u32))
}