@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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog — `@ai-matrx/meet`
2
2
 
3
+ ## 0.11.49
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/meet/v0.11.48..npm/meet/v0.11.49 -- apps/shared/meet`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.11.48
10
+
11
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
12
+ `git diff npm/meet/v0.11.47..npm/meet/v0.11.48 -- apps/shared/meet`).
13
+ No source changes intended and no consumer action required.
14
+
3
15
  ## 0.11.47
4
16
 
5
17
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -150,6 +150,11 @@ interface MeetParticipant {
150
150
  * `reconnecting` tile for `reconnect_grace_seconds`. Absent/false = a live participant.
151
151
  */
152
152
  readonly reconnecting?: boolean | undefined;
153
+ /**
154
+ * P1 `net-video-auto-off`: this person's camera is paused by their network (to keep their
155
+ * audio), not by their choice — from their own heartbeat. Absent/false = not paused.
156
+ */
157
+ readonly videoPaused?: boolean | undefined;
153
158
  readonly joinedAt: string;
154
159
  /**
155
160
  * LiveKit participant attributes, verbatim. The note-taker announces itself
@@ -150,6 +150,11 @@ interface MeetParticipant {
150
150
  * `reconnecting` tile for `reconnect_grace_seconds`. Absent/false = a live participant.
151
151
  */
152
152
  readonly reconnecting?: boolean | undefined;
153
+ /**
154
+ * P1 `net-video-auto-off`: this person's camera is paused by their network (to keep their
155
+ * audio), not by their choice — from their own heartbeat. Absent/false = not paused.
156
+ */
157
+ readonly videoPaused?: boolean | undefined;
153
158
  readonly joinedAt: string;
154
159
  /**
155
160
  * LiveKit participant attributes, verbatim. The note-taker announces itself
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { M as MeetingRecord, P as ParticipantIdentity, R as RoomName, a as MeetParticipant } from './collab-D1TX5uf9.js';
2
+ import { M as MeetingRecord, P as ParticipantIdentity, R as RoomName, a as MeetParticipant } from './collab-Dk3r37MW.js';
3
3
 
4
4
  /**
5
5
  * THE PRODUCT UI (R6/R7). Not a headless kernel with a note telling the host to
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { M as MeetingRecord, P as ParticipantIdentity, R as RoomName, a as MeetParticipant } from './collab-D1TX5uf9.cjs';
2
+ import { M as MeetingRecord, P as ParticipantIdentity, R as RoomName, a as MeetParticipant } from './collab-Dk3r37MW.cjs';
3
3
 
4
4
  /**
5
5
  * THE PRODUCT UI (R6/R7). Not a headless kernel with a note telling the host to
package/dist/core.cjs CHANGED
@@ -496,10 +496,11 @@ function invalidResponse(operation, detail) {
496
496
 
497
497
  // src/core/connection-supervisor.ts
498
498
  var DEFAULT_CONNECTION_POLICY = {
499
- bannerDelayMs: 2e3,
499
+ bannerDelayMs: 5e3,
500
500
  autoRejoin: true,
501
501
  autoRejoinMaxAttempts: 5,
502
- reconnectGraceSeconds: 30
502
+ reconnectGraceSeconds: 30,
503
+ videoAutoOffAfterPoorSeconds: 20
503
504
  };
504
505
  function clampInt(value, min, max, fallback) {
505
506
  if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
@@ -513,7 +514,8 @@ function connectionPolicyFrom(raw) {
513
514
  bannerDelayMs: clampInt(row["reconnect_banner_delay_ms"], 0, 1e4, d.bannerDelayMs),
514
515
  autoRejoin: typeof row["auto_rejoin"] === "boolean" ? row["auto_rejoin"] : d.autoRejoin,
515
516
  autoRejoinMaxAttempts: clampInt(row["auto_rejoin_max_attempts"], 0, 20, d.autoRejoinMaxAttempts),
516
- reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds)
517
+ reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds),
518
+ videoAutoOffAfterPoorSeconds: clampInt(row["video_auto_off_after_poor_seconds"], 0, 600, d.videoAutoOffAfterPoorSeconds)
517
519
  };
518
520
  }
519
521
  var LIVEKIT_BASE_DELAYS_MS = [0, 300, 1200, 2700, 4800, 7e3];
@@ -3784,11 +3786,14 @@ function recordingLabel(state) {
3784
3786
 
3785
3787
  // src/core/live-policy.ts
3786
3788
  var DEFAULT_LIVE_POLICY = {
3789
+ headlessPowers: "members_admit",
3787
3790
  chatMode: "everyone",
3788
3791
  avLock: "off",
3789
3792
  timeLimitMinutes: 0,
3790
3793
  timeLimitWarningMinutes: 5,
3791
- recordingConsent: "notice"
3794
+ recordingConsent: "notice",
3795
+ shareWhoCan: "everyone",
3796
+ shareConcurrency: "one_takeover"
3792
3797
  };
3793
3798
  function oneOf(value, allowed, fallback) {
3794
3799
  return typeof value === "string" && allowed.includes(value) ? value : fallback;
@@ -3800,6 +3805,7 @@ function whole(value, fallback) {
3800
3805
  function livePolicyFrom(raw) {
3801
3806
  const o = typeof raw === "object" && raw !== null ? raw : {};
3802
3807
  return {
3808
+ headlessPowers: oneOf(o["headless_powers"], ["members_admit", "none", "cohosts_only"], DEFAULT_LIVE_POLICY.headlessPowers),
3803
3809
  chatMode: oneOf(o["chat_mode"], ["everyone", "hosts_only", "off"], DEFAULT_LIVE_POLICY.chatMode),
3804
3810
  avLock: oneOf(o["attendee_av_lock"], ["off", "mic", "camera", "both"], DEFAULT_LIVE_POLICY.avLock),
3805
3811
  timeLimitMinutes: whole(o["time_limit_minutes"], 0),
@@ -3808,11 +3814,13 @@ function livePolicyFrom(raw) {
3808
3814
  o["recording_consent"],
3809
3815
  ["notice", "ok_or_leave", "view_only_until_agree"],
3810
3816
  DEFAULT_LIVE_POLICY.recordingConsent
3811
- )
3817
+ ),
3818
+ shareWhoCan: oneOf(o["share_who_can"], ["everyone", "host_only", "org", "presenters"], DEFAULT_LIVE_POLICY.shareWhoCan),
3819
+ shareConcurrency: oneOf(o["share_concurrency"], ["one_takeover", "one_replace", "multiple"], DEFAULT_LIVE_POLICY.shareConcurrency)
3812
3820
  };
3813
3821
  }
3814
3822
  function samePolicy(a, b) {
3815
- return a.chatMode === b.chatMode && a.avLock === b.avLock && a.timeLimitMinutes === b.timeLimitMinutes && a.timeLimitWarningMinutes === b.timeLimitWarningMinutes && a.recordingConsent === b.recordingConsent;
3823
+ 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;
3816
3824
  }
3817
3825
  var MODERATORS = /* @__PURE__ */ new Set(["host", "cohost"]);
3818
3826
  function isLockedFor(policy, role, kind) {
@@ -3822,6 +3830,12 @@ function isLockedFor(policy, role, kind) {
3822
3830
  function consentPending(policy, role, recordingOn, given) {
3823
3831
  return recordingOn && !given && policy.recordingConsent !== "notice" && !MODERATORS.has(role);
3824
3832
  }
3833
+ function mayAdmitNow(role, participants, policy) {
3834
+ if (role === "none") return false;
3835
+ if (role === "host" || role === "cohost") return true;
3836
+ const hostIn = participants.some((p) => p.role === "host" || p.role === "cohost");
3837
+ return !hostIn && policy.headlessPowers === "members_admit";
3838
+ }
3825
3839
 
3826
3840
  // src/core/attendance.ts
3827
3841
  var IDLE_PREVIEW = { state: "idle", unavailable: null };
@@ -3861,6 +3875,7 @@ var NO_GRANTS = {
3861
3875
  canPublish: false,
3862
3876
  canSubscribe: false
3863
3877
  };
3878
+ var UNKNOWN_LOCAL_NETWORK = { path: "unknown", quality: "unknown", videoPaused: false };
3864
3879
  var NO_SHARE = { state: "none", reason: null, remedy: null, at: 0 };
3865
3880
  var EMPTY = {
3866
3881
  phase: "resolving",
@@ -3869,6 +3884,7 @@ var EMPTY = {
3869
3884
  roomName: null,
3870
3885
  connection: "stable",
3871
3886
  connectionBanner: false,
3887
+ localNetwork: UNKNOWN_LOCAL_NETWORK,
3872
3888
  share: NO_SHARE,
3873
3889
  call: null,
3874
3890
  waiting: null,
@@ -4383,6 +4399,14 @@ function applyModeration(store, payload, now) {
4383
4399
  }
4384
4400
  return HANDLED;
4385
4401
  }
4402
+ case "share-ended": {
4403
+ const reason = text2(payload["reason"]) === "not_allowed" ? "not_allowed" : "replaced";
4404
+ const by = text2(payload["by"]);
4405
+ store.patch({
4406
+ share: { state: "ended", reason, remedy: null, at: now(), ...by !== null ? { by } : {} }
4407
+ });
4408
+ return HANDLED;
4409
+ }
4386
4410
  case "muted":
4387
4411
  case "ask-unmute":
4388
4412
  case "removed": {
@@ -4818,6 +4842,187 @@ var TRANSCRIPTION_ATTRIBUTES = {
4818
4842
  speakerIdentity: "lk.transcribed_participant_identity"
4819
4843
  };
4820
4844
 
4845
+ // src/core/network-health.ts
4846
+ function pathOf(reading) {
4847
+ if (reading.relayed === null && reading.tcp === null) return "unknown";
4848
+ return reading.relayed === true || reading.tcp === true ? "restricted" : "direct";
4849
+ }
4850
+ var POOR_RTT_MS = 300;
4851
+ var RECOVERED_RTT_MS = 220;
4852
+ var POOR_LOSS = 0.08;
4853
+ var RECOVERED_LOSS = 0.03;
4854
+ function createSelfAssessor(options = {}) {
4855
+ const poorSamples = options.poorSamples ?? 2;
4856
+ const goodSamples = options.goodSamples ?? 3;
4857
+ let quality = "unknown";
4858
+ let badRun = 0;
4859
+ let goodRun = 0;
4860
+ return {
4861
+ push(reading) {
4862
+ const { rttMs, lossRatio } = reading;
4863
+ if (rttMs === null && lossRatio === null) return quality;
4864
+ const bad = rttMs !== null && rttMs >= POOR_RTT_MS || lossRatio !== null && lossRatio >= POOR_LOSS;
4865
+ const clear = (rttMs === null || rttMs < RECOVERED_RTT_MS) && (lossRatio === null || lossRatio < RECOVERED_LOSS);
4866
+ badRun = bad ? badRun + 1 : 0;
4867
+ goodRun = clear ? goodRun + 1 : 0;
4868
+ if (badRun >= poorSamples) quality = "poor";
4869
+ else if (quality !== "poor" && !bad) quality = "good";
4870
+ else if (quality === "poor" && goodRun >= goodSamples) quality = "good";
4871
+ return quality;
4872
+ },
4873
+ current: () => quality,
4874
+ reset() {
4875
+ quality = "unknown";
4876
+ badRun = 0;
4877
+ goodRun = 0;
4878
+ }
4879
+ };
4880
+ }
4881
+ var RANK = {
4882
+ lost: 0,
4883
+ poor: 1,
4884
+ good: 2,
4885
+ excellent: 3,
4886
+ unknown: 4
4887
+ };
4888
+ function worstQuality(a, b) {
4889
+ if (b === null || b === void 0 || b === "unknown") return a;
4890
+ if (a === "unknown") return b;
4891
+ return RANK[b] < RANK[a] ? b : a;
4892
+ }
4893
+ var ALIVE_TOPIC = "mx.alive";
4894
+ function encodeBeat(beat) {
4895
+ return JSON.stringify({ q: beat.q, vp: beat.vp });
4896
+ }
4897
+ function decodeBeat(payload) {
4898
+ try {
4899
+ const raw = JSON.parse(payload);
4900
+ const q = raw.q === "good" || raw.q === "poor" ? raw.q : "unknown";
4901
+ return { q, vp: raw.vp === true };
4902
+ } catch {
4903
+ return null;
4904
+ }
4905
+ }
4906
+ function heartbeatIntervalMs(roomSize) {
4907
+ return 2e3 * Math.max(1, Math.ceil(Math.max(1, roomSize) / 25));
4908
+ }
4909
+ function staleAfterMs(roomSize) {
4910
+ return heartbeatIntervalMs(roomSize) * 3 + 1e3;
4911
+ }
4912
+ function createLivenessTracker(now) {
4913
+ const lastHeard = /* @__PURE__ */ new Map();
4914
+ const beats = /* @__PURE__ */ new Map();
4915
+ return {
4916
+ heard(who, beat) {
4917
+ lastHeard.set(who, now());
4918
+ if (beat !== null) beats.set(who, beat);
4919
+ },
4920
+ stale(present, roomSize) {
4921
+ const limit = staleAfterMs(roomSize);
4922
+ const at = now();
4923
+ const out = /* @__PURE__ */ new Set();
4924
+ for (const who of present) {
4925
+ const heard = lastHeard.get(who);
4926
+ if (heard !== void 0 && at - heard > limit) out.add(who);
4927
+ }
4928
+ return out;
4929
+ },
4930
+ reported: (who) => beats.get(who) ?? null,
4931
+ restartClock() {
4932
+ const at = now();
4933
+ for (const who of lastHeard.keys()) lastHeard.set(who, at);
4934
+ },
4935
+ forget(who) {
4936
+ lastHeard.delete(who);
4937
+ beats.delete(who);
4938
+ },
4939
+ clear() {
4940
+ lastHeard.clear();
4941
+ beats.clear();
4942
+ }
4943
+ };
4944
+ }
4945
+ function createVideoAutoOff(afterMs, now) {
4946
+ let poorSince = null;
4947
+ let armed = true;
4948
+ return {
4949
+ step(quality, cameraOn) {
4950
+ if (quality === "good") {
4951
+ poorSince = null;
4952
+ armed = true;
4953
+ return null;
4954
+ }
4955
+ if (quality !== "poor" || !cameraOn) {
4956
+ poorSince = quality === "poor" ? poorSince : null;
4957
+ return null;
4958
+ }
4959
+ const at = now();
4960
+ if (poorSince === null) poorSince = at;
4961
+ const limit = afterMs();
4962
+ if (!armed || limit <= 0 || at - poorSince < limit) return null;
4963
+ armed = false;
4964
+ return "pause";
4965
+ },
4966
+ retried() {
4967
+ armed = false;
4968
+ },
4969
+ reset() {
4970
+ poorSince = null;
4971
+ armed = true;
4972
+ }
4973
+ };
4974
+ }
4975
+ function readingFromStats(entries) {
4976
+ const byId2 = /* @__PURE__ */ new Map();
4977
+ for (const entry of entries) byId2.set(entry.id, entry);
4978
+ let pair;
4979
+ for (const entry of byId2.values()) {
4980
+ if (entry.type === "transport" && typeof entry["selectedCandidatePairId"] === "string") {
4981
+ pair = byId2.get(entry["selectedCandidatePairId"]);
4982
+ if (pair !== void 0) break;
4983
+ }
4984
+ }
4985
+ if (pair === void 0) {
4986
+ for (const entry of byId2.values()) {
4987
+ if (entry.type === "candidate-pair" && entry["nominated"] === true && entry["state"] === "succeeded") {
4988
+ pair = entry;
4989
+ break;
4990
+ }
4991
+ }
4992
+ }
4993
+ let lossRatio = null;
4994
+ for (const entry of byId2.values()) {
4995
+ if (entry.type === "remote-inbound-rtp" && typeof entry["fractionLost"] === "number") {
4996
+ lossRatio = Math.max(lossRatio ?? 0, entry["fractionLost"]);
4997
+ }
4998
+ }
4999
+ if (pair === void 0) return { rttMs: null, lossRatio, relayed: null, tcp: null };
5000
+ const rtt = pair["currentRoundTripTime"];
5001
+ const local = typeof pair["localCandidateId"] === "string" ? byId2.get(pair["localCandidateId"]) : void 0;
5002
+ const candidateType = local?.["candidateType"];
5003
+ const protocol = local?.["protocol"];
5004
+ const relayProtocol = local?.["relayProtocol"];
5005
+ return {
5006
+ rttMs: typeof rtt === "number" ? Math.round(rtt * 1e3) : null,
5007
+ lossRatio,
5008
+ relayed: typeof candidateType === "string" ? candidateType === "relay" : null,
5009
+ tcp: typeof protocol === "string" || typeof relayProtocol === "string" ? protocol === "tcp" || relayProtocol === "tcp" || relayProtocol === "tls" : null
5010
+ };
5011
+ }
5012
+ function mergeReadings(readings) {
5013
+ const max = (values) => {
5014
+ const known = values.filter((v) => v !== null);
5015
+ return known.length === 0 ? null : Math.max(...known);
5016
+ };
5017
+ const any = (values) => values.every((v) => v === null) ? null : values.some((v) => v === true);
5018
+ return {
5019
+ rttMs: max(readings.map((r) => r.rttMs)),
5020
+ lossRatio: max(readings.map((r) => r.lossRatio)),
5021
+ relayed: any(readings.map((r) => r.relayed)),
5022
+ tcp: any(readings.map((r) => r.tcp))
5023
+ };
5024
+ }
5025
+
4821
5026
  // src/core/livekit-driver.ts
4822
5027
  function mapQuality(quality) {
4823
5028
  switch (quality) {
@@ -4907,6 +5112,30 @@ function createLiveKitDriver(handlers, options = {}) {
4907
5112
  let projectRoster = null;
4908
5113
  let audioCapture = DEFAULT_AUDIO_PROCESSING;
4909
5114
  let videoEffect = { kind: "none" };
5115
+ let statsTimer = null;
5116
+ function stopStatsPoll() {
5117
+ if (statsTimer !== null) clearInterval(statsTimer);
5118
+ statsTimer = null;
5119
+ }
5120
+ function startStatsPoll(target) {
5121
+ stopStatsPoll();
5122
+ if (handlers.networkReading === void 0) return;
5123
+ const sample = async () => {
5124
+ if (room !== target) return;
5125
+ const manager = target.engine.pcManager;
5126
+ if (manager === void 0) return;
5127
+ const readings = [];
5128
+ for (const transport of [manager.publisher, manager.subscriber]) {
5129
+ try {
5130
+ const report = await transport?.getStats();
5131
+ if (report !== void 0) readings.push(readingFromStats(report.values()));
5132
+ } catch {
5133
+ }
5134
+ }
5135
+ if (readings.length > 0 && room === target) handlers.networkReading?.(mergeReadings(readings));
5136
+ };
5137
+ statsTimer = setInterval(() => void sample(), 2e3);
5138
+ }
4910
5139
  async function load() {
4911
5140
  if (room !== null) return room;
4912
5141
  let module2;
@@ -4981,6 +5210,7 @@ function createLiveKitDriver(handlers, options = {}) {
4981
5210
  participant?.identity ?? null
4982
5211
  );
4983
5212
  }).on(RoomEvent.Disconnected, (reason) => {
5213
+ stopStatsPoll();
4984
5214
  audioContainer?.remove();
4985
5215
  audioContainer = null;
4986
5216
  handlers.disconnected(reasonName(reason));
@@ -5066,6 +5296,7 @@ function createLiveKitDriver(handlers, options = {}) {
5066
5296
  projectRoster?.();
5067
5297
  handlers.roomMetadata?.(target.metadata ?? "");
5068
5298
  handlers.audioPlayback?.(target.canPlaybackAudio);
5299
+ startStatsPoll(target);
5069
5300
  },
5070
5301
  async disconnect() {
5071
5302
  if (room === null) return;
@@ -5076,6 +5307,7 @@ function createLiveKitDriver(handlers, options = {}) {
5076
5307
  } catch {
5077
5308
  }
5078
5309
  projectRoster = null;
5310
+ stopStatsPoll();
5079
5311
  audioContainer?.remove();
5080
5312
  audioContainer = null;
5081
5313
  await target.disconnect();
@@ -6852,6 +7084,76 @@ function createRoomEngine(options) {
6852
7084
  return personConnections.get(personOf(who)) ?? [who];
6853
7085
  }
6854
7086
  const departures = /* @__PURE__ */ new Map();
7087
+ const liveness = createLivenessTracker(now);
7088
+ const assessor = createSelfAssessor();
7089
+ const autoOff = createVideoAutoOff(() => connectionPolicy.videoAutoOffAfterPoorSeconds * 1e3, now);
7090
+ const lkQuality = /* @__PURE__ */ new Map();
7091
+ let silent = /* @__PURE__ */ new Set();
7092
+ let healthTimer = null;
7093
+ let lastBeatAt = 0;
7094
+ let wasUnstable = false;
7095
+ function patchLocalNetwork(next) {
7096
+ const before = store.snapshot().localNetwork;
7097
+ const merged = { ...before, ...next };
7098
+ if (merged.path !== before.path || merged.quality !== before.quality || merged.videoPaused !== before.videoPaused) {
7099
+ store.patch({ localNetwork: merged });
7100
+ }
7101
+ }
7102
+ function effectiveQuality(person, connection, isLocal) {
7103
+ const fromServer = lkQuality.get(person) ?? "unknown";
7104
+ if (isLocal) return worstQuality(fromServer, assessor.current());
7105
+ return worstQuality(fromServer, connection === void 0 ? null : liveness.reported(connection)?.q);
7106
+ }
7107
+ function refreshQualities() {
7108
+ const snap = store.snapshot();
7109
+ for (const p of snap.participants) {
7110
+ if (p.reconnecting === true) continue;
7111
+ const next = effectiveQuality(p.identity, mainConnection.get(p.identity), p.isLocal);
7112
+ if (next !== "unknown" && next !== p.quality) store.setQuality(p.identity, next);
7113
+ }
7114
+ }
7115
+ function stopHealth() {
7116
+ if (healthTimer !== null) clearInterval(healthTimer);
7117
+ healthTimer = null;
7118
+ liveness.clear();
7119
+ assessor.reset();
7120
+ autoOff.reset();
7121
+ silent = /* @__PURE__ */ new Set();
7122
+ lkQuality.clear();
7123
+ lastBeatAt = 0;
7124
+ wasUnstable = false;
7125
+ if (store.snapshot().localNetwork !== UNKNOWN_LOCAL_NETWORK) store.patch({ localNetwork: UNKNOWN_LOCAL_NETWORK });
7126
+ }
7127
+ function healthTick() {
7128
+ const snap = store.snapshot();
7129
+ const target = driver;
7130
+ if (disposed || target === null || snap.phase !== "in_call") return;
7131
+ if (snap.connection !== "stable") {
7132
+ wasUnstable = true;
7133
+ return;
7134
+ }
7135
+ if (wasUnstable) {
7136
+ wasUnstable = false;
7137
+ liveness.restartClock();
7138
+ }
7139
+ const size = liveRoster.length;
7140
+ const at = now();
7141
+ if (at - lastBeatAt >= heartbeatIntervalMs(size)) {
7142
+ lastBeatAt = at;
7143
+ const beat = encodeBeat({ q: assessor.current(), vp: snap.localNetwork.videoPaused });
7144
+ void target.publishData(ALIVE_TOPIC, beat).catch(() => void 0);
7145
+ }
7146
+ const remotes = liveRoster.filter((p) => !p.isLocal).map((p) => p.identity);
7147
+ const next = liveness.stale(remotes, size);
7148
+ if (next.size !== silent.size || [...next].some((who) => !silent.has(who))) {
7149
+ silent = next;
7150
+ projectRoster(liveRoster, new Date(at).toISOString());
7151
+ }
7152
+ }
7153
+ function startHealth() {
7154
+ if (healthTimer !== null) return;
7155
+ healthTimer = setInterval(healthTick, 1e3);
7156
+ }
6855
7157
  function requestAsNow() {
6856
7158
  if (lastRequest === null) return null;
6857
7159
  const media2 = store.snapshot().media;
@@ -6870,6 +7172,7 @@ function createRoomEngine(options) {
6870
7172
  media.onDisconnected();
6871
7173
  store.patch({ share: NO_SHARE, connection: "stable" });
6872
7174
  clearGhosts();
7175
+ stopHealth();
6873
7176
  await target?.disconnect();
6874
7177
  }
6875
7178
  async function leaveRoom() {
@@ -6961,6 +7264,8 @@ function createRoomEngine(options) {
6961
7264
  },
6962
7265
  participantLeft(who, reason) {
6963
7266
  departures.set(who, reason);
7267
+ liveness.forget(who);
7268
+ silent.delete(who);
6964
7269
  },
6965
7270
  roomMetadata(metadata) {
6966
7271
  let parsed = {};
@@ -6994,7 +7299,27 @@ function createRoomEngine(options) {
6994
7299
  store.setSpeaking([...new Set(identities.map((who) => personOf(who)))]);
6995
7300
  },
6996
7301
  quality(who, value) {
6997
- store.setQuality(personOf(who), value);
7302
+ const person = personOf(who);
7303
+ lkQuality.set(person, value);
7304
+ const local = store.snapshot().participants.find((p) => p.identity === person)?.isLocal === true;
7305
+ store.setQuality(person, value === "lost" ? value : effectiveQuality(person, who, local));
7306
+ },
7307
+ networkReading(reading) {
7308
+ const snap = store.snapshot();
7309
+ if (snap.phase !== "in_call" || snap.connection !== "stable") return;
7310
+ startHealth();
7311
+ const quality = assessor.push(reading);
7312
+ const path = pathOf(reading);
7313
+ patchLocalNetwork({ quality, ...path !== "unknown" ? { path } : {} });
7314
+ refreshQualities();
7315
+ const cameraOn = snap.media.camera.enabled;
7316
+ if (autoOff.step(quality, cameraOn) === "pause" && driver !== null) {
7317
+ const target = driver;
7318
+ report("info", "meet: the camera was paused because the connection is poor", "The audio is kept; Try video again turns it back on.");
7319
+ void enableTrack(target, "camera", false).then((state) => {
7320
+ if (!state.enabled) patchLocalNetwork({ videoPaused: true });
7321
+ });
7322
+ }
6998
7323
  },
6999
7324
  connectionState(state) {
7000
7325
  supervisor.driverState(state);
@@ -7089,6 +7414,22 @@ function createRoomEngine(options) {
7089
7414
  return;
7090
7415
  }
7091
7416
  const sender = personOf(from ?? "");
7417
+ if (from !== null) {
7418
+ const beat = topic === ALIVE_TOPIC ? decodeBeat(payload) : null;
7419
+ const before = liveness.reported(from);
7420
+ liveness.heard(from, beat);
7421
+ if (topic === ALIVE_TOPIC) {
7422
+ startHealth();
7423
+ if (beat !== null && (before?.q !== beat.q || before?.vp !== beat.vp || silent.has(from))) {
7424
+ silent.delete(from);
7425
+ projectRoster(liveRoster, new Date(now()).toISOString());
7426
+ refreshQualities();
7427
+ } else if (silent.delete(from)) {
7428
+ projectRoster(liveRoster, new Date(now()).toISOString());
7429
+ }
7430
+ return;
7431
+ }
7432
+ }
7092
7433
  switch (topic) {
7093
7434
  case DATA_TOPICS.reaction:
7094
7435
  if (typeof envelope.emoji === "string") {
@@ -7331,7 +7672,16 @@ function createRoomEngine(options) {
7331
7672
  store.setParticipants(
7332
7673
  [...people.values()].map(({ entry }) => {
7333
7674
  const projected = projectParticipant(entry, joinedAt);
7334
- return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
7675
+ if (entry.reconnecting === true || !entry.isLocal && silent.has(entry.identity)) {
7676
+ return { ...projected, reconnecting: true, quality: "lost" };
7677
+ }
7678
+ const videoPaused = entry.isLocal ? store.snapshot().localNetwork.videoPaused : liveness.reported(entry.identity)?.vp === true;
7679
+ const quality = effectiveQuality(projected.identity, entry.identity, entry.isLocal);
7680
+ return {
7681
+ ...projected,
7682
+ ...quality !== "unknown" && quality !== "lost" ? { quality } : {},
7683
+ ...videoPaused ? { videoPaused: true } : {}
7684
+ };
7335
7685
  })
7336
7686
  );
7337
7687
  scheduleGhostExpiry();
@@ -7517,7 +7867,12 @@ function createRoomEngine(options) {
7517
7867
  },
7518
7868
  async setCameraEnabled(enabled) {
7519
7869
  if (driver === null) return;
7520
- await enableTrack(driver, "camera", enabled);
7870
+ const state = await enableTrack(driver, "camera", enabled);
7871
+ if (enabled && state.enabled && store.snapshot().localNetwork.videoPaused) {
7872
+ autoOff.retried();
7873
+ patchLocalNetwork({ videoPaused: false });
7874
+ }
7875
+ if (!enabled && store.snapshot().localNetwork.videoPaused) patchLocalNetwork({ videoPaused: false });
7521
7876
  },
7522
7877
  async setScreenShareEnabled(enabled) {
7523
7878
  if (driver === null) return;
@@ -7563,7 +7918,8 @@ function createRoomEngine(options) {
7563
7918
  }
7564
7919
  },
7565
7920
  dismissShareNotice() {
7566
- if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
7921
+ const state = store.snapshot().share.state;
7922
+ if (state === "refused" || state === "ended") store.patch({ share: NO_SHARE });
7567
7923
  },
7568
7924
  async react(emoji) {
7569
7925
  const local = store.snapshot().localIdentity;
@@ -8681,7 +9037,7 @@ function observe(input) {
8681
9037
  const phase = observedPhase(input);
8682
9038
  const inCall = phase === "in_call";
8683
9039
  const actual = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
8684
- const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
9040
+ const canAdmit = snapshot !== null && mayAdmitNow(actual, snapshot.participants, snapshot.livePolicy);
8685
9041
  const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
8686
9042
  const deviceInPlay = phase === "joining" || inCall;
8687
9043
  const region = snapshot?.mediaRegion;