@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/react.cjs CHANGED
@@ -38,6 +38,7 @@ __export(react_exports, {
38
38
  CAPTION_LANGUAGES: () => CAPTION_LANGUAGES,
39
39
  CONSENT_NOTICE_ATTRIBUTE: () => CONSENT_NOTICE_ATTRIBUTE,
40
40
  DATA_TOPICS: () => DATA_TOPICS,
41
+ DEFAULT_CONNECTION_POLICY: () => DEFAULT_CONNECTION_POLICY,
41
42
  DEFAULT_EFFECTS: () => DEFAULT_EFFECTS,
42
43
  DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
43
44
  EMPTY_COLLAB: () => EMPTY_COLLAB,
@@ -49,12 +50,14 @@ __export(react_exports, {
49
50
  GUEST_RECORD_NOT_SHARED: () => GUEST_RECORD_NOT_SHARED,
50
51
  GuestRecordAccessControl: () => GuestRecordAccessControl,
51
52
  IDLE_MEDIA: () => IDLE_MEDIA,
53
+ IDLE_MEDIA_REGION: () => IDLE_MEDIA_REGION,
52
54
  IDLE_PHASES: () => IDLE_PHASES,
53
55
  IDLE_TRACK: () => IDLE_TRACK,
54
56
  IncomingCallCard: () => IncomingCallCard,
55
57
  IncomingCallHost: () => IncomingCallHost,
56
58
  JOIN_REFUSAL_REASONS: () => JOIN_REFUSAL_REASONS,
57
59
  LISTEN_ONLY_NOTICE: () => LISTEN_ONLY_NOTICE,
60
+ LIVEKIT_BASE_DELAYS_MS: () => LIVEKIT_BASE_DELAYS_MS,
58
61
  LIVE_PHASES: () => LIVE_PHASES,
59
62
  MEETING_INVITE_KIND: () => MEETING_INVITE_KIND,
60
63
  MEETING_INVITE_VERSION: () => MEETING_INVITE_VERSION,
@@ -84,6 +87,7 @@ __export(react_exports, {
84
87
  PARTICIPANT_STANDING_ATTRIBUTE: () => PARTICIPANT_STANDING_ATTRIBUTE,
85
88
  REACTION_TTL_MS: () => REACTION_TTL_MS,
86
89
  REFUSED_ABSENCE: () => REFUSED_ABSENCE,
90
+ RESUME_WINDOW_MS: () => RESUME_WINDOW_MS,
87
91
  ROLE_RANK: () => ROLE_RANK,
88
92
  ROOM_EVENTS: () => ROOM_EVENTS,
89
93
  ROOM_EVENT_PREFIX: () => ROOM_EVENT_PREFIX,
@@ -114,6 +118,7 @@ __export(react_exports, {
114
118
  attendanceTotals: () => attendanceTotals,
115
119
  attendeeKey: () => attendeeKey,
116
120
  autoAssign: () => autoAssign,
121
+ autoRejoinDelayMs: () => autoRejoinDelayMs,
117
122
  availableActivities: () => availableActivities,
118
123
  breakoutRemainingMs: () => breakoutRemainingMs,
119
124
  callInviteAction: () => callInviteAction,
@@ -125,15 +130,19 @@ __export(react_exports, {
125
130
  captionSegment: () => captionSegment,
126
131
  captureHeldOf: () => captureHeldOf,
127
132
  changesPatch: () => changesPatch,
133
+ classifyDeviceFailure: () => classifyDeviceFailure,
128
134
  clockPosition: () => clockPosition,
129
135
  collabFromState: () => collabFromState,
130
136
  computeStage: () => computeStage,
137
+ connectionPolicyFrom: () => connectionPolicyFrom,
131
138
  countLabel: () => countLabel,
132
139
  createCallCenter: () => createCallCenter,
133
140
  createCallInviteHandler: () => createCallInviteHandler,
141
+ createConnectionSupervisor: () => createConnectionSupervisor,
134
142
  createDeviceManager: () => createDeviceManager,
135
143
  createInRoomClient: () => createInRoomClient,
136
144
  createLiveKitDriver: () => createLiveKitDriver,
145
+ createMediaSupervisor: () => createMediaSupervisor,
137
146
  createMeetAi: () => createMeetAi,
138
147
  createMeetApi: () => createMeetApi,
139
148
  createMeetRepository: () => createMeetRepository,
@@ -142,13 +151,23 @@ __export(react_exports, {
142
151
  createMeetingRecordCache: () => createMeetingRecordCache,
143
152
  createMeetingSession: () => createMeetingSession,
144
153
  createMemoryDeviceStorage: () => createMemoryDeviceStorage,
154
+ createReachabilityProbe: () => createReachabilityProbe,
155
+ createReconnectPolicy: () => createReconnectPolicy,
145
156
  createRecordingController: () => createRecordingController,
146
157
  createRoomEngine: () => createRoomEngine,
147
158
  createTokenClient: () => createTokenClient,
148
159
  createWebDeviceStorage: () => createWebDeviceStorage,
149
160
  createWebEffectsStorage: () => createWebEffectsStorage,
161
+ createWebNetworkPort: () => createWebNetworkPort,
162
+ createWebResumeStorage: () => createWebResumeStorage,
150
163
  createWebRoomTokenStorage: () => createWebRoomTokenStorage,
151
164
  defaultCaptionLanguage: () => defaultCaptionLanguage,
165
+ detectBrowser: () => detectBrowser,
166
+ detectOs: () => detectOs,
167
+ deviceChipLabel: () => deviceChipLabel,
168
+ deviceFixSteps: () => deviceFixSteps,
169
+ deviceNoticeCode: () => deviceNoticeCode,
170
+ deviceUnavailableReason: () => deviceUnavailableReason,
152
171
  displayNameFor: () => displayNameFor,
153
172
  fileDownloadPath: () => fileDownloadPath,
154
173
  focusFromRow: () => focusFromRow,
@@ -159,6 +178,7 @@ __export(react_exports, {
159
178
  guestRecordAccessSetting: () => guestRecordAccessSetting,
160
179
  hasEnded: () => hasEnded,
161
180
  hostUserIds: () => hostUserIds,
181
+ httpUrlOf: () => httpUrlOf,
162
182
  invalidResponse: () => invalidResponse,
163
183
  isGuest: () => isGuest,
164
184
  isGuestIdentity: () => isGuestIdentity,
@@ -172,6 +192,7 @@ __export(react_exports, {
172
192
  isRealName: () => isRealName,
173
193
  isRefusedPhase: () => isRefusedPhase,
174
194
  isRoomEventTopic: () => isRoomEventTopic,
195
+ isUnannouncedDeparture: () => isUnannouncedDeparture,
175
196
  isWaitingPhase: () => isWaitingPhase,
176
197
  learnName: () => learnName,
177
198
  learnNames: () => learnNames,
@@ -194,6 +215,7 @@ __export(react_exports, {
194
215
  meetingTopic: () => meetingTopic,
195
216
  meetingWhen: () => meetingWhen,
196
217
  mergeCaption: () => mergeCaption,
218
+ mergeGhosts: () => mergeGhosts,
197
219
  mergeRecord: () => mergeRecord,
198
220
  microphoneNoticeCode: () => microphoneNoticeCode,
199
221
  moveToRoom: () => moveToRoom,
@@ -302,11 +324,358 @@ __export(react_exports, {
302
324
  useTrack: () => useTrack,
303
325
  userTopic: () => userTopic,
304
326
  wasRefused: () => wasRefused,
327
+ webMediaPlatform: () => webMediaPlatform,
305
328
  withItem: () => withItem,
306
329
  wrapUpStatus: () => wrapUpStatus
307
330
  });
308
331
  module.exports = __toCommonJS(react_exports);
309
332
 
333
+ // src/core/connection-supervisor.ts
334
+ var DEFAULT_CONNECTION_POLICY = {
335
+ bannerDelayMs: 2e3,
336
+ autoRejoin: true,
337
+ autoRejoinMaxAttempts: 5,
338
+ reconnectGraceSeconds: 30
339
+ };
340
+ function clampInt(value, min, max, fallback) {
341
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
342
+ return Math.min(max, Math.max(min, Math.round(value)));
343
+ }
344
+ function connectionPolicyFrom(raw) {
345
+ if (typeof raw !== "object" || raw === null) return DEFAULT_CONNECTION_POLICY;
346
+ const row = raw;
347
+ const d = DEFAULT_CONNECTION_POLICY;
348
+ return {
349
+ bannerDelayMs: clampInt(row["reconnect_banner_delay_ms"], 0, 1e4, d.bannerDelayMs),
350
+ autoRejoin: typeof row["auto_rejoin"] === "boolean" ? row["auto_rejoin"] : d.autoRejoin,
351
+ autoRejoinMaxAttempts: clampInt(row["auto_rejoin_max_attempts"], 0, 20, d.autoRejoinMaxAttempts),
352
+ reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds)
353
+ };
354
+ }
355
+ var LIVEKIT_BASE_DELAYS_MS = [0, 300, 1200, 2700, 4800, 7e3];
356
+ function createReconnectPolicy(options = {}) {
357
+ const delays = options.delaysMs !== void 0 && options.delaysMs.length > 0 ? options.delaysMs : LIVEKIT_BASE_DELAYS_MS;
358
+ const giveUpMs = options.giveUpMs ?? 45e3;
359
+ const maxAttempts = options.maxAttempts ?? 12;
360
+ const random = options.random ?? Math.random;
361
+ return {
362
+ nextRetryDelayInMs({ retryCount, elapsedMs }) {
363
+ if (retryCount >= maxAttempts || elapsedMs >= giveUpMs) return null;
364
+ const base = delays[Math.min(retryCount, delays.length - 1)] ?? 0;
365
+ const spread = Math.min(2e3, Math.max(500, base / 2));
366
+ return Math.round(base + random() * spread);
367
+ }
368
+ };
369
+ }
370
+ function createWebNetworkPort() {
371
+ const scope = globalThis;
372
+ return {
373
+ online: () => scope.navigator?.onLine !== false,
374
+ subscribe(listener) {
375
+ if (typeof scope.addEventListener !== "function") return () => void 0;
376
+ const on = () => listener(true);
377
+ const off = () => listener(false);
378
+ scope.addEventListener("online", on);
379
+ scope.addEventListener("offline", off);
380
+ return () => {
381
+ scope.removeEventListener?.("online", on);
382
+ scope.removeEventListener?.("offline", off);
383
+ };
384
+ }
385
+ };
386
+ }
387
+ function createReachabilityProbe(urls, timeoutMs = 4e3) {
388
+ return async () => {
389
+ const targets = urls().filter((url) => url.length > 0);
390
+ if (targets.length === 0 || typeof fetch !== "function") return true;
391
+ const one = async (url) => {
392
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
393
+ const timer = setTimeout(() => controller?.abort(), timeoutMs);
394
+ try {
395
+ await fetch(url, {
396
+ method: "GET",
397
+ mode: "no-cors",
398
+ cache: "no-store",
399
+ credentials: "omit",
400
+ ...controller !== null ? { signal: controller.signal } : {}
401
+ });
402
+ return true;
403
+ } catch {
404
+ return false;
405
+ } finally {
406
+ clearTimeout(timer);
407
+ }
408
+ };
409
+ const answers = await Promise.all(targets.map(one));
410
+ return answers.every(Boolean);
411
+ };
412
+ }
413
+ function httpUrlOf(serverUrl) {
414
+ return serverUrl.replace(/^ws(s?):\/\//i, "http$1://");
415
+ }
416
+ var RESUME_WINDOW_MS = 15 * 6e4;
417
+ var RESUME_KEY = "mx-meet:resume:";
418
+ function createWebResumeStorage() {
419
+ const storage = globalThis.sessionStorage;
420
+ if (storage === void 0) return void 0;
421
+ return {
422
+ load(roomName) {
423
+ try {
424
+ const raw = storage.getItem(RESUME_KEY + roomName);
425
+ if (raw === null) return null;
426
+ const parsed = JSON.parse(raw);
427
+ return typeof parsed?.at === "number" && typeof parsed.request?.roomName === "string" ? parsed : null;
428
+ } catch {
429
+ return null;
430
+ }
431
+ },
432
+ save(roomName, marker) {
433
+ try {
434
+ storage.setItem(RESUME_KEY + roomName, JSON.stringify(marker));
435
+ } catch {
436
+ }
437
+ },
438
+ clear(roomName) {
439
+ try {
440
+ storage.removeItem(RESUME_KEY + roomName);
441
+ } catch {
442
+ }
443
+ }
444
+ };
445
+ }
446
+ var CLEARS_RESUME = (phase) => phase === "left" || phase === "removed" || phase === "superseded" || phase === "inactive" || phase === "ended" || phase === "record" || phase === "prejoin" || phase.startsWith("refused:") || phase.startsWith("waiting:");
447
+ function autoRejoinDelayMs(attempt, random = Math.random) {
448
+ const base = Math.min(15e3, 1e3 * 2 ** Math.max(0, attempt));
449
+ return Math.round(base * (0.5 + random()));
450
+ }
451
+ function createConnectionSupervisor(options) {
452
+ const { store, network } = options;
453
+ const now = options.now ?? (() => Date.now());
454
+ const random = options.random ?? Math.random;
455
+ const probeEveryMs = options.probeEveryMs ?? 3e3;
456
+ let driverRegion = null;
457
+ let probeTimer = null;
458
+ let probing = false;
459
+ let bannerTimer = null;
460
+ let rejoinTimer = null;
461
+ let rejoinAttempts = 0;
462
+ let lastPhase = store.snapshot().phase;
463
+ let lastSavedAt = 0;
464
+ let disposed = false;
465
+ const inCall = () => store.snapshot().phase === "in_call";
466
+ const dropped = () => store.snapshot().phase === "disconnected";
467
+ function setRegion(next) {
468
+ if (store.snapshot().connection !== next) store.patch({ connection: next });
469
+ }
470
+ function regionFor(reachable) {
471
+ if (!network.online()) return "offline";
472
+ if (reachable === false) return "server_unreachable";
473
+ if (dropped()) return reachable === true ? "stable" : store.snapshot().connection;
474
+ return driverRegion ?? "stable";
475
+ }
476
+ function needsWatch() {
477
+ const snap = store.snapshot();
478
+ if (dropped()) return true;
479
+ return inCall() && (driverRegion !== null || snap.connection !== "stable");
480
+ }
481
+ function stopProbe() {
482
+ if (probeTimer !== null) clearTimeout(probeTimer);
483
+ probeTimer = null;
484
+ }
485
+ function scheduleProbe(delayMs) {
486
+ if (disposed || probeTimer !== null) return;
487
+ probeTimer = setTimeout(() => {
488
+ probeTimer = null;
489
+ void runProbe();
490
+ }, delayMs);
491
+ }
492
+ async function runProbe() {
493
+ if (disposed || probing) return;
494
+ if (!needsWatch()) return;
495
+ if (!network.online()) {
496
+ setRegion("offline");
497
+ scheduleProbe(probeEveryMs);
498
+ return;
499
+ }
500
+ probing = true;
501
+ let reachable = false;
502
+ try {
503
+ reachable = await options.probe();
504
+ } catch {
505
+ reachable = false;
506
+ } finally {
507
+ probing = false;
508
+ }
509
+ if (disposed) return;
510
+ if (inCall() || dropped()) setRegion(regionFor(reachable));
511
+ if (dropped() && reachable) maybeRejoin();
512
+ if (needsWatch()) scheduleProbe(Math.round(probeEveryMs * (0.75 + random() * 0.5)));
513
+ }
514
+ function syncBanner() {
515
+ const snap = store.snapshot();
516
+ const unstable = snap.phase === "in_call" && snap.connection !== "stable";
517
+ if (!unstable) {
518
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
519
+ bannerTimer = null;
520
+ if (snap.connectionBanner) store.patch({ connectionBanner: false });
521
+ return;
522
+ }
523
+ if (snap.connectionBanner || bannerTimer !== null) return;
524
+ const delay = options.policy().bannerDelayMs;
525
+ if (delay <= 0) {
526
+ store.patch({ connectionBanner: true });
527
+ return;
528
+ }
529
+ bannerTimer = setTimeout(() => {
530
+ bannerTimer = null;
531
+ const latest = store.snapshot();
532
+ if (latest.phase === "in_call" && latest.connection !== "stable" && !latest.connectionBanner) {
533
+ store.patch({ connectionBanner: true });
534
+ }
535
+ }, delay);
536
+ }
537
+ function stopRejoin() {
538
+ if (rejoinTimer !== null) clearTimeout(rejoinTimer);
539
+ rejoinTimer = null;
540
+ }
541
+ function maybeRejoin() {
542
+ if (disposed || rejoinTimer !== null || !dropped()) return;
543
+ const policy = options.policy();
544
+ if (!policy.autoRejoin) return;
545
+ if (rejoinAttempts >= policy.autoRejoinMaxAttempts) return;
546
+ if (!network.online()) return;
547
+ const attempt = rejoinAttempts;
548
+ rejoinTimer = setTimeout(() => {
549
+ rejoinTimer = null;
550
+ if (disposed || !dropped() || !network.online()) return;
551
+ rejoinAttempts = attempt + 1;
552
+ options.report?.("info", `meet: rejoining by itself (attempt ${rejoinAttempts} of ${policy.autoRejoinMaxAttempts})`);
553
+ void options.autoRejoin().catch(() => void 0);
554
+ }, autoRejoinDelayMs(attempt, random));
555
+ }
556
+ function saveMarker(force) {
557
+ const resume = options.resume;
558
+ const snap = store.snapshot();
559
+ if (resume === void 0 || snap.roomName === null) return;
560
+ const at = now();
561
+ if (!force && at - lastSavedAt < 1e4) return;
562
+ const request = options.currentRequest();
563
+ if (request === null) return;
564
+ lastSavedAt = at;
565
+ resume.save(snap.roomName, { request, at });
566
+ }
567
+ const unsubscribeStore = store.subscribe(() => {
568
+ if (disposed) return;
569
+ const snap = store.snapshot();
570
+ const phase = snap.phase;
571
+ const changed = phase !== lastPhase;
572
+ lastPhase = phase;
573
+ if (changed) {
574
+ if (phase === "in_call") {
575
+ rejoinAttempts = 0;
576
+ stopRejoin();
577
+ saveMarker(true);
578
+ } else if (phase === "disconnected") {
579
+ driverRegion = null;
580
+ saveMarker(true);
581
+ stopProbe();
582
+ scheduleProbe(0);
583
+ } else {
584
+ stopRejoin();
585
+ if (phase !== "joining") driverRegion = null;
586
+ if (CLEARS_RESUME(phase) && snap.roomName !== null) options.resume?.clear(snap.roomName);
587
+ }
588
+ } else if (phase === "in_call" || phase === "disconnected") {
589
+ saveMarker(false);
590
+ }
591
+ syncBanner();
592
+ });
593
+ const unsubscribeNetwork = network.subscribe((online) => {
594
+ if (disposed) return;
595
+ if (!online) {
596
+ if (inCall() || dropped()) setRegion("offline");
597
+ stopRejoin();
598
+ stopProbe();
599
+ if (needsWatch()) scheduleProbe(probeEveryMs);
600
+ return;
601
+ }
602
+ stopProbe();
603
+ if (needsWatch()) scheduleProbe(0);
604
+ });
605
+ return {
606
+ driverState(state) {
607
+ if (disposed) return;
608
+ if (state === "connected") {
609
+ driverRegion = null;
610
+ if (inCall() || store.snapshot().phase === "joining") setRegion(network.online() ? "stable" : "offline");
611
+ if (!needsWatch()) stopProbe();
612
+ return;
613
+ }
614
+ if (state === "reconnecting" || state === "signal_reconnecting") {
615
+ driverRegion = state;
616
+ if (inCall()) {
617
+ setRegion(network.online() ? state : "offline");
618
+ stopProbe();
619
+ scheduleProbe(0);
620
+ }
621
+ }
622
+ },
623
+ exitConnection() {
624
+ return network.online() ? "server_unreachable" : "offline";
625
+ },
626
+ resumable(roomName) {
627
+ const resume = options.resume;
628
+ if (resume === void 0) return null;
629
+ const marker = resume.load(roomName);
630
+ if (marker === null) return null;
631
+ if (now() - marker.at > RESUME_WINDOW_MS) {
632
+ resume.clear(roomName);
633
+ return null;
634
+ }
635
+ return marker;
636
+ },
637
+ dispose() {
638
+ disposed = true;
639
+ stopProbe();
640
+ stopRejoin();
641
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
642
+ bannerTimer = null;
643
+ unsubscribeStore();
644
+ unsubscribeNetwork();
645
+ }
646
+ };
647
+ }
648
+ var DELIBERATE_DEPARTURES = /* @__PURE__ */ new Set([
649
+ "CLIENT_INITIATED",
650
+ "PARTICIPANT_REMOVED",
651
+ "ROOM_DELETED",
652
+ "ROOM_CLOSED",
653
+ "DUPLICATE_IDENTITY",
654
+ "USER_REJECTED",
655
+ "USER_UNAVAILABLE",
656
+ "SIP_TRUNK_FAILURE"
657
+ ]);
658
+ function isUnannouncedDeparture(reason) {
659
+ return reason === null || !DELIBERATE_DEPARTURES.has(reason);
660
+ }
661
+ function mergeGhosts(live, ghosts, at) {
662
+ const present = new Set(live.map((p) => p.identity));
663
+ const kept = /* @__PURE__ */ new Map();
664
+ const extra = [];
665
+ for (const [identity, ghost] of ghosts) {
666
+ if (present.has(identity) || ghost.until <= at) continue;
667
+ kept.set(identity, ghost);
668
+ extra.push({
669
+ ...ghost.participant,
670
+ micEnabled: false,
671
+ cameraEnabled: false,
672
+ screenSharing: false,
673
+ reconnecting: true
674
+ });
675
+ }
676
+ return { roster: [...live, ...extra], kept };
677
+ }
678
+
310
679
  // src/react/provider.tsx
311
680
  var import_global_slot2 = require("@ai-matrx/kit/global-slot");
312
681
  var import_react = require("react");
@@ -703,7 +1072,8 @@ function parseRoomToken(body, roomName) {
703
1072
  admissionState: readString(payload, "admission_state") ?? void 0,
704
1073
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
705
1074
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
706
- observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone"
1075
+ observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
1076
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
707
1077
  };
708
1078
  }
709
1079
  function createTokenClient(options) {
@@ -1408,6 +1778,10 @@ function nextPhase(from, event) {
1408
1778
  return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
1409
1779
  case "join":
1410
1780
  return JOINABLE_FROM.has(from) ? "joining" : null;
1781
+ case "auto_rejoin":
1782
+ return from === "disconnected" ? "joining" : null;
1783
+ case "resumed":
1784
+ return from === "resolving" ? "joining" : null;
1411
1785
  case "back_to_prejoin":
1412
1786
  return from === "left" || from === "disconnected" ? "prejoin" : null;
1413
1787
  case "retry":
@@ -2125,13 +2499,19 @@ function recordingLabel(state) {
2125
2499
  }
2126
2500
 
2127
2501
  // src/core/attendance.ts
2502
+ var IDLE_MEDIA_REGION = {
2503
+ permission: { microphone: "unknown", camera: "unknown" },
2504
+ audioOutput: "ok",
2505
+ notice: null,
2506
+ offer: null
2507
+ };
2128
2508
  var IDLE_TRACK = { enabled: false, unavailable: null };
2129
2509
  var IDLE_MEDIA = {
2130
2510
  microphone: IDLE_TRACK,
2131
2511
  camera: IDLE_TRACK
2132
2512
  };
2133
- function unavailableFrom(error) {
2134
- return { code: error.code, message: error.message, remedy: error.remedy };
2513
+ function unavailableFrom(error, reason) {
2514
+ return reason === void 0 ? { code: error.code, message: error.message, remedy: error.remedy } : { code: error.code, message: error.message, remedy: error.remedy, reason };
2135
2515
  }
2136
2516
  var LISTEN_ONLY_NOTICE = "You're attending without a microphone \u2014 others can't hear you";
2137
2517
  function isListenOnly(media) {
@@ -3547,6 +3927,7 @@ var EMPTY = {
3547
3927
  phaseNotice: null,
3548
3928
  roomName: null,
3549
3929
  connection: "stable",
3930
+ connectionBanner: false,
3550
3931
  share: NO_SHARE,
3551
3932
  call: null,
3552
3933
  waiting: null,
@@ -3578,6 +3959,7 @@ var EMPTY = {
3578
3959
  wrapUp: null,
3579
3960
  noteTakerRefusal: null,
3580
3961
  media: IDLE_MEDIA,
3962
+ mediaRegion: IDLE_MEDIA_REGION,
3581
3963
  failure: null,
3582
3964
  collab: EMPTY_COLLAB,
3583
3965
  breakoutRoomId: null
@@ -3795,7 +4177,7 @@ function createMeetStore(initial = {}) {
3795
4177
  },
3796
4178
  setLocalTrack(kind, next) {
3797
4179
  const current = state.media[kind];
3798
- if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.message === next.unavailable?.message) {
4180
+ if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.reason === next.unavailable?.reason && current.unavailable?.message === next.unavailable?.message) {
3799
4181
  return;
3800
4182
  }
3801
4183
  commit({ ...state, media: { ...state.media, [kind]: next } });
@@ -4380,6 +4762,342 @@ function createMeetSessionIdentity(options) {
4380
4762
  });
4381
4763
  }
4382
4764
 
4765
+ // src/core/media-supervisor.ts
4766
+ function classifyDeviceFailure(error, permission) {
4767
+ if (permission === "denied") return "denied";
4768
+ switch (error.code) {
4769
+ case "permission-denied":
4770
+ if (/by system/i.test(error.message) || permission === "granted") return "blocked_os";
4771
+ return "denied";
4772
+ case "device-not-found":
4773
+ return "none";
4774
+ case "device-in-use":
4775
+ return "busy";
4776
+ case "insecure-context":
4777
+ return "blocked_site";
4778
+ default:
4779
+ return "failed";
4780
+ }
4781
+ }
4782
+ function deviceNoticeCode(kind, reason) {
4783
+ if (reason === "denied" || reason === "blocked_site" || reason === "blocked_os") {
4784
+ return kind === "microphone" ? "mic-blocked" : "camera-blocked";
4785
+ }
4786
+ return "device-lost";
4787
+ }
4788
+ function deviceUnavailableReason(track, permission) {
4789
+ if (track.enabled) return null;
4790
+ if (permission === "denied") return "denied";
4791
+ const refused = track.unavailable;
4792
+ if (refused === null) return null;
4793
+ return refused.reason ?? classifyDeviceFailure({ code: refused.code, message: refused.message }, permission);
4794
+ }
4795
+ function deviceChipLabel(kind, reason) {
4796
+ const what = kind === "microphone" ? "Mic" : "Camera";
4797
+ switch (reason) {
4798
+ case "denied":
4799
+ case "blocked_site":
4800
+ case "blocked_os":
4801
+ return `${what} blocked`;
4802
+ case "none":
4803
+ return kind === "microphone" ? "No microphone found" : "No camera found";
4804
+ case "busy":
4805
+ return `${what} in use by another app`;
4806
+ case "revoked":
4807
+ return `${what} access was turned off`;
4808
+ default:
4809
+ return `${what} didn't start`;
4810
+ }
4811
+ }
4812
+ function deviceFixSteps(kind, reason, where) {
4813
+ const device = kind === "microphone" ? "Microphone" : "Camera";
4814
+ const lower = device.toLowerCase();
4815
+ switch (reason) {
4816
+ case "denied":
4817
+ case "revoked":
4818
+ case "blocked_site":
4819
+ switch (where.browser) {
4820
+ case "firefox":
4821
+ return [`Click the crossed-out ${lower} icon in the address bar`, "Clear the Blocked setting", "Press Try again and choose Allow"];
4822
+ case "safari":
4823
+ 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"];
4824
+ default:
4825
+ return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
4826
+ }
4827
+ case "blocked_os": {
4828
+ const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
4829
+ if (where.os === "mac") return [`System Settings \u203A Privacy & Security \u203A ${device}`, `Turn on ${app}`, "Reopen the browser if asked, then Try again"];
4830
+ if (where.os === "windows") return [`Settings \u203A Privacy & security \u203A ${device}`, `Turn on ${device} access for apps`, "Press Try again"];
4831
+ return [`Allow ${lower} access for ${app} in your system settings`, "Press Try again"];
4832
+ }
4833
+ case "busy":
4834
+ return [`Close other apps using the ${lower} (Zoom, Teams, another tab)`, "Press Try again"];
4835
+ case "none":
4836
+ return [`Connect a ${lower}`, "Press Try again"];
4837
+ default:
4838
+ return [`Pick another ${lower} in Settings`, "Press Try again"];
4839
+ }
4840
+ }
4841
+ function detectBrowser(userAgent) {
4842
+ if (/Edg\//.test(userAgent)) return "edge";
4843
+ if (/Firefox\//.test(userAgent)) return "firefox";
4844
+ if (/Chrome\/|Chromium\/|CriOS\//.test(userAgent)) return "chrome";
4845
+ if (/Safari\//.test(userAgent)) return "safari";
4846
+ return "other";
4847
+ }
4848
+ function detectOs(userAgent, maxTouchPoints = 0) {
4849
+ if (/iPhone|iPad|iPod/.test(userAgent)) return "ios";
4850
+ if (/Macintosh/.test(userAgent)) return maxTouchPoints > 1 ? "ios" : "mac";
4851
+ if (/Android/.test(userAgent)) return "android";
4852
+ if (/Windows/.test(userAgent)) return "windows";
4853
+ if (/Linux|CrOS/.test(userAgent)) return "linux";
4854
+ return "other";
4855
+ }
4856
+ function webMediaPlatform() {
4857
+ const scope = globalThis;
4858
+ const nav = scope.navigator;
4859
+ if (nav === void 0) return null;
4860
+ const ua = nav.userAgent ?? "";
4861
+ const reading = (state) => state === "granted" || state === "denied" || state === "prompt" ? state : "unknown";
4862
+ const query = async (kind) => {
4863
+ try {
4864
+ return await nav.permissions?.query({ name: kind }) ?? null;
4865
+ } catch {
4866
+ return null;
4867
+ }
4868
+ };
4869
+ return {
4870
+ browser: detectBrowser(ua),
4871
+ os: detectOs(ua, nav.maxTouchPoints ?? 0),
4872
+ async queryPermission(kind) {
4873
+ return reading((await query(kind))?.state);
4874
+ },
4875
+ watchPermission(kind, onChange) {
4876
+ let status = null;
4877
+ let stopped = false;
4878
+ void query(kind).then((found) => {
4879
+ if (stopped || found === null) return;
4880
+ status = found;
4881
+ found.onchange = () => onChange(reading(found.state));
4882
+ });
4883
+ return () => {
4884
+ stopped = true;
4885
+ if (status !== null) status.onchange = null;
4886
+ };
4887
+ },
4888
+ async listDevices() {
4889
+ try {
4890
+ return await nav.mediaDevices?.enumerateDevices() ?? [];
4891
+ } catch {
4892
+ return [];
4893
+ }
4894
+ },
4895
+ onVisible(callback) {
4896
+ const doc = scope.document;
4897
+ if (doc === void 0) return () => void 0;
4898
+ const handler = () => {
4899
+ if (doc.visibilityState === "visible") callback();
4900
+ };
4901
+ doc.addEventListener("visibilitychange", handler);
4902
+ return () => doc.removeEventListener("visibilitychange", handler);
4903
+ }
4904
+ };
4905
+ }
4906
+ var INPUT_KIND = { microphone: "audioinput", camera: "videoinput" };
4907
+ function isAlias(deviceId2) {
4908
+ return deviceId2 === "default" || deviceId2 === "communications" || deviceId2 === "";
4909
+ }
4910
+ function createMediaSupervisor(options) {
4911
+ const { store, platform } = options;
4912
+ let unsubscribers = [];
4913
+ let started = false;
4914
+ let noticeId = 0;
4915
+ let known = null;
4916
+ let inUse = { audioinput: null, videoinput: null };
4917
+ function region() {
4918
+ return store.snapshot().mediaRegion;
4919
+ }
4920
+ function setRegion(next) {
4921
+ const current = region();
4922
+ const merged = { ...current, ...next };
4923
+ 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) {
4924
+ return;
4925
+ }
4926
+ store.patch({ mediaRegion: merged });
4927
+ }
4928
+ function setPermission(kind, reading) {
4929
+ const current = region().permission;
4930
+ if (current[kind] === reading) return;
4931
+ setRegion({ permission: { ...current, [kind]: reading } });
4932
+ const track = store.snapshot().media[kind];
4933
+ if (reading === "granted" && !track.enabled && track.unavailable?.reason === "denied") {
4934
+ store.setLocalTrack(kind, {
4935
+ enabled: false,
4936
+ unavailable: { ...track.unavailable, reason: "failed", remedy: "Access is allowed now. Press Try again." }
4937
+ });
4938
+ }
4939
+ }
4940
+ async function snapshotDevices() {
4941
+ const list = platform === null ? [] : await platform.listDevices();
4942
+ const out = { audioinput: /* @__PURE__ */ new Map(), videoinput: /* @__PURE__ */ new Map() };
4943
+ for (const device of list) {
4944
+ if (device.kind === "audioinput" || device.kind === "videoinput") out[device.kind].set(device.deviceId, device.label);
4945
+ }
4946
+ return out;
4947
+ }
4948
+ function readInUse() {
4949
+ const driver = options.driver();
4950
+ if (driver === null) return;
4951
+ const active = driver.activeDevices();
4952
+ inUse = { audioinput: active.audioinput, videoinput: active.videoinput };
4953
+ }
4954
+ async function handleDevicesChanged() {
4955
+ if (platform === null) return;
4956
+ const before = known;
4957
+ const now = await snapshotDevices();
4958
+ known = now;
4959
+ const driver = options.driver();
4960
+ if (driver === null || before === null) {
4961
+ readInUse();
4962
+ return;
4963
+ }
4964
+ for (const kind of ["microphone", "camera"]) {
4965
+ const deviceKind = INPUT_KIND[kind];
4966
+ const previous = inUse[deviceKind];
4967
+ const track = store.snapshot().media[kind];
4968
+ const gone = previous !== null && !isAlias(previous) && before[deviceKind].has(previous) && !now[deviceKind].has(previous);
4969
+ if (gone) {
4970
+ const from = before[deviceKind].get(previous) ?? "";
4971
+ const remaining = [...now[deviceKind].entries()].filter(([id]) => !isAlias(id));
4972
+ if (remaining.length === 0) {
4973
+ store.setLocalTrack(kind, {
4974
+ enabled: false,
4975
+ unavailable: {
4976
+ code: "device-not-found",
4977
+ message: `meet.${kind}: the ${kind} was disconnected and no other is connected`,
4978
+ remedy: `Connect a ${kind === "microphone" ? "microphone" : "camera"}, then press Try again.`,
4979
+ reason: "none"
4980
+ }
4981
+ });
4982
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "lost", from, to: null } });
4983
+ continue;
4984
+ }
4985
+ if (kind === "camera") {
4986
+ const [nextId, nextLabel] = remaining[0];
4987
+ try {
4988
+ await driver.switchDevice("videoinput", nextId);
4989
+ if (track.enabled || track.unavailable !== null) {
4990
+ await driver.setCameraEnabled(true);
4991
+ store.setLocalTrack("camera", { enabled: true, unavailable: null });
4992
+ }
4993
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "switched", from, to: nextLabel } });
4994
+ } catch (error) {
4995
+ const refused = await unavailable(error, "camera", "meet.camera(fallback)");
4996
+ store.setLocalTrack("camera", { enabled: false, unavailable: refused });
4997
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "lost", from, to: null } });
4998
+ }
4999
+ } else {
5000
+ const active = driver.activeDevices().audioinput;
5001
+ const to = (active !== null ? now.audioinput.get(active) : void 0) ?? now.audioinput.get("default") ?? remaining[0][1];
5002
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "switched", from, to } });
5003
+ }
5004
+ continue;
5005
+ }
5006
+ const added = [...now[deviceKind].entries()].find(([id]) => !isAlias(id) && !before[deviceKind].has(id));
5007
+ if (added !== void 0 && added[0] !== driver.activeDevices()[deviceKind]) {
5008
+ setRegion({ offer: { kind, deviceId: added[0], label: added[1] } });
5009
+ }
5010
+ }
5011
+ readInUse();
5012
+ }
5013
+ async function unavailable(error, kind, operation) {
5014
+ const normalized = normalizeDeviceError(error, operation);
5015
+ const permission = platform === null ? "unknown" : await platform.queryPermission(kind);
5016
+ if (permission !== "unknown") setPermission(kind, permission);
5017
+ return unavailableFrom(normalized, classifyDeviceFailure(normalized, permission));
5018
+ }
5019
+ return {
5020
+ start() {
5021
+ if (started || platform === null) return;
5022
+ started = true;
5023
+ for (const kind of ["microphone", "camera"]) {
5024
+ void platform.queryPermission(kind).then((reading) => setPermission(kind, reading));
5025
+ unsubscribers.push(platform.watchPermission(kind, (reading) => setPermission(kind, reading)));
5026
+ }
5027
+ unsubscribers.push(
5028
+ platform.onVisible(() => {
5029
+ const driver = options.driver();
5030
+ if (driver?.startAudio === void 0 || region().audioOutput !== "blocked") return;
5031
+ driver.startAudio().catch(() => void 0);
5032
+ })
5033
+ );
5034
+ },
5035
+ stop() {
5036
+ for (const off of unsubscribers) off();
5037
+ unsubscribers = [];
5038
+ started = false;
5039
+ },
5040
+ unavailable,
5041
+ onAudioPlayback(canPlay) {
5042
+ setRegion({ audioOutput: canPlay ? "ok" : "blocked" });
5043
+ },
5044
+ onDevicesChanged() {
5045
+ void handleDevicesChanged().catch((error) => {
5046
+ options.report("warn", `meet: device change could not be handled: ${String(error)}`, "Pick the device in Settings.");
5047
+ });
5048
+ },
5049
+ onDeviceError(error, deviceKind) {
5050
+ if (deviceKind === null) return;
5051
+ const kind = deviceKind === "audioinput" ? "microphone" : "camera";
5052
+ void unavailable(error, kind, `meet.${kind}`).then((refused) => {
5053
+ const track = store.snapshot().media[kind];
5054
+ if (track.enabled) return;
5055
+ store.setLocalTrack(kind, { enabled: false, unavailable: refused });
5056
+ });
5057
+ },
5058
+ onConnected() {
5059
+ void snapshotDevices().then((now) => {
5060
+ known = now;
5061
+ readInUse();
5062
+ });
5063
+ },
5064
+ onDisconnected() {
5065
+ known = null;
5066
+ inUse = { audioinput: null, videoinput: null };
5067
+ setRegion({ audioOutput: "ok", notice: null, offer: null });
5068
+ },
5069
+ async enableAudio() {
5070
+ const driver = options.driver();
5071
+ if (driver?.startAudio === void 0) return;
5072
+ try {
5073
+ await driver.startAudio();
5074
+ setRegion({ audioOutput: "ok" });
5075
+ } catch (error) {
5076
+ options.report("warn", `meet: sound could not be enabled: ${String(error)}`, "Click Enable sound again, or check the tab is not muted.");
5077
+ }
5078
+ },
5079
+ async acceptOffer() {
5080
+ const offer = region().offer;
5081
+ const driver = options.driver();
5082
+ setRegion({ offer: null });
5083
+ if (offer === null || driver === null) return;
5084
+ try {
5085
+ await driver.switchDevice(INPUT_KIND[offer.kind], offer.deviceId);
5086
+ readInUse();
5087
+ } catch (error) {
5088
+ const refused = await unavailable(error, offer.kind, "meet.switchDevice");
5089
+ options.report("warn", refused.message, refused.remedy);
5090
+ }
5091
+ },
5092
+ dismissOffer() {
5093
+ setRegion({ offer: null });
5094
+ },
5095
+ dismissNotice() {
5096
+ setRegion({ notice: null });
5097
+ }
5098
+ };
5099
+ }
5100
+
4383
5101
  // src/core/room-events.ts
4384
5102
  var RECORDING_STATES = [
4385
5103
  "idle",
@@ -4826,6 +5544,18 @@ function createRoomEngine(options) {
4826
5544
  let disposed = false;
4827
5545
  let moving = false;
4828
5546
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
5547
+ let lastServerUrl = null;
5548
+ let connectionPolicy = DEFAULT_CONNECTION_POLICY;
5549
+ let ghosts = /* @__PURE__ */ new Map();
5550
+ let ghostTimer = null;
5551
+ let liveRoster = [];
5552
+ const departures = /* @__PURE__ */ new Map();
5553
+ function requestAsNow() {
5554
+ if (lastRequest === null) return null;
5555
+ const media2 = store.snapshot().media;
5556
+ if (store.snapshot().phase !== "in_call") return lastRequest;
5557
+ return { ...lastRequest, mode: media2.camera.enabled ? "video" : "audio", microphone: media2.microphone.enabled };
5558
+ }
4829
5559
  async function dropRoom() {
4830
5560
  const target = driver;
4831
5561
  driver = null;
@@ -4835,7 +5565,9 @@ function createRoomEngine(options) {
4835
5565
  }
4836
5566
  store.setLocalTrack("microphone", IDLE_TRACK);
4837
5567
  store.setLocalTrack("camera", IDLE_TRACK);
5568
+ media.onDisconnected();
4838
5569
  store.patch({ share: NO_SHARE, connection: "stable" });
5570
+ clearGhosts();
4839
5571
  await target?.disconnect();
4840
5572
  }
4841
5573
  async function leaveRoom() {
@@ -4899,11 +5631,26 @@ function createRoomEngine(options) {
4899
5631
  report("error", normalized.message, normalized.remedy);
4900
5632
  return normalized;
4901
5633
  }
5634
+ const media = createMediaSupervisor({
5635
+ store,
5636
+ platform: options.mediaPlatform === void 0 ? webMediaPlatform() : options.mediaPlatform,
5637
+ driver: () => driver,
5638
+ report
5639
+ });
4902
5640
  const handlers = {
5641
+ audioPlayback(canPlay) {
5642
+ media.onAudioPlayback(canPlay);
5643
+ },
5644
+ devicesChanged() {
5645
+ media.onDevicesChanged();
5646
+ },
5647
+ mediaDeviceError(error, kind) {
5648
+ media.onDeviceError(error, kind);
5649
+ },
4903
5650
  participants(list) {
4904
5651
  const joinedAt = new Date(now()).toISOString();
4905
5652
  const before = new Set(store.snapshot().participants.map((p) => p.identity));
4906
- store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
5653
+ projectRoster(list, joinedAt);
4907
5654
  const snapshot = store.snapshot();
4908
5655
  const newcomer = snapshot.participants.some(
4909
5656
  (participant) => !participant.isLocal && !before.has(participant.identity)
@@ -4915,6 +5662,9 @@ function createRoomEngine(options) {
4915
5662
  );
4916
5663
  }
4917
5664
  },
5665
+ participantLeft(who, reason) {
5666
+ departures.set(who, reason);
5667
+ },
4918
5668
  roomMetadata(metadata) {
4919
5669
  let spotlight = null;
4920
5670
  if (metadata.length > 0) {
@@ -4941,12 +5691,8 @@ function createRoomEngine(options) {
4941
5691
  store.setQuality(asParticipantIdentity(who), value);
4942
5692
  },
4943
5693
  connectionState(state) {
4944
- if (state === "connected") {
4945
- if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
4946
- store.dispatch({ type: "connected" });
4947
- } else if (state === "reconnecting") {
4948
- store.patch({ connection: "reconnecting" });
4949
- }
5694
+ supervisor.driverState(state);
5695
+ if (state === "connected" && store.snapshot().phase === "joining") store.dispatch({ type: "connected" });
4950
5696
  },
4951
5697
  transcription(event) {
4952
5698
  const snapshot = store.snapshot();
@@ -5079,6 +5825,7 @@ function createRoomEngine(options) {
5079
5825
  };
5080
5826
  async function resolveExitAndApply(hint) {
5081
5827
  const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
5828
+ lastRequest = requestAsNow();
5082
5829
  await dropRoom();
5083
5830
  let server;
5084
5831
  if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
@@ -5092,7 +5839,7 @@ function createRoomEngine(options) {
5092
5839
  }
5093
5840
  if (disposed) return;
5094
5841
  const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5095
- store.patch({ connection: resolved.connection });
5842
+ store.patch({ connection: server === null ? supervisor.exitConnection() : resolved.connection });
5096
5843
  if (resolved.to === "waiting:knocking") {
5097
5844
  store.patch({
5098
5845
  waiting: {
@@ -5124,6 +5871,8 @@ function createRoomEngine(options) {
5124
5871
  ...request.observe === true ? { observe: true } : {}
5125
5872
  });
5126
5873
  current = token;
5874
+ lastServerUrl = token.serverUrl;
5875
+ connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
5127
5876
  store.patch({
5128
5877
  localIdentity: token.identity,
5129
5878
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -5163,6 +5912,7 @@ function createRoomEngine(options) {
5163
5912
  store.dispatch({ type: "connected" });
5164
5913
  store.setLocalTrack("microphone", IDLE_TRACK);
5165
5914
  store.setLocalTrack("camera", IDLE_TRACK);
5915
+ media.start();
5166
5916
  const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
5167
5917
  store.patch({ effects });
5168
5918
  if (!effects.noiseSuppression || effects.background !== "none") {
@@ -5187,6 +5937,7 @@ function createRoomEngine(options) {
5187
5937
  if (request.microphone !== false) {
5188
5938
  await enableTrack(created, "microphone", true);
5189
5939
  }
5940
+ media.onConnected();
5190
5941
  }
5191
5942
  async function enableTrack(target, kind, enabled) {
5192
5943
  try {
@@ -5196,10 +5947,10 @@ function createRoomEngine(options) {
5196
5947
  store.setLocalTrack(kind, state);
5197
5948
  return state;
5198
5949
  } catch (error) {
5199
- const normalized = normalizeDeviceError(error, `meet.${kind}`);
5200
- const state = { enabled: false, unavailable: unavailableFrom(normalized) };
5950
+ const refused = await media.unavailable(error, kind, `meet.${kind}`);
5951
+ const state = { enabled: false, unavailable: refused };
5201
5952
  store.setLocalTrack(kind, state);
5202
- report("warn", normalized.message, normalized.remedy);
5953
+ report("warn", refused.message, refused.remedy);
5203
5954
  return state;
5204
5955
  }
5205
5956
  }
@@ -5228,7 +5979,59 @@ function createRoomEngine(options) {
5228
5979
  throw normalized;
5229
5980
  }
5230
5981
  }
5231
- return {
5982
+ function projectRoster(list, joinedAt) {
5983
+ const at = now();
5984
+ const previous = new Map(liveRoster.map((p) => [p.identity, p]));
5985
+ const present = new Set(list.map((p) => p.identity));
5986
+ const graceMs = connectionPolicy.reconnectGraceSeconds * 1e3;
5987
+ for (const [who, reason] of departures) {
5988
+ const last = previous.get(who);
5989
+ if (last !== void 0 && !last.isLocal && !present.has(who) && graceMs > 0 && isUnannouncedDeparture(reason)) {
5990
+ ghosts.set(who, { participant: last, until: at + graceMs });
5991
+ }
5992
+ }
5993
+ departures.clear();
5994
+ liveRoster = list;
5995
+ const merged = mergeGhosts(list, ghosts, at);
5996
+ ghosts = merged.kept;
5997
+ store.setParticipants(
5998
+ merged.roster.map((entry) => {
5999
+ const projected = projectParticipant(entry, joinedAt);
6000
+ return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
6001
+ })
6002
+ );
6003
+ scheduleGhostExpiry();
6004
+ }
6005
+ function scheduleGhostExpiry() {
6006
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
6007
+ ghostTimer = null;
6008
+ if (ghosts.size === 0 || disposed) return;
6009
+ const next = Math.min(...[...ghosts.values()].map((g) => g.until));
6010
+ ghostTimer = setTimeout(() => {
6011
+ ghostTimer = null;
6012
+ if (disposed || driver === null) return;
6013
+ projectRoster(liveRoster, new Date(now()).toISOString());
6014
+ }, Math.max(0, next - now()) + 50);
6015
+ }
6016
+ function clearGhosts() {
6017
+ ghosts = /* @__PURE__ */ new Map();
6018
+ departures.clear();
6019
+ liveRoster = [];
6020
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
6021
+ ghostTimer = null;
6022
+ }
6023
+ const supervisor = createConnectionSupervisor({
6024
+ store,
6025
+ network: options.connection?.network ?? createWebNetworkPort(),
6026
+ probe: () => options.connection?.probe?.(current?.serverUrl ?? lastServerUrl) ?? Promise.resolve(true),
6027
+ policy: () => connectionPolicy,
6028
+ autoRejoin: () => engine.autoRejoin(),
6029
+ currentRequest: requestAsNow,
6030
+ resume: options.connection?.resume,
6031
+ now,
6032
+ report: (level, message) => report(level, message)
6033
+ });
6034
+ const engine = {
5232
6035
  join(request) {
5233
6036
  if (joining !== null) return joining;
5234
6037
  if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
@@ -5273,6 +6076,35 @@ function createRoomEngine(options) {
5273
6076
  });
5274
6077
  return joining;
5275
6078
  },
6079
+ async autoRejoin() {
6080
+ const request = lastRequest;
6081
+ if (request === null || joining !== null) return joining ?? void 0;
6082
+ if (!store.dispatch({ type: "auto_rejoin" })) return;
6083
+ tokens.invalidate(request.roomName);
6084
+ current = null;
6085
+ joining = connect(request).catch((error) => {
6086
+ throw failJoin(error, "meet.autoRejoin");
6087
+ }).finally(() => {
6088
+ joining = null;
6089
+ });
6090
+ return joining;
6091
+ },
6092
+ resume(roomName) {
6093
+ const marker = supervisor.resumable(roomName);
6094
+ if (marker === null || joining !== null) return false;
6095
+ if (!store.dispatch({ type: "resumed" })) return false;
6096
+ lastRequest = { ...marker.request, roomName };
6097
+ report("info", "meet: this tab was in the call before the page reloaded \u2014 rejoining");
6098
+ tokens.invalidate(roomName);
6099
+ current = null;
6100
+ joining = connect(lastRequest).catch((error) => {
6101
+ throw failJoin(error, "meet.resume");
6102
+ }).finally(() => {
6103
+ joining = null;
6104
+ });
6105
+ joining.catch(() => void 0);
6106
+ return true;
6107
+ },
5276
6108
  async moveToBreakout(roomId) {
5277
6109
  const request = lastRequest;
5278
6110
  if (request === null || driver === null) {
@@ -5427,6 +6259,10 @@ function createRoomEngine(options) {
5427
6259
  if (driver === null) return;
5428
6260
  await driver.switchDevice(kind, deviceId2);
5429
6261
  },
6262
+ enableAudio: () => media.enableAudio(),
6263
+ acceptDeviceOffer: () => media.acceptOffer(),
6264
+ dismissDeviceOffer: () => media.dismissOffer(),
6265
+ dismissDeviceNotice: () => media.dismissNotice(),
5430
6266
  pin(who) {
5431
6267
  store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
5432
6268
  },
@@ -5440,6 +6276,9 @@ function createRoomEngine(options) {
5440
6276
  token: () => current,
5441
6277
  async dispose() {
5442
6278
  disposed = true;
6279
+ supervisor.dispose();
6280
+ media.stop();
6281
+ clearGhosts();
5443
6282
  if (driver !== null) {
5444
6283
  const target = driver;
5445
6284
  driver = null;
@@ -5448,6 +6287,7 @@ function createRoomEngine(options) {
5448
6287
  store.reset();
5449
6288
  }
5450
6289
  };
6290
+ return engine;
5451
6291
  }
5452
6292
  function canModerate(participant) {
5453
6293
  return participant !== void 0 && isHostRole(participant.role);
@@ -5758,8 +6598,11 @@ function mapConnectionState(state) {
5758
6598
  case "connecting":
5759
6599
  return "connecting";
5760
6600
  case "reconnecting":
5761
- case "signalReconnecting":
5762
6601
  return "reconnecting";
6602
+ // Only the signal socket is down (S6b): its own region value, so the supervisor can tell a
6603
+ // signal blip from a full media reconnect.
6604
+ case "signalReconnecting":
6605
+ return "signal_reconnecting";
5763
6606
  default:
5764
6607
  return "disconnected";
5765
6608
  }
@@ -5840,8 +6683,12 @@ function createLiveKitDriver(handlers, options = {}) {
5840
6683
  // R5: large meetings degrade gracefully instead of melting a laptop.
5841
6684
  adaptiveStream: true,
5842
6685
  dynacast: true,
6686
+ // S6b: OUR retry schedule — jittered from attempt 0 (the SDK's default has no jitter on its
6687
+ // first two attempts), give-up limits in one place (`connection-supervisor.ts`).
6688
+ reconnectPolicy: createReconnectPolicy(),
5843
6689
  ...options.roomOptions
5844
6690
  });
6691
+ const reasonName = (reason) => reason === void 0 || reason === null ? null : module2.DisconnectReason?.[reason] ?? String(reason);
5845
6692
  const roster = () => {
5846
6693
  const local = snapshotParticipant(created.localParticipant, true);
5847
6694
  const remotes = [...created.remoteParticipants.values()].map(
@@ -5859,8 +6706,17 @@ function createLiveKitDriver(handlers, options = {}) {
5859
6706
  if (track.kind !== Track.Kind.Audio) return;
5860
6707
  for (const element of track.detach()) element.remove();
5861
6708
  });
5862
- 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) => {
6709
+ created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
6710
+ handlers.participantLeft?.(participant.identity, reasonName(reason));
6711
+ roster();
6712
+ }).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) => {
5863
6713
  handlers.roomMetadata?.(metadata ?? "");
6714
+ }).on(RoomEvent.AudioPlaybackStatusChanged, (canPlay) => {
6715
+ handlers.audioPlayback?.(canPlay);
6716
+ }).on(RoomEvent.MediaDevicesChanged, () => {
6717
+ handlers.devicesChanged?.();
6718
+ }).on(RoomEvent.MediaDevicesError, (error, kind) => {
6719
+ handlers.mediaDeviceError?.(error, kind === "audioinput" || kind === "videoinput" ? kind : null);
5864
6720
  }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
5865
6721
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
5866
6722
  }).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
@@ -5876,8 +6732,7 @@ function createLiveKitDriver(handlers, options = {}) {
5876
6732
  }).on(RoomEvent.Disconnected, (reason) => {
5877
6733
  audioContainer?.remove();
5878
6734
  audioContainer = null;
5879
- const named = reason === void 0 ? null : module2?.DisconnectReason?.[reason] ?? String(reason);
5880
- handlers.disconnected(named);
6735
+ handlers.disconnected(reasonName(reason));
5881
6736
  });
5882
6737
  try {
5883
6738
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -5958,6 +6813,7 @@ function createLiveKitDriver(handlers, options = {}) {
5958
6813
  }
5959
6814
  projectRoster?.();
5960
6815
  handlers.roomMetadata?.(target.metadata ?? "");
6816
+ handlers.audioPlayback?.(target.canPlaybackAudio);
5961
6817
  },
5962
6818
  async disconnect() {
5963
6819
  if (room === null) return;
@@ -6084,6 +6940,11 @@ function createLiveKitDriver(handlers, options = {}) {
6084
6940
  } catch (error) {
6085
6941
  throw normalizeDeviceError(error, "meet.switchDevice");
6086
6942
  }
6943
+ },
6944
+ async startAudio() {
6945
+ const target = room;
6946
+ if (target === null) return;
6947
+ await target.startAudio();
6087
6948
  }
6088
6949
  };
6089
6950
  }
@@ -6267,7 +7128,14 @@ function MeetRuntime(props) {
6267
7128
  effectsStorage,
6268
7129
  resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null,
6269
7130
  // §3.4: on every LiveKit disconnect, ask the server what happened to this connection.
6270
- readExit: (connection) => meetings.readExit(connection)
7131
+ readExit: (connection) => meetings.readExit(connection),
7132
+ // S6b: the connection supervisor's ports — the device's network, whether OUR servers (the
7133
+ // API and the media server) answer, and the per-tab reload-after-offline marker.
7134
+ connection: {
7135
+ network: createWebNetworkPort(),
7136
+ resume: createWebResumeStorage(),
7137
+ probe: (serverUrl) => createReachabilityProbe(() => [baseUrl, ...serverUrl !== null ? [httpUrlOf(serverUrl)] : []])()
7138
+ }
6271
7139
  });
6272
7140
  const meetings = createMeetingSession({
6273
7141
  manager,
@@ -6484,22 +7352,10 @@ function observedRole(role, standing) {
6484
7352
  if (role === "host" || role === "cohost") return role;
6485
7353
  return isLinkFollower(standing) ? "guest" : "participant";
6486
7354
  }
6487
- function observedDevice(track) {
7355
+ function observedDevice(track, permission = "unknown") {
6488
7356
  if (track.enabled) return "on";
6489
- const refused = track.unavailable;
6490
- if (refused === null) return "muted_by_me";
6491
- switch (refused.code) {
6492
- case "permission-denied":
6493
- return "unavailable:denied";
6494
- case "device-not-found":
6495
- return "unavailable:none";
6496
- case "device-in-use":
6497
- return "unavailable:busy";
6498
- case "insecure-context":
6499
- return "unavailable:blocked_site";
6500
- default:
6501
- return "unavailable:failed";
6502
- }
7357
+ const reason = deviceUnavailableReason(track, permission);
7358
+ return reason === null ? "muted_by_me" : `unavailable:${reason}`;
6503
7359
  }
6504
7360
  function endedRecord(snapshot, given) {
6505
7361
  const record = snapshot?.meeting ?? given;
@@ -6532,9 +7388,10 @@ function observe(input) {
6532
7388
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
6533
7389
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
6534
7390
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
6535
- audioBlocked: null,
6536
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera) : null,
6537
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone) : null
7391
+ // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
7392
+ audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
7393
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
7394
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
6538
7395
  };
6539
7396
  }
6540
7397
  function staticObservation(phase, reason = null) {
@@ -6595,14 +7452,14 @@ function participantAttributes(p) {
6595
7452
  "data-meet-participant-role": observedRole(p.role, p.standing),
6596
7453
  "data-meet-participant-self": String(p.isLocal)
6597
7454
  };
6598
- const quality = observedQuality(p.quality);
7455
+ const quality = p.reconnecting === true ? "reconnecting" : observedQuality(p.quality);
6599
7456
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
6600
7457
  return attrs;
6601
7458
  }
6602
- function microphoneNoticeCode(track) {
6603
- const refused = track.unavailable;
6604
- if (refused === null) return null;
6605
- return refused.code === "permission-denied" || refused.code === "insecure-context" ? "mic-blocked" : "device-lost";
7459
+ function microphoneNoticeCode(track, permission = "unknown") {
7460
+ const reason = deviceUnavailableReason(track, permission);
7461
+ if (reason === null) return null;
7462
+ return deviceNoticeCode("microphone", reason) === "mic-blocked" ? "mic-blocked" : "device-lost";
6606
7463
  }
6607
7464
 
6608
7465
  // src/react/root.tsx
@@ -7703,6 +8560,26 @@ function useConsentNotice() {
7703
8560
  if (agent === null) return null;
7704
8561
  return `${agent.displayName} is in this meeting: it is transcribing what is said and writing notes everyone here can read.`;
7705
8562
  }
8563
+ function useMediaSupervisor() {
8564
+ const host = useMeetHost();
8565
+ const snapshot = useMeetSnapshot();
8566
+ const engine = host?.engine ?? null;
8567
+ return {
8568
+ region: snapshot?.mediaRegion ?? IDLE_MEDIA_REGION,
8569
+ where: (0, import_react4.useMemo)(() => {
8570
+ const platform = webMediaPlatform();
8571
+ return { browser: platform?.browser ?? "other", os: platform?.os ?? "other" };
8572
+ }, []),
8573
+ enableAudio: (0, import_react4.useCallback)(async () => engine?.enableAudio(), [engine]),
8574
+ acceptOffer: (0, import_react4.useCallback)(async () => engine?.acceptDeviceOffer(), [engine]),
8575
+ dismissOffer: (0, import_react4.useCallback)(() => engine?.dismissDeviceOffer(), [engine]),
8576
+ dismissNotice: (0, import_react4.useCallback)(() => engine?.dismissDeviceNotice(), [engine]),
8577
+ retry: (0, import_react4.useCallback)(
8578
+ async (kind) => kind === "microphone" ? engine?.setMicrophoneEnabled(true) : engine?.setCameraEnabled(true),
8579
+ [engine]
8580
+ )
8581
+ };
8582
+ }
7706
8583
  function useLocalMedia() {
7707
8584
  const snapshot = useMeetSnapshot();
7708
8585
  return snapshot?.media ?? IDLE_MEDIA;
@@ -8814,6 +9691,7 @@ function useMeetCore() {
8814
9691
  reason: snapshot?.reason ?? null,
8815
9692
  phaseNotice: snapshot?.phaseNotice ?? null,
8816
9693
  connection: snapshot?.connection ?? "stable",
9694
+ connectionBanner: snapshot?.connectionBanner ?? false,
8817
9695
  share: snapshot?.share ?? NO_SHARE,
8818
9696
  call: snapshot?.call ?? null,
8819
9697
  actions
@@ -10080,7 +10958,8 @@ function ParticipantTile({
10080
10958
  participant.displayName,
10081
10959
  participant.isLocal ? " (you)" : "",
10082
10960
  participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "AI" }) : null,
10083
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RoleBadge, { role: participant.role, standing: participant.standing })
10961
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RoleBadge, { role: participant.role, standing: participant.standing }),
10962
+ participant.reconnecting === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Reconnecting\u2026" }) : null
10084
10963
  ] }),
10085
10964
  !participant.micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
10086
10965
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(QualityBars, { quality: participant.quality })
@@ -10123,6 +11002,12 @@ function QualityBars({ quality }) {
10123
11002
  const label = quality === "lost" ? "Connection lost" : quality === "poor" ? "Poor connection" : `Connection ${quality}`;
10124
11003
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: `mx-meet__signal mx-meet__signal--${quality}`, title: label, "aria-label": label, role: "img", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SignalIcon, { bars, width: 14, height: 14 }) });
10125
11004
  }
11005
+ var CONNECTION_BANNER = {
11006
+ signal_reconnecting: "Reconnecting\u2026",
11007
+ reconnecting: "Reconnecting\u2026",
11008
+ offline: "You're offline. Reconnecting when your internet is back\u2026",
11009
+ server_unreachable: "Can't reach the meeting server. Retrying\u2026"
11010
+ };
10126
11011
  function Stage({ pageSize }) {
10127
11012
  const stage = useStage(pageSize);
10128
11013
  const columns = gridColumns(stage.tiles.length);
@@ -10543,12 +11428,83 @@ function Toast({ text: text3, failed, onDone }) {
10543
11428
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: onDone, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, { width: 14, height: 14 }) })
10544
11429
  ] });
10545
11430
  }
11431
+ function MediaNotices() {
11432
+ const media = useLocalMedia();
11433
+ const { region, where, enableAudio, acceptOffer, dismissOffer, dismissNotice, retry } = useMediaSupervisor();
11434
+ const [fixing, setFixing] = (0, import_react10.useState)(null);
11435
+ const chips = ["microphone", "camera"].flatMap((kind) => {
11436
+ const reason = deviceUnavailableReason(media[kind], region.permission[kind]);
11437
+ return reason === null ? [] : [{ kind, reason }];
11438
+ });
11439
+ const fixReason = fixing === null ? null : chips.find((chip) => chip.kind === fixing)?.reason ?? null;
11440
+ const notice = region.notice;
11441
+ const offer = region.offer;
11442
+ const name = (kind) => kind === "microphone" ? "Microphone" : "Camera";
11443
+ if (chips.length === 0 && region.audioOutput !== "blocked" && notice === null && offer === null) return null;
11444
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-notices", children: [
11445
+ region.audioOutput === "blocked" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-pill", role: "alert", "data-meet-notice": "audio-blocked", children: [
11446
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "Sound is paused in this tab" }),
11447
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void enableAudio(), children: "Enable sound" })
11448
+ ] }) : null,
11449
+ chips.map(({ kind, reason }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-pill", role: "status", "data-meet-notice": deviceNoticeCode(kind, reason), children: [
11450
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: deviceChipLabel(kind, reason) }),
11451
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11452
+ "button",
11453
+ {
11454
+ type: "button",
11455
+ className: "mx-meet__primary mx-meet__primary--small",
11456
+ "aria-label": kind === "microphone" ? "Fix microphone" : "Fix camera",
11457
+ "aria-expanded": fixing === kind,
11458
+ onClick: () => setFixing(fixing === kind ? null : kind),
11459
+ children: "Fix"
11460
+ }
11461
+ )
11462
+ ] }, kind)),
11463
+ fixing !== null && fixReason !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__popover mx-meet__fix-sheet", role: "dialog", "aria-label": `Fix ${name(fixing).toLowerCase()}`, children: [
11464
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ol", { className: "mx-meet__fix-steps", children: deviceFixSteps(fixing, fixReason, where).map((step) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("li", { children: step }, step)) }),
11465
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__fix-actions", children: [
11466
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11467
+ "button",
11468
+ {
11469
+ type: "button",
11470
+ className: "mx-meet__primary mx-meet__primary--small",
11471
+ onClick: () => {
11472
+ const kind = fixing;
11473
+ setFixing(null);
11474
+ void retry(kind);
11475
+ },
11476
+ children: "Try again"
11477
+ }
11478
+ ),
11479
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Close", onClick: () => setFixing(null), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, { width: 14, height: 14 }) })
11480
+ ] })
11481
+ ] }) : null,
11482
+ notice !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11483
+ "div",
11484
+ {
11485
+ className: "mx-meet__media-pill",
11486
+ role: "status",
11487
+ ...notice.outcome === "lost" ? { "data-meet-notice": "device-lost" } : {},
11488
+ children: [
11489
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: notice.outcome === "switched" ? `${notice.from || name(notice.kind)} disconnected \xB7 now using ${notice.to || "the default"}` : `${notice.from || name(notice.kind)} disconnected` }),
11490
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismissNotice, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, { width: 14, height: 14 }) })
11491
+ ]
11492
+ }
11493
+ ) : null,
11494
+ offer !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-pill", role: "status", children: [
11495
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: `New ${offer.kind}: ${offer.label || name(offer.kind)}` }),
11496
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void acceptOffer(), children: "Use it" }),
11497
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismissOffer, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, { width: 14, height: 14 }) })
11498
+ ] }) : null
11499
+ ] });
11500
+ }
10546
11501
  function MeetingOverlays() {
10547
11502
  const reactions = useReactions();
10548
11503
  const { notice, dismiss } = useModerationNotice();
10549
11504
  const room = useRoom();
10550
11505
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
10551
11506
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ReactionOverlay, { reactions }),
11507
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MediaNotices, {}),
10552
11508
  notice !== null && notice.kind !== "removed" ? (
10553
11509
  // A host's request ("the host is asking you to unmute"), not an error:
10554
11510
  // an assertive status, so it is announced at once without being filed
@@ -11830,7 +12786,7 @@ function MeetSkinRoom({
11830
12786
  headerControls !== void 0 && headerControls !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__header-controls", "data-meet-header-controls": "", children: headerControls }) : null,
11831
12787
  isHost ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HostMenu, {}) : null
11832
12788
  ] }),
