leptos_use/
use_display_media.rs

1use crate::core::MaybeRwSignal;
2use crate::sendwrap_fn;
3use cfg_if::cfg_if;
4use default_struct_builder::DefaultBuilder;
5use leptos::prelude::*;
6use leptos::reactive::wrappers::read::Signal;
7use wasm_bindgen::{JsCast, JsValue};
8
9/// Reactive [`mediaDevices.getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) streaming.
10///
11/// ## Demo
12///
13/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_display_media)
14///
15/// ## Usage
16///
17/// ```
18/// # use leptos::prelude::*;
19/// # use leptos::logging::{log, error};
20/// # use leptos_use::{use_display_media, UseDisplayMediaReturn};
21/// #
22/// # #[component]
23/// # fn Demo() -> impl IntoView {
24/// let video_ref = NodeRef::<leptos::html::Video>::new();
25///
26/// let UseDisplayMediaReturn { stream, start, .. } = use_display_media();
27///
28/// start();
29///
30/// Effect::new(move |_|
31///     video_ref.get().map(|v| {
32///         match stream.get() {
33///             Some(Ok(s)) => v.set_src_object(Some(&s)),
34///             Some(Err(e)) => error!("Failed to get media stream: {:?}", e),
35///             None => log!("No stream yet"),
36///         }
37///     })
38/// );
39///
40/// view! { <video node_ref=video_ref controls=false autoplay=true muted=true></video> }
41/// # }
42/// ```
43///
44/// ## SendWrapped Return
45///
46/// The returned closures `start` and `stop` are sendwrapped functions. They can
47/// only be called from the same thread that called `use_display_media`.
48///
49/// ## Server-Side Rendering
50///
51/// On the server calls to `start` or any other way to enable the stream will be ignored
52/// and the stream will always be `None`.
53pub fn use_display_media(
54) -> UseDisplayMediaReturn<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
55    use_display_media_with_options(UseDisplayMediaOptions::default())
56}
57
58/// Version of [`use_display_media`] that accepts a [`UseDisplayMediaOptions`].
59pub fn use_display_media_with_options(
60    options: UseDisplayMediaOptions,
61) -> UseDisplayMediaReturn<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
62    let UseDisplayMediaOptions { enabled, audio } = options;
63
64    let (enabled, set_enabled) = enabled.into_signal();
65
66    let (stream, set_stream) = signal_local(None::<Result<web_sys::MediaStream, JsValue>>);
67
68    let _start = move || async move {
69        cfg_if! { if #[cfg(not(feature = "ssr"))] {
70            if stream.get_untracked().is_some() {
71                return;
72            }
73
74            let stream = create_media(audio).await;
75
76            set_stream.update(|s| *s = Some(stream));
77        } else {
78            let _ = audio;
79        }}
80    };
81
82    let _stop = move || {
83        if let Some(Ok(stream)) = stream.get_untracked() {
84            for track in stream.get_tracks() {
85                track.unchecked_ref::<web_sys::MediaStreamTrack>().stop();
86            }
87        }
88
89        set_stream.set(None);
90    };
91
92    let start = sendwrap_fn!(move || {
93        cfg_if! { if #[cfg(not(feature = "ssr"))] {
94            leptos::task::spawn_local(async move {
95                _start().await;
96                stream.with_untracked(move |stream| {
97                    if let Some(Ok(_)) = stream {
98                        set_enabled.set(true);
99                    }
100                });
101            });
102        }}
103    });
104
105    let stop = sendwrap_fn!(move || {
106        _stop();
107        set_enabled.set(false);
108    });
109
110    Effect::watch(
111        move || enabled.get(),
112        move |enabled, _, _| {
113            if *enabled {
114                leptos::task::spawn_local(async move {
115                    _start().await;
116                });
117            } else {
118                _stop();
119            }
120        },
121        true,
122    );
123
124    UseDisplayMediaReturn {
125        stream: stream.into(),
126        start,
127        stop,
128        enabled,
129        set_enabled,
130    }
131}
132
133#[cfg(not(feature = "ssr"))]
134async fn create_media(audio: bool) -> Result<web_sys::MediaStream, JsValue> {
135    use crate::js_fut;
136    use crate::use_window::use_window;
137
138    let media = use_window()
139        .navigator()
140        .ok_or_else(|| JsValue::from_str("Failed to access window.navigator"))
141        .and_then(|n| n.media_devices())?;
142
143    let constraints = web_sys::DisplayMediaStreamConstraints::new();
144    if audio {
145        constraints.set_audio(&JsValue::from(true));
146    }
147
148    let promise = media.get_display_media_with_constraints(&constraints)?;
149    let res = js_fut!(promise).await?;
150
151    Ok::<_, JsValue>(web_sys::MediaStream::unchecked_from_js(res))
152}
153
154// NOTE: there's no video value because it has to be `true`. Otherwise the stream would always resolve to an Error.
155/// Options for [`use_display_media`].
156#[derive(DefaultBuilder, Clone, Copy, Debug)]
157pub struct UseDisplayMediaOptions {
158    /// If the stream is enabled. Defaults to `false`.
159    enabled: MaybeRwSignal<bool>,
160
161    /// A value of `true` indicates that the returned [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream)
162    /// will contain an audio track, if audio is supported and available for the display surface chosen by the user.
163    /// The default value is `false`.
164    audio: bool,
165}
166
167impl Default for UseDisplayMediaOptions {
168    fn default() -> Self {
169        Self {
170            enabled: false.into(),
171            audio: false,
172        }
173    }
174}
175
176/// Return type of [`use_display_media`]
177#[derive(Clone)]
178pub struct UseDisplayMediaReturn<StartFn, StopFn>
179where
180    StartFn: Fn() + Clone + Send + Sync,
181    StopFn: Fn() + Clone + Send + Sync,
182{
183    /// The current [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) if it exists.
184    /// Initially this is `None` until `start` resolved successfully.
185    /// In case the stream couldn't be started, for example because the user didn't grant permission,
186    /// this has the value `Some(Err(...))`.
187    pub stream: Signal<Option<Result<web_sys::MediaStream, JsValue>>, LocalStorage>,
188
189    /// Starts the screen streaming. Triggers the ask for permission if not already granted.
190    pub start: StartFn,
191
192    /// Stops the screen streaming
193    pub stop: StopFn,
194
195    /// A value of `true` indicates that the returned [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream)
196    /// has resolved successfully and thus the stream is enabled.
197    pub enabled: Signal<bool>,
198
199    /// A value of `true` is the same as calling `start()` whereas `false` is the same as calling `stop()`.
200    pub set_enabled: WriteSignal<bool>,
201}