@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/dist/core.js CHANGED
@@ -204,6 +204,352 @@ function invalidResponse(operation, detail) {
204
204
  );
205
205
  }
206
206
 
207
+ // src/core/connection-supervisor.ts
208
+ var DEFAULT_CONNECTION_POLICY = {
209
+ bannerDelayMs: 2e3,
210
+ autoRejoin: true,
211
+ autoRejoinMaxAttempts: 5,
212
+ reconnectGraceSeconds: 30
213
+ };
214
+ function clampInt(value, min, max, fallback) {
215
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
216
+ return Math.min(max, Math.max(min, Math.round(value)));
217
+ }
218
+ function connectionPolicyFrom(raw) {
219
+ if (typeof raw !== "object" || raw === null) return DEFAULT_CONNECTION_POLICY;
220
+ const row = raw;
221
+ const d = DEFAULT_CONNECTION_POLICY;
222
+ return {
223
+ bannerDelayMs: clampInt(row["reconnect_banner_delay_ms"], 0, 1e4, d.bannerDelayMs),
224
+ autoRejoin: typeof row["auto_rejoin"] === "boolean" ? row["auto_rejoin"] : d.autoRejoin,
225
+ autoRejoinMaxAttempts: clampInt(row["auto_rejoin_max_attempts"], 0, 20, d.autoRejoinMaxAttempts),
226
+ reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds)
227
+ };
228
+ }
229
+ var LIVEKIT_BASE_DELAYS_MS = [0, 300, 1200, 2700, 4800, 7e3];
230
+ function createReconnectPolicy(options = {}) {
231
+ const delays = options.delaysMs !== void 0 && options.delaysMs.length > 0 ? options.delaysMs : LIVEKIT_BASE_DELAYS_MS;
232
+ const giveUpMs = options.giveUpMs ?? 45e3;
233
+ const maxAttempts = options.maxAttempts ?? 12;
234
+ const random = options.random ?? Math.random;
235
+ return {
236
+ nextRetryDelayInMs({ retryCount, elapsedMs }) {
237
+ if (retryCount >= maxAttempts || elapsedMs >= giveUpMs) return null;
238
+ const base = delays[Math.min(retryCount, delays.length - 1)] ?? 0;
239
+ const spread = Math.min(2e3, Math.max(500, base / 2));
240
+ return Math.round(base + random() * spread);
241
+ }
242
+ };
243
+ }
244
+ function createWebNetworkPort() {
245
+ const scope = globalThis;
246
+ return {
247
+ online: () => scope.navigator?.onLine !== false,
248
+ subscribe(listener) {
249
+ if (typeof scope.addEventListener !== "function") return () => void 0;
250
+ const on = () => listener(true);
251
+ const off = () => listener(false);
252
+ scope.addEventListener("online", on);
253
+ scope.addEventListener("offline", off);
254
+ return () => {
255
+ scope.removeEventListener?.("online", on);
256
+ scope.removeEventListener?.("offline", off);
257
+ };
258
+ }
259
+ };
260
+ }
261
+ function createReachabilityProbe(urls, timeoutMs = 4e3) {
262
+ return async () => {
263
+ const targets = urls().filter((url) => url.length > 0);
264
+ if (targets.length === 0 || typeof fetch !== "function") return true;
265
+ const one = async (url) => {
266
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
267
+ const timer = setTimeout(() => controller?.abort(), timeoutMs);
268
+ try {
269
+ await fetch(url, {
270
+ method: "GET",
271
+ mode: "no-cors",
272
+ cache: "no-store",
273
+ credentials: "omit",
274
+ ...controller !== null ? { signal: controller.signal } : {}
275
+ });
276
+ return true;
277
+ } catch {
278
+ return false;
279
+ } finally {
280
+ clearTimeout(timer);
281
+ }
282
+ };
283
+ const answers = await Promise.all(targets.map(one));
284
+ return answers.every(Boolean);
285
+ };
286
+ }
287
+ function httpUrlOf(serverUrl) {
288
+ return serverUrl.replace(/^ws(s?):\/\//i, "http$1://");
289
+ }
290
+ var RESUME_WINDOW_MS = 15 * 6e4;
291
+ var RESUME_KEY = "mx-meet:resume:";
292
+ function createWebResumeStorage() {
293
+ const storage = globalThis.sessionStorage;
294
+ if (storage === void 0) return void 0;
295
+ return {
296
+ load(roomName) {
297
+ try {
298
+ const raw = storage.getItem(RESUME_KEY + roomName);
299
+ if (raw === null) return null;
300
+ const parsed = JSON.parse(raw);
301
+ return typeof parsed?.at === "number" && typeof parsed.request?.roomName === "string" ? parsed : null;
302
+ } catch {
303
+ return null;
304
+ }
305
+ },
306
+ save(roomName, marker) {
307
+ try {
308
+ storage.setItem(RESUME_KEY + roomName, JSON.stringify(marker));
309
+ } catch {
310
+ }
311
+ },
312
+ clear(roomName) {
313
+ try {
314
+ storage.removeItem(RESUME_KEY + roomName);
315
+ } catch {
316
+ }
317
+ }
318
+ };
319
+ }
320
+ var CLEARS_RESUME = (phase) => phase === "left" || phase === "removed" || phase === "superseded" || phase === "inactive" || phase === "ended" || phase === "record" || phase === "prejoin" || phase.startsWith("refused:") || phase.startsWith("waiting:");
321
+ function autoRejoinDelayMs(attempt, random = Math.random) {
322
+ const base = Math.min(15e3, 1e3 * 2 ** Math.max(0, attempt));
323
+ return Math.round(base * (0.5 + random()));
324
+ }
325
+ function createConnectionSupervisor(options) {
326
+ const { store, network } = options;
327
+ const now = options.now ?? (() => Date.now());
328
+ const random = options.random ?? Math.random;
329
+ const probeEveryMs = options.probeEveryMs ?? 3e3;
330
+ let driverRegion = null;
331
+ let probeTimer = null;
332
+ let probing = false;
333
+ let bannerTimer = null;
334
+ let rejoinTimer = null;
335
+ let rejoinAttempts = 0;
336
+ let lastPhase = store.snapshot().phase;
337
+ let lastSavedAt = 0;
338
+ let disposed = false;
339
+ const inCall = () => store.snapshot().phase === "in_call";
340
+ const dropped = () => store.snapshot().phase === "disconnected";
341
+ function setRegion(next) {
342
+ if (store.snapshot().connection !== next) store.patch({ connection: next });
343
+ }
344
+ function regionFor(reachable) {
345
+ if (!network.online()) return "offline";
346
+ if (reachable === false) return "server_unreachable";
347
+ if (dropped()) return reachable === true ? "stable" : store.snapshot().connection;
348
+ return driverRegion ?? "stable";
349
+ }
350
+ function needsWatch() {
351
+ const snap = store.snapshot();
352
+ if (dropped()) return true;
353
+ return inCall() && (driverRegion !== null || snap.connection !== "stable");
354
+ }
355
+ function stopProbe() {
356
+ if (probeTimer !== null) clearTimeout(probeTimer);
357
+ probeTimer = null;
358
+ }
359
+ function scheduleProbe(delayMs) {
360
+ if (disposed || probeTimer !== null) return;
361
+ probeTimer = setTimeout(() => {
362
+ probeTimer = null;
363
+ void runProbe();
364
+ }, delayMs);
365
+ }
366
+ async function runProbe() {
367
+ if (disposed || probing) return;
368
+ if (!needsWatch()) return;
369
+ if (!network.online()) {
370
+ setRegion("offline");
371
+ scheduleProbe(probeEveryMs);
372
+ return;
373
+ }
374
+ probing = true;
375
+ let reachable = false;
376
+ try {
377
+ reachable = await options.probe();
378
+ } catch {
379
+ reachable = false;
380
+ } finally {
381
+ probing = false;
382
+ }
383
+ if (disposed) return;
384
+ if (inCall() || dropped()) setRegion(regionFor(reachable));
385
+ if (dropped() && reachable) maybeRejoin();
386
+ if (needsWatch()) scheduleProbe(Math.round(probeEveryMs * (0.75 + random() * 0.5)));
387
+ }
388
+ function syncBanner() {
389
+ const snap = store.snapshot();
390
+ const unstable = snap.phase === "in_call" && snap.connection !== "stable";
391
+ if (!unstable) {
392
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
393
+ bannerTimer = null;
394
+ if (snap.connectionBanner) store.patch({ connectionBanner: false });
395
+ return;
396
+ }
397
+ if (snap.connectionBanner || bannerTimer !== null) return;
398
+ const delay = options.policy().bannerDelayMs;
399
+ if (delay <= 0) {
400
+ store.patch({ connectionBanner: true });
401
+ return;
402
+ }
403
+ bannerTimer = setTimeout(() => {
404
+ bannerTimer = null;
405
+ const latest = store.snapshot();
406
+ if (latest.phase === "in_call" && latest.connection !== "stable" && !latest.connectionBanner) {
407
+ store.patch({ connectionBanner: true });
408
+ }
409
+ }, delay);
410
+ }
411
+ function stopRejoin() {
412
+ if (rejoinTimer !== null) clearTimeout(rejoinTimer);
413
+ rejoinTimer = null;
414
+ }
415
+ function maybeRejoin() {
416
+ if (disposed || rejoinTimer !== null || !dropped()) return;
417
+ const policy = options.policy();
418
+ if (!policy.autoRejoin) return;
419
+ if (rejoinAttempts >= policy.autoRejoinMaxAttempts) return;
420
+ if (!network.online()) return;
421
+ const attempt = rejoinAttempts;
422
+ rejoinTimer = setTimeout(() => {
423
+ rejoinTimer = null;
424
+ if (disposed || !dropped() || !network.online()) return;
425
+ rejoinAttempts = attempt + 1;
426
+ options.report?.("info", `meet: rejoining by itself (attempt ${rejoinAttempts} of ${policy.autoRejoinMaxAttempts})`);
427
+ void options.autoRejoin().catch(() => void 0);
428
+ }, autoRejoinDelayMs(attempt, random));
429
+ }
430
+ function saveMarker(force) {
431
+ const resume = options.resume;
432
+ const snap = store.snapshot();
433
+ if (resume === void 0 || snap.roomName === null) return;
434
+ const at = now();
435
+ if (!force && at - lastSavedAt < 1e4) return;
436
+ const request = options.currentRequest();
437
+ if (request === null) return;
438
+ lastSavedAt = at;
439
+ resume.save(snap.roomName, { request, at });
440
+ }
441
+ const unsubscribeStore = store.subscribe(() => {
442
+ if (disposed) return;
443
+ const snap = store.snapshot();
444
+ const phase = snap.phase;
445
+ const changed = phase !== lastPhase;
446
+ lastPhase = phase;
447
+ if (changed) {
448
+ if (phase === "in_call") {
449
+ rejoinAttempts = 0;
450
+ stopRejoin();
451
+ saveMarker(true);
452
+ } else if (phase === "disconnected") {
453
+ driverRegion = null;
454
+ saveMarker(true);
455
+ stopProbe();
456
+ scheduleProbe(0);
457
+ } else {
458
+ stopRejoin();
459
+ if (phase !== "joining") driverRegion = null;
460
+ if (CLEARS_RESUME(phase) && snap.roomName !== null) options.resume?.clear(snap.roomName);
461
+ }
462
+ } else if (phase === "in_call" || phase === "disconnected") {
463
+ saveMarker(false);
464
+ }
465
+ syncBanner();
466
+ });
467
+ const unsubscribeNetwork = network.subscribe((online) => {
468
+ if (disposed) return;
469
+ if (!online) {
470
+ if (inCall() || dropped()) setRegion("offline");
471
+ stopRejoin();
472
+ stopProbe();
473
+ if (needsWatch()) scheduleProbe(probeEveryMs);
474
+ return;
475
+ }
476
+ stopProbe();
477
+ if (needsWatch()) scheduleProbe(0);
478
+ });
479
+ return {
480
+ driverState(state) {
481
+ if (disposed) return;
482
+ if (state === "connected") {
483
+ driverRegion = null;
484
+ if (inCall() || store.snapshot().phase === "joining") setRegion(network.online() ? "stable" : "offline");
485
+ if (!needsWatch()) stopProbe();
486
+ return;
487
+ }
488
+ if (state === "reconnecting" || state === "signal_reconnecting") {
489
+ driverRegion = state;
490
+ if (inCall()) {
491
+ setRegion(network.online() ? state : "offline");
492
+ stopProbe();
493
+ scheduleProbe(0);
494
+ }
495
+ }
496
+ },
497
+ exitConnection() {
498
+ return network.online() ? "server_unreachable" : "offline";
499
+ },
500
+ resumable(roomName) {
501
+ const resume = options.resume;
502
+ if (resume === void 0) return null;
503
+ const marker = resume.load(roomName);
504
+ if (marker === null) return null;
505
+ if (now() - marker.at > RESUME_WINDOW_MS) {
506
+ resume.clear(roomName);
507
+ return null;
508
+ }
509
+ return marker;
510
+ },
511
+ dispose() {
512
+ disposed = true;
513
+ stopProbe();
514
+ stopRejoin();
515
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
516
+ bannerTimer = null;
517
+ unsubscribeStore();
518
+ unsubscribeNetwork();
519
+ }
520
+ };
521
+ }
522
+ var DELIBERATE_DEPARTURES = /* @__PURE__ */ new Set([
523
+ "CLIENT_INITIATED",
524
+ "PARTICIPANT_REMOVED",
525
+ "ROOM_DELETED",
526
+ "ROOM_CLOSED",
527
+ "DUPLICATE_IDENTITY",
528
+ "USER_REJECTED",
529
+ "USER_UNAVAILABLE",
530
+ "SIP_TRUNK_FAILURE"
531
+ ]);
532
+ function isUnannouncedDeparture(reason) {
533
+ return reason === null || !DELIBERATE_DEPARTURES.has(reason);
534
+ }
535
+ function mergeGhosts(live, ghosts, at) {
536
+ const present = new Set(live.map((p) => p.identity));
537
+ const kept = /* @__PURE__ */ new Map();
538
+ const extra = [];
539
+ for (const [identity, ghost] of ghosts) {
540
+ if (present.has(identity) || ghost.until <= at) continue;
541
+ kept.set(identity, ghost);
542
+ extra.push({
543
+ ...ghost.participant,
544
+ micEnabled: false,
545
+ cameraEnabled: false,
546
+ screenSharing: false,
547
+ reconnecting: true
548
+ });
549
+ }
550
+ return { roster: [...live, ...extra], kept };
551
+ }
552
+
207
553
  // src/core/tokens.ts
208
554
  function createWebRoomTokenStorage(onDiagnostic) {
209
555
  const store = globalThis.localStorage;
@@ -389,7 +735,8 @@ function parseRoomToken(body, roomName) {
389
735
  admissionState: readString(payload, "admission_state") ?? void 0,
390
736
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
391
737
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
392
- observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone"
738
+ observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
739
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
393
740
  };
394
741
  }
395
742
  function createTokenClient(options) {
@@ -1119,6 +1466,10 @@ function nextPhase(from, event) {
1119
1466
  return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
1120
1467
  case "join":
1121
1468
  return JOINABLE_FROM.has(from) ? "joining" : null;
1469
+ case "auto_rejoin":
1470
+ return from === "disconnected" ? "joining" : null;
1471
+ case "resumed":
1472
+ return from === "resolving" ? "joining" : null;
1122
1473
  case "back_to_prejoin":
1123
1474
  return from === "left" || from === "disconnected" ? "prejoin" : null;
1124
1475
  case "retry":
@@ -2996,13 +3347,19 @@ function recordingLabel(state) {
2996
3347
  }
2997
3348
 
2998
3349
  // src/core/attendance.ts
3350
+ var IDLE_MEDIA_REGION = {
3351
+ permission: { microphone: "unknown", camera: "unknown" },
3352
+ audioOutput: "ok",
3353
+ notice: null,
3354
+ offer: null
3355
+ };
2999
3356
  var IDLE_TRACK = { enabled: false, unavailable: null };
3000
3357
  var IDLE_MEDIA = {
3001
3358
  microphone: IDLE_TRACK,
3002
3359
  camera: IDLE_TRACK
3003
3360
  };
3004
- function unavailableFrom(error) {
3005
- return { code: error.code, message: error.message, remedy: error.remedy };
3361
+ function unavailableFrom(error, reason) {
3362
+ return reason === void 0 ? { code: error.code, message: error.message, remedy: error.remedy } : { code: error.code, message: error.message, remedy: error.remedy, reason };
3006
3363
  }
3007
3364
  var LISTEN_ONLY_NOTICE = "You're attending without a microphone \u2014 others can't hear you";
3008
3365
  function isListenOnly(media) {
@@ -3032,6 +3389,7 @@ var EMPTY = {
3032
3389
  phaseNotice: null,
3033
3390
  roomName: null,
3034
3391
  connection: "stable",
3392
+ connectionBanner: false,
3035
3393
  share: NO_SHARE,
3036
3394
  call: null,
3037
3395
  waiting: null,
@@ -3063,6 +3421,7 @@ var EMPTY = {
3063
3421
  wrapUp: null,
3064
3422
  noteTakerRefusal: null,
3065
3423
  media: IDLE_MEDIA,
3424
+ mediaRegion: IDLE_MEDIA_REGION,
3066
3425
  failure: null,
3067
3426
  collab: EMPTY_COLLAB,
3068
3427
  breakoutRoomId: null
@@ -3280,7 +3639,7 @@ function createMeetStore(initial = {}) {
3280
3639
  },
3281
3640
  setLocalTrack(kind, next) {
3282
3641
  const current = state.media[kind];
3283
- if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.message === next.unavailable?.message) {
3642
+ if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.reason === next.unavailable?.reason && current.unavailable?.message === next.unavailable?.message) {
3284
3643
  return;
3285
3644
  }
3286
3645
  commit({ ...state, media: { ...state.media, [kind]: next } });
@@ -3986,8 +4345,11 @@ function mapConnectionState(state) {
3986
4345
  case "connecting":
3987
4346
  return "connecting";
3988
4347
  case "reconnecting":
3989
- case "signalReconnecting":
3990
4348
  return "reconnecting";
4349
+ // Only the signal socket is down (S6b): its own region value, so the supervisor can tell a
4350
+ // signal blip from a full media reconnect.
4351
+ case "signalReconnecting":
4352
+ return "signal_reconnecting";
3991
4353
  default:
3992
4354
  return "disconnected";
3993
4355
  }
@@ -4068,8 +4430,12 @@ function createLiveKitDriver(handlers, options = {}) {
4068
4430
  // R5: large meetings degrade gracefully instead of melting a laptop.
4069
4431
  adaptiveStream: true,
4070
4432
  dynacast: true,
4433
+ // S6b: OUR retry schedule — jittered from attempt 0 (the SDK's default has no jitter on its
4434
+ // first two attempts), give-up limits in one place (`connection-supervisor.ts`).
4435
+ reconnectPolicy: createReconnectPolicy(),
4071
4436
  ...options.roomOptions
4072
4437
  });
4438
+ const reasonName = (reason) => reason === void 0 || reason === null ? null : module.DisconnectReason?.[reason] ?? String(reason);
4073
4439
  const roster = () => {
4074
4440
  const local = snapshotParticipant(created.localParticipant, true);
4075
4441
  const remotes = [...created.remoteParticipants.values()].map(
@@ -4087,8 +4453,17 @@ function createLiveKitDriver(handlers, options = {}) {
4087
4453
  if (track.kind !== Track.Kind.Audio) return;
4088
4454
  for (const element of track.detach()) element.remove();
4089
4455
  });
4090
- 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) => {
4456
+ created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
4457
+ handlers.participantLeft?.(participant.identity, reasonName(reason));
4458
+ roster();
4459
+ }).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) => {
4091
4460
  handlers.roomMetadata?.(metadata ?? "");
4461
+ }).on(RoomEvent.AudioPlaybackStatusChanged, (canPlay) => {
4462
+ handlers.audioPlayback?.(canPlay);
4463
+ }).on(RoomEvent.MediaDevicesChanged, () => {
4464
+ handlers.devicesChanged?.();
4465
+ }).on(RoomEvent.MediaDevicesError, (error, kind) => {
4466
+ handlers.mediaDeviceError?.(error, kind === "audioinput" || kind === "videoinput" ? kind : null);
4092
4467
  }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
4093
4468
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
4094
4469
  }).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
@@ -4104,8 +4479,7 @@ function createLiveKitDriver(handlers, options = {}) {
4104
4479
  }).on(RoomEvent.Disconnected, (reason) => {
4105
4480
  audioContainer?.remove();
4106
4481
  audioContainer = null;
4107
- const named = reason === void 0 ? null : module?.DisconnectReason?.[reason] ?? String(reason);
4108
- handlers.disconnected(named);
4482
+ handlers.disconnected(reasonName(reason));
4109
4483
  });
4110
4484
  try {
4111
4485
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -4186,6 +4560,7 @@ function createLiveKitDriver(handlers, options = {}) {
4186
4560
  }
4187
4561
  projectRoster?.();
4188
4562
  handlers.roomMetadata?.(target.metadata ?? "");
4563
+ handlers.audioPlayback?.(target.canPlaybackAudio);
4189
4564
  },
4190
4565
  async disconnect() {
4191
4566
  if (room === null) return;
@@ -4312,6 +4687,11 @@ function createLiveKitDriver(handlers, options = {}) {
4312
4687
  } catch (error) {
4313
4688
  throw normalizeDeviceError(error, "meet.switchDevice");
4314
4689
  }
4690
+ },
4691
+ async startAudio() {
4692
+ const target = room;
4693
+ if (target === null) return;
4694
+ await target.startAudio();
4315
4695
  }
4316
4696
  };
