Skip to main content

workflow_nw/
media.rs

1//!
2//! Media control helpers
3//!
4//! # Synopsis
5//! ```
6//! use workflow_log::log_info;
7//! use workflow_nw::prelude::*;
8//! use workflow_nw::result::Result;
9//! use nw_sys::prelude::OptionsTrait;
10//!
11//! fn choose_desktop_media()->Result<()>{
12//!     // create Application instance
13//!     let app = Application::new()?;
14//!
15//!     // choose desktop media
16//!     app.choose_desktop_media(
17//!         nw_sys::screen::MediaSources::ScreenAndWindow,
18//!         move |stream_id: Option<String>|->Result<()>{
19//!             if let Some(stream_id) = stream_id{
20//!                 render_media(stream_id)?;
21//!             }
22//!             Ok(())
23//!         }
24//!     )?;
25//!     
26//!     Ok(())
27//! }
28//!
29//! fn render_media(stream_id:String)->Result<()>{
30//!     log_info!("stream_id: {:?}", stream_id);
31//!      
32//!     let video_element_id = "video_el".to_string();
33//!     let video_constraints = VideoConstraints::new()
34//!         .source_id(&stream_id)
35//!         .max_height(1000);
36//!
37//!     workflow_nw::media::render_media(
38//!         video_element_id,
39//!         video_constraints,
40//!         None,
41//!         move |stream|->Result<()>{
42//!             workflow_nw::application::app().unwrap().set_media_stream(stream)?;
43//!             Ok(())
44//!         }
45//!     )?;
46//!      
47//!     Ok(())
48//! }
49//! ```
50
51use crate::application::app;
52use crate::result::Result;
53use js_sys::Object;
54use nw_sys::prelude::OptionsTrait;
55use std::fmt;
56use std::sync::Arc;
57use wasm_bindgen::{JsCast, prelude::*};
58use web_sys::MediaStream;
59use workflow_dom::utils::{document, window};
60use workflow_log::{log_debug, log_error};
61use workflow_wasm::prelude::*;
62
63/// MediaStream track kind
64pub enum MediaStreamTrackKind {
65    /// Video tracks only.
66    Video,
67    /// Audio tracks only.
68    Audio,
69    /// Both audio and video tracks.
70    All,
71}
72
73impl fmt::Display for MediaStreamTrackKind {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        match self {
76            Self::Video => write!(f, "Video"),
77            Self::Audio => write!(f, "Audio"),
78            Self::All => write!(f, "All"),
79        }
80    }
81}
82
83#[wasm_bindgen]
84extern "C" {
85    /// Video Constraints
86    ///
87    ///
88    #[wasm_bindgen(extends = Object)]
89    #[derive(Debug, Clone, PartialEq, Eq)]
90    pub type VideoConstraints;
91}
92
93impl OptionsTrait for VideoConstraints {}
94
95impl VideoConstraints {
96    /// Source Id
97    ///
98    ///
99    ///
100    /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSupportedConstraints)
101    pub fn source_id(self, source_id: &str) -> Self {
102        self.set("mandatory.chromeMediaSource", JsValue::from("desktop"))
103            .set("mandatory.chromeMediaSourceId", JsValue::from(source_id))
104    }
105
106    /// Max Width
107    ///
108    ///
109    ///
110    /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSupportedConstraints)
111    pub fn max_width(self, max_width: u32) -> Self {
112        self.set("mandatory.maxWidth", JsValue::from(max_width))
113    }
114
115    /// Max Height
116    ///
117    ///
118    ///
119    /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSupportedConstraints)
120    pub fn max_height(self, max_height: u32) -> Self {
121        self.set("mandatory.maxHeight", JsValue::from(max_height))
122    }
123
124    /// Device Id
125    ///
126    /// a device ID or an array of device IDs which are acceptable and/or required.
127    ///
128    /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSupportedConstraints)
129    pub fn device_id(self, device_id: &str) -> Self {
130        self.set("deviceId", JsValue::from(device_id))
131    }
132
133    /// Group Id
134    ///
135    /// a group ID or an array of group IDs which are acceptable and/or required.
136    ///
137    /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSupportedConstraints)
138    pub fn group_id(self, group_id: &str) -> Self {
139        self.set("groupId", JsValue::from(group_id))
140    }
141
142    /// Aspect ratio of video
143    ///
144    /// specifying the video aspect ratio or range of aspect ratios
145    /// which are acceptable and/or required.
146    ///
147    /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSupportedConstraints)
148    pub fn aspect_ratio(self, aspect_ratio: f32) -> Self {
149        self.set("aspectRatio", JsValue::from(aspect_ratio))
150    }
151
152    /// Facing mode
153    ///
154    /// Object specifying a facing or an array of facings which are acceptable
155    /// and/or required.
156    ///
157    /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSupportedConstraints)
158    pub fn facing_mode(self, facing_mode: &str) -> Self {
159        self.set("facingMode", JsValue::from(facing_mode))
160    }
161
162    /// Frame rate
163    ///
164    /// frame rate or range of frame rates which are acceptable and/or required.
165    ///
166    /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSupportedConstraints)
167    pub fn frame_rate(self, frame_rate: f32) -> Self {
168        self.set("frameRate", JsValue::from(frame_rate))
169    }
170
171    /// Width of video
172    ///
173    /// video width or range of widths which are acceptable and/or required.
174    ///
175    /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSupportedConstraints)
176    pub fn width(self, width: u16) -> Self {
177        self.set("width", JsValue::from(width))
178    }
179
180    ///Height of video
181    ///
182    /// video height or range of heights which are acceptable and/or required.
183    ///
184    /// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSupportedConstraints)
185    pub fn height(self, height: u16) -> Self {
186        self.set("height", JsValue::from(height))
187    }
188}
189
190/// Get user media
191///
192/// [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)
193///
194pub fn get_user_media(
195    video_constraints: VideoConstraints,
196    audio_constraints: Option<JsValue>,
197    callback: Arc<dyn Fn(Option<MediaStream>)>,
198) -> Result<()> {
199    let app = match app() {
200        Some(app) => app,
201        None => return Err("app is not initialized".to_string().into()),
202    };
203
204    let navigator = window().navigator();
205    let media_devices = navigator.media_devices()?;
206
207    log_debug!("navigator: {:?}", navigator);
208    log_debug!("media_devices: {:?}", media_devices);
209    log_debug!("video_constraints: {:?}", video_constraints);
210
211    let audio_constraints = audio_constraints.unwrap_or_else(|| JsValue::from(false));
212
213    let constraints = web_sys::MediaStreamConstraints::new();
214    constraints.set_audio(&audio_constraints);
215    constraints.set_video(&JsValue::from(&video_constraints));
216
217    log_debug!("constraints: {:?}", constraints);
218
219    let promise = media_devices.get_user_media_with_constraints(&constraints)?;
220
221    let mut callback_ = Callback::default();
222    let app_clone = app.clone();
223    let callback_id = callback_.get_id();
224    callback_.set_closure(move |value: JsValue| {
225        let _ = app_clone.callbacks.remove(&callback_id);
226        match value.dyn_into::<MediaStream>() {
227            Ok(media_stream) => {
228                callback(Some(media_stream));
229            }
230            _ => {
231                callback(None);
232            }
233        }
234    });
235
236    let binding = match callback_.closure() {
237        Ok(b) => b,
238        Err(err) => {
239            return Err(format!(
240                "media::get_user_media(), callback_.closure() failed, error: {err:?}",
241            )
242            .into());
243        }
244    };
245
246    let _ = promise.then(binding.as_ref());
247
248    app.callbacks.retain(callback_)?;
249    Ok(())
250}
251
252/// render media to a video element
253pub fn render_media<F>(
254    video_element_id: String,
255    video_constraints: VideoConstraints,
256    audio_constraints: Option<JsValue>,
257    callback: F,
258) -> Result<()>
259where
260    F: 'static + Fn(Option<MediaStream>) -> Result<()>,
261{
262    get_user_media(
263        video_constraints,
264        audio_constraints,
265        Arc::new(move |value| {
266            let media_stream = if let Some(media_stream) = value {
267                let el = document().get_element_by_id(&video_element_id).unwrap();
268                match el.dyn_into::<web_sys::HtmlVideoElement>() {
269                    Ok(el) => {
270                        el.set_src_object(Some(&media_stream));
271                    }
272                    Err(err) => {
273                        log_error!(
274                            "Unable to cast element to HtmlVideoElement: element = {:?}",
275                            err
276                        );
277                    }
278                }
279
280                Some(media_stream)
281            } else {
282                None
283            };
284
285            callback(media_stream)
286                .map_err(|err| {
287                    log_error!("render_media callback error: {:?}", err);
288                })
289                .ok();
290        }),
291    )?;
292    Ok(())
293}
294
295#[cfg(all(test, target_arch = "wasm32"))]
296mod test {
297    use crate as workflow_nw;
298    use workflow_nw::result::Result;
299    #[test]
300    fn nw_media_test() -> Result<()> {
301        use nw_sys::prelude::OptionsTrait;
302        use workflow_log::log_info;
303        use workflow_nw::prelude::*;
304        use workflow_nw::result::Result;
305
306        choose_desktop_media().unwrap();
307
308        fn choose_desktop_media() -> Result<()> {
309            // create Application instance
310            let app = Application::new()?;
311
312            // choose desktop media
313            app.choose_desktop_media(
314                nw_sys::screen::MediaSources::ScreenAndWindow,
315                move |stream_id: Option<String>| -> Result<()> {
316                    if let Some(stream_id) = stream_id {
317                        render_media(stream_id)?;
318                    }
319                    Ok(())
320                },
321            )?;
322            Ok(())
323        }
324
325        fn render_media(stream_id: String) -> Result<()> {
326            log_info!("stream_id: {:?}", stream_id);
327
328            let video_element_id = "video_el".to_string();
329            let video_constraints = VideoConstraints::new()
330                .source_id(&stream_id)
331                .max_height(1000);
332
333            workflow_nw::media::render_media(
334                video_element_id,
335                video_constraints,
336                None,
337                move |stream| -> Result<()> {
338                    workflow_nw::application::app()
339                        .unwrap()
340                        .set_media_stream(stream)?;
341                    Ok(())
342                },
343            )?;
344
345            Ok(())
346        }
347        Ok(())
348    }
349}