2021-06-25 18:14:40 +02:00
|
|
|
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";
|
2021-05-18 16:38:56 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A store that contains the camera state requested by the user (on or off).
|
|
|
|
*/
|
|
|
|
function createRequestedCameraState() {
|
|
|
|
const { subscribe, set, update } = writable(true);
|
|
|
|
|
|
|
|
return {
|
|
|
|
subscribe,
|
|
|
|
enableWebcam: () => set(true),
|
|
|
|
disableWebcam: () => set(false),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A store that contains the microphone state requested by the user (on or off).
|
|
|
|
*/
|
|
|
|
function createRequestedMicrophoneState() {
|
|
|
|
const { subscribe, set, update } = writable(true);
|
|
|
|
|
|
|
|
return {
|
|
|
|
subscribe,
|
|
|
|
enableMicrophone: () => set(true),
|
|
|
|
disableMicrophone: () => set(false),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A store that contains whether the EnableCameraScene is shown or not.
|
|
|
|
*/
|
|
|
|
function createEnableCameraSceneVisibilityStore() {
|
|
|
|
const { subscribe, set, update } = writable(false);
|
|
|
|
|
|
|
|
return {
|
|
|
|
subscribe,
|
|
|
|
showEnableCameraScene: () => set(true),
|
|
|
|
hideEnableCameraScene: () => set(false),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export const requestedCameraState = createRequestedCameraState();
|
|
|
|
export const requestedMicrophoneState = createRequestedMicrophoneState();
|
|
|
|
export const enableCameraSceneVisibilityStore = createEnableCameraSceneVisibilityStore();
|
|
|
|
|
2021-05-20 18:05:03 +02:00
|
|
|
/**
|
|
|
|
* A store containing whether the webcam was enabled in the last 10 seconds
|
|
|
|
*/
|
|
|
|
const enabledWebCam10secondsAgoStore = readable(false, function start(set) {
|
2021-06-25 18:14:40 +02:00
|
|
|
let timeout: NodeJS.Timeout | null = null;
|
2021-05-20 18:05:03 +02:00
|
|
|
|
|
|
|
const unsubscribe = requestedCameraState.subscribe((enabled) => {
|
|
|
|
if (enabled === true) {
|
|
|
|
if (timeout) {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
}
|
|
|
|
timeout = setTimeout(() => {
|
|
|
|
set(false);
|
|
|
|
}, 10000);
|
|
|
|
set(true);
|
|
|
|
} else {
|
|
|
|
set(false);
|
|
|
|
}
|
2021-06-25 18:14:40 +02:00
|
|
|
});
|
2021-05-20 18:05:03 +02:00
|
|
|
|
|
|
|
return function stop() {
|
|
|
|
unsubscribe();
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A store containing whether the webcam was enabled in the last 5 seconds
|
|
|
|
*/
|
|
|
|
const userMoved5SecondsAgoStore = readable(false, function start(set) {
|
2021-06-25 18:14:40 +02:00
|
|
|
let timeout: NodeJS.Timeout | null = null;
|
2021-05-20 18:05:03 +02:00
|
|
|
|
|
|
|
const unsubscribe = userMovingStore.subscribe((moving) => {
|
|
|
|
if (moving === true) {
|
|
|
|
if (timeout) {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
}
|
|
|
|
set(true);
|
|
|
|
} else {
|
|
|
|
timeout = setTimeout(() => {
|
|
|
|
set(false);
|
|
|
|
}, 5000);
|
|
|
|
}
|
2021-06-25 18:14:40 +02:00
|
|
|
});
|
2021-05-20 18:05:03 +02:00
|
|
|
|
|
|
|
return function stop() {
|
|
|
|
unsubscribe();
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
2021-05-26 12:19:58 +02:00
|
|
|
* A store containing whether the mouse is getting close the bottom right corner.
|
2021-05-20 18:05:03 +02:00
|
|
|
*/
|
|
|
|
const mouseInBottomRight = readable(false, function start(set) {
|
|
|
|
let lastInBottomRight = false;
|
2021-06-25 18:14:40 +02:00
|
|
|
const gameDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("game");
|
2021-05-20 18:05:03 +02:00
|
|
|
|
|
|
|
const detectInBottomRight = (event: MouseEvent) => {
|
|
|
|
const rect = gameDiv.getBoundingClientRect();
|
2021-06-25 18:14:40 +02:00
|
|
|
const inBottomRight = event.x - rect.left > (rect.width * 3) / 4 && event.y - rect.top > (rect.height * 3) / 4;
|
2021-05-20 18:05:03 +02:00
|
|
|
if (inBottomRight !== lastInBottomRight) {
|
|
|
|
lastInBottomRight = inBottomRight;
|
|
|
|
set(inBottomRight);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
document.addEventListener("mousemove", detectInBottomRight);
|
2021-05-20 18:05:03 +02:00
|
|
|
|
|
|
|
return function stop() {
|
2021-06-25 18:14:40 +02:00
|
|
|
document.removeEventListener("mousemove", detectInBottomRight);
|
|
|
|
};
|
2021-05-20 18:05:03 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2021-06-25 18:14:40 +02:00
|
|
|
export const cameraEnergySavingStore = derived(
|
|
|
|
[userMoved5SecondsAgoStore, peerStore, enabledWebCam10secondsAgoStore, mouseInBottomRight],
|
|
|
|
([$userMoved5SecondsAgoStore, $peerStore, $enabledWebCam10secondsAgoStore, $mouseInBottomRight]) => {
|
|
|
|
return (
|
|
|
|
!$mouseInBottomRight &&
|
|
|
|
!$userMoved5SecondsAgoStore &&
|
|
|
|
$peerStore.size === 0 &&
|
|
|
|
!$enabledWebCam10secondsAgoStore
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
2021-05-20 18:05:03 +02:00
|
|
|
|
2021-05-18 16:38:56 +02:00
|
|
|
/**
|
|
|
|
* A store that contains video constraints.
|
|
|
|
*/
|
|
|
|
function createVideoConstraintStore() {
|
|
|
|
const { subscribe, set, update } = writable({
|
|
|
|
width: { min: 640, ideal: 1280, max: 1920 },
|
|
|
|
height: { min: 400, ideal: 720 },
|
|
|
|
frameRate: { ideal: localUserStore.getVideoQualityValue() },
|
|
|
|
facingMode: "user",
|
2021-06-25 18:14:40 +02:00
|
|
|
resizeMode: "crop-and-scale",
|
|
|
|
aspectRatio: 1.777777778,
|
2021-05-19 11:17:43 +02:00
|
|
|
} as MediaTrackConstraints);
|
2021-05-18 16:38:56 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
subscribe,
|
2021-06-25 18:14:40 +02:00
|
|
|
setDeviceId: (deviceId: string | undefined) =>
|
|
|
|
update((constraints) => {
|
|
|
|
if (deviceId !== undefined) {
|
|
|
|
constraints.deviceId = {
|
|
|
|
exact: deviceId,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
delete constraints.deviceId;
|
|
|
|
}
|
2021-05-18 16:38:56 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
return constraints;
|
|
|
|
}),
|
|
|
|
setFrameRate: (frameRate: number) =>
|
|
|
|
update((constraints) => {
|
|
|
|
constraints.frameRate = { ideal: frameRate };
|
2021-05-19 11:17:43 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
return constraints;
|
|
|
|
}),
|
2021-05-18 16:38:56 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export const videoConstraintStore = createVideoConstraintStore();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A store that contains video constraints.
|
|
|
|
*/
|
|
|
|
function createAudioConstraintStore() {
|
|
|
|
const { subscribe, set, update } = writable({
|
|
|
|
//TODO: make these values configurable in the game settings menu and store them in localstorage
|
|
|
|
autoGainControl: false,
|
|
|
|
echoCancellation: true,
|
2021-06-25 18:14:40 +02:00
|
|
|
noiseSuppression: true,
|
|
|
|
} as boolean | MediaTrackConstraints);
|
2021-05-18 16:38:56 +02:00
|
|
|
|
|
|
|
let selectedDeviceId = null;
|
|
|
|
|
|
|
|
return {
|
|
|
|
subscribe,
|
2021-06-25 18:14:40 +02:00
|
|
|
setDeviceId: (deviceId: string | undefined) =>
|
|
|
|
update((constraints) => {
|
|
|
|
selectedDeviceId = deviceId;
|
2021-05-18 16:38:56 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
if (typeof constraints === "boolean") {
|
|
|
|
constraints = {};
|
|
|
|
}
|
|
|
|
if (deviceId !== undefined) {
|
|
|
|
constraints.deviceId = {
|
|
|
|
exact: selectedDeviceId,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
delete constraints.deviceId;
|
|
|
|
}
|
2021-05-18 16:38:56 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
return constraints;
|
|
|
|
}),
|
2021-05-18 16:38:56 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export const audioConstraintStore = createAudioConstraintStore();
|
|
|
|
|
|
|
|
let timeout: NodeJS.Timeout;
|
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
let previousComputedVideoConstraint: boolean | MediaTrackConstraints = false;
|
|
|
|
let previousComputedAudioConstraint: boolean | MediaTrackConstraints = false;
|
2021-05-19 11:17:43 +02:00
|
|
|
|
2021-05-18 16:38:56 +02:00
|
|
|
/**
|
|
|
|
* A store containing the media constraints we want to apply.
|
|
|
|
*/
|
|
|
|
export const mediaStreamConstraintsStore = derived(
|
|
|
|
[
|
|
|
|
requestedCameraState,
|
|
|
|
requestedMicrophoneState,
|
|
|
|
gameOverlayVisibilityStore,
|
|
|
|
enableCameraSceneVisibilityStore,
|
|
|
|
videoConstraintStore,
|
|
|
|
audioConstraintStore,
|
2021-05-19 11:17:43 +02:00
|
|
|
privacyShutdownStore,
|
2021-05-20 18:05:03 +02:00
|
|
|
cameraEnergySavingStore,
|
2021-06-25 18:14:40 +02:00
|
|
|
],
|
|
|
|
(
|
2021-05-18 16:38:56 +02:00
|
|
|
[
|
|
|
|
$requestedCameraState,
|
|
|
|
$requestedMicrophoneState,
|
|
|
|
$gameOverlayVisibilityStore,
|
|
|
|
$enableCameraSceneVisibilityStore,
|
|
|
|
$videoConstraintStore,
|
|
|
|
$audioConstraintStore,
|
2021-05-19 11:17:43 +02:00
|
|
|
$privacyShutdownStore,
|
2021-05-20 18:05:03 +02:00
|
|
|
$cameraEnergySavingStore,
|
2021-06-25 18:14:40 +02:00
|
|
|
],
|
|
|
|
set
|
2021-05-18 16:38:56 +02:00
|
|
|
) => {
|
2021-06-25 18:14:40 +02:00
|
|
|
let currentVideoConstraint: boolean | MediaTrackConstraints = $videoConstraintStore;
|
|
|
|
let currentAudioConstraint: boolean | MediaTrackConstraints = $audioConstraintStore;
|
2021-05-19 11:17:43 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
if ($enableCameraSceneVisibilityStore) {
|
|
|
|
set({
|
|
|
|
video: currentVideoConstraint,
|
|
|
|
audio: currentAudioConstraint,
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2021-05-18 16:38:56 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
// Disable webcam if the user requested so
|
|
|
|
if ($requestedCameraState === false) {
|
|
|
|
currentVideoConstraint = false;
|
|
|
|
}
|
2021-05-18 16:38:56 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
// Disable microphone if the user requested so
|
|
|
|
if ($requestedMicrophoneState === false) {
|
|
|
|
currentAudioConstraint = false;
|
|
|
|
}
|
2021-05-18 16:38:56 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
// Disable webcam and microphone when in a Jitsi
|
|
|
|
if ($gameOverlayVisibilityStore === false) {
|
|
|
|
currentVideoConstraint = false;
|
|
|
|
currentAudioConstraint = false;
|
|
|
|
}
|
2021-05-18 16:38:56 +02:00
|
|
|
|
2021-07-27 14:29:09 +02:00
|
|
|
// Disable webcam for privacy reasons (the game is not visible and we were talking to no one)
|
2021-06-25 18:14:40 +02:00
|
|
|
if ($privacyShutdownStore === true) {
|
|
|
|
currentVideoConstraint = false;
|
|
|
|
}
|
2021-05-18 16:38:56 +02:00
|
|
|
|
2021-07-27 14:29:09 +02:00
|
|
|
// Disable webcam for energy reasons (the user is not moving and we are talking to no one)
|
2021-06-25 18:14:40 +02:00
|
|
|
if ($cameraEnergySavingStore === true) {
|
|
|
|
currentVideoConstraint = false;
|
|
|
|
currentAudioConstraint = false;
|
|
|
|
}
|
2021-05-18 16:38:56 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
// 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 };
|
|
|
|
}
|
2021-05-20 18:05:03 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
if (timeout) {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
}
|
2021-05-18 16:38:56 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
// Let's wait a little bit to avoid sending too many constraint changes.
|
|
|
|
timeout = setTimeout(() => {
|
|
|
|
set({
|
|
|
|
video: currentVideoConstraint,
|
|
|
|
audio: currentAudioConstraint,
|
|
|
|
});
|
|
|
|
}, 100);
|
2021-05-19 11:17:43 +02:00
|
|
|
}
|
2021-06-25 18:14:40 +02:00
|
|
|
},
|
|
|
|
{
|
|
|
|
video: false,
|
|
|
|
audio: false,
|
|
|
|
} as MediaStreamConstraints
|
|
|
|
);
|
2021-05-18 16:38:56 +02:00
|
|
|
|
|
|
|
export type LocalStreamStoreValue = StreamSuccessValue | StreamErrorValue;
|
|
|
|
|
|
|
|
interface StreamSuccessValue {
|
2021-06-25 18:14:40 +02:00
|
|
|
type: "success";
|
|
|
|
stream: MediaStream | null;
|
2021-05-18 16:38:56 +02:00
|
|
|
// The constraints that we got (and not the one that have been requested)
|
2021-06-25 18:14:40 +02:00
|
|
|
constraints: MediaStreamConstraints;
|
2021-05-18 16:38:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface StreamErrorValue {
|
2021-06-25 18:14:40 +02:00
|
|
|
type: "error";
|
|
|
|
error: Error;
|
|
|
|
constraints: MediaStreamConstraints;
|
2021-05-18 16:38:56 +02:00
|
|
|
}
|
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
let currentStream: MediaStream | null = null;
|
2021-05-18 16:38:56 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops the camera from filming
|
|
|
|
*/
|
|
|
|
function stopCamera(): void {
|
|
|
|
if (currentStream) {
|
|
|
|
for (const track of currentStream.getVideoTracks()) {
|
|
|
|
track.stop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops the microphone from listening
|
|
|
|
*/
|
|
|
|
function stopMicrophone(): void {
|
|
|
|
if (currentStream) {
|
|
|
|
for (const track of currentStream.getAudioTracks()) {
|
|
|
|
track.stop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A store containing the MediaStream object (or null if nothing requested, or Error if an error occurred)
|
|
|
|
*/
|
2021-06-25 18:14:40 +02:00
|
|
|
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.');
|
|
|
|
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;
|
|
|
|
}
|
2021-05-18 16:38:56 +02:00
|
|
|
}
|
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
if (constraints.audio === false) {
|
2021-05-19 11:17:43 +02:00
|
|
|
stopMicrophone();
|
2021-06-25 18:14:40 +02:00
|
|
|
}
|
|
|
|
if (constraints.video === false) {
|
2021-05-19 11:17:43 +02:00
|
|
|
stopCamera();
|
2021-06-25 18:14:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (constraints.audio === false && constraints.video === false) {
|
|
|
|
currentStream = null;
|
2021-05-18 16:38:56 +02:00
|
|
|
set({
|
2021-06-25 18:14:40 +02:00
|
|
|
type: "success",
|
|
|
|
stream: null,
|
|
|
|
constraints,
|
2021-05-18 16:38:56 +02:00
|
|
|
});
|
|
|
|
return;
|
2021-06-25 18:14:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
stopMicrophone();
|
|
|
|
stopCamera();
|
|
|
|
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
|
2021-05-18 16:38:56 +02:00
|
|
|
set({
|
2021-06-25 18:14:40 +02:00
|
|
|
type: "success",
|
|
|
|
stream: currentStream,
|
|
|
|
constraints,
|
2021-05-18 16:38:56 +02:00
|
|
|
});
|
2021-06-25 18:14:40 +02:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
}
|
2021-05-18 16:38:56 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
/*constraints.video = false;
|
2021-05-18 16:38:56 +02:00
|
|
|
if (constraints.audio === false) {
|
|
|
|
console.info("Error. Unable to get microphone and/or camera access.", $mediaStreamConstraintsStore, e);
|
|
|
|
set({
|
|
|
|
type: 'error',
|
|
|
|
error: e,
|
|
|
|
constraints
|
|
|
|
});
|
|
|
|
// Let's make as if the user did not ask.
|
|
|
|
requestedCameraState.disableWebcam();
|
|
|
|
} else {
|
|
|
|
console.info("Error. Unable to get microphone and/or camera access. Trying audio only.", $mediaStreamConstraintsStore, e);
|
|
|
|
try {
|
|
|
|
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
|
|
set({
|
|
|
|
type: 'success',
|
|
|
|
stream: currentStream,
|
|
|
|
constraints
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
} catch (e2) {
|
|
|
|
console.info("Error. Unable to get microphone fallback access.", $mediaStreamConstraintsStore, e2);
|
|
|
|
set({
|
|
|
|
type: 'error',
|
|
|
|
error: e,
|
|
|
|
constraints
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}*/
|
2021-06-25 18:14:40 +02:00
|
|
|
}
|
|
|
|
})();
|
|
|
|
}
|
|
|
|
);
|
2021-05-19 11:17:43 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A store containing the real active media constrained (not the one requested by the user, but the one we got from the system)
|
|
|
|
*/
|
|
|
|
export const obtainedMediaConstraintStore = derived(localStreamStore, ($localStreamStore) => {
|
|
|
|
return $localStreamStore.constraints;
|
|
|
|
});
|
2021-05-20 18:05:03 +02:00
|
|
|
|
2021-06-01 16:17:36 +02:00
|
|
|
/**
|
|
|
|
* Device list
|
|
|
|
*/
|
|
|
|
export const deviceListStore = readable<MediaDeviceInfo[]>([], function start(set) {
|
|
|
|
let deviceListCanBeQueried = false;
|
|
|
|
|
|
|
|
const queryDeviceList = () => {
|
|
|
|
// Note: so far, we are ignoring any failures.
|
2021-06-25 18:14:40 +02:00
|
|
|
navigator.mediaDevices
|
|
|
|
.enumerateDevices()
|
|
|
|
.then((mediaDeviceInfos) => {
|
|
|
|
set(mediaDeviceInfos);
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
console.error(e);
|
|
|
|
throw e;
|
|
|
|
});
|
2021-06-01 16:17:36 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const unsubscribe = localStreamStore.subscribe((streamResult) => {
|
|
|
|
if (streamResult.type === "success" && streamResult.stream !== null) {
|
|
|
|
if (deviceListCanBeQueried === false) {
|
|
|
|
queryDeviceList();
|
|
|
|
deviceListCanBeQueried = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (navigator.mediaDevices) {
|
2021-06-25 18:14:40 +02:00
|
|
|
navigator.mediaDevices.addEventListener("devicechange", queryDeviceList);
|
2021-06-01 16:17:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return function stop() {
|
|
|
|
unsubscribe();
|
|
|
|
if (navigator.mediaDevices) {
|
2021-06-25 18:14:40 +02:00
|
|
|
navigator.mediaDevices.removeEventListener("devicechange", queryDeviceList);
|
2021-06-01 16:17:36 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
export const cameraListStore = derived(deviceListStore, ($deviceListStore) => {
|
2021-06-25 18:14:40 +02:00
|
|
|
return $deviceListStore.filter((device) => device.kind === "videoinput");
|
2021-06-01 16:17:36 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
export const microphoneListStore = derived(deviceListStore, ($deviceListStore) => {
|
2021-06-25 18:14:40 +02:00
|
|
|
return $deviceListStore.filter((device) => device.kind === "audioinput");
|
2021-06-01 16:17:36 +02:00
|
|
|
});
|
2021-06-01 16:43:16 +02:00
|
|
|
|
|
|
|
// TODO: detect the new webcam and automatically switch on it.
|
|
|
|
cameraListStore.subscribe((devices) => {
|
|
|
|
// If the selected camera is unplugged, let's remove the constraint on deviceId
|
|
|
|
const constraints = get(videoConstraintStore);
|
|
|
|
if (!constraints.deviceId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we cannot find the device ID, let's remove it.
|
|
|
|
// @ts-ignore
|
2021-06-25 18:14:40 +02:00
|
|
|
if (!devices.find((device) => device.deviceId === constraints.deviceId.exact)) {
|
2021-06-01 16:43:16 +02:00
|
|
|
videoConstraintStore.setDeviceId(undefined);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
microphoneListStore.subscribe((devices) => {
|
|
|
|
// If the selected camera is unplugged, let's remove the constraint on deviceId
|
|
|
|
const constraints = get(audioConstraintStore);
|
2021-06-25 18:14:40 +02:00
|
|
|
if (typeof constraints === "boolean") {
|
2021-06-01 16:43:16 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!constraints.deviceId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we cannot find the device ID, let's remove it.
|
|
|
|
// @ts-ignore
|
2021-06-25 18:14:40 +02:00
|
|
|
if (!devices.find((device) => device.deviceId === constraints.deviceId.exact)) {
|
2021-06-01 16:43:16 +02:00
|
|
|
audioConstraintStore.setDeviceId(undefined);
|
|
|
|
}
|
|
|
|
});
|
2021-06-03 14:10:31 +02:00
|
|
|
|
2021-06-25 18:14:40 +02:00
|
|
|
localStreamStore.subscribe((streamResult) => {
|
|
|
|
if (streamResult.type === "error") {
|
2021-06-04 16:19:41 +02:00
|
|
|
if (streamResult.error.name === BrowserTooOldError.NAME || streamResult.error.name === WebviewOnOldIOS.NAME) {
|
2021-06-03 14:10:31 +02:00
|
|
|
errorStore.addErrorMessage(streamResult.error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|