Merge branch 'develop' into patch-1

This commit is contained in:
TabascoEye 2020-12-02 15:51:15 +01:00 committed by GitHub
commit 922f76bc43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 2107 additions and 233 deletions

3
.gitignore vendored
View file

@ -4,3 +4,6 @@
Vagrantfile Vagrantfile
docker-compose.override.yaml docker-compose.override.yaml
*.DS_Store *.DS_Store
maps/yarn.lock
maps/dist/computer.js
maps/dist/computer.js.map

View file

@ -49,7 +49,7 @@
"multer": "^1.4.2", "multer": "^1.4.2",
"prom-client": "^12.0.0", "prom-client": "^12.0.0",
"query-string": "^6.13.3", "query-string": "^6.13.3",
"systeminformation": "^4.27.11", "systeminformation": "^4.30.5",
"ts-node-dev": "^1.0.0-pre.44", "ts-node-dev": "^1.0.0-pre.44",
"typescript": "^3.8.3", "typescript": "^3.8.3",
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0", "uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",

View file

@ -2345,10 +2345,10 @@ supports-color@^7.1.0:
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
systeminformation@^4.27.11: systeminformation@^4.30.5:
version "4.27.11" version "4.30.5"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.27.11.tgz#6dbe96e48091444f80dab6c05ee1901286826b60" resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.30.5.tgz#2219c305c8be56a2cfa527a5519c45bc81d4916c"
integrity sha512-U7bigXbOnsB8k1vNHS0Y13RCsRz5/UohiUmND+3mMUL6vfzrpbe/h4ZqewowB+B+tJNnmGFDj08Z8xGfYo45dQ== integrity sha512-aYWs8yttl8ePpr6VOQ/Ak8cznuc9L/NQODVhbOKhInX73ZMLvV2BS86Mzr7LLfmUteVFR36CTDNQgiJgRqq+SQ==
table@^5.2.3: table@^5.2.3:
version "5.4.6" version "5.4.6"

1
front/.gitignore vendored
View file

@ -1,5 +1,6 @@
/node_modules/ /node_modules/
/dist/bundle.js /dist/bundle.js
/dist/tests/
/yarn-error.log /yarn-error.log
/dist/webpack.config.js /dist/webpack.config.js
/dist/webpack.config.js.map /dist/webpack.config.js.map

View file

@ -72,7 +72,11 @@
</div> </div>
</div> </div>
<div id="cowebsite" class="cowebsite hidden"></div> <div id="cowebsite" class="cowebsite hidden">
<button class="close-btn" id="cowebsite-close">
<img src="resources/logos/close.svg"/>
</button>
</div>
<div class="audio-playing"> <div class="audio-playing">
<img src="/resources/logos/megaphone.svg"/> <img src="/resources/logos/megaphone.svg"/>
</div> </div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

View file

@ -284,6 +284,23 @@ body {
#cowebsite.hidden { #cowebsite.hidden {
transform: translateX(100%); transform: translateX(100%);
} }
#cowebsite .close-btn{
position: absolute;
top: 10px;
right: -100px;
background: none;
border: none;
cursor: pointer;
animation: right .2s ease;
}
#cowebsite .close-btn img{
height: 15px;
right: 15px;
}
#cowebsite:hover .close-btn{
right: 10px;
}
} }
@media (max-aspect-ratio: 1/1) { @media (max-aspect-ratio: 1/1) {
.game-overlay { .game-overlay {
@ -901,3 +918,31 @@ div.modal-report-user{
background-color: #ffffff; background-color: #ffffff;
} }
/** Action button **/
div.action{
position: absolute;
width: 100%;
height: auto;
text-align: center;
bottom: 40px;
transition: all .5s ease;
animation: mymove .5s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
div.action p.action-body{
padding: 10px;
background-color: #2d2d2dba;
color: #fff;
font-size: 12px;
text-align: center;
max-width: 150px;
margin-left: calc(50% - 75px);
border-radius: 15px;
}
@keyframes mymove {
0% {bottom: 40px;}
50% {bottom: 30px;}
100% {bottom: 40px;}
}

View file

@ -12,12 +12,14 @@ const URL_ROOM_STARTED = '/Floor0/floor0.json';
class ConnectionManager { class ConnectionManager {
private localUser!:LocalUser; private localUser!:LocalUser;
private connexionType?: GameConnexionTypes
/** /**
* Tries to login to the node server and return the starting map url to be loaded * Tries to login to the node server and return the starting map url to be loaded
*/ */
public async initGameConnexion(): Promise<Room> { public async initGameConnexion(): Promise<Room> {
const connexionType = urlManager.getGameConnexionType(); const connexionType = urlManager.getGameConnexionType();
this.connexionType = connexionType;
if(connexionType === GameConnexionTypes.register) { if(connexionType === GameConnexionTypes.register) {
const organizationMemberToken = urlManager.getOrganizationToken(); const organizationMemberToken = urlManager.getOrganizationToken();
const data = await Axios.post(`${API_URL}/register`, {organizationMemberToken}).then(res => res.data); const data = await Axios.post(`${API_URL}/register`, {organizationMemberToken}).then(res => res.data);
@ -108,6 +110,10 @@ class ConnectionManager {
}); });
}); });
} }
get getConnexionType(){
return this.connexionType;
}
} }
export const connectionManager = new ConnectionManager(); export const connectionManager = new ConnectionManager();

View file

