From 7232bbaef9adedd5244ff5c88777dd73898dc017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 11 Aug 2020 22:32:55 +0200 Subject: [PATCH 01/12] Adding LayoutManager to position videos as cleverly as possible --- front/src/WebRtc/HtmlUtils.ts | 10 ++++ front/src/WebRtc/LayoutManager.ts | 94 +++++++++++++++++++++++++++++++ front/src/index.ts | 2 +- 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 front/src/WebRtc/HtmlUtils.ts create mode 100644 front/src/WebRtc/LayoutManager.ts diff --git a/front/src/WebRtc/HtmlUtils.ts b/front/src/WebRtc/HtmlUtils.ts new file mode 100644 index 00000000..c2e6ff6d --- /dev/null +++ b/front/src/WebRtc/HtmlUtils.ts @@ -0,0 +1,10 @@ +export class HtmlUtils { + public static getElementByIdOrFail(id: string): T { + const elem = document.getElementById(id); + if (elem === null) { + throw new Error("Cannot find HTML element with id '"+id+"'"); + } + // FIXME: does not check the type of the returned type + return elem as T; + } +} diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts new file mode 100644 index 00000000..cf986d0c --- /dev/null +++ b/front/src/WebRtc/LayoutManager.ts @@ -0,0 +1,94 @@ +import {HtmlUtils} from "./HtmlUtils"; + +export enum LayoutMode { + // All videos are displayed on the right side of the screen. If there is a screen sharing, it is displayed in the middle. + Presentation = "Presentation", + // Videos take the whole page. + VideoChat = "VideoChat", +} + +export enum DivImportance { + // For screen sharing + Important = "Important", + // For normal video + Normal = "Normal", +} + +/** + * This class is in charge of the video-conference layout. + * It receives positioning requests for videos and does its best to place them on the screen depending on the active layout mode. + */ +export class LayoutManager { + private mode: LayoutMode = LayoutMode.Presentation; + + private importantDivs: Map = new Map(); + private normalDivs: Map = new Map(); + + public add(importance: DivImportance, userId: string, html: string): void { + const div = document.createElement('div'); + div.append(html); + div.id = "user-"+userId; + + if (importance === DivImportance.Important) { + this.importantDivs.set(userId, div); + + // If this is the first video with high importance, let's switch mode automatically. + if (this.importantDivs.size === 1 && this.mode === LayoutMode.VideoChat) { + this.switchLayoutMode(LayoutMode.Presentation); + } + } else if (importance === DivImportance.Normal) { + this.normalDivs.set(userId, div); + } else { + throw new Error('Unexpected importance'); + } + + this.positionDiv(div, importance); + } + + private positionDiv(elem: HTMLDivElement, importance: DivImportance): void { + if (this.mode === LayoutMode.VideoChat) { + const chatModeDiv = HtmlUtils.getElementByIdOrFail('chat-mode'); + chatModeDiv.appendChild(elem); + } else { + if (importance === DivImportance.Important) { + const mainSectionDiv = HtmlUtils.getElementByIdOrFail('main-section'); + mainSectionDiv.appendChild(elem); + } else if (importance === DivImportance.Normal) { + const sideBarDiv = HtmlUtils.getElementByIdOrFail('sidebar'); + sideBarDiv.appendChild(elem); + } + } + } + + /** + * Removes the DIV matching userId. + */ + public remove(userId: string): void { + let div = this.importantDivs.get(userId); + if (div !== undefined) { + div.remove(); + this.importantDivs.delete(userId); + return; + } + + div = this.normalDivs.get(userId); + if (div !== undefined) { + div.remove(); + this.normalDivs.delete(userId); + return; + } + + throw new Error('Could not find user ID "'+userId+'"'); + } + + private switchLayoutMode(layoutMode: LayoutMode) { + this.mode = layoutMode; + + for (let div of this.importantDivs.values()) { + this.positionDiv(div, DivImportance.Important); + } + for (let div of this.normalDivs.values()) { + this.positionDiv(div, DivImportance.Normal); + } + } +} diff --git a/front/src/index.ts b/front/src/index.ts index 7634351f..d64a8f2e 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -11,7 +11,7 @@ import {FourOFourScene} from "./Phaser/Reconnecting/FourOFourScene"; import {CustomizeScene} from "./Phaser/Login/CustomizeScene"; const config: GameConfig = { - title: "Office game", + title: "WorkAdventure", width: window.innerWidth / RESOLUTION, height: window.innerHeight / RESOLUTION, parent: "game", From 83fe024c452f207b3c8154cb55fec07242066aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 11 Aug 2020 22:40:54 +0200 Subject: [PATCH 02/12] Adjusting class in chat mode based on number of divs displayed. --- front/src/WebRtc/LayoutManager.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts index cf986d0c..63a02356 100644 --- a/front/src/WebRtc/LayoutManager.ts +++ b/front/src/WebRtc/LayoutManager.ts @@ -43,6 +43,7 @@ export class LayoutManager { } this.positionDiv(div, importance); + this.adjustVideoChatClass(); } private positionDiv(elem: HTMLDivElement, importance: DivImportance): void { @@ -68,6 +69,7 @@ export class LayoutManager { if (div !== undefined) { div.remove(); this.importantDivs.delete(userId); + this.adjustVideoChatClass(); return; } @@ -75,12 +77,30 @@ export class LayoutManager { if (div !== undefined) { div.remove(); this.normalDivs.delete(userId); + this.adjustVideoChatClass(); return; } throw new Error('Could not find user ID "'+userId+'"'); } + private adjustVideoChatClass(): void { + const chatModeDiv = HtmlUtils.getElementByIdOrFail('chat-mode'); + chatModeDiv.classList.remove('one-col', 'two-col', 'three-col', 'four-col'); + + const nbUsers = this.importantDivs.size + this.normalDivs.size; + + if (nbUsers <= 1) { + chatModeDiv.classList.add('one-col'); + } else if (nbUsers <= 4) { + chatModeDiv.classList.add('two-col'); + } else if (nbUsers <= 9) { + chatModeDiv.classList.add('three-col'); + } else { + chatModeDiv.classList.add('four-col'); + } + } + private switchLayoutMode(layoutMode: LayoutMode) { this.mode = layoutMode; From 9f6c6e0ce18172b5c57c4a391bc4c275a55978ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 13 Aug 2020 18:21:48 +0200 Subject: [PATCH 03/12] Adding CoWebsiteManager + first working version of flex video --- front/dist/index.html | 52 ++++++- front/dist/resources/style/style.css | 196 ++++++++++++++++++++++----- front/src/WebRtc/CoWebsiteManager.ts | 56 ++++++++ front/src/WebRtc/LayoutManager.ts | 22 ++- front/src/WebRtc/MediaManager.ts | 31 ++++- front/src/index.ts | 20 ++- 6 files changed, 326 insertions(+), 51 deletions(-) create mode 100644 front/src/WebRtc/CoWebsiteManager.ts diff --git a/front/dist/index.html b/front/dist/index.html index a680c59a..92a7bf3c 100644 --- a/front/dist/index.html +++ b/front/dist/index.html @@ -39,7 +39,53 @@ WorkAdventure -
+
+
+
+
+ +
+ + + + +
+
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+
+ -->
diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index 5f0e1cab..45b61679 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -27,7 +27,7 @@ video{ -webkit-transform: scaleX(-1); transform: scaleX(-1); } -.webrtc{ +/*.webrtc{ display: none; position: absolute; right: 0px; @@ -36,21 +36,22 @@ video{ } .webrtc.active{ display: block; -} +}*/ -.webrtc, .activeCam{} -.activeCam .video-container{ - position: absolute; - height: 25%; +/*.webrtc, .activeCam{}*/ +/*.activeCam*/ .video-container{ + position: relative; + /*height: 25%; top: 10px; margin: 5px; right: -100px; - transition: all 0.2s ease; - border-color: black; + transition: all 0.2s ease;*/ + /*border-color: black; border-style: solid; - border-width: 0.2px; + border-width: 0.2px;*/ + background-color: #00000099; } -.activeCam .video-container i{ +/*.activeCam*/ .video-container i{ position: absolute; width: 100px; height: 65px; @@ -63,10 +64,10 @@ video{ font-size: 28px; color: white; } -.activeCam .video-container img.active{ +/*.activeCam*/ .video-container img.active{ display: block; } -.activeCam .video-container img{ +/*.activeCam*/ .video-container img{ position: absolute; display: none; width: 15px; @@ -78,34 +79,28 @@ video{ padding: 10px; z-index: 2; } -.activeCam .video-container video{ +/*.activeCam*/ .video-container video{ height: 100%; } -.webrtc:hover .activeCam .video-container{ +/*.webrtc:hover .activeCam .video-container{ right: 10px; -} -.activeCam .video-container#div-myCamVideo{ +}*/ +/*.activeCam*/ .video-container#div-myCamVideo{ border: none; } -.activeCam .video-container video#myCamVideo{ - width: 200px; - height: 113px; +/*.activeCam*/ + +#div-myCamVideo { + position: fixed; + right: 0; + bottom: 0; } -/*CSS size for 2 - 3 elements*/ -.activeCam .video-container:nth-child(1){ - /*this is for camera of user*/ - top: 75%; -} -.activeCam .video-container:nth-child(2){ - top: 0%; -} -.activeCam .video-container:nth-child(3){ - top: 25%; -} -.activeCam .video-container:nth-child(4) { - top: 50%; +video#myCamVideo{ + width: 15vw; + /*width: 200px;*/ + /*height: 113px;*/ } /*btn animation*/ @@ -122,7 +117,7 @@ video{ transition-timing-function: ease-in-out; bottom: 20px; } -.webrtc:hover .btn-cam-action div{ +#activeCam:hover .btn-cam-action div{ transform: translateY(0); } .btn-cam-action div:hover{ @@ -237,3 +232,138 @@ video{ .webrtcsetup.active{ display: block; } + + +/* New layout */ +body { + margin: 0; + height: 100vh; + width: 100vw; +} +.main-container { + height: 100vh; + width: 100vw; + display: flex; + align-items: stretch; +} + +@media (min-aspect-ratio: 1/1) { + .main-container { + flex-direction: row + } + + .game-overlay { + flex-direction: row + } + + .sidebar { + flex-direction: column + } +} +@media (max-aspect-ratio: 1/1) { + .main-container { + flex-direction: column + } + + .game-overlay { + flex-direction: column + } + + .sidebar { + flex-direction: row + } +} + +.game { + flex-basis: 100%; + position: relative; /* Position relative is needed for the game-overlay. */ +} + +/* A potentially shared website could appear in an iframe in the cowebsite space. */ +.cowebsite { + flex-basis: 100%; + transition: flex-basis 0.5s; +} + +/*.cowebsite:hover { + flex-basis: 100%; +}*/ + +.cowebsite iframe { + width: 100%; + height: 100%; +} + + +.game-overlay { + display: none; + position: absolute; + width: 100%; + height: 100%; + /* TODO: DO WE NEED FLEX HERE???? WE WANT A SIDEBAR OF EXACTLY 25% (note: flex useful for direction!!!) */ +} + +.game-overlay.active { + display: flex; +} + +.game-overlay video { + width: 100% +} + +.main-section { + flex: 0 0 75%; + display: flex; + justify-content: start; + /*align-items: flex-start;*/ + flex-wrap: wrap; +} + +.main-section div { + margin: 5%; + flex-basis: 90%; + /*flex-shrink: 2;*/ +} + +.sidebar { + flex: 0 0 25%; + display: flex; +} + +.sidebar > div { + height: 15%; + margin: 5%; +} + +.chat-mode { + display: flex; + width: 100%; + + flex-wrap: wrap; + + padding: 1%; +} + +.chat-mode div { + margin: 1%; +} + +.chat-mode.one-col div { + flex-basis: 98%; +} + +.chat-mode.two-col div { + flex-basis: 48%; +} + +.chat-mode.three-col div { + flex-basis: 31.333333%; +} + +.chat-mode.four-col div { + flex-basis: 23%; +} + +.chat-mode div:last-child { + flex-grow: 5; +} diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts new file mode 100644 index 00000000..0150760c --- /dev/null +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -0,0 +1,56 @@ +import {HtmlUtils} from "./HtmlUtils"; + +export type CoWebsiteStateChangedCallback = () => void; + +export class CoWebsiteManager { + + private static observers = new Array(); + + public static loadCoWebsite(url: string): void { + const cowebsiteDiv = HtmlUtils.getElementByIdOrFail("cowebsite"); + cowebsiteDiv.innerHTML = ''; + + const iframe = document.createElement('iframe'); + iframe.id = 'cowebsite-iframe'; + iframe.src = url; + cowebsiteDiv.appendChild(iframe); + CoWebsiteManager.fire(); + } + + public static closeCoWebsite(): void { + const cowebsiteDiv = HtmlUtils.getElementByIdOrFail("cowebsite"); + cowebsiteDiv.innerHTML = ''; + CoWebsiteManager.fire(); + } + + public static getGameSize(): {width: number, height: number} { + const iframe = document.getElementById('cowebsite-iframe'); + if (iframe === null) { + return { + width: window.innerWidth, + height: window.innerHeight + } + } + if (window.innerWidth >= window.innerHeight) { + return { + width: window.innerWidth / 2, + height: window.innerHeight + } + } else { + return { + width: window.innerWidth, + height: window.innerHeight / 2 + } + } + } + + public static onStateChange(observer: CoWebsiteStateChangedCallback) { + CoWebsiteManager.observers.push(observer); + } + + private static fire(): void { + for (const callback of CoWebsiteManager.observers) { + callback(); + } + } +} diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts index 63a02356..670e05bd 100644 --- a/front/src/WebRtc/LayoutManager.ts +++ b/front/src/WebRtc/LayoutManager.ts @@ -18,7 +18,7 @@ export enum DivImportance { * This class is in charge of the video-conference layout. * It receives positioning requests for videos and does its best to place them on the screen depending on the active layout mode. */ -export class LayoutManager { +class LayoutManager { private mode: LayoutMode = LayoutMode.Presentation; private importantDivs: Map = new Map(); @@ -26,7 +26,7 @@ export class LayoutManager { public add(importance: DivImportance, userId: string, html: string): void { const div = document.createElement('div'); - div.append(html); + div.innerHTML = html; div.id = "user-"+userId; if (importance === DivImportance.Important) { @@ -65,6 +65,7 @@ export class LayoutManager { * Removes the DIV matching userId. */ public remove(userId: string): void { + console.log('Removing video for userID '+userId+'.'); let div = this.importantDivs.get(userId); if (div !== undefined) { div.remove(); @@ -81,7 +82,8 @@ export class LayoutManager { return; } - throw new Error('Could not find user ID "'+userId+'"'); + console.log('Cannot remove userID '+userId+'. Already removed?'); + //throw new Error('Could not find user ID "'+userId+'"'); } private adjustVideoChatClass(): void { @@ -104,6 +106,16 @@ export class LayoutManager { private switchLayoutMode(layoutMode: LayoutMode) { this.mode = layoutMode; + if (layoutMode === LayoutMode.Presentation) { + HtmlUtils.getElementByIdOrFail('sidebar').style.display = 'block'; + HtmlUtils.getElementByIdOrFail('main-section').style.display = 'block'; + HtmlUtils.getElementByIdOrFail('chat-mode').style.display = 'none'; + } else { + HtmlUtils.getElementByIdOrFail('sidebar').style.display = 'none'; + HtmlUtils.getElementByIdOrFail('main-section').style.display = 'none'; + HtmlUtils.getElementByIdOrFail('chat-mode').style.display = 'block'; + } + for (let div of this.importantDivs.values()) { this.positionDiv(div, DivImportance.Important); } @@ -112,3 +124,7 @@ export class LayoutManager { } } } + +const layoutManager = new LayoutManager(); + +export { layoutManager }; diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index e69850a2..fb7c34f1 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -1,3 +1,5 @@ +import {DivImportance, layoutManager} from "./LayoutManager"; + const videoConstraint: boolean|MediaTrackConstraints = { width: { ideal: 1280 }, height: { ideal: 720 }, @@ -73,8 +75,8 @@ export class MediaManager { } activeVisio(){ - const webRtc = this.getElementByIdOrFail('webRtc'); - webRtc.classList.add('active'); + const gameOverlay = this.getElementByIdOrFail('game-overlay'); + gameOverlay.classList.add('active'); } enabledCamera() { @@ -184,10 +186,11 @@ export class MediaManager { */ addActiveVideo(userId : string, userName: string = ""){ this.webrtcInAudio.play(); - const elementRemoteVideo = this.getElementByIdOrFail("activeCam"); + + //const elementRemoteVideo = this.getElementByIdOrFail("activeCam"); userName = userName.toUpperCase(); const color = this.getColorByString(userName); - elementRemoteVideo.insertAdjacentHTML('beforeend', ` + /*elementRemoteVideo.insertAdjacentHTML('beforeend', `
@@ -195,7 +198,20 @@ export class MediaManager {
- `); + `);*/ + + const html = ` +
+
+ + ${userName} + + +
+ `; + + layoutManager.add(DivImportance.Normal, userId, html); + this.remoteVideo.set(userId, this.getElementByIdOrFail(userId)); } @@ -274,11 +290,12 @@ export class MediaManager { * @param userId */ removeActiveVideo(userId : string){ - const element = document.getElementById(`div-${userId}`); + /*const element = document.getElementById(`div-${userId}`); if(!element){ return; } - element.remove(); + element.remove();*/ + layoutManager.remove(userId); this.remoteVideo.delete(userId); } diff --git a/front/src/index.ts b/front/src/index.ts index d64a8f2e..75ad0fe6 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -4,16 +4,21 @@ import {DEBUG_MODE, RESOLUTION} from "./Enum/EnvironmentVariable"; import {cypressAsserter} from "./Cypress/CypressAsserter"; import {LoginScene} from "./Phaser/Login/LoginScene"; import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene"; -import {gameManager} from "./Phaser/Game/GameManager"; import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene"; import {EnableCameraScene} from "./Phaser/Login/EnableCameraScene"; import {FourOFourScene} from "./Phaser/Reconnecting/FourOFourScene"; import {CustomizeScene} from "./Phaser/Login/CustomizeScene"; +import {HtmlUtils} from "./WebRtc/HtmlUtils"; +import {CoWebsiteManager} from "./WebRtc/CoWebsiteManager"; + +//CoWebsiteManager.loadCoWebsite('https://thecodingmachine.com'); + +const {width, height} = CoWebsiteManager.getGameSize(); const config: GameConfig = { title: "WorkAdventure", - width: window.innerWidth / RESOLUTION, - height: window.innerHeight / RESOLUTION, + width: width / RESOLUTION, + height: height / RESOLUTION, parent: "game", scene: [LoginScene, SelectCharacterScene, EnableCameraScene, ReconnectingScene, FourOFourScene, CustomizeScene], zoom: RESOLUTION, @@ -30,5 +35,12 @@ cypressAsserter.gameStarted(); const game = new Phaser.Game(config); window.addEventListener('resize', function (event) { - game.scale.resize(window.innerWidth / RESOLUTION, window.innerHeight / RESOLUTION); + const {width, height} = CoWebsiteManager.getGameSize(); + + game.scale.resize(width / RESOLUTION, height / RESOLUTION); +}); +CoWebsiteManager.onStateChange(() => { + const {width, height} = CoWebsiteManager.getGameSize(); + + game.scale.resize(width / RESOLUTION, height / RESOLUTION); }); From 0041e088a4289b2517326d8284bfcd5f83e8f21c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 13 Aug 2020 18:28:22 +0200 Subject: [PATCH 04/12] Fixing position of self webcam in CoWebsite mode --- front/dist/resources/style/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index 45b61679..9fe4c9ea 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -92,7 +92,7 @@ video{ /*.activeCam*/ #div-myCamVideo { - position: fixed; + position: absolute; right: 0; bottom: 0; } From 88c099fc1390ab9d2c9354caf9accb13f3c641fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Sun, 16 Aug 2020 23:19:04 +0200 Subject: [PATCH 05/12] Improving layout Fixing left-right switch on all cameras (except current player camera) --- front/dist/resources/style/style.css | 45 ++++++++++++++++------------ front/src/Phaser/Game/GameScene.ts | 41 +++++++++++++++---------- front/src/WebRtc/LayoutManager.ts | 6 +++- front/src/WebRtc/MediaManager.ts | 16 +++++----- 4 files changed, 63 insertions(+), 45 deletions(-) diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index 9fe4c9ea..8f188cca 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -23,10 +23,6 @@ body .message-info.info{ body .message-info.warning{ background: #ffa500d6; } -video{ - -webkit-transform: scaleX(-1); - transform: scaleX(-1); -} /*.webrtc{ display: none; position: absolute; @@ -99,10 +95,13 @@ video{ video#myCamVideo{ width: 15vw; + -webkit-transform: scaleX(-1); + transform: scaleX(-1); /*width: 200px;*/ /*height: 113px;*/ } + /*btn animation*/ .btn-cam-action div{ cursor: pointer; @@ -113,7 +112,7 @@ video#myCamVideo{ background: #666; box-shadow: 2px 2px 24px #444; border-radius: 48px; - transform: translateY(12vw); + transform: translateY(12vh); transition-timing-function: ease-in-out; bottom: 20px; } @@ -249,28 +248,37 @@ body { @media (min-aspect-ratio: 1/1) { .main-container { - flex-direction: row + flex-direction: row; } .game-overlay { - flex-direction: row + flex-direction: row; } .sidebar { - flex-direction: column + flex-direction: column; + } + + .sidebar > div { + height: 15%; } } @media (max-aspect-ratio: 1/1) { .main-container { - flex-direction: column + flex-direction: column; } .game-overlay { - flex-direction: column + flex-direction: column; } .sidebar { - flex-direction: row + flex-direction: row; + align-items: flex-end; + } + + .sidebar > div { + width: 15%; } } @@ -319,7 +327,7 @@ body { flex-wrap: wrap; } -.main-section div { +.main-section > div { margin: 5%; flex-basis: 90%; /*flex-shrink: 2;*/ @@ -331,8 +339,7 @@ body { } .sidebar > div { - height: 15%; - margin: 5%; + margin: 2%; } .chat-mode { @@ -348,22 +355,22 @@ body { margin: 1%; } -.chat-mode.one-col div { +.chat-mode.one-col > div { flex-basis: 98%; } -.chat-mode.two-col div { +.chat-mode.two-col > div { flex-basis: 48%; } -.chat-mode.three-col div { +.chat-mode.three-col > div { flex-basis: 31.333333%; } -.chat-mode.four-col div { +.chat-mode.four-col > div { flex-basis: 23%; } -.chat-mode div:last-child { +.chat-mode > div:last-child { flex-grow: 5; } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 487f3fb1..6505ad11 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,34 +1,33 @@ import {GameManager, gameManager, HasMovedEvent} from "./GameManager"; import { Connection, - GroupCreatedUpdatedMessageInterface, MessageUserJoined, + GroupCreatedUpdatedMessageInterface, + MessageUserJoined, MessageUserMovedInterface, - MessageUserPositionInterface, PointInterface, PositionInterface + MessageUserPositionInterface, + PointInterface, + PositionInterface } from "../../Connection"; import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player"; -import { DEBUG_MODE, ZOOM_LEVEL, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; -import { - ITiledMap, - ITiledMapLayer, - ITiledMapLayerProperty, - ITiledTileSet -} from "../Map/ITiledMap"; +import {DEBUG_MODE, POSITION_DELAY, ZOOM_LEVEL} from "../../Enum/EnvironmentVariable"; +import {ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledTileSet} from "../Map/ITiledMap"; import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; -import Texture = Phaser.Textures.Texture; -import Sprite = Phaser.GameObjects.Sprite; -import CanvasTexture = Phaser.Textures.CanvasTexture; import {AddPlayerInterface} from "./AddPlayerInterface"; import {PlayerAnimationNames} from "../Player/Animation"; import {PlayerMovement} from "./PlayerMovement"; import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; import {RemotePlayer} from "../Entity/RemotePlayer"; -import GameObject = Phaser.GameObjects.GameObject; -import { Queue } from 'queue-typescript'; +import {Queue} from 'queue-typescript'; import {SimplePeer} from "../../WebRtc/SimplePeer"; import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; -import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import {FourOFourSceneName} from "../Reconnecting/FourOFourScene"; -import {LAYERS, loadAllLayers} from "../Entity/body_character"; +import {loadAllLayers} from "../Entity/body_character"; +import {layoutManager, LayoutMode} from "../../WebRtc/LayoutManager"; +import Texture = Phaser.Textures.Texture; +import Sprite = Phaser.GameObjects.Sprite; +import CanvasTexture = Phaser.Textures.CanvasTexture; +import GameObject = Phaser.GameObjects.GameObject; +import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; export enum Textures { @@ -364,6 +363,16 @@ export class GameScene extends Phaser.Scene { } }, 500); } + + // FIXME: change this to use the class for input + this.input.keyboard.on('keyup-' + 'M', function () { + const mode = layoutManager.getLayoutMode(); + if (mode === LayoutMode.Presentation) { + layoutManager.switchLayoutMode(LayoutMode.VideoChat); + } else { + layoutManager.switchLayoutMode(LayoutMode.Presentation); + } + }); } private getExitSceneUrl(layer: ITiledMapLayer): string|undefined { diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts index 670e05bd..7e99d496 100644 --- a/front/src/WebRtc/LayoutManager.ts +++ b/front/src/WebRtc/LayoutManager.ts @@ -103,7 +103,7 @@ class LayoutManager { } } - private switchLayoutMode(layoutMode: LayoutMode) { + public switchLayoutMode(layoutMode: LayoutMode) { this.mode = layoutMode; if (layoutMode === LayoutMode.Presentation) { @@ -123,6 +123,10 @@ class LayoutManager { this.positionDiv(div, DivImportance.Normal); } } + + public getLayoutMode(): LayoutMode { + return this.mode; + } } const layoutManager = new LayoutManager(); diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index fb7c34f1..9804317b 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -204,7 +204,7 @@ export class MediaManager {
- ${userName} + ${userName}
@@ -248,11 +248,10 @@ export class MediaManager { if (element) { element.style.opacity = "0"; } - element = document.getElementById(`div-${userId}`); - if (!element) { - return; + element = document.getElementById(`name-${userId}`); + if (element) { + element.style.display = "block"; } - element.style.borderStyle = "solid"; } /** @@ -264,11 +263,10 @@ export class MediaManager { if(element){ element.style.opacity = "1"; } - element = document.getElementById(`div-${userId}`); - if(!element){ - return; + element = document.getElementById(`name-${userId}`); + if(element){ + element.style.display = "none"; } - element.style.borderStyle = "none"; } /** From 15097779454ae03493a706faa7b81feb79e93b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Sun, 16 Aug 2020 23:45:03 +0200 Subject: [PATCH 06/12] Improving video CSS (work on overlay) --- front/dist/resources/style/style.css | 12 ++++++++++-- front/src/WebRtc/LayoutManager.ts | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index 8f188cca..d7a9a560 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -260,7 +260,7 @@ body { } .sidebar > div { - height: 15%; + max-height: 21%; } } @media (max-aspect-ratio: 1/1) { @@ -278,7 +278,7 @@ body { } .sidebar > div { - width: 15%; + max-width: 21%; } } @@ -342,6 +342,14 @@ body { margin: 2%; } +/* Let's make sure videos are vertically centered if they need to be cropped */ +.media-container { + display: flex; + justify-content: center; + flex-direction: column; + overflow: hidden; +} + .chat-mode { display: flex; width: 100%; diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts index 7e99d496..98c20b4c 100644 --- a/front/src/WebRtc/LayoutManager.ts +++ b/front/src/WebRtc/LayoutManager.ts @@ -28,6 +28,7 @@ class LayoutManager { const div = document.createElement('div'); div.innerHTML = html; div.id = "user-"+userId; + div.className = "media-container" if (importance === DivImportance.Important) { this.importantDivs.set(userId, div); From 05ca8c813e727b86e172682f5656989695fe0c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Sun, 16 Aug 2020 23:49:31 +0200 Subject: [PATCH 07/12] Fixing chat mode canceling flex display --- front/src/WebRtc/LayoutManager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts index 98c20b4c..a0001693 100644 --- a/front/src/WebRtc/LayoutManager.ts +++ b/front/src/WebRtc/LayoutManager.ts @@ -108,13 +108,13 @@ class LayoutManager { this.mode = layoutMode; if (layoutMode === LayoutMode.Presentation) { - HtmlUtils.getElementByIdOrFail('sidebar').style.display = 'block'; - HtmlUtils.getElementByIdOrFail('main-section').style.display = 'block'; + HtmlUtils.getElementByIdOrFail('sidebar').style.display = 'flex'; + HtmlUtils.getElementByIdOrFail('main-section').style.display = 'flex'; HtmlUtils.getElementByIdOrFail('chat-mode').style.display = 'none'; } else { HtmlUtils.getElementByIdOrFail('sidebar').style.display = 'none'; HtmlUtils.getElementByIdOrFail('main-section').style.display = 'none'; - HtmlUtils.getElementByIdOrFail('chat-mode').style.display = 'block'; + HtmlUtils.getElementByIdOrFail('chat-mode').style.display = 'flex'; } for (let div of this.importantDivs.values()) { From 7fe2cc19c35209852b65a4d647ed473800b0cb34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 17 Aug 2020 15:20:03 +0200 Subject: [PATCH 08/12] Adding buttons to switch mode --- front/dist/resources/objects/layout_modes.png | Bin 0 -> 297 bytes front/dist/resources/style/style.css | 2 + front/src/Phaser/Game/GameScene.ts | 54 +++++++++++++++--- 3 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 front/dist/resources/objects/layout_modes.png diff --git a/front/dist/resources/objects/layout_modes.png b/front/dist/resources/objects/layout_modes.png new file mode 100644 index 0000000000000000000000000000000000000000..abd9adaf5336965b6a87b6af603dae1d24d9c43b GIT binary patch literal 297 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=A3R+gLn`LHy>XDM*+7K#Vok-r zyX&M(QYJ3b&(APEv_@o)46C=d=2I)18U|UrhIKJlKFS4VyH+uzoc_*m^SeBgi`~tc zpa0G{p>ACq$H2%UkRW~FRJelBf%Oc3**Fv$7}A-4h|aM1e?7l}VLyk!O)iHNrV9+l z4%Uom4J-l<42)6?g?Ek%m^K_f%zoh^^NSCRB|jLfz;>Ox&)9Dm`4wnO{4aNnU$g4Y zZ2$kIrmB~L31sfy8Ef1aX6Dv1hcPaiw+ div { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 6505ad11..2ef7d630 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -106,6 +106,9 @@ export class GameScene extends Phaser.Scene { private PositionNextScene: Array> = new Array>(); private startLayerName: string|undefined; + private presentationModeSprite!: Sprite; + private chatModeSprite!: Sprite; + private repositionCallback!: (this: Window, ev: UIEvent) => void; static createFromUrl(mapUrlFile: string, instance: string, key: string|null = null): GameScene { const mapKey = GameScene.getMapKeyByUrl(mapUrlFile); @@ -158,6 +161,12 @@ export class GameScene extends Phaser.Scene { ); }); + this.load.spritesheet( + 'layout_modes', + 'resources/objects/layout_modes.png', + {frameWidth: 32, frameHeight: 32} + ); + loadAllLayers(this.load); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); @@ -213,6 +222,7 @@ export class GameScene extends Phaser.Scene { this.scene.stop(this.scene.key); this.scene.remove(this.scene.key); + window.removeEventListener('resize', this.repositionCallback); }) // When connection is performed, let's connect SimplePeer @@ -364,15 +374,39 @@ export class GameScene extends Phaser.Scene { }, 500); } + // FIXME: handle display / hide based on number of cameras connected + this.presentationModeSprite = this.add.sprite(2, this.game.renderer.height - 2, 'layout_modes', 0); + this.presentationModeSprite.setScrollFactor(0, 0); + this.presentationModeSprite.setOrigin(0, 1); + this.presentationModeSprite.setInteractive(); + this.presentationModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); + this.chatModeSprite = this.add.sprite(36, this.game.renderer.height - 2, 'layout_modes', 3); + this.chatModeSprite.setScrollFactor(0, 0); + this.chatModeSprite.setOrigin(0, 1); + this.chatModeSprite.setInteractive(); + this.chatModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); + // FIXME: change this to use the class for input - this.input.keyboard.on('keyup-' + 'M', function () { - const mode = layoutManager.getLayoutMode(); - if (mode === LayoutMode.Presentation) { - layoutManager.switchLayoutMode(LayoutMode.VideoChat); - } else { - layoutManager.switchLayoutMode(LayoutMode.Presentation); - } + this.input.keyboard.on('keyup-' + 'M', () => { + this.switchLayoutMode(); }); + + this.repositionCallback = this.reposition.bind(this); + window.addEventListener('resize', this.repositionCallback); + this.reposition(); + } + + private switchLayoutMode(): void { + const mode = layoutManager.getLayoutMode(); + if (mode === LayoutMode.Presentation) { + layoutManager.switchLayoutMode(LayoutMode.VideoChat); + this.presentationModeSprite.setFrame(1); + this.chatModeSprite.setFrame(2); + } else { + layoutManager.switchLayoutMode(LayoutMode.Presentation); + this.presentationModeSprite.setFrame(0); + this.chatModeSprite.setFrame(3); + } } private getExitSceneUrl(layer: ITiledMapLayer): string|undefined { @@ -634,6 +668,7 @@ export class GameScene extends Phaser.Scene { this.simplePeer.unregister(); this.scene.stop(); this.scene.remove(this.scene.key); + window.removeEventListener('resize', this.repositionCallback); this.scene.start(nextSceneKey.key, { startLayerName: nextSceneKey.hash }); @@ -821,4 +856,9 @@ export class GameScene extends Phaser.Scene { const endPos = mapUrlStart.indexOf(".json"); return mapUrlStart.substring(startPos, endPos); } + + private reposition(): void { + this.presentationModeSprite.setY(this.game.renderer.height - 2); + this.chatModeSprite.setY(this.game.renderer.height - 2); + } } From 6516e621b04b08dce54788774fd296d6712bc32d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 17 Aug 2020 16:12:53 +0200 Subject: [PATCH 09/12] Adding display / hide of layout buttons when a meet start / ends --- front/src/Phaser/Game/GameScene.ts | 22 ++++++++++++++++++---- front/src/WebRtc/SimplePeer.ts | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 2ef7d630..c4517545 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -18,7 +18,7 @@ import {PlayerMovement} from "./PlayerMovement"; import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; import {RemotePlayer} from "../Entity/RemotePlayer"; import {Queue} from 'queue-typescript'; -import {SimplePeer} from "../../WebRtc/SimplePeer"; +import {SimplePeer, UserSimplePeer} from "../../WebRtc/SimplePeer"; import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; import {FourOFourSceneName} from "../Reconnecting/FourOFourScene"; import {loadAllLayers} from "../Entity/body_character"; @@ -227,6 +227,19 @@ export class GameScene extends Phaser.Scene { // When connection is performed, let's connect SimplePeer this.simplePeer = new SimplePeer(this.connection); + const self = this; + this.simplePeer.registerPeerConnectionListener({ + onConnect(user: UserSimplePeer) { + self.presentationModeSprite.setVisible(true); + self.chatModeSprite.setVisible(true); + }, + onDisconnect(userId: string) { + if (self.simplePeer.getNbConnections() === 0) { + self.presentationModeSprite.setVisible(false); + self.chatModeSprite.setVisible(false); + } + } + }) this.scene.wake(); this.scene.sleep(ReconnectingSceneName); @@ -374,19 +387,20 @@ export class GameScene extends Phaser.Scene { }, 500); } - // FIXME: handle display / hide based on number of cameras connected this.presentationModeSprite = this.add.sprite(2, this.game.renderer.height - 2, 'layout_modes', 0); this.presentationModeSprite.setScrollFactor(0, 0); this.presentationModeSprite.setOrigin(0, 1); this.presentationModeSprite.setInteractive(); + this.presentationModeSprite.setVisible(false); this.presentationModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); this.chatModeSprite = this.add.sprite(36, this.game.renderer.height - 2, 'layout_modes', 3); this.chatModeSprite.setScrollFactor(0, 0); this.chatModeSprite.setOrigin(0, 1); this.chatModeSprite.setInteractive(); + this.chatModeSprite.setVisible(false); this.chatModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); - - // FIXME: change this to use the class for input + + // FIXME: change this to use the UserInputManager class for input this.input.keyboard.on('keyup-' + 'M', () => { this.switchLayoutMode(); }); diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 553c9307..acb52059 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -14,6 +14,12 @@ export interface UserSimplePeer{ initiator?: boolean; } +export interface PeerConnectionListener { + onConnect(user: UserSimplePeer): void; + + onDisconnect(userId: string): void; +} + /** * This class manages connections to all the peers in the same group as me. */ @@ -24,6 +30,7 @@ export class SimplePeer { private PeerConnectionArray: Map = new Map(); private readonly updateLocalStreamCallback: (media: MediaStream) => void; + private readonly peerConnectionListeners: Array = new Array(); constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") { this.Connection = Connection; @@ -34,6 +41,14 @@ export class SimplePeer { this.initialise(); } + public registerPeerConnectionListener(peerConnectionListener: PeerConnectionListener) { + this.peerConnectionListeners.push(peerConnectionListener); + } + + public getNbConnections(): number { + return this.PeerConnectionArray.size; + } + /** * permit to listen when user could start visio */ @@ -182,6 +197,10 @@ export class SimplePeer { }); this.addMedia(user.userId); + + for (let peerConnectionListener of this.peerConnectionListeners) { + peerConnectionListener.onConnect(user); + } } /** @@ -203,6 +222,9 @@ export class SimplePeer { peer.destroy(); this.PeerConnectionArray.delete(userId) //console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size); + for (let peerConnectionListener of this.peerConnectionListeners) { + peerConnectionListener.onDisconnect(userId); + } } catch (err) { console.error("closeConnection", err) } From beb0d1ef0aaf0dffeba8bbacf35c73ea5762e00f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 17 Aug 2020 16:18:39 +0200 Subject: [PATCH 10/12] Linter fix --- front/src/WebRtc/LayoutManager.ts | 4 ++-- front/src/WebRtc/SimplePeer.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts index a0001693..6695fe7f 100644 --- a/front/src/WebRtc/LayoutManager.ts +++ b/front/src/WebRtc/LayoutManager.ts @@ -117,10 +117,10 @@ class LayoutManager { HtmlUtils.getElementByIdOrFail('chat-mode').style.display = 'flex'; } - for (let div of this.importantDivs.values()) { + for (const div of this.importantDivs.values()) { this.positionDiv(div, DivImportance.Important); } - for (let div of this.normalDivs.values()) { + for (const div of this.normalDivs.values()) { this.positionDiv(div, DivImportance.Normal); } } diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index acb52059..fdc2d0c2 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -198,7 +198,7 @@ export class SimplePeer { this.addMedia(user.userId); - for (let peerConnectionListener of this.peerConnectionListeners) { + for (const peerConnectionListener of this.peerConnectionListeners) { peerConnectionListener.onConnect(user); } } @@ -222,7 +222,7 @@ export class SimplePeer { peer.destroy(); this.PeerConnectionArray.delete(userId) //console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size); - for (let peerConnectionListener of this.peerConnectionListeners) { + for (const peerConnectionListener of this.peerConnectionListeners) { peerConnectionListener.onDisconnect(userId); } } catch (err) { From 0f305b0c1224fb5792878e68412db80d103e8982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 17 Aug 2020 21:59:26 +0200 Subject: [PATCH 11/12] CSS cleanup --- front/dist/resources/style/style.css | 37 ++++++---------------------- 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index b3d98539..30e099ef 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -23,31 +23,12 @@ body .message-info.info{ body .message-info.warning{ background: #ffa500d6; } -/*.webrtc{ - display: none; - position: absolute; - right: 0px; - height: 100%; - width: 300px; -} -.webrtc.active{ - display: block; -}*/ - -/*.webrtc, .activeCam{}*/ -/*.activeCam*/ .video-container{ +.video-container{ position: relative; - /*height: 25%; - top: 10px; - margin: 5px; - right: -100px; - transition: all 0.2s ease;*/ - /*border-color: black; - border-style: solid; - border-width: 0.2px;*/ + transition: all 0.2s ease; background-color: #00000099; } -/*.activeCam*/ .video-container i{ +.video-container i{ position: absolute; width: 100px; height: 65px; @@ -60,10 +41,10 @@ body .message-info.warning{ font-size: 28px; color: white; } -/*.activeCam*/ .video-container img.active{ +.video-container img.active{ display: block; } -/*.activeCam*/ .video-container img{ +.video-container img{ position: absolute; display: none; width: 15px; @@ -75,17 +56,13 @@ body .message-info.warning{ padding: 10px; z-index: 2; } -/*.activeCam*/ .video-container video{ +.video-container video{ height: 100%; } -/*.webrtc:hover .activeCam .video-container{ - right: 10px; -}*/ -/*.activeCam*/ .video-container#div-myCamVideo{ +.video-container#div-myCamVideo{ border: none; } -/*.activeCam*/ #div-myCamVideo { position: absolute; From fc78249eaeb53c1f00e13af77ea26a9b41693969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 17 Aug 2020 22:03:08 +0200 Subject: [PATCH 12/12] Code cleanup --- front/src/WebRtc/MediaManager.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 9804317b..39a61738 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -187,18 +187,8 @@ export class MediaManager { addActiveVideo(userId : string, userName: string = ""){ this.webrtcInAudio.play(); - //const elementRemoteVideo = this.getElementByIdOrFail("activeCam"); userName = userName.toUpperCase(); const color = this.getColorByString(userName); - /*elementRemoteVideo.insertAdjacentHTML('beforeend', ` -
-
- - ${userName} - - -
- `);*/ const html = `
@@ -288,11 +278,6 @@ export class MediaManager { * @param userId */ removeActiveVideo(userId : string){ - /*const element = document.getElementById(`div-${userId}`); - if(!element){ - return; - } - element.remove();*/ layoutManager.remove(userId); this.remoteVideo.delete(userId); }