fix: wait for meet microphone readiness

This commit is contained in:
Peter Steinberger
2026-05-04 05:39:20 +01:00
parent 65f2c2a0db
commit 796d4ab43d
3 changed files with 64 additions and 3 deletions

View File

@@ -66,6 +66,7 @@ Docs: https://docs.openclaw.ai
- Google Meet: stop advertising legacy `mode: "realtime"` to agents and config UIs, while keeping it as a hidden compatibility alias for `mode: "agent"`, so new joins use the STT -> OpenClaw agent -> TTS path instead of selecting the direct realtime voice fallback.
- Google Meet: add `chrome.audioBufferBytes` for generated command-pair SoX audio commands and lower the default buffer from SoX's 8192 bytes to 4096 bytes to reduce Chrome talk-back latency.
- Google Meet: split realtime provider config into agent-mode transcription and bidi-mode voice providers, and migrate legacy Gemini Live bidi configs with `doctor --fix`, so Gemini Live can back direct bidi fallback without breaking the default OpenClaw agent talk-back path.
- Google Meet: keep waiting for the Meet microphone to unmute during join intro readiness instead of permanently skipping talk-back when Meet briefly reports the local mic as muted.
- Google Meet: expose `voiceCall.postDtmfSpeechDelayMs` in the plugin manifest schema and setup hints, so manifest-based config editing accepts the runtime-supported Twilio delay key. Thanks @vincentkoc.
- Google Meet: keep explicit non-Google `realtime.provider` values as the transcription provider compatibility fallback when `realtime.transcriptionProvider` is unset. Thanks @vincentkoc.
- Google Meet: make Twilio setup status require an enabled `voice-call` plugin entry instead of treating a missing entry as ready. Thanks @vincentkoc.

View File

@@ -118,7 +118,7 @@ function requestUrl(input: RequestInfo | URL): URL {
}
function mockLocalMeetBrowserRequest(
browserActResult: Record<string, unknown> = {
browserActResult: Record<string, unknown> | (() => Record<string, unknown>) = {
inCall: true,
micMuted: false,
title: "Meet call",
@@ -158,7 +158,11 @@ function mockLocalMeetBrowserRequest(
};
}
if (request.path === "/act") {
return { result: JSON.stringify(browserActResult) };
return {
result: JSON.stringify(
typeof browserActResult === "function" ? browserActResult() : browserActResult,
),
};
}
throw new Error(`unexpected browser request path ${request.path}`);
},
@@ -2766,6 +2770,57 @@ describe("google-meet plugin", () => {
}
});
it("keeps waiting while the Meet microphone is muted during intro readiness", async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", { value: "darwin" });
try {
let inspectCount = 0;
mockLocalMeetBrowserRequest(() => {
inspectCount += 1;
return {
inCall: true,
micMuted: true,
title: "Meet call",
url: "https://meet.google.com/abc-defg-hij",
};
});
const { methods } = setup({
chrome: {
audioBridgeCommand: ["bridge", "start"],
waitForInCallMs: 1000,
},
});
const handler = methods.get("googlemeet.join") as
| ((ctx: {
params: Record<string, unknown>;
respond: ReturnType<typeof vi.fn>;
}) => Promise<void>)
| undefined;
const respond = vi.fn();
await handler?.({
params: { url: "https://meet.google.com/abc-defg-hij" },
respond,
});
expect(respond.mock.calls[0]?.[1]).toMatchObject({
spoken: false,
session: {
chrome: {
health: {
micMuted: true,
speechReady: false,
speechBlockedReason: "meet-microphone-muted",
},
},
},
});
expect(inspectCount).toBeGreaterThanOrEqual(2);
} finally {
Object.defineProperty(process, "platform", { value: originalPlatform });
}
});
it("joins Chrome on a paired node without local Chrome or BlackHole", async () => {
const { methods, nodesList, nodesInvoke } = setup(
{

View File

@@ -604,7 +604,12 @@ export class GoogleMeetRuntime {
return false;
}
const blocked = health?.speechBlockedReason;
if (blocked && blocked !== "not-in-call" && blocked !== "browser-unverified") {
if (
blocked &&
blocked !== "not-in-call" &&
blocked !== "browser-unverified" &&
blocked !== "meet-microphone-muted"
) {
return false;
}
}