use std::{
cell::{Cell, RefCell},
ffi::c_void,
ptr::null_mut,
time::Duration,
};
use inherit_methods_macro::inherit_methods;
use objc2::{
DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send,
rc::{Allocated, Retained},
runtime::AnyObject,
sel,
};
use objc2_av_foundation::{
AVPlayer, AVPlayerItem, AVPlayerItemDidPlayToEndTimeNotification, AVPlayerItemStatus,
};
use objc2_av_kit::{AVPlayerView, AVPlayerViewControlsStyle};
use objc2_core_media::{CMTime, kCMTimeZero};
use objc2_foundation::{
NSDictionary, NSKeyValueChangeKey, NSKeyValueObservingOptions, NSNotification,
NSNotificationCenter, NSObject, NSObjectNSKeyValueObserverRegistration, NSString, NSURL,
ns_string,
};
use winio_callback::Callback;
use winio_handle::AsContainer;
use winio_primitive::{Point, Size};
use crate::{Error, GlobalRuntime, Result, Widget, catch, from_nsstring};
#[derive(Debug)]
pub struct Media {
handle: Widget,
view: Retained<AVPlayerView>,
url: Option<Retained<NSURL>>,
delegate: Retained<PlayerDelegate>,
}
#[inherit_methods(from = "self.handle")]
impl Media {
pub fn new(parent: impl AsContainer) -> Result<Self> {
let parent = parent.as_container();
let mtm = parent.as_app_kit().mtm();
catch(|| unsafe {
let view = AVPlayerView::new(mtm);
let handle = Widget::from_nsview(parent, Retained::cast_unchecked(view.clone()))?;
let delegate = PlayerDelegate::new(mtm);
view.setControlsStyle(AVPlayerViewControlsStyle::None);
Ok(Self {
handle,
view,
url: None,
delegate,
})
})
.flatten()
}
pub fn is_visible(&self) -> Result<bool>;
pub fn set_visible(&mut self, v: bool) -> Result<()>;
pub fn is_enabled(&self) -> Result<bool>;
pub fn set_enabled(&mut self, v: bool) -> Result<()>;
pub fn loc(&self) -> Result<Point>;
pub fn set_loc(&mut self, p: Point) -> Result<()>;
pub fn size(&self) -> Result<Size>;
pub fn set_size(&mut self, v: Size) -> Result<()>;
pub fn tooltip(&self) -> Result<String>;
pub fn set_tooltip(&mut self, s: impl AsRef<str>) -> Result<()>;
pub fn url(&self) -> Result<String> {
Ok(self
.url
.as_ref()
.and_then(|url| url.absoluteString())
.map(|s| from_nsstring(&s))
.unwrap_or_default())
}
pub async fn load(&mut self, url: impl AsRef<str>) -> Result<()> {
let item = catch(|| unsafe {
let url = NSURL::URLWithString(&NSString::from_str(url.as_ref()))
.ok_or(Error::NullPointer)?;
let mtm = self.delegate.mtm();
let item = AVPlayerItem::playerItemWithURL(&url, mtm);
item.addObserver_forKeyPath_options_context(
&self.delegate,
ns_string!("status"),
NSKeyValueObservingOptions::New,
null_mut(),
);
NSNotificationCenter::defaultCenter().addObserver_selector_name_object(
&self.delegate,
sel!(runLoop:),
Some(AVPlayerItemDidPlayToEndTimeNotification),
Some(&item),
);
let player = AVPlayer::playerWithPlayerItem(Some(&item), mtm);
self.view.setPlayer(Some(&player));
let ivars = self.delegate.ivars();
ivars.player.replace(Some(player));
ivars.looped.set(false);
ivars.rate.set(1.0);
self.url = Some(url);
Ok(item)
})
.flatten()?;
self.delegate.ivars().notify.wait().await;
catch(|| unsafe {
if item.status() == AVPlayerItemStatus::ReadyToPlay {
Ok(())
} else {
Err(Error::NS(item.error()))
}
})
.flatten()
}
pub fn play(&mut self) -> Result<()> {
if Some(self.current_time()?) == self.full_time()? {
self.set_current_time(Duration::ZERO)?;
}
catch(|| unsafe {
if let Some(player) = self.view.player() {
player.play();
}
})?;
self.set_playback_rate(self.delegate.ivars().rate.get())
}
pub fn pause(&mut self) -> Result<()> {
catch(|| unsafe {
if let Some(player) = self.view.player() {
player.pause();
}
})
}
pub fn full_time(&self) -> Result<Option<Duration>> {
catch(|| unsafe {
self.view
.player()
.and_then(|player| player.currentItem())
.and_then(|item| {
let t = item.duration();
Duration::try_from_secs_f64(t.seconds()).ok()
})
})
}
pub fn current_time(&self) -> Result<Duration> {
catch(|| unsafe {
self.view
.player()
.and_then(|player| {
let ct = player.currentTime();
Duration::try_from_secs_f64(ct.seconds()).ok()
})
.unwrap_or_default()
})
}
pub fn set_current_time(&mut self, t: Duration) -> Result<()> {
catch(|| unsafe {
if let Some(player) = self.view.player() {
player.seekToTime(CMTime::with_seconds(t.as_secs_f64(), 600));
}
})
}
pub fn volume(&self) -> Result<f64> {
catch(|| unsafe {
self.view
.player()
.map(|player| player.volume() as _)
.unwrap_or_default()
})
}
pub fn set_volume(&mut self, v: f64) -> Result<()> {
catch(|| unsafe {
if let Some(player) = self.view.player() {
player.setVolume(v as _);
}
})
}
pub fn is_muted(&self) -> Result<bool> {
catch(|| unsafe {
self.view
.player()
.map(|player| player.isMuted())
.unwrap_or_default()
})
}
pub fn set_muted(&mut self, v: bool) -> Result<()> {
catch(|| unsafe {
if let Some(player) = self.view.player() {
player.setMuted(v);
}
})
}
pub fn is_looped(&self) -> Result<bool> {
Ok(self.delegate.ivars().looped.get())
}
pub fn set_looped(&mut self, v: bool) -> Result<()> {
self.delegate.ivars().looped.set(v);
Ok(())
}
pub fn playback_rate(&self) -> Result<f64> {
catch(|| unsafe {
self.view
.player()
.map(|player| player.rate() as _)
.unwrap_or_default()
})
}
pub fn set_playback_rate(&mut self, v: f64) -> Result<()> {
self.delegate.ivars().rate.set(v);
catch(|| unsafe {
if let Some(player) = self.view.player() {
player.setRate(v as _);
}
})
}
}
winio_handle::impl_as_widget!(Media, handle);
#[derive(Debug, Default)]
struct PlayerDelegateIvars {
notify: Callback,
looped: Cell<bool>,
rate: Cell<f64>,
player: RefCell<Option<Retained<AVPlayer>>>,
}
define_class! {
#[unsafe(super(NSObject))]
#[name = "WinioPlayerDelegate"]
#[ivars = PlayerDelegateIvars]
#[thread_kind = MainThreadOnly]
#[derive(Debug)]
struct PlayerDelegate;
#[allow(non_snake_case)]
impl PlayerDelegate {
#[unsafe(method_id(init))]
fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
let this = this.set_ivars(PlayerDelegateIvars::default());
unsafe { msg_send![super(this), init] }
}
#[unsafe(method(observeValueForKeyPath:ofObject:change:context:))]
unsafe fn observeValueForKeyPath_ofObject_change_context(
&self,
key_path: Option<&NSString>,
_object: Option<&AnyObject>,
_change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
_context: *mut c_void,
) {
if let Some(path) = key_path && path.isEqualToString(ns_string!("status")) {
self.ivars().notify.signal::<GlobalRuntime>(());
}
}
#[unsafe(method(runLoop:))]
unsafe fn runLoop(&self, notification: &NSNotification) {
if self.ivars().looped.get()
&& let Some(player) = self.ivars().player.borrow().as_ref()
{
unsafe {
player.seekToTime(kCMTimeZero);
player.play();
player.setRate(self.ivars().rate.get() as _);
}
}
}
}
}
impl PlayerDelegate {
pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
unsafe { msg_send![mtm.alloc::<Self>(), init] }
}
}