yew_hooks/hooks/
use_media.rs

1use std::cmp::{max_by, min_by};
2use std::rc::Rc;
3
4use wasm_bindgen::{prelude::*, JsValue};
5use web_sys::{HtmlMediaElement, TimeRanges};
6use yew::{prelude::*, TargetCast};
7
8use super::{use_event, use_mut_latest};
9
10/// Options for media
11#[derive(Default)]
12pub struct UseMediaOptions {
13    /// Auto play the media.
14    pub auto_play: bool,
15    pub onplay: Option<Box<dyn FnMut(Event)>>,
16    pub onplaying: Option<Box<dyn FnMut(Event)>>,
17    pub onwaiting: Option<Box<dyn FnMut(Event)>>,
18    pub onpause: Option<Box<dyn FnMut(Event)>>,
19    pub onvolumechange: Option<Box<dyn FnMut(Event)>>,
20    pub ondurationchange: Option<Box<dyn FnMut(Event)>>,
21    pub ontimeupdate: Option<Box<dyn FnMut(Event)>>,
22    pub onprogress: Option<Box<dyn FnMut(Event)>>,
23}
24
25impl UseMediaOptions {
26    /// Set `auto_play` to true
27    pub fn enable_auto_play() -> Self {
28        Self {
29            auto_play: true,
30            ..Self::default()
31        }
32    }
33}
34
35/// State handle for the [`use_media`] hook.
36pub struct UseMediaHandle {
37    pub buffered: UseStateHandle<Vec<(f64, f64)>>,
38    pub duration: UseStateHandle<f64>,
39    pub paused: UseStateHandle<bool>,
40    pub muted: UseStateHandle<bool>,
41    pub time: UseStateHandle<f64>,
42    pub volume: UseStateHandle<f64>,
43    pub playing: UseStateHandle<bool>,
44
45    play: Rc<dyn Fn()>,
46    pause: Rc<dyn Fn()>,
47    seek: Rc<dyn Fn(f64)>,
48    set_volume: Rc<dyn Fn(f64)>,
49    mute: Rc<dyn Fn()>,
50    unmute: Rc<dyn Fn()>,
51}
52
53impl UseMediaHandle {
54    /// Play the media.
55    pub fn play(&self) {
56        (self.play)();
57    }
58
59    /// Pause the media.
60    pub fn pause(&self) {
61        (self.pause)();
62    }
63
64    /// Mute the media.
65    pub fn mute(&self) {
66        (self.mute)();
67    }
68
69    /// Unmute the media.
70    pub fn unmute(&self) {
71        (self.unmute)();
72    }
73
74    /// Set volume of the media.
75    pub fn set_volume(&self, value: f64) {
76        (self.set_volume)(value);
77    }
78
79    /// Seek the media.
80    pub fn seek(&self, value: f64) {
81        (self.seek)(value);
82    }
83}
84
85impl Clone for UseMediaHandle {
86    fn clone(&self) -> Self {
87        Self {
88            buffered: self.buffered.clone(),
89            duration: self.duration.clone(),
90            paused: self.paused.clone(),
91            muted: self.muted.clone(),
92            time: self.time.clone(),
93            volume: self.volume.clone(),
94            playing: self.playing.clone(),
95
96            play: self.play.clone(),
97            pause: self.pause.clone(),
98            seek: self.seek.clone(),
99            set_volume: self.set_volume.clone(),
100            mute: self.mute.clone(),
101            unmute: self.unmute.clone(),
102        }
103    }
104}
105
106/// This hook plays video or audio and exposes its controls.
107///
108/// # Example
109///
110/// ```rust
111/// # use yew::prelude::*;
112/// #
113/// use yew_hooks::prelude::*;
114///
115/// #[function_component(UseMedia)]
116/// fn media() -> Html {
117///     let node_video = use_node_ref();
118///
119///     let video = use_media(
120///         node_video.clone(),
121///         "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4".to_string(),
122///     );
123///
124///     let onplay = {
125///         let video = video.clone();
126///         Callback::from(move |_| video.play())
127///     };
128///
129///     let onpause = {
130///         let video = video.clone();
131///         Callback::from(move |_| video.pause())
132///     };
133///
134///     let onmute = {
135///         let video = video.clone();
136///         Callback::from(move |_| video.mute())
137///     };
138///
139///     let onunmute = {
140///         let video = video.clone();
141///         Callback::from(move |_| video.unmute())
142///     };
143///
144///     let onvol = {
145///         let video = video.clone();
146///         Callback::from(move |_| video.set_volume(0.5))
147///     };
148///     let onseek = {
149///         let video = video.clone();
150///         Callback::from(move |_| video.seek(*video.time + 5.0))
151///     };
152///
153///     html! {
154///         <>
155///             <p><video ref={node_video} width="640" height="360" /></p>
156///             <p>
157///                 { " Duration: " } { *video.duration }
158///                 { " Time: " } { *video.time }
159///                 { " Volume: " } { *video.volume }
160///             </p>
161///             <p>
162///                 <button onclick={onplay} disabled={*video.playing}>{ "Play" }</button>
163///                 <button onclick={onpause} disabled={*video.paused}>{ "Pause" }</button>
164///                 <button onclick={onmute} disabled={*video.muted}>{ "Mute" }</button>
165///                 <button onclick={onunmute} disabled={!*video.muted}>{ "Unmute" }</button>
166///                 <button onclick={onvol}>{ "Volume: 50%" }</button>
167///                 <button onclick={onseek}>{ "Seek: +5 secs" }</button>
168///             </p>
169///         </>
170///     }
171/// }
172/// ```
173#[hook]
174pub fn use_media(node: NodeRef, src: String) -> UseMediaHandle {
175    use_media_with_options(node, src, UseMediaOptions::default())
176}
177
178/// This hook plays video or audio and exposes its controls with options.
179/// see [`use_media`]
180#[hook]
181pub fn use_media_with_options(
182    node: NodeRef,
183    src: String,
184    options: UseMediaOptions,
185) -> UseMediaHandle {
186    let buffered = use_state(Vec::new);
187    let duration = use_state(|| 0.0);
188    let paused = use_state(|| true);
189    let muted = use_state(|| false);
190    let time = use_state(|| 0.0);
191    let volume = use_state(|| 1.0);
192    let playing = use_state(|| false);
193
194    let onplay_ref = use_mut_latest(options.onplay);
195    let onplaying_ref = use_mut_latest(options.onplaying);
196    let onwaiting_ref = use_mut_latest(options.onwaiting);
197    let onpause_ref = use_mut_latest(options.onpause);
198    let onvolumechange_ref = use_mut_latest(options.onvolumechange);
199    let ondurationchange_ref = use_mut_latest(options.ondurationchange);
200    let ontimeupdate_ref = use_mut_latest(options.ontimeupdate);
201    let onprogress_ref = use_mut_latest(options.onprogress);
202
203    let play_lock = use_mut_ref(|| false);
204
205    {
206        let node = node.clone();
207        let paused = paused.clone();
208        use_event(node, "play", move |e: Event| {
209            paused.set(false);
210
211            let onplay_ref = onplay_ref.current();
212            let onplay = &mut *onplay_ref.borrow_mut();
213            if let Some(onplay) = onplay {
214                onplay(e);
215            }
216        });
217    }
218
219    {
220        let node = node.clone();
221        let playing = playing.clone();
222        use_event(node, "playing", move |e: Event| {
223            playing.set(true);
224
225            let onplaying_ref = onplaying_ref.current();
226            let onplaying = &mut *onplaying_ref.borrow_mut();
227            if let Some(onplaying) = onplaying {
228                onplaying(e);
229            }
230        });
231    }
232
233    {
234        let node = node.clone();
235        let playing = playing.clone();
236        use_event(node, "waiting", move |e: Event| {
237            playing.set(false);
238
239            let onwaiting_ref = onwaiting_ref.current();
240            let onwaiting = &mut *onwaiting_ref.borrow_mut();
241            if let Some(onwaiting) = onwaiting {
242                onwaiting(e);
243            }
244        });
245    }
246
247    {
248        let node = node.clone();
249        let paused = paused.clone();
250        let playing = playing.clone();
251        use_event(node, "pause", move |e: Event| {
252            paused.set(true);
253            playing.set(false);
254
255            let onpause_ref = onpause_ref.current();
256            let onpause = &mut *onpause_ref.borrow_mut();
257            if let Some(onpause) = onpause {
258                onpause(e);
259            }
260        });
261    }
262
263    {
264        let node = node.clone();
265        let muted = muted.clone();
266        let volume = volume.clone();
267        use_event(node, "volumechange", move |e: Event| {
268            if let Some(media) = e.target_dyn_into::<HtmlMediaElement>() {
269                muted.set(media.muted());
270                volume.set(media.volume());
271            }
272
273            let onvolumechange_ref = onvolumechange_ref.current();
274            let onvolumechange = &mut *onvolumechange_ref.borrow_mut();
275            if let Some(onvolumechange) = onvolumechange {
276                onvolumechange(e);
277            }
278        });
279    }
280
281    {
282        let node = node.clone();
283        let duration = duration.clone();
284        let buffered = buffered.clone();
285        use_event(node, "durationchange", move |e: Event| {
286            if let Some(media) = e.target_dyn_into::<HtmlMediaElement>() {
287                duration.set(media.duration());
288                buffered.set(parse_time_ranges(media.buffered()));
289            }
290
291            let ondurationchange_ref = ondurationchange_ref.current();
292            let ondurationchange = &mut *ondurationchange_ref.borrow_mut();
293            if let Some(ondurationchange) = ondurationchange {
294                ondurationchange(e);
295            }
296        });
297    }
298
299    {
300        let node = node.clone();
301        let time = time.clone();
302        use_event(node, "timeupdate", move |e: Event| {
303            if let Some(media) = e.target_dyn_into::<HtmlMediaElement>() {
304                time.set(media.current_time());
305            }
306
307            let ontimeupdate_ref = ontimeupdate_ref.current();
308            let ontimeupdate = &mut *ontimeupdate_ref.borrow_mut();
309            if let Some(ontimeupdate) = ontimeupdate {
310                ontimeupdate(e);
311            }
312        });
313    }
314
315    {
316        let node = node.clone();
317        let buffered = buffered.clone();
318        use_event(node, "progress", move |e: Event| {
319            if let Some(media) = e.target_dyn_into::<HtmlMediaElement>() {
320                buffered.set(parse_time_ranges(media.buffered()));
321            }
322
323            let onprogress_ref = onprogress_ref.current();
324            let onprogress = &mut *onprogress_ref.borrow_mut();
325            if let Some(onprogress) = onprogress {
326                onprogress(e);
327            }
328        });
329    }
330
331    let play = {
332        let play_lock = play_lock.clone();
333        let node = node.clone();
334        Rc::new(move || {
335            if !*play_lock.borrow() {
336                if let Some(media) = node.cast::<HtmlMediaElement>() {
337                    if let Ok(promise) = media.play() {
338                        *play_lock.borrow_mut() = true;
339                        let play_lock = play_lock.clone();
340                        let closure = Closure::wrap(Box::new(move |_| {
341                            *play_lock.borrow_mut() = false;
342                        })
343                            as Box<dyn FnMut(JsValue)>);
344                        let _ = promise.then2(&closure, &closure);
345                        closure.forget();
346                    }
347                }
348            }
349        })
350    };
351
352    let pause = {
353        let node = node.clone();
354        Rc::new(move || {
355            if !*play_lock.borrow() {
356                if let Some(media) = node.cast::<HtmlMediaElement>() {
357                    let _ = media.pause();
358                }
359            }
360        })
361    };
362
363    let seek = {
364        let duration = duration.clone();
365        let node = node.clone();
366        Rc::new(move |time: f64| {
367            if let Some(media) = node.cast::<HtmlMediaElement>() {
368                let time = max_by(0.0, time, |x, y| x.partial_cmp(y).unwrap());
369                let time = min_by(*duration, time, |x, y| x.partial_cmp(y).unwrap());
370                media.set_current_time(time);
371            }
372        })
373    };
374
375    let set_volume = {
376        let volume = volume.clone();
377        let node = node.clone();
378        Rc::new(move |vol: f64| {
379            if let Some(media) = node.cast::<HtmlMediaElement>() {
380                let vol = max_by(0.0, vol, |x, y| x.partial_cmp(y).unwrap());
381                let vol = min_by(1.0, vol, |x, y| x.partial_cmp(y).unwrap());
382                media.set_volume(vol);
383                volume.set(vol);
384            }
385        })
386    };
387
388    let mute = {
389        let node = node.clone();
390        let muted = muted.clone();
391        Rc::new(move || {
392            if let Some(media) = node.cast::<HtmlMediaElement>() {
393                media.set_muted(true);
394                muted.set(true);
395            }
396        })
397    };
398
399    let unmute = {
400        let node = node.clone();
401        let muted = muted.clone();
402        Rc::new(move || {
403            if let Some(media) = node.cast::<HtmlMediaElement>() {
404                media.set_muted(false);
405                muted.set(false);
406            }
407        })
408    };
409
410    {
411        let volume = volume.clone();
412        let muted = muted.clone();
413        let paused = paused.clone();
414        let play = play.clone();
415        let auto_play = options.auto_play;
416        use_effect_with((node, src), move |(node, src)| {
417            if let Some(media) = node.cast::<HtmlMediaElement>() {
418                media.set_controls(false);
419                media.set_src(src);
420
421                volume.set(media.volume());
422                muted.set(media.muted());
423                paused.set(media.paused());
424
425                if auto_play && media.paused() {
426                    play();
427                }
428            }
429
430            || ()
431        });
432    }
433
434    UseMediaHandle {
435        buffered,
436        duration,
437        paused,
438        muted,
439        time,
440        volume,
441        playing,
442
443        play,
444        pause,
445        seek,
446        set_volume,
447        mute,
448        unmute,
449    }
450}
451
452fn parse_time_ranges(ranges: TimeRanges) -> Vec<(f64, f64)> {
453    let mut result = vec![];
454    if ranges.length() > 0 {
455        for index in 0..ranges.length() {
456            result.push((
457                ranges.start(index).unwrap_throw(),
458                ranges.end(index).unwrap_throw(),
459            ));
460        }
461    }
462    result
463}