Skip to main content

leptos_use/
use_user_media.rs

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