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    let start = sendwrap_fn!(move || {
100        cfg_if! { if #[cfg(not(feature = "ssr"))] {
101            leptos::task::spawn_local(async move {
102                _start().await;
103                stream.with_untracked(move |stream| {
104                    if let Some(sendwrapped_stream) = stream && sendwrapped_stream.as_ref().is_ok() {
105                        set_enabled.set(true);
106                    }
107                });
108            });
109        }}
110    });
111
112    let stop = sendwrap_fn!(move || {
113        _stop();
114        set_enabled.set(false);
115    });
116
117    Effect::watch(
118        move || enabled.get(),
119        move |enabled, _, _| {
120            if *enabled {
121                leptos::task::spawn_local(async move {
122                    _start().await;
123                });
124            } else {
125                _stop();
126            }
127        },
128        true,
129    );
130
131    UseDisplayMediaReturn {
132        stream: stream.read_only(),
133        start,
134        stop,
135        enabled,
136        set_enabled,
137    }
138}
139
140#[cfg(not(feature = "ssr"))]
141async fn create_media(audio: bool) -> Result<web_sys::MediaStream, JsValue> {
142    use crate::js_fut;
143    use crate::use_window::use_window;
144
145    let media = use_window()
146        .navigator()
147        .ok_or_else(|| JsValue::from_str("Failed to access window.navigator"))
148        .and_then(|n| n.media_devices())?;
149
150    let constraints = web_sys::DisplayMediaStreamConstraints::new();
151    if audio {
152        constraints.set_audio(&JsValue::from(true));
153    }
154
155    let promise = media.get_display_media_with_constraints(&constraints)?;
156    let res = js_fut!(promise).await?;
157
158    Ok::<_, JsValue>(web_sys::MediaStream::unchecked_from_js(res))
159}
160
161// NOTE: there's no video value because it has to be `true`. Otherwise the stream would always resolve to an Error.
162/// Options for [`use_display_media`].
163#[derive(DefaultBuilder, Clone, Copy, Debug)]
164pub struct UseDisplayMediaOptions {
165    /// If the stream is enabled. Defaults to `false`.
166    enabled: MaybeRwSignal<bool>,
167
168    /// A value of `true` indicates that the returned [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream)
169    /// will contain an audio track, if audio is supported and available for the display surface chosen by the user.
170    /// The default value is `false`.
171    audio: bool,
172}
173
174impl Default for UseDisplayMediaOptions {
175    fn default() -> Self {
176        Self {
177            enabled: false.into(),
178            audio: false,
179        }
180    }
181}
182
183/// Return type of [`use_display_media`]
184#[derive(Clone)]
185pub struct UseDisplayMediaReturn<StartFn, StopFn>
186where
187    StartFn: Fn() + Clone + Send + Sync,
188    StopFn: Fn() + Clone + Send + Sync,
189{
190    /// The current [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) if it exists.
191    /// Initially this is `None` until `start` resolved successfully.
192    /// In case the stream couldn't be started, for example because the user didn't grant permission,
193    /// this has the value `Some(Err(...))`.
194    pub stream: OptionLocalSignal<Result<web_sys::MediaStream, JsValue>>,
195
196    /// Starts the screen streaming. Triggers the ask for permission if not already granted.
197    pub start: StartFn,
198
199    /// Stops the screen streaming
200    pub stop: StopFn,
201
202    /// A value of `true` indicates that the returned [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream)
203    /// has resolved successfully and thus the stream is enabled.
204    pub enabled: Signal<bool>,
205
206    /// A value of `true` is the same as calling `start()` whereas `false` is the same as calling `stop()`.
207    pub set_enabled: WriteSignal<bool>,
208}