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