Skip to main content

winio_ui_app_kit/widgets/
media.rs

1use 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}