mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix: detect muted Google Meet microphone
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Google Meet: use the local call-control microphone button instead of disabled remote participant mute buttons, and block realtime speech when the OpenClaw Meet microphone remains muted.
|
||||
- Google Meet: refresh realtime browser state during status and retry delayed speech after Meet finishes joining, so a just-opened in-call tab no longer leaves speech stuck behind stale `not-in-call` health.
|
||||
- Plugins/install: recover the install ledger from the managed npm root when `plugins/installs.json` is empty or partial, so reinstalling Discord and Codex no longer makes the other installed plugin disappear.
|
||||
- Google Meet: grant Meet media permissions through the Playwright browser context when CDP grants do not affect the attached Chrome page, and report in-call microphone/speaker permission problems instead of marking realtime speech ready.
|
||||
|
||||
@@ -2176,6 +2176,102 @@ describe("google-meet plugin", () => {
|
||||
expect(result.manualActionMessage).toContain("Allow microphone/camera/speaker permissions");
|
||||
});
|
||||
|
||||
it("uses the local Meet microphone control instead of remote participant mute buttons", () => {
|
||||
const makeButton = (label: string, disabled = false) => ({
|
||||
disabled,
|
||||
innerText: "",
|
||||
textContent: "",
|
||||
click: vi.fn(),
|
||||
getAttribute: vi.fn((name: string) => (name === "aria-label" ? label : null)),
|
||||
});
|
||||
const remoteMute = makeButton("You can't remotely mute Peter Steinberger's microphone", true);
|
||||
const localMic = makeButton("Turn on microphone");
|
||||
const document = {
|
||||
body: { innerText: "", textContent: "" },
|
||||
title: "Meet",
|
||||
querySelector: vi.fn(() => null),
|
||||
querySelectorAll: vi.fn((selector: string) => {
|
||||
if (selector === "button") {
|
||||
return [makeButton("Leave call"), remoteMute, localMic];
|
||||
}
|
||||
if (selector === "input") {
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
}),
|
||||
};
|
||||
const context = createContext({
|
||||
JSON,
|
||||
document,
|
||||
location: {
|
||||
href: "https://meet.google.com/abc-defg-hij",
|
||||
hostname: "meet.google.com",
|
||||
},
|
||||
window: {},
|
||||
});
|
||||
const inspect = new Script(
|
||||
`(${chromeTransportTesting.meetStatusScriptForTest({
|
||||
allowMicrophone: true,
|
||||
autoJoin: false,
|
||||
captureCaptions: false,
|
||||
guestName: "OpenClaw Agent",
|
||||
})})`,
|
||||
).runInContext(context) as () => string;
|
||||
|
||||
const result = JSON.parse(inspect()) as { micMuted?: boolean; notes?: string[] };
|
||||
|
||||
expect(result.micMuted).toBe(true);
|
||||
expect(localMic.click).toHaveBeenCalledTimes(1);
|
||||
expect(remoteMute.click).not.toHaveBeenCalled();
|
||||
expect(result.notes).toContain("Attempted to turn on the Meet microphone for realtime mode.");
|
||||
});
|
||||
|
||||
it("blocks realtime speech while the Meet microphone remains muted", async () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, "platform", { value: "darwin" });
|
||||
try {
|
||||
mockLocalMeetBrowserRequest({
|
||||
inCall: true,
|
||||
micMuted: true,
|
||||
title: "Meet call",
|
||||
url: "https://meet.google.com/abc-defg-hij",
|
||||
});
|
||||
const { methods } = setup({
|
||||
chrome: {
|
||||
audioBridgeCommand: ["bridge", "start"],
|
||||
waitForInCallMs: 1,
|
||||
},
|
||||
});
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} 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(
|
||||
{
|
||||
|
||||
@@ -144,6 +144,13 @@ function evaluateSpeechReadiness(session: GoogleMeetSession): {
|
||||
};
|
||||
}
|
||||
if (health?.inCall === true) {
|
||||
if (health.micMuted === true) {
|
||||
return {
|
||||
ready: false,
|
||||
reason: "meet-microphone-muted",
|
||||
message: "Turn on the OpenClaw Google Meet microphone before asking OpenClaw to speak.",
|
||||
};
|
||||
}
|
||||
if (session.chrome.audioBridge) {
|
||||
return { ready: true };
|
||||
}
|
||||
|
||||
@@ -350,6 +350,11 @@ function meetStatusScript(params: {
|
||||
const label = buttonLabel(button);
|
||||
return pattern.test(label) && !button.disabled;
|
||||
});
|
||||
const findCallControlButton = (pattern) =>
|
||||
buttons.find((button) => {
|
||||
const label = buttonLabel(button);
|
||||
return pattern.test(label) && !/remotely mute|someone else/i.test(label) && !button.disabled;
|
||||
});
|
||||
const input = [...document.querySelectorAll('input')].find((el) =>
|
||||
/your name/i.test(el.getAttribute('aria-label') || el.placeholder || '')
|
||||
);
|
||||
@@ -364,7 +369,17 @@ function meetStatusScript(params: {
|
||||
const host = location.hostname.toLowerCase();
|
||||
const pageUrl = location.href;
|
||||
const permissionNeeded = /permission needed|microphone problem|speaker problem|allow.*(microphone|camera)|blocked.*(microphone|camera)|permission.*(microphone|camera|speaker)/i.test(permissionText);
|
||||
const mic = buttons.find((button) => /turn off microphone|turn on microphone|microphone/i.test(button.getAttribute('aria-label') || text(button)));
|
||||
let mic = findCallControlButton(/^\\s*turn (?:off|on) microphone\\b/i);
|
||||
if (!mic) {
|
||||
const callControls = document.querySelector('[role="region"][aria-label="Call controls"]');
|
||||
mic = [...(callControls?.querySelectorAll('button') || [])].find((button) =>
|
||||
/^\\s*turn (?:off|on) microphone\\b/i.test(buttonLabel(button))
|
||||
);
|
||||
}
|
||||
if (!readOnly && allowMicrophone && mic && /turn on microphone/i.test(buttonLabel(mic))) {
|
||||
mic.click();
|
||||
notes.push("Attempted to turn on the Meet microphone for realtime mode.");
|
||||
}
|
||||
if (!readOnly && !allowMicrophone && mic && /turn off microphone/i.test(mic.getAttribute('aria-label') || text(mic))) {
|
||||
mic.click();
|
||||
notes.push("Muted Meet microphone for observe-only mode.");
|
||||
@@ -495,7 +510,7 @@ function meetStatusScript(params: {
|
||||
clickedJoin: Boolean(join),
|
||||
clickedMicrophoneChoice: Boolean(allowMicrophone && microphoneChoice),
|
||||
inCall,
|
||||
micMuted: mic ? /turn on microphone/i.test(mic.getAttribute('aria-label') || text(mic)) : undefined,
|
||||
micMuted: mic ? /turn on microphone/i.test(buttonLabel(mic)) : undefined,
|
||||
lobbyWaiting,
|
||||
leaveReason,
|
||||
captioning,
|
||||
@@ -623,7 +638,7 @@ async function openMeetWithBrowserRequest(params: {
|
||||
timeoutMs: Math.min(timeoutMs, 10_000),
|
||||
});
|
||||
browser = mergeBrowserNotes(parseMeetBrowserStatus(evaluated) ?? browser, permissionNotes);
|
||||
if (browser?.inCall === true) {
|
||||
if (browser?.inCall === true && (params.mode !== "realtime" || browser.micMuted !== true)) {
|
||||
return { launched: true, browser };
|
||||
}
|
||||
if (browser?.manualActionRequired === true) {
|
||||
|
||||
@@ -24,7 +24,8 @@ type GoogleMeetSpeechBlockedReason =
|
||||
| GoogleMeetManualActionReason
|
||||
| "not-in-call"
|
||||
| "browser-unverified"
|
||||
| "audio-bridge-unavailable";
|
||||
| "audio-bridge-unavailable"
|
||||
| "meet-microphone-muted";
|
||||
|
||||
export type GoogleMeetChromeHealth = {
|
||||
inCall?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user