diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dae9149138..34261c6238b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/google-meet/index.test.ts b/extensions/google-meet/index.test.ts index d80a5ddfa46..de31f55d92e 100644 --- a/extensions/google-meet/index.test.ts +++ b/extensions/google-meet/index.test.ts @@ -118,7 +118,7 @@ function requestUrl(input: RequestInfo | URL): URL { } function mockLocalMeetBrowserRequest( - browserActResult: Record = { + browserActResult: Record | (() => Record) = { 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; + respond: ReturnType; + }) => Promise) + | 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( { diff --git a/extensions/google-meet/src/runtime.ts b/extensions/google-meet/src/runtime.ts index 023599f1562..f985f3e7198 100644 --- a/extensions/google-meet/src/runtime.ts +++ b/extensions/google-meet/src/runtime.ts @@ -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; } }