@ai-matrx/meet 0.11.47 → 0.11.49

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.js CHANGED
@@ -147,10 +147,11 @@ function webTabPlatform() {
147
147
 
148
148
  // src/core/connection-supervisor.ts
149
149
  var DEFAULT_CONNECTION_POLICY = {
150
- bannerDelayMs: 2e3,
150
+ bannerDelayMs: 5e3,
151
151
  autoRejoin: true,
152
152
  autoRejoinMaxAttempts: 5,
153
- reconnectGraceSeconds: 30
153
+ reconnectGraceSeconds: 30,
154
+ videoAutoOffAfterPoorSeconds: 20
154
155
  };
155
156
  function clampInt(value, min, max, fallback) {
156
157
  if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
@@ -164,7 +165,8 @@ function connectionPolicyFrom(raw) {
164
165
  bannerDelayMs: clampInt(row["reconnect_banner_delay_ms"], 0, 1e4, d.bannerDelayMs),
165
166
  autoRejoin: typeof row["auto_rejoin"] === "boolean" ? row["auto_rejoin"] : d.autoRejoin,
166
167
  autoRejoinMaxAttempts: clampInt(row["auto_rejoin_max_attempts"], 0, 20, d.autoRejoinMaxAttempts),
167
- reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds)
168
+ reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds),
169
+ videoAutoOffAfterPoorSeconds: clampInt(row["video_auto_off_after_poor_seconds"], 0, 600, d.videoAutoOffAfterPoorSeconds)
168
170
  };
169
171
  }
170
172
  var LIVEKIT_BASE_DELAYS_MS = [0, 300, 1200, 2700, 4800, 7e3];
