@ai-matrx/meet 0.11.50 → 0.11.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs CHANGED
@@ -491,7 +491,8 @@ var DEFAULT_CONNECTION_POLICY = {
491
491
  autoRejoin: true,
492
492
  autoRejoinMaxAttempts: 5,
493
493
  reconnectGraceSeconds: 30,
494
- videoAutoOffAfterPoorSeconds: 20
494
+ videoAutoOffAfterPoorSeconds: 20,
495
+ captionsStartTimeoutSeconds: 20
495
496
  };
496
497
  function clampInt(value, min, max, fallback) {
497
498
  if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
@@ -506,7 +507,8 @@ function connectionPolicyFrom(raw) {
506
507
  autoRejoin: typeof row["auto_rejoin"] === "boolean" ? row["auto_rejoin"] : d.autoRejoin,
507
508
  autoRejoinMaxAttempts: clampInt(row["auto_rejoin_max_attempts"], 0, 20, d.autoRejoinMaxAttempts),
508
509
  reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds),
509
- videoAutoOffAfterPoorSeconds: clampInt(row["video_auto_off_after_poor_seconds"], 0, 600, d.videoAutoOffAfterPoorSeconds)
510
+ videoAutoOffAfterPoorSeconds: clampInt(row["video_auto_off_after_poor_seconds"], 0, 600, d.videoAutoOffAfterPoorSeconds),
511
+ captionsStartTimeoutSeconds: clampInt(row["captions_start_timeout_seconds"], 1, 600, d.captionsStartTimeoutSeconds)
510
512
  };
511
513
  }
512
514
  var LIVEKIT_BASE_DELAYS_MS = [0, 300, 1200, 2700, 4800, 7e3];
