winio_ui_app_kit/widgets/
media.rs1use std::{
2 cell::{Cell, RefCell},
3 ffi::c_void,
4 ptr::null_mut,
5 time::Duration,
6};
7
8use inherit_methods_macro::inherit_methods;
9use objc2::{
10 DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send,
11 rc::{Allocated, Retained},
12 runtime::AnyObject,
13 sel,
14};
15use objc2_av_foundation::{
16 AVPlayer, AVPlayerItem, AVPlayerItemDidPlayToEndTimeNotification, AVPlayerItemStatus,
17};
18use objc2_av_kit::{AVPlayerView, AVPlayerViewControlsStyle};
19use objc2_core_media::{CMTime, kCMTimeZero};
20use objc2_foundation::{
21 NSDictionary, NSKeyValueChangeKey, NSKeyValueObservingOptions, NSNotification,
22 NSNotificationCenter, NSObject, NSObjectNSKeyValueObserverRegistration, NSString, NSURL,
23 ns_string,
24};
25use winio_callback::Callback;
26use winio_handle::AsContainer;
27use winio_primitive::{Point, Size};
28
29use crate::{Error, GlobalRuntime, Result, Widget, catch, from_nsstring};
30
31#[derive(Debug)]
32pub struct Media {
33 handle: Widget,
34 view: Retained<AVPlayerView>,
35 url: Option<Retained<NSURL>>,
36 delegate: Retained<PlayerDelegate>,
37}
38
39#[inherit_methods(from = "self.handle")]
40impl Media {
41 pub fn new(parent: impl AsContainer) -> Result<Self> {
42 let parent = parent.as_container();
43 let mtm = parent.as_app_kit().mtm();
44
45 catch(|| unsafe {
46 let view = AVPlayerView::new(mtm);
47 let handle = Widget::from_nsview(parent, Retained::cast_unchecked(view.clone()))?;
48
49 let delegate = PlayerDelegate::new(mtm);
50
51 view.setControlsStyle(AVPlayerViewControlsStyle::None);
52
53 Ok(Self {
54 handle,
55 view,
56 url: None,
57 delegate,
58 })
59 })
60 .flatten()
61 }
62
63 pub fn is_visible(&self) -> Result<bool>;
64
65 pub fn set_visible(&mut self, v: bool) -> Result<()>;
66
67 pub fn is_enabled(&self) -> Result<bool>;
68
69 pub fn set_enabled(&mut self, v: bool) -> Result<()>;
70
71 pub fn loc(&self) -> Result<Point>;
72
73 pub fn set_loc(&mut self, p: Point) -> Result<()>;
74
75 pub fn size(&self) -> Result<Size>;
76
77 pub fn set_size(&mut self, v: Size) -> Result<()>;
78
79 pub fn tooltip(&self) -> Result<String>;
80
81 pub fn set_tooltip(&mut self, s: impl AsRef<str>) -> Result<()>;
82
83 pub fn url(&self) -> Result<String> {
84 Ok(self
85 .url
86 .as_ref()
87 .and_then(|url| url.absoluteString())
88 .map(|s| from_nsstring(&s))
89 .unwrap_or_default())
90 }
91
92 pub async fn load(&mut self, url: impl AsRef<str>) -> Result<()> {
93 let item = catch(|| unsafe {
94 let url = NSURL::URLWithString(&NSString::from_str(url.as_ref()))
95 .ok_or(Error::NullPointer)?;
96 let mtm = self.delegate.mtm();
97 let item = AVPlayerItem::playerItemWithURL(&url, mtm);
98 item.addObserver_forKeyPath_options_context(
99 &self.delegate,
100 ns_string!("status"),
101 NSKeyValueObservingOptions::New,
102 null_mut(),
103 );
104 NSNotificationCenter::defaultCenter().addObserver_selector_name_object(
105 &self.delegate,
106 sel!(runLoop:),
107 Some(AVPlayerItemDidPlayToEndTimeNotification),
108 Some(&item),
109 );
110 let player = AVPlayer::playerWithPlayerItem(Some(&item), mtm);
111 self.view.setPlayer(Some(&player));
112 let ivars = self.delegate.ivars();
113 ivars.player.replace(Some(player));
114 ivars.looped.set(false);
115 ivars.rate.set(1.0);
116 self.url = Some(url);
117 Ok(item)
118 })
119 .flatten()?;
120 self.delegate.ivars().notify.wait().await;
121 catch(|| unsafe {
122 if item.status() == AVPlayerItemStatus::ReadyToPlay {
123 Ok(())
124 } else {
125 Err(Error::NS(item.error()))
126 }
127 })
128 .flatten()
129 }
130
131 pub fn play(&mut self) -> Result<()> {
132 if Some(self.current_time()?) == self.full_time()? {
133 self.set_current_time(Duration::ZERO)?;
134 }
135 catch(|| unsafe {
136 if let Some(player) = self.view.player() {
137 player.play();
138 }
139 })?;
140 self.set_playback_rate(self.delegate.ivars().rate.get())
141 }
142
143 pub fn pause(&mut self) -> Result<()> {
144 catch(|| unsafe {
145 if let Some(player) = self.view.player() {
146 player.pause();
147 }
148 })
149 }
150
151 pub fn full_time(&self) -> Result<Option<Duration>> {
152 catch(|| unsafe {
153 self.view
154 .player()
155 .and_then(|player| player.currentItem())
156 .and_then(|item| {
157 let t = item.duration();
158 Duration::try_from_secs_f64(t.seconds()).ok()
159 })
160 })
161 }
162
163 pub fn current_time(&self) -> Result<Duration> {
164 catch(|| unsafe {
165 self.view
166 .player()
167 .and_then(|player| {
168 let ct = player.currentTime();
169 Duration::try_from_secs_f64(ct.seconds()).ok()
170 })
171 .unwrap_or_default()
172 })
173 }
174
175 pub fn set_current_time(&mut self, t: Duration) -> Result<()> {
176 catch(|| unsafe {
177 if let Some(player) = self.view.player() {
178 player.seekToTime(CMTime::with_seconds(t.as_secs_f64(), 600));
179 }
180 })
181 }
182
183 pub fn volume(&self) -> Result<f64> {
184 catch(|| unsafe {
185 self.view
186 .player()
187 .map(|player| player.volume() as _)
188 .unwrap_or_default()
189 })
190 }
191
192 pub fn set_volume(&mut self, v: f64) -> Result<()> {
193 catch(|| unsafe {
194 if let Some(player) = self.view.player() {
195 player.setVolume(v as _);
196 }
197 })
198 }
199
200 pub fn is_muted(&self) -> Result<bool> {
201 catch(|| unsafe {
202 self.view
203 .player()
204 .map(|player| player.isMuted())
205 .unwrap_or_default()
206 })
207 }
208
209 pub fn set_muted(&mut self, v: bool) -> Result<()> {
210 catch(|| unsafe {
211 if let Some(player) = self.view.player() {
212 player.setMuted(v);
213 }
214 })
215 }
216
217 pub fn is_looped(&self) -> Result<bool> {
218 Ok(self.delegate.ivars().looped.get())
219 }
220
221 pub fn set_looped(&mut self, v: bool) -> Result<()> {
222 self.delegate.ivars().looped.set(v);
223 Ok(())
224 }
225
226 pub fn playback_rate(&self) -> Result<f64> {
227 catch(|| unsafe {
228 self.view
229 .player()
230 .map(|player| player.rate() as _)
231 .unwrap_or_default()
232 })
233 }
234
235 pub fn set_playback_rate(&mut self, v: f64) -> Result<()> {
236 self.delegate.ivars().rate.set(v);
237 catch(|| unsafe {
238 if let Some(player) = self.view.player() {
239 player.setRate(v as _);
240 }
241 })
242 }
243}
244
245winio_handle::impl_as_widget!(Media, handle);
246
247#[derive(Debug, Default)]
248struct PlayerDelegateIvars {
249 notify: Callback,
250 looped: Cell<bool>,
251 rate: Cell<f64>,
252 player: RefCell<Option<Retained<AVPlayer>>>,
253}
254
255define_class! {
256 #[unsafe(super(NSObject))]
257 #[name = "WinioPlayerDelegate"]
258 #[ivars = PlayerDelegateIvars]
259 #[thread_kind = MainThreadOnly]
260 #[derive(Debug)]
261 struct PlayerDelegate;
262
263 #[allow(non_snake_case)]
264 impl PlayerDelegate {
265 #[unsafe(method_id(init))]
266 fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
267 let this = this.set_ivars(PlayerDelegateIvars::default());
268 unsafe { msg_send![super(this), init] }
269 }
270
271 #[unsafe(method(observeValueForKeyPath:ofObject:change:context:))]
272 unsafe fn observeValueForKeyPath_ofObject_change_context(
273 &self,
274 key_path: Option<&NSString>,
275 _object: Option<&AnyObject>,
276 _change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
277 _context: *mut c_void,
278 ) {
279 if let Some(path) = key_path && path.isEqualToString(ns_string!("status")) {
280 self.ivars().notify.signal::<GlobalRuntime>(());
281 }
282 }
283
284 #[unsafe(method(runLoop:))]
285 unsafe fn runLoop(&self, notification: &NSNotification) {
286 if self.ivars().looped.get()
287 && let Some(player) = self.ivars().player.borrow().as_ref()
288 {
289 unsafe {
290 player.seekToTime(kCMTimeZero);
291 player.play();
292 player.setRate(self.ivars().rate.get() as _);
293 }
294 }
295 }
296 }
297}
298
299impl PlayerDelegate {
300 pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
301 unsafe { msg_send![mtm.alloc::<Self>(), init] }
302 }
303}