Skip to content

Commit a19c65a

Browse files
Show max 20 canvases (#158)
* Max 20 canvases on screen: The PeerList The commit introduces struct component PeerList which is to resposible for active Peer management. At current stage, the PeerList should expand and collapse, claiming dedicated screen chunk, accept (hardcoded at the moment) list of Peers and enable basic filterming function. * Max 20 canvases on screen: extract Canvas generator In preparation to limit Canvases, the commit extracts a function responsible for thier preparation given vector of peer indetifiers. No changes in canvas preparation logic were introduced. * Max 20 canvases on screen: Setup canvas limit The canvas number is limited up to 20. The limit criteria is simple alphabetical order. All peers can be seen in the PeerList. * Max 20 canvases on screen: PeerList refinement The commit turns raw-text PeerList into scrollable list of PeerListItems, a struct component that might be further developed with functional features. * Add peer heartbeat monitor (#157) The commit adds peer heartbeat monitoring functionality that registers each heartbeat received and removes peers that stopped missed all heartbeat opportunities in current monitoring period. The heartbeat-based peer removal will take up to 2x of monitoring period duration to detect a lost peer. * Update README.md (#159) * updating base images (#160) * updating base images * update base image * cache build * fmt * make ci green again * pass linter --------- Co-authored-by: Dario A Lencina-Talarico <[email protected]>
1 parent c12d61d commit a19c65a

File tree

9 files changed

+424
-149
lines changed

9 files changed

+424
-149
lines changed

yew-ui/src/components/attendants.rs

+88-147
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
use super::icons::push_pin::PushPinIcon;
2-
use crate::constants::{USERS_ALLOWED_TO_STREAM, WEBTRANSPORT_HOST};
1+
use crate::components::{canvas_generator, peer_list::PeerList};
2+
use crate::constants::{CANVAS_LIMIT, USERS_ALLOWED_TO_STREAM, WEBTRANSPORT_HOST};
33
use crate::{components::host::Host, constants::ACTIX_WEBSOCKET};
44
use log::{error, warn};
5-
use std::rc::Rc;
65
use types::protos::media_packet::media_packet::MediaType;
76
use videocall_client::{MediaDeviceAccess, VideoCallClient, VideoCallClientOptions};
8-
use wasm_bindgen::JsCast;
97
use wasm_bindgen::JsValue;
108
use web_sys::*;
119
use yew::prelude::*;
12-
use yew::virtual_dom::VNode;
1310
use yew::{html, Component, Context, Html};
1411

1512
#[derive(Debug)]
@@ -31,11 +28,16 @@ pub enum MeetingAction {
3128
ToggleVideoOnOff,
3229
}
3330

31+
pub enum UserScreenAction {
32+
TogglePeerList,
33+
}
34+
3435
pub enum Msg {
3536
WsAction(WsAction),
3637
MeetingAction(MeetingAction),
3738
OnPeerAdded(String),
3839
OnFirstFrame((String, MediaType)),
40+
UserScreenAction(UserScreenAction),
3941
}
4042

4143
impl From<WsAction> for Msg {
@@ -44,6 +46,12 @@ impl From<WsAction> for Msg {
4446
}
4547
}
4648

49+
impl From<UserScreenAction> for Msg {
50+
fn from(action: UserScreenAction) -> Self {
51+
Msg::UserScreenAction(action)
52+
}
53+
}
54+
4755
impl From<MeetingAction> for Msg {
4856
fn from(action: MeetingAction) -> Self {
4957
Msg::MeetingAction(action)
@@ -69,6 +77,7 @@ pub struct AttendantsComponent {
6977
pub share_screen: bool,
7078
pub mic_enabled: bool,
7179
pub video_enabled: bool,
80+
pub peer_list_open: bool,
7281
pub error: Option<String>,
7382
}
7483

@@ -133,6 +142,7 @@ impl Component for AttendantsComponent {
133142
share_screen: false,
134143
mic_enabled: false,
135144
video_enabled: false,
145+
peer_list_open: false,
136146
error: None,
137147
}
138148
}
@@ -197,162 +207,93 @@ impl Component for AttendantsComponent {
197207
}
198208
true
199209
}
210+
Msg::UserScreenAction(action) => {
211+
match action {
212+
UserScreenAction::TogglePeerList => {
213+
self.peer_list_open = !self.peer_list_open;
214+
}
215+
}
216+
true
217+
}
200218
}
201219
}
202220

