@gakr-gakr/google-meet 0.1.0
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/autobot.plugin.json +532 -0
- package/doctor-contract-api.ts +1 -0
- package/index.ts +1224 -0
- package/package.json +47 -0
- package/src/agent-consult.ts +158 -0
- package/src/calendar.ts +252 -0
- package/src/cli.ts +2350 -0
- package/src/config-compat.ts +84 -0
- package/src/config.ts +589 -0
- package/src/create.ts +157 -0
- package/src/drive.ts +72 -0
- package/src/google-api-errors.ts +20 -0
- package/src/meet.ts +1024 -0
- package/src/node-host.ts +520 -0
- package/src/oauth.ts +229 -0
- package/src/realtime-node.ts +780 -0
- package/src/realtime.ts +1334 -0
- package/src/runtime.ts +1008 -0
- package/src/setup.ts +285 -0
- package/src/transports/chrome-browser-proxy.ts +204 -0
- package/src/transports/chrome-create.ts +364 -0
- package/src/transports/chrome.ts +1065 -0
- package/src/transports/twilio.ts +57 -0
- package/src/transports/types.ts +147 -0
- package/src/voice-call-gateway.ts +241 -0
- package/tsconfig.json +16 -0
package/src/node-host.ts
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
|
+
import { formatErrorMessage } from "autobot/plugin-sdk/error-runtime";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND,
|
|
7
|
+
DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND,
|
|
8
|
+
} from "./config.js";
|
|
9
|
+
import {
|
|
10
|
+
GOOGLE_MEET_SYSTEM_PROFILER_COMMAND,
|
|
11
|
+
outputMentionsBlackHole2ch,
|
|
12
|
+
} from "./transports/chrome.js";
|
|
13
|
+
|
|
14
|
+
type NodeBridgeSession = {
|
|
15
|
+
id: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
mode?: string;
|
|
18
|
+
outputCommand: { command: string; args: string[] };
|
|
19
|
+
input?: ChildProcess;
|
|
20
|
+
output?: ChildProcess;
|
|
21
|
+
chunks: Buffer[];
|
|
22
|
+
waiters: Array<() => void>;
|
|
23
|
+
closed: boolean;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
lastInputAt?: string;
|
|
26
|
+
lastOutputAt?: string;
|
|
27
|
+
lastClearAt?: string;
|
|
28
|
+
lastInputBytes: number;
|
|
29
|
+
lastOutputBytes: number;
|
|
30
|
+
closedAt?: string;
|
|
31
|
+
clearCount: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const sessions = new Map<string, NodeBridgeSession>();
|
|
35
|
+
|
|
36
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
37
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
38
|
+
? (value as Record<string, unknown>)
|
|
39
|
+
: {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readString(value: unknown): string | undefined {
|
|
43
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readStringArray(value: unknown): string[] | undefined {
|
|
47
|
+
if (!Array.isArray(value)) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const result = value.filter(
|
|
51
|
+
(entry): entry is string => typeof entry === "string" && entry.length > 0,
|
|
52
|
+
);
|
|
53
|
+
return result.length > 0 ? result : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readNumber(value: unknown, fallback: number): number {
|
|
57
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function runCommandWithTimeout(argv: string[], timeoutMs: number) {
|
|
61
|
+
const [command, ...args] = argv;
|
|
62
|
+
if (!command) {
|
|
63
|
+
throw new Error("command must not be empty");
|
|
64
|
+
}
|
|
65
|
+
const result = spawnSync(command, args, {
|
|
66
|
+
encoding: "utf8",
|
|
67
|
+
timeout: timeoutMs,
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
code: typeof result.status === "number" ? result.status : result.error ? 1 : 0,
|
|
71
|
+
stdout: result.stdout ?? "",
|
|
72
|
+
stderr: result.stderr ?? (result.error ? formatErrorMessage(result.error) : ""),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertBlackHoleAvailable(timeoutMs: number) {
|
|
77
|
+
if (process.platform !== "darwin") {
|
|
78
|
+
throw new Error("Chrome Meet transport with blackhole-2ch audio is currently macOS-only");
|
|
79
|
+
}
|
|
80
|
+
const result = runCommandWithTimeout(
|
|
81
|
+
[GOOGLE_MEET_SYSTEM_PROFILER_COMMAND, "SPAudioDataType"],
|
|
82
|
+
timeoutMs,
|
|
83
|
+
);
|
|
84
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
85
|
+
if (result.code !== 0 || !outputMentionsBlackHole2ch(output)) {
|
|
86
|
+
throw new Error("BlackHole 2ch audio device not found on the node.");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function splitCommand(argv: string[]): { command: string; args: string[] } {
|
|
91
|
+
const [command, ...args] = argv;
|
|
92
|
+
if (!command) {
|
|
93
|
+
throw new Error("audio command must not be empty");
|
|
94
|
+
}
|
|
95
|
+
return { command, args };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function wake(session: NodeBridgeSession) {
|
|
99
|
+
const waiters = session.waiters.splice(0);
|
|
100
|
+
for (const waiter of waiters) {
|
|
101
|
+
waiter();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function stopSession(session: NodeBridgeSession) {
|
|
106
|
+
const wasClosed = session.closed;
|
|
107
|
+
session.closed = true;
|
|
108
|
+
session.closedAt ??= new Date().toISOString();
|
|
109
|
+
terminateChild(session.input);
|
|
110
|
+
terminateChild(session.output);
|
|
111
|
+
if (!wasClosed) {
|
|
112
|
+
wake(session);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function attachOutputProcessHandlers(session: NodeBridgeSession, outputProcess: ChildProcess) {
|
|
117
|
+
outputProcess.on("exit", () => {
|
|
118
|
+
if (session.output === outputProcess) {
|
|
119
|
+
stopSession(session);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
outputProcess.on("error", () => {
|
|
123
|
+
if (session.output === outputProcess) {
|
|
124
|
+
stopSession(session);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
outputProcess.stdin?.on?.("error", () => {
|
|
128
|
+
if (session.output === outputProcess) {
|
|
129
|
+
stopSession(session);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function startOutputProcess(command: { command: string; args: string[] }) {
|
|
135
|
+
return spawn(command.command, command.args, {
|
|
136
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function startCommandPair(params: {
|
|
141
|
+
inputCommand: string[];
|
|
142
|
+
outputCommand: string[];
|
|
143
|
+
url?: string;
|
|
144
|
+
mode?: string;
|
|
145
|
+
}): NodeBridgeSession {
|
|
146
|
+
const input = splitCommand(params.inputCommand);
|
|
147
|
+
const output = splitCommand(params.outputCommand);
|
|
148
|
+
const session: NodeBridgeSession = {
|
|
149
|
+
id: `meet_node_${randomUUID()}`,
|
|
150
|
+
url: params.url,
|
|
151
|
+
mode: params.mode,
|
|
152
|
+
outputCommand: output,
|
|
153
|
+
chunks: [],
|
|
154
|
+
waiters: [],
|
|
155
|
+
closed: false,
|
|
156
|
+
createdAt: new Date().toISOString(),
|
|
157
|
+
lastInputBytes: 0,
|
|
158
|
+
lastOutputBytes: 0,
|
|
159
|
+
clearCount: 0,
|
|
160
|
+
};
|
|
161
|
+
const outputProcess = startOutputProcess(output);
|
|
162
|
+
const inputProcess = spawn(input.command, input.args, {
|
|
163
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
164
|
+
});
|
|
165
|
+
session.input = inputProcess;
|
|
166
|
+
session.output = outputProcess;
|
|
167
|
+
inputProcess.stdout?.on("data", (chunk) => {
|
|
168
|
+
const audio = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
169
|
+
session.lastInputAt = new Date().toISOString();
|
|
170
|
+
session.lastInputBytes += audio.byteLength;
|
|
171
|
+
session.chunks.push(audio);
|
|
172
|
+
if (session.chunks.length > 200) {
|
|
173
|
+
session.chunks.splice(0, session.chunks.length - 200);
|
|
174
|
+
}
|
|
175
|
+
wake(session);
|
|
176
|
+
});
|
|
177
|
+
inputProcess.on("exit", () => stopSession(session));
|
|
178
|
+
attachOutputProcessHandlers(session, outputProcess);
|
|
179
|
+
inputProcess.on("error", () => stopSession(session));
|
|
180
|
+
sessions.set(session.id, session);
|
|
181
|
+
return session;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function terminateChild(child?: ChildProcess) {
|
|
185
|
+
if (!child) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
let exited = child.exitCode !== null || child.signalCode !== null;
|
|
189
|
+
child.once?.("exit", () => {
|
|
190
|
+
exited = true;
|
|
191
|
+
});
|
|
192
|
+
try {
|
|
193
|
+
child.kill("SIGTERM");
|
|
194
|
+
} catch {
|
|
195
|
+
// Best-effort cleanup for node-host child processes.
|
|
196
|
+
}
|
|
197
|
+
const timer = setTimeout(() => {
|
|
198
|
+
if (exited) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
child.kill("SIGKILL");
|
|
203
|
+
} catch {
|
|
204
|
+
// Process may have exited after the grace check.
|
|
205
|
+
}
|
|
206
|
+
}, 2_000);
|
|
207
|
+
timer.unref?.();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function pullAudio(params: Record<string, unknown>) {
|
|
211
|
+
const bridgeId = readString(params.bridgeId);
|
|
212
|
+
if (!bridgeId) {
|
|
213
|
+
throw new Error("bridgeId required");
|
|
214
|
+
}
|
|
215
|
+
const session = sessions.get(bridgeId);
|
|
216
|
+
if (!session) {
|
|
217
|
+
throw new Error(`unknown bridgeId: ${bridgeId}`);
|
|
218
|
+
}
|
|
219
|
+
const timeoutMs = Math.min(readNumber(params.timeoutMs, 250), 2_000);
|
|
220
|
+
if (session.chunks.length === 0 && !session.closed) {
|
|
221
|
+
await Promise.race([
|
|
222
|
+
sleep(timeoutMs),
|
|
223
|
+
new Promise<void>((resolve) => {
|
|
224
|
+
session.waiters.push(resolve);
|
|
225
|
+
}),
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
const chunk = session.chunks.shift();
|
|
229
|
+
return {
|
|
230
|
+
bridgeId,
|
|
231
|
+
closed: session.closed,
|
|
232
|
+
base64: chunk ? chunk.toString("base64") : undefined,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function pushAudio(params: Record<string, unknown>) {
|
|
237
|
+
const bridgeId = readString(params.bridgeId);
|
|
238
|
+
const base64 = readString(params.base64);
|
|
239
|
+
if (!bridgeId || !base64) {
|
|
240
|
+
throw new Error("bridgeId and base64 required");
|
|
241
|
+
}
|
|
242
|
+
const session = sessions.get(bridgeId);
|
|
243
|
+
if (!session || session.closed) {
|
|
244
|
+
throw new Error(`bridge is not open: ${bridgeId}`);
|
|
245
|
+
}
|
|
246
|
+
const audio = Buffer.from(base64, "base64");
|
|
247
|
+
session.lastOutputAt = new Date().toISOString();
|
|
248
|
+
session.lastOutputBytes += audio.byteLength;
|
|
249
|
+
try {
|
|
250
|
+
session.output?.stdin?.write(audio);
|
|
251
|
+
} catch {
|
|
252
|
+
stopSession(session);
|
|
253
|
+
throw new Error(`bridge is not open: ${bridgeId}`);
|
|
254
|
+
}
|
|
255
|
+
return { bridgeId, ok: true };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function clearAudio(params: Record<string, unknown>) {
|
|
259
|
+
const bridgeId = readString(params.bridgeId);
|
|
260
|
+
if (!bridgeId) {
|
|
261
|
+
throw new Error("bridgeId required");
|
|
262
|
+
}
|
|
263
|
+
const session = sessions.get(bridgeId);
|
|
264
|
+
if (!session || session.closed) {
|
|
265
|
+
throw new Error(`bridge is not open: ${bridgeId}`);
|
|
266
|
+
}
|
|
267
|
+
const previousOutput = session.output;
|
|
268
|
+
const outputProcess = startOutputProcess(session.outputCommand);
|
|
269
|
+
session.output = outputProcess;
|
|
270
|
+
attachOutputProcessHandlers(session, outputProcess);
|
|
271
|
+
session.clearCount += 1;
|
|
272
|
+
session.lastClearAt = new Date().toISOString();
|
|
273
|
+
terminateChild(previousOutput);
|
|
274
|
+
return { bridgeId, ok: true, clearCount: session.clearCount };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function startChrome(params: Record<string, unknown>) {
|
|
278
|
+
const url = readString(params.url);
|
|
279
|
+
if (!url) {
|
|
280
|
+
throw new Error("url required");
|
|
281
|
+
}
|
|
282
|
+
const timeoutMs = readNumber(params.joinTimeoutMs, 30_000);
|
|
283
|
+
const mode = readString(params.mode);
|
|
284
|
+
|
|
285
|
+
let bridgeId: string | undefined;
|
|
286
|
+
let audioBridge: { type: "external-command" | "node-command-pair" } | undefined;
|
|
287
|
+
if (mode === "agent" || mode === "bidi" || mode === "realtime") {
|
|
288
|
+
assertBlackHoleAvailable(Math.min(timeoutMs, 10_000));
|
|
289
|
+
|
|
290
|
+
const healthCommand = readStringArray(params.audioBridgeHealthCommand);
|
|
291
|
+
if (healthCommand) {
|
|
292
|
+
const health = runCommandWithTimeout(healthCommand, timeoutMs);
|
|
293
|
+
if (health.code !== 0) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Chrome audio bridge health check failed: ${health.stderr || health.stdout || health.code}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const bridgeCommand = readStringArray(params.audioBridgeCommand);
|
|
301
|
+
if (bridgeCommand) {
|
|
302
|
+
if (mode === "agent") {
|
|
303
|
+
throw new Error(
|
|
304
|
+
"Chrome agent mode requires audioInputCommand and audioOutputCommand so AutoBot can run STT and regular TTS directly.",
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const bridge = runCommandWithTimeout(bridgeCommand, timeoutMs);
|
|
308
|
+
if (bridge.code !== 0) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`failed to start Chrome audio bridge: ${bridge.stderr || bridge.stdout || bridge.code}`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
audioBridge = { type: "external-command" };
|
|
314
|
+
} else {
|
|
315
|
+
const session = startCommandPair({
|
|
316
|
+
inputCommand: readStringArray(params.audioInputCommand) ?? [
|
|
317
|
+
...DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND,
|
|
318
|
+
],
|
|
319
|
+
outputCommand: readStringArray(params.audioOutputCommand) ?? [
|
|
320
|
+
...DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND,
|
|
321
|
+
],
|
|
322
|
+
url,
|
|
323
|
+
mode,
|
|
324
|
+
});
|
|
325
|
+
bridgeId = session.id;
|
|
326
|
+
audioBridge = { type: "node-command-pair" };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (params.launch !== false) {
|
|
331
|
+
const argv = ["open", "-a", "Google Chrome"];
|
|
332
|
+
const browserProfile = readString(params.browserProfile);
|
|
333
|
+
if (browserProfile) {
|
|
334
|
+
argv.push("--args", `--profile-directory=${browserProfile}`);
|
|
335
|
+
}
|
|
336
|
+
argv.push(url);
|
|
337
|
+
const result = runCommandWithTimeout(argv, timeoutMs);
|
|
338
|
+
if (result.code !== 0) {
|
|
339
|
+
if (bridgeId) {
|
|
340
|
+
const session = sessions.get(bridgeId);
|
|
341
|
+
if (session) {
|
|
342
|
+
stopSession(session);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
throw new Error(
|
|
346
|
+
`failed to launch Chrome for Meet: ${result.stderr || result.stdout || result.code}`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
launched: params.launch !== false,
|
|
353
|
+
bridgeId,
|
|
354
|
+
audioBridge,
|
|
355
|
+
browser:
|
|
356
|
+
params.launch !== false
|
|
357
|
+
? {
|
|
358
|
+
status: "chrome-opened",
|
|
359
|
+
browserUrl: url,
|
|
360
|
+
notes: [
|
|
361
|
+
"Browser page control is handled by AutoBot browser automation when using chrome-node.",
|
|
362
|
+
],
|
|
363
|
+
}
|
|
364
|
+
: undefined,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function bridgeStatus(params: Record<string, unknown>) {
|
|
369
|
+
const bridgeId = readString(params.bridgeId);
|
|
370
|
+
const session = bridgeId ? sessions.get(bridgeId) : undefined;
|
|
371
|
+
return {
|
|
372
|
+
bridge: session
|
|
373
|
+
? {
|
|
374
|
+
bridgeId,
|
|
375
|
+
closed: session.closed,
|
|
376
|
+
createdAt: session.createdAt,
|
|
377
|
+
lastInputAt: session.lastInputAt,
|
|
378
|
+
lastOutputAt: session.lastOutputAt,
|
|
379
|
+
lastClearAt: session.lastClearAt,
|
|
380
|
+
lastInputBytes: session.lastInputBytes,
|
|
381
|
+
lastOutputBytes: session.lastOutputBytes,
|
|
382
|
+
clearCount: session.clearCount,
|
|
383
|
+
queuedInputChunks: session.chunks.length,
|
|
384
|
+
}
|
|
385
|
+
: bridgeId
|
|
386
|
+
? { bridgeId, closed: true }
|
|
387
|
+
: undefined,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function normalizeMeetKey(value?: string): string | undefined {
|
|
392
|
+
if (!value) {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const url = new URL(value);
|
|
397
|
+
if (url.hostname.toLowerCase() !== "meet.google.com") {
|
|
398
|
+
return value;
|
|
399
|
+
}
|
|
400
|
+
const match = /^\/([a-z]{3}-[a-z]{4}-[a-z]{3})(?:$|[/?#])/i.exec(url.pathname);
|
|
401
|
+
return match?.[1]?.toLowerCase() ?? value;
|
|
402
|
+
} catch {
|
|
403
|
+
return value;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function summarizeSession(session: NodeBridgeSession) {
|
|
408
|
+
return {
|
|
409
|
+
bridgeId: session.id,
|
|
410
|
+
url: session.url,
|
|
411
|
+
mode: session.mode,
|
|
412
|
+
closed: session.closed,
|
|
413
|
+
createdAt: session.createdAt,
|
|
414
|
+
closedAt: session.closedAt,
|
|
415
|
+
lastInputAt: session.lastInputAt,
|
|
416
|
+
lastOutputAt: session.lastOutputAt,
|
|
417
|
+
lastInputBytes: session.lastInputBytes,
|
|
418
|
+
lastOutputBytes: session.lastOutputBytes,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function listSessions(params: Record<string, unknown>) {
|
|
423
|
+
const urlKey = normalizeMeetKey(readString(params.url));
|
|
424
|
+
const mode = readString(params.mode);
|
|
425
|
+
const bridges = [...sessions.values()]
|
|
426
|
+
.filter((session) => !session.closed)
|
|
427
|
+
.filter((session) => !urlKey || normalizeMeetKey(session.url) === urlKey)
|
|
428
|
+
.filter((session) => !mode || session.mode === mode)
|
|
429
|
+
.map(summarizeSession);
|
|
430
|
+
return { bridges };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function stopSessionsByUrl(params: Record<string, unknown>) {
|
|
434
|
+
const urlKey = normalizeMeetKey(readString(params.url));
|
|
435
|
+
if (!urlKey) {
|
|
436
|
+
throw new Error("url required");
|
|
437
|
+
}
|
|
438
|
+
const mode = readString(params.mode);
|
|
439
|
+
const exceptBridgeId = readString(params.exceptBridgeId);
|
|
440
|
+
let stopped = 0;
|
|
441
|
+
for (const [bridgeId, session] of sessions) {
|
|
442
|
+
if (exceptBridgeId && bridgeId === exceptBridgeId) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (normalizeMeetKey(session.url) !== urlKey) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (mode && session.mode !== mode) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const wasClosed = session.closed;
|
|
452
|
+
stopSession(session);
|
|
453
|
+
sessions.delete(bridgeId);
|
|
454
|
+
if (!wasClosed) {
|
|
455
|
+
stopped += 1;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return { ok: true, stopped };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function stopChrome(params: Record<string, unknown>) {
|
|
462
|
+
const bridgeId = readString(params.bridgeId);
|
|
463
|
+
if (!bridgeId) {
|
|
464
|
+
return { ok: true, stopped: false };
|
|
465
|
+
}
|
|
466
|
+
const session = sessions.get(bridgeId);
|
|
467
|
+
if (!session) {
|
|
468
|
+
return { ok: true, stopped: false };
|
|
469
|
+
}
|
|
470
|
+
stopSession(session);
|
|
471
|
+
sessions.delete(bridgeId);
|
|
472
|
+
return { ok: true, stopped: true };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export async function handleGoogleMeetNodeHostCommand(paramsJSON?: string | null): Promise<string> {
|
|
476
|
+
let raw: unknown = {};
|
|
477
|
+
if (paramsJSON) {
|
|
478
|
+
try {
|
|
479
|
+
raw = JSON.parse(paramsJSON) as unknown;
|
|
480
|
+
} catch {
|
|
481
|
+
throw new Error("Google Meet node host received malformed params JSON.");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const params = asRecord(raw);
|
|
485
|
+
const action = readString(params.action);
|
|
486
|
+
let result: unknown;
|
|
487
|
+
switch (action) {
|
|
488
|
+
case "setup":
|
|
489
|
+
assertBlackHoleAvailable(10_000);
|
|
490
|
+
result = { ok: true };
|
|
491
|
+
break;
|
|
492
|
+
case "start":
|
|
493
|
+
result = startChrome(params);
|
|
494
|
+
break;
|
|
495
|
+
case "status":
|
|
496
|
+
result = bridgeStatus(params);
|
|
497
|
+
break;
|
|
498
|
+
case "list":
|
|
499
|
+
result = listSessions(params);
|
|
500
|
+
break;
|
|
501
|
+
case "stopByUrl":
|
|
502
|
+
result = stopSessionsByUrl(params);
|
|
503
|
+
break;
|
|
504
|
+
case "pullAudio":
|
|
505
|
+
result = await pullAudio(params);
|
|
506
|
+
break;
|
|
507
|
+
case "pushAudio":
|
|
508
|
+
result = pushAudio(params);
|
|
509
|
+
break;
|
|
510
|
+
case "clearAudio":
|
|
511
|
+
result = clearAudio(params);
|
|
512
|
+
break;
|
|
513
|
+
case "stop":
|
|
514
|
+
result = stopChrome(params);
|
|
515
|
+
break;
|
|
516
|
+
default:
|
|
517
|
+
throw new Error("unsupported googlemeet.chrome action");
|
|
518
|
+
}
|
|
519
|
+
return JSON.stringify(result);
|
|
520
|
+
}
|