Merge pull request #1163 from thecodingmachine/svelte_video_overlay
Migrating the video overlay in Svelte
This commit is contained in:
commit
6ed8ce9af1
BIN
front/dist/resources/objects/layout_modes.png
vendored
BIN
front/dist/resources/objects/layout_modes.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 297 B |
@ -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}'"
|
||||
},
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="typescript">
|
||||
import {enableCameraSceneVisibilityStore, gameOverlayVisibilityStore} from "../Stores/MediaStore";
|
||||
import {enableCameraSceneVisibilityStore} from "../Stores/MediaStore";
|
||||
import CameraControls from "./CameraControls.svelte";
|
||||
import MyCamera from "./MyCamera.svelte";
|
||||
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
|
||||
@ -21,10 +21,13 @@
|
||||
import AudioPlaying from "./UI/AudioPlaying.svelte";
|
||||
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
|
||||
import ErrorDialog from "./UI/ErrorDialog.svelte";
|
||||
import VideoOverlay from "./Video/VideoOverlay.svelte";
|
||||
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
|
||||
import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore";
|
||||
import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@ -68,6 +71,7 @@
|
||||
-->
|
||||
{#if $gameOverlayVisibilityStore}
|
||||
<div>
|
||||
<VideoOverlay></VideoOverlay>
|
||||
<MyCamera></MyCamera>
|
||||
<CameraControls></CameraControls>
|
||||
</div>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="btn-cam-action">
|
||||
<div class="btn-layout" on:click={switchLayoutMode} class:hide={$peerStore.size === 0}>
|
||||
{#if $layoutModeStore === LayoutMode.Presentation }
|
||||
<img src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode">
|
||||
{:else}
|
||||
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode">
|
||||
{/if}
|
||||
</div>
|
||||
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}>
|
||||
{#if $requestedScreenSharingState}
|
||||
<img src={monitorImg} alt="Start screen sharing">
|
||||
|
@ -58,7 +58,7 @@
|
||||
|
||||
|
||||
<div class="horizontal-sound-meter" class:active={display}>
|
||||
{#each [...Array(NB_BARS).keys()] as i}
|
||||
{#each [...Array(NB_BARS).keys()] as i (i)}
|
||||
<div style={color(i, volume)}></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -6,8 +6,6 @@
|
||||
export let stream: MediaStream|null;
|
||||
let volume = 0;
|
||||
|
||||
const NB_BARS = 5;
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
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 @@
|
||||
|
||||
|
||||
<div class="sound-progress" class:active={display}>
|
||||
<span class:active={volume > 1}></span>
|
||||
<span class:active={volume > 2}></span>
|
||||
<span class:active={volume > 3}></span>
|
||||
<span class:active={volume > 4}></span>
|
||||
<span class:active={volume > 5}></span>
|
||||
<span class:active={volume > 10}></span>
|
||||
<span class:active={volume > 15}></span>
|
||||
<span class:active={volume > 40}></span>
|
||||
<span class:active={volume > 70}></span>
|
||||
</div>
|
||||
|
35
front/src/Components/Video/ChatLayout.svelte
Normal file
35
front/src/Components/Video/ChatLayout.svelte
Normal file
@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore";
|
||||
import {afterUpdate, onDestroy} from "svelte";
|
||||
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore";
|
||||
import MediaBox from "./MediaBox.svelte";
|
||||
|
||||
let cssClass = 'one-col';
|
||||
|
||||
const unsubscribe = streamableCollectionStore.subscribe((displayableMedias) => {
|
||||
const nbUsers = displayableMedias.size;
|
||||
if (nbUsers <= 1) {
|
||||
cssClass = 'one-col';
|
||||
} else if (nbUsers <= 4) {
|
||||
cssClass = 'two-col';
|
||||
} else if (nbUsers <= 9) {
|
||||
cssClass = 'three-col';
|
||||
} else {
|
||||
cssClass = 'four-col';
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
afterUpdate(() => {
|
||||
biggestAvailableAreaStore.recompute();
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="chat-mode {cssClass}">
|
||||
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
|
||||
<MediaBox streamable={peer}></MediaBox>
|
||||
{/each}
|
||||
</div>
|
16
front/src/Components/Video/LocalStreamMediaBox.svelte
Normal file
16
front/src/Components/Video/LocalStreamMediaBox.svelte
Normal file
@ -0,0 +1,16 @@
|
||||
<script lang="typescript">
|
||||
import type {ScreenSharingLocalMedia} from "../../Stores/ScreenSharingStore";
|
||||
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||
import {srcObject} from "./utils";
|
||||
|
||||
export let peer : ScreenSharingLocalMedia;
|
||||
let stream = peer.stream;
|
||||
export let cssClass : string|undefined;
|
||||
</script>
|
||||
|
||||
|
||||
<div class="video-container {cssClass ? cssClass : ''}" class:hide={!stream}>
|
||||
{#if stream}
|
||||
<video use:srcObject={stream} autoplay muted playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||
{/if}
|
||||
</div>
|
20
front/src/Components/Video/MediaBox.svelte
Normal file
20
front/src/Components/Video/MediaBox.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import {VideoPeer} from "../../WebRtc/VideoPeer";
|
||||
import VideoMediaBox from "./VideoMediaBox.svelte";
|
||||
import ScreenSharingMediaBox from "./ScreenSharingMediaBox.svelte";
|
||||
import {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer";
|
||||
import LocalStreamMediaBox from "./LocalStreamMediaBox.svelte";
|
||||
import type {Streamable} from "../../Stores/StreamableCollectionStore";
|
||||
|
||||
export let streamable: Streamable;
|
||||
</script>
|
||||
|
||||
<div class="media-container">
|
||||
{#if streamable instanceof VideoPeer}
|
||||
<VideoMediaBox peer={streamable}/>
|
||||
{:else if streamable instanceof ScreenSharingPeer}
|
||||
<ScreenSharingMediaBox peer={streamable}/>
|
||||
{:else}
|
||||
<LocalStreamMediaBox peer={streamable} cssClass=""/>
|
||||
{/if}
|
||||
</div>
|
24
front/src/Components/Video/PresentationLayout.svelte
Normal file
24
front/src/Components/Video/PresentationLayout.svelte
Normal file
@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore";
|
||||
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||
import {afterUpdate} from "svelte";
|
||||
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore";
|
||||
import MediaBox from "./MediaBox.svelte";
|
||||
|
||||
afterUpdate(() => {
|
||||
biggestAvailableAreaStore.recompute();
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="main-section">
|
||||
{#if $videoFocusStore }
|
||||
<MediaBox streamable={$videoFocusStore}></MediaBox>
|
||||
{/if}
|
||||
</div>
|
||||
<aside class="sidebar">
|
||||
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
|
||||
{#if peer !== $videoFocusStore }
|
||||
<MediaBox streamable={peer}></MediaBox>
|
||||
{/if}
|
||||
{/each}
|
||||
</aside>
|
33
front/src/Components/Video/ScreenSharingMediaBox.svelte
Normal file
33
front/src/Components/Video/ScreenSharingMediaBox.svelte
Normal file
@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import type {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer";
|
||||
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||
import {getColorByString, srcObject} from "./utils";
|
||||
|
||||
export let peer: ScreenSharingPeer;
|
||||
let streamStore = peer.streamStore;
|
||||
let name = peer.userName;
|
||||
let statusStore = peer.statusStore;
|
||||
|
||||
</script>
|
||||
|
||||
<div class="video-container">
|
||||
{#if $statusStore === 'connecting'}
|
||||
<div class="connecting-spinner"></div>
|
||||
{/if}
|
||||
{#if $statusStore === 'error'}
|
||||
<div class="rtc-error"></div>
|
||||
{/if}
|
||||
{#if $streamStore === null}
|
||||
<i style="background-color: {getColorByString(name)};">{name}</i>
|
||||
{:else}
|
||||
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.video-container {
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
48
front/src/Components/Video/VideoMediaBox.svelte
Normal file
48
front/src/Components/Video/VideoMediaBox.svelte
Normal file
@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import type {VideoPeer} from "../../WebRtc/VideoPeer";
|
||||
import SoundMeterWidget from "../SoundMeterWidget.svelte";
|
||||
import microphoneCloseImg from "../images/microphone-close.svg";
|
||||
import reportImg from "./images/report.svg";
|
||||
import blockSignImg from "./images/blockSign.svg";
|
||||
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||
import {showReportScreenStore} from "../../Stores/ShowReportScreenStore";
|
||||
import {getColorByString, srcObject} from "./utils";
|
||||
|
||||
export let peer: VideoPeer;
|
||||
let streamStore = peer.streamStore;
|
||||
let name = peer.userName;
|
||||
let statusStore = peer.statusStore;
|
||||
let constraintStore = peer.constraintsStore;
|
||||
|
||||
function openReport(peer: VideoPeer): void {
|
||||
showReportScreenStore.set({ userId:peer.userId, userName: peer.userName });
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="video-container">
|
||||
{#if $statusStore === 'connecting'}
|
||||
<div class="connecting-spinner"></div>
|
||||
{/if}
|
||||
{#if $statusStore === 'error'}
|
||||
<div class="rtc-error"></div>
|
||||
{/if}
|
||||
{#if !$constraintStore || $constraintStore.video === false}
|
||||
<i style="background-color: {getColorByString(name)};">{name}</i>
|
||||
{/if}
|
||||
{#if $constraintStore && $constraintStore.audio === false}
|
||||
<img src={microphoneCloseImg} alt="Muted">
|
||||
{/if}
|
||||
<button class="report" on:click={() => openReport(peer)}>
|
||||
<img alt="Report this user" src={reportImg}>
|
||||
<span>Report/Block</span>
|
||||
</button>
|
||||
{#if $streamStore }
|
||||
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||
{/if}
|
||||
<img src={blockSignImg} class="block-logo" alt="Block" />
|
||||
{#if $constraintStore && $constraintStore.audio !== false}
|
||||
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>
|
||||
{/if}
|
||||
</div>
|
||||
|
23
front/src/Components/Video/VideoOverlay.svelte
Normal file
23
front/src/Components/Video/VideoOverlay.svelte
Normal file
@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import {LayoutMode} from "../../WebRtc/LayoutManager";
|
||||
import {layoutModeStore} from "../../Stores/StreamableCollectionStore";
|
||||
import PresentationLayout from "./PresentationLayout.svelte";
|
||||
import ChatLayout from "./ChatLayout.svelte";
|
||||
|
||||
</script>
|
||||
|
||||
<div class="video-overlay">
|
||||
{#if $layoutModeStore === LayoutMode.Presentation }
|
||||
<PresentationLayout />
|
||||
{:else }
|
||||
<ChatLayout />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.video-overlay {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
22
front/src/Components/Video/images/blockSign.svg
Normal file
22
front/src/Components/Video/images/blockSign.svg
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" id="svg2985" version="1.1" inkscape:version="0.48.4 r9939" width="485.33627" height="485.33627" sodipodi:docname="600px-France_road_sign_B1j.svg[1].png">
|
||||
<metadata id="metadata2991">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs id="defs2989"/>
|
||||
<sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1272" inkscape:window-height="745" id="namedview2987" showgrid="false" inkscape:snap-global="true" inkscape:snap-grids="true" inkscape:snap-bbox="true" inkscape:bbox-paths="true" inkscape:bbox-nodes="true" inkscape:snap-bbox-edge-midpoints="true" inkscape:snap-bbox-midpoints="true" inkscape:object-paths="true" inkscape:snap-intersection-paths="true" inkscape:object-nodes="true" inkscape:snap-smooth-nodes="true" inkscape:snap-midpoints="true" inkscape:snap-object-midpoints="true" inkscape:snap-center="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" inkscape:zoom="0.59970176" inkscape:cx="390.56499" inkscape:cy="244.34365" inkscape:window-x="86" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="layer1">
|
||||
<inkscape:grid type="xygrid" id="grid2995" empspacing="5" visible="true" enabled="true" snapvisiblegridlinesonly="true" originx="-57.33186px" originy="-57.33186px"/>
|
||||
</sodipodi:namedview>
|
||||
<g inkscape:groupmode="layer" id="layer1" inkscape:label="1" style="display:inline" transform="translate(-57.33186,-57.33186)">
|
||||
<path sodipodi:type="arc" style="color:#000000;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path2997" sodipodi:cx="300" sodipodi:cy="300" sodipodi:rx="240" sodipodi:ry="240" d="M 540,300 C 540,432.54834 432.54834,540 300,540 167.45166,540 60,432.54834 60,300 60,167.45166 167.45166,60 300,60 432.54834,60 540,167.45166 540,300 z" transform="matrix(1.0058783,0,0,1.0058783,-1.76349,-1.76349)"/>
|
||||
<path sodipodi:type="arc" style="color:#000000;fill:#ff0000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path4005" sodipodi:cx="304.75" sodipodi:cy="214.75" sodipodi:rx="44.75" sodipodi:ry="44.75" d="m 349.5,214.75 c 0,24.71474 -20.03526,44.75 -44.75,44.75 -24.71474,0 -44.75,-20.03526 -44.75,-44.75 0,-24.71474 20.03526,-44.75 44.75,-44.75 24.71474,0 44.75,20.03526 44.75,44.75 z" transform="matrix(5.1364411,0,0,5.1364411,-1265.3304,-803.05073)"/>
|
||||
<rect style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="rect4001" width="345" height="80.599998" x="127.5" y="259.70001"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
1
front/src/Components/Video/images/report.svg
Normal file
1
front/src/Components/Video/images/report.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.1 KiB |
27
front/src/Components/Video/utils.ts
Normal file
27
front/src/Components/Video/utils.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
front/src/Components/images/layout-chat.svg
Normal file
1
front/src/Components/images/layout-chat.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48.97 39.04"><defs><style>.cls-1{fill:#fff;}</style></defs><rect class="cls-1" x="0.7" y="0.5" width="11.76" height="9.75"/><path class="cls-1" d="M35.08,11.78v8.75H24.31V11.78H35.08m1-1H23.31V21.53H36.08V10.78Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="0.5" y="14.77" width="11.76" height="9.75"/><path class="cls-1" d="M34.87,26.05V34.8H24.11V26.05H34.87m1-1H23.11V35.8H35.87V25.05Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="0.5" y="28.79" width="11.76" height="9.75"/><path class="cls-1" d="M34.87,40.07v8.75H24.11V40.07H34.87m1-1H23.11V49.82H35.87V39.07Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="18.7" y="0.5" width="11.76" height="9.75"/><path class="cls-1" d="M53.08,11.78v8.75H42.31V11.78H53.08m1-1H41.31V21.53H54.08V10.78Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="18.5" y="14.77" width="11.76" height="9.75"/><path class="cls-1" d="M52.87,26.05V34.8H42.11V26.05H52.87m1-1H41.11V35.8H53.87V25.05Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="18.5" y="28.79" width="11.76" height="9.75"/><path class="cls-1" d="M52.87,40.07v8.75H42.11V40.07H52.87m1-1H41.11V49.82H53.87V39.07Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="36.7" y="0.5" width="11.76" height="9.75"/><path class="cls-1" d="M71.08,11.78v8.75H60.31V11.78H71.08m1-1H59.31V21.53H72.08V10.78Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="36.5" y="14.77" width="11.76" height="9.75"/><path class="cls-1" d="M70.87,26.05V34.8H60.11V26.05H70.87m1-1H59.11V35.8H71.87V25.05Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="36.5" y="28.79" width="11.76" height="9.75"/><path class="cls-1" d="M70.87,40.07v8.75H60.11V40.07H70.87m1-1H59.11V49.82H71.87V39.07Z" transform="translate(-23.11 -10.78)"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
front/src/Components/images/layout-presentation.svg
Normal file
1
front/src/Components/images/layout-presentation.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84.83 54"><defs><style>.cls-1{fill:#fff;}</style></defs><rect class="cls-1" x="0.5" y="0.5" width="63.13" height="53"/><path class="cls-1" d="M67.12,6V58H5V6H67.12m1-1H4V59H68.12V5Z" transform="translate(-4 -5)"/><rect class="cls-1" x="68.87" y="0.75" width="15.46" height="12.86"/><path class="cls-1" d="M87.83,6.25V18.12H73.37V6.25H87.83m1-1H72.37V19.12H88.83V5.25Z" transform="translate(-4 -5)"/><rect class="cls-1" x="68.87" y="17.69" width="15.46" height="12.86"/><path class="cls-1" d="M87.83,23.19V35.05H73.37V23.19H87.83m1-1H72.37V36.05H88.83V22.19Z" transform="translate(-4 -5)"/><rect class="cls-1" x="68.87" y="34.75" width="15.46" height="12.86"/><path class="cls-1" d="M87.83,40.25V52.12H73.37V40.25H87.83m1-1H72.37V53.12H88.83V39.25Z" transform="translate(-4 -5)"/></svg>
|
After Width: | Height: | Size: 873 B |
@ -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<Phaser.Tilemaps.Tileset>;
|
||||
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<number, ActionableItem> = new Map<number, ActionableItem>();
|
||||
// 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<number, RemotePlayer>();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -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', () => {
|
||||
|
127
front/src/Stores/BiggestAvailableAreaStore.ts
Normal file
127
front/src/Stores/BiggestAvailableAreaStore.ts
Normal file
@ -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<HTMLCanvasElement>('#game canvas');
|
||||
if (get(layoutModeStore) === LayoutMode.VideoChat) {
|
||||
const children = document.querySelectorAll<HTMLDivElement>('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<HTMLDivElement>('div.main-section > div').values());
|
||||
const sidebarChildren = Array.from(document.querySelectorAll<HTMLDivElement>('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<HTMLDivElement>('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<Box>({xStart:0, yStart: 0, xEnd: 1, yEnd: 1});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
recompute: () => {
|
||||
set(findBiggestAvailableArea());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const biggestAvailableAreaStore = createBiggestAvailableAreaStore();
|
17
front/src/Stores/GameOverlayStoreVisibility.ts
Normal file
17
front/src/Stores/GameOverlayStoreVisibility.ts
Normal file
@ -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();
|
@ -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();
|
||||
|
||||
/**
|
||||
|
@ -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<number, UserSimplePeerInterface>();
|
||||
let peers = new Map<number, VideoPeer>();
|
||||
|
||||
const { subscribe, set, update } = writable(users);
|
||||
const { subscribe, set, update } = writable(peers);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
connectToSimplePeer: (simplePeer: SimplePeer) => {
|
||||
users = new Map<number, UserSimplePeerInterface>();
|
||||
set(users);
|
||||
peers = new Map<number, VideoPeer>();
|
||||
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<number, ScreenSharingPeer>();
|
||||
|
||||
const { subscribe, set, update } = writable(peers);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
connectToSimplePeer: (simplePeer: SimplePeer) => {
|
||||
peers = new Map<number, ScreenSharingPeer>();
|
||||
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<number, ScreenSharingPeer>();
|
||||
|
||||
return readable<Map<number, ScreenSharingPeer>>(peers, function start(set) {
|
||||
|
||||
let unsubscribes: (()=>void)[] = [];
|
||||
|
||||
const unsubscribe = screenSharingPeerStore.subscribe((screenSharingPeers) => {
|
||||
for (const unsubscribe of unsubscribes) {
|
||||
unsubscribe();
|
||||
}
|
||||
unsubscribes = [];
|
||||
|
||||
peers = new Map<number, ScreenSharingPeer>();
|
||||
|
||||
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();
|
||||
|
||||
|
||||
|
@ -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<ScreenSharingLocalMedia>, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber;
|
||||
}
|
||||
|
||||
/**
|
||||
* The representation of the screen sharing stream.
|
||||
*/
|
||||
export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia|null>(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();
|
||||
};
|
||||
})
|
||||
|
3
front/src/Stores/ShowReportScreenStore.ts
Normal file
3
front/src/Stores/ShowReportScreenStore.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import {writable} from "svelte/store";
|
||||
|
||||
export const showReportScreenStore = writable<{userId: number, userName: string}|null>(null);
|
43
front/src/Stores/StreamableCollectionStore.ts
Normal file
43
front/src/Stores/StreamableCollectionStore.ts
Normal file
@ -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>(LayoutMode.Presentation);
|
||||
|
||||
/**
|
||||
* A store that contains everything that can produce a stream (so the peers + the local screen sharing stream)
|
||||
*/
|
||||
function createStreamableCollectionStore(): Readable<Map<string, Streamable>> {
|
||||
|
||||
return derived([
|
||||
screenSharingStreamStore,
|
||||
peerStore,
|
||||
screenSharingLocalMedia,
|
||||
], ([
|
||||
$screenSharingStreamStore,
|
||||
$peerStore,
|
||||
$screenSharingLocalMedia,
|
||||
], set) => {
|
||||
|
||||
const peers = new Map<string, Streamable>();
|
||||
|
||||
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();
|
47
front/src/Stores/VideoFocusStore.ts
Normal file
47
front/src/Stores/VideoFocusStore.ts
Normal file
@ -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<Streamable | null>(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();
|
@ -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);
|
||||
}
|
||||
|
@ -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<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
||||
private normalDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
||||
private listener: CenterListener|null = null;
|
||||
|
||||
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
|
||||
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
||||
|
||||
public setListener(centerListener: CenterListener|null) {
|
||||
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<HTMLDivElement>('chat-mode');
|
||||
chatModeDiv.appendChild(elem);
|
||||
} else {
|
||||
if (importance === DivImportance.Important) {
|
||||
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section');
|
||||
mainSectionDiv.appendChild(elem);
|
||||
} else if (importance === DivImportance.Normal) {
|
||||
const sideBarDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('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<HTMLDivElement>('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<HTMLDivElement>('sidebar').style.display = 'flex';
|
||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section').style.display = 'flex';
|
||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode').style.display = 'none';
|
||||
} else {
|
||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').style.display = 'none';
|
||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section').style.display = 'none';
|
||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('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<HTMLCanvasElement>('#game canvas');
|
||||
if (this.mode === LayoutMode.VideoChat) {
|
||||
const children = document.querySelectorAll<HTMLDivElement>('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<HTMLDivElement>('div.main-section > div').values());
|
||||
const sidebarChildren = Array.from(document.querySelectorAll<HTMLDivElement>('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<HTMLDivElement>('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);
|
||||
|
@ -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<string, HTMLVideoElement> = new Map<string, HTMLVideoElement>();
|
||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||
//mySoundMeterElement: HTMLDivElement;
|
||||
|
||||
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
||||
stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
|
||||
showReportModalCallBacks: Set<ShowReportCallBack> = new Set<ShowReportCallBack>();
|
||||
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
||||
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
|
||||
|
||||
|
||||
|
||||
@ -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<string, SoundMeter> = new Map<string, SoundMeter>();
|
||||
private soundMeterElements: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();*/
|
||||
|
||||
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<HTMLVideoElement>('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 = `
|
||||
<div id="div-${userId}" class="video-container">
|
||||
<div class="connecting-spinner"></div>
|
||||
<div class="rtc-error" style="display: none"></div>
|
||||
<i id="name-${userId}" style="background-color: ${color};">${userName}</i>
|
||||
<img id="microphone-${userId}" title="mute" src="resources/logos/microphone-close.svg">
|
||||
<button id="report-${userId}" class="report">
|
||||
<img title="report this user" src="resources/logos/report.svg">
|
||||
<span>Report/Block</span>
|
||||
</button>
|
||||
<video id="${userId}" autoplay playsinline></video>
|
||||
<img src="resources/logos/blockSign.svg" id="blocking-${userId}" class="block-logo">
|
||||
<div id="soundMeter-${userId}" class="sound-progress">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
layoutManager.add(DivImportance.Normal, userId, html);
|
||||
|
||||
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(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<HTMLImageElement>(`report-${userId}`);
|
||||
reportBanUserActionEl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showReportUser();
|
||||
});
|
||||
}
|
||||
|
||||
addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){
|
||||
|
||||
userId = this.getScreenSharingId(userId);
|
||||
const html = `
|
||||
<div id="div-${userId}" class="video-container">
|
||||
<video id="${userId}" autoplay playsinline></video>
|
||||
</div>
|
||||
`;
|
||||
|
||||
layoutManager.add(divImportance, userId, html);
|
||||
|
||||
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
|
||||
}
|
||||
|
||||
private getScreenSharingId(userId: string): string {
|
||||
return `screen-sharing-${userId}`;
|
||||
}
|
||||
@ -248,61 +137,6 @@ export class MediaManager {
|
||||
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('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<HTMLImageElement>('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") {
|
||||
|
@ -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<MediaStream | null>;
|
||||
public readonly statusStore: Readable<PeerStatus>;
|
||||
|
||||
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<MediaStream|null>(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<PeerStatus>("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);
|
||||
|
@ -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")
|
||||
|
@ -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<MediaStream | null>;
|
||||
public readonly statusStore: Readable<PeerStatus>;
|
||||
public readonly constraintsStore: Readable<MediaStreamConstraints|null>;
|
||||
|
||||
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<MediaStream|null>(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<MediaStreamConstraints|null>(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<PeerStatus>("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);
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user