@ai-matrx/meet 0.11.42 → 0.11.44

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/core.cjs CHANGED
@@ -110,6 +110,7 @@ __export(core_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,
@@ -145,6 +146,7 @@ __export(core_exports, {
145
146
  createReconnectPolicy: () => createReconnectPolicy,
146
147
  createRecordingController: () => createRecordingController,
147
148
  createRoomEngine: () => createRoomEngine,
149
+ createSpeakingWhileMutedDetector: () => createSpeakingWhileMutedDetector,
148
150
  createTokenClient: () => createTokenClient,
149
151
  createWebDeviceStorage: () => createWebDeviceStorage,
150
152
  createWebEffectsStorage: () => createWebEffectsStorage,
@@ -153,6 +155,7 @@ __export(core_exports, {
153
155
  createWebRoomTokenStorage: () => createWebRoomTokenStorage,
154
156
  defaultCaptionLanguage: () => defaultCaptionLanguage,
155
157
  detectBrowser: () => detectBrowser,
158
+ detectCapabilities: () => detectCapabilities,
156
159
  detectOs: () => detectOs,
157
160
  deviceChipLabel: () => deviceChipLabel,
158
161
  deviceFixSteps: () => deviceFixSteps,
@@ -177,6 +180,7 @@ __export(core_exports, {
177
180
  isLinkFollower: () => isLinkFollower,
178
181
  isListenOnly: () => isListenOnly,
179
182
  isLivePhase: () => isLivePhase,
183
+ isMobileUserAgent: () => isMobileUserAgent,
180
184
  isNoteTaker: () => isNoteTaker,
181
185
  isObserverRole: () => isObserverRole,
182
186
  isRealName: () => isRealName,
@@ -268,6 +272,7 @@ __export(core_exports, {
268
272
  transcriptDocument: () => transcriptDocument,
269
273
  unavailableFrom: () => unavailableFrom,
270
274
  uniqueAttendees: () => uniqueAttendees,
275
+ unsupportedWords: () => unsupportedWords,
271
276
  untranslatedLines: () => untranslatedLines,
272
277
  userTopic: () => userTopic,
273
278
  wasRefused: () => wasRefused,
@@ -911,6 +916,8 @@ var MEET_ROUTES = {
911
916
  noteTakerStop: "/api/v1/meet/agent/stop",
912
917
  /** In-meeting Q&A. Streams NDJSON; body is `{meeting_id, question}` only. */
913
918
  intelligenceAsk: "/api/v1/meet/intelligence/ask",
919
+ /** Anyone in the meeting (guests with the room pass included) asks for captions for themselves. */
920
+ noteTakerCaptions: "/api/v1/meet/agent/captions",
914
921
  /** A host ends the meeting for everyone. Served by MRI-A6. */
915
922
  meetingEnd: "/api/v1/meet/end",
916
923
  /**
@@ -1057,7 +1064,8 @@ function parseRoomToken(body, roomName) {
1057
1064
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
1058
1065
  observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
1059
1066
  connectionPolicy: connectionPolicyFrom(payload["connection_policy"]),
1060
- leavePolicy: leavePolicyFrom(payload["leave_policy"])
1067
+ leavePolicy: leavePolicyFrom(payload["leave_policy"]),
1068
+ canReclaim: payload["can_reclaim"] === true
1061
1069
  };
1062
1070
  }
1063
1071
  function createTokenClient(options) {
@@ -1519,6 +1527,42 @@ function createMeetAi(options) {
1519
1527
  },
1520
1528
  "meet.ai.startNoteTaker"
1521
1529
  ),
1530
+ requestCaptions: async ({ meetingId }) => {
1531
+ const roomToken = options.resolveRoomToken?.() ?? null;
1532
+ const operation = "meet.ai.requestCaptions";
1533
+ try {
1534
+ const response = await send({
1535
+ path: MEET_ROUTES.noteTakerCaptions,
1536
+ body: { meeting_id: meetingId },
1537
+ operation,
1538
+ requireAuth: roomToken === null,
1539
+ ...roomToken !== null ? { headers: { [MEETING_PASS_HEADER]: roomToken } } : {}
1540
+ });
1541
+ if (!response.ok) {
1542
+ options.store.patch({ captionsRefusal: await refusalFor(response, operation) });
1543
+ return;
1544
+ }
1545
+ let payload = null;
1546
+ try {
1547
+ payload = await response.json();
1548
+ } catch {
1549
+ payload = null;
1550
+ }
1551
+ const row = typeof payload === "object" && payload !== null ? payload : {};
1552
+ const detail = typeof row["detail"] === "string" ? row["detail"] : "";
1553
+ options.store.patch({
1554
+ captionsRefusal: row["dispatched"] === true ? null : { status: 200, reason: detail, remedy: "" }
1555
+ });
1556
+ } catch (error) {
1557
+ options.store.patch({
1558
+ captionsRefusal: {
1559
+ status: 0,
1560
+ reason: error.message,
1561
+ remedy: error.remedy ?? ""
1562
+ }
1563
+ });
1564
+ }
1565
+ },
1522
1566
  stopNoteTaker: ({ meetingId }) => dispatch(
1523
1567
  MEET_ROUTES.noteTakerStop,
1524
1568
  { meeting_id: meetingId },
@@ -1557,7 +1601,7 @@ function exactNoteTakerParticipant(meeting, participants) {
1557
1601
  }
1558
1602
  function noteTakerStatus(args) {
1559
1603
  const present = exactNoteTakerParticipant(args.meeting, args.participants) !== null;
1560
- const raw = args.meeting?.metadata[NOTE_TAKER_METADATA_KEY];
1604
+ const raw = args.meeting?.metadata?.[NOTE_TAKER_METADATA_KEY];
1561
1605
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
1562
1606
  const recorded = readState(row["state"]);
1563
1607
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
@@ -3290,6 +3334,8 @@ function planFromRow(row) {
3290
3334
  minutes: num2(row["minutes"]) ?? 0,
3291
3335
  openedAt: str(row["opened_at"]),
3292
3336
  endsAt: str(row["ends_at"]),
3337
+ closingAt: str(row["closing_at"]),
3338
+ moveMode: row["move_mode"] === "auto" ? "auto" : "invite",
3293
3339
  closedAt: str(row["closed_at"]),
3294
3340
  broadcasts: (Array.isArray(row["broadcasts"]) ? row["broadcasts"] : []).map(broadcastFrom).filter((b) => b !== null)
3295
3341
  };
@@ -3441,9 +3487,17 @@ function autoAssign(identities, rooms, exclude = []) {
3441
3487
  return out;
3442
3488
  }
3443
3489
  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);
3490
+ if (plan === null || plan.state !== "open") return null;
3491
+ const ends = [plan.endsAt, plan.closingAt].map((stamp) => stamp === null ? Number.NaN : Date.parse(stamp)).filter((time) => !Number.isNaN(time));
3492
+ if (ends.length === 0) return null;
3493
+ return Math.max(0, Math.min(...ends) - now);
3494
+ }
3495
+ var BREAKOUT_COUNTDOWN_MS = 3e4;
3496
+ function breakoutClosingMs(plan, now) {
3497
+ if (plan === null || plan.state !== "open") return null;
3498
+ const remaining = breakoutRemainingMs(plan, now);
3499
+ if (remaining === null) return null;
3500
+ return plan.closingAt !== null || remaining <= BREAKOUT_COUNTDOWN_MS ? remaining : null;
3447
3501
  }
3448
3502
  function defaultCaptionLanguage(settings, viewerLanguage) {
3449
3503
  const configured = settings?.captionTranslationLanguage.trim() ?? "";
@@ -3690,12 +3744,55 @@ function recordingLabel(state) {
3690
3744
  }
3691
3745
  }
3692
3746
 
3747
+ // src/core/live-policy.ts
3748
+ var DEFAULT_LIVE_POLICY = {
3749
+ chatMode: "everyone",
3750
+ avLock: "off",
3751
+ timeLimitMinutes: 0,
3752
+ timeLimitWarningMinutes: 5,
3753
+ recordingConsent: "notice"
3754
+ };
3755
+ function oneOf(value, allowed, fallback) {
3756
+ return typeof value === "string" && allowed.includes(value) ? value : fallback;
3757
+ }
3758
+ function whole(value, fallback) {
3759
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
3760
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
3761
+ }
3762
+ function livePolicyFrom(raw) {
3763
+ const o = typeof raw === "object" && raw !== null ? raw : {};
3764
+ return {
3765
+ chatMode: oneOf(o["chat_mode"], ["everyone", "hosts_only", "off"], DEFAULT_LIVE_POLICY.chatMode),
3766
+ avLock: oneOf(o["attendee_av_lock"], ["off", "mic", "camera", "both"], DEFAULT_LIVE_POLICY.avLock),
3767
+ timeLimitMinutes: whole(o["time_limit_minutes"], 0),
3768
+ timeLimitWarningMinutes: whole(o["time_limit_warning_minutes"], DEFAULT_LIVE_POLICY.timeLimitWarningMinutes),
3769
+ recordingConsent: oneOf(
3770
+ o["recording_consent"],
3771
+ ["notice", "ok_or_leave", "view_only_until_agree"],
3772
+ DEFAULT_LIVE_POLICY.recordingConsent
3773
+ )
3774
+ };
3775
+ }
3776
+ function samePolicy(a, b) {
3777
+ return a.chatMode === b.chatMode && a.avLock === b.avLock && a.timeLimitMinutes === b.timeLimitMinutes && a.timeLimitWarningMinutes === b.timeLimitWarningMinutes && a.recordingConsent === b.recordingConsent;
3778
+ }
3779
+ var MODERATORS = /* @__PURE__ */ new Set(["host", "cohost"]);
3780
+ function isLockedFor(policy, role, kind) {
3781
+ if (MODERATORS.has(role)) return false;
3782
+ return policy.avLock === "both" || (kind === "microphone" ? policy.avLock === "mic" : policy.avLock === "camera");
3783
+ }
3784
+ function consentPending(policy, role, recordingOn, given) {
3785
+ return recordingOn && !given && policy.recordingConsent !== "notice" && !MODERATORS.has(role);
3786
+ }
3787
+
3693
3788
  // src/core/attendance.ts
3789
+ var IDLE_PREVIEW = { state: "idle", unavailable: null };
3694
3790
  var IDLE_MEDIA_REGION = {
3695
3791
  permission: { microphone: "unknown", camera: "unknown" },
3696
3792
  audioOutput: "ok",
3697
3793
  notice: null,
3698
- offer: null
3794
+ offer: null,
3795
+ preview: { microphone: IDLE_PREVIEW, camera: IDLE_PREVIEW }
3699
3796
  };
3700
3797
  var IDLE_TRACK = { enabled: false, unavailable: null };
3701
3798
  var IDLE_MEDIA = {
@@ -3760,6 +3857,11 @@ var EMPTY = {
3760
3857
  presenter: null,
3761
3858
  waitingCount: 0,
3762
3859
  leavePolicy: DEFAULT_LEAVE_POLICY,
3860
+ livePolicy: DEFAULT_LIVE_POLICY,
3861
+ canReclaim: false,
3862
+ recordingConsentGiven: false,
3863
+ sessionStartedAt: null,
3864
+ mutedByHost: false,
3763
3865
  moderationNotice: null,
3764
3866
  actionNotice: null,
3765
3867
  privateChatRequest: null,
@@ -3769,6 +3871,8 @@ var EMPTY = {
3769
3871
  names: {},
3770
3872
  wrapUp: null,
3771
3873
  noteTakerRefusal: null,
3874
+ captionsOn: false,
3875
+ captionsRefusal: null,
3772
3876
  media: IDLE_MEDIA,
3773
3877
  mediaRegion: IDLE_MEDIA_REGION,
3774
3878
  failure: null,
@@ -3973,7 +4077,12 @@ function createMeetStore(initial = {}) {
3973
4077
  if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.reason === next.unavailable?.reason && current.unavailable?.message === next.unavailable?.message) {
3974
4078
  return;
3975
4079
  }
3976
- commit({ ...state, media: { ...state.media, [kind]: next } });
4080
+ commit({
4081
+ ...state,
4082
+ media: { ...state.media, [kind]: next },
4083
+ // The person turned their own microphone on again: the host's mute is over.
4084
+ ...kind === "microphone" && next.enabled && state.mutedByHost ? { mutedByHost: false } : {}
4085
+ });
3977
4086
  },
3978
4087
  reset() {
3979
4088
  const phase = nextPhase(state.phase, { type: "reset", teardown: true }) ?? EMPTY.phase;
@@ -4199,10 +4308,11 @@ function moderationText(kind, payload) {
4199
4308
  switch (kind) {
4200
4309
  case "muted": {
4201
4310
  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.`;
4311
+ const who = payload["by_role"] === "host" || payload["by_role"] === "cohost" ? "the host" : by;
4312
+ 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
4313
  }
4204
4314
  case "ask-unmute":
4205
- return `${by} is asking you to unmute.`;
4315
+ return `${by === "The host" ? "The host" : by} asked you to unmute.`;
4206
4316
  case "removed":
4207
4317
  return `${by} removed you from this meeting.`;
4208
4318
  default:
@@ -4228,7 +4338,7 @@ function applyModeration(store, payload, now) {
4228
4338
  grants: { ...snapshot.grants, role },
4229
4339
  moderationNotice: {
4230
4340
  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.",
4341
+ 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
4342
  at: now()
4233
4343
  }
4234
4344
  });
@@ -4240,7 +4350,10 @@ function applyModeration(store, payload, now) {
4240
4350
  case "removed": {
4241
4351
  const sentence = moderationText(kind, payload);
4242
4352
  if (sentence === null) return ignored("a moderation event with no sentence");
4243
- store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
4353
+ store.patch({
4354
+ moderationNotice: { kind, text: sentence, at: now() },
4355
+ ...kind === "muted" && (text2(payload["source"]) ?? "microphone") === "microphone" ? { mutedByHost: true } : {}
4356
+ });
4244
4357
  return HANDLED;
4245
4358
  }
4246
4359
  default:
@@ -4606,6 +4719,47 @@ function gridColumns(count2) {
4606
4719
  return 5;
4607
4720
  }
4608
4721
 
4722
+ // src/core/capabilities.ts
4723
+ var IN_APP = [
4724
+ [/Instagram/i, "Instagram"],
4725
+ [/FBAN|FBAV|FB_IAB/i, "Facebook"],
4726
+ [/LinkedInApp/i, "LinkedIn"],
4727
+ [/MicroMessenger/i, "WeChat"],
4728
+ [/Snapchat/i, "Snapchat"],
4729
+ [/\bLine\//i, "LINE"],
4730
+ [/Twitter|TwitterAndroid/i, "X"],
4731
+ [/TikTok|musical_ly|BytedanceWebview/i, "TikTok"]
4732
+ ];
4733
+ function isMobileUserAgent(userAgent, maxTouchPoints = 0) {
4734
+ if (/iPhone|iPod|Android|Mobile|iPad/i.test(userAgent)) return true;
4735
+ return /Macintosh/.test(userAgent) && maxTouchPoints > 1;
4736
+ }
4737
+ function detectCapabilities(scope = typeof globalThis === "undefined" ? null : globalThis) {
4738
+ if (scope === null || scope.navigator === void 0) {
4739
+ return { canJoin: true, reason: null, inAppBrowser: null, mobile: false, canShareScreen: false };
4740
+ }
4741
+ const ua = scope.navigator.userAgent ?? "";
4742
+ const webrtc = typeof scope.RTCPeerConnection === "function" || typeof scope.webkitRTCPeerConnection === "function";
4743
+ const mobile = isMobileUserAgent(ua, scope.navigator.maxTouchPoints ?? 0);
4744
+ const inAppBrowser = IN_APP.find(([pattern]) => pattern.test(ua))?.[1] ?? null;
4745
+ return {
4746
+ canJoin: webrtc,
4747
+ reason: webrtc ? null : "no_webrtc",
4748
+ inAppBrowser,
4749
+ mobile,
4750
+ canShareScreen: !mobile && typeof scope.navigator.mediaDevices?.getDisplayMedia === "function"
4751
+ };
4752
+ }
4753
+ function unsupportedWords(capabilities) {
4754
+ if (capabilities.inAppBrowser !== null) {
4755
+ return {
4756
+ title: `${capabilities.inAppBrowser}'s browser can't join calls`,
4757
+ body: "Open this link in Chrome, Safari, Edge or Firefox"
4758
+ };
4759
+ }
4760
+ return { title: "This browser can't join calls", body: "Open this link in Chrome, Safari, Edge or Firefox" };
4761
+ }
4762
+
4609
4763
  // src/core/room-driver.ts
4610
4764
  var DATA_TOPICS = {
4611
4765
  reaction: "mx.reaction",
@@ -4760,7 +4914,15 @@ function createLiveKitDriver(handlers, options = {}) {
4760
4914
  created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
4761
4915
  handlers.participantLeft?.(participant.identity, reasonName(reason));
4762
4916
  roster();
4763
- }).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
4917
+ }).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, (publication) => {
4918
+ if (publication.source === Track.Source.ScreenShare) {
4919
+ const ended = () => {
4920
+ void created.localParticipant.setScreenShareEnabled(false).catch(() => void 0);
4921
+ };
4922
+ publication.track?.mediaStreamTrack.addEventListener("ended", ended, { once: true });
4923
+ }
4924
+ roster();
4925
+ }).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
4764
4926
  handlers.roomMetadata?.(metadata ?? "");
4765
4927
  }).on(RoomEvent.AudioPlaybackStatusChanged, (canPlay) => {
4766
4928
  handlers.audioPlayback?.(canPlay);
@@ -4901,7 +5063,10 @@ function createLiveKitDriver(handlers, options = {}) {
4901
5063
  },
4902
5064
  async setScreenShareEnabled(enabled) {
4903
5065
  try {
4904
- await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(enabled);
5066
+ await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(
5067
+ enabled,
5068
+ enabled ? { audio: true, systemAudio: "include" } : void 0
5069
+ );
4905
5070
  } catch (error) {
4906
5071
  throw normalizeScreenShareError(error, "meet.setScreenShareEnabled");
4907
5072
  }
@@ -4940,8 +5105,13 @@ function createLiveKitDriver(handlers, options = {}) {
4940
5105
  supportsVideoEffects() {
4941
5106
  return videoEffectsSupported();
4942
5107
  },
5108
+ screenShareHasAudio() {
5109
+ const target = room;
5110
+ if (target === null) return false;
5111
+ return target.localParticipant.getTrackPublications().some((p) => String(p.source) === "screen_share_audio" && p.track !== void 0);
5112
+ },
4943
5113
  supportsScreenShare() {
4944
- return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getDisplayMedia === "function";
5114
+ return detectCapabilities().canShareScreen;
4945
5115
  },
4946
5116
  attach(identity, kind, element) {
4947
5117
  const target = requireRoom("attach");
@@ -5062,6 +5232,7 @@ function applyDoorRow(store, attendee) {
5062
5232
  function decidedState(state) {
5063
5233
  return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
5064
5234
  }
5235
+ var LIVE_POLICY_POLL_MS = 3e3;
5065
5236
  function createMeetingSession(options) {
5066
5237
  const { manager, repository, store, identity } = options;
5067
5238
  let channel = null;
@@ -5203,6 +5374,46 @@ function createMeetingSession(options) {
5203
5374
  stopWaiting();
5204
5375
  }
5205
5376
  }
5377
+ let policyTimer = null;
5378
+ let policyReading = false;
5379
+ async function readLivePolicy() {
5380
+ const meeting = store.snapshot().meeting;
5381
+ const pass = options.resolvePass?.() ?? null;
5382
+ if (meeting === null || pass === null || policyReading) return;
5383
+ policyReading = true;
5384
+ try {
5385
+ const response = await options.api.request({
5386
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
5387
+ method: "GET",
5388
+ requireAuth: false,
5389
+ operation: "meet.state(policy)",
5390
+ headers: { [MEETING_PASS_HEADER]: pass }
5391
+ });
5392
+ if (!response.ok) return;
5393
+ const read = await response.json();
5394
+ const next = livePolicyFrom(read.live_policy);
5395
+ const started = read.session_started_at ? Date.parse(read.session_started_at) : NaN;
5396
+ const snapshot = store.snapshot();
5397
+ const startedAt = Number.isFinite(started) ? started : snapshot.sessionStartedAt;
5398
+ if (!samePolicy(snapshot.livePolicy, next) || startedAt !== snapshot.sessionStartedAt) {
5399
+ store.patch({ livePolicy: next, sessionStartedAt: startedAt });
5400
+ }
5401
+ } catch {
5402
+ } finally {
5403
+ policyReading = false;
5404
+ }
5405
+ }
5406
+ function syncLivePolicyTimer() {
5407
+ const inCall = store.snapshot().phase === "in_call";
5408
+ if (inCall && policyTimer === null) {
5409
+ void readLivePolicy();
5410
+ policyTimer = setInterval(() => void readLivePolicy(), LIVE_POLICY_POLL_MS);
5411
+ } else if (!inCall && policyTimer !== null) {
5412
+ clearInterval(policyTimer);
5413
+ policyTimer = null;
5414
+ }
5415
+ }
5416
+ const unsubscribePolicy = store.subscribe(syncLivePolicyTimer);
5206
5417
  async function readState2() {
5207
5418
  if (reading !== null) return reading;
5208
5419
  const meeting = store.snapshot().meeting;
@@ -5589,6 +5800,9 @@ function createMeetingSession(options) {
5589
5800
  },
5590
5801
  dispose() {
5591
5802
  stopWaiting();
5803
+ unsubscribePolicy();
5804
+ if (policyTimer !== null) clearInterval(policyTimer);
5805
+ policyTimer = null;
5592
5806
  channel?.close();
5593
5807
  channel = null;
5594
5808
  meetingChannel?.close();
@@ -6202,7 +6416,7 @@ function deviceFixSteps(kind, reason, where) {
6202
6416
  case "safari":
6203
6417
  return where.os === "ios" ? [`Settings \u203A Apps \u203A Safari \u203A ${device}`, "Choose Allow", "Return here and press Try again"] : ["Safari \u203A Settings for This Website", `Set ${device} to Allow`, "Press Try again"];
6204
6418
  default:
6205
- return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
6419
+ return ["Click the site settings icon in the address bar", `Set ${device} to Allow`, "Press Try again"];
6206
6420
  }
6207
6421
  case "blocked_os": {
6208
6422
  const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
@@ -6300,7 +6514,7 @@ function createMediaSupervisor(options) {
6300
6514
  function setRegion(next) {
6301
6515
  const current = region();
6302
6516
  const merged = { ...current, ...next };
6303
- if (merged.audioOutput === current.audioOutput && merged.notice === current.notice && merged.offer === current.offer && merged.permission.microphone === current.permission.microphone && merged.permission.camera === current.permission.camera) {
6517
+ if (merged.audioOutput === current.audioOutput && merged.notice === current.notice && merged.offer === current.offer && merged.preview === current.preview && merged.permission.microphone === current.permission.microphone && merged.permission.camera === current.permission.camera) {
6304
6518
  return;
6305
6519
  }
6306
6520
  store.patch({ mediaRegion: merged });
@@ -6396,7 +6610,7 @@ function createMediaSupervisor(options) {
6396
6610
  if (permission !== "unknown") setPermission(kind, permission);
6397
6611
  return unavailableFrom(normalized, classifyDeviceFailure(normalized, permission));
6398
6612
  }
6399
- return {
6613
+ const api = {
6400
6614
  start() {
6401
6615
  if (started || platform === null) return;
6402
6616
  started = true;
@@ -6418,6 +6632,16 @@ function createMediaSupervisor(options) {
6418
6632
  started = false;
6419
6633
  },
6420
6634
  unavailable,
6635
+ async preview(kind, outcome) {
6636
+ api.start();
6637
+ let next;
6638
+ if (typeof outcome === "string") next = { state: outcome, unavailable: null };
6639
+ else next = { state: "unavailable", unavailable: await unavailable(outcome.error, kind, `meet.${kind}Preview`) };
6640
+ const current = region().preview;
6641
+ const was = current[kind];
6642
+ if (was.state === next.state && was.unavailable?.reason === next.unavailable?.reason && was.unavailable?.message === next.unavailable?.message) return;
6643
+ setRegion({ preview: { ...region().preview, [kind]: next } });
6644
+ },
6421
6645
  onAudioPlayback(canPlay) {
6422
6646
  setRegion({ audioOutput: canPlay ? "ok" : "blocked" });
6423
6647
  },
@@ -6476,6 +6700,7 @@ function createMediaSupervisor(options) {
6476
6700
  setRegion({ notice: null });
6477
6701
  }
6478
6702
  };
6703
+ return api;
6479
6704
  }
6480
6705
 
6481
6706
  // src/core/room.ts
@@ -6637,7 +6862,8 @@ function createRoomEngine(options) {
6637
6862
  message: normalized.message,
6638
6863
  remedy: normalized.remedy,
6639
6864
  reason: normalized.reason,
6640
- retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3
6865
+ retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3,
6866
+ stage: "join"
6641
6867
  }
6642
6868
  });
6643
6869
  store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
@@ -6906,6 +7132,7 @@ function createRoomEngine(options) {
6906
7132
  lastServerUrl = token.serverUrl;
6907
7133
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6908
7134
  if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
7135
+ store.patch({ canReclaim: token.canReclaim === true });
6909
7136
  store.patch({
6910
7137
  localIdentity: token.identity,
6911
7138
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -7027,6 +7254,10 @@ function createRoomEngine(options) {
7027
7254
  }
7028
7255
  departures.clear();
7029
7256
  liveRoster = list;
7257
+ const localNow = list.find((p) => p.isLocal);
7258
+ if (localNow !== void 0 && !localNow.screenSharing && store.snapshot().share.state === "presenting") {
7259
+ store.patch({ share: NO_SHARE });
7260
+ }
7030
7261
  const merged = mergeGhosts(list, ghosts, at);
7031
7262
  ghosts = merged.kept;
7032
7263
  const people = /* @__PURE__ */ new Map();
@@ -7246,7 +7477,10 @@ function createRoomEngine(options) {
7246
7477
  try {
7247
7478
  await driver.setScreenShareEnabled(enabled);
7248
7479
  ambiguousShareDenials = 0;
7249
- store.patch({ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now() } });
7480
+ const audio = enabled ? driver?.screenShareHasAudio?.() : void 0;
7481
+ store.patch({
7482
+ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now(), ...audio !== void 0 ? { audio } : {} }
7483
+ });
7250
7484
  } catch (error) {
7251
7485
  const kind = classifyScreenShareError(error);
7252
7486
  if (kind === "ambiguous") {
@@ -7313,13 +7547,14 @@ function createRoomEngine(options) {
7313
7547
  return driver?.supportsVideoEffects?.() ?? false;
7314
7548
  },
7315
7549
  supportsScreenShare() {
7316
- return driver?.supportsScreenShare?.() ?? true;
7550
+ return driver?.supportsScreenShare?.() ?? detectCapabilities().canShareScreen;
7317
7551
  },
7318
7552
  async switchDevice(kind, deviceId) {
7319
7553
  if (driver === null) return;
7320
7554
  await driver.switchDevice(kind, deviceId);
7321
7555
  },
7322
7556
  enableAudio: () => media.enableAudio(),
7557
+ previewDevice: (kind, outcome) => media.preview(kind, outcome),
7323
7558
  acceptDeviceOffer: () => media.acceptOffer(),
7324
7559
  dismissDeviceOffer: () => media.dismissOffer(),
7325
7560
  dismissDeviceNotice: () => media.dismissNotice(),
@@ -7405,8 +7640,12 @@ var MODERATION_ACTIONS = [
7405
7640
  "lower_hand",
7406
7641
  "lower_all_hands",
7407
7642
  "make_cohost",
7643
+ "make_presenter",
7644
+ "revoke_presenter",
7645
+ "set_av_lock",
7408
7646
  "revoke_cohost",
7409
7647
  "transfer_host",
7648
+ "reclaim_host",
7410
7649
  "rename",
7411
7650
  "remove",
7412
7651
  "report"
@@ -8336,14 +8575,31 @@ function takeMoveRequest(store) {
8336
8575
 
8337
8576
  // src/core/observation.ts
8338
8577
  function observedRole(role, standing) {
8339
- if (role === "host" || role === "cohost") return role;
8578
+ if (role === "host" || role === "cohost" || role === "presenter") return role;
8340
8579
  return isLinkFollower(standing) ? "guest" : "participant";
8341
8580
  }
8342
- function observedDevice(track, permission = "unknown") {
8581
+ function observedDevice(track, permission = "unknown", held = null) {
8582
+ if (held === "locked") return "locked";
8343
8583
  if (track.enabled) return "on";
8584
+ if (held === "muted_by_host") return "muted_by_host";
8344
8585
  const reason = deviceUnavailableReason(track, permission);
8345
8586
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
8346
8587
  }
8588
+ function observedPreview(preview, permission = "unknown") {
8589
+ if (permission === "denied") return "unavailable:denied";
8590
+ switch (preview.state) {
8591
+ case "on":
8592
+ return "on";
8593
+ case "off":
8594
+ return "muted_by_me";
8595
+ case "prompting":
8596
+ return "prompting";
8597
+ case "unavailable":
8598
+ return `unavailable:${preview.unavailable?.reason ?? "failed"}`;
8599
+ default:
8600
+ return null;
8601
+ }
8602
+ }
8347
8603
  function endedRecord(snapshot, given) {
8348
8604
  const record = snapshot?.meeting ?? given;
8349
8605
  return record !== null && record.endedAt !== null;
@@ -8367,20 +8623,34 @@ function observe(input) {
8367
8623
  const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
8368
8624
  const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
8369
8625
  const deviceInPlay = phase === "joining" || inCall;
8626
+ const region = snapshot?.mediaRegion;
8627
+ const atPrejoin = phase === "prejoin" && region !== void 0;
8370
8628
  return {
8371
8629
  phase,
8372
8630
  reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
8373
8631
  role,
8374
- connection: snapshot?.connection ?? "stable",
8632
+ // `net-blip`: in the call, a drop shorter than `reconnect_banner_delay_ms` is not something the
8633
+ // person sees — the contract says what is on screen, so it reads `stable` until the banner shows.
8634
+ connection: snapshot === null ? "stable" : inCall && snapshot.connection !== "stable" && !snapshot.connectionBanner ? "stable" : snapshot.connection,
8375
8635
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
8376
8636
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
8377
8637
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
8378
8638
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
8379
8639
  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
8640
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera, lockedFor(snapshot, "camera", inCall)) : atPrejoin ? observedPreview(region.preview.camera, region.permission.camera) : null,
8641
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone, lockedFor(snapshot, "microphone", inCall)) : atPrejoin ? observedPreview(region.preview.microphone, region.permission.microphone) : null
8382
8642
  };
8383
8643
  }
8644
+ function lockedFor(snapshot, kind, inCall) {
8645
+ if (!inCall) return null;
8646
+ const role = effectiveRole(snapshot);
8647
+ if (isLockedFor(snapshot.livePolicy, role, kind)) return "locked";
8648
+ if (snapshot.livePolicy.recordingConsent === "view_only_until_agree" && consentPending(snapshot.livePolicy, role, snapshot.recording === "recording", snapshot.recordingConsentGiven)) {
8649
+ return "locked";
8650
+ }
8651
+ if (kind === "microphone" && snapshot.mutedByHost) return "muted_by_host";
8652
+ return null;
8653
+ }
8384
8654
  function staticObservation(phase, reason = null) {
8385
8655
  return {
8386
8656
  phase,
@@ -8448,4 +8718,37 @@ function microphoneNoticeCode(track, permission = "unknown") {
8448
8718
  if (reason === null) return null;
8449
8719
  return deviceNoticeCode("microphone", reason) === "mic-blocked" ? "mic-blocked" : "device-lost";
8450
8720
  }
8721
+
8722
+ // src/core/speaking-detector.ts
8723
+ function createSpeakingWhileMutedDetector(options = {}) {
8724
+ const threshold = options.threshold ?? 0.06;
8725
+ const holdMs = options.holdMs ?? 600;
8726
+ const gapMs = options.gapMs ?? 400;
8727
+ const cooldownMs = options.cooldownMs ?? 2e4;
8728
+ let speechStart = null;
8729
+ let lastLoud = null;
8730
+ let firedAt = null;
8731
+ return {
8732
+ feed(level, at) {
8733
+ if (level >= threshold) {
8734
+ if (speechStart === null || lastLoud === null || at - lastLoud > gapMs) speechStart = at;
8735
+ lastLoud = at;
8736
+ } else if (lastLoud !== null && at - lastLoud > gapMs) {
8737
+ speechStart = null;
8738
+ lastLoud = null;
8739
+ }
8740
+ if (speechStart === null || at - speechStart < holdMs) return false;
8741
+ if (firedAt !== null && at - firedAt < cooldownMs) return false;
8742
+ firedAt = at;
8743
+ speechStart = null;
8744
+ lastLoud = null;
8745
+ return true;
8746
+ },
8747
+ reset() {
8748
+ speechStart = null;
8749
+ lastLoud = null;
8750
+ firedAt = null;
8751
+ }
8752
+ };
8753
+ }
8451
8754
  //# sourceMappingURL=core.cjs.map