@ai-matrx/meet 0.11.41 → 0.11.43

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
@@ -110,6 +110,7 @@ __export(src_exports, {
110
110
  autoAssign: () => autoAssign,
111
111
  autoRejoinDelayMs: () => autoRejoinDelayMs,
112
112
  availableActivities: () => availableActivities,
113
+ breakoutClosingMs: () => breakoutClosingMs,
113
114
  breakoutRemainingMs: () => breakoutRemainingMs,
114
115
  callInviteAction: () => callInviteAction,
115
116
  callInviteText: () => callInviteText,
@@ -911,6 +912,8 @@ var MEET_ROUTES = {
911
912
  noteTakerStop: "/api/v1/meet/agent/stop",
912
913
  /** In-meeting Q&A. Streams NDJSON; body is `{meeting_id, question}` only. */
913
914
  intelligenceAsk: "/api/v1/meet/intelligence/ask",
915
+ /** Anyone in the meeting (guests with the room pass included) asks for captions for themselves. */
916
+ noteTakerCaptions: "/api/v1/meet/agent/captions",
914
917
  /** A host ends the meeting for everyone. Served by MRI-A6. */
915
918
  meetingEnd: "/api/v1/meet/end",
916
919
  /**
@@ -1057,7 +1060,8 @@ function parseRoomToken(body, roomName) {
1057
1060
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
1058
1061
  observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
1059
1062
  connectionPolicy: connectionPolicyFrom(payload["connection_policy"]),
1060
- leavePolicy: leavePolicyFrom(payload["leave_policy"])
1063
+ leavePolicy: leavePolicyFrom(payload["leave_policy"]),
1064
+ canReclaim: payload["can_reclaim"] === true
1061
1065
  };
1062
1066
  }
1063
1067
  function createTokenClient(options) {
@@ -1519,6 +1523,42 @@ function createMeetAi(options) {
1519
1523
  },
1520
1524
  "meet.ai.startNoteTaker"
1521
1525
  ),
1526
+ requestCaptions: async ({ meetingId }) => {
1527
+ const roomToken = options.resolveRoomToken?.() ?? null;
1528
+ const operation = "meet.ai.requestCaptions";
1529
+ try {
1530
+ const response = await send({
1531
+ path: MEET_ROUTES.noteTakerCaptions,
1532
+ body: { meeting_id: meetingId },
1533
+ operation,
1534
+ requireAuth: roomToken === null,
1535
+ ...roomToken !== null ? { headers: { [MEETING_PASS_HEADER]: roomToken } } : {}
1536
+ });
1537
+ if (!response.ok) {
1538
+ options.store.patch({ captionsRefusal: await refusalFor(response, operation) });
1539
+ return;
1540
+ }
1541
+ let payload = null;
1542
+ try {
1543
+ payload = await response.json();
1544
+ } catch {
1545
+ payload = null;
1546
+ }
1547
+ const row = typeof payload === "object" && payload !== null ? payload : {};
1548
+ const detail = typeof row["detail"] === "string" ? row["detail"] : "";
1549
+ options.store.patch({
1550
+ captionsRefusal: row["dispatched"] === true ? null : { status: 200, reason: detail, remedy: "" }
1551
+ });
1552
+ } catch (error) {
1553
+ options.store.patch({
1554
+ captionsRefusal: {
1555
+ status: 0,
1556
+ reason: error.message,
1557
+ remedy: error.remedy ?? ""
1558
+ }
1559
+ });
1560
+ }
1561
+ },
1522
1562
  stopNoteTaker: ({ meetingId }) => dispatch(
1523
1563
  MEET_ROUTES.noteTakerStop,
1524
1564
  { meeting_id: meetingId },
@@ -1557,7 +1597,7 @@ function exactNoteTakerParticipant(meeting, participants) {
1557
1597
  }
1558
1598
  function noteTakerStatus(args) {
1559
1599
  const present = exactNoteTakerParticipant(args.meeting, args.participants) !== null;
1560
- const raw = args.meeting?.metadata[NOTE_TAKER_METADATA_KEY];
1600
+ const raw = args.meeting?.metadata?.[NOTE_TAKER_METADATA_KEY];
1561
1601
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
1562
1602
  const recorded = readState(row["state"]);
1563
1603
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
@@ -3290,6 +3330,8 @@ function planFromRow(row) {
3290
3330
  minutes: num2(row["minutes"]) ?? 0,
3291
3331
  openedAt: str(row["opened_at"]),
3292
3332
  endsAt: str(row["ends_at"]),
3333
+ closingAt: str(row["closing_at"]),
3334
+ moveMode: row["move_mode"] === "auto" ? "auto" : "invite",
3293
3335
  closedAt: str(row["closed_at"]),
3294
3336
  broadcasts: (Array.isArray(row["broadcasts"]) ? row["broadcasts"] : []).map(broadcastFrom).filter((b) => b !== null)
3295
3337
  };
@@ -3441,9 +3483,17 @@ function autoAssign(identities, rooms, exclude = []) {
3441
3483
  return out;
3442
3484
  }
3443
3485
  function breakoutRemainingMs(plan, now) {
3444
- if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
3445
- const end = Date.parse(plan.endsAt);
3446
- return Number.isNaN(end) ? null : Math.max(0, end - now);
3486
+ if (plan === null || plan.state !== "open") return null;
3487
+ const ends = [plan.endsAt, plan.closingAt].map((stamp) => stamp === null ? Number.NaN : Date.parse(stamp)).filter((time) => !Number.isNaN(time));
3488
+ if (ends.length === 0) return null;
3489
+ return Math.max(0, Math.min(...ends) - now);
3490
+ }
3491
+ var BREAKOUT_COUNTDOWN_MS = 3e4;
3492
+ function breakoutClosingMs(plan, now) {
3493
+ if (plan === null || plan.state !== "open") return null;
3494
+ const remaining = breakoutRemainingMs(plan, now);
3495
+ if (remaining === null) return null;
3496
+ return plan.closingAt !== null || remaining <= BREAKOUT_COUNTDOWN_MS ? remaining : null;
3447
3497
  }
3448
3498
  function defaultCaptionLanguage(settings, viewerLanguage) {
3449
3499
  const configured = settings?.captionTranslationLanguage.trim() ?? "";
@@ -3690,6 +3740,47 @@ function recordingLabel(state) {
3690
3740
  }
3691
3741
  }
3692
3742
 
3743
+ // src/core/live-policy.ts
3744
+ var DEFAULT_LIVE_POLICY = {
3745
+ chatMode: "everyone",
3746
+ avLock: "off",
3747
+ timeLimitMinutes: 0,
3748
+ timeLimitWarningMinutes: 5,
3749
+ recordingConsent: "notice"
3750
+ };
3751
+ function oneOf(value, allowed, fallback) {
3752
+ return typeof value === "string" && allowed.includes(value) ? value : fallback;
3753
+ }
3754
+ function whole(value, fallback) {
3755
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
3756
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
3757
+ }
3758
+ function livePolicyFrom(raw) {
3759
+ const o = typeof raw === "object" && raw !== null ? raw : {};
3760
+ return {
3761
+ chatMode: oneOf(o["chat_mode"], ["everyone", "hosts_only", "off"], DEFAULT_LIVE_POLICY.chatMode),
3762
+ avLock: oneOf(o["attendee_av_lock"], ["off", "mic", "camera", "both"], DEFAULT_LIVE_POLICY.avLock),
3763
+ timeLimitMinutes: whole(o["time_limit_minutes"], 0),
3764
+ timeLimitWarningMinutes: whole(o["time_limit_warning_minutes"], DEFAULT_LIVE_POLICY.timeLimitWarningMinutes),
3765
+ recordingConsent: oneOf(
3766
+ o["recording_consent"],
3767
+ ["notice", "ok_or_leave", "view_only_until_agree"],
3768
+ DEFAULT_LIVE_POLICY.recordingConsent
3769
+ )
3770
+ };
3771
+ }
3772
+ function samePolicy(a, b) {
3773
+ return a.chatMode === b.chatMode && a.avLock === b.avLock && a.timeLimitMinutes === b.timeLimitMinutes && a.timeLimitWarningMinutes === b.timeLimitWarningMinutes && a.recordingConsent === b.recordingConsent;
3774
+ }
3775
+ var MODERATORS = /* @__PURE__ */ new Set(["host", "cohost"]);
3776
+ function isLockedFor(policy, role, kind) {
3777
+ if (MODERATORS.has(role)) return false;
3778
+ return policy.avLock === "both" || (kind === "microphone" ? policy.avLock === "mic" : policy.avLock === "camera");
3779
+ }
3780
+ function consentPending(policy, role, recordingOn, given) {
3781
+ return recordingOn && !given && policy.recordingConsent !== "notice" && !MODERATORS.has(role);
3782
+ }
3783
+
3693
3784
  // src/core/attendance.ts
3694
3785
  var IDLE_MEDIA_REGION = {
3695
3786
  permission: { microphone: "unknown", camera: "unknown" },
@@ -3760,6 +3851,11 @@ var EMPTY = {
3760
3851
  presenter: null,
3761
3852
  waitingCount: 0,
3762
3853
  leavePolicy: DEFAULT_LEAVE_POLICY,
3854
+ livePolicy: DEFAULT_LIVE_POLICY,
3855
+ canReclaim: false,
3856
+ recordingConsentGiven: false,
3857
+ sessionStartedAt: null,
3858
+ mutedByHost: false,
3763
3859
  moderationNotice: null,
3764
3860
  actionNotice: null,
3765
3861
  privateChatRequest: null,
@@ -3769,6 +3865,8 @@ var EMPTY = {
3769
3865
  names: {},
3770
3866
  wrapUp: null,
3771
3867
  noteTakerRefusal: null,
3868
+ captionsOn: false,
3869
+ captionsRefusal: null,
3772
3870
  media: IDLE_MEDIA,
3773
3871
  mediaRegion: IDLE_MEDIA_REGION,
3774
3872
  failure: null,
@@ -3973,7 +4071,12 @@ function createMeetStore(initial = {}) {
3973
4071
  if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.reason === next.unavailable?.reason && current.unavailable?.message === next.unavailable?.message) {
3974
4072
  return;
3975
4073
  }
3976
- commit({ ...state, media: { ...state.media, [kind]: next } });
4074
+ commit({
4075
+ ...state,
4076
+ media: { ...state.media, [kind]: next },
4077
+ // The person turned their own microphone on again: the host's mute is over.
4078
+ ...kind === "microphone" && next.enabled && state.mutedByHost ? { mutedByHost: false } : {}
4079
+ });
3977
4080
  },
3978
4081
  reset() {
3979
4082
  const phase = nextPhase(state.phase, { type: "reset", teardown: true }) ?? EMPTY.phase;
@@ -4011,8 +4114,7 @@ var ROOM_EVENTS = {
4011
4114
  /**
4012
4115
  * A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
4013
4116
  * `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
4014
- * person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
4015
- * role}` — to everyone.
4117
+ * person concerned; `role {identity, role}` — to everyone.
4016
4118
  */
4017
4119
  moderation: "matrx.meet.moderation",
4018
4120
  /**
@@ -4200,10 +4302,11 @@ function moderationText(kind, payload) {
4200
4302
  switch (kind) {
4201
4303
  case "muted": {
4202
4304
  const source = text2(payload["source"]) ?? "microphone";
4203
- return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
4305
+ const who = payload["by_role"] === "host" || payload["by_role"] === "cohost" ? "the host" : by;
4306
+ return source === "microphone" ? `You've been muted by ${who}.` : source === "camera" ? `Your camera was turned off by ${who}.` : `Your screen share was stopped by ${who}.`;
4204
4307
  }
4205
4308
  case "ask-unmute":
4206
- return `${by} is asking you to unmute.`;
4309
+ return `${by === "The host" ? "The host" : by} asked you to unmute.`;
4207
4310
  case "removed":
4208
4311
  return `${by} removed you from this meeting.`;
4209
4312
  default:
@@ -4229,7 +4332,7 @@ function applyModeration(store, payload, now) {
4229
4332
  grants: { ...snapshot.grants, role },
4230
4333
  moderationNotice: {
4231
4334
  kind: "role",
4232
- text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
4335
+ text: role === "host" ? "You are now the host." : role === "cohost" ? "You're now a co-host." : role === "presenter" ? "You're now a presenter." : snapshot.grants.role === "presenter" ? "You're now a participant: you can no longer present." : "You're now a participant: you are no longer a co-host.",
4233
4336
  at: now()
4234
4337
  }
4235
4338
  });
@@ -4241,7 +4344,10 @@ function applyModeration(store, payload, now) {
4241
4344
  case "removed": {
4242
4345
  const sentence = moderationText(kind, payload);
4243
4346
  if (sentence === null) return ignored("a moderation event with no sentence");
4244
- store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
4347
+ store.patch({
4348
+ moderationNotice: { kind, text: sentence, at: now() },
4349
+ ...kind === "muted" && (text2(payload["source"]) ?? "microphone") === "microphone" ? { mutedByHost: true } : {}
4350
+ });
4245
4351
  return HANDLED;
4246
4352
  }
4247
4353
  default:
@@ -5063,6 +5169,7 @@ function applyDoorRow(store, attendee) {
5063
5169
  function decidedState(state) {
5064
5170
  return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
5065
5171
  }
5172
+ var LIVE_POLICY_POLL_MS = 3e3;
5066
5173
  function createMeetingSession(options) {
5067
5174
  const { manager, repository, store, identity } = options;
5068
5175
  let channel = null;
@@ -5204,6 +5311,46 @@ function createMeetingSession(options) {
5204
5311
  stopWaiting();
5205
5312
  }
5206
5313
  }
5314
+ let policyTimer = null;
5315
+ let policyReading = false;
5316
+ async function readLivePolicy() {
5317
+ const meeting = store.snapshot().meeting;
5318
+ const pass = options.resolvePass?.() ?? null;
5319
+ if (meeting === null || pass === null || policyReading) return;
5320
+ policyReading = true;
5321
+ try {
5322
+ const response = await options.api.request({
5323
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
5324
+ method: "GET",
5325
+ requireAuth: false,
5326
+ operation: "meet.state(policy)",
5327
+ headers: { [MEETING_PASS_HEADER]: pass }
5328
+ });
5329
+ if (!response.ok) return;
5330
+ const read = await response.json();
5331
+ const next = livePolicyFrom(read.live_policy);
5332
+ const started = read.session_started_at ? Date.parse(read.session_started_at) : NaN;
5333
+ const snapshot = store.snapshot();
5334
+ const startedAt = Number.isFinite(started) ? started : snapshot.sessionStartedAt;
5335
+ if (!samePolicy(snapshot.livePolicy, next) || startedAt !== snapshot.sessionStartedAt) {
5336
+ store.patch({ livePolicy: next, sessionStartedAt: startedAt });
5337
+ }
5338
+ } catch {
5339
+ } finally {
5340
+ policyReading = false;
5341
+ }
5342
+ }
5343
+ function syncLivePolicyTimer() {
5344
+ const inCall = store.snapshot().phase === "in_call";
5345
+ if (inCall && policyTimer === null) {
5346
+ void readLivePolicy();
5347
+ policyTimer = setInterval(() => void readLivePolicy(), LIVE_POLICY_POLL_MS);
5348
+ } else if (!inCall && policyTimer !== null) {
5349
+ clearInterval(policyTimer);
5350
+ policyTimer = null;
5351
+ }
5352
+ }
5353
+ const unsubscribePolicy = store.subscribe(syncLivePolicyTimer);
5207
5354
  async function readState2() {
5208
5355
  if (reading !== null) return reading;
5209
5356
  const meeting = store.snapshot().meeting;
@@ -5590,6 +5737,9 @@ function createMeetingSession(options) {
5590
5737
  },
5591
5738
  dispose() {
5592
5739
  stopWaiting();
5740
+ unsubscribePolicy();
5741
+ if (policyTimer !== null) clearInterval(policyTimer);
5742
+ policyTimer = null;
5593
5743
  channel?.close();
5594
5744
  channel = null;
5595
5745
  meetingChannel?.close();
@@ -6907,6 +7057,7 @@ function createRoomEngine(options) {
6907
7057
  lastServerUrl = token.serverUrl;
6908
7058
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6909
7059
  if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
7060
+ store.patch({ canReclaim: token.canReclaim === true });
6910
7061
  store.patch({
6911
7062
  localIdentity: token.identity,
6912
7063
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -7401,13 +7552,17 @@ var MODERATION_ACTIONS = [
7401
7552
  "ask_unmute",
7402
7553
  "ask_all_unmute",
7403
7554
  "spotlight",
7404
- "present",
7555
+ "sync_presenter",
7405
7556
  "raise_hand",
7406
7557
  "lower_hand",
7407
7558
  "lower_all_hands",
7408
7559
  "make_cohost",
7560
+ "make_presenter",
7561
+ "revoke_presenter",
7562
+ "set_av_lock",
7409
7563
  "revoke_cohost",
7410
7564
  "transfer_host",
7565
+ "reclaim_host",
7411
7566
  "rename",
7412
7567
  "remove",
7413
7568
  "report"
@@ -7433,18 +7588,17 @@ function createInRoomClient(options) {
7433
7588
  function refusal2(response, operation, remedy) {
7434
7589
  return refusalFromResponse(response, operation, remedy);
7435
7590
  }
7436
- let toldPresenting = false;
7591
+ let toldSharing = false;
7437
7592
  let tellChain = Promise.resolve();
7438
7593
  store.subscribe(() => {
7439
7594
  const local = store.snapshot().participants.find((p) => p.isLocal);
7440
7595
  const sharing = local?.screenSharing === true;
7441
- if (sharing === toldPresenting || local === void 0) return;
7442
- toldPresenting = sharing;
7443
- const on = sharing;
7444
- tellChain = tellChain.then(() => client.moderate({ action: "present", on })).then(
7596
+ if (sharing === toldSharing || local === void 0) return;
7597
+ toldSharing = sharing;
7598
+ tellChain = tellChain.then(() => client.moderate({ action: "sync_presenter" })).then(
7445
7599
  () => void 0,
7446
7600
  (error) => {
7447
- console.warn(`[meet] the server did not take presenter=${String(on)}: ${String(error)}`);
7601
+ console.warn(`[meet] the server did not take the screen-share change: ${String(error)}`);
7448
7602
  }
7449
7603
  );
7450
7604
  });
@@ -7461,8 +7615,7 @@ function createInRoomClient(options) {
7461
7615
  action: request.action,
7462
7616
  ...request.identity != null ? { identity: request.identity } : {},
7463
7617
  ...request.name !== void 0 ? { name: request.name } : {},
7464
- ...request.reason !== void 0 ? { reason: request.reason } : {},
7465
- ...request.on !== void 0 ? { on: request.on } : {}
7618
+ ...request.reason !== void 0 ? { reason: request.reason } : {}
7466
7619
  },
7467
7620
  ...hdrs !== void 0 ? { headers: hdrs } : {}
7468
7621
  });
@@ -8339,11 +8492,13 @@ function takeMoveRequest(store) {
8339
8492
 
8340
8493
  // src/core/observation.ts
8341
8494
  function observedRole(role, standing) {
8342
- if (role === "host" || role === "cohost") return role;
8495
+ if (role === "host" || role === "cohost" || role === "presenter") return role;
8343
8496
  return isLinkFollower(standing) ? "guest" : "participant";
8344
8497
  }
8345
- function observedDevice(track, permission = "unknown") {
8498
+ function observedDevice(track, permission = "unknown", held = null) {
8499
+ if (held === "locked") return "locked";
8346
8500
  if (track.enabled) return "on";
8501
+ if (held === "muted_by_host") return "muted_by_host";
8347
8502
  const reason = deviceUnavailableReason(track, permission);
8348
8503
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
8349
8504
  }
@@ -8380,10 +8535,20 @@ function observe(input) {
8380
8535
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
8381
8536
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
8382
8537
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
8383
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
8384
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
8538
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera, lockedFor(snapshot, "camera", inCall)) : null,
8539
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone, lockedFor(snapshot, "microphone", inCall)) : null
8385
8540
  };
8386
8541
  }
8542
+ function lockedFor(snapshot, kind, inCall) {
8543
+ if (!inCall) return null;
8544
+ const role = effectiveRole(snapshot);
8545
+ if (isLockedFor(snapshot.livePolicy, role, kind)) return "locked";
8546
+ if (snapshot.livePolicy.recordingConsent === "view_only_until_agree" && consentPending(snapshot.livePolicy, role, snapshot.recording === "recording", snapshot.recordingConsentGiven)) {
8547
+ return "locked";
8548
+ }
8549
+ if (kind === "microphone" && snapshot.mutedByHost) return "muted_by_host";
8550
+ return null;
8551
+ }
8387
8552
  function staticObservation(phase, reason = null) {
8388
8553
  return {
8389
8554
  phase,