@@ -4357,6 +4359,7 @@ var EMPTY = {
4357
4359
  noteTakerRefusal: null,
4358
4360
  captionsOn: false,
4359
4361
  captionsRefusal: null,
4362
+ captionsStartTimeoutSeconds: 20,
4360
4363
  media: IDLE_MEDIA,
4361
4364
  mediaRegion: IDLE_MEDIA_REGION,
4362
4365
  failure: null,
@@ -6106,6 +6109,16 @@ var ALIVE_TOPIC = "mx.alive";
6106
6109
  function encodeBeat(beat) {
6107
6110
  return JSON.stringify({ q: beat.q, vp: beat.vp });
6108
6111
  }
6112
+ function encodeBye() {
6113
+ return JSON.stringify({ bye: true });
6114
+ }
6115
+ function isBye(payload) {
6116
+ try {
6117
+ return JSON.parse(payload).bye === true;
6118
+ } catch {
6119
+ return false;
6120
+ }
6121
+ }
6109
6122
  function decodeBeat(payload) {
6110
6123
  try {
6111
6124
  const raw = JSON.parse(payload);
@@ -6332,6 +6345,7 @@ function createRoomEngine(options) {
6332
6345
  const autoOff = createVideoAutoOff(() => connectionPolicy.videoAutoOffAfterPoorSeconds * 1e3, now);
6333
6346
  const lkQuality = /* @__PURE__ */ new Map();
6334
6347
  let silent = /* @__PURE__ */ new Set();
6348
+ const saidBye = /* @__PURE__ */ new Set();
6335
6349
  let healthTimer = null;
6336
6350
  let lastBeatAt = 0;
6337
6351
  let wasUnstable = false;
@@ -6362,6 +6376,7 @@ function createRoomEngine(options) {
6362
6376
  assessor.reset();
6363
6377
  autoOff.reset();
6364
6378
  silent = /* @__PURE__ */ new Set();
6379
+ saidBye.clear();
6365
6380
  lkQuality.clear();
6366
6381
  lastBeatAt = 0;
6367
6382
  wasUnstable = false;
@@ -6419,6 +6434,13 @@ function createRoomEngine(options) {
6419
6434
  await target?.disconnect();
6420
6435
  }
6421
6436
  async function leaveRoom() {
6437
+ const target = driver;
6438
+ if (target !== null && store.snapshot().phase === "in_call") {
6439
+ await Promise.race([
6440
+ target.publishData(ALIVE_TOPIC, encodeBye()).catch(() => void 0),
6441
+ new Promise((resolve) => setTimeout(resolve, 300))
6442
+ ]);
6443
+ }
6422
6444
  store.dispatch({ type: "leave" });
6423
6445
  await dropRoom();
6424
6446
  options.onLeft?.(null);
@@ -6514,8 +6536,6 @@ function createRoomEngine(options) {
6514
6536
  },
6515
6537
  participantLeft(who, reason) {
6516
6538
  departures.set(who, reason);
6517
- liveness.forget(who);
6518
- silent.delete(who);
6519
6539
  },
6520
6540
  roomMetadata(metadata) {
6521
6541
  let parsed = {};
@@ -6558,8 +6578,12 @@ function createRoomEngine(options) {
6558
6578
  const snap = store.snapshot();
6559
6579
  if (snap.phase !== "in_call" || snap.connection !== "stable") return;
6560
6580
  startHealth();
6581
+ const before = assessor.current();
6561
6582
  const quality = assessor.push(reading);
6562
6583
  const path = pathOf(reading);
6584
+ if (quality !== before) {
6585
+ report("info", `meet: this connection reads ${quality} (round trip ${reading.rttMs ?? "?"} ms, path ${path})`, "Diagnostic only.");
6586
+ }
6563
6587
  patchLocalNetwork({ quality, ...path !== "unknown" ? { path } : {} });
6564
6588
  refreshQualities();
6565
6589
  const cameraOn = snap.media.camera.enabled;
@@ -6574,6 +6598,11 @@ function createRoomEngine(options) {
6574
6598
  connectionState(state) {
6575
6599
  supervisor.driverState(state);
6576
6600
  if (state === "connected" && store.snapshot().phase === "joining") store.dispatch({ type: "connected" });
6601
+ if (state === "connected") {
6602
+ startHealth();
6603
+ lastBeatAt = 0;
6604
+ healthTick();
6605
+ }
6577
6606
  },
6578
6607
  transcription(event) {
6579
6608
  const snapshot = store.snapshot();
@@ -6665,6 +6694,10 @@ function createRoomEngine(options) {
6665
6694
  }
6666
6695
  const sender = personOf(from ?? "");
6667
6696
  if (from !== null) {
6697
+ if (topic === ALIVE_TOPIC && isBye(payload)) {
6698
+ saidBye.add(from);
6699
+ return;
6700
+ }
6668
6701
  const beat = topic === ALIVE_TOPIC ? decodeBeat(payload) : null;
6669
6702
  const before = liveness.reported(from);
6670
6703
  liveness.heard(from, beat);
@@ -6779,6 +6812,7 @@ function createRoomEngine(options) {
6779
6812
  current = token;
6780
6813
  lastServerUrl = token.serverUrl;
6781
6814
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6815
+ store.patch({ captionsStartTimeoutSeconds: connectionPolicy.captionsStartTimeoutSeconds });
6782
6816
  if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
6783
6817
  store.patch({ canReclaim: token.canReclaim === true });
6784
6818
  store.patch({
@@ -6896,10 +6930,17 @@ function createRoomEngine(options) {
6896
6930
  const graceMs = connectionPolicy.reconnectGraceSeconds * 1e3;
6897
6931
  for (const [who, reason] of departures) {
6898
6932
  const last = previous.get(who);
6899
- if (last !== void 0 && !last.isLocal && !present.has(who) && graceMs > 0 && isUnannouncedDeparture(reason)) {
6933
+ const dropped = !saidBye.has(who) && (isUnannouncedDeparture(reason) || silent.has(who));
6934
+ report("info", `meet: ${who} left the room (${reason ?? "no reason"}${dropped ? ", held as reconnecting" : ""})`, "Diagnostic only.");
6935
+ if (last !== void 0 && !last.isLocal && !present.has(who) && graceMs > 0 && dropped) {
6900
6936
  ghosts.set(who, { participant: last, until: at + graceMs });
6901
6937
  }
6902
6938
  }
6939
+ for (const who of departures.keys()) {
6940
+ liveness.forget(who);
6941
+ silent.delete(who);
6942
+ saidBye.delete(who);
6943
+ }
6903
6944
  departures.clear();
6904
6945
  liveRoster = list;
6905
6946
  const localNow = list.find((p) => p.isLocal);
@@ -9226,6 +9267,22 @@ function createMeetingRecordCache(options = {}) {
9226
9267
  };
9227
9268
  }
9228
9269
 
9270
+ // src/core/captions-state.ts
9271
+ var CAPTIONS_TIMEOUT_REASON = "Captions are unavailable right now.";
9272
+ function resolveCaptionsState(i) {
9273
+ if (!i.on) return { state: "off", reason: null, remedy: null };
9274
+ if (i.takerState === "transcribing" && i.takerPresent) return { state: "live", reason: null, remedy: null };
9275
+ if (i.takerState === "joining" && !i.timedOut) return { state: "starting", reason: null, remedy: null };
9276
+ if (i.refusal !== null && i.refusal.reason.length > 0) {
9277
+ return { state: "unavailable", reason: i.refusal.reason, remedy: i.refusal.remedy.length > 0 ? i.refusal.remedy : null };
9278
+ }
9279
+ if (i.takerState === "ended" || i.takerState === "handoff_pending") {
9280
+ return { state: "unavailable", reason: i.takerLabel, remedy: null };
9281
+ }
9282
+ if (i.timedOut) return { state: "unavailable", reason: CAPTIONS_TIMEOUT_REASON, remedy: null };
9283
+ return { state: "starting", reason: null, remedy: null };
9284
+ }
9285
+
9229
9286
  // src/core/layout.ts
9230
9287
  var DEFAULT_PAGE_SIZE = 12;
9231
9288
  function orderParticipants(participants, activeSpeakers) {
@@ -9316,7 +9373,8 @@ function useRoom() {
9316
9373
  local,
9317
9374
  // An observer (role `viewer`) publishes nothing: no Share screen to press and be refused (MD-16).
9318
9375
  sharingAllowed: snapshot === null || canShareUnder(snapshot.livePolicy, effectiveRole(snapshot), effectiveStanding(snapshot)),
9319
- canShareScreen: (engine?.supportsScreenShare() ?? true) && !isObserverRole(snapshot?.grants.role ?? null) && (snapshot === null || canShareUnder(snapshot.livePolicy, effectiveRole(snapshot), effectiveStanding(snapshot))),
9376
+ canShareScreen: (engine?.supportsScreenShare() ?? true) && !isObserverRole(snapshot?.grants.role ?? null) && // The server's word that sharing was taken away arrives before the rules refresh does: the control goes at once.
9377
+ !(snapshot?.share.state === "ended" && snapshot.share.reason === "not_allowed") && (snapshot === null || canShareUnder(snapshot.livePolicy, effectiveRole(snapshot), effectiveStanding(snapshot))),
9320
9378
  otherPresenter: otherPresenterOf(snapshot),
9321
9379
  join: (0, import_react4.useCallback)(
9322
9380
  async (request) => requireEngine(engine, "join").join(request),
@@ -9713,32 +9771,37 @@ function useCaptions() {
9713
9771
  if (host === null || meeting === null) return;
9714
9772
  void host.ai.requestCaptions({ meetingId: meeting.id });
9715
9773
  }, [host, meeting]);
9774
+ const [timedOut, setTimedOut] = (0, import_react4.useState)(false);
9775
+ const timeoutSeconds = snapshot?.captionsStartTimeoutSeconds ?? 20;
9776
+ const live = taker.state === "transcribing" && taker.present;
9777
+ const [attempt, setAttempt] = (0, import_react4.useState)(0);
9778
+ (0, import_react4.useEffect)(() => {
9779
+ setTimedOut(false);
9780
+ if (!on || live) return void 0;
9781
+ const id = setTimeout(() => setTimedOut(true), timeoutSeconds * 1e3);
9782
+ return () => clearTimeout(id);
9783
+ }, [on, live, timeoutSeconds, attempt]);
9716
9784
  const toggle = (0, import_react4.useCallback)(() => {
9717
9785
  if (host === null) return;
9718
9786
  const next = !on;
9719
9787
  host.store.patch({ captionsOn: next, captionsRefusal: null });
9720
9788
  if (next && taker.state !== "joining" && taker.state !== "transcribing") request();
9721
9789
  }, [host, on, taker.state, request]);
9722
- let state = "off";
9723
- let reason = null;
9724
- let remedy = null;
9725
- if (on) {
9726
- if (taker.state === "transcribing" && taker.present) {
9727
- state = "live";
9728
- } else if (taker.state === "joining") {
9729
- state = "starting";
9730
- } else if (refusal2 !== null && refusal2.reason.length > 0) {
9731
- state = "unavailable";
9732
- reason = refusal2.reason;
9733
- remedy = refusal2.remedy.length > 0 ? refusal2.remedy : null;
9734
- } else if (taker.state === "ended" || taker.state === "handoff_pending") {
9735
- state = "unavailable";
9736
- reason = noteTakerLabel(taker);
9737
- } else {
9738
- state = "starting";
9739
- }
9740
- }
9741
- return { on, state, reason, remedy, toggle };
9790
+ const retry = (0, import_react4.useCallback)(() => {
9791
+ if (host === null) return;
9792
+ host.store.patch({ captionsOn: true, captionsRefusal: null });
9793
+ setAttempt((n) => n + 1);
9794
+ request();
9795
+ }, [host, request]);
9796
+ const view = resolveCaptionsState({
9797
+ on,
9798
+ takerState: taker.state,
9799
+ takerPresent: taker.present,
9800
+ refusal: refusal2,
9801
+ takerLabel: on ? noteTakerLabel(taker) : "",
9802
+ timedOut
9803
+ });
9804
+ return { on, state: view.state, reason: view.reason, remedy: view.remedy, toggle, retry };
9742
9805
  }
9743
9806
  function useEndMeeting() {
9744
9807
  const host = useMeetHost();
@@ -14257,11 +14320,14 @@ function Captions({ lines = 3 }) {
14257
14320
  const captions = useCaptions();
14258
14321
  if (!captions.on) return null;
14259
14322
  if (captions.state === "unavailable") {
14260
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__captions", role: "status", "data-meet-notice": "captions-unavailable", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__caption", children: [
14261
- "Captions are unavailable. ",
14262
- captions.reason,
14263
- captions.remedy !== null ? ` ${captions.remedy}` : ""
14264
- ] }) });
14323
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__captions", role: "status", "data-meet-notice": "captions-unavailable", children: [
14324
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__caption", children: [
14325
+ (captions.reason ?? "").startsWith("Captions are unavailable") ? "" : "Captions are unavailable. ",
14326
+ captions.reason,
14327
+ captions.remedy !== null ? ` ${captions.remedy}` : ""
14328
+ ] }),
14329
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__caption-retry", onClick: captions.retry, children: "Try again" })
14330
+ ] });
14265
14331
  }
14266
14332
  if (visible.length === 0) {
14267
14333
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__captions", "aria-live": "polite", "aria-label": "Live captions", "data-meet-caption": captions.state, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__caption mx-meet__caption--interim", children: captions.state === "starting" ? "Starting captions. They appear as people speak." : "Captions are on. They appear as people speak." }) });