@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.js CHANGED
@@ -632,6 +632,8 @@ var MEET_ROUTES = {
632
632
  noteTakerStop: "/api/v1/meet/agent/stop",
633
633
  /** In-meeting Q&A. Streams NDJSON; body is `{meeting_id, question}` only. */
634
634
  intelligenceAsk: "/api/v1/meet/intelligence/ask",
635
+ /** Anyone in the meeting (guests with the room pass included) asks for captions for themselves. */
636
+ noteTakerCaptions: "/api/v1/meet/agent/captions",
635
637
  /** A host ends the meeting for everyone. Served by MRI-A6. */
636
638
  meetingEnd: "/api/v1/meet/end",
637
639
  /**
@@ -778,7 +780,8 @@ function parseRoomToken(body, roomName) {
778
780
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
779
781
  observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
780
782
  connectionPolicy: connectionPolicyFrom(payload["connection_policy"]),
781
- leavePolicy: leavePolicyFrom(payload["leave_policy"])
783
+ leavePolicy: leavePolicyFrom(payload["leave_policy"]),
784
+ canReclaim: payload["can_reclaim"] === true
782
785
  };
783
786
  }
784
787
  function createTokenClient(options) {
@@ -1240,6 +1243,42 @@ function createMeetAi(options) {
1240
1243
  },
1241
1244
  "meet.ai.startNoteTaker"
1242
1245
  ),
1246
+ requestCaptions: async ({ meetingId }) => {
1247
+ const roomToken = options.resolveRoomToken?.() ?? null;
1248
+ const operation = "meet.ai.requestCaptions";
1249
+ try {
1250
+ const response = await send({
1251
+ path: MEET_ROUTES.noteTakerCaptions,
1252
+ body: { meeting_id: meetingId },
1253
+ operation,
1254
+ requireAuth: roomToken === null,
1255
+ ...roomToken !== null ? { headers: { [MEETING_PASS_HEADER]: roomToken } } : {}
1256
+ });
1257
+ if (!response.ok) {
1258
+ options.store.patch({ captionsRefusal: await refusalFor(response, operation) });
1259
+ return;
1260
+ }
1261
+ let payload = null;
1262
+ try {
1263
+ payload = await response.json();
1264
+ } catch {
1265
+ payload = null;
1266
+ }
1267
+ const row = typeof payload === "object" && payload !== null ? payload : {};
1268
+ const detail = typeof row["detail"] === "string" ? row["detail"] : "";
1269
+ options.store.patch({
1270
+ captionsRefusal: row["dispatched"] === true ? null : { status: 200, reason: detail, remedy: "" }
1271
+ });
1272
+ } catch (error) {
1273
+ options.store.patch({
1274
+ captionsRefusal: {
1275
+ status: 0,
1276
+ reason: error.message,
1277
+ remedy: error.remedy ?? ""
1278
+ }
1279
+ });
1280
+ }
1281
+ },
1243
1282
  stopNoteTaker: ({ meetingId }) => dispatch(
1244
1283
  MEET_ROUTES.noteTakerStop,
1245
1284
  { meeting_id: meetingId },
@@ -1278,7 +1317,7 @@ function exactNoteTakerParticipant(meeting, participants) {
1278
1317
  }
1279
1318
  function noteTakerStatus(args) {
1280
1319
  const present = exactNoteTakerParticipant(args.meeting, args.participants) !== null;
1281
- const raw = args.meeting?.metadata[NOTE_TAKER_METADATA_KEY];
1320
+ const raw = args.meeting?.metadata?.[NOTE_TAKER_METADATA_KEY];
1282
1321
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
1283
1322
  const recorded = readState(row["state"]);
1284
1323
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
@@ -3011,6 +3050,8 @@ function planFromRow(row) {
3011
3050
  minutes: num2(row["minutes"]) ?? 0,
3012
3051
  openedAt: str(row["opened_at"]),
3013
3052
  endsAt: str(row["ends_at"]),
3053
+ closingAt: str(row["closing_at"]),
3054
+ moveMode: row["move_mode"] === "auto" ? "auto" : "invite",
3014
3055
  closedAt: str(row["closed_at"]),
3015
3056
  broadcasts: (Array.isArray(row["broadcasts"]) ? row["broadcasts"] : []).map(broadcastFrom).filter((b) => b !== null)
3016
3057
  };
@@ -3162,9 +3203,17 @@ function autoAssign(identities, rooms, exclude = []) {
3162
3203
  return out;
3163
3204
  }
3164
3205
  function breakoutRemainingMs(plan, now) {
3165
- if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
3166
- const end = Date.parse(plan.endsAt);
3167
- return Number.isNaN(end) ? null : Math.max(0, end - now);
3206
+ if (plan === null || plan.state !== "open") return null;
3207
+ const ends = [plan.endsAt, plan.closingAt].map((stamp) => stamp === null ? Number.NaN : Date.parse(stamp)).filter((time) => !Number.isNaN(time));
3208
+ if (ends.length === 0) return null;
3209
+ return Math.max(0, Math.min(...ends) - now);
3210
+ }
3211
+ var BREAKOUT_COUNTDOWN_MS = 3e4;
3212
+ function breakoutClosingMs(plan, now) {
3213
+ if (plan === null || plan.state !== "open") return null;
3214
+ const remaining = breakoutRemainingMs(plan, now);
3215
+ if (remaining === null) return null;
3216
+ return plan.closingAt !== null || remaining <= BREAKOUT_COUNTDOWN_MS ? remaining : null;
3168
3217
  }
3169
3218
  function defaultCaptionLanguage(settings, viewerLanguage) {
3170
3219
  const configured = settings?.captionTranslationLanguage.trim() ?? "";
@@ -3411,12 +3460,55 @@ function recordingLabel(state) {
3411
3460
  }
3412
3461
  }
3413
3462
 
3463
+ // src/core/live-policy.ts
3464
+ var DEFAULT_LIVE_POLICY = {
3465
+ chatMode: "everyone",
3466
+ avLock: "off",
3467
+ timeLimitMinutes: 0,
3468
+ timeLimitWarningMinutes: 5,
3469
+ recordingConsent: "notice"
3470
+ };
3471
+ function oneOf(value, allowed, fallback) {
3472
+ return typeof value === "string" && allowed.includes(value) ? value : fallback;
3473
+ }
3474
+ function whole(value, fallback) {
3475
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
3476
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
3477
+ }
3478
+ function livePolicyFrom(raw) {
3479
+ const o = typeof raw === "object" && raw !== null ? raw : {};
3480
+ return {
3481
+ chatMode: oneOf(o["chat_mode"], ["everyone", "hosts_only", "off"], DEFAULT_LIVE_POLICY.chatMode),
3482
+ avLock: oneOf(o["attendee_av_lock"], ["off", "mic", "camera", "both"], DEFAULT_LIVE_POLICY.avLock),
3483
+ timeLimitMinutes: whole(o["time_limit_minutes"], 0),
3484
+ timeLimitWarningMinutes: whole(o["time_limit_warning_minutes"], DEFAULT_LIVE_POLICY.timeLimitWarningMinutes),
3485
+ recordingConsent: oneOf(
3486
+ o["recording_consent"],
3487
+ ["notice", "ok_or_leave", "view_only_until_agree"],
3488
+ DEFAULT_LIVE_POLICY.recordingConsent
3489
+ )
3490
+ };
3491
+ }
3492
+ function samePolicy(a, b) {
3493
+ return a.chatMode === b.chatMode && a.avLock === b.avLock && a.timeLimitMinutes === b.timeLimitMinutes && a.timeLimitWarningMinutes === b.timeLimitWarningMinutes && a.recordingConsent === b.recordingConsent;
3494
+ }
3495
+ var MODERATORS = /* @__PURE__ */ new Set(["host", "cohost"]);
3496
+ function isLockedFor(policy, role, kind) {
3497
+ if (MODERATORS.has(role)) return false;
3498
+ return policy.avLock === "both" || (kind === "microphone" ? policy.avLock === "mic" : policy.avLock === "camera");
3499
+ }
3500
+ function consentPending(policy, role, recordingOn, given) {
3501
+ return recordingOn && !given && policy.recordingConsent !== "notice" && !MODERATORS.has(role);
3502
+ }
3503
+
3414
3504
  // src/core/attendance.ts
3505
+ var IDLE_PREVIEW = { state: "idle", unavailable: null };
3415
3506
  var IDLE_MEDIA_REGION = {
3416
3507
  permission: { microphone: "unknown", camera: "unknown" },
3417
3508
  audioOutput: "ok",
3418
3509
  notice: null,
3419
- offer: null
3510
+ offer: null,
3511
+ preview: { microphone: IDLE_PREVIEW, camera: IDLE_PREVIEW }
3420
3512
  };
3421
3513
  var IDLE_TRACK = { enabled: false, unavailable: null };
3422
3514
  var IDLE_MEDIA = {
@@ -3481,6 +3573,11 @@ var EMPTY = {
3481
3573
  presenter: null,
3482
3574
  waitingCount: 0,
3483
3575
  leavePolicy: DEFAULT_LEAVE_POLICY,
3576
+ livePolicy: DEFAULT_LIVE_POLICY,
3577
+ canReclaim: false,
3578
+ recordingConsentGiven: false,
3579
+ sessionStartedAt: null,
3580
+ mutedByHost: false,
3484
3581
  moderationNotice: null,
3485
3582
  actionNotice: null,
3486
3583
  privateChatRequest: null,
@@ -3490,6 +3587,8 @@ var EMPTY = {
3490
3587
  names: {},
3491
3588
  wrapUp: null,
3492
3589
  noteTakerRefusal: null,
3590
+ captionsOn: false,
3591
+ captionsRefusal: null,
3493
3592
  media: IDLE_MEDIA,
3494
3593
  mediaRegion: IDLE_MEDIA_REGION,
3495
3594
  failure: null,
@@ -3694,7 +3793,12 @@ function createMeetStore(initial = {}) {
3694
3793
  if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.reason === next.unavailable?.reason && current.unavailable?.message === next.unavailable?.message) {
3695
3794
  return;
3696
3795
  }
3697
- commit({ ...state, media: { ...state.media, [kind]: next } });
3796
+ commit({
3797
+ ...state,
3798
+ media: { ...state.media, [kind]: next },
3799
+ // The person turned their own microphone on again: the host's mute is over.
3800
+ ...kind === "microphone" && next.enabled && state.mutedByHost ? { mutedByHost: false } : {}
3801
+ });
3698
3802
  },
3699
3803
  reset() {
3700
3804
  const phase = nextPhase(state.phase, { type: "reset", teardown: true }) ?? EMPTY.phase;
@@ -3920,10 +4024,11 @@ function moderationText(kind, payload) {
3920
4024
  switch (kind) {
3921
4025
  case "muted": {
3922
4026
  const source = text2(payload["source"]) ?? "microphone";
3923
- return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
4027
+ const who = payload["by_role"] === "host" || payload["by_role"] === "cohost" ? "the host" : by;
4028
+ 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}.`;
3924
4029
  }
3925
4030
  case "ask-unmute":
3926
- return `${by} is asking you to unmute.`;
4031
+ return `${by === "The host" ? "The host" : by} asked you to unmute.`;
3927
4032
  case "removed":
3928
4033
  return `${by} removed you from this meeting.`;
3929
4034
  default:
@@ -3949,7 +4054,7 @@ function applyModeration(store, payload, now) {
3949
4054
  grants: { ...snapshot.grants, role },
3950
4055
  moderationNotice: {
3951
4056
  kind: "role",
3952
- text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
4057
+ 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.",
3953
4058
  at: now()
3954
4059
  }
3955
4060
  });
@@ -3961,7 +4066,10 @@ function applyModeration(store, payload, now) {
3961
4066
  case "removed": {
3962
4067
  const sentence = moderationText(kind, payload);
3963
4068
  if (sentence === null) return ignored("a moderation event with no sentence");
3964
- store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
4069
+ store.patch({
4070
+ moderationNotice: { kind, text: sentence, at: now() },
4071
+ ...kind === "muted" && (text2(payload["source"]) ?? "microphone") === "microphone" ? { mutedByHost: true } : {}
4072
+ });
3965
4073
  return HANDLED;
3966
4074
  }
3967
4075
  default:
@@ -4327,6 +4435,47 @@ function gridColumns(count2) {
4327
4435
  return 5;
4328
4436
  }
4329
4437
 
4438
+ // src/core/capabilities.ts
4439
+ var IN_APP = [
4440
+ [/Instagram/i, "Instagram"],
4441
+ [/FBAN|FBAV|FB_IAB/i, "Facebook"],
4442
+ [/LinkedInApp/i, "LinkedIn"],
4443
+ [/MicroMessenger/i, "WeChat"],
4444
+ [/Snapchat/i, "Snapchat"],
4445
+ [/\bLine\//i, "LINE"],
4446
+ [/Twitter|TwitterAndroid/i, "X"],
4447
+ [/TikTok|musical_ly|BytedanceWebview/i, "TikTok"]
4448
+ ];
4449
+ function isMobileUserAgent(userAgent, maxTouchPoints = 0) {
4450
+ if (/iPhone|iPod|Android|Mobile|iPad/i.test(userAgent)) return true;
4451
+ return /Macintosh/.test(userAgent) && maxTouchPoints > 1;
4452
+ }
4453
+ function detectCapabilities(scope = typeof globalThis === "undefined" ? null : globalThis) {
4454
+ if (scope === null || scope.navigator === void 0) {
4455
+ return { canJoin: true, reason: null, inAppBrowser: null, mobile: false, canShareScreen: false };
4456
+ }
4457
+ const ua = scope.navigator.userAgent ?? "";
4458
+ const webrtc = typeof scope.RTCPeerConnection === "function" || typeof scope.webkitRTCPeerConnection === "function";
4459
+ const mobile = isMobileUserAgent(ua, scope.navigator.maxTouchPoints ?? 0);
4460
+ const inAppBrowser = IN_APP.find(([pattern]) => pattern.test(ua))?.[1] ?? null;
4461
+ return {
4462
+ canJoin: webrtc,
4463
+ reason: webrtc ? null : "no_webrtc",
4464
+ inAppBrowser,
4465
+ mobile,
4466
+ canShareScreen: !mobile && typeof scope.navigator.mediaDevices?.getDisplayMedia === "function"
4467
+ };
4468
+ }
4469
+ function unsupportedWords(capabilities) {
4470
+ if (capabilities.inAppBrowser !== null) {
4471
+ return {
4472
+ title: `${capabilities.inAppBrowser}'s browser can't join calls`,
4473
+ body: "Open this link in Chrome, Safari, Edge or Firefox"
4474
+ };
4475
+ }
4476
+ return { title: "This browser can't join calls", body: "Open this link in Chrome, Safari, Edge or Firefox" };
4477
+ }
4478
+
4330
4479
  // src/core/room-driver.ts
4331
4480
  var DATA_TOPICS = {
4332
4481
  reaction: "mx.reaction",
@@ -4481,7 +4630,15 @@ function createLiveKitDriver(handlers, options = {}) {
4481
4630
  created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
4482
4631
  handlers.participantLeft?.(participant.identity, reasonName(reason));
4483
4632
  roster();
4484
- }).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) => {
4633
+ }).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, (publication) => {
4634
+ if (publication.source === Track.Source.ScreenShare) {
4635
+ const ended = () => {
4636
+ void created.localParticipant.setScreenShareEnabled(false).catch(() => void 0);
4637
+ };
4638
+ publication.track?.mediaStreamTrack.addEventListener("ended", ended, { once: true });
4639
+ }
4640
+ roster();
4641
+ }).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
4485
4642
  handlers.roomMetadata?.(metadata ?? "");
4486
4643
  }).on(RoomEvent.AudioPlaybackStatusChanged, (canPlay) => {
4487
4644
  handlers.audioPlayback?.(canPlay);
@@ -4622,7 +4779,10 @@ function createLiveKitDriver(handlers, options = {}) {
4622
4779
  },
4623
4780
  async setScreenShareEnabled(enabled) {
4624
4781
  try {
4625
- await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(enabled);
4782
+ await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(
4783
+ enabled,
4784
+ enabled ? { audio: true, systemAudio: "include" } : void 0
4785
+ );
4626
4786
  } catch (error) {
4627
4787
  throw normalizeScreenShareError(error, "meet.setScreenShareEnabled");
4628
4788
  }
@@ -4661,8 +4821,13 @@ function createLiveKitDriver(handlers, options = {}) {
4661
4821
  supportsVideoEffects() {
4662
4822
  return videoEffectsSupported();
4663
4823
  },
4824
+ screenShareHasAudio() {
4825
+ const target = room;
4826
+ if (target === null) return false;
4827
+ return target.localParticipant.getTrackPublications().some((p) => String(p.source) === "screen_share_audio" && p.track !== void 0);
4828
+ },
4664
4829
  supportsScreenShare() {
4665
- return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getDisplayMedia === "function";
4830
+ return detectCapabilities().canShareScreen;
4666
4831
  },
4667
4832
  attach(identity, kind, element) {
4668
4833
  const target = requireRoom("attach");
@@ -4783,6 +4948,7 @@ function applyDoorRow(store, attendee) {
4783
4948
  function decidedState(state) {
4784
4949
  return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
4785
4950
  }
4951
+ var LIVE_POLICY_POLL_MS = 3e3;
4786
4952
  function createMeetingSession(options) {
4787
4953
  const { manager, repository, store, identity } = options;
4788
4954
  let channel = null;
@@ -4924,6 +5090,46 @@ function createMeetingSession(options) {
4924
5090
  stopWaiting();
4925
5091
  }
4926
5092
  }
5093
+ let policyTimer = null;
5094
+ let policyReading = false;
5095
+ async function readLivePolicy() {
5096
+ const meeting = store.snapshot().meeting;
5097
+ const pass = options.resolvePass?.() ?? null;
5098
+ if (meeting === null || pass === null || policyReading) return;
5099
+ policyReading = true;
5100
+ try {
5101
+ const response = await options.api.request({
5102
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
5103
+ method: "GET",
5104
+ requireAuth: false,
5105
+ operation: "meet.state(policy)",
5106
+ headers: { [MEETING_PASS_HEADER]: pass }
5107
+ });
5108
+ if (!response.ok) return;
5109
+ const read = await response.json();
5110
+ const next = livePolicyFrom(read.live_policy);
5111
+ const started = read.session_started_at ? Date.parse(read.session_started_at) : NaN;
5112
+ const snapshot = store.snapshot();
5113
+ const startedAt = Number.isFinite(started) ? started : snapshot.sessionStartedAt;
5114
+ if (!samePolicy(snapshot.livePolicy, next) || startedAt !== snapshot.sessionStartedAt) {
5115
+ store.patch({ livePolicy: next, sessionStartedAt: startedAt });
5116
+ }
5117
+ } catch {
5118
+ } finally {
5119
+ policyReading = false;
5120
+ }
5121
+ }
5122
+ function syncLivePolicyTimer() {
5123
+ const inCall = store.snapshot().phase === "in_call";
5124
+ if (inCall && policyTimer === null) {
5125
+ void readLivePolicy();
5126
+ policyTimer = setInterval(() => void readLivePolicy(), LIVE_POLICY_POLL_MS);
5127
+ } else if (!inCall && policyTimer !== null) {
5128
+ clearInterval(policyTimer);
5129
+ policyTimer = null;
5130
+ }
5131
+ }
5132
+ const unsubscribePolicy = store.subscribe(syncLivePolicyTimer);
4927
5133
  async function readState2() {
4928
5134
  if (reading !== null) return reading;
4929
5135
  const meeting = store.snapshot().meeting;
@@ -5310,6 +5516,9 @@ function createMeetingSession(options) {
5310
5516
  },
5311
5517
  dispose() {
5312
5518
  stopWaiting();
5519
+ unsubscribePolicy();
5520
+ if (policyTimer !== null) clearInterval(policyTimer);
5521
+ policyTimer = null;
5313
5522
  channel?.close();
5314
5523
  channel = null;
5315
5524
  meetingChannel?.close();
@@ -5923,7 +6132,7 @@ function deviceFixSteps(kind, reason, where) {
5923
6132
  case "safari":
5924
6133
  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"];
5925
6134
  default:
5926
- return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
6135
+ return ["Click the site settings icon in the address bar", `Set ${device} to Allow`, "Press Try again"];
5927
6136
  }
5928
6137
  case "blocked_os": {
5929
6138
  const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
@@ -6021,7 +6230,7 @@ function createMediaSupervisor(options) {
6021
6230
  function setRegion(next) {
6022
6231
  const current = region();
6023
6232
  const merged = { ...current, ...next };
6024
- 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) {
6233
+ 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) {
6025
6234
  return;
6026
6235
  }
6027
6236
  store.patch({ mediaRegion: merged });
@@ -6117,7 +6326,7 @@ function createMediaSupervisor(options) {
6117
6326
  if (permission !== "unknown") setPermission(kind, permission);
6118
6327
  return unavailableFrom(normalized, classifyDeviceFailure(normalized, permission));
6119
6328
  }
6120
- return {
6329
+ const api = {
6121
6330
  start() {
6122
6331
  if (started || platform === null) return;
6123
6332
  started = true;
@@ -6139,6 +6348,16 @@ function createMediaSupervisor(options) {
6139
6348
  started = false;
6140
6349
  },
6141
6350
  unavailable,
6351
+ async preview(kind, outcome) {
6352
+ api.start();
6353
+ let next;
6354
+ if (typeof outcome === "string") next = { state: outcome, unavailable: null };
6355
+ else next = { state: "unavailable", unavailable: await unavailable(outcome.error, kind, `meet.${kind}Preview`) };
6356
+ const current = region().preview;
6357
+ const was = current[kind];
6358
+ if (was.state === next.state && was.unavailable?.reason === next.unavailable?.reason && was.unavailable?.message === next.unavailable?.message) return;
6359
+ setRegion({ preview: { ...region().preview, [kind]: next } });
6360
+ },
6142
6361
  onAudioPlayback(canPlay) {
6143
6362
  setRegion({ audioOutput: canPlay ? "ok" : "blocked" });
6144
6363
  },
@@ -6197,6 +6416,7 @@ function createMediaSupervisor(options) {
6197
6416
  setRegion({ notice: null });
6198
6417
  }
6199
6418
  };
6419
+ return api;
6200
6420
  }
6201
6421
 
6202
6422
  // src/core/room.ts
@@ -6358,7 +6578,8 @@ function createRoomEngine(options) {
6358
6578
  message: normalized.message,
6359
6579
  remedy: normalized.remedy,
6360
6580
  reason: normalized.reason,
6361
- retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3
6581
+ retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3,
6582
+ stage: "join"
6362
6583
  }
6363
6584
  });
6364
6585
  store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
@@ -6627,6 +6848,7 @@ function createRoomEngine(options) {
6627
6848
  lastServerUrl = token.serverUrl;
6628
6849
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6629
6850
  if (token.leavePolicy !== void 0) store.patch({ leavePolicy: token.leavePolicy });
6851
+ store.patch({ canReclaim: token.canReclaim === true });
6630
6852
  store.patch({
6631
6853
  localIdentity: token.identity,
6632
6854
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6748,6 +6970,10 @@ function createRoomEngine(options) {
6748
6970
  }
6749
6971
  departures.clear();
6750
6972
  liveRoster = list;
6973
+ const localNow = list.find((p) => p.isLocal);
6974
+ if (localNow !== void 0 && !localNow.screenSharing && store.snapshot().share.state === "presenting") {
6975
+ store.patch({ share: NO_SHARE });
6976
+ }
6751
6977
  const merged = mergeGhosts(list, ghosts, at);
6752
6978
  ghosts = merged.kept;
6753
6979
  const people = /* @__PURE__ */ new Map();
@@ -6967,7 +7193,10 @@ function createRoomEngine(options) {
6967
7193
  try {
6968
7194
  await driver.setScreenShareEnabled(enabled);
6969
7195
  ambiguousShareDenials = 0;
6970
- store.patch({ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now() } });
7196
+ const audio = enabled ? driver?.screenShareHasAudio?.() : void 0;
7197
+ store.patch({
7198
+ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now(), ...audio !== void 0 ? { audio } : {} }
7199
+ });
6971
7200
  } catch (error) {
6972
7201
  const kind = classifyScreenShareError(error);
6973
7202
  if (kind === "ambiguous") {
@@ -7034,13 +7263,14 @@ function createRoomEngine(options) {
7034
7263
  return driver?.supportsVideoEffects?.() ?? false;
7035
7264
  },
7036
7265
  supportsScreenShare() {
7037
- return driver?.supportsScreenShare?.() ?? true;
7266
+ return driver?.supportsScreenShare?.() ?? detectCapabilities().canShareScreen;
7038
7267
  },
7039
7268
  async switchDevice(kind, deviceId) {
7040
7269
  if (driver === null) return;
7041
7270
  await driver.switchDevice(kind, deviceId);
7042
7271
  },
7043
7272
  enableAudio: () => media.enableAudio(),
7273
+ previewDevice: (kind, outcome) => media.preview(kind, outcome),
7044
7274
  acceptDeviceOffer: () => media.acceptOffer(),
7045
7275
  dismissDeviceOffer: () => media.dismissOffer(),
7046
7276
  dismissDeviceNotice: () => media.dismissNotice(),
@@ -7126,8 +7356,12 @@ var MODERATION_ACTIONS = [
7126
7356
  "lower_hand",
7127
7357
  "lower_all_hands",
7128
7358
  "make_cohost",
7359
+ "make_presenter",
7360
+ "revoke_presenter",
7361
+ "set_av_lock",
7129
7362
  "revoke_cohost",
7130
7363
  "transfer_host",
7364
+ "reclaim_host",
7131
7365
  "rename",
7132
7366
  "remove",
7133
7367
  "report"
@@ -8057,14 +8291,31 @@ function takeMoveRequest(store) {
8057
8291
 
8058
8292
  // src/core/observation.ts
8059
8293
  function observedRole(role, standing) {
8060
- if (role === "host" || role === "cohost") return role;
8294
+ if (role === "host" || role === "cohost" || role === "presenter") return role;
8061
8295
  return isLinkFollower(standing) ? "guest" : "participant";
8062
8296
  }
8063
- function observedDevice(track, permission = "unknown") {
8297
+ function observedDevice(track, permission = "unknown", held = null) {
8298
+ if (held === "locked") return "locked";
8064
8299
  if (track.enabled) return "on";
8300
+ if (held === "muted_by_host") return "muted_by_host";
8065
8301
  const reason = deviceUnavailableReason(track, permission);
8066
8302
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
8067
8303
  }
8304
+ function observedPreview(preview, permission = "unknown") {
8305
+ if (permission === "denied") return "unavailable:denied";
8306
+ switch (preview.state) {
8307
+ case "on":
8308
+ return "on";
8309
+ case "off":
8310
+ return "muted_by_me";
8311
+ case "prompting":
8312
+ return "prompting";
8313
+ case "unavailable":
8314
+ return `unavailable:${preview.unavailable?.reason ?? "failed"}`;
8315
+ default:
8316
+ return null;
8317
+ }
8318
+ }
8068
8319
  function endedRecord(snapshot, given) {
8069
8320
  const record = snapshot?.meeting ?? given;
8070
8321
  return record !== null && record.endedAt !== null;
@@ -8088,20 +8339,34 @@ function observe(input) {
8088
8339
  const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
8089
8340
  const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
8090
8341
  const deviceInPlay = phase === "joining" || inCall;
8342
+ const region = snapshot?.mediaRegion;
8343
+ const atPrejoin = phase === "prejoin" && region !== void 0;
8091
8344
  return {
8092
8345
  phase,
8093
8346
  reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
8094
8347
  role,
8095
- connection: snapshot?.connection ?? "stable",
8348
+ // `net-blip`: in the call, a drop shorter than `reconnect_banner_delay_ms` is not something the
8349
+ // person sees — the contract says what is on screen, so it reads `stable` until the banner shows.
8350
+ connection: snapshot === null ? "stable" : inCall && snapshot.connection !== "stable" && !snapshot.connectionBanner ? "stable" : snapshot.connection,
8096
8351
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
8097
8352
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
8098
8353
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
8099
8354
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
8100
8355
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
8101
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
8102
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
8356
+ 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,
8357
+ 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
8103
8358
  };
8104
8359
  }
8360
+ function lockedFor(snapshot, kind, inCall) {
8361
+ if (!inCall) return null;
8362
+ const role = effectiveRole(snapshot);
8363
+ if (isLockedFor(snapshot.livePolicy, role, kind)) return "locked";
8364
+ if (snapshot.livePolicy.recordingConsent === "view_only_until_agree" && consentPending(snapshot.livePolicy, role, snapshot.recording === "recording", snapshot.recordingConsentGiven)) {
8365
+ return "locked";
8366
+ }
8367
+ if (kind === "microphone" && snapshot.mutedByHost) return "muted_by_host";
8368
+ return null;
8369
+ }
8105
8370
  function staticObservation(phase, reason = null) {
8106
8371
  return {
8107
8372
  phase,
@@ -8169,6 +8434,39 @@ function microphoneNoticeCode(track, permission = "unknown") {
8169
8434
  if (reason === null) return null;
8170
8435
  return deviceNoticeCode("microphone", reason) === "mic-blocked" ? "mic-blocked" : "device-lost";
8171
8436
  }
8437
+
8438
+ // src/core/speaking-detector.ts
8439
+ function createSpeakingWhileMutedDetector(options = {}) {
8440
+ const threshold = options.threshold ?? 0.06;
8441
+ const holdMs = options.holdMs ?? 600;
8442
+ const gapMs = options.gapMs ?? 400;
8443
+ const cooldownMs = options.cooldownMs ?? 2e4;
8444
+ let speechStart = null;
8445
+ let lastLoud = null;
8446
+ let firedAt = null;
8447
+ return {
8448
+ feed(level, at) {
8449
+ if (level >= threshold) {
8450
+ if (speechStart === null || lastLoud === null || at - lastLoud > gapMs) speechStart = at;
8451
+ lastLoud = at;
8452
+ } else if (lastLoud !== null && at - lastLoud > gapMs) {
8453
+ speechStart = null;
8454
+ lastLoud = null;
8455
+ }
8456
+ if (speechStart === null || at - speechStart < holdMs) return false;
8457
+ if (firedAt !== null && at - firedAt < cooldownMs) return false;
8458
+ firedAt = at;
8459
+ speechStart = null;
8460
+ lastLoud = null;
8461
+ return true;
8462
+ },
8463
+ reset() {
8464
+ speechStart = null;
8465
+ lastLoud = null;
8466
+ firedAt = null;
8467
+ }
8468
+ };
8469
+ }
8172
8470
  export {
8173
8471
  ATTENDANCE_CSV_HEADER,
8174
8472
  CALL_INVITE_KIND,
@@ -8250,6 +8548,7 @@ export {
8250
8548
  autoAssign,
8251
8549
  autoRejoinDelayMs,
8252
8550
  availableActivities,
8551
+ breakoutClosingMs,
8253
8552
  breakoutRemainingMs,
8254
8553
  callInviteAction,
8255
8554
  callInviteText,
@@ -8285,6 +8584,7 @@ export {
8285
8584
  createReconnectPolicy,
8286
8585
  createRecordingController,
8287
8586
  createRoomEngine,
8587
+ createSpeakingWhileMutedDetector,
8288
8588
  createTokenClient,
8289
8589
  createWebDeviceStorage,
8290
8590
  createWebEffectsStorage,
@@ -8293,6 +8593,7 @@ export {
8293
8593
  createWebRoomTokenStorage,
8294
8594
  defaultCaptionLanguage,
8295
8595
  detectBrowser,
8596
+ detectCapabilities,
8296
8597
  detectOs,
8297
8598
  deviceChipLabel,
8298
8599
  deviceFixSteps,
@@ -8317,6 +8618,7 @@ export {
8317
8618
  isLinkFollower,
8318
8619
  isListenOnly,
8319
8620
  isLivePhase,
8621
+ isMobileUserAgent,
8320
8622
  isNoteTaker,
8321
8623
  isObserverRole,
8322
8624
  isRealName,
@@ -8408,6 +8710,7 @@ export {
8408
8710
  transcriptDocument,
8409
8711
  unavailableFrom,
8410
8712
  uniqueAttendees,
8713
+ unsupportedWords,
8411
8714
  untranslatedLines,
8412
8715
  userTopic,
8413
8716
  wasRefused,