Skip to content

Commit

Permalink
Factor out media device query and selection (#112)
Browse files Browse the repository at this point in the history
* factor out model::media_access::MediaAccess

...and tidy up AttendantsCommponent::create()

* factor MediaDevicesList out of DeviceSelector component

* cargo fmt

* use AtomicBool instead of Cell<Bool>

* cargo fmt

* bug fix: was calling on_granted() when permission was denied.

---------

Co-authored-by: Dario A Lencina-Talarico <darioalessandrolencina@gmail.com>
  • Loading branch information
ronen and darioalessandro authored Aug 1, 2023
1 parent 443df43 commit 1ff8bf3
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 146 deletions.
93 changes: 50 additions & 43 deletions yew-ui/src/components/attendants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::rc::Rc;
use crate::constants::WEBTRANSPORT_HOST;
use crate::model::connection::{ConnectOptions, Connection};
use crate::model::decode::PeerDecodeManager;
use crate::model::media_devices::MediaDeviceAccess;
use crate::model::MediaPacketWrapper;
use crate::{components::host::Host, constants::ACTIX_WEBSOCKET};
use gloo_console::log;
Expand All @@ -11,14 +12,12 @@ use types::protos::media_packet::MediaPacket;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;

use super::icons::push_pin::PushPinIcon;
use web_sys::*;
use yew::prelude::*;
use yew::virtual_dom::VNode;
use yew::{html, Component, Context, Html};

use super::device_permissions::request_permissions;
use super::icons::push_pin::PushPinIcon;

#[derive(Debug)]
pub enum WsAction {
Connect(bool),
Expand Down Expand Up @@ -72,43 +71,72 @@ pub struct AttendantsComponentProps {
pub struct AttendantsComponent {
pub connection: Option<Connection>,
pub peer_decode_manager: PeerDecodeManager,
pub media_device_access: MediaDeviceAccess,
pub outbound_audio_buffer: [u8; 2000],
pub share_screen: bool,
pub webtransport_enabled: bool,
pub mic_enabled: bool,
pub video_enabled: bool,
pub error: Option<String>,
pub media_access_granted: bool,
}

impl Component for AttendantsComponent {
type Message = Msg;
type Properties = AttendantsComponentProps;
impl AttendantsComponent {
fn is_connected(&self) -> bool {
match &self.connection {
Some(connection) => connection.is_connected(),
None => false,
}
}

fn create(ctx: &Context<Self>) -> Self {
let webtransport_enabled = ctx.props().webtransport_enabled;
fn create_peer_decoder_manager(ctx: &Context<Self>) -> PeerDecodeManager {
let mut peer_decode_manager = PeerDecodeManager::new();
let link = ctx.link().clone();
peer_decode_manager.on_peer_added = Callback::from(move |email| {
link.send_message(Msg::OnPeerAdded(email));
});
let link = ctx.link().clone();
peer_decode_manager.on_first_frame = Callback::from(move |(email, media_type)| {
link.send_message(Msg::OnFirstFrame((email, media_type)));
});
peer_decode_manager.on_peer_added = {
let link = ctx.link().clone();
Callback::from(move |email| link.send_message(Msg::OnPeerAdded(email)))
};
peer_decode_manager.on_first_frame = {
let link = ctx.link().clone();
Callback::from(move |(email, media_type)| {
link.send_message(Msg::OnFirstFrame((email, media_type)))
})
};
peer_decode_manager.get_video_canvas_id = Callback::from(|email| email);
peer_decode_manager.get_screen_canvas_id =
Callback::from(|email| format!("screen-share-{}", &email));
peer_decode_manager
}

fn create_media_device_access(ctx: &Context<Self>) -> MediaDeviceAccess {
let mut media_device_access = MediaDeviceAccess::new();
media_device_access.on_granted = {
let link = ctx.link().clone();
Callback::from(move |_| link.send_message(WsAction::MediaPermissionsGranted))
};
media_device_access.on_denied = {
let link = ctx.link().clone();
Callback::from(move |_| {
link.send_message(WsAction::MediaPermissionsError("Error requesting permissions. Please make sure to allow access to both camera and microphone.".to_string()))
})
};
media_device_access
}
}

impl Component for AttendantsComponent {
type Message = Msg;
type Properties = AttendantsComponentProps;

fn create(ctx: &Context<Self>) -> Self {
Self {
connection: None,
peer_decode_manager,
peer_decode_manager: Self::create_peer_decoder_manager(ctx),
media_device_access: Self::create_media_device_access(ctx),
outbound_audio_buffer: [0; 2000],
share_screen: false,
mic_enabled: false,
video_enabled: false,
webtransport_enabled,
webtransport_enabled: ctx.props().webtransport_enabled,
error: None,
media_access_granted: false,
}
}

Expand Down Expand Up @@ -166,23 +194,11 @@ impl Component for AttendantsComponent {
true
}
WsAction::RequestMediaPermissions => {
let future = request_permissions();
let link = ctx.link().clone();
wasm_bindgen_futures::spawn_local(async move {
match future.await {
Ok(_) => {
link.send_message(WsAction::MediaPermissionsGranted);
}
Err(_) => {
link.send_message(WsAction::MediaPermissionsError("Error requesting permissions. Please make sure to allow access to both camera and microphone.".to_string()));
}
}
});
self.media_device_access.request();
false
}
WsAction::MediaPermissionsGranted => {
self.error = None;
self.media_access_granted = true;
ctx.link()
.send_message(WsAction::Connect(self.webtransport_enabled));
true
Expand Down Expand Up @@ -229,7 +245,7 @@ impl Component for AttendantsComponent {
fn view(&self, ctx: &Context<Self>) -> Html {
let email = ctx.props().email.clone();
let on_packet = ctx.link().callback(Msg::OnOutboundPacket);
let media_access_granted = self.media_access_granted;
let media_access_granted = self.media_device_access.is_granted();
let rows: Vec<VNode> = self
.peer_decode_manager
.sorted_keys()
Expand Down Expand Up @@ -320,15 +336,6 @@ impl Component for AttendantsComponent {
}
}

impl AttendantsComponent {
fn is_connected(&self) -> bool {
match &self.connection {
Some(connection) => connection.is_connected(),
None => false,
}
}
}

// props for the video component
#[derive(Properties, Debug, PartialEq)]
pub struct UserVideoProps {
Expand Down
23 changes: 0 additions & 23 deletions yew-ui/src/components/device_permissions.rs

This file was deleted.

122 changes: 43 additions & 79 deletions yew-ui/src/components/device_selector.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
use gloo_utils::window;
use js_sys::Array;
use js_sys::Promise;
use crate::model::media_devices::MediaDeviceList;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::EventTarget;
use web_sys::HtmlSelectElement;
use web_sys::MediaDeviceInfo;
use web_sys::MediaDeviceKind;
use yew::prelude::*;

pub struct DeviceSelector {
audio_devices: Vec<MediaDeviceInfo>,
video_devices: Vec<MediaDeviceInfo>,
video_selected: Option<String>,
audio_selected: Option<String>,
media_devices: MediaDeviceList,
}

pub enum Msg {
DevicesLoaded(Vec<MediaDeviceInfo>),
DevicesLoaded,
OnCameraSelect(String),
OnMicSelect(String),
LoadDevices(),
Expand All @@ -29,6 +20,21 @@ pub struct DeviceSelectorProps {
pub on_microphone_select: Callback<String>,
}

impl DeviceSelector {
fn create_media_device_list(ctx: &Context<DeviceSelector>) -> MediaDeviceList {
let mut media_devices = MediaDeviceList::new();
let link = ctx.link().clone();
let on_microphone_select = ctx.props().on_microphone_select.clone();
let on_camera_select = ctx.props().on_camera_select.clone();
media_devices.on_loaded = Callback::from(move |_| link.send_message(Msg::DevicesLoaded));
media_devices.audio_inputs.on_selected =
Callback::from(move |device_id| on_microphone_select.emit(device_id));
media_devices.video_inputs.on_selected =
Callback::from(move |device_id| on_camera_select.emit(device_id));
media_devices
}
}

impl Component for DeviceSelector {
type Message = Msg;
type Properties = DeviceSelectorProps;
Expand All @@ -39,10 +45,7 @@ impl Component for DeviceSelector {
link.send_message(Msg::LoadDevices());
});
Self {
audio_devices: Vec::new(),
video_devices: Vec::new(),
audio_selected: None,
video_selected: None,
media_devices: Self::create_media_device_list(ctx),
}
}

Expand All @@ -52,94 +55,55 @@ impl Component for DeviceSelector {
}
}

fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::LoadDevices() => {
let link = ctx.link().clone();
wasm_bindgen_futures::spawn_local(async move {
let navigator = window().navigator();
let media_devices = navigator.media_devices().unwrap();

let promise: Promise = media_devices
.enumerate_devices()
.expect("enumerate devices");
let future = JsFuture::from(promise);
let devices = future
.await
.expect("await devices")
.unchecked_into::<Array>();
let devices = devices.to_vec();
let devices = devices
.into_iter()
.map(|d| d.unchecked_into::<MediaDeviceInfo>())
.collect::<Vec<MediaDeviceInfo>>();
link.send_message(Msg::DevicesLoaded(devices));
});
self.media_devices.load();
false
}
Msg::DevicesLoaded(devices) => {
self.audio_devices = devices
.clone()
.into_iter()
.filter(|device| device.kind() == MediaDeviceKind::Audioinput)
.collect();
self.video_devices = devices
.into_iter()
.filter(|device| device.kind() == MediaDeviceKind::Videoinput)
.collect();
ctx.props()
.on_camera_select
.emit(self.video_devices[0].device_id());
ctx.props()
.on_microphone_select
.emit(self.audio_devices[0].device_id());
self.video_selected = Some(self.video_devices[0].device_id());
self.audio_selected = Some(self.audio_devices[0].device_id());
true
}
Msg::DevicesLoaded => true,
Msg::OnCameraSelect(camera) => {
ctx.props().on_camera_select.emit(camera.clone());
self.video_selected = Some(camera);
self.media_devices.video_inputs.select(&camera);
true
}
Msg::OnMicSelect(mic) => {
ctx.props().on_microphone_select.emit(mic.clone());
self.audio_selected = Some(mic);
self.media_devices.audio_inputs.select(&mic);
true
}
}
}

fn view(&self, ctx: &Context<Self>) -> Html {
let selected_mic = self.audio_selected.clone().unwrap_or_default();
let selected_camera = self.video_selected.clone().unwrap_or_default();
let mics = self.media_devices.audio_inputs.devices();
let cameras = self.media_devices.video_inputs.devices();
let selected_mic = self.media_devices.audio_inputs.selected();
let selected_camera = self.media_devices.video_inputs.selected();
fn selection(event: Event) -> String {
event
.target()
.expect("Event should have a target when dispatched")
.unchecked_into::<HtmlSelectElement>()
.value()
}

html! {
<div class={"device-selector-wrapper"}>
<label for={"audio-select"}>{ "Audio:" }</label>
<select id={"audio-select"} class={"device-selector"}
onchange={ctx.link().callback(|e: Event| {
let target: EventTarget = e
.target()
.expect("Event should have a target when dispatched");
let new_audio = target.unchecked_into::<HtmlSelectElement>().value();
Msg::OnMicSelect(new_audio)
})}>
{ for self.audio_devices.iter().map(|device| html! {
onchange={ctx.link().callback(|e: Event| Msg::OnMicSelect(selection(e)))}
>
{ for mics.iter().map(|device| html! {
<option value={device.device_id()} selected={selected_mic == device.device_id()}>
{ device.label() }
</option>
}) }
</select>
<br/>
<label for={"video-select"}>{ "Video:" }</label>
<select id={"video-select"} class={"device-selector"} onchange={ctx.link().callback(|e:Event| {
let target: EventTarget = e
.target()
.expect("Event should have a target when dispatched");
let new_audio = target.unchecked_into::<HtmlSelectElement>().value();
Msg::OnCameraSelect(new_audio)
})}>
{ for self.video_devices.iter().map(|device| html! {
<select id={"video-select"} class={"device-selector"}
onchange={ctx.link().callback(|e:Event| Msg::OnCameraSelect(selection(e))) }
>
{ for cameras.iter().map(|device| html! {
<option value={device.device_id()} selected={selected_camera == device.device_id()}>
{ device.label() }
</option>
Expand Down
1 change: 0 additions & 1 deletion yew-ui/src/components/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
pub mod attendants;
pub mod device_permissions;
pub mod device_selector;
pub mod host;
pub mod icons;
Expand Down
1 change: 1 addition & 0 deletions yew-ui/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![feature(future_join)]
#![feature(once_cell)]

mod components;
mod constants;
Expand Down
Loading

0 comments on commit 1ff8bf3

Please sign in to comment.