leptos_use/
use_user_media.rs

1use crate::core::MaybeRwSignal;
2use default_struct_builder::DefaultBuilder;
3use js_sys::{Object, Reflect};
4use leptos::prelude::*;
5use wasm_bindgen::{JsCast, JsValue};
6
7/// Reactive [`mediaDevices.getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) streaming.
8///
9/// ## Demo
10///
11/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_user_media)
12///
13/// ## Usage
14///
15/// ```
16/// # use leptos::prelude::*;
17/// # use leptos::logging::{log, error};
18/// # use leptos_use::{use_user_media, UseUserMediaReturn};
19/// #
20/// # #[component]
21/// # fn Demo() -> impl IntoView {
22/// let video_ref = NodeRef::<leptos::html::Video>::new();
23///
24/// let UseUserMediaReturn { stream, start, .. } = use_user_media();
25///
26/// start();
27///
28/// Effect::new(move |_|
29///     video_ref.get().map(|v| {
30///         match stream.get() {
31///             Some(Ok(s)) => v.set_src_object(Some(&s)),
32///             Some(Err(e)) => error!("Failed to get media stream: {:?}", e),
33///             None => log!("No stream yet"),
34///         }
35///     })
36/// );
37///
38/// view! { <video node_ref=video_ref controls=false autoplay=true muted=true></video> }
39/// # }
40/// ```
41///
42/// ## Server-Side Rendering
43///
44/// On the server calls to `start` or any other way to enable the stream will be ignored
45/// and the stream will always be `None`.
46pub fn use_user_media(
47) -> UseUserMediaReturn<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
48    use_user_media_with_options(UseUserMediaOptions::default())
49}
50
51/// Version of [`use_user_media`] that takes a `UseUserMediaOptions`. See [`use_user_media`] for how to use.
52pub fn use_user_media_with_options(
53    options: UseUserMediaOptions,
54) -> UseUserMediaReturn<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
55    let UseUserMediaOptions {
56        enabled,
57        video,
58        audio,
59        ..
60    } = options;
61
62    let (enabled, set_enabled) = enabled.into_signal();
63
64    let (stream, set_stream) = signal_local(None::<Result<web_sys::MediaStream, JsValue>>);
65
66    let _start = {
67        let audio = audio.clone();
68        let video = video.clone();
69
70        move || async move {
71            #[cfg(not(feature = "ssr"))]
72            {
73                if stream.get_untracked().is_some() {
74                    return;
75                }
76
77                let stream = create_media(Some(video), Some(audio)).await;
78
79                set_stream.update(|s| *s = Some(stream));
80            }
81
82            #[cfg(feature = "ssr")]
83            {
84                let _ = video;
85                let _ = audio;
86            }
87        }
88    };
89
90    let _stop = move || {
91        if let Some(Ok(stream)) = stream.get_untracked() {
92            for track in stream.get_tracks() {
93                track.unchecked_ref::<web_sys::MediaStreamTrack>().stop();
94            }
95        }
96
97        set_stream.set(None);
98    };
99
100    let start = {
101        let _start = _start.clone();
102        move || {
103            #[cfg(not(feature = "ssr"))]
104            {
105                leptos::task::spawn_local({
106                    let _start = _start.clone();
107
108                    async move {
109                        _start().await;
110                        stream.with_untracked(move |stream| {
111                            if let Some(Ok(_)) = stream {
112                                set_enabled.set(true);
113                            }
114                        });
115                    }
116                });
117            }
118        }
119    };
120
121    let stop = move || {
122        _stop();
123        set_enabled.set(false);
124    };
125
126    Effect::watch(
127        move || enabled.get(),
128        move |enabled, _, _| {
129            if *enabled {
130                leptos::task::spawn_local({
131                    let _start = _start.clone();
132
133                    async move {
134                        _start().await;
135                    }
136                });
137            } else {
138                _stop();
139            }
140        },
141        true,
142    );
143
144    UseUserMediaReturn {
145        stream: stream.into(),
146        start,
147        stop,
148        enabled,
149        set_enabled,
150    }
151}
152
153#[cfg(not(feature = "ssr"))]
154async fn create_media(
155    video: Option<VideoConstraints>,
156    audio: Option<AudioConstraints>,
157) -> Result<web_sys::MediaStream, JsValue> {
158    use crate::js_fut;
159    use crate::use_window::use_window;
160    use js_sys::Array;
161
162    let media = use_window()
163        .navigator()
164        .ok_or_else(|| JsValue::from_str("Failed to access window.navigator"))
165        .and_then(|n| n.media_devices())?;
166
167    let constraints = web_sys::MediaStreamConstraints::new();
168    if let Some(video_shadow_constraints) = video {
169        match video_shadow_constraints {
170            VideoConstraints::Bool(b) => constraints.set_video(&JsValue::from(b)),
171            VideoConstraints::Constraints(boxed_constraints) => {
172                let VideoTrackConstraints {
173                    device_id,
174                    facing_mode,
175                    frame_rate,
176                    height,
177                    width,
178                    viewport_height,
179                    viewport_width,
180                    viewport_offset_x,
181                    viewport_offset_y,
182                } = *boxed_constraints;
183
184                let video_constraints = web_sys::MediaTrackConstraints::new();
185
186                if !device_id.is_empty() {
187                    video_constraints.set_device_id(
188                        &Array::from_iter(device_id.into_iter().map(JsValue::from)).into(),
189                    );
190                }
191
192                if let Some(value) = facing_mode {
193                    video_constraints.set_facing_mode(&value.to_jsvalue());
194                }
195
196                if let Some(value) = frame_rate {
197                    video_constraints.set_frame_rate(&value.to_jsvalue());
198                }
199
200                if let Some(value) = height {
201                    video_constraints.set_height(&value.to_jsvalue());
202                }
203
204                if let Some(value) = width {
205                    video_constraints.set_width(&value.to_jsvalue());
206                }
207
208                if let Some(value) = viewport_height {
209                    video_constraints.set_viewport_height(&value.to_jsvalue());
210                }
211
212                if let Some(value) = viewport_width {
213                    video_constraints.set_viewport_width(&value.to_jsvalue());
214                }
215                if let Some(value) = viewport_offset_x {
216                    video_constraints.set_viewport_offset_x(&value.to_jsvalue());
217                }
218
219                if let Some(value) = viewport_offset_y {
220                    video_constraints.set_viewport_offset_y(&value.to_jsvalue());
221                }
222
223                constraints.set_video(&JsValue::from(video_constraints));
224            }
225        }
226    }
227    if let Some(audio_shadow_constraints) = audio {
228        match audio_shadow_constraints {
229            AudioConstraints::Bool(b) => constraints.set_audio(&JsValue::from(b)),
230            AudioConstraints::Constraints(boxed_constraints) => {
231                let AudioTrackConstraints {
232                    device_id,
233                    auto_gain_control,
234                    channel_count,
235                    echo_cancellation,
236                    noise_suppression,
237                } = *boxed_constraints;
238
239                let audio_constraints = web_sys::MediaTrackConstraints::new();
240
241                if !device_id.is_empty() {
242                    audio_constraints.set_device_id(
243                        &Array::from_iter(device_id.into_iter().map(JsValue::from)).into(),
244                    );
245                }
246                if let Some(value) = auto_gain_control {
247                    audio_constraints.set_auto_gain_control(&JsValue::from(&value.to_jsvalue()));
248                }
249                if let Some(value) = channel_count {
250                    audio_constraints.set_channel_count(&JsValue::from(&value.to_jsvalue()));
251                }
252                if let Some(value) = echo_cancellation {
253                    audio_constraints.set_echo_cancellation(&JsValue::from(&value.to_jsvalue()));
254                }
255                if let Some(value) = noise_suppression {
256                    audio_constraints.set_noise_suppression(&JsValue::from(&value.to_jsvalue()));
257                }
258
259                constraints.set_audio(&JsValue::from(audio_constraints));
260            }
261        }
262    }
263
264    let promise = media.get_user_media_with_constraints(&constraints)?;
265    let res = js_fut!(promise).await?;
266
267    Ok::<_, JsValue>(web_sys::MediaStream::unchecked_from_js(res))
268}
269
270/// Options for [`use_user_media_with_options`].
271///
272/// Either or both constraints must be specified.
273/// If the browser cannot find all media tracks with the specified types that meet the constraints given,
274/// then the returned promise is rejected with `NotFoundError`
275#[derive(DefaultBuilder, Clone, Debug)]
276pub struct UseUserMediaOptions {
277    /// If the stream is enabled. Defaults to `false`.
278    enabled: MaybeRwSignal<bool>,
279    /// Constraint parameter describing video media type requested
280    /// The default value is `true`.
281    #[builder(into)]
282    video: VideoConstraints,
283    /// Constraint parameter describing audio media type requested
284    /// The default value is `false`.
285    #[builder(into)]
286    audio: AudioConstraints,
287}
288
289impl Default for UseUserMediaOptions {
290    fn default() -> Self {
291        Self {
292            enabled: false.into(),
293            video: true.into(),
294            audio: false.into(),
295        }
296    }
297}
298
299/// Return type of [`use_user_media`].
300#[derive(Clone)]
301pub struct UseUserMediaReturn<StartFn, StopFn>
302where
303    StartFn: Fn() + Clone + Send + Sync,
304    StopFn: Fn() + Clone + Send + Sync,
305{
306    /// The current [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) if it exists.
307    /// Initially this is `None` until `start` resolved successfully.
308    /// In case the stream couldn't be started, for example because the user didn't grant permission,
309    /// this has the value `Some(Err(...))`.
310    pub stream: Signal<Option<Result<web_sys::MediaStream, JsValue>>, LocalStorage>,
311
312    /// Starts the screen streaming. Triggers the ask for permission if not already granted.
313    pub start: StartFn,
314
315    /// Stops the screen streaming
316    pub stop: StopFn,
317
318    /// A value of `true` indicates that the returned [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream)
319    /// has resolved successfully and thus the stream is enabled.
320    pub enabled: Signal<bool>,
321
322    /// A value of `true` is the same as calling `start()` whereas `false` is the same as calling `stop()`.
323    pub set_enabled: WriteSignal<bool>,
324}
325
326#[derive(Clone, Debug)]
327pub enum ConstraintExactIdeal<T> {
328    Single(Option<T>),
329    ExactIdeal { exact: Option<T>, ideal: Option<T> },
330}
331
332impl<T> Default for ConstraintExactIdeal<T>
333where
334    T: Default,
335{
336    fn default() -> Self {
337        ConstraintExactIdeal::Single(Some(T::default()))
338    }
339}
340
341impl<T> ConstraintExactIdeal<T> {
342    pub fn exact(mut self, value: T) -> Self {
343        if let ConstraintExactIdeal::ExactIdeal {
344            exact: ref mut e, ..
345        } = &mut self
346        {
347            *e = Some(value);
348        }
349
350        self
351    }
352
353    pub fn ideal(mut self, value: T) -> Self {
354        if let ConstraintExactIdeal::ExactIdeal {
355            ideal: ref mut i, ..
356        } = &mut self
357        {
358            *i = Some(value);
359        }
360
361        self
362    }
363}
364
365impl<T> ConstraintExactIdeal<T>
366where
367    T: Into<JsValue> + Clone,
368{
369    pub fn to_jsvalue(&self) -> JsValue {
370        match self {
371            ConstraintExactIdeal::Single(value) => value.clone().unwrap().into(),
372            ConstraintExactIdeal::ExactIdeal { exact, ideal } => {
373                let obj = Object::new();
374
375                if let Some(value) = exact {
376                    Reflect::set(&obj, &JsValue::from_str("exact"), &value.clone().into()).unwrap();
377                }
378                if let Some(value) = ideal {
379                    Reflect::set(&obj, &JsValue::from_str("ideal"), &value.clone().into()).unwrap();
380                }
381
382                JsValue::from(obj)
383            }
384        }
385    }
386}
387
388impl From<&'static str> for ConstraintExactIdeal<&'static str> {
389    fn from(value: &'static str) -> Self {
390        ConstraintExactIdeal::Single(Some(value))
391    }
392}
393
394#[derive(Clone, Debug)]
395pub enum ConstraintRange<T> {
396    Single(Option<T>),
397    Range {
398        min: Option<T>,
399        max: Option<T>,
400        exact: Option<T>,
401        ideal: Option<T>,
402    },
403}
404
405impl<T> Default for ConstraintRange<T>
406where
407    T: Default,
408{
409    fn default() -> Self {
410        ConstraintRange::Single(Some(T::default()))
411    }
412}
413
414impl<T> ConstraintRange<T>
415where
416    T: Clone + std::fmt::Debug,
417{
418    pub fn new(value: Option<T>) -> Self {
419        ConstraintRange::Single(value)
420    }
421
422    pub fn min(mut self, value: T) -> Self {
423        if let ConstraintRange::Range { ref mut min, .. } = self {
424            *min = Some(value);
425        }
426        self
427    }
428
429    pub fn max(mut self, value: T) -> Self {
430        if let ConstraintRange::Range { ref mut max, .. } = self {
431            *max = Some(value);
432        }
433        self
434    }
435
436    pub fn exact(mut self, value: T) -> Self {
437        if let ConstraintRange::Range { ref mut exact, .. } = &mut self {
438            *exact = Some(value);
439        }
440
441        self
442    }
443
444    pub fn ideal(mut self, value: T) -> Self {
445        if let ConstraintRange::Range { ref mut ideal, .. } = &mut self {
446            *ideal = Some(value);
447        }
448
449        self
450    }
451}
452
453impl<T> ConstraintRange<T>
454where
455    T: Into<JsValue> + Clone,
456{
457    pub fn to_jsvalue(&self) -> JsValue {
458        match self {
459            ConstraintRange::Single(value) => value.clone().unwrap().into(),
460            ConstraintRange::Range {
461                min,
462                max,
463                exact,
464                ideal,
465            } => {
466                let obj = Object::new();
467
468                if let Some(min_value) = min {
469                    Reflect::set(&obj, &JsValue::from_str("min"), &min_value.clone().into())
470                        .unwrap();
471                }
472                if let Some(max_value) = max {
473                    Reflect::set(&obj, &JsValue::from_str("max"), &max_value.clone().into())
474                        .unwrap();
475                }
476                if let Some(value) = exact {
477                    Reflect::set(&obj, &JsValue::from_str("exact"), &value.clone().into()).unwrap();
478                }
479                if let Some(value) = ideal {
480                    Reflect::set(&obj, &JsValue::from_str("ideal"), &value.clone().into()).unwrap();
481                }
482
483                JsValue::from(obj)
484            }
485        }
486    }
487}
488
489impl From<f64> for ConstraintDouble {
490    fn from(value: f64) -> Self {
491        ConstraintRange::Single(Some(value))
492    }
493}
494
495impl From<u32> for ConstraintULong {
496    fn from(value: u32) -> Self {
497        ConstraintRange::Single(Some(value))
498    }
499}
500
501pub type ConstraintBool = ConstraintExactIdeal<bool>;
502
503impl From<bool> for ConstraintBool {
504    fn from(value: bool) -> Self {
505        ConstraintExactIdeal::Single(Some(value))
506    }
507}
508
509pub type ConstraintDouble = ConstraintRange<f64>;
510pub type ConstraintULong = ConstraintRange<u32>;
511
512#[derive(Clone, Copy, Debug)]
513pub enum FacingMode {
514    User,
515    Environment,
516    Left,
517    Right,
518}
519
520impl FacingMode {
521    pub fn as_str(self) -> &'static str {
522        match self {
523            FacingMode::User => "user",
524            FacingMode::Environment => "environment",
525            FacingMode::Left => "left",
526            FacingMode::Right => "right",
527        }
528    }
529}
530
531pub type ConstraintFacingMode = ConstraintExactIdeal<FacingMode>;
532
533impl From<FacingMode> for ConstraintFacingMode {
534    fn from(value: FacingMode) -> Self {
535        ConstraintFacingMode::Single(Some(value))
536    }
537}
538
539impl ConstraintFacingMode {
540    pub fn to_jsvalue(&self) -> JsValue {
541        match self {
542            ConstraintExactIdeal::Single(value) => JsValue::from_str((*value).unwrap().as_str()),
543            ConstraintExactIdeal::ExactIdeal { exact, ideal } => {
544                let obj = Object::new();
545
546                if let Some(value) = exact {
547                    Reflect::set(
548                        &obj,
549                        &JsValue::from_str("exact"),
550                        &JsValue::from_str(value.as_str()),
551                    )
552                    .unwrap();
553                }
554                if let Some(value) = ideal {
555                    Reflect::set(
556                        &obj,
557                        &JsValue::from_str("ideal"),
558                        &JsValue::from_str(value.as_str()),
559                    )
560                    .unwrap();
561                }
562
563                JsValue::from(obj)
564            }
565        }
566    }
567}
568
569#[derive(Clone, Debug)]
570pub enum AudioConstraints {
571    Bool(bool),
572    Constraints(Box<AudioTrackConstraints>),
573}
574
575impl From<bool> for AudioConstraints {
576    fn from(value: bool) -> Self {
577        AudioConstraints::Bool(value)
578    }
579}
580
581impl From<AudioTrackConstraints> for AudioConstraints {
582    fn from(value: AudioTrackConstraints) -> Self {
583        AudioConstraints::Constraints(Box::new(value))
584    }
585}
586
587#[derive(Clone, Debug)]
588pub enum VideoConstraints {
589    Bool(bool),
590    Constraints(Box<VideoTrackConstraints>),
591}
592
593impl From<bool> for VideoConstraints {
594    fn from(value: bool) -> Self {
595        VideoConstraints::Bool(value)
596    }
597}
598
599impl From<VideoTrackConstraints> for VideoConstraints {
600    fn from(value: VideoTrackConstraints) -> Self {
601        VideoConstraints::Constraints(Box::new(value))
602    }
603}
604
605pub trait IntoDeviceIds<M> {
606    fn into_device_ids(self) -> Vec<String>;
607}
608
609impl<T> IntoDeviceIds<String> for T
610where
611    T: Into<String>,
612{
613    fn into_device_ids(self) -> Vec<String> {
614        vec![self.into()]
615    }
616}
617
618pub struct VecMarker;
619
620impl<T, I> IntoDeviceIds<VecMarker> for T
621where
622    T: IntoIterator<Item = I>,
623    I: Into<String>,
624{
625    fn into_device_ids(self) -> Vec<String> {
626        self.into_iter().map(Into::into).collect()
627    }
628}
629
630#[derive(DefaultBuilder, Default, Clone, Debug)]
631#[allow(dead_code)]
632pub struct AudioTrackConstraints {
633    #[builder(skip)]
634    device_id: Vec<String>,
635
636    #[builder(into)]
637    auto_gain_control: Option<ConstraintBool>,
638    #[builder(into)]
639    channel_count: Option<ConstraintULong>,
640    #[builder(into)]
641    echo_cancellation: Option<ConstraintBool>,
642    #[builder(into)]
643    noise_suppression: Option<ConstraintBool>,
644}
645
646impl AudioTrackConstraints {
647    pub fn new() -> Self {
648        AudioTrackConstraints::default()
649    }
650
651    pub fn device_id<M>(mut self, value: impl IntoDeviceIds<M>) -> Self {
652        self.device_id = value.into_device_ids();
653        self
654    }
655}
656
657#[derive(DefaultBuilder, Default, Clone, Debug)]
658pub struct VideoTrackConstraints {
659    #[builder(skip)]
660    pub device_id: Vec<String>,
661
662    #[builder(into)]
663    pub facing_mode: Option<ConstraintFacingMode>,
664    #[builder(into)]
665    pub frame_rate: Option<ConstraintDouble>,
666    #[builder(into)]
667    pub height: Option<ConstraintULong>,
668    #[builder(into)]
669    pub width: Option<ConstraintULong>,
670    #[builder(into)]
671    pub viewport_offset_x: Option<ConstraintULong>,
672    #[builder(into)]
673    pub viewport_offset_y: Option<ConstraintULong>,
674    #[builder(into)]
675    pub viewport_height: Option<ConstraintULong>,
676    #[builder(into)]
677    pub viewport_width: Option<ConstraintULong>,
678}
679
680impl VideoTrackConstraints {
681    pub fn new() -> Self {
682        VideoTrackConstraints::default()
683    }
684
685    pub fn device_id<M>(mut self, value: impl IntoDeviceIds<M>) -> Self {
686        self.device_id = value.into_device_ids();
687        self
688    }
689}