Skip to main content

leptos_use/
use_display_media.rs

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