diff --git a/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte b/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte index 8f4de785..ed4a84ca 100644 --- a/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte +++ b/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte @@ -3,10 +3,11 @@ import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore"; import firefoxImg from "./images/help-setting-camera-permission-firefox.png"; import chromeImg from "./images/help-setting-camera-permission-chrome.png"; + import {getNavigatorType, isAndroid as isAndroidFct, NavigatorType} from "../../WebRtc/DeviceUtils"; - let isAndroid = window.navigator.userAgent.includes('Android'); - let isFirefox = window.navigator.userAgent.includes('Firefox'); - let isChrome = window.navigator.userAgent.includes('Chrome'); + let isAndroid = isAndroidFct(); + let isFirefox = getNavigatorType() === NavigatorType.firefox; + let isChrome = getNavigatorType() === NavigatorType.chrome; function refresh() { window.location.reload(); diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index b19a9356..bd32266c 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -4,7 +4,7 @@ import { userMovingStore } from "./GameStore"; import { HtmlUtils } from "../WebRtc/HtmlUtils"; import { BrowserTooOldError } from "./Errors/BrowserTooOldError"; import { errorStore } from "./ErrorStore"; -import { isIOS } from "../WebRtc/DeviceUtils"; +import { getNavigatorType, isIOS, NavigatorType } from "../WebRtc/DeviceUtils"; import { WebviewOnOldIOS } from "./Errors/WebviewOnOldIOS"; import { gameOverlayVisibilityStore } from "./GameOverlayStoreVisibility"; import { peerStore } from "./PeerStore"; @@ -339,18 +339,19 @@ interface StreamErrorValue { } let currentStream: MediaStream | null = null; +let oldConstraints = { video: false, audio: false }; +//only firefox correctly implements the 'enabled' track property, on chrome we have to stop the track then reinstantiate the stream +const implementCorrectTrackBehavior = getNavigatorType() === NavigatorType.firefox; /** * Stops the camera from filming */ function applyCameraConstraints(currentStream: MediaStream | null, constraints: MediaTrackConstraints | boolean): void { - if (currentStream) { - for (const track of currentStream.getVideoTracks()) { - track.enabled = constraints !== false; - if (constraints && constraints !== true) { - track.applyConstraints(constraints); - } - } + if (!currentStream) { + return; + } + for (const track of currentStream.getVideoTracks()) { + toggleConstraints(track, constraints); } } @@ -361,13 +362,22 @@ function applyMicrophoneConstraints( currentStream: MediaStream | null, constraints: MediaTrackConstraints | boolean ): void { - if (currentStream) { - for (const track of currentStream.getAudioTracks()) { - track.enabled = constraints !== false; - if (constraints && constraints !== true) { - track.applyConstraints(constraints); - } - } + if (!currentStream) { + return; + } + for (const track of currentStream.getAudioTracks()) { + toggleConstraints(track, constraints); + } +} + +function toggleConstraints(track: MediaStreamTrack, constraints: MediaTrackConstraints | boolean): void { + if (implementCorrectTrackBehavior) { + track.enabled = constraints !== false; + } else if (constraints === false) { + track.stop(); + } + if (constraints && constraints !== true) { + track.applyConstraints(constraints); } } @@ -379,6 +389,53 @@ export const localStreamStore = derived, LocalS ($mediaStreamConstraintsStore, set) => { const constraints = { ...$mediaStreamConstraintsStore }; + async function initStream(constraints: MediaStreamConstraints) { + try { + const newStream = await navigator.mediaDevices.getUserMedia(constraints); + if (currentStream) { + //we need stop all tracks to make sure the old stream will be garbage collected + currentStream.getTracks().forEach((t) => t.stop()); + } + currentStream = newStream; + set({ + type: "success", + stream: currentStream, + }); + return; + } catch (e) { + if (constraints.video !== false || constraints.audio !== false) { + console.info( + "Error. Unable to get microphone and/or camera access. Trying audio only.", + constraints, + e + ); + // TODO: does it make sense to pop this error when retrying? + set({ + type: "error", + error: e, + }); + // Let's try without video constraints + if (constraints.video !== false) { + requestedCameraState.disableWebcam(); + } + if (constraints.audio !== false) { + requestedMicrophoneState.disableMicrophone(); + } + } else if (!constraints.video && !constraints.audio) { + set({ + type: "error", + error: new MediaStreamConstraintsError(), + }); + } else { + console.info("Error. Unable to get microphone and/or camera access.", constraints, e); + set({ + type: "error", + error: e, + }); + } + } + } + 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.'); @@ -405,57 +462,31 @@ export const localStreamStore = derived, LocalS applyMicrophoneConstraints(currentStream, constraints.audio || false); applyCameraConstraints(currentStream, constraints.video || false); - if (currentStream === null) { - // we need to assign a first value to the stream because getUserMedia is async - set({ - type: "success", - stream: null, - }); - (async () => { - try { - currentStream = await navigator.mediaDevices.getUserMedia(constraints); - set({ - type: "success", - stream: currentStream, - }); - return; - } catch (e) { - if (constraints.video !== false || constraints.audio !== 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, - }); - // Let's try without video constraints - if (constraints.video !== false) { - requestedCameraState.disableWebcam(); - } - if (constraints.audio !== false) { - requestedMicrophoneState.disableMicrophone(); - } - } else if (!constraints.video && !constraints.audio) { - set({ - type: "error", - error: new MediaStreamConstraintsError(), - }); - } else { - console.info( - "Error. Unable to get microphone and/or camera access.", - $mediaStreamConstraintsStore, - e - ); - set({ - type: "error", - error: e, - }); - } - } - })(); + if (implementCorrectTrackBehavior) { + //on good navigators like firefox, we can instantiate the stream once and simply disable or enable the tracks as needed + if (currentStream === null) { + // we need to assign a first value to the stream because getUserMedia is async + set({ + type: "success", + stream: null, + }); + initStream(constraints); + } + } else { + //on bad navigators like chrome, we have to stop the tracks when we mute and reinstantiate the stream when we need to unmute + if (constraints.audio === false && constraints.video === false) { + currentStream = null; + set({ + type: "success", + stream: null, + }); + } else if ((constraints.audio && !oldConstraints.audio) || (!oldConstraints.video && constraints.video)) { + initStream(constraints); + } + oldConstraints = { + video: !!constraints.video, + audio: !!constraints.audio, + }; } } ); diff --git a/front/src/WebRtc/DeviceUtils.ts b/front/src/WebRtc/DeviceUtils.ts index 5113fc72..9be07501 100644 --- a/front/src/WebRtc/DeviceUtils.ts +++ b/front/src/WebRtc/DeviceUtils.ts @@ -7,3 +7,23 @@ export function isIOS(): boolean { (navigator.userAgent.includes("Mac") && "ontouchend" in document) ); } + +export enum NavigatorType { + firefox = 1, + chrome, + safari, +} + +export function getNavigatorType(): NavigatorType { + if (window.navigator.userAgent.includes("Firefox")) { + return NavigatorType.firefox; + } else if (window.navigator.userAgent.includes("Chrome")) { + return NavigatorType.chrome; + } else if (window.navigator.userAgent.includes("Safari")) { + return NavigatorType.safari; + } + throw "Couldn't detect navigator type"; +} +export function isAndroid(): boolean { + return window.navigator.userAgent.includes("Android"); +}