diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index e77fb133..790dbaaa 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -20,7 +20,7 @@ jobs: # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@master + - uses: rlespinasse/github-slug-action@1.1.1 - name: "Build and push front image" uses: docker/build-push-action@v1 diff --git a/back/package.json b/back/package.json index d989fb1e..bb34e186 100644 --- a/back/package.json +++ b/back/package.json @@ -49,7 +49,7 @@ "multer": "^1.4.2", "prom-client": "^12.0.0", "query-string": "^6.13.3", - "systeminformation": "^4.26.5", + "systeminformation": "^4.27.11", "ts-node-dev": "^1.0.0-pre.44", "typescript": "^3.8.3", "uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0", diff --git a/back/src/Controller/PrometheusController.ts b/back/src/Controller/PrometheusController.ts index 05570466..e854cf43 100644 --- a/back/src/Controller/PrometheusController.ts +++ b/back/src/Controller/PrometheusController.ts @@ -1,5 +1,4 @@ import {App} from "../Server/sifrr.server"; -import {IoSocketController} from "_Controller/IoSocketController"; import {HttpRequest, HttpResponse} from "uWebSockets.js"; const register = require('prom-client').register; const collectDefaultMetrics = require('prom-client').collectDefaultMetrics; diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 5b42f418..eaad701a 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -152,7 +152,7 @@ export class GameRoom { closestItem.join(user); } else { const closestUser : User = closestItem; - const group: Group = new Group([ + const group: Group = new Group(this.roomId,[ user, closestUser ], this.connectCallback, this.disconnectCallback, this.positionNotifier); @@ -200,7 +200,6 @@ export class GameRoom { if (group === undefined) { throw new Error("The user is part of no group"); } - const oldPosition = group.getPosition(); group.leave(user); if (group.isEmpty()) { this.positionNotifier.leave(group); @@ -209,6 +208,7 @@ export class GameRoom { throw new Error("Could not find group "+group.getId()+" referenced by user "+user.id+" in World."); } this.groups.delete(group); + //todo: is the group garbage collected? } else { group.updatePosition(); //this.positionNotifier.updatePosition(group, group.getPosition(), oldPosition); diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index d3b042a6..f26a0e0d 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -3,6 +3,7 @@ import { User } from "./User"; import {PositionInterface} from "_Model/PositionInterface"; import {Movable} from "_Model/Movable"; import {PositionNotifier} from "_Model/PositionNotifier"; +import {gaugeManager} from "../Services/GaugeManager"; export class Group implements Movable { static readonly MAX_PER_GROUP = 4; @@ -13,12 +14,23 @@ export class Group implements Movable { private users: Set; private x!: number; private y!: number; + private hasEditedGauge: boolean = false; + private wasDestroyed: boolean = false; + private roomId: string; - constructor(users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) { + constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) { + this.roomId = roomId; this.users = new Set(); this.id = Group.nextId; Group.nextId++; + //we only send a event for prometheus metrics if the group lives more than 5 seconds + setTimeout(() => { + if (!this.wasDestroyed) { + this.hasEditedGauge = true; + gaugeManager.incNbGroupsPerRoomGauge(roomId); + } + }, 5000); users.forEach((user: User) => { this.join(user); @@ -113,9 +125,11 @@ export class Group implements Movable { */ destroy(): void { + if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId); for (const user of this.users) { this.leave(user); } + this.wasDestroyed = true; } get getSize(){ diff --git a/back/src/Services/GaugeManager.ts b/back/src/Services/GaugeManager.ts new file mode 100644 index 00000000..f8af822b --- /dev/null +++ b/back/src/Services/GaugeManager.ts @@ -0,0 +1,54 @@ +import {Counter, Gauge} from "prom-client"; + +//this class should manage all the custom metrics used by prometheus +class GaugeManager { + private nbClientsGauge: Gauge; + private nbClientsPerRoomGauge: Gauge; + private nbGroupsPerRoomGauge: Gauge; + private nbGroupsPerRoomCounter: Counter; + + constructor() { + this.nbClientsGauge = new Gauge({ + name: 'workadventure_nb_sockets', + help: 'Number of connected sockets', + labelNames: [ ] + }); + this.nbClientsPerRoomGauge = new Gauge({ + name: 'workadventure_nb_clients_per_room', + help: 'Number of clients per room', + labelNames: [ 'room' ] + }); + + this.nbGroupsPerRoomCounter = new Counter({ + name: 'workadventure_counter_groups_per_room', + help: 'Counter of groups per room', + labelNames: [ 'room' ] + }); + this.nbGroupsPerRoomGauge = new Gauge({ + name: 'workadventure_nb_groups_per_room', + help: 'Number of groups per room', + labelNames: [ 'room' ] + }); + } + + incNbClientPerRoomGauge(roomId: string): void { + this.nbClientsGauge.inc(); + this.nbClientsPerRoomGauge.inc({ room: roomId }); + } + + decNbClientPerRoomGauge(roomId: string): void { + this.nbClientsGauge.dec(); + this.nbClientsPerRoomGauge.dec({ room: roomId }); + } + + incNbGroupsPerRoomGauge(roomId: string): void { + this.nbGroupsPerRoomCounter.inc({ room: roomId }) + this.nbGroupsPerRoomGauge.inc({ room: roomId }) + } + + decNbGroupsPerRoomGauge(roomId: string): void { + this.nbGroupsPerRoomGauge.dec({ room: roomId }) + } +} + +export const gaugeManager = new GaugeManager(); \ No newline at end of file diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 35f97c37..4bd26778 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -23,7 +23,6 @@ import { WebRtcStartMessage, QueryJitsiJwtMessage, SendJitsiJwtMessage, - CharacterLayerMessage, SendUserMessage } from "../Messages/generated/messages_pb"; import {PointInterface} from "../Model/Websocket/PointInterface"; @@ -37,11 +36,11 @@ import {Movable} from "../Model/Movable"; import {PositionInterface} from "../Model/PositionInterface"; import {adminApi, CharacterTexture} from "./AdminApi"; import Direction = PositionMessage.Direction; -import {Gauge} from "prom-client"; import {emitError, emitInBatch} from "./IoSocketHelpers"; import Jwt from "jsonwebtoken"; import {JITSI_URL} from "../Enum/EnvironmentVariable"; import {clientEventsEmitter} from "./ClientEventsEmitter"; +import {gaugeManager} from "./GaugeManager"; interface AdminSocketRoomsList { [index: string]: number; @@ -58,30 +57,13 @@ export interface AdminSocketData { export class SocketManager { private Worlds: Map = new Map(); private sockets: Map = new Map(); - private nbClientsGauge: Gauge; - private nbClientsPerRoomGauge: Gauge; - + constructor() { - this.nbClientsGauge = new Gauge({ - name: 'workadventure_nb_sockets', - help: 'Number of connected sockets', - labelNames: [ ] + clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => { + gaugeManager.incNbClientPerRoomGauge(roomId); }); - this.nbClientsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_clients_per_room', - help: 'Number of clients per room', - labelNames: [ 'room' ] - }); - - clientEventsEmitter.registerToClientJoin((clientUUid, roomId) => { - this.nbClientsGauge.inc(); - // Let's log server load when a user joins - console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)'); - }); - clientEventsEmitter.registerToClientLeave((clientUUid, roomId) => { - this.nbClientsGauge.dec(); - // Let's log server load when a user leaves - console.log('A user left (', this.sockets.size, ' connected users)'); + clientEventsEmitter.registerToClientLeave((clientUUid: string, roomId: string) => { + gaugeManager.decNbClientPerRoomGauge(roomId); }); } @@ -107,7 +89,6 @@ export class SocketManager { const viewport = client.viewport; try { this.sockets.set(client.userId, client); //todo: should this be at the end of the function? - clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId); //join new previous room const gameRoom = this.joinRoom(client, position); @@ -377,8 +358,8 @@ export class SocketManager { } finally { //delete Client.roomId; this.sockets.delete(Client.userId); - this.nbClientsPerRoomGauge.dec({ room: Client.roomId }); clientEventsEmitter.emitClientLeave(Client.userUuid, Client.roomId); + console.log('A user left (', this.sockets.size, ' connected users)'); } } } @@ -410,8 +391,6 @@ export class SocketManager { private joinRoom(client : ExSocketInterface, position: PointInterface): GameRoom { const roomId = client.roomId; - //join user in room - this.nbClientsPerRoomGauge.inc({ room: roomId }); client.position = position; const world = this.Worlds.get(roomId) @@ -425,6 +404,8 @@ export class SocketManager { }); //join world world.join(client, client.position); + clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId); + console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)'); return world; } diff --git a/back/yarn.lock b/back/yarn.lock index fea8516e..1bd7c802 100644 --- a/back/yarn.lock +++ b/back/yarn.lock @@ -2345,10 +2345,10 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -systeminformation@^4.26.5: - version "4.27.5" - resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.27.5.tgz#af304fbfd0e7ba51c87512333691b58b4ad90e43" - integrity sha512-EysogxKqREk54ZYDEFcsCODv8GymKZcyiSfegYit8dKhPjzuQr+KX4GFHjssWjYrWFEIM2bYNsFrZX5eufeAXg== +systeminformation@^4.27.11: + version "4.27.11" + resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.27.11.tgz#6dbe96e48091444f80dab6c05ee1901286826b60" + integrity sha512-U7bigXbOnsB8k1vNHS0Y13RCsRz5/UohiUmND+3mMUL6vfzrpbe/h4ZqewowB+B+tJNnmGFDj08Z8xGfYo45dQ== table@^5.2.3: version "5.4.6" diff --git a/front/dist/index.html b/front/dist/index.html index 8e957965..0e696622 100644 --- a/front/dist/index.html +++ b/front/dist/index.html @@ -42,30 +42,14 @@
-
-
+
+
-
- -
@@ -88,7 +72,7 @@
-
+
diff --git a/front/dist/resources/logos/send-bkack.svg b/front/dist/resources/logos/send-bkack.svg new file mode 100644 index 00000000..b4e44d9a --- /dev/null +++ b/front/dist/resources/logos/send-bkack.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/front/dist/resources/logos/send-white.svg b/front/dist/resources/logos/send-white.svg new file mode 100644 index 00000000..8c45e8fe --- /dev/null +++ b/front/dist/resources/logos/send-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/dist/resources/logos/send-yellow.svg b/front/dist/resources/logos/send-yellow.svg new file mode 100644 index 00000000..8fb4f2ec --- /dev/null +++ b/front/dist/resources/logos/send-yellow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/dist/resources/logos/setting-black.svg b/front/dist/resources/logos/setting-black.svg new file mode 100644 index 00000000..a3098e19 --- /dev/null +++ b/front/dist/resources/logos/setting-black.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/front/dist/resources/logos/setting-white.svg b/front/dist/resources/logos/setting-white.svg new file mode 100644 index 00000000..64ffba6d --- /dev/null +++ b/front/dist/resources/logos/setting-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/dist/resources/logos/setting-yellow.svg b/front/dist/resources/logos/setting-yellow.svg new file mode 100644 index 00000000..1fd29378 --- /dev/null +++ b/front/dist/resources/logos/setting-yellow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index 23a4bba6..edd3ddf0 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -242,15 +242,10 @@ body { .main-container { height: 100vh; width: 100vw; - display: flex; - align-items: stretch; + position: absolute; } @media (min-aspect-ratio: 1/1) { - .main-container { - flex-direction: row; - } - .game-overlay { flex-direction: row; } @@ -266,12 +261,21 @@ body { .sidebar > div:hover { max-height: 25%; } + + #cowebsite { + right: 0; + top: 0; + width: 50%; + height: 100vh; + } + #cowebsite.loading { + transform: translateX(90%); + } + #cowebsite.hidden { + transform: translateX(100%); + } } @media (max-aspect-ratio: 1/1) { - .main-container { - flex-direction: column; - } - .game-overlay { flex-direction: column; } @@ -288,24 +292,36 @@ body { .sidebar > div:hover { max-width: 25%; } + + #cowebsite { + left: 0; + bottom: 0; + width: 100%; + height: 50%; + } + #cowebsite.loading { + transform: translateY(90%); + } + #cowebsite.hidden { + transform: translateY(100%); + } } -.game { - flex-basis: 100%; +#game { + width: 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 { + position: fixed; + transition: transform 0.5s; +} +#cowebsite.loading { + background-color: gray; } -/*.cowebsite:hover { - flex-basis: 100%; -}*/ - -.cowebsite > iframe { +#cowebsite > iframe { width: 100%; height: 100%; } @@ -445,23 +461,51 @@ body { color: white; z-index: 200; transition: all 0.1s ease-out; - top: 100%; - width: 100px; + top: calc(100% + 2px); + width: 200px; height: 40px; - background-color: black; - left: calc(50% - 50px); - border-radius: 0 0 10px 10px; + background-color: #2d2d2dba; + left: calc(50% - 100px); + border-radius: 15px 15px 15px 15px; text-align: center; } +.message-container div.clear{ + width: 100px; + left: calc(50% - 50px); +} + +.main-console div.console img, +.message-container div.clear img{ + margin-top: 6px; + width: 30px; + height: 30px; + cursor: pointer; + padding: 0 5px; + transition: all .5s ease; + transform: rotateY(0); + opacity: 0.5; +} +.main-console div.console img:hover, +.message-container div.clear img:hover{ + opacity: 1; +} + +.main-console div.console img.active, +.message-container div.clear img{ + transform: rotateY(3.142rad); + opacity: 1; +} + .main-console div.console p, .message-container div.clear p{ - margin-top: 6px; + margin-top: 12px; } .main-console div.console:hover, .message-container div.clear:hover { cursor: pointer; + top: calc(100% + 5px); transform: scale(1.2) translateY(3px); } diff --git a/front/src/Administration/ConsoleGlobalMessageManager.ts b/front/src/Administration/ConsoleGlobalMessageManager.ts index 8258d0ed..a79ecf75 100644 --- a/front/src/Administration/ConsoleGlobalMessageManager.ts +++ b/front/src/Administration/ConsoleGlobalMessageManager.ts @@ -2,6 +2,7 @@ import {HtmlUtils} from "../WebRtc/HtmlUtils"; import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {RoomConnection} from "../Connexion/RoomConnection"; import {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels"; +import {ADMIN_URL} from "../Enum/EnvironmentVariable"; export const CLASS_CONSOLE_MESSAGE = 'main-console'; export const INPUT_CONSOLE_MESSAGE = 'input-send-text'; @@ -17,8 +18,10 @@ interface EventTargetFiles extends EventTarget { export class ConsoleGlobalMessageManager { - private divMainConsole: HTMLDivElement; - private buttonMainConsole: HTMLDivElement; + private readonly divMainConsole: HTMLDivElement; + private readonly buttonMainConsole: HTMLDivElement; + private readonly buttonSendMainConsole: HTMLImageElement; + private readonly buttonSettingsMainConsole: HTMLImageElement; private activeConsole: boolean = false; private userInputManager!: UserInputManager; private static cssLoaded: boolean = false; @@ -27,6 +30,8 @@ export class ConsoleGlobalMessageManager { this.buttonMainConsole = document.createElement('div'); this.buttonMainConsole.classList.add('console'); this.divMainConsole = document.createElement('div'); + this.buttonSendMainConsole = document.createElement('img'); + this.buttonSettingsMainConsole = document.createElement('img'); this.userInputManager = userInputManager; this.initialise(); } @@ -75,17 +80,26 @@ export class ConsoleGlobalMessageManager { menu.appendChild(textAudio); this.divMainConsole.appendChild(menu); - const buttonText = document.createElement('p'); - buttonText.innerText = 'Console'; - - this.buttonMainConsole.appendChild(buttonText); - this.buttonMainConsole.addEventListener('click', () => { + this.buttonSendMainConsole.src = 'resources/logos/send-yellow.svg'; + this.buttonSendMainConsole.addEventListener('click', () => { if(this.activeConsole){ this.disabled(); }else{ + this.buttonSendMainConsole.classList.add('active'); this.active(); } }); + this.buttonMainConsole.appendChild(this.buttonSendMainConsole); + + this.buttonSettingsMainConsole.src = 'resources/logos/setting-yellow.svg'; + this.buttonSettingsMainConsole.addEventListener('click', () => { + window.open(ADMIN_URL, '_blank'); + }); + this.buttonMainConsole.appendChild(this.buttonSettingsMainConsole); + + /*const buttonText = document.createElement('p'); + buttonText.innerText = 'Console'; + this.buttonMainConsole.appendChild(buttonText);*/ this.divMainConsole.className = CLASS_CONSOLE_MESSAGE; this.divMainConsole.appendChild(this.buttonMainConsole); @@ -293,17 +307,18 @@ export class ConsoleGlobalMessageManager { this.Connection.emitGlobalMessage(GlobalMessage); } - active(){ this.userInputManager.clearAllInputKeyboard(); this.activeConsole = true; this.divMainConsole.style.top = '0'; + this.buttonSendMainConsole.classList.add('active'); } disabled(){ this.userInputManager.initKeyBoardEvent(); this.activeConsole = false; this.divMainConsole.style.top = '-80%'; + this.buttonSendMainConsole.classList.remove('active'); } private getSectionId(id: string) : string { diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 2e963e5e..60f9cd3b 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -1,5 +1,6 @@ const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true"; const API_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.API_URL || "api.workadventure.localhost"); +const ADMIN_URL = API_URL.replace('api', 'admin'); const TURN_SERVER: string = process.env.TURN_SERVER || "turn:numb.viagenie.ca"; const TURN_USER: string = process.env.TURN_USER || 'g.parant@thecodingmachine.com'; const TURN_PASSWORD: string = process.env.TURN_PASSWORD || 'itcugcOHxle9Acqi$'; @@ -13,6 +14,7 @@ const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new export { DEBUG_MODE, API_URL, + ADMIN_URL, RESOLUTION, ZOOM_LEVEL, POSITION_DELAY, diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 8119d268..96648255 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -12,7 +12,6 @@ import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player import { DEBUG_MODE, JITSI_PRIVATE_MODE, - JITSI_URL, POSITION_DELAY, RESOLUTION, ZOOM_LEVEL @@ -23,7 +22,6 @@ import { ITiledMapLayerProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap"; -import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; import {AddPlayerInterface} from "./AddPlayerInterface"; import {PlayerAnimationNames} from "../Player/Animation"; import {PlayerMovement} from "./PlayerMovement"; @@ -32,7 +30,7 @@ import {RemotePlayer} from "../Entity/RemotePlayer"; import {Queue} from 'queue-typescript'; import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer"; import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; -import {loadAllLayers, loadCustomTexture, loadObject, loadPlayerCharacters} from "../Entity/body_character"; +import {loadAllLayers, loadObject, loadPlayerCharacters} from "../Entity/body_character"; import {CenterListener, layoutManager, LayoutMode} from "../../WebRtc/LayoutManager"; import Texture = Phaser.Textures.Texture; import Sprite = Phaser.GameObjects.Sprite; @@ -40,7 +38,7 @@ import CanvasTexture = Phaser.Textures.CanvasTexture; import GameObject = Phaser.GameObjects.GameObject; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import {GameMap} from "./GameMap"; -import {CoWebsiteManager} from "../../WebRtc/CoWebsiteManager"; +import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; import {mediaManager} from "../../WebRtc/MediaManager"; import {FourOFourSceneName} from "../Reconnecting/FourOFourScene"; import {ItemFactoryInterface} from "../Items/ItemFactoryInterface"; @@ -55,11 +53,7 @@ import {UserMessageManager} from "../../Administration/UserMessageManager"; import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMessageManager"; import {ResizableScene} from "../Login/ResizableScene"; import {Room} from "../../Connexion/Room"; - - -export enum Textures { - Player = "male1" -} +import {jitsiFactory} from "../../WebRtc/JitsiFactory"; export interface GameSceneInitInterface { initPosition: PointInterface|null @@ -147,8 +141,6 @@ export class GameScene extends ResizableScene implements CenterListener { private outlinedItem: ActionableItem|null = null; private userInputManager!: UserInputManager; - private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any - static createFromUrl(room: Room, mapUrlFile: string, gameSceneKey: string|null = null): GameScene { // We use the map URL as a key if (gameSceneKey === null) { @@ -300,13 +292,6 @@ export class GameScene extends ResizableScene implements CenterListener { // }); // }); } - - // TEST: let's load a module dynamically! - /*let foo = "http://maps.workadventure.localhost/computer.js"; - import(/* webpackIgnore: true * / foo).then(result => { - console.log(result); - - });*/ } //hook initialisation @@ -484,9 +469,9 @@ export class GameScene extends ResizableScene implements CenterListener { this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue) => { if (newValue === undefined) { - CoWebsiteManager.closeCoWebsite(); + coWebsiteManager.closeCoWebsite(); } else { - CoWebsiteManager.loadCoWebsite(newValue as string); + coWebsiteManager.loadCoWebsite(newValue as string); } }); this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => { @@ -1231,53 +1216,14 @@ export class GameScene extends ResizableScene implements CenterListener { } public startJitsi(roomName: string, jwt?: string): void { - CoWebsiteManager.insertCoWebsite((cowebsiteDiv => { - const domain = JITSI_URL; - const options = { - roomName: roomName, - jwt: jwt, - width: "100%", - height: "100%", - parentNode: cowebsiteDiv, - configOverwrite: { - prejoinPageEnabled: false - }, - interfaceConfigOverwrite: { - SHOW_CHROME_EXTENSION_BANNER: false, - MOBILE_APP_PROMO: false, - - HIDE_INVITE_MORE_HEADER: true, - - // Note: hiding brand does not seem to work, we probably need to put this on the server side. - SHOW_BRAND_WATERMARK: false, - SHOW_JITSI_WATERMARK: false, - SHOW_POWERED_BY: false, - SHOW_PROMOTIONAL_CLOSE_PAGE: false, - SHOW_WATERMARK_FOR_GUESTS: false, - - TOOLBAR_BUTTONS: [ - 'microphone', 'camera', 'closedcaptions', 'desktop', /*'embedmeeting',*/ 'fullscreen', - 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording', - 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand', - 'videoquality', 'filmstrip', /*'invite',*/ 'feedback', 'stats', 'shortcuts', - 'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', /*'security'*/ - ], - } - }; - if (!options.jwt) { - delete options.jwt; - } - this.jitsiApi = new (window as any).JitsiMeetExternalAPI(domain, options); // eslint-disable-line @typescript-eslint/no-explicit-any - this.jitsiApi.executeCommand('displayName', gameManager.getPlayerName()); - })); + jitsiFactory.start(roomName, gameManager.getPlayerName(), jwt); this.connection.setSilent(true); mediaManager.hideGameOverlay(); } public stopJitsi(): void { this.connection.setSilent(false); - this.jitsiApi?.dispose(); - CoWebsiteManager.closeCoWebsite(); + jitsiFactory.stop(); mediaManager.showGameOverlay(); } diff --git a/front/src/Phaser/Player/Animation.ts b/front/src/Phaser/Player/Animation.ts index 8ca7d671..471f6d5f 100644 --- a/front/src/Phaser/Player/Animation.ts +++ b/front/src/Phaser/Player/Animation.ts @@ -1,4 +1,3 @@ -import {Textures} from "../Game/GameScene"; export enum PlayerAnimationNames { WalkDown = 'down', diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index 1793335b..ab63e60a 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -2,47 +2,90 @@ import {HtmlUtils} from "./HtmlUtils"; export type CoWebsiteStateChangedCallback = () => void; -export class CoWebsiteManager { +enum iframeStates { + closed = 1, + loading, // loading an iframe can be slow, so we show some placeholder until it is ready + opened, +} - private static observers = new Array(); +const cowebsiteDivId = "cowebsite"; // the id of the parent div of the iframe. +const animationTime = 500; //time used by the css transitions, in ms. - public static loadCoWebsite(url: string): void { - const cowebsiteDiv = HtmlUtils.getElementByIdOrFail("cowebsite"); +class CoWebsiteManager { + + private opened: iframeStates = iframeStates.closed; + + private observers = new Array(); + + private close(): HTMLDivElement { + const cowebsiteDiv = HtmlUtils.getElementByIdOrFail(cowebsiteDivId); + cowebsiteDiv.classList.remove('loaded'); //edit the css class to trigger the transition + cowebsiteDiv.classList.add('hidden'); + this.opened = iframeStates.closed; + return cowebsiteDiv; + } + private load(): HTMLDivElement { + const cowebsiteDiv = HtmlUtils.getElementByIdOrFail(cowebsiteDivId); + cowebsiteDiv.classList.remove('hidden'); //edit the css class to trigger the transition + cowebsiteDiv.classList.add('loading'); + this.opened = iframeStates.loading; + return cowebsiteDiv; + } + private open(): HTMLDivElement { + const cowebsiteDiv = HtmlUtils.getElementByIdOrFail(cowebsiteDivId); + cowebsiteDiv.classList.remove('loading', 'hidden'); //edit the css class to trigger the transition + this.opened = iframeStates.opened; + return cowebsiteDiv; + } + + public loadCoWebsite(url: string): void { + const cowebsiteDiv = this.load(); cowebsiteDiv.innerHTML = ''; const iframe = document.createElement('iframe'); iframe.id = 'cowebsite-iframe'; iframe.src = url; + const onloadPromise = new Promise((resolve) => { + iframe.onload = () => resolve(); + }); cowebsiteDiv.appendChild(iframe); - //iframe.onload = () => { - // onload can be long to trigger. Maybe we should display the website, whatever happens, after 1 second? - CoWebsiteManager.fire(); - //} + const onTimeoutPromise = new Promise((resolve) => { + setTimeout(() => resolve(), 2000); + }); + Promise.race([onloadPromise, onTimeoutPromise]).then(() => { + this.open(); + setTimeout(() => { + this.fire(); + }, animationTime) + }); } /** * Just like loadCoWebsite but the div can be filled by the user. */ - public static insertCoWebsite(callback: (cowebsite: HTMLDivElement) => void): void { - const cowebsiteDiv = HtmlUtils.getElementByIdOrFail("cowebsite"); - cowebsiteDiv.innerHTML = ''; - - callback(cowebsiteDiv); - //iframe.onload = () => { - // onload can be long to trigger. Maybe we should display the website, whatever happens, after 1 second? - CoWebsiteManager.fire(); - //} + public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise): void { + const cowebsiteDiv = this.load(); + callback(cowebsiteDiv).then(() => { + this.open(); + setTimeout(() => { + this.fire(); + }, animationTime) + }); } - public static closeCoWebsite(): void { - const cowebsiteDiv = HtmlUtils.getElementByIdOrFail("cowebsite"); - cowebsiteDiv.innerHTML = ''; - CoWebsiteManager.fire(); + public closeCoWebsite(): Promise { + return new Promise((resolve, reject) => { + const cowebsiteDiv = this.close(); + this.fire(); + setTimeout(() => { + resolve(); + setTimeout(() => cowebsiteDiv.innerHTML = '', 500) + }, animationTime) + }); } - public static getGameSize(): {width: number, height: number} { - const hasChildren = HtmlUtils.getElementByIdOrFail("cowebsite").children.length > 0; - if (hasChildren === false) { + public getGameSize(): {width: number, height: number} { + if (this.opened !== iframeStates.opened) { return { width: window.innerWidth, height: window.innerHeight @@ -61,13 +104,15 @@ export class CoWebsiteManager { } } - public static onStateChange(observer: CoWebsiteStateChangedCallback) { - CoWebsiteManager.observers.push(observer); + public onStateChange(observer: CoWebsiteStateChangedCallback) { + this.observers.push(observer); } - private static fire(): void { - for (const callback of CoWebsiteManager.observers) { + private fire(): void { + for (const callback of this.observers) { callback(); } } } + +export const coWebsiteManager = new CoWebsiteManager(); \ No newline at end of file diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts new file mode 100644 index 00000000..45b9b3cf --- /dev/null +++ b/front/src/WebRtc/JitsiFactory.ts @@ -0,0 +1,89 @@ +import {JITSI_URL} from "../Enum/EnvironmentVariable"; +import {mediaManager} from "./MediaManager"; +import {coWebsiteManager} from "./CoWebsiteManager"; +declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any + +const interfaceConfig = { + SHOW_CHROME_EXTENSION_BANNER: false, + MOBILE_APP_PROMO: false, + + HIDE_INVITE_MORE_HEADER: true, + + // Note: hiding brand does not seem to work, we probably need to put this on the server side. + SHOW_BRAND_WATERMARK: false, + SHOW_JITSI_WATERMARK: false, + SHOW_POWERED_BY: false, + SHOW_PROMOTIONAL_CLOSE_PAGE: false, + SHOW_WATERMARK_FOR_GUESTS: false, + + TOOLBAR_BUTTONS: [ + 'microphone', 'camera', 'closedcaptions', 'desktop', /*'embedmeeting',*/ 'fullscreen', + 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording', + 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand', + 'videoquality', 'filmstrip', /*'invite',*/ 'feedback', 'stats', 'shortcuts', + 'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', /*'security'*/ + ], +}; + +class JitsiFactory { + private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any + private audioCallback = this.onAudioChange.bind(this); + private videoCallback = this.onVideoChange.bind(this); + + public start(roomName: string, playerName:string, jwt?: string): void { + coWebsiteManager.insertCoWebsite((cowebsiteDiv => { + const domain = JITSI_URL; + const options: any = { // eslint-disable-line @typescript-eslint/no-explicit-any + roomName: roomName, + jwt: jwt, + width: "100%", + height: "100%", + parentNode: cowebsiteDiv, + configOverwrite: { + startWithAudioMuted: !mediaManager.constraintsMedia.audio, + startWithVideoMuted: mediaManager.constraintsMedia.video === false, + prejoinPageEnabled: false + }, + interfaceConfigOverwrite: interfaceConfig, + }; + if (!options.jwt) { + delete options.jwt; + } + + return new Promise((resolve) => { + options.onload = () => resolve(); //we want for the iframe to be loaded before triggering animations. + this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options); + this.jitsiApi.executeCommand('displayName', playerName); + + this.jitsiApi.addListener('audioMuteStatusChanged', this.audioCallback); + this.jitsiApi.addListener('videoMuteStatusChanged', this.videoCallback); + }); + })); + } + + public async stop(): Promise { + await coWebsiteManager.closeCoWebsite(); + this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback); + this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback); + this.jitsiApi?.dispose(); + } + + private onAudioChange({muted}: {muted: boolean}): void { + if (muted && mediaManager.constraintsMedia.audio === true) { + mediaManager.disableMicrophone(); + } else if(!muted && mediaManager.constraintsMedia.audio === false) { + mediaManager.enableMicrophone(); + } + } + + private onVideoChange({muted}: {muted: boolean}): void { + if (muted && mediaManager.constraintsMedia.video !== false) { + mediaManager.disableCamera(); + } else if(!muted && mediaManager.constraintsMedia.video === false) { + mediaManager.enableCamera(); + } + } + +} + +export const jitsiFactory = new JitsiFactory(); \ No newline at end of file diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 0cbfe123..ab6d9a3b 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -1,5 +1,6 @@ import {DivImportance, layoutManager} from "./LayoutManager"; import {HtmlUtils} from "./HtmlUtils"; +declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any const videoConstraint: boolean|MediaTrackConstraints = { width: { ideal: 1280 }, @@ -134,7 +135,7 @@ export class MediaManager { gameOverlay.classList.remove('active'); } - private enableCamera() { + public enableCamera() { this.cinemaClose.style.display = "none"; this.cinemaBtn.classList.remove("disabled"); this.cinema.style.display = "block"; @@ -144,7 +145,7 @@ export class MediaManager { }); } - private async disableCamera() { + public async disableCamera() { this.cinemaClose.style.display = "block"; this.cinema.style.display = "none"; this.cinemaBtn.classList.add("disabled"); @@ -160,7 +161,7 @@ export class MediaManager { } } - private enableMicrophone() { + public enableMicrophone() { this.microphoneClose.style.display = "none"; this.microphone.style.display = "block"; this.microphoneBtn.classList.remove("disabled"); @@ -171,7 +172,7 @@ export class MediaManager { }); } - private async disableMicrophone() { + public async disableMicrophone() { this.microphoneClose.style.display = "block"; this.microphone.style.display = "none"; this.microphoneBtn.classList.add("disabled"); @@ -245,15 +246,11 @@ export class MediaManager { } private _startScreenCapture() { - // getDisplayMedia was moved to mediaDevices in 2018. Typescript definitions are not up to date yet. - // See: https://github.com/w3c/mediacapture-screen-share/pull/86 - // https://github.com/microsoft/TypeScript/issues/31821 - if ((navigator as any).getDisplayMedia) { // eslint-disable-line @typescript-eslint/no-explicit-any - return (navigator as any).getDisplayMedia({video: true}); // eslint-disable-line @typescript-eslint/no-explicit-any - } else if ((navigator.mediaDevices as any).getDisplayMedia) { // eslint-disable-line @typescript-eslint/no-explicit-any - return (navigator.mediaDevices as any).getDisplayMedia({video: true}); // eslint-disable-line @typescript-eslint/no-explicit-any + if (navigator.getDisplayMedia) { + return navigator.getDisplayMedia({video: true}); + } else if (navigator.mediaDevices.getDisplayMedia) { + return navigator.mediaDevices.getDisplayMedia({video: true}); } else { - //return navigator.mediaDevices.getUserMedia(({video: {mediaSource: 'screen'}} as any)); return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars reject("error sharing screen"); }); @@ -336,12 +333,6 @@ export class MediaManager { return this.getCamera(); } - /** - * - * @param userId - * @param reportCallBack - * @param userName - */ addActiveVideo(userId: string, reportCallBack: ReportCallback|undefined, userName: string = ""){ this.webrtcInAudio.play(); @@ -373,13 +364,8 @@ export class MediaManager { this.remoteVideo.set(userId, this.getElementByIdOrFail(userId)); } - - /** - * - * @param userId - */ + addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){ - //this.webrtcInAudio.play(); userId = `screen-sharing-${userId}`; const html = ` @@ -392,11 +378,7 @@ export class MediaManager { this.remoteVideo.set(userId, this.getElementByIdOrFail(userId)); } - - /** - * - * @param userId - */ + disabledMicrophoneByUserId(userId: number){ const element = document.getElementById(`microphone-${userId}`); if(!element){ @@ -404,11 +386,7 @@ export class MediaManager { } element.classList.add('active') } - - /** - * - * @param userId - */ + enabledMicrophoneByUserId(userId: number){ const element = document.getElementById(`microphone-${userId}`); if(!element){ @@ -416,11 +394,7 @@ export class MediaManager { } element.classList.remove('active') } - - /** - * - * @param userId - */ + disabledVideoByUserId(userId: number) { let element = document.getElementById(`${userId}`); if (element) { @@ -431,11 +405,7 @@ export class MediaManager { element.style.display = "block"; } } - - /** - * - * @param userId - */ + enabledVideoByUserId(userId: number){ let element = document.getElementById(`${userId}`); if(element){ @@ -447,11 +417,6 @@ export class MediaManager { } } - /** - * - * @param userId - * @param stream - */ addStreamRemoteVideo(userId: string, stream : MediaStream){ const remoteVideo = this.remoteVideo.get(userId); if (remoteVideo === undefined) { @@ -468,11 +433,7 @@ export class MediaManager { this.addStreamRemoteVideo(`screen-sharing-${userId}`, stream); } - - /** - * - * @param userId - */ + removeActiveVideo(userId: string){ layoutManager.remove(userId); this.remoteVideo.delete(userId); @@ -522,11 +483,7 @@ export class MediaManager { const connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null; return connnectingSpinnerDiv; } - - /** - * - * @param str - */ + private getColorByString(str: String) : String|null { let hash = 0; if (str.length === 0) return null; diff --git a/front/src/index.ts b/front/src/index.ts index e12d8707..fe7ceb34 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -10,12 +10,9 @@ import {FourOFourScene} from "./Phaser/Reconnecting/FourOFourScene"; import WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer; import {OutlinePipeline} from "./Phaser/Shaders/OutlinePipeline"; import {CustomizeScene} from "./Phaser/Login/CustomizeScene"; -import {CoWebsiteManager} from "./WebRtc/CoWebsiteManager"; -import {gameManager} from "./Phaser/Game/GameManager"; import {ResizableScene} from "./Phaser/Login/ResizableScene"; import {EntryScene} from "./Phaser/Login/EntryScene"; - -//CoWebsiteManager.loadCoWebsite('https://thecodingmachine.com'); +import {coWebsiteManager} from "./WebRtc/CoWebsiteManager"; // Load Jitsi if the environment variable is set. if (JITSI_URL) { @@ -24,7 +21,7 @@ if (JITSI_URL) { document.head.appendChild(jitsiScript); } -const {width, height} = CoWebsiteManager.getGameSize(); +const {width, height} = coWebsiteManager.getGameSize(); const config: GameConfig = { title: "WorkAdventure", @@ -53,8 +50,7 @@ cypressAsserter.gameStarted(); const game = new Phaser.Game(config); window.addEventListener('resize', function (event) { - const {width, height} = CoWebsiteManager.getGameSize(); - + const {width, height} = coWebsiteManager.getGameSize(); game.scale.resize(width / RESOLUTION, height / RESOLUTION); // Let's trigger the onResize method of any active scene that is a ResizableScene @@ -64,8 +60,7 @@ window.addEventListener('resize', function (event) { } } }); -CoWebsiteManager.onStateChange(() => { - const {width, height} = CoWebsiteManager.getGameSize(); - +coWebsiteManager.onStateChange(() => { + const {width, height} = coWebsiteManager.getGameSize(); game.scale.resize(width / RESOLUTION, height / RESOLUTION); });