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}