mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-22 22:29:30 +00:00
Give the microphone back when the audio graph fails to build
Building the analyser can fail after getUserMedia has already resolved, and the capture then outlived its own failure: the microphone stayed open, and its in-use light on, behind a meter reporting the microphone as unavailable, until the menu closed. Cleanup did two jobs under one name. Releasing what has been acquired is now its own step, which teardown and the failure path both take. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -227,6 +227,33 @@ describe("useMicrophoneLevel", () => {
|
||||
expect(result.current).toEqual({ type: "unavailable" });
|
||||
});
|
||||
|
||||
test("a capture is released when the audio graph fails to build", async () => {
|
||||
const { stream, stop } = fakeStream();
|
||||
vi.stubGlobal("navigator", {
|
||||
mediaDevices: { getUserMedia: vi.fn().mockResolvedValue(stream) },
|
||||
});
|
||||
// Whatever the browser does after the microphone is already open.
|
||||
vi.stubGlobal(
|
||||
"AudioContext",
|
||||
class {
|
||||
public constructor() {
|
||||
throw new Error("no audio backend");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useMicrophoneLevel("mic-1", true, STEPS),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(result.current).toEqual({ type: "unavailable" }),
|
||||
);
|
||||
|
||||
// Otherwise the microphone stays open, and its in-use light on, behind a
|
||||
// meter that says it is unavailable.
|
||||
expect(stop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("a missing microphone is reported as absent, not as silence", () => {
|
||||
const getUserMedia = vi.fn();
|
||||
vi.stubGlobal("navigator", { mediaDevices: { getUserMedia } });
|
||||
|
||||
@@ -82,8 +82,8 @@ export function useMicrophoneLevel(
|
||||
let context: AudioContext | undefined;
|
||||
let frame: number | undefined;
|
||||
|
||||
const dispose = (): void => {
|
||||
disposed = true;
|
||||
/** Gives back whatever has been acquired so far. */
|
||||
const release = (): void => {
|
||||
if (frame !== undefined) cancelAnimationFrame(frame);
|
||||
stream?.getTracks().forEach((t) => t.stop());
|
||||
// close() rejects if the context is already closed, which is possible if
|
||||
@@ -94,6 +94,11 @@ export function useMicrophoneLevel(
|
||||
frame = undefined;
|
||||
};
|
||||
|
||||
const dispose = (): void => {
|
||||
disposed = true;
|
||||
release();
|
||||
};
|
||||
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ audio: { deviceId: { exact: deviceId } } })
|
||||
.then((acquired) => {
|
||||
@@ -137,6 +142,11 @@ export function useMicrophoneLevel(
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
if (disposed) return;
|
||||
// The microphone may already be open: building the audio graph can
|
||||
// fail after getUserMedia has resolved, and the capture would then
|
||||
// outlive its own failure, holding the microphone-in-use indicator on
|
||||
// behind a meter that says the microphone is unavailable.
|
||||
release();
|
||||
rootLogger
|
||||
.getChild("[useMicrophoneLevel]")
|
||||
.warn("Could not open microphone for level metering", e);
|
||||
|
||||
Reference in New Issue
Block a user