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}