Merge pull request #238 from moufmouf/layoutmanager

Adding LayoutManager to position videos according to chosen layout / importance
This commit is contained in:
David Négrier 2020-08-17 22:08:41 +02:00 committed by GitHub
commit e1989c1c21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 563 additions and 97 deletions

50
front/dist/index.html vendored
View File

@ -39,6 +39,52 @@
<title>WorkAdventure</title> <title>WorkAdventure</title>
</head> </head>
<body id="body" style="margin: 0"> <body id="body" style="margin: 0">
<div class="main-container">
<div id="game" class="game" style="/*background: red;*/">
<div id="game-overlay" class="game-overlay" style="/*background: violet*/;">
<div id="main-section" class="main-section">
<!--<div style="background: lightpink;">a</div>
<div style="background: lightpink;">a</div> -->
</div>
<aside id="sidebar" class="sidebar">
<!--<div style="background: lightgreen;">a</div>
<div style="background: green;">b</div>
<div style="background: darkgreen;">c</div>
<div style="background: darkgreen;">d</div>-->
</aside>
<div id="chat-mode" class="chat-mode three-col" style="display: none;">
<!--<div style="background: lightgreen;">a</div>
<div style="background: green;">b</div>
<div style="background: darkgreen;">c</div>
<div style="background: darkolivegreen;">d</div>
<div style="background: darkolivegreen;">d</div>
<div style="background: darkgreen;">c</div>
<div style="background: green;">b</div>
<div style="background: lightgreen;">last elem for game</div>-->
</div>
<div id="activeCam" class="activeCam">
<div id="div-myCamVideo" class="video-container">
<video id="myCamVideo" autoplay muted></video>
</div>
<div class="btn-cam-action">
<div class="btn-micro">
<img id="microphone" src="resources/logos/microphone.svg">
<img id="microphone-close" src="resources/logos/microphone-close.svg">
</div>
<div class="btn-video">
<img id="cinema" src="resources/logos/cinema.svg">
<img id="cinema-close" src="resources/logos/cinema-close.svg">
</div>
</div>
</div>
</div>
</div>
<div id="cowebsite" class="cowebsite"></div>
</div>
<!--
<div id="webRtc" class="webrtc"> <div id="webRtc" class="webrtc">
<div id="activeCam" class="activeCam"> <div id="activeCam" class="activeCam">
<div id="div-myCamVideo" class="video-container"> <div id="div-myCamVideo" class="video-container">
@ -54,11 +100,9 @@
<img id="cinema" src="resources/logos/cinema.svg"> <img id="cinema" src="resources/logos/cinema.svg">
<img id="cinema-close" src="resources/logos/cinema-close.svg"> <img id="cinema-close" src="resources/logos/cinema-close.svg">
</div> </div>
<!--<div class="btn-call">
<img src="resources/logos/phone.svg">
</div>-->
</div> </div>
</div> </div>
-->
<div id="webRtcSetup" class="webrtcsetup"> <div id="webRtcSetup" class="webrtcsetup">
<img id="webRtcSetupNoVideo" class="background-img" src="resources/logos/cinema-close.svg"> <img id="webRtcSetupNoVideo" class="background-img" src="resources/logos/cinema-close.svg">
<video id="myCamVideoSetup" autoplay muted></video> <video id="myCamVideoSetup" autoplay muted></video>

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

View File

