Skip to content

Commit

Permalink
Show max 20 canvases (#158)
Browse files Browse the repository at this point in the history
* 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 <darioalessandrolencina@gmail.com>
  • Loading branch information
mkamonMdt and darioalessandro authored Jul 4, 2024
1 parent c12d61d commit a19c65a
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 149 deletions.
235 changes: 88 additions & 147 deletions yew-ui/src/components/attendants.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
use super::icons::push_pin::PushPinIcon;
use crate::constants::{USERS_ALLOWED_TO_STREAM, WEBTRANSPORT_HOST};
use crate::components::{canvas_generator, peer_list::PeerList};
use crate::constants::{CANVAS_LIMIT, USERS_ALLOWED_TO_STREAM, WEBTRANSPORT_HOST};
use crate::{components::host::Host, constants::ACTIX_WEBSOCKET};
use log::{error, warn};
use std::rc::Rc;
use types::protos::media_packet::media_packet::MediaType;
use videocall_client::{MediaDeviceAccess, VideoCallClient, VideoCallClientOptions};
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use web_sys::*;
use yew::prelude::*;
use yew::virtual_dom::VNode;
use yew::{html, Component, Context, Html};

#[derive(Debug)]
Expand All @@ -31,11 +28,16 @@ pub enum MeetingAction {
ToggleVideoOnOff,
}

pub enum UserScreenAction {
TogglePeerList,
}

pub enum Msg {
WsAction(WsAction),
MeetingAction(MeetingAction),
OnPeerAdded(String),
OnFirstFrame((String, MediaType)),
UserScreenAction(UserScreenAction),
}

impl From<WsAction> for Msg {
Expand All @@ -44,6 +46,12 @@ impl From<WsAction> for Msg {
}
}

impl From<UserScreenAction> for Msg {
fn from(action: UserScreenAction) -> Self {
Msg::UserScreenAction(action)
}
}

impl From<MeetingAction> for Msg {
fn from(action: MeetingAction) -> Self {
Msg::MeetingAction(action)
Expand All @@ -69,6 +77,7 @@ pub struct AttendantsComponent {
pub share_screen: bool,
pub mic_enabled: bool,
pub video_enabled: bool,
pub peer_list_open: bool,
pub error: Option<String>,
}

Expand Down Expand Up @@ -133,6 +142,7 @@ impl Component for AttendantsComponent {
share_screen: false,
mic_enabled: false,
video_enabled: false,
peer_list_open: false,
error: None,
}
}
Expand Down Expand Up @@ -197,162 +207,93 @@ impl Component for AttendantsComponent {
}
true
}
Msg::UserScreenAction(action) => {
match action {
UserScreenAction::TogglePeerList => {
self.peer_list_open = !self.peer_list_open;
}
}
true
}
}
}

