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::{prelude::*, JsCast};
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,
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 #[wasm_bindgen(extends = Object)]
86 #[derive(Debug, Clone, PartialEq, Eq)]
87 pub type VideoConstraints;
88}
89
90impl OptionsTrait for VideoConstraints {}
91
92impl VideoConstraints {
93 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 pub fn max_width(self, max_width: u32) -> Self {
109 self.set("mandatory.maxWidth", JsValue::from(max_width))
110 }
111
112 pub fn max_height(self, max_height: u32) -> Self {
118 self.set("mandatory.maxHeight", JsValue::from(max_height))
119 }
120
121 pub fn device_id(self, device_id: &str) -> Self {
127 self.set("deviceId", JsValue::from(device_id))
128 }
129
130 pub fn group_id(self, group_id: &str) -> Self {
136 self.set("groupId", JsValue::from(group_id))
137 }
138
139 pub fn aspect_ratio(self, aspect_ratio: f32) -> Self {
146 self.set("aspectRatio", JsValue::from(aspect_ratio))
147 }
148
149 pub fn facing_mode(self, facing_mode: &str) -> Self {
156 self.set("facingMode", JsValue::from(facing_mode))
157 }
158
159 pub fn frame_rate(self, frame_rate: f32) -> Self {
165 self.set("frameRate", JsValue::from(frame_rate))
166 }
167
168 pub fn width(self, width: u16) -> Self {
174 self.set("width", JsValue::from(width))
175 }
176
177 pub fn height(self, height: u16) -> Self {
183 self.set("height", JsValue::from(height))
184 }
185}
186
187pub 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
246pub 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 let app = Application::new()?;
305
306 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}