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}