11833
- core.connection !== "stable" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__banner", role: "status", children: "Reconnecting\u2026" }) : null,
12789
+ core.connectionBanner && core.connection !== "stable" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": `connection-${core.connection}`, children: CONNECTION_BANNER[core.connection] }) : null,
11834
12790
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ShareNotice, {}),
11835
12791
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Stage, {}),
11836
12792
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Captions, {}),
@@ -11864,7 +12820,7 @@ var REFUSED_TITLES = {
11864
12820
  registration_required: "Register to join this meeting"
11865
12821
  };
11866
12822
  function ExitScreen({ onLeave }) {
11867
- const { phase, reason, host, snapshot, actions, call } = useMeetCore();
12823
+ const { phase, reason, host, snapshot, actions, call, connection } = useMeetCore();
11868
12824
  const [busy, setBusy] = (0, import_react10.useState)(false);
11869
12825
  const failure = snapshot?.failure ?? null;
11870
12826
  const rejoin = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -11920,7 +12876,7 @@ function ExitScreen({ onLeave }) {
11920
12876
  break;
11921
12877
  case "disconnected":
11922
12878
  title = "You lost your connection to the meeting";
11923
- line = failure?.remedy ?? "Check your internet connection, then rejoin.";
12879
+ 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.";
11924
12880
  notice = "disconnected";
11925
12881
  actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11926
12882
  rejoin,
@@ -12529,6 +13485,7 @@ function MeetingSkinRootBody({
12529
13485
  host.store.dispatch({ type: "reset" });
12530
13486
  }
12531
13487
  if (host.store.snapshot().phase !== "resolving") return;
13488
+ if (given?.linkRun?.reason == null && host.engine.resume(roomName)) return;
12532
13489
  const reason = given?.linkRun?.reason ?? null;
12533
13490
  if (reason !== null && given?.linkRun !== void 0) {
12534
13491
  const words = linkRunWords(given.linkRun);