@ai-matrx/meet 0.11.42 → 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;
@@ -4199,10 +4302,11 @@ function moderationText(kind, payload) {
4199
4302
  switch (kind) {
4200
4303
  case "muted": {
4201
4304
  const source = text2(payload["source"]) ?? "microphone";
4202
- 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}.`;
4203
4307
  }
4204
4308
  case "ask-unmute":
4205
- return `${by} is asking you to unmute.`;
4309
+ return `${by === "The host" ? "The host" : by} asked you to unmute.`;
4206
4310
  case "removed":
4207
4311
  return `${by} removed you from this meeting.`;
4208
4312
  default:
@@ -4228,7 +4332,7 @@ function applyModeration(store, payload, now) {
4228
4332
  grants: { ...snapshot.grants, role },
4229
4333
  moderationNotice: {
4230
4334
  kind: "role",
4231
- 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.",
4232
4336
  at: now()
4233
4337
  }
4234
4338
  });
@@ -4240,7 +4344,10 @@ function applyModeration(store, payload, now) {
4240
4344
  case "removed": {
4241
4345
  const sentence = moderationText(kind, payload);
4242
4346
  if (sentence === null) return ignored("a moderation event with no sentence");
4243
- 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
+ });
4244
4351
  return HANDLED;
4245
4352
  }
4246
4353
  default:
@@ -5062,6 +5169,7 @@ function applyDoorRow(store, attendee) {
5062
5169
  function decidedState(state) {
5063
5170
  return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
5064
5171
  }
5172
+ var LIVE_POLICY_POLL_MS = 3e3;
5065
5173
  function createMeetingSession(options) {
5066
5174
  const { manager, repository, store, identity } = options;
5067
5175
  let channel = null;
@@ -5203,6 +5311,46 @@ function createMeetingSession(options) {
5203
5311
  stopWaiting();
5204
5312
  }
5205
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);
5206
5354
  async function readState2() {
5207
5355
  if (reading !== null) return reading;
5208
5356
  const meeting = store.snapshot().meeting;
@@ -5589,6 +5737,9 @@ function createMeetingSession(options) {
5589
5737
  },
5590
5738
  dispose() {
5591
5739
  stopWaiting();
5740
+ unsubscribePolicy();
5741
+ if (policyTimer !== null) clearInterval(policyTimer);
5742
+ policyTimer = null;
5592
5743
  channel?.close();
5593
5744
  channel = null;
5594
5745
  meetingChannel?.close();
@@ -6906,6 +7057,7 @@ function createRoomEngine(options) {
6906
7057
  lastServerUrl = token.serverUrl;
6907
7058
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6908
7059
  if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
7060
+ store.patch({ canReclaim: token.canReclaim === true });
6909
7061
  store.patch({
6910
7062
  localIdentity: token.identity,
6911
7063
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -7405,8 +7557,12 @@ var MODERATION_ACTIONS = [
7405
7557
  "lower_hand",
7406
7558
  "lower_all_hands",
7407
7559
  "make_cohost",
7560
+ "make_presenter",
7561
+ "revoke_presenter",
7562
+ "set_av_lock",
7408
7563
  "revoke_cohost",
7409
7564
  "transfer_host",
7565
+ "reclaim_host",
7410
7566
  "rename",
7411
7567
  "remove",
7412
7568
  "report"
@@ -8336,11 +8492,13 @@ function takeMoveRequest(store) {
8336
8492
 
8337
8493
  // src/core/observation.ts
8338
8494
  function observedRole(role, standing) {
8339
- if (role === "host" || role === "cohost") return role;
8495
+ if (role === "host" || role === "cohost" || role === "presenter") return role;
8340
8496
  return isLinkFollower(standing) ? "guest" : "participant";
8341
8497
  }
8342
- function observedDevice(track, permission = "unknown") {
8498
+ function observedDevice(track, permission = "unknown", held = null) {
8499
+ if (held === "locked") return "locked";
8343
8500
  if (track.enabled) return "on";
8501
+ if (held === "muted_by_host") return "muted_by_host";
8344
8502
  const reason = deviceUnavailableReason(track, permission);
8345
8503
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
8346
8504
  }
@@ -8377,10 +8535,20 @@ function observe(input) {
8377
8535
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
8378
8536
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
8379
8537
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
8380
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
8381
- 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
8382
8540
  };
8383
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
+ }
8384
8552
  function staticObservation(phase, reason = null) {
8385
8553
  return {
8386
8554
  phase,