From bb5054d403cb78aab02fb0430f75edc022108842 Mon Sep 17 00:00:00 2001 From: Maccesch Date: Tue, 5 Dec 2023 23:12:31 +0000 Subject: [PATCH] made display media api closer to solidjs-use and rest of library --- CHANGELOG.md | 2 +- Cargo.toml | 1 + examples/use_display_media/src/main.rs | 49 +++-- examples/use_display_media/style/output.css | 63 ++++++- src/core/maybe_rw_signal.rs | 11 ++ src/lib.rs | 1 - src/use_display_media.rs | 193 ++++++++++++++++---- 7 files changed, 269 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 199ab63..eaea869 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New Functions 🚀 -- `use_display_media` +- `use_display_media` (thanks to @seanaye) ### Breaking Changes 🛠 diff --git a/Cargo.toml b/Cargo.toml index a4bcbda..d5d324b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ features = [ "MediaDevices", "MediaQueryList", "MediaStream", + "MediaStreamTrack", "MouseEvent", "MutationObserver", "MutationObserverInit", diff --git a/examples/use_display_media/src/main.rs b/examples/use_display_media/src/main.rs index 01cb48e..d1d50d8 100644 --- a/examples/use_display_media/src/main.rs +++ b/examples/use_display_media/src/main.rs @@ -1,22 +1,50 @@ use leptos::*; -use leptos_use::docs::{demo_or_body, Note}; -use leptos_use::use_display_media; +use leptos_use::docs::demo_or_body; +use leptos_use::{use_display_media, UseDisplayMediaReturn}; #[component] fn Demo() -> impl IntoView { - let stream = use_display_media(None); let video_ref = create_node_ref::(); - create_effect(move |_| match stream.get() { - Some(Ok(s)) => { - video_ref.get().expect("video element ref not created").set_src_object(Some(&s)); - video_ref.get().map(|v| v.play()); + let UseDisplayMediaReturn { + stream, + enabled, + set_enabled, + .. + } = use_display_media(); + + create_effect(move |_| { + match stream.get() { + Some(Ok(s)) => { + video_ref.get().map(|v| v.set_src_object(Some(&s))); + return; + } + Some(Err(e)) => logging::error!("Failed to get media stream: {:?}", e), + None => logging::log!("No stream yet"), } - Some(Err(e)) => log::error!("Failed to get media stream: {:?}", e), - None => log::debug!("No stream yet"), + + video_ref.get().map(|v| v.set_src_object(None)); }); - view! { } + view! { +
+
+ +
+ +
+ +
+
+ } } fn main() { @@ -27,4 +55,3 @@ fn main() { view! { } }) } - diff --git a/examples/use_display_media/style/output.css b/examples/use_display_media/style/output.css index ab5191f..291c6c5 100644 --- a/examples/use_display_media/style/output.css +++ b/examples/use_display_media/style/output.css @@ -1,4 +1,4 @@ -[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { +[type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { -webkit-appearance: none; -moz-appearance: none; appearance: none; @@ -15,7 +15,7 @@ --tw-shadow: 0 0 #0000; } -[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { +[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { outline: 2px solid transparent; outline-offset: 2px; --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); @@ -44,6 +44,11 @@ input::placeholder,textarea::placeholder { ::-webkit-date-and-time-value { min-height: 1.5em; + text-align: inherit; +} + +::-webkit-datetime-edit { + display: inline-flex; } ::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field { @@ -61,7 +66,7 @@ select { print-color-adjust: exact; } -[multiple] { +[multiple],[size]:where(select:not([size="1"])) { background-image: initial; background-position: initial; background-repeat: unset; @@ -126,10 +131,26 @@ select { background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); } +@media (forced-colors: active) { + [type='checkbox']:checked { + -webkit-appearance: auto; + -moz-appearance: auto; + appearance: auto; + } +} + [type='radio']:checked { background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); } +@media (forced-colors: active) { + [type='radio']:checked { + -webkit-appearance: auto; + -moz-appearance: auto; + appearance: auto; + } +} + [type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus { border-color: transparent; background-color: currentColor; @@ -144,6 +165,14 @@ select { background-repeat: no-repeat; } +@media (forced-colors: active) { + [type='checkbox']:indeterminate { + -webkit-appearance: auto; + -moz-appearance: auto; + appearance: auto; + } +} + [type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { border-color: transparent; background-color: currentColor; @@ -264,8 +293,32 @@ select { --tw-backdrop-sepia: ; } -.block { - display: block; +.static { + position: static; +} + +.flex { + display: flex; +} + +.h-96 { + height: 24rem; +} + +.w-auto { + width: auto; +} + +.flex-col { + flex-direction: column; +} + +.gap-4 { + gap: 1rem; +} + +.text-center { + text-align: center; } .text-\[--brand-color\] { diff --git a/src/core/maybe_rw_signal.rs b/src/core/maybe_rw_signal.rs index 6aad544..f1e8bac 100644 --- a/src/core/maybe_rw_signal.rs +++ b/src/core/maybe_rw_signal.rs @@ -1,4 +1,5 @@ use leptos::*; +use std::fmt::Debug; pub enum MaybeRwSignal where @@ -33,6 +34,16 @@ impl Default for MaybeRwSignal { } } +impl Debug for MaybeRwSignal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(t) => f.debug_tuple("Static").field(t).finish(), + Self::DynamicRw(r, w) => f.debug_tuple("DynamicRw").field(r).field(w).finish(), + Self::DynamicRead(s) => f.debug_tuple("DynamicRead").field(s).finish(), + } + } +} + impl From> for MaybeRwSignal { fn from(s: Signal) -> Self { Self::DynamicRead(s) diff --git a/src/lib.rs b/src/lib.rs index 09811d6..d42079d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,7 +69,6 @@ pub use is_none::*; pub use is_ok::*; pub use is_some::*; pub use on_click_outside::*; -pub use use_display_media::*; pub use signal_debounced::*; pub use signal_throttled::*; pub use use_active_element::*; diff --git a/src/use_display_media.rs b/src/use_display_media.rs index 15d54f9..2dbc5f4 100644 --- a/src/use_display_media.rs +++ b/src/use_display_media.rs @@ -1,10 +1,12 @@ -use leptos::*; -use wasm_bindgen::{JsValue, JsCast}; -use web_sys::{DisplayMediaStreamConstraints, MediaStream}; +use crate::core::MaybeRwSignal; use crate::use_window::use_window; +use cfg_if::cfg_if; +use default_struct_builder::DefaultBuilder; +use leptos::*; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{DisplayMediaStreamConstraints, MediaStream}; - -/// Get a Resource containing a media stream from the user's display. +/// Reactive [`mediaDevices.getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) streaming. /// /// ## Demo /// @@ -14,49 +16,174 @@ use crate::use_window::use_window; /// /// ``` /// # use leptos::*; -/// # use leptos_use::use_display_media; +/// # use leptos_use::{use_display_media, UseDisplayMediaReturn}; /// # /// # #[component] /// # fn Demo() -> impl IntoView { -/// # let stream = use_display_media(None); -/// # -/// # let video_ref = create_node_ref::(); -/// # create_effect(move |_| match stream.get() { -/// # Some(Ok(s)) => { -/// # video_ref.get().expect("video element ref not created").set_src_object(Some(&s)); -/// # video_ref.get().map(|v| v.play()); -/// # } -/// # Some(Err(e)) => log::error!("Failed to get media stream: {:?}", e), -/// # None => log::debug!("No stream yet"), -/// # }); -/// # -/// # view! { } +/// let video_ref = create_node_ref::(); +/// +/// let UseDisplayMediaReturn { stream, start, .. } = use_display_media(); +/// +/// start(); +/// +/// create_effect(move |_| +/// video_ref.get().map(|v| { +/// match stream.get() { +/// Some(Ok(s)) => v.set_src_object(Some(&s)), +/// Some(Err(e)) => logging::error!("Failed to get media stream: {:?}", e), +/// None => logging::log!("No stream yet"), +/// } +/// }) +/// ); +/// +/// view! { } /// # } /// ``` -pub fn use_display_media(options: S) -> UseDisplayReturn -where - S: Into>>, -{ - let opts: MaybeSignal> = options.into(); - create_local_resource(move || opts.with(|o| o.as_ref().cloned()), create_media) +/// +/// ## Server-Side Rendering +/// +/// On the server calls to `start` or any other way to enable the stream will be ignored +/// and the stream will always be `None`. +pub fn use_display_media() -> UseDisplayMediaReturn { + use_display_media_with_options(UseDisplayMediaOptions::default()) } -async fn create_media(opts: Option) -> Result { +/// Version of [`use_display_media`] that accepts a [`UseDisplayMediaOptions`]. +pub fn use_display_media_with_options( + options: UseDisplayMediaOptions, +) -> UseDisplayMediaReturn { + let UseDisplayMediaOptions { enabled, audio } = options; + + let (enabled, set_enabled) = enabled.into_signal(); + + let (stream, set_stream) = create_signal(None::>); + + let _start = move || async move { + cfg_if! { if #[cfg(not(feature = "ssr"))] { + if stream.get_untracked().is_some() { + return; + } + + let stream = create_media(audio).await; + + set_stream.update(|s| *s = Some(stream)); + }} + }; + + let _stop = move || { + if let Some(Ok(stream)) = stream.get_untracked() { + for track in stream.get_tracks() { + track.unchecked_ref::().stop(); + } + } + + set_stream.set(None); + }; + + let start = move || { + cfg_if! { if #[cfg(not(feature = "ssr"))] { + spawn_local(async move { + _start().await; + stream.with_untracked(move |stream| { + if let Some(Ok(_)) = stream { + set_enabled.set(true); + } + }); + }); + }} + }; + + let stop = move || { + _stop(); + set_enabled.set(false); + }; + + let _ = watch( + move || enabled.get(), + move |enabled, _, _| { + if *enabled { + spawn_local(async move { + _start().await; + }); + } else { + _stop(); + } + }, + true, + ); + + UseDisplayMediaReturn { + stream: stream.into(), + start, + stop, + enabled, + set_enabled, + } +} + +#[cfg(not(feature = "ssr"))] +async fn create_media(audio: bool) -> Result { let media = use_window() .navigator() .ok_or_else(|| JsValue::from_str("Failed to access window.navigator")) .and_then(|n| n.media_devices())?; - let promise = match opts { - Some(o) => media.get_display_media_with_constraints(&o), - None => media.get_display_media(), - }?; + let mut constraints = DisplayMediaStreamConstraints::new(); + if audio { + constraints.audio(&JsValue::from(true)); + } + + let promise = media.get_display_media_with_constraints(&constraints)?; let res = wasm_bindgen_futures::JsFuture::from(promise).await?; + Ok::<_, JsValue>(MediaStream::unchecked_from_js(res)) } +// NOTE: there's no video value because it has to be `true`. Otherwise the stream would always resolve to an Error. +/// Options for [`use_display_media`]. +#[derive(DefaultBuilder, Clone, Copy, Debug)] +pub struct UseDisplayMediaOptions { + /// If the stream is enabled. Defaults to `false`. + enabled: MaybeRwSignal, -/// A leptos resource which optionally accepts a [DisplayMediaParamContraints](https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.DisplayMediaStreamConstraints.html) -/// The resource contains a result containing the media stream or the rejected JsValue -type UseDisplayReturn = Resource, Result>; + /// A value of `true` indicates that the returned [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) + /// will contain an audio track, if audio is supported and available for the display surface chosen by the user. + /// The default value is `false`. + audio: bool, +} +impl Default for UseDisplayMediaOptions { + fn default() -> Self { + Self { + enabled: false.into(), + audio: false, + } + } +} + +/// Return type of [`use_display_media`] +#[derive(Clone)] +pub struct UseDisplayMediaReturn +where + StartFn: Fn() + Clone, + StopFn: Fn() + Clone, +{ + /// The current [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) if it exists. + /// Initially this is `None` until `start` resolved successfully. + /// In case the stream couldn't be started, for example because the user didn't grant permission, + /// this has the value `Some(Err(...))`. + pub stream: Signal>>, + + /// Starts the screen streaming. Triggers the ask for permission if not already granted. + pub start: StartFn, + + /// Stops the screen streaming + pub stop: StopFn, + + /// A value of `true` indicates that the returned [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) + /// has resolved successfully and thus the stream is enabled. + pub enabled: Signal, + + /// A value of `true` is the same as calling `start()` whereas `false` is the same as calling `stop()`. + pub set_enabled: WriteSignal, +}