mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 05:51:39 +00:00
fix(talk): handle reentrant Google activation cancellation
This commit is contained in:
@@ -32,7 +32,7 @@ export function buildGoogleLiveUrl(session: RealtimeTalkJsonPcmWebSocketSessionR
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export type GoogleLiveConnectionState =
|
||||
type GoogleLiveConnectionState =
|
||||
| "idle"
|
||||
| "connecting"
|
||||
| "ready"
|
||||
|
||||
@@ -67,6 +67,15 @@ class FakeAudioContext {
|
||||
createGain() {
|
||||
return { connect() {}, disconnect() {}, gain: { value: 1 } };
|
||||
}
|
||||
|
||||
createAnalyser() {
|
||||
return {
|
||||
fftSize: 0,
|
||||
smoothingTimeConstant: 0,
|
||||
disconnect() {},
|
||||
getFloatTimeDomainData: (samples: Float32Array) => samples.fill(0.25),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createSession(): RealtimeTalkJsonPcmWebSocketSessionResult {
|
||||
@@ -247,6 +256,23 @@ describe("Google Live setup timeout", () => {
|
||||
expect(audioContexts.every((context) => context.close.mock.calls.length === 1)).toBe(true);
|
||||
});
|
||||
|
||||
it("reclaims the meter when an input-level callback cancels activation", async () => {
|
||||
let stopDuringActivation = () => undefined;
|
||||
const onInputLevel = vi.fn(() => stopDuringActivation());
|
||||
const transport = createTransport({ onInputLevel });
|
||||
stopDuringActivation = () => transport.stop({ emitClosed: false });
|
||||
const { start, socket } = await beginTransport(transport);
|
||||
socket.emitOpen();
|
||||
socket.emitMessage({ setupComplete: {} });
|
||||
await expect(start).resolves.toBe("ready");
|
||||
|
||||
expect(() => transport.activate()).toThrow("Google Live transport activation cancelled");
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
expect(stopInputTrack).toHaveBeenCalledOnce();
|
||||
expect(socket.readyState).toBe(3);
|
||||
expect(audioContexts.every((context) => context.close.mock.calls.length === 1)).toBe(true);
|
||||
});
|
||||
|
||||
it("clears the deadline after Google setup completes", async () => {
|
||||
const onStatus = vi.fn();
|
||||
const transport = createTransport({ onStatus });
|
||||
|
||||
@@ -209,28 +209,26 @@ export class GoogleLiveRealtimeTalkTransport implements RealtimeTalkTransport {
|
||||
}
|
||||
try {
|
||||
this.ctx.callbacks.onStatus?.("listening");
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.assertActivationCurrent();
|
||||
this.emitTalkEvent({ type: "session.ready" });
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.assertActivationCurrent();
|
||||
if (this.ctx.callbacks.onInputLevel && this.media && this.inputContext) {
|
||||
this.inputMeter = new RealtimeTalkMediaStreamMeter(this.ctx.callbacks.onInputLevel);
|
||||
this.inputMeter.start(this.media, this.inputContext);
|
||||
}
|
||||
if (this.closed) {
|
||||
return;
|
||||
const inputMeter = new RealtimeTalkMediaStreamMeter(this.ctx.callbacks.onInputLevel);
|
||||
this.inputMeter = inputMeter;
|
||||
inputMeter.start(this.media, this.inputContext);
|
||||
if (this.closed || !this.lifecycle.isActive || this.inputMeter !== inputMeter) {
|
||||
// start() publishes synchronously before installing its interval. A
|
||||
// reentrant stop must reclaim the interval that start() installs next.
|
||||
inputMeter.stop(false);
|
||||
}
|
||||
this.assertActivationCurrent();
|
||||
}
|
||||
this.startMicrophonePump();
|
||||
if (this.camera.stream && !this.cameraPublished) {
|
||||
this.cameraPublished = true;
|
||||
this.ctx.callbacks.onVideoStream?.(this.camera.stream);
|
||||
}
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.assertActivationCurrent();
|
||||
this.startVideoFrames();
|
||||
} catch (error) {
|
||||
try {
|
||||
@@ -242,6 +240,12 @@ export class GoogleLiveRealtimeTalkTransport implements RealtimeTalkTransport {
|
||||
}
|
||||
}
|
||||
|
||||
private assertActivationCurrent(): void {
|
||||
if (this.closed || !this.lifecycle.isActive) {
|
||||
throw new Error("Google Live transport activation cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
async setVideoEnabled(enabled: boolean): Promise<void> {
|
||||
await this.camera.setEnabled(enabled);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user