@ai-matrx/meet 0.7.9 → 0.7.11
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/CHANGELOG.md +51 -0
- package/dist/index.cjs +1917 -1311
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +292 -1
- package/dist/index.d.ts +292 -1
- package/dist/index.js +1917 -1311
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +2360 -669
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +94 -3
- package/dist/react.d.ts +94 -3
- package/dist/react.js +2361 -670
- package/dist/react.js.map +1 -1
- package/dist/styles.css +293 -0
- package/dist/tokens.css +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -221,7 +221,14 @@ var MEET_ROUTES = {
|
|
|
221
221
|
*/
|
|
222
222
|
moderate: "/api/v1/meet/moderate",
|
|
223
223
|
/** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
|
|
224
|
-
chat: "/api/v1/meet/chat"
|
|
224
|
+
chat: "/api/v1/meet/chat",
|
|
225
|
+
/**
|
|
226
|
+
* Meet wave 5: polls, Q&A, the shared whiteboard and breakout rooms. `POST` acts
|
|
227
|
+
* (`{meeting_id, action, …}`), `GET ?meeting_id=` reads what this viewer may see.
|
|
228
|
+
*/
|
|
229
|
+
collab: "/api/v1/meet/collab",
|
|
230
|
+
/** Meet wave 5: translate finished caption lines into this viewer's language. */
|
|
231
|
+
captionsTranslate: "/api/v1/meet/captions/translate"
|
|
225
232
|
};
|
|
226
233
|
function meetingPath(template, meetingId) {
|
|
227
234
|
return template.replace("{meeting_id}", encodeURIComponent(meetingId));
|
|
@@ -280,7 +287,8 @@ function parseRoomToken(body, roomName) {
|
|
|
280
287
|
// server that has not landed MRI-A11 — and never invents a host from
|
|
281
288
|
// silence, because both fallbacks below grant nothing.
|
|
282
289
|
role: readRole(payload, "role") ?? (isHost ? "host" : identity.startsWith("guest:") ? "guest" : "participant"),
|
|
283
|
-
lobby: readBoolean(payload, "lobby", false)
|
|
290
|
+
lobby: readBoolean(payload, "lobby", false),
|
|
291
|
+
breakoutRoomId: readString(payload, "breakout_room_id") ?? null
|
|
284
292
|
};
|
|
285
293
|
}
|
|
286
294
|
function createTokenClient(options) {
|
|
@@ -328,6 +336,7 @@ function createTokenClient(options) {
|
|
|
328
336
|
room_name: input.roomName,
|
|
329
337
|
...input.meetingId !== void 0 ? { meeting_id: input.meetingId } : {},
|
|
330
338
|
...input.slug !== void 0 ? { slug: input.slug } : {},
|
|
339
|
+
...input.breakoutRoomId != null ? { breakout_room_id: input.breakoutRoomId } : {},
|
|
331
340
|
organization_id: input.identity.organizationId,
|
|
332
341
|
display_name: input.identity.displayName,
|
|
333
342
|
device_id: input.identity.deviceId,
|
|
@@ -391,7 +400,8 @@ function createTokenClient(options) {
|
|
|
391
400
|
return {
|
|
392
401
|
get(input) {
|
|
393
402
|
const entry = cache.get(input.roomName);
|
|
394
|
-
|
|
403
|
+
const sameRoom = entry !== void 0 && (entry.token.breakoutRoomId ?? null) === (input.breakoutRoomId ?? null);
|
|
404
|
+
if (entry !== void 0 && sameRoom) {
|
|
395
405
|
if (entry.inFlight !== null) return entry.inFlight;
|
|
396
406
|
if (!expired(entry)) return Promise.resolve(entry.token);
|
|
397
407
|
}
|
|
@@ -437,7 +447,8 @@ function placeholder(roomName) {
|
|
|
437
447
|
canRecord: false,
|
|
438
448
|
isHost: false,
|
|
439
449
|
role: "participant",
|
|
440
|
-
lobby: false
|
|
450
|
+
lobby: false,
|
|
451
|
+
breakoutRoomId: null
|
|
441
452
|
};
|
|
442
453
|
}
|
|
443
454
|
async function serverSentence(response) {
|
|
@@ -1166,6 +1177,339 @@ function createCallCenter(options) {
|
|
|
1166
1177
|
};
|
|
1167
1178
|
}
|
|
1168
1179
|
|
|
1180
|
+
// src/core/collab.ts
|
|
1181
|
+
var EMPTY_COLLAB = {
|
|
1182
|
+
loaded: false,
|
|
1183
|
+
error: null,
|
|
1184
|
+
settings: null,
|
|
1185
|
+
polls: [],
|
|
1186
|
+
questions: [],
|
|
1187
|
+
strokes: [],
|
|
1188
|
+
breakout: null,
|
|
1189
|
+
broadcast: null,
|
|
1190
|
+
moveRequest: null,
|
|
1191
|
+
panelRequest: null,
|
|
1192
|
+
captionLanguage: null,
|
|
1193
|
+
captionTranslations: {},
|
|
1194
|
+
captionTranslationError: null
|
|
1195
|
+
};
|
|
1196
|
+
function obj(value) {
|
|
1197
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
1198
|
+
}
|
|
1199
|
+
function str(value) {
|
|
1200
|
+
return typeof value === "string" ? value : null;
|
|
1201
|
+
}
|
|
1202
|
+
function num(value) {
|
|
1203
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1204
|
+
}
|
|
1205
|
+
function strings(value) {
|
|
1206
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
1207
|
+
}
|
|
1208
|
+
function pollFromRow(row) {
|
|
1209
|
+
const id = str(row["id"]);
|
|
1210
|
+
if (id === null) return null;
|
|
1211
|
+
const options = [];
|
|
1212
|
+
for (const raw of Array.isArray(row["options"]) ? row["options"] : []) {
|
|
1213
|
+
const option = obj(raw);
|
|
1214
|
+
const optionId = option === null ? null : str(option["id"]);
|
|
1215
|
+
if (option === null || optionId === null) continue;
|
|
1216
|
+
const votes = num(option["votes"]);
|
|
1217
|
+
options.push({
|
|
1218
|
+
id: optionId,
|
|
1219
|
+
text: str(option["text"]) ?? "",
|
|
1220
|
+
...votes !== null ? { votes } : {},
|
|
1221
|
+
...Array.isArray(option["voters"]) ? { voters: strings(option["voters"]) } : {}
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
const state = str(row["state"]);
|
|
1225
|
+
return {
|
|
1226
|
+
id,
|
|
1227
|
+
question: str(row["question"]) ?? "",
|
|
1228
|
+
options,
|
|
1229
|
+
multiple: row["multiple"] === true,
|
|
1230
|
+
anonymous: row["anonymous"] === true,
|
|
1231
|
+
state: state === "live" || state === "closed" ? state : "draft",
|
|
1232
|
+
resultsShared: row["results_shared"] === true,
|
|
1233
|
+
voterCount: num(row["voter_count"]),
|
|
1234
|
+
myVote: strings(row["my_vote"]),
|
|
1235
|
+
createdByName: str(row["created_by_name"]) ?? "",
|
|
1236
|
+
createdAt: str(row["created_at"])
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
function questionFromRow(row) {
|
|
1240
|
+
const id = str(row["id"]);
|
|
1241
|
+
if (id === null) return null;
|
|
1242
|
+
const state = str(row["state"]);
|
|
1243
|
+
return {
|
|
1244
|
+
id,
|
|
1245
|
+
text: str(row["text"]) ?? "",
|
|
1246
|
+
askerName: str(row["asker_name"]) ?? "",
|
|
1247
|
+
anonymous: row["anonymous"] === true,
|
|
1248
|
+
state: state === "answered" || state === "dismissed" ? state : "open",
|
|
1249
|
+
answer: str(row["answer"]),
|
|
1250
|
+
answeredByName: str(row["answered_by_name"]),
|
|
1251
|
+
upvotes: num(row["upvotes"]) ?? 0,
|
|
1252
|
+
upvotedByMe: row["upvoted_by_me"] === true,
|
|
1253
|
+
mine: row["mine"] === true,
|
|
1254
|
+
createdAt: str(row["created_at"])
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
function strokeFromRow(row) {
|
|
1258
|
+
const id = str(row["id"]);
|
|
1259
|
+
if (id === null || !Array.isArray(row["points"])) return null;
|
|
1260
|
+
const points = [];
|
|
1261
|
+
for (const raw of row["points"]) {
|
|
1262
|
+
if (Array.isArray(raw) && typeof raw[0] === "number" && typeof raw[1] === "number") {
|
|
1263
|
+
points.push([raw[0], raw[1]]);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return {
|
|
1267
|
+
id,
|
|
1268
|
+
points,
|
|
1269
|
+
color: str(row["color"]) ?? "#111827",
|
|
1270
|
+
width: num(row["width"]) ?? 3,
|
|
1271
|
+
tool: row["tool"] === "highlighter" ? "highlighter" : "pen",
|
|
1272
|
+
by: str(row["by"]) ?? "",
|
|
1273
|
+
byName: str(row["by_name"]) ?? ""
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
function broadcastFrom(value) {
|
|
1277
|
+
const row = obj(value);
|
|
1278
|
+
const text3 = row === null ? null : str(row["text"]);
|
|
1279
|
+
if (row === null || text3 === null) return null;
|
|
1280
|
+
return { text: text3, by: str(row["by"]) ?? "", at: str(row["at"]) ?? "" };
|
|
1281
|
+
}
|
|
1282
|
+
function planFromRow(row) {
|
|
1283
|
+
const id = str(row["id"]);
|
|
1284
|
+
if (id === null) return null;
|
|
1285
|
+
const rooms = [];
|
|
1286
|
+
for (const raw of Array.isArray(row["rooms"]) ? row["rooms"] : []) {
|
|
1287
|
+
const room = obj(raw);
|
|
1288
|
+
const roomId = room === null ? null : str(room["id"]);
|
|
1289
|
+
if (room === null || roomId === null) continue;
|
|
1290
|
+
rooms.push({ id: roomId, name: str(room["name"]) ?? roomId, identities: strings(room["identities"]) });
|
|
1291
|
+
}
|
|
1292
|
+
return {
|
|
1293
|
+
id,
|
|
1294
|
+
state: row["state"] === "open" ? "open" : "closed",
|
|
1295
|
+
rooms,
|
|
1296
|
+
allowChoose: row["allow_choose"] === true,
|
|
1297
|
+
minutes: num(row["minutes"]) ?? 0,
|
|
1298
|
+
openedAt: str(row["opened_at"]),
|
|
1299
|
+
endsAt: str(row["ends_at"]),
|
|
1300
|
+
closedAt: str(row["closed_at"]),
|
|
1301
|
+
broadcasts: (Array.isArray(row["broadcasts"]) ? row["broadcasts"] : []).map(broadcastFrom).filter((b) => b !== null)
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
function settingsFromRow(row) {
|
|
1305
|
+
if (row === null) return null;
|
|
1306
|
+
return {
|
|
1307
|
+
pollsEnabled: row["polls_enabled"] === true,
|
|
1308
|
+
pollMaxOptions: num(row["poll_max_options"]) ?? 10,
|
|
1309
|
+
qaEnabled: row["qa_enabled"] === true,
|
|
1310
|
+
qaAnonymousAllowed: row["qa_anonymous_allowed"] === true,
|
|
1311
|
+
whiteboardEnabled: row["whiteboard_enabled"] === true,
|
|
1312
|
+
breakoutsEnabled: row["breakouts_enabled"] === true,
|
|
1313
|
+
breakoutMaxRooms: num(row["breakout_max_rooms"]) ?? 20,
|
|
1314
|
+
breakoutDefaultMinutes: num(row["breakout_default_minutes"]) ?? 15,
|
|
1315
|
+
captionTranslationEnabled: row["caption_translation_enabled"] === true,
|
|
1316
|
+
captionTranslationLanguage: str(row["caption_translation_language"]) ?? ""
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function collabFromState(body, previous) {
|
|
1320
|
+
const list = (key, parse) => (Array.isArray(body[key]) ? body[key] : []).map((raw) => {
|
|
1321
|
+
const row = obj(raw);
|
|
1322
|
+
return row === null ? null : parse(row);
|
|
1323
|
+
}).filter((item) => item !== null);
|
|
1324
|
+
const breakoutRow = obj(body["breakout"]);
|
|
1325
|
+
return {
|
|
1326
|
+
...previous,
|
|
1327
|
+
loaded: true,
|
|
1328
|
+
error: null,
|
|
1329
|
+
settings: settingsFromRow(obj(body["settings"])),
|
|
1330
|
+
polls: list("polls", pollFromRow),
|
|
1331
|
+
questions: list("questions", questionFromRow),
|
|
1332
|
+
strokes: list("strokes", strokeFromRow),
|
|
1333
|
+
breakout: breakoutRow === null ? null : planFromRow(breakoutRow)
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
function upsert(items, next) {
|
|
1337
|
+
const index = items.findIndex((item) => item.id === next.id);
|
|
1338
|
+
if (index === -1) return [...items, next];
|
|
1339
|
+
const copy = items.slice();
|
|
1340
|
+
copy[index] = next;
|
|
1341
|
+
return copy;
|
|
1342
|
+
}
|
|
1343
|
+
function withItem(collab, row, fromRoom = false) {
|
|
1344
|
+
const kind = str(row["kind"]);
|
|
1345
|
+
if (kind === "poll") {
|
|
1346
|
+
const poll = pollFromRow(row);
|
|
1347
|
+
if (poll === null) return collab;
|
|
1348
|
+
const known = collab.polls.find((p) => p.id === poll.id);
|
|
1349
|
+
const merged = fromRoom && known !== void 0 ? { ...poll, myVote: known.myVote } : poll;
|
|
1350
|
+
return { ...collab, polls: upsert(collab.polls, merged) };
|
|
1351
|
+
}
|
|
1352
|
+
if (kind === "question") {
|
|
1353
|
+
const question = questionFromRow(row);
|
|
1354
|
+
if (question === null) return collab;
|
|
1355
|
+
const known = collab.questions.find((q) => q.id === question.id);
|
|
1356
|
+
const merged = fromRoom && known !== void 0 ? { ...question, mine: known.mine, upvotedByMe: known.upvotedByMe } : question;
|
|
1357
|
+
return { ...collab, questions: upsert(collab.questions, merged) };
|
|
1358
|
+
}
|
|
1359
|
+
if (kind === "stroke") {
|
|
1360
|
+
const stroke = strokeFromRow(row);
|
|
1361
|
+
if (stroke === null) return collab;
|
|
1362
|
+
return { ...collab, strokes: upsert(collab.strokes, stroke) };
|
|
1363
|
+
}
|
|
1364
|
+
if (kind === "breakout") {
|
|
1365
|
+
const plan = planFromRow(row);
|
|
1366
|
+
if (plan === null) return collab;
|
|
1367
|
+
return { ...collab, breakout: plan };
|
|
1368
|
+
}
|
|
1369
|
+
return collab;
|
|
1370
|
+
}
|
|
1371
|
+
function without(collab, ids) {
|
|
1372
|
+
const gone = new Set(ids);
|
|
1373
|
+
return {
|
|
1374
|
+
...collab,
|
|
1375
|
+
polls: collab.polls.filter((p) => !gone.has(p.id)),
|
|
1376
|
+
questions: collab.questions.filter((q) => !gone.has(q.id)),
|
|
1377
|
+
strokes: collab.strokes.filter((s) => !gone.has(s.id))
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
function applyCollabPacket(collab, payload, context) {
|
|
1381
|
+
const item = obj(payload["item"]);
|
|
1382
|
+
const audience = str(payload["audience"]);
|
|
1383
|
+
if (item !== null) {
|
|
1384
|
+
const kind = str(item["kind"]);
|
|
1385
|
+
const hostOnlyView = kind === "poll" || kind === "question";
|
|
1386
|
+
if (hostOnlyView && context.isHost && audience !== "hosts") {
|
|
1387
|
+
return { next: null, dropped: "the room copy of an item a host receives in full" };
|
|
1388
|
+
}
|
|
1389
|
+
let next = withItem(collab, item, true);
|
|
1390
|
+
if (kind === "poll" && str(item["state"]) === "live") {
|
|
1391
|
+
const before = collab.polls.find((p) => p.id === str(item["id"]));
|
|
1392
|
+
if (before === void 0 || before.state !== "live") {
|
|
1393
|
+
next = { ...next, panelRequest: { panel: "polls", at: context.now } };
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
if (kind === "breakout") {
|
|
1397
|
+
const plan = planFromRow(item);
|
|
1398
|
+
if (plan !== null && plan.state === "closed") {
|
|
1399
|
+
next = { ...next, moveRequest: { roomId: null, at: context.now } };
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
const move = obj(payload["move"]);
|
|
1403
|
+
if (move !== null && str(move["identity"]) === context.localIdentity) {
|
|
1404
|
+
next = { ...next, moveRequest: { roomId: str(move["room_id"]), at: context.now } };
|
|
1405
|
+
}
|
|
1406
|
+
return { next };
|
|
1407
|
+
}
|
|
1408
|
+
if (Array.isArray(payload["removed"])) {
|
|
1409
|
+
return { next: without(collab, strings(payload["removed"])) };
|
|
1410
|
+
}
|
|
1411
|
+
if (Array.isArray(payload["hidden"])) {
|
|
1412
|
+
return context.isHost ? { next: null, dropped: "hosts keep dismissed questions" } : { next: without(collab, strings(payload["hidden"])) };
|
|
1413
|
+
}
|
|
1414
|
+
if (payload["cleared"] === "whiteboard") {
|
|
1415
|
+
return { next: { ...collab, strokes: [] } };
|
|
1416
|
+
}
|
|
1417
|
+
const broadcast = broadcastFrom(payload["broadcast"]);
|
|
1418
|
+
if (broadcast !== null) {
|
|
1419
|
+
return { next: { ...collab, broadcast } };
|
|
1420
|
+
}
|
|
1421
|
+
return { next: null, dropped: "a collab packet with nothing this version understands" };
|
|
1422
|
+
}
|
|
1423
|
+
function assignedRoom(plan, identity) {
|
|
1424
|
+
if (plan === null || plan.state !== "open" || identity === null) return null;
|
|
1425
|
+
return plan.rooms.find((room) => room.identities.includes(identity)) ?? null;
|
|
1426
|
+
}
|
|
1427
|
+
function autoAssign(identities, rooms, exclude = []) {
|
|
1428
|
+
const count2 = Math.max(1, Math.floor(rooms));
|
|
1429
|
+
const out = Array.from({ length: count2 }, () => []);
|
|
1430
|
+
const skip = new Set(exclude);
|
|
1431
|
+
let index = 0;
|
|
1432
|
+
for (const identity of identities) {
|
|
1433
|
+
if (skip.has(identity)) continue;
|
|
1434
|
+
out[index % count2].push(identity);
|
|
1435
|
+
index += 1;
|
|
1436
|
+
}
|
|
1437
|
+
return out;
|
|
1438
|
+
}
|
|
1439
|
+
function breakoutRemainingMs(plan, now) {
|
|
1440
|
+
if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
|
|
1441
|
+
const end = Date.parse(plan.endsAt);
|
|
1442
|
+
return Number.isNaN(end) ? null : Math.max(0, end - now);
|
|
1443
|
+
}
|
|
1444
|
+
function defaultCaptionLanguage(settings, viewerLanguage) {
|
|
1445
|
+
const configured = settings?.captionTranslationLanguage.trim() ?? "";
|
|
1446
|
+
if (configured.length > 0) return configured;
|
|
1447
|
+
const own = (viewerLanguage ?? "").trim();
|
|
1448
|
+
return own.length > 0 ? own : "en";
|
|
1449
|
+
}
|
|
1450
|
+
var CAPTION_LANGUAGES = [
|
|
1451
|
+
{ tag: "en", name: "English" },
|
|
1452
|
+
{ tag: "es", name: "Spanish" },
|
|
1453
|
+
{ tag: "fr", name: "French" },
|
|
1454
|
+
{ tag: "de", name: "German" },
|
|
1455
|
+
{ tag: "it", name: "Italian" },
|
|
1456
|
+
{ tag: "pt", name: "Portuguese" },
|
|
1457
|
+
{ tag: "nl", name: "Dutch" },
|
|
1458
|
+
{ tag: "pl", name: "Polish" },
|
|
1459
|
+
{ tag: "ru", name: "Russian" },
|
|
1460
|
+
{ tag: "uk", name: "Ukrainian" },
|
|
1461
|
+
{ tag: "tr", name: "Turkish" },
|
|
1462
|
+
{ tag: "ar", name: "Arabic" },
|
|
1463
|
+
{ tag: "he", name: "Hebrew" },
|
|
1464
|
+
{ tag: "fa", name: "Persian" },
|
|
1465
|
+
{ tag: "hi", name: "Hindi" },
|
|
1466
|
+
{ tag: "bn", name: "Bengali" },
|
|
1467
|
+
{ tag: "zh", name: "Chinese" },
|
|
1468
|
+
{ tag: "ja", name: "Japanese" },
|
|
1469
|
+
{ tag: "ko", name: "Korean" },
|
|
1470
|
+
{ tag: "vi", name: "Vietnamese" },
|
|
1471
|
+
{ tag: "th", name: "Thai" },
|
|
1472
|
+
{ tag: "id", name: "Indonesian" },
|
|
1473
|
+
{ tag: "sv", name: "Swedish" }
|
|
1474
|
+
];
|
|
1475
|
+
async function loadMeetingActivities(api, meetingId, roomToken = null) {
|
|
1476
|
+
let response;
|
|
1477
|
+
try {
|
|
1478
|
+
response = await api.request({
|
|
1479
|
+
path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(meetingId)}`,
|
|
1480
|
+
method: "GET",
|
|
1481
|
+
operation: "meet.loadMeetingActivities",
|
|
1482
|
+
requireAuth: roomToken === null,
|
|
1483
|
+
...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
|
|
1484
|
+
});
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
return {
|
|
1487
|
+
collab: null,
|
|
1488
|
+
refusal: {
|
|
1489
|
+
message: `Polls, questions and the whiteboard could not be read (${error.message}).`,
|
|
1490
|
+
remedy: error.remedy ?? "Reload the page."
|
|
1491
|
+
}
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
if (!response.ok) {
|
|
1495
|
+
let body = null;
|
|
1496
|
+
try {
|
|
1497
|
+
body = await response.json();
|
|
1498
|
+
} catch {
|
|
1499
|
+
body = null;
|
|
1500
|
+
}
|
|
1501
|
+
const said = readServerRefusal(body);
|
|
1502
|
+
return {
|
|
1503
|
+
collab: null,
|
|
1504
|
+
refusal: {
|
|
1505
|
+
message: said?.reason ?? `Polls and questions could not be read (the server answered ${response.status}).`,
|
|
1506
|
+
remedy: said?.remedy || "Reload the page."
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
return { collab: collabFromState(await response.json(), EMPTY_COLLAB), refusal: null };
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1169
1513
|
// src/core/repository.ts
|
|
1170
1514
|
var MEET_SCHEMA = "communication";
|
|
1171
1515
|
var TABLES = {
|
|
@@ -1241,7 +1585,19 @@ var INVITEE_COLUMNS = "id,meeting_id,invitee_user_id,email,display_name,role,rsv
|
|
|
1241
1585
|
var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
|
|
1242
1586
|
var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
|
|
1243
1587
|
var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
|
|
1244
|
-
var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,is_agent,joined_at,left_at";
|
|
1588
|
+
var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,is_agent,joined_at,left_at,sessions:metadata->sessions";
|
|
1589
|
+
function projectStays(value) {
|
|
1590
|
+
if (!Array.isArray(value)) return [];
|
|
1591
|
+
const stays = [];
|
|
1592
|
+
for (const item of value) {
|
|
1593
|
+
if (item === null || typeof item !== "object") continue;
|
|
1594
|
+
const joined = item["joined_at"];
|
|
1595
|
+
const left = item["left_at"];
|
|
1596
|
+
if (typeof joined !== "string" || joined.length === 0) continue;
|
|
1597
|
+
stays.push({ joinedAt: joined, leftAt: typeof left === "string" && left.length > 0 ? left : null });
|
|
1598
|
+
}
|
|
1599
|
+
return stays;
|
|
1600
|
+
}
|
|
1245
1601
|
function text(row, key) {
|
|
1246
1602
|
const value = row[key];
|
|
1247
1603
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
@@ -1340,7 +1696,7 @@ var OCCURRENCE_STATES = ["scheduled", "moved", "cancelled"];
|
|
|
1340
1696
|
function rsvpState(value) {
|
|
1341
1697
|
return RSVP_STATES.includes(value) ? value : "needs_action";
|
|
1342
1698
|
}
|
|
1343
|
-
function
|
|
1699
|
+
function num2(row, key) {
|
|
1344
1700
|
const value = row[key];
|
|
1345
1701
|
return typeof value === "number" ? value : null;
|
|
1346
1702
|
}
|
|
@@ -1362,7 +1718,7 @@ function projectInvitee(row) {
|
|
|
1362
1718
|
rsvpState: rsvpState(text(row, "rsvp_state")),
|
|
1363
1719
|
respondedAt: when(row, "responded_at"),
|
|
1364
1720
|
rsvpNote: text(row, "rsvp_note"),
|
|
1365
|
-
lastSentSequence:
|
|
1721
|
+
lastSentSequence: num2(row, "last_sent_sequence"),
|
|
1366
1722
|
lastSentAt: when(row, "last_sent_at")
|
|
1367
1723
|
};
|
|
1368
1724
|
}
|
|
@@ -1373,10 +1729,10 @@ function projectOccurrence(row) {
|
|
|
1373
1729
|
meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
|
|
1374
1730
|
originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
|
|
1375
1731
|
occurrenceStart: when(row, "occurrence_start") ?? requireText(row, "occurrence_start", operation),
|
|
1376
|
-
durationMinutes:
|
|
1732
|
+
durationMinutes: num2(row, "duration_minutes"),
|
|
1377
1733
|
// An unknown state is shown as scheduled, never dropped: the row is real.
|
|
1378
1734
|
state: OCCURRENCE_STATES.includes(state) ? state : "scheduled",
|
|
1379
|
-
sequence:
|
|
1735
|
+
sequence: num2(row, "sequence") ?? 0
|
|
1380
1736
|
};
|
|
1381
1737
|
}
|
|
1382
1738
|
function projectUpcoming(row) {
|
|
@@ -1405,9 +1761,9 @@ function projectOverride(row) {
|
|
|
1405
1761
|
originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
|
|
1406
1762
|
state: text(row, "state") === "moved" ? "moved" : "cancelled",
|
|
1407
1763
|
newStart: when(row, "new_start"),
|
|
1408
|
-
newDurationMinutes:
|
|
1764
|
+
newDurationMinutes: num2(row, "new_duration_minutes"),
|
|
1409
1765
|
reason: text(row, "reason"),
|
|
1410
|
-
calendarSequence:
|
|
1766
|
+
calendarSequence: num2(row, "calendar_sequence") ?? 0,
|
|
1411
1767
|
deletedAt: when(row, "deleted_at")
|
|
1412
1768
|
};
|
|
1413
1769
|
}
|
|
@@ -1431,7 +1787,7 @@ function projectRsvpInvitation(body) {
|
|
|
1431
1787
|
title: text(meeting, "title") ?? "Meeting",
|
|
1432
1788
|
slug: requireText(meeting, "slug", operation),
|
|
1433
1789
|
scheduledFor: when(meeting, "scheduled_for"),
|
|
1434
|
-
durationMinutes:
|
|
1790
|
+
durationMinutes: num2(meeting, "duration_minutes"),
|
|
1435
1791
|
timeZone: text(meeting, "time_zone") ?? "UTC",
|
|
1436
1792
|
agenda: text(meeting, "agenda"),
|
|
1437
1793
|
recurrenceRule: text(meeting, "recurrence_rule"),
|
|
@@ -1465,6 +1821,7 @@ function changesPatch(changes) {
|
|
|
1465
1821
|
patch["scheduled_duration_minutes"] = changes.scheduledDurationMinutes;
|
|
1466
1822
|
}
|
|
1467
1823
|
if (changes.recurrenceRule !== void 0) patch["recurrence_rule"] = changes.recurrenceRule;
|
|
1824
|
+
if (changes.keepExceptions !== void 0) patch["keep_exceptions"] = changes.keepExceptions;
|
|
1468
1825
|
return patch;
|
|
1469
1826
|
}
|
|
1470
1827
|
function createMeetRepository(options) {
|
|
@@ -1611,7 +1968,8 @@ function createMeetRepository(options) {
|
|
|
1611
1968
|
admissionState: states.includes(admission) ? admission : "admitted",
|
|
1612
1969
|
isAgent: bool(row, "is_agent", false),
|
|
1613
1970
|
joinedAt: text(row, "joined_at"),
|
|
1614
|
-
leftAt: text(row, "left_at")
|
|
1971
|
+
leftAt: text(row, "left_at"),
|
|
1972
|
+
sessions: projectStays(row["sessions"])
|
|
1615
1973
|
};
|
|
1616
1974
|
}
|
|
1617
1975
|
function requireOrg(organizationId, operation) {
|
|
@@ -2005,8 +2363,8 @@ function projectLibraryRecording(row) {
|
|
|
2005
2363
|
fileId: text(row, "file_id"),
|
|
2006
2364
|
startedAt: text(row, "started_at"),
|
|
2007
2365
|
endedAt: text(row, "ended_at"),
|
|
2008
|
-
durationMs:
|
|
2009
|
-
sizeBytes:
|
|
2366
|
+
durationMs: num2(row, "duration_ms"),
|
|
2367
|
+
sizeBytes: num2(row, "size_bytes"),
|
|
2010
2368
|
failureReason: text(row, "failure_reason"),
|
|
2011
2369
|
archivedAt: text(row, "deleted_at"),
|
|
2012
2370
|
organizationId: asOrganizationId(requireText(row, "organization_id", operation))
|
|
@@ -2033,7 +2391,7 @@ function projectSearchHit(row) {
|
|
|
2033
2391
|
speaker: text(row, "speaker"),
|
|
2034
2392
|
snippet: text(row, "snippet") ?? "",
|
|
2035
2393
|
at: text(row, "said_at"),
|
|
2036
|
-
rank:
|
|
2394
|
+
rank: num2(row, "rank") ?? 0
|
|
2037
2395
|
};
|
|
2038
2396
|
}
|
|
2039
2397
|
function snippetRuns(snippet) {
|
|
@@ -2050,1386 +2408,1460 @@ function snippetRuns(snippet) {
|
|
|
2050
2408
|
return runs;
|
|
2051
2409
|
}
|
|
2052
2410
|
|
|
2053
|
-
// src/core/
|
|
2054
|
-
|
|
2055
|
-
"
|
|
2056
|
-
"starting",
|
|
2057
|
-
"recording",
|
|
2058
|
-
"stopping",
|
|
2059
|
-
"processing",
|
|
2060
|
-
"available",
|
|
2061
|
-
"expired",
|
|
2062
|
-
"failed"
|
|
2063
|
-
];
|
|
2064
|
-
var ROOM_EVENT_PREFIX = "matrx.meet.";
|
|
2065
|
-
var ROOM_EVENTS = {
|
|
2066
|
-
/** One durable `meet_notes` row was written. Payload `{ note: <row> }`. */
|
|
2067
|
-
note: "matrx.meet.note",
|
|
2068
|
-
/** `meet_meetings.metadata.note_taker` changed. Payload `{ meeting_id, note_taker }`. */
|
|
2069
|
-
noteTaker: "matrx.meet.note_taker",
|
|
2070
|
-
/** A wrap-up pass finished. Payload `{ meeting_id, note_ids, segment_count }`. */
|
|
2071
|
-
wrapUp: "matrx.meet.wrap_up",
|
|
2072
|
-
/** The meeting ended for everyone. Payload `{ meeting_id, ended_at, trigger }`. */
|
|
2073
|
-
ended: "matrx.meet.ended",
|
|
2074
|
-
/**
|
|
2075
|
-
* A `meet_recordings` row changed state. Payload
|
|
2076
|
-
* `{ meeting_id, recording: { id, state, started_by } }`.
|
|
2077
|
-
*
|
|
2078
|
-
* 🚨 THIS IS THE ONLY WAY A GUEST LEARNS A RECORDING IS RUNNING. PreJoin
|
|
2079
|
-
* promises every joiner an indicator, and `anon` holds no SELECT grant on
|
|
2080
|
-
* `meet_recordings` (MRI-A10 settled that widening it is forbidden), so
|
|
2081
|
-
* without this packet the promise was a sentence with nothing behind it: on
|
|
2082
|
-
* production 2026-09-09 the guest lane showed no indicator at all while the
|
|
2083
|
-
* host recorded (MRI-C8).
|
|
2084
|
-
*/
|
|
2085
|
-
recording: "matrx.meet.recording",
|
|
2086
|
-
/**
|
|
2087
|
-
* A chat message to everyone was SAVED (Meet wave 2). Payload
|
|
2088
|
-
* `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
|
|
2089
|
-
*/
|
|
2090
|
-
chat: "matrx.meet.chat",
|
|
2091
|
-
/**
|
|
2092
|
-
* A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
|
|
2093
|
-
* `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
|
|
2094
|
-
* person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
|
|
2095
|
-
* role}` — to everyone.
|
|
2096
|
-
*/
|
|
2097
|
-
moderation: "matrx.meet.moderation"
|
|
2098
|
-
};
|
|
2099
|
-
function isRoomEventTopic(topic) {
|
|
2100
|
-
return topic.startsWith(ROOM_EVENT_PREFIX);
|
|
2101
|
-
}
|
|
2102
|
-
function object(value) {
|
|
2103
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
2411
|
+
// src/core/names.ts
|
|
2412
|
+
function isGuestIdentity(identity) {
|
|
2413
|
+
return String(identity).startsWith("guest:");
|
|
2104
2414
|
}
|
|
2105
|
-
function
|
|
2106
|
-
return
|
|
2415
|
+
function isAgentIdentity(identity) {
|
|
2416
|
+
return String(identity).startsWith("agent:");
|
|
2107
2417
|
}
|
|
2108
|
-
var
|
|
2109
|
-
|
|
2110
|
-
|
|
2418
|
+
var NOTE_TAKER_PRODUCT_NAME = "Matrx note-taker";
|
|
2419
|
+
var AGENT_PRODUCT_NAME = "Matrx agent";
|
|
2420
|
+
var UNNAMED_GUEST = "Unnamed guest";
|
|
2421
|
+
var UNNAMED_PARTICIPANT = "Unnamed participant";
|
|
2422
|
+
var PLACEHOLDER_NAMES = [
|
|
2423
|
+
NOTE_TAKER_PRODUCT_NAME,
|
|
2424
|
+
AGENT_PRODUCT_NAME,
|
|
2425
|
+
UNNAMED_GUEST,
|
|
2426
|
+
UNNAMED_PARTICIPANT
|
|
2427
|
+
];
|
|
2428
|
+
function isPlaceholderName(name) {
|
|
2429
|
+
return typeof name === "string" && PLACEHOLDER_NAMES.includes(name.trim());
|
|
2111
2430
|
}
|
|
2112
|
-
function
|
|
2113
|
-
const
|
|
2114
|
-
if (
|
|
2115
|
-
return
|
|
2116
|
-
}
|
|
2117
|
-
const payload = object(args.payload);
|
|
2118
|
-
if (payload === null) {
|
|
2119
|
-
return ignored(`${topic} carried no object payload`);
|
|
2120
|
-
}
|
|
2121
|
-
const now = args.now ?? (() => Date.now());
|
|
2122
|
-
switch (topic) {
|
|
2123
|
-
case ROOM_EVENTS.note:
|
|
2124
|
-
return applyNote(store, payload);
|
|
2125
|
-
case ROOM_EVENTS.noteTaker:
|
|
2126
|
-
return applyNoteTaker(store, payload);
|
|
2127
|
-
case ROOM_EVENTS.wrapUp:
|
|
2128
|
-
return applyWrapUp(store, payload, now);
|
|
2129
|
-
case ROOM_EVENTS.ended:
|
|
2130
|
-
return applyEnded(store, payload);
|
|
2131
|
-
case ROOM_EVENTS.recording:
|
|
2132
|
-
return applyRecording(store, payload);
|
|
2133
|
-
case ROOM_EVENTS.chat:
|
|
2134
|
-
return applyChat(store, payload);
|
|
2135
|
-
case ROOM_EVENTS.moderation:
|
|
2136
|
-
return applyModeration(store, payload, now);
|
|
2137
|
-
default:
|
|
2138
|
-
return ignored(`${topic} is not known to this version of @ai-matrx/meet`);
|
|
2431
|
+
function placeholderNameFor(identity) {
|
|
2432
|
+
const key = String(identity);
|
|
2433
|
+
if (isAgentIdentity(key)) {
|
|
2434
|
+
return key.startsWith("agent:note_taker:") || key.startsWith("agent:note-taker") ? NOTE_TAKER_PRODUCT_NAME : AGENT_PRODUCT_NAME;
|
|
2139
2435
|
}
|
|
2436
|
+
return isGuestIdentity(key) ? UNNAMED_GUEST : UNNAMED_PARTICIPANT;
|
|
2140
2437
|
}
|
|
2141
|
-
function
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
if (
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
if (meeting !== null && row["meeting_id"] !== meeting.id) {
|
|
2149
|
-
return ignored("a note for a different meeting than this room is showing");
|
|
2150
|
-
}
|
|
2151
|
-
try {
|
|
2152
|
-
store.upsertNote({ ...projectNote(row), source: "room" });
|
|
2153
|
-
} catch (error) {
|
|
2154
|
-
return ignored(`a note row this client could not read: ${String(error)}`);
|
|
2155
|
-
}
|
|
2156
|
-
return HANDLED;
|
|
2438
|
+
function isRealName(identity, name) {
|
|
2439
|
+
if (typeof name !== "string") return false;
|
|
2440
|
+
const trimmed = name.trim();
|
|
2441
|
+
if (trimmed.length === 0) return false;
|
|
2442
|
+
if (trimmed === String(identity)) return false;
|
|
2443
|
+
if (isPlaceholderName(trimmed)) return false;
|
|
2444
|
+
return true;
|
|
2157
2445
|
}
|
|
2158
|
-
function
|
|
2159
|
-
const
|
|
2160
|
-
if (
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
}
|
|
2165
|
-
const meetingId = text2(payload["meeting_id"]);
|
|
2166
|
-
if (meetingId !== null && meetingId !== meeting.id) {
|
|
2167
|
-
return ignored("a note-taker state for a different meeting");
|
|
2168
|
-
}
|
|
2169
|
-
const next = {
|
|
2170
|
-
...meeting,
|
|
2171
|
-
metadata: { ...meeting.metadata, note_taker: noteTaker }
|
|
2172
|
-
};
|
|
2173
|
-
store.patch({ meeting: next });
|
|
2174
|
-
return HANDLED;
|
|
2446
|
+
function learnName(book, identity, name) {
|
|
2447
|
+
const key = String(identity);
|
|
2448
|
+
if (key.length === 0) return book;
|
|
2449
|
+
if (!isRealName(key, name)) return book;
|
|
2450
|
+
const learned = name.trim();
|
|
2451
|
+
if (book[key] === learned) return book;
|
|
2452
|
+
return { ...book, [key]: learned };
|
|
2175
2453
|
}
|
|
2176
|
-
function
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
if (meeting !== null && meetingId !== meeting.id) {
|
|
2181
|
-
return ignored("a wrap-up for a different meeting");
|
|
2182
|
-
}
|
|
2183
|
-
const rawIds = payload["note_ids"];
|
|
2184
|
-
const noteIds = Array.isArray(rawIds) ? rawIds.filter((value) => typeof value === "string") : [];
|
|
2185
|
-
const segmentCount = payload["segment_count"];
|
|
2186
|
-
const announcement = {
|
|
2187
|
-
meetingId: asMeetingId(meetingId),
|
|
2188
|
-
noteIds,
|
|
2189
|
-
segmentCount: typeof segmentCount === "number" ? segmentCount : 0,
|
|
2190
|
-
at: new Date(now()).toISOString()
|
|
2191
|
-
};
|
|
2192
|
-
store.announceWrapUp(announcement);
|
|
2193
|
-
return HANDLED;
|
|
2194
|
-
}
|
|
2195
|
-
function applyRecording(store, payload) {
|
|
2196
|
-
const row = object(payload["recording"]);
|
|
2197
|
-
if (row === null) return ignored("a recording event with no `recording` row");
|
|
2198
|
-
const meetingId = text2(payload["meeting_id"]);
|
|
2199
|
-
const meeting = store.snapshot().meeting;
|
|
2200
|
-
if (meeting !== null && meetingId !== null && meetingId !== meeting.id) {
|
|
2201
|
-
return ignored("a recording state for a different meeting than this room is showing");
|
|
2202
|
-
}
|
|
2203
|
-
const state = text2(row["state"]);
|
|
2204
|
-
if (state === null || !RECORDING_STATES.includes(state)) {
|
|
2205
|
-
return ignored(`a recording state this version does not know: ${String(row["state"])}`);
|
|
2206
|
-
}
|
|
2207
|
-
const by = text2(row["started_by"]);
|
|
2208
|
-
store.setRecording(
|
|
2209
|
-
state,
|
|
2210
|
-
by === null ? null : by,
|
|
2211
|
-
"room",
|
|
2212
|
-
text2(row["id"])
|
|
2213
|
-
);
|
|
2214
|
-
return HANDLED;
|
|
2215
|
-
}
|
|
2216
|
-
function applyEnded(store, payload) {
|
|
2217
|
-
const meetingId = text2(payload["meeting_id"]);
|
|
2218
|
-
const meeting = store.snapshot().meeting;
|
|
2219
|
-
if (meeting === null) return ignored("an ended event with no meeting record");
|
|
2220
|
-
if (meetingId !== null && meetingId !== meeting.id) {
|
|
2221
|
-
return ignored("an ended event for a different meeting");
|
|
2222
|
-
}
|
|
2223
|
-
const endedAt = text2(payload["ended_at"]) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2224
|
-
if (meeting.endedAt === endedAt) return HANDLED;
|
|
2225
|
-
store.patch({ meeting: { ...meeting, endedAt } });
|
|
2226
|
-
return HANDLED;
|
|
2227
|
-
}
|
|
2228
|
-
function chatFromRow(row) {
|
|
2229
|
-
const id = text2(row["id"]);
|
|
2230
|
-
const identity = text2(row["sender_identity"]);
|
|
2231
|
-
const body = text2(row["body"]);
|
|
2232
|
-
if (id === null || identity === null || body === null) return null;
|
|
2233
|
-
return {
|
|
2234
|
-
id,
|
|
2235
|
-
identity: asParticipantIdentity(identity),
|
|
2236
|
-
displayName: text2(row["sender_name"]) ?? "Participant",
|
|
2237
|
-
text: body,
|
|
2238
|
-
at: text2(row["created_at"]) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2239
|
-
// `false` only from a server whose chat table has not landed: the room heard
|
|
2240
|
-
// it, nobody will see it later — and the line says so.
|
|
2241
|
-
saved: row["saved"] !== false
|
|
2242
|
-
};
|
|
2243
|
-
}
|
|
2244
|
-
function applyChat(store, payload) {
|
|
2245
|
-
const row = object(payload["message"]);
|
|
2246
|
-
if (row === null) return ignored("a chat event with no `message` row");
|
|
2247
|
-
const meeting = store.snapshot().meeting;
|
|
2248
|
-
if (meeting !== null && text2(row["meeting_id"]) !== null && row["meeting_id"] !== meeting.id) {
|
|
2249
|
-
return ignored("a chat message for a different meeting");
|
|
2250
|
-
}
|
|
2251
|
-
const message = chatFromRow(row);
|
|
2252
|
-
if (message === null) return ignored("a chat row with no id, sender or body");
|
|
2253
|
-
store.addChat(message);
|
|
2254
|
-
return HANDLED;
|
|
2454
|
+
function learnNames(book, entries) {
|
|
2455
|
+
let next = book;
|
|
2456
|
+
for (const entry of entries) next = learnName(next, entry.identity, entry.displayName);
|
|
2457
|
+
return next;
|
|
2255
2458
|
}
|
|
2256
|
-
function
|
|
2257
|
-
const
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
}
|
|
2263
|
-
case "ask-unmute":
|
|
2264
|
-
return `${by} is asking you to unmute.`;
|
|
2265
|
-
case "removed":
|
|
2266
|
-
return `${by} removed you from this meeting.`;
|
|
2267
|
-
default:
|
|
2268
|
-
return null;
|
|
2269
|
-
}
|
|
2459
|
+
function displayNameFor(book, identity, preferred) {
|
|
2460
|
+
const key = String(identity);
|
|
2461
|
+
if (isRealName(key, preferred)) return preferred.trim();
|
|
2462
|
+
const known = book[key];
|
|
2463
|
+
if (known !== void 0) return known;
|
|
2464
|
+
return placeholderNameFor(key);
|
|
2270
2465
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2466
|
+
|
|
2467
|
+
// src/core/recording.ts
|
|
2468
|
+
function createRecordingController(options) {
|
|
2469
|
+
async function post(route, body) {
|
|
2470
|
+
let response;
|
|
2471
|
+
try {
|
|
2472
|
+
response = await options.api.request({
|
|
2473
|
+
path: route,
|
|
2474
|
+
method: "POST",
|
|
2475
|
+
// A GUEST CAN NEVER START OR STOP A RECORDING (R5) — refused here,
|
|
2476
|
+
// before the wire, with the same sentence the hidden control implies.
|
|
2477
|
+
requireAuth: true,
|
|
2478
|
+
operation: "meet.recording",
|
|
2479
|
+
body
|
|
2480
|
+
});
|
|
2481
|
+
} catch (error) {
|
|
2482
|
+
if (error.code === "session-unavailable") {
|
|
2483
|
+
throw new MeetError(
|
|
2484
|
+
"forbidden",
|
|
2485
|
+
"meet.recording: recording requires an authenticated host",
|
|
2486
|
+
"A guest can never start or stop a recording (R5). The control is hidden for guests."
|
|
2487
|
+
);
|
|
2289
2488
|
}
|
|
2290
|
-
|
|
2489
|
+
throw error;
|
|
2291
2490
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
if (identity === snapshot.localIdentity) {
|
|
2299
|
-
store.patch({
|
|
2300
|
-
grants: { ...snapshot.grants, role },
|
|
2301
|
-
moderationNotice: {
|
|
2302
|
-
kind: "role",
|
|
2303
|
-
text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
|
|
2304
|
-
at: now()
|
|
2305
|
-
}
|
|
2306
|
-
});
|
|
2307
|
-
}
|
|
2308
|
-
return HANDLED;
|
|
2491
|
+
if (response.status === 403) {
|
|
2492
|
+
throw new MeetError(
|
|
2493
|
+
"forbidden",
|
|
2494
|
+
"meet.recording: the server refused this recording action",
|
|
2495
|
+
"The actor is not a host of this meeting, or the org policy is `disabled`. The server is the authority; do not re-decide it on the client."
|
|
2496
|
+
);
|
|
2309
2497
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
return HANDLED;
|
|
2498
|
+
if (!response.ok) {
|
|
2499
|
+
throw new MeetError(
|
|
2500
|
+
"transport",
|
|
2501
|
+
`meet.recording: the server returned ${response.status}`,
|
|
2502
|
+
"Egress could not be controlled. The recording state stays where it was rather than showing a recording indicator for a recording that is not running."
|
|
2503
|
+
);
|
|
2317
2504
|
}
|
|
2318
|
-
default:
|
|
2319
|
-
return ignored(`a moderation kind this version does not know: ${String(kind)}`);
|
|
2320
2505
|
}
|
|
2321
|
-
}
|
|
2322
|
-
var ROLES = ["host", "cohost", "participant", "guest"];
|
|
2323
|
-
|
|
2324
|
-
// src/core/devices.ts
|
|
2325
|
-
var KINDS = ["audioinput", "videoinput", "audiooutput"];
|
|
2326
|
-
function isDeviceKind(value) {
|
|
2327
|
-
return value === "audioinput" || value === "videoinput" || value === "audiooutput";
|
|
2328
|
-
}
|
|
2329
|
-
function createWebDeviceStorage(key = "mx.meet.devices") {
|
|
2330
|
-
const store = globalThis.localStorage;
|
|
2331
|
-
if (store === void 0) return createMemoryDeviceStorage();
|
|
2332
2506
|
return {
|
|
2333
|
-
|
|
2334
|
-
|
|
2507
|
+
canStart(meeting, isHost) {
|
|
2508
|
+
if (meeting === null) return false;
|
|
2509
|
+
if (meeting.recordingPolicy === "disabled") return false;
|
|
2510
|
+
if (meeting.recordingPolicy === "always-on") return false;
|
|
2511
|
+
return isHost;
|
|
2512
|
+
},
|
|
2513
|
+
async start({ meetingId, roomName, by }) {
|
|
2514
|
+
options.store.setRecording("starting", by, "local");
|
|
2335
2515
|
try {
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
} catch {
|
|
2341
|
-
|
|
2516
|
+
await post(MEET_ROUTES.recordingStart, {
|
|
2517
|
+
meeting_id: meetingId,
|
|
2518
|
+
room_name: roomName
|
|
2519
|
+
});
|
|
2520
|
+
} catch (error) {
|
|
2521
|
+
options.store.setRecording("idle", null, "local");
|
|
2522
|
+
throw error;
|
|
2342
2523
|
}
|
|
2524
|
+
options.announce?.("starting", by);
|
|
2343
2525
|
},
|
|
2344
|
-
|
|
2526
|
+
async stop({ meetingId, by }) {
|
|
2527
|
+
options.store.setRecording("stopping", options.store.snapshot().recordingStartedBy, "local");
|
|
2345
2528
|
try {
|
|
2346
|
-
|
|
2347
|
-
} catch {
|
|
2529
|
+
await post(MEET_ROUTES.recordingStop, { meeting_id: meetingId });
|
|
2530
|
+
} catch (error) {
|
|
2531
|
+
options.store.setRecording(
|
|
2532
|
+
"recording",
|
|
2533
|
+
options.store.snapshot().recordingStartedBy,
|
|
2534
|
+
"local"
|
|
2535
|
+
);
|
|
2536
|
+
throw error;
|
|
2537
|
+
}
|
|
2538
|
+
options.announce?.("stopping", by);
|
|
2539
|
+
},
|
|
2540
|
+
consentNotice(meeting) {
|
|
2541
|
+
if (meeting === null || meeting.recordingPolicy === "disabled") return null;
|
|
2542
|
+
if (meeting.recordingPolicy === "always-on") {
|
|
2543
|
+
return "This meeting is recorded and transcribed automatically. By joining, you consent to being recorded.";
|
|
2348
2544
|
}
|
|
2545
|
+
return "The host can record and transcribe this meeting. You will see a recording indicator whenever it is running.";
|
|
2546
|
+
},
|
|
2547
|
+
async acknowledge(meetingId) {
|
|
2548
|
+
await options.acknowledgeConsent?.(meetingId);
|
|
2349
2549
|
}
|
|
2350
2550
|
};
|
|
2351
2551
|
}
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
}
|
|
2360
|
-
};
|
|
2552
|
+
var RECORDING_IN_FLIGHT = [
|
|
2553
|
+
"starting",
|
|
2554
|
+
"recording",
|
|
2555
|
+
"stopping"
|
|
2556
|
+
];
|
|
2557
|
+
function isRecordingInFlight(state) {
|
|
2558
|
+
return RECORDING_IN_FLIGHT.includes(state);
|
|
2361
2559
|
}
|
|
2362
|
-
function
|
|
2363
|
-
const storage = options.storage ?? createMemoryDeviceStorage();
|
|
2364
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
2365
|
-
const stored = storage.read();
|
|
2366
|
-
let devices = {
|
|
2367
|
-
audioinput: [],
|
|
2368
|
-
videoinput: [],
|
|
2369
|
-
audiooutput: []
|
|
2370
|
-
};
|
|
2371
|
-
let selected = {
|
|
2372
|
-
audioinput: stored["audioinput"] ?? null,
|
|
2373
|
-
videoinput: stored["videoinput"] ?? null,
|
|
2374
|
-
audiooutput: stored["audiooutput"] ?? null
|
|
2375
|
-
};
|
|
2376
|
-
let labelsHidden = true;
|
|
2377
|
-
let permission = "unknown";
|
|
2378
|
-
let fallbackNotice = null;
|
|
2379
|
-
let current = {
|
|
2380
|
-
devices,
|
|
2381
|
-
selected,
|
|
2382
|
-
labelsHidden,
|
|
2383
|
-
permission,
|
|
2384
|
-
fallbackNotice
|
|
2385
|
-
};
|
|
2386
|
-
function snapshot() {
|
|
2387
|
-
return current;
|
|
2388
|
-
}
|
|
2389
|
-
function commit() {
|
|
2390
|
-
current = { devices, selected, labelsHidden, permission, fallbackNotice };
|
|
2391
|
-
return current;
|
|
2392
|
-
}
|
|
2393
|
-
function emit() {
|
|
2394
|
-
const next = commit();
|
|
2395
|
-
options.onChange?.(next);
|
|
2396
|
-
for (const listener of listeners) listener(next);
|
|
2397
|
-
return next;
|
|
2398
|
-
}
|
|
2399
|
-
async function enumerate() {
|
|
2400
|
-
let raw;
|
|
2401
|
-
try {
|
|
2402
|
-
raw = await options.media.enumerateDevices();
|
|
2403
|
-
} catch (error) {
|
|
2404
|
-
throw normalizeDeviceError(error, "meet.enumerateDevices");
|
|
2405
|
-
}
|
|
2406
|
-
const grouped = {
|
|
2407
|
-
audioinput: [],
|
|
2408
|
-
videoinput: [],
|
|
2409
|
-
audiooutput: []
|
|
2410
|
-
};
|
|
2411
|
-
let anyLabel = false;
|
|
2412
|
-
for (const device of raw) {
|
|
2413
|
-
if (!isDeviceKind(device.kind)) continue;
|
|
2414
|
-
if (device.label.length > 0) anyLabel = true;
|
|
2415
|
-
grouped[device.kind].push({
|
|
2416
|
-
deviceId: device.deviceId,
|
|
2417
|
-
kind: device.kind,
|
|
2418
|
-
label: device.label
|
|
2419
|
-
});
|
|
2420
|
-
}
|
|
2421
|
-
devices = grouped;
|
|
2422
|
-
labelsHidden = !anyLabel && raw.length > 0;
|
|
2423
|
-
const lost = [];
|
|
2424
|
-
for (const kind of KINDS) {
|
|
2425
|
-
const current2 = selected[kind];
|
|
2426
|
-
if (current2 === null) continue;
|
|
2427
|
-
if (!grouped[kind].some((device) => device.deviceId === current2)) {
|
|
2428
|
-
selected = { ...selected, [kind]: null };
|
|
2429
|
-
lost.push(kind);
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
fallbackNotice = lost.length === 0 ? null : `A remembered ${lost.join(" and ")} device is no longer connected \u2014 using the system default. Pick another in the device menu.`;
|
|
2433
|
-
return emit();
|
|
2434
|
-
}
|
|
2435
|
-
const onDeviceChange = () => {
|
|
2436
|
-
void enumerate();
|
|
2437
|
-
};
|
|
2438
|
-
options.media.addEventListener?.("devicechange", onDeviceChange);
|
|
2439
|
-
return {
|
|
2440
|
-
snapshot,
|
|
2441
|
-
refresh: enumerate,
|
|
2442
|
-
async requestPermission(kinds) {
|
|
2443
|
-
if (!kinds.audio && !kinds.video) {
|
|
2444
|
-
throw new MeetError(
|
|
2445
|
-
"misconfigured",
|
|
2446
|
-
"meet.requestPermission: asked for neither audio nor video",
|
|
2447
|
-
"Request at least one kind. Prompting for nothing silently resolves and leaves labels hidden, which looks like a broken picker."
|
|
2448
|
-
);
|
|
2449
|
-
}
|
|
2450
|
-
try {
|
|
2451
|
-
const stream = await options.media.getUserMedia({
|
|
2452
|
-
...kinds.audio ? { audio: true } : {},
|
|
2453
|
-
...kinds.video ? { video: true } : {}
|
|
2454
|
-
});
|
|
2455
|
-
for (const track of stream.getTracks()) track.stop();
|
|
2456
|
-
permission = "granted";
|
|
2457
|
-
} catch (error) {
|
|
2458
|
-
permission = "denied";
|
|
2459
|
-
emit();
|
|
2460
|
-
throw normalizeDeviceError(error, "meet.requestPermission");
|
|
2461
|
-
}
|
|
2462
|
-
return enumerate();
|
|
2463
|
-
},
|
|
2464
|
-
select(kind, deviceId) {
|
|
2465
|
-
selected = { ...selected, [kind]: deviceId };
|
|
2466
|
-
fallbackNotice = null;
|
|
2467
|
-
storage.write({
|
|
2468
|
-
...storage.read(),
|
|
2469
|
-
[kind]: deviceId
|
|
2470
|
-
});
|
|
2471
|
-
return emit();
|
|
2472
|
-
},
|
|
2473
|
-
resolve(kind) {
|
|
2474
|
-
const current2 = selected[kind];
|
|
2475
|
-
if (current2 !== null && devices[kind].some((device) => device.deviceId === current2)) {
|
|
2476
|
-
return current2;
|
|
2477
|
-
}
|
|
2478
|
-
return devices[kind][0]?.deviceId ?? null;
|
|
2479
|
-
},
|
|
2480
|
-
subscribe(listener) {
|
|
2481
|
-
listeners.add(listener);
|
|
2482
|
-
return () => listeners.delete(listener);
|
|
2483
|
-
},
|
|
2484
|
-
dispose() {
|
|
2485
|
-
options.media.removeEventListener?.("devicechange", onDeviceChange);
|
|
2486
|
-
listeners.clear();
|
|
2487
|
-
}
|
|
2488
|
-
};
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
// src/core/format.ts
|
|
2492
|
-
function participantSummary(participants) {
|
|
2493
|
-
const names = participants.filter((p) => !p.isLocal).map((p) => p.displayName);
|
|
2494
|
-
if (names.length === 0) return "Just you";
|
|
2495
|
-
if (names.length === 1) return names[0] ?? "";
|
|
2496
|
-
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
|
2497
|
-
return `${names[0]}, ${names[1]} and ${names.length - 2} ${names.length - 2 === 1 ? "other" : "others"}`;
|
|
2498
|
-
}
|
|
2499
|
-
function callStateLabel(state, outgoing) {
|
|
2560
|
+
function recordingLabel(state) {
|
|
2500
2561
|
switch (state) {
|
|
2501
|
-
case "
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
return outgoing ? "On another call" : "You were busy";
|
|
2514
|
-
case "cancelled":
|
|
2515
|
-
return outgoing ? "Cancelled" : "Cancelled by caller";
|
|
2516
|
-
case "ended":
|
|
2517
|
-
return "Call ended";
|
|
2562
|
+
case "starting":
|
|
2563
|
+
return "Starting recording\u2026";
|
|
2564
|
+
case "recording":
|
|
2565
|
+
return "Recording";
|
|
2566
|
+
case "stopping":
|
|
2567
|
+
return "Stopping recording\u2026";
|
|
2568
|
+
case "processing":
|
|
2569
|
+
return "Processing recording\u2026";
|
|
2570
|
+
case "available":
|
|
2571
|
+
return "Recording ready";
|
|
2572
|
+
case "expired":
|
|
2573
|
+
return "Recording expired";
|
|
2518
2574
|
case "failed":
|
|
2519
|
-
return "
|
|
2575
|
+
return "Recording failed";
|
|
2520
2576
|
default:
|
|
2521
|
-
return
|
|
2577
|
+
return null;
|
|
2522
2578
|
}
|
|
2523
2579
|
}
|
|
2524
|
-
function countLabel(count2, singular, plural = `${singular}s`) {
|
|
2525
|
-
return `${count2} ${count2 === 1 ? singular : plural}`;
|
|
2526
|
-
}
|
|
2527
2580
|
|
|
2528
|
-
// src/core/
|
|
2529
|
-
var
|
|
2530
|
-
var
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
invite_id: invite.id,
|
|
2537
|
-
room_name: invite.roomName,
|
|
2538
|
-
mode: invite.mode,
|
|
2539
|
-
caller_name: invite.callerName,
|
|
2540
|
-
expires_at: invite.expiresAt
|
|
2541
|
-
}
|
|
2542
|
-
};
|
|
2543
|
-
}
|
|
2544
|
-
function callInviteText(invite) {
|
|
2545
|
-
return invite.mode === "video" ? `${invite.callerName} started a video call` : `${invite.callerName} started an audio call`;
|
|
2581
|
+
// src/core/attendance.ts
|
|
2582
|
+
var IDLE_TRACK = { enabled: false, unavailable: null };
|
|
2583
|
+
var IDLE_MEDIA = {
|
|
2584
|
+
microphone: IDLE_TRACK,
|
|
2585
|
+
camera: IDLE_TRACK
|
|
2586
|
+
};
|
|
2587
|
+
function unavailableFrom(error) {
|
|
2588
|
+
return { code: error.code, message: error.message, remedy: error.remedy };
|
|
2546
2589
|
}
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
return
|
|
2550
|
-
kind: CALL_INVITE_KIND,
|
|
2551
|
-
versions: [CALL_INVITE_VERSION],
|
|
2552
|
-
choices: (payload) => {
|
|
2553
|
-
if (Date.parse(payload.expires_at) < now()) return [];
|
|
2554
|
-
return [
|
|
2555
|
-
{ id: "join", label: payload.mode === "video" ? "Join video" : "Join call", tone: "primary" },
|
|
2556
|
-
{ id: "decline", label: "Decline", tone: "neutral" }
|
|
2557
|
-
];
|
|
2558
|
-
},
|
|
2559
|
-
summarize: (payload) => payload.mode === "video" ? `${payload.caller_name} started a video call` : `${payload.caller_name} started an audio call`,
|
|
2560
|
-
execute: async (payload, context) => {
|
|
2561
|
-
const settledAt = new Date(now()).toISOString();
|
|
2562
|
-
const inviteId = asCallId(payload.invite_id);
|
|
2563
|
-
const pending = await options.repository.pendingInvites(asUserId(String(context.actorId)));
|
|
2564
|
-
const live = pending.find((invite) => invite.id === inviteId) ?? null;
|
|
2565
|
-
if (live === null) {
|
|
2566
|
-
return {
|
|
2567
|
-
kind: CALL_INVITE_KIND,
|
|
2568
|
-
messageId: context.messageId,
|
|
2569
|
-
actorId: context.actorId,
|
|
2570
|
-
outcome: "unavailable",
|
|
2571
|
-
label: "This call has ended",
|
|
2572
|
-
settledAt,
|
|
2573
|
-
detail: "The invite is no longer ringing \u2014 it was answered elsewhere, cancelled, or it timed out. The chips are replaced by the outcome rather than left clickable."
|
|
2574
|
-
};
|
|
2575
|
-
}
|
|
2576
|
-
if (context.choice === "decline") {
|
|
2577
|
-
await options.calls.decline(inviteId);
|
|
2578
|
-
return {
|
|
2579
|
-
kind: CALL_INVITE_KIND,
|
|
2580
|
-
messageId: context.messageId,
|
|
2581
|
-
actorId: context.actorId,
|
|
2582
|
-
outcome: "declined",
|
|
2583
|
-
label: "Declined",
|
|
2584
|
-
settledAt
|
|
2585
|
-
};
|
|
2586
|
-
}
|
|
2587
|
-
const accepted = await options.calls.accept(inviteId, live.mode);
|
|
2588
|
-
await options.onJoin(accepted);
|
|
2589
|
-
return {
|
|
2590
|
-
kind: CALL_INVITE_KIND,
|
|
2591
|
-
messageId: context.messageId,
|
|
2592
|
-
actorId: context.actorId,
|
|
2593
|
-
outcome: "applied",
|
|
2594
|
-
label: "Joined",
|
|
2595
|
-
settledAt
|
|
2596
|
-
};
|
|
2597
|
-
}
|
|
2598
|
-
};
|
|
2590
|
+
var LISTEN_ONLY_NOTICE = "You're attending without a microphone \u2014 others can't hear you";
|
|
2591
|
+
function isListenOnly(media) {
|
|
2592
|
+
return !media.microphone.enabled;
|
|
2599
2593
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
function meetingInviteAction(payload) {
|
|
2603
|
-
return { kind: MEETING_INVITE_KIND, version: MEETING_INVITE_VERSION, payload };
|
|
2604
|
-
}
|
|
2605
|
-
function createMeetingInviteHandler(options) {
|
|
2606
|
-
const now = options.now ?? (() => Date.now());
|
|
2607
|
-
return {
|
|
2608
|
-
kind: MEETING_INVITE_KIND,
|
|
2609
|
-
versions: [MEETING_INVITE_VERSION],
|
|
2610
|
-
// A DURABLE LINK NEVER EXPIRES (R3) — the chip stays useful after the
|
|
2611
|
-
// meeting too, because that is when people go looking for the notes.
|
|
2612
|
-
choices: () => [{ id: "open", label: "Open meeting", tone: "primary" }],
|
|
2613
|
-
summarize: (payload) => payload.scheduled_for === null ? payload.title : `${payload.title} \u2014 ${payload.scheduled_for}`,
|
|
2614
|
-
execute: async (payload, context) => {
|
|
2615
|
-
if (payload.slug.length === 0) {
|
|
2616
|
-
throw new MeetError(
|
|
2617
|
-
"invalid-response",
|
|
2618
|
-
"meet.meeting-invite: the payload carries no slug",
|
|
2619
|
-
"A meeting invitation without a durable slug cannot be opened. Fix the sender; never synthesize a link on the receiving side."
|
|
2620
|
-
);
|
|
2621
|
-
}
|
|
2622
|
-
await options.onOpen(payload);
|
|
2623
|
-
return {
|
|
2624
|
-
kind: MEETING_INVITE_KIND,
|
|
2625
|
-
messageId: context.messageId,
|
|
2626
|
-
actorId: context.actorId,
|
|
2627
|
-
outcome: "applied",
|
|
2628
|
-
label: "Opened",
|
|
2629
|
-
settledAt: new Date(now()).toISOString()
|
|
2630
|
-
};
|
|
2631
|
-
}
|
|
2632
|
-
};
|
|
2633
|
-
}
|
|
2634
|
-
|
|
2635
|
-
// src/core/layout.ts
|
|
2636
|
-
var DEFAULT_PAGE_SIZE = 12;
|
|
2637
|
-
function orderParticipants(participants, activeSpeakers) {
|
|
2638
|
-
const speakerRank = /* @__PURE__ */ new Map();
|
|
2639
|
-
activeSpeakers.forEach((identity, index) => speakerRank.set(identity, index));
|
|
2640
|
-
return [...participants].sort((a, b) => {
|
|
2641
|
-
const speakerA = speakerRank.get(a.identity) ?? Number.MAX_SAFE_INTEGER;
|
|
2642
|
-
const speakerB = speakerRank.get(b.identity) ?? Number.MAX_SAFE_INTEGER;
|
|
2643
|
-
if (speakerA !== speakerB) return speakerA - speakerB;
|
|
2644
|
-
if (a.handRaised !== b.handRaised) return a.handRaised ? -1 : 1;
|
|
2645
|
-
if (a.cameraEnabled !== b.cameraEnabled) return a.cameraEnabled ? -1 : 1;
|
|
2646
|
-
if (a.joinedAt !== b.joinedAt) return a.joinedAt < b.joinedAt ? -1 : 1;
|
|
2647
|
-
return a.identity < b.identity ? -1 : 1;
|
|
2648
|
-
});
|
|
2649
|
-
}
|
|
2650
|
-
function computeStage(args) {
|
|
2651
|
-
const pageSize = Math.max(1, args.pageSize ?? DEFAULT_PAGE_SIZE);
|
|
2652
|
-
const ordered = orderParticipants(args.participants, args.activeSpeakers);
|
|
2653
|
-
const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
|
|
2654
|
-
const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
|
|
2655
|
-
const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
|
|
2656
|
-
const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
|
|
2657
|
-
const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
|
|
2658
|
-
const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
|
|
2659
|
-
const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
|
|
2660
|
-
const tiles = rest.slice(page * pageSize, page * pageSize + pageSize);
|
|
2661
|
-
return {
|
|
2662
|
-
feature,
|
|
2663
|
-
tiles,
|
|
2664
|
-
overflow: rest.length - tiles.length,
|
|
2665
|
-
page,
|
|
2666
|
-
pageCount,
|
|
2667
|
-
presenting: sharer !== null
|
|
2668
|
-
};
|
|
2669
|
-
}
|
|
2670
|
-
function gridColumns(count2) {
|
|
2671
|
-
if (count2 <= 1) return 1;
|
|
2672
|
-
if (count2 <= 4) return 2;
|
|
2673
|
-
if (count2 <= 9) return 3;
|
|
2674
|
-
if (count2 <= 16) return 4;
|
|
2675
|
-
return 5;
|
|
2594
|
+
function listenOnlyNotice(media) {
|
|
2595
|
+
return isListenOnly(media) ? LISTEN_ONLY_NOTICE : null;
|
|
2676
2596
|
}
|
|
2677
2597
|
|
|
2678
|
-
// src/core/
|
|
2679
|
-
var
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2598
|
+
// src/core/store.ts
|
|
2599
|
+
var DEFAULT_EFFECTS = {
|
|
2600
|
+
noiseSuppression: true,
|
|
2601
|
+
background: "none",
|
|
2602
|
+
backgroundImage: null
|
|
2683
2603
|
};
|
|
2684
|
-
var
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
/**
|
|
2690
|
-
* WHO IS BEING TRANSCRIBED. The LiveKit JS client reports the PUBLISHER of a
|
|
2691
|
-
* text stream, and the publisher is always the note-taker — so a caption
|
|
2692
|
-
* carries the speaker in this attribute and falls back to the publisher only
|
|
2693
|
-
* when it is absent (which attributes the line to the agent, visibly, rather
|
|
2694
|
-
* than to the wrong person, silently).
|
|
2695
|
-
*/
|
|
2696
|
-
speakerIdentity: "lk.transcribed_participant_identity"
|
|
2604
|
+
var NO_GRANTS = {
|
|
2605
|
+
role: "participant",
|
|
2606
|
+
canRecord: false,
|
|
2607
|
+
canPublish: false,
|
|
2608
|
+
canSubscribe: false
|
|
2697
2609
|
};
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
name: name ?? "",
|
|
2732
|
-
metadata: participant.metadata ?? null,
|
|
2733
|
-
// ATTRIBUTES ARE PART OF THE SNAPSHOT, not read ad hoc later: the note-taker
|
|
2734
|
-
// announces the consent notice here (D10) and the roster is the only place
|
|
2735
|
-
// this package looks at a participant.
|
|
2736
|
-
attributes: participant.attributes ?? {},
|
|
2737
|
-
isLocal,
|
|
2738
|
-
micEnabled: participant.isMicrophoneEnabled,
|
|
2739
|
-
cameraEnabled: participant.isCameraEnabled,
|
|
2740
|
-
screenSharing: participant.isScreenShareEnabled
|
|
2741
|
-
};
|
|
2742
|
-
}
|
|
2743
|
-
var DEFAULT_AUDIO_PROCESSING = {
|
|
2744
|
-
noiseSuppression: true,
|
|
2745
|
-
echoCancellation: true,
|
|
2746
|
-
autoGainControl: true
|
|
2610
|
+
var EMPTY = {
|
|
2611
|
+
phase: "idle",
|
|
2612
|
+
meeting: null,
|
|
2613
|
+
localIdentity: null,
|
|
2614
|
+
grants: NO_GRANTS,
|
|
2615
|
+
participants: [],
|
|
2616
|
+
activeSpeakers: [],
|
|
2617
|
+
pinned: null,
|
|
2618
|
+
layout: "grid",
|
|
2619
|
+
lobby: [],
|
|
2620
|
+
locked: false,
|
|
2621
|
+
recording: "idle",
|
|
2622
|
+
recordingStartedBy: null,
|
|
2623
|
+
recordingSource: "local",
|
|
2624
|
+
recordingId: null,
|
|
2625
|
+
chat: [],
|
|
2626
|
+
chatLoaded: false,
|
|
2627
|
+
chatHistoryError: null,
|
|
2628
|
+
reactions: [],
|
|
2629
|
+
spotlight: null,
|
|
2630
|
+
moderationNotice: null,
|
|
2631
|
+
actionNotice: null,
|
|
2632
|
+
privateChatRequest: null,
|
|
2633
|
+
effects: DEFAULT_EFFECTS,
|
|
2634
|
+
transcript: [],
|
|
2635
|
+
notes: [],
|
|
2636
|
+
names: {},
|
|
2637
|
+
wrapUp: null,
|
|
2638
|
+
noteTakerRefusal: null,
|
|
2639
|
+
media: IDLE_MEDIA,
|
|
2640
|
+
failure: null,
|
|
2641
|
+
collab: EMPTY_COLLAB,
|
|
2642
|
+
breakoutRoomId: null
|
|
2747
2643
|
};
|
|
2748
|
-
function
|
|
2749
|
-
const
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
}
|
|
2644
|
+
function effectiveRole(snapshot) {
|
|
2645
|
+
const me = snapshot.participants.find(
|
|
2646
|
+
(participant) => participant.identity === snapshot.localIdentity
|
|
2647
|
+
);
|
|
2648
|
+
const fromRoster = me?.role ?? "participant";
|
|
2649
|
+
const fromToken = snapshot.grants.role;
|
|
2650
|
+
return ROLE_RANK[fromRoster] >= ROLE_RANK[fromToken] ? fromRoster : fromToken;
|
|
2756
2651
|
}
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
const processors = await import("@livekit/track-processors");
|
|
2767
|
-
const processor = effect.kind === "blur" ? processors.BackgroundBlur(effect.radius ?? 12) : processors.VirtualBackground(effect.imageUrl);
|
|
2768
|
-
await track.setProcessor(processor);
|
|
2769
|
-
} catch (error) {
|
|
2770
|
-
throw new MeetError(
|
|
2771
|
-
"media-unsupported",
|
|
2772
|
-
"The background effect could not start on this camera",
|
|
2773
|
-
"Background effects need a browser with WebGL2 (current Chrome, Edge, Firefox or Safari). Your video continues without the effect.",
|
|
2774
|
-
error
|
|
2775
|
-
);
|
|
2776
|
-
}
|
|
2777
|
-
}
|
|
2778
|
-
var encoder = new TextEncoder();
|
|
2779
|
-
var decoder = new TextDecoder();
|
|
2780
|
-
function createLiveKitDriver(handlers, options = {}) {
|
|
2781
|
-
let room = null;
|
|
2782
|
-
let projectRoster = null;
|
|
2783
|
-
let audioCapture = DEFAULT_AUDIO_PROCESSING;
|
|
2784
|
-
let videoEffect = { kind: "none" };
|
|
2785
|
-
async function load() {
|
|
2786
|
-
if (room !== null) return room;
|
|
2787
|
-
let module;
|
|
2788
|
-
try {
|
|
2789
|
-
module = await import("livekit-client");
|
|
2790
|
-
} catch (error) {
|
|
2791
|
-
throw new MeetError(
|
|
2792
|
-
"misconfigured",
|
|
2793
|
-
"livekit-client could not be loaded",
|
|
2794
|
-
"`livekit-client` is a dependency of @ai-matrx/meet and is imported lazily at connect time. If this failed, the bundler excluded it \u2014 remove it from any `external`/`serverExternalPackages` list, or pass your own driver factory.",
|
|
2795
|
-
error
|
|
2796
|
-
);
|
|
2797
|
-
}
|
|
2798
|
-
const { Room: LiveKitRoom, RoomEvent, Track } = module;
|
|
2799
|
-
const created = new LiveKitRoom({
|
|
2800
|
-
// R5: large meetings degrade gracefully instead of melting a laptop.
|
|
2801
|
-
adaptiveStream: true,
|
|
2802
|
-
dynacast: true,
|
|
2803
|
-
...options.roomOptions
|
|
2804
|
-
});
|
|
2805
|
-
const roster = () => {
|
|
2806
|
-
const local = snapshotParticipant(created.localParticipant, true);
|
|
2807
|
-
const remotes = [...created.remoteParticipants.values()].map(
|
|
2808
|
-
(participant) => snapshotParticipant(participant, false)
|
|
2809
|
-
);
|
|
2810
|
-
handlers.participants([local, ...remotes]);
|
|
2811
|
-
};
|
|
2812
|
-
projectRoster = roster;
|
|
2813
|
-
created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, roster).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
|
|
2814
|
-
handlers.roomMetadata?.(metadata ?? "");
|
|
2815
|
-
}).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
2816
|
-
handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
|
|
2817
|
-
}).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
|
|
2818
|
-
handlers.quality(participant.identity, mapQuality(quality));
|
|
2819
|
-
}).on(RoomEvent.ConnectionStateChanged, (state) => {
|
|
2820
|
-
handlers.connectionState(mapConnectionState(state));
|
|
2821
|
-
}).on(RoomEvent.DataReceived, (payload, participant, _kind, topic) => {
|
|
2822
|
-
handlers.data(
|
|
2823
|
-
topic ?? "",
|
|
2824
|
-
decoder.decode(payload),
|
|
2825
|
-
participant?.identity ?? null
|
|
2826
|
-
);
|
|
2827
|
-
}).on(RoomEvent.Disconnected, (reason) => {
|
|
2828
|
-
handlers.disconnected(reason === void 0 ? null : String(reason));
|
|
2829
|
-
});
|
|
2830
|
-
try {
|
|
2831
|
-
created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
|
|
2832
|
-
const attributes = { ...reader.info.attributes ?? {} };
|
|
2833
|
-
const segmentId = attributes[TRANSCRIPTION_ATTRIBUTES.segmentId] ?? reader.info.id;
|
|
2834
|
-
const trackId = attributes[TRANSCRIPTION_ATTRIBUTES.trackId] ?? null;
|
|
2835
|
-
const final = attributes[TRANSCRIPTION_ATTRIBUTES.final] === "true";
|
|
2836
|
-
const speakerIdentity = attributes[TRANSCRIPTION_ATTRIBUTES.speakerIdentity] ?? participantInfo.identity;
|
|
2837
|
-
const at = new Date(reader.info.timestamp || Date.now()).toISOString();
|
|
2838
|
-
void (async () => {
|
|
2839
|
-
let text3 = "";
|
|
2840
|
-
try {
|
|
2841
|
-
for await (const chunk of reader) {
|
|
2842
|
-
text3 += chunk;
|
|
2843
|
-
handlers.transcription({
|
|
2844
|
-
segmentId,
|
|
2845
|
-
speakerIdentity,
|
|
2846
|
-
publisherIdentity: participantInfo.identity,
|
|
2847
|
-
text: text3,
|
|
2848
|
-
final,
|
|
2849
|
-
trackId,
|
|
2850
|
-
at
|
|
2851
|
-
});
|
|
2852
|
-
}
|
|
2853
|
-
} catch (error) {
|
|
2854
|
-
handlers.data(
|
|
2855
|
-
TRANSCRIPTION_TOPIC,
|
|
2856
|
-
JSON.stringify({ error: String(error?.message ?? error) }),
|
|
2857
|
-
participantInfo.identity
|
|
2858
|
-
);
|
|
2859
|
-
}
|
|
2860
|
-
})();
|
|
2861
|
-
});
|
|
2862
|
-
} catch {
|
|
2863
|
-
}
|
|
2864
|
-
sources = {
|
|
2865
|
-
camera: Track.Source.Camera,
|
|
2866
|
-
microphone: Track.Source.Microphone,
|
|
2867
|
-
screen: Track.Source.ScreenShare
|
|
2868
|
-
};
|
|
2869
|
-
room = created;
|
|
2870
|
-
return created;
|
|
2871
|
-
}
|
|
2872
|
-
let sources = null;
|
|
2873
|
-
function requireRoom(operation) {
|
|
2874
|
-
if (room === null) {
|
|
2875
|
-
throw new MeetError(
|
|
2876
|
-
"transport",
|
|
2877
|
-
`${operation}: the room is not connected`,
|
|
2878
|
-
"Call connect() first. The engine never reaches this \u2014 it gates every media call on its own phase \u2014 so seeing it means a consumer drove the driver directly."
|
|
2879
|
-
);
|
|
2880
|
-
}
|
|
2881
|
-
return room;
|
|
2652
|
+
var REACTION_TTL_MS = 5e3;
|
|
2653
|
+
var CHAT_LIMIT = 200;
|
|
2654
|
+
function createMeetStore(initial = {}) {
|
|
2655
|
+
let state = { ...EMPTY, ...initial };
|
|
2656
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2657
|
+
function commit(next) {
|
|
2658
|
+
if (next === state) return;
|
|
2659
|
+
state = next;
|
|
2660
|
+
for (const listener of listeners) listener();
|
|
2882
2661
|
}
|
|
2883
2662
|
return {
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
} catch (error) {
|
|
2889
|
-
throw normalizeDeviceError(error, "meet.connect");
|
|
2890
|
-
}
|
|
2891
|
-
projectRoster?.();
|
|
2892
|
-
handlers.roomMetadata?.(target.metadata ?? "");
|
|
2663
|
+
snapshot: () => state,
|
|
2664
|
+
subscribe(listener) {
|
|
2665
|
+
listeners.add(listener);
|
|
2666
|
+
return () => listeners.delete(listener);
|
|
2893
2667
|
},
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
const target = room;
|
|
2897
|
-
room = null;
|
|
2898
|
-
try {
|
|
2899
|
-
target.unregisterTextStreamHandler(TRANSCRIPTION_TOPIC);
|
|
2900
|
-
} catch {
|
|
2901
|
-
}
|
|
2902
|
-
projectRoster = null;
|
|
2903
|
-
await target.disconnect();
|
|
2668
|
+
patch(next) {
|
|
2669
|
+
commit({ ...state, ...next });
|
|
2904
2670
|
},
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2671
|
+
setParticipants(participants) {
|
|
2672
|
+
const names = learnNames(state.names, participants);
|
|
2673
|
+
const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
|
|
2674
|
+
const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
|
|
2675
|
+
const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
|
|
2676
|
+
commit({
|
|
2677
|
+
...state,
|
|
2678
|
+
names,
|
|
2679
|
+
participants: participants.map((participant) => ({
|
|
2680
|
+
...participant,
|
|
2681
|
+
// 🚨 THE ONE ROSTER RESOLUTION. Every surface that renders a person —
|
|
2682
|
+
// the tile, the People row, the header summary, every Pin/Mute label —
|
|
2683
|
+
// reads `participant.displayName`, so this is the single place a raw
|
|
2684
|
+
// identity could reach a screen, and it is the place that asks the
|
|
2685
|
+
// name book. `roster-names.test.ts` fails if a token ever gets past it.
|
|
2686
|
+
displayName: displayNameFor(names, participant.identity, participant.displayName),
|
|
2687
|
+
handRaised: hands.get(participant.identity) ?? participant.handRaised,
|
|
2688
|
+
handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
|
|
2689
|
+
quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
|
|
2690
|
+
}))
|
|
2691
|
+
});
|
|
2914
2692
|
},
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2693
|
+
setSpeaking(identities) {
|
|
2694
|
+
const speaking = new Set(identities);
|
|
2695
|
+
commit({
|
|
2696
|
+
...state,
|
|
2697
|
+
activeSpeakers: identities,
|
|
2698
|
+
participants: state.participants.map(
|
|
2699
|
+
(participant) => participant.speaking === speaking.has(participant.identity) ? participant : { ...participant, speaking: speaking.has(participant.identity) }
|
|
2700
|
+
)
|
|
2701
|
+
});
|
|
2923
2702
|
},
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2703
|
+
setQuality(identity, quality) {
|
|
2704
|
+
commit({
|
|
2705
|
+
...state,
|
|
2706
|
+
participants: state.participants.map(
|
|
2707
|
+
(participant) => participant.identity === identity ? { ...participant, quality } : participant
|
|
2708
|
+
)
|
|
2709
|
+
});
|
|
2930
2710
|
},
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2711
|
+
setHand(identity, raised, at) {
|
|
2712
|
+
commit({
|
|
2713
|
+
...state,
|
|
2714
|
+
participants: state.participants.map(
|
|
2715
|
+
(participant) => participant.identity === identity ? {
|
|
2716
|
+
...participant,
|
|
2717
|
+
handRaised: raised,
|
|
2718
|
+
// A hand that is ALREADY up keeps its place in the queue when
|
|
2719
|
+
// it is re-announced (to a late joiner): the first time wins.
|
|
2720
|
+
handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
|
|
2721
|
+
} : participant
|
|
2722
|
+
)
|
|
2723
|
+
});
|
|
2724
|
+
},
|
|
2725
|
+
addChat(message) {
|
|
2726
|
+
if (state.chat.some((existing) => existing.id === message.id)) return;
|
|
2727
|
+
const chat = [...state.chat, message];
|
|
2728
|
+
const names = learnName(state.names, message.identity, message.displayName);
|
|
2729
|
+
commit({ ...state, chat: chat.slice(-CHAT_LIMIT), names });
|
|
2730
|
+
},
|
|
2731
|
+
loadChat(messages) {
|
|
2732
|
+
const known = new Set(state.chat.map((message) => message.id));
|
|
2733
|
+
const merged = [...messages.filter((message) => !known.has(message.id)), ...state.chat].sort(
|
|
2734
|
+
(a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : 0
|
|
2735
|
+
);
|
|
2736
|
+
const names = learnNames(
|
|
2737
|
+
state.names,
|
|
2738
|
+
messages.map((message) => ({ identity: message.identity, displayName: message.displayName }))
|
|
2937
2739
|
);
|
|
2740
|
+
commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, chatHistoryError: null, names });
|
|
2938
2741
|
},
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
await track.restartTrack({
|
|
2949
|
-
...audioCapture,
|
|
2950
|
-
...target.getActiveDevice("audioinput") !== void 0 ? { deviceId: target.getActiveDevice("audioinput") } : {}
|
|
2951
|
-
});
|
|
2952
|
-
} catch (error) {
|
|
2953
|
-
throw normalizeDeviceError(error, "meet.setAudioProcessing");
|
|
2742
|
+
settleChat(localId, next) {
|
|
2743
|
+
const index = state.chat.findIndex((message) => message.id === localId);
|
|
2744
|
+
if (index === -1) return;
|
|
2745
|
+
const current = state.chat[index];
|
|
2746
|
+
let replacement;
|
|
2747
|
+
if ("failed" in next && typeof next.failed === "string" && !("id" in next)) {
|
|
2748
|
+
replacement = { ...current, failed: next.failed };
|
|
2749
|
+
} else {
|
|
2750
|
+
replacement = next;
|
|
2954
2751
|
}
|
|
2752
|
+
const duplicate = replacement.id !== localId && state.chat.some((message) => message.id === replacement.id);
|
|
2753
|
+
const chat = duplicate ? state.chat.filter((message) => message.id !== localId) : state.chat.map((message, position) => position === index ? replacement : message);
|
|
2754
|
+
commit({ ...state, chat });
|
|
2955
2755
|
},
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
const target = room;
|
|
2959
|
-
if (target === null) return;
|
|
2960
|
-
await applyVideoEffect(target, effect);
|
|
2961
|
-
},
|
|
2962
|
-
supportsVideoEffects() {
|
|
2963
|
-
return videoEffectsSupported();
|
|
2756
|
+
addReaction(reaction) {
|
|
2757
|
+
commit({ ...state, reactions: [...state.reactions, reaction] });
|
|
2964
2758
|
},
|
|
2965
|
-
|
|
2966
|
-
const
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
if (participant === void 0 || source === void 0) return () => void 0;
|
|
2970
|
-
const publication = participant.getTrackPublication(source);
|
|
2971
|
-
const track = publication?.track;
|
|
2972
|
-
if (track === void 0) return () => void 0;
|
|
2973
|
-
track.attach(element);
|
|
2974
|
-
return () => {
|
|
2975
|
-
track.detach(element);
|
|
2976
|
-
};
|
|
2759
|
+
expireReactions(now) {
|
|
2760
|
+
const live = state.reactions.filter((reaction) => now - reaction.at < REACTION_TTL_MS);
|
|
2761
|
+
if (live.length === state.reactions.length) return;
|
|
2762
|
+
commit({ ...state, reactions: live });
|
|
2977
2763
|
},
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2764
|
+
setRecording(next, by, source, recordingId = null) {
|
|
2765
|
+
if (state.recordingSource === "record" && source !== "record") {
|
|
2766
|
+
const aboutTheSameRecording = source === "room" && (recordingId === null || state.recordingId === null || recordingId === state.recordingId);
|
|
2767
|
+
if (aboutTheSameRecording || isRecordingInFlight(state.recording)) return;
|
|
2982
2768
|
}
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
videoinput: target.getActiveDevice("videoinput") ?? null,
|
|
2986
|
-
audiooutput: target.getActiveDevice("audiooutput") ?? null
|
|
2987
|
-
};
|
|
2988
|
-
},
|
|
2989
|
-
async switchDevice(kind, deviceId) {
|
|
2990
|
-
try {
|
|
2991
|
-
await requireRoom("switchDevice").switchActiveDevice(kind, deviceId);
|
|
2992
|
-
} catch (error) {
|
|
2993
|
-
throw normalizeDeviceError(error, "meet.switchDevice");
|
|
2769
|
+
if (state.recording === next && state.recordingStartedBy === by && state.recordingSource === source && state.recordingId === (recordingId ?? state.recordingId)) {
|
|
2770
|
+
return;
|
|
2994
2771
|
}
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
return String(identity).startsWith("guest:");
|
|
3002
|
-
}
|
|
3003
|
-
function isAgentIdentity(identity) {
|
|
3004
|
-
return String(identity).startsWith("agent:");
|
|
3005
|
-
}
|
|
3006
|
-
var NOTE_TAKER_PRODUCT_NAME = "Matrx note-taker";
|
|
3007
|
-
var AGENT_PRODUCT_NAME = "Matrx agent";
|
|
3008
|
-
var UNNAMED_GUEST = "Unnamed guest";
|
|
3009
|
-
var UNNAMED_PARTICIPANT = "Unnamed participant";
|
|
3010
|
-
var PLACEHOLDER_NAMES = [
|
|
3011
|
-
NOTE_TAKER_PRODUCT_NAME,
|
|
3012
|
-
AGENT_PRODUCT_NAME,
|
|
3013
|
-
UNNAMED_GUEST,
|
|
3014
|
-
UNNAMED_PARTICIPANT
|
|
3015
|
-
];
|
|
3016
|
-
function isPlaceholderName(name) {
|
|
3017
|
-
return typeof name === "string" && PLACEHOLDER_NAMES.includes(name.trim());
|
|
3018
|
-
}
|
|
3019
|
-
function placeholderNameFor(identity) {
|
|
3020
|
-
const key = String(identity);
|
|
3021
|
-
if (isAgentIdentity(key)) {
|
|
3022
|
-
return key.startsWith("agent:note_taker:") || key.startsWith("agent:note-taker") ? NOTE_TAKER_PRODUCT_NAME : AGENT_PRODUCT_NAME;
|
|
3023
|
-
}
|
|
3024
|
-
return isGuestIdentity(key) ? UNNAMED_GUEST : UNNAMED_PARTICIPANT;
|
|
3025
|
-
}
|
|
3026
|
-
function isRealName(identity, name) {
|
|
3027
|
-
if (typeof name !== "string") return false;
|
|
3028
|
-
const trimmed = name.trim();
|
|
3029
|
-
if (trimmed.length === 0) return false;
|
|
3030
|
-
if (trimmed === String(identity)) return false;
|
|
3031
|
-
if (isPlaceholderName(trimmed)) return false;
|
|
3032
|
-
return true;
|
|
3033
|
-
}
|
|
3034
|
-
function learnName(book, identity, name) {
|
|
3035
|
-
const key = String(identity);
|
|
3036
|
-
if (key.length === 0) return book;
|
|
3037
|
-
if (!isRealName(key, name)) return book;
|
|
3038
|
-
const learned = name.trim();
|
|
3039
|
-
if (book[key] === learned) return book;
|
|
3040
|
-
return { ...book, [key]: learned };
|
|
3041
|
-
}
|
|
3042
|
-
function learnNames(book, entries) {
|
|
3043
|
-
let next = book;
|
|
3044
|
-
for (const entry of entries) next = learnName(next, entry.identity, entry.displayName);
|
|
3045
|
-
return next;
|
|
3046
|
-
}
|
|
3047
|
-
function displayNameFor(book, identity, preferred) {
|
|
3048
|
-
const key = String(identity);
|
|
3049
|
-
if (isRealName(key, preferred)) return preferred.trim();
|
|
3050
|
-
const known = book[key];
|
|
3051
|
-
if (known !== void 0) return known;
|
|
3052
|
-
return placeholderNameFor(key);
|
|
3053
|
-
}
|
|
3054
|
-
|
|
3055
|
-
// src/core/recording.ts
|
|
3056
|
-
function createRecordingController(options) {
|
|
3057
|
-
async function post(route, body) {
|
|
3058
|
-
let response;
|
|
3059
|
-
try {
|
|
3060
|
-
response = await options.api.request({
|
|
3061
|
-
path: route,
|
|
3062
|
-
method: "POST",
|
|
3063
|
-
// A GUEST CAN NEVER START OR STOP A RECORDING (R5) — refused here,
|
|
3064
|
-
// before the wire, with the same sentence the hidden control implies.
|
|
3065
|
-
requireAuth: true,
|
|
3066
|
-
operation: "meet.recording",
|
|
3067
|
-
body
|
|
2772
|
+
commit({
|
|
2773
|
+
...state,
|
|
2774
|
+
recording: next,
|
|
2775
|
+
recordingStartedBy: by,
|
|
2776
|
+
recordingSource: source,
|
|
2777
|
+
recordingId: recordingId ?? state.recordingId
|
|
3068
2778
|
});
|
|
3069
|
-
} catch (error) {
|
|
3070
|
-
if (error.code === "session-unavailable") {
|
|
3071
|
-
throw new MeetError(
|
|
3072
|
-
"forbidden",
|
|
3073
|
-
"meet.recording: recording requires an authenticated host",
|
|
3074
|
-
"A guest can never start or stop a recording (R5). The control is hidden for guests."
|
|
3075
|
-
);
|
|
3076
|
-
}
|
|
3077
|
-
throw error;
|
|
3078
|
-
}
|
|
3079
|
-
if (response.status === 403) {
|
|
3080
|
-
throw new MeetError(
|
|
3081
|
-
"forbidden",
|
|
3082
|
-
"meet.recording: the server refused this recording action",
|
|
3083
|
-
"The actor is not a host of this meeting, or the org policy is `disabled`. The server is the authority; do not re-decide it on the client."
|
|
3084
|
-
);
|
|
3085
|
-
}
|
|
3086
|
-
if (!response.ok) {
|
|
3087
|
-
throw new MeetError(
|
|
3088
|
-
"transport",
|
|
3089
|
-
`meet.recording: the server returned ${response.status}`,
|
|
3090
|
-
"Egress could not be controlled. The recording state stays where it was rather than showing a recording indicator for a recording that is not running."
|
|
3091
|
-
);
|
|
3092
|
-
}
|
|
3093
|
-
}
|
|
3094
|
-
return {
|
|
3095
|
-
canStart(meeting, isHost) {
|
|
3096
|
-
if (meeting === null) return false;
|
|
3097
|
-
if (meeting.recordingPolicy === "disabled") return false;
|
|
3098
|
-
if (meeting.recordingPolicy === "always-on") return false;
|
|
3099
|
-
return isHost;
|
|
3100
2779
|
},
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
meeting_id: meetingId,
|
|
3106
|
-
room_name: roomName
|
|
3107
|
-
});
|
|
3108
|
-
} catch (error) {
|
|
3109
|
-
options.store.setRecording("idle", null, "local");
|
|
3110
|
-
throw error;
|
|
3111
|
-
}
|
|
3112
|
-
options.announce?.("starting", by);
|
|
2780
|
+
upsertLobby(entry) {
|
|
2781
|
+
const index = state.lobby.findIndex((existing) => existing.identity === entry.identity);
|
|
2782
|
+
const lobby = index === -1 ? [...state.lobby, entry] : state.lobby.map((existing, position) => position === index ? entry : existing);
|
|
2783
|
+
commit({ ...state, lobby });
|
|
3113
2784
|
},
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
2785
|
+
setAdmission(identity, admissionState, by) {
|
|
2786
|
+
commit({
|
|
2787
|
+
...state,
|
|
2788
|
+
lobby: state.lobby.map(
|
|
2789
|
+
(entry) => entry.identity === identity ? {
|
|
2790
|
+
...entry,
|
|
2791
|
+
state: admissionState,
|
|
2792
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2793
|
+
decidedBy: by
|
|
2794
|
+
} : entry
|
|
2795
|
+
)
|
|
2796
|
+
});
|
|
2797
|
+
},
|
|
2798
|
+
learn(entries) {
|
|
2799
|
+
const names = learnNames(state.names, entries);
|
|
2800
|
+
if (names === state.names) return;
|
|
2801
|
+
commit({ ...state, names });
|
|
2802
|
+
},
|
|
2803
|
+
ingestCaption(segment) {
|
|
2804
|
+
const names = learnName(state.names, segment.identity, segment.speaker);
|
|
2805
|
+
const transcript = mergeCaption(state.transcript, segment);
|
|
2806
|
+
if (transcript === state.transcript && names === state.names) return;
|
|
2807
|
+
commit({ ...state, transcript, names });
|
|
2808
|
+
},
|
|
2809
|
+
ingestRecord(segment) {
|
|
2810
|
+
const names = learnName(state.names, segment.identity, segment.speaker);
|
|
2811
|
+
const transcript = mergeRecord(state.transcript, segment);
|
|
2812
|
+
if (transcript === state.transcript && names === state.names) return;
|
|
2813
|
+
commit({ ...state, transcript, names });
|
|
2814
|
+
},
|
|
2815
|
+
upsertNote(note) {
|
|
2816
|
+
const index = state.notes.findIndex((existing2) => existing2.id === note.id);
|
|
2817
|
+
if (index === -1) {
|
|
2818
|
+
commit({ ...state, notes: [...state.notes, note] });
|
|
2819
|
+
return;
|
|
3125
2820
|
}
|
|
3126
|
-
|
|
2821
|
+
const existing = state.notes[index];
|
|
2822
|
+
if (existing !== void 0 && existing.text === note.text && existing.kind === note.kind && existing.supersededBy === note.supersededBy && existing.source === note.source) {
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
if (existing !== void 0 && existing.source === "record" && note.source === "room") {
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
commit({
|
|
2829
|
+
...state,
|
|
2830
|
+
notes: state.notes.map((entry, position) => position === index ? note : entry)
|
|
2831
|
+
});
|
|
3127
2832
|
},
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
if (
|
|
3131
|
-
|
|
2833
|
+
announceWrapUp(announcement) {
|
|
2834
|
+
const current = state.wrapUp;
|
|
2835
|
+
if (current !== null && current.at === announcement.at) return;
|
|
2836
|
+
commit({ ...state, wrapUp: announcement });
|
|
2837
|
+
},
|
|
2838
|
+
setLocalTrack(kind, next) {
|
|
2839
|
+
const current = state.media[kind];
|
|
2840
|
+
if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.message === next.unavailable?.message) {
|
|
2841
|
+
return;
|
|
3132
2842
|
}
|
|
3133
|
-
|
|
2843
|
+
commit({ ...state, media: { ...state.media, [kind]: next } });
|
|
3134
2844
|
},
|
|
3135
|
-
|
|
3136
|
-
|
|
2845
|
+
reset() {
|
|
2846
|
+
commit({ ...EMPTY, effects: state.effects });
|
|
3137
2847
|
}
|
|
3138
2848
|
};
|
|
3139
2849
|
}
|
|
3140
|
-
|
|
2850
|
+
|
|
2851
|
+
// src/core/room-events.ts
|
|
2852
|
+
var RECORDING_STATES = [
|
|
2853
|
+
"idle",
|
|
3141
2854
|
"starting",
|
|
3142
2855
|
"recording",
|
|
3143
|
-
"stopping"
|
|
2856
|
+
"stopping",
|
|
2857
|
+
"processing",
|
|
2858
|
+
"available",
|
|
2859
|
+
"expired",
|
|
2860
|
+
"failed"
|
|
3144
2861
|
];
|
|
3145
|
-
|
|
3146
|
-
|
|
2862
|
+
var ROOM_EVENT_PREFIX = "matrx.meet.";
|
|
2863
|
+
var ROOM_EVENTS = {
|
|
2864
|
+
/** One durable `meet_notes` row was written. Payload `{ note: <row> }`. */
|
|
2865
|
+
note: "matrx.meet.note",
|
|
2866
|
+
/** `meet_meetings.metadata.note_taker` changed. Payload `{ meeting_id, note_taker }`. */
|
|
2867
|
+
noteTaker: "matrx.meet.note_taker",
|
|
2868
|
+
/** A wrap-up pass finished. Payload `{ meeting_id, note_ids, segment_count }`. */
|
|
2869
|
+
wrapUp: "matrx.meet.wrap_up",
|
|
2870
|
+
/** The meeting ended for everyone. Payload `{ meeting_id, ended_at, trigger }`. */
|
|
2871
|
+
ended: "matrx.meet.ended",
|
|
2872
|
+
/**
|
|
2873
|
+
* A `meet_recordings` row changed state. Payload
|
|
2874
|
+
* `{ meeting_id, recording: { id, state, started_by } }`.
|
|
2875
|
+
*
|
|
2876
|
+
* 🚨 THIS IS THE ONLY WAY A GUEST LEARNS A RECORDING IS RUNNING. PreJoin
|
|
2877
|
+
* promises every joiner an indicator, and `anon` holds no SELECT grant on
|
|
2878
|
+
* `meet_recordings` (MRI-A10 settled that widening it is forbidden), so
|
|
2879
|
+
* without this packet the promise was a sentence with nothing behind it: on
|
|
2880
|
+
* production 2026-09-09 the guest lane showed no indicator at all while the
|
|
2881
|
+
* host recorded (MRI-C8).
|
|
2882
|
+
*/
|
|
2883
|
+
recording: "matrx.meet.recording",
|
|
2884
|
+
/**
|
|
2885
|
+
* A chat message to everyone was SAVED (Meet wave 2). Payload
|
|
2886
|
+
* `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
|
|
2887
|
+
*/
|
|
2888
|
+
chat: "matrx.meet.chat",
|
|
2889
|
+
/**
|
|
2890
|
+
* A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
|
|
2891
|
+
* `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
|
|
2892
|
+
* person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
|
|
2893
|
+
* role}` — to everyone.
|
|
2894
|
+
*/
|
|
2895
|
+
moderation: "matrx.meet.moderation",
|
|
2896
|
+
/**
|
|
2897
|
+
* A meeting ROOM ITEM changed (Meet wave 5): a poll, a Q&A question, a whiteboard
|
|
2898
|
+
* stroke or the breakout plan. Payload `{ meeting_id, item }` · `{ removed: [ids] }` ·
|
|
2899
|
+
* `{ hidden: [ids] }` · `{ cleared: "whiteboard" }` · `{ broadcast }` · `{ item, move }`.
|
|
2900
|
+
* A host's fuller copy of a poll or question carries `audience: "hosts"`.
|
|
2901
|
+
*/
|
|
2902
|
+
collab: "matrx.meet.collab"
|
|
2903
|
+
};
|
|
2904
|
+
function isRoomEventTopic(topic) {
|
|
2905
|
+
return topic.startsWith(ROOM_EVENT_PREFIX);
|
|
3147
2906
|
}
|
|
3148
|
-
function
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
2907
|
+
function object(value) {
|
|
2908
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
2909
|
+
}
|
|
2910
|
+
function text2(value) {
|
|
2911
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
2912
|
+
}
|
|
2913
|
+
var HANDLED = { handled: true };
|
|
2914
|
+
function ignored(reason) {
|
|
2915
|
+
return { handled: false, ignored: reason };
|
|
2916
|
+
}
|
|
2917
|
+
function applyRoomEvent(args) {
|
|
2918
|
+
const { store, topic } = args;
|
|
2919
|
+
if (!isRoomEventTopic(topic)) {
|
|
2920
|
+
return ignored(`${topic} is not a Meet room event`);
|
|
2921
|
+
}
|
|
2922
|
+
const payload = object(args.payload);
|
|
2923
|
+
if (payload === null) {
|
|
2924
|
+
return ignored(`${topic} carried no object payload`);
|
|
2925
|
+
}
|
|
2926
|
+
const now = args.now ?? (() => Date.now());
|
|
2927
|
+
switch (topic) {
|
|
2928
|
+
case ROOM_EVENTS.note:
|
|
2929
|
+
return applyNote(store, payload);
|
|
2930
|
+
case ROOM_EVENTS.noteTaker:
|
|
2931
|
+
return applyNoteTaker(store, payload);
|
|
2932
|
+
case ROOM_EVENTS.wrapUp:
|
|
2933
|
+
return applyWrapUp(store, payload, now);
|
|
2934
|
+
case ROOM_EVENTS.ended:
|
|
2935
|
+
return applyEnded(store, payload);
|
|
2936
|
+
case ROOM_EVENTS.recording:
|
|
2937
|
+
return applyRecording(store, payload);
|
|
2938
|
+
case ROOM_EVENTS.chat:
|
|
2939
|
+
return applyChat(store, payload);
|
|
2940
|
+
case ROOM_EVENTS.moderation:
|
|
2941
|
+
return applyModeration(store, payload, now);
|
|
2942
|
+
case ROOM_EVENTS.collab:
|
|
2943
|
+
return applyCollab(store, payload, now);
|
|
3164
2944
|
default:
|
|
3165
|
-
return
|
|
2945
|
+
return ignored(`${topic} is not known to this version of @ai-matrx/meet`);
|
|
3166
2946
|
}
|
|
3167
2947
|
}
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
2948
|
+
function applyCollab(store, payload, now) {
|
|
2949
|
+
const snapshot = store.snapshot();
|
|
2950
|
+
const meeting = snapshot.meeting;
|
|
2951
|
+
if (meeting !== null && text2(payload["meeting_id"]) !== null && payload["meeting_id"] !== meeting.id) {
|
|
2952
|
+
return ignored("a collab event for a different meeting than this room is showing");
|
|
2953
|
+
}
|
|
2954
|
+
const result = applyCollabPacket(snapshot.collab, payload, {
|
|
2955
|
+
localIdentity: snapshot.localIdentity,
|
|
2956
|
+
isHost: ROLE_RANK[effectiveRole(snapshot)] >= ROLE_RANK.cohost,
|
|
2957
|
+
now: now()
|
|
2958
|
+
});
|
|
2959
|
+
if (result.next === null) return ignored(result.dropped ?? "a collab packet that changed nothing");
|
|
2960
|
+
store.patch({ collab: result.next });
|
|
2961
|
+
return HANDLED;
|
|
3177
2962
|
}
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
return
|
|
2963
|
+
function applyNote(store, payload) {
|
|
2964
|
+
const row = object(payload["note"]);
|
|
2965
|
+
if (row === null) return ignored("a note event with no `note` row");
|
|
2966
|
+
if (text2(row["id"]) === null || text2(row["meeting_id"]) === null) {
|
|
2967
|
+
return ignored("a note row with no id or meeting_id");
|
|
2968
|
+
}
|
|
2969
|
+
const meeting = store.snapshot().meeting;
|
|
2970
|
+
if (meeting !== null && row["meeting_id"] !== meeting.id) {
|
|
2971
|
+
return ignored("a note for a different meeting than this room is showing");
|
|
2972
|
+
}
|
|
2973
|
+
try {
|
|
2974
|
+
store.upsertNote({ ...projectNote(row), source: "room" });
|
|
2975
|
+
} catch (error) {
|
|
2976
|
+
return ignored(`a note row this client could not read: ${String(error)}`);
|
|
2977
|
+
}
|
|
2978
|
+
return HANDLED;
|
|
3181
2979
|
}
|
|
3182
|
-
function
|
|
3183
|
-
|
|
2980
|
+
function applyNoteTaker(store, payload) {
|
|
2981
|
+
const noteTaker = object(payload["note_taker"]);
|
|
2982
|
+
if (noteTaker === null) return ignored("a note-taker event with no state");
|
|
2983
|
+
const meeting = store.snapshot().meeting;
|
|
2984
|
+
if (meeting === null) {
|
|
2985
|
+
return ignored("a note-taker state with no meeting record to attach it to");
|
|
2986
|
+
}
|
|
2987
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
2988
|
+
if (meetingId !== null && meetingId !== meeting.id) {
|
|
2989
|
+
return ignored("a note-taker state for a different meeting");
|
|
2990
|
+
}
|
|
2991
|
+
const next = {
|
|
2992
|
+
...meeting,
|
|
2993
|
+
metadata: { ...meeting.metadata, note_taker: noteTaker }
|
|
2994
|
+
};
|
|
2995
|
+
store.patch({ meeting: next });
|
|
2996
|
+
return HANDLED;
|
|
3184
2997
|
}
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
}
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
transcript: [],
|
|
3223
|
-
notes: [],
|
|
3224
|
-
names: {},
|
|
3225
|
-
wrapUp: null,
|
|
3226
|
-
noteTakerRefusal: null,
|
|
3227
|
-
media: IDLE_MEDIA,
|
|
3228
|
-
failure: null
|
|
3229
|
-
};
|
|
3230
|
-
function effectiveRole(snapshot) {
|
|
3231
|
-
const me = snapshot.participants.find(
|
|
3232
|
-
(participant) => participant.identity === snapshot.localIdentity
|
|
2998
|
+
function applyWrapUp(store, payload, now) {
|
|
2999
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
3000
|
+
if (meetingId === null) return ignored("a wrap-up event with no meeting id");
|
|
3001
|
+
const meeting = store.snapshot().meeting;
|
|
3002
|
+
if (meeting !== null && meetingId !== meeting.id) {
|
|
3003
|
+
return ignored("a wrap-up for a different meeting");
|
|
3004
|
+
}
|
|
3005
|
+
const rawIds = payload["note_ids"];
|
|
3006
|
+
const noteIds = Array.isArray(rawIds) ? rawIds.filter((value) => typeof value === "string") : [];
|
|
3007
|
+
const segmentCount = payload["segment_count"];
|
|
3008
|
+
const announcement = {
|
|
3009
|
+
meetingId: asMeetingId(meetingId),
|
|
3010
|
+
noteIds,
|
|
3011
|
+
segmentCount: typeof segmentCount === "number" ? segmentCount : 0,
|
|
3012
|
+
at: new Date(now()).toISOString()
|
|
3013
|
+
};
|
|
3014
|
+
store.announceWrapUp(announcement);
|
|
3015
|
+
return HANDLED;
|
|
3016
|
+
}
|
|
3017
|
+
function applyRecording(store, payload) {
|
|
3018
|
+
const row = object(payload["recording"]);
|
|
3019
|
+
if (row === null) return ignored("a recording event with no `recording` row");
|
|
3020
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
3021
|
+
const meeting = store.snapshot().meeting;
|
|
3022
|
+
if (meeting !== null && meetingId !== null && meetingId !== meeting.id) {
|
|
3023
|
+
return ignored("a recording state for a different meeting than this room is showing");
|
|
3024
|
+
}
|
|
3025
|
+
const state = text2(row["state"]);
|
|
3026
|
+
if (state === null || !RECORDING_STATES.includes(state)) {
|
|
3027
|
+
return ignored(`a recording state this version does not know: ${String(row["state"])}`);
|
|
3028
|
+
}
|
|
3029
|
+
const by = text2(row["started_by"]);
|
|
3030
|
+
store.setRecording(
|
|
3031
|
+
state,
|
|
3032
|
+
by === null ? null : by,
|
|
3033
|
+
"room",
|
|
3034
|
+
text2(row["id"])
|
|
3233
3035
|
);
|
|
3234
|
-
|
|
3235
|
-
const fromToken = snapshot.grants.role;
|
|
3236
|
-
return ROLE_RANK[fromRoster] >= ROLE_RANK[fromToken] ? fromRoster : fromToken;
|
|
3036
|
+
return HANDLED;
|
|
3237
3037
|
}
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
if (next === state) return;
|
|
3245
|
-
state = next;
|
|
3246
|
-
for (const listener of listeners) listener();
|
|
3038
|
+
function applyEnded(store, payload) {
|
|
3039
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
3040
|
+
const meeting = store.snapshot().meeting;
|
|
3041
|
+
if (meeting === null) return ignored("an ended event with no meeting record");
|
|
3042
|
+
if (meetingId !== null && meetingId !== meeting.id) {
|
|
3043
|
+
return ignored("an ended event for a different meeting");
|
|
3247
3044
|
}
|
|
3045
|
+
const endedAt = text2(payload["ended_at"]) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3046
|
+
if (meeting.endedAt === endedAt) return HANDLED;
|
|
3047
|
+
store.patch({ meeting: { ...meeting, endedAt } });
|
|
3048
|
+
return HANDLED;
|
|
3049
|
+
}
|
|
3050
|
+
function chatFromRow(row) {
|
|
3051
|
+
const id = text2(row["id"]);
|
|
3052
|
+
const identity = text2(row["sender_identity"]);
|
|
3053
|
+
const body = text2(row["body"]);
|
|
3054
|
+
if (id === null || identity === null || body === null) return null;
|
|
3248
3055
|
return {
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3056
|
+
id,
|
|
3057
|
+
identity: asParticipantIdentity(identity),
|
|
3058
|
+
displayName: text2(row["sender_name"]) ?? "Participant",
|
|
3059
|
+
text: body,
|
|
3060
|
+
at: text2(row["created_at"]) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3061
|
+
// `false` only from a server whose chat table has not landed: the room heard
|
|
3062
|
+
// it, nobody will see it later — and the line says so.
|
|
3063
|
+
saved: row["saved"] !== false
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
function applyChat(store, payload) {
|
|
3067
|
+
const row = object(payload["message"]);
|
|
3068
|
+
if (row === null) return ignored("a chat event with no `message` row");
|
|
3069
|
+
const meeting = store.snapshot().meeting;
|
|
3070
|
+
if (meeting !== null && text2(row["meeting_id"]) !== null && row["meeting_id"] !== meeting.id) {
|
|
3071
|
+
return ignored("a chat message for a different meeting");
|
|
3072
|
+
}
|
|
3073
|
+
const message = chatFromRow(row);
|
|
3074
|
+
if (message === null) return ignored("a chat row with no id, sender or body");
|
|
3075
|
+
store.addChat(message);
|
|
3076
|
+
return HANDLED;
|
|
3077
|
+
}
|
|
3078
|
+
function moderationText(kind, payload) {
|
|
3079
|
+
const by = text2(payload["by"]) ?? "The host";
|
|
3080
|
+
switch (kind) {
|
|
3081
|
+
case "muted": {
|
|
3082
|
+
const source = text2(payload["source"]) ?? "microphone";
|
|
3083
|
+
return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
|
|
3084
|
+
}
|
|
3085
|
+
case "ask-unmute":
|
|
3086
|
+
return `${by} is asking you to unmute.`;
|
|
3087
|
+
case "removed":
|
|
3088
|
+
return `${by} removed you from this meeting.`;
|
|
3089
|
+
default:
|
|
3090
|
+
return null;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
function applyModeration(store, payload, now) {
|
|
3094
|
+
const snapshot = store.snapshot();
|
|
3095
|
+
const meetingId = text2(payload["meeting_id"]);
|
|
3096
|
+
if (snapshot.meeting !== null && meetingId !== null && meetingId !== snapshot.meeting.id) {
|
|
3097
|
+
return ignored("a moderation event for a different meeting");
|
|
3098
|
+
}
|
|
3099
|
+
const kind = text2(payload["kind"]);
|
|
3100
|
+
switch (kind) {
|
|
3101
|
+
case "lower-hands": {
|
|
3102
|
+
const raw = payload["identities"];
|
|
3103
|
+
const everyone = raw === "*";
|
|
3104
|
+
const named = new Set(
|
|
3105
|
+
Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
|
|
3106
|
+
);
|
|
3107
|
+
for (const participant of snapshot.participants) {
|
|
3108
|
+
if (participant.handRaised && (everyone || named.has(participant.identity))) {
|
|
3109
|
+
store.setHand(participant.identity, false);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
return HANDLED;
|
|
3113
|
+
}
|
|
3114
|
+
case "role": {
|
|
3115
|
+
const identity = text2(payload["identity"]);
|
|
3116
|
+
const role = text2(payload["role"]);
|
|
3117
|
+
if (identity === null || role === null || !ROLES.includes(role)) {
|
|
3118
|
+
return ignored("a role event with no identity or an unknown role");
|
|
3119
|
+
}
|
|
3120
|
+
if (identity === snapshot.localIdentity) {
|
|
3121
|
+
store.patch({
|
|
3122
|
+
grants: { ...snapshot.grants, role },
|
|
3123
|
+
moderationNotice: {
|
|
3124
|
+
kind: "role",
|
|
3125
|
+
text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
|
|
3126
|
+
at: now()
|
|
3127
|
+
}
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
return HANDLED;
|
|
3131
|
+
}
|
|
3132
|
+
case "muted":
|
|
3133
|
+
case "ask-unmute":
|
|
3134
|
+
case "removed": {
|
|
3135
|
+
const sentence = moderationText(kind, payload);
|
|
3136
|
+
if (sentence === null) return ignored("a moderation event with no sentence");
|
|
3137
|
+
store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
|
|
3138
|
+
return HANDLED;
|
|
3139
|
+
}
|
|
3140
|
+
default:
|
|
3141
|
+
return ignored(`a moderation kind this version does not know: ${String(kind)}`);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
var ROLES = ["host", "cohost", "participant", "guest"];
|
|
3145
|
+
|
|
3146
|
+
// src/core/devices.ts
|
|
3147
|
+
var KINDS = ["audioinput", "videoinput", "audiooutput"];
|
|
3148
|
+
function isDeviceKind(value) {
|
|
3149
|
+
return value === "audioinput" || value === "videoinput" || value === "audiooutput";
|
|
3150
|
+
}
|
|
3151
|
+
function createWebDeviceStorage(key = "mx.meet.devices") {
|
|
3152
|
+
const store = globalThis.localStorage;
|
|
3153
|
+
if (store === void 0) return createMemoryDeviceStorage();
|
|
3154
|
+
return {
|
|
3155
|
+
durable: true,
|
|
3156
|
+
read() {
|
|
3157
|
+
try {
|
|
3158
|
+
const raw = store.getItem(key);
|
|
3159
|
+
if (raw === null) return {};
|
|
3160
|
+
const parsed = JSON.parse(raw);
|
|
3161
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
3162
|
+
} catch {
|
|
3163
|
+
return {};
|
|
3164
|
+
}
|
|
3296
3165
|
},
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3166
|
+
write(value) {
|
|
3167
|
+
try {
|
|
3168
|
+
store.setItem(key, JSON.stringify(value));
|
|
3169
|
+
} catch {
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
function createMemoryDeviceStorage() {
|
|
3175
|
+
let value = {};
|
|
3176
|
+
return {
|
|
3177
|
+
durable: false,
|
|
3178
|
+
read: () => value,
|
|
3179
|
+
write: (next) => {
|
|
3180
|
+
value = next;
|
|
3181
|
+
}
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
function createDeviceManager(options) {
|
|
3185
|
+
const storage = options.storage ?? createMemoryDeviceStorage();
|
|
3186
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
3187
|
+
const stored = storage.read();
|
|
3188
|
+
let devices = {
|
|
3189
|
+
audioinput: [],
|
|
3190
|
+
videoinput: [],
|
|
3191
|
+
audiooutput: []
|
|
3192
|
+
};
|
|
3193
|
+
let selected = {
|
|
3194
|
+
audioinput: stored["audioinput"] ?? null,
|
|
3195
|
+
videoinput: stored["videoinput"] ?? null,
|
|
3196
|
+
audiooutput: stored["audiooutput"] ?? null
|
|
3197
|
+
};
|
|
3198
|
+
let labelsHidden = true;
|
|
3199
|
+
let permission = "unknown";
|
|
3200
|
+
let fallbackNotice = null;
|
|
3201
|
+
let current = {
|
|
3202
|
+
devices,
|
|
3203
|
+
selected,
|
|
3204
|
+
labelsHidden,
|
|
3205
|
+
permission,
|
|
3206
|
+
fallbackNotice
|
|
3207
|
+
};
|
|
3208
|
+
function snapshot() {
|
|
3209
|
+
return current;
|
|
3210
|
+
}
|
|
3211
|
+
function commit() {
|
|
3212
|
+
current = { devices, selected, labelsHidden, permission, fallbackNotice };
|
|
3213
|
+
return current;
|
|
3214
|
+
}
|
|
3215
|
+
function emit() {
|
|
3216
|
+
const next = commit();
|
|
3217
|
+
options.onChange?.(next);
|
|
3218
|
+
for (const listener of listeners) listener(next);
|
|
3219
|
+
return next;
|
|
3220
|
+
}
|
|
3221
|
+
async function enumerate() {
|
|
3222
|
+
let raw;
|
|
3223
|
+
try {
|
|
3224
|
+
raw = await options.media.enumerateDevices();
|
|
3225
|
+
} catch (error) {
|
|
3226
|
+
throw normalizeDeviceError(error, "meet.enumerateDevices");
|
|
3227
|
+
}
|
|
3228
|
+
const grouped = {
|
|
3229
|
+
audioinput: [],
|
|
3230
|
+
videoinput: [],
|
|
3231
|
+
audiooutput: []
|
|
3232
|
+
};
|
|
3233
|
+
let anyLabel = false;
|
|
3234
|
+
for (const device of raw) {
|
|
3235
|
+
if (!isDeviceKind(device.kind)) continue;
|
|
3236
|
+
if (device.label.length > 0) anyLabel = true;
|
|
3237
|
+
grouped[device.kind].push({
|
|
3238
|
+
deviceId: device.deviceId,
|
|
3239
|
+
kind: device.kind,
|
|
3240
|
+
label: device.label
|
|
3309
3241
|
});
|
|
3242
|
+
}
|
|
3243
|
+
devices = grouped;
|
|
3244
|
+
labelsHidden = !anyLabel && raw.length > 0;
|
|
3245
|
+
const lost = [];
|
|
3246
|
+
for (const kind of KINDS) {
|
|
3247
|
+
const current2 = selected[kind];
|
|
3248
|
+
if (current2 === null) continue;
|
|
3249
|
+
if (!grouped[kind].some((device) => device.deviceId === current2)) {
|
|
3250
|
+
selected = { ...selected, [kind]: null };
|
|
3251
|
+
lost.push(kind);
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
fallbackNotice = lost.length === 0 ? null : `A remembered ${lost.join(" and ")} device is no longer connected \u2014 using the system default. Pick another in the device menu.`;
|
|
3255
|
+
return emit();
|
|
3256
|
+
}
|
|
3257
|
+
const onDeviceChange = () => {
|
|
3258
|
+
void enumerate();
|
|
3259
|
+
};
|
|
3260
|
+
options.media.addEventListener?.("devicechange", onDeviceChange);
|
|
3261
|
+
return {
|
|
3262
|
+
snapshot,
|
|
3263
|
+
refresh: enumerate,
|
|
3264
|
+
async requestPermission(kinds) {
|
|
3265
|
+
if (!kinds.audio && !kinds.video) {
|
|
3266
|
+
throw new MeetError(
|
|
3267
|
+
"misconfigured",
|
|
3268
|
+
"meet.requestPermission: asked for neither audio nor video",
|
|
3269
|
+
"Request at least one kind. Prompting for nothing silently resolves and leaves labels hidden, which looks like a broken picker."
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3272
|
+
try {
|
|
3273
|
+
const stream = await options.media.getUserMedia({
|
|
3274
|
+
...kinds.audio ? { audio: true } : {},
|
|
3275
|
+
...kinds.video ? { video: true } : {}
|
|
3276
|
+
});
|
|
3277
|
+
for (const track of stream.getTracks()) track.stop();
|
|
3278
|
+
permission = "granted";
|
|
3279
|
+
} catch (error) {
|
|
3280
|
+
permission = "denied";
|
|
3281
|
+
emit();
|
|
3282
|
+
throw normalizeDeviceError(error, "meet.requestPermission");
|
|
3283
|
+
}
|
|
3284
|
+
return enumerate();
|
|
3310
3285
|
},
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3286
|
+
select(kind, deviceId) {
|
|
3287
|
+
selected = { ...selected, [kind]: deviceId };
|
|
3288
|
+
fallbackNotice = null;
|
|
3289
|
+
storage.write({
|
|
3290
|
+
...storage.read(),
|
|
3291
|
+
[kind]: deviceId
|
|
3292
|
+
});
|
|
3293
|
+
return emit();
|
|
3316
3294
|
},
|
|
3317
|
-
|
|
3318
|
-
const
|
|
3319
|
-
|
|
3320
|
-
|
|
3295
|
+
resolve(kind) {
|
|
3296
|
+
const current2 = selected[kind];
|
|
3297
|
+
if (current2 !== null && devices[kind].some((device) => device.deviceId === current2)) {
|
|
3298
|
+
return current2;
|
|
3299
|
+
}
|
|
3300
|
+
return devices[kind][0]?.deviceId ?? null;
|
|
3301
|
+
},
|
|
3302
|
+
subscribe(listener) {
|
|
3303
|
+
listeners.add(listener);
|
|
3304
|
+
return () => listeners.delete(listener);
|
|
3305
|
+
},
|
|
3306
|
+
dispose() {
|
|
3307
|
+
options.media.removeEventListener?.("devicechange", onDeviceChange);
|
|
3308
|
+
listeners.clear();
|
|
3309
|
+
}
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
// src/core/format.ts
|
|
3314
|
+
function participantSummary(participants) {
|
|
3315
|
+
const names = participants.filter((p) => !p.isLocal).map((p) => p.displayName);
|
|
3316
|
+
if (names.length === 0) return "Just you";
|
|
3317
|
+
if (names.length === 1) return names[0] ?? "";
|
|
3318
|
+
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
|
3319
|
+
return `${names[0]}, ${names[1]} and ${names.length - 2} ${names.length - 2 === 1 ? "other" : "others"}`;
|
|
3320
|
+
}
|
|
3321
|
+
function callStateLabel(state, outgoing) {
|
|
3322
|
+
switch (state) {
|
|
3323
|
+
case "dialing":
|
|
3324
|
+
case "ringing":
|
|
3325
|
+
return outgoing ? "Calling\u2026" : "Incoming call";
|
|
3326
|
+
case "connecting":
|
|
3327
|
+
return "Connecting\u2026";
|
|
3328
|
+
case "active":
|
|
3329
|
+
return "In call";
|
|
3330
|
+
case "declined":
|
|
3331
|
+
return outgoing ? "Declined" : "You declined";
|
|
3332
|
+
case "missed":
|
|
3333
|
+
return outgoing ? "No answer" : "Missed call";
|
|
3334
|
+
case "busy":
|
|
3335
|
+
return outgoing ? "On another call" : "You were busy";
|
|
3336
|
+
case "cancelled":
|
|
3337
|
+
return outgoing ? "Cancelled" : "Cancelled by caller";
|
|
3338
|
+
case "ended":
|
|
3339
|
+
return "Call ended";
|
|
3340
|
+
case "failed":
|
|
3341
|
+
return "Call failed";
|
|
3342
|
+
default:
|
|
3343
|
+
return "Call";
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
function countLabel(count2, singular, plural = `${singular}s`) {
|
|
3347
|
+
return `${count2} ${count2 === 1 ? singular : plural}`;
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
// src/core/invitations.ts
|
|
3351
|
+
var CALL_INVITE_KIND = "meet.call-invite";
|
|
3352
|
+
var CALL_INVITE_VERSION = 1;
|
|
3353
|
+
function callInviteAction(invite) {
|
|
3354
|
+
return {
|
|
3355
|
+
kind: CALL_INVITE_KIND,
|
|
3356
|
+
version: CALL_INVITE_VERSION,
|
|
3357
|
+
payload: {
|
|
3358
|
+
invite_id: invite.id,
|
|
3359
|
+
room_name: invite.roomName,
|
|
3360
|
+
mode: invite.mode,
|
|
3361
|
+
caller_name: invite.callerName,
|
|
3362
|
+
expires_at: invite.expiresAt
|
|
3363
|
+
}
|
|
3364
|
+
};
|
|
3365
|
+
}
|
|
3366
|
+
function callInviteText(invite) {
|
|
3367
|
+
return invite.mode === "video" ? `${invite.callerName} started a video call` : `${invite.callerName} started an audio call`;
|
|
3368
|
+
}
|
|
3369
|
+
function createCallInviteHandler(options) {
|
|
3370
|
+
const now = options.now ?? (() => Date.now());
|
|
3371
|
+
return {
|
|
3372
|
+
kind: CALL_INVITE_KIND,
|
|
3373
|
+
versions: [CALL_INVITE_VERSION],
|
|
3374
|
+
choices: (payload) => {
|
|
3375
|
+
if (Date.parse(payload.expires_at) < now()) return [];
|
|
3376
|
+
return [
|
|
3377
|
+
{ id: "join", label: payload.mode === "video" ? "Join video" : "Join call", tone: "primary" },
|
|
3378
|
+
{ id: "decline", label: "Decline", tone: "neutral" }
|
|
3379
|
+
];
|
|
3380
|
+
},
|
|
3381
|
+
summarize: (payload) => payload.mode === "video" ? `${payload.caller_name} started a video call` : `${payload.caller_name} started an audio call`,
|
|
3382
|
+
execute: async (payload, context) => {
|
|
3383
|
+
const settledAt = new Date(now()).toISOString();
|
|
3384
|
+
const inviteId = asCallId(payload.invite_id);
|
|
3385
|
+
const pending = await options.repository.pendingInvites(asUserId(String(context.actorId)));
|
|
3386
|
+
const live = pending.find((invite) => invite.id === inviteId) ?? null;
|
|
3387
|
+
if (live === null) {
|
|
3388
|
+
return {
|
|
3389
|
+
kind: CALL_INVITE_KIND,
|
|
3390
|
+
messageId: context.messageId,
|
|
3391
|
+
actorId: context.actorId,
|
|
3392
|
+
outcome: "unavailable",
|
|
3393
|
+
label: "This call has ended",
|
|
3394
|
+
settledAt,
|
|
3395
|
+
detail: "The invite is no longer ringing \u2014 it was answered elsewhere, cancelled, or it timed out. The chips are replaced by the outcome rather than left clickable."
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
if (context.choice === "decline") {
|
|
3399
|
+
await options.calls.decline(inviteId);
|
|
3400
|
+
return {
|
|
3401
|
+
kind: CALL_INVITE_KIND,
|
|
3402
|
+
messageId: context.messageId,
|
|
3403
|
+
actorId: context.actorId,
|
|
3404
|
+
outcome: "declined",
|
|
3405
|
+
label: "Declined",
|
|
3406
|
+
settledAt
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
const accepted = await options.calls.accept(inviteId, live.mode);
|
|
3410
|
+
await options.onJoin(accepted);
|
|
3411
|
+
return {
|
|
3412
|
+
kind: CALL_INVITE_KIND,
|
|
3413
|
+
messageId: context.messageId,
|
|
3414
|
+
actorId: context.actorId,
|
|
3415
|
+
outcome: "applied",
|
|
3416
|
+
label: "Joined",
|
|
3417
|
+
settledAt
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
var MEETING_INVITE_KIND = "meet.meeting-invite";
|
|
3423
|
+
var MEETING_INVITE_VERSION = 1;
|
|
3424
|
+
function meetingInviteAction(payload) {
|
|
3425
|
+
return { kind: MEETING_INVITE_KIND, version: MEETING_INVITE_VERSION, payload };
|
|
3426
|
+
}
|
|
3427
|
+
function createMeetingInviteHandler(options) {
|
|
3428
|
+
const now = options.now ?? (() => Date.now());
|
|
3429
|
+
return {
|
|
3430
|
+
kind: MEETING_INVITE_KIND,
|
|
3431
|
+
versions: [MEETING_INVITE_VERSION],
|
|
3432
|
+
// A DURABLE LINK NEVER EXPIRES (R3) — the chip stays useful after the
|
|
3433
|
+
// meeting too, because that is when people go looking for the notes.
|
|
3434
|
+
choices: () => [{ id: "open", label: "Open meeting", tone: "primary" }],
|
|
3435
|
+
summarize: (payload) => payload.scheduled_for === null ? payload.title : `${payload.title} \u2014 ${payload.scheduled_for}`,
|
|
3436
|
+
execute: async (payload, context) => {
|
|
3437
|
+
if (payload.slug.length === 0) {
|
|
3438
|
+
throw new MeetError(
|
|
3439
|
+
"invalid-response",
|
|
3440
|
+
"meet.meeting-invite: the payload carries no slug",
|
|
3441
|
+
"A meeting invitation without a durable slug cannot be opened. Fix the sender; never synthesize a link on the receiving side."
|
|
3442
|
+
);
|
|
3443
|
+
}
|
|
3444
|
+
await options.onOpen(payload);
|
|
3445
|
+
return {
|
|
3446
|
+
kind: MEETING_INVITE_KIND,
|
|
3447
|
+
messageId: context.messageId,
|
|
3448
|
+
actorId: context.actorId,
|
|
3449
|
+
outcome: "applied",
|
|
3450
|
+
label: "Opened",
|
|
3451
|
+
settledAt: new Date(now()).toISOString()
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
// src/core/layout.ts
|
|
3458
|
+
var DEFAULT_PAGE_SIZE = 12;
|
|
3459
|
+
function orderParticipants(participants, activeSpeakers) {
|
|
3460
|
+
const speakerRank = /* @__PURE__ */ new Map();
|
|
3461
|
+
activeSpeakers.forEach((identity, index) => speakerRank.set(identity, index));
|
|
3462
|
+
return [...participants].sort((a, b) => {
|
|
3463
|
+
const speakerA = speakerRank.get(a.identity) ?? Number.MAX_SAFE_INTEGER;
|
|
3464
|
+
const speakerB = speakerRank.get(b.identity) ?? Number.MAX_SAFE_INTEGER;
|
|
3465
|
+
if (speakerA !== speakerB) return speakerA - speakerB;
|
|
3466
|
+
if (a.handRaised !== b.handRaised) return a.handRaised ? -1 : 1;
|
|
3467
|
+
if (a.cameraEnabled !== b.cameraEnabled) return a.cameraEnabled ? -1 : 1;
|
|
3468
|
+
if (a.joinedAt !== b.joinedAt) return a.joinedAt < b.joinedAt ? -1 : 1;
|
|
3469
|
+
return a.identity < b.identity ? -1 : 1;
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
3472
|
+
function computeStage(args) {
|
|
3473
|
+
const pageSize = Math.max(1, args.pageSize ?? DEFAULT_PAGE_SIZE);
|
|
3474
|
+
const ordered = orderParticipants(args.participants, args.activeSpeakers);
|
|
3475
|
+
const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
|
|
3476
|
+
const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
|
|
3477
|
+
const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
|
|
3478
|
+
const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
|
|
3479
|
+
const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
|
|
3480
|
+
const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
|
|
3481
|
+
const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
|
|
3482
|
+
const tiles = rest.slice(page * pageSize, page * pageSize + pageSize);
|
|
3483
|
+
return {
|
|
3484
|
+
feature,
|
|
3485
|
+
tiles,
|
|
3486
|
+
overflow: rest.length - tiles.length,
|
|
3487
|
+
page,
|
|
3488
|
+
pageCount,
|
|
3489
|
+
presenting: sharer !== null
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
function gridColumns(count2) {
|
|
3493
|
+
if (count2 <= 1) return 1;
|
|
3494
|
+
if (count2 <= 4) return 2;
|
|
3495
|
+
if (count2 <= 9) return 3;
|
|
3496
|
+
if (count2 <= 16) return 4;
|
|
3497
|
+
return 5;
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
// src/core/room-driver.ts
|
|
3501
|
+
var DATA_TOPICS = {
|
|
3502
|
+
hand: "mx.hand",
|
|
3503
|
+
reaction: "mx.reaction",
|
|
3504
|
+
chat: "mx.chat"
|
|
3505
|
+
};
|
|
3506
|
+
var TRANSCRIPTION_TOPIC = "lk.transcription";
|
|
3507
|
+
var TRANSCRIPTION_ATTRIBUTES = {
|
|
3508
|
+
segmentId: "lk.segment_id",
|
|
3509
|
+
trackId: "lk.transcribed_track_id",
|
|
3510
|
+
final: "lk.transcription_final",
|
|
3511
|
+
/**
|
|
3512
|
+
* WHO IS BEING TRANSCRIBED. The LiveKit JS client reports the PUBLISHER of a
|
|
3513
|
+
* text stream, and the publisher is always the note-taker — so a caption
|
|
3514
|
+
* carries the speaker in this attribute and falls back to the publisher only
|
|
3515
|
+
* when it is absent (which attributes the line to the agent, visibly, rather
|
|
3516
|
+
* than to the wrong person, silently).
|
|
3517
|
+
*/
|
|
3518
|
+
speakerIdentity: "lk.transcribed_participant_identity"
|
|
3519
|
+
};
|
|
3520
|
+
|
|
3521
|
+
// src/core/livekit-driver.ts
|
|
3522
|
+
function mapQuality(quality) {
|
|
3523
|
+
switch (quality) {
|
|
3524
|
+
case "excellent":
|
|
3525
|
+
return "excellent";
|
|
3526
|
+
case "good":
|
|
3527
|
+
return "good";
|
|
3528
|
+
case "poor":
|
|
3529
|
+
return "poor";
|
|
3530
|
+
case "lost":
|
|
3531
|
+
return "lost";
|
|
3532
|
+
default:
|
|
3533
|
+
return "unknown";
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
function mapConnectionState(state) {
|
|
3537
|
+
switch (state) {
|
|
3538
|
+
case "connected":
|
|
3539
|
+
return "connected";
|
|
3540
|
+
case "connecting":
|
|
3541
|
+
return "connecting";
|
|
3542
|
+
case "reconnecting":
|
|
3543
|
+
case "signalReconnecting":
|
|
3544
|
+
return "reconnecting";
|
|
3545
|
+
default:
|
|
3546
|
+
return "disconnected";
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
function snapshotParticipant(participant, isLocal) {
|
|
3550
|
+
const name = participant.name;
|
|
3551
|
+
return {
|
|
3552
|
+
identity: participant.identity,
|
|
3553
|
+
name: name ?? "",
|
|
3554
|
+
metadata: participant.metadata ?? null,
|
|
3555
|
+
// ATTRIBUTES ARE PART OF THE SNAPSHOT, not read ad hoc later: the note-taker
|
|
3556
|
+
// announces the consent notice here (D10) and the roster is the only place
|
|
3557
|
+
// this package looks at a participant.
|
|
3558
|
+
attributes: participant.attributes ?? {},
|
|
3559
|
+
isLocal,
|
|
3560
|
+
micEnabled: participant.isMicrophoneEnabled,
|
|
3561
|
+
cameraEnabled: participant.isCameraEnabled,
|
|
3562
|
+
screenSharing: participant.isScreenShareEnabled
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
var DEFAULT_AUDIO_PROCESSING = {
|
|
3566
|
+
noiseSuppression: true,
|
|
3567
|
+
echoCancellation: true,
|
|
3568
|
+
autoGainControl: true
|
|
3569
|
+
};
|
|
3570
|
+
function videoEffectsSupported() {
|
|
3571
|
+
const scope = globalThis;
|
|
3572
|
+
if (typeof scope.OffscreenCanvas === "undefined" || scope.document === void 0) return false;
|
|
3573
|
+
try {
|
|
3574
|
+
return scope.document.createElement("canvas").getContext("webgl2") !== null;
|
|
3575
|
+
} catch {
|
|
3576
|
+
return false;
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
async function applyVideoEffect(target, effect) {
|
|
3580
|
+
const { Track } = await import("livekit-client");
|
|
3581
|
+
const track = target.localParticipant.getTrackPublication(Track.Source.Camera)?.track;
|
|
3582
|
+
if (track === void 0) return;
|
|
3583
|
+
try {
|
|
3584
|
+
if (effect.kind === "none") {
|
|
3585
|
+
await track.stopProcessor();
|
|
3586
|
+
return;
|
|
3587
|
+
}
|
|
3588
|
+
const processors = await import("@livekit/track-processors");
|
|
3589
|
+
const processor = effect.kind === "blur" ? processors.BackgroundBlur(effect.radius ?? 12) : processors.VirtualBackground(effect.imageUrl);
|
|
3590
|
+
await track.setProcessor(processor);
|
|
3591
|
+
} catch (error) {
|
|
3592
|
+
throw new MeetError(
|
|
3593
|
+
"media-unsupported",
|
|
3594
|
+
"The background effect could not start on this camera",
|
|
3595
|
+
"Background effects need a browser with WebGL2 (current Chrome, Edge, Firefox or Safari). Your video continues without the effect.",
|
|
3596
|
+
error
|
|
3597
|
+
);
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
var encoder = new TextEncoder();
|
|
3601
|
+
var decoder = new TextDecoder();
|
|
3602
|
+
function createLiveKitDriver(handlers, options = {}) {
|
|
3603
|
+
let room = null;
|
|
3604
|
+
let projectRoster = null;
|
|
3605
|
+
let audioCapture = DEFAULT_AUDIO_PROCESSING;
|
|
3606
|
+
let videoEffect = { kind: "none" };
|
|
3607
|
+
async function load() {
|
|
3608
|
+
if (room !== null) return room;
|
|
3609
|
+
let module;
|
|
3610
|
+
try {
|
|
3611
|
+
module = await import("livekit-client");
|
|
3612
|
+
} catch (error) {
|
|
3613
|
+
throw new MeetError(
|
|
3614
|
+
"misconfigured",
|
|
3615
|
+
"livekit-client could not be loaded",
|
|
3616
|
+
"`livekit-client` is a dependency of @ai-matrx/meet and is imported lazily at connect time. If this failed, the bundler excluded it \u2014 remove it from any `external`/`serverExternalPackages` list, or pass your own driver factory.",
|
|
3617
|
+
error
|
|
3321
3618
|
);
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3619
|
+
}
|
|
3620
|
+
const { Room: LiveKitRoom, RoomEvent, Track } = module;
|
|
3621
|
+
const created = new LiveKitRoom({
|
|
3622
|
+
// R5: large meetings degrade gracefully instead of melting a laptop.
|
|
3623
|
+
adaptiveStream: true,
|
|
3624
|
+
dynacast: true,
|
|
3625
|
+
...options.roomOptions
|
|
3626
|
+
});
|
|
3627
|
+
const roster = () => {
|
|
3628
|
+
const local = snapshotParticipant(created.localParticipant, true);
|
|
3629
|
+
const remotes = [...created.remoteParticipants.values()].map(
|
|
3630
|
+
(participant) => snapshotParticipant(participant, false)
|
|
3325
3631
|
);
|
|
3326
|
-
|
|
3327
|
-
}
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
if (
|
|
3331
|
-
const
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3632
|
+
handlers.participants([local, ...remotes]);
|
|
3633
|
+
};
|
|
3634
|
+
projectRoster = roster;
|
|
3635
|
+
created.on(RoomEvent.TrackSubscribed, (track) => {
|
|
3636
|
+
if (track.kind !== Track.Kind.Audio || typeof document === "undefined") return;
|
|
3637
|
+
const element = track.attach();
|
|
3638
|
+
element.setAttribute("data-mx-meet-audio", "");
|
|
3639
|
+
audioSink().appendChild(element);
|
|
3640
|
+
}).on(RoomEvent.TrackUnsubscribed, (track) => {
|
|
3641
|
+
if (track.kind !== Track.Kind.Audio) return;
|
|
3642
|
+
for (const element of track.detach()) element.remove();
|
|
3643
|
+
});
|
|
3644
|
+
created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, roster).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
|
|
3645
|
+
handlers.roomMetadata?.(metadata ?? "");
|
|
3646
|
+
}).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
3647
|
+
handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
|
|
3648
|
+
}).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
|
|
3649
|
+
handlers.quality(participant.identity, mapQuality(quality));
|
|
3650
|
+
}).on(RoomEvent.ConnectionStateChanged, (state) => {
|
|
3651
|
+
handlers.connectionState(mapConnectionState(state));
|
|
3652
|
+
}).on(RoomEvent.DataReceived, (payload, participant, _kind, topic) => {
|
|
3653
|
+
handlers.data(
|
|
3654
|
+
topic ?? "",
|
|
3655
|
+
decoder.decode(payload),
|
|
3656
|
+
participant?.identity ?? null
|
|
3657
|
+
);
|
|
3658
|
+
}).on(RoomEvent.Disconnected, (reason) => {
|
|
3659
|
+
audioContainer?.remove();
|
|
3660
|
+
audioContainer = null;
|
|
3661
|
+
handlers.disconnected(reason === void 0 ? null : String(reason));
|
|
3662
|
+
});
|
|
3663
|
+
try {
|
|
3664
|
+
created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
|
|
3665
|
+
const attributes = { ...reader.info.attributes ?? {} };
|
|
3666
|
+
const segmentId = attributes[TRANSCRIPTION_ATTRIBUTES.segmentId] ?? reader.info.id;
|
|
3667
|
+
const trackId = attributes[TRANSCRIPTION_ATTRIBUTES.trackId] ?? null;
|
|
3668
|
+
const final = attributes[TRANSCRIPTION_ATTRIBUTES.final] === "true";
|
|
3669
|
+
const speakerIdentity = attributes[TRANSCRIPTION_ATTRIBUTES.speakerIdentity] ?? participantInfo.identity;
|
|
3670
|
+
const at = new Date(reader.info.timestamp || Date.now()).toISOString();
|
|
3671
|
+
void (async () => {
|
|
3672
|
+
let text3 = "";
|
|
3673
|
+
try {
|
|
3674
|
+
for await (const chunk of reader) {
|
|
3675
|
+
text3 += chunk;
|
|
3676
|
+
handlers.transcription({
|
|
3677
|
+
segmentId,
|
|
3678
|
+
speakerIdentity,
|
|
3679
|
+
publisherIdentity: participantInfo.identity,
|
|
3680
|
+
text: text3,
|
|
3681
|
+
final,
|
|
3682
|
+
trackId,
|
|
3683
|
+
at
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
} catch (error) {
|
|
3687
|
+
handlers.data(
|
|
3688
|
+
TRANSCRIPTION_TOPIC,
|
|
3689
|
+
JSON.stringify({ error: String(error?.message ?? error) }),
|
|
3690
|
+
participantInfo.identity
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3693
|
+
})();
|
|
3694
|
+
});
|
|
3695
|
+
} catch {
|
|
3696
|
+
}
|
|
3697
|
+
sources = {
|
|
3698
|
+
camera: Track.Source.Camera,
|
|
3699
|
+
microphone: Track.Source.Microphone,
|
|
3700
|
+
screen: Track.Source.ScreenShare
|
|
3701
|
+
};
|
|
3702
|
+
trackEvents = {
|
|
3703
|
+
subscribed: RoomEvent.TrackSubscribed,
|
|
3704
|
+
localPublished: RoomEvent.LocalTrackPublished
|
|
3705
|
+
};
|
|
3706
|
+
room = created;
|
|
3707
|
+
return created;
|
|
3708
|
+
}
|
|
3709
|
+
let sources = null;
|
|
3710
|
+
let trackEvents = null;
|
|
3711
|
+
let audioContainer = null;
|
|
3712
|
+
function audioSink() {
|
|
3713
|
+
if (audioContainer !== null && audioContainer.isConnected) return audioContainer;
|
|
3714
|
+
const container = document.createElement("div");
|
|
3715
|
+
container.setAttribute("data-mx-meet-audio-sink", "");
|
|
3716
|
+
container.setAttribute("aria-hidden", "true");
|
|
3717
|
+
container.style.display = "none";
|
|
3718
|
+
document.body.appendChild(container);
|
|
3719
|
+
audioContainer = container;
|
|
3720
|
+
return container;
|
|
3721
|
+
}
|
|
3722
|
+
function requireRoom(operation) {
|
|
3723
|
+
if (room === null) {
|
|
3724
|
+
throw new MeetError(
|
|
3725
|
+
"transport",
|
|
3726
|
+
`${operation}: the room is not connected`,
|
|
3727
|
+
"Call connect() first. The engine never reaches this \u2014 it gates every media call on its own phase \u2014 so seeing it means a consumer drove the driver directly."
|
|
3728
|
+
);
|
|
3729
|
+
}
|
|
3730
|
+
return room;
|
|
3731
|
+
}
|
|
3732
|
+
return {
|
|
3733
|
+
async connect(serverUrl, token) {
|
|
3734
|
+
const target = await load();
|
|
3735
|
+
try {
|
|
3736
|
+
await target.connect(serverUrl, token);
|
|
3737
|
+
} catch (error) {
|
|
3738
|
+
throw normalizeDeviceError(error, "meet.connect");
|
|
3337
3739
|
}
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
commit({ ...state, chat });
|
|
3341
|
-
},
|
|
3342
|
-
addReaction(reaction) {
|
|
3343
|
-
commit({ ...state, reactions: [...state.reactions, reaction] });
|
|
3740
|
+
projectRoster?.();
|
|
3741
|
+
handlers.roomMetadata?.(target.metadata ?? "");
|
|
3344
3742
|
},
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3743
|
+
async disconnect() {
|
|
3744
|
+
if (room === null) return;
|
|
3745
|
+
const target = room;
|
|
3746
|
+
room = null;
|
|
3747
|
+
try {
|
|
3748
|
+
target.unregisterTextStreamHandler(TRANSCRIPTION_TOPIC);
|
|
3749
|
+
} catch {
|
|
3750
|
+
}
|
|
3751
|
+
projectRoster = null;
|
|
3752
|
+
audioContainer?.remove();
|
|
3753
|
+
audioContainer = null;
|
|
3754
|
+
await target.disconnect();
|
|
3349
3755
|
},
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3756
|
+
async setMicrophoneEnabled(enabled) {
|
|
3757
|
+
try {
|
|
3758
|
+
await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
|
|
3759
|
+
enabled,
|
|
3760
|
+
audioCapture
|
|
3761
|
+
);
|
|
3762
|
+
} catch (error) {
|
|
3763
|
+
throw normalizeDeviceError(error, "meet.setMicrophoneEnabled");
|
|
3354
3764
|
}
|
|
3355
|
-
|
|
3356
|
-
|
|
3765
|
+
},
|
|
3766
|
+
async setCameraEnabled(enabled) {
|
|
3767
|
+
try {
|
|
3768
|
+
const target = requireRoom("setCameraEnabled");
|
|
3769
|
+
await target.localParticipant.setCameraEnabled(enabled);
|
|
3770
|
+
if (enabled && videoEffect.kind !== "none") await applyVideoEffect(target, videoEffect);
|
|
3771
|
+
} catch (error) {
|
|
3772
|
+
throw normalizeDeviceError(error, "meet.setCameraEnabled");
|
|
3357
3773
|
}
|
|
3358
|
-
commit({
|
|
3359
|
-
...state,
|
|
3360
|
-
recording: next,
|
|
3361
|
-
recordingStartedBy: by,
|
|
3362
|
-
recordingSource: source,
|
|
3363
|
-
recordingId: recordingId ?? state.recordingId
|
|
3364
|
-
});
|
|
3365
3774
|
},
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3775
|
+
async setScreenShareEnabled(enabled) {
|
|
3776
|
+
try {
|
|
3777
|
+
await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(enabled);
|
|
3778
|
+
} catch (error) {
|
|
3779
|
+
throw normalizeDeviceError(error, "meet.setScreenShareEnabled");
|
|
3780
|
+
}
|
|
3370
3781
|
},
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3379
|
-
decidedBy: by
|
|
3380
|
-
} : entry
|
|
3381
|
-
)
|
|
3382
|
-
});
|
|
3782
|
+
async publishData(topic, payload, to) {
|
|
3783
|
+
await requireRoom("publishData").localParticipant.publishData(
|
|
3784
|
+
// `reliable` is not optional for a control plane: a dropped
|
|
3785
|
+
// "hand lowered" leaves a hand up on every other screen forever.
|
|
3786
|
+
encoder.encode(payload),
|
|
3787
|
+
to === void 0 || to.length === 0 ? { reliable: true, topic } : { reliable: true, topic, destinationIdentities: [...to] }
|
|
3788
|
+
);
|
|
3383
3789
|
},
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3790
|
+
async setAudioProcessing(processing) {
|
|
3791
|
+
audioCapture = { ...audioCapture, ...processing };
|
|
3792
|
+
const target = room;
|
|
3793
|
+
if (target === null) return;
|
|
3794
|
+
const { Track } = await import("livekit-client");
|
|
3795
|
+
const publication = target.localParticipant.getTrackPublication(Track.Source.Microphone);
|
|
3796
|
+
const track = publication?.track;
|
|
3797
|
+
if (track === void 0 || publication?.isMuted === true) return;
|
|
3798
|
+
try {
|
|
3799
|
+
await track.restartTrack({
|
|
3800
|
+
...audioCapture,
|
|
3801
|
+
...target.getActiveDevice("audioinput") !== void 0 ? { deviceId: target.getActiveDevice("audioinput") } : {}
|
|
3802
|
+
});
|
|
3803
|
+
} catch (error) {
|
|
3804
|
+
throw normalizeDeviceError(error, "meet.setAudioProcessing");
|
|
3805
|
+
}
|
|
3388
3806
|
},
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
const
|
|
3392
|
-
if (
|
|
3393
|
-
|
|
3807
|
+
async setVideoEffect(effect) {
|
|
3808
|
+
videoEffect = effect;
|
|
3809
|
+
const target = room;
|
|
3810
|
+
if (target === null) return;
|
|
3811
|
+
await applyVideoEffect(target, effect);
|
|
3394
3812
|
},
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
const transcript = mergeRecord(state.transcript, segment);
|
|
3398
|
-
if (transcript === state.transcript && names === state.names) return;
|
|
3399
|
-
commit({ ...state, transcript, names });
|
|
3813
|
+
supportsVideoEffects() {
|
|
3814
|
+
return videoEffectsSupported();
|
|
3400
3815
|
},
|
|
3401
|
-
|
|
3402
|
-
const
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
return;
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3816
|
+
attach(identity, kind, element) {
|
|
3817
|
+
const target = requireRoom("attach");
|
|
3818
|
+
const source = sources?.[kind];
|
|
3819
|
+
if (source === void 0) return () => void 0;
|
|
3820
|
+
let current = null;
|
|
3821
|
+
const sync = () => {
|
|
3822
|
+
const participant = target.localParticipant.identity === identity ? target.localParticipant : target.remoteParticipants.get(identity);
|
|
3823
|
+
const track = participant?.getTrackPublication(source)?.track;
|
|
3824
|
+
if (track === void 0 || track === current) return;
|
|
3825
|
+
current?.detach(element);
|
|
3826
|
+
track.attach(element);
|
|
3827
|
+
current = track;
|
|
3828
|
+
};
|
|
3829
|
+
const onTrack = (...args) => {
|
|
3830
|
+
const who = args[args.length - 1];
|
|
3831
|
+
if (who?.identity === identity) sync();
|
|
3832
|
+
};
|
|
3833
|
+
sync();
|
|
3834
|
+
const events = trackEvents;
|
|
3835
|
+
if (events !== null) {
|
|
3836
|
+
target.on(events.subscribed, onTrack);
|
|
3837
|
+
target.on(events.localPublished, onTrack);
|
|
3413
3838
|
}
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
commit({ ...state, wrapUp: announcement });
|
|
3839
|
+
return () => {
|
|
3840
|
+
if (events !== null) {
|
|
3841
|
+
target.off(events.subscribed, onTrack);
|
|
3842
|
+
target.off(events.localPublished, onTrack);
|
|
3843
|
+
}
|
|
3844
|
+
current?.detach(element);
|
|
3845
|
+
current = null;
|
|
3846
|
+
};
|
|
3423
3847
|
},
|
|
3424
|
-
|
|
3425
|
-
const
|
|
3426
|
-
if (
|
|
3427
|
-
return;
|
|
3848
|
+
activeDevices() {
|
|
3849
|
+
const target = room;
|
|
3850
|
+
if (target === null) {
|
|
3851
|
+
return { audioinput: null, videoinput: null, audiooutput: null };
|
|
3428
3852
|
}
|
|
3429
|
-
|
|
3853
|
+
return {
|
|
3854
|
+
audioinput: target.getActiveDevice("audioinput") ?? null,
|
|
3855
|
+
videoinput: target.getActiveDevice("videoinput") ?? null,
|
|
3856
|
+
audiooutput: target.getActiveDevice("audiooutput") ?? null
|
|
3857
|
+
};
|
|
3430
3858
|
},
|
|
3431
|
-
|
|
3432
|
-
|
|
3859
|
+
async switchDevice(kind, deviceId) {
|
|
3860
|
+
try {
|
|
3861
|
+
await requireRoom("switchDevice").switchActiveDevice(kind, deviceId);
|
|
3862
|
+
} catch (error) {
|
|
3863
|
+
throw normalizeDeviceError(error, "meet.switchDevice");
|
|
3864
|
+
}
|
|
3433
3865
|
}
|
|
3434
3866
|
};
|
|
3435
3867
|
}
|
|
@@ -3901,6 +4333,7 @@ function kindOf(person) {
|
|
|
3901
4333
|
if (person.role === "host") return "host";
|
|
3902
4334
|
if (person.role === "cohost") return "cohost";
|
|
3903
4335
|
if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
|
|
4336
|
+
if (person.userId === null) return "guest";
|
|
3904
4337
|
return "member";
|
|
3905
4338
|
}
|
|
3906
4339
|
function ms(iso) {
|
|
@@ -3956,12 +4389,19 @@ function coveredMs(intervals) {
|
|
|
3956
4389
|
}
|
|
3957
4390
|
function attendanceReport(meeting, attendees, names = {}) {
|
|
3958
4391
|
const end = ms(meeting.endedAt);
|
|
4392
|
+
const measuredOf = /* @__PURE__ */ new Map();
|
|
3959
4393
|
const perJoin = attendees.map((person) => {
|
|
3960
|
-
const
|
|
3961
|
-
|
|
3962
|
-
const
|
|
3963
|
-
const
|
|
3964
|
-
|
|
4394
|
+
const stays = person.sessions !== void 0 && person.sessions.length > 0 ? person.sessions.map((stay) => [ms(stay.joinedAt), ms(stay.leftAt)]) : [[ms(person.joinedAt), ms(person.leftAt)]];
|
|
4395
|
+
let toEnd = false;
|
|
4396
|
+
const measured = [];
|
|
4397
|
+
for (const [joined, left] of stays) {
|
|
4398
|
+
const until = left ?? end;
|
|
4399
|
+
if (joined === null || until === null || until < joined) continue;
|
|
4400
|
+
if (left === null) toEnd = true;
|
|
4401
|
+
measured.push([joined, until]);
|
|
4402
|
+
}
|
|
4403
|
+
const duration = measured.length === 0 ? null : coveredMs(measured);
|
|
4404
|
+
const row = {
|
|
3965
4405
|
id: person.id,
|
|
3966
4406
|
identity: person.identity,
|
|
3967
4407
|
name: displayNameFor(names, person.identity, person.displayName),
|
|
@@ -3970,10 +4410,12 @@ function attendanceReport(meeting, attendees, names = {}) {
|
|
|
3970
4410
|
joinedAt: person.joinedAt,
|
|
3971
4411
|
leftAt: person.leftAt,
|
|
3972
4412
|
durationMs: duration,
|
|
3973
|
-
durationToEnd: duration !== null &&
|
|
4413
|
+
durationToEnd: duration !== null && toEnd,
|
|
3974
4414
|
admission: person.admissionState,
|
|
3975
|
-
joins: 1
|
|
4415
|
+
joins: Math.max(1, person.sessions?.length ?? 0)
|
|
3976
4416
|
};
|
|
4417
|
+
measuredOf.set(row, measured);
|
|
4418
|
+
return row;
|
|
3977
4419
|
});
|
|
3978
4420
|
const groups = /* @__PURE__ */ new Map();
|
|
3979
4421
|
attendees.forEach((person, index) => {
|
|
@@ -3996,17 +4438,14 @@ function attendanceReport(meeting, attendees, names = {}) {
|
|
|
3996
4438
|
const left = ms(row.leftAt);
|
|
3997
4439
|
return left !== null && (latest === null || left > latest) ? left : latest;
|
|
3998
4440
|
}, null);
|
|
3999
|
-
const intervals = came.flatMap((row) =>
|
|
4000
|
-
const joined = ms(row.joinedAt);
|
|
4001
|
-
return joined !== null && row.durationMs !== null ? [[joined, joined + row.durationMs]] : [];
|
|
4002
|
-
});
|
|
4441
|
+
const intervals = came.flatMap((row) => measuredOf.get(row) ?? []);
|
|
4003
4442
|
return {
|
|
4004
4443
|
...first,
|
|
4005
4444
|
kind,
|
|
4006
4445
|
leftAt: open ? null : lastLeave === null ? null : new Date(lastLeave).toISOString(),
|
|
4007
4446
|
durationMs: intervals.length === 0 ? null : coveredMs(intervals),
|
|
4008
4447
|
durationToEnd: came.some((row) => row.durationToEnd),
|
|
4009
|
-
joins: came.length === 0 ? joins.length : came.
|
|
4448
|
+
joins: came.length === 0 ? joins.length : came.reduce((total, row) => total + row.joins, 0)
|
|
4010
4449
|
};
|
|
4011
4450
|
});
|
|
4012
4451
|
const rank = KIND_RANK;
|
|
@@ -4397,6 +4836,7 @@ function createRoomEngine(options) {
|
|
|
4397
4836
|
let joining = null;
|
|
4398
4837
|
let lastRequest = null;
|
|
4399
4838
|
let disposed = false;
|
|
4839
|
+
let moving = false;
|
|
4400
4840
|
const confirmingEnds = /* @__PURE__ */ new WeakSet();
|
|
4401
4841
|
async function leaveRoom() {
|
|
4402
4842
|
const target = driver;
|
|
@@ -4626,6 +5066,7 @@ function createRoomEngine(options) {
|
|
|
4626
5066
|
}
|
|
4627
5067
|
},
|
|
4628
5068
|
disconnected(reason) {
|
|
5069
|
+
if (moving) return;
|
|
4629
5070
|
if (current !== null) {
|
|
4630
5071
|
tokens.invalidate(current.roomName);
|
|
4631
5072
|
current = null;
|
|
@@ -4640,11 +5081,13 @@ function createRoomEngine(options) {
|
|
|
4640
5081
|
roomName: request.roomName,
|
|
4641
5082
|
identity,
|
|
4642
5083
|
...request.meetingId !== void 0 ? { meetingId: request.meetingId } : {},
|
|
4643
|
-
...request.slug !== void 0 ? { slug: request.slug } : {}
|
|
5084
|
+
...request.slug !== void 0 ? { slug: request.slug } : {},
|
|
5085
|
+
...request.breakoutRoomId != null ? { breakoutRoomId: request.breakoutRoomId } : {}
|
|
4644
5086
|
});
|
|
4645
5087
|
current = token;
|
|
4646
5088
|
store.patch({
|
|
4647
5089
|
localIdentity: token.identity,
|
|
5090
|
+
breakoutRoomId: token.breakoutRoomId ?? null,
|
|
4648
5091
|
grants: {
|
|
4649
5092
|
role: token.role,
|
|
4650
5093
|
canRecord: token.canRecord,
|
|
@@ -4763,6 +5206,56 @@ function createRoomEngine(options) {
|
|
|
4763
5206
|
});
|
|
4764
5207
|
return joining;
|
|
4765
5208
|
},
|
|
5209
|
+
async moveToBreakout(roomId) {
|
|
5210
|
+
const request = lastRequest;
|
|
5211
|
+
if (request === null || driver === null) {
|
|
5212
|
+
report(
|
|
5213
|
+
"warn",
|
|
5214
|
+
"meet.moveToBreakout: this engine is not in a meeting room",
|
|
5215
|
+
"Join the meeting first; a breakout is a room OF a meeting."
|
|
5216
|
+
);
|
|
5217
|
+
return;
|
|
5218
|
+
}
|
|
5219
|
+
if ((store.snapshot().breakoutRoomId ?? null) === roomId) return;
|
|
5220
|
+
if (joining !== null) await joining.catch(() => void 0);
|
|
5221
|
+
const previous = driver;
|
|
5222
|
+
const next = {
|
|
5223
|
+
...request,
|
|
5224
|
+
breakoutRoomId: roomId,
|
|
5225
|
+
// Arrive as you were: camera and microphone as they are now.
|
|
5226
|
+
mode: store.snapshot().media.camera.enabled ? "video" : "audio",
|
|
5227
|
+
microphone: store.snapshot().media.microphone.enabled
|
|
5228
|
+
};
|
|
5229
|
+
moving = true;
|
|
5230
|
+
try {
|
|
5231
|
+
driver = null;
|
|
5232
|
+
tokens.invalidate(request.roomName);
|
|
5233
|
+
current = null;
|
|
5234
|
+
await previous.disconnect();
|
|
5235
|
+
lastRequest = next;
|
|
5236
|
+
joining = connect(next).finally(() => {
|
|
5237
|
+
joining = null;
|
|
5238
|
+
});
|
|
5239
|
+
await joining;
|
|
5240
|
+
} catch (error) {
|
|
5241
|
+
const normalized = normalizeMeetError(error, "meet.moveToBreakout");
|
|
5242
|
+
report("warn", normalized.message, normalized.remedy);
|
|
5243
|
+
if (roomId !== null) {
|
|
5244
|
+
lastRequest = { ...next, breakoutRoomId: null };
|
|
5245
|
+
joining = connect(lastRequest).finally(() => {
|
|
5246
|
+
joining = null;
|
|
5247
|
+
});
|
|
5248
|
+
await joining.catch((again) => {
|
|
5249
|
+
throw fail(again, "meet.moveToBreakout");
|
|
5250
|
+
});
|
|
5251
|
+
} else {
|
|
5252
|
+
throw fail(error, "meet.moveToBreakout");
|
|
5253
|
+
}
|
|
5254
|
+
throw normalized;
|
|
5255
|
+
} finally {
|
|
5256
|
+
moving = false;
|
|
5257
|
+
}
|
|
5258
|
+
},
|
|
4766
5259
|
leave: leaveRoom,
|
|
4767
5260
|
// Turning a device on mid-meeting is the SAME operation as turning it on
|
|
4768
5261
|
// during the join, so it runs the same function. A failure here used to
|
|
@@ -5032,6 +5525,109 @@ function createInRoomClient(options) {
|
|
|
5032
5525
|
(row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
|
|
5033
5526
|
).filter((message) => message !== null);
|
|
5034
5527
|
store.loadChat(messages);
|
|
5528
|
+
},
|
|
5529
|
+
async loadCollab() {
|
|
5530
|
+
const operation = "meet.loadCollab";
|
|
5531
|
+
const id = meetingId(operation);
|
|
5532
|
+
const hdrs = headers();
|
|
5533
|
+
const response = await api.request({
|
|
5534
|
+
path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(id)}`,
|
|
5535
|
+
method: "GET",
|
|
5536
|
+
operation,
|
|
5537
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
5538
|
+
});
|
|
5539
|
+
if (!response.ok) {
|
|
5540
|
+
const error = await refusal2(
|
|
5541
|
+
response,
|
|
5542
|
+
operation,
|
|
5543
|
+
"Polls, questions and the whiteboard could not be read. New ones still arrive."
|
|
5544
|
+
);
|
|
5545
|
+
store.patch({ collab: { ...store.snapshot().collab, loaded: true, error: error.message } });
|
|
5546
|
+
throw error;
|
|
5547
|
+
}
|
|
5548
|
+
const body = await response.json();
|
|
5549
|
+
store.patch({ collab: collabFromState(body, store.snapshot().collab) });
|
|
5550
|
+
},
|
|
5551
|
+
async collab(request) {
|
|
5552
|
+
const operation = `meet.collab(${request.action})`;
|
|
5553
|
+
const hdrs = headers();
|
|
5554
|
+
const response = await api.request({
|
|
5555
|
+
path: MEET_ROUTES.collab,
|
|
5556
|
+
method: "POST",
|
|
5557
|
+
operation,
|
|
5558
|
+
body: {
|
|
5559
|
+
meeting_id: meetingId(operation),
|
|
5560
|
+
action: request.action,
|
|
5561
|
+
...request.itemId !== void 0 ? { item_id: request.itemId } : {},
|
|
5562
|
+
...request.question !== void 0 ? { question: request.question } : {},
|
|
5563
|
+
...request.options !== void 0 ? { options: request.options } : {},
|
|
5564
|
+
...request.multiple !== void 0 ? { multiple: request.multiple } : {},
|
|
5565
|
+
...request.anonymous !== void 0 ? { anonymous: request.anonymous } : {},
|
|
5566
|
+
...request.optionIds !== void 0 ? { option_ids: request.optionIds } : {},
|
|
5567
|
+
...request.text !== void 0 ? { text: request.text } : {},
|
|
5568
|
+
...request.stroke !== void 0 ? { stroke: request.stroke } : {},
|
|
5569
|
+
...request.rooms !== void 0 ? { rooms: request.rooms } : {},
|
|
5570
|
+
...request.allowChoose !== void 0 ? { allow_choose: request.allowChoose } : {},
|
|
5571
|
+
...request.minutes !== void 0 ? { minutes: request.minutes } : {},
|
|
5572
|
+
...request.identity !== void 0 ? { identity: request.identity } : {},
|
|
5573
|
+
...request.roomId !== void 0 ? { room_id: request.roomId } : {}
|
|
5574
|
+
},
|
|
5575
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
5576
|
+
});
|
|
5577
|
+
if (!response.ok) {
|
|
5578
|
+
throw await refusal2(response, operation, "Nothing was changed.");
|
|
5579
|
+
}
|
|
5580
|
+
const payload = await response.json();
|
|
5581
|
+
const removed = Array.isArray(payload["removed"]) ? payload["removed"].filter((v) => typeof v === "string") : [];
|
|
5582
|
+
let next = store.snapshot().collab;
|
|
5583
|
+
const item = payload["item"];
|
|
5584
|
+
if (typeof item === "object" && item !== null) next = withItem(next, item);
|
|
5585
|
+
if (removed.length > 0) {
|
|
5586
|
+
const gone = new Set(removed);
|
|
5587
|
+
next = {
|
|
5588
|
+
...next,
|
|
5589
|
+
polls: next.polls.filter((p) => !gone.has(p.id)),
|
|
5590
|
+
questions: next.questions.filter((q) => !gone.has(q.id)),
|
|
5591
|
+
strokes: next.strokes.filter((st) => !gone.has(st.id))
|
|
5592
|
+
};
|
|
5593
|
+
}
|
|
5594
|
+
store.patch({ collab: next });
|
|
5595
|
+
const itemId = typeof item === "object" && item !== null && typeof item["id"] === "string" ? item["id"] : null;
|
|
5596
|
+
return {
|
|
5597
|
+
action: request.action,
|
|
5598
|
+
message: typeof payload["message"] === "string" ? payload["message"] : "Done.",
|
|
5599
|
+
removed,
|
|
5600
|
+
itemId
|
|
5601
|
+
};
|
|
5602
|
+
},
|
|
5603
|
+
async translateCaptions(language, lines) {
|
|
5604
|
+
if (lines.length === 0) return;
|
|
5605
|
+
const operation = "meet.translateCaptions";
|
|
5606
|
+
const hdrs = headers();
|
|
5607
|
+
const response = await api.request({
|
|
5608
|
+
path: MEET_ROUTES.captionsTranslate,
|
|
5609
|
+
method: "POST",
|
|
5610
|
+
operation,
|
|
5611
|
+
body: { meeting_id: meetingId(operation), target_language: language, lines },
|
|
5612
|
+
...hdrs !== void 0 ? { headers: hdrs } : {}
|
|
5613
|
+
});
|
|
5614
|
+
if (!response.ok) {
|
|
5615
|
+
const error = await refusal2(response, operation, "Captions stay in the language they were spoken.");
|
|
5616
|
+
store.patch({
|
|
5617
|
+
collab: { ...store.snapshot().collab, captionTranslationError: error.message }
|
|
5618
|
+
});
|
|
5619
|
+
throw error;
|
|
5620
|
+
}
|
|
5621
|
+
const payload = await response.json();
|
|
5622
|
+
const current = store.snapshot().collab;
|
|
5623
|
+
if (current.captionLanguage !== language) return;
|
|
5624
|
+
const merged = { ...current.captionTranslations };
|
|
5625
|
+
for (const [id, value] of Object.entries(payload.translations ?? {})) {
|
|
5626
|
+
if (typeof value === "string") merged[id] = value;
|
|
5627
|
+
}
|
|
5628
|
+
store.patch({
|
|
5629
|
+
collab: { ...current, captionTranslations: merged, captionTranslationError: null }
|
|
5630
|
+
});
|
|
5035
5631
|
}
|
|
5036
5632
|
};
|
|
5037
5633
|
}
|
|
@@ -5432,10 +6028,12 @@ export {
|
|
|
5432
6028
|
ATTENDANCE_CSV_HEADER,
|
|
5433
6029
|
CALL_INVITE_KIND,
|
|
5434
6030
|
CALL_INVITE_VERSION,
|
|
6031
|
+
CAPTION_LANGUAGES,
|
|
5435
6032
|
CONSENT_NOTICE_ATTRIBUTE,
|
|
5436
6033
|
DATA_TOPICS,
|
|
5437
6034
|
DEFAULT_EFFECTS,
|
|
5438
6035
|
DEFAULT_PAGE_SIZE,
|
|
6036
|
+
EMPTY_COLLAB,
|
|
5439
6037
|
END_MEETING_NOT_SERVED,
|
|
5440
6038
|
FILE_SESSION_PATH,
|
|
5441
6039
|
GUEST_RECORD_ACCESS_CHOICES,
|
|
@@ -5473,6 +6071,7 @@ export {
|
|
|
5473
6071
|
absenceSentence,
|
|
5474
6072
|
activeWrapUpSummary,
|
|
5475
6073
|
announceMeeting,
|
|
6074
|
+
applyCollabPacket,
|
|
5476
6075
|
applyRoomEvent,
|
|
5477
6076
|
asCallId,
|
|
5478
6077
|
asMeetingId,
|
|
@@ -5480,10 +6079,13 @@ export {
|
|
|
5480
6079
|
asParticipantIdentity,
|
|
5481
6080
|
asRoomName,
|
|
5482
6081
|
asUserId,
|
|
6082
|
+
assignedRoom,
|
|
5483
6083
|
attendanceCsv,
|
|
5484
6084
|
attendanceReport,
|
|
5485
6085
|
attendanceTotals,
|
|
5486
6086
|
attendeeKey,
|
|
6087
|
+
autoAssign,
|
|
6088
|
+
breakoutRemainingMs,
|
|
5487
6089
|
callInviteAction,
|
|
5488
6090
|
callInviteText,
|
|
5489
6091
|
callStateLabel,
|
|
@@ -5493,6 +6095,7 @@ export {
|
|
|
5493
6095
|
captionSegment,
|
|
5494
6096
|
changesPatch,
|
|
5495
6097
|
clockPosition,
|
|
6098
|
+
collabFromState,
|
|
5496
6099
|
computeStage,
|
|
5497
6100
|
countLabel,
|
|
5498
6101
|
createCallCenter,
|
|
@@ -5513,6 +6116,7 @@ export {
|
|
|
5513
6116
|
createWebDeviceStorage,
|
|
5514
6117
|
createWebEffectsStorage,
|
|
5515
6118
|
createWebRoomTokenStorage,
|
|
6119
|
+
defaultCaptionLanguage,
|
|
5516
6120
|
displayNameFor,
|
|
5517
6121
|
fileDownloadPath,
|
|
5518
6122
|
gridColumns,
|
|
@@ -5535,6 +6139,7 @@ export {
|
|
|
5535
6139
|
listenOnlyNotice,
|
|
5536
6140
|
loadFollowUpDraft,
|
|
5537
6141
|
loadGuestMeetingRecord,
|
|
6142
|
+
loadMeetingActivities,
|
|
5538
6143
|
loadMeetingChat,
|
|
5539
6144
|
loadMeetingRecord,
|
|
5540
6145
|
mapConnectionState,
|
|
@@ -5590,6 +6195,7 @@ export {
|
|
|
5590
6195
|
uniqueAttendees,
|
|
5591
6196
|
userTopic,
|
|
5592
6197
|
wasRefused,
|
|
6198
|
+
withItem,
|
|
5593
6199
|
wrapUpStatus
|
|
5594
6200
|
};
|
|
5595
6201
|
//# sourceMappingURL=index.js.map
|