@ -23,34 +23,12 @@ body .message-info.info{
body .message-info.warning{ body .message-info.warning{
background: #ffa500d6; background: #ffa500d6;
} }
video{ .video-container{
-webkit-transform: scaleX(-1); position: relative;
transform: scaleX(-1);
}
.webrtc{
display: none;
position: absolute;
right: 0px;
height: 100%;
width: 300px;
}
.webrtc.active{
display: block;
}
.webrtc, .activeCam{}
.activeCam .video-container{
position: absolute;
height: 25%;
top: 10px;
margin: 5px;
right: -100px;
transition: all 0.2s ease; transition: all 0.2s ease;
border-color: black; background-color: #00000099;
border-style: solid;
border-width: 0.2px;
} }
.activeCam .video-container i{ .video-container i{
position: absolute; position: absolute;
width: 100px; width: 100px;
height: 65px; height: 65px;
@ -63,10 +41,10 @@ video{
font-size: 28px; font-size: 28px;
color: white; color: white;
} }
.activeCam .video-container img.active{ .video-container img.active{
display: block; display: block;
} }
.activeCam .video-container img{ .video-container img{
position: absolute; position: absolute;
display: none; display: none;
width: 15px; width: 15px;
@ -78,36 +56,29 @@ video{
padding: 10px; padding: 10px;
z-index: 2; z-index: 2;
} }
.activeCam .video-container video{ .video-container video{
height: 100%; height: 100%;
} }
.webrtc:hover .activeCam .video-container{ .video-container#div-myCamVideo{
right: 10px;
}
.activeCam .video-container#div-myCamVideo{
border: none; border: none;
} }
.activeCam .video-container video#myCamVideo{
width: 200px; #div-myCamVideo {
height: 113px; position: absolute;
right: 0;
bottom: 0;
} }
/*CSS size for 2 - 3 elements*/ video#myCamVideo{
.activeCam .video-container:nth-child(1){ width: 15vw;
/*this is for camera of user*/ -webkit-transform: scaleX(-1);
top: 75%; transform: scaleX(-1);
} /*width: 200px;*/
.activeCam .video-container:nth-child(2){ /*height: 113px;*/
top: 0%;
}
.activeCam .video-container:nth-child(3){
top: 25%;
}
.activeCam .video-container:nth-child(4) {
top: 50%;
} }
/*btn animation*/ /*btn animation*/
.btn-cam-action div{ .btn-cam-action div{
cursor: pointer; cursor: pointer;
@ -118,11 +89,11 @@ video{
background: #666; background: #666;
box-shadow: 2px 2px 24px #444; box-shadow: 2px 2px 24px #444;
border-radius: 48px; border-radius: 48px;
transform: translateY(12vw); transform: translateY(12vh);
transition-timing-function: ease-in-out; transition-timing-function: ease-in-out;
bottom: 20px; bottom: 20px;
} }
.webrtc:hover .btn-cam-action div{ #activeCam:hover .btn-cam-action div{
transform: translateY(0); transform: translateY(0);
} }
.btn-cam-action div:hover{ .btn-cam-action div:hover{
@ -237,3 +208,156 @@ video{
.webrtcsetup.active{ .webrtcsetup.active{
display: block; 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;
}
.sidebar > div {
max-height: 21%;
}
}
@media (max-aspect-ratio: 1/1) {
.main-container {
flex-direction: column;
}
.game-overlay {
flex-direction: column;
}
.sidebar {
flex-direction: row;
align-items: flex-end;
}
.sidebar > div {
max-width: 21%;
}
}
.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 {
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%;
flex-wrap: wrap;
align-items: flex-start;
padding: 1%;
}
.chat-mode div {
margin: 1%;
max-height: 96%;
}
.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;
}

View File

@ -1,34 +1,33 @@
import {GameManager, gameManager, HasMovedEvent} from "./GameManager"; import {GameManager, gameManager, HasMovedEvent} from "./GameManager";
import { import {
Connection, Connection,
GroupCreatedUpdatedMessageInterface, MessageUserJoined, GroupCreatedUpdatedMessageInterface,
MessageUserJoined,
MessageUserMovedInterface, MessageUserMovedInterface,
MessageUserPositionInterface, PointInterface, PositionInterface MessageUserPositionInterface,
PointInterface,
PositionInterface
} from "../../Connection"; } from "../../Connection";
import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player"; import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player";
import { DEBUG_MODE, ZOOM_LEVEL, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; import {DEBUG_MODE, POSITION_DELAY, ZOOM_LEVEL} from "../../Enum/EnvironmentVariable";
import { import {ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledTileSet} from "../Map/ITiledMap";
ITiledMap,
ITiledMapLayer,
ITiledMapLayerProperty,
ITiledTileSet
} from "../Map/ITiledMap";
import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; 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 {AddPlayerInterface} from "./AddPlayerInterface";
import {PlayerAnimationNames} from "../Player/Animation"; import {PlayerAnimationNames} from "../Player/Animation";
import {PlayerMovement} from "./PlayerMovement"; import {PlayerMovement} from "./PlayerMovement";
import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator";
import {RemotePlayer} from "../Entity/RemotePlayer"; 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 {SimplePeer, UserSimplePeer} from "../../WebRtc/SimplePeer";
import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene";
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import {FourOFourSceneName} from "../Reconnecting/FourOFourScene"; 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 { export enum Textures {
@ -107,6 +106,9 @@ export class GameScene extends Phaser.Scene {
private PositionNextScene: Array<Array<{ key: string, hash: string }>> = new Array<Array<{ key: string, hash: string }>>(); private PositionNextScene: Array<Array<{ key: string, hash: string }>> = new Array<Array<{ key: string, hash: string }>>();
private startLayerName: string|undefined; 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 { static createFromUrl(mapUrlFile: string, instance: string, key: string|null = null): GameScene {
const mapKey = GameScene.getMapKeyByUrl(mapUrlFile); const mapKey = GameScene.getMapKeyByUrl(mapUrlFile);
@ -159,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); loadAllLayers(this.load);
this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
@ -214,10 +222,24 @@ export class GameScene extends Phaser.Scene {
this.scene.stop(this.scene.key); this.scene.stop(this.scene.key);
this.scene.remove(this.scene.key); this.scene.remove(this.scene.key);
window.removeEventListener('resize', this.repositionCallback);
}) })
// When connection is performed, let's connect SimplePeer // When connection is performed, let's connect SimplePeer
this.simplePeer = new SimplePeer(this.connection); 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.wake();
this.scene.sleep(ReconnectingSceneName); this.scene.sleep(ReconnectingSceneName);
@ -364,6 +386,41 @@ export class GameScene extends Phaser.Scene {
} }
}, 500); }, 500);
} }
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 UserInputManager class for input
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 { private getExitSceneUrl(layer: ITiledMapLayer): string|undefined {
@ -625,6 +682,7 @@ export class GameScene extends Phaser.Scene {
this.simplePeer.unregister(); this.simplePeer.unregister();
this.scene.stop(); this.scene.stop();
this.scene.remove(this.scene.key); this.scene.remove(this.scene.key);
window.removeEventListener('resize', this.repositionCallback);
this.scene.start(nextSceneKey.key, { this.scene.start(nextSceneKey.key, {
startLayerName: nextSceneKey.hash startLayerName: nextSceneKey.hash
}); });
@ -812,4 +870,9 @@ export class GameScene extends Phaser.Scene {
const endPos = mapUrlStart.indexOf(".json"); const endPos = mapUrlStart.indexOf(".json");
return mapUrlStart.substring(startPos, endPos); return mapUrlStart.substring(startPos, endPos);
} }
private reposition(): void {
this.presentationModeSprite.setY(this.game.renderer.height - 2);
this.chatModeSprite.setY(this.game.renderer.height - 2);
}
} }