fn view(&self, ctx: &Context<Self>) -> Html {
let email = ctx.props().email.clone();
let media_access_granted = self.media_device_access.is_granted();
let rows: Vec<VNode> = self
.client
.sorted_peer_keys()
.iter()
.map(|key| {
if !USERS_ALLOWED_TO_STREAM.is_empty()
&& !USERS_ALLOWED_TO_STREAM.iter().any(|host| host == key)
{
return html! {};
}
let screen_share_css = if self.client.is_awaiting_peer_screen_frame(key) {
"grid-item hidden"
} else {
"grid-item"
};
let screen_share_div_id = Rc::new(format!("screen-share-{}-div", &key));
let peer_video_div_id = Rc::new(format!("peer-video-{}-div", &key));
html! {
<>
<div class={screen_share_css} id={(*screen_share_div_id).clone()}>
// Canvas for Screen share.
<div class="canvas-container">
<canvas id={format!("screen-share-{}", &key)}></canvas>
<h4 class="floating-name">{format!("{}-screen", &key)}</h4>
<button onclick={Callback::from(move |_| {
toggle_pinned_div(&(*screen_share_div_id).clone());
})} class="pin-icon">
<PushPinIcon/>
</button>
</div>
</div>
<div class="grid-item" id={(*peer_video_div_id).clone()}>
// One canvas for the User Video
<div class="canvas-container">
<UserVideo id={key.clone()}></UserVideo>
<h4 class="floating-name">{key.clone()}</h4>
<button onclick={
Callback::from(move |_| {
toggle_pinned_div(&(*peer_video_div_id).clone());
})} class="pin-icon">
<PushPinIcon/>
</button>
</div>
</div>
</>
}
})
.collect();

let toggle_peer_list = ctx.link().callback(|_| UserScreenAction::TogglePeerList);

let peers = self.client.sorted_peer_keys();
let rows = canvas_generator::generate(
&self.client,
peers.iter().take(CANVAS_LIMIT).cloned().collect(),
);

html! {
<div class="grid-container">
{ self.error.as_ref().map(|error| html! { <p>{ error }</p> }) }
{ rows }
{
if USERS_ALLOWED_TO_STREAM.iter().any(|host| host == &email) || USERS_ALLOWED_TO_STREAM.is_empty() {
html! {
<nav class="host">
<div class="controls">
<button
class="bg-yew-blue p-2 rounded-md text-white"
onclick={ctx.link().callback(|_| MeetingAction::ToggleScreenShare)}>
{ if self.share_screen { "Stop Screen Share"} else { "Share Screen"} }
</button>
<button
class="bg-yew-blue p-2 rounded-md text-white"
onclick={ctx.link().callback(|_| MeetingAction::ToggleVideoOnOff)}>
{ if !self.video_enabled { "Start Video"} else { "Stop Video"} }
</button>
<button
class="bg-yew-blue p-2 rounded-md text-white"
onclick={ctx.link().callback(|_| MeetingAction::ToggleMicMute)}>
{ if !self.mic_enabled { "Unmute"} else { "Mute"} }
<div id="main-container">
<div id="grid-container" style={if self.peer_list_open {"width: 80%;"} else {"width: 100%;"}}>
{ self.error.as_ref().map(|error| html! { <p>{ error }</p> }) }
{ rows }
{
if USERS_ALLOWED_TO_STREAM.iter().any(|host| host == &email) || USERS_ALLOWED_TO_STREAM.is_empty() {
html! {
<nav class="host">
<div class="controls">
<button
class="bg-yew-blue p-2 rounded-md text-white"
onclick={ctx.link().callback(|_| MeetingAction::ToggleScreenShare)}>
{ if self.share_screen { "Stop Screen Share"} else { "Share Screen"} }
</button>
</div>
{
if media_access_granted {
html! {<Host client={self.client.clone()} share_screen={self.share_screen} mic_enabled={self.mic_enabled} video_enabled={self.video_enabled} />}
} else {
html! {<></>}
<button
class="bg-yew-blue p-2 rounded-md text-white"
onclick={ctx.link().callback(|_| MeetingAction::ToggleVideoOnOff)}>
{ if !self.video_enabled { "Start Video"} else { "Stop Video"} }
</button>
<button
class="bg-yew-blue p-2 rounded-md text-white"
onclick={ctx.link().callback(|_| MeetingAction::ToggleMicMute)}>
{ if !self.mic_enabled { "Unmute"} else { "Mute"} }
</button>
<button
class="bg-yew-blue p-2 rounded-md text-white"
onclick={toggle_peer_list.clone()}>
{ if !self.peer_list_open { "Open Peers"} else { "Close Peers"} }
</button>
</div>
{
if media_access_granted {
html! {<Host client={self.client.clone()} share_screen={self.share_screen} mic_enabled={self.mic_enabled} video_enabled={self.video_enabled} />}
} else {
html! {<></>}
}
}
}
<h4 class="floating-name">{email}</h4>
<h4 class="floating-name">{email}</h4>

{if !self.client.is_connected() {
html! {<h4>{"Connecting"}</h4>}
} else {
html! {<h4>{"Connected"}</h4>}
}}
{if !self.client.is_connected() {
html! {<h4>{"Connecting"}</h4>}
} else {
html! {<h4>{"Connected"}</h4>}
}}

{if ctx.props().e2ee_enabled {
html! {<h4>{"End to End Encryption Enabled"}</h4>}
} else {
html! {<h4>{"End to End Encryption Disabled"}</h4>}
}}
</nav>
{if ctx.props().e2ee_enabled {
html! {<h4>{"End to End Encryption Enabled"}</h4>}
} else {
html! {<h4>{"End to End Encryption Disabled"}</h4>}
}}
</nav>
}
} else {
error!("User not allowed to stream");
error!("allowed users {}", USERS_ALLOWED_TO_STREAM.join(", "));
html! {}
}
} else {
error!("User not allowed to stream");
error!("allowed users {}", USERS_ALLOWED_TO_STREAM.join(", "));
html! {}
}
}
</div>
<div id="peer-list-container" class={if self.peer_list_open {"visible"} else {""}}>
<PeerList peers={peers} onclose={toggle_peer_list} />
</div>
</div>
}
}
}

// props for the video component
#[derive(Properties, Debug, PartialEq)]
pub struct UserVideoProps {
pub id: String,
}

// user video functional component
#[function_component(UserVideo)]
fn user_video(props: &UserVideoProps) -> Html {
// create use_effect hook that gets called only once and sets a thumbnail
// for the user video
let video_ref = use_state(NodeRef::default);
let video_ref_clone = video_ref.clone();
use_effect_with_deps(
move |_| {
// Set thumbnail for the video
let video = (*video_ref_clone).cast::<HtmlCanvasElement>().unwrap();
let ctx = video
.get_context("2d")
.unwrap()
.unwrap()
.unchecked_into::<CanvasRenderingContext2d>();
ctx.clear_rect(0.0, 0.0, video.width() as f64, video.height() as f64);
|| ()
},
vec![props.id.clone()],
);

html! {
<canvas ref={(*video_ref).clone()} id={props.id.clone()}></canvas>
}
}

fn toggle_pinned_div(div_id: &str) {
if let Some(div) = window()
.and_then(|w| w.document())
.and_then(|doc| doc.get_element_by_id(div_id))
{
// if the div does not have the grid-item-pinned css class, add it to it
if !div.class_list().contains("grid-item-pinned") {
div.class_list().add_1("grid-item-pinned").unwrap();
} else {
// else remove it
div.class_list().remove_1("grid-item-pinned").unwrap();
}
}
}
Loading

0 comments on commit a19c65a

Please sign in to comment.