From f5f71f32eed44fa2feb13cd55a0341fb5e55dcc5 Mon Sep 17 00:00:00 2001 From: Alexis Faizeau Date: Tue, 8 Feb 2022 11:19:29 +0100 Subject: [PATCH 1/2] Fix cowebsite closing on fast opening/closing --- .../Layouts/PresentationLayout.svelte | 8 +- .../Phaser/Game/GameMapPropertiesListener.ts | 55 ++---- front/src/Phaser/Game/GameScene.ts | 18 +- front/src/WebRtc/CoWebsiteManager.ts | 161 ++++++++---------- front/src/WebRtc/JitsiFactory.ts | 10 +- 5 files changed, 97 insertions(+), 155 deletions(-) diff --git a/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte b/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte index f4a9f939..1ecb53e0 100644 --- a/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte +++ b/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte @@ -10,13 +10,9 @@ function closeCoWebsite() { if ($highlightedEmbedScreen?.type === "cowebsite") { if ($highlightedEmbedScreen.embed.closable) { - coWebsiteManager.closeCoWebsite($highlightedEmbedScreen.embed).catch(() => { - console.error("Error during co-website highlighted closing"); - }); + coWebsiteManager.closeCoWebsite($highlightedEmbedScreen.embed); } else { - coWebsiteManager.unloadCoWebsite($highlightedEmbedScreen.embed).catch(() => { - console.error("Error during co-website highlighted unloading"); - }); + coWebsiteManager.unloadCoWebsite($highlightedEmbedScreen.embed); } } } diff --git a/front/src/Phaser/Game/GameMapPropertiesListener.ts b/front/src/Phaser/Game/GameMapPropertiesListener.ts index 497b6cbc..6ac00665 100644 --- a/front/src/Phaser/Game/GameMapPropertiesListener.ts +++ b/front/src/Phaser/Game/GameMapPropertiesListener.ts @@ -9,17 +9,12 @@ import { get } from "svelte/store"; import { ON_ACTION_TRIGGER_BUTTON, ON_ICON_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager"; import type { ITiledMapLayer } from "../Map/ITiledMap"; import { GameMapProperties } from "./GameMapProperties"; - -enum OpenCoWebsiteState { - ASLEEP, - OPENED, - MUST_BE_CLOSE, -} +import type CancelablePromise from "cancelable-promise"; interface OpenCoWebsite { actionId: string; coWebsite?: CoWebsite; - state: OpenCoWebsiteState; + loadPromise?: CancelablePromise; } export class GameMapPropertiesListener { @@ -106,34 +101,16 @@ export class GameMapPropertiesListener { return; } - this.coWebsitesOpenByLayer.set(layer, { + const coWebsiteOpen: OpenCoWebsite = { actionId: actionId, - coWebsite: undefined, - state: OpenCoWebsiteState.ASLEEP, - }); + }; + + this.coWebsitesOpenByLayer.set(layer, coWebsiteOpen); const loadCoWebsiteFunction = (coWebsite: CoWebsite) => { - coWebsiteManager - .loadCoWebsite(coWebsite) - .then((coWebsite) => { - const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer); - if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) { - coWebsiteManager.closeCoWebsite(coWebsite).catch(() => { - console.error("Error during a co-website closing"); - }); - this.coWebsitesOpenByLayer.delete(layer); - this.coWebsitesActionTriggerByLayer.delete(layer); - } else { - this.coWebsitesOpenByLayer.set(layer, { - actionId, - coWebsite, - state: OpenCoWebsiteState.OPENED, - }); - } - }) - .catch(() => { - console.error("Error during loading a co-website: " + coWebsite.url); - }); + coWebsiteOpen.loadPromise = coWebsiteManager.loadCoWebsite(coWebsite).catch(() => { + console.error("Error during loading a co-website: " + coWebsite.url); + }); layoutManagerActionStore.removeAction(actionId); }; @@ -149,6 +126,8 @@ export class GameMapPropertiesListener { false ); + coWebsiteOpen.coWebsite = coWebsite; + loadCoWebsiteFunction(coWebsite); }; @@ -180,11 +159,7 @@ export class GameMapPropertiesListener { false ); - const ObjectByLayer = this.coWebsitesOpenByLayer.get(layer); - - if (ObjectByLayer) { - ObjectByLayer.coWebsite = coWebsite; - } + coWebsiteOpen.coWebsite = coWebsite; } if (!websiteTriggerProperty) { @@ -228,12 +203,12 @@ export class GameMapPropertiesListener { return; } - if (coWebsiteOpen.state === OpenCoWebsiteState.ASLEEP) { - coWebsiteOpen.state = OpenCoWebsiteState.MUST_BE_CLOSE; + if (coWebsiteOpen.loadPromise !== undefined) { + coWebsiteOpen.loadPromise.cancel(); } if (coWebsiteOpen.coWebsite !== undefined) { - coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite).catch((e) => console.error(e)); + coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite); } this.coWebsitesOpenByLayer.delete(layer); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 9b758a45..39b84b76 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1289,22 +1289,18 @@ ${escapedMessage} }); }); - iframeListener.registerAnswerer("closeCoWebsite", async (coWebsiteId) => { + iframeListener.registerAnswerer("closeCoWebsite", (coWebsiteId) => { const coWebsite = coWebsiteManager.getCoWebsiteById(coWebsiteId); if (!coWebsite) { throw new Error("Unknown co-website"); } - return coWebsiteManager.closeCoWebsite(coWebsite).catch((error) => { - throw new Error("Error on closing co-website"); - }); + return coWebsiteManager.closeCoWebsite(coWebsite); }); - iframeListener.registerAnswerer("closeCoWebsites", async () => { - return await coWebsiteManager.closeCoWebsites().catch((error) => { - throw new Error("Error on closing all co-websites"); - }); + iframeListener.registerAnswerer("closeCoWebsites", () => { + return coWebsiteManager.closeCoWebsites(); }); iframeListener.registerAnswerer("getMapData", () => { @@ -1568,7 +1564,7 @@ ${escapedMessage} public cleanupClosingScene(): void { // stop playing audio, close any open website, stop any open Jitsi - coWebsiteManager.closeCoWebsites().catch((e) => console.error(e)); + coWebsiteManager.closeCoWebsites(); // Stop the script, if any const scripts = this.getScriptUrls(this.mapFile); for (const script of scripts) { @@ -2138,9 +2134,7 @@ ${escapedMessage} public stopJitsi(): void { const coWebsite = coWebsiteManager.searchJitsi(); if (coWebsite) { - coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => { - console.error("Error during Jitsi co-website closing", e); - }); + coWebsiteManager.closeCoWebsite(coWebsite); } } diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index 476526da..0758409f 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -9,6 +9,7 @@ import { isMediaBreakpointDown } from "../Utils/BreakpointsUtils"; import { jitsiFactory } from "./JitsiFactory"; import { gameManager } from "../Phaser/Game/GameManager"; import { LayoutMode } from "./LayoutManager"; +import CancelablePromise from "cancelable-promise"; export enum iframeStates { closed = 1, @@ -53,11 +54,7 @@ class CoWebsiteManager { private _onResize: Subject = 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 = Promise.resolve(); + private cowebsiteDom: HTMLDivElement; private resizing: boolean = false; private gameOverlayDom: HTMLDivElement; @@ -148,13 +145,9 @@ class CoWebsiteManager { } if (coWebsite.closable) { - this.closeCoWebsite(coWebsite).catch(() => { - console.error("Error during closing a co-website by a button"); - }); + this.closeCoWebsite(coWebsite); } else { - this.unloadCoWebsite(coWebsite).catch(() => { - console.error("Error during unloading a co-website by a button"); - }); + this.unloadCoWebsite(coWebsite); } }); @@ -647,7 +640,7 @@ class CoWebsiteManager { return newCoWebsite; } - public loadCoWebsite(coWebsite: CoWebsite): Promise { + public loadCoWebsite(coWebsite: CoWebsite): CancelablePromise { if (get(coWebsitesNotAsleep).length < 1) { coWebsites.remove(coWebsite); coWebsites.add(coWebsite, 0); @@ -663,7 +656,7 @@ class CoWebsiteManager { const mainCoWebsite = this.getMainCoWebsite(); - return new Promise((resolve, reject) => { + return new CancelablePromise((resolve, reject, cancel) => { const onloadPromise = new Promise((resolve) => { coWebsite.iframe.onload = () => { coWebsite.state.set("ready"); @@ -683,8 +676,7 @@ class CoWebsiteManager { jitsiFactory.restart(); } - this.currentOperationPromise = this.currentOperationPromise - .then(() => Promise.race([onloadPromise, onTimeoutPromise])) + const race = CancelablePromise.race([onloadPromise, onTimeoutPromise]) .then(() => { if (mainCoWebsite && mainCoWebsite.iframe.id === coWebsite.iframe.id) { this.openMain(); @@ -701,83 +693,72 @@ class CoWebsiteManager { this.removeCoWebsiteFromStack(coWebsite); return reject(); }); - }); - } - public unloadCoWebsite(coWebsite: CoWebsite): Promise { - return new Promise((resolve, reject) => { - this.removeHighlightCoWebsite(coWebsite); - - coWebsite.iframe.parentNode?.removeChild(coWebsite.iframe); - coWebsite.state.set("asleep"); - coWebsites.remove(coWebsite); - - if (coWebsite.jitsi) { - jitsiFactory.stop(); - const gameScene = gameManager.getCurrentGameScene(); - gameScene.enableMediaBehaviors(); - } - - const mainCoWebsite = this.getMainCoWebsite(); - - if (mainCoWebsite) { - this.removeHighlightCoWebsite(mainCoWebsite); - this.goToMain(mainCoWebsite); - this.resizeAllIframes(); - } else { - this.closeMain(); - } - - coWebsites.add(coWebsite, get(coWebsites).length); - - resolve(); - }); - } - - public closeCoWebsite(coWebsite: CoWebsite): Promise { - this.currentOperationPromise = this.currentOperationPromise.then( - () => - new Promise((resolve) => { - if (coWebsite.jitsi) { - jitsiFactory.destroy(); - const gameScene = gameManager.getCurrentGameScene(); - gameScene.enableMediaBehaviors(); - } - - if (get(coWebsites).length === 1) { - this.fire(); - } - - if (coWebsite.allowApi) { - iframeListener.unregisterIframe(coWebsite.iframe); - } - - this.removeCoWebsiteFromStack(coWebsite); - - const mainCoWebsite = this.getMainCoWebsite(); - - if (mainCoWebsite) { - this.removeHighlightCoWebsite(mainCoWebsite); - this.goToMain(mainCoWebsite); - this.resizeAllIframes(); - } else { - this.closeMain(); - } - resolve(); - }) - ); - return this.currentOperationPromise; - } - - public closeCoWebsites(): Promise { - return (this.currentOperationPromise = this.currentOperationPromise.then(() => { - get(coWebsites).forEach((coWebsite: CoWebsite) => { - this.closeCoWebsite(coWebsite).catch(() => { - console.error("Error during closing a co-website"); - }); + cancel(() => { + race.cancel(); + this.unloadCoWebsite(coWebsite); }); - })); - return this.currentOperationPromise; + }); + } + + public unloadCoWebsite(coWebsite: CoWebsite): void { + this.removeHighlightCoWebsite(coWebsite); + + coWebsite.iframe.parentNode?.removeChild(coWebsite.iframe); + coWebsite.state.set("asleep"); + coWebsites.remove(coWebsite); + + if (coWebsite.jitsi) { + jitsiFactory.stop(); + const gameScene = gameManager.getCurrentGameScene(); + gameScene.enableMediaBehaviors(); + } + + const mainCoWebsite = this.getMainCoWebsite(); + + if (mainCoWebsite) { + this.removeHighlightCoWebsite(mainCoWebsite); + this.goToMain(mainCoWebsite); + this.resizeAllIframes(); + } else { + this.closeMain(); + } + + coWebsites.add(coWebsite, get(coWebsites).length); + } + + public closeCoWebsite(coWebsite: CoWebsite): void { + if (coWebsite.jitsi) { + jitsiFactory.destroy(); + const gameScene = gameManager.getCurrentGameScene(); + gameScene.enableMediaBehaviors(); + } + + if (get(coWebsites).length === 1) { + this.fire(); + } + + if (coWebsite.allowApi) { + iframeListener.unregisterIframe(coWebsite.iframe); + } + + this.removeCoWebsiteFromStack(coWebsite); + + const mainCoWebsite = this.getMainCoWebsite(); + + if (mainCoWebsite) { + this.removeHighlightCoWebsite(mainCoWebsite); + this.goToMain(mainCoWebsite); + this.resizeAllIframes(); + } else { + this.closeMain(); + } + } + + public closeCoWebsites(): void { + get(coWebsites).forEach((coWebsite: CoWebsite) => { + this.closeCoWebsite(coWebsite); + }); } public getGameSize(): { width: number; height: number } { diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts index 8f7ed952..a8f10e10 100644 --- a/front/src/WebRtc/JitsiFactory.ts +++ b/front/src/WebRtc/JitsiFactory.ts @@ -145,7 +145,7 @@ class JitsiFactory { const coWebsite = coWebsiteManager.searchJitsi(); if (coWebsite) { - await coWebsiteManager.closeCoWebsite(coWebsite); + coWebsiteManager.closeCoWebsite(coWebsite); } // Jitsi meet external API maintains some data in local storage @@ -214,13 +214,9 @@ class JitsiFactory { private closeOrUnload = function (coWebsite: CoWebsite) { if (coWebsite.closable) { - coWebsiteManager.closeCoWebsite(coWebsite).catch(() => { - console.error("Error during closing a Jitsi Meet"); - }); + coWebsiteManager.closeCoWebsite(coWebsite); } else { - coWebsiteManager.unloadCoWebsite(coWebsite).catch(() => { - console.error("Error during unloading a Jitsi Meet"); - }); + coWebsiteManager.unloadCoWebsite(coWebsite); } }; From 7b6a3949bc95f22d2bf6a025d293b494f2266ff9 Mon Sep 17 00:00:00 2001 From: Alexis Faizeau Date: Thu, 10 Feb 2022 11:40:36 +0100 Subject: [PATCH 2/2] Move CoWebsite to generic class --- .../CoWebsiteThumbnailSlot.svelte | 27 +- .../EmbedScreens/CoWebsitesContainer.svelte | 2 +- .../Layouts/PresentationLayout.svelte | 10 +- .../Phaser/Game/GameMapPropertiesListener.ts | 33 +- front/src/Phaser/Game/GameScene.ts | 64 +++- front/src/Stores/CoWebsiteStore.ts | 15 +- front/src/Stores/EmbedScreensStore.ts | 4 +- front/src/WebRtc/CoWebsite/CoWesbite.ts | 17 + front/src/WebRtc/CoWebsite/JitsiCoWebsite.ts | 61 ++++ front/src/WebRtc/CoWebsite/SimpleCoWebsite.ts | 133 +++++++ front/src/WebRtc/CoWebsiteManager.ts | 337 +++++++----------- front/src/WebRtc/JitsiFactory.ts | 154 ++++---- 12 files changed, 508 insertions(+), 349 deletions(-) create mode 100644 front/src/WebRtc/CoWebsite/CoWesbite.ts create mode 100644 front/src/WebRtc/CoWebsite/JitsiCoWebsite.ts create mode 100644 front/src/WebRtc/CoWebsite/SimpleCoWebsite.ts diff --git a/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte b/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte index b71a35c0..2043315b 100644 --- a/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte +++ b/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte @@ -2,9 +2,9 @@ import { onMount } from "svelte"; import { ICON_URL } from "../../Enum/EnvironmentVariable"; - import { coWebsitesNotAsleep, mainCoWebsite } from "../../Stores/CoWebsiteStore"; + import { coWebsitesNotAsleep, mainCoWebsite, jitsiCoWebsite } from "../../Stores/CoWebsiteStore"; import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore"; - import type { CoWebsite } from "../../WebRtc/CoWebsiteManager"; + import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite"; import { iframeStates } from "../../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; @@ -14,16 +14,15 @@ let icon: HTMLImageElement; let iconLoaded = false; - let state = coWebsite.state; - - const coWebsiteUrl = coWebsite.iframe.src; - const urlObject = new URL(coWebsiteUrl); + let state = coWebsite.getStateSubscriber(); + let isJitsi: boolean = false; onMount(() => { - icon.src = coWebsite.jitsi + isJitsi = Boolean($jitsiCoWebsite && $jitsiCoWebsite.getId() === coWebsite.getId()); + icon.src = isJitsi ? "/resources/logos/meet.svg" - : `${ICON_URL}/icon?url=${urlObject.hostname}&size=64..96..256&fallback_icon_color=14304c`; - icon.alt = coWebsite.altMessage ?? urlObject.hostname; + : `${ICON_URL}/icon?url=${coWebsite.getUrl().hostname}&size=64..96..256&fallback_icon_color=14304c`; + icon.alt = coWebsite.getUrl().hostname; icon.onload = () => { iconLoaded = true; }; @@ -33,10 +32,10 @@ if (vertical) { coWebsiteManager.goToMain(coWebsite); } else if ($mainCoWebsite) { - if ($mainCoWebsite.iframe.id === coWebsite.iframe.id) { + if ($mainCoWebsite.getId() === coWebsite.getId()) { const coWebsites = $coWebsitesNotAsleep; const newMain = $highlightedEmbedScreen ?? coWebsites.length > 1 ? coWebsites[1] : undefined; - if (newMain && newMain.iframe.id !== $mainCoWebsite.iframe.id) { + if (newMain && newMain.getId() !== $mainCoWebsite.getId()) { coWebsiteManager.goToMain(newMain); } else if (coWebsiteManager.getMainState() === iframeStates.closed) { coWebsiteManager.displayMain(); @@ -65,11 +64,11 @@ let isHighlight: boolean = false; let isMain: boolean = false; $: { - isMain = $mainCoWebsite !== undefined && $mainCoWebsite.iframe === coWebsite.iframe; + isMain = $mainCoWebsite !== undefined && $mainCoWebsite.getId() === coWebsite.getId(); isHighlight = $highlightedEmbedScreen !== null && $highlightedEmbedScreen.type === "cowebsite" && - $highlightedEmbedScreen.embed.iframe === coWebsite.iframe; + $highlightedEmbedScreen.embed.getId() === coWebsite.getId(); } @@ -86,7 +85,7 @@ 0}
- {#each [...$coWebsites.values()] as coWebsite, index (coWebsite.iframe.id)} + {#each [...$coWebsites.values()] as coWebsite, index (coWebsite.getId())} {/each}
diff --git a/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte b/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte index 1ecb53e0..dbf7ee71 100644 --- a/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte +++ b/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte @@ -9,10 +9,12 @@ function closeCoWebsite() { if ($highlightedEmbedScreen?.type === "cowebsite") { - if ($highlightedEmbedScreen.embed.closable) { + if ($highlightedEmbedScreen.embed.isClosable()) { coWebsiteManager.closeCoWebsite($highlightedEmbedScreen.embed); } else { - coWebsiteManager.unloadCoWebsite($highlightedEmbedScreen.embed); + coWebsiteManager.unloadCoWebsite($highlightedEmbedScreen.embed).catch((err) => { + console.error("Cannot unload co-website", err); + }); } } } @@ -64,9 +66,9 @@ /> {/key} {:else if $highlightedEmbedScreen.type === "cowebsite"} - {#key $highlightedEmbedScreen.embed.iframe.id} + {#key $highlightedEmbedScreen.embed.getId()}
diff --git a/front/src/Phaser/Game/GameMapPropertiesListener.ts b/front/src/Phaser/Game/GameMapPropertiesListener.ts index 6ac00665..22073610 100644 --- a/front/src/Phaser/Game/GameMapPropertiesListener.ts +++ b/front/src/Phaser/Game/GameMapPropertiesListener.ts @@ -1,7 +1,6 @@ import type { GameScene } from "./GameScene"; import type { GameMap } from "./GameMap"; import { scriptUtils } from "../../Api/ScriptUtils"; -import type { CoWebsite } from "../../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; import { localUserStore } from "../../Connexion/LocalUserStore"; @@ -9,12 +8,12 @@ import { get } from "svelte/store"; import { ON_ACTION_TRIGGER_BUTTON, ON_ICON_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager"; import type { ITiledMapLayer } from "../Map/ITiledMap"; import { GameMapProperties } from "./GameMapProperties"; -import type CancelablePromise from "cancelable-promise"; +import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite"; +import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite"; interface OpenCoWebsite { actionId: string; coWebsite?: CoWebsite; - loadPromise?: CancelablePromise; } export class GameMapPropertiesListener { @@ -108,26 +107,26 @@ export class GameMapPropertiesListener { this.coWebsitesOpenByLayer.set(layer, coWebsiteOpen); const loadCoWebsiteFunction = (coWebsite: CoWebsite) => { - coWebsiteOpen.loadPromise = coWebsiteManager.loadCoWebsite(coWebsite).catch(() => { - console.error("Error during loading a co-website: " + coWebsite.url); + coWebsiteManager.loadCoWebsite(coWebsite).catch(() => { + console.error("Error during loading a co-website: " + coWebsite.getUrl()); }); layoutManagerActionStore.removeAction(actionId); }; const openCoWebsiteFunction = () => { - const coWebsite = coWebsiteManager.addCoWebsite( - openWebsiteProperty ?? "", - this.scene.MapUrlFile, + const coWebsite = new SimpleCoWebsite( + new URL(openWebsiteProperty ?? "", this.scene.MapUrlFile), allowApiProperty, websitePolicyProperty, websiteWidthProperty, - websitePositionProperty, false ); coWebsiteOpen.coWebsite = coWebsite; + coWebsiteManager.addCoWebsiteToStore(coWebsite, websitePositionProperty); + loadCoWebsiteFunction(coWebsite); }; @@ -149,17 +148,17 @@ export class GameMapPropertiesListener { userInputManager: this.scene.userInputManager, }); } else if (websiteTriggerProperty === ON_ICON_TRIGGER_BUTTON) { - const coWebsite = coWebsiteManager.addCoWebsite( - openWebsiteProperty, - this.scene.MapUrlFile, + const coWebsite = new SimpleCoWebsite( + new URL(openWebsiteProperty ?? "", this.scene.MapUrlFile), allowApiProperty, websitePolicyProperty, websiteWidthProperty, - websitePositionProperty, false ); coWebsiteOpen.coWebsite = coWebsite; + + coWebsiteManager.addCoWebsiteToStore(coWebsite, websitePositionProperty); } if (!websiteTriggerProperty) { @@ -203,12 +202,10 @@ export class GameMapPropertiesListener { return; } - if (coWebsiteOpen.loadPromise !== undefined) { - coWebsiteOpen.loadPromise.cancel(); - } + const coWebsite = coWebsiteOpen.coWebsite; - if (coWebsiteOpen.coWebsite !== undefined) { - coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite); + if (coWebsite) { + coWebsiteManager.closeCoWebsite(coWebsite); } this.coWebsitesOpenByLayer.delete(layer); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 39b84b76..443a6256 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -5,7 +5,7 @@ import { get, Unsubscriber } from "svelte/store"; import { userMessageManager } from "../../Administration/UserMessageManager"; import { connectionManager } from "../../Connexion/ConnectionManager"; -import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; +import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; import { urlManager } from "../../Url/UrlManager"; import { mediaManager } from "../../WebRtc/MediaManager"; import { UserInputManager } from "../UserInput/UserInputManager"; @@ -22,7 +22,13 @@ import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/Pl import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager"; import { iframeListener } from "../../Api/IframeListener"; -import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; +import { + DEBUG_MODE, + JITSI_PRIVATE_MODE, + JITSI_URL, + MAX_PER_GROUP, + POSITION_DELAY, +} from "../../Enum/EnvironmentVariable"; import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; import { Room } from "../../Connexion/Room"; import { jitsiFactory } from "../../WebRtc/JitsiFactory"; @@ -92,6 +98,9 @@ import { MapStore } from "../../Stores/Utils/MapStore"; import { followUsersColorStore } from "../../Stores/FollowStore"; import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler"; import { locale } from "../../i18n/i18n-svelte"; +import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite"; +import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite"; +import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite"; export interface GameSceneInitInterface { initPosition: PointInterface | null; reconnecting: boolean; @@ -794,7 +803,19 @@ export class GameScene extends DirtyScene { * Triggered when we receive the JWT token to connect to Jitsi */ this.connection.sendJitsiJwtMessageStream.subscribe((message) => { - this.startJitsi(message.jitsiRoom, message.jwt); + if (!JITSI_URL) { + throw new Error("Missing JITSI_URL environment variable."); + } + + let domain = JITSI_URL; + + if (domain.substring(0, 7) !== "http://" && domain.substring(0, 8) !== "https://") { + domain = `${location.protocol}//${domain}`; + } + + const coWebsite = new JitsiCoWebsite(new URL(domain), false, undefined, undefined, false); + coWebsiteManager.addCoWebsiteToStore(coWebsite, 0); + this.startJitsi(coWebsite, message.jitsiRoom, message.jwt); }); this.messageSubscription = this.connection.worldFullMessageStream.subscribe((message) => { @@ -963,12 +984,25 @@ export class GameScene extends DirtyScene { const openJitsiRoomFunction = () => { const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance); const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined; + if (JITSI_PRIVATE_MODE && !jitsiUrl) { const adminTag = allProps.get(GameMapProperties.JITSI_ADMIN_ROOM_TAG) as string | undefined; this.connection?.emitQueryJitsiJwtMessage(roomName, adminTag); } else { - this.startJitsi(roomName, undefined); + let domain = jitsiUrl || JITSI_URL; + if (domain === undefined) { + throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map."); + } + + if (domain.substring(0, 7) !== "http://" && domain.substring(0, 8) !== "https://") { + domain = `${location.protocol}//${domain}`; + } + + const coWebsite = new JitsiCoWebsite(new URL(domain), false, undefined, undefined, false); + + coWebsiteManager.addCoWebsiteToStore(coWebsite, 0); + this.startJitsi(coWebsite, roomName, undefined); } layoutManagerActionStore.removeAction("jitsi"); }; @@ -1260,13 +1294,11 @@ ${escapedMessage} throw new Error("Unknown query source"); } - const coWebsite = coWebsiteManager.addCoWebsite( - openCoWebsite.url, - iframeListener.getBaseUrlFromSource(source), + const coWebsite: SimpleCoWebsite = new SimpleCoWebsite( + new URL(openCoWebsite.url, iframeListener.getBaseUrlFromSource(source)), openCoWebsite.allowApi, openCoWebsite.allowPolicy, openCoWebsite.widthPercent, - openCoWebsite.position, openCoWebsite.closable ?? true ); @@ -1275,7 +1307,7 @@ ${escapedMessage} } return { - id: coWebsite.iframe.id, + id: coWebsite.getId(), }; }); @@ -1284,7 +1316,7 @@ ${escapedMessage} return coWebsites.map((coWebsite: CoWebsite) => { return { - id: coWebsite.iframe.id, + id: coWebsite.getId(), }; }); }); @@ -2112,7 +2144,7 @@ ${escapedMessage} mediaManager.hideMyCamera(); } - public startJitsi(roomName: string, jwt?: string): void { + public startJitsi(coWebsite: JitsiCoWebsite, roomName: string, jwt?: string): void { const allProps = this.gameMap.getCurrentProperties(); const jitsiConfig = this.safeParseJSONstring( allProps.get(GameMapProperties.JITSI_CONFIG) as string | undefined, @@ -2124,10 +2156,14 @@ ${escapedMessage} ); const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined; - jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl).catch(() => { - console.error("Cannot start a Jitsi co-website"); + coWebsite.setJitsiLoadPromise( + jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl) + ); + + coWebsiteManager.loadCoWebsite(coWebsite).catch((err) => { + console.error(err); }); - this.disableMediaBehaviors(); + analyticsClient.enteredJitsi(roomName, this.room.id); } diff --git a/front/src/Stores/CoWebsiteStore.ts b/front/src/Stores/CoWebsiteStore.ts index 4227c405..7f922db4 100644 --- a/front/src/Stores/CoWebsiteStore.ts +++ b/front/src/Stores/CoWebsiteStore.ts @@ -1,5 +1,6 @@ import { derived, get, writable } from "svelte/store"; -import type { CoWebsite } from "../WebRtc/CoWebsiteManager"; +import type { CoWebsite } from "../WebRtc/CoWebsite/CoWesbite"; +import { JitsiCoWebsite } from "../WebRtc/CoWebsite/JitsiCoWebsite"; function createCoWebsiteStore() { const { subscribe, set, update } = writable(Array()); @@ -9,7 +10,7 @@ function createCoWebsiteStore() { return { subscribe, add: (coWebsite: CoWebsite, position?: number) => { - coWebsite.state.subscribe((value) => { + coWebsite.getStateSubscriber().subscribe((value) => { update((currentArray) => currentArray); }); @@ -31,7 +32,7 @@ function createCoWebsiteStore() { }, remove: (coWebsite: CoWebsite) => { update((currentArray) => [ - ...currentArray.filter((currentCoWebsite) => currentCoWebsite.iframe.id !== coWebsite.iframe.id), + ...currentArray.filter((currentCoWebsite) => currentCoWebsite.getId() !== coWebsite.getId()), ]); }, empty: () => { @@ -43,9 +44,13 @@ function createCoWebsiteStore() { export const coWebsites = createCoWebsiteStore(); export const coWebsitesNotAsleep = derived([coWebsites], ([$coWebsites]) => - $coWebsites.filter((coWebsite) => get(coWebsite.state) !== "asleep") + $coWebsites.filter((coWebsite) => coWebsite.getState() !== "asleep") ); export const mainCoWebsite = derived([coWebsites], ([$coWebsites]) => - $coWebsites.find((coWebsite) => get(coWebsite.state) !== "asleep") + $coWebsites.find((coWebsite) => coWebsite.getState() !== "asleep") +); + +export const jitsiCoWebsite = derived([coWebsites], ([$coWebsites]) => + $coWebsites.find((coWebsite) => coWebsite instanceof JitsiCoWebsite) ); diff --git a/front/src/Stores/EmbedScreensStore.ts b/front/src/Stores/EmbedScreensStore.ts index 0db7c675..724733b3 100644 --- a/front/src/Stores/EmbedScreensStore.ts +++ b/front/src/Stores/EmbedScreensStore.ts @@ -1,5 +1,5 @@ import { derived, get, writable } from "svelte/store"; -import type { CoWebsite } from "../WebRtc/CoWebsiteManager"; +import type { CoWebsite } from "../WebRtc/CoWebsite/CoWesbite"; import { LayoutMode } from "../WebRtc/LayoutManager"; import { coWebsites } from "./CoWebsiteStore"; import { Streamable, streamableCollectionStore } from "./StreamableCollectionStore"; @@ -31,7 +31,7 @@ function createHighlightedEmbedScreenStore() { embedScreen.type !== currentEmbedScreen.type || (embedScreen.type === "cowebsite" && currentEmbedScreen.type === "cowebsite" && - embedScreen.embed.iframe.id !== currentEmbedScreen.embed.iframe.id) || + embedScreen.embed.getId() !== currentEmbedScreen.embed.getId()) || (embedScreen.type === "streamable" && currentEmbedScreen.type === "streamable" && embedScreen.embed.uniqueId !== currentEmbedScreen.embed.uniqueId) diff --git a/front/src/WebRtc/CoWebsite/CoWesbite.ts b/front/src/WebRtc/CoWebsite/CoWesbite.ts new file mode 100644 index 00000000..50ce3b9f --- /dev/null +++ b/front/src/WebRtc/CoWebsite/CoWesbite.ts @@ -0,0 +1,17 @@ +import type CancelablePromise from "cancelable-promise"; +import type { Readable, Writable } from "svelte/store"; + +export type CoWebsiteState = "asleep" | "loading" | "ready"; + +export interface CoWebsite { + getId(): string; + getUrl(): URL; + getState(): CoWebsiteState; + getStateSubscriber(): Readable; + getIframe(): HTMLIFrameElement | undefined; + getLoadIframe(): CancelablePromise | undefined; + getWidthPercent(): number | undefined; + isClosable(): boolean; + load(): CancelablePromise; + unload(): Promise; +} diff --git a/front/src/WebRtc/CoWebsite/JitsiCoWebsite.ts b/front/src/WebRtc/CoWebsite/JitsiCoWebsite.ts new file mode 100644 index 00000000..66491569 --- /dev/null +++ b/front/src/WebRtc/CoWebsite/JitsiCoWebsite.ts @@ -0,0 +1,61 @@ +import CancelablePromise from "cancelable-promise"; +import { gameManager } from "../../Phaser/Game/GameManager"; +import { coWebsiteManager } from "../CoWebsiteManager"; +import { jitsiFactory } from "../JitsiFactory"; +import { SimpleCoWebsite } from "./SimpleCoWebsite"; + +export class JitsiCoWebsite extends SimpleCoWebsite { + private jitsiLoadPromise?: CancelablePromise; + + constructor(url: URL, allowApi?: boolean, allowPolicy?: string, widthPercent?: number, closable?: boolean) { + const coWebsite = coWebsiteManager.searchJitsi(); + + if (coWebsite) { + coWebsiteManager.closeCoWebsite(coWebsite); + } + + super(url, allowApi, allowPolicy, widthPercent, closable); + } + + setJitsiLoadPromise(promise: CancelablePromise): void { + this.jitsiLoadPromise = promise; + } + + load(): CancelablePromise { + return new CancelablePromise((resolve, reject, cancel) => { + this.state.set("loading"); + + gameManager.getCurrentGameScene().disableMediaBehaviors(); + jitsiFactory.restart(); + + if (!this.jitsiLoadPromise) { + return reject("Undefined Jitsi start callback"); + } + + const jitsiLoading = this.jitsiLoadPromise + .then((iframe) => { + this.iframe = iframe; + this.iframe.classList.add("pixel"); + this.state.set("ready"); + return resolve(iframe); + }) + .catch((err) => { + return reject(err); + }); + + cancel(() => { + jitsiLoading.cancel(); + this.unload().catch((err) => { + console.error("Cannot unload Jitsi co-website while cancel loading", err); + }); + }); + }); + } + + unload(): Promise { + jitsiFactory.destroy(); + gameManager.getCurrentGameScene().enableMediaBehaviors(); + + return super.unload(); + } +} diff --git a/front/src/WebRtc/CoWebsite/SimpleCoWebsite.ts b/front/src/WebRtc/CoWebsite/SimpleCoWebsite.ts new file mode 100644 index 00000000..1ad585b4 --- /dev/null +++ b/front/src/WebRtc/CoWebsite/SimpleCoWebsite.ts @@ -0,0 +1,133 @@ +import CancelablePromise from "cancelable-promise"; +import { get, Readable, writable, Writable } from "svelte/store"; +import { iframeListener } from "../../Api/IframeListener"; +import { coWebsiteManager } from "../CoWebsiteManager"; +import type { CoWebsite, CoWebsiteState } from "./CoWesbite"; + +export class SimpleCoWebsite implements CoWebsite { + protected id: string; + protected url: URL; + protected state: Writable; + protected iframe?: HTMLIFrameElement; + protected loadIframe?: CancelablePromise; + protected allowApi?: boolean; + protected allowPolicy?: string; + protected widthPercent?: number; + protected closable: boolean; + + constructor(url: URL, allowApi?: boolean, allowPolicy?: string, widthPercent?: number, closable?: boolean) { + this.id = coWebsiteManager.generateUniqueId(); + this.url = url; + this.state = writable("asleep" as CoWebsiteState); + this.allowApi = allowApi; + this.allowPolicy = allowPolicy; + this.widthPercent = widthPercent; + this.closable = closable ?? false; + } + + getId(): string { + return this.id; + } + + getUrl(): URL { + return this.url; + } + + getState(): CoWebsiteState { + return get(this.state); + } + + getStateSubscriber(): Readable { + return this.state; + } + + getIframe(): HTMLIFrameElement | undefined { + return this.iframe; + } + + getLoadIframe(): CancelablePromise | undefined { + return this.loadIframe; + } + + getWidthPercent(): number | undefined { + return this.widthPercent; + } + + isClosable(): boolean { + return this.closable; + } + + load(): CancelablePromise { + this.loadIframe = new CancelablePromise((resolve, reject, cancel) => { + this.state.set("loading"); + + const iframe = document.createElement("iframe"); + this.iframe = iframe; + this.iframe.src = this.url.toString(); + this.iframe.id = this.id; + + if (this.allowPolicy) { + this.iframe.allow = this.allowPolicy; + } + + if (this.allowApi) { + iframeListener.registerIframe(this.iframe); + } + + this.iframe.classList.add("pixel"); + + const onloadPromise = new Promise((resolve) => { + if (this.iframe) { + this.iframe.onload = () => { + this.state.set("ready"); + resolve(); + }; + } + }); + + const onTimeoutPromise = new Promise((resolve) => { + setTimeout(() => resolve(), 2000); + }); + + coWebsiteManager.getCoWebsiteBuffer().appendChild(this.iframe); + + const race = CancelablePromise.race([onloadPromise, onTimeoutPromise]) + .then(() => { + return resolve(iframe); + }) + .catch((err) => { + console.error("Error on co-website loading => ", err); + return reject(); + }); + + cancel(() => { + race.cancel(); + this.unload().catch((err) => { + console.error("Cannot unload co-website while cancel loading", err); + }); + }); + }); + + return this.loadIframe; + } + + unload(): Promise { + return new Promise((resolve) => { + if (this.iframe) { + if (this.allowApi) { + iframeListener.unregisterIframe(this.iframe); + } + this.iframe.parentNode?.removeChild(this.iframe); + } + + if (this.loadIframe) { + this.loadIframe.cancel(); + this.loadIframe = undefined; + } + + this.state.set("asleep"); + + resolve(); + }); + } +} diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index 0758409f..f67b6e18 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -1,15 +1,13 @@ import { HtmlUtils } from "./HtmlUtils"; import { Subject } from "rxjs"; -import { iframeListener } from "../Api/IframeListener"; import { waScaleManager } from "../Phaser/Services/WaScaleManager"; -import { coWebsites, coWebsitesNotAsleep, mainCoWebsite } from "../Stores/CoWebsiteStore"; -import { get, Writable, writable } from "svelte/store"; +import { coWebsites, coWebsitesNotAsleep, jitsiCoWebsite, mainCoWebsite } from "../Stores/CoWebsiteStore"; +import { get } from "svelte/store"; import { embedScreenLayout, highlightedEmbedScreen } from "../Stores/EmbedScreensStore"; import { isMediaBreakpointDown } from "../Utils/BreakpointsUtils"; -import { jitsiFactory } from "./JitsiFactory"; -import { gameManager } from "../Phaser/Game/GameManager"; import { LayoutMode } from "./LayoutManager"; -import CancelablePromise from "cancelable-promise"; +import type { CoWebsite } from "./CoWebsite/CoWesbite"; +import type CancelablePromise from "cancelable-promise"; export enum iframeStates { closed = 1, @@ -35,20 +33,6 @@ interface TouchMoveCoordinates { y: number; } -export type CoWebsiteState = "asleep" | "loading" | "ready"; - -export type CoWebsite = { - iframe: HTMLIFrameElement; - url: URL; - state: Writable; - closable: boolean; - allowPolicy: string | undefined; - allowApi: boolean | undefined; - widthPercent?: number | undefined; - jitsi?: boolean; - altMessage?: string; -}; - class CoWebsiteManager { private openedMain: iframeStates = iframeStates.closed; @@ -144,10 +128,12 @@ class CoWebsiteManager { throw new Error("Undefined main co-website on closing"); } - if (coWebsite.closable) { + if (coWebsite.isClosable()) { this.closeCoWebsite(coWebsite); } else { - this.unloadCoWebsite(coWebsite); + this.unloadCoWebsite(coWebsite).catch((err) => { + console.error("Cannot unload co-website on click on close button", err); + }); } }); @@ -234,7 +220,10 @@ class CoWebsiteManager { return; } - coWebsite.iframe.style.display = "none"; + const iframe = coWebsite.getIframe(); + if (iframe) { + iframe.style.display = "none"; + } this.resizing = true; document.addEventListener("mousemove", movecallback); }); @@ -250,7 +239,10 @@ class CoWebsiteManager { return; } - coWebsite.iframe.style.display = "flex"; + const iframe = coWebsite.getIframe(); + if (iframe) { + iframe.style.display = "flex"; + } this.resizing = false; }); @@ -263,7 +255,10 @@ class CoWebsiteManager { return; } - coWebsite.iframe.style.display = "none"; + const iframe = coWebsite.getIframe(); + if (iframe) { + iframe.style.display = "none"; + } this.resizing = true; const touchEvent = event.touches[0]; this.previousTouchMoveCoordinates = { x: touchEvent.pageX, y: touchEvent.pageY }; @@ -282,7 +277,10 @@ class CoWebsiteManager { return; } - coWebsite.iframe.style.display = "flex"; + const iframe = coWebsite.getIframe(); + if (iframe) { + iframe.style.display = "flex"; + } this.resizing = false; }); } @@ -306,9 +304,12 @@ class CoWebsiteManager { public displayMain() { const coWebsite = this.getMainCoWebsite(); if (coWebsite) { - coWebsite.iframe.style.display = "block"; + const iframe = coWebsite.getIframe(); + if (iframe) { + iframe.style.display = "block"; + } } - this.loadMain(); + this.loadMain(coWebsite?.getWidthPercent()); this.openMain(); this.fire(); } @@ -316,7 +317,10 @@ class CoWebsiteManager { public hideMain() { const coWebsite = this.getMainCoWebsite(); if (coWebsite) { - coWebsite.iframe.style.display = "none"; + const iframe = coWebsite.getIframe(); + if (iframe) { + iframe.style.display = "none"; + } } this.cowebsiteDom.classList.add("closing"); this.cowebsiteDom.classList.remove("opened"); @@ -402,7 +406,9 @@ class CoWebsiteManager { } public getCoWebsiteById(coWebsiteId: string): CoWebsite | undefined { - return get(coWebsites).find((coWebsite: CoWebsite) => coWebsite.iframe.id === coWebsiteId); + return get(coWebsites).find((coWebsite: CoWebsite) => { + return coWebsite.getId() === coWebsiteId; + }); } private getCoWebsiteByPosition(position: number): CoWebsite | undefined { @@ -422,7 +428,9 @@ class CoWebsiteManager { } private getPositionByCoWebsite(coWebsite: CoWebsite): number { - return get(coWebsites).findIndex((currentCoWebsite) => currentCoWebsite.iframe.id === coWebsite.iframe.id); + return get(coWebsites).findIndex((currentCoWebsite) => { + return currentCoWebsite.getId() === coWebsite.getId(); + }); } private getSlotByCowebsite(coWebsite: CoWebsite): HTMLDivElement | undefined { @@ -436,7 +444,7 @@ class CoWebsiteManager { if (index === 0) { id += "main"; } else { - id += coWebsite.iframe.id; + id += coWebsite.getId(); } const slot = HtmlUtils.getElementById(id); @@ -453,60 +461,72 @@ class CoWebsiteManager { const bounding = coWebsiteSlot.getBoundingClientRect(); - coWebsite.iframe.style.top = bounding.top + "px"; - coWebsite.iframe.style.left = bounding.left + "px"; - coWebsite.iframe.style.width = bounding.right - bounding.left + "px"; - coWebsite.iframe.style.height = bounding.bottom - bounding.top + "px"; + const iframe = coWebsite.getIframe(); + + if (iframe) { + iframe.style.top = bounding.top + "px"; + iframe.style.left = bounding.left + "px"; + iframe.style.width = bounding.right - bounding.left + "px"; + iframe.style.height = bounding.bottom - bounding.top + "px"; + } } public resizeAllIframes() { const mainCoWebsite = this.getCoWebsiteByPosition(0); + const mainIframe = mainCoWebsite?.getIframe(); const highlightEmbed = get(highlightedEmbedScreen); - get(coWebsites).forEach((coWebsite) => { - const notMain = !mainCoWebsite || (mainCoWebsite && mainCoWebsite.iframe.id !== coWebsite.iframe.id); + get(coWebsites).forEach((coWebsite: CoWebsite) => { + const iframe = coWebsite.getIframe(); + if (!iframe) { + return; + } + + const notMain = !mainCoWebsite || (mainCoWebsite && mainIframe && mainIframe.id !== iframe.id); const notHighlighEmbed = !highlightEmbed || (highlightEmbed && (highlightEmbed.type !== "cowebsite" || - (highlightEmbed.type === "cowebsite" && - highlightEmbed.embed.iframe.id !== coWebsite.iframe.id))); + (highlightEmbed.type === "cowebsite" && highlightEmbed.embed.getId() !== coWebsite.getId()))); - if (coWebsite.iframe.classList.contains("main") && notMain) { - coWebsite.iframe.classList.remove("main"); + if (iframe.classList.contains("main") && notMain) { + iframe.classList.remove("main"); } - if (coWebsite.iframe.classList.contains("highlighted") && notHighlighEmbed) { - coWebsite.iframe.classList.remove("highlighted"); - coWebsite.iframe.classList.add("pixel"); - coWebsite.iframe.style.top = "-1px"; - coWebsite.iframe.style.left = "-1px"; + if (iframe.classList.contains("highlighted") && notHighlighEmbed) { + iframe.classList.remove("highlighted"); + iframe.classList.add("pixel"); + iframe.style.top = "-1px"; + iframe.style.left = "-1px"; } if (notMain && notHighlighEmbed) { - coWebsite.iframe.classList.add("pixel"); - coWebsite.iframe.style.top = "-1px"; - coWebsite.iframe.style.left = "-1px"; + iframe.classList.add("pixel"); + iframe.style.top = "-1px"; + iframe.style.left = "-1px"; } this.setIframeOffset(coWebsite); }); - if (mainCoWebsite) { - mainCoWebsite.iframe.classList.add("main"); - mainCoWebsite.iframe.classList.remove("pixel"); + if (mainIframe) { + mainIframe.classList.add("main"); + mainIframe.classList.remove("pixel"); } if (highlightEmbed && highlightEmbed.type === "cowebsite") { - highlightEmbed.embed.iframe.classList.add("highlighted"); - highlightEmbed.embed.iframe.classList.remove("pixel"); + const highlightEmbedIframe = highlightEmbed.embed.getIframe(); + if (highlightEmbedIframe) { + highlightEmbedIframe.classList.add("highlighted"); + highlightEmbedIframe.classList.remove("pixel"); + } } } private removeHighlightCoWebsite(coWebsite: CoWebsite) { const highlighted = get(highlightedEmbedScreen); - if (highlighted && highlighted.type === "cowebsite" && highlighted.embed.iframe.id === coWebsite.iframe.id) { + if (highlighted && highlighted.type === "cowebsite" && highlighted.embed.getId() === coWebsite.getId()) { highlightedEmbedScreen.removeHighlight(); } } @@ -519,7 +539,9 @@ class CoWebsiteManager { this.closeMain(); } - coWebsite.iframe.remove(); + coWebsite.unload().catch((err) => { + console.error("Cannot unload cowebsite on remove from stack"); + }); } public goToMain(coWebsite: CoWebsite) { @@ -531,8 +553,8 @@ class CoWebsiteManager { isMediaBreakpointDown("lg") && get(embedScreenLayout) === LayoutMode.Presentation && mainCoWebsite && - mainCoWebsite.iframe.id !== coWebsite.iframe.id && - get(mainCoWebsite.state) !== "asleep" + mainCoWebsite.getId() !== coWebsite.getId() && + mainCoWebsite.getState() !== "asleep" ) { highlightedEmbedScreen.toggleHighlight({ type: "cowebsite", @@ -544,25 +566,15 @@ class CoWebsiteManager { } public searchJitsi(): CoWebsite | undefined { - return get(coWebsites).find((coWebsite: CoWebsite) => coWebsite.jitsi); + return get(jitsiCoWebsite); } - private initialiseCowebsite(coWebsite: CoWebsite, position: number | undefined) { - if (coWebsite.allowPolicy) { - coWebsite.iframe.allow = coWebsite.allowPolicy; - } - - if (coWebsite.allowApi) { - iframeListener.registerIframe(coWebsite.iframe); - } - - coWebsite.iframe.classList.add("pixel"); - + public addCoWebsiteToStore(coWebsite: CoWebsite, position: number | undefined) { const coWebsitePosition = position === undefined ? get(coWebsites).length : position; coWebsites.add(coWebsite, coWebsitePosition); } - private generateUniqueId() { + public generateUniqueId() { let id = undefined; do { id = "cowebsite-iframe-" + (Math.random() + 1).toString(36).substring(7); @@ -571,80 +583,11 @@ class CoWebsiteManager { return id; } - public addCoWebsite( - url: string, - base: string, - allowApi?: boolean, - allowPolicy?: string, - widthPercent?: number, - position?: number, - closable?: boolean, - altMessage?: string - ): CoWebsite { - const iframe = document.createElement("iframe"); - const fullUrl = new URL(url, base); - iframe.src = fullUrl.toString(); - iframe.id = this.generateUniqueId(); - - const newCoWebsite: CoWebsite = { - iframe, - url: fullUrl, - state: writable("asleep" as CoWebsiteState), - closable: closable ?? false, - allowPolicy, - allowApi, - widthPercent, - altMessage, - }; - - this.initialiseCowebsite(newCoWebsite, position); - - return newCoWebsite; - } - - public addCoWebsiteFromIframe( - iframe: HTMLIFrameElement, - allowApi?: boolean, - allowPolicy?: string, - widthPercent?: number, - position?: number, - closable?: boolean, - jitsi?: boolean - ): CoWebsite { - if (get(coWebsitesNotAsleep).length < 1) { - this.loadMain(widthPercent); - } - - iframe.id = this.generateUniqueId(); - - const newCoWebsite: CoWebsite = { - iframe, - url: new URL(iframe.src), - state: writable("ready" as CoWebsiteState), - closable: closable ?? false, - allowPolicy, - allowApi, - widthPercent, - jitsi, - }; - - if (position === 0) { - this.openMain(); - setTimeout(() => { - this.fire(); - }, animationTime); - } - - this.initialiseCowebsite(newCoWebsite, position); - - return newCoWebsite; - } - - public loadCoWebsite(coWebsite: CoWebsite): CancelablePromise { + public loadCoWebsite(coWebsite: CoWebsite): CancelablePromise { if (get(coWebsitesNotAsleep).length < 1) { coWebsites.remove(coWebsite); coWebsites.add(coWebsite, 0); - this.loadMain(coWebsite.widthPercent); + this.loadMain(coWebsite.getWidthPercent()); } // Check if the main is hide @@ -652,96 +595,56 @@ class CoWebsiteManager { this.displayMain(); } - coWebsite.state.set("loading"); + const coWebsiteLloading = coWebsite + .load() + .then(() => { + const mainCoWebsite = this.getMainCoWebsite(); + if (mainCoWebsite && mainCoWebsite.getId() === coWebsite.getId()) { + this.openMain(); - const mainCoWebsite = this.getMainCoWebsite(); - - return new CancelablePromise((resolve, reject, cancel) => { - const onloadPromise = new Promise((resolve) => { - coWebsite.iframe.onload = () => { - coWebsite.state.set("ready"); - resolve(); - }; + setTimeout(() => { + this.fire(); + }, animationTime); + } + this.resizeAllIframes(); + }) + .catch((err) => { + console.error("Error on co-website loading => ", err); + this.removeCoWebsiteFromStack(coWebsite); }); - const onTimeoutPromise = new Promise((resolve) => { - setTimeout(() => resolve(), 2000); - }); - - this.cowebsiteBufferDom.appendChild(coWebsite.iframe); - - if (coWebsite.jitsi) { - const gameScene = gameManager.getCurrentGameScene(); - gameScene.disableMediaBehaviors(); - jitsiFactory.restart(); - } - - const race = CancelablePromise.race([onloadPromise, onTimeoutPromise]) - .then(() => { - if (mainCoWebsite && mainCoWebsite.iframe.id === coWebsite.iframe.id) { - this.openMain(); - - setTimeout(() => { - this.fire(); - }, animationTime); - } - - return resolve(coWebsite); - }) - .catch((err) => { - console.error("Error on co-website loading => ", err); - this.removeCoWebsiteFromStack(coWebsite); - return reject(); - }); - - cancel(() => { - race.cancel(); - this.unloadCoWebsite(coWebsite); - }); - }); + return coWebsiteLloading; } - public unloadCoWebsite(coWebsite: CoWebsite): void { + public unloadCoWebsite(coWebsite: CoWebsite): Promise { this.removeHighlightCoWebsite(coWebsite); - coWebsite.iframe.parentNode?.removeChild(coWebsite.iframe); - coWebsite.state.set("asleep"); - coWebsites.remove(coWebsite); + return coWebsite + .unload() + .then(() => { + coWebsites.remove(coWebsite); + const mainCoWebsite = this.getMainCoWebsite(); - if (coWebsite.jitsi) { - jitsiFactory.stop(); - const gameScene = gameManager.getCurrentGameScene(); - gameScene.enableMediaBehaviors(); - } + if (mainCoWebsite) { + this.removeHighlightCoWebsite(mainCoWebsite); + this.goToMain(mainCoWebsite); + this.resizeAllIframes(); + } else { + this.closeMain(); + } - const mainCoWebsite = this.getMainCoWebsite(); - - if (mainCoWebsite) { - this.removeHighlightCoWebsite(mainCoWebsite); - this.goToMain(mainCoWebsite); - this.resizeAllIframes(); - } else { - this.closeMain(); - } - - coWebsites.add(coWebsite, get(coWebsites).length); + coWebsites.add(coWebsite, get(coWebsites).length); + }) + .catch(() => { + console.error(); + }); } public closeCoWebsite(coWebsite: CoWebsite): void { - if (coWebsite.jitsi) { - jitsiFactory.destroy(); - const gameScene = gameManager.getCurrentGameScene(); - gameScene.enableMediaBehaviors(); - } - if (get(coWebsites).length === 1) { this.fire(); } - if (coWebsite.allowApi) { - iframeListener.unregisterIframe(coWebsite.iframe); - } - this.removeCoWebsiteFromStack(coWebsite); const mainCoWebsite = this.getMainCoWebsite(); diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts index a8f10e10..1faa9897 100644 --- a/front/src/WebRtc/JitsiFactory.ts +++ b/front/src/WebRtc/JitsiFactory.ts @@ -1,7 +1,9 @@ import { JITSI_URL } from "../Enum/EnvironmentVariable"; -import { CoWebsite, coWebsiteManager } from "./CoWebsiteManager"; +import { coWebsiteManager } from "./CoWebsiteManager"; import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore"; import { get } from "svelte/store"; +import type { CoWebsite } from "./CoWebsite/CoWesbite"; +import CancelablePromise from "cancelable-promise"; interface jitsiConfigInterface { startWithAudioMuted: boolean; @@ -134,89 +136,91 @@ class JitsiFactory { return slugify(instance.replace("/", "-") + "-" + roomName); } - public async start( + public start( roomName: string, playerName: string, jwt?: string, config?: object, interfaceConfig?: object, jitsiUrl?: string - ) { - const coWebsite = coWebsiteManager.searchJitsi(); + ): CancelablePromise { + return new CancelablePromise((resolve, reject, cancel) => { + // 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 + // after some time the URLs get so huge that loading the iframe + // becomes slow and eventually breaks completely. Thus lets just + // clear jitsi local storage before starting a new conference. + window.localStorage.removeItem("jitsiLocalStorage"); - if (coWebsite) { - coWebsiteManager.closeCoWebsite(coWebsite); - } - - // 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 - // after some time the URLs get so huge that loading the iframe - // becomes slow and eventually breaks completely. Thus lets just - // clear jitsi local storage before starting a new conference. - window.localStorage.removeItem("jitsiLocalStorage"); - - 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: JitsiOptions = { - roomName: roomName, - jwt: jwt, - width: "100%", - height: "100%", - parentNode: coWebsiteManager.getCoWebsiteBuffer(), - configOverwrite: mergeConfig(config), - interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig }, - }; - - if (!options.jwt) { - delete options.jwt; - } - - const doResolve = (): void => { - const iframe = coWebsiteManager.getCoWebsiteBuffer().querySelector('[id*="jitsi" i]'); - if (iframe && this.jitsiApi) { - const coWebsite = coWebsiteManager.addCoWebsiteFromIframe( - iframe, - false, - undefined, - undefined, - 0, - false, - true - ); - - this.jitsiApi.addListener("videoConferenceLeft", () => { - this.closeOrUnload(coWebsite); - }); - - this.jitsiApi.addListener("readyToClose", () => { - this.closeOrUnload(coWebsite); - }); + const domain = jitsiUrl || JITSI_URL; + if (domain === undefined) { + throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map."); } - coWebsiteManager.resizeAllIframes(); - }; + const loadScript = this.loadJitsiScript(domain).then(() => { + const options: JitsiOptions = { + roomName: roomName, + jwt: jwt, + width: "100%", + height: "100%", + parentNode: coWebsiteManager.getCoWebsiteBuffer(), + configOverwrite: mergeConfig(config), + interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig }, + }; - this.jitsiApi = undefined; + if (!options.jwt) { + delete options.jwt; + } - options.onload = () => doResolve(); //we want for the iframe to be loaded before triggering animations. - setTimeout(() => doResolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load - this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options); - this.jitsiApi.executeCommand("displayName", playerName); + const timemout = setTimeout(() => doResolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load - this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback); - this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback); + const doResolve = (): void => { + clearTimeout(timemout); + const iframe = coWebsiteManager + .getCoWebsiteBuffer() + .querySelector('[id*="jitsi" i]'); + if (iframe && this.jitsiApi) { + this.jitsiApi.addListener("videoConferenceLeft", () => { + this.closeOrUnload(); + }); + + this.jitsiApi.addListener("readyToClose", () => { + this.closeOrUnload(); + }); + + return resolve(iframe); + } + }; + + this.jitsiApi = undefined; + + options.onload = () => doResolve(); //we want for the iframe to be loaded before triggering animations. + this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options); + this.jitsiApi.executeCommand("displayName", playerName); + + this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback); + this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback); + }); + + cancel(() => { + loadScript.cancel(); + }); + }); } - private closeOrUnload = function (coWebsite: CoWebsite) { - if (coWebsite.closable) { + private closeOrUnload = function () { + const coWebsite = coWebsiteManager.searchJitsi(); + if (!coWebsite) { + return; + } + + if (coWebsite.isClosable()) { coWebsiteManager.closeCoWebsite(coWebsite); } else { - coWebsiteManager.unloadCoWebsite(coWebsite); + coWebsiteManager.unloadCoWebsite(coWebsite).catch((err) => { + console.error("Cannot unload co-website from the Jitsi factory", err); + }); } }; @@ -229,8 +233,6 @@ class JitsiFactory { this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback); const coWebsite = coWebsiteManager.searchJitsi(); - console.log("jitsi api ", this.jitsiApi); - console.log("iframe cowebsite", coWebsite?.iframe); if (!coWebsite) { this.destroy(); @@ -238,11 +240,11 @@ class JitsiFactory { } this.jitsiApi.addListener("videoConferenceLeft", () => { - this.closeOrUnload(coWebsite); + this.closeOrUnload(); }); this.jitsiApi.addListener("readyToClose", () => { - this.closeOrUnload(coWebsite); + this.closeOrUnload(); }); } @@ -280,8 +282,8 @@ class JitsiFactory { } } - private async loadJitsiScript(domain: string): Promise { - return new Promise((resolve, reject) => { + private loadJitsiScript(domain: string): CancelablePromise { + return new CancelablePromise((resolve, reject, cancel) => { if (this.jitsiScriptLoaded) { resolve(); return; @@ -300,6 +302,10 @@ class JitsiFactory { }; document.head.appendChild(jitsiScript); + + cancel(() => { + jitsiScript.remove(); + }); }); } }