View File

@ -0,0 +1,56 @@
import {HtmlUtils} from "./HtmlUtils";
export type CoWebsiteStateChangedCallback = () => void;
export class CoWebsiteManager {
private static observers = new Array<CoWebsiteStateChangedCallback>();
public static loadCoWebsite(url: string): void {
const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("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<HTMLDivElement>("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();
}
}
}

View File

@ -0,0 +1,10 @@
export class HtmlUtils {
public static getElementByIdOrFail<T extends HTMLElement>(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;
}
}

View File

@ -0,0 +1,135 @@
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.
*/
class LayoutManager {
private mode: LayoutMode = LayoutMode.Presentation;
private importantDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
private normalDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
public add(importance: DivImportance, userId: string, html: string): void {
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);
// 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);
this.adjustVideoChatClass();
}
private positionDiv(elem: HTMLDivElement, importance: DivImportance): void {
if (this.mode === LayoutMode.VideoChat) {
const chatModeDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode');
chatModeDiv.appendChild(elem);
} else {
if (importance === DivImportance.Important) {
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section');
mainSectionDiv.appendChild(elem);
} else if (importance === DivImportance.Normal) {
const sideBarDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar');
sideBarDiv.appendChild(elem);
}
}
}
/**
* 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();
this.importantDivs.delete(userId);
this.adjustVideoChatClass();
return;
}
div = this.normalDivs.get(userId);
if (div !== undefined) {
div.remove();
this.normalDivs.delete(userId);
this.adjustVideoChatClass();
return;
}
console.log('Cannot remove userID '+userId+'. Already removed?');
//throw new Error('Could not find user ID "'+userId+'"');
}
private adjustVideoChatClass(): void {
const chatModeDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('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');
}
}
public switchLayoutMode(layoutMode: LayoutMode) {
this.mode = layoutMode;
if (layoutMode === LayoutMode.Presentation) {
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').style.display = 'flex';
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section').style.display = 'flex';
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode').style.display = 'none';
} else {
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').style.display = 'none';
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section').style.display = 'none';
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode').style.display = 'flex';
}
for (const div of this.importantDivs.values()) {
this.positionDiv(div, DivImportance.Important);
}
for (const div of this.normalDivs.values()) {
this.positionDiv(div, DivImportance.Normal);
}
}
public getLayoutMode(): LayoutMode {
return this.mode;
}
}
const layoutManager = new LayoutManager();
export { layoutManager };

View File

@ -1,3 +1,5 @@
import {DivImportance, layoutManager} from "./LayoutManager";
const videoConstraint: boolean|MediaTrackConstraints = { const videoConstraint: boolean|MediaTrackConstraints = {
width: { ideal: 1280 }, width: { ideal: 1280 },
height: { ideal: 720 }, height: { ideal: 720 },
@ -73,8 +75,8 @@ export class MediaManager {
} }
activeVisio(){ activeVisio(){
const webRtc = this.getElementByIdOrFail('webRtc'); const gameOverlay = this.getElementByIdOrFail('game-overlay');
webRtc.classList.add('active'); gameOverlay.classList.add('active');
} }
enabledCamera() { enabledCamera() {
@ -184,18 +186,22 @@ export class MediaManager {
*/ */
addActiveVideo(userId : string, userName: string = ""){ addActiveVideo(userId : string, userName: string = ""){
this.webrtcInAudio.play(); this.webrtcInAudio.play();
const elementRemoteVideo = this.getElementByIdOrFail("activeCam");
userName = userName.toUpperCase(); userName = userName.toUpperCase();
const color = this.getColorByString(userName); const color = this.getColorByString(userName);
elementRemoteVideo.insertAdjacentHTML('beforeend', `
<div id="div-${userId}" class="video-container" style="border-color: ${color};"> const html = `
<div id="div-${userId}" class="video-container">
<div class="connecting-spinner"></div> <div class="connecting-spinner"></div>
<div class="rtc-error" style="display: none"></div> <div class="rtc-error" style="display: none"></div>
<i style="background-color: ${color};">${userName}</i> <i id="name-${userId}" style="background-color: ${color};">${userName}</i>
<img id="microphone-${userId}" src="resources/logos/microphone-close.svg"> <img id="microphone-${userId}" src="resources/logos/microphone-close.svg">
<video id="${userId}" autoplay></video> <video id="${userId}" autoplay></video>
</div> </div>
`); `;
layoutManager.add(DivImportance.Normal, userId, html);
this.remoteVideo.set(userId, this.getElementByIdOrFail<HTMLVideoElement>(userId)); this.remoteVideo.set(userId, this.getElementByIdOrFail<HTMLVideoElement>(userId));
} }
@ -232,11 +238,10 @@ export class MediaManager {
if (element) { if (element) {
element.style.opacity = "0"; element.style.opacity = "0";
} }
element = document.getElementById(`div-${userId}`); element = document.getElementById(`name-${userId}`);
if (!element) { if (element) {
return; element.style.display = "block";
} }
element.style.borderStyle = "solid";
} }
/** /**
@ -248,11 +253,10 @@ export class MediaManager {
if(element){ if(element){
element.style.opacity = "1"; element.style.opacity = "1";
} }
element = document.getElementById(`div-${userId}`); element = document.getElementById(`name-${userId}`);
if(!element){ if(element){
return; element.style.display = "none";
} }
element.style.borderStyle = "none";
} }
/** /**
@ -274,11 +278,7 @@ export class MediaManager {
* @param userId * @param userId
*/ */
removeActiveVideo(userId : string){ removeActiveVideo(userId : string){
const element = document.getElementById(`div-${userId}`); layoutManager.remove(userId);
if(!element){
return;
}
element.remove();
this.remoteVideo.delete(userId); this.remoteVideo.delete(userId);
} }

