1use crate::core::{MaybeRwSignal, OptionLocalRwSignal, OptionLocalSignal};
2use default_struct_builder::DefaultBuilder;
3use js_sys::{Object, Reflect};
4use leptos::prelude::*;
5use wasm_bindgen::{JsCast, JsValue};
6
7pub fn use_user_media()
49-> UseUserMediaReturn<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
50 use_user_media_with_options(UseUserMediaOptions::default())
51}
52
53pub fn use_user_media_with_options(
55 options: UseUserMediaOptions,
56) -> UseUserMediaReturn<impl Fn() + Clone + Send + Sync, impl Fn() + Clone + Send + Sync> {
57 let UseUserMediaOptions {
58 enabled,
59 video,
60 audio,
61 ..
62 } = options;
63
64 let (enabled, set_enabled) = enabled.into_signal();
65
66 let stream = OptionLocalRwSignal::<Result<web_sys::MediaStream, JsValue>>::new();
67
68 let _start = {
69 let audio = audio.clone();
70 let video = video.clone();
71
72 move || async move {
73 #[cfg(not(feature = "ssr"))]
74 {
75 if stream.get_untracked().is_some() {
76 return;
77 }
78
79 let new_stream = create_media(Some(video), Some(audio)).await;
80
81 stream.update(|s| *s = Some(new_stream));
82 }
83
84 #[cfg(feature = "ssr")]
85 {
86 let _ = video;
87 let _ = audio;
88 }
89 }
90 };
91
92 let _stop = move || {
93 if let Some(sendwrapped_stream) = stream.get_untracked()
94 && let Ok(stream) = sendwrapped_stream.as_ref()
95 {
96 for track in stream.get_tracks() {
97 track.unchecked_ref::<web_sys::MediaStreamTrack>().stop();
98 }
99 }
100
101 stream.set(None);
102 };
103
104 let start = {
105 #[cfg(not(feature = "ssr"))]
106 let _start = _start.clone();
107 move || {
108 #[cfg(not(feature = "ssr"))]
109 {
110 leptos::task::spawn_local({
111 let _start = _start.clone();
112
113 async move {
114 _start().await;
115 stream.with_untracked(move |stream| {
116 if let Some(sendwrapped_stream) = stream
117 && sendwrapped_stream.as_ref().is_ok()
118 {
119 set_enabled.set(true);
120 }
121 });
122 }
123 });
124 }
125 }
126 };
127
128 let stop = move || {
129 _stop();
130 set_enabled.set(false);
131 };
132
133 Effect::watch(
134 move || enabled.get(),
135 move |enabled, _, _| {
136 if *enabled {
137 leptos::task::spawn_local({
138 #[cfg(not(feature = "ssr"))]
139 let _start = _start.clone();
140
141 async move {
142 _start().await;
143 }
144 });
145 } else {
146 _stop();
147 }
148 },
149 true,
150 );
151
152 UseUserMediaReturn {
153 stream: stream.read_only(),
154 start,
155 stop,
156 enabled,
157 set_enabled,
158 }
159}
160
161#[cfg(not(feature = "ssr"))]
162async fn create_media(
163 video: Option<VideoConstraints>,
164 audio: Option<AudioConstraints>,
165) -> Result<web_sys::MediaStream, JsValue> {
166 use crate::use_window::use_window;
167 use crate::{js, js_fut};
168 use js_sys::Array;
169
170 let media = use_window()
171 .navigator()
172 .ok_or_else(|| JsValue::from_str("Failed to access window.navigator"))
173 .and_then(|n| n.media_devices())?;
174
175 let constraints = web_sys::MediaStreamConstraints::new();
176 if let Some(video_shadow_constraints) = video {
177 match video_shadow_constraints {
178 VideoConstraints::Bool(b) => constraints.set_video(&JsValue::from(b)),
179 VideoConstraints::Constraints(boxed_constraints) => {
180 let VideoTrackConstraints {
181 device_id,
182 facing_mode,
183 frame_rate,
184 height,
185 width,
186 viewport_height,
187 viewport_width,
188 viewport_offset_x,
189 viewport_offset_y,
190 zoom,
191 } = *boxed_constraints;
192
193 let video_constraints = web_sys::MediaTrackConstraints::new();
194
195 if !device_id.is_empty() {
196 video_constraints.set_device_id(
197 &Array::from_iter(device_id.into_iter().map(JsValue::from)).into(),
198 );
199 }
200
201 if let Some(value) = facing_mode {
202 video_constraints.set_facing_mode(&value.to_jsvalue());
203 }
204
205 if let Some(value) = frame_rate {
206 video_constraints.set_frame_rate(&value.to_jsvalue());
207 }
208
209 if let Some(value) = height {
210 video_constraints.set_height(&value.to_jsvalue());
211 }
212
213 if let Some(value) = width {
214 video_constraints.set_width(&value.to_jsvalue());
215 }
216
217 if let Some(value) = viewport_height {
218 video_constraints.set_viewport_height(&value.to_jsvalue());
219 }
220
221 if let Some(value) = viewport_width {
222 video_constraints.set_viewport_width(&value.to_jsvalue());
223 }
224 if let Some(value) = viewport_offset_x {
225 video_constraints.set_viewport_offset_x(&value.to_jsvalue());
226 }
227
228 if let Some(value) = viewport_offset_y {
229 video_constraints.set_viewport_offset_y(&value.to_jsvalue());
230 }
231
232 let js_value = JsValue::from(video_constraints);
233
234 if let Some(value) = zoom {
235 js! { js_value["zoom"] = value.to_jsvalue()};
237 }
238
239 constraints.set_video(&js_value);
240 }
241 }
242 }
243 if let Some(audio_shadow_constraints) = audio {
244 match audio_shadow_constraints {
245 AudioConstraints::Bool(b) => constraints.set_audio(&JsValue::from(b)),
246 AudioConstraints::Constraints(boxed_constraints) => {
247 let AudioTrackConstraints {
248 device_id,
249 auto_gain_control,
250 channel_count,
251 echo_cancellation,
252 noise_suppression,
253 } = *boxed_constraints;
254
255 let audio_constraints = web_sys::MediaTrackConstraints::new();
256
257 if !device_id.is_empty() {
258 audio_constraints.set_device_id(
259 &Array::from_iter(device_id.into_iter().map(JsValue::from)).into(),
260 );
261 }
262 if let Some(value) = auto_gain_control {
263 audio_constraints.set_auto_gain_control(&JsValue::from(&value.to_jsvalue()));
264 }
265 if let Some(value) = channel_count {
266 audio_constraints.set_channel_count(&JsValue::from(&value.to_jsvalue()));
267 }
268 if let Some(value) = echo_cancellation {
269 audio_constraints.set_echo_cancellation(&JsValue::from(&value.to_jsvalue()));
270 }
271 if let Some(value) = noise_suppression {
272 audio_constraints.set_noise_suppression(&JsValue::from(&value.to_jsvalue()));
273 }
274
275 constraints.set_audio(&JsValue::from(audio_constraints));
276 }
277 }
278 }
279
280 let promise = media.get_user_media_with_constraints(&constraints)?;
281 let res = js_fut!(promise).await?;
282
283 Ok::<_, JsValue>(web_sys::MediaStream::unchecked_from_js(res))
284}
285
286#[derive(DefaultBuilder, Clone, Debug)]
292pub struct UseUserMediaOptions {
293 enabled: MaybeRwSignal<bool>,
295 #[builder(into)]
298 video: VideoConstraints,
299 #[builder(into)]
302 audio: AudioConstraints,
303}
304
305impl Default for UseUserMediaOptions {
306 fn default() -> Self {
307 Self {
308 enabled: false.into(),
309 video: true.into(),
310 audio: false.into(),
311 }
312 }
313}
314
315#[derive(Clone)]
317pub struct UseUserMediaReturn<StartFn, StopFn>
318where
319 StartFn: Fn() + Clone + Send + Sync,
320 StopFn: Fn() + Clone + Send + Sync,
321{
322 pub stream: OptionLocalSignal<Result<web_sys::MediaStream, JsValue>>,
327
328 pub start: StartFn,
330
331 pub stop: StopFn,
333
334 pub enabled: Signal<bool>,
337
338 pub set_enabled: WriteSignal<bool>,
340}
341
342#[derive(Clone, Debug)]
343pub enum ConstraintExactIdeal<T> {
344 Single(Option<T>),
345 ExactIdeal { exact: Option<T>, ideal: Option<T> },
346}
347
348impl<T> Default for ConstraintExactIdeal<T>
349where
350 T: Default,
351{
352 fn default() -> Self {
353 ConstraintExactIdeal::Single(Some(T::default()))
354 }
355}
356
357impl<T> ConstraintExactIdeal<T> {
358 pub fn exact(mut self, value: T) -> Self {
359 if let ConstraintExactIdeal::ExactIdeal { exact: e, .. } = &mut self {
360 *e = Some(value);
361 }
362
363 self
364 }
365
366 pub fn ideal(mut self, value: T) -> Self {
367 if let ConstraintExactIdeal::ExactIdeal { ideal: i, .. } = &mut self {
368 *i = Some(value);
369 }
370
371 self
372 }
373}
374
375impl<T> ConstraintExactIdeal<T>
376where
377 T: Into<JsValue> + Clone,
378{
379 pub fn to_jsvalue(&self) -> JsValue {
380 match self {
381 ConstraintExactIdeal::Single(value) => value.clone().unwrap().into(),
382 ConstraintExactIdeal::ExactIdeal { exact, ideal } => {
383 let obj = Object::new();
384
385 if let Some(value) = exact {
386 Reflect::set(&obj, &JsValue::from_str("exact"), &value.clone().into()).unwrap();
387 }
388 if let Some(value) = ideal {
389 Reflect::set(&obj, &JsValue::from_str("ideal"), &value.clone().into()).unwrap();
390 }
391
392 JsValue::from(obj)
393 }
394 }
395 }
396}
397
398impl From<&'static str> for ConstraintExactIdeal<&'static str> {
399 fn from(value: &'static str) -> Self {
400 ConstraintExactIdeal::Single(Some(value))
401 }
402}
403
404#[derive(Clone, Debug)]
405pub enum ConstraintBoolOrRange<T> {
406 Bool(bool),
407 Single(Option<T>),
408 Range {
409 min: Option<T>,
410 max: Option<T>,
411 exact: Option<T>,
412 ideal: Option<T>,
413 },
414}
415
416impl<T> ConstraintBoolOrRange<T>
417where
418 T: Into<JsValue> + Clone,
419{
420 pub fn to_jsvalue(&self) -> JsValue {
421 match self {
422 Self::Bool(value) => JsValue::from_bool(*value),
423 Self::Single(value) => value.clone().unwrap().into(),
424 Self::Range {
425 min,
426 max,
427 exact,
428 ideal,
429 } => {
430 let obj = Object::new();
431
432 if let Some(min_value) = min {
433 Reflect::set(&obj, &JsValue::from_str("min"), &min_value.clone().into())
434 .unwrap();
435 }
436 if let Some(max_value) = max {
437 Reflect::set(&obj, &JsValue::from_str("max"), &max_value.clone().into())
438 .unwrap();
439 }
440 if let Some(value) = exact {
441 Reflect::set(&obj, &JsValue::from_str("exact"), &value.clone().into()).unwrap();
442 }
443 if let Some(value) = ideal {
444 Reflect::set(&obj, &JsValue::from_str("ideal"), &value.clone().into()).unwrap();
445 }
446
447 JsValue::from(obj)
448 }
449 }
450 }
451}
452
453impl<T: Default> Default for ConstraintBoolOrRange<T> {
454 fn default() -> Self {
455 ConstraintBoolOrRange::Single(Some(T::default()))
456 }
457}
458
459impl<T> From<bool> for ConstraintBoolOrRange<T> {
460 fn from(value: bool) -> Self {
461 ConstraintBoolOrRange::Bool(value)
462 }
463}
464
465impl<T> From<ConstraintRange<T>> for ConstraintBoolOrRange<T> {
466 fn from(value: ConstraintRange<T>) -> Self {
467 match value {
468 ConstraintRange::Single(value) => ConstraintBoolOrRange::Single(value),
469 ConstraintRange::Range {
470 min,
471 max,
472 exact,
473 ideal,
474 } => ConstraintBoolOrRange::Range {
475 min,
476 max,
477 exact,
478 ideal,
479 },
480 }
481 }
482}
483
484impl From<f64> for ConstraintBoolOrRange<f64> {
485 fn from(value: f64) -> Self {
486 Self::Single(Some(value))
487 }
488}
489
490impl From<u32> for ConstraintBoolOrRange<u32> {
491 fn from(value: u32) -> Self {
492 Self::Single(Some(value))
493 }
494}
495
496#[derive(Clone, Debug)]
497pub enum ConstraintRange<T> {
498 Single(Option<T>),
499 Range {
500 min: Option<T>,
501 max: Option<T>,
502 exact: Option<T>,
503 ideal: Option<T>,
504 },
505}
506
507impl<T> Default for ConstraintRange<T>
508where
509 T: Default,
510{
511 fn default() -> Self {
512 ConstraintRange::Single(Some(T::default()))
513 }
514}
515
516pub trait ConstraintRangeBuilder<T> {
517 fn min(self, value: T) -> Self;
518 fn max(self, value: T) -> Self;
519 fn exact(self, value: T) -> Self;
520 fn ideal(self, value: T) -> Self;
521}
522
523impl<T> ConstraintRange<T>
524where
525 T: Clone + std::fmt::Debug,
526{
527 pub fn new(value: Option<T>) -> Self {
528 ConstraintRange::Single(value)
529 }
530}
531
532macro_rules! impl_constraint_range_builder {
533 ($ty:ty) => {
534 impl<T> ConstraintRangeBuilder<T> for $ty {
535 fn min(mut self, value: T) -> Self {
536 if let Self::Range { ref mut min, .. } = self {
537 *min = Some(value);
538 }
539 self
540 }
541
542 fn max(mut self, value: T) -> Self {
543 if let Self::Range { ref mut max, .. } = self {
544 *max = Some(value);
545 }
546 self
547 }
548
549 fn exact(mut self, value: T) -> Self {
550 if let Self::Range { exact, .. } = &mut self {
551 *exact = Some(value);
552 }
553
554 self
555 }
556
557 fn ideal(mut self, value: T) -> Self {
558 if let Self::Range { ideal, .. } = &mut self {
559 *ideal = Some(value);
560 }
561
562 self
563 }
564 }
565 };
566}
567
568impl_constraint_range_builder!(ConstraintRange<T>);
569impl_constraint_range_builder!(ConstraintBoolOrRange<T>);
570
571impl<T> ConstraintRange<T>
572where
573 T: Into<JsValue> + Clone,
574{
575 pub fn to_jsvalue(&self) -> JsValue {
576 match self {
577 ConstraintRange::Single(value) => value.clone().unwrap().into(),
578 ConstraintRange::Range {
579 min,
580 max,
581 exact,
582 ideal,
583 } => {
584 let obj = Object::new();
585
586 if let Some(min_value) = min {
587 Reflect::set(&obj, &JsValue::from_str("min"), &min_value.clone().into())
588 .unwrap();
589 }
590 if let Some(max_value) = max {
591 Reflect::set(&obj, &JsValue::from_str("max"), &max_value.clone().into())
592 .unwrap();
593 }
594 if let Some(value) = exact {
595 Reflect::set(&obj, &JsValue::from_str("exact"), &value.clone().into()).unwrap();
596 }
597 if let Some(value) = ideal {
598 Reflect::set(&obj, &JsValue::from_str("ideal"), &value.clone().into()).unwrap();
599 }
600
601 JsValue::from(obj)
602 }
603 }
604 }
605}
606
607impl From<f64> for ConstraintDouble {
608 fn from(value: f64) -> Self {
609 ConstraintRange::Single(Some(value))
610 }
611}
612
613impl From<u32> for ConstraintULong {
614 fn from(value: u32) -> Self {
615 ConstraintRange::Single(Some(value))
616 }
617}
618
619pub type ConstraintBool = ConstraintExactIdeal<bool>;
620
621impl From<bool> for ConstraintBool {
622 fn from(value: bool) -> Self {
623 ConstraintExactIdeal::Single(Some(value))
624 }
625}
626
627pub type ConstraintDouble = ConstraintRange<f64>;
628pub type ConstraintULong = ConstraintRange<u32>;
629
630#[derive(Clone, Copy, Debug)]
631pub enum FacingMode {
632 User,
633 Environment,
634 Left,
635 Right,
636}
637
638impl FacingMode {
639 pub fn as_str(self) -> &'static str {
640 match self {
641 FacingMode::User => "user",
642 FacingMode::Environment => "environment",
643 FacingMode::Left => "left",
644 FacingMode::Right => "right",
645 }
646 }
647}
648
649pub type ConstraintFacingMode = ConstraintExactIdeal<FacingMode>;
650
651impl From<FacingMode> for ConstraintFacingMode {
652 fn from(value: FacingMode) -> Self {
653 ConstraintFacingMode::Single(Some(value))
654 }
655}
656
657impl ConstraintFacingMode {
658 pub fn to_jsvalue(&self) -> JsValue {
659 match self {
660 ConstraintExactIdeal::Single(value) => JsValue::from_str((*value).unwrap().as_str()),
661 ConstraintExactIdeal::ExactIdeal { exact, ideal } => {
662 let obj = Object::new();
663
664 if let Some(value) = exact {
665 Reflect::set(
666 &obj,
667 &JsValue::from_str("exact"),
668 &JsValue::from_str(value.as_str()),
669 )
670 .unwrap();
671 }
672 if let Some(value) = ideal {
673 Reflect::set(
674 &obj,
675 &JsValue::from_str("ideal"),
676 &JsValue::from_str(value.as_str()),
677 )
678 .unwrap();
679 }
680
681 JsValue::from(obj)
682 }
683 }
684 }
685}
686
687#[derive(Clone, Debug)]
688pub enum AudioConstraints {
689 Bool(bool),
690 Constraints(Box<AudioTrackConstraints>),
691}
692
693impl From<bool> for AudioConstraints {
694 fn from(value: bool) -> Self {
695 AudioConstraints::Bool(value)
696 }
697}
698
699impl From<AudioTrackConstraints> for AudioConstraints {
700 fn from(value: AudioTrackConstraints) -> Self {
701 AudioConstraints::Constraints(Box::new(value))
702 }
703}
704
705#[derive(Clone, Debug)]
706pub enum VideoConstraints {
707 Bool(bool),
708 Constraints(Box<VideoTrackConstraints>),
709}
710
711impl From<bool> for VideoConstraints {
712 fn from(value: bool) -> Self {
713 VideoConstraints::Bool(value)
714 }
715}
716
717impl From<VideoTrackConstraints> for VideoConstraints {
718 fn from(value: VideoTrackConstraints) -> Self {
719 VideoConstraints::Constraints(Box::new(value))
720 }
721}
722
723pub trait IntoDeviceIds<M> {
724 fn into_device_ids(self) -> Vec<String>;
725}
726
727impl<T> IntoDeviceIds<String> for T
728where
729 T: Into<String>,
730{
731 fn into_device_ids(self) -> Vec<String> {
732 vec![self.into()]
733 }
734}
735
736pub struct VecMarker;
737
738impl<T, I> IntoDeviceIds<VecMarker> for T
739where
740 T: IntoIterator<Item = I>,
741 I: Into<String>,
742{
743 fn into_device_ids(self) -> Vec<String> {
744 self.into_iter().map(Into::into).collect()
745 }
746}
747
748#[derive(DefaultBuilder, Default, Clone, Debug)]
749#[allow(dead_code)]
750pub struct AudioTrackConstraints {
751 #[builder(skip)]
752 device_id: Vec<String>,
753
754 #[builder(into)]
755 auto_gain_control: Option<ConstraintBool>,
756 #[builder(into)]
757 channel_count: Option<ConstraintULong>,
758 #[builder(into)]
759 echo_cancellation: Option<ConstraintBool>,
760 #[builder(into)]
761 noise_suppression: Option<ConstraintBool>,
762}
763
764impl AudioTrackConstraints {
765 pub fn new() -> Self {
766 AudioTrackConstraints::default()
767 }
768
769 pub fn device_id<M>(mut self, value: impl IntoDeviceIds<M>) -> Self {
770 self.device_id = value.into_device_ids();
771 self
772 }
773}
774
775#[derive(DefaultBuilder, Default, Clone, Debug)]
776pub struct VideoTrackConstraints {
777 #[builder(skip)]
778 pub device_id: Vec<String>,
779
780 #[builder(into)]
781 pub facing_mode: Option<ConstraintFacingMode>,
782 #[builder(into)]
783 pub frame_rate: Option<ConstraintDouble>,
784 #[builder(into)]
785 pub height: Option<ConstraintULong>,
786 #[builder(into)]
787 pub width: Option<ConstraintULong>,
788 #[builder(into)]
789 pub viewport_offset_x: Option<ConstraintULong>,
790 #[builder(into)]
791 pub viewport_offset_y: Option<ConstraintULong>,
792 #[builder(into)]
793 pub viewport_height: Option<ConstraintULong>,
794 #[builder(into)]
795 pub viewport_width: Option<ConstraintULong>,
796 #[builder(into)]
797 pub zoom: Option<ConstraintBoolOrRange<f64>>,
798}
799
800impl VideoTrackConstraints {
801 pub fn new() -> Self {
802 VideoTrackConstraints::default()
803 }
804
805 pub fn device_id<M>(mut self, value: impl IntoDeviceIds<M>) -> Self {
806 self.device_id = value.into_device_ids();
807 self
808 }
809}