@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/index.cjs CHANGED
@@ -36,6 +36,7 @@ __export(src_exports, {
36
36
  CAPTION_LANGUAGES: () => CAPTION_LANGUAGES,
37
37
  CONSENT_NOTICE_ATTRIBUTE: () => CONSENT_NOTICE_ATTRIBUTE,
38
38
  DATA_TOPICS: () => DATA_TOPICS,
39
+ DEFAULT_CONNECTION_POLICY: () => DEFAULT_CONNECTION_POLICY,
39
40
  DEFAULT_EFFECTS: () => DEFAULT_EFFECTS,
40
41
  DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
41
42
  EMPTY_COLLAB: () => EMPTY_COLLAB,
@@ -46,10 +47,12 @@ __export(src_exports, {
46
47
  GUEST_RECORD_ACCESS_METADATA_KEY: () => GUEST_RECORD_ACCESS_METADATA_KEY,
47
48
  GUEST_RECORD_NOT_SHARED: () => GUEST_RECORD_NOT_SHARED,
48
49
  IDLE_MEDIA: () => IDLE_MEDIA,
50
+ IDLE_MEDIA_REGION: () => IDLE_MEDIA_REGION,
49
51
  IDLE_PHASES: () => IDLE_PHASES,
50
52
  IDLE_TRACK: () => IDLE_TRACK,
51
53
  JOIN_REFUSAL_REASONS: () => JOIN_REFUSAL_REASONS,
52
54
  LISTEN_ONLY_NOTICE: () => LISTEN_ONLY_NOTICE,
55
+ LIVEKIT_BASE_DELAYS_MS: () => LIVEKIT_BASE_DELAYS_MS,
53
56
  LIVE_PHASES: () => LIVE_PHASES,
54
57
  MEETING_INVITE_KIND: () => MEETING_INVITE_KIND,
55
58
  MEETING_INVITE_VERSION: () => MEETING_INVITE_VERSION,
@@ -73,6 +76,7 @@ __export(src_exports, {
73
76
  PARTICIPANT_STANDING_ATTRIBUTE: () => PARTICIPANT_STANDING_ATTRIBUTE,
74
77
  REACTION_TTL_MS: () => REACTION_TTL_MS,
75
78
  REFUSED_ABSENCE: () => REFUSED_ABSENCE,
79
+ RESUME_WINDOW_MS: () => RESUME_WINDOW_MS,
76
80
  ROLE_RANK: () => ROLE_RANK,
77
81
  ROOM_EVENTS: () => ROOM_EVENTS,
78
82
  ROOM_EVENT_PREFIX: () => ROOM_EVENT_PREFIX,
@@ -102,6 +106,7 @@ __export(src_exports, {
102
106
  attendanceTotals: () => attendanceTotals,
103
107
  attendeeKey: () => attendeeKey,
104
108
  autoAssign: () => autoAssign,
109
+ autoRejoinDelayMs: () => autoRejoinDelayMs,
105
110
  availableActivities: () => availableActivities,
106
111
  breakoutRemainingMs: () => breakoutRemainingMs,
107
112
  callInviteAction: () => callInviteAction,
@@ -113,15 +118,19 @@ __export(src_exports, {
113
118
  captionSegment: () => captionSegment,
114
119
  captureHeldOf: () => captureHeldOf,
115
120
  changesPatch: () => changesPatch,
121
+ classifyDeviceFailure: () => classifyDeviceFailure,
116
122
  clockPosition: () => clockPosition,
117
123
  collabFromState: () => collabFromState,
118
124
  computeStage: () => computeStage,
125
+ connectionPolicyFrom: () => connectionPolicyFrom,
119
126
  countLabel: () => countLabel,
120
127
  createCallCenter: () => createCallCenter,
121
128
  createCallInviteHandler: () => createCallInviteHandler,
129
+ createConnectionSupervisor: () => createConnectionSupervisor,
122
130
  createDeviceManager: () => createDeviceManager,
123
131
  createInRoomClient: () => createInRoomClient,
124
132
  createLiveKitDriver: () => createLiveKitDriver,
133
+ createMediaSupervisor: () => createMediaSupervisor,
125
134
  createMeetAi: () => createMeetAi,
126
135
  createMeetApi: () => createMeetApi,
127
136
  createMeetRepository: () => createMeetRepository,
@@ -130,13 +139,23 @@ __export(src_exports, {
130
139
  createMeetingRecordCache: () => createMeetingRecordCache,
131
140
  createMeetingSession: () => createMeetingSession,
132
141
  createMemoryDeviceStorage: () => createMemoryDeviceStorage,
142
+ createReachabilityProbe: () => createReachabilityProbe,
143
+ createReconnectPolicy: () => createReconnectPolicy,
133
144
  createRecordingController: () => createRecordingController,
134
145
  createRoomEngine: () => createRoomEngine,
135
146
  createTokenClient: () => createTokenClient,
136
147
  createWebDeviceStorage: () => createWebDeviceStorage,
137
148
  createWebEffectsStorage: () => createWebEffectsStorage,
149
+ createWebNetworkPort: () => createWebNetworkPort,
150
+ createWebResumeStorage: () => createWebResumeStorage,
138
151
  createWebRoomTokenStorage: () => createWebRoomTokenStorage,
139
152
  defaultCaptionLanguage: () => defaultCaptionLanguage,
153
+ detectBrowser: () => detectBrowser,
154
+ detectOs: () => detectOs,
155
+ deviceChipLabel: () => deviceChipLabel,
156
+ deviceFixSteps: () => deviceFixSteps,
157
+ deviceNoticeCode: () => deviceNoticeCode,
158
+ deviceUnavailableReason: () => deviceUnavailableReason,
140
159
  displayNameFor: () => displayNameFor,
141
160
  fileDownloadPath: () => fileDownloadPath,
142
161
  focusFromRow: () => focusFromRow,
@@ -147,6 +166,7 @@ __export(src_exports, {
147
166
  guestRecordAccessSetting: () => guestRecordAccessSetting,
148
167
  hasEnded: () => hasEnded,
149
168
  hostUserIds: () => hostUserIds,
169
+ httpUrlOf: () => httpUrlOf,
150
170
  invalidResponse: () => invalidResponse,
151
171
  isGuest: () => isGuest,
152
172
  isGuestIdentity: () => isGuestIdentity,
@@ -160,6 +180,7 @@ __export(src_exports, {
160
180
  isRealName: () => isRealName,
161
181
  isRefusedPhase: () => isRefusedPhase,
162
182
  isRoomEventTopic: () => isRoomEventTopic,
183
+ isUnannouncedDeparture: () => isUnannouncedDeparture,
163
184
  isWaitingPhase: () => isWaitingPhase,
164
185
  learnName: () => learnName,
165
186
  learnNames: () => learnNames,
@@ -181,6 +202,7 @@ __export(src_exports, {
181
202
  meetingTopic: () => meetingTopic,
182
203
  meetingWhen: () => meetingWhen,
183
204
  mergeCaption: () => mergeCaption,
205
+ mergeGhosts: () => mergeGhosts,
184
206
  mergeRecord: () => mergeRecord,
185
207
  microphoneNoticeCode: () => microphoneNoticeCode,
186
208
  moveToRoom: () => moveToRoom,
@@ -245,6 +267,7 @@ __export(src_exports, {
245
267
  untranslatedLines: () => untranslatedLines,
246
268
  userTopic: () => userTopic,
247
269
  wasRefused: () => wasRefused,
270
+ webMediaPlatform: () => webMediaPlatform,
248
271
  withItem: () => withItem,
249
272
  wrapUpStatus: () => wrapUpStatus
250
273
  });
@@ -456,6 +479,352 @@ function invalidResponse(operation, detail) {
456
479
  );
457
480
  }
458
481
 
482
+ // src/core/connection-supervisor.ts
483
+ var DEFAULT_CONNECTION_POLICY = {
484
+ bannerDelayMs: 2e3,
485
+ autoRejoin: true,
486
+ autoRejoinMaxAttempts: 5,
487
+ reconnectGraceSeconds: 30
488
+ };
489
+ function clampInt(value, min, max, fallback) {
490
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
491
+ return Math.min(max, Math.max(min, Math.round(value)));
492
+ }
493
+ function connectionPolicyFrom(raw) {
494
+ if (typeof raw !== "object" || raw === null) return DEFAULT_CONNECTION_POLICY;
495
+ const row = raw;
496
+ const d = DEFAULT_CONNECTION_POLICY;
497
+ return {
498
+ bannerDelayMs: clampInt(row["reconnect_banner_delay_ms"], 0, 1e4, d.bannerDelayMs),
499
+ autoRejoin: typeof row["auto_rejoin"] === "boolean" ? row["auto_rejoin"] : d.autoRejoin,
500
+ autoRejoinMaxAttempts: clampInt(row["auto_rejoin_max_attempts"], 0, 20, d.autoRejoinMaxAttempts),
501
+ reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds)
502
+ };
503
+ }
504
+ var LIVEKIT_BASE_DELAYS_MS = [0, 300, 1200, 2700, 4800, 7e3];
505
+ function createReconnectPolicy(options = {}) {
506
+ const delays = options.delaysMs !== void 0 && options.delaysMs.length > 0 ? options.delaysMs : LIVEKIT_BASE_DELAYS_MS;
507
+ const giveUpMs = options.giveUpMs ?? 45e3;
508
+ const maxAttempts = options.maxAttempts ?? 12;
509
+ const random = options.random ?? Math.random;
510
+ return {
511
+ nextRetryDelayInMs({ retryCount, elapsedMs }) {
512
+ if (retryCount >= maxAttempts || elapsedMs >= giveUpMs) return null;
513
+ const base = delays[Math.min(retryCount, delays.length - 1)] ?? 0;
514
+ const spread = Math.min(2e3, Math.max(500, base / 2));
515
+ return Math.round(base + random() * spread);
516
+ }
517
+ };
518
+ }
519
+ function createWebNetworkPort() {
520
+ const scope = globalThis;
521
+ return {
522
+ online: () => scope.navigator?.onLine !== false,
523
+ subscribe(listener) {
524
+ if (typeof scope.addEventListener !== "function") return () => void 0;
525
+ const on = () => listener(true);
526
+ const off = () => listener(false);
527
+ scope.addEventListener("online", on);
528
+ scope.addEventListener("offline", off);
529
+ return () => {
530
+ scope.removeEventListener?.("online", on);
531
+ scope.removeEventListener?.("offline", off);
532
+ };
533
+ }
534
+ };
535
+ }
536
+ function createReachabilityProbe(urls, timeoutMs = 4e3) {
537
+ return async () => {
538
+ const targets = urls().filter((url) => url.length > 0);
539
+ if (targets.length === 0 || typeof fetch !== "function") return true;
540
+ const one = async (url) => {
541
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
542
+ const timer = setTimeout(() => controller?.abort(), timeoutMs);
543
+ try {
544
+ await fetch(url, {
545
+ method: "GET",
546
+ mode: "no-cors",
547
+ cache: "no-store",
548
+ credentials: "omit",
549
+ ...controller !== null ? { signal: controller.signal } : {}
550
+ });
551
+ return true;
552
+ } catch {
553
+ return false;
554
+ } finally {
555
+ clearTimeout(timer);
556
+ }
557
+ };
558
+ const answers = await Promise.all(targets.map(one));
559
+ return answers.every(Boolean);
560
+ };
561
+ }
562
+ function httpUrlOf(serverUrl) {
563
+ return serverUrl.replace(/^ws(s?):\/\//i, "http$1://");
564
+ }
565
+ var RESUME_WINDOW_MS = 15 * 6e4;
566
+ var RESUME_KEY = "mx-meet:resume:";
567
+ function createWebResumeStorage() {
568
+ const storage = globalThis.sessionStorage;
569
+ if (storage === void 0) return void 0;
570
+ return {
571
+ load(roomName) {
572
+ try {
573
+ const raw = storage.getItem(RESUME_KEY + roomName);
574
+ if (raw === null) return null;
575
+ const parsed = JSON.parse(raw);
576
+ return typeof parsed?.at === "number" && typeof parsed.request?.roomName === "string" ? parsed : null;
577
+ } catch {
578
+ return null;
579
+ }
580
+ },
581
+ save(roomName, marker) {
582
+ try {
583
+ storage.setItem(RESUME_KEY + roomName, JSON.stringify(marker));
584
+ } catch {
585
+ }
586
+ },
587
+ clear(roomName) {
588
+ try {
589
+ storage.removeItem(RESUME_KEY + roomName);
590
+ } catch {
591
+ }
592
+ }
593
+ };
594
+ }
595
+ var CLEARS_RESUME = (phase) => phase === "left" || phase === "removed" || phase === "superseded" || phase === "inactive" || phase === "ended" || phase === "record" || phase === "prejoin" || phase.startsWith("refused:") || phase.startsWith("waiting:");
596
+ function autoRejoinDelayMs(attempt, random = Math.random) {
597
+ const base = Math.min(15e3, 1e3 * 2 ** Math.max(0, attempt));
598
+ return Math.round(base * (0.5 + random()));
599
+ }
600
+ function createConnectionSupervisor(options) {
601
+ const { store, network } = options;
602
+ const now = options.now ?? (() => Date.now());
603
+ const random = options.random ?? Math.random;
604
+ const probeEveryMs = options.probeEveryMs ?? 3e3;
605
+ let driverRegion = null;
606
+ let probeTimer = null;
607
+ let probing = false;
608
+ let bannerTimer = null;
609
+ let rejoinTimer = null;
610
+ let rejoinAttempts = 0;
611
+ let lastPhase = store.snapshot().phase;
612
+ let lastSavedAt = 0;
613
+ let disposed = false;
614
+ const inCall = () => store.snapshot().phase === "in_call";
615
+ const dropped = () => store.snapshot().phase === "disconnected";
616
+ function setRegion(next) {
617
+ if (store.snapshot().connection !== next) store.patch({ connection: next });
618
+ }
619
+ function regionFor(reachable) {
620
+ if (!network.online()) return "offline";
621
+ if (reachable === false) return "server_unreachable";
622
+ if (dropped()) return reachable === true ? "stable" : store.snapshot().connection;
623
+ return driverRegion ?? "stable";
624
+ }
625
+ function needsWatch() {
626
+ const snap = store.snapshot();
627
+ if (dropped()) return true;
628
+ return inCall() && (driverRegion !== null || snap.connection !== "stable");
629
+ }
630
+ function stopProbe() {
631
+ if (probeTimer !== null) clearTimeout(probeTimer);
632
+ probeTimer = null;
633
+ }
634
+ function scheduleProbe(delayMs) {
635
+ if (disposed || probeTimer !== null) return;
636
+ probeTimer = setTimeout(() => {
637
+ probeTimer = null;
638
+ void runProbe();
639
+ }, delayMs);
640
+ }
641
+ async function runProbe() {
642
+ if (disposed || probing) return;
643
+ if (!needsWatch()) return;
644
+ if (!network.online()) {
645
+ setRegion("offline");
646
+ scheduleProbe(probeEveryMs);
647
+ return;
648
+ }
649
+ probing = true;
650
+ let reachable = false;
651
+ try {
652
+ reachable = await options.probe();
653
+ } catch {
654
+ reachable = false;
655
+ } finally {
656
+ probing = false;
657
+ }
658
+ if (disposed) return;
659
+ if (inCall() || dropped()) setRegion(regionFor(reachable));
660
+ if (dropped() && reachable) maybeRejoin();
661
+ if (needsWatch()) scheduleProbe(Math.round(probeEveryMs * (0.75 + random() * 0.5)));
662
+ }
663
+ function syncBanner() {
664
+ const snap = store.snapshot();
665
+ const unstable = snap.phase === "in_call" && snap.connection !== "stable";
666
+ if (!unstable) {
667
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
668
+ bannerTimer = null;
669
+ if (snap.connectionBanner) store.patch({ connectionBanner: false });
670
+ return;
671
+ }
672
+ if (snap.connectionBanner || bannerTimer !== null) return;
673
+ const delay = options.policy().bannerDelayMs;
674
+ if (delay <= 0) {
675
+ store.patch({ connectionBanner: true });
676
+ return;
677
+ }
678
+ bannerTimer = setTimeout(() => {
679
+ bannerTimer = null;
680
+ const latest = store.snapshot();
681
+ if (latest.phase === "in_call" && latest.connection !== "stable" && !latest.connectionBanner) {
682
+ store.patch({ connectionBanner: true });
683
+ }
684
+ }, delay);
685
+ }
686
+ function stopRejoin() {
687
+ if (rejoinTimer !== null) clearTimeout(rejoinTimer);
688
+ rejoinTimer = null;
689
+ }
690
+ function maybeRejoin() {
691
+ if (disposed || rejoinTimer !== null || !dropped()) return;
692
+ const policy = options.policy();
693
+ if (!policy.autoRejoin) return;
694
+ if (rejoinAttempts >= policy.autoRejoinMaxAttempts) return;
695
+ if (!network.online()) return;
696
+ const attempt = rejoinAttempts;
697
+ rejoinTimer = setTimeout(() => {
698
+ rejoinTimer = null;
699
+ if (disposed || !dropped() || !network.online()) return;
700
+ rejoinAttempts = attempt + 1;
701
+ options.report?.("info", `meet: rejoining by itself (attempt ${rejoinAttempts} of ${policy.autoRejoinMaxAttempts})`);
702
+ void options.autoRejoin().catch(() => void 0);
703
+ }, autoRejoinDelayMs(attempt, random));
704
+ }
705
+ function saveMarker(force) {
706
+ const resume = options.resume;
707
+ const snap = store.snapshot();
708
+ if (resume === void 0 || snap.roomName === null) return;
709
+ const at = now();
710
+ if (!force && at - lastSavedAt < 1e4) return;
711
+ const request = options.currentRequest();
712
+ if (request === null) return;
713
+ lastSavedAt = at;
714
+ resume.save(snap.roomName, { request, at });
715
+ }
716
+ const unsubscribeStore = store.subscribe(() => {
717
+ if (disposed) return;
718
+ const snap = store.snapshot();
719
+ const phase = snap.phase;
720
+ const changed = phase !== lastPhase;
721
+ lastPhase = phase;
722
+ if (changed) {
723
+ if (phase === "in_call") {
724
+ rejoinAttempts = 0;
725
+ stopRejoin();
726
+ saveMarker(true);
727
+ } else if (phase === "disconnected") {
728
+ driverRegion = null;
729
+ saveMarker(true);
730
+ stopProbe();
731
+ scheduleProbe(0);
732
+ } else {
733
+ stopRejoin();
734
+ if (phase !== "joining") driverRegion = null;
735
+ if (CLEARS_RESUME(phase) && snap.roomName !== null) options.resume?.clear(snap.roomName);
736
+ }
737
+ } else if (phase === "in_call" || phase === "disconnected") {
738
+ saveMarker(false);
739
+ }
740
+ syncBanner();
741
+ });
742
+ const unsubscribeNetwork = network.subscribe((online) => {
743
+ if (disposed) return;
744
+ if (!online) {
745
+ if (inCall() || dropped()) setRegion("offline");
746
+ stopRejoin();
747
+ stopProbe();
748
+ if (needsWatch()) scheduleProbe(probeEveryMs);
749
+ return;
750
+ }
751
+ stopProbe();
752
+ if (needsWatch()) scheduleProbe(0);
753
+ });
754
+ return {
755
+ driverState(state) {
756
+ if (disposed) return;
757
+ if (state === "connected") {
758
+ driverRegion = null;
759
+ if (inCall() || store.snapshot().phase === "joining") setRegion(network.online() ? "stable" : "offline");
760
+ if (!needsWatch()) stopProbe();
761
+ return;
762
+ }
763
+ if (state === "reconnecting" || state === "signal_reconnecting") {
764
+ driverRegion = state;
765
+ if (inCall()) {
766
+ setRegion(network.online() ? state : "offline");
767
+ stopProbe();
768
+ scheduleProbe(0);
769
+ }
770
+ }
771
+ },
772
+ exitConnection() {
773
+ return network.online() ? "server_unreachable" : "offline";
774
+ },
775
+ resumable(roomName) {
776
+ const resume = options.resume;
777
+ if (resume === void 0) return null;
778
+ const marker = resume.load(roomName);
779
+ if (marker === null) return null;
780
+ if (now() - marker.at > RESUME_WINDOW_MS) {
781
+ resume.clear(roomName);
782
+ return null;
783
+ }
784
+ return marker;
785
+ },
786
+ dispose() {
787
+ disposed = true;
788
+ stopProbe();
789
+ stopRejoin();
790
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
791
+ bannerTimer = null;
792
+ unsubscribeStore();
793
+ unsubscribeNetwork();
794
+ }
795
+ };
796
+ }
797
+ var DELIBERATE_DEPARTURES = /* @__PURE__ */ new Set([
798
+ "CLIENT_INITIATED",
799
+ "PARTICIPANT_REMOVED",
800
+ "ROOM_DELETED",
801
+ "ROOM_CLOSED",
802
+ "DUPLICATE_IDENTITY",
803
+ "USER_REJECTED",
804
+ "USER_UNAVAILABLE",
805
+ "SIP_TRUNK_FAILURE"
806
+ ]);
807
+ function isUnannouncedDeparture(reason) {
808
+ return reason === null || !DELIBERATE_DEPARTURES.has(reason);
809
+ }
810
+ function mergeGhosts(live, ghosts, at) {
811
+ const present = new Set(live.map((p) => p.identity));
812
+ const kept = /* @__PURE__ */ new Map();
813
+ const extra = [];
814
+ for (const [identity, ghost] of ghosts) {
815
+ if (present.has(identity) || ghost.until <= at) continue;
816
+ kept.set(identity, ghost);
817
+ extra.push({
818
+ ...ghost.participant,
819
+ micEnabled: false,
820
+ cameraEnabled: false,
821
+ screenSharing: false,
822
+ reconnecting: true
823
+ });
824
+ }
825
+ return { roster: [...live, ...extra], kept };
826
+ }
827
+
459
828
  // src/core/tokens.ts
460
829
  function createWebRoomTokenStorage(onDiagnostic) {
461
830
  const store = globalThis.localStorage;
@@ -641,7 +1010,8 @@ function parseRoomToken(body, roomName) {
641
1010
  admissionState: readString(payload, "admission_state") ?? void 0,
642
1011
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
643
1012
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
644
- observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone"
1013
+ observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
1014
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
645
1015
  };
646
1016
  }
647
1017
  function createTokenClient(options) {
@@ -1371,6 +1741,10 @@ function nextPhase(from, event) {
1371
1741
  return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
1372
1742
  case "join":
1373
1743
  return JOINABLE_FROM.has(from) ? "joining" : null;
1744
+ case "auto_rejoin":
1745
+ return from === "disconnected" ? "joining" : null;
1746
+ case "resumed":
1747
+ return from === "resolving" ? "joining" : null;
1374
1748
  case "back_to_prejoin":
1375
1749
  return from === "left" || from === "disconnected" ? "prejoin" : null;
1376
1750
  case "retry":
@@ -3248,13 +3622,19 @@ function recordingLabel(state) {
3248
3622
  }
3249
3623
 
3250
3624
  // src/core/attendance.ts
3625
+ var IDLE_MEDIA_REGION = {
3626
+ permission: { microphone: "unknown", camera: "unknown" },
3627
+ audioOutput: "ok",
3628
+ notice: null,
3629
+ offer: null
3630
+ };
3251
3631
  var IDLE_TRACK = { enabled: false, unavailable: null };
3252
3632
  var IDLE_MEDIA = {
3253
3633
  microphone: IDLE_TRACK,
3254
3634
  camera: IDLE_TRACK
3255
3635
  };
3256
- function unavailableFrom(error) {
3257
- return { code: error.code, message: error.message, remedy: error.remedy };
3636
+ function unavailableFrom(error, reason) {
3637
+ return reason === void 0 ? { code: error.code, message: error.message, remedy: error.remedy } : { code: error.code, message: error.message, remedy: error.remedy, reason };
3258
3638
  }
3259
3639
  var LISTEN_ONLY_NOTICE = "You're attending without a microphone \u2014 others can't hear you";
3260
3640
  function isListenOnly(media) {
@@ -3284,6 +3664,7 @@ var EMPTY = {
3284
3664
  phaseNotice: null,
3285
3665
  roomName: null,
3286
3666
  connection: "stable",
3667
+ connectionBanner: false,
3287
3668
  share: NO_SHARE,
3288
3669
  call: null,
3289
3670
  waiting: null,
@@ -3315,6 +3696,7 @@ var EMPTY = {
3315
3696
  wrapUp: null,
3316
3697
  noteTakerRefusal: null,
3317
3698
  media: IDLE_MEDIA,
3699
+ mediaRegion: IDLE_MEDIA_REGION,
3318
3700
  failure: null,
3319
3701
  collab: EMPTY_COLLAB,
3320
3702
  breakoutRoomId: null
@@ -3532,7 +3914,7 @@ function createMeetStore(initial = {}) {
3532
3914
  },
3533
3915
  setLocalTrack(kind, next) {
3534
3916
  const current = state.media[kind];
3535
- if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.message === next.unavailable?.message) {
3917
+ if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.reason === next.unavailable?.reason && current.unavailable?.message === next.unavailable?.message) {
3536
3918
  return;
3537
3919
  }
3538
3920
  commit({ ...state, media: { ...state.media, [kind]: next } });
@@ -4238,8 +4620,11 @@ function mapConnectionState(state) {
4238
4620
  case "connecting":
4239
4621
  return "connecting";
4240
4622
  case "reconnecting":
4241
- case "signalReconnecting":
4242
4623
  return "reconnecting";
4624
+ // Only the signal socket is down (S6b): its own region value, so the supervisor can tell a
4625
+ // signal blip from a full media reconnect.
4626
+ case "signalReconnecting":
4627
+ return "signal_reconnecting";
4243
4628
  default:
4244
4629
  return "disconnected";
4245
4630
  }
@@ -4320,8 +4705,12 @@ function createLiveKitDriver(handlers, options = {}) {
4320
4705
  // R5: large meetings degrade gracefully instead of melting a laptop.
4321
4706
  adaptiveStream: true,
4322
4707
  dynacast: true,
4708
+ // S6b: OUR retry schedule — jittered from attempt 0 (the SDK's default has no jitter on its
4709
+ // first two attempts), give-up limits in one place (`connection-supervisor.ts`).
4710
+ reconnectPolicy: createReconnectPolicy(),
4323
4711
  ...options.roomOptions
4324
4712
  });
4713
+ const reasonName = (reason) => reason === void 0 || reason === null ? null : module2.DisconnectReason?.[reason] ?? String(reason);
4325
4714
  const roster = () => {
4326
4715
  const local = snapshotParticipant(created.localParticipant, true);
4327
4716
  const remotes = [...created.remoteParticipants.values()].map(
@@ -4339,8 +4728,17 @@ function createLiveKitDriver(handlers, options = {}) {
4339
4728
  if (track.kind !== Track.Kind.Audio) return;
4340
4729
  for (const element of track.detach()) element.remove();
4341
4730
  });
4342
- 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) => {
4731
+ created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
4732
+ handlers.participantLeft?.(participant.identity, reasonName(reason));
4733
+ roster();
4734
+ }).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) => {
4343
4735
  handlers.roomMetadata?.(metadata ?? "");
4736
+ }).on(RoomEvent.AudioPlaybackStatusChanged, (canPlay) => {
4737
+ handlers.audioPlayback?.(canPlay);
4738
+ }).on(RoomEvent.MediaDevicesChanged, () => {
4739
+ handlers.devicesChanged?.();
4740
+ }).on(RoomEvent.MediaDevicesError, (error, kind) => {
4741
+ handlers.mediaDeviceError?.(error, kind === "audioinput" || kind === "videoinput" ? kind : null);
4344
4742
  }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
4345
4743
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
4346
4744
  }).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
@@ -4356,8 +4754,7 @@ function createLiveKitDriver(handlers, options = {}) {
4356
4754
  }).on(RoomEvent.Disconnected, (reason) => {
4357
4755
  audioContainer?.remove();
4358
4756
  audioContainer = null;
4359
- const named = reason === void 0 ? null : module2?.DisconnectReason?.[reason] ?? String(reason);
4360
- handlers.disconnected(named);
4757
+ handlers.disconnected(reasonName(reason));
4361
4758
  });
4362
4759
  try {
4363
4760
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -4438,6 +4835,7 @@ function createLiveKitDriver(handlers, options = {}) {
4438
4835
  }
4439
4836
  projectRoster?.();
4440
4837
  handlers.roomMetadata?.(target.metadata ?? "");
4838
+ handlers.audioPlayback?.(target.canPlaybackAudio);
4441
4839
  },
4442
4840
  async disconnect() {
4443
4841
  if (room === null) return;
@@ -4564,6 +4962,11 @@ function createLiveKitDriver(handlers, options = {}) {
4564
4962
  } catch (error) {
4565
4963
  throw normalizeDeviceError(error, "meet.switchDevice");
4566
4964
  }
4965
+ },
4966
+ async startAudio() {
4967
+ const target = room;
4968
+ if (target === null) return;
4969
+ await target.startAudio();
4567
4970
  }
4568
4971
  };
4569
4972
  }
@@ -5659,6 +6062,342 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
5659
6062
  return await response.json();
5660
6063
  }
5661
6064
 
6065
+ // src/core/media-supervisor.ts
6066
+ function classifyDeviceFailure(error, permission) {
6067
+ if (permission === "denied") return "denied";
6068
+ switch (error.code) {
6069
+ case "permission-denied":
6070
+ if (/by system/i.test(error.message) || permission === "granted") return "blocked_os";
6071
+ return "denied";
6072
+ case "device-not-found":
6073
+ return "none";
6074
+ case "device-in-use":
6075
+ return "busy";
6076
+ case "insecure-context":
6077
+ return "blocked_site";
6078
+ default:
6079
+ return "failed";
6080
+ }
6081
+ }
6082
+ function deviceNoticeCode(kind, reason) {
6083
+ if (reason === "denied" || reason === "blocked_site" || reason === "blocked_os") {
6084
+ return kind === "microphone" ? "mic-blocked" : "camera-blocked";
6085
+ }
6086
+ return "device-lost";
6087
+ }
6088
+ function deviceUnavailableReason(track, permission) {
6089
+ if (track.enabled) return null;
6090
+ if (permission === "denied") return "denied";
6091
+ const refused = track.unavailable;
6092
+ if (refused === null) return null;
6093
+ return refused.reason ?? classifyDeviceFailure({ code: refused.code, message: refused.message }, permission);
6094
+ }
6095
+ function deviceChipLabel(kind, reason) {
6096
+ const what = kind === "microphone" ? "Mic" : "Camera";
6097
+ switch (reason) {
6098
+ case "denied":
6099
+ case "blocked_site":
6100
+ case "blocked_os":
6101
+ return `${what} blocked`;
6102
+ case "none":
6103
+ return kind === "microphone" ? "No microphone found" : "No camera found";
6104
+ case "busy":
6105
+ return `${what} in use by another app`;
6106
+ case "revoked":
6107
+ return `${what} access was turned off`;
6108
+ default:
6109
+ return `${what} didn't start`;
6110
+ }
6111
+ }
6112
+ function deviceFixSteps(kind, reason, where) {
6113
+ const device = kind === "microphone" ? "Microphone" : "Camera";
6114
+ const lower = device.toLowerCase();
6115
+ switch (reason) {
6116
+ case "denied":
6117
+ case "revoked":
6118
+ case "blocked_site":
6119
+ switch (where.browser) {
6120
+ case "firefox":
6121
+ return [`Click the crossed-out ${lower} icon in the address bar`, "Clear the Blocked setting", "Press Try again and choose Allow"];
6122
+ case "safari":
6123
+ 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"];
6124
+ default:
6125
+ return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
6126
+ }
6127
+ case "blocked_os": {
6128
+ const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
6129
+ if (where.os === "mac") return [`System Settings \u203A Privacy & Security \u203A ${device}`, `Turn on ${app}`, "Reopen the browser if asked, then Try again"];
6130
+ if (where.os === "windows") return [`Settings \u203A Privacy & security \u203A ${device}`, `Turn on ${device} access for apps`, "Press Try again"];
6131
+ return [`Allow ${lower} access for ${app} in your system settings`, "Press Try again"];
6132
+ }
6133
+ case "busy":
6134
+ return [`Close other apps using the ${lower} (Zoom, Teams, another tab)`, "Press Try again"];
6135
+ case "none":
6136
+ return [`Connect a ${lower}`, "Press Try again"];
6137
+ default:
6138
+ return [`Pick another ${lower} in Settings`, "Press Try again"];
6139
+ }
6140
+ }
6141
+ function detectBrowser(userAgent) {
6142
+ if (/Edg\//.test(userAgent)) return "edge";
6143
+ if (/Firefox\//.test(userAgent)) return "firefox";
6144
+ if (/Chrome\/|Chromium\/|CriOS\//.test(userAgent)) return "chrome";
6145
+ if (/Safari\//.test(userAgent)) return "safari";
6146
+ return "other";
6147
+ }
6148
+ function detectOs(userAgent, maxTouchPoints = 0) {
6149
+ if (/iPhone|iPad|iPod/.test(userAgent)) return "ios";
6150
+ if (/Macintosh/.test(userAgent)) return maxTouchPoints > 1 ? "ios" : "mac";
6151
+ if (/Android/.test(userAgent)) return "android";
6152
+ if (/Windows/.test(userAgent)) return "windows";
6153
+ if (/Linux|CrOS/.test(userAgent)) return "linux";
6154
+ return "other";
6155
+ }
6156
+ function webMediaPlatform() {
6157
+ const scope = globalThis;
6158
+ const nav = scope.navigator;
6159
+ if (nav === void 0) return null;
6160
+ const ua = nav.userAgent ?? "";
6161
+ const reading = (state) => state === "granted" || state === "denied" || state === "prompt" ? state : "unknown";
6162
+ const query = async (kind) => {
6163
+ try {
6164
+ return await nav.permissions?.query({ name: kind }) ?? null;
6165
+ } catch {
6166
+ return null;
6167
+ }
6168
+ };
6169
+ return {
6170
+ browser: detectBrowser(ua),
6171
+ os: detectOs(ua, nav.maxTouchPoints ?? 0),
6172
+ async queryPermission(kind) {
6173
+ return reading((await query(kind))?.state);
6174
+ },
6175
+ watchPermission(kind, onChange) {
6176
+ let status = null;
6177
+ let stopped = false;
6178
+ void query(kind).then((found) => {
6179
+ if (stopped || found === null) return;
6180
+ status = found;
6181
+ found.onchange = () => onChange(reading(found.state));
6182
+ });
6183
+ return () => {
6184
+ stopped = true;
6185
+ if (status !== null) status.onchange = null;
6186
+ };
6187
+ },
6188
+ async listDevices() {
6189
+ try {
6190
+ return await nav.mediaDevices?.enumerateDevices() ?? [];
6191
+ } catch {
6192
+ return [];
6193
+ }
6194
+ },
6195
+ onVisible(callback) {
6196
+ const doc = scope.document;
6197
+ if (doc === void 0) return () => void 0;
6198
+ const handler = () => {
6199
+ if (doc.visibilityState === "visible") callback();
6200
+ };
6201
+ doc.addEventListener("visibilitychange", handler);
6202
+ return () => doc.removeEventListener("visibilitychange", handler);
6203
+ }
6204
+ };
6205
+ }
6206
+ var INPUT_KIND = { microphone: "audioinput", camera: "videoinput" };
6207
+ function isAlias(deviceId) {
6208
+ return deviceId === "default" || deviceId === "communications" || deviceId === "";
6209
+ }
6210
+ function createMediaSupervisor(options) {
6211
+ const { store, platform } = options;
6212
+ let unsubscribers = [];
6213
+ let started = false;
6214
+ let noticeId = 0;
6215
+ let known = null;
6216
+ let inUse = { audioinput: null, videoinput: null };
6217
+ function region() {
6218
+ return store.snapshot().mediaRegion;
6219
+ }
6220
+ function setRegion(next) {
6221
+ const current = region();
6222
+ const merged = { ...current, ...next };
6223
+ 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) {
6224
+ return;
6225
+ }
6226
+ store.patch({ mediaRegion: merged });
6227
+ }
6228
+ function setPermission(kind, reading) {
6229
+ const current = region().permission;
6230
+ if (current[kind] === reading) return;
6231
+ setRegion({ permission: { ...current, [kind]: reading } });
6232
+ const track = store.snapshot().media[kind];
6233
+ if (reading === "granted" && !track.enabled && track.unavailable?.reason === "denied") {
6234
+ store.setLocalTrack(kind, {
6235
+ enabled: false,
6236
+ unavailable: { ...track.unavailable, reason: "failed", remedy: "Access is allowed now. Press Try again." }
6237
+ });
6238
+ }
6239
+ }
6240
+ async function snapshotDevices() {
6241
+ const list = platform === null ? [] : await platform.listDevices();
6242
+ const out = { audioinput: /* @__PURE__ */ new Map(), videoinput: /* @__PURE__ */ new Map() };
6243
+ for (const device of list) {
6244
+ if (device.kind === "audioinput" || device.kind === "videoinput") out[device.kind].set(device.deviceId, device.label);
6245
+ }
6246
+ return out;
6247
+ }
6248
+ function readInUse() {
6249
+ const driver = options.driver();
6250
+ if (driver === null) return;
6251
+ const active = driver.activeDevices();
6252
+ inUse = { audioinput: active.audioinput, videoinput: active.videoinput };
6253
+ }
6254
+ async function handleDevicesChanged() {
6255
+ if (platform === null) return;
6256
+ const before = known;
6257
+ const now = await snapshotDevices();
6258
+ known = now;
6259
+ const driver = options.driver();
6260
+ if (driver === null || before === null) {
6261
+ readInUse();
6262
+ return;
6263
+ }
6264
+ for (const kind of ["microphone", "camera"]) {
6265
+ const deviceKind = INPUT_KIND[kind];
6266
+ const previous = inUse[deviceKind];
6267
+ const track = store.snapshot().media[kind];
6268
+ const gone = previous !== null && !isAlias(previous) && before[deviceKind].has(previous) && !now[deviceKind].has(previous);
6269
+ if (gone) {
6270
+ const from = before[deviceKind].get(previous) ?? "";
6271
+ const remaining = [...now[deviceKind].entries()].filter(([id]) => !isAlias(id));
6272
+ if (remaining.length === 0) {
6273
+ store.setLocalTrack(kind, {
6274
+ enabled: false,
6275
+ unavailable: {
6276
+ code: "device-not-found",
6277
+ message: `meet.${kind}: the ${kind} was disconnected and no other is connected`,
6278
+ remedy: `Connect a ${kind === "microphone" ? "microphone" : "camera"}, then press Try again.`,
6279
+ reason: "none"
6280
+ }
6281
+ });
6282
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "lost", from, to: null } });
6283
+ continue;
6284
+ }
6285
+ if (kind === "camera") {
6286
+ const [nextId, nextLabel] = remaining[0];
6287
+ try {
6288
+ await driver.switchDevice("videoinput", nextId);
6289
+ if (track.enabled || track.unavailable !== null) {
6290
+ await driver.setCameraEnabled(true);
6291
+ store.setLocalTrack("camera", { enabled: true, unavailable: null });
6292
+ }
6293
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "switched", from, to: nextLabel } });
6294
+ } catch (error) {
6295
+ const refused = await unavailable(error, "camera", "meet.camera(fallback)");
6296
+ store.setLocalTrack("camera", { enabled: false, unavailable: refused });
6297
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "lost", from, to: null } });
6298
+ }
6299
+ } else {
6300
+ const active = driver.activeDevices().audioinput;
6301
+ const to = (active !== null ? now.audioinput.get(active) : void 0) ?? now.audioinput.get("default") ?? remaining[0][1];
6302
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "switched", from, to } });
6303
+ }
6304
+ continue;
6305
+ }
6306
+ const added = [...now[deviceKind].entries()].find(([id]) => !isAlias(id) && !before[deviceKind].has(id));
6307
+ if (added !== void 0 && added[0] !== driver.activeDevices()[deviceKind]) {
6308
+ setRegion({ offer: { kind, deviceId: added[0], label: added[1] } });
6309
+ }
6310
+ }
6311
+ readInUse();
6312
+ }
6313
+ async function unavailable(error, kind, operation) {
6314
+ const normalized = normalizeDeviceError(error, operation);
6315
+ const permission = platform === null ? "unknown" : await platform.queryPermission(kind);
6316
+ if (permission !== "unknown") setPermission(kind, permission);
6317
+ return unavailableFrom(normalized, classifyDeviceFailure(normalized, permission));
6318
+ }
6319
+ return {
6320
+ start() {
6321
+ if (started || platform === null) return;
6322
+ started = true;
6323
+ for (const kind of ["microphone", "camera"]) {
6324
+ void platform.queryPermission(kind).then((reading) => setPermission(kind, reading));
6325
+ unsubscribers.push(platform.watchPermission(kind, (reading) => setPermission(kind, reading)));
6326
+ }
6327
+ unsubscribers.push(
6328
+ platform.onVisible(() => {
6329
+ const driver = options.driver();
6330
+ if (driver?.startAudio === void 0 || region().audioOutput !== "blocked") return;
6331
+ driver.startAudio().catch(() => void 0);
6332
+ })
6333
+ );
6334
+ },
6335
+ stop() {
6336
+ for (const off of unsubscribers) off();
6337
+ unsubscribers = [];
6338
+ started = false;
6339
+ },
6340
+ unavailable,
6341
+ onAudioPlayback(canPlay) {
6342
+ setRegion({ audioOutput: canPlay ? "ok" : "blocked" });
6343
+ },
6344
+ onDevicesChanged() {
6345
+ void handleDevicesChanged().catch((error) => {
6346
+ options.report("warn", `meet: device change could not be handled: ${String(error)}`, "Pick the device in Settings.");
6347
+ });
6348
+ },
6349
+ onDeviceError(error, deviceKind) {
6350
+ if (deviceKind === null) return;
6351
+ const kind = deviceKind === "audioinput" ? "microphone" : "camera";
6352
+ void unavailable(error, kind, `meet.${kind}`).then((refused) => {
6353
+ const track = store.snapshot().media[kind];
6354
+ if (track.enabled) return;
6355
+ store.setLocalTrack(kind, { enabled: false, unavailable: refused });
6356
+ });
6357
+ },
6358
+ onConnected() {
6359
+ void snapshotDevices().then((now) => {
6360
+ known = now;
6361
+ readInUse();
6362
+ });
6363
+ },
6364
+ onDisconnected() {
6365
+ known = null;
6366
+ inUse = { audioinput: null, videoinput: null };
6367
+ setRegion({ audioOutput: "ok", notice: null, offer: null });
6368
+ },
6369
+ async enableAudio() {
6370
+ const driver = options.driver();
6371
+ if (driver?.startAudio === void 0) return;
6372
+ try {
6373
+ await driver.startAudio();
6374
+ setRegion({ audioOutput: "ok" });
6375
+ } catch (error) {
6376
+ options.report("warn", `meet: sound could not be enabled: ${String(error)}`, "Click Enable sound again, or check the tab is not muted.");
6377
+ }
6378
+ },
6379
+ async acceptOffer() {
6380
+ const offer = region().offer;
6381
+ const driver = options.driver();
6382
+ setRegion({ offer: null });
6383
+ if (offer === null || driver === null) return;
6384
+ try {
6385
+ await driver.switchDevice(INPUT_KIND[offer.kind], offer.deviceId);
6386
+ readInUse();
6387
+ } catch (error) {
6388
+ const refused = await unavailable(error, offer.kind, "meet.switchDevice");
6389
+ options.report("warn", refused.message, refused.remedy);
6390
+ }
6391
+ },
6392
+ dismissOffer() {
6393
+ setRegion({ offer: null });
6394
+ },
6395
+ dismissNotice() {
6396
+ setRegion({ notice: null });
6397
+ }
6398
+ };
6399
+ }
6400
+
5662
6401
  // src/core/room.ts
5663
6402
  function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
5664
6403
  return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
@@ -5730,6 +6469,18 @@ function createRoomEngine(options) {
5730
6469
  let disposed = false;
5731
6470
  let moving = false;
5732
6471
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
6472
+ let lastServerUrl = null;
6473
+ let connectionPolicy = DEFAULT_CONNECTION_POLICY;
6474
+ let ghosts = /* @__PURE__ */ new Map();
6475
+ let ghostTimer = null;
6476
+ let liveRoster = [];
6477
+ const departures = /* @__PURE__ */ new Map();
6478
+ function requestAsNow() {
6479
+ if (lastRequest === null) return null;
6480
+ const media2 = store.snapshot().media;
6481
+ if (store.snapshot().phase !== "in_call") return lastRequest;
6482
+ return { ...lastRequest, mode: media2.camera.enabled ? "video" : "audio", microphone: media2.microphone.enabled };
6483
+ }
5733
6484
  async function dropRoom() {
5734
6485
  const target = driver;
5735
6486
  driver = null;
@@ -5739,7 +6490,9 @@ function createRoomEngine(options) {
5739
6490
  }
5740
6491
  store.setLocalTrack("microphone", IDLE_TRACK);
5741
6492
  store.setLocalTrack("camera", IDLE_TRACK);
6493
+ media.onDisconnected();
5742
6494
  store.patch({ share: NO_SHARE, connection: "stable" });
6495
+ clearGhosts();
5743
6496
  await target?.disconnect();
5744
6497
  }
5745
6498
  async function leaveRoom() {
@@ -5803,11 +6556,26 @@ function createRoomEngine(options) {
5803
6556
  report("error", normalized.message, normalized.remedy);
5804
6557
  return normalized;
5805
6558
  }
6559
+ const media = createMediaSupervisor({
6560
+ store,
6561
+ platform: options.mediaPlatform === void 0 ? webMediaPlatform() : options.mediaPlatform,
6562
+ driver: () => driver,
6563
+ report
6564
+ });
5806
6565
  const handlers = {
6566
+ audioPlayback(canPlay) {
6567
+ media.onAudioPlayback(canPlay);
6568
+ },
6569
+ devicesChanged() {
6570
+ media.onDevicesChanged();
6571
+ },
6572
+ mediaDeviceError(error, kind) {
6573
+ media.onDeviceError(error, kind);
6574
+ },
5807
6575
  participants(list) {
5808
6576
  const joinedAt = new Date(now()).toISOString();
5809
6577
  const before = new Set(store.snapshot().participants.map((p) => p.identity));
5810
- store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
6578
+ projectRoster(list, joinedAt);
5811
6579
  const snapshot = store.snapshot();
5812
6580
  const newcomer = snapshot.participants.some(
5813
6581
  (participant) => !participant.isLocal && !before.has(participant.identity)
@@ -5819,6 +6587,9 @@ function createRoomEngine(options) {
5819
6587
  );
5820
6588
  }
5821
6589
  },
6590
+ participantLeft(who, reason) {
6591
+ departures.set(who, reason);
6592
+ },
5822
6593
  roomMetadata(metadata) {
5823
6594
  let spotlight = null;
5824
6595
  if (metadata.length > 0) {
@@ -5845,12 +6616,8 @@ function createRoomEngine(options) {
5845
6616
  store.setQuality(asParticipantIdentity(who), value);
5846
6617
  },
5847
6618
  connectionState(state) {
5848
- if (state === "connected") {
5849
- if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
5850
- store.dispatch({ type: "connected" });
5851
- } else if (state === "reconnecting") {
5852
- store.patch({ connection: "reconnecting" });
5853
- }
6619
+ supervisor.driverState(state);
6620
+ if (state === "connected" && store.snapshot().phase === "joining") store.dispatch({ type: "connected" });
5854
6621
  },
5855
6622
  transcription(event) {
5856
6623
  const snapshot = store.snapshot();
@@ -5983,6 +6750,7 @@ function createRoomEngine(options) {
5983
6750
  };
5984
6751
  async function resolveExitAndApply(hint) {
5985
6752
  const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
6753
+ lastRequest = requestAsNow();
5986
6754
  await dropRoom();
5987
6755
  let server;
5988
6756
  if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
@@ -5996,7 +6764,7 @@ function createRoomEngine(options) {
5996
6764
  }
5997
6765
  if (disposed) return;
5998
6766
  const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5999
- store.patch({ connection: resolved.connection });
6767
+ store.patch({ connection: server === null ? supervisor.exitConnection() : resolved.connection });
6000
6768
  if (resolved.to === "waiting:knocking") {
6001
6769
  store.patch({
6002
6770
  waiting: {
@@ -6028,6 +6796,8 @@ function createRoomEngine(options) {
6028
6796
  ...request.observe === true ? { observe: true } : {}
6029
6797
  });
6030
6798
  current = token;
6799
+ lastServerUrl = token.serverUrl;
6800
+ connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6031
6801
  store.patch({
6032
6802
  localIdentity: token.identity,
6033
6803
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6067,6 +6837,7 @@ function createRoomEngine(options) {
6067
6837
  store.dispatch({ type: "connected" });
6068
6838
  store.setLocalTrack("microphone", IDLE_TRACK);
6069
6839
  store.setLocalTrack("camera", IDLE_TRACK);
6840
+ media.start();
6070
6841
  const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
6071
6842
  store.patch({ effects });
6072
6843
  if (!effects.noiseSuppression || effects.background !== "none") {
@@ -6091,6 +6862,7 @@ function createRoomEngine(options) {
6091
6862
  if (request.microphone !== false) {
6092
6863
  await enableTrack(created, "microphone", true);
6093
6864
  }
6865
+ media.onConnected();
6094
6866
  }
6095
6867
  async function enableTrack(target, kind, enabled) {
6096
6868
  try {
@@ -6100,10 +6872,10 @@ function createRoomEngine(options) {
6100
6872
  store.setLocalTrack(kind, state);
6101
6873
  return state;
6102
6874
  } catch (error) {
6103
- const normalized = normalizeDeviceError(error, `meet.${kind}`);
6104
- const state = { enabled: false, unavailable: unavailableFrom(normalized) };
6875
+ const refused = await media.unavailable(error, kind, `meet.${kind}`);
6876
+ const state = { enabled: false, unavailable: refused };
6105
6877
  store.setLocalTrack(kind, state);
6106
- report("warn", normalized.message, normalized.remedy);
6878
+ report("warn", refused.message, refused.remedy);
6107
6879
  return state;
6108
6880
  }
6109
6881
  }
@@ -6132,7 +6904,59 @@ function createRoomEngine(options) {
6132
6904
  throw normalized;
6133
6905
  }
6134
6906
  }
6135
- return {
6907
+ function projectRoster(list, joinedAt) {
6908
+ const at = now();
6909
+ const previous = new Map(liveRoster.map((p) => [p.identity, p]));
6910
+ const present = new Set(list.map((p) => p.identity));
6911
+ const graceMs = connectionPolicy.reconnectGraceSeconds * 1e3;
6912
+ for (const [who, reason] of departures) {
6913
+ const last = previous.get(who);
6914
+ if (last !== void 0 && !last.isLocal && !present.has(who) && graceMs > 0 && isUnannouncedDeparture(reason)) {
6915
+ ghosts.set(who, { participant: last, until: at + graceMs });
6916
+ }
6917
+ }
6918
+ departures.clear();
6919
+ liveRoster = list;
6920
+ const merged = mergeGhosts(list, ghosts, at);
6921
+ ghosts = merged.kept;
6922
+ store.setParticipants(
6923
+ merged.roster.map((entry) => {
6924
+ const projected = projectParticipant(entry, joinedAt);
6925
+ return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
6926
+ })
6927
+ );
6928
+ scheduleGhostExpiry();
6929
+ }
6930
+ function scheduleGhostExpiry() {
6931
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
6932
+ ghostTimer = null;
6933
+ if (ghosts.size === 0 || disposed) return;
6934
+ const next = Math.min(...[...ghosts.values()].map((g) => g.until));
6935
+ ghostTimer = setTimeout(() => {
6936
+ ghostTimer = null;
6937
+ if (disposed || driver === null) return;
6938
+ projectRoster(liveRoster, new Date(now()).toISOString());
6939
+ }, Math.max(0, next - now()) + 50);
6940
+ }
6941
+ function clearGhosts() {
6942
+ ghosts = /* @__PURE__ */ new Map();
6943
+ departures.clear();
6944
+ liveRoster = [];
6945
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
6946
+ ghostTimer = null;
6947
+ }
6948
+ const supervisor = createConnectionSupervisor({
6949
+ store,
6950
+ network: options.connection?.network ?? createWebNetworkPort(),
6951
+ probe: () => options.connection?.probe?.(current?.serverUrl ?? lastServerUrl) ?? Promise.resolve(true),
6952
+ policy: () => connectionPolicy,
6953
+ autoRejoin: () => engine.autoRejoin(),
6954
+ currentRequest: requestAsNow,
6955
+ resume: options.connection?.resume,
6956
+ now,
6957
+ report: (level, message) => report(level, message)
6958
+ });
6959
+ const engine = {
6136
6960
  join(request) {
6137
6961
  if (joining !== null) return joining;
6138
6962
  if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
@@ -6177,6 +7001,35 @@ function createRoomEngine(options) {
6177
7001
  });
6178
7002
  return joining;
6179
7003
  },
7004
+ async autoRejoin() {
7005
+ const request = lastRequest;
7006
+ if (request === null || joining !== null) return joining ?? void 0;
7007
+ if (!store.dispatch({ type: "auto_rejoin" })) return;
7008
+ tokens.invalidate(request.roomName);
7009
+ current = null;
7010
+ joining = connect(request).catch((error) => {
7011
+ throw failJoin(error, "meet.autoRejoin");
7012
+ }).finally(() => {
7013
+ joining = null;
7014
+ });
7015
+ return joining;
7016
+ },
7017
+ resume(roomName) {
7018
+ const marker = supervisor.resumable(roomName);
7019
+ if (marker === null || joining !== null) return false;
7020
+ if (!store.dispatch({ type: "resumed" })) return false;
7021
+ lastRequest = { ...marker.request, roomName };
7022
+ report("info", "meet: this tab was in the call before the page reloaded \u2014 rejoining");
7023
+ tokens.invalidate(roomName);
7024
+ current = null;
7025
+ joining = connect(lastRequest).catch((error) => {
7026
+ throw failJoin(error, "meet.resume");
7027
+ }).finally(() => {
7028
+ joining = null;
7029
+ });
7030
+ joining.catch(() => void 0);
7031
+ return true;
7032
+ },
6180
7033
  async moveToBreakout(roomId) {
6181
7034
  const request = lastRequest;
6182
7035
  if (request === null || driver === null) {
@@ -6331,6 +7184,10 @@ function createRoomEngine(options) {
6331
7184
  if (driver === null) return;
6332
7185
  await driver.switchDevice(kind, deviceId);
6333
7186
  },
7187
+ enableAudio: () => media.enableAudio(),
7188
+ acceptDeviceOffer: () => media.acceptOffer(),
7189
+ dismissDeviceOffer: () => media.dismissOffer(),
7190
+ dismissDeviceNotice: () => media.dismissNotice(),
6334
7191
  pin(who) {
6335
7192
  store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
6336
7193
  },
@@ -6344,6 +7201,9 @@ function createRoomEngine(options) {
6344
7201
  token: () => current,
6345
7202
  async dispose() {
6346
7203
  disposed = true;
7204
+ supervisor.dispose();
7205
+ media.stop();
7206
+ clearGhosts();
6347
7207
  if (driver !== null) {
6348
7208
  const target = driver;
6349
7209
  driver = null;
@@ -6352,6 +7212,7 @@ function createRoomEngine(options) {
6352
7212
  store.reset();
6353
7213
  }
6354
7214
  };
7215
+ return engine;
6355
7216
  }
6356
7217
  function canModerate(participant) {
6357
7218
  return participant !== void 0 && isHostRole(participant.role);
@@ -7326,22 +8187,10 @@ function observedRole(role, standing) {
7326
8187
  if (role === "host" || role === "cohost") return role;
7327
8188
  return isLinkFollower(standing) ? "guest" : "participant";
7328
8189
  }
7329
- function observedDevice(track) {
8190
+ function observedDevice(track, permission = "unknown") {
7330
8191
  if (track.enabled) return "on";
7331
- const refused = track.unavailable;
7332
- if (refused === null) return "muted_by_me";
7333
- switch (refused.code) {
7334
- case "permission-denied":
7335
- return "unavailable:denied";
7336
- case "device-not-found":
7337
- return "unavailable:none";
7338
- case "device-in-use":
7339
- return "unavailable:busy";
7340
- case "insecure-context":
7341
- return "unavailable:blocked_site";
7342
- default:
7343
- return "unavailable:failed";
7344
- }
8192
+ const reason = deviceUnavailableReason(track, permission);
8193
+ return reason === null ? "muted_by_me" : `unavailable:${reason}`;
7345
8194
  }
7346
8195
  function endedRecord(snapshot, given) {
7347
8196
  const record = snapshot?.meeting ?? given;
@@ -7374,9 +8223,10 @@ function observe(input) {
7374
8223
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
7375
8224
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
7376
8225
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
7377
- audioBlocked: null,
7378
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera) : null,
7379
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone) : null
8226
+ // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
8227
+ audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
8228
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
8229
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
7380
8230
  };
7381
8231
  }
7382
8232
  function staticObservation(phase, reason = null) {
@@ -7437,13 +8287,13 @@ function participantAttributes(p) {
7437
8287
  "data-meet-participant-role": observedRole(p.role, p.standing),
7438
8288
  "data-meet-participant-self": String(p.isLocal)
7439
8289
  };
7440
- const quality = observedQuality(p.quality);
8290
+ const quality = p.reconnecting === true ? "reconnecting" : observedQuality(p.quality);
7441
8291
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
7442
8292
  return attrs;
7443
8293
  }
7444
- function microphoneNoticeCode(track) {
7445
- const refused = track.unavailable;
7446
- if (refused === null) return null;
7447
- return refused.code === "permission-denied" || refused.code === "insecure-context" ? "mic-blocked" : "device-lost";
8294
+ function microphoneNoticeCode(track, permission = "unknown") {
8295
+ const reason = deviceUnavailableReason(track, permission);
8296
+ if (reason === null) return null;
8297
+ return deviceNoticeCode("microphone", reason) === "mic-blocked" ? "mic-blocked" : "device-lost";
7448
8298
  }
7449
8299
  //# sourceMappingURL=index.cjs.map