@ -6,10 +6,8 @@ export class Room {
public readonly isPublic: boolean; public readonly isPublic: boolean;
private mapUrl: string|undefined; private mapUrl: string|undefined;
private instance: string|undefined; private instance: string|undefined;
public readonly hash: string;
constructor(id: string) { constructor(id: string) {
this.hash = '';
if (id.startsWith('/')) { if (id.startsWith('/')) {
id = id.substr(1); id = id.substr(1);
} }
@ -24,12 +22,29 @@ export class Room {
const indexOfHash = this.id.indexOf('#'); const indexOfHash = this.id.indexOf('#');
if (indexOfHash !== -1) { if (indexOfHash !== -1) {
const idWithHash = this.id;
this.id = this.id.substr(0, indexOfHash); this.id = this.id.substr(0, indexOfHash);
this.hash = idWithHash.substr(indexOfHash + 1);
} }
} }
public static getIdFromIdentifier(identifier: string, baseUrl: string, currentInstance: string): {roomId: string, hash: string} {
let roomId = '';
let hash = '';
if (!identifier.startsWith('/_/') && !identifier.startsWith('/@/')) { //relative file link
const absoluteExitSceneUrl = new URL(identifier, baseUrl);
roomId = '_/'+currentInstance+'/'+absoluteExitSceneUrl.hostname + absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId
hash = absoluteExitSceneUrl.hash;
hash = hash.substring(1); //remove the leading diese
} else { //absolute room Id
const parts = identifier.split('#');
roomId = parts[0];
roomId = roomId.substring(1); //remove the leading slash
if (parts.length > 1) {
hash = parts[1]
}
}
return {roomId, hash}
}
public async getMapUrl(): Promise<string> { public async getMapUrl(): Promise<string> {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
if (this.mapUrl !== undefined) { if (this.mapUrl !== undefined) {

View file

@ -52,6 +52,7 @@ export class RoomConnection implements RoomConnection {
private static websocketFactory: null|((url: string)=>any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any private static websocketFactory: null|((url: string)=>any) = null; // eslint-disable-line @typescript-eslint/no-explicit-any
private closed: boolean = false; private closed: boolean = false;
private tags: string[] = []; private tags: string[] = [];
private intervalId!: NodeJS.Timeout;
public static setWebsocketFactory(websocketFactory: (url: string)=>any): void { // eslint-disable-line @typescript-eslint/no-explicit-any public static setWebsocketFactory(websocketFactory: (url: string)=>any): void { // eslint-disable-line @typescript-eslint/no-explicit-any
RoomConnection.websocketFactory = websocketFactory; RoomConnection.websocketFactory = websocketFactory;
@ -89,9 +90,13 @@ export class RoomConnection implements RoomConnection {
this.socket.onopen = (ev) => { this.socket.onopen = (ev) => {
//we manually ping every 20s to not be logged out by the server, even when the game is in background. //we manually ping every 20s to not be logged out by the server, even when the game is in background.
const pingMessage = new PingMessage(); const pingMessage = new PingMessage();
setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay); this.intervalId = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay);
}; };
this.socket.onclose = () => {
clearTimeout(this.intervalId);
}
this.socket.onmessage = (messageEvent) => { this.socket.onmessage = (messageEvent) => {
const arrayBuffer: ArrayBuffer = messageEvent.data; const arrayBuffer: ArrayBuffer = messageEvent.data;
const message = ServerToClientMessage.deserializeBinary(new Uint8Array(arrayBuffer)); const message = ServerToClientMessage.deserializeBinary(new Uint8Array(arrayBuffer));

View file

@ -226,6 +226,10 @@ export const CLOTHES_RESOURCES: Array<BodyResourceDescriptionInterface> = [
{name:"clothes_68",img: "resources/customisation/character_clothes/character_clothes67.png"}, {name:"clothes_68",img: "resources/customisation/character_clothes/character_clothes67.png"},
{name:"clothes_69",img: "resources/customisation/character_clothes/character_clothes68.png"}, {name:"clothes_69",img: "resources/customisation/character_clothes/character_clothes68.png"},
{name:"clothes_70",img: "resources/customisation/character_clothes/character_clothes69.png"}, {name:"clothes_70",img: "resources/customisation/character_clothes/character_clothes69.png"},
{name:"clothes_pride_shirt",img: "resources/customisation/character_clothes/pride_shirt.png"},
{name:"clothes_black_hoodie",img: "resources/customisation/character_clothes/black_hoodie.png"},
{name:"clothes_white_hoodie",img: "resources/customisation/character_clothes/white_hoodie.png"},
{name:"clothes_engelbert",img: "resources/customisation/character_clothes/engelbert.png"}
]; ];
export const HATS_RESOURCES: Array<BodyResourceDescriptionInterface> = [ export const HATS_RESOURCES: Array<BodyResourceDescriptionInterface> = [
@ -254,7 +258,8 @@ export const HATS_RESOURCES: Array<BodyResourceDescriptionInterface> = [
{name: "hats_23", img: "resources/customisation/character_hats/character_hats23.png"}, {name: "hats_23", img: "resources/customisation/character_hats/character_hats23.png"},
{name: "hats_24", img: "resources/customisation/character_hats/character_hats24.png"}, {name: "hats_24", img: "resources/customisation/character_hats/character_hats24.png"},
{name: "hats_25", img: "resources/customisation/character_hats/character_hats25.png"}, {name: "hats_25", img: "resources/customisation/character_hats/character_hats25.png"},
{name: "hats_26", img: "resources/customisation/character_hats/character_hats26.png"} {name: "hats_26", img: "resources/customisation/character_hats/character_hats26.png"},
{name: "tinfoil_hat1", img: "resources/customisation/character_hats/tinfoil_hat1.png"}
]; ];
export const ACCESSORIES_RESOURCES: Array<BodyResourceDescriptionInterface> = [ export const ACCESSORIES_RESOURCES: Array<BodyResourceDescriptionInterface> = [
@ -289,7 +294,9 @@ export const ACCESSORIES_RESOURCES: Array<BodyResourceDescriptionInterface> = [
{name: "accessory_29", img: "resources/customisation/character_accessories/character_accessories29.png"}, {name: "accessory_29", img: "resources/customisation/character_accessories/character_accessories29.png"},
{name: "accessory_30", img: "resources/customisation/character_accessories/character_accessories30.png"}, {name: "accessory_30", img: "resources/customisation/character_accessories/character_accessories30.png"},
{name: "accessory_31", img: "resources/customisation/character_accessories/character_accessories31.png"}, {name: "accessory_31", img: "resources/customisation/character_accessories/character_accessories31.png"},
{name: "accessory_32", img: "resources/customisation/character_accessories/character_accessories32.png"} {name: "accessory_32", img: "resources/customisation/character_accessories/character_accessories32.png"},
{name: "accessory_mate_bottle", img: "resources/customisation/character_accessories/mate_bottle1.png"},
{name: "accessory_mask", img: "resources/customisation/character_accessories/mask.png"}
]; ];
export const LAYERS: Array<Array<BodyResourceDescriptionInterface>> = [ export const LAYERS: Array<Array<BodyResourceDescriptionInterface>> = [

View file

@ -39,20 +39,16 @@ export class GameManager {
public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise<void> { public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise<void> {
const roomID = room.id; const roomID = room.id;
const mapUrl = await room.getMapUrl(); const mapUrl = await room.getMapUrl();
console.log('Loading map '+roomID+' at url '+mapUrl);
const gameIndex = scenePlugin.getIndex(mapUrl); const gameIndex = scenePlugin.getIndex(roomID);
if(gameIndex === -1){ if(gameIndex === -1){
const game : Phaser.Scene = GameScene.createFromUrl(room, mapUrl); const game : Phaser.Scene = new GameScene(room, mapUrl);
console.log('Adding scene '+mapUrl); scenePlugin.add(roomID, game, false);
scenePlugin.add(mapUrl, game, false);
} }
} }
public async goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin) { public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void {
const url = await this.startRoom.getMapUrl(); scenePlugin.start(this.startRoom.id);
console.log('Starting scene '+url);
scenePlugin.start(url);
} }
} }

View file

@ -31,7 +31,12 @@ import {Queue} from 'queue-typescript';
import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer"; import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer";
import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene";
import {loadAllLayers, loadObject, loadPlayerCharacters} from "../Entity/body_character"; import {loadAllLayers, loadObject, loadPlayerCharacters} from "../Entity/body_character";
import {CenterListener, layoutManager, LayoutMode} from "../../WebRtc/LayoutManager"; import {
CenterListener,
layoutManager,
LayoutMode,
ON_ACTION_TRIGGER_BUTTON, TRIGGER_JITSI_PROPERTIES, TRIGGER_WEBSITE_PROPERTIES
} from "../../WebRtc/LayoutManager";
import Texture = Phaser.Textures.Texture; import Texture = Phaser.Textures.Texture;
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
import CanvasTexture = Phaser.Textures.CanvasTexture; import CanvasTexture = Phaser.Textures.CanvasTexture;
@ -54,6 +59,7 @@ import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMes
import {ResizableScene} from "../Login/ResizableScene"; import {ResizableScene} from "../Login/ResizableScene";
import {Room} from "../../Connexion/Room"; import {Room} from "../../Connexion/Room";
import {jitsiFactory} from "../../WebRtc/JitsiFactory"; import {jitsiFactory} from "../../WebRtc/JitsiFactory";
import {urlManager} from "../../Url/UrlManager";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface|null initPosition: PointInterface|null
@ -89,6 +95,8 @@ interface DeleteGroupEventInterface {
groupId: number groupId: number
} }
const defaultStartLayerName = 'start';
export class GameScene extends ResizableScene implements CenterListener { export class GameScene extends ResizableScene implements CenterListener {
GameManager : GameManager; GameManager : GameManager;
Terrains : Array<Phaser.Tilemaps.Tileset>; Terrains : Array<Phaser.Tilemaps.Tileset>;
@ -118,7 +126,6 @@ export class GameScene extends ResizableScene implements CenterListener {
private createPromise: Promise<void>; private createPromise: Promise<void>;
private createPromiseResolve!: (value?: void | PromiseLike<void>) => void; private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
MapKey: string;
MapUrlFile: string; MapUrlFile: string;
RoomId: string; RoomId: string;
instance: string; instance: string;
@ -132,7 +139,6 @@ export class GameScene extends ResizableScene implements CenterListener {
y: -1000 y: -1000
} }
private PositionNextScene: Array<Array<{ key: string, hash: string }>> = new Array<Array<{ key: string, hash: string }>>();
private presentationModeSprite!: Sprite; private presentationModeSprite!: Sprite;
private chatModeSprite!: Sprite; private chatModeSprite!: Sprite;
private gameMap!: GameMap; private gameMap!: GameMap;
@ -140,18 +146,11 @@ export class GameScene extends ResizableScene implements CenterListener {
// The item that can be selected by pressing the space key. // The item that can be selected by pressing the space key.
private outlinedItem: ActionableItem|null = null; private outlinedItem: ActionableItem|null = null;
private userInputManager!: UserInputManager; private userInputManager!: UserInputManager;
private startLayerName!: string | null;
static createFromUrl(room: Room, mapUrlFile: string, gameSceneKey: string|null = null): GameScene { constructor(private room: Room, MapUrlFile: string) {
// We use the map URL as a key
if (gameSceneKey === null) {
gameSceneKey = mapUrlFile;
}
return new GameScene(room, mapUrlFile, gameSceneKey);
}
constructor(private room: Room, MapUrlFile: string, gameSceneKey: string) {
super({ super({
key: gameSceneKey key: room.id
}); });
this.GameManager = gameManager; this.GameManager = gameManager;
@ -159,7 +158,6 @@ export class GameScene extends ResizableScene implements CenterListener {
this.groups = new Map<number, Sprite>(); this.groups = new Map<number, Sprite>();
this.instance = room.getInstance(); this.instance = room.getInstance();
this.MapKey = MapUrlFile;
this.MapUrlFile = MapUrlFile; this.MapUrlFile = MapUrlFile;
this.RoomId = room.id; this.RoomId = room.id;
@ -179,15 +177,15 @@ export class GameScene extends ResizableScene implements CenterListener {
}); });
}); });
this.load.scenePlugin('AnimatedTiles', 'resources/plugins/AnimatedTiles.js', 'animatedTiles', 'animatedTiles'); this.load.scenePlugin('AnimatedTiles', 'resources/plugins/AnimatedTiles.js', 'animatedTiles', 'animatedTiles');
this.load.on('filecomplete-tilemapJSON-'+this.MapKey, (key: string, type: string, data: unknown) => { this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => {
this.onMapLoad(data); this.onMapLoad(data);
}); });
//TODO strategy to add access token //TODO strategy to add access token
this.load.tilemapTiledJSON(this.MapKey, this.MapUrlFile); this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile);
// If the map has already been loaded as part of another GameScene, the "on load" event will not be triggered. // If the map has already been loaded as part of another GameScene, the "on load" event will not be triggered.
// In this case, we check in the cache to see if the map is here and trigger the event manually. // In this case, we check in the cache to see if the map is here and trigger the event manually.
if (this.cache.tilemap.exists(this.MapKey)) { if (this.cache.tilemap.exists(this.MapUrlFile)) {
const data = this.cache.tilemap.get(this.MapKey); const data = this.cache.tilemap.get(this.MapUrlFile);
this.onMapLoad(data); this.onMapLoad(data);
} }
@ -298,14 +296,17 @@ export class GameScene extends ResizableScene implements CenterListener {
//hook initialisation //hook initialisation
init(initData : GameSceneInitInterface) { init(initData : GameSceneInitInterface) {
if (initData.initPosition !== undefined) { if (initData.initPosition !== undefined) {
this.initPosition = initData.initPosition; this.initPosition = initData.initPosition; //todo: still used?
} }
} }
//hook create scene //hook create scene
create(): void { create(): void {
urlManager.pushRoomIdToUrl(this.room);
this.startLayerName = urlManager.getStartLayerNameFromUrl();
//initalise map //initalise map
this.Map = this.add.tilemap(this.MapKey); this.Map = this.add.tilemap(this.MapUrlFile);
this.gameMap = new GameMap(this.mapFile); this.gameMap = new GameMap(this.mapFile);
const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/')); const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/'));
this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => { this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => {
@ -315,25 +316,21 @@ export class GameScene extends ResizableScene implements CenterListener {
//permit to set bound collision //permit to set bound collision
this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
// Let's alter browser history
let path = this.room.id;
if (this.room.hash) {
path += '#'+this.room.hash;
}
window.history.pushState({}, 'WorkAdventure', path);
//add layer on map //add layer on map
this.Layers = new Array<Phaser.Tilemaps.DynamicTilemapLayer>(); this.Layers = new Array<Phaser.Tilemaps.DynamicTilemapLayer>();
let depth = -2; let depth = -2;
for (const layer of this.mapFile.layers) { for (const layer of this.mapFile.layers) {
if (layer.type === 'tilelayer') { if (layer.type === 'tilelayer') {
this.addLayer(this.Map.createDynamicLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); this.addLayer(this.Map.createDynamicLayer(layer.name, this.Terrains, 0, 0).setDepth(depth));
const exitSceneUrl = this.getExitSceneUrl(layer);
if (exitSceneUrl !== undefined) {
this.loadNextGame(exitSceneUrl);
}
const exitUrl = this.getExitUrl(layer);
if (exitUrl !== undefined) {
this.loadNextGame(exitUrl);
} }
if (layer.type === 'tilelayer' && this.getExitSceneUrl(layer) !== undefined) {
this.loadNextGameFromExitSceneUrl(layer, this.mapFile.width);
} else if (layer.type === 'tilelayer' && this.getExitUrl(layer) !== undefined) {
console.log('Loading exitUrl ', this.getExitUrl(layer))
this.loadNextGameFromExitUrl(layer, this.mapFile.width);
} }
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') { if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
depth = 10000; depth = 10000;
@ -480,7 +477,6 @@ export class GameScene extends ResizableScene implements CenterListener {
if (position === undefined) { if (position === undefined) {
throw new Error('Position missing from UserMovedMessage'); throw new Error('Position missing from UserMovedMessage');
} }
//console.log('Received position ', position.getX(), position.getY(), "from user", message.getUserid());
const messageUserMoved: MessageUserMovedInterface = { const messageUserMoved: MessageUserMovedInterface = {
userId: message.getUserid(), userId: message.getUserid(),
@ -513,7 +509,7 @@ export class GameScene extends ResizableScene implements CenterListener {
this.simplePeer.unregister(); this.simplePeer.unregister();
const gameSceneKey = 'somekey' + Math.round(Math.random() * 10000); const gameSceneKey = 'somekey' + Math.round(Math.random() * 10000);
const game: Phaser.Scene = GameScene.createFromUrl(this.room, this.MapUrlFile, gameSceneKey); const game: Phaser.Scene = new GameScene(this.room, this.MapUrlFile);
this.scene.add(gameSceneKey, game, true, this.scene.add(gameSceneKey, game, true,
{ {
initPosition: { initPosition: {
@ -579,17 +575,38 @@ export class GameScene extends ResizableScene implements CenterListener {
} }
private triggerOnMapLayerPropertyChange(){ private triggerOnMapLayerPropertyChange(){
this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue) => { this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => {
if (newValue) this.onMapExit(newValue as string);
});
this.gameMap.onPropertyChange('exitUrl', (newValue, oldValue) => {
if (newValue) this.onMapExit(newValue as string);
});
this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue, allProps) => {
if (newValue === undefined) { if (newValue === undefined) {
layoutManager.removeActionButton('openWebsite', this.userInputManager);
coWebsiteManager.closeCoWebsite(); coWebsiteManager.closeCoWebsite();
} else { }else{
const openWebsiteFunction = () => {
coWebsiteManager.loadCoWebsite(newValue as string); coWebsiteManager.loadCoWebsite(newValue as string);
layoutManager.removeActionButton('openWebsite', this.userInputManager);
};
const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES);
if(openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
layoutManager.addActionButton('openWebsite', 'Click on SPACE to open the web site', () => {
openWebsiteFunction();
}, this.userInputManager);
}else{
openWebsiteFunction();
}
} }
}); });
this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => { this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => {
if (newValue === undefined) { if (newValue === undefined) {
layoutManager.removeActionButton('jitsiRoom', this.userInputManager);
this.stopJitsi(); this.stopJitsi();
} else { }else{
const openJitsiRoomFunction = () => {
if (JITSI_PRIVATE_MODE) { if (JITSI_PRIVATE_MODE) {
const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined; const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined;
@ -597,6 +614,17 @@ export class GameScene extends ResizableScene implements CenterListener {
} else { } else {
this.startJitsi(newValue as string); this.startJitsi(newValue as string);
} }
layoutManager.removeActionButton('jitsiRoom', this.userInputManager);
}
const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES);
if(jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
layoutManager.addActionButton('jitsiRoom', 'Click on SPACE to enter in jitsi meet room', () => {
openJitsiRoomFunction();
}, this.userInputManager);
}else{
openJitsiRoomFunction();
}
} }
}) })
this.gameMap.onPropertyChange('silent', (newValue, oldValue) => { this.gameMap.onPropertyChange('silent', (newValue, oldValue) => {
@ -608,6 +636,25 @@ export class GameScene extends ResizableScene implements CenterListener {
}); });
} }
private onMapExit(exitKey: string) {
const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance);
if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey);
urlManager.pushStartLayerNameToUrl(hash);
if (roomId !== this.scene.key) {
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
this.connection.closeConnection();
this.simplePeer.unregister();
this.scene.stop();
this.scene.remove(this.scene.key);
this.scene.start(roomId);
} else {
//if the exit points to the current map, we simply teleport the user back to the startLayer
this.initPositionFromLayerName(hash || defaultStartLayerName);
this.CurrentPlayer.x = this.startX;
this.CurrentPlayer.y = this.startY;
}
}
private switchLayoutMode(): void { private switchLayoutMode(): void {
//if discussion is activated, this layout cannot be activated //if discussion is activated, this layout cannot be activated
if(mediaManager.activatedDiscussion){ if(mediaManager.activatedDiscussion){
@ -632,12 +679,12 @@ export class GameScene extends ResizableScene implements CenterListener {
this.startY = this.initPosition.y; this.startY = this.initPosition.y;
} else { } else {
// Now, let's find the start layer // Now, let's find the start layer
if (this.room.hash) { if (this.startLayerName) {
this.initPositionFromLayerName(this.room.hash); this.initPositionFromLayerName(this.startLayerName);
} }
if (this.startX === undefined) { if (this.startX === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position. // If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName("start"); this.initPositionFromLayerName(defaultStartLayerName);
} }
} }
// Still no start position? Something is wrong with the map, we need a "start" layer. // Still no start position? Something is wrong with the map, we need a "start" layer.
@ -651,7 +698,7 @@ export class GameScene extends ResizableScene implements CenterListener {
private initPositionFromLayerName(layerName: string) { private initPositionFromLayerName(layerName: string) {
for (const layer of this.mapFile.layers) { for (const layer of this.mapFile.layers) {
if (layerName === layer.name && layer.type === 'tilelayer' && (layerName === "start" || this.isStartLayer(layer))) { if (layerName === layer.name && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) {
const startPosition = this.startUser(layer); const startPosition = this.startUser(layer);
this.startX = startPosition.x; this.startX = startPosition.x;
this.startY = startPosition.y; this.startY = startPosition.y;
@ -663,14 +710,13 @@ export class GameScene extends ResizableScene implements CenterListener {
return this.getProperty(layer, "exitUrl") as string|undefined; return this.getProperty(layer, "exitUrl") as string|undefined;
} }
/**
* @deprecated the map property exitSceneUrl is deprecated
*/
private getExitSceneUrl(layer: ITiledMapLayer): string|undefined { private getExitSceneUrl(layer: ITiledMapLayer): string|undefined {
return this.getProperty(layer, "exitSceneUrl") as string|undefined; return this.getProperty(layer, "exitSceneUrl") as string|undefined;
} }
private getExitSceneInstance(layer: ITiledMapLayer): string|undefined {
return this.getProperty(layer, "exitInstance") as string|undefined;
}
private isStartLayer(layer: ITiledMapLayer): boolean { private isStartLayer(layer: ITiledMapLayer): boolean {
return this.getProperty(layer, "startLayer") == true; return this.getProperty(layer, "startLayer") == true;
} }
@ -680,73 +726,18 @@ export class GameScene extends ResizableScene implements CenterListener {
if (!properties) { if (!properties) {
return undefined; return undefined;
} }
const obj = properties.find((property: ITiledMapLayerProperty) => property.name === name); const obj = properties.find((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase());
if (obj === undefined) { if (obj === undefined) {
return undefined; return undefined;
} }
return obj.value; return obj.value;
} }
private loadNextGameFromExitSceneUrl(layer: ITiledMapLayer, mapWidth: number) {
const exitSceneUrl = this.getExitSceneUrl(layer);
if (exitSceneUrl === undefined) {
throw new Error('Layer is not an exit scene layer.');
}
let instance = this.getExitSceneInstance(layer);
if (instance === undefined) {
instance = this.instance;
}
const absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href;
const absoluteExitSceneUrlWithoutProtocol = absoluteExitSceneUrl.toString().substr(absoluteExitSceneUrl.toString().indexOf('://')+3);
const roomId = '_/'+instance+'/'+absoluteExitSceneUrlWithoutProtocol;
this.loadNextGame(layer, mapWidth, roomId);
}
private loadNextGameFromExitUrl(layer: ITiledMapLayer, mapWidth: number) {
const exitUrl = this.getExitUrl(layer);
if (exitUrl === undefined) {
throw new Error('Layer is not an exit layer.');
}
const fullPath = new URL(exitUrl, window.location.toString()).pathname;
this.loadNextGame(layer, mapWidth, fullPath);
}
//todo: push that into the gameManager //todo: push that into the gameManager
private loadNextGame(layer: ITiledMapLayer, mapWidth: number, roomId: string){ private async loadNextGame(exitSceneIdentifier: string){
const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance);
const room = new Room(roomId); const room = new Room(roomId);
gameManager.loadMap(room, this.scene); await gameManager.loadMap(room, this.scene);
const exitSceneKey = roomId;
const tiles : number[] = layer.data as number[];
for (let key=0; key < tiles.length; key++) {
const objectKey = tiles[key];
if(objectKey === 0){
continue;
}
//key + 1 because the start x = 0;
const y : number = parseInt(((key + 1) / mapWidth).toString());
const x : number = key - (y * mapWidth);
let hash = new URL(roomId, this.MapUrlFile).hash;
if (hash) {
hash = hash.substr(1);
}
//push and save switching case
if (this.PositionNextScene[y] === undefined) {
this.PositionNextScene[y] = new Array<{key: string, hash: string}>();
}
room.getMapUrl().then((url: string) => {
this.PositionNextScene[y][x] = {
key: url,
hash
}
})
}
} }
private startUser(layer: ITiledMapLayer): PositionInterface { private startUser(layer: ITiledMapLayer): PositionInterface {
@ -909,6 +900,7 @@ export class GameScene extends ResizableScene implements CenterListener {
* @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate.
*/ */
update(time: number, delta: number) : void { update(time: number, delta: number) : void {
mediaManager.setLastUpdateScene();
this.currentTick = time; this.currentTick = time;
this.CurrentPlayer.moveUser(delta); this.CurrentPlayer.moveUser(delta);
@ -946,33 +938,6 @@ export class GameScene extends ResizableScene implements CenterListener {
} }
player.updatePosition(moveEvent); player.updatePosition(moveEvent);
}); });
const nextSceneKey = this.checkToExit();
if (!nextSceneKey) return;
if (nextSceneKey.key !== this.scene.key) {
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
this.connection.closeConnection();
this.simplePeer.unregister();
this.scene.stop();
this.scene.remove(this.scene.key);
this.scene.start(nextSceneKey.key);
} else {
//if the exit points to the current map, we simply teleport the user back to the startLayer
this.initPositionFromLayerName(this.room.hash || 'start');
this.CurrentPlayer.x = this.startX;
this.CurrentPlayer.y = this.startY;
}
}
private checkToExit(): {key: string, hash: string} | null {
const x = Math.floor(this.CurrentPlayer.x / 32);
const y = Math.floor(this.CurrentPlayer.y / 32);
if (this.PositionNextScene[y] !== undefined && this.PositionNextScene[y][x] !== undefined) {
return this.PositionNextScene[y][x];
} else {
return null;
}
} }
/** /**
@ -1204,12 +1169,19 @@ export class GameScene extends ResizableScene implements CenterListener {
jitsiFactory.start(roomName, gameManager.getPlayerName(), jwt); jitsiFactory.start(roomName, gameManager.getPlayerName(), jwt);
this.connection.setSilent(true); this.connection.setSilent(true);
mediaManager.hideGameOverlay(); mediaManager.hideGameOverlay();
//permit to stop jitsi when user close iframe
mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => {
this.stopJitsi();
});
} }
public stopJitsi(): void { public stopJitsi(): void {
this.connection.setSilent(false); this.connection.setSilent(false);
jitsiFactory.stop(); jitsiFactory.stop();
mediaManager.showGameOverlay(); mediaManager.showGameOverlay();
mediaManager.removeTriggerCloseJitsiFrameButton('close-jisi');
} }
private loadSpritesheet(name: string, url: string): Promise<void> { private loadSpritesheet(name: string, url: string): Promise<void> {

View file

@ -79,4 +79,11 @@ export class UserInputManager {
return event; return event;
}); });
} }
addSpaceEventListner(callback : Function){
this.Scene.input.keyboard.addListener('keyup-SPACE', callback);
}
removeSpaceEventListner(callback : Function){
this.Scene.input.keyboard.removeListener('keyup-SPACE', callback);
}
} }

View file

@ -1,3 +1,4 @@
import {Room} from "../Connexion/Room";
export enum GameConnexionTypes { export enum GameConnexionTypes {
anonymous=1, anonymous=1,
@ -45,6 +46,20 @@ class UrlManager {
return newUrl; return newUrl;
} }
public pushRoomIdToUrl(room:Room): void {
if (window.location.pathname === room.id) return;
const hash = window.location.hash;
history.pushState({}, 'WorkAdventure', room.id+hash);
}
public getStartLayerNameFromUrl(): string|null {
const hash = window.location.hash;
return hash.length > 1 ? hash.substring(1) : null;
}
pushStartLayerNameToUrl(startLayerName: string): void {
window.location.hash = startLayerName;
}
} }
export const urlManager = new UrlManager(); export const urlManager = new UrlManager();

View file

@ -44,7 +44,9 @@ class CoWebsiteManager {
public loadCoWebsite(url: string): void { public loadCoWebsite(url: string): void {
this.load(); this.load();
this.cowebsiteDiv.innerHTML = ''; this.cowebsiteDiv.innerHTML = `<button class="close-btn" id="cowebsite-close">
<img src="resources/logos/close.svg">
</button>`;
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.id = 'cowebsite-iframe'; iframe.id = 'cowebsite-iframe';
@ -83,7 +85,9 @@ class CoWebsiteManager {
this.close(); this.close();
this.fire(); this.fire();
setTimeout(() => { setTimeout(() => {
this.cowebsiteDiv.innerHTML = ''; this.cowebsiteDiv.innerHTML = `<button class="close-btn" id="cowebsite-close">
<img src="resources/logos/close.svg">
</button>`;
resolve(); resolve();
}, animationTime) }, animationTime)
})); }));

View file

@ -1,6 +1,9 @@
import {HtmlUtils} from "./HtmlUtils"; import {HtmlUtils} from "./HtmlUtils";
import {MediaManager, ReportCallback, UpdatedLocalStreamCallback} from "./MediaManager"; import {MediaManager, ReportCallback, UpdatedLocalStreamCallback} from "./MediaManager";
import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import {connectionManager} from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager";
export type SendMessageCallback = (message:string) => void; export type SendMessageCallback = (message:string) => void;
export class DiscussionManager { export class DiscussionManager {
@ -115,7 +118,11 @@ export class DiscussionManager {
divParticipant.appendChild(divImgParticipant); divParticipant.appendChild(divImgParticipant);
divParticipant.appendChild(divPParticipant); divParticipant.appendChild(divPParticipant);
if(!isMe) { if(
!isMe
&& connectionManager.getConnexionType
&& connectionManager.getConnexionType !== GameConnexionTypes.anonymous
) {
const reportBanUserAction: HTMLButtonElement = document.createElement('button'); const reportBanUserAction: HTMLButtonElement = document.createElement('button');
reportBanUserAction.classList.add('report-btn') reportBanUserAction.classList.add('report-btn')
reportBanUserAction.innerText = 'Report'; reportBanUserAction.innerText = 'Report';

View file

@ -63,6 +63,9 @@ class JitsiFactory {
} }
public async stop(): Promise<void> { public async stop(): Promise<void> {
if(!this.jitsiApi){
return;
}
await coWebsiteManager.closeCoWebsite(); await coWebsiteManager.closeCoWebsite();
this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback); this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback);
this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback); this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback);

View file

@ -1,3 +1,4 @@
import { UserInputManager } from "../Phaser/UserInput/UserInputManager";
import {HtmlUtils} from "./HtmlUtils"; import {HtmlUtils} from "./HtmlUtils";
export enum LayoutMode { export enum LayoutMode {
@ -22,6 +23,10 @@ export interface CenterListener {
onCenterChange(): void; onCenterChange(): void;
} }
export const ON_ACTION_TRIGGER_BUTTON = 'onaction';
export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger';
export const TRIGGER_JITSI_PROPERTIES = 'jitsiTrigger';
/** /**
* This class is in charge of the video-conference layout. * 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. * It receives positioning requests for videos and does its best to place them on the screen depending on the active layout mode.
@ -33,6 +38,9 @@ class LayoutManager {
private normalDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>(); private normalDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
private listener: CenterListener|null = null; private listener: CenterListener|null = null;
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
public setListener(centerListener: CenterListener|null) { public setListener(centerListener: CenterListener|null) {
this.listener = centerListener; this.listener = centerListener;
} }
@ -305,6 +313,48 @@ class LayoutManager {
} }
} }
} }
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager){
//delete previous element
this.removeActionButton(id, userInputManager);
//create div and text html component
const p = document.createElement('p');
p.classList.add('action-body');
p.innerText = text;
const div = document.createElement('div');
div.classList.add('action');
div.id = id;
div.appendChild(p);
this.actionButtonInformation.set(id, div);
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
mainContainer.appendChild(div);
const callBackFunctionTrigger = (() => {
console.log('user click on space => ', id);
callBack();
});
//add trigger action
this.actionButtonTrigger.set(id, callBackFunctionTrigger);
userInputManager.addSpaceEventListner(callBackFunctionTrigger);
}
public removeActionButton(id: string, userInputManager: UserInputManager){
//delete previous element
const previousDiv = this.actionButtonInformation.get(id);
if(previousDiv){
previousDiv.remove();
this.actionButtonInformation.delete(id);
}
const previousEventCallback = this.actionButtonTrigger.get(id);
if(previousEventCallback){
userInputManager.removeSpaceEventListner(previousEventCallback);
}
}
} }
const layoutManager = new LayoutManager(); const layoutManager = new LayoutManager();

View file

@ -40,12 +40,20 @@ export class MediaManager {
private cinemaBtn: HTMLDivElement; private cinemaBtn: HTMLDivElement;
private monitorBtn: HTMLDivElement; private monitorBtn: HTMLDivElement;
private previousConstraint : MediaStreamConstraints;
private focused : boolean = true;
private lastUpdateScene : Date = new Date();
private setTimeOutlastUpdateScene? : NodeJS.Timeout;
private discussionManager: DiscussionManager; private discussionManager: DiscussionManager;
private userInputManager?: UserInputManager; private userInputManager?: UserInputManager;
private hasCamera = true; private hasCamera = true;
private triggerCloseJistiFrame : Map<String, Function> = new Map<String, Function>();
constructor() { constructor() {
this.myCamVideo = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('myCamVideo'); this.myCamVideo = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('myCamVideo');
@ -98,9 +106,35 @@ export class MediaManager {
//update tracking //update tracking
}); });
this.previousConstraint = JSON.parse(JSON.stringify(this.constraintsMedia));
this.pingCameraStatus();
this.checkActiveUser(); //todo: desactivated in case of bug
this.discussionManager = new DiscussionManager(this,''); this.discussionManager = new DiscussionManager(this,'');
} }
public setLastUpdateScene(){
this.lastUpdateScene = new Date();
}
public blurCamera() {
if(!this.focused){
return;
}
this.focused = false;
this.previousConstraint = JSON.parse(JSON.stringify(this.constraintsMedia));
this.disableCamera();
}
public focusCamera() {
if(this.focused){
return;
}
this.focused = true;
this.applyPreviousConfig();
}
public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void { public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void {
this.updatedLocalStreamCallBacks.add(callback); this.updatedLocalStreamCallBacks.add(callback);
} }
@ -138,20 +172,27 @@ export class MediaManager {
public showGameOverlay(){ public showGameOverlay(){
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay'); const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.add('active'); gameOverlay.classList.add('active');
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail('cowebsite-close');
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
}
buttonCloseFrame.removeEventListener('click', functionTrigger);
} }
public hideGameOverlay(){ public hideGameOverlay(){
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay'); const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.remove('active'); gameOverlay.classList.remove('active');
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail('cowebsite-close');
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
}
buttonCloseFrame.addEventListener('click', functionTrigger);
} }
public enableCamera() { public enableCamera() {
if(!this.hasCamera){ this.enableCameraStyle();
return;
}
this.cinemaClose.style.display = "none";
this.cinemaBtn.classList.remove("disabled");
this.cinema.style.display = "block";
this.constraintsMedia.video = videoConstraint; this.constraintsMedia.video = videoConstraint;
this.getCamera().then((stream: MediaStream) => { this.getCamera().then((stream: MediaStream) => {
this.triggerUpdatedLocalStreamCallbacks(stream); this.triggerUpdatedLocalStreamCallbacks(stream);
@ -159,7 +200,8 @@ export class MediaManager {
} }
public async disableCamera() { public async disableCamera() {
this.disabledCameraView(); this.disableCameraStyle();
if (this.constraintsMedia.audio !== false) { if (this.constraintsMedia.audio !== false) {
const stream = await this.getCamera(); const stream = await this.getCamera();
this.triggerUpdatedLocalStreamCallbacks(stream); this.triggerUpdatedLocalStreamCallbacks(stream);
@ -168,19 +210,8 @@ export class MediaManager {
} }
} }
private disabledCameraView(){
this.cinemaClose.style.display = "block";
this.cinema.style.display = "none";
this.cinemaBtn.classList.add("disabled");
this.constraintsMedia.video = false;
this.myCamVideo.srcObject = null;
this.stopCamera();
}
public enableMicrophone() { public enableMicrophone() {
this.microphoneClose.style.display = "none"; this.enableMicrophoneStyle();
this.microphone.style.display = "block";
this.microphoneBtn.classList.remove("disabled");
this.constraintsMedia.audio = true; this.constraintsMedia.audio = true;
this.getCamera().then((stream) => { this.getCamera().then((stream) => {
@ -189,10 +220,7 @@ export class MediaManager {
} }
public async disableMicrophone() { public async disableMicrophone() {
this.microphoneClose.style.display = "block"; this.disableMicrophoneStyle();
this.microphone.style.display = "none";
this.microphoneBtn.classList.add("disabled");
this.constraintsMedia.audio = false;
this.stopMicrophone(); this.stopMicrophone();
if (this.constraintsMedia.video !== false) { if (this.constraintsMedia.video !== false) {
@ -203,6 +231,52 @@ export class MediaManager {
} }
} }
private applyPreviousConfig() {
this.constraintsMedia = this.previousConstraint;
if(!this.constraintsMedia.video){
this.disableCameraStyle();
}else{
this.enableCameraStyle();
}
if(!this.constraintsMedia.audio){
this.disableMicrophoneStyle()
}else{
this.enableMicrophoneStyle()
}
this.getCamera().then((stream: MediaStream) => {
this.triggerUpdatedLocalStreamCallbacks(stream);
});
}
private enableCameraStyle(){
this.cinemaClose.style.display = "none";
this.cinemaBtn.classList.remove("disabled");
this.cinema.style.display = "block";
}
private disableCameraStyle(){
this.cinemaClose.style.display = "block";
this.cinema.style.display = "none";
this.cinemaBtn.classList.add("disabled");
this.constraintsMedia.video = false;
this.myCamVideo.srcObject = null;
this.stopCamera();
}
private enableMicrophoneStyle(){
this.microphoneClose.style.display = "none";
this.microphone.style.display = "block";
this.microphoneBtn.classList.remove("disabled");
}
private disableMicrophoneStyle(){
this.microphoneClose.style.display = "block";
this.microphone.style.display = "none";
this.microphoneBtn.classList.add("disabled");
this.constraintsMedia.audio = false;
}
private enableScreenSharing() { private enableScreenSharing() {
this.monitorClose.style.display = "none"; this.monitorClose.style.display = "none";
this.monitor.style.display = "block"; this.monitor.style.display = "block";
@ -285,7 +359,7 @@ export class MediaManager {
return this.getLocalStream().catch(() => { return this.getLocalStream().catch(() => {
console.info('Error get camera, trying with video option at null'); console.info('Error get camera, trying with video option at null');
this.disabledCameraView(); this.disableCameraStyle();
return this.getLocalStream().then((stream : MediaStream) => { return this.getLocalStream().then((stream : MediaStream) => {
this.hasCamera = false; this.hasCamera = false;
return stream; return stream;
@ -412,7 +486,7 @@ export class MediaManager {
if(!element){ if(!element){
return; return;
} }
element.classList.add('active') element.classList.add('active') //todo: why does a method 'disable' add a class 'active'?
} }
enabledMicrophoneByUserId(userId: number){ enabledMicrophoneByUserId(userId: number){
@ -420,7 +494,7 @@ export class MediaManager {
if(!element){ if(!element){
return; return;
} }
element.classList.remove('active') element.classList.remove('active') //todo: why does a method 'enable' remove a class 'active'?
} }
disabledVideoByUserId(userId: number) { disabledVideoByUserId(userId: number) {
@ -445,7 +519,7 @@ export class MediaManager {
} }
} }
addStreamRemoteVideo(userId: string, stream : MediaStream){ addStreamRemoteVideo(userId: string, stream : MediaStream): void {
const remoteVideo = this.remoteVideo.get(userId); const remoteVideo = this.remoteVideo.get(userId);
if (remoteVideo === undefined) { if (remoteVideo === undefined) {
throw `Unable to find video for ${userId}`; throw `Unable to find video for ${userId}`;
@ -594,6 +668,29 @@ export class MediaManager {
public removeParticipant(userId: number|string){ public removeParticipant(userId: number|string){
this.discussionManager.removeParticipant(userId); this.discussionManager.removeParticipant(userId);
} }
public addTriggerCloseJitsiFrameButton(id: String, Function: Function){
this.triggerCloseJistiFrame.set(id, Function);
}
public removeTriggerCloseJitsiFrameButton(id: String){
this.triggerCloseJistiFrame.delete(id);
}
private triggerCloseJitsiFrameButton(): void {
for (const callback of this.triggerCloseJistiFrame.values()) {
callback();
}
}
/**
* For some reasons, the microphone muted icon or the stream is not always up to date.
* Here, every 30 seconds, we are "reseting" the streams and sending again the constraints to the other peers via the data channel again (see SimplePeer::pushVideoToRemoteUser)
**/
private pingCameraStatus(){
/*setInterval(() => {
console.log('ping camera status');
this.triggerUpdatedLocalStreamCallbacks(this.localStream);
}, 30000);*/
}
public addNewMessage(name: string, message: string, isMe: boolean = false){ public addNewMessage(name: string, message: string, isMe: boolean = false){
this.discussionManager.addMessage(name, message, isMe); this.discussionManager.addMessage(name, message, isMe);
@ -615,6 +712,22 @@ export class MediaManager {
public setUserInputManager(userInputManager : UserInputManager){ public setUserInputManager(userInputManager : UserInputManager){
this.discussionManager.setUserInputManager(userInputManager); this.discussionManager.setUserInputManager(userInputManager);
} }
//check if user is active
private checkActiveUser(){
if(this.setTimeOutlastUpdateScene){
clearTimeout(this.setTimeOutlastUpdateScene);
}
this.setTimeOutlastUpdateScene = setTimeout(() => {
const now = new Date();
//if last update is more of 10 sec
if( (now.getTime() - this.lastUpdateScene.getTime()) > 10000) {
this.blurCamera();
}else{
this.focusCamera();
}
this.checkActiveUser();
}, this.focused ? 10000 : 1000);
}
} }
export const mediaManager = new MediaManager(); export const mediaManager = new MediaManager();

View file

@ -1,7 +1,6 @@
import { import {
WebRtcDisconnectMessageInterface, WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface, WebRtcSignalReceivedMessageInterface,
WebRtcStartMessageInterface
} from "../Connexion/ConnexionModels"; } from "../Connexion/ConnexionModels";
import { import {
mediaManager, mediaManager,
@ -29,7 +28,7 @@ export interface PeerConnectionListener {
* 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.
*/ */
export class SimplePeer { export class SimplePeer {
private Users: Array<UserSimplePeerInterface> = new Array<UserSimplePeerInterface>(); private Users: Array<UserSimplePeerInterface> = new Array<UserSimplePeerInterface>(); //todo: this array should be fusionned with PeerConnectionArray
private PeerScreenSharingConnectionArray: Map<number, ScreenSharingPeer> = new Map<number, ScreenSharingPeer>(); private PeerScreenSharingConnectionArray: Map<number, ScreenSharingPeer> = new Map<number, ScreenSharingPeer>();
private PeerConnectionArray: Map<number, VideoPeer> = new Map<number, VideoPeer>(); private PeerConnectionArray: Map<number, VideoPeer> = new Map<number, VideoPeer>();
@ -95,12 +94,9 @@ export class SimplePeer {
this.Users.push(user); this.Users.push(user);
// Note: the clients array contain the list of all clients (even the ones we are already connected to in case a user joints a group) // Note: the clients array contain the list of all clients (even the ones we are already connected to in case a user joints a group)
// So we can receive a request we already had before. (which will abort at the first line of createPeerConnection) // So we can receive a request we already had before. (which will abort at the first line of createPeerConnection)
// TODO: refactor this to only send a message to connect to one user (rather than several users). => DONE
// This would be symmetrical to the way we handle disconnection. // This would be symmetrical to the way we handle disconnection.
//console.log('Start message', data);
//start connection //start connection
//this.startWebRtc();
console.log('receiveWebrtcStart. Initiator: ', user.initiator) console.log('receiveWebrtcStart. Initiator: ', user.initiator)
if(!user.initiator){ if(!user.initiator){
return; return;
@ -204,8 +200,6 @@ export class SimplePeer {
/** /**
* This is triggered twice. Once by the server, and once by a remote client disconnecting * This is triggered twice. Once by the server, and once by a remote client disconnecting
*
* @param userId
*/ */
private closeConnection(userId : number) { private closeConnection(userId : number) {
try { try {
@ -226,6 +220,12 @@ export class SimplePeer {
for (const peerConnectionListener of this.peerConnectionListeners) { for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onDisconnect(userId); peerConnectionListener.onDisconnect(userId);
} }
const userIndex = this.Users.findIndex(user => user.userId === userId);
if(userIndex < 0){
throw 'Couln\'t delete user';
} else {
this.Users.splice(userIndex, 1);
}
} catch (err) { } catch (err) {
console.error("closeConnection", err) console.error("closeConnection", err)
} }
@ -233,8 +233,6 @@ export class SimplePeer {
/** /**
* This is triggered twice. Once by the server, and once by a remote client disconnecting * This is triggered twice. Once by the server, and once by a remote client disconnecting
*
* @param userId
*/ */
private closeScreenSharingConnection(userId : number) { private closeScreenSharingConnection(userId : number) {
try { try {
@ -246,7 +244,6 @@ export class SimplePeer {
} }
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
//console.log('Closing connection with '+userId);
peer.destroy(); peer.destroy();
if(!this.PeerScreenSharingConnectionArray.delete(userId)){ if(!this.PeerScreenSharingConnectionArray.delete(userId)){
throw 'Couln\'t delete peer screen sharing connexion'; throw 'Couln\'t delete peer screen sharing connexion';
@ -313,10 +310,6 @@ export class SimplePeer {
} }
} }
/**
*
* @param userId
*/
private pushVideoToRemoteUser(userId : number) { private pushVideoToRemoteUser(userId : number) {
try { try {
const PeerConnection = this.PeerConnectionArray.get(userId); const PeerConnection = this.PeerConnectionArray.get(userId);
@ -331,6 +324,9 @@ export class SimplePeer {
} }
for (const track of localStream.getTracks()) { for (const track of localStream.getTracks()) {
//todo: this is a ugly hack to reduce the amount of error in console. Find a better way.
if ((track as any).added !== undefined) continue; // eslint-disable-line @typescript-eslint/no-explicit-any
(track as any).added = true; // eslint-disable-line @typescript-eslint/no-explicit-any
PeerConnection.addTrack(track, localStream); PeerConnection.addTrack(track, localStream);
} }
}catch (e) { }catch (e) {

View file

@ -84,22 +84,18 @@ export class VideoPeer extends Peer {
console.log("data", message); console.log("data", message);
if(message.type === MESSAGE_TYPE_CONSTRAINT) { if(message.type === MESSAGE_TYPE_CONSTRAINT) {
const constraint = message; if (message.audio) {
if (constraint.audio) {
mediaManager.enabledMicrophoneByUserId(this.userId); mediaManager.enabledMicrophoneByUserId(this.userId);
} else { } else {
mediaManager.disabledMicrophoneByUserId(this.userId); mediaManager.disabledMicrophoneByUserId(this.userId);
} }
if (constraint.video || constraint.screen) { if (message.video || message.screen) {
mediaManager.enabledVideoByUserId(this.userId); mediaManager.enabledVideoByUserId(this.userId);
} else { } else {
this.stream(undefined);
mediaManager.disabledVideoByUserId(this.userId); mediaManager.disabledVideoByUserId(this.userId);
} }
} } else if(message.type === 'message') {
if(message.type === 'message') {
mediaManager.addNewMessage(message.name, message.message); mediaManager.addNewMessage(message.name, message.message);
} }
}); });
@ -122,21 +118,15 @@ export class VideoPeer extends Peer {
/** /**
* Sends received stream to screen. * Sends received stream to screen.
*/ */
private stream(stream?: MediaStream) { private stream(stream: MediaStream) {
//console.log(`VideoPeer::stream => ${this.userId}`, stream);
if(!stream){
mediaManager.disabledVideoByUserId(this.userId);
mediaManager.disabledMicrophoneByUserId(this.userId);
} else {
try { try {
mediaManager.addStreamRemoteVideo("" + this.userId, stream); mediaManager.addStreamRemoteVideo("" + this.userId, stream);
}catch (err){ }catch (err){
console.error(err); console.error(err);
//Force add streem video //Force add streem video
setTimeout(() => { /*setTimeout(() => {
this.stream(stream); this.stream(stream);
}, 500); }, 500);*/ //todo: find a way to prevent infinite regression.
}
} }
} }

View file

@ -0,0 +1,42 @@
import "jasmine";
import {Room} from "../../../src/Connexion/Room";
describe("Room getIdFromIdentifier()", () => {
it("should work with an absolute room id and no hash as parameter", () => {
const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', '', '');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual('');
});
it("should work with an absolute room id and a hash as parameters", () => {
const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json#start', '', '');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual("start");
});
it("should work with an absolute room id, regardless of baseUrl or instance", () => {
const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', 'https://another.domain/_/global/test.json', 'lol');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual('');
});
it("should work with a relative file link and no hash as parameters", () => {
const {roomId, hash} = Room.getIdFromIdentifier('./test2.json', 'https://maps.workadventu.re/test.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual('');
});
it("should work with a relative file link with no dot", () => {
const {roomId, hash} = Room.getIdFromIdentifier('test2.json', 'https://maps.workadventu.re/test.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual('');
});
it("should work with a relative file link two levels deep", () => {
const {roomId, hash} = Room.getIdFromIdentifier('../floor1/Floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/floor1/Floor1.json');
expect(hash).toEqual('');
});
it("should work with a relative file link and a hash as parameters", () => {
const {roomId, hash} = Room.getIdFromIdentifier('./test2.json#start', 'https://maps.workadventu.re/test.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual("start");
});
});

1
maps/.gitignore vendored
View file

@ -1 +1,2 @@
/node_modules/ /node_modules/
dist/

View file

@ -44,6 +44,11 @@
"name":"jitsiRoom", "name":"jitsiRoom",
"type":"string", "type":"string",
"value":"tcm-chillzone-2" "value":"tcm-chillzone-2"
},
{
"name":"jitsiTrigger",
"type":"string",
"value":"onaction"
}], }],
"type":"tilelayer", "type":"tilelayer",
"visible":true, "visible":true,
@ -61,7 +66,12 @@
{ {
"name":"jitsiRoom", "name":"jitsiRoom",
"type":"string", "type":"string",
"value":"tcm-chillzone-1" "value":"tcm-chillzone-11"
},
{
"name":"jitsiTrigger",
"type":"string",
"value":"onaction"
}], }],
"type":"tilelayer", "type":"tilelayer",
"visible":true, "visible":true,
@ -128,6 +138,11 @@
"name":"openWebsite", "name":"openWebsite",
"type":"string", "type":"string",
"value":"https:\/\/app.swile.co\/" "value":"https:\/\/app.swile.co\/"
},
{
"name":"openWebsiteTrigger",
"type":"string",
"value":"onaction"
}], }],
"type":"tilelayer", "type":"tilelayer",
"visible":true, "visible":true,

1577
maps/yarn.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -2251,9 +2251,9 @@
} }
}, },
"dot-prop": { "dot-prop": {
"version": "4.2.0", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz",
"integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"is-obj": "^1.0.0" "is-obj": "^1.0.0"