4317
4697
  }
@@ -5407,6 +5787,342 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
5407
5787
  return await response.json();
5408
5788
  }
5409
5789
 
5790
+ // src/core/media-supervisor.ts
5791
+ function classifyDeviceFailure(error, permission) {
5792
+ if (permission === "denied") return "denied";
5793
+ switch (error.code) {
5794
+ case "permission-denied":
5795
+ if (/by system/i.test(error.message) || permission === "granted") return "blocked_os";
5796
+ return "denied";
5797
+ case "device-not-found":
5798
+ return "none";
5799
+ case "device-in-use":
5800
+ return "busy";
5801
+ case "insecure-context":
5802
+ return "blocked_site";
5803
+ default:
5804
+ return "failed";
5805
+ }
5806
+ }
5807
+ function deviceNoticeCode(kind, reason) {
5808
+ if (reason === "denied" || reason === "blocked_site" || reason === "blocked_os") {
5809
+ return kind === "microphone" ? "mic-blocked" : "camera-blocked";
5810
+ }
5811
+ return "device-lost";
5812
+ }
5813
+ function deviceUnavailableReason(track, permission) {
5814
+ if (track.enabled) return null;
5815
+ if (permission === "denied") return "denied";
5816
+ const refused = track.unavailable;
5817
+ if (refused === null) return null;
5818
+ return refused.reason ?? classifyDeviceFailure({ code: refused.code, message: refused.message }, permission);
5819
+ }
5820
+ function deviceChipLabel(kind, reason) {
5821
+ const what = kind === "microphone" ? "Mic" : "Camera";
5822
+ switch (reason) {
5823
+ case "denied":
5824
+ case "blocked_site":
5825
+ case "blocked_os":
5826
+ return `${what} blocked`;
5827
+ case "none":
5828
+ return kind === "microphone" ? "No microphone found" : "No camera found";
5829
+ case "busy":
5830
+ return `${what} in use by another app`;
5831
+ case "revoked":
5832
+ return `${what} access was turned off`;
5833
+ default:
5834
+ return `${what} didn't start`;
5835
+ }
5836
+ }
5837
+ function deviceFixSteps(kind, reason, where) {
5838
+ const device = kind === "microphone" ? "Microphone" : "Camera";
5839
+ const lower = device.toLowerCase();
5840
+ switch (reason) {
5841
+ case "denied":
5842
+ case "revoked":
5843
+ case "blocked_site":
5844
+ switch (where.browser) {
5845
+ case "firefox":
5846
+ return [`Click the crossed-out ${lower} icon in the address bar`, "Clear the Blocked setting", "Press Try again and choose Allow"];
5847
+ case "safari":
5848
+ 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"];
5849
+ default:
5850
+ return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
5851
+ }
5852
+ case "blocked_os": {
5853
+ const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
5854
+ if (where.os === "mac") return [`System Settings \u203A Privacy & Security \u203A ${device}`, `Turn on ${app}`, "Reopen the browser if asked, then Try again"];
5855
+ if (where.os === "windows") return [`Settings \u203A Privacy & security \u203A ${device}`, `Turn on ${device} access for apps`, "Press Try again"];
5856
+ return [`Allow ${lower} access for ${app} in your system settings`, "Press Try again"];
5857
+ }
5858
+ case "busy":
5859
+ return [`Close other apps using the ${lower} (Zoom, Teams, another tab)`, "Press Try again"];
5860
+ case "none":
5861
+ return [`Connect a ${lower}`, "Press Try again"];
5862
+ default:
5863
+ return [`Pick another ${lower} in Settings`, "Press Try again"];
5864
+ }
5865
+ }
5866
+ function detectBrowser(userAgent) {
5867
+ if (/Edg\//.test(userAgent)) return "edge";
5868
+ if (/Firefox\//.test(userAgent)) return "firefox";
5869
+ if (/Chrome\/|Chromium\/|CriOS\//.test(userAgent)) return "chrome";
5870
+ if (/Safari\//.test(userAgent)) return "safari";
5871
+ return "other";
5872
+ }
5873
+ function detectOs(userAgent, maxTouchPoints = 0) {
5874
+ if (/iPhone|iPad|iPod/.test(userAgent)) return "ios";
5875
+ if (/Macintosh/.test(userAgent)) return maxTouchPoints > 1 ? "ios" : "mac";
5876
+ if (/Android/.test(userAgent)) return "android";
5877
+ if (/Windows/.test(userAgent)) return "windows";
5878
+ if (/Linux|CrOS/.test(userAgent)) return "linux";
5879
+ return "other";
5880
+ }
5881
+ function webMediaPlatform() {
5882
+ const scope = globalThis;
5883
+ const nav = scope.navigator;
5884
+ if (nav === void 0) return null;
5885
+ const ua = nav.userAgent ?? "";
5886
+ const reading = (state) => state === "granted" || state === "denied" || state === "prompt" ? state : "unknown";
5887
+ const query = async (kind) => {
5888
+ try {
5889
+ return await nav.permissions?.query({ name: kind }) ?? null;
5890
+ } catch {
5891
+ return null;
5892
+ }
5893
+ };
5894
+ return {
5895
+ browser: detectBrowser(ua),
5896
+ os: detectOs(ua, nav.maxTouchPoints ?? 0),
5897
+ async queryPermission(kind) {
5898
+ return reading((await query(kind))?.state);
5899
+ },
5900
+ watchPermission(kind, onChange) {
5901
+ let status = null;
5902
+ let stopped = false;
5903
+ void query(kind).then((found) => {
5904
+ if (stopped || found === null) return;
5905
+ status = found;
5906
+ found.onchange = () => onChange(reading(found.state));
5907
+ });
5908
+ return () => {
5909
+ stopped = true;
5910
+ if (status !== null) status.onchange = null;
5911
+ };
5912
+ },
5913
+ async listDevices() {
5914
+ try {
5915
+ return await nav.mediaDevices?.enumerateDevices() ?? [];
5916
+ } catch {
5917
+ return [];
5918
+ }
5919
+ },
5920
+ onVisible(callback) {
5921
+ const doc = scope.document;
5922
+ if (doc === void 0) return () => void 0;
5923
+ const handler = () => {
5924
+ if (doc.visibilityState === "visible") callback();
5925
+ };
5926
+ doc.addEventListener("visibilitychange", handler);
5927
+ return () => doc.removeEventListener("visibilitychange", handler);
5928
+ }
5929
+ };
5930
+ }
5931
+ var INPUT_KIND = { microphone: "audioinput", camera: "videoinput" };
5932
+ function isAlias(deviceId) {
5933
+ return deviceId === "default" || deviceId === "communications" || deviceId === "";
5934
+ }
5935
+ function createMediaSupervisor(options) {
5936
+ const { store, platform } = options;
5937
+ let unsubscribers = [];
5938
+ let started = false;
5939
+ let noticeId = 0;
5940
+ let known = null;
5941
+ let inUse = { audioinput: null, videoinput: null };
5942
+ function region() {
5943
+ return store.snapshot().mediaRegion;
5944
+ }
5945
+ function setRegion(next) {
5946
+ const current = region();
5947
+ const merged = { ...current, ...next };
5948
+ 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) {
5949
+ return;
5950
+ }
5951
+ store.patch({ mediaRegion: merged });
5952
+ }
5953
+ function setPermission(kind, reading) {
5954
+ const current = region().permission;
5955
+ if (current[kind] === reading) return;
5956
+ setRegion({ permission: { ...current, [kind]: reading } });
5957
+ const track = store.snapshot().media[kind];
5958
+ if (reading === "granted" && !track.enabled && track.unavailable?.reason === "denied") {
5959
+ store.setLocalTrack(kind, {
5960
+ enabled: false,
5961
+ unavailable: { ...track.unavailable, reason: "failed", remedy: "Access is allowed now. Press Try again." }
5962
+ });
5963
+ }
5964
+ }
5965
+ async function snapshotDevices() {
5966
+ const list = platform === null ? [] : await platform.listDevices();
5967
+ const out = { audioinput: /* @__PURE__ */ new Map(), videoinput: /* @__PURE__ */ new Map() };
5968
+ for (const device of list) {
5969
+ if (device.kind === "audioinput" || device.kind === "videoinput") out[device.kind].set(device.deviceId, device.label);
5970
+ }
5971
+ return out;
5972
+ }
5973
+ function readInUse() {
5974
+ const driver = options.driver();
5975
+ if (driver === null) return;
5976
+ const active = driver.activeDevices();
5977
+ inUse = { audioinput: active.audioinput, videoinput: active.videoinput };
5978
+ }
5979
+ async function handleDevicesChanged() {
5980
+ if (platform === null) return;
5981
+ const before = known;
5982
+ const now = await snapshotDevices();
5983
+ known = now;
5984
+ const driver = options.driver();
5985
+ if (driver === null || before === null) {
5986
+ readInUse();
5987
+ return;
5988
+ }
5989
+ for (const kind of ["microphone", "camera"]) {
5990
+ const deviceKind = INPUT_KIND[kind];
5991
+ const previous = inUse[deviceKind];
5992
+ const track = store.snapshot().media[kind];
5993
+ const gone = previous !== null && !isAlias(previous) && before[deviceKind].has(previous) && !now[deviceKind].has(previous);
5994
+ if (gone) {
5995
+ const from = before[deviceKind].get(previous) ?? "";
5996
+ const remaining = [...now[deviceKind].entries()].filter(([id]) => !isAlias(id));
5997
+ if (remaining.length === 0) {
5998
+ store.setLocalTrack(kind, {
5999
+ enabled: false,
6000
+ unavailable: {
6001
+ code: "device-not-found",
6002
+ message: `meet.${kind}: the ${kind} was disconnected and no other is connected`,
6003
+ remedy: `Connect a ${kind === "microphone" ? "microphone" : "camera"}, then press Try again.`,
6004
+ reason: "none"
6005
+ }
6006
+ });
6007
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "lost", from, to: null } });
6008
+ continue;
6009
+ }
6010
+ if (kind === "camera") {
6011
+ const [nextId, nextLabel] = remaining[0];
6012
+ try {
6013
+ await driver.switchDevice("videoinput", nextId);
6014
+ if (track.enabled || track.unavailable !== null) {
6015
+ await driver.setCameraEnabled(true);
6016
+ store.setLocalTrack("camera", { enabled: true, unavailable: null });
6017
+ }
6018
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "switched", from, to: nextLabel } });
6019
+ } catch (error) {
6020
+ const refused = await unavailable(error, "camera", "meet.camera(fallback)");
6021
+ store.setLocalTrack("camera", { enabled: false, unavailable: refused });
6022
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "lost", from, to: null } });
6023
+ }
6024
+ } else {
6025
+ const active = driver.activeDevices().audioinput;
6026
+ const to = (active !== null ? now.audioinput.get(active) : void 0) ?? now.audioinput.get("default") ?? remaining[0][1];
6027
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "switched", from, to } });
6028
+ }
6029
+ continue;
6030
+ }
6031
+ const added = [...now[deviceKind].entries()].find(([id]) => !isAlias(id) && !before[deviceKind].has(id));
6032
+ if (added !== void 0 && added[0] !== driver.activeDevices()[deviceKind]) {
6033
+ setRegion({ offer: { kind, deviceId: added[0], label: added[1] } });
6034
+ }
6035
+ }
6036
+ readInUse();
6037
+ }
6038
+ async function unavailable(error, kind, operation) {
6039
+ const normalized = normalizeDeviceError(error, operation);
6040
+ const permission = platform === null ? "unknown" : await platform.queryPermission(kind);
6041
+ if (permission !== "unknown") setPermission(kind, permission);
6042
+ return unavailableFrom(normalized, classifyDeviceFailure(normalized, permission));
6043
+ }
6044
+ return {
6045
+ start() {
6046
+ if (started || platform === null) return;
6047
+ started = true;
6048
+ for (const kind of ["microphone", "camera"]) {
6049
+ void platform.queryPermission(kind).then((reading) => setPermission(kind, reading));
6050
+ unsubscribers.push(platform.watchPermission(kind, (reading) => setPermission(kind, reading)));
6051
+ }
6052
+ unsubscribers.push(
6053
+ platform.onVisible(() => {
6054
+ const driver = options.driver();
6055
+ if (driver?.startAudio === void 0 || region().audioOutput !== "blocked") return;
6056
+ driver.startAudio().catch(() => void 0);
6057
+ })
6058
+ );
6059
+ },
6060
+ stop() {
6061
+ for (const off of unsubscribers) off();
6062
+ unsubscribers = [];
6063
+ started = false;
6064
+ },
6065
+ unavailable,
6066
+ onAudioPlayback(canPlay) {
6067
+ setRegion({ audioOutput: canPlay ? "ok" : "blocked" });
6068
+ },
6069
+ onDevicesChanged() {
6070
+ void handleDevicesChanged().catch((error) => {
6071
+ options.report("warn", `meet: device change could not be handled: ${String(error)}`, "Pick the device in Settings.");
6072
+ });
6073
+ },
6074
+ onDeviceError(error, deviceKind) {
6075
+ if (deviceKind === null) return;
6076
+ const kind = deviceKind === "audioinput" ? "microphone" : "camera";
6077
+ void unavailable(error, kind, `meet.${kind}`).then((refused) => {
6078
+ const track = store.snapshot().media[kind];
6079
+ if (track.enabled) return;
6080
+ store.setLocalTrack(kind, { enabled: false, unavailable: refused });
6081
+ });
6082
+ },
6083
+ onConnected() {
6084
+ void snapshotDevices().then((now) => {
6085
+ known = now;
6086
+ readInUse();
6087
+ });
6088
+ },
6089
+ onDisconnected() {
6090
+ known = null;
6091
+ inUse = { audioinput: null, videoinput: null };
6092
+ setRegion({ audioOutput: "ok", notice: null, offer: null });
6093
+ },
6094
+ async enableAudio() {
6095
+ const driver = options.driver();
6096
+ if (driver?.startAudio === void 0) return;
6097
+ try {
6098
+ await driver.startAudio();
6099
+ setRegion({ audioOutput: "ok" });
6100
+ } catch (error) {
6101
+ options.report("warn", `meet: sound could not be enabled: ${String(error)}`, "Click Enable sound again, or check the tab is not muted.");
6102
+ }
6103
+ },
6104
+ async acceptOffer() {
6105
+ const offer = region().offer;
6106
+ const driver = options.driver();
6107
+ setRegion({ offer: null });
6108
+ if (offer === null || driver === null) return;
6109
+ try {
6110
+ await driver.switchDevice(INPUT_KIND[offer.kind], offer.deviceId);
6111
+ readInUse();
6112
+ } catch (error) {
6113
+ const refused = await unavailable(error, offer.kind, "meet.switchDevice");
6114
+ options.report("warn", refused.message, refused.remedy);
6115
+ }
6116
+ },
6117
+ dismissOffer() {
6118
+ setRegion({ offer: null });
6119
+ },
6120
+ dismissNotice() {
6121
+ setRegion({ notice: null });
6122
+ }
6123
+ };
6124
+ }
6125
+
5410
6126
  // src/core/room.ts
