Merge remote-tracking branch 'remotes/upstream/develop' into tiles-start-positions

This commit is contained in:
jonny 2021-06-25 18:14:40 +02:00
commit 7f61e9addd
182 changed files with 17118 additions and 4494 deletions

View file

@ -0,0 +1,126 @@
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();

View 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();

View file

@ -1,13 +1,14 @@
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 { derived, get, Readable, readable, writable, Writable } from "svelte/store";
import { localUserStore } from "../Connexion/LocalUserStore";
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";
import { peerStore } from "./PeerStore";
import { privacyShutdownStore } from "./PrivacyShutdownStore";
/**
* A store that contains the camera state requested by the user (on or off).
@ -35,35 +36,6 @@ function createRequestedMicrophoneState() {
};
}
/**
* A store containing whether the current page is visible or not.
*/
export const visibilityStore = readable(document.visibilityState === 'visible', function start(set) {
const onVisibilityChange = () => {
set(document.visibilityState === 'visible');
};
document.addEventListener('visibilitychange', onVisibilityChange);
return function stop() {
document.removeEventListener('visibilitychange', onVisibilityChange);
};
});
/**
* 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,49 +51,13 @@ function createEnableCameraSceneVisibilityStore() {
export const requestedCameraState = createRequestedCameraState();
export const requestedMicrophoneState = createRequestedMicrophoneState();
export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore();
export const enableCameraSceneVisibilityStore = createEnableCameraSceneVisibilityStore();
/**
* A store that contains "true" if the webcam should be stopped for privacy reasons - i.e. if the the user left the the page while not in a discussion.
*/
function createPrivacyShutdownStore() {
let privacyEnabled = false;
const { subscribe, set, update } = writable(privacyEnabled);
visibilityStore.subscribe((isVisible) => {
if (!isVisible && get(peerStore).size === 0) {
privacyEnabled = true;
set(true);
}
if (isVisible) {
privacyEnabled = false;
set(false);
}
});
peerStore.subscribe((peers) => {
if (peers.size === 0 && get(visibilityStore) === false) {
privacyEnabled = true;
set(true);
}
});
return {
subscribe,
};
}
export const privacyShutdownStore = createPrivacyShutdownStore();
/**
* A store containing whether the webcam was enabled in the last 10 seconds
*/
const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
let timeout: NodeJS.Timeout|null = null;
let timeout: NodeJS.Timeout | null = null;
const unsubscribe = requestedCameraState.subscribe((enabled) => {
if (enabled === true) {
@ -135,7 +71,7 @@ const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
} else {
set(false);
}
})
});
return function stop() {
unsubscribe();
@ -146,7 +82,7 @@ const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
* A store containing whether the webcam was enabled in the last 5 seconds
*/
const userMoved5SecondsAgoStore = readable(false, function start(set) {
let timeout: NodeJS.Timeout|null = null;
let timeout: NodeJS.Timeout | null = null;
const unsubscribe = userMovingStore.subscribe((moving) => {
if (moving === true) {
@ -158,45 +94,51 @@ const userMoved5SecondsAgoStore = readable(false, function start(set) {
timeout = setTimeout(() => {
set(false);
}, 5000);
}
})
});
return function stop() {
unsubscribe();
};
});
/**
* A store containing whether the mouse is getting close the bottom right corner.
*/
const mouseInBottomRight = readable(false, function start(set) {
let lastInBottomRight = false;
const gameDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('game');
const gameDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("game");
const detectInBottomRight = (event: MouseEvent) => {
const rect = gameDiv.getBoundingClientRect();
const inBottomRight = event.x - rect.left > rect.width * 3 / 4 && event.y - rect.top > rect.height * 3 / 4;
const inBottomRight = event.x - rect.left > (rect.width * 3) / 4 && event.y - rect.top > (rect.height * 3) / 4;
if (inBottomRight !== lastInBottomRight) {
lastInBottomRight = inBottomRight;
set(inBottomRight);
}
};
document.addEventListener('mousemove', detectInBottomRight);
document.addEventListener("mousemove", detectInBottomRight);
return function stop() {
document.removeEventListener('mousemove', detectInBottomRight);
}
document.removeEventListener("mousemove", detectInBottomRight);
};
});
/**
* A store that contains "true" if the webcam should be stopped for energy efficiency reason - i.e. we are not moving and not in a conversation.
*/
export const cameraEnergySavingStore = derived([userMoved5SecondsAgoStore, peerStore, enabledWebCam10secondsAgoStore, mouseInBottomRight], ([$userMoved5SecondsAgoStore,$peerStore, $enabledWebCam10secondsAgoStore, $mouseInBottomRight]) => {
return !$mouseInBottomRight && !$userMoved5SecondsAgoStore && $peerStore.size === 0 && !$enabledWebCam10secondsAgoStore;
});
export const cameraEnergySavingStore = derived(
[userMoved5SecondsAgoStore, peerStore, enabledWebCam10secondsAgoStore, mouseInBottomRight],
([$userMoved5SecondsAgoStore, $peerStore, $enabledWebCam10secondsAgoStore, $mouseInBottomRight]) => {
return (
!$mouseInBottomRight &&
!$userMoved5SecondsAgoStore &&
$peerStore.size === 0 &&
!$enabledWebCam10secondsAgoStore
);
}
);
/**
* A store that contains video constraints.
@ -207,28 +149,30 @@ function createVideoConstraintStore() {
height: { min: 400, ideal: 720 },
frameRate: { ideal: localUserStore.getVideoQualityValue() },
facingMode: "user",
resizeMode: 'crop-and-scale',
aspectRatio: 1.777777778
resizeMode: "crop-and-scale",
aspectRatio: 1.777777778,
} as MediaTrackConstraints);
return {
subscribe,
setDeviceId: (deviceId: string|undefined) => update((constraints) => {
if (deviceId !== undefined) {
constraints.deviceId = {
exact: deviceId
};
} else {
delete constraints.deviceId;
}
setDeviceId: (deviceId: string | undefined) =>
update((constraints) => {
if (deviceId !== undefined) {
constraints.deviceId = {
exact: deviceId,
};
} else {
delete constraints.deviceId;
}
return constraints;
}),
setFrameRate: (frameRate: number) => update((constraints) => {
constraints.frameRate = { ideal: frameRate };
return constraints;
}),
setFrameRate: (frameRate: number) =>
update((constraints) => {
constraints.frameRate = { ideal: frameRate };
return constraints;
})
return constraints;
}),
};
}
@ -242,39 +186,39 @@ function createAudioConstraintStore() {
//TODO: make these values configurable in the game settings menu and store them in localstorage
autoGainControl: false,
echoCancellation: true,
noiseSuppression: true
} as boolean|MediaTrackConstraints);
noiseSuppression: true,
} as boolean | MediaTrackConstraints);
let selectedDeviceId = null;
return {
subscribe,
setDeviceId: (deviceId: string|undefined) => update((constraints) => {
selectedDeviceId = deviceId;
setDeviceId: (deviceId: string | undefined) =>
update((constraints) => {
selectedDeviceId = deviceId;
if (typeof(constraints) === 'boolean') {
constraints = {}
}
if (deviceId !== undefined) {
constraints.deviceId = {
exact: selectedDeviceId
};
} else {
delete constraints.deviceId;
}
if (typeof constraints === "boolean") {
constraints = {};
}
if (deviceId !== undefined) {
constraints.deviceId = {
exact: selectedDeviceId,
};
} else {
delete constraints.deviceId;
}
return constraints;
})
return constraints;
}),
};
}
export const audioConstraintStore = createAudioConstraintStore();
let timeout: NodeJS.Timeout;
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false;
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false;
let previousComputedVideoConstraint: boolean | MediaTrackConstraints = false;
let previousComputedAudioConstraint: boolean | MediaTrackConstraints = false;
/**
* A store containing the media constraints we want to apply.
@ -289,7 +233,8 @@ export const mediaStreamConstraintsStore = derived(
audioConstraintStore,
privacyShutdownStore,
cameraEnergySavingStore,
], (
],
(
[
$requestedCameraState,
$requestedMicrophoneState,
@ -299,92 +244,97 @@ export const mediaStreamConstraintsStore = derived(
$audioConstraintStore,
$privacyShutdownStore,
$cameraEnergySavingStore,
], set
],
set
) => {
let currentVideoConstraint: boolean | MediaTrackConstraints = $videoConstraintStore;
let currentAudioConstraint: boolean | MediaTrackConstraints = $audioConstraintStore;
let currentVideoConstraint: boolean|MediaTrackConstraints = $videoConstraintStore;
let currentAudioConstraint: boolean|MediaTrackConstraints = $audioConstraintStore;
if ($enableCameraSceneVisibilityStore) {
set({
video: currentVideoConstraint,
audio: currentAudioConstraint,
});
return;
}
// Disable webcam if the user requested so
if ($requestedCameraState === false) {
currentVideoConstraint = false;
}
// Disable microphone if the user requested so
if ($requestedMicrophoneState === false) {
currentAudioConstraint = false;
}
// Disable webcam and microphone when in a Jitsi
if ($gameOverlayVisibilityStore === false) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Disable webcam for privacy reasons (the game is not visible and we were talking to noone)
if ($privacyShutdownStore === true) {
currentVideoConstraint = false;
}
// Disable webcam for energy reasons (the user is not moving and we are talking to noone)
if ($cameraEnergySavingStore === true) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Let's make the changes only if the new value is different from the old one.
if (previousComputedVideoConstraint != currentVideoConstraint || previousComputedAudioConstraint != currentAudioConstraint) {
previousComputedVideoConstraint = currentVideoConstraint;
previousComputedAudioConstraint = currentAudioConstraint;
// Let's copy the objects.
if (typeof previousComputedVideoConstraint !== 'boolean') {
previousComputedVideoConstraint = {...previousComputedVideoConstraint};
}
if (typeof previousComputedAudioConstraint !== 'boolean') {
previousComputedAudioConstraint = {...previousComputedAudioConstraint};
}
if (timeout) {
clearTimeout(timeout);
}
// Let's wait a little bit to avoid sending too many constraint changes.
timeout = setTimeout(() => {
if ($enableCameraSceneVisibilityStore) {
set({
video: currentVideoConstraint,
audio: currentAudioConstraint,
});
}, 100);
}
}, {
video: false,
audio: false
} as MediaStreamConstraints);
return;
}
// Disable webcam if the user requested so
if ($requestedCameraState === false) {
currentVideoConstraint = false;
}
// Disable microphone if the user requested so
if ($requestedMicrophoneState === false) {
currentAudioConstraint = false;
}
// Disable webcam and microphone when in a Jitsi
if ($gameOverlayVisibilityStore === false) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Disable webcam for privacy reasons (the game is not visible and we were talking to noone)
if ($privacyShutdownStore === true) {
currentVideoConstraint = false;
}
// Disable webcam for energy reasons (the user is not moving and we are talking to noone)
if ($cameraEnergySavingStore === true) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}
// Let's make the changes only if the new value is different from the old one.
if (
previousComputedVideoConstraint != currentVideoConstraint ||
previousComputedAudioConstraint != currentAudioConstraint
) {
previousComputedVideoConstraint = currentVideoConstraint;
previousComputedAudioConstraint = currentAudioConstraint;
// Let's copy the objects.
if (typeof previousComputedVideoConstraint !== "boolean") {
previousComputedVideoConstraint = { ...previousComputedVideoConstraint };
}
if (typeof previousComputedAudioConstraint !== "boolean") {
previousComputedAudioConstraint = { ...previousComputedAudioConstraint };
}
if (timeout) {
clearTimeout(timeout);
}
// Let's wait a little bit to avoid sending too many constraint changes.
timeout = setTimeout(() => {
set({
video: currentVideoConstraint,
audio: currentAudioConstraint,
});
}, 100);
}
},
{
video: false,
audio: false,
} as MediaStreamConstraints
);
export type LocalStreamStoreValue = StreamSuccessValue | StreamErrorValue;
interface StreamSuccessValue {
type: "success",
stream: MediaStream|null,
type: "success";
stream: MediaStream | null;
// The constraints that we got (and not the one that have been requested)
constraints: MediaStreamConstraints
constraints: MediaStreamConstraints;
}
interface StreamErrorValue {
type: "error",
error: Error,
constraints: MediaStreamConstraints
type: "error";
error: Error;
constraints: MediaStreamConstraints;
}
let currentStream : MediaStream|null = null;
let currentStream: MediaStream | null = null;
/**
* Stops the camera from filming
@ -411,84 +361,94 @@ function stopMicrophone(): void {
/**
* A store containing the MediaStream object (or null if nothing requested, or Error if an error occurred)
*/
export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(mediaStreamConstraintsStore, ($mediaStreamConstraintsStore, set) => {
const constraints = { ...$mediaStreamConstraintsStore };
export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(
mediaStreamConstraintsStore,
($mediaStreamConstraintsStore, set) => {
const constraints = { ...$mediaStreamConstraintsStore };
if (navigator.mediaDevices === undefined) {
if (window.location.protocol === 'http:') {
//throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.');
if (navigator.mediaDevices === undefined) {
if (window.location.protocol === "http:") {
//throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.');
set({
type: "error",
error: new Error("Unable to access your camera or microphone. You need to use a HTTPS connection."),
constraints,
});
return;
} else if (isIOS()) {
set({
type: "error",
error: new WebviewOnOldIOS(),
constraints,
});
return;
} else {
set({
type: "error",
error: new BrowserTooOldError(),
constraints,
});
return;
}
}
if (constraints.audio === false) {
stopMicrophone();
}
if (constraints.video === false) {
stopCamera();
}
if (constraints.audio === false && constraints.video === false) {
currentStream = null;
set({
type: 'error',
error: new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.'),
constraints
});
return;
} else if (isIOS()) {
set({
type: 'error',
error: new WebviewOnOldIOS(),
constraints
});
return;
} else {
set({
type: 'error',
error: new BrowserTooOldError(),
constraints
type: "success",
stream: null,
constraints,
});
return;
}
}
if (constraints.audio === false) {
stopMicrophone();
}
if (constraints.video === false) {
stopCamera();
}
if (constraints.audio === false && constraints.video === false) {
currentStream = null;
set({
type: 'success',
stream: null,
constraints
});
return;
}
(async () => {
try {
stopMicrophone();
stopCamera();
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
set({
type: 'success',
stream: currentStream,
constraints
});
return;
} catch (e) {
if (constraints.video !== false) {
console.info("Error. Unable to get microphone and/or camera access. Trying audio only.", $mediaStreamConstraintsStore, e);
// TODO: does it make sense to pop this error when retrying?
(async () => {
try {
stopMicrophone();
stopCamera();
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
set({
type: 'error',
error: e,
constraints
type: "success",
stream: currentStream,
constraints,
});
// Let's try without video constraints
requestedCameraState.disableWebcam();
} else {
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
set({
type: 'error',
error: e,
constraints
});
}
return;
} catch (e) {
if (constraints.video !== false) {
console.info(
"Error. Unable to get microphone and/or camera access. Trying audio only.",
$mediaStreamConstraintsStore,
e
);
// TODO: does it make sense to pop this error when retrying?
set({
type: "error",
error: e,
constraints,
});
// Let's try without video constraints
requestedCameraState.disableWebcam();
} else {
console.info(
"Error. Unable to get microphone and/or camera access.",
$mediaStreamConstraintsStore,
e
);
set({
type: "error",
error: e,
constraints,
});
}
/*constraints.video = false;
/*constraints.video = false;
if (constraints.audio === false) {
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
set({
@ -517,9 +477,10 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
});
}
}*/
}
})();
});
}
})();
}
);
/**
* A store containing the real active media constrained (not the one requested by the user, but the one we got from the system)
@ -536,12 +497,15 @@ export const deviceListStore = readable<MediaDeviceInfo[]>([], function start(se
const queryDeviceList = () => {
// Note: so far, we are ignoring any failures.
navigator.mediaDevices.enumerateDevices().then((mediaDeviceInfos) => {
set(mediaDeviceInfos);
}).catch((e) => {
console.error(e);
throw e;
});
navigator.mediaDevices
.enumerateDevices()
.then((mediaDeviceInfos) => {
set(mediaDeviceInfos);
})
.catch((e) => {
console.error(e);
throw e;
});
};
const unsubscribe = localStreamStore.subscribe((streamResult) => {
@ -554,23 +518,23 @@ export const deviceListStore = readable<MediaDeviceInfo[]>([], function start(se
});
if (navigator.mediaDevices) {
navigator.mediaDevices.addEventListener('devicechange', queryDeviceList);
navigator.mediaDevices.addEventListener("devicechange", queryDeviceList);
}
return function stop() {
unsubscribe();
if (navigator.mediaDevices) {
navigator.mediaDevices.removeEventListener('devicechange', queryDeviceList);
navigator.mediaDevices.removeEventListener("devicechange", queryDeviceList);
}
};
});
export const cameraListStore = derived(deviceListStore, ($deviceListStore) => {
return $deviceListStore.filter(device => device.kind === 'videoinput');
return $deviceListStore.filter((device) => device.kind === "videoinput");
});
export const microphoneListStore = derived(deviceListStore, ($deviceListStore) => {
return $deviceListStore.filter(device => device.kind === 'audioinput');
return $deviceListStore.filter((device) => device.kind === "audioinput");
});
// TODO: detect the new webcam and automatically switch on it.
@ -583,7 +547,7 @@ cameraListStore.subscribe((devices) => {
// If we cannot find the device ID, let's remove it.
// @ts-ignore
if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) {
if (!devices.find((device) => device.deviceId === constraints.deviceId.exact)) {
videoConstraintStore.setDeviceId(undefined);
}
});
@ -591,7 +555,7 @@ cameraListStore.subscribe((devices) => {
microphoneListStore.subscribe((devices) => {
// If the selected camera is unplugged, let's remove the constraint on deviceId
const constraints = get(audioConstraintStore);
if (typeof constraints === 'boolean') {
if (typeof constraints === "boolean") {
return;
}
if (!constraints.deviceId) {
@ -600,13 +564,13 @@ microphoneListStore.subscribe((devices) => {
// If we cannot find the device ID, let's remove it.
// @ts-ignore
if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) {
if (!devices.find((device) => device.deviceId === constraints.deviceId.exact)) {
audioConstraintStore.setDeviceId(undefined);
}
});
localStreamStore.subscribe(streamResult => {
if (streamResult.type === 'error') {
localStreamStore.subscribe((streamResult) => {
if (streamResult.type === "error") {
if (streamResult.error.name === BrowserTooOldError.NAME || streamResult.error.name === WebviewOnOldIOS.NAME) {
errorStore.addErrorMessage(streamResult.error);
}

View file

@ -1,36 +1,121 @@
import { derived, writable, Writable } from "svelte/store";
import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
import type {SimplePeer} from "../WebRtc/SimplePeer";
import { 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) {
update(users => {
users.set(user.userId, user);
return users;
});
onConnect(peer: RemotePeer) {
if (peer instanceof VideoPeer) {
update((users) => {
users.set(peer.userId, peer);
return users;
});
}
},
onDisconnect(userId: number) {
update(users => {
update((users) => {
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) => {
users.delete(userId);
return users;
});
},
});
},
};
}
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();

View file

@ -0,0 +1,36 @@
import { get, writable } from "svelte/store";
import { peerStore } from "./PeerStore";
import { visibilityStore } from "./VisibilityStore";
/**
* A store that contains "true" if the webcam should be stopped for privacy reasons - i.e. if the the user left the the page while not in a discussion.
*/
function createPrivacyShutdownStore() {
let privacyEnabled = false;
const { subscribe, set, update } = writable(privacyEnabled);
visibilityStore.subscribe((isVisible) => {
if (!isVisible && get(peerStore).size === 0) {
privacyEnabled = true;
set(true);
}
if (isVisible) {
privacyEnabled = false;
set(false);
}
});
peerStore.subscribe((peers) => {
if (peers.size === 0 && get(visibilityStore) === false) {
privacyEnabled = true;
set(true);
}
});
return {
subscribe,
};
}
export const privacyShutdownStore = createPrivacyShutdownStore();

View file

@ -1,18 +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
} from "./MediaStore";
import { derived, get, Readable, readable, writable, Writable } from "svelte/store";
import { peerStore } from "./PeerStore";
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
declare const navigator: any; // eslint-disable-line @typescript-eslint/no-explicit-any
/**
* A store that contains the camera state requested by the user (on or off).
@ -29,7 +21,7 @@ function createRequestedScreenSharingState() {
export const requestedScreenSharingState = createRequestedScreenSharingState();
let currentStream : MediaStream|null = null;
let currentStream: MediaStream | null = null;
/**
* Stops the camera from filming
@ -43,27 +35,17 @@ function stopScreenSharing(): void {
currentStream = null;
}
let previousComputedVideoConstraint: boolean|MediaTrackConstraints = false;
let previousComputedAudioConstraint: boolean|MediaTrackConstraints = false;
let previousComputedVideoConstraint: boolean | MediaTrackConstraints = false;
let previousComputedAudioConstraint: boolean | MediaTrackConstraints = false;
/**
* A store containing the media constraints we want to apply.
*/
export const screenSharingConstraintsStore = derived(
[
requestedScreenSharingState,
gameOverlayVisibilityStore,
peerStore,
], (
[
$requestedScreenSharingState,
$gameOverlayVisibilityStore,
$peerStore,
], set
) => {
let currentVideoConstraint: boolean|MediaTrackConstraints = true;
let currentAudioConstraint: boolean|MediaTrackConstraints = false;
[requestedScreenSharingState, gameOverlayVisibilityStore, peerStore],
([$requestedScreenSharingState, $gameOverlayVisibilityStore, $peerStore], set) => {
let currentVideoConstraint: boolean | MediaTrackConstraints = true;
let currentAudioConstraint: boolean | MediaTrackConstraints = false;
// Disable screen sharing if the user requested so
if (!$requestedScreenSharingState) {
@ -84,7 +66,10 @@ export const screenSharingConstraintsStore = derived(
}
// Let's make the changes only if the new value is different from the old one.
if (previousComputedVideoConstraint != currentVideoConstraint || previousComputedAudioConstraint != currentAudioConstraint) {
if (
previousComputedVideoConstraint != currentVideoConstraint ||
previousComputedAudioConstraint != currentAudioConstraint
) {
previousComputedVideoConstraint = currentVideoConstraint;
previousComputedAudioConstraint = currentAudioConstraint;
// Let's copy the objects.
@ -100,85 +85,89 @@ export const screenSharingConstraintsStore = derived(
audio: currentAudioConstraint,
});
}
}, {
},
{
video: false,
audio: false
} as MediaStreamConstraints);
audio: false,
} as MediaStreamConstraints
);
/**
* A store containing the MediaStream object for ScreenSharing (or null if nothing requested, or Error if an error occurred)
*/
export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(screenSharingConstraintsStore, ($screenSharingConstraintsStore, set) => {
const constraints = $screenSharingConstraintsStore;
export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstraints>, LocalStreamStoreValue>(
screenSharingConstraintsStore,
($screenSharingConstraintsStore, set) => {
const constraints = $screenSharingConstraintsStore;
if ($screenSharingConstraintsStore.video === false && $screenSharingConstraintsStore.audio === false) {
stopScreenSharing();
requestedScreenSharingState.disableScreenSharing();
set({
type: 'success',
stream: null,
constraints
});
return;
}
let currentStreamPromise: Promise<MediaStream>;
if (navigator.getDisplayMedia) {
currentStreamPromise = navigator.getDisplayMedia({constraints});
} else if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
currentStreamPromise = navigator.mediaDevices.getDisplayMedia({constraints});
} else {
stopScreenSharing();
set({
type: 'error',
error: new Error('Your browser does not support sharing screen'),
constraints
});
return;
}
(async () => {
try {
if ($screenSharingConstraintsStore.video === false && $screenSharingConstraintsStore.audio === false) {
stopScreenSharing();
currentStream = await currentStreamPromise;
// If stream ends (for instance if user clicks the stop screen sharing button in the browser), let's close the view
for (const track of currentStream.getTracks()) {
track.onended = () => {
stopScreenSharing();
requestedScreenSharingState.disableScreenSharing();
previousComputedVideoConstraint = false;
previousComputedAudioConstraint = false;
set({
type: 'success',
stream: null,
constraints: {
video: false,
audio: false
}
});
};
}
requestedScreenSharingState.disableScreenSharing();
set({
type: 'success',
stream: currentStream,
constraints
type: "success",
stream: null,
constraints,
});
return;
} catch (e) {
currentStream = null;
requestedScreenSharingState.disableScreenSharing();
console.info("Error. Unable to share screen.", e);
set({
type: 'error',
error: e,
constraints
});
}
})();
});
let currentStreamPromise: Promise<MediaStream>;
if (navigator.getDisplayMedia) {
currentStreamPromise = navigator.getDisplayMedia({ constraints });
} else if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
currentStreamPromise = navigator.mediaDevices.getDisplayMedia({ constraints });
} else {
stopScreenSharing();
set({
type: "error",
error: new Error("Your browser does not support sharing screen"),
constraints,
});
return;
}
(async () => {
try {
stopScreenSharing();
currentStream = await currentStreamPromise;
// If stream ends (for instance if user clicks the stop screen sharing button in the browser), let's close the view
for (const track of currentStream.getTracks()) {
track.onended = () => {
stopScreenSharing();
requestedScreenSharingState.disableScreenSharing();
previousComputedVideoConstraint = false;
previousComputedAudioConstraint = false;
set({
type: "success",
stream: null,
constraints: {
video: false,
audio: false,
},
});
};
}
set({
type: "success",
stream: currentStream,
constraints,
});
return;
} catch (e) {
currentStream = null;
requestedScreenSharingState.disableScreenSharing();
console.info("Error. Unable to share screen.", e);
set({
type: "error",
error: e,
constraints,
});
}
})();
}
);
/**
* A store containing whether the screen sharing button should be displayed or hidden.
@ -191,3 +180,32 @@ 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();
};
});

View file

@ -0,0 +1,3 @@
import { writable } from "svelte/store";
export const showReportScreenStore = writable<{ userId: number; userName: string } | null>(null);

View file

@ -0,0 +1,36 @@
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();

View file

@ -0,0 +1,49 @@
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();

View file

@ -0,0 +1,16 @@
import { readable } from "svelte/store";
/**
* A store containing whether the current page is visible or not.
*/
export const visibilityStore = readable(document.visibilityState === "visible", function start(set) {
const onVisibilityChange = () => {
set(document.visibilityState === "visible");
};
document.addEventListener("visibilitychange", onVisibilityChange);
return function stop() {
document.removeEventListener("visibilitychange", onVisibilityChange);
};
});