@ai-matrx/meet 0.11.28 → 0.11.30
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 +6 -0
- package/dist/{collab-CUxjUtrb.d.cts → collab-XY6Xj1dG.d.cts} +5 -0
- package/dist/{collab-CUxjUtrb.d.ts → collab-XY6Xj1dG.d.ts} +5 -0
- package/dist/{components-DPdU3POT.d.cts → components-4f9Mf8fz.d.cts} +1 -1
- package/dist/{components-CUPzD0y3.d.ts → components-Cwep2NYU.d.ts} +1 -1
- package/dist/core.cjs +893 -43
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +549 -169
- package/dist/core.d.ts +549 -169
- package/dist/core.js +893 -43
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +893 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +893 -43
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1005 -48
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +6 -12
- package/dist/react.d.ts +6 -12
- package/dist/react.js +1005 -48
- package/dist/react.js.map +1 -1
- package/dist/skins/meet.cjs +263 -27
- package/dist/skins/meet.cjs.map +1 -1
- package/dist/skins/meet.d.cts +2 -2
- package/dist/skins/meet.d.ts +2 -2
- package/dist/skins/meet.js +263 -27
- package/dist/skins/meet.js.map +1 -1
- package/dist/styles.css +46 -0
- package/package.json +4 -4
package/dist/react.js
CHANGED
|
@@ -1,6 +1,352 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
+
// src/core/connection-supervisor.ts
|
|
5
|
+
var DEFAULT_CONNECTION_POLICY = {
|
|
6
|
+
bannerDelayMs: 2e3,
|
|
7
|
+
autoRejoin: true,
|
|
8
|
+
autoRejoinMaxAttempts: 5,
|
|
9
|
+
reconnectGraceSeconds: 30
|
|
10
|
+
};
|
|
11
|
+
function clampInt(value, min, max, fallback) {
|
|
12
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
13
|
+
return Math.min(max, Math.max(min, Math.round(value)));
|
|
14
|
+
}
|
|
15
|
+
function connectionPolicyFrom(raw) {
|
|
16
|
+
if (typeof raw !== "object" || raw === null) return DEFAULT_CONNECTION_POLICY;
|
|
17
|
+
const row = raw;
|
|
18
|
+
const d = DEFAULT_CONNECTION_POLICY;
|
|
19
|
+
return {
|
|
20
|
+
bannerDelayMs: clampInt(row["reconnect_banner_delay_ms"], 0, 1e4, d.bannerDelayMs),
|
|
21
|
+
autoRejoin: typeof row["auto_rejoin"] === "boolean" ? row["auto_rejoin"] : d.autoRejoin,
|
|
22
|
+
autoRejoinMaxAttempts: clampInt(row["auto_rejoin_max_attempts"], 0, 20, d.autoRejoinMaxAttempts),
|
|
23
|
+
reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
var LIVEKIT_BASE_DELAYS_MS = [0, 300, 1200, 2700, 4800, 7e3];
|
|
27
|
+
function createReconnectPolicy(options = {}) {
|
|
28
|
+
const delays = options.delaysMs !== void 0 && options.delaysMs.length > 0 ? options.delaysMs : LIVEKIT_BASE_DELAYS_MS;
|
|
29
|
+
const giveUpMs = options.giveUpMs ?? 45e3;
|
|
30
|
+
const maxAttempts = options.maxAttempts ?? 12;
|
|
31
|
+
const random = options.random ?? Math.random;
|
|
32
|
+
return {
|
|
33
|
+
nextRetryDelayInMs({ retryCount, elapsedMs }) {
|
|
34
|
+
if (retryCount >= maxAttempts || elapsedMs >= giveUpMs) return null;
|
|
35
|
+
const base = delays[Math.min(retryCount, delays.length - 1)] ?? 0;
|
|
36
|
+
const spread = Math.min(2e3, Math.max(500, base / 2));
|
|
37
|
+
return Math.round(base + random() * spread);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function createWebNetworkPort() {
|
|
42
|
+
const scope = globalThis;
|
|
43
|
+
return {
|
|
44
|
+
online: () => scope.navigator?.onLine !== false,
|
|
45
|
+
subscribe(listener) {
|
|
46
|
+
if (typeof scope.addEventListener !== "function") return () => void 0;
|
|
47
|
+
const on = () => listener(true);
|
|
48
|
+
const off = () => listener(false);
|
|
49
|
+
scope.addEventListener("online", on);
|
|
50
|
+
scope.addEventListener("offline", off);
|
|
51
|
+
return () => {
|
|
52
|
+
scope.removeEventListener?.("online", on);
|
|
53
|
+
scope.removeEventListener?.("offline", off);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function createReachabilityProbe(urls, timeoutMs = 4e3) {
|
|
59
|
+
return async () => {
|
|
60
|
+
const targets = urls().filter((url) => url.length > 0);
|
|
61
|
+
if (targets.length === 0 || typeof fetch !== "function") return true;
|
|
62
|
+
const one = async (url) => {
|
|
63
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
64
|
+
const timer = setTimeout(() => controller?.abort(), timeoutMs);
|
|
65
|
+
try {
|
|
66
|
+
await fetch(url, {
|
|
67
|
+
method: "GET",
|
|
68
|
+
mode: "no-cors",
|
|
69
|
+
cache: "no-store",
|
|
70
|
+
credentials: "omit",
|
|
71
|
+
...controller !== null ? { signal: controller.signal } : {}
|
|
72
|
+
});
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const answers = await Promise.all(targets.map(one));
|
|
81
|
+
return answers.every(Boolean);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function httpUrlOf(serverUrl) {
|
|
85
|
+
return serverUrl.replace(/^ws(s?):\/\//i, "http$1://");
|
|
86
|
+
}
|
|
87
|
+
var RESUME_WINDOW_MS = 15 * 6e4;
|
|
88
|
+
var RESUME_KEY = "mx-meet:resume:";
|
|
89
|
+
function createWebResumeStorage() {
|
|
90
|
+
const storage = globalThis.sessionStorage;
|
|
91
|
+
if (storage === void 0) return void 0;
|
|
92
|
+
return {
|
|
93
|
+
load(roomName) {
|
|
94
|
+
try {
|
|
95
|
+
const raw = storage.getItem(RESUME_KEY + roomName);
|
|
96
|
+
if (raw === null) return null;
|
|
97
|
+
const parsed = JSON.parse(raw);
|
|
98
|
+
return typeof parsed?.at === "number" && typeof parsed.request?.roomName === "string" ? parsed : null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
save(roomName, marker) {
|
|
104
|
+
try {
|
|
105
|
+
storage.setItem(RESUME_KEY + roomName, JSON.stringify(marker));
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
clear(roomName) {
|
|
110
|
+
try {
|
|
111
|
+
storage.removeItem(RESUME_KEY + roomName);
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
var CLEARS_RESUME = (phase) => phase === "left" || phase === "removed" || phase === "superseded" || phase === "inactive" || phase === "ended" || phase === "record" || phase === "prejoin" || phase.startsWith("refused:") || phase.startsWith("waiting:");
|
|
118
|
+
function autoRejoinDelayMs(attempt, random = Math.random) {
|
|
119
|
+
const base = Math.min(15e3, 1e3 * 2 ** Math.max(0, attempt));
|
|
120
|
+
return Math.round(base * (0.5 + random()));
|
|
121
|
+
}
|
|
122
|
+
function createConnectionSupervisor(options) {
|
|
123
|
+
const { store, network } = options;
|
|
124
|
+
const now = options.now ?? (() => Date.now());
|
|
125
|
+
const random = options.random ?? Math.random;
|
|
126
|
+
const probeEveryMs = options.probeEveryMs ?? 3e3;
|
|
127
|
+
let driverRegion = null;
|
|
128
|
+
let probeTimer = null;
|
|
129
|
+
let probing = false;
|
|
130
|
+
let bannerTimer = null;
|
|
131
|
+
let rejoinTimer = null;
|
|
132
|
+
let rejoinAttempts = 0;
|
|
133
|
+
let lastPhase = store.snapshot().phase;
|
|
134
|
+
let lastSavedAt = 0;
|
|
135
|
+
let disposed = false;
|
|
136
|
+
const inCall = () => store.snapshot().phase === "in_call";
|
|
137
|
+
const dropped = () => store.snapshot().phase === "disconnected";
|
|
138
|
+
function setRegion(next) {
|
|
139
|
+
if (store.snapshot().connection !== next) store.patch({ connection: next });
|
|
140
|
+
}
|
|
141
|
+
function regionFor(reachable) {
|
|
142
|
+
if (!network.online()) return "offline";
|
|
143
|
+
if (reachable === false) return "server_unreachable";
|
|
144
|
+
if (dropped()) return reachable === true ? "stable" : store.snapshot().connection;
|
|
145
|
+
return driverRegion ?? "stable";
|
|
146
|
+
}
|
|
147
|
+
function needsWatch() {
|
|
148
|
+
const snap = store.snapshot();
|
|
149
|
+
if (dropped()) return true;
|
|
150
|
+
return inCall() && (driverRegion !== null || snap.connection !== "stable");
|
|
151
|
+
}
|
|
152
|
+
function stopProbe() {
|
|
153
|
+
if (probeTimer !== null) clearTimeout(probeTimer);
|
|
154
|
+
probeTimer = null;
|
|
155
|
+
}
|
|
156
|
+
function scheduleProbe(delayMs) {
|
|
157
|
+
if (disposed || probeTimer !== null) return;
|
|
158
|
+
probeTimer = setTimeout(() => {
|
|
159
|
+
probeTimer = null;
|
|
160
|
+
void runProbe();
|
|
161
|
+
}, delayMs);
|
|
162
|
+
}
|
|
163
|
+
async function runProbe() {
|
|
164
|
+
if (disposed || probing) return;
|
|
165
|
+
if (!needsWatch()) return;
|
|
166
|
+
if (!network.online()) {
|
|
167
|
+
setRegion("offline");
|
|
168
|
+
scheduleProbe(probeEveryMs);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
probing = true;
|
|
172
|
+
let reachable = false;
|
|
173
|
+
try {
|
|
174
|
+
reachable = await options.probe();
|
|
175
|
+
} catch {
|
|
176
|
+
reachable = false;
|
|
177
|
+
} finally {
|
|
178
|
+
probing = false;
|
|
179
|
+
}
|
|
180
|
+
if (disposed) return;
|
|
181
|
+
if (inCall() || dropped()) setRegion(regionFor(reachable));
|
|
182
|
+
if (dropped() && reachable) maybeRejoin();
|
|
183
|
+
if (needsWatch()) scheduleProbe(Math.round(probeEveryMs * (0.75 + random() * 0.5)));
|
|
184
|
+
}
|
|
185
|
+
function syncBanner() {
|
|
186
|
+
const snap = store.snapshot();
|
|
187
|
+
const unstable = snap.phase === "in_call" && snap.connection !== "stable";
|
|
188
|
+
if (!unstable) {
|
|
189
|
+
if (bannerTimer !== null) clearTimeout(bannerTimer);
|
|
190
|
+
bannerTimer = null;
|
|
191
|
+
if (snap.connectionBanner) store.patch({ connectionBanner: false });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (snap.connectionBanner || bannerTimer !== null) return;
|
|
195
|
+
const delay = options.policy().bannerDelayMs;
|
|
196
|
+
if (delay <= 0) {
|
|
197
|
+
store.patch({ connectionBanner: true });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
bannerTimer = setTimeout(() => {
|
|
201
|
+
bannerTimer = null;
|
|
202
|
+
const latest = store.snapshot();
|
|
203
|
+
if (latest.phase === "in_call" && latest.connection !== "stable" && !latest.connectionBanner) {
|
|
204
|
+
store.patch({ connectionBanner: true });
|
|
205
|
+
}
|
|
206
|
+
}, delay);
|
|
207
|
+
}
|
|
208
|
+
function stopRejoin() {
|
|
209
|
+
if (rejoinTimer !== null) clearTimeout(rejoinTimer);
|
|
210
|
+
rejoinTimer = null;
|
|
211
|
+
}
|
|
212
|
+
function maybeRejoin() {
|
|
213
|
+
if (disposed || rejoinTimer !== null || !dropped()) return;
|
|
214
|
+
const policy = options.policy();
|
|
215
|
+
if (!policy.autoRejoin) return;
|
|
216
|
+
if (rejoinAttempts >= policy.autoRejoinMaxAttempts) return;
|
|
217
|
+
if (!network.online()) return;
|
|
218
|
+
const attempt = rejoinAttempts;
|
|
219
|
+
rejoinTimer = setTimeout(() => {
|
|
220
|
+
rejoinTimer = null;
|
|
221
|
+
if (disposed || !dropped() || !network.online()) return;
|
|
222
|
+
rejoinAttempts = attempt + 1;
|
|
223
|
+
options.report?.("info", `meet: rejoining by itself (attempt ${rejoinAttempts} of ${policy.autoRejoinMaxAttempts})`);
|
|
224
|
+
void options.autoRejoin().catch(() => void 0);
|
|
225
|
+
}, autoRejoinDelayMs(attempt, random));
|
|
226
|
+
}
|
|
227
|
+
function saveMarker(force) {
|
|
228
|
+
const resume = options.resume;
|
|
229
|
+
const snap = store.snapshot();
|
|
230
|
+
if (resume === void 0 || snap.roomName === null) return;
|
|
231
|
+
const at = now();
|
|
232
|
+
if (!force && at - lastSavedAt < 1e4) return;
|
|
233
|
+
const request = options.currentRequest();
|
|
234
|
+
if (request === null) return;
|
|
235
|
+
lastSavedAt = at;
|
|
236
|
+
resume.save(snap.roomName, { request, at });
|
|
237
|
+
}
|
|
238
|
+
const unsubscribeStore = store.subscribe(() => {
|
|
239
|
+
if (disposed) return;
|
|
240
|
+
const snap = store.snapshot();
|
|
241
|
+
const phase = snap.phase;
|
|
242
|
+
const changed = phase !== lastPhase;
|
|
243
|
+
lastPhase = phase;
|
|
244
|
+
if (changed) {
|
|
245
|
+
if (phase === "in_call") {
|
|
246
|
+
rejoinAttempts = 0;
|
|
247
|
+
stopRejoin();
|
|
248
|
+
saveMarker(true);
|
|
249
|
+
} else if (phase === "disconnected") {
|
|
250
|
+
driverRegion = null;
|
|
251
|
+
saveMarker(true);
|
|
252
|
+
stopProbe();
|
|
253
|
+
scheduleProbe(0);
|
|
254
|
+
} else {
|
|
255
|
+
stopRejoin();
|
|
256
|
+
if (phase !== "joining") driverRegion = null;
|
|
257
|
+
if (CLEARS_RESUME(phase) && snap.roomName !== null) options.resume?.clear(snap.roomName);
|
|
258
|
+
}
|
|
259
|
+
} else if (phase === "in_call" || phase === "disconnected") {
|
|
260
|
+
saveMarker(false);
|
|
261
|
+
}
|
|
262
|
+
syncBanner();
|
|
263
|
+
});
|
|
264
|
+
const unsubscribeNetwork = network.subscribe((online) => {
|
|
265
|
+
if (disposed) return;
|
|
266
|
+
if (!online) {
|
|
267
|
+
if (inCall() || dropped()) setRegion("offline");
|
|
268
|
+
stopRejoin();
|
|
269
|
+
stopProbe();
|
|
270
|
+
if (needsWatch()) scheduleProbe(probeEveryMs);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
stopProbe();
|
|
274
|
+
if (needsWatch()) scheduleProbe(0);
|
|
275
|
+
});
|
|
276
|
+
return {
|
|
277
|
+
driverState(state) {
|
|
278
|
+
if (disposed) return;
|
|
279
|
+
if (state === "connected") {
|
|
280
|
+
driverRegion = null;
|
|
281
|
+
if (inCall() || store.snapshot().phase === "joining") setRegion(network.online() ? "stable" : "offline");
|
|
282
|
+
if (!needsWatch()) stopProbe();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (state === "reconnecting" || state === "signal_reconnecting") {
|
|
286
|
+
driverRegion = state;
|
|
287
|
+
if (inCall()) {
|
|
288
|
+
setRegion(network.online() ? state : "offline");
|
|
289
|
+
stopProbe();
|
|
290
|
+
scheduleProbe(0);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
exitConnection() {
|
|
295
|
+
return network.online() ? "server_unreachable" : "offline";
|
|
296
|
+
},
|
|
297
|
+
resumable(roomName) {
|
|
298
|
+
const resume = options.resume;
|
|
299
|
+
if (resume === void 0) return null;
|
|
300
|
+
const marker = resume.load(roomName);
|
|
301
|
+
if (marker === null) return null;
|
|
302
|
+
if (now() - marker.at > RESUME_WINDOW_MS) {
|
|
303
|
+
resume.clear(roomName);
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return marker;
|
|
307
|
+
},
|
|
308
|
+
dispose() {
|
|
309
|
+
disposed = true;
|
|
310
|
+
stopProbe();
|
|
311
|
+
stopRejoin();
|
|
312
|
+
if (bannerTimer !== null) clearTimeout(bannerTimer);
|
|
313
|
+
bannerTimer = null;
|
|
314
|
+
unsubscribeStore();
|
|
315
|
+
unsubscribeNetwork();
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
var DELIBERATE_DEPARTURES = /* @__PURE__ */ new Set([
|
|
320
|
+
"CLIENT_INITIATED",
|
|
321
|
+
"PARTICIPANT_REMOVED",
|
|
322
|
+
"ROOM_DELETED",
|
|
323
|
+
"ROOM_CLOSED",
|
|
324
|
+
"DUPLICATE_IDENTITY",
|
|
325
|
+
"USER_REJECTED",
|
|
326
|
+
"USER_UNAVAILABLE",
|
|
327
|
+
"SIP_TRUNK_FAILURE"
|
|
328
|
+
]);
|
|
329
|
+
function isUnannouncedDeparture(reason) {
|
|
330
|
+
return reason === null || !DELIBERATE_DEPARTURES.has(reason);
|
|
331
|
+
}
|
|
332
|
+
function mergeGhosts(live, ghosts, at) {
|
|
333
|
+
const present = new Set(live.map((p) => p.identity));
|
|
334
|
+
const kept = /* @__PURE__ */ new Map();
|
|
335
|
+
const extra = [];
|
|
336
|
+
for (const [identity, ghost] of ghosts) {
|
|
337
|
+
if (present.has(identity) || ghost.until <= at) continue;
|
|
338
|
+
kept.set(identity, ghost);
|
|
339
|
+
extra.push({
|
|
340
|
+
...ghost.participant,
|
|
341
|
+
micEnabled: false,
|
|
342
|
+
cameraEnabled: false,
|
|
343
|
+
screenSharing: false,
|
|
344
|
+
reconnecting: true
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return { roster: [...live, ...extra], kept };
|
|
348
|
+
}
|
|
349
|
+
|
|
4
350
|
// src/react/provider.tsx
|
|
5
351
|
import { globalSlot as globalSlot2 } from "@ai-matrx/kit/global-slot";
|
|
6
352
|
import {
|
|
@@ -410,7 +756,8 @@ function parseRoomToken(body, roomName) {
|
|
|
410
756
|
admissionState: readString(payload, "admission_state") ?? void 0,
|
|
411
757
|
pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
|
|
412
758
|
breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
|
|
413
|
-
observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone"
|
|
759
|
+
observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
|
|
760
|
+
connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
|
|
414
761
|
};
|
|
415
762
|
}
|
|
416
763
|
function createTokenClient(options) {
|
|
@@ -1115,6 +1462,10 @@ function nextPhase(from, event) {
|
|
|
1115
1462
|
return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
|
|
1116
1463
|
case "join":
|
|
1117
1464
|
return JOINABLE_FROM.has(from) ? "joining" : null;
|
|
1465
|
+
case "auto_rejoin":
|
|
1466
|
+
return from === "disconnected" ? "joining" : null;
|
|
1467
|
+
case "resumed":
|
|
1468
|
+
return from === "resolving" ? "joining" : null;
|
|
1118
1469
|
case "back_to_prejoin":
|
|
1119
1470
|
return from === "left" || from === "disconnected" ? "prejoin" : null;
|
|
1120
1471
|
case "retry":
|
|
@@ -1832,13 +2183,19 @@ function recordingLabel(state) {
|
|
|
1832
2183
|
}
|
|
1833
2184
|
|
|
1834
2185
|
// src/core/attendance.ts
|
|
2186
|
+
var IDLE_MEDIA_REGION = {
|
|
2187
|
+
permission: { microphone: "unknown", camera: "unknown" },
|
|
2188
|
+
audioOutput: "ok",
|
|
2189
|
+
notice: null,
|
|
2190
|
+
offer: null
|
|
2191
|
+
};
|
|
1835
2192
|
var IDLE_TRACK = { enabled: false, unavailable: null };
|
|
1836
2193
|
var IDLE_MEDIA = {
|
|
1837
2194
|
microphone: IDLE_TRACK,
|
|
1838
2195
|
camera: IDLE_TRACK
|
|
1839
2196
|
};
|
|
1840
|
-
function unavailableFrom(error) {
|
|
1841
|
-
return { code: error.code, message: error.message, remedy: error.remedy };
|
|
2197
|
+
function unavailableFrom(error, reason) {
|
|
2198
|
+
return reason === void 0 ? { code: error.code, message: error.message, remedy: error.remedy } : { code: error.code, message: error.message, remedy: error.remedy, reason };
|
|
1842
2199
|
}
|
|
1843
2200
|
var LISTEN_ONLY_NOTICE = "You're attending without a microphone \u2014 others can't hear you";
|
|
1844
2201
|
function isListenOnly(media) {
|
|
@@ -3254,6 +3611,7 @@ var EMPTY = {
|
|
|
3254
3611
|
phaseNotice: null,
|
|
3255
3612
|
roomName: null,
|
|
3256
3613
|
connection: "stable",
|
|
3614
|
+
connectionBanner: false,
|
|
3257
3615
|
share: NO_SHARE,
|
|
3258
3616
|
call: null,
|
|
3259
3617
|
waiting: null,
|
|
@@ -3285,6 +3643,7 @@ var EMPTY = {
|
|
|
3285
3643
|
wrapUp: null,
|
|
3286
3644
|
noteTakerRefusal: null,
|
|
3287
3645
|
media: IDLE_MEDIA,
|
|
3646
|
+
mediaRegion: IDLE_MEDIA_REGION,
|
|
3288
3647
|
failure: null,
|
|
3289
3648
|
collab: EMPTY_COLLAB,
|
|
3290
3649
|
breakoutRoomId: null
|
|
@@ -3502,7 +3861,7 @@ function createMeetStore(initial = {}) {
|
|
|
3502
3861
|
},
|
|
3503
3862
|
setLocalTrack(kind, next) {
|
|
3504
3863
|
const current = state.media[kind];
|
|
3505
|
-
if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.message === next.unavailable?.message) {
|
|
3864
|
+
if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.reason === next.unavailable?.reason && current.unavailable?.message === next.unavailable?.message) {
|
|
3506
3865
|
return;
|
|
3507
3866
|
}
|
|
3508
3867
|
commit({ ...state, media: { ...state.media, [kind]: next } });
|
|
@@ -4089,6 +4448,342 @@ function createMeetSessionIdentity(options) {
|
|
|
4089
4448
|
});
|
|
4090
4449
|
}
|
|
4091
4450
|
|
|
4451
|
+
// src/core/media-supervisor.ts
|
|
4452
|
+
function classifyDeviceFailure(error, permission) {
|
|
4453
|
+
if (permission === "denied") return "denied";
|
|
4454
|
+
switch (error.code) {
|
|
4455
|
+
case "permission-denied":
|
|
4456
|
+
if (/by system/i.test(error.message) || permission === "granted") return "blocked_os";
|
|
4457
|
+
return "denied";
|
|
4458
|
+
case "device-not-found":
|
|
4459
|
+
return "none";
|
|
4460
|
+
case "device-in-use":
|
|
4461
|
+
return "busy";
|
|
4462
|
+
case "insecure-context":
|
|
4463
|
+
return "blocked_site";
|
|
4464
|
+
default:
|
|
4465
|
+
return "failed";
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
function deviceNoticeCode(kind, reason) {
|
|
4469
|
+
if (reason === "denied" || reason === "blocked_site" || reason === "blocked_os") {
|
|
4470
|
+
return kind === "microphone" ? "mic-blocked" : "camera-blocked";
|
|
4471
|
+
}
|
|
4472
|
+
return "device-lost";
|
|
4473
|
+
}
|
|
4474
|
+
function deviceUnavailableReason(track, permission) {
|
|
4475
|
+
if (track.enabled) return null;
|
|
4476
|
+
if (permission === "denied") return "denied";
|
|
4477
|
+
const refused = track.unavailable;
|
|
4478
|
+
if (refused === null) return null;
|
|
4479
|
+
return refused.reason ?? classifyDeviceFailure({ code: refused.code, message: refused.message }, permission);
|
|
4480
|
+
}
|
|
4481
|
+
function deviceChipLabel(kind, reason) {
|
|
4482
|
+
const what = kind === "microphone" ? "Mic" : "Camera";
|
|
4483
|
+
switch (reason) {
|
|
4484
|
+
case "denied":
|
|
4485
|
+
case "blocked_site":
|
|
4486
|
+
case "blocked_os":
|
|
4487
|
+
return `${what} blocked`;
|
|
4488
|
+
case "none":
|
|
4489
|
+
return kind === "microphone" ? "No microphone found" : "No camera found";
|
|
4490
|
+
case "busy":
|
|
4491
|
+
return `${what} in use by another app`;
|
|
4492
|
+
case "revoked":
|
|
4493
|
+
return `${what} access was turned off`;
|
|
4494
|
+
default:
|
|
4495
|
+
return `${what} didn't start`;
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
function deviceFixSteps(kind, reason, where) {
|
|
4499
|
+
const device = kind === "microphone" ? "Microphone" : "Camera";
|
|
4500
|
+
const lower = device.toLowerCase();
|
|
4501
|
+
switch (reason) {
|
|
4502
|
+
case "denied":
|
|
4503
|
+
case "revoked":
|
|
4504
|
+
case "blocked_site":
|
|
4505
|
+
switch (where.browser) {
|
|
4506
|
+
case "firefox":
|
|
4507
|
+
return [`Click the crossed-out ${lower} icon in the address bar`, "Clear the Blocked setting", "Press Try again and choose Allow"];
|
|
4508
|
+
case "safari":
|
|
4509
|
+
return where.os === "ios" ? [`Settings \u203A Apps \u203A Safari \u203A ${device}`, "Choose Allow", "Return here and press Try again"] : ["Safari \u203A Settings for This Website", `Set ${device} to Allow`, "Press Try again"];
|
|
4510
|
+
default:
|
|
4511
|
+
return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
|
|
4512
|
+
}
|
|
4513
|
+
case "blocked_os": {
|
|
4514
|
+
const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
|
|
4515
|
+
if (where.os === "mac") return [`System Settings \u203A Privacy & Security \u203A ${device}`, `Turn on ${app}`, "Reopen the browser if asked, then Try again"];
|
|
4516
|
+
if (where.os === "windows") return [`Settings \u203A Privacy & security \u203A ${device}`, `Turn on ${device} access for apps`, "Press Try again"];
|
|
4517
|
+
return [`Allow ${lower} access for ${app} in your system settings`, "Press Try again"];
|
|
4518
|
+
}
|
|
4519
|
+
case "busy":
|
|
4520
|
+
return [`Close other apps using the ${lower} (Zoom, Teams, another tab)`, "Press Try again"];
|
|
4521
|
+
case "none":
|
|
4522
|
+
return [`Connect a ${lower}`, "Press Try again"];
|
|
4523
|
+
default:
|
|
4524
|
+
return [`Pick another ${lower} in Settings`, "Press Try again"];
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
function detectBrowser(userAgent) {
|
|
4528
|
+
if (/Edg\//.test(userAgent)) return "edge";
|
|
4529
|
+
if (/Firefox\//.test(userAgent)) return "firefox";
|
|
4530
|
+
if (/Chrome\/|Chromium\/|CriOS\//.test(userAgent)) return "chrome";
|
|
4531
|
+
if (/Safari\//.test(userAgent)) return "safari";
|
|
4532
|
+
return "other";
|
|
4533
|
+
}
|
|
4534
|
+
function detectOs(userAgent, maxTouchPoints = 0) {
|
|
4535
|
+
if (/iPhone|iPad|iPod/.test(userAgent)) return "ios";
|
|
4536
|
+
if (/Macintosh/.test(userAgent)) return maxTouchPoints > 1 ? "ios" : "mac";
|
|
4537
|
+
if (/Android/.test(userAgent)) return "android";
|
|
4538
|
+
if (/Windows/.test(userAgent)) return "windows";
|
|
4539
|
+
if (/Linux|CrOS/.test(userAgent)) return "linux";
|
|
4540
|
+
return "other";
|
|
4541
|
+
}
|
|
4542
|
+
function webMediaPlatform() {
|
|
4543
|
+
const scope = globalThis;
|
|
4544
|
+
const nav = scope.navigator;
|
|
4545
|
+
if (nav === void 0) return null;
|
|
4546
|
+
const ua = nav.userAgent ?? "";
|
|
4547
|
+
const reading = (state) => state === "granted" || state === "denied" || state === "prompt" ? state : "unknown";
|
|
4548
|
+
const query = async (kind) => {
|
|
4549
|
+
try {
|
|
4550
|
+
return await nav.permissions?.query({ name: kind }) ?? null;
|
|
4551
|
+
} catch {
|
|
4552
|
+
return null;
|
|
4553
|
+
}
|
|
4554
|
+
};
|
|
4555
|
+
return {
|
|
4556
|
+
browser: detectBrowser(ua),
|
|
4557
|
+
os: detectOs(ua, nav.maxTouchPoints ?? 0),
|
|
4558
|
+
async queryPermission(kind) {
|
|
4559
|
+
return reading((await query(kind))?.state);
|
|
4560
|
+
},
|
|
4561
|
+
watchPermission(kind, onChange) {
|
|
4562
|
+
let status = null;
|
|
4563
|
+
let stopped = false;
|
|
4564
|
+
void query(kind).then((found) => {
|
|
4565
|
+
if (stopped || found === null) return;
|
|
4566
|
+
status = found;
|
|
4567
|
+
found.onchange = () => onChange(reading(found.state));
|
|
4568
|
+
});
|
|
4569
|
+
return () => {
|
|
4570
|
+
stopped = true;
|
|
4571
|
+
if (status !== null) status.onchange = null;
|
|
4572
|
+
};
|
|
4573
|
+
},
|
|
4574
|
+
async listDevices() {
|
|
4575
|
+
try {
|
|
4576
|
+
return await nav.mediaDevices?.enumerateDevices() ?? [];
|
|
4577
|
+
} catch {
|
|
4578
|
+
return [];
|
|
4579
|
+
}
|
|
4580
|
+
},
|
|
4581
|
+
onVisible(callback) {
|
|
4582
|
+
const doc = scope.document;
|
|
4583
|
+
if (doc === void 0) return () => void 0;
|
|
4584
|
+
const handler = () => {
|
|
4585
|
+
if (doc.visibilityState === "visible") callback();
|
|
4586
|
+
};
|
|
4587
|
+
doc.addEventListener("visibilitychange", handler);
|
|
4588
|
+
return () => doc.removeEventListener("visibilitychange", handler);
|
|
4589
|
+
}
|
|
4590
|
+
};
|
|
4591
|
+
}
|
|
4592
|
+
var INPUT_KIND = { microphone: "audioinput", camera: "videoinput" };
|
|
4593
|
+
function isAlias(deviceId2) {
|
|
4594
|
+
return deviceId2 === "default" || deviceId2 === "communications" || deviceId2 === "";
|
|
4595
|
+
}
|
|
4596
|
+
function createMediaSupervisor(options) {
|
|
4597
|
+
const { store, platform } = options;
|
|
4598
|
+
let unsubscribers = [];
|
|
4599
|
+
let started = false;
|
|
4600
|
+
let noticeId = 0;
|
|
4601
|
+
let known = null;
|
|
4602
|
+
let inUse = { audioinput: null, videoinput: null };
|
|
4603
|
+
function region() {
|
|
4604
|
+
return store.snapshot().mediaRegion;
|
|
4605
|
+
}
|
|
4606
|
+
function setRegion(next) {
|
|
4607
|
+
const current = region();
|
|
4608
|
+
const merged = { ...current, ...next };
|
|
4609
|
+
if (merged.audioOutput === current.audioOutput && merged.notice === current.notice && merged.offer === current.offer && merged.permission.microphone === current.permission.microphone && merged.permission.camera === current.permission.camera) {
|
|
4610
|
+
return;
|
|
4611
|
+
}
|
|
4612
|
+
store.patch({ mediaRegion: merged });
|
|
4613
|
+
}
|
|
4614
|
+
function setPermission(kind, reading) {
|
|
4615
|
+
const current = region().permission;
|
|
4616
|
+
if (current[kind] === reading) return;
|
|
4617
|
+
setRegion({ permission: { ...current, [kind]: reading } });
|
|
4618
|
+
const track = store.snapshot().media[kind];
|
|
4619
|
+
if (reading === "granted" && !track.enabled && track.unavailable?.reason === "denied") {
|
|
4620
|
+
store.setLocalTrack(kind, {
|
|
4621
|
+
enabled: false,
|
|
4622
|
+
unavailable: { ...track.unavailable, reason: "failed", remedy: "Access is allowed now. Press Try again." }
|
|
4623
|
+
});
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
async function snapshotDevices() {
|
|
4627
|
+
const list = platform === null ? [] : await platform.listDevices();
|
|
4628
|
+
const out = { audioinput: /* @__PURE__ */ new Map(), videoinput: /* @__PURE__ */ new Map() };
|
|
4629
|
+
for (const device of list) {
|
|
4630
|
+
if (device.kind === "audioinput" || device.kind === "videoinput") out[device.kind].set(device.deviceId, device.label);
|
|
4631
|
+
}
|
|
4632
|
+
return out;
|
|
4633
|
+
}
|
|
4634
|
+
function readInUse() {
|
|
4635
|
+
const driver = options.driver();
|
|
4636
|
+
if (driver === null) return;
|
|
4637
|
+
const active = driver.activeDevices();
|
|
4638
|
+
inUse = { audioinput: active.audioinput, videoinput: active.videoinput };
|
|
4639
|
+
}
|
|
4640
|
+
async function handleDevicesChanged() {
|
|
4641
|
+
if (platform === null) return;
|
|
4642
|
+
const before = known;
|
|
4643
|
+
const now = await snapshotDevices();
|
|
4644
|
+
known = now;
|
|
4645
|
+
const driver = options.driver();
|
|
4646
|
+
if (driver === null || before === null) {
|
|
4647
|
+
readInUse();
|
|
4648
|
+
return;
|
|
4649
|
+
}
|
|
4650
|
+
for (const kind of ["microphone", "camera"]) {
|
|
4651
|
+
const deviceKind = INPUT_KIND[kind];
|
|
4652
|
+
const previous = inUse[deviceKind];
|
|
4653
|
+
const track = store.snapshot().media[kind];
|
|
4654
|
+
const gone = previous !== null && !isAlias(previous) && before[deviceKind].has(previous) && !now[deviceKind].has(previous);
|
|
4655
|
+
if (gone) {
|
|
4656
|
+
const from = before[deviceKind].get(previous) ?? "";
|
|
4657
|
+
const remaining = [...now[deviceKind].entries()].filter(([id]) => !isAlias(id));
|
|
4658
|
+
if (remaining.length === 0) {
|
|
4659
|
+
store.setLocalTrack(kind, {
|
|
4660
|
+
enabled: false,
|
|
4661
|
+
unavailable: {
|
|
4662
|
+
code: "device-not-found",
|
|
4663
|
+
message: `meet.${kind}: the ${kind} was disconnected and no other is connected`,
|
|
4664
|
+
remedy: `Connect a ${kind === "microphone" ? "microphone" : "camera"}, then press Try again.`,
|
|
4665
|
+
reason: "none"
|
|
4666
|
+
}
|
|
4667
|
+
});
|
|
4668
|
+
setRegion({ notice: { id: ++noticeId, kind, outcome: "lost", from, to: null } });
|
|
4669
|
+
continue;
|
|
4670
|
+
}
|
|
4671
|
+
if (kind === "camera") {
|
|
4672
|
+
const [nextId, nextLabel] = remaining[0];
|
|
4673
|
+
try {
|
|
4674
|
+
await driver.switchDevice("videoinput", nextId);
|
|
4675
|
+
if (track.enabled || track.unavailable !== null) {
|
|
4676
|
+
await driver.setCameraEnabled(true);
|
|
4677
|
+
store.setLocalTrack("camera", { enabled: true, unavailable: null });
|
|
4678
|
+
}
|
|
4679
|
+
setRegion({ notice: { id: ++noticeId, kind, outcome: "switched", from, to: nextLabel } });
|
|
4680
|
+
} catch (error) {
|
|
4681
|
+
const refused = await unavailable(error, "camera", "meet.camera(fallback)");
|
|
4682
|
+
store.setLocalTrack("camera", { enabled: false, unavailable: refused });
|
|
4683
|
+
setRegion({ notice: { id: ++noticeId, kind, outcome: "lost", from, to: null } });
|
|
4684
|
+
}
|
|
4685
|
+
} else {
|
|
4686
|
+
const active = driver.activeDevices().audioinput;
|
|
4687
|
+
const to = (active !== null ? now.audioinput.get(active) : void 0) ?? now.audioinput.get("default") ?? remaining[0][1];
|
|
4688
|
+
setRegion({ notice: { id: ++noticeId, kind, outcome: "switched", from, to } });
|
|
4689
|
+
}
|
|
4690
|
+
continue;
|
|
4691
|
+
}
|
|
4692
|
+
const added = [...now[deviceKind].entries()].find(([id]) => !isAlias(id) && !before[deviceKind].has(id));
|
|
4693
|
+
if (added !== void 0 && added[0] !== driver.activeDevices()[deviceKind]) {
|
|
4694
|
+
setRegion({ offer: { kind, deviceId: added[0], label: added[1] } });
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
readInUse();
|
|
4698
|
+
}
|
|
4699
|
+
async function unavailable(error, kind, operation) {
|
|
4700
|
+
const normalized = normalizeDeviceError(error, operation);
|
|
4701
|
+
const permission = platform === null ? "unknown" : await platform.queryPermission(kind);
|
|
4702
|
+
if (permission !== "unknown") setPermission(kind, permission);
|
|
4703
|
+
return unavailableFrom(normalized, classifyDeviceFailure(normalized, permission));
|
|
4704
|
+
}
|
|
4705
|
+
return {
|
|
4706
|
+
start() {
|
|
4707
|
+
if (started || platform === null) return;
|
|
4708
|
+
started = true;
|
|
4709
|
+
for (const kind of ["microphone", "camera"]) {
|
|
4710
|
+
void platform.queryPermission(kind).then((reading) => setPermission(kind, reading));
|
|
4711
|
+
unsubscribers.push(platform.watchPermission(kind, (reading) => setPermission(kind, reading)));
|
|
4712
|
+
}
|
|
4713
|
+
unsubscribers.push(
|
|
4714
|
+
platform.onVisible(() => {
|
|
4715
|
+
const driver = options.driver();
|
|
4716
|
+
if (driver?.startAudio === void 0 || region().audioOutput !== "blocked") return;
|
|
4717
|
+
driver.startAudio().catch(() => void 0);
|
|
4718
|
+
})
|
|
4719
|
+
);
|
|
4720
|
+
},
|
|
4721
|
+
stop() {
|
|
4722
|
+
for (const off of unsubscribers) off();
|
|
4723
|
+
unsubscribers = [];
|
|
4724
|
+
started = false;
|
|
4725
|
+
},
|
|
4726
|
+
unavailable,
|
|
4727
|
+
onAudioPlayback(canPlay) {
|
|
4728
|
+
setRegion({ audioOutput: canPlay ? "ok" : "blocked" });
|
|
4729
|
+
},
|
|
4730
|
+
onDevicesChanged() {
|
|
4731
|
+
void handleDevicesChanged().catch((error) => {
|
|
4732
|
+
options.report("warn", `meet: device change could not be handled: ${String(error)}`, "Pick the device in Settings.");
|
|
4733
|
+
});
|
|
4734
|
+
},
|
|
4735
|
+
onDeviceError(error, deviceKind) {
|
|
4736
|
+
if (deviceKind === null) return;
|
|
4737
|
+
const kind = deviceKind === "audioinput" ? "microphone" : "camera";
|
|
4738
|
+
void unavailable(error, kind, `meet.${kind}`).then((refused) => {
|
|
4739
|
+
const track = store.snapshot().media[kind];
|
|
4740
|
+
if (track.enabled) return;
|
|
4741
|
+
store.setLocalTrack(kind, { enabled: false, unavailable: refused });
|
|
4742
|
+
});
|
|
4743
|
+
},
|
|
4744
|
+
onConnected() {
|
|
4745
|
+
void snapshotDevices().then((now) => {
|
|
4746
|
+
known = now;
|
|
4747
|
+
readInUse();
|
|
4748
|
+
});
|
|
4749
|
+
},
|
|
4750
|
+
onDisconnected() {
|
|
4751
|
+
known = null;
|
|
4752
|
+
inUse = { audioinput: null, videoinput: null };
|
|
4753
|
+
setRegion({ audioOutput: "ok", notice: null, offer: null });
|
|
4754
|
+
},
|
|
4755
|
+
async enableAudio() {
|
|
4756
|
+
const driver = options.driver();
|
|
4757
|
+
if (driver?.startAudio === void 0) return;
|
|
4758
|
+
try {
|
|
4759
|
+
await driver.startAudio();
|
|
4760
|
+
setRegion({ audioOutput: "ok" });
|
|
4761
|
+
} catch (error) {
|
|
4762
|
+
options.report("warn", `meet: sound could not be enabled: ${String(error)}`, "Click Enable sound again, or check the tab is not muted.");
|
|
4763
|
+
}
|
|
4764
|
+
},
|
|
4765
|
+
async acceptOffer() {
|
|
4766
|
+
const offer = region().offer;
|
|
4767
|
+
const driver = options.driver();
|
|
4768
|
+
setRegion({ offer: null });
|
|
4769
|
+
if (offer === null || driver === null) return;
|
|
4770
|
+
try {
|
|
4771
|
+
await driver.switchDevice(INPUT_KIND[offer.kind], offer.deviceId);
|
|
4772
|
+
readInUse();
|
|
4773
|
+
} catch (error) {
|
|
4774
|
+
const refused = await unavailable(error, offer.kind, "meet.switchDevice");
|
|
4775
|
+
options.report("warn", refused.message, refused.remedy);
|
|
4776
|
+
}
|
|
4777
|
+
},
|
|
4778
|
+
dismissOffer() {
|
|
4779
|
+
setRegion({ offer: null });
|
|
4780
|
+
},
|
|
4781
|
+
dismissNotice() {
|
|
4782
|
+
setRegion({ notice: null });
|
|
4783
|
+
}
|
|
4784
|
+
};
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4092
4787
|
// src/core/room-events.ts
|
|
4093
4788
|
var RECORDING_STATES = [
|
|
4094
4789
|
"idle",
|
|
@@ -4535,6 +5230,18 @@ function createRoomEngine(options) {
|
|
|
4535
5230
|
let disposed = false;
|
|
4536
5231
|
let moving = false;
|
|
4537
5232
|
const confirmingEnds = /* @__PURE__ */ new WeakSet();
|
|
5233
|
+
let lastServerUrl = null;
|
|
5234
|
+
let connectionPolicy = DEFAULT_CONNECTION_POLICY;
|
|
5235
|
+
let ghosts = /* @__PURE__ */ new Map();
|
|
5236
|
+
let ghostTimer = null;
|
|
5237
|
+
let liveRoster = [];
|
|
5238
|
+
const departures = /* @__PURE__ */ new Map();
|
|
5239
|
+
function requestAsNow() {
|
|
5240
|
+
if (lastRequest === null) return null;
|
|
5241
|
+
const media2 = store.snapshot().media;
|
|
5242
|
+
if (store.snapshot().phase !== "in_call") return lastRequest;
|
|
5243
|
+
return { ...lastRequest, mode: media2.camera.enabled ? "video" : "audio", microphone: media2.microphone.enabled };
|
|
5244
|
+
}
|
|
4538
5245
|
async function dropRoom() {
|
|
4539
5246
|
const target = driver;
|
|
4540
5247
|
driver = null;
|
|
@@ -4544,7 +5251,9 @@ function createRoomEngine(options) {
|
|
|
4544
5251
|
}
|
|
4545
5252
|
store.setLocalTrack("microphone", IDLE_TRACK);
|
|
4546
5253
|
store.setLocalTrack("camera", IDLE_TRACK);
|
|
5254
|
+
media.onDisconnected();
|
|
4547
5255
|
store.patch({ share: NO_SHARE, connection: "stable" });
|
|
5256
|
+
clearGhosts();
|
|
4548
5257
|
await target?.disconnect();
|
|
4549
5258
|
}
|
|
4550
5259
|
async function leaveRoom() {
|
|
@@ -4608,11 +5317,26 @@ function createRoomEngine(options) {
|
|
|
4608
5317
|
report("error", normalized.message, normalized.remedy);
|
|
4609
5318
|
return normalized;
|
|
4610
5319
|
}
|
|
5320
|
+
const media = createMediaSupervisor({
|
|
5321
|
+
store,
|
|
5322
|
+
platform: options.mediaPlatform === void 0 ? webMediaPlatform() : options.mediaPlatform,
|
|
5323
|
+
driver: () => driver,
|
|
5324
|
+
report
|
|
5325
|
+
});
|
|
4611
5326
|
const handlers = {
|
|
5327
|
+
audioPlayback(canPlay) {
|
|
5328
|
+
media.onAudioPlayback(canPlay);
|
|
5329
|
+
},
|
|
5330
|
+
devicesChanged() {
|
|
5331
|
+
media.onDevicesChanged();
|
|
5332
|
+
},
|
|
5333
|
+
mediaDeviceError(error, kind) {
|
|
5334
|
+
media.onDeviceError(error, kind);
|
|
5335
|
+
},
|
|
4612
5336
|
participants(list) {
|
|
4613
5337
|
const joinedAt = new Date(now()).toISOString();
|
|
4614
5338
|
const before = new Set(store.snapshot().participants.map((p) => p.identity));
|
|
4615
|
-
|
|
5339
|
+
projectRoster(list, joinedAt);
|
|
4616
5340
|
const snapshot = store.snapshot();
|
|
4617
5341
|
const newcomer = snapshot.participants.some(
|
|
4618
5342
|
(participant) => !participant.isLocal && !before.has(participant.identity)
|
|
@@ -4624,6 +5348,9 @@ function createRoomEngine(options) {
|
|
|
4624
5348
|
);
|
|
4625
5349
|
}
|
|
4626
5350
|
},
|
|
5351
|
+
participantLeft(who, reason) {
|
|
5352
|
+
departures.set(who, reason);
|
|
5353
|
+
},
|
|
4627
5354
|
roomMetadata(metadata) {
|
|
4628
5355
|
let spotlight = null;
|
|
4629
5356
|
if (metadata.length > 0) {
|
|
@@ -4650,12 +5377,8 @@ function createRoomEngine(options) {
|
|
|
4650
5377
|
store.setQuality(asParticipantIdentity(who), value);
|
|
4651
5378
|
},
|
|
4652
5379
|
connectionState(state) {
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
store.dispatch({ type: "connected" });
|
|
4656
|
-
} else if (state === "reconnecting") {
|
|
4657
|
-
store.patch({ connection: "reconnecting" });
|
|
4658
|
-
}
|
|
5380
|
+
supervisor.driverState(state);
|
|
5381
|
+
if (state === "connected" && store.snapshot().phase === "joining") store.dispatch({ type: "connected" });
|
|
4659
5382
|
},
|
|
4660
5383
|
transcription(event) {
|
|
4661
5384
|
const snapshot = store.snapshot();
|
|
@@ -4788,6 +5511,7 @@ function createRoomEngine(options) {
|
|
|
4788
5511
|
};
|
|
4789
5512
|
async function resolveExitAndApply(hint) {
|
|
4790
5513
|
const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
|
|
5514
|
+
lastRequest = requestAsNow();
|
|
4791
5515
|
await dropRoom();
|
|
4792
5516
|
let server;
|
|
4793
5517
|
if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
|
|
@@ -4801,7 +5525,7 @@ function createRoomEngine(options) {
|
|
|
4801
5525
|
}
|
|
4802
5526
|
if (disposed) return;
|
|
4803
5527
|
const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
|
|
4804
|
-
store.patch({ connection: resolved.connection });
|
|
5528
|
+
store.patch({ connection: server === null ? supervisor.exitConnection() : resolved.connection });
|
|
4805
5529
|
if (resolved.to === "waiting:knocking") {
|
|
4806
5530
|
store.patch({
|
|
4807
5531
|
waiting: {
|
|
@@ -4833,6 +5557,8 @@ function createRoomEngine(options) {
|
|
|
4833
5557
|
...request.observe === true ? { observe: true } : {}
|
|
4834
5558
|
});
|
|
4835
5559
|
current = token;
|
|
5560
|
+
lastServerUrl = token.serverUrl;
|
|
5561
|
+
connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
|
|
4836
5562
|
store.patch({
|
|
4837
5563
|
localIdentity: token.identity,
|
|
4838
5564
|
breakoutRoomId: token.breakoutRoomId ?? null,
|
|
@@ -4872,6 +5598,7 @@ function createRoomEngine(options) {
|
|
|
4872
5598
|
store.dispatch({ type: "connected" });
|
|
4873
5599
|
store.setLocalTrack("microphone", IDLE_TRACK);
|
|
4874
5600
|
store.setLocalTrack("camera", IDLE_TRACK);
|
|
5601
|
+
media.start();
|
|
4875
5602
|
const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
|
|
4876
5603
|
store.patch({ effects });
|
|
4877
5604
|
if (!effects.noiseSuppression || effects.background !== "none") {
|
|
@@ -4896,6 +5623,7 @@ function createRoomEngine(options) {
|
|
|
4896
5623
|
if (request.microphone !== false) {
|
|
4897
5624
|
await enableTrack(created, "microphone", true);
|
|
4898
5625
|
}
|
|
5626
|
+
media.onConnected();
|
|
4899
5627
|
}
|
|
4900
5628
|
async function enableTrack(target, kind, enabled) {
|
|
4901
5629
|
try {
|
|
@@ -4905,10 +5633,10 @@ function createRoomEngine(options) {
|
|
|
4905
5633
|
store.setLocalTrack(kind, state);
|
|
4906
5634
|
return state;
|
|
4907
5635
|
} catch (error) {
|
|
4908
|
-
const
|
|
4909
|
-
const state = { enabled: false, unavailable:
|
|
5636
|
+
const refused = await media.unavailable(error, kind, `meet.${kind}`);
|
|
5637
|
+
const state = { enabled: false, unavailable: refused };
|
|
4910
5638
|
store.setLocalTrack(kind, state);
|
|
4911
|
-
report("warn",
|
|
5639
|
+
report("warn", refused.message, refused.remedy);
|
|
4912
5640
|
return state;
|
|
4913
5641
|
}
|
|
4914
5642
|
}
|
|
@@ -4937,7 +5665,59 @@ function createRoomEngine(options) {
|
|
|
4937
5665
|
throw normalized;
|
|
4938
5666
|
}
|
|
4939
5667
|
}
|
|
4940
|
-
|
|
5668
|
+
function projectRoster(list, joinedAt) {
|
|
5669
|
+
const at = now();
|
|
5670
|
+
const previous = new Map(liveRoster.map((p) => [p.identity, p]));
|
|
5671
|
+
const present = new Set(list.map((p) => p.identity));
|
|
5672
|
+
const graceMs = connectionPolicy.reconnectGraceSeconds * 1e3;
|
|
5673
|
+
for (const [who, reason] of departures) {
|
|
5674
|
+
const last = previous.get(who);
|
|
5675
|
+
if (last !== void 0 && !last.isLocal && !present.has(who) && graceMs > 0 && isUnannouncedDeparture(reason)) {
|
|
5676
|
+
ghosts.set(who, { participant: last, until: at + graceMs });
|
|
5677
|
+
}
|
|
5678
|
+
}
|
|
5679
|
+
departures.clear();
|
|
5680
|
+
liveRoster = list;
|
|
5681
|
+
const merged = mergeGhosts(list, ghosts, at);
|
|
5682
|
+
ghosts = merged.kept;
|
|
5683
|
+
store.setParticipants(
|
|
5684
|
+
merged.roster.map((entry) => {
|
|
5685
|
+
const projected = projectParticipant(entry, joinedAt);
|
|
5686
|
+
return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
|
|
5687
|
+
})
|
|
5688
|
+
);
|
|
5689
|
+
scheduleGhostExpiry();
|
|
5690
|
+
}
|
|
5691
|
+
function scheduleGhostExpiry() {
|
|
5692
|
+
if (ghostTimer !== null) clearTimeout(ghostTimer);
|
|
5693
|
+
ghostTimer = null;
|
|
5694
|
+
if (ghosts.size === 0 || disposed) return;
|
|
5695
|
+
const next = Math.min(...[...ghosts.values()].map((g) => g.until));
|
|
5696
|
+
ghostTimer = setTimeout(() => {
|
|
5697
|
+
ghostTimer = null;
|
|
5698
|
+
if (disposed || driver === null) return;
|
|
5699
|
+
projectRoster(liveRoster, new Date(now()).toISOString());
|
|
5700
|
+
}, Math.max(0, next - now()) + 50);
|
|
5701
|
+
}
|
|
5702
|
+
function clearGhosts() {
|
|
5703
|
+
ghosts = /* @__PURE__ */ new Map();
|
|
5704
|
+
departures.clear();
|
|
5705
|
+
liveRoster = [];
|
|
5706
|
+
if (ghostTimer !== null) clearTimeout(ghostTimer);
|
|
5707
|
+
ghostTimer = null;
|
|
5708
|
+
}
|
|
5709
|
+
const supervisor = createConnectionSupervisor({
|
|
5710
|
+
store,
|
|
5711
|
+
network: options.connection?.network ?? createWebNetworkPort(),
|
|
5712
|
+
probe: () => options.connection?.probe?.(current?.serverUrl ?? lastServerUrl) ?? Promise.resolve(true),
|
|
5713
|
+
policy: () => connectionPolicy,
|
|
5714
|
+
autoRejoin: () => engine.autoRejoin(),
|
|
5715
|
+
currentRequest: requestAsNow,
|
|
5716
|
+
resume: options.connection?.resume,
|
|
5717
|
+
now,
|
|
5718
|
+
report: (level, message) => report(level, message)
|
|
5719
|
+
});
|
|
5720
|
+
const engine = {
|
|
4941
5721
|
join(request) {
|
|
4942
5722
|
if (joining !== null) return joining;
|
|
4943
5723
|
if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
|
|
@@ -4982,6 +5762,35 @@ function createRoomEngine(options) {
|
|
|
4982
5762
|
});
|
|
4983
5763
|
return joining;
|
|
4984
5764
|
},
|
|
5765
|
+
async autoRejoin() {
|
|
5766
|
+
const request = lastRequest;
|
|
5767
|
+
if (request === null || joining !== null) return joining ?? void 0;
|
|
5768
|
+
if (!store.dispatch({ type: "auto_rejoin" })) return;
|
|
5769
|
+
tokens.invalidate(request.roomName);
|
|
5770
|
+
current = null;
|
|
5771
|
+
joining = connect(request).catch((error) => {
|
|
5772
|
+
throw failJoin(error, "meet.autoRejoin");
|
|
5773
|
+
}).finally(() => {
|
|
5774
|
+
joining = null;
|
|
5775
|
+
});
|
|
5776
|
+
return joining;
|
|
5777
|
+
},
|
|
5778
|
+
resume(roomName) {
|
|
5779
|
+
const marker = supervisor.resumable(roomName);
|
|
5780
|
+
if (marker === null || joining !== null) return false;
|
|
5781
|
+
if (!store.dispatch({ type: "resumed" })) return false;
|
|
5782
|
+
lastRequest = { ...marker.request, roomName };
|
|
5783
|
+
report("info", "meet: this tab was in the call before the page reloaded \u2014 rejoining");
|
|
5784
|
+
tokens.invalidate(roomName);
|
|
5785
|
+
current = null;
|
|
5786
|
+
joining = connect(lastRequest).catch((error) => {
|
|
5787
|
+
throw failJoin(error, "meet.resume");
|
|
5788
|
+
}).finally(() => {
|
|
5789
|
+
joining = null;
|
|
5790
|
+
});
|
|
5791
|
+
joining.catch(() => void 0);
|
|
5792
|
+
return true;
|
|
5793
|
+
},
|
|
4985
5794
|
async moveToBreakout(roomId) {
|
|
4986
5795
|
const request = lastRequest;
|
|
4987
5796
|
if (request === null || driver === null) {
|
|
@@ -5136,6 +5945,10 @@ function createRoomEngine(options) {
|
|
|
5136
5945
|
if (driver === null) return;
|
|
5137
5946
|
await driver.switchDevice(kind, deviceId2);
|
|
5138
5947
|
},
|
|
5948
|
+
enableAudio: () => media.enableAudio(),
|
|
5949
|
+
acceptDeviceOffer: () => media.acceptOffer(),
|
|
5950
|
+
dismissDeviceOffer: () => media.dismissOffer(),
|
|
5951
|
+
dismissDeviceNotice: () => media.dismissNotice(),
|
|
5139
5952
|
pin(who) {
|
|
5140
5953
|
store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
|
|
5141
5954
|
},
|
|
@@ -5149,6 +5962,9 @@ function createRoomEngine(options) {
|
|
|
5149
5962
|
token: () => current,
|
|
5150
5963
|
async dispose() {
|
|
5151
5964
|
disposed = true;
|
|
5965
|
+
supervisor.dispose();
|
|
5966
|
+
media.stop();
|
|
5967
|
+
clearGhosts();
|
|
5152
5968
|
if (driver !== null) {
|
|
5153
5969
|
const target = driver;
|
|
5154
5970
|
driver = null;
|
|
@@ -5157,6 +5973,7 @@ function createRoomEngine(options) {
|
|
|
5157
5973
|
store.reset();
|
|
5158
5974
|
}
|
|
5159
5975
|
};
|
|
5976
|
+
return engine;
|
|
5160
5977
|
}
|
|
5161
5978
|
function canModerate(participant) {
|
|
5162
5979
|
return participant !== void 0 && isHostRole(participant.role);
|
|
@@ -5467,8 +6284,11 @@ function mapConnectionState(state) {
|
|
|
5467
6284
|
case "connecting":
|
|
5468
6285
|
return "connecting";
|
|
5469
6286
|
case "reconnecting":
|
|
5470
|
-
case "signalReconnecting":
|
|
5471
6287
|
return "reconnecting";
|
|
6288
|
+
// Only the signal socket is down (S6b): its own region value, so the supervisor can tell a
|
|
6289
|
+
// signal blip from a full media reconnect.
|
|
6290
|
+
case "signalReconnecting":
|
|
6291
|
+
return "signal_reconnecting";
|
|
5472
6292
|
default:
|
|
5473
6293
|
return "disconnected";
|
|
5474
6294
|
}
|
|
@@ -5549,8 +6369,12 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
5549
6369
|
// R5: large meetings degrade gracefully instead of melting a laptop.
|
|
5550
6370
|
adaptiveStream: true,
|
|
5551
6371
|
dynacast: true,
|
|
6372
|
+
// S6b: OUR retry schedule — jittered from attempt 0 (the SDK's default has no jitter on its
|
|
6373
|
+
// first two attempts), give-up limits in one place (`connection-supervisor.ts`).
|
|
6374
|
+
reconnectPolicy: createReconnectPolicy(),
|
|
5552
6375
|
...options.roomOptions
|
|
5553
6376
|
});
|
|
6377
|
+
const reasonName = (reason) => reason === void 0 || reason === null ? null : module.DisconnectReason?.[reason] ?? String(reason);
|
|
5554
6378
|
const roster = () => {
|
|
5555
6379
|
const local = snapshotParticipant(created.localParticipant, true);
|
|
5556
6380
|
const remotes = [...created.remoteParticipants.values()].map(
|
|
@@ -5568,8 +6392,17 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
5568
6392
|
if (track.kind !== Track.Kind.Audio) return;
|
|
5569
6393
|
for (const element of track.detach()) element.remove();
|
|
5570
6394
|
});
|
|
5571
|
-
created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected,
|
|
6395
|
+
created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
|
|
6396
|
+
handlers.participantLeft?.(participant.identity, reasonName(reason));
|
|
6397
|
+
roster();
|
|
6398
|
+
}).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) => {
|
|
5572
6399
|
handlers.roomMetadata?.(metadata ?? "");
|
|
6400
|
+
}).on(RoomEvent.AudioPlaybackStatusChanged, (canPlay) => {
|
|
6401
|
+
handlers.audioPlayback?.(canPlay);
|
|
6402
|
+
}).on(RoomEvent.MediaDevicesChanged, () => {
|
|
6403
|
+
handlers.devicesChanged?.();
|
|
6404
|
+
}).on(RoomEvent.MediaDevicesError, (error, kind) => {
|
|
6405
|
+
handlers.mediaDeviceError?.(error, kind === "audioinput" || kind === "videoinput" ? kind : null);
|
|
5573
6406
|
}).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
5574
6407
|
handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
|
|
5575
6408
|
}).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
|
|
@@ -5585,8 +6418,7 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
5585
6418
|
}).on(RoomEvent.Disconnected, (reason) => {
|
|
5586
6419
|
audioContainer?.remove();
|
|
5587
6420
|
audioContainer = null;
|
|
5588
|
-
|
|
5589
|
-
handlers.disconnected(named);
|
|
6421
|
+
handlers.disconnected(reasonName(reason));
|
|
5590
6422
|
});
|
|
5591
6423
|
try {
|
|
5592
6424
|
created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
|
|
@@ -5667,6 +6499,7 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
5667
6499
|
}
|
|
5668
6500
|
projectRoster?.();
|
|
5669
6501
|
handlers.roomMetadata?.(target.metadata ?? "");
|
|
6502
|
+
handlers.audioPlayback?.(target.canPlaybackAudio);
|
|
5670
6503
|
},
|
|
5671
6504
|
async disconnect() {
|
|
5672
6505
|
if (room === null) return;
|
|
@@ -5793,6 +6626,11 @@ function createLiveKitDriver(handlers, options = {}) {
|
|
|
5793
6626
|
} catch (error) {
|
|
5794
6627
|
throw normalizeDeviceError(error, "meet.switchDevice");
|
|
5795
6628
|
}
|
|
6629
|
+
},
|
|
6630
|
+
async startAudio() {
|
|
6631
|
+
const target = room;
|
|
6632
|
+
if (target === null) return;
|
|
6633
|
+
await target.startAudio();
|
|
5796
6634
|
}
|
|
5797
6635
|
};
|
|
5798
6636
|
}
|
|
@@ -5976,7 +6814,14 @@ function MeetRuntime(props) {
|
|
|
5976
6814
|
effectsStorage,
|
|
5977
6815
|
resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null,
|
|
5978
6816
|
// §3.4: on every LiveKit disconnect, ask the server what happened to this connection.
|
|
5979
|
-
readExit: (connection) => meetings.readExit(connection)
|
|
6817
|
+
readExit: (connection) => meetings.readExit(connection),
|
|
6818
|
+
// S6b: the connection supervisor's ports — the device's network, whether OUR servers (the
|
|
6819
|
+
// API and the media server) answer, and the per-tab reload-after-offline marker.
|
|
6820
|
+
connection: {
|
|
6821
|
+
network: createWebNetworkPort(),
|
|
6822
|
+
resume: createWebResumeStorage(),
|
|
6823
|
+
probe: (serverUrl) => createReachabilityProbe(() => [baseUrl, ...serverUrl !== null ? [httpUrlOf(serverUrl)] : []])()
|
|
6824
|
+
}
|
|
5980
6825
|
});
|
|
5981
6826
|
const meetings = createMeetingSession({
|
|
5982
6827
|
manager,
|
|
@@ -6193,22 +7038,10 @@ function observedRole(role, standing) {
|
|
|
6193
7038
|
if (role === "host" || role === "cohost") return role;
|
|
6194
7039
|
return isLinkFollower(standing) ? "guest" : "participant";
|
|
6195
7040
|
}
|
|
6196
|
-
function observedDevice(track) {
|
|
7041
|
+
function observedDevice(track, permission = "unknown") {
|
|
6197
7042
|
if (track.enabled) return "on";
|
|
6198
|
-
const
|
|
6199
|
-
|
|
6200
|
-
switch (refused.code) {
|
|
6201
|
-
case "permission-denied":
|
|
6202
|
-
return "unavailable:denied";
|
|
6203
|
-
case "device-not-found":
|
|
6204
|
-
return "unavailable:none";
|
|
6205
|
-
case "device-in-use":
|
|
6206
|
-
return "unavailable:busy";
|
|
6207
|
-
case "insecure-context":
|
|
6208
|
-
return "unavailable:blocked_site";
|
|
6209
|
-
default:
|
|
6210
|
-
return "unavailable:failed";
|
|
6211
|
-
}
|
|
7043
|
+
const reason = deviceUnavailableReason(track, permission);
|
|
7044
|
+
return reason === null ? "muted_by_me" : `unavailable:${reason}`;
|
|
6212
7045
|
}
|
|
6213
7046
|
function endedRecord(snapshot, given) {
|
|
6214
7047
|
const record = snapshot?.meeting ?? given;
|
|
@@ -6241,9 +7074,10 @@ function observe(input) {
|
|
|
6241
7074
|
hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
|
|
6242
7075
|
lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
|
|
6243
7076
|
recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
7077
|
+
// S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
|
|
7078
|
+
audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
|
|
7079
|
+
camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
|
|
7080
|
+
microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
|
|
6247
7081
|
};
|
|
6248
7082
|
}
|
|
6249
7083
|
function staticObservation(phase, reason = null) {
|
|
@@ -6304,14 +7138,14 @@ function participantAttributes(p) {
|
|
|
6304
7138
|
"data-meet-participant-role": observedRole(p.role, p.standing),
|
|
6305
7139
|
"data-meet-participant-self": String(p.isLocal)
|
|
6306
7140
|
};
|
|
6307
|
-
const quality = observedQuality(p.quality);
|
|
7141
|
+
const quality = p.reconnecting === true ? "reconnecting" : observedQuality(p.quality);
|
|
6308
7142
|
if (quality !== null) attrs["data-meet-participant-connection"] = quality;
|
|
6309
7143
|
return attrs;
|
|
6310
7144
|
}
|
|
6311
|
-
function microphoneNoticeCode(track) {
|
|
6312
|
-
const
|
|
6313
|
-
if (
|
|
6314
|
-
return
|
|
7145
|
+
function microphoneNoticeCode(track, permission = "unknown") {
|
|
7146
|
+
const reason = deviceUnavailableReason(track, permission);
|
|
7147
|
+
if (reason === null) return null;
|
|
7148
|
+
return deviceNoticeCode("microphone", reason) === "mic-blocked" ? "mic-blocked" : "device-lost";
|
|
6315
7149
|
}
|
|
6316
7150
|
|
|
6317
7151
|
// src/react/root.tsx
|
|
@@ -7412,6 +8246,26 @@ function useConsentNotice() {
|
|
|
7412
8246
|
if (agent === null) return null;
|
|
7413
8247
|
return `${agent.displayName} is in this meeting: it is transcribing what is said and writing notes everyone here can read.`;
|
|
7414
8248
|
}
|
|
8249
|
+
function useMediaSupervisor() {
|
|
8250
|
+
const host = useMeetHost();
|
|
8251
|
+
const snapshot = useMeetSnapshot();
|
|
8252
|
+
const engine = host?.engine ?? null;
|
|
8253
|
+
return {
|
|
8254
|
+
region: snapshot?.mediaRegion ?? IDLE_MEDIA_REGION,
|
|
8255
|
+
where: useMemo3(() => {
|
|
8256
|
+
const platform = webMediaPlatform();
|
|
8257
|
+
return { browser: platform?.browser ?? "other", os: platform?.os ?? "other" };
|
|
8258
|
+
}, []),
|
|
8259
|
+
enableAudio: useCallback2(async () => engine?.enableAudio(), [engine]),
|
|
8260
|
+
acceptOffer: useCallback2(async () => engine?.acceptDeviceOffer(), [engine]),
|
|
8261
|
+
dismissOffer: useCallback2(() => engine?.dismissDeviceOffer(), [engine]),
|
|
8262
|
+
dismissNotice: useCallback2(() => engine?.dismissDeviceNotice(), [engine]),
|
|
8263
|
+
retry: useCallback2(
|
|
8264
|
+
async (kind) => kind === "microphone" ? engine?.setMicrophoneEnabled(true) : engine?.setCameraEnabled(true),
|
|
8265
|
+
[engine]
|
|
8266
|
+
)
|
|
8267
|
+
};
|
|
8268
|
+
}
|
|
7415
8269
|
function useLocalMedia() {
|
|
7416
8270
|
const snapshot = useMeetSnapshot();
|
|
7417
8271
|
return snapshot?.media ?? IDLE_MEDIA;
|
|
@@ -8523,6 +9377,7 @@ function useMeetCore() {
|
|
|
8523
9377
|
reason: snapshot?.reason ?? null,
|
|
8524
9378
|
phaseNotice: snapshot?.phaseNotice ?? null,
|
|
8525
9379
|
connection: snapshot?.connection ?? "stable",
|
|
9380
|
+
connectionBanner: snapshot?.connectionBanner ?? false,
|
|
8526
9381
|
share: snapshot?.share ?? NO_SHARE,
|
|
8527
9382
|
call: snapshot?.call ?? null,
|
|
8528
9383
|
actions
|
|
@@ -9789,7 +10644,8 @@ function ParticipantTile({
|
|
|
9789
10644
|
participant.displayName,
|
|
9790
10645
|
participant.isLocal ? " (you)" : "",
|
|
9791
10646
|
participant.isAgent ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "AI" }) : null,
|
|
9792
|
-
/* @__PURE__ */ jsx6(RoleBadge, { role: participant.role, standing: participant.standing })
|
|
10647
|
+
/* @__PURE__ */ jsx6(RoleBadge, { role: participant.role, standing: participant.standing }),
|
|
10648
|
+
participant.reconnecting === true ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Reconnecting\u2026" }) : null
|
|
9793
10649
|
] }),
|
|
9794
10650
|
!participant.micEnabled ? /* @__PURE__ */ jsx6(MicOffIcon, { width: 14, height: 14 }) : null,
|
|
9795
10651
|
/* @__PURE__ */ jsx6(QualityBars, { quality: participant.quality })
|
|
@@ -9832,6 +10688,12 @@ function QualityBars({ quality }) {
|
|
|
9832
10688
|
const label = quality === "lost" ? "Connection lost" : quality === "poor" ? "Poor connection" : `Connection ${quality}`;
|
|
9833
10689
|
return /* @__PURE__ */ jsx6("span", { className: `mx-meet__signal mx-meet__signal--${quality}`, title: label, "aria-label": label, role: "img", children: /* @__PURE__ */ jsx6(SignalIcon, { bars, width: 14, height: 14 }) });
|
|
9834
10690
|
}
|
|
10691
|
+
var CONNECTION_BANNER = {
|
|
10692
|
+
signal_reconnecting: "Reconnecting\u2026",
|
|
10693
|
+
reconnecting: "Reconnecting\u2026",
|
|
10694
|
+
offline: "You're offline. Reconnecting when your internet is back\u2026",
|
|
10695
|
+
server_unreachable: "Can't reach the meeting server. Retrying\u2026"
|
|
10696
|
+
};
|
|
9835
10697
|
function Stage({ pageSize }) {
|
|
9836
10698
|
const stage = useStage(pageSize);
|
|
9837
10699
|
const columns = gridColumns(stage.tiles.length);
|
|
@@ -10252,12 +11114,83 @@ function Toast({ text: text3, failed, onDone }) {
|
|
|
10252
11114
|
/* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: onDone, children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
|
|
10253
11115
|
] });
|
|
10254
11116
|
}
|
|
11117
|
+
function MediaNotices() {
|
|
11118
|
+
const media = useLocalMedia();
|
|
11119
|
+
const { region, where, enableAudio, acceptOffer, dismissOffer, dismissNotice, retry } = useMediaSupervisor();
|
|
11120
|
+
const [fixing, setFixing] = useState4(null);
|
|
11121
|
+
const chips = ["microphone", "camera"].flatMap((kind) => {
|
|
11122
|
+
const reason = deviceUnavailableReason(media[kind], region.permission[kind]);
|
|
11123
|
+
return reason === null ? [] : [{ kind, reason }];
|
|
11124
|
+
});
|
|
11125
|
+
const fixReason = fixing === null ? null : chips.find((chip) => chip.kind === fixing)?.reason ?? null;
|
|
11126
|
+
const notice = region.notice;
|
|
11127
|
+
const offer = region.offer;
|
|
11128
|
+
const name = (kind) => kind === "microphone" ? "Microphone" : "Camera";
|
|
11129
|
+
if (chips.length === 0 && region.audioOutput !== "blocked" && notice === null && offer === null) return null;
|
|
11130
|
+
return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-notices", children: [
|
|
11131
|
+
region.audioOutput === "blocked" ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-pill", role: "alert", "data-meet-notice": "audio-blocked", children: [
|
|
11132
|
+
/* @__PURE__ */ jsx6("span", { children: "Sound is paused in this tab" }),
|
|
11133
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void enableAudio(), children: "Enable sound" })
|
|
11134
|
+
] }) : null,
|
|
11135
|
+
chips.map(({ kind, reason }) => /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-pill", role: "status", "data-meet-notice": deviceNoticeCode(kind, reason), children: [
|
|
11136
|
+
/* @__PURE__ */ jsx6("span", { children: deviceChipLabel(kind, reason) }),
|
|
11137
|
+
/* @__PURE__ */ jsx6(
|
|
11138
|
+
"button",
|
|
11139
|
+
{
|
|
11140
|
+
type: "button",
|
|
11141
|
+
className: "mx-meet__primary mx-meet__primary--small",
|
|
11142
|
+
"aria-label": kind === "microphone" ? "Fix microphone" : "Fix camera",
|
|
11143
|
+
"aria-expanded": fixing === kind,
|
|
11144
|
+
onClick: () => setFixing(fixing === kind ? null : kind),
|
|
11145
|
+
children: "Fix"
|
|
11146
|
+
}
|
|
11147
|
+
)
|
|
11148
|
+
] }, kind)),
|
|
11149
|
+
fixing !== null && fixReason !== null ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__popover mx-meet__fix-sheet", role: "dialog", "aria-label": `Fix ${name(fixing).toLowerCase()}`, children: [
|
|
11150
|
+
/* @__PURE__ */ jsx6("ol", { className: "mx-meet__fix-steps", children: deviceFixSteps(fixing, fixReason, where).map((step) => /* @__PURE__ */ jsx6("li", { children: step }, step)) }),
|
|
11151
|
+
/* @__PURE__ */ jsxs3("div", { className: "mx-meet__fix-actions", children: [
|
|
11152
|
+
/* @__PURE__ */ jsx6(
|
|
11153
|
+
"button",
|
|
11154
|
+
{
|
|
11155
|
+
type: "button",
|
|
11156
|
+
className: "mx-meet__primary mx-meet__primary--small",
|
|
11157
|
+
onClick: () => {
|
|
11158
|
+
const kind = fixing;
|
|
11159
|
+
setFixing(null);
|
|
11160
|
+
void retry(kind);
|
|
11161
|
+
},
|
|
11162
|
+
children: "Try again"
|
|
11163
|
+
}
|
|
11164
|
+
),
|
|
11165
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Close", onClick: () => setFixing(null), children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
|
|
11166
|
+
] })
|
|
11167
|
+
] }) : null,
|
|
11168
|
+
notice !== null ? /* @__PURE__ */ jsxs3(
|
|
11169
|
+
"div",
|
|
11170
|
+
{
|
|
11171
|
+
className: "mx-meet__media-pill",
|
|
11172
|
+
role: "status",
|
|
11173
|
+
...notice.outcome === "lost" ? { "data-meet-notice": "device-lost" } : {},
|
|
11174
|
+
children: [
|
|
11175
|
+
/* @__PURE__ */ jsx6("span", { children: notice.outcome === "switched" ? `${notice.from || name(notice.kind)} disconnected \xB7 now using ${notice.to || "the default"}` : `${notice.from || name(notice.kind)} disconnected` }),
|
|
11176
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismissNotice, children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
|
|
11177
|
+
]
|
|
11178
|
+
}
|
|
11179
|
+
) : null,
|
|
11180
|
+
offer !== null ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-pill", role: "status", children: [
|
|
11181
|
+
/* @__PURE__ */ jsx6("span", { children: `New ${offer.kind}: ${offer.label || name(offer.kind)}` }),
|
|
11182
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void acceptOffer(), children: "Use it" }),
|
|
11183
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismissOffer, children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
|
|
11184
|
+
] }) : null
|
|
11185
|
+
] });
|
|
11186
|
+
}
|
|
10255
11187
|
function MeetingOverlays() {
|
|
10256
11188
|
const reactions = useReactions();
|
|
10257
11189
|
const { notice, dismiss } = useModerationNotice();
|
|
10258
11190
|
const room = useRoom();
|
|
10259
11191
|
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
10260
11192
|
/* @__PURE__ */ jsx6(ReactionOverlay, { reactions }),
|
|
11193
|
+
/* @__PURE__ */ jsx6(MediaNotices, {}),
|
|
10261
11194
|
notice !== null && notice.kind !== "removed" ? (
|
|
10262
11195
|
// A host's request ("the host is asking you to unmute"), not an error:
|
|
10263
11196
|
// an assertive status, so it is announced at once without being filed
|
|
@@ -11539,7 +12472,7 @@ function MeetSkinRoom({
|
|
|
11539
12472
|
headerControls !== void 0 && headerControls !== null ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__header-controls", "data-meet-header-controls": "", children: headerControls }) : null,
|
|
11540
12473
|
isHost ? /* @__PURE__ */ jsx6(HostMenu, {}) : null
|
|
11541
12474
|
] }),
|
|
11542
|
-
core.connection !== "stable" ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__banner", role: "status", children:
|
|
12475
|
+
core.connectionBanner && core.connection !== "stable" ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": `connection-${core.connection}`, children: CONNECTION_BANNER[core.connection] }) : null,
|
|
11543
12476
|
/* @__PURE__ */ jsx6(ShareNotice, {}),
|
|
11544
12477
|
/* @__PURE__ */ jsx6(Stage, {}),
|
|
11545
12478
|
/* @__PURE__ */ jsx6(Captions, {}),
|
|
@@ -11573,7 +12506,7 @@ var REFUSED_TITLES = {
|
|
|
11573
12506
|
registration_required: "Register to join this meeting"
|
|
11574
12507
|
};
|
|
11575
12508
|
function ExitScreen({ onLeave }) {
|
|
11576
|
-
const { phase, reason, host, snapshot, actions, call } = useMeetCore();
|
|
12509
|
+
const { phase, reason, host, snapshot, actions, call, connection } = useMeetCore();
|
|
11577
12510
|
const [busy, setBusy] = useState4(false);
|
|
11578
12511
|
const failure = snapshot?.failure ?? null;
|
|
11579
12512
|
const rejoin = /* @__PURE__ */ jsx6(
|
|
@@ -11629,7 +12562,7 @@ function ExitScreen({ onLeave }) {
|
|
|
11629
12562
|
break;
|
|
11630
12563
|
case "disconnected":
|
|
11631
12564
|
title = "You lost your connection to the meeting";
|
|
11632
|
-
line = failure?.remedy ?? "Check your internet connection, then rejoin.";
|
|
12565
|
+
line = connection === "offline" ? "You're offline. You'll rejoin automatically when your internet is back." : connection === "server_unreachable" ? "Can't reach the meeting server. You'll rejoin automatically when it answers." : failure?.remedy ?? "Check your internet connection, then rejoin.";
|
|
11633
12566
|
notice = "disconnected";
|
|
11634
12567
|
actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
11635
12568
|
rejoin,
|
|
@@ -12238,6 +13171,7 @@ function MeetingSkinRootBody({
|
|
|
12238
13171
|
host.store.dispatch({ type: "reset" });
|
|
12239
13172
|
}
|
|
12240
13173
|
if (host.store.snapshot().phase !== "resolving") return;
|
|
13174
|
+
if (given?.linkRun?.reason == null && host.engine.resume(roomName)) return;
|
|
12241
13175
|
const reason = given?.linkRun?.reason ?? null;
|
|
12242
13176
|
if (reason !== null && given?.linkRun !== void 0) {
|
|
12243
13177
|
const words = linkRunWords(given.linkRun);
|
|
@@ -12523,6 +13457,7 @@ export {
|
|
|
12523
13457
|
CAPTION_LANGUAGES,
|
|
12524
13458
|
CONSENT_NOTICE_ATTRIBUTE,
|
|
12525
13459
|
DATA_TOPICS,
|
|
13460
|
+
DEFAULT_CONNECTION_POLICY,
|
|
12526
13461
|
DEFAULT_EFFECTS,
|
|
12527
13462
|
DEFAULT_PAGE_SIZE,
|
|
12528
13463
|
EMPTY_COLLAB,
|
|
@@ -12534,12 +13469,14 @@ export {
|
|
|
12534
13469
|
GUEST_RECORD_NOT_SHARED,
|
|
12535
13470
|
GuestRecordAccessControl,
|
|
12536
13471
|
IDLE_MEDIA,
|
|
13472
|
+
IDLE_MEDIA_REGION,
|
|
12537
13473
|
IDLE_PHASES,
|
|
12538
13474
|
IDLE_TRACK,
|
|
12539
13475
|
IncomingCallCard,
|
|
12540
13476
|
IncomingCallHost,
|
|
12541
13477
|
JOIN_REFUSAL_REASONS,
|
|
12542
13478
|
LISTEN_ONLY_NOTICE,
|
|
13479
|
+
LIVEKIT_BASE_DELAYS_MS,
|
|
12543
13480
|
LIVE_PHASES,
|
|
12544
13481
|
MEETING_INVITE_KIND,
|
|
12545
13482
|
MEETING_INVITE_VERSION,
|
|
@@ -12569,6 +13506,7 @@ export {
|
|
|
12569
13506
|
PARTICIPANT_STANDING_ATTRIBUTE,
|
|
12570
13507
|
REACTION_TTL_MS,
|
|
12571
13508
|
REFUSED_ABSENCE,
|
|
13509
|
+
RESUME_WINDOW_MS,
|
|
12572
13510
|
ROLE_RANK,
|
|
12573
13511
|
ROOM_EVENTS,
|
|
12574
13512
|
ROOM_EVENT_PREFIX,
|
|
@@ -12599,6 +13537,7 @@ export {
|
|
|
12599
13537
|
attendanceTotals,
|
|
12600
13538
|
attendeeKey,
|
|
12601
13539
|
autoAssign,
|
|
13540
|
+
autoRejoinDelayMs,
|
|
12602
13541
|
availableActivities,
|
|
12603
13542
|
breakoutRemainingMs,
|
|
12604
13543
|
callInviteAction,
|
|
@@ -12610,15 +13549,19 @@ export {
|
|
|
12610
13549
|
captionSegment,
|
|
12611
13550
|
captureHeldOf,
|
|
12612
13551
|
changesPatch,
|
|
13552
|
+
classifyDeviceFailure,
|
|
12613
13553
|
clockPosition,
|
|
12614
13554
|
collabFromState,
|
|
12615
13555
|
computeStage,
|
|
13556
|
+
connectionPolicyFrom,
|
|
12616
13557
|
countLabel,
|
|
12617
13558
|
createCallCenter,
|
|
12618
13559
|
createCallInviteHandler,
|
|
13560
|
+
createConnectionSupervisor,
|
|
12619
13561
|
createDeviceManager,
|
|
12620
13562
|
createInRoomClient,
|
|
12621
13563
|
createLiveKitDriver,
|
|
13564
|
+
createMediaSupervisor,
|
|
12622
13565
|
createMeetAi,
|
|
12623
13566
|
createMeetApi,
|
|
12624
13567
|
createMeetRepository,
|
|
@@ -12627,13 +13570,23 @@ export {
|
|
|
12627
13570
|
createMeetingRecordCache,
|
|
12628
13571
|
createMeetingSession,
|
|
12629
13572
|
createMemoryDeviceStorage,
|
|
13573
|
+
createReachabilityProbe,
|
|
13574
|
+
createReconnectPolicy,
|
|
12630
13575
|
createRecordingController,
|
|
12631
13576
|
createRoomEngine,
|
|
12632
13577
|
createTokenClient,
|
|
12633
13578
|
createWebDeviceStorage,
|
|
12634
13579
|
createWebEffectsStorage,
|
|
13580
|
+
createWebNetworkPort,
|
|
13581
|
+
createWebResumeStorage,
|
|
12635
13582
|
createWebRoomTokenStorage,
|
|
12636
13583
|
defaultCaptionLanguage,
|
|
13584
|
+
detectBrowser,
|
|
13585
|
+
detectOs,
|
|
13586
|
+
deviceChipLabel,
|
|
13587
|
+
deviceFixSteps,
|
|
13588
|
+
deviceNoticeCode,
|
|
13589
|
+
deviceUnavailableReason,
|
|
12637
13590
|
displayNameFor,
|
|
12638
13591
|
fileDownloadPath,
|
|
12639
13592
|
focusFromRow,
|
|
@@ -12644,6 +13597,7 @@ export {
|
|
|
12644
13597
|
guestRecordAccessSetting,
|
|
12645
13598
|
hasEnded,
|
|
12646
13599
|
hostUserIds,
|
|
13600
|
+
httpUrlOf,
|
|
12647
13601
|
invalidResponse,
|
|
12648
13602
|
isGuest,
|
|
12649
13603
|
isGuestIdentity,
|
|
@@ -12657,6 +13611,7 @@ export {
|
|
|
12657
13611
|
isRealName,
|
|
12658
13612
|
isRefusedPhase,
|
|
12659
13613
|
isRoomEventTopic,
|
|
13614
|
+
isUnannouncedDeparture,
|
|
12660
13615
|
isWaitingPhase,
|
|
12661
13616
|
learnName,
|
|
12662
13617
|
learnNames,
|
|
@@ -12679,6 +13634,7 @@ export {
|
|
|
12679
13634
|
meetingTopic,
|
|
12680
13635
|
meetingWhen,
|
|
12681
13636
|
mergeCaption,
|
|
13637
|
+
mergeGhosts,
|
|
12682
13638
|
mergeRecord,
|
|
12683
13639
|
microphoneNoticeCode,
|
|
12684
13640
|
moveToRoom,
|
|
@@ -12787,6 +13743,7 @@ export {
|
|
|
12787
13743
|
useTrack,
|
|
12788
13744
|
userTopic,
|
|
12789
13745
|
wasRefused,
|
|
13746
|
+
webMediaPlatform,
|
|
12790
13747
|
withItem,
|
|
12791
13748
|
wrapUpStatus
|
|
12792
13749
|
};
|