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

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