Fixing camera led always on

This commit fixes a race condition in the "getUserMedia" call that could lead to the webcam being in an always on state.

If we open and close the webcam really quickly, the camera close was sometimes passing BEFORE the open was fully resolved (because getUserMedia can take a few 100ms to answer properly).
As a result, the webcam would stay open.
To solve this, we are putting all calls to getUserMedia in a resolve chain (`currentGetUserMediaPromise`)

Closes #2149
This commit is contained in:
David Négrier 2022-05-10 10:46:06 +02:00
parent 06d3332499
commit ae9170ba85

View File

@ -414,6 +414,13 @@ async function toggleConstraints(track: MediaStreamTrack, constraints: MediaTrac
} }
} }
// This promise is important to queue the calls to "getUserMedia"
// Otherwise, this can happen:
// User requests a start then a stop of the camera quickly
// The promise to start the cam starts. Before the promise is fulfilled, the camera is stopped.
// Then, the MediaStream of the camera start resolves (resulting in the LED being turned on instead of off)
let currentGetUserMediaPromise: Promise<MediaStream | undefined> = Promise.resolve(undefined);
/** /**
* A store containing the MediaStream object (or null if nothing requested, or Error if an error occurred) * A store containing the MediaStream object (or null if nothing requested, or Error if an error occurred)
*/ */
@ -422,50 +429,59 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
($mediaStreamConstraintsStore, set) => { ($mediaStreamConstraintsStore, set) => {
const constraints = { ...$mediaStreamConstraintsStore }; const constraints = { ...$mediaStreamConstraintsStore };
async function initStream(constraints: MediaStreamConstraints) { function initStream(constraints: MediaStreamConstraints): Promise<MediaStream | undefined> {
try { currentGetUserMediaPromise = currentGetUserMediaPromise.then(() => {
if (currentStream) { return navigator.mediaDevices
//we need stop all tracks to make sure the old stream will be garbage collected .getUserMedia(constraints)
//currentStream.getTracks().forEach((t) => t.stop()); .then((stream) => {
} // Close old stream
currentStream = await navigator.mediaDevices.getUserMedia(constraints); if (currentStream) {
set({ //we need stop all tracks to make sure the old stream will be garbage collected
type: "success", currentStream.getTracks().forEach((t) => t.stop());
stream: currentStream, }
});
return; currentStream = stream;
} catch (e) { set({
if (constraints.video !== false || constraints.audio !== false) { type: "success",
console.info( stream: currentStream,
"Error. Unable to get microphone and/or camera access. Trying audio only.", });
constraints, return stream;
e })
); .catch((e) => {
// TODO: does it make sense to pop this error when retrying? if (constraints.video !== false || constraints.audio !== false) {
set({ console.info(
type: "error", "Error. Unable to get microphone and/or camera access. Trying audio only.",
error: e instanceof Error ? e : new Error("An unknown error happened"), constraints,
e
);
// TODO: does it make sense to pop this error when retrying?
set({
type: "error",
error: e instanceof Error ? e : new Error("An unknown error happened"),
});
// 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 instanceof Error ? e : new Error("An unknown error happened"),
});
}
return undefined;
}); });
// Let's try without video constraints });
if (constraints.video !== false) { return currentGetUserMediaPromise;
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 instanceof Error ? e : new Error("An unknown error happened"),
});
}
}
} }
if (navigator.mediaDevices === undefined) { if (navigator.mediaDevices === undefined) {
@ -491,46 +507,62 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
} }
} }
applyMicrophoneConstraints(currentStream, constraints.audio || false).catch((e) => console.error(e)); if (currentStream === null) {
applyCameraConstraints(currentStream, constraints.video || false).catch((e) => console.error(e)); // we need to assign a first value to the stream because getUserMedia is async
set({
if (implementCorrectTrackBehavior) { type: "success",
//on good navigators like firefox, we can instantiate the stream once and simply disable or enable the tracks as needed stream: null,
if (currentStream === null) { });
// we need to assign a first value to the stream because getUserMedia is async
set({
type: "success",
stream: null,
});
initStream(constraints).catch((e) => {
set({
type: "error",
error: e instanceof Error ? e : new Error("An unknown error happened"),
});
});
}
} 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,
});
} //we reemit the stream if it was muted just to be sure
else if (constraints.audio /* && !oldConstraints.audio*/ || (!oldConstraints.video && constraints.video)) {
initStream(constraints).catch((e) => {
set({
type: "error",
error: e instanceof Error ? e : new Error("An unknown error happened"),
});
});
}
oldConstraints = {
video: !!constraints.video,
audio: !!constraints.audio,
};
} }
(async () => {
await applyMicrophoneConstraints(currentStream, constraints.audio || false).catch((e) => console.error(e));
await applyCameraConstraints(currentStream, constraints.video || false).catch((e) => console.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) {
initStream(constraints).catch((e) => {
set({
type: "error",
error: e instanceof Error ? e : new Error("An unknown error happened"),
});
});
}
} 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) {
currentGetUserMediaPromise = currentGetUserMediaPromise.then(() => {
if (currentStream) {
//we need stop all tracks to make sure the old stream will be garbage collected
currentStream.getTracks().forEach((t) => t.stop());
}
currentStream = null;
set({
type: "success",
stream: null,
});
return undefined;
});
} //we reemit the stream if it was muted just to be sure
else if (
constraints.audio /* && !oldConstraints.audio*/ ||
(!oldConstraints.video && constraints.video)
) {
initStream(constraints).catch((e) => {
set({
type: "error",
error: e instanceof Error ? e : new Error("An unknown error happened"),
});
});
}
oldConstraints = {
video: !!constraints.video,
audio: !!constraints.audio,
};
}
})().catch((e) => console.error(e));
} }
); );