@openclaw/google-meet 2026.5.2 → 2026.5.3-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/calendar-6EQiwLUb.js +126 -0
- package/dist/chrome-create-B0wV2zaj.js +963 -0
- package/dist/cli-CSyT9_N6.js +1377 -0
- package/dist/create-0ye_2zVk.js +106 -0
- package/dist/index.js +3509 -0
- package/dist/oauth-BJwzuzT-.js +141 -0
- package/package.json +13 -6
- package/google-meet.live.test.ts +0 -85
- package/index.create.test.ts +0 -595
- package/index.test.ts +0 -3616
- package/index.ts +0 -1170
- package/node-host.test.ts +0 -222
- package/src/agent-consult.ts +0 -70
- package/src/calendar.ts +0 -243
- package/src/cli.test.ts +0 -1013
- package/src/cli.ts +0 -2136
- package/src/config.ts +0 -509
- package/src/create.ts +0 -153
- package/src/drive.ts +0 -72
- package/src/google-api-errors.ts +0 -20
- package/src/meet.ts +0 -1024
- package/src/node-host.ts +0 -498
- package/src/oauth.test.ts +0 -71
- package/src/oauth.ts +0 -229
- package/src/realtime-node.ts +0 -271
- package/src/realtime.ts +0 -423
- package/src/runtime.ts +0 -804
- package/src/setup.ts +0 -276
- package/src/test-support/plugin-harness.ts +0 -225
- package/src/transports/chrome-browser-proxy.ts +0 -198
- package/src/transports/chrome-create.ts +0 -367
- package/src/transports/chrome.ts +0 -925
- package/src/transports/twilio.ts +0 -46
- package/src/transports/types.ts +0 -113
- package/src/voice-call-gateway.test.ts +0 -79
- package/src/voice-call-gateway.ts +0 -213
- package/tsconfig.json +0 -16
package/src/transports/chrome.ts
DELETED
|
@@ -1,925 +0,0 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
|
2
|
-
import { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime";
|
|
3
|
-
import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime";
|
|
4
|
-
import type { RuntimeLogger } from "openclaw/plugin-sdk/plugin-runtime";
|
|
5
|
-
import type { GoogleMeetConfig } from "../config.js";
|
|
6
|
-
import {
|
|
7
|
-
startNodeRealtimeAudioBridge,
|
|
8
|
-
type ChromeNodeRealtimeAudioBridgeHandle,
|
|
9
|
-
} from "../realtime-node.js";
|
|
10
|
-
import {
|
|
11
|
-
startCommandRealtimeAudioBridge,
|
|
12
|
-
type ChromeRealtimeAudioBridgeHandle,
|
|
13
|
-
} from "../realtime.js";
|
|
14
|
-
import {
|
|
15
|
-
asBrowserTabs,
|
|
16
|
-
callBrowserProxyOnNode,
|
|
17
|
-
isSameMeetUrlForReuse,
|
|
18
|
-
normalizeMeetUrlForReuse,
|
|
19
|
-
readBrowserTab,
|
|
20
|
-
resolveChromeNode,
|
|
21
|
-
type BrowserTab,
|
|
22
|
-
} from "./chrome-browser-proxy.js";
|
|
23
|
-
import type { GoogleMeetChromeHealth } from "./types.js";
|
|
24
|
-
|
|
25
|
-
export const GOOGLE_MEET_SYSTEM_PROFILER_COMMAND = "/usr/sbin/system_profiler";
|
|
26
|
-
|
|
27
|
-
type BrowserRequestParams = {
|
|
28
|
-
method: "GET" | "POST" | "DELETE";
|
|
29
|
-
path: string;
|
|
30
|
-
body?: unknown;
|
|
31
|
-
timeoutMs: number;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type BrowserRequestCaller = (params: BrowserRequestParams) => Promise<unknown>;
|
|
35
|
-
|
|
36
|
-
const chromeTransportDeps: {
|
|
37
|
-
callGatewayFromCli: typeof callGatewayFromCli;
|
|
38
|
-
} = {
|
|
39
|
-
callGatewayFromCli,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const __testing = {
|
|
43
|
-
setDepsForTest(deps: { callGatewayFromCli?: typeof callGatewayFromCli } | null) {
|
|
44
|
-
chromeTransportDeps.callGatewayFromCli = deps?.callGatewayFromCli ?? callGatewayFromCli;
|
|
45
|
-
},
|
|
46
|
-
meetStatusScriptForTest: meetStatusScript,
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export function outputMentionsBlackHole2ch(output: string): boolean {
|
|
50
|
-
return /\bBlackHole\s+2ch\b/i.test(output);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function assertBlackHole2chAvailable(params: {
|
|
54
|
-
runtime: PluginRuntime;
|
|
55
|
-
timeoutMs: number;
|
|
56
|
-
}): Promise<void> {
|
|
57
|
-
if (process.platform !== "darwin") {
|
|
58
|
-
throw new Error("Chrome Meet transport with blackhole-2ch audio is currently macOS-only");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const result = await params.runtime.system.runCommandWithTimeout(
|
|
62
|
-
[GOOGLE_MEET_SYSTEM_PROFILER_COMMAND, "SPAudioDataType"],
|
|
63
|
-
{ timeoutMs: params.timeoutMs },
|
|
64
|
-
);
|
|
65
|
-
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
66
|
-
if (result.code !== 0 || !outputMentionsBlackHole2ch(output)) {
|
|
67
|
-
const hint =
|
|
68
|
-
params.runtime.system.formatNativeDependencyHint?.({
|
|
69
|
-
packageName: "BlackHole 2ch",
|
|
70
|
-
downloadCommand: "brew install blackhole-2ch",
|
|
71
|
-
}) ?? "";
|
|
72
|
-
throw new Error(
|
|
73
|
-
[
|
|
74
|
-
"BlackHole 2ch audio device not found.",
|
|
75
|
-
"Install BlackHole 2ch and route Chrome input/output through the OpenClaw audio bridge.",
|
|
76
|
-
hint,
|
|
77
|
-
]
|
|
78
|
-
.filter(Boolean)
|
|
79
|
-
.join(" "),
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function launchChromeMeet(params: {
|
|
85
|
-
runtime: PluginRuntime;
|
|
86
|
-
config: GoogleMeetConfig;
|
|
87
|
-
fullConfig: OpenClawConfig;
|
|
88
|
-
meetingSessionId: string;
|
|
89
|
-
mode: "realtime" | "transcribe";
|
|
90
|
-
url: string;
|
|
91
|
-
logger: RuntimeLogger;
|
|
92
|
-
}): Promise<{
|
|
93
|
-
launched: boolean;
|
|
94
|
-
audioBridge?:
|
|
95
|
-
| { type: "external-command" }
|
|
96
|
-
| ({ type: "command-pair" } & ChromeRealtimeAudioBridgeHandle);
|
|
97
|
-
browser?: GoogleMeetChromeHealth;
|
|
98
|
-
}> {
|
|
99
|
-
let audioBridge:
|
|
100
|
-
| { type: "external-command" }
|
|
101
|
-
| ({ type: "command-pair" } & ChromeRealtimeAudioBridgeHandle)
|
|
102
|
-
| undefined;
|
|
103
|
-
|
|
104
|
-
if (params.mode === "realtime") {
|
|
105
|
-
await assertBlackHole2chAvailable({
|
|
106
|
-
runtime: params.runtime,
|
|
107
|
-
timeoutMs: Math.min(params.config.chrome.joinTimeoutMs, 10_000),
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
if (params.config.chrome.audioBridgeHealthCommand) {
|
|
111
|
-
const health = await params.runtime.system.runCommandWithTimeout(
|
|
112
|
-
params.config.chrome.audioBridgeHealthCommand,
|
|
113
|
-
{ timeoutMs: params.config.chrome.joinTimeoutMs },
|
|
114
|
-
);
|
|
115
|
-
if (health.code !== 0) {
|
|
116
|
-
throw new Error(
|
|
117
|
-
`Chrome audio bridge health check failed: ${health.stderr || health.stdout || health.code}`,
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (params.config.chrome.audioBridgeCommand) {
|
|
123
|
-
const bridge = await params.runtime.system.runCommandWithTimeout(
|
|
124
|
-
params.config.chrome.audioBridgeCommand,
|
|
125
|
-
{ timeoutMs: params.config.chrome.joinTimeoutMs },
|
|
126
|
-
);
|
|
127
|
-
if (bridge.code !== 0) {
|
|
128
|
-
throw new Error(
|
|
129
|
-
`failed to start Chrome audio bridge: ${bridge.stderr || bridge.stdout || bridge.code}`,
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
audioBridge = { type: "external-command" };
|
|
133
|
-
} else {
|
|
134
|
-
if (!params.config.chrome.audioInputCommand || !params.config.chrome.audioOutputCommand) {
|
|
135
|
-
throw new Error(
|
|
136
|
-
"Chrome realtime mode requires chrome.audioInputCommand and chrome.audioOutputCommand, or chrome.audioBridgeCommand for an external bridge.",
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
audioBridge = {
|
|
140
|
-
type: "command-pair",
|
|
141
|
-
...(await startCommandRealtimeAudioBridge({
|
|
142
|
-
config: params.config,
|
|
143
|
-
fullConfig: params.fullConfig,
|
|
144
|
-
runtime: params.runtime,
|
|
145
|
-
meetingSessionId: params.meetingSessionId,
|
|
146
|
-
inputCommand: params.config.chrome.audioInputCommand,
|
|
147
|
-
outputCommand: params.config.chrome.audioOutputCommand,
|
|
148
|
-
logger: params.logger,
|
|
149
|
-
})),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (!params.config.chrome.launch) {
|
|
155
|
-
return { launched: false, audioBridge };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
let commandPairBridgeStopped = false;
|
|
159
|
-
const stopCommandPairBridge = async () => {
|
|
160
|
-
if (commandPairBridgeStopped) {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
commandPairBridgeStopped = true;
|
|
164
|
-
if (audioBridge?.type === "command-pair") {
|
|
165
|
-
await audioBridge.stop();
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
const result = await openMeetWithBrowserRequest({
|
|
171
|
-
callBrowser: callLocalBrowserRequest,
|
|
172
|
-
config: params.config,
|
|
173
|
-
mode: params.mode,
|
|
174
|
-
url: params.url,
|
|
175
|
-
});
|
|
176
|
-
return { ...result, audioBridge };
|
|
177
|
-
} catch (error) {
|
|
178
|
-
await stopCommandPairBridge();
|
|
179
|
-
throw error;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function parseNodeStartResult(raw: unknown): {
|
|
184
|
-
launched?: boolean;
|
|
185
|
-
bridgeId?: string;
|
|
186
|
-
audioBridge?: { type?: string };
|
|
187
|
-
browser?: GoogleMeetChromeHealth;
|
|
188
|
-
} {
|
|
189
|
-
const value =
|
|
190
|
-
raw && typeof raw === "object" && "payload" in raw
|
|
191
|
-
? (raw as { payload?: unknown }).payload
|
|
192
|
-
: raw;
|
|
193
|
-
if (!value || typeof value !== "object") {
|
|
194
|
-
throw new Error("Google Meet node returned an invalid start result.");
|
|
195
|
-
}
|
|
196
|
-
return value as {
|
|
197
|
-
launched?: boolean;
|
|
198
|
-
bridgeId?: string;
|
|
199
|
-
audioBridge?: { type?: string };
|
|
200
|
-
browser?: GoogleMeetChromeHealth;
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function parseMeetBrowserStatus(result: unknown): GoogleMeetChromeHealth | undefined {
|
|
205
|
-
const record = result && typeof result === "object" ? (result as Record<string, unknown>) : {};
|
|
206
|
-
const raw = record.result;
|
|
207
|
-
if (typeof raw !== "string" || !raw.trim()) {
|
|
208
|
-
return undefined;
|
|
209
|
-
}
|
|
210
|
-
const parsed = JSON.parse(raw) as {
|
|
211
|
-
inCall?: boolean;
|
|
212
|
-
micMuted?: boolean;
|
|
213
|
-
lobbyWaiting?: boolean;
|
|
214
|
-
leaveReason?: string;
|
|
215
|
-
captioning?: boolean;
|
|
216
|
-
captionsEnabledAttempted?: boolean;
|
|
217
|
-
transcriptLines?: number;
|
|
218
|
-
lastCaptionAt?: string;
|
|
219
|
-
lastCaptionSpeaker?: string;
|
|
220
|
-
lastCaptionText?: string;
|
|
221
|
-
recentTranscript?: GoogleMeetChromeHealth["recentTranscript"];
|
|
222
|
-
manualActionRequired?: boolean;
|
|
223
|
-
manualActionReason?: GoogleMeetChromeHealth["manualActionReason"];
|
|
224
|
-
manualActionMessage?: string;
|
|
225
|
-
url?: string;
|
|
226
|
-
title?: string;
|
|
227
|
-
notes?: string[];
|
|
228
|
-
};
|
|
229
|
-
return {
|
|
230
|
-
inCall: parsed.inCall,
|
|
231
|
-
micMuted: parsed.micMuted,
|
|
232
|
-
lobbyWaiting: parsed.lobbyWaiting,
|
|
233
|
-
leaveReason: parsed.leaveReason,
|
|
234
|
-
captioning: parsed.captioning,
|
|
235
|
-
captionsEnabledAttempted: parsed.captionsEnabledAttempted,
|
|
236
|
-
transcriptLines: parsed.transcriptLines,
|
|
237
|
-
lastCaptionAt: parsed.lastCaptionAt,
|
|
238
|
-
lastCaptionSpeaker: parsed.lastCaptionSpeaker,
|
|
239
|
-
lastCaptionText: parsed.lastCaptionText,
|
|
240
|
-
recentTranscript: parsed.recentTranscript,
|
|
241
|
-
manualActionRequired: parsed.manualActionRequired,
|
|
242
|
-
manualActionReason: parsed.manualActionReason,
|
|
243
|
-
manualActionMessage: parsed.manualActionMessage,
|
|
244
|
-
browserUrl: parsed.url,
|
|
245
|
-
browserTitle: parsed.title,
|
|
246
|
-
status: "browser-control",
|
|
247
|
-
notes: Array.isArray(parsed.notes)
|
|
248
|
-
? parsed.notes.filter((note): note is string => typeof note === "string")
|
|
249
|
-
: undefined,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async function callLocalBrowserRequest(params: BrowserRequestParams) {
|
|
254
|
-
return await chromeTransportDeps.callGatewayFromCli(
|
|
255
|
-
"browser.request",
|
|
256
|
-
{
|
|
257
|
-
json: true,
|
|
258
|
-
timeout: String(params.timeoutMs + 5_000),
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
method: params.method,
|
|
262
|
-
path: params.path,
|
|
263
|
-
body: params.body,
|
|
264
|
-
timeoutMs: params.timeoutMs,
|
|
265
|
-
},
|
|
266
|
-
{ progress: false },
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function mergeBrowserNotes(
|
|
271
|
-
browser: GoogleMeetChromeHealth | undefined,
|
|
272
|
-
notes: string[],
|
|
273
|
-
): GoogleMeetChromeHealth | undefined {
|
|
274
|
-
if (!browser || notes.length === 0) {
|
|
275
|
-
return browser;
|
|
276
|
-
}
|
|
277
|
-
return {
|
|
278
|
-
...browser,
|
|
279
|
-
notes: [...new Set([...(browser.notes ?? []), ...notes])],
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function parsePermissionGrantNotes(result: unknown): string[] {
|
|
284
|
-
const record = result && typeof result === "object" ? (result as Record<string, unknown>) : {};
|
|
285
|
-
const unsupportedPermissions = Array.isArray(record.unsupportedPermissions)
|
|
286
|
-
? record.unsupportedPermissions.filter((value): value is string => typeof value === "string")
|
|
287
|
-
: [];
|
|
288
|
-
const notes = ["Granted Meet microphone/camera permissions through browser control."];
|
|
289
|
-
if (unsupportedPermissions.includes("speakerSelection")) {
|
|
290
|
-
notes.push("Chrome did not accept the optional Meet speaker-selection permission.");
|
|
291
|
-
}
|
|
292
|
-
return notes;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async function grantMeetMediaPermissions(params: {
|
|
296
|
-
callBrowser: BrowserRequestCaller;
|
|
297
|
-
timeoutMs: number;
|
|
298
|
-
allowMicrophone: boolean;
|
|
299
|
-
}): Promise<string[]> {
|
|
300
|
-
if (!params.allowMicrophone) {
|
|
301
|
-
return ["Observe-only mode skips Meet microphone/camera permission grants."];
|
|
302
|
-
}
|
|
303
|
-
try {
|
|
304
|
-
const result = await params.callBrowser({
|
|
305
|
-
method: "POST",
|
|
306
|
-
path: "/permissions/grant",
|
|
307
|
-
body: {
|
|
308
|
-
origin: "https://meet.google.com",
|
|
309
|
-
permissions: ["audioCapture", "videoCapture"],
|
|
310
|
-
optionalPermissions: ["speakerSelection"],
|
|
311
|
-
timeoutMs: Math.min(params.timeoutMs, 5_000),
|
|
312
|
-
},
|
|
313
|
-
timeoutMs: Math.min(params.timeoutMs, 5_000),
|
|
314
|
-
});
|
|
315
|
-
return parsePermissionGrantNotes(result);
|
|
316
|
-
} catch (error) {
|
|
317
|
-
return [
|
|
318
|
-
`Could not grant Meet media permissions automatically: ${
|
|
319
|
-
error instanceof Error ? error.message : String(error)
|
|
320
|
-
}`,
|
|
321
|
-
];
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function meetStatusScript(params: {
|
|
326
|
-
allowMicrophone: boolean;
|
|
327
|
-
autoJoin: boolean;
|
|
328
|
-
captureCaptions: boolean;
|
|
329
|
-
guestName: string;
|
|
330
|
-
}) {
|
|
331
|
-
return `() => {
|
|
332
|
-
const text = (node) => (node?.innerText || node?.textContent || "").trim();
|
|
333
|
-
const allowMicrophone = ${JSON.stringify(params.allowMicrophone)};
|
|
334
|
-
const captureCaptions = ${JSON.stringify(params.captureCaptions)};
|
|
335
|
-
const buttons = [...document.querySelectorAll('button')];
|
|
336
|
-
const notes = [];
|
|
337
|
-
const findButton = (pattern) =>
|
|
338
|
-
buttons.find((button) => {
|
|
339
|
-
const label = [
|
|
340
|
-
button.getAttribute("aria-label"),
|
|
341
|
-
button.getAttribute("data-tooltip"),
|
|
342
|
-
text(button),
|
|
343
|
-
]
|
|
344
|
-
.filter(Boolean)
|
|
345
|
-
.join(" ");
|
|
346
|
-
return pattern.test(label) && !button.disabled;
|
|
347
|
-
});
|
|
348
|
-
const input = [...document.querySelectorAll('input')].find((el) =>
|
|
349
|
-
/your name/i.test(el.getAttribute('aria-label') || el.placeholder || '')
|
|
350
|
-
);
|
|
351
|
-
if (${JSON.stringify(params.autoJoin)} && input && !input.value) {
|
|
352
|
-
input.focus();
|
|
353
|
-
input.value = ${JSON.stringify(params.guestName)};
|
|
354
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
355
|
-
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
356
|
-
}
|
|
357
|
-
const pageText = text(document.body).toLowerCase();
|
|
358
|
-
const host = location.hostname.toLowerCase();
|
|
359
|
-
const pageUrl = location.href;
|
|
360
|
-
const permissionNeeded = /permission needed|allow.*(microphone|camera)|blocked.*(microphone|camera)|permission.*(microphone|camera|speaker)/i.test(pageText);
|
|
361
|
-
const mic = buttons.find((button) => /turn off microphone|turn on microphone|microphone/i.test(button.getAttribute('aria-label') || text(button)));
|
|
362
|
-
if (!allowMicrophone && mic && /turn off microphone/i.test(mic.getAttribute('aria-label') || text(mic))) {
|
|
363
|
-
mic.click();
|
|
364
|
-
notes.push("Muted Meet microphone for observe-only mode.");
|
|
365
|
-
}
|
|
366
|
-
const join = ${JSON.stringify(params.autoJoin)}
|
|
367
|
-
? findButton(/join now|ask to join/i)
|
|
368
|
-
: null;
|
|
369
|
-
if (join) join.click();
|
|
370
|
-
const microphoneChoice = findButton(/\\buse microphone\\b/i);
|
|
371
|
-
const noMicrophoneChoice = findButton(/\\b(continue|join|use) without (microphone|mic)\\b|\\bnot now\\b/i);
|
|
372
|
-
if (allowMicrophone && microphoneChoice) {
|
|
373
|
-
microphoneChoice.click();
|
|
374
|
-
notes.push("Accepted Meet microphone prompt with browser automation.");
|
|
375
|
-
} else if (!allowMicrophone && noMicrophoneChoice) {
|
|
376
|
-
noMicrophoneChoice.click();
|
|
377
|
-
notes.push("Skipped Meet microphone prompt for observe-only mode.");
|
|
378
|
-
}
|
|
379
|
-
const inCall = buttons.some((button) => /leave call/i.test(button.getAttribute('aria-label') || text(button)));
|
|
380
|
-
let captioning = false;
|
|
381
|
-
let captionsEnabledAttempted = false;
|
|
382
|
-
let transcriptLines = 0;
|
|
383
|
-
let lastCaptionAt;
|
|
384
|
-
let lastCaptionSpeaker;
|
|
385
|
-
let lastCaptionText;
|
|
386
|
-
let recentTranscript = [];
|
|
387
|
-
const captionSelector = '[role="region"][aria-label*="aption" i], [aria-live="polite"][role="region"], div[aria-live="polite"]';
|
|
388
|
-
const captionState = (() => {
|
|
389
|
-
if (!captureCaptions) return undefined;
|
|
390
|
-
const w = window;
|
|
391
|
-
if (!inCall && !w.__openclawMeetCaptions) return undefined;
|
|
392
|
-
if (!w.__openclawMeetCaptions) {
|
|
393
|
-
w.__openclawMeetCaptions = {
|
|
394
|
-
enabledAttempted: false,
|
|
395
|
-
observerInstalled: false,
|
|
396
|
-
lines: [],
|
|
397
|
-
seen: {}
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
return w.__openclawMeetCaptions;
|
|
401
|
-
})();
|
|
402
|
-
const recordCaption = (speaker, captionText) => {
|
|
403
|
-
if (!captionState) return;
|
|
404
|
-
const clean = String(captionText || "").replace(/\\s+/g, " ").trim();
|
|
405
|
-
const cleanSpeaker = String(speaker || "").replace(/\\s+/g, " ").trim();
|
|
406
|
-
if (!clean || clean.length < 2) return;
|
|
407
|
-
if (/^(turn on captions|turn off captions|captions)$/i.test(clean)) return;
|
|
408
|
-
const key = (cleanSpeaker + "\\n" + clean).toLowerCase();
|
|
409
|
-
if (captionState.seen[key]) return;
|
|
410
|
-
captionState.seen[key] = true;
|
|
411
|
-
const entry = { at: new Date().toISOString(), speaker: cleanSpeaker || undefined, text: clean };
|
|
412
|
-
captionState.lines.push(entry);
|
|
413
|
-
if (captionState.lines.length > 50) captionState.lines.splice(0, captionState.lines.length - 50);
|
|
414
|
-
};
|
|
415
|
-
const scrapeCaptions = () => {
|
|
416
|
-
if (!captionState) return;
|
|
417
|
-
const regions = [...document.querySelectorAll(captionSelector)];
|
|
418
|
-
for (const region of regions) {
|
|
419
|
-
const raw = text(region);
|
|
420
|
-
if (!raw) continue;
|
|
421
|
-
const pieces = raw.split(/\\n+/).map((part) => part.trim()).filter(Boolean);
|
|
422
|
-
if (pieces.length >= 2) {
|
|
423
|
-
recordCaption(pieces[0], pieces.slice(1).join(" "));
|
|
424
|
-
} else {
|
|
425
|
-
recordCaption("", pieces[0] || raw);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
|
-
if (captionState) {
|
|
430
|
-
if (inCall && !captionState.enabledAttempted) {
|
|
431
|
-
const captionButton = findButton(/turn on captions|show captions|captions/i);
|
|
432
|
-
const captionLabel = captionButton ? (captionButton.getAttribute("aria-label") || captionButton.getAttribute("data-tooltip") || text(captionButton)) : "";
|
|
433
|
-
if (captionButton) {
|
|
434
|
-
captionState.enabledAttempted = true;
|
|
435
|
-
captionsEnabledAttempted = true;
|
|
436
|
-
if (!/turn off captions|hide captions/i.test(captionLabel)) {
|
|
437
|
-
captionButton.click();
|
|
438
|
-
notes.push("Attempted to enable Meet captions for observe-only transcript health.");
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
} else if (captionState.enabledAttempted) {
|
|
442
|
-
captionsEnabledAttempted = true;
|
|
443
|
-
}
|
|
444
|
-
if (inCall && !captionState.observerInstalled) {
|
|
445
|
-
captionState.observerInstalled = true;
|
|
446
|
-
new MutationObserver(scrapeCaptions).observe(document.body, {
|
|
447
|
-
childList: true,
|
|
448
|
-
subtree: true,
|
|
449
|
-
characterData: true
|
|
450
|
-
});
|
|
451
|
-
notes.push("Installed Meet caption observer for observe-only transcript health.");
|
|
452
|
-
}
|
|
453
|
-
if (inCall) {
|
|
454
|
-
scrapeCaptions();
|
|
455
|
-
}
|
|
456
|
-
const lines = Array.isArray(captionState.lines) ? captionState.lines : [];
|
|
457
|
-
const last = lines[lines.length - 1];
|
|
458
|
-
captioning = document.querySelector(captionSelector) !== null || lines.length > 0;
|
|
459
|
-
transcriptLines = lines.length;
|
|
460
|
-
lastCaptionAt = last?.at;
|
|
461
|
-
lastCaptionSpeaker = last?.speaker;
|
|
462
|
-
lastCaptionText = last?.text;
|
|
463
|
-
recentTranscript = lines.slice(-5);
|
|
464
|
-
}
|
|
465
|
-
const lobbyWaiting = !inCall && /asking to be let in|you.?ll join when someone lets you in|waiting to be let in|ask to join/i.test(pageText);
|
|
466
|
-
const leaveReason = /you left the meeting|you.?ve left the meeting|removed from the meeting|you were removed|call ended|meeting ended/i.test(pageText)
|
|
467
|
-
? pageText.match(/you left the meeting|you.?ve left the meeting|removed from the meeting|you were removed|call ended|meeting ended/i)?.[0]
|
|
468
|
-
: undefined;
|
|
469
|
-
let manualActionReason;
|
|
470
|
-
let manualActionMessage;
|
|
471
|
-
if (!inCall && (host === "accounts.google.com" || /use your google account|to continue to google meet|choose an account|sign in to (join|continue)/i.test(pageText))) {
|
|
472
|
-
manualActionReason = "google-login-required";
|
|
473
|
-
manualActionMessage = "Sign in to Google in the OpenClaw browser profile, then retry the Meet join.";
|
|
474
|
-
} else if (!inCall && /asking to be let in|you.?ll join when someone lets you in|waiting to be let in|ask to join/i.test(pageText)) {
|
|
475
|
-
manualActionReason = "meet-admission-required";
|
|
476
|
-
manualActionMessage = "Admit the OpenClaw browser participant in Google Meet, then retry speech.";
|
|
477
|
-
} else if (permissionNeeded) {
|
|
478
|
-
manualActionReason = "meet-permission-required";
|
|
479
|
-
manualActionMessage = allowMicrophone
|
|
480
|
-
? "Allow microphone/camera/speaker permissions for Meet in the OpenClaw browser profile, then retry."
|
|
481
|
-
: "Join without microphone/camera permissions in the OpenClaw browser profile, then retry.";
|
|
482
|
-
} else if (!inCall && (allowMicrophone ? !microphoneChoice : !noMicrophoneChoice) && /do you want people to hear you in the meeting/i.test(pageText)) {
|
|
483
|
-
manualActionReason = "meet-audio-choice-required";
|
|
484
|
-
manualActionMessage = allowMicrophone
|
|
485
|
-
? "Meet is showing the microphone choice. Click Use microphone in the OpenClaw browser profile, then retry."
|
|
486
|
-
: "Meet is showing the microphone choice. Choose the no-microphone option in the OpenClaw browser profile, then retry.";
|
|
487
|
-
}
|
|
488
|
-
return JSON.stringify({
|
|
489
|
-
clickedJoin: Boolean(join),
|
|
490
|
-
clickedMicrophoneChoice: Boolean(allowMicrophone && microphoneChoice),
|
|
491
|
-
inCall,
|
|
492
|
-
micMuted: mic ? /turn on microphone/i.test(mic.getAttribute('aria-label') || text(mic)) : undefined,
|
|
493
|
-
lobbyWaiting,
|
|
494
|
-
leaveReason,
|
|
495
|
-
captioning,
|
|
496
|
-
captionsEnabledAttempted,
|
|
497
|
-
transcriptLines,
|
|
498
|
-
lastCaptionAt,
|
|
499
|
-
lastCaptionSpeaker,
|
|
500
|
-
lastCaptionText,
|
|
501
|
-
recentTranscript,
|
|
502
|
-
manualActionRequired: Boolean(manualActionReason),
|
|
503
|
-
manualActionReason,
|
|
504
|
-
manualActionMessage,
|
|
505
|
-
title: document.title,
|
|
506
|
-
url: pageUrl,
|
|
507
|
-
notes
|
|
508
|
-
});
|
|
509
|
-
}`;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
async function openMeetWithBrowserProxy(params: {
|
|
513
|
-
runtime: PluginRuntime;
|
|
514
|
-
nodeId: string;
|
|
515
|
-
config: GoogleMeetConfig;
|
|
516
|
-
mode: "realtime" | "transcribe";
|
|
517
|
-
url: string;
|
|
518
|
-
}): Promise<{ launched: boolean; browser?: GoogleMeetChromeHealth }> {
|
|
519
|
-
return await openMeetWithBrowserRequest({
|
|
520
|
-
callBrowser: async (request) =>
|
|
521
|
-
await callBrowserProxyOnNode({
|
|
522
|
-
runtime: params.runtime,
|
|
523
|
-
nodeId: params.nodeId,
|
|
524
|
-
method: request.method,
|
|
525
|
-
path: request.path,
|
|
526
|
-
body: request.body,
|
|
527
|
-
timeoutMs: request.timeoutMs,
|
|
528
|
-
}),
|
|
529
|
-
config: params.config,
|
|
530
|
-
mode: params.mode,
|
|
531
|
-
url: params.url,
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
async function openMeetWithBrowserRequest(params: {
|
|
536
|
-
callBrowser: BrowserRequestCaller;
|
|
537
|
-
config: GoogleMeetConfig;
|
|
538
|
-
mode: "realtime" | "transcribe";
|
|
539
|
-
url: string;
|
|
540
|
-
}): Promise<{ launched: boolean; browser?: GoogleMeetChromeHealth }> {
|
|
541
|
-
if (!params.config.chrome.launch) {
|
|
542
|
-
return { launched: false };
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const timeoutMs = Math.max(1_000, params.config.chrome.joinTimeoutMs);
|
|
546
|
-
let targetId: string | undefined;
|
|
547
|
-
let tab: BrowserTab | undefined;
|
|
548
|
-
if (params.config.chrome.reuseExistingTab) {
|
|
549
|
-
const tabs = asBrowserTabs(
|
|
550
|
-
await params.callBrowser({
|
|
551
|
-
method: "GET",
|
|
552
|
-
path: "/tabs",
|
|
553
|
-
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
554
|
-
}),
|
|
555
|
-
);
|
|
556
|
-
tab = tabs.find((entry) => isSameMeetUrlForReuse(entry.url, params.url));
|
|
557
|
-
targetId = tab?.targetId;
|
|
558
|
-
if (targetId) {
|
|
559
|
-
await params.callBrowser({
|
|
560
|
-
method: "POST",
|
|
561
|
-
path: "/tabs/focus",
|
|
562
|
-
body: { targetId },
|
|
563
|
-
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
if (!targetId) {
|
|
568
|
-
tab = readBrowserTab(
|
|
569
|
-
await params.callBrowser({
|
|
570
|
-
method: "POST",
|
|
571
|
-
path: "/tabs/open",
|
|
572
|
-
body: { url: params.url },
|
|
573
|
-
timeoutMs,
|
|
574
|
-
}),
|
|
575
|
-
);
|
|
576
|
-
targetId = tab?.targetId;
|
|
577
|
-
}
|
|
578
|
-
if (!targetId) {
|
|
579
|
-
return {
|
|
580
|
-
launched: true,
|
|
581
|
-
browser: {
|
|
582
|
-
status: "browser-control",
|
|
583
|
-
notes: ["Browser proxy opened Meet but did not return a targetId."],
|
|
584
|
-
browserUrl: tab?.url,
|
|
585
|
-
browserTitle: tab?.title,
|
|
586
|
-
},
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const permissionNotes = await grantMeetMediaPermissions({
|
|
591
|
-
allowMicrophone: params.mode === "realtime",
|
|
592
|
-
callBrowser: params.callBrowser,
|
|
593
|
-
timeoutMs,
|
|
594
|
-
});
|
|
595
|
-
const deadline = Date.now() + Math.max(0, params.config.chrome.waitForInCallMs);
|
|
596
|
-
let browser: GoogleMeetChromeHealth | undefined = {
|
|
597
|
-
status: "browser-control",
|
|
598
|
-
browserUrl: tab?.url,
|
|
599
|
-
browserTitle: tab?.title,
|
|
600
|
-
notes: permissionNotes,
|
|
601
|
-
};
|
|
602
|
-
do {
|
|
603
|
-
try {
|
|
604
|
-
const evaluated = await params.callBrowser({
|
|
605
|
-
method: "POST",
|
|
606
|
-
path: "/act",
|
|
607
|
-
body: {
|
|
608
|
-
kind: "evaluate",
|
|
609
|
-
targetId,
|
|
610
|
-
fn: meetStatusScript({
|
|
611
|
-
allowMicrophone: params.mode === "realtime",
|
|
612
|
-
captureCaptions: params.mode === "transcribe",
|
|
613
|
-
guestName: params.config.chrome.guestName,
|
|
614
|
-
autoJoin: params.config.chrome.autoJoin,
|
|
615
|
-
}),
|
|
616
|
-
},
|
|
617
|
-
timeoutMs: Math.min(timeoutMs, 10_000),
|
|
618
|
-
});
|
|
619
|
-
browser = mergeBrowserNotes(parseMeetBrowserStatus(evaluated) ?? browser, permissionNotes);
|
|
620
|
-
if (browser?.inCall === true) {
|
|
621
|
-
return { launched: true, browser };
|
|
622
|
-
}
|
|
623
|
-
if (browser?.manualActionRequired === true) {
|
|
624
|
-
return { launched: true, browser };
|
|
625
|
-
}
|
|
626
|
-
} catch (error) {
|
|
627
|
-
browser = {
|
|
628
|
-
...browser,
|
|
629
|
-
inCall: false,
|
|
630
|
-
manualActionRequired: true,
|
|
631
|
-
manualActionReason: "browser-control-unavailable",
|
|
632
|
-
manualActionMessage:
|
|
633
|
-
"Open the OpenClaw browser profile, finish Google Meet login, admission, or permission prompts, then retry.",
|
|
634
|
-
notes: [
|
|
635
|
-
...permissionNotes,
|
|
636
|
-
`Browser control could not inspect or auto-join Meet: ${
|
|
637
|
-
error instanceof Error ? error.message : String(error)
|
|
638
|
-
}`,
|
|
639
|
-
],
|
|
640
|
-
};
|
|
641
|
-
break;
|
|
642
|
-
}
|
|
643
|
-
if (Date.now() <= deadline) {
|
|
644
|
-
await new Promise((resolve) => setTimeout(resolve, 750));
|
|
645
|
-
}
|
|
646
|
-
} while (Date.now() <= deadline);
|
|
647
|
-
return { launched: true, browser };
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function isRecoverableMeetTab(tab: BrowserTab, url?: string): boolean {
|
|
651
|
-
if (url) {
|
|
652
|
-
return isSameMeetUrlForReuse(tab.url, url);
|
|
653
|
-
}
|
|
654
|
-
if (normalizeMeetUrlForReuse(tab.url)) {
|
|
655
|
-
return true;
|
|
656
|
-
}
|
|
657
|
-
const tabUrl = tab.url ?? "";
|
|
658
|
-
return (
|
|
659
|
-
tabUrl.startsWith("https://accounts.google.com/") &&
|
|
660
|
-
/sign in|google accounts|meet/i.test(tab.title ?? "")
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
async function inspectRecoverableMeetTab(params: {
|
|
665
|
-
callBrowser: BrowserRequestCaller;
|
|
666
|
-
config: GoogleMeetConfig;
|
|
667
|
-
mode?: "realtime" | "transcribe";
|
|
668
|
-
timeoutMs: number;
|
|
669
|
-
tab: BrowserTab;
|
|
670
|
-
targetId: string;
|
|
671
|
-
}) {
|
|
672
|
-
const allowMicrophone = params.mode !== "transcribe";
|
|
673
|
-
await params.callBrowser({
|
|
674
|
-
method: "POST",
|
|
675
|
-
path: "/tabs/focus",
|
|
676
|
-
body: { targetId: params.targetId },
|
|
677
|
-
timeoutMs: Math.min(params.timeoutMs, 5_000),
|
|
678
|
-
});
|
|
679
|
-
const permissionNotes = await grantMeetMediaPermissions({
|
|
680
|
-
allowMicrophone,
|
|
681
|
-
callBrowser: params.callBrowser,
|
|
682
|
-
timeoutMs: params.timeoutMs,
|
|
683
|
-
});
|
|
684
|
-
const evaluated = await params.callBrowser({
|
|
685
|
-
method: "POST",
|
|
686
|
-
path: "/act",
|
|
687
|
-
body: {
|
|
688
|
-
kind: "evaluate",
|
|
689
|
-
targetId: params.targetId,
|
|
690
|
-
fn: meetStatusScript({
|
|
691
|
-
allowMicrophone,
|
|
692
|
-
captureCaptions: params.mode === "transcribe",
|
|
693
|
-
guestName: params.config.chrome.guestName,
|
|
694
|
-
autoJoin: false,
|
|
695
|
-
}),
|
|
696
|
-
},
|
|
697
|
-
timeoutMs: Math.min(params.timeoutMs, 10_000),
|
|
698
|
-
});
|
|
699
|
-
const browser = mergeBrowserNotes(
|
|
700
|
-
parseMeetBrowserStatus(evaluated) ?? {
|
|
701
|
-
status: "browser-control",
|
|
702
|
-
browserUrl: params.tab.url,
|
|
703
|
-
browserTitle: params.tab.title,
|
|
704
|
-
},
|
|
705
|
-
permissionNotes,
|
|
706
|
-
);
|
|
707
|
-
const manual = browser?.manualActionRequired
|
|
708
|
-
? browser.manualActionMessage || browser.manualActionReason
|
|
709
|
-
: undefined;
|
|
710
|
-
return {
|
|
711
|
-
found: true,
|
|
712
|
-
targetId: params.targetId,
|
|
713
|
-
tab: params.tab,
|
|
714
|
-
browser,
|
|
715
|
-
message:
|
|
716
|
-
manual ?? (browser?.inCall ? "Existing Meet tab is in-call." : "Existing Meet tab focused."),
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
export async function recoverCurrentMeetTab(params: {
|
|
721
|
-
config: GoogleMeetConfig;
|
|
722
|
-
mode?: "realtime" | "transcribe";
|
|
723
|
-
url?: string;
|
|
724
|
-
}): Promise<{
|
|
725
|
-
transport: "chrome";
|
|
726
|
-
nodeId?: undefined;
|
|
727
|
-
found: boolean;
|
|
728
|
-
targetId?: string;
|
|
729
|
-
tab?: BrowserTab;
|
|
730
|
-
browser?: GoogleMeetChromeHealth;
|
|
731
|
-
message: string;
|
|
732
|
-
}> {
|
|
733
|
-
const timeoutMs = Math.max(1_000, params.config.chrome.joinTimeoutMs);
|
|
734
|
-
const tabs = asBrowserTabs(
|
|
735
|
-
await callLocalBrowserRequest({
|
|
736
|
-
method: "GET",
|
|
737
|
-
path: "/tabs",
|
|
738
|
-
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
739
|
-
}),
|
|
740
|
-
);
|
|
741
|
-
const tab = tabs.find((entry) => isRecoverableMeetTab(entry, params.url));
|
|
742
|
-
const targetId = tab?.targetId;
|
|
743
|
-
if (!tab || !targetId) {
|
|
744
|
-
return {
|
|
745
|
-
transport: "chrome",
|
|
746
|
-
found: false,
|
|
747
|
-
tab,
|
|
748
|
-
message: params.url
|
|
749
|
-
? `No existing Meet tab matched ${params.url}.`
|
|
750
|
-
: "No existing Meet tab found in local Chrome.",
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
return {
|
|
754
|
-
transport: "chrome",
|
|
755
|
-
...(await inspectRecoverableMeetTab({
|
|
756
|
-
callBrowser: callLocalBrowserRequest,
|
|
757
|
-
config: params.config,
|
|
758
|
-
mode: params.mode,
|
|
759
|
-
timeoutMs,
|
|
760
|
-
tab,
|
|
761
|
-
targetId,
|
|
762
|
-
})),
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
export async function recoverCurrentMeetTabOnNode(params: {
|
|
767
|
-
runtime: PluginRuntime;
|
|
768
|
-
config: GoogleMeetConfig;
|
|
769
|
-
mode?: "realtime" | "transcribe";
|
|
770
|
-
url?: string;
|
|
771
|
-
}): Promise<{
|
|
772
|
-
transport: "chrome-node";
|
|
773
|
-
nodeId: string;
|
|
774
|
-
found: boolean;
|
|
775
|
-
targetId?: string;
|
|
776
|
-
tab?: BrowserTab;
|
|
777
|
-
browser?: GoogleMeetChromeHealth;
|
|
778
|
-
message: string;
|
|
779
|
-
}> {
|
|
780
|
-
const nodeId = await resolveChromeNode({
|
|
781
|
-
runtime: params.runtime,
|
|
782
|
-
requestedNode: params.config.chromeNode.node,
|
|
783
|
-
});
|
|
784
|
-
const timeoutMs = Math.max(1_000, params.config.chrome.joinTimeoutMs);
|
|
785
|
-
const tabs = asBrowserTabs(
|
|
786
|
-
await callBrowserProxyOnNode({
|
|
787
|
-
runtime: params.runtime,
|
|
788
|
-
nodeId,
|
|
789
|
-
method: "GET",
|
|
790
|
-
path: "/tabs",
|
|
791
|
-
timeoutMs: Math.min(timeoutMs, 5_000),
|
|
792
|
-
}),
|
|
793
|
-
);
|
|
794
|
-
const tab = tabs.find((entry) => isRecoverableMeetTab(entry, params.url));
|
|
795
|
-
const targetId = tab?.targetId;
|
|
796
|
-
if (!tab || !targetId) {
|
|
797
|
-
return {
|
|
798
|
-
transport: "chrome-node",
|
|
799
|
-
nodeId,
|
|
800
|
-
found: false,
|
|
801
|
-
tab,
|
|
802
|
-
message: params.url
|
|
803
|
-
? `No existing Meet tab matched ${params.url}.`
|
|
804
|
-
: "No existing Meet tab found on the selected Chrome node.",
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
return {
|
|
808
|
-
transport: "chrome-node",
|
|
809
|
-
nodeId,
|
|
810
|
-
...(await inspectRecoverableMeetTab({
|
|
811
|
-
callBrowser: async (request) =>
|
|
812
|
-
await callBrowserProxyOnNode({
|
|
813
|
-
runtime: params.runtime,
|
|
814
|
-
nodeId,
|
|
815
|
-
method: request.method,
|
|
816
|
-
path: request.path,
|
|
817
|
-
body: request.body,
|
|
818
|
-
timeoutMs: request.timeoutMs,
|
|
819
|
-
}),
|
|
820
|
-
config: params.config,
|
|
821
|
-
mode: params.mode,
|
|
822
|
-
timeoutMs,
|
|
823
|
-
tab,
|
|
824
|
-
targetId,
|
|
825
|
-
})),
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
export async function launchChromeMeetOnNode(params: {
|
|
830
|
-
runtime: PluginRuntime;
|
|
831
|
-
config: GoogleMeetConfig;
|
|
832
|
-
fullConfig: OpenClawConfig;
|
|
833
|
-
meetingSessionId: string;
|
|
834
|
-
mode: "realtime" | "transcribe";
|
|
835
|
-
url: string;
|
|
836
|
-
logger: RuntimeLogger;
|
|
837
|
-
}): Promise<{
|
|
838
|
-
nodeId: string;
|
|
839
|
-
launched: boolean;
|
|
840
|
-
audioBridge?:
|
|
841
|
-
| { type: "external-command" }
|
|
842
|
-
| ({ type: "node-command-pair" } & ChromeNodeRealtimeAudioBridgeHandle);
|
|
843
|
-
browser?: GoogleMeetChromeHealth;
|
|
844
|
-
}> {
|
|
845
|
-
const nodeId = await resolveChromeNode({
|
|
846
|
-
runtime: params.runtime,
|
|
847
|
-
requestedNode: params.config.chromeNode.node,
|
|
848
|
-
});
|
|
849
|
-
try {
|
|
850
|
-
await params.runtime.nodes.invoke({
|
|
851
|
-
nodeId,
|
|
852
|
-
command: "googlemeet.chrome",
|
|
853
|
-
params: {
|
|
854
|
-
action: "stopByUrl",
|
|
855
|
-
url: params.url,
|
|
856
|
-
mode: params.mode,
|
|
857
|
-
},
|
|
858
|
-
timeoutMs: 5_000,
|
|
859
|
-
});
|
|
860
|
-
} catch (error) {
|
|
861
|
-
params.logger.debug?.(
|
|
862
|
-
`[google-meet] node bridge cleanup before join ignored: ${
|
|
863
|
-
error instanceof Error ? error.message : String(error)
|
|
864
|
-
}`,
|
|
865
|
-
);
|
|
866
|
-
}
|
|
867
|
-
const browserControl = await openMeetWithBrowserProxy({
|
|
868
|
-
runtime: params.runtime,
|
|
869
|
-
nodeId,
|
|
870
|
-
config: params.config,
|
|
871
|
-
mode: params.mode,
|
|
872
|
-
url: params.url,
|
|
873
|
-
});
|
|
874
|
-
const raw = await params.runtime.nodes.invoke({
|
|
875
|
-
nodeId,
|
|
876
|
-
command: "googlemeet.chrome",
|
|
877
|
-
params: {
|
|
878
|
-
action: "start",
|
|
879
|
-
url: params.url,
|
|
880
|
-
mode: params.mode,
|
|
881
|
-
launch: false,
|
|
882
|
-
browserProfile: params.config.chrome.browserProfile,
|
|
883
|
-
joinTimeoutMs: params.config.chrome.joinTimeoutMs,
|
|
884
|
-
audioInputCommand: params.config.chrome.audioInputCommand,
|
|
885
|
-
audioOutputCommand: params.config.chrome.audioOutputCommand,
|
|
886
|
-
audioBridgeCommand: params.config.chrome.audioBridgeCommand,
|
|
887
|
-
audioBridgeHealthCommand: params.config.chrome.audioBridgeHealthCommand,
|
|
888
|
-
},
|
|
889
|
-
timeoutMs: params.config.chrome.joinTimeoutMs + 5_000,
|
|
890
|
-
});
|
|
891
|
-
const result = parseNodeStartResult(raw);
|
|
892
|
-
if (result.audioBridge?.type === "node-command-pair") {
|
|
893
|
-
if (!result.bridgeId) {
|
|
894
|
-
throw new Error("Google Meet node did not return an audio bridge id.");
|
|
895
|
-
}
|
|
896
|
-
const bridge = await startNodeRealtimeAudioBridge({
|
|
897
|
-
config: params.config,
|
|
898
|
-
fullConfig: params.fullConfig,
|
|
899
|
-
runtime: params.runtime,
|
|
900
|
-
meetingSessionId: params.meetingSessionId,
|
|
901
|
-
nodeId,
|
|
902
|
-
bridgeId: result.bridgeId,
|
|
903
|
-
logger: params.logger,
|
|
904
|
-
});
|
|
905
|
-
return {
|
|
906
|
-
nodeId,
|
|
907
|
-
launched: browserControl.launched || result.launched === true,
|
|
908
|
-
audioBridge: bridge,
|
|
909
|
-
browser: browserControl.browser ?? result.browser,
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
if (result.audioBridge?.type === "external-command") {
|
|
913
|
-
return {
|
|
914
|
-
nodeId,
|
|
915
|
-
launched: browserControl.launched || result.launched === true,
|
|
916
|
-
audioBridge: { type: "external-command" },
|
|
917
|
-
browser: browserControl.browser ?? result.browser,
|
|
918
|
-
};
|
|
919
|
-
}
|
|
920
|
-
return {
|
|
921
|
-
nodeId,
|
|
922
|
-
launched: browserControl.launched || result.launched === true,
|
|
923
|
-
browser: browserControl.browser ?? result.browser,
|
|
924
|
-
};
|
|
925
|
-
}
|