mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 05:51:39 +00:00
fix(talk): cancel pending media acquisition
This commit is contained in:
@@ -123,6 +123,25 @@ describe("realtime Talk microphone inputs", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("settles microphone cancellation before browser permission resolves", async () => {
|
||||
const stop = vi.fn();
|
||||
let resolveMedia: (stream: MediaStream) => void = () => undefined;
|
||||
const pending = new Promise<MediaStream>((resolve) => {
|
||||
resolveMedia = resolve;
|
||||
});
|
||||
vi.stubGlobal("navigator", {
|
||||
mediaDevices: { getUserMedia: vi.fn(() => pending) },
|
||||
});
|
||||
const controller = new AbortController();
|
||||
|
||||
const opening = openRealtimeTalkInput(undefined, { signal: controller.signal });
|
||||
controller.abort();
|
||||
|
||||
await expect(opening).rejects.toMatchObject({ name: "AbortError" });
|
||||
resolveMedia({ getTracks: () => [{ stop }] } as unknown as MediaStream);
|
||||
await vi.waitFor(() => expect(stop).toHaveBeenCalledOnce());
|
||||
});
|
||||
|
||||
it("acquires camera separately so camera errors cannot stop microphone input", async () => {
|
||||
const audio = { getTracks: () => [] } as unknown as MediaStream;
|
||||
const camera = { getTracks: () => [] } as unknown as MediaStream;
|
||||
@@ -172,10 +191,10 @@ describe("realtime Talk microphone inputs", () => {
|
||||
const opening = openRealtimeTalkCamera(undefined, { signal: controller.signal });
|
||||
await vi.waitFor(() => expect(getUserMedia).toHaveBeenCalledOnce());
|
||||
controller.abort();
|
||||
resolveCamera(camera);
|
||||
|
||||
await expect(opening).rejects.toMatchObject({ name: "AbortError" });
|
||||
expect(videoStop).toHaveBeenCalledOnce();
|
||||
resolveCamera(camera);
|
||||
await vi.waitFor(() => expect(videoStop).toHaveBeenCalledOnce());
|
||||
});
|
||||
|
||||
it("enables voice processing with the system default microphone", async () => {
|
||||
|
||||
@@ -167,6 +167,44 @@ function realtimeTalkAbortReason(signal: AbortSignal): Error {
|
||||
: new DOMException("Realtime Talk input cancelled", "AbortError");
|
||||
}
|
||||
|
||||
async function awaitRealtimeTalkMediaRequest(
|
||||
request: Promise<MediaStream>,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<MediaStream> {
|
||||
if (!signal) {
|
||||
return await request;
|
||||
}
|
||||
if (signal.aborted) {
|
||||
void request.then(
|
||||
(stream) => stream.getTracks().forEach((track) => track.stop()),
|
||||
() => undefined,
|
||||
);
|
||||
throw realtimeTalkAbortReason(signal);
|
||||
}
|
||||
let removeAbortListener = () => undefined;
|
||||
const aborted = new Promise<never>((_resolve, reject) => {
|
||||
const onAbort = () => reject(realtimeTalkAbortReason(signal));
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
||||
});
|
||||
try {
|
||||
return await Promise.race([request, aborted]);
|
||||
} catch (error) {
|
||||
if (signal.aborted) {
|
||||
// Browser permission prompts are not cancellable. Release any stream that
|
||||
// arrives after the lifecycle owner has already moved on.
|
||||
void request.then(
|
||||
(stream) => stream.getTracks().forEach((track) => track.stop()),
|
||||
() => undefined,
|
||||
);
|
||||
throw realtimeTalkAbortReason(signal);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
removeAbortListener();
|
||||
}
|
||||
}
|
||||
|
||||
export async function openRealtimeTalkInput(
|
||||
inputDeviceId: string | undefined,
|
||||
options: { signal?: AbortSignal } = {},
|
||||
@@ -177,9 +215,12 @@ export async function openRealtimeTalkInput(
|
||||
}
|
||||
let audio: MediaStream;
|
||||
try {
|
||||
audio = await devices.getUserMedia({
|
||||
audio: realtimeTalkAudioConstraints(inputDeviceId),
|
||||
});
|
||||
audio = await awaitRealtimeTalkMediaRequest(
|
||||
devices.getUserMedia({
|
||||
audio: realtimeTalkAudioConstraints(inputDeviceId),
|
||||
}),
|
||||
options.signal,
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
inputDeviceId?.trim() &&
|
||||
@@ -208,9 +249,12 @@ export async function openRealtimeTalkCamera(
|
||||
const deviceId = videoDeviceId?.trim();
|
||||
let camera: MediaStream;
|
||||
try {
|
||||
camera = await devices.getUserMedia({
|
||||
video: deviceId ? { deviceId: { exact: deviceId } } : true,
|
||||
});
|
||||
camera = await awaitRealtimeTalkMediaRequest(
|
||||
devices.getUserMedia({
|
||||
video: deviceId ? { deviceId: { exact: deviceId } } : true,
|
||||
}),
|
||||
options.signal,
|
||||
);
|
||||
if (options.signal?.aborted) {
|
||||
camera.getTracks().forEach((track) => track.stop());
|
||||
throw realtimeTalkAbortReason(options.signal);
|
||||
|
||||
Reference in New Issue
Block a user