Merge branch 'develop' of github.com:thecodingmachine/workadventure into iframe_api

# Conflicts:
#	front/dist/.gitignore
#	front/dist/index.tmpl.html
#	front/src/Phaser/Game/GameScene.ts
#	front/src/WebRtc/CoWebsiteManager.ts
This commit is contained in:
David Négrier 2021-03-28 16:53:15 +02:00
commit aaaa192b71
46 changed files with 1223 additions and 403 deletions

View file

@ -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<HTMLDivElement>(audioPlayerDivId);
this.audioPlayerCtrl = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(audioPlayerCtrlId);
this.audioPlayerVol = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(audioPlayerVolId);
this.audioPlayerMute = HtmlUtils.getElementByIdOrFail<HTMLInputElement>(audioPlayerMuteId);
const storedVolume = localStorage.getItem('volume')
if (storedVolume === null) {
this.setVolume(1);
} else {
this.volume = parseFloat(storedVolume);
HtmlUtils.getElementByIdOrFail<HTMLInputElement>('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<HTMLInputElement>('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<HTMLInputElement>('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<HTMLInputElement>('audioplayer_volume_icon_playing').classList.add('muted');
this.audioPlayerMute.classList.add('muted');
} else {
HtmlUtils.getElementByIdOrFail<HTMLInputElement>('audioplayer_volume_icon_playing').classList.remove('muted');
this.audioPlayerMute.classList.remove('muted');
}
}
const volumeElem = HtmlUtils.getElementByIdOrFail<HTMLInputElement>('audioplayer_volume');
volumeElem.oninput = (ev: Event)=> {
this.audioPlayerVol.oninput = (ev: Event)=> {
this.setVolume(parseFloat((<HTMLInputElement>ev.currentTarget).value));
this.changeVolume();

View file

@ -1,37 +1,109 @@
import {HtmlUtils} from "./HtmlUtils";
import {Subject} from "rxjs";
import {iframeListener} from "../Api/IframeListener";
export type CoWebsiteStateChangedCallback = () => void;
enum iframeStates {
closed = 1,
loading, // loading an iframe can be slow, so we show some placeholder until it is ready
opened,
}
const cowebsiteDivId = "cowebsite"; // the id of the parent div of the iframe.
const cowebsiteDivId = 'cowebsite'; // the id of the whole container.
const cowebsiteMainDomId = 'cowebsite-main'; // the id of the parent div of the iframe.
const cowebsiteAsideDomId = 'cowebsite-aside'; // the id of the parent div of the iframe.
const cowebsiteCloseButtonId = 'cowebsite-close';
const cowebsiteFullScreenButtonId = 'cowebsite-fullscreen';
const cowebsiteOpenFullScreenImageId = 'cowebsite-fullscreen-open';
const cowebsiteCloseFullScreenImageId = 'cowebsite-fullscreen-close';
const animationTime = 500; //time used by the css transitions, in ms.
class CoWebsiteManager {
private opened: iframeStates = iframeStates.closed;
private observers = new Array<CoWebsiteStateChangedCallback>();
private _onResize: Subject<void> = new Subject();
public onResize = this._onResize.asObservable();
/**
* Quickly going in and out of an iframe trigger can create conflicts between the iframe states.
* So we use this promise to queue up every cowebsite state transition
*/
private currentOperationPromise: Promise<void> = Promise.resolve();
private cowebsiteDiv: HTMLDivElement;
private resizing: boolean = false;
private cowebsiteMainDom: HTMLDivElement;
private cowebsiteAsideDom: HTMLDivElement;
get width(): number {
return this.cowebsiteDiv.clientWidth;
}
set width(width: number) {
this.cowebsiteDiv.style.width = width+'px';
}
get height(): number {
return this.cowebsiteDiv.clientHeight;
}
set height(height: number) {
this.cowebsiteDiv.style.height = height+'px';
}
get verticalMode(): boolean {
return window.innerWidth < window.innerHeight;
}
get isFullScreen(): boolean {
return this.verticalMode ? this.height === window.innerHeight : this.width === window.innerWidth;
}
constructor() {
this.cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId);
this.cowebsiteMainDom = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteMainDomId);
this.cowebsiteAsideDom = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteAsideDomId);
this.initResizeListeners();
HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId).addEventListener('click', () => {
this.closeCoWebsite();
});
HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId).addEventListener('click', () => {
this.fullscreen();
});
}
private initResizeListeners() {
const movecallback = (event:MouseEvent) => {
this.verticalMode ? this.height -= event.movementY / this.getDevicePixelRatio() : this.width -= event.movementX / this.getDevicePixelRatio();
this.fire();
}
this.cowebsiteAsideDom.addEventListener('mousedown', (event) => {
this.resizing = true;
this.getIframeDom().style.display = 'none';
document.addEventListener('mousemove', movecallback);
});
document.addEventListener('mouseup', (event) => {
if (!this.resizing) return;
document.removeEventListener('mousemove', movecallback);
this.getIframeDom().style.display = 'block';
this.resizing = false;
});
}
private getDevicePixelRatio(): number {
//on chrome engines, movementX and movementY return global screens coordinates while other browser return pixels
//so on chrome-based browser we need to adjust using 'devicePixelRatio'
return window.navigator.userAgent.includes('Firefox') ? 1 : window.devicePixelRatio;
}
private close(): void {
this.cowebsiteDiv.classList.remove('loaded'); //edit the css class to trigger the transition
this.cowebsiteDiv.classList.add('hidden');
this.opened = iframeStates.closed;
this.resetStyle();
}
private load(): void {
this.cowebsiteDiv.classList.remove('hidden'); //edit the css class to trigger the transition
@ -41,18 +113,23 @@ class CoWebsiteManager {
private open(): void {
this.cowebsiteDiv.classList.remove('loading', 'hidden'); //edit the css class to trigger the transition
this.opened = iframeStates.opened;
this.resetStyle();
}
public resetStyle() {
this.cowebsiteDiv.style.width = '';
this.cowebsiteDiv.style.height = '';
}
private getIframeDom(): HTMLIFrameElement {
const iframe = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId).querySelector('iframe');
if (!iframe) throw new Error('Could not find iframe!');
return iframe;
}
public loadCoWebsite(url: string, base: string, allowApi?: boolean, allowPolicy?: string): void {
this.load();
this.cowebsiteDiv.innerHTML = `<button class="close-btn" id="cowebsite-close">
<img src="resources/logos/close.svg">
</button>`;
setTimeout(() => {
HtmlUtils.getElementByIdOrFail('cowebsite-close').addEventListener('click', () => {
this.closeCoWebsite();
});
}, 100);
this.cowebsiteMainDom.innerHTML = ``;
const iframe = document.createElement('iframe');
iframe.id = 'cowebsite-iframe';
@ -66,7 +143,7 @@ class CoWebsiteManager {
if (allowApi) {
iframeListener.registerIframe(iframe);
}
this.cowebsiteDiv.appendChild(iframe);
this.cowebsiteMainDom.appendChild(iframe);
const onTimeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve(), 2000);
});
@ -83,7 +160,8 @@ class CoWebsiteManager {
*/
public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise<void>): void {
this.load();
this.currentOperationPromise = this.currentOperationPromise.then(() => callback(this.cowebsiteDiv)).then(() => {
this.cowebsiteMainDom.innerHTML = ``;
this.currentOperationPromise = this.currentOperationPromise.then(() => callback(this.cowebsiteMainDom)).then(() => {
this.open();
setTimeout(() => {
this.fire();
@ -101,9 +179,7 @@ class CoWebsiteManager {
iframeListener.unregisterIframe(iframe);
}
setTimeout(() => {
this.cowebsiteDiv.innerHTML = `<button class="close-btn" id="cowebsite-close">
<img src="resources/logos/close.svg">
</button>`;
this.cowebsiteMainDom.innerHTML = ``;
resolve();
}, animationTime)
}));
@ -117,27 +193,35 @@ class CoWebsiteManager {
height: window.innerHeight
}
}
if (window.innerWidth >= window.innerHeight) {
if (!this.verticalMode) {
return {
width: window.innerWidth / 2,
width: window.innerWidth - this.width,
height: window.innerHeight
}
} else {
return {
width: window.innerWidth,
height: window.innerHeight / 2
height: window.innerHeight - this.height,
}
}
}
//todo: is it still useful to allow any kind of observers?
public onStateChange(observer: CoWebsiteStateChangedCallback) {
this.observers.push(observer);
private fire(): void {
this._onResize.next();
}
private fire(): void {
for (const callback of this.observers) {
callback();
private fullscreen(): void {
if (this.isFullScreen) {
this.resetStyle();
this.fire();
//we don't trigger a resize of the phaser game since it won't be visible anyway.
HtmlUtils.getElementByIdOrFail(cowebsiteOpenFullScreenImageId).style.display = 'inline';
HtmlUtils.getElementByIdOrFail(cowebsiteCloseFullScreenImageId).style.display = 'none';
} else {
this.verticalMode ? this.height = window.innerHeight : this.width = window.innerWidth;
//we don't trigger a resize of the phaser game since it won't be visible anyway.
HtmlUtils.getElementByIdOrFail(cowebsiteOpenFullScreenImageId).style.display = 'none';
HtmlUtils.getElementByIdOrFail(cowebsiteCloseFullScreenImageId).style.display = 'inline';
}
}
}

View file

@ -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<void> {
return new Promise<void>((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();

View file

@ -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.

View file

@ -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);
}
}
/**