diff --git a/README.md b/README.md index 26f1e816..cf640d9c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg) +![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg) [![Discord](https://img.shields.io/discord/821338762134290432?label=Discord)](https://discord.gg/JVVhXzcE) ![WorkAdventure landscape image](README-INTRO.jpg) diff --git a/front/dist/index.tmpl.html b/front/dist/index.tmpl.html index f57d512e..ce0cbad3 100644 --- a/front/dist/index.tmpl.html +++ b/front/dist/index.tmpl.html @@ -98,12 +98,12 @@
- +
diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index b0cdeed7..1bda9602 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -363,10 +363,14 @@ body { justify-content: center; } +.audioplayer > div { + padding-right: 1.2rem; +} + #audioplayerctrl { position: fixed; top: 0; - right: 50%; + right: calc(50% - 120px); padding: 0.3rem 0.5rem; color: white; transition: transform 0.5s; diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts index 8ac8c7b2..1e4267de 100644 --- a/front/src/Connexion/LocalUserStore.ts +++ b/front/src/Connexion/LocalUserStore.ts @@ -1,12 +1,15 @@ import {LocalUser} from "./LocalUser"; -const characterLayersKey = 'characterLayers'; -const gameQualityKey = 'gameQuality'; -const videoQualityKey = 'videoQuality'; +const playerNameKey = 'playerName'; +const selectedPlayerKey = 'selectedPlayer'; +const customCursorPositionKey = 'customCursorPosition'; +const characterLayersKey = 'characterLayers'; +const gameQualityKey = 'gameQuality'; +const videoQualityKey = 'videoQuality'; +const audioPlayerVolumeKey = 'audioVolume'; +const audioPlayerMuteKey = 'audioMute'; -//todo: add localstorage fallback class LocalUserStore { - saveUser(localUser: LocalUser) { localStorage.setItem('localUser', JSON.stringify(localUser)); } @@ -14,48 +17,62 @@ class LocalUserStore { const data = localStorage.getItem('localUser'); return data ? JSON.parse(data) : null; } - + setName(name:string): void { - window.localStorage.setItem('playerName', name); + localStorage.setItem(playerNameKey, name); } getName(): string { - return window.localStorage.getItem('playerName') ?? ''; + return localStorage.getItem(playerNameKey) || ''; } setPlayerCharacterIndex(playerCharacterIndex: number): void { - window.localStorage.setItem('selectedPlayer', ''+playerCharacterIndex); + localStorage.setItem(selectedPlayerKey, ''+playerCharacterIndex); } getPlayerCharacterIndex(): number { - return parseInt(window.localStorage.getItem('selectedPlayer') || ''); + return parseInt(localStorage.getItem(selectedPlayerKey) || ''); } setCustomCursorPosition(activeRow:number, selectedLayers: number[]): void { - window.localStorage.setItem('customCursorPosition', JSON.stringify({activeRow, selectedLayers})); + localStorage.setItem(customCursorPositionKey, JSON.stringify({activeRow, selectedLayers})); } getCustomCursorPosition(): {activeRow:number, selectedLayers:number[]}|null { - return JSON.parse(window.localStorage.getItem('customCursorPosition') || "null"); + return JSON.parse(localStorage.getItem(customCursorPositionKey) || "null"); } setCharacterLayers(layers: string[]): void { - window.localStorage.setItem(characterLayersKey, JSON.stringify(layers)); + localStorage.setItem(characterLayersKey, JSON.stringify(layers)); } getCharacterLayers(): string[]|null { - return JSON.parse(window.localStorage.getItem(characterLayersKey) || "null"); - } - - getGameQualityValue(): number { - return parseInt(window.localStorage.getItem(gameQualityKey) || '') || 60; + return JSON.parse(localStorage.getItem(characterLayersKey) || "null"); } + setGameQualityValue(value: number): void { localStorage.setItem(gameQualityKey, '' + value); } - - getVideoQualityValue(): number { - return parseInt(window.localStorage.getItem(videoQualityKey) || '') || 20; + getGameQualityValue(): number { + return parseInt(localStorage.getItem(gameQualityKey) || '60'); } + setVideoQualityValue(value: number): void { localStorage.setItem(videoQualityKey, '' + value); } + getVideoQualityValue(): number { + return parseInt(localStorage.getItem(videoQualityKey) || '20'); + } + + setAudioPlayerVolume(value: number): void { + localStorage.setItem(audioPlayerVolumeKey, '' + value); + } + getAudioPlayerVolume(): number { + return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || '1'); + } + + setAudioPlayerMuted(value: boolean): void { + localStorage.setItem(audioPlayerMuteKey, value.toString()); + } + getAudioPlayerMuted(): boolean { + return localStorage.getItem(audioPlayerMuteKey) === 'true'; + } } -export const localUserStore = new LocalUserStore(); \ No newline at end of file +export const localUserStore = new LocalUserStore(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 512319c6..9321dbce 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -29,7 +29,9 @@ import { ON_ACTION_TRIGGER_BUTTON, TRIGGER_JITSI_PROPERTIES, TRIGGER_WEBSITE_PROPERTIES, - WEBSITE_MESSAGE_PROPERTIES + WEBSITE_MESSAGE_PROPERTIES, + AUDIO_VOLUME_PROPERTY, + AUDIO_LOOP_PROPERTY } from "../../WebRtc/LayoutManager"; import {GameMap} from "./GameMap"; import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; @@ -455,16 +457,12 @@ export class GameScene extends ResizableScene implements CenterListener { }); this.connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { - audioManager.decreaseVolume(); this.shareGroupPosition(groupPositionMessage); - this.openChatIcon.setVisible(true); }) this.connection.onGroupDeleted((groupId: number) => { - audioManager.restoreVolume(); try { this.deleteGroup(groupId); - this.openChatIcon.setVisible(false); } catch (e) { console.error(e); } @@ -517,11 +515,15 @@ export class GameScene extends ResizableScene implements CenterListener { onConnect(user: UserSimplePeerInterface) { self.presentationModeSprite.setVisible(true); self.chatModeSprite.setVisible(true); + self.openChatIcon.setVisible(true); + audioManager.decreaseVolume(); }, onDisconnect(userId: number) { if (self.simplePeer.getNbConnections() === 0) { self.presentationModeSprite.setVisible(false); self.chatModeSprite.setVisible(false); + self.openChatIcon.setVisible(false); + audioManager.restoreVolume(); } } }) @@ -630,7 +632,8 @@ export class GameScene extends ResizableScene implements CenterListener { }else{ const openJitsiRoomFunction = () => { const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance); - if (JITSI_PRIVATE_MODE) { + const jitsiUrl = allProps.get("jitsiUrl") as string|undefined; + if (JITSI_PRIVATE_MODE && !jitsiUrl) { const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined; this.connection.emitQueryJitsiJwtMessage(roomName, adminTag); @@ -661,14 +664,15 @@ export class GameScene extends ResizableScene implements CenterListener { this.connection.setSilent(true); } }); - this.gameMap.onPropertyChange('playAudio', (newValue, oldValue) => { - newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl()); + this.gameMap.onPropertyChange('playAudio', (newValue, oldValue, allProps) => { + const volume = allProps.get(AUDIO_VOLUME_PROPERTY) as number|undefined; + const loop = allProps.get(AUDIO_LOOP_PROPERTY) as boolean|undefined; + newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), volume, loop); }); - + // TODO: This legacy property should be removed at some point this.gameMap.onPropertyChange('playAudioLoop', (newValue, oldValue) => { - newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl()); + newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), undefined, true); }); - } private getMapDirUrl(): string { @@ -1206,8 +1210,9 @@ export class GameScene extends ResizableScene implements CenterListener { const allProps = this.gameMap.getCurrentProperties(); const jitsiConfig = this.safeParseJSONstring(allProps.get("jitsiConfig") as string|undefined, 'jitsiConfig'); const jitsiInterfaceConfig = this.safeParseJSONstring(allProps.get("jitsiInterfaceConfig") as string|undefined, 'jitsiInterfaceConfig'); + const jitsiUrl = allProps.get("jitsiUrl") as string|undefined; - jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig); + jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl); this.connection.setSilent(true); mediaManager.hideGameOverlay(); diff --git a/front/src/WebRtc/AudioManager.ts b/front/src/WebRtc/AudioManager.ts index 24fb74a1..60255a77 100644 --- a/front/src/WebRtc/AudioManager.ts +++ b/front/src/WebRtc/AudioManager.ts @@ -1,5 +1,6 @@ import {HtmlUtils} from "./HtmlUtils"; import {isUndefined} from "generic-type-guard"; +import {localUserStore} from "../Connexion/LocalUserStore"; enum audioStates { closed = 0, @@ -9,6 +10,8 @@ enum audioStates { const audioPlayerDivId = "audioplayer"; const audioPlayerCtrlId = "audioplayerctrl"; +const audioPlayerVolId = "audioplayer_volume"; +const audioPlayerMuteId = "audioplayer_volume_icon_playing"; const animationTime = 500; class AudioManager { @@ -17,6 +20,8 @@ class AudioManager { private audioPlayerDiv: HTMLDivElement; private audioPlayerCtrl: HTMLDivElement; private audioPlayerElem: HTMLAudioElement | undefined; + private audioPlayerVol: HTMLInputElement; + private audioPlayerMute: HTMLInputElement; private volume = 1; private muted = false; @@ -26,19 +31,19 @@ class AudioManager { constructor() { this.audioPlayerDiv = HtmlUtils.getElementByIdOrFail(audioPlayerDivId); this.audioPlayerCtrl = HtmlUtils.getElementByIdOrFail(audioPlayerCtrlId); + this.audioPlayerVol = HtmlUtils.getElementByIdOrFail(audioPlayerVolId); + this.audioPlayerMute = HtmlUtils.getElementByIdOrFail(audioPlayerMuteId); - const storedVolume = localStorage.getItem('volume') - if (storedVolume === null) { - this.setVolume(1); - } else { - this.volume = parseFloat(storedVolume); - HtmlUtils.getElementByIdOrFail('audioplayer_volume').value = storedVolume; + this.volume = localUserStore.getAudioPlayerVolume(); + this.audioPlayerVol.value = '' + this.volume; + + this.muted = localUserStore.getAudioPlayerMuted(); + if (this.muted) { + this.audioPlayerMute.classList.add('muted'); } - - HtmlUtils.getElementByIdOrFail('audioplayer_volume').value = '' + this.volume; } - public playAudio(url: string|number|boolean, mapDirUrl: string, loop=false): void { + public playAudio(url: string|number|boolean, mapDirUrl: string, volume: number|undefined, loop=false): void { const audioPath = url as string; let realAudioPath = ''; @@ -50,7 +55,7 @@ class AudioManager { realAudioPath = mapDirUrl + '/' + url; } - this.loadAudio(realAudioPath); + this.loadAudio(realAudioPath, volume); if (loop) { this.loop(); @@ -75,26 +80,29 @@ class AudioManager { } private changeVolume(talking = false): void { - if (!isUndefined(this.audioPlayerElem)) { - this.audioPlayerElem.volume = this.naturalVolume(talking && this.decreaseWhileTalking); - this.audioPlayerElem.muted = this.muted; + if (isUndefined(this.audioPlayerElem)) { + return; } - } - private naturalVolume(makeSofter: boolean = false): number { - const volume = this.volume - const retVol = makeSofter && !this.volumeReduced ? Math.pow(volume * 0.5, 3) : volume - this.volumeReduced = makeSofter - return retVol; + const reduceVolume = talking && this.decreaseWhileTalking; + if (reduceVolume && !this.volumeReduced) { + this.volume *= 0.5; + } else if (!reduceVolume && this.volumeReduced) { + this.volume *= 2.0; + } + this.volumeReduced = reduceVolume; + + this.audioPlayerElem.volume = this.volume; + this.audioPlayerVol.value = '' + this.volume; + this.audioPlayerElem.muted = this.muted; } private setVolume(volume: number): void { this.volume = volume; - localStorage.setItem('volume', '' + volume); + localUserStore.setAudioPlayerVolume(volume); } - - private loadAudio(url: string): void { + private loadAudio(url: string, volume: number|undefined): void { this.load(); /* Solution 1, remove whole audio player */ @@ -112,23 +120,24 @@ class AudioManager { this.audioPlayerElem.append(srcElem); this.audioPlayerDiv.append(this.audioPlayerElem); + this.volume = volume ? Math.min(volume, this.volume) : this.volume; this.changeVolume(); this.audioPlayerElem.play(); const muteElem = HtmlUtils.getElementByIdOrFail('audioplayer_mute'); - muteElem.onclick = (ev: Event)=> { + muteElem.onclick = (ev: Event) => { this.muted = !this.muted; this.changeVolume(); + localUserStore.setAudioPlayerMuted(this.muted); if (this.muted) { - HtmlUtils.getElementByIdOrFail('audioplayer_volume_icon_playing').classList.add('muted'); + this.audioPlayerMute.classList.add('muted'); } else { - HtmlUtils.getElementByIdOrFail('audioplayer_volume_icon_playing').classList.remove('muted'); + this.audioPlayerMute.classList.remove('muted'); } } - const volumeElem = HtmlUtils.getElementByIdOrFail('audioplayer_volume'); - volumeElem.oninput = (ev: Event)=> { + this.audioPlayerVol.oninput = (ev: Event)=> { this.setVolume(parseFloat((ev.currentTarget).value)); this.changeVolume(); diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts index 70ee66bf..983b08e2 100644 --- a/front/src/WebRtc/JitsiFactory.ts +++ b/front/src/WebRtc/JitsiFactory.ts @@ -72,6 +72,7 @@ class JitsiFactory { private audioCallback = this.onAudioChange.bind(this); private videoCallback = this.onVideoChange.bind(this); private previousConfigMeet? : jitsiConfigInterface; + private jitsiScriptLoaded: boolean = false; /** * Slugifies the room name and prepends the room name with the instance @@ -80,11 +81,11 @@ class JitsiFactory { return slugify(instance.replace('/', '-') + "-" + roomName); } - public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object): void { + public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object, jitsiUrl?: string): void { //save previous config this.previousConfigMeet = getDefaultConfig(); - coWebsiteManager.insertCoWebsite((cowebsiteDiv => { + coWebsiteManager.insertCoWebsite((async cowebsiteDiv => { // Jitsi meet external API maintains some data in local storage // which is sent via the appData URL parameter when joining a // conference. Problem is that this data grows indefinitely. Thus @@ -93,7 +94,12 @@ class JitsiFactory { // clear jitsi local storage before starting a new conference. window.localStorage.removeItem("jitsiLocalStorage"); - const domain = JITSI_URL; + const domain = jitsiUrl || JITSI_URL; + if (domain === undefined) { + throw new Error('Missing JITSI_URL environment variable or jitsiUrl parameter in the map.') + } + await this.loadJitsiScript(domain); + const options: any = { // eslint-disable-line @typescript-eslint/no-explicit-any roomName: roomName, jwt: jwt, @@ -157,6 +163,32 @@ class JitsiFactory { mediaManager.enableCamera(); } } + + private async loadJitsiScript(domain: string): Promise { + return new Promise((resolve, reject) => { + if (this.jitsiScriptLoaded) { + resolve(); + return; + } + + this.jitsiScriptLoaded = true; + + // Load Jitsi if the environment variable is set. + const jitsiScript = document.createElement('script'); + jitsiScript.src = 'https://' + domain + '/external_api.js'; + jitsiScript.onload = () => { + resolve(); + } + jitsiScript.onerror = () => { + reject(); + } + + document.head.appendChild(jitsiScript); + + }) + + + } } export const jitsiFactory = new JitsiFactory(); diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts index 91d78798..233b5327 100644 --- a/front/src/WebRtc/LayoutManager.ts +++ b/front/src/WebRtc/LayoutManager.ts @@ -31,6 +31,9 @@ export const TRIGGER_JITSI_PROPERTIES = 'jitsiTrigger'; export const WEBSITE_MESSAGE_PROPERTIES = 'openWebsiteTriggerMessage'; export const JITSI_MESSAGE_PROPERTIES = 'jitsiTriggerMessage'; +export const AUDIO_VOLUME_PROPERTY = 'audioVolume'; +export const AUDIO_LOOP_PROPERTY = 'audioLoop'; + /** * 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. diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 2baeef11..73a87d14 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -63,7 +63,7 @@ export class SimplePeer { } public getNbConnections(): number { - return this.PeerConnectionArray.size; + return this.Users.length; } /** @@ -230,9 +230,6 @@ export class SimplePeer { this.closeScreenSharingConnection(userId); - for (const peerConnectionListener of this.peerConnectionListeners) { - peerConnectionListener.onDisconnect(userId); - } const userIndex = this.Users.findIndex(user => user.userId === userId); if(userIndex < 0){ throw 'Couln\'t delete user'; @@ -250,6 +247,10 @@ export class SimplePeer { this.PeerScreenSharingConnectionArray.delete(userId); } } + + for (const peerConnectionListener of this.peerConnectionListeners) { + peerConnectionListener.onDisconnect(userId); + } } /** diff --git a/front/src/index.ts b/front/src/index.ts index 1b8b7205..c313c301 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -16,13 +16,6 @@ import {HelpCameraSettingsScene} from "./Phaser/Menu/HelpCameraSettingsScene"; import {localUserStore} from "./Connexion/LocalUserStore"; import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene"; -// Load Jitsi if the environment variable is set. -if (JITSI_URL) { - const jitsiScript = document.createElement('script'); - jitsiScript.src = 'https://' + JITSI_URL + '/external_api.js'; - document.head.appendChild(jitsiScript); -} - const {width, height} = coWebsiteManager.getGameSize(); const valueGameQuality = localUserStore.getGameQualityValue(); diff --git a/maps/tests/jitsi_custom_url.json b/maps/tests/jitsi_custom_url.json new file mode 100644 index 00000000..65e3be9f --- /dev/null +++ b/maps/tests/jitsi_custom_url.json @@ -0,0 +1,94 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":10, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":10, + "id":1, + "name":"floor", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":2, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":5, + "name":"jitsiConf", + "opacity":1, + "properties":[ + { + "name":"jitsiRoom", + "type":"string", + "value":"myRoom" + }, + { + "name":"jitsiUrl", + "type":"string", + "value":"meet.jit.si" + }], + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"floorLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":6, + "nextobjectid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.3.3", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.2, + "width":10 +} \ No newline at end of file