5411
6127
  function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
5412
6128
  return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
@@ -5478,6 +6194,18 @@ function createRoomEngine(options) {
5478
6194
  let disposed = false;
5479
6195
  let moving = false;
5480
6196
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
6197
+ let lastServerUrl = null;
6198
+ let connectionPolicy = DEFAULT_CONNECTION_POLICY;
6199
+ let ghosts = /* @__PURE__ */ new Map();
6200
+ let ghostTimer = null;
6201
+ let liveRoster = [];
6202
+ const departures = /* @__PURE__ */ new Map();
6203
+ function requestAsNow() {
6204
+ if (lastRequest === null) return null;
6205
+ const media2 = store.snapshot().media;
6206
+ if (store.snapshot().phase !== "in_call") return lastRequest;
6207
+ return { ...lastRequest, mode: media2.camera.enabled ? "video" : "audio", microphone: media2.microphone.enabled };
6208
+ }
5481
6209
  async function dropRoom() {
5482
6210
  const target = driver;
5483
6211
  driver = null;
@@ -5487,7 +6215,9 @@ function createRoomEngine(options) {
5487
6215
  }
5488
6216
  store.setLocalTrack("microphone", IDLE_TRACK);
5489
6217
  store.setLocalTrack("camera", IDLE_TRACK);
6218
+ media.onDisconnected();
5490
6219
  store.patch({ share: NO_SHARE, connection: "stable" });
6220
+ clearGhosts();
5491
6221
  await target?.disconnect();
5492
6222
  }
5493
6223
  async function leaveRoom() {
@@ -5551,11 +6281,26 @@ function createRoomEngine(options) {
5551
6281
  report("error", normalized.message, normalized.remedy);
5552
6282
  return normalized;
5553
6283
  }
6284
+ const media = createMediaSupervisor({
6285
+ store,
6286
+ platform: options.mediaPlatform === void 0 ? webMediaPlatform() : options.mediaPlatform,
6287
+ driver: () => driver,
6288
+ report
6289
+ });
5554
6290
  const handlers = {
6291
+ audioPlayback(canPlay) {
6292
+ media.onAudioPlayback(canPlay);
6293
+ },
6294
+ devicesChanged() {
6295
+ media.onDevicesChanged();
6296
+ },
6297
+ mediaDeviceError(error, kind) {
6298
+ media.onDeviceError(error, kind);
6299
+ },
5555
6300
  participants(list) {
5556
6301
  const joinedAt = new Date(now()).toISOString();
5557
6302
  const before = new Set(store.snapshot().participants.map((p) => p.identity));
5558
- store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
6303
+ projectRoster(list, joinedAt);
5559
6304
  const snapshot = store.snapshot();
5560
6305
  const newcomer = snapshot.participants.some(
5561
6306
  (participant) => !participant.isLocal && !before.has(participant.identity)
@@ -5567,6 +6312,9 @@ function createRoomEngine(options) {
5567
6312
  );
5568
6313
  }
5569
6314
  },
6315
+ participantLeft(who, reason) {
6316
+ departures.set(who, reason);
6317
+ },
5570
6318
  roomMetadata(metadata) {
5571
6319
  let spotlight = null;
5572
6320
  if (metadata.length > 0) {
@@ -5593,12 +6341,8 @@ function createRoomEngine(options) {
5593
6341
  store.setQuality(asParticipantIdentity(who), value);
5594
6342
  },
5595
6343
  connectionState(state) {
5596
- if (state === "connected") {
5597
- if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
5598
- store.dispatch({ type: "connected" });
5599
- } else if (state === "reconnecting") {
5600
- store.patch({ connection: "reconnecting" });
5601
- }
6344
+ supervisor.driverState(state);
6345
+ if (state === "connected" && store.snapshot().phase === "joining") store.dispatch({ type: "connected" });
5602
6346
  },
5603
6347
  transcription(event) {
5604
6348
  const snapshot = store.snapshot();
@@ -5731,6 +6475,7 @@ function createRoomEngine(options) {
5731
6475
  };
5732
6476
  async function resolveExitAndApply(hint) {
5733
6477
  const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
6478
+ lastRequest = requestAsNow();
5734
6479
  await dropRoom();
5735
6480
  let server;
5736
6481
  if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
@@ -5744,7 +6489,7 @@ function createRoomEngine(options) {
5744
6489
  }
5745
6490
  if (disposed) return;
5746
6491
  const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5747
- store.patch({ connection: resolved.connection });
6492
+ store.patch({ connection: server === null ? supervisor.exitConnection() : resolved.connection });
5748
6493
  if (resolved.to === "waiting:knocking") {
5749
6494
  store.patch({
5750
6495
  waiting: {
@@ -5776,6 +6521,8 @@ function createRoomEngine(options) {
5776
6521
  ...request.observe === true ? { observe: true } : {}
5777
6522
  });
5778
6523
  current = token;
6524
+ lastServerUrl = token.serverUrl;
6525
+ connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
5779
6526
  store.patch({
5780
6527
  localIdentity: token.identity,
5781
6528
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -5815,6 +6562,7 @@ function createRoomEngine(options) {
5815
6562
  store.dispatch({ type: "connected" });
5816
6563
  store.setLocalTrack("microphone", IDLE_TRACK);
5817
6564
  store.setLocalTrack("camera", IDLE_TRACK);
6565
+ media.start();
5818
6566
  const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
5819
6567
  store.patch({ effects });
5820
6568
  if (!effects.noiseSuppression || effects.background !== "none") {
@@ -5839,6 +6587,7 @@ function createRoomEngine(options) {
5839
6587
  if (request.microphone !== false) {
5840
6588
  await enableTrack(created, "microphone", true);
5841
6589
  }
6590
+ media.onConnected();
5842
6591
  }
5843
6592
  async function enableTrack(target, kind, enabled) {
5844
6593
  try {
@@ -5848,10 +6597,10 @@ function createRoomEngine(options) {
5848
6597
  store.setLocalTrack(kind, state);
5849
6598
  return state;
5850
6599
  } catch (error) {
5851
- const normalized = normalizeDeviceError(error, `meet.${kind}`);
5852
- const state = { enabled: false, unavailable: unavailableFrom(normalized) };
6600
+ const refused = await media.unavailable(error, kind, `meet.${kind}`);
6601
+ const state = { enabled: false, unavailable: refused };
5853
6602
  store.setLocalTrack(kind, state);
5854
- report("warn", normalized.message, normalized.remedy);
6603
+ report("warn", refused.message, refused.remedy);
5855
6604
  return state;
5856
6605
  }
5857
6606
  }
@@ -5880,7 +6629,59 @@ function createRoomEngine(options) {
5880
6629
  throw normalized;
5881
6630
  }
5882
6631
  }
5883
- return {
6632
+ function projectRoster(list, joinedAt) {
6633
+ const at = now();
6634
+ const previous = new Map(liveRoster.map((p) => [p.identity, p]));
6635
+ const present = new Set(list.map((p) => p.identity));
6636
+ const graceMs = connectionPolicy.reconnectGraceSeconds * 1e3;
6637
+ for (const [who, reason] of departures) {
6638
+ const last = previous.get(who);
6639
+ if (last !== void 0 && !last.isLocal && !present.has(who) && graceMs > 0 && isUnannouncedDeparture(reason)) {
6640
+ ghosts.set(who, { participant: last, until: at + graceMs });
6641
+ }
6642
+ }
6643
+ departures.clear();
6644
+ liveRoster = list;
6645
+ const merged = mergeGhosts(list, ghosts, at);
6646
+ ghosts = merged.kept;
6647
+ store.setParticipants(
6648
+ merged.roster.map((entry) => {
6649
+ const projected = projectParticipant(entry, joinedAt);
6650
+ return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
6651
+ })
6652
+ );
6653
+ scheduleGhostExpiry();
6654
+ }
6655
+ function scheduleGhostExpiry() {
6656
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
6657
+ ghostTimer = null;
6658
+ if (ghosts.size === 0 || disposed) return;
6659
+ const next = Math.min(...[...ghosts.values()].map((g) => g.until));
6660
+ ghostTimer = setTimeout(() => {
6661
+ ghostTimer = null;
6662
+ if (disposed || driver === null) return;
6663
+ projectRoster(liveRoster, new Date(now()).toISOString());
6664
+ }, Math.max(0, next - now()) + 50);
6665
+ }
6666
+ function clearGhosts() {
6667
+ ghosts = /* @__PURE__ */ new Map();
6668
+ departures.clear();
6669
+ liveRoster = [];
6670
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
6671
+ ghostTimer = null;
6672
+ }
6673
+ const supervisor = createConnectionSupervisor({
6674
+ store,
6675
+ network: options.connection?.network ?? createWebNetworkPort(),
6676
+ probe: () => options.connection?.probe?.(current?.serverUrl ?? lastServerUrl) ?? Promise.resolve(true),
6677
+ policy: () => connectionPolicy,
6678
+ autoRejoin: () => engine.autoRejoin(),
6679
+ currentRequest: requestAsNow,
6680
+ resume: options.connection?.resume,
6681
+ now,
6682
+ report: (level, message) => report(level, message)
6683
+ });
6684
+ const engine = {
5884
6685
  join(request) {
5885
6686
  if (joining !== null) return joining;
5886
6687
  if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
@@ -5925,6 +6726,35 @@ function createRoomEngine(options) {
5925
6726
  });
5926
6727
  return joining;
5927
6728
  },
6729
+ async autoRejoin() {
6730
+ const request = lastRequest;
6731
+ if (request === null || joining !== null) return joining ?? void 0;
6732
+ if (!store.dispatch({ type: "auto_rejoin" })) return;
6733
+ tokens.invalidate(request.roomName);
6734
+ current = null;
6735
+ joining = connect(request).catch((error) => {
6736
+ throw failJoin(error, "meet.autoRejoin");
6737
+ }).finally(() => {
6738
+ joining = null;
6739
+ });
6740
+ return joining;
6741
+ },
6742
+ resume(roomName) {
6743
+ const marker = supervisor.resumable(roomName);
6744
+ if (marker === null || joining !== null) return false;
6745
+ if (!store.dispatch({ type: "resumed" })) return false;
6746
+ lastRequest = { ...marker.request, roomName };
6747
+ report("info", "meet: this tab was in the call before the page reloaded \u2014 rejoining");
6748
+ tokens.invalidate(roomName);
6749
+ current = null;
6750
+ joining = connect(lastRequest).catch((error) => {
6751
+ throw failJoin(error, "meet.resume");
6752
+ }).finally(() => {
6753
+ joining = null;
6754
+ });
6755
+ joining.catch(() => void 0);
6756
+ return true;
6757
+ },
5928
6758
  async moveToBreakout(roomId) {
5929
6759
  const request = lastRequest;
5930
6760
  if (request === null || driver === null) {
@@ -6079,6 +6909,10 @@ function createRoomEngine(options) {
6079
6909
  if (driver === null) return;
6080
6910
  await driver.switchDevice(kind, deviceId);
6081
6911
  },
6912
+ enableAudio: () => media.enableAudio(),
6913
+ acceptDeviceOffer: () => media.acceptOffer(),
6914
+ dismissDeviceOffer: () => media.dismissOffer(),
6915
+ dismissDeviceNotice: () => media.dismissNotice(),
6082
6916
  pin(who) {
6083
6917
  store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
6084
6918
  },
@@ -6092,6 +6926,9 @@ function createRoomEngine(options) {
6092
6926
  token: () => current,
6093
6927
  async dispose() {
6094
6928
  disposed = true;
6929
+ supervisor.dispose();
6930
+ media.stop();
6931
+ clearGhosts();
6095
6932
  if (driver !== null) {
6096
6933
  const target = driver;
6097
6934
  driver = null;
@@ -6100,6 +6937,7 @@ function createRoomEngine(options) {
6100
6937
  store.reset();
6101
6938
  }
6102
6939
  };
6940
+ return engine;
6103
6941
  }
6104
6942
  function canModerate(participant) {
6105
6943
  return participant !== void 0 && isHostRole(participant.role);
@@ -7074,22 +7912,10 @@ function observedRole(role, standing) {
7074
7912
  if (role === "host" || role === "cohost") return role;
7075
7913
  return isLinkFollower(standing) ? "guest" : "participant";
7076
7914
  }
7077
- function observedDevice(track) {
7915
+ function observedDevice(track, permission = "unknown") {
7078
7916
  if (track.enabled) return "on";
7079
- const refused = track.unavailable;
7080
- if (refused === null) return "muted_by_me";
7081
- switch (refused.code) {
7082
- case "permission-denied":
7083
- return "unavailable:denied";
7084
- case "device-not-found":
7085
- return "unavailable:none";
7086
- case "device-in-use":
7087
- return "unavailable:busy";
7088
- case "insecure-context":
7089
- return "unavailable:blocked_site";
7090
- default:
7091
- return "unavailable:failed";
7092
- }
7917
+ const reason = deviceUnavailableReason(track, permission);
7918
+ return reason === null ? "muted_by_me" : `unavailable:${reason}`;
7093
7919
  }
7094
7920
  function endedRecord(snapshot, given) {
7095
7921
  const record = snapshot?.meeting ?? given;
@@ -7122,9 +7948,10 @@ function observe(input) {
7122
7948
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
7123
7949
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
7124
7950
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
7125
- audioBlocked: null,
7126
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera) : null,
7127
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone) : null
7951
+ // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
7952
+ audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
7953
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
7954
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
7128
7955
  };
7129
7956
  }
7130
7957
  function staticObservation(phase, reason = null) {
@@ -7185,14 +8012,14 @@ function participantAttributes(p) {
7185
8012
  "data-meet-participant-role": observedRole(p.role, p.standing),
7186
8013
  "data-meet-participant-self": String(p.isLocal)
7187
8014
  };
7188
- const quality = observedQuality(p.quality);
8015
+ const quality = p.reconnecting === true ? "reconnecting" : observedQuality(p.quality);
7189
8016
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
7190
8017
  return attrs;
7191
8018
  }
7192
- function microphoneNoticeCode(track) {
7193
- const refused = track.unavailable;
7194
- if (refused === null) return null;
7195
- return refused.code === "permission-denied" || refused.code === "insecure-context" ? "mic-blocked" : "device-lost";
8019
+ function microphoneNoticeCode(track, permission = "unknown") {
8020
+ const reason = deviceUnavailableReason(track, permission);
8021
+ if (reason === null) return null;
8022
+ return deviceNoticeCode("microphone", reason) === "mic-blocked" ? "mic-blocked" : "device-lost";
7196
8023
  }
7197
8024
  export {
7198
8025
  ATTENDANCE_CSV_HEADER,
@@ -7201,6 +8028,7 @@ export {
7201
8028
  CAPTION_LANGUAGES,
7202
8029
  CONSENT_NOTICE_ATTRIBUTE,
7203
8030
  DATA_TOPICS,
8031
+ DEFAULT_CONNECTION_POLICY,
7204
8032
  DEFAULT_EFFECTS,
7205
8033
  DEFAULT_PAGE_SIZE,
7206
8034
  EMPTY_COLLAB,
@@ -7211,10 +8039,12 @@ export {
7211
8039
  GUEST_RECORD_ACCESS_METADATA_KEY,
7212
8040
  GUEST_RECORD_NOT_SHARED,
7213
8041
  IDLE_MEDIA,
8042
+ IDLE_MEDIA_REGION,
7214
8043
  IDLE_PHASES,
7215
8044
  IDLE_TRACK,
7216
8045
  JOIN_REFUSAL_REASONS,
7217
8046
  LISTEN_ONLY_NOTICE,
8047
+ LIVEKIT_BASE_DELAYS_MS,
7218
8048
  LIVE_PHASES,
7219
8049
  MEETING_INVITE_KIND,
7220
8050
  MEETING_INVITE_VERSION,
@@ -7238,6 +8068,7 @@ export {
7238
8068
  PARTICIPANT_STANDING_ATTRIBUTE,
7239
8069
  REACTION_TTL_MS,
7240
8070
  REFUSED_ABSENCE,
8071
+ RESUME_WINDOW_MS,
7241
8072
  ROLE_RANK,
7242
8073
  ROOM_EVENTS,
7243
8074
  ROOM_EVENT_PREFIX,
@@ -7267,6 +8098,7 @@ export {
7267
8098
  attendanceTotals,
7268
8099
  attendeeKey,
7269
8100
  autoAssign,
8101
+ autoRejoinDelayMs,
7270
8102
  availableActivities,
7271
8103
  breakoutRemainingMs,
7272
8104
  callInviteAction,
@@ -7278,15 +8110,19 @@ export {
7278
8110
  captionSegment,
7279
8111
  captureHeldOf,
7280
8112
  changesPatch,
8113
+ classifyDeviceFailure,
7281
8114
  clockPosition,
7282
8115
  collabFromState,
7283
8116
  computeStage,
8117
+ connectionPolicyFrom,
7284
8118
  countLabel,
7285
8119
  createCallCenter,
7286
8120
  createCallInviteHandler,
8121
+ createConnectionSupervisor,
7287
8122
  createDeviceManager,
7288
8123
  createInRoomClient,
7289
8124
  createLiveKitDriver,
8125
+ createMediaSupervisor,
7290
8126
  createMeetAi,
7291
8127
  createMeetApi,
7292
8128
  createMeetRepository,
@@ -7295,13 +8131,23 @@ export {
7295
8131
  createMeetingRecordCache,
7296
8132
  createMeetingSession,
7297
8133
  createMemoryDeviceStorage,
8134
+ createReachabilityProbe,
8135
+ createReconnectPolicy,
7298
8136
  createRecordingController,
7299
8137
  createRoomEngine,
7300
8138
  createTokenClient,
7301
8139
  createWebDeviceStorage,
7302
8140
  createWebEffectsStorage,
8141
+ createWebNetworkPort,
8142
+ createWebResumeStorage,
7303
8143
  createWebRoomTokenStorage,
7304
8144
  defaultCaptionLanguage,
8145
+ detectBrowser,
8146
+ detectOs,
8147
+ deviceChipLabel,
8148
+ deviceFixSteps,
8149
+ deviceNoticeCode,
8150
+ deviceUnavailableReason,
7305
8151
  displayNameFor,
7306
8152
  fileDownloadPath,
7307
8153
  focusFromRow,
@@ -7312,6 +8158,7 @@ export {
7312
8158
  guestRecordAccessSetting,
7313
8159
  hasEnded,
7314
8160
  hostUserIds,
8161
+ httpUrlOf,
7315
8162
  invalidResponse,
7316
8163
  isGuest,
7317
8164
  isGuestIdentity,
@@ -7325,6 +8172,7 @@ export {
7325
8172
  isRealName,
7326
8173
  isRefusedPhase,
7327
8174
  isRoomEventTopic,
8175
+ isUnannouncedDeparture,
7328
8176
  isWaitingPhase,
7329
8177
  learnName,
7330
8178
  learnNames,
@@ -7346,6 +8194,7 @@ export {
7346
8194
  meetingTopic,
7347
8195
  meetingWhen,
7348
8196
  mergeCaption,
8197
+ mergeGhosts,
7349
8198
  mergeRecord,
7350
8199
  microphoneNoticeCode,
7351
8200
  moveToRoom,
@@ -7410,6 +8259,7 @@ export {
7410
8259
  untranslatedLines,
7411
8260
  userTopic,
7412
8261
  wasRefused,
8262
+ webMediaPlatform,
7413
8263
  withItem,
7414
8264
  wrapUpStatus
7415
8265
  };