@@ -2444,11 +2446,14 @@ function recordingLabel(state) {
2444
2446
 
2445
2447
  // src/core/live-policy.ts
2446
2448
  var DEFAULT_LIVE_POLICY = {
2449
+ headlessPowers: "members_admit",
2447
2450
  chatMode: "everyone",
2448
2451
  avLock: "off",
2449
2452
  timeLimitMinutes: 0,
2450
2453
  timeLimitWarningMinutes: 5,
2451
- recordingConsent: "notice"
2454
+ recordingConsent: "notice",
2455
+ shareWhoCan: "everyone",
2456
+ shareConcurrency: "one_takeover"
2452
2457
  };
2453
2458
  function oneOf(value, allowed, fallback) {
2454
2459
  return typeof value === "string" && allowed.includes(value) ? value : fallback;
@@ -2460,6 +2465,7 @@ function whole(value, fallback) {
2460
2465
  function livePolicyFrom(raw) {
2461
2466
  const o = typeof raw === "object" && raw !== null ? raw : {};
2462
2467
  return {
2468
+ headlessPowers: oneOf(o["headless_powers"], ["members_admit", "none", "cohosts_only"], DEFAULT_LIVE_POLICY.headlessPowers),
2463
2469
  chatMode: oneOf(o["chat_mode"], ["everyone", "hosts_only", "off"], DEFAULT_LIVE_POLICY.chatMode),
2464
2470
  avLock: oneOf(o["attendee_av_lock"], ["off", "mic", "camera", "both"], DEFAULT_LIVE_POLICY.avLock),
2465
2471
  timeLimitMinutes: whole(o["time_limit_minutes"], 0),
@@ -2468,17 +2474,33 @@ function livePolicyFrom(raw) {
2468
2474
  o["recording_consent"],
2469
2475
  ["notice", "ok_or_leave", "view_only_until_agree"],
2470
2476
  DEFAULT_LIVE_POLICY.recordingConsent
2471
- )
2477
+ ),
2478
+ shareWhoCan: oneOf(o["share_who_can"], ["everyone", "host_only", "org", "presenters"], DEFAULT_LIVE_POLICY.shareWhoCan),
2479
+ shareConcurrency: oneOf(o["share_concurrency"], ["one_takeover", "one_replace", "multiple"], DEFAULT_LIVE_POLICY.shareConcurrency)
2472
2480
  };
2473
2481
  }
2474
2482
  function samePolicy(a, b) {
2475
- return a.chatMode === b.chatMode && a.avLock === b.avLock && a.timeLimitMinutes === b.timeLimitMinutes && a.timeLimitWarningMinutes === b.timeLimitWarningMinutes && a.recordingConsent === b.recordingConsent;
2483
+ return a.headlessPowers === b.headlessPowers && a.chatMode === b.chatMode && a.avLock === b.avLock && a.timeLimitMinutes === b.timeLimitMinutes && a.timeLimitWarningMinutes === b.timeLimitWarningMinutes && a.recordingConsent === b.recordingConsent && a.shareWhoCan === b.shareWhoCan && a.shareConcurrency === b.shareConcurrency;
2476
2484
  }
2477
2485
  var MODERATORS = /* @__PURE__ */ new Set(["host", "cohost"]);
2478
2486
  function isLockedFor(policy, role, kind) {
2479
2487
  if (MODERATORS.has(role)) return false;
2480
2488
  return policy.avLock === "both" || (kind === "microphone" ? policy.avLock === "mic" : policy.avLock === "camera");
2481
2489
  }
2490
+ function canShareUnder(policy, role, standing) {
2491
+ if (role === "viewer") return false;
2492
+ if (MODERATORS.has(role)) return true;
2493
+ switch (policy.shareWhoCan) {
2494
+ case "host_only":
2495
+ return false;
2496
+ case "presenters":
2497
+ return role === "presenter";
2498
+ case "org":
2499
+ return role === "presenter" || standing === "member";
2500
+ default:
2501
+ return true;
2502
+ }
2503
+ }
2482
2504
  function consentPending(policy, role, recordingOn, given) {
2483
2505
  return recordingOn && !given && policy.recordingConsent !== "notice" && !MODERATORS.has(role);
2484
2506
  }
@@ -2490,6 +2512,12 @@ function minutesLeft(policy, startedAtMs, nowMs) {
2490
2512
  if (policy.timeLimitMinutes <= 0 || startedAtMs === null) return null;
2491
2513
  return Math.ceil((startedAtMs + policy.timeLimitMinutes * 6e4 - nowMs) / 6e4);
2492
2514
  }
2515
+ function mayAdmitNow(role, participants, policy) {
2516
+ if (role === "none") return false;
2517
+ if (role === "host" || role === "cohost") return true;
2518
+ const hostIn = participants.some((p) => p.role === "host" || p.role === "cohost");
2519
+ return !hostIn && policy.headlessPowers === "members_admit";
2520
+ }
2493
2521
 
2494
2522
  // src/core/attendance.ts
2495
2523
  var IDLE_PREVIEW = { state: "idle", unavailable: null };
@@ -3950,6 +3978,7 @@ var NO_GRANTS = {
3950
3978
  canPublish: false,
3951
3979
  canSubscribe: false
3952
3980
  };
3981
+ var UNKNOWN_LOCAL_NETWORK = { path: "unknown", quality: "unknown", videoPaused: false };
3953
3982
  var NO_SHARE = { state: "none", reason: null, remedy: null, at: 0 };
3954
3983
  var EMPTY = {
3955
3984
  phase: "resolving",
@@ -3958,6 +3987,7 @@ var EMPTY = {
3958
3987
  roomName: null,
3959
3988
  connection: "stable",
3960
3989
  connectionBanner: false,
3990
+ localNetwork: UNKNOWN_LOCAL_NETWORK,
3961
3991
  share: NO_SHARE,
3962
3992
  call: null,
3963
3993
  waiting: null,
@@ -5541,6 +5571,14 @@ function applyModeration(store, payload, now) {
5541
5571
  }
5542
5572
  return HANDLED;
5543
5573
  }
5574
+ case "share-ended": {
5575
+ const reason = text2(payload["reason"]) === "not_allowed" ? "not_allowed" : "replaced";
5576
+ const by = text2(payload["by"]);
5577
+ store.patch({
5578
+ share: { state: "ended", reason, remedy: null, at: now(), ...by !== null ? { by } : {} }
5579
+ });
5580
+ return HANDLED;
5581
+ }
5544
5582
  case "muted":
5545
5583
  case "ask-unmute":
5546
5584
  case "removed": {
@@ -5638,6 +5676,187 @@ var TRANSCRIPTION_ATTRIBUTES = {
5638
5676
  speakerIdentity: "lk.transcribed_participant_identity"
5639
5677
  };
5640
5678
 
5679
+ // src/core/network-health.ts
5680
+ function pathOf(reading) {
5681
+ if (reading.relayed === null && reading.tcp === null) return "unknown";
5682
+ return reading.relayed === true || reading.tcp === true ? "restricted" : "direct";
5683
+ }
5684
+ var POOR_RTT_MS = 300;
5685
+ var RECOVERED_RTT_MS = 220;
5686
+ var POOR_LOSS = 0.08;
5687
+ var RECOVERED_LOSS = 0.03;
5688
+ function createSelfAssessor(options = {}) {
5689
+ const poorSamples = options.poorSamples ?? 2;
5690
+ const goodSamples = options.goodSamples ?? 3;
5691
+ let quality = "unknown";
5692
+ let badRun = 0;
5693
+ let goodRun = 0;
5694
+ return {
5695
+ push(reading) {
5696
+ const { rttMs, lossRatio } = reading;
5697
+ if (rttMs === null && lossRatio === null) return quality;
5698
+ const bad = rttMs !== null && rttMs >= POOR_RTT_MS || lossRatio !== null && lossRatio >= POOR_LOSS;
5699
+ const clear = (rttMs === null || rttMs < RECOVERED_RTT_MS) && (lossRatio === null || lossRatio < RECOVERED_LOSS);
5700
+ badRun = bad ? badRun + 1 : 0;
5701
+ goodRun = clear ? goodRun + 1 : 0;
5702
+ if (badRun >= poorSamples) quality = "poor";
5703
+ else if (quality !== "poor" && !bad) quality = "good";
5704
+ else if (quality === "poor" && goodRun >= goodSamples) quality = "good";
5705
+ return quality;
5706
+ },
5707
+ current: () => quality,
5708
+ reset() {
5709
+ quality = "unknown";
5710
+ badRun = 0;
5711
+ goodRun = 0;
5712
+ }
5713
+ };
5714
+ }
5715
+ var RANK = {
5716
+ lost: 0,
5717
+ poor: 1,
5718
+ good: 2,
5719
+ excellent: 3,
5720
+ unknown: 4
5721
+ };
5722
+ function worstQuality(a, b) {
5723
+ if (b === null || b === void 0 || b === "unknown") return a;
5724
+ if (a === "unknown") return b;
5725
+ return RANK[b] < RANK[a] ? b : a;
5726
+ }
5727
+ var ALIVE_TOPIC = "mx.alive";
5728
+ function encodeBeat(beat) {
5729
+ return JSON.stringify({ q: beat.q, vp: beat.vp });
5730
+ }
5731
+ function decodeBeat(payload) {
5732
+ try {
5733
+ const raw = JSON.parse(payload);
5734
+ const q = raw.q === "good" || raw.q === "poor" ? raw.q : "unknown";
5735
+ return { q, vp: raw.vp === true };
5736
+ } catch {
5737
+ return null;
5738
+ }
5739
+ }
5740
+ function heartbeatIntervalMs(roomSize) {
5741
+ return 2e3 * Math.max(1, Math.ceil(Math.max(1, roomSize) / 25));
5742
+ }
5743
+ function staleAfterMs(roomSize) {
5744
+ return heartbeatIntervalMs(roomSize) * 3 + 1e3;
5745
+ }
5746
+ function createLivenessTracker(now) {
5747
+ const lastHeard = /* @__PURE__ */ new Map();
5748
+ const beats = /* @__PURE__ */ new Map();
5749
+ return {
5750
+ heard(who, beat) {
5751
+ lastHeard.set(who, now());
5752
+ if (beat !== null) beats.set(who, beat);
5753
+ },
5754
+ stale(present, roomSize) {
5755
+ const limit = staleAfterMs(roomSize);
5756
+ const at = now();
5757
+ const out = /* @__PURE__ */ new Set();
5758
+ for (const who of present) {
5759
+ const heard = lastHeard.get(who);
5760
+ if (heard !== void 0 && at - heard > limit) out.add(who);
5761
+ }
5762
+ return out;
5763
+ },
5764
+ reported: (who) => beats.get(who) ?? null,
5765
+ restartClock() {
5766
+ const at = now();
5767
+ for (const who of lastHeard.keys()) lastHeard.set(who, at);
5768
+ },
5769
+ forget(who) {
5770
+ lastHeard.delete(who);
5771
+ beats.delete(who);
5772
+ },
5773
+ clear() {
5774
+ lastHeard.clear();
5775
+ beats.clear();
5776
+ }
5777
+ };
5778
+ }
5779
+ function createVideoAutoOff(afterMs, now) {
5780
+ let poorSince = null;
5781
+ let armed = true;
5782
+ return {
5783
+ step(quality, cameraOn) {
5784
+ if (quality === "good") {
5785
+ poorSince = null;
5786
+ armed = true;
5787
+ return null;
5788
+ }
5789
+ if (quality !== "poor" || !cameraOn) {
5790
+ poorSince = quality === "poor" ? poorSince : null;
5791
+ return null;
5792
+ }
5793
+ const at = now();
5794
+ if (poorSince === null) poorSince = at;
5795
+ const limit = afterMs();
5796
+ if (!armed || limit <= 0 || at - poorSince < limit) return null;
5797
+ armed = false;
5798
+ return "pause";
5799
+ },
5800
+ retried() {
5801
+ armed = false;
5802
+ },
5803
+ reset() {
5804
+ poorSince = null;
5805
+ armed = true;
5806
+ }
5807
+ };
5808
+ }
5809
+ function readingFromStats(entries) {
5810
+ const byId2 = /* @__PURE__ */ new Map();
5811
+ for (const entry of entries) byId2.set(entry.id, entry);
5812
+ let pair;
5813
+ for (const entry of byId2.values()) {
5814
+ if (entry.type === "transport" && typeof entry["selectedCandidatePairId"] === "string") {
5815
+ pair = byId2.get(entry["selectedCandidatePairId"]);
5816
+ if (pair !== void 0) break;
5817
+ }
5818
+ }
5819
+ if (pair === void 0) {
5820
+ for (const entry of byId2.values()) {
5821
+ if (entry.type === "candidate-pair" && entry["nominated"] === true && entry["state"] === "succeeded") {
5822
+ pair = entry;
5823
+ break;
5824
+ }
5825
+ }
5826
+ }
5827
+ let lossRatio = null;
5828
+ for (const entry of byId2.values()) {
5829
+ if (entry.type === "remote-inbound-rtp" && typeof entry["fractionLost"] === "number") {
5830
+ lossRatio = Math.max(lossRatio ?? 0, entry["fractionLost"]);
5831
+ }
5832
+ }
5833
+ if (pair === void 0) return { rttMs: null, lossRatio, relayed: null, tcp: null };
5834
+ const rtt = pair["currentRoundTripTime"];
5835
+ const local = typeof pair["localCandidateId"] === "string" ? byId2.get(pair["localCandidateId"]) : void 0;
5836
+ const candidateType = local?.["candidateType"];
5837
+ const protocol = local?.["protocol"];
5838
+ const relayProtocol = local?.["relayProtocol"];
5839
+ return {
5840
+ rttMs: typeof rtt === "number" ? Math.round(rtt * 1e3) : null,
5841
+ lossRatio,
5842
+ relayed: typeof candidateType === "string" ? candidateType === "relay" : null,
5843
+ tcp: typeof protocol === "string" || typeof relayProtocol === "string" ? protocol === "tcp" || relayProtocol === "tcp" || relayProtocol === "tls" : null
5844
+ };
5845
+ }
5846
+ function mergeReadings(readings) {
5847
+ const max = (values) => {
5848
+ const known = values.filter((v) => v !== null);
5849
+ return known.length === 0 ? null : Math.max(...known);
5850
+ };
5851
+ const any = (values) => values.every((v) => v === null) ? null : values.some((v) => v === true);
5852
+ return {
5853
+ rttMs: max(readings.map((r) => r.rttMs)),
5854
+ lossRatio: max(readings.map((r) => r.lossRatio)),
5855
+ relayed: any(readings.map((r) => r.relayed)),
5856
+ tcp: any(readings.map((r) => r.tcp))
5857
+ };
5858
+ }
5859
+
5641
5860
  // src/core/room.ts
5642
5861
  function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
5643
5862
  return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
@@ -5730,6 +5949,76 @@ function createRoomEngine(options) {
5730
5949
  return personConnections.get(personOf(who)) ?? [who];
5731
5950
  }
5732
5951
  const departures = /* @__PURE__ */ new Map();
5952
+ const liveness = createLivenessTracker(now);
5953
+ const assessor = createSelfAssessor();
5954
+ const autoOff = createVideoAutoOff(() => connectionPolicy.videoAutoOffAfterPoorSeconds * 1e3, now);
5955
+ const lkQuality = /* @__PURE__ */ new Map();
5956
+ let silent = /* @__PURE__ */ new Set();
5957
+ let healthTimer = null;
5958
+ let lastBeatAt = 0;
5959
+ let wasUnstable = false;
5960
+ function patchLocalNetwork(next) {
5961
+ const before = store.snapshot().localNetwork;
5962
+ const merged = { ...before, ...next };
5963
+ if (merged.path !== before.path || merged.quality !== before.quality || merged.videoPaused !== before.videoPaused) {
5964
+ store.patch({ localNetwork: merged });
5965
+ }
5966
+ }
5967
+ function effectiveQuality(person, connection, isLocal) {
5968
+ const fromServer = lkQuality.get(person) ?? "unknown";
5969
+ if (isLocal) return worstQuality(fromServer, assessor.current());
5970
+ return worstQuality(fromServer, connection === void 0 ? null : liveness.reported(connection)?.q);
5971
+ }
5972
+ function refreshQualities() {
5973
+ const snap = store.snapshot();
5974
+ for (const p of snap.participants) {
5975
+ if (p.reconnecting === true) continue;
5976
+ const next = effectiveQuality(p.identity, mainConnection.get(p.identity), p.isLocal);
5977
+ if (next !== "unknown" && next !== p.quality) store.setQuality(p.identity, next);
5978
+ }
5979
+ }
5980
+ function stopHealth() {
5981
+ if (healthTimer !== null) clearInterval(healthTimer);
5982
+ healthTimer = null;
5983
+ liveness.clear();
5984
+ assessor.reset();
5985
+ autoOff.reset();
5986
+ silent = /* @__PURE__ */ new Set();
5987
+ lkQuality.clear();
5988
+ lastBeatAt = 0;
5989
+ wasUnstable = false;
5990
+ if (store.snapshot().localNetwork !== UNKNOWN_LOCAL_NETWORK) store.patch({ localNetwork: UNKNOWN_LOCAL_NETWORK });
5991
+ }
5992
+ function healthTick() {
5993
+ const snap = store.snapshot();
5994
+ const target = driver;
5995
+ if (disposed || target === null || snap.phase !== "in_call") return;
5996
+ if (snap.connection !== "stable") {
5997
+ wasUnstable = true;
5998
+ return;
5999
+ }
6000
+ if (wasUnstable) {
6001
+ wasUnstable = false;
6002
+ liveness.restartClock();
6003
+ }
6004
+ const size = liveRoster.length;
6005
+ const at = now();
6006
+ if (at - lastBeatAt >= heartbeatIntervalMs(size)) {
6007
+ lastBeatAt = at;
6008
+ const beat = encodeBeat({ q: assessor.current(), vp: snap.localNetwork.videoPaused });
6009
+ void target.publishData(ALIVE_TOPIC, beat).catch(() => void 0);
6010
+ }
6011
+ const remotes = liveRoster.filter((p) => !p.isLocal).map((p) => p.identity);
6012
+ const next = liveness.stale(remotes, size);
6013
+ if (next.size !== silent.size || [...next].some((who) => !silent.has(who))) {
6014
+ silent = next;
6015
+ projectRoster(liveRoster, new Date(at).toISOString());
6016
+ }
6017
+ }
6018
+ function startHealth() {
6019
+ if (healthTimer !== null) return;
6020
+ healthTimer = setInterval(healthTick, 1e3);
6021
+ }
5733
6022
  function requestAsNow() {
5734
6023
  if (lastRequest === null) return null;
5735
6024
  const media2 = store.snapshot().media;
@@ -5748,6 +6037,7 @@ function createRoomEngine(options) {
5748
6037
  media.onDisconnected();
5749
6038
  store.patch({ share: NO_SHARE, connection: "stable" });
5750
6039
  clearGhosts();
6040
+ stopHealth();
5751
6041
  await target?.disconnect();
5752
6042
  }
5753
6043
  async function leaveRoom() {
@@ -5839,6 +6129,8 @@ function createRoomEngine(options) {
5839
6129
  },
5840
6130
  participantLeft(who, reason) {
5841
6131
  departures.set(who, reason);
6132
+ liveness.forget(who);
6133
+ silent.delete(who);
5842
6134
  },
5843
6135
  roomMetadata(metadata) {
5844
6136
  let parsed = {};
@@ -5872,7 +6164,27 @@ function createRoomEngine(options) {
5872
6164
  store.setSpeaking([...new Set(identities.map((who) => personOf(who)))]);
5873
6165
  },
5874
6166
  quality(who, value) {
5875
- store.setQuality(personOf(who), value);
6167
+ const person = personOf(who);
6168
+ lkQuality.set(person, value);
6169
+ const local = store.snapshot().participants.find((p) => p.identity === person)?.isLocal === true;
6170
+ store.setQuality(person, value === "lost" ? value : effectiveQuality(person, who, local));
6171
+ },
6172
+ networkReading(reading) {
6173
+ const snap = store.snapshot();
6174
+ if (snap.phase !== "in_call" || snap.connection !== "stable") return;
6175
+ startHealth();
6176
+ const quality = assessor.push(reading);
6177
+ const path = pathOf(reading);
6178
+ patchLocalNetwork({ quality, ...path !== "unknown" ? { path } : {} });
6179
+ refreshQualities();
6180
+ const cameraOn = snap.media.camera.enabled;
6181
+ if (autoOff.step(quality, cameraOn) === "pause" && driver !== null) {
6182
+ const target = driver;
6183
+ report("info", "meet: the camera was paused because the connection is poor", "The audio is kept; Try video again turns it back on.");
6184
+ void enableTrack(target, "camera", false).then((state) => {
6185
+ if (!state.enabled) patchLocalNetwork({ videoPaused: true });
6186
+ });
6187
+ }
5876
6188
  },
5877
6189
  connectionState(state) {
5878
6190
  supervisor.driverState(state);
@@ -5967,6 +6279,22 @@ function createRoomEngine(options) {
5967
6279
  return;
5968
6280
  }
5969
6281
  const sender = personOf(from ?? "");
6282
+ if (from !== null) {
6283
+ const beat = topic === ALIVE_TOPIC ? decodeBeat(payload) : null;
6284
+ const before = liveness.reported(from);
6285
+ liveness.heard(from, beat);
6286
+ if (topic === ALIVE_TOPIC) {
6287
+ startHealth();
6288
+ if (beat !== null && (before?.q !== beat.q || before?.vp !== beat.vp || silent.has(from))) {
6289
+ silent.delete(from);
6290
+ projectRoster(liveRoster, new Date(now()).toISOString());
6291
+ refreshQualities();
6292
+ } else if (silent.delete(from)) {
6293
+ projectRoster(liveRoster, new Date(now()).toISOString());
6294
+ }
6295
+ return;
6296
+ }
6297
+ }
5970
6298
  switch (topic) {
5971
6299
  case DATA_TOPICS.reaction:
5972
6300
  if (typeof envelope.emoji === "string") {
@@ -6209,7 +6537,16 @@ function createRoomEngine(options) {
6209
6537
  store.setParticipants(
6210
6538
  [...people.values()].map(({ entry }) => {
6211
6539
  const projected = projectParticipant(entry, joinedAt);
6212
- return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
6540
+ if (entry.reconnecting === true || !entry.isLocal && silent.has(entry.identity)) {
6541
+ return { ...projected, reconnecting: true, quality: "lost" };
6542
+ }
6543
+ const videoPaused = entry.isLocal ? store.snapshot().localNetwork.videoPaused : liveness.reported(entry.identity)?.vp === true;
6544
+ const quality = effectiveQuality(projected.identity, entry.identity, entry.isLocal);
6545
+ return {
6546
+ ...projected,
6547
+ ...quality !== "unknown" && quality !== "lost" ? { quality } : {},
6548
+ ...videoPaused ? { videoPaused: true } : {}
6549
+ };
6213
6550
  })
6214
6551
  );
6215
6552
  scheduleGhostExpiry();
@@ -6395,7 +6732,12 @@ function createRoomEngine(options) {
6395
6732
  },
6396
6733
  async setCameraEnabled(enabled) {
6397
6734
  if (driver === null) return;
6398
- await enableTrack(driver, "camera", enabled);
6735
+ const state = await enableTrack(driver, "camera", enabled);
6736
+ if (enabled && state.enabled && store.snapshot().localNetwork.videoPaused) {
6737
+ autoOff.retried();
6738
+ patchLocalNetwork({ videoPaused: false });
6739
+ }
6740
+ if (!enabled && store.snapshot().localNetwork.videoPaused) patchLocalNetwork({ videoPaused: false });
6399
6741
  },
6400
6742
  async setScreenShareEnabled(enabled) {
6401
6743
  if (driver === null) return;
@@ -6441,7 +6783,8 @@ function createRoomEngine(options) {
6441
6783
  }
6442
6784
  },
6443
6785
  dismissShareNotice() {
6444
- if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
6786
+ const state = store.snapshot().share.state;
6787
+ if (state === "refused" || state === "ended") store.patch({ share: NO_SHARE });
6445
6788
  },
6446
6789
  async react(emoji) {
6447
6790
  const local = store.snapshot().localIdentity;
@@ -6919,6 +7262,30 @@ function createLiveKitDriver(handlers, options = {}) {
6919
7262
  let projectRoster = null;
6920
7263
  let audioCapture = DEFAULT_AUDIO_PROCESSING;
6921
7264
  let videoEffect = { kind: "none" };
7265
+ let statsTimer = null;
7266
+ function stopStatsPoll() {
7267
+ if (statsTimer !== null) clearInterval(statsTimer);
7268
+ statsTimer = null;
7269
+ }
7270
+ function startStatsPoll(target) {
7271
+ stopStatsPoll();
7272
+ if (handlers.networkReading === void 0) return;
7273
+ const sample = async () => {
7274
+ if (room !== target) return;
7275
+ const manager = target.engine.pcManager;
7276
+ if (manager === void 0) return;
7277
+ const readings = [];
7278
+ for (const transport of [manager.publisher, manager.subscriber]) {
7279
+ try {
7280
+ const report = await transport?.getStats();
7281
+ if (report !== void 0) readings.push(readingFromStats(report.values()));
7282
+ } catch {
7283
+ }
7284
+ }
7285
+ if (readings.length > 0 && room === target) handlers.networkReading?.(mergeReadings(readings));
7286
+ };
7287
+ statsTimer = setInterval(() => void sample(), 2e3);
7288
+ }
6922
7289
  async function load() {
6923
7290
  if (room !== null) return room;
6924
7291
  let module;
@@ -6993,6 +7360,7 @@ function createLiveKitDriver(handlers, options = {}) {
6993
7360
  participant?.identity ?? null
6994
7361
  );
6995
7362
  }).on(RoomEvent.Disconnected, (reason) => {
7363
+ stopStatsPoll();
6996
7364
  audioContainer?.remove();
6997
7365
  audioContainer = null;
6998
7366
  handlers.disconnected(reasonName(reason));
@@ -7078,6 +7446,7 @@ function createLiveKitDriver(handlers, options = {}) {
7078
7446
  projectRoster?.();
7079
7447
  handlers.roomMetadata?.(target.metadata ?? "");
7080
7448
  handlers.audioPlayback?.(target.canPlaybackAudio);
7449
+ startStatsPoll(target);
7081
7450
  },
7082
7451
  async disconnect() {
7083
7452
  if (room === null) return;
@@ -7088,6 +7457,7 @@ function createLiveKitDriver(handlers, options = {}) {
7088
7457
  } catch {
7089
7458
  }
7090
7459
  projectRoster = null;
7460
+ stopStatsPoll();
7091
7461
  audioContainer?.remove();
7092
7462
  audioContainer = null;
7093
7463
  await target.disconnect();
@@ -7682,7 +8052,7 @@ function observe(input) {
7682
8052
  const phase = observedPhase(input);
7683
8053
  const inCall = phase === "in_call";
7684
8054
  const actual = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
7685
- const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
8055
+ const canAdmit = snapshot !== null && mayAdmitNow(actual, snapshot.participants, snapshot.livePolicy);
7686
8056
  const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
7687
8057
  const deviceInPlay = phase === "joining" || inCall;
7688
8058
  const region = snapshot?.mediaRegion;
@@ -8538,6 +8908,13 @@ function requireSession(session, action) {
8538
8908
  if (session === null) throw noRuntime(action);
8539
8909
  return session;
8540
8910
  }
8911
+ function otherPresenterOf(snapshot) {
8912
+ if (snapshot === null || snapshot.presenter === null || snapshot.presenter === snapshot.localIdentity) return null;
8913
+ const rule = snapshot.livePolicy.shareConcurrency;
8914
+ if (rule === "multiple") return null;
8915
+ const who = snapshot.participants.find((p) => p.identity === snapshot.presenter);
8916
+ return who === void 0 ? null : { name: who.displayName, rule };
8917
+ }
8541
8918
  function useRoom() {
8542
8919
  const host = useMeetHost();
8543
8920
  const snapshot = useMeetSnapshot();
@@ -8552,7 +8929,9 @@ function useRoom() {
8552
8929
  secondDevice: snapshot?.secondDevice ?? null,
8553
8930
  local,
8554
8931
  // An observer (role `viewer`) publishes nothing: no Share screen to press and be refused (MD-16).
8555
- canShareScreen: (engine?.supportsScreenShare() ?? true) && !isObserverRole(snapshot?.grants.role ?? null),
8932
+ sharingAllowed: snapshot === null || canShareUnder(snapshot.livePolicy, effectiveRole(snapshot), effectiveStanding(snapshot)),
8933
+ canShareScreen: (engine?.supportsScreenShare() ?? true) && !isObserverRole(snapshot?.grants.role ?? null) && (snapshot === null || canShareUnder(snapshot.livePolicy, effectiveRole(snapshot), effectiveStanding(snapshot))),
8934
+ otherPresenter: otherPresenterOf(snapshot),
8556
8935
  join: useCallback2(
8557
8936
  async (request) => requireEngine(engine, "join").join(request),
8558
8937
  [engine]
@@ -8703,7 +9082,7 @@ function useLobby() {
8703
9082
  declined: (snapshot?.lobby ?? []).filter((entry) => entry.state === "denied"),
8704
9083
  // No runtime means no host powers — `<LobbyPanel>` renders nothing rather
8705
9084
  // than an admit button that would throw.
8706
- canAdmit: session !== null && isHost,
9085
+ canAdmit: session !== null && (isHost || snapshot !== null && snapshot.phase === "in_call" && mayAdmitNow(snapshot.participants.find((p) => p.identity === snapshot.localIdentity)?.role ?? "none", snapshot.participants, snapshot.livePolicy)),
8707
9086
  admit: useCallback2(
8708
9087
  async (entry) => requireSession(session, "admit").admit(entry, true),
8709
9088
  [session]
@@ -11577,7 +11956,8 @@ function ParticipantTile({
11577
11956
  participant.isLocal ? " (you)" : "",
11578
11957
  participant.isAgent ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "AI" }) : null,
11579
11958
  /* @__PURE__ */ jsx6(RoleBadge, { role: participant.role, standing: participant.standing }),
11580
- participant.reconnecting === true ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Reconnecting\u2026" }) : null
11959
+ participant.reconnecting === true ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Reconnecting\u2026" }) : null,
11960
+ participant.reconnecting !== true && participant.videoPaused === true && !participant.isLocal ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", "data-meet-video-paused": "", children: "Video paused to save audio" }) : null
11581
11961
  ] }),
11582
11962
  !participant.micEnabled ? /* @__PURE__ */ jsx6(MicOffIcon, { width: 14, height: 14 }) : null,
11583
11963
  /* @__PURE__ */ jsx6(QualityBars, { quality: participant.quality })
@@ -11667,6 +12047,8 @@ function ControlBar({ onLeave }) {
11667
12047
  const queue = useHandQueue();
11668
12048
  const micUnavailable = useLocalMedia().microphone.unavailable;
11669
12049
  const live = useLivePolicy();
12050
+ const core = useMeetCore();
12051
+ const [askPresentInstead, setAskPresentInstead] = useState5(false);
11670
12052
  const unread = useUnreadChat(panel === "chat");
11671
12053
  const [whiteboard, setWhiteboard] = useState5(false);
11672
12054
  const app = useMeetAppPanel();
@@ -11755,15 +12137,39 @@ function ControlBar({ onLeave }) {
11755
12137
  }
11756
12138
  ),
11757
12139
  /* @__PURE__ */ jsx6(CaptionsControl, {}),
11758
- room.canShareScreen || room.local?.screenSharing === true ? /* @__PURE__ */ jsx6(
11759
- ControlButton,
11760
- {
11761
- label: room.local?.screenSharing === true ? "Stop sharing" : "Share screen",
11762
- active: room.local?.screenSharing === true,
11763
- onClick: () => void room.toggleScreenShare(),
11764
- children: /* @__PURE__ */ jsx6(ScreenShareIcon, {})
11765
- }
11766
- ) : null,
12140
+ room.canShareScreen || room.local?.screenSharing === true && core.share.state !== "ended" ? /* @__PURE__ */ jsxs4("span", { className: "mx-meet__anchor", children: [
12141
+ /* @__PURE__ */ jsx6(
12142
+ ControlButton,
12143
+ {
12144
+ label: room.local?.screenSharing === true ? "Stop sharing" : "Share screen",
12145
+ active: room.local?.screenSharing === true,
12146
+ onClick: () => {
12147
+ if (room.local?.screenSharing !== true && room.otherPresenter !== null) setAskPresentInstead(true);
12148
+ else void room.toggleScreenShare();
12149
+ },
12150
+ children: /* @__PURE__ */ jsx6(ScreenShareIcon, {})
12151
+ }
12152
+ ),
12153
+ askPresentInstead && room.otherPresenter !== null ? /* @__PURE__ */ jsxs4("div", { className: "mx-meet__confirm", role: "group", "aria-label": "Someone else is presenting", "data-meet-notice": "share-takeover", children: [
12154
+ /* @__PURE__ */ jsxs4("span", { className: "mx-meet__confirm-text", children: [
12155
+ room.otherPresenter.name,
12156
+ " is presenting"
12157
+ ] }),
12158
+ /* @__PURE__ */ jsx6(
12159
+ "button",
12160
+ {
12161
+ type: "button",
12162
+ className: "mx-meet__primary mx-meet__primary--small",
12163
+ onClick: () => {
12164
+ setAskPresentInstead(false);
12165
+ void room.toggleScreenShare();
12166
+ },
12167
+ children: room.otherPresenter.rule === "one_takeover" ? "Present instead" : "Share anyway"
12168
+ }
12169
+ ),
12170
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: () => setAskPresentInstead(false), children: "Cancel" })
12171
+ ] }) : null
12172
+ ] }) : null,
11767
12173
  /* @__PURE__ */ jsxs4("span", { className: "mx-meet__anchor", children: [
11768
12174
  /* @__PURE__ */ jsx6(
11769
12175
  ControlButton,
@@ -11860,7 +12266,7 @@ function ControlBar({ onLeave }) {
11860
12266
  ]
11861
12267
  }
11862
12268
  ),
11863
- !room.canShareScreen && room.local?.screenSharing !== true ? (
12269
+ !room.canShareScreen && room.sharingAllowed && room.local?.screenSharing !== true ? (
11864
12270
  // `share-unsupported-mobile`: the control is absent; the menu says where it lives.
11865
12271
  /* @__PURE__ */ jsxs4("p", { className: "mx-meet__menu-note", role: "note", "data-meet-notice": "share-unsupported", children: [
11866
12272
  /* @__PURE__ */ jsx6(ScreenShareIcon, { width: 16, height: 16 }),
@@ -13865,13 +14271,53 @@ function MeetSkinRoom({
13865
14271
  ] }),
13866
14272
  core.connectionBanner && core.connection !== "stable" ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": `connection-${core.connection}`, children: CONNECTION_BANNER[core.connection] }) : null,
13867
14273
  /* @__PURE__ */ jsx6(ShareNotice, {}),
14274
+ /* @__PURE__ */ jsx6(NetworkNotice, {}),
13868
14275
  /* @__PURE__ */ jsx6(Stage, {}),
13869
14276
  /* @__PURE__ */ jsx6(Captions, {}),
13870
14277
  /* @__PURE__ */ jsx6(ControlBar, { ...onLeave !== void 0 ? { onLeave } : {} })
13871
14278
  ] });
13872
14279
  }
14280
+ var RESTRICTED_NOTICE_MS = 3e4;
14281
+ function NetworkNotice() {
14282
+ const { snapshot, connectionBanner } = useMeetCore();
14283
+ const room = useRoom();
14284
+ const network = snapshot?.localNetwork;
14285
+ const [closed, setClosed] = useState5(() => /* @__PURE__ */ new Set());
14286
+ const [restrictedSince, setRestrictedSince] = useState5(null);
14287
+ const [, setTick] = useState5(0);
14288
+ const restricted = network?.path === "restricted";
14289
+ useEffect6(() => {
14290
+ if (!restricted || restrictedSince !== null) return;
14291
+ setRestrictedSince(Date.now());
14292
+ const timer = setTimeout(() => setTick((n) => n + 1), RESTRICTED_NOTICE_MS + 50);
14293
+ return () => clearTimeout(timer);
14294
+ }, [restricted, restrictedSince]);
14295
+ if (network === void 0 || snapshot?.phase !== "in_call" || connectionBanner) return null;
14296
+ const close = (code) => () => setClosed((prev) => /* @__PURE__ */ new Set([...prev, code]));
14297
+ const cameraOn = room.local?.cameraEnabled === true;
14298
+ if (network.videoPaused && !cameraOn) {
14299
+ return /* @__PURE__ */ jsxs4("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "video-paused", children: [
14300
+ "Your video is paused to save your audio",
14301
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: () => void room.toggleCamera(), children: "Try video again" })
14302
+ ] });
14303
+ }
14304
+ if (network.quality === "poor" && !closed.has("connection-poor")) {
14305
+ return /* @__PURE__ */ jsxs4("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "connection-poor", children: [
14306
+ "Your connection is unstable",
14307
+ cameraOn ? /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: () => void room.toggleCamera(), children: "Turn off camera" }) : null,
14308
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: close("connection-poor"), children: "Dismiss" })
14309
+ ] });
14310
+ }
14311
+ if (restricted && !closed.has("restricted-network") && restrictedSince !== null && Date.now() - restrictedSince < RESTRICTED_NOTICE_MS) {
14312
+ return /* @__PURE__ */ jsxs4("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "restricted-network", children: [
14313
+ "Restricted network \xB7 quality may be reduced",
14314
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: close("restricted-network"), children: "Dismiss" })
14315
+ ] });
14316
+ }
14317
+ return null;
14318
+ }
13873
14319
  function ShareNotice() {
13874
- const { share, actions } = useMeetCore();
14320
+ const { share, actions, snapshot } = useMeetCore();
13875
14321
  const [soundHintClosed, setSoundHintClosed] = useState5(null);
13876
14322
  if (share.state === "presenting" && share.audio === false && soundHintClosed !== share.at) {
13877
14323
  return /* @__PURE__ */ jsxs4("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-audio-missing", children: [
@@ -13880,6 +14326,16 @@ function ShareNotice() {
13880
14326
  /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: () => setSoundHintClosed(share.at), children: "Dismiss" })
13881
14327
  ] });
13882
14328
  }
14329
+ if (share.state === "ended") {
14330
+ const replaced = share.reason === "replaced";
14331
+ const rule = snapshot?.livePolicy.shareWhoCan ?? "host_only";
14332
+ const why = replaced ? `${share.by ?? "Someone"} is now presenting` : rule === "presenters" ? "Sharing is turned off. Only presenters can share their screen" : rule === "org" ? "Sharing is turned off. Only people in the organization can share their screen" : "Sharing is turned off. Only the host can share their screen";
14333
+ return /* @__PURE__ */ jsxs4("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": replaced ? "share-ended" : "share-unavailable", children: [
14334
+ replaced ? "Your presentation ended" : "Your presentation was stopped",
14335
+ /* @__PURE__ */ jsx6("span", { className: "mx-meet__remedy", children: why }),
14336
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__link", onClick: () => actions.dismissShareNotice(), children: "Dismiss" })
14337
+ ] });
14338
+ }
13883
14339
  if (share.state !== "refused") return null;
13884
14340
  const text3 = share.reason === "unsupported" ? "You can't share your screen from this browser" : share.reason === "blocked" ? "Screen sharing is blocked" : "Couldn't start sharing your screen";
13885
14341
  return /* @__PURE__ */ jsxs4("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-unavailable", children: [
@@ -14298,6 +14754,7 @@ function MeetingRecordBody({
14298
14754
  }
14299
14755
  if (record.loading || record.record === null) {
14300
14756
  return /* @__PURE__ */ jsx7("div", { className: "mx-meet mx-meet--record", children: /* @__PURE__ */ jsxs5("div", { className: "mx-meet__empty", role: "status", children: [
14757
+ /* @__PURE__ */ jsx7("p", { className: "mx-meet__empty-title", "data-meet-notice": "meeting-ended", children: "The meeting has ended" }),
14301
14758
  /* @__PURE__ */ jsx7("p", { className: "mx-meet__empty-title", children: "Opening the meeting record\u2026" }),
14302
14759
  /* @__PURE__ */ jsx7("p", { className: "mx-meet__empty-body", children: "Reading the summary, the notes and the transcript this meeting left behind." })
14303
14760
  ] }) });