View File

@ -14,6 +14,12 @@ export interface UserSimplePeer{
initiator?: boolean; 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. * This class manages connections to all the peers in the same group as me.
*/ */
@ -24,6 +30,7 @@ export class SimplePeer {
private PeerConnectionArray: Map<string, SimplePeerNamespace.Instance> = new Map<string, SimplePeerNamespace.Instance>(); private PeerConnectionArray: Map<string, SimplePeerNamespace.Instance> = new Map<string, SimplePeerNamespace.Instance>();
private readonly updateLocalStreamCallback: (media: MediaStream) => void; private readonly updateLocalStreamCallback: (media: MediaStream) => void;
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") { constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") {
this.Connection = Connection; this.Connection = Connection;
@ -34,6 +41,14 @@ export class SimplePeer {
this.initialise(); 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 * permit to listen when user could start visio
*/ */
@ -182,6 +197,10 @@ export class SimplePeer {
}); });
this.addMedia(user.userId); this.addMedia(user.userId);
for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onConnect(user);
}
} }
/** /**
@ -203,6 +222,9 @@ export class SimplePeer {
peer.destroy(); peer.destroy();
this.PeerConnectionArray.delete(userId) this.PeerConnectionArray.delete(userId)
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size); //console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onDisconnect(userId);
}
} catch (err) { } catch (err) {
console.error("closeConnection", err) console.error("closeConnection", err)
} }

View File

@ -4,16 +4,21 @@ import {DEBUG_MODE, RESOLUTION} from "./Enum/EnvironmentVariable";
import {cypressAsserter} from "./Cypress/CypressAsserter"; import {cypressAsserter} from "./Cypress/CypressAsserter";
import {LoginScene} from "./Phaser/Login/LoginScene"; import {LoginScene} from "./Phaser/Login/LoginScene";
import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene"; import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene";
import {gameManager} from "./Phaser/Game/GameManager";
import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene"; import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene";
import {EnableCameraScene} from "./Phaser/Login/EnableCameraScene"; import {EnableCameraScene} from "./Phaser/Login/EnableCameraScene";
import {FourOFourScene} from "./Phaser/Reconnecting/FourOFourScene"; import {FourOFourScene} from "./Phaser/Reconnecting/FourOFourScene";
import {CustomizeScene} from "./Phaser/Login/CustomizeScene"; 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 = { const config: GameConfig = {
title: "Office game", title: "WorkAdventure",
width: window.innerWidth / RESOLUTION, width: width / RESOLUTION,
height: window.innerHeight / RESOLUTION, height: height / RESOLUTION,
parent: "game", parent: "game",
scene: [LoginScene, SelectCharacterScene, EnableCameraScene, ReconnectingScene, FourOFourScene, CustomizeScene], scene: [LoginScene, SelectCharacterScene, EnableCameraScene, ReconnectingScene, FourOFourScene, CustomizeScene],
zoom: RESOLUTION, zoom: RESOLUTION,
@ -30,5 +35,12 @@ cypressAsserter.gameStarted();
const game = new Phaser.Game(config); const game = new Phaser.Game(config);
window.addEventListener('resize', function (event) { 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);
}); });