203221
fn view(&self, ctx: &Context<Self>) -> Html {
204222
let email = ctx.props().email.clone();
205223
let media_access_granted = self.media_device_access.is_granted();
206-
let rows: Vec<VNode> = self
207-
.client
208-
.sorted_peer_keys()
209-
.iter()
210-
.map(|key| {
211-
if !USERS_ALLOWED_TO_STREAM.is_empty()
212-
&& !USERS_ALLOWED_TO_STREAM.iter().any(|host| host == key)
213-
{
214-
return html! {};
215-
}
216-
let screen_share_css = if self.client.is_awaiting_peer_screen_frame(key) {
217-
"grid-item hidden"
218-
} else {
219-
"grid-item"
220-
};
221-
let screen_share_div_id = Rc::new(format!("screen-share-{}-div", &key));
222-
let peer_video_div_id = Rc::new(format!("peer-video-{}-div", &key));
223-
html! {
224-
<>
225-
<div class={screen_share_css} id={(*screen_share_div_id).clone()}>
226-
// Canvas for Screen share.
227-
<div class="canvas-container">
228-
<canvas id={format!("screen-share-{}", &key)}></canvas>
229-
<h4 class="floating-name">{format!("{}-screen", &key)}</h4>
230-
<button onclick={Callback::from(move |_| {
231-
toggle_pinned_div(&(*screen_share_div_id).clone());
232-
})} class="pin-icon">
233-
<PushPinIcon/>
234-
</button>
235-
</div>
236-
</div>
237-
<div class="grid-item" id={(*peer_video_div_id).clone()}>
238-
// One canvas for the User Video
239-
<div class="canvas-container">
240-
<UserVideo id={key.clone()}></UserVideo>
241-
<h4 class="floating-name">{key.clone()}</h4>
242-
<button onclick={
243-
Callback::from(move |_| {
244-
toggle_pinned_div(&(*peer_video_div_id).clone());
245-
})} class="pin-icon">
246-
<PushPinIcon/>
247-
</button>
248-
</div>
249-
</div>
250-
</>
251-
}
252-
})
253-
.collect();
224+
225+
let toggle_peer_list = ctx.link().callback(|_| UserScreenAction::TogglePeerList);
226+
227+
let peers = self.client.sorted_peer_keys();
228+
let rows = canvas_generator::generate(
229+
&self.client,
230+
peers.iter().take(CANVAS_LIMIT).cloned().collect(),
231+
);
232+
254233
html! {
255-
<div class="grid-container">
256-
{ self.error.as_ref().map(|error| html! { <p>{ error }</p> }) }
257-
{ rows }
258-
{
259-
if USERS_ALLOWED_TO_STREAM.iter().any(|host| host == &email) || USERS_ALLOWED_TO_STREAM.is_empty() {
260-
html! {
261-
<nav class="host">
262-
<div class="controls">
263-
<button
264-
class="bg-yew-blue p-2 rounded-md text-white"
265-
onclick={ctx.link().callback(|_| MeetingAction::ToggleScreenShare)}>
266-
{ if self.share_screen { "Stop Screen Share"} else { "Share Screen"} }
267-
</button>
268-
<button
269-
class="bg-yew-blue p-2 rounded-md text-white"
270-
onclick={ctx.link().callback(|_| MeetingAction::ToggleVideoOnOff)}>
271-
{ if !self.video_enabled { "Start Video"} else { "Stop Video"} }
272-
</button>
273-
<button
274-
class="bg-yew-blue p-2 rounded-md text-white"
275-
onclick={ctx.link().callback(|_| MeetingAction::ToggleMicMute)}>
276-
{ if !self.mic_enabled { "Unmute"} else { "Mute"} }
234+
<div id="main-container">
235+
<div id="grid-container" style={if self.peer_list_open {"width: 80%;"} else {"width: 100%;"}}>
236+
{ self.error.as_ref().map(|error| html! { <p>{ error }</p> }) }
237+
{ rows }
238+
{
239+
if USERS_ALLOWED_TO_STREAM.iter().any(|host| host == &email) || USERS_ALLOWED_TO_STREAM.is_empty() {
240+
html! {
241+
<nav class="host">
242+
<div class="controls">
243+
<button
244+
class="bg-yew-blue p-2 rounded-md text-white"
245+
onclick={ctx.link().callback(|_| MeetingAction::ToggleScreenShare)}>
246+
{ if self.share_screen { "Stop Screen Share"} else { "Share Screen"} }
277247
</button>
278-
</div>
279-
{
280-
if media_access_granted {
281-
html! {<Host client={self.client.clone()} share_screen={self.share_screen} mic_enabled={self.mic_enabled} video_enabled={self.video_enabled} />}
282-
} else {
283-
html! {<></>}
248+
<button
249+
class="bg-yew-blue p-2 rounded-md text-white"
250+
onclick={ctx.link().callback(|_| MeetingAction::ToggleVideoOnOff)}>
251+
{ if !self.video_enabled { "Start Video"} else { "Stop Video"} }
252+
</button>
253+
<button
254+
class="bg-yew-blue p-2 rounded-md text-white"
255+
onclick={ctx.link().callback(|_| MeetingAction::ToggleMicMute)}>
256+
{ if !self.mic_enabled { "Unmute"} else { "Mute"} }
257+
</button>
258+
<button
259+
class="bg-yew-blue p-2 rounded-md text-white"
260+
onclick={toggle_peer_list.clone()}>
261+
{ if !self.peer_list_open { "Open Peers"} else { "Close Peers"} }
262+
</button>
263+
</div>
264+
{
265+
if media_access_granted {
266+
html! {<Host client={self.client.clone()} share_screen={self.share_screen} mic_enabled={self.mic_enabled} video_enabled={self.video_enabled} />}
267+
} else {
268+
html! {<></>}
269+
}
284270
}
285-
}
286-
<h4 class="floating-name">{email}</h4>
271+
<h4 class="floating-name">{email}</h4>
287272

288-
{if !self.client.is_connected() {
289-
html! {<h4>{"Connecting"}</h4>}
290-
} else {
291-
html! {<h4>{"Connected"}</h4>}
292-
}}
273+
{if !self.client.is_connected() {
274+
html! {<h4>{"Connecting"}</h4>}
275+
} else {
276+
html! {<h4>{"Connected"}</h4>}
277+
}}
293278

294-
{if ctx.props().e2ee_enabled {
295-
html! {<h4>{"End to End Encryption Enabled"}</h4>}
296-
} else {
297-
html! {<h4>{"End to End Encryption Disabled"}</h4>}
298-
}}
299-
</nav>
279+
{if ctx.props().e2ee_enabled {
280+
html! {<h4>{"End to End Encryption Enabled"}</h4>}
281+
} else {
282+
html! {<h4>{"End to End Encryption Disabled"}</h4>}
283+
}}
284+
</nav>
285+
}
286+
} else {
287+
error!("User not allowed to stream");
288+
error!("allowed users {}", USERS_ALLOWED_TO_STREAM.join(", "));
289+
html! {}
300290
}
301-
} else {
302-
error!("User not allowed to stream");
303-
error!("allowed users {}", USERS_ALLOWED_TO_STREAM.join(", "));
304-
html! {}
305291
}
306-
}
292+
</div>
293+
<div id="peer-list-container" class={if self.peer_list_open {"visible"} else {""}}>
294+
<PeerList peers={peers} onclose={toggle_peer_list} />
295+
</div>
307296
</div>
308297
}
309298
}
310299
}
311-
312-
// props for the video component
313-
#[derive(Properties, Debug, PartialEq)]
314-
pub struct UserVideoProps {
315-
pub id: String,
316-
}
317-
318-
// user video functional component
319-
#[function_component(UserVideo)]
320-
fn user_video(props: &UserVideoProps) -> Html {
321-
// create use_effect hook that gets called only once and sets a thumbnail
322-
// for the user video
323-
let video_ref = use_state(NodeRef::default);
324-
let video_ref_clone = video_ref.clone();
325-
use_effect_with_deps(
326-
move |_| {
327-
// Set thumbnail for the video
328-
let video = (*video_ref_clone).cast::<HtmlCanvasElement>().unwrap();
329-
let ctx = video
330-
.get_context("2d")
331-
.unwrap()
332-
.unwrap()
333-
.unchecked_into::<CanvasRenderingContext2d>();
334-
ctx.clear_rect(0.0, 0.0, video.width() as f64, video.height() as f64);
335-
|| ()
336-
},
337-
vec![props.id.clone()],
338-
);
339-
340-
html! {
341-
<canvas ref={(*video_ref).clone()} id={props.id.clone()}></canvas>
342-
}
343-
}
344-
345-
fn toggle_pinned_div(div_id: &str) {
346-
if let Some(div) = window()
347-
.and_then(|w| w.document())
348-
.and_then(|doc| doc.get_element_by_id(div_id))
349-
{
350-
// if the div does not have the grid-item-pinned css class, add it to it
351-
if !div.class_list().contains("grid-item-pinned") {
352-
div.class_list().add_1("grid-item-pinned").unwrap();
353-
} else {
354-
// else remove it
355-
div.class_list().remove_1("grid-item-pinned").unwrap();
356-
}
357-
}
358-
}

0 commit comments

Comments
 (0)