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