1use 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
63pub enum MediaStreamTrackKind {
65 Video,
67 Audio,
69 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 #[wasm_bindgen(extends = Object)]
89 #[derive(Debug, Clone, PartialEq, Eq)]
90 pub type VideoConstraints;
91}
92
93impl OptionsTrait for VideoConstraints {}
94
95impl VideoConstraints {
96 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 pub fn max_width(self, max_width: u32) -> Self {
112 self.set("mandatory.maxWidth", JsValue::from(max_width))
113 }
114
115 pub fn max_height(self, max_height: u32) -> Self {
121 self.set("mandatory.maxHeight", JsValue::from(max_height))
122 }
123
124 pub fn device_id(self, device_id: &str) -> Self {
130 self.set("deviceId", JsValue::from(device_id))
131 }
132
133 pub fn group_id(self, group_id: &str) -> Self {
139 self.set("groupId", JsValue::from(group_id))
140 }
141
142 pub fn aspect_ratio(self, aspect_ratio: f32) -> Self {
149 self.set("aspectRatio", JsValue::from(aspect_ratio))
150 }
151
152 pub fn facing_mode(self, facing_mode: &str) -> Self {
159 self.set("facingMode", JsValue::from(facing_mode))
160 }
161
162 pub fn frame_rate(self, frame_rate: f32) -> Self {
168 self.set("frameRate", JsValue::from(frame_rate))
169 }
170
171 pub fn width(self, width: u16) -> Self {
177 self.set("width", JsValue::from(width))
178 }
179
180 pub fn height(self, height: u16) -> Self {
186 self.set("height", JsValue::from(height))
187 }
188}
189
190pub 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
252pub 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 let app = Application::new()?;
311
312 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}