Skip to main content

leptos_use/
use_user_media.rs

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
7/// Reactive [`mediaDevices.getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) streaming.
8///
9/// ## Demo
10///
11/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_user_media)
12///
13/// ## Usage
14///
15/// ```
16/// # use leptos::prelude::*;
17/// # use leptos::logging::{log, error};
18/// # use leptos_use::{use_user_media, UseUserMediaReturn};
19/// #
20/// # #[component]
21/// # fn Demo() -> impl IntoView {
22/// let video_ref = NodeRef::<leptos::html::Video>::new();
23///
24/// let UseUserMediaReturn { stream, start, .. } = use_user_media();
25///
26/// start();
27///
28/// Effect::new(move |_|
29///     video_ref.get().map(|v| {
30///         match stream.get() {
31///             Some(Ok(s)) => v.set_src_object(Some(&s)),
32///             Some(Err(e)) => error!("Failed to get media stream: {:?}", e),
33///             None => log!("No stream yet"),
34///         }
35///     })
36/// );
37///
38/// view! { <video node_ref=video_ref controls=false autoplay=true muted=true></video> }
39/// # }
40/// ```
41///
42/// ## Server-Side Rendering
43///
44/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
45///
46/// On the server calls to `start` or any other way to enable the stream will be ignored
47/// and the stream will always be `None`.
48pub 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
53/// Version of [`use_user_media`] that takes a `UseUserMediaOptions`. See [`use_user_media`] for how to use.
54pub 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    // Without this the tracks keep running (and the camera/microphone indicator
105    // stays on) after the owner is disposed, with no handle left to stop them.
106    #[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                    // TODO : once web_sys implements this add it to the methods above
241                    _ = 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/// Options for [`use_user_media_with_options`].
292///
293/// Either or both constraints must be specified.
294/// If the browser cannot find all media tracks with the specified types that meet the constraints given,
295/// then the returned promise is rejected with `NotFoundError`
296#[derive(DefaultBuilder, Clone, Debug)]
297pub struct UseUserMediaOptions {
298    /// If the stream is enabled. Defaults to `false`.
299    enabled: MaybeRwSignal<bool>,
300    /// Constraint parameter describing video media type requested
301    /// The default value is `true`.
302    #[builder(into)]
303    video: VideoConstraints,
304    /// Constraint parameter describing audio media type requested
305    /// The default value is `false`.
306    #[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/// Return type of [`use_user_media`].
321#[derive(Clone)]
322pub struct UseUserMediaReturn<StartFn, StopFn>
323where
324    StartFn: Fn() + Clone + Send + Sync,
325    StopFn: Fn() + Clone + Send + Sync,
326{
327    /// The current [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) if it exists.
328    /// Initially this is `None` until `start` resolved successfully.
329    /// In case the stream couldn't be started, for example because the user didn't grant permission,
330    /// this has the value `Some(Err(...))`.
331    pub stream: OptionLocalSignal<Result<web_sys::MediaStream, JsValue>>,
332
333    /// Starts the screen streaming. Triggers the ask for permission if not already granted.
334    pub start: StartFn,
335
336    /// Stops the screen streaming
337    pub stop: StopFn,
338
339    /// A value of `true` indicates that the returned [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream)
340    /// has resolved successfully and thus the stream is enabled.
341    pub enabled: Signal<bool>,
342
343    /// A value of `true` is the same as calling `start()` whereas `false` is the same as calling `stop()`.
344    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}