diff --git a/front/dist/resources/objects/layout_modes.png b/front/dist/resources/objects/layout_modes.png deleted file mode 100644 index abd9adaf..00000000 Binary files a/front/dist/resources/objects/layout_modes.png and /dev/null differ diff --git a/front/package.json b/front/package.json index 61205855..ee031668 100644 --- a/front/package.json +++ b/front/package.json @@ -64,8 +64,8 @@ "lint": "node_modules/.bin/eslint src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", "precommit": "lint-staged", - "svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\" --watch", - "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\"", + "svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch", + "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"", "pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'", "pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'" }, diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte index 2e159d2d..8ade9398 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -1,5 +1,5 @@
@@ -68,6 +71,7 @@ --> {#if $gameOverlayVisibilityStore}
+
diff --git a/front/src/Components/CameraControls.svelte b/front/src/Components/CameraControls.svelte index 5c17a9fe..d6b31af4 100644 --- a/front/src/Components/CameraControls.svelte +++ b/front/src/Components/CameraControls.svelte @@ -7,6 +7,11 @@ import cinemaCloseImg from "./images/cinema-close.svg"; import microphoneImg from "./images/microphone.svg"; import microphoneCloseImg from "./images/microphone-close.svg"; + import layoutPresentationImg from "./images/layout-presentation.svg"; + import layoutChatImg from "./images/layout-chat.svg"; + import {layoutModeStore} from "../Stores/StreamableCollectionStore"; + import {LayoutMode} from "../WebRtc/LayoutManager"; + import {peerStore} from "../Stores/PeerStore"; function screenSharingClick(): void { if ($requestedScreenSharingState === true) { @@ -32,10 +37,24 @@ } } + function switchLayoutMode() { + if ($layoutModeStore === LayoutMode.Presentation) { + $layoutModeStore = LayoutMode.VideoChat; + } else { + $layoutModeStore = LayoutMode.Presentation; + } + }
+
+ {#if $layoutModeStore === LayoutMode.Presentation } + Switch to mosaic mode + {:else} + Switch to presentation mode + {/if} +
{#if $requestedScreenSharingState} Start screen sharing diff --git a/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte b/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte index a22da2fa..79ad1810 100644 --- a/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte +++ b/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte @@ -58,7 +58,7 @@
- {#each [...Array(NB_BARS).keys()] as i} + {#each [...Array(NB_BARS).keys()] as i (i)}
{/each}
diff --git a/front/src/Components/SoundMeterWidget.svelte b/front/src/Components/SoundMeterWidget.svelte index 30650e3f..74d3f7b3 100644 --- a/front/src/Components/SoundMeterWidget.svelte +++ b/front/src/Components/SoundMeterWidget.svelte @@ -6,8 +6,6 @@ export let stream: MediaStream|null; let volume = 0; - const NB_BARS = 5; - let timeout: ReturnType; const soundMeter = new SoundMeter(); let display = false; @@ -23,7 +21,7 @@ timeout = setInterval(() => { try{ - volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0)); + volume = soundMeter.getVolume(); //console.log(volume); }catch(err){ @@ -45,9 +43,9 @@
- 1}> - 2}> - 3}> - 4}> 5}> + 10}> + 15}> + 40}> + 70}>
diff --git a/front/src/Components/Video/ChatLayout.svelte b/front/src/Components/Video/ChatLayout.svelte new file mode 100644 index 00000000..ac91217e --- /dev/null +++ b/front/src/Components/Video/ChatLayout.svelte @@ -0,0 +1,35 @@ + + +
+ {#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)} + + {/each} +
diff --git a/front/src/Components/Video/LocalStreamMediaBox.svelte b/front/src/Components/Video/LocalStreamMediaBox.svelte new file mode 100644 index 00000000..55c7f22c --- /dev/null +++ b/front/src/Components/Video/LocalStreamMediaBox.svelte @@ -0,0 +1,16 @@ + + + +
+ {#if stream} + + {/if} +
diff --git a/front/src/Components/Video/MediaBox.svelte b/front/src/Components/Video/MediaBox.svelte new file mode 100644 index 00000000..9160a7a9 --- /dev/null +++ b/front/src/Components/Video/MediaBox.svelte @@ -0,0 +1,20 @@ + + +
+ {#if streamable instanceof VideoPeer} + + {:else if streamable instanceof ScreenSharingPeer} + + {:else} + + {/if} +
diff --git a/front/src/Components/Video/PresentationLayout.svelte b/front/src/Components/Video/PresentationLayout.svelte new file mode 100644 index 00000000..f68dd2f1 --- /dev/null +++ b/front/src/Components/Video/PresentationLayout.svelte @@ -0,0 +1,24 @@ + + +
+ {#if $videoFocusStore } + + {/if} +
+ diff --git a/front/src/Components/Video/ScreenSharingMediaBox.svelte b/front/src/Components/Video/ScreenSharingMediaBox.svelte new file mode 100644 index 00000000..c6e1564f --- /dev/null +++ b/front/src/Components/Video/ScreenSharingMediaBox.svelte @@ -0,0 +1,33 @@ + + +
+ {#if $statusStore === 'connecting'} +
+ {/if} + {#if $statusStore === 'error'} +
+ {/if} + {#if $streamStore === null} + {name} + {:else} + + {/if} +
+ + diff --git a/front/src/Components/Video/VideoMediaBox.svelte b/front/src/Components/Video/VideoMediaBox.svelte new file mode 100644 index 00000000..1a581914 --- /dev/null +++ b/front/src/Components/Video/VideoMediaBox.svelte @@ -0,0 +1,48 @@ + + +
+ {#if $statusStore === 'connecting'} +
+ {/if} + {#if $statusStore === 'error'} +
+ {/if} + {#if !$constraintStore || $constraintStore.video === false} + {name} + {/if} + {#if $constraintStore && $constraintStore.audio === false} + Muted + {/if} + + {#if $streamStore } + + {/if} + + {#if $constraintStore && $constraintStore.audio !== false} + + {/if} +
+ diff --git a/front/src/Components/Video/VideoOverlay.svelte b/front/src/Components/Video/VideoOverlay.svelte new file mode 100644 index 00000000..feb13743 --- /dev/null +++ b/front/src/Components/Video/VideoOverlay.svelte @@ -0,0 +1,23 @@ + + +
+ {#if $layoutModeStore === LayoutMode.Presentation } + + {:else } + + {/if} +
+ + diff --git a/front/src/Components/Video/images/blockSign.svg b/front/src/Components/Video/images/blockSign.svg new file mode 100644 index 00000000..c64ba294 --- /dev/null +++ b/front/src/Components/Video/images/blockSign.svg @@ -0,0 +1,22 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/front/src/Components/Video/images/report.svg b/front/src/Components/Video/images/report.svg new file mode 100644 index 00000000..14753256 --- /dev/null +++ b/front/src/Components/Video/images/report.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Components/Video/utils.ts b/front/src/Components/Video/utils.ts new file mode 100644 index 00000000..ab7a63be --- /dev/null +++ b/front/src/Components/Video/utils.ts @@ -0,0 +1,27 @@ +export function getColorByString(str: string) : string|null { + let hash = 0; + if (str.length === 0) { + return null; + } + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + hash = hash & hash; + } + let color = '#'; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 255; + color += ('00' + value.toString(16)).substr(-2); + } + return color; +} + +export function srcObject(node: HTMLVideoElement, stream: MediaStream) { + node.srcObject = stream; + return { + update(newStream: MediaStream) { + if (node.srcObject != newStream) { + node.srcObject = newStream + } + } + } +} diff --git a/front/src/Components/images/layout-chat.svg b/front/src/Components/images/layout-chat.svg new file mode 100644 index 00000000..af071d49 --- /dev/null +++ b/front/src/Components/images/layout-chat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Components/images/layout-presentation.svg b/front/src/Components/images/layout-presentation.svg new file mode 100644 index 00000000..bf65d002 --- /dev/null +++ b/front/src/Components/images/layout-presentation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 7c07f187..e5507c2f 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -35,10 +35,10 @@ import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; import { HtmlUtils } from "../../WebRtc/HtmlUtils"; import { jitsiFactory } from "../../WebRtc/JitsiFactory"; import { - AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY, CenterListener, + AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY, + Box, JITSI_MESSAGE_PROPERTIES, layoutManager, - LayoutMode, ON_ACTION_TRIGGER_BUTTON, TRIGGER_JITSI_PROPERTIES, TRIGGER_WEBSITE_PROPERTIES, @@ -94,6 +94,9 @@ import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import AnimatedTiles from "phaser-animated-tiles"; import {soundManager} from "./SoundManager"; +import {screenSharingPeerStore} from "../../Stores/PeerStore"; +import {videoFocusStore} from "../../Stores/VideoFocusStore"; +import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore"; export interface GameSceneInitInterface { initPosition: PointInterface | null, @@ -132,7 +135,7 @@ interface DeleteGroupEventInterface { const defaultStartLayerName = 'start'; -export class GameScene extends DirtyScene implements CenterListener { +export class GameScene extends DirtyScene { Terrains: Array; CurrentPlayer!: Player; MapPlayers!: Phaser.Physics.Arcade.Group; @@ -172,8 +175,6 @@ export class GameScene extends DirtyScene implements CenterListener { y: -1000 } - private presentationModeSprite!: Sprite; - private chatModeSprite!: Sprite; private gameMap!: GameMap; private actionableItems: Map = new Map(); // The item that can be selected by pressing the space key. @@ -277,7 +278,6 @@ export class GameScene extends DirtyScene implements CenterListener { this.onMapLoad(data); } - this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', { frameWidth: 32, frameHeight: 32 }); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); //eslint-disable-next-line @typescript-eslint/no-explicit-any (this.load as any).rexWebFont({ @@ -497,10 +497,6 @@ export class GameScene extends DirtyScene implements CenterListener { this.outlinedItem?.activate(); }); - this.presentationModeSprite = new PresentationModeIcon(this, 36, this.game.renderer.height - 2); - this.presentationModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); - this.chatModeSprite = new ChatModeIcon(this, 70, this.game.renderer.height - 2); - this.chatModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); this.openChatIcon = new OpenChatIcon(this, 2, this.game.renderer.height - 2) // FIXME: change this to use the UserInputManager class for input @@ -512,7 +508,8 @@ export class GameScene extends DirtyScene implements CenterListener { this.reposition(); // From now, this game scene will be notified of reposition events - layoutManager.setListener(this); + biggestAvailableAreaStore.subscribe((box) => this.updateCameraOffset(box)); + this.triggerOnMapLayerPropertyChange(); this.listenToIframeEvents(); @@ -643,21 +640,19 @@ export class GameScene extends DirtyScene implements CenterListener { // When connection is performed, let's connect SimplePeer this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName); peerStore.connectToSimplePeer(this.simplePeer); + screenSharingPeerStore.connectToSimplePeer(this.simplePeer); + videoFocusStore.connectToSimplePeer(this.simplePeer); this.GlobalMessageManager = new GlobalMessageManager(this.connection); userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); const self = this; this.simplePeer.registerPeerConnectionListener({ - onConnect(user: UserSimplePeerInterface) { - self.presentationModeSprite.setVisible(true); - self.chatModeSprite.setVisible(true); + onConnect(peer) { 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(); } @@ -1058,23 +1053,6 @@ ${escapedMessage} this.MapPlayersByKey = new Map(); } - private switchLayoutMode(): void { - //if discussion is activated, this layout cannot be activated - if (mediaManager.activatedDiscussion) { - return; - } - const mode = layoutManager.getLayoutMode(); - if (mode === LayoutMode.Presentation) { - layoutManager.switchLayoutMode(LayoutMode.VideoChat); - this.presentationModeSprite.setFrame(1); - this.chatModeSprite.setFrame(2); - } else { - layoutManager.switchLayoutMode(LayoutMode.Presentation); - this.presentationModeSprite.setFrame(0); - this.chatModeSprite.setFrame(3); - } - } - private initStartXAndStartY() { // If there is an init position passed if (this.initPosition !== null) { @@ -1187,7 +1165,7 @@ ${escapedMessage} initCamera() { this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); this.cameras.main.startFollow(this.CurrentPlayer, true); - this.updateCameraOffset(); + biggestAvailableAreaStore.recompute(); } createCollisionWithPlayer() { @@ -1334,7 +1312,7 @@ ${escapedMessage} * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. */ update(time: number, delta: number): void { - mediaManager.updateScene(); + this.dirty = false; this.currentTick = time; this.CurrentPlayer.moveUser(delta); @@ -1564,20 +1542,17 @@ ${escapedMessage} } private reposition(): void { - this.presentationModeSprite.setY(this.game.renderer.height - 2); - this.chatModeSprite.setY(this.game.renderer.height - 2); this.openChatIcon.setY(this.game.renderer.height - 2); // Recompute camera offset if needed - this.updateCameraOffset(); + biggestAvailableAreaStore.recompute(); } /** * Updates the offset of the character compared to the center of the screen according to the layout manager * (tries to put the character in the center of the remaining space if there is a discussion going on. */ - private updateCameraOffset(): void { - const array = layoutManager.findBiggestAvailableArray(); + private updateCameraOffset(array: Box): void { const xCenter = (array.xEnd - array.xStart) / 2 + array.xStart; const yCenter = (array.yEnd - array.yStart) / 2 + array.yStart; @@ -1587,10 +1562,6 @@ ${escapedMessage} this.cameras.main.setFollowOffset((xCenter - game.offsetWidth / 2) * window.devicePixelRatio / this.scale.zoom, (yCenter - game.offsetHeight / 2) * window.devicePixelRatio / this.scale.zoom); } - public onCenterChange(): void { - this.updateCameraOffset(); - } - public startJitsi(roomName: string, jwt?: string): void { const allProps = this.gameMap.getCurrentProperties(); const jitsiConfig = this.safeParseJSONstring(allProps.get("jitsiConfig") as string | undefined, 'jitsiConfig'); @@ -1650,6 +1621,6 @@ ${escapedMessage} zoomByFactor(zoomFactor: number) { waScaleManager.zoomModifier *= zoomFactor; - this.updateCameraOffset(); + biggestAvailableAreaStore.recompute(); } } diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index bf9c9a08..4fc58f5d 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -3,17 +3,17 @@ import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCha import {SelectCompanionScene, SelectCompanionSceneName} from "../Login/SelectCompanionScene"; import {gameManager} from "../Game/GameManager"; import {localUserStore} from "../../Connexion/LocalUserStore"; -import {mediaManager} from "../../WebRtc/MediaManager"; import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu"; import {connectionManager} from "../../Connexion/ConnectionManager"; import {GameConnexionTypes} from "../../Url/UrlManager"; import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; import {menuIconVisible} from "../../Stores/MenuStore"; +import {videoConstraintStore} from "../../Stores/MediaStore"; +import {showReportScreenStore} from "../../Stores/ShowReportScreenStore"; import { HtmlUtils } from '../../WebRtc/HtmlUtils'; import { iframeListener } from '../../Api/IframeListener'; import { Subscription } from 'rxjs'; -import { videoConstraintStore } from "../../Stores/MediaStore"; import {registerMenuCommandStream} from "../../Api/Events/ui/MenuItemRegisterEvent"; import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem"; import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore"; @@ -111,9 +111,11 @@ export class MenuScene extends Phaser.Scene { }); this.gameReportElement = new ReportMenu(this, connectionManager.getConnexionType === GameConnexionTypes.anonymous); - mediaManager.setShowReportModalCallBacks((userId, userName) => { + showReportScreenStore.subscribe((user) => { this.closeAll(); - this.gameReportElement.open(parseInt(userId), userName); + if (user !== null) { + this.gameReportElement.open(user.userId, user.userName); + } }); this.input.keyboard.on('keyup-TAB', () => { diff --git a/front/src/Stores/BiggestAvailableAreaStore.ts b/front/src/Stores/BiggestAvailableAreaStore.ts new file mode 100644 index 00000000..716f37fc --- /dev/null +++ b/front/src/Stores/BiggestAvailableAreaStore.ts @@ -0,0 +1,127 @@ +import {get, writable} from "svelte/store"; +import type {Box} from "../WebRtc/LayoutManager"; +import {HtmlUtils} from "../WebRtc/HtmlUtils"; +import {LayoutMode} from "../WebRtc/LayoutManager"; +import {layoutModeStore} from "./StreamableCollectionStore"; + +/** + * Tries to find the biggest available box of remaining space (this is a space where we can center the character) + */ +function findBiggestAvailableArea(): Box { + const game = HtmlUtils.querySelectorOrFail('#game canvas'); + if (get(layoutModeStore) === LayoutMode.VideoChat) { + const children = document.querySelectorAll('div.chat-mode > div'); + const htmlChildren = Array.from(children.values()); + + // No chat? Let's go full center + if (htmlChildren.length === 0) { + return { + xStart: 0, + yStart: 0, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } + + const lastDiv = htmlChildren[htmlChildren.length - 1]; + // Compute area between top right of the last div and bottom right of window + const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth)) + * (game.offsetHeight - lastDiv.offsetTop); + + // Compute area between bottom of last div and bottom of the screen on whole width + const area2 = game.offsetWidth + * (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight)); + + if (area1 < 0 && area2 < 0) { + // If screen is full, let's not attempt something foolish and simply center character in the middle. + return { + xStart: 0, + yStart: 0, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } + if (area1 <= area2) { + return { + xStart: 0, + yStart: lastDiv.offsetTop + lastDiv.offsetHeight, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } else { + return { + xStart: lastDiv.offsetLeft + lastDiv.offsetWidth, + yStart: lastDiv.offsetTop, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } + } else { + // Possible destinations: at the center bottom or at the right bottom. + const mainSectionChildren = Array.from(document.querySelectorAll('div.main-section > div').values()); + const sidebarChildren = Array.from(document.querySelectorAll('aside.sidebar > div').values()); + + // No presentation? Let's center on the screen + if (mainSectionChildren.length === 0) { + return { + xStart: 0, + yStart: 0, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } + + // At this point, we know we have at least one element in the main section. + const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1]; + + const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight)) + * (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth); + + let leftSideBar: number; + let bottomSideBar: number; + if (sidebarChildren.length === 0) { + leftSideBar = HtmlUtils.getElementByIdOrFail('sidebar').offsetLeft; + bottomSideBar = 0; + } else { + const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1]; + leftSideBar = lastSideBarChildren.offsetLeft; + bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight; + } + const sideBarArea = (game.offsetWidth - leftSideBar) + * (game.offsetHeight - bottomSideBar); + + if (presentationArea <= sideBarArea) { + return { + xStart: leftSideBar, + yStart: bottomSideBar, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } else { + return { + xStart: 0, + yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight, + xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area + yEnd: game.offsetHeight + } + } + } +} + + +/** + * A store that contains the list of (video) peers we are connected to. + */ +function createBiggestAvailableAreaStore() { + + const { subscribe, set } = writable({xStart:0, yStart: 0, xEnd: 1, yEnd: 1}); + + return { + subscribe, + recompute: () => { + set(findBiggestAvailableArea()); + } + }; +} + +export const biggestAvailableAreaStore = createBiggestAvailableAreaStore(); diff --git a/front/src/Stores/GameOverlayStoreVisibility.ts b/front/src/Stores/GameOverlayStoreVisibility.ts new file mode 100644 index 00000000..c58c929d --- /dev/null +++ b/front/src/Stores/GameOverlayStoreVisibility.ts @@ -0,0 +1,17 @@ +import {writable} from "svelte/store"; + +/** + * A store that contains whether the game overlay is shown or not. + * Typically, the overlay is hidden when entering Jitsi meet. + */ +function createGameOverlayVisibilityStore() { + const { subscribe, set, update } = writable(false); + + return { + subscribe, + showGameOverlay: () => set(true), + hideGameOverlay: () => set(false), + }; +} + +export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore(); diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index d622511e..b2cd9f42 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -1,13 +1,13 @@ import {derived, get, Readable, readable, writable, Writable} from "svelte/store"; import {peerStore} from "./PeerStore"; import {localUserStore} from "../Connexion/LocalUserStore"; -import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap"; import {userMovingStore} from "./GameStore"; import {HtmlUtils} from "../WebRtc/HtmlUtils"; import {BrowserTooOldError} from "./Errors/BrowserTooOldError"; import {errorStore} from "./ErrorStore"; import {isIOS} from "../WebRtc/DeviceUtils"; import {WebviewOnOldIOS} from "./Errors/WebviewOnOldIOS"; +import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility"; /** * A store that contains the camera state requested by the user (on or off). @@ -50,20 +50,6 @@ export const visibilityStore = readable(document.visibilityState === 'visible', }; }); -/** - * A store that contains whether the game overlay is shown or not. - * Typically, the overlay is hidden when entering Jitsi meet. - */ -function createGameOverlayVisibilityStore() { - const { subscribe, set, update } = writable(false); - - return { - subscribe, - showGameOverlay: () => set(true), - hideGameOverlay: () => set(false), - }; -} - /** * A store that contains whether the EnableCameraScene is shown or not. */ @@ -79,7 +65,6 @@ function createEnableCameraSceneVisibilityStore() { export const requestedCameraState = createRequestedCameraState(); export const requestedMicrophoneState = createRequestedMicrophoneState(); -export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore(); export const enableCameraSceneVisibilityStore = createEnableCameraSceneVisibilityStore(); /** diff --git a/front/src/Stores/PeerStore.ts b/front/src/Stores/PeerStore.ts index a582e692..b3690595 100644 --- a/front/src/Stores/PeerStore.ts +++ b/front/src/Stores/PeerStore.ts @@ -1,26 +1,62 @@ -import { derived, writable, Writable } from "svelte/store"; -import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer"; -import type {SimplePeer} from "../WebRtc/SimplePeer"; +import {derived, get, readable, writable} from "svelte/store"; +import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer"; +import {VideoPeer} from "../WebRtc/VideoPeer"; +import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer"; /** - * A store that contains the camera state requested by the user (on or off). + * A store that contains the list of (video) peers we are connected to. */ function createPeerStore() { - let users = new Map(); + let peers = new Map(); - const { subscribe, set, update } = writable(users); + const { subscribe, set, update } = writable(peers); return { subscribe, connectToSimplePeer: (simplePeer: SimplePeer) => { - users = new Map(); - set(users); + peers = new Map(); + set(peers); simplePeer.registerPeerConnectionListener({ - onConnect(user: UserSimplePeerInterface) { + onConnect(peer: RemotePeer) { + if (peer instanceof VideoPeer) { + update(users => { + users.set(peer.userId, peer); + return users; + }); + } + }, + onDisconnect(userId: number) { update(users => { - users.set(user.userId, user); + users.delete(userId); return users; }); + } + }) + } + }; +} + +/** + * A store that contains the list of screen sharing peers we are connected to. + */ +function createScreenSharingPeerStore() { + let peers = new Map(); + + const { subscribe, set, update } = writable(peers); + + return { + subscribe, + connectToSimplePeer: (simplePeer: SimplePeer) => { + peers = new Map(); + set(peers); + simplePeer.registerPeerConnectionListener({ + onConnect(peer: RemotePeer) { + if (peer instanceof ScreenSharingPeer) { + update(users => { + users.set(peer.userId, peer); + return users; + }); + } }, onDisconnect(userId: number) { update(users => { @@ -34,3 +70,56 @@ function createPeerStore() { } export const peerStore = createPeerStore(); +export const screenSharingPeerStore = createScreenSharingPeerStore(); + +/** + * A store that contains ScreenSharingPeer, ONLY if those ScreenSharingPeer are emitting a stream towards us! + */ +function createScreenSharingStreamStore() { + let peers = new Map(); + + return readable>(peers, function start(set) { + + let unsubscribes: (()=>void)[] = []; + + const unsubscribe = screenSharingPeerStore.subscribe((screenSharingPeers) => { + for (const unsubscribe of unsubscribes) { + unsubscribe(); + } + unsubscribes = []; + + peers = new Map(); + + screenSharingPeers.forEach((screenSharingPeer: ScreenSharingPeer, key: number) => { + + if (screenSharingPeer.isReceivingScreenSharingStream()) { + peers.set(key, screenSharingPeer); + } + + unsubscribes.push(screenSharingPeer.streamStore.subscribe((stream) => { + if (stream) { + peers.set(key, screenSharingPeer); + } else { + peers.delete(key); + } + set(peers); + })); + + }); + + set(peers); + + }); + + return function stop() { + unsubscribe(); + for (const unsubscribe of unsubscribes) { + unsubscribe(); + } + }; + }) +} + +export const screenSharingStreamStore = createScreenSharingStreamStore(); + + diff --git a/front/src/Stores/ScreenSharingStore.ts b/front/src/Stores/ScreenSharingStore.ts index ec5aa46f..ccb8c02c 100644 --- a/front/src/Stores/ScreenSharingStore.ts +++ b/front/src/Stores/ScreenSharingStore.ts @@ -1,16 +1,10 @@ import {derived, get, Readable, readable, writable, Writable} from "svelte/store"; import {peerStore} from "./PeerStore"; -import {localUserStore} from "../Connexion/LocalUserStore"; -import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap"; -import {userMovingStore} from "./GameStore"; -import {HtmlUtils} from "../WebRtc/HtmlUtils"; -import { - audioConstraintStore, cameraEnergySavingStore, - enableCameraSceneVisibilityStore, - gameOverlayVisibilityStore, LocalStreamStoreValue, privacyShutdownStore, - requestedCameraState, - requestedMicrophoneState, videoConstraintStore +import type { + LocalStreamStoreValue, } from "./MediaStore"; +import {DivImportance} from "../WebRtc/LayoutManager"; +import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility"; declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -191,3 +185,33 @@ export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set) set($peerStore.size !== 0); }); + +export interface ScreenSharingLocalMedia { + uniqueId: string; + stream: MediaStream|null; + //subscribe(this: void, run: Subscriber, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber; +} + +/** + * The representation of the screen sharing stream. + */ +export const screenSharingLocalMedia = readable(null, function start(set) { + + const localMedia: ScreenSharingLocalMedia = { + uniqueId: "localScreenSharingStream", + stream: null + } + + const unsubscribe = screenSharingLocalStreamStore.subscribe((screenSharingLocalStream) => { + if (screenSharingLocalStream.type === "success") { + localMedia.stream = screenSharingLocalStream.stream; + } else { + localMedia.stream = null; + } + set(localMedia); + }); + + return function stop() { + unsubscribe(); + }; +}) diff --git a/front/src/Stores/ShowReportScreenStore.ts b/front/src/Stores/ShowReportScreenStore.ts new file mode 100644 index 00000000..f05e545a --- /dev/null +++ b/front/src/Stores/ShowReportScreenStore.ts @@ -0,0 +1,3 @@ +import {writable} from "svelte/store"; + +export const showReportScreenStore = writable<{userId: number, userName: string}|null>(null); diff --git a/front/src/Stores/StreamableCollectionStore.ts b/front/src/Stores/StreamableCollectionStore.ts new file mode 100644 index 00000000..bbcd354f --- /dev/null +++ b/front/src/Stores/StreamableCollectionStore.ts @@ -0,0 +1,43 @@ +import {derived, get, Readable, writable} from "svelte/store"; +import {ScreenSharingLocalMedia, screenSharingLocalMedia} from "./ScreenSharingStore"; +import { peerStore, screenSharingStreamStore} from "./PeerStore"; +import type {RemotePeer} from "../WebRtc/SimplePeer"; +import {LayoutMode} from "../WebRtc/LayoutManager"; + +export type Streamable = RemotePeer | ScreenSharingLocalMedia; + +export const layoutModeStore = writable(LayoutMode.Presentation); + +/** + * A store that contains everything that can produce a stream (so the peers + the local screen sharing stream) + */ +function createStreamableCollectionStore(): Readable> { + + return derived([ + screenSharingStreamStore, + peerStore, + screenSharingLocalMedia, + ], ([ + $screenSharingStreamStore, + $peerStore, + $screenSharingLocalMedia, + ], set) => { + + const peers = new Map(); + + const addPeer = (peer: Streamable) => { + peers.set(peer.uniqueId, peer); + }; + + $screenSharingStreamStore.forEach(addPeer); + $peerStore.forEach(addPeer); + + if ($screenSharingLocalMedia?.stream) { + addPeer($screenSharingLocalMedia); + } + + set(peers); + }); +} + +export const streamableCollectionStore = createStreamableCollectionStore(); diff --git a/front/src/Stores/VideoFocusStore.ts b/front/src/Stores/VideoFocusStore.ts new file mode 100644 index 00000000..28e948ce --- /dev/null +++ b/front/src/Stores/VideoFocusStore.ts @@ -0,0 +1,47 @@ +import {writable} from "svelte/store"; +import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer"; +import {VideoPeer} from "../WebRtc/VideoPeer"; +import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer"; +import type {Streamable} from "./StreamableCollectionStore"; + +/** + * A store that contains the peer / media that has currently the "importance" focus. + */ +function createVideoFocusStore() { + const { subscribe, set, update } = writable(null); + + let focusedMedia: Streamable | null = null; + + return { + subscribe, + focus: (media: Streamable) => { + focusedMedia = media; + set(media); + }, + removeFocus: () => { + focusedMedia = null; + set(null); + }, + toggleFocus: (media: Streamable) => { + if (media !== focusedMedia) { + focusedMedia = media; + } else { + focusedMedia = null; + } + set(focusedMedia); + }, + connectToSimplePeer: (simplePeer: SimplePeer) => { + simplePeer.registerPeerConnectionListener({ + onConnect(peer: RemotePeer) { + }, + onDisconnect(userId: number) { + if ((focusedMedia instanceof VideoPeer || focusedMedia instanceof ScreenSharingPeer) && focusedMedia.userId === userId) { + set(null); + } + } + }) + } + }; +} + +export const videoFocusStore = createVideoFocusStore(); diff --git a/front/src/WebRtc/DiscussionManager.ts b/front/src/WebRtc/DiscussionManager.ts index 504ee91b..b728eca0 100644 --- a/front/src/WebRtc/DiscussionManager.ts +++ b/front/src/WebRtc/DiscussionManager.ts @@ -1,9 +1,9 @@ import {HtmlUtils} from "./HtmlUtils"; -import type {ShowReportCallBack} from "./MediaManager"; import type {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {connectionManager} from "../Connexion/ConnectionManager"; import {GameConnexionTypes} from "../Url/UrlManager"; import {iframeListener} from "../Api/IframeListener"; +import {showReportScreenStore} from "../Stores/ShowReportScreenStore"; export type SendMessageCallback = (message:string) => void; @@ -104,11 +104,10 @@ export class DiscussionManager { } public addParticipant( - userId: number|string, + userId: number|'me', name: string|undefined, img?: string|undefined, isMe: boolean = false, - showReportCallBack?: ShowReportCallBack ) { const divParticipant: HTMLDivElement = document.createElement('div'); divParticipant.classList.add('participant'); @@ -132,16 +131,13 @@ export class DiscussionManager { !isMe && connectionManager.getConnexionType && connectionManager.getConnexionType !== GameConnexionTypes.anonymous + && userId !== 'me' ) { const reportBanUserAction: HTMLButtonElement = document.createElement('button'); reportBanUserAction.classList.add('report-btn') reportBanUserAction.innerText = 'Report'; reportBanUserAction.addEventListener('click', () => { - if(showReportCallBack) { - showReportCallBack(`${userId}`, name); - }else{ - console.info('report feature is not activated!'); - } + showReportScreenStore.set({ userId: userId, userName: name ? name : ''}); }); divParticipant.appendChild(reportBanUserAction); } diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts index 3d92baac..da295a98 100644 --- a/front/src/WebRtc/LayoutManager.ts +++ b/front/src/WebRtc/LayoutManager.ts @@ -15,14 +15,6 @@ export enum DivImportance { Normal = "Normal", } -/** - * Classes implementing this interface can be notified when the center of the screen (the player position) should be - * changed. - */ -export interface CenterListener { - onCenterChange(): void; -} - export const ON_ACTION_TRIGGER_BUTTON = 'onaction'; export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger'; @@ -34,293 +26,12 @@ 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. - */ +export type Box = {xStart: number, yStart: number, xEnd: number, yEnd: number}; + class LayoutManager { - private mode: LayoutMode = LayoutMode.Presentation; - - private importantDivs: Map = new Map(); - private normalDivs: Map = new Map(); - private listener: CenterListener|null = null; - private actionButtonTrigger: Map = new Map(); private actionButtonInformation: Map = new Map(); - public setListener(centerListener: CenterListener|null) { - this.listener = centerListener; - } - - public add(importance: DivImportance, userId: string, html: string): void { - const div = document.createElement('div'); - div.innerHTML = html; - div.id = "user-"+userId; - div.className = "media-container" - div.onclick = () => { - const parentId = div.parentElement?.id; - if (parentId === 'sidebar' || parentId === 'chat-mode') { - this.focusOn(userId); - } else { - this.removeFocusOn(userId); - } - } - - if (importance === DivImportance.Important) { - this.importantDivs.set(userId, div); - - // If this is the first video with high importance, let's switch mode automatically. - if (this.importantDivs.size === 1 && this.mode === LayoutMode.VideoChat) { - this.switchLayoutMode(LayoutMode.Presentation); - } - } else if (importance === DivImportance.Normal) { - this.normalDivs.set(userId, div); - } else { - throw new Error('Unexpected importance'); - } - - this.positionDiv(div, importance); - this.adjustVideoChatClass(); - this.listener?.onCenterChange(); - } - - private positionDiv(elem: HTMLDivElement, importance: DivImportance): void { - if (this.mode === LayoutMode.VideoChat) { - const chatModeDiv = HtmlUtils.getElementByIdOrFail('chat-mode'); - chatModeDiv.appendChild(elem); - } else { - if (importance === DivImportance.Important) { - const mainSectionDiv = HtmlUtils.getElementByIdOrFail('main-section'); - mainSectionDiv.appendChild(elem); - } else if (importance === DivImportance.Normal) { - const sideBarDiv = HtmlUtils.getElementByIdOrFail('sidebar'); - sideBarDiv.appendChild(elem); - } - } - } - - /** - * Put the screen in presentation mode and move elem in presentation mode (and all other videos in normal mode) - */ - private focusOn(userId: string): void { - const focusedDiv = this.getDivByUserId(userId); - for (const [importantUserId, importantDiv] of this.importantDivs.entries()) { - //this.positionDiv(importantDiv, DivImportance.Normal); - this.importantDivs.delete(importantUserId); - this.normalDivs.set(importantUserId, importantDiv); - } - this.normalDivs.delete(userId); - this.importantDivs.set(userId, focusedDiv); - //this.positionDiv(focusedDiv, DivImportance.Important); - this.switchLayoutMode(LayoutMode.Presentation); - } - - /** - * Removes userId from presentation mode - */ - private removeFocusOn(userId: string): void { - const importantDiv = this.importantDivs.get(userId); - if (importantDiv === undefined) { - throw new Error('Div with user id "'+userId+'" is not in important mode'); - } - this.normalDivs.set(userId, importantDiv); - this.importantDivs.delete(userId); - - this.positionDiv(importantDiv, DivImportance.Normal); - } - - private getDivByUserId(userId: string): HTMLDivElement { - let div = this.importantDivs.get(userId); - if (div !== undefined) { - return div; - } - div = this.normalDivs.get(userId); - if (div !== undefined) { - return div; - } - throw new Error('Could not find media with user id '+userId); - } - - /** - * Removes the DIV matching userId. - */ - public remove(userId: string): void { - console.log('Removing video for userID '+userId+'.'); - let div = this.importantDivs.get(userId); - if (div !== undefined) { - div.remove(); - this.importantDivs.delete(userId); - this.adjustVideoChatClass(); - this.listener?.onCenterChange(); - return; - } - - div = this.normalDivs.get(userId); - if (div !== undefined) { - div.remove(); - this.normalDivs.delete(userId); - this.adjustVideoChatClass(); - this.listener?.onCenterChange(); - return; - } - - console.log('Cannot remove userID '+userId+'. Already removed?'); - //throw new Error('Could not find user ID "'+userId+'"'); - } - - private adjustVideoChatClass(): void { - const chatModeDiv = HtmlUtils.getElementByIdOrFail('chat-mode'); - chatModeDiv.classList.remove('one-col', 'two-col', 'three-col', 'four-col'); - - const nbUsers = this.importantDivs.size + this.normalDivs.size; - - if (nbUsers <= 1) { - chatModeDiv.classList.add('one-col'); - } else if (nbUsers <= 4) { - chatModeDiv.classList.add('two-col'); - } else if (nbUsers <= 9) { - chatModeDiv.classList.add('three-col'); - } else { - chatModeDiv.classList.add('four-col'); - } - } - - public switchLayoutMode(layoutMode: LayoutMode) { - this.mode = layoutMode; - - if (layoutMode === LayoutMode.Presentation) { - HtmlUtils.getElementByIdOrFail('sidebar').style.display = 'flex'; - HtmlUtils.getElementByIdOrFail('main-section').style.display = 'flex'; - HtmlUtils.getElementByIdOrFail('chat-mode').style.display = 'none'; - } else { - HtmlUtils.getElementByIdOrFail('sidebar').style.display = 'none'; - HtmlUtils.getElementByIdOrFail('main-section').style.display = 'none'; - HtmlUtils.getElementByIdOrFail('chat-mode').style.display = 'grid'; - } - - for (const div of this.importantDivs.values()) { - this.positionDiv(div, DivImportance.Important); - } - for (const div of this.normalDivs.values()) { - this.positionDiv(div, DivImportance.Normal); - } - this.listener?.onCenterChange(); - } - - public getLayoutMode(): LayoutMode { - return this.mode; - } - - /*public getGameCenter(): {x: number, y: number} { - - }*/ - - /** - * Tries to find the biggest available box of remaining space (this is a space where we can center the character) - */ - public findBiggestAvailableArray(): {xStart: number, yStart: number, xEnd: number, yEnd: number} { - const game = HtmlUtils.querySelectorOrFail('#game canvas'); - if (this.mode === LayoutMode.VideoChat) { - const children = document.querySelectorAll('div.chat-mode > div'); - const htmlChildren = Array.from(children.values()); - - // No chat? Let's go full center - if (htmlChildren.length === 0) { - return { - xStart: 0, - yStart: 0, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } - - const lastDiv = htmlChildren[htmlChildren.length - 1]; - // Compute area between top right of the last div and bottom right of window - const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth)) - * (game.offsetHeight - lastDiv.offsetTop); - - // Compute area between bottom of last div and bottom of the screen on whole width - const area2 = game.offsetWidth - * (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight)); - - if (area1 < 0 && area2 < 0) { - // If screen is full, let's not attempt something foolish and simply center character in the middle. - return { - xStart: 0, - yStart: 0, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } - if (area1 <= area2) { - console.log('lastDiv', lastDiv.offsetTop, lastDiv.offsetHeight); - return { - xStart: 0, - yStart: lastDiv.offsetTop + lastDiv.offsetHeight, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } else { - console.log('lastDiv', lastDiv.offsetTop); - return { - xStart: lastDiv.offsetLeft + lastDiv.offsetWidth, - yStart: lastDiv.offsetTop, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } - } else { - // Possible destinations: at the center bottom or at the right bottom. - const mainSectionChildren = Array.from(document.querySelectorAll('div.main-section > div').values()); - const sidebarChildren = Array.from(document.querySelectorAll('aside.sidebar > div').values()); - - // No presentation? Let's center on the screen - if (mainSectionChildren.length === 0) { - return { - xStart: 0, - yStart: 0, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } - - // At this point, we know we have at least one element in the main section. - const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1]; - - const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight)) - * (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth); - - let leftSideBar: number; - let bottomSideBar: number; - if (sidebarChildren.length === 0) { - leftSideBar = HtmlUtils.getElementByIdOrFail('sidebar').offsetLeft; - bottomSideBar = 0; - } else { - const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1]; - leftSideBar = lastSideBarChildren.offsetLeft; - bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight; - } - const sideBarArea = (game.offsetWidth - leftSideBar) - * (game.offsetHeight - bottomSideBar); - - if (presentationArea <= sideBarArea) { - return { - xStart: leftSideBar, - yStart: bottomSideBar, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } else { - return { - xStart: 0, - yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight, - xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area - yEnd: game.offsetHeight - } - } - } - } - public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager){ //delete previous element this.removeActionButton(id, userInputManager); diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index fb85252f..faa5edf7 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -7,7 +7,7 @@ import type { UserSimplePeerInterface } from "./SimplePeer"; import { SoundMeter } from "../Phaser/Components/SoundMeter"; import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable"; import { - gameOverlayVisibilityStore, localStreamStore, + localStreamStore, } from "../Stores/MediaStore"; import { screenSharingLocalStreamStore @@ -17,20 +17,13 @@ import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore" export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void; export type StartScreenSharingCallback = (media: MediaStream) => void; export type StopScreenSharingCallback = (media: MediaStream) => void; -export type ReportCallback = (message: string) => void; -export type ShowReportCallBack = (userId: string, userName: string | undefined) => void; -export type HelpCameraSettingsCallBack = () => void; import {cowebsiteCloseButtonId} from "./CoWebsiteManager"; +import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility"; export class MediaManager { - private remoteVideo: Map = new Map(); - //FIX ME SOUNDMETER: check stalability of sound meter calculation - //mySoundMeterElement: HTMLDivElement; - - startScreenSharingCallBacks: Set = new Set(); - stopScreenSharingCallBacks: Set = new Set(); - showReportModalCallBacks: Set = new Set(); + startScreenSharingCallBacks : Set = new Set(); + stopScreenSharingCallBacks : Set = new Set(); @@ -40,21 +33,8 @@ export class MediaManager { private userInputManager?: UserInputManager; - //FIX ME SOUNDMETER: check stalability of sound meter calculation - /*private mySoundMeter?: SoundMeter|null; - private soundMeters: Map = new Map(); - private soundMeterElements: Map = new Map();*/ - constructor() { - this.pingCameraStatus(); - - //FIX ME SOUNDMETER: check stability of sound meter calculation - /*this.mySoundMeterElement = (HtmlUtils.getElementByIdOrFail('mySoundMeter')); - this.mySoundMeterElement.childNodes.forEach((value: ChildNode, index) => { - this.mySoundMeterElement.children.item(index)?.classList.remove('active'); - });*/ - //Check of ask notification navigator permission this.getNotification(); @@ -68,7 +48,6 @@ export class MediaManager { } }); - let isScreenSharing = false; screenSharingLocalStreamStore.subscribe((result) => { if (result.type === 'error') { console.error(result.error); @@ -77,32 +56,7 @@ export class MediaManager { }, this.userInputManager); return; } - - if (result.stream !== null) { - isScreenSharing = true; - this.addScreenSharingActiveVideo('me', DivImportance.Normal); - HtmlUtils.getElementByIdOrFail('screen-sharing-me').srcObject = result.stream; - } else { - if (isScreenSharing) { - isScreenSharing = false; - this.removeActiveScreenSharingVideo('me'); - } - } - }); - - /*screenSharingAvailableStore.subscribe((available) => { - if (available) { - document.querySelector('.btn-monitor')?.classList.remove('hide'); - } else { - document.querySelector('.btn-monitor')?.classList.add('hide'); - } - });*/ - } - - public updateScene(){ - //FIX ME SOUNDMETER: check stability of sound meter calculation - //this.updateSoudMeter(); } public showGameOverlay(): void { @@ -137,71 +91,6 @@ export class MediaManager { gameOverlayVisibilityStore.hideGameOverlay(); } - - - addActiveVideo(user: UserSimplePeerInterface, userName: string = "") { - - const userId = '' + user.userId - - userName = userName.toUpperCase(); - const color = this.getColorByString(userName); - - const html = ` -
-
- - ${userName} - - - - -
- - - - - -
-
- `; - - layoutManager.add(DivImportance.Normal, userId, html); - - this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail(userId)); - - //permit to create participant in discussion part - const showReportUser = () => { - for (const callBack of this.showReportModalCallBacks) { - callBack(userId, userName); - } - }; - this.addNewParticipant(userId, userName, undefined, showReportUser); - - const reportBanUserActionEl: HTMLImageElement = HtmlUtils.getElementByIdOrFail(`report-${userId}`); - reportBanUserActionEl.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - showReportUser(); - }); - } - - addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){ - - userId = this.getScreenSharingId(userId); - const html = ` -
- -
- `; - - layoutManager.add(divImportance, userId, html); - - this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail(userId)); - } - private getScreenSharingId(userId: string): string { return `screen-sharing-${userId}`; } @@ -248,61 +137,6 @@ export class MediaManager { const blockLogoElement = HtmlUtils.getElementByIdOrFail('blocking-' + userId); show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active'); } - addStreamRemoteVideo(userId: string, stream: MediaStream): void { - const remoteVideo = this.remoteVideo.get(userId); - if (remoteVideo === undefined) { - throw `Unable to find video for ${userId}`; - } - remoteVideo.srcObject = stream; - - //FIX ME SOUNDMETER: check stalability of sound meter calculation - //sound metter - /*const soundMeter = new SoundMeter(); - soundMeter.connectToSource(stream, new AudioContext()); - this.soundMeters.set(userId, soundMeter); - this.soundMeterElements.set(userId, HtmlUtils.getElementByIdOrFail('soundMeter-'+userId));*/ - } - addStreamRemoteScreenSharing(userId: string, stream: MediaStream) { - // In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet - const remoteVideo = this.remoteVideo.get(this.getScreenSharingId(userId)); - if (remoteVideo === undefined) { - this.addScreenSharingActiveVideo(userId); - } - - this.addStreamRemoteVideo(this.getScreenSharingId(userId), stream); - } - - removeActiveVideo(userId: string) { - layoutManager.remove(userId); - this.remoteVideo.delete(userId); - - //FIX ME SOUNDMETER: check stalability of sound meter calculation - /*this.soundMeters.get(userId)?.stop(); - this.soundMeters.delete(userId); - this.soundMeterElements.delete(userId);*/ - - //permit to remove user in discussion part - this.removeParticipant(userId); - } - removeActiveScreenSharingVideo(userId: string) { - this.removeActiveVideo(this.getScreenSharingId(userId)) - } - - isConnecting(userId: string): void { - const connectingSpinnerDiv = this.getSpinner(userId); - if (connectingSpinnerDiv === null) { - return; - } - connectingSpinnerDiv.style.display = 'block'; - } - - isConnected(userId: string): void { - const connectingSpinnerDiv = this.getSpinner(userId); - if (connectingSpinnerDiv === null) { - return; - } - connectingSpinnerDiv.style.display = 'none'; - } isError(userId: string): void { console.info("isError", `div-${userId}`); @@ -326,33 +160,11 @@ export class MediaManager { if (!element) { return null; } - const connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement | null; - return connnectingSpinnerDiv; + const connectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null; + return connectingSpinnerDiv; } - private getColorByString(str: String): String | null { - let hash = 0; - if (str.length === 0) return null; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - hash = hash & hash; - } - let color = '#'; - for (let i = 0; i < 3; i++) { - const value = (hash >> (i * 8)) & 255; - color += ('00' + value.toString(16)).substr(-2); - } - return color; - } - - public addNewParticipant(userId: number | string, name: string | undefined, img?: string, showReportUserCallBack?: ShowReportCallBack) { - discussionManager.addParticipant(userId, name, img, false, showReportUserCallBack); - } - - public removeParticipant(userId: number | string) { - discussionManager.removeParticipant(userId); - } - public addTriggerCloseJitsiFrameButton(id: String, Function: Function) { + public addTriggerCloseJitsiFrameButton(id: String, Function: Function){ this.triggerCloseJistiFrame.set(id, Function); } @@ -365,16 +177,6 @@ export class MediaManager { callback(); } } - /** - * For some reasons, the microphone muted icon or the stream is not always up to date. - * Here, every 30 seconds, we are "reseting" the streams and sending again the constraints to the other peers via the data channel again (see SimplePeer::pushVideoToRemoteUser) - **/ - private pingCameraStatus() { - /*setInterval(() => { - console.log('ping camera status'); - this.triggerUpdatedLocalStreamCallbacks(this.localStream); - }, 30000);*/ - } public addNewMessage(name: string, message: string, isMe: boolean = false) { discussionManager.addMessage(name, message, isMe); @@ -389,61 +191,11 @@ export class MediaManager { discussionManager.onSendMessageCallback(userId, callback); } - get activatedDiscussion() { - return discussionManager.activatedDiscussion; - } - - public setUserInputManager(userInputManager: UserInputManager) { + public setUserInputManager(userInputManager : UserInputManager){ this.userInputManager = userInputManager; discussionManager.setUserInputManager(userInputManager); } - public setShowReportModalCallBacks(callback: ShowReportCallBack) { - this.showReportModalCallBacks.add(callback); - } - - //FIX ME SOUNDMETER: check stalability of sound meter calculation - /*updateSoudMeter(){ - try{ - const volume = parseInt(((this.mySoundMeter ? this.mySoundMeter.getVolume() : 0) / 10).toFixed(0)); - this.setVolumeSoundMeter(volume, this.mySoundMeterElement); - - for(const indexUserId of this.soundMeters.keys()){ - const soundMeter = this.soundMeters.get(indexUserId); - const soundMeterElement = this.soundMeterElements.get(indexUserId); - if (!soundMeter || !soundMeterElement) { - return; - } - const volumeByUser = parseInt((soundMeter.getVolume() / 10).toFixed(0)); - this.setVolumeSoundMeter(volumeByUser, soundMeterElement); - } - } catch (err) { - //console.error(err); - } - }*/ - - private setVolumeSoundMeter(volume: number, element: HTMLDivElement) { - if (volume <= 0 && !element.classList.contains('active')) { - return; - } - element.classList.remove('active'); - if (volume <= 0) { - return; - } - element.classList.add('active'); - element.childNodes.forEach((value: ChildNode, index) => { - const elementChildren = element.children.item(index); - if (!elementChildren) { - return; - } - elementChildren.classList.remove('active'); - if ((index + 1) > volume) { - return; - } - elementChildren.classList.add('active'); - }); - } - public getNotification(){ //Get notification if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") { diff --git a/front/src/WebRtc/ScreenSharingPeer.ts b/front/src/WebRtc/ScreenSharingPeer.ts index d797f59b..be534276 100644 --- a/front/src/WebRtc/ScreenSharingPeer.ts +++ b/front/src/WebRtc/ScreenSharingPeer.ts @@ -1,9 +1,11 @@ import type * as SimplePeerNamespace from "simple-peer"; import {mediaManager} from "./MediaManager"; -import {STUN_SERVER, TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable"; +import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable"; import type {RoomConnection} from "../Connexion/RoomConnection"; -import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer"; +import {MESSAGE_TYPE_CONSTRAINT, PeerStatus} from "./VideoPeer"; import type {UserSimplePeerInterface} from "./SimplePeer"; +import {Readable, readable, writable, Writable} from "svelte/store"; +import {videoFocusStore} from "../Stores/VideoFocusStore"; const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); @@ -17,9 +19,12 @@ export class ScreenSharingPeer extends Peer { private isReceivingStream:boolean = false; public toClose: boolean = false; public _connected: boolean = false; - private userId: number; + public readonly userId: number; + public readonly uniqueId: string; + public readonly streamStore: Readable; + public readonly statusStore: Readable; - constructor(user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, stream: MediaStream | null) { + constructor(user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, stream: MediaStream | null) { super({ initiator: initiator ? initiator : false, //reconnectTimer: 10000, @@ -38,6 +43,55 @@ export class ScreenSharingPeer extends Peer { }); this.userId = user.userId; + this.uniqueId = 'screensharing_'+this.userId; + + this.streamStore = readable(null, (set) => { + const onStream = (stream: MediaStream|null) => { + videoFocusStore.focus(this); + set(stream); + }; + const onData = (chunk: Buffer) => { + // We unfortunately need to rely on an event to let the other party know a stream has stopped. + // It seems there is no native way to detect that. + // TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event + const message = JSON.parse(chunk.toString('utf8')); + if (message.streamEnded !== true) { + console.error('Unexpected message on screen sharing peer connection'); + return; + } + set(null); + } + + this.on('stream', onStream); + this.on('data', onData); + + return () => { + this.off('stream', onStream); + this.off('data', onData); + }; + }); + + this.statusStore = readable("connecting", (set) => { + const onConnect = () => { + set('connected'); + }; + const onError = () => { + set('error'); + }; + const onClose = () => { + set('closed'); + }; + + this.on('connect', onConnect); + this.on('error', onError); + this.on('close', onClose); + + return () => { + this.off('connect', onConnect); + this.off('error', onError); + this.off('close', onClose); + }; + }); //start listen signal for the peer connection this.on('signal', (data: unknown) => { @@ -54,27 +108,13 @@ export class ScreenSharingPeer extends Peer { this.destroy(); }); - this.on('data', (chunk: Buffer) => { - // We unfortunately need to rely on an event to let the other party know a stream has stopped. - // It seems there is no native way to detect that. - const message = JSON.parse(chunk.toString('utf8')); - if (message.streamEnded !== true) { - console.error('Unexpected message on screen sharing peer connection'); - return; - } - mediaManager.removeActiveScreenSharingVideo("" + this.userId); - }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any this.on('error', (err: any) => { console.error(`screen sharing error => ${this.userId} => ${err.code}`, err); - //mediaManager.isErrorScreenSharing(this.userId); }); this.on('connect', () => { this._connected = true; - // FIXME: we need to put the loader on the screen sharing connection - mediaManager.isConnected("" + this.userId); console.info(`connect => ${this.userId}`); }); @@ -88,7 +128,6 @@ export class ScreenSharingPeer extends Peer { } private sendWebrtcScreenSharingSignal(data: unknown) { - //console.log("sendWebrtcScreenSharingSignal", data); try { this.connection.sendWebrtcScreenSharingSignal(data, this.userId); }catch (e) { @@ -100,13 +139,9 @@ export class ScreenSharingPeer extends Peer { * Sends received stream to screen. */ private stream(stream?: MediaStream) { - //console.log(`ScreenSharingPeer::stream => ${this.userId}`, stream); - //console.log(`stream => ${this.userId} => `, stream); if(!stream){ - mediaManager.removeActiveScreenSharingVideo("" + this.userId); this.isReceivingStream = false; } else { - mediaManager.addStreamRemoteScreenSharing("" + this.userId, stream); this.isReceivingStream = true; } } @@ -121,7 +156,6 @@ export class ScreenSharingPeer extends Peer { if(!this.toClose){ return; } - mediaManager.removeActiveScreenSharingVideo("" + this.userId); // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. //console.log('Closing connection with '+userId); diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 2a502bab..ecc0e21b 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -6,19 +6,15 @@ import { mediaManager, StartScreenSharingCallback, StopScreenSharingCallback, - UpdatedLocalStreamCallback } from "./MediaManager"; import {ScreenSharingPeer} from "./ScreenSharingPeer"; import {MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer"; import type {RoomConnection} from "../Connexion/RoomConnection"; -import {connectionManager} from "../Connexion/ConnectionManager"; -import {GameConnexionTypes} from "../Url/UrlManager"; import {blackListManager} from "./BlackListManager"; import {get} from "svelte/store"; import {localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore} from "../Stores/MediaStore"; import {screenSharingLocalStreamStore} from "../Stores/ScreenSharingStore"; -import {DivImportance, layoutManager} from "./LayoutManager"; -import {HtmlUtils} from "./HtmlUtils"; +import {discussionManager} from "./DiscussionManager"; export interface UserSimplePeerInterface{ userId: number; @@ -28,8 +24,10 @@ export interface UserSimplePeerInterface{ webRtcPassword?: string|undefined; } +export type RemotePeer = VideoPeer | ScreenSharingPeer; + export interface PeerConnectionListener { - onConnect(user: UserSimplePeerInterface): void; + onConnect(user: RemotePeer): void; onDisconnect(userId: number): void; } @@ -124,7 +122,6 @@ export class SimplePeer { // This would be symmetrical to the way we handle disconnection. //start connection - //console.log('receiveWebrtcStart. Initiator: ', user.initiator) if(!user.initiator){ return; } @@ -159,20 +156,15 @@ export class SimplePeer { let name = user.name; if (!name) { - const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === user.userId); - if (userSearch) { - name = userSearch.name; - } + name = this.getName(user.userId); } - mediaManager.removeActiveVideo("" + user.userId); - - mediaManager.addActiveVideo(user, name); + discussionManager.removeParticipant(user.userId); this.lastWebrtcUserName = user.webRtcUser; this.lastWebrtcPassword = user.webRtcPassword; - const peer = new VideoPeer(user, user.initiator ? user.initiator : false, this.Connection, localStream); + const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream); //permit to send message mediaManager.addSendMessageCallback(user.userId,(message: string) => { @@ -196,11 +188,20 @@ export class SimplePeer { this.PeerConnectionArray.set(user.userId, peer); for (const peerConnectionListener of this.peerConnectionListeners) { - peerConnectionListener.onConnect(user); + peerConnectionListener.onConnect(peer); } return peer; } + private getName(userId: number): string { + const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId); + if (userSearch) { + return userSearch.name || ''; + } else { + return ''; + } + } + /** * create peer connection to bind users */ @@ -221,23 +222,19 @@ export class SimplePeer { return null; } - // We should display the screen sharing ONLY if we are not initiator - if (!user.initiator) { - mediaManager.removeActiveScreenSharingVideo("" + user.userId); - mediaManager.addScreenSharingActiveVideo("" + user.userId); - } - // Enrich the user with last known credentials (if they are not set in the user object, which happens when a user triggers the screen sharing) if (user.webRtcUser === undefined) { user.webRtcUser = this.lastWebrtcUserName; user.webRtcPassword = this.lastWebrtcPassword; } - const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, this.Connection, stream); + const name = this.getName(user.userId); + + const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, name, this.Connection, stream); this.PeerScreenSharingConnectionArray.set(user.userId, peer); for (const peerConnectionListener of this.peerConnectionListeners) { - peerConnectionListener.onConnect(user); + peerConnectionListener.onConnect(peer); } return peer; } @@ -288,7 +285,7 @@ export class SimplePeer { */ private closeScreenSharingConnection(userId : number) { try { - mediaManager.removeActiveScreenSharingVideo("" + userId); + //mediaManager.removeActiveScreenSharingVideo("" + userId); const peer = this.PeerScreenSharingConnectionArray.get(userId); if (peer === undefined) { console.warn("closeScreenSharingConnection => Tried to close connection for user "+userId+" but could not find user") diff --git a/front/src/WebRtc/VideoPeer.ts b/front/src/WebRtc/VideoPeer.ts index 5ca8952c..0840d6ff 100644 --- a/front/src/WebRtc/VideoPeer.ts +++ b/front/src/WebRtc/VideoPeer.ts @@ -5,11 +5,15 @@ import type {RoomConnection} from "../Connexion/RoomConnection"; import {blackListManager} from "./BlackListManager"; import type {Subscription} from "rxjs"; import type {UserSimplePeerInterface} from "./SimplePeer"; -import {get} from "svelte/store"; +import {get, readable, Readable} from "svelte/store"; import {obtainedMediaConstraintStore} from "../Stores/MediaStore"; +import {DivImportance} from "./LayoutManager"; +import {discussionManager} from "./DiscussionManager"; const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); +export type PeerStatus = "connecting" | "connected" | "error" | "closed"; + export const MESSAGE_TYPE_CONSTRAINT = 'constraint'; export const MESSAGE_TYPE_MESSAGE = 'message'; export const MESSAGE_TYPE_BLOCKED = 'blocked'; @@ -22,12 +26,15 @@ export class VideoPeer extends Peer { public _connected: boolean = false; private remoteStream!: MediaStream; private blocked: boolean = false; - private userId: number; - private userName: string; + public readonly userId: number; + public readonly uniqueId: string; private onBlockSubscribe: Subscription; private onUnBlockSubscribe: Subscription; + public readonly streamStore: Readable; + public readonly statusStore: Readable; + public readonly constraintsStore: Readable; - constructor(public user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, localStream: MediaStream | null) { + constructor(public user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, localStream: MediaStream | null) { super({ initiator: initiator ? initiator : false, //reconnectTimer: 10000, @@ -46,7 +53,68 @@ export class VideoPeer extends Peer { }); this.userId = user.userId; - this.userName = user.name || ''; + this.uniqueId = 'video_'+this.userId; + + this.streamStore = readable(null, (set) => { + const onStream = (stream: MediaStream|null) => { + set(stream); + }; + const onData = (chunk: Buffer) => { + this.on('data', (chunk: Buffer) => { + const message = JSON.parse(chunk.toString('utf8')); + if (message.type === MESSAGE_TYPE_CONSTRAINT) { + if (!message.video) { + set(null); + } + } + }); + } + + this.on('stream', onStream); + this.on('data', onData); + + return () => { + this.off('stream', onStream); + this.off('data', onData); + }; + }); + + this.constraintsStore = readable(null, (set) => { + const onData = (chunk: Buffer) => { + const message = JSON.parse(chunk.toString('utf8')); + if(message.type === MESSAGE_TYPE_CONSTRAINT) { + set(message); + } + } + + this.on('data', onData); + + return () => { + this.off('data', onData); + }; + }); + + this.statusStore = readable("connecting", (set) => { + const onConnect = () => { + set('connected'); + }; + const onError = () => { + set('error'); + }; + const onClose = () => { + set('closed'); + }; + + this.on('connect', onConnect); + this.on('error', onError); + this.on('close', onClose); + + return () => { + this.off('connect', onConnect); + this.off('error', onError); + this.off('close', onClose); + }; + }); //start listen signal for the peer connection this.on('signal', (data: unknown) => { @@ -69,8 +137,6 @@ export class VideoPeer extends Peer { this.on('connect', () => { this._connected = true; - mediaManager.isConnected("" + this.userId); - console.info(`connect => ${this.userId}`); }); this.on('data', (chunk: Buffer) => { @@ -152,7 +218,6 @@ export class VideoPeer extends Peer { if (blackListManager.isBlackListed(this.userId) || this.blocked) { this.toggleRemoteStream(false); } - mediaManager.addStreamRemoteVideo("" + this.userId, stream); }catch (err){ console.error(err); } @@ -169,7 +234,7 @@ export class VideoPeer extends Peer { } this.onBlockSubscribe.unsubscribe(); this.onUnBlockSubscribe.unsubscribe(); - mediaManager.removeActiveVideo("" + this.userId); + discussionManager.removeParticipant(this.userId); // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. super.destroy(error); diff --git a/front/style/style.scss b/front/style/style.scss index 5c958309..eb34287a 100644 --- a/front/style/style.scss +++ b/front/style/style.scss @@ -35,102 +35,109 @@ body .message-info.info{ body .message-info.warning{ background: #ffa500d6; } -.video-container{ + +.video-container { position: relative; transition: all 0.2s ease; background-color: #00000099; cursor: url('./images/cursor_pointer.png'), pointer; -} -.video-container i{ - position: absolute; - width: 100px; - height: 100px; - left: calc(50% - 50px); - top: calc(50% - 50px); - background-color: black; - border-radius: 50%; - text-align: center; - padding-top: 32px; - font-size: 28px; - color: white; - overflow: hidden; -} -.video-container img{ - position: absolute; - display: none; - width: 40px; - height: 40px; - left: 5px; - bottom: 5px; - padding: 10px; - z-index: 2; -} -.video-container img.block-logo { - left: 30%; - bottom: 15%; - width: 150px; - height: 150px; -} + video { + width: 100%; + height: 100%; + max-height: 90vh; + cursor: url('./images/cursor_pointer.png'), pointer; + } -.video-container button.report{ - display: block; - cursor: url('./images/cursor_pointer.png'), pointer; - background: none; - background-color: rgba(0, 0, 0, 0); - border: none; - background-color: black; - border-radius: 15px; - position: absolute; - width: 0px; - height: 35px; - right: 5px; - bottom: 5px; - padding: 0px; - overflow: hidden; - z-index: 2; - transition: all .5s ease; -} + i { + position: absolute; + width: 100px; + height: 100px; + left: calc(50% - 50px); + top: calc(50% - 50px); + background-color: black; + border-radius: 50%; + text-align: center; + padding-top: 32px; + font-size: 28px; + color: white; + overflow: hidden; + } -.video-container:hover button.report{ - width: 35px; - padding: 10px; -} + img { + position: absolute; + display: none; + width: 40px; + height: 40px; + left: 5px; + bottom: 5px; + padding: 10px; + z-index: 2; + } -.video-container button.report:hover { - width: 160px; -} + img.block-logo { + left: 30%; + bottom: 15%; + width: 150px; + height: 150px; + } -.video-container button.report img{ - position: absolute; - display: block; - bottom: 5px; - left: 5px; - margin: 0; - padding: 0; - cursor: url('./images/cursor_pointer.png'), pointer; - width: 25px; - height: 25px; -} -.video-container button.report span{ - position: absolute; - bottom: 6px; - left: 36px; - color: white; - font-size: 16px; - cursor: url('./images/cursor_pointer.png'), pointer; -} -.video-container img.active { - display: block !important; -} + button.report{ + display: block; + cursor: url('./images/cursor_pointer.png'), pointer; + background: none; + background-color: rgba(0, 0, 0, 0); + border: none; + background-color: black; + border-radius: 15px; + position: absolute; + width: 0px; + height: 35px; + right: 5px; + bottom: 5px; + padding: 0px; + overflow: hidden; + z-index: 2; + transition: all .5s ease; -.video-container video{ - height: 100%; - cursor: url('./images/cursor_pointer.png'), pointer; -} + img{ + position: absolute; + display: block; + bottom: 5px; + left: 5px; + margin: 0; + padding: 0; + cursor: url('./images/cursor_pointer.png'), pointer; + width: 25px; + height: 25px; + } -.video-container video:focus{ - outline: none; + span { + position: absolute; + bottom: 6px; + left: 36px; + color: white; + font-size: 16px; + cursor: url('./images/cursor_pointer.png'), pointer; + } + + img.active { + display: block !important; + } + } + + &:hover button.report{ + width: 35px; + padding: 10px; + + &:hover { + width: 160px; + } + } + + video:focus{ + outline: none; + } } .video-container.div-myCamVideo{ @@ -204,7 +211,7 @@ video.myCamVideo{ display: inline-flex; bottom: 10px; right: 15px; - width: 180px; + width: 240px; height: 40px; text-align: center; align-content: center; @@ -224,7 +231,7 @@ video.myCamVideo{ background: #666; box-shadow: 2px 2px 24px #444; border-radius: 48px; - transform: translateY(20px); + transform: translateY(15px); transition-timing-function: ease-in-out; margin: 0 4%; } @@ -263,6 +270,16 @@ video.myCamVideo{ .btn-cam-action:hover .btn-monitor.hide{ transform: translateY(60px); } +.btn-layout{ + pointer-events: auto; + transition: all .15s; +} +.btn-layout.hide { + transform: translateY(60px); +} +.btn-cam-action:hover .btn-layout.hide{ + transform: translateY(60px); +} .btn-copy{ pointer-events: auto; transition: all .3s; diff --git a/front/webpack.config.ts b/front/webpack.config.ts index 3a69b74a..3bfc0a48 100644 --- a/front/webpack.config.ts +++ b/front/webpack.config.ts @@ -95,6 +95,7 @@ module.exports = { if (warning.code === 'a11y-no-onchange') { return } if (warning.code === 'a11y-autofocus') { return } + if (warning.code === 'a11y-media-has-caption') { return } // process as usual handleWarning(warning);