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