@ai-matrx/meet 0.11.43 → 0.11.45

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
@@ -323,7 +323,8 @@ function createWebResumeStorage() {
323
323
  }
324
324
  };
325
325
  }
326
- var CLEARS_RESUME = (phase) => phase === "left" || phase === "removed" || phase === "superseded" || phase === "inactive" || phase === "ended" || phase === "record" || phase === "prejoin" || phase.startsWith("refused:") || phase.startsWith("waiting:");
326
+ var CLEARS_RESUME = (phase) => phase === "left" || phase === "removed" || phase === "superseded" || phase === "inactive" || phase === "ended" || phase === "record" || phase === "prejoin" || phase === "denied" || phase === "knock_expired" || phase.startsWith("refused:");
327
+ var WAITING_RESUMES = (phase) => phase.startsWith("waiting:");
327
328
  function autoRejoinDelayMs(attempt, random = Math.random) {
328
329
  const base = Math.min(15e3, 1e3 * 2 ** Math.max(0, attempt));
329
330
  return Math.round(base * (0.5 + random()));
@@ -455,6 +456,10 @@ function createConnectionSupervisor(options) {
455
456
  rejoinAttempts = 0;
456
457
  stopRejoin();
457
458
  saveMarker(true);
459
+ } else if (WAITING_RESUMES(phase)) {
460
+ stopRejoin();
461
+ driverRegion = null;
462
+ saveMarker(true);
458
463
  } else if (phase === "disconnected") {
459
464
  driverRegion = null;
460
465
  saveMarker(true);
@@ -758,6 +763,7 @@ function parseRoomToken(body, roomName) {
758
763
  connectionIdentity: readString(payload, "connection_identity") ?? personKey,
759
764
  connectionKind: readString(payload, "connection_kind") === "companion" ? "companion" : "main",
760
765
  secondDeviceChoice,
766
+ secondDeviceRule: readString(payload, "second_device_rule"),
761
767
  displayName: readString(payload, "display_name") ?? readString(payload, "displayName") ?? identity,
762
768
  expiresAt,
763
769
  canPublish: readBoolean(payload, "can_publish", true),
@@ -1496,6 +1502,12 @@ var JOINABLE_FROM = /* @__PURE__ */ new Set([
1496
1502
  // allows the press; the server's gate decides whether it knocks or is refused again.
1497
1503
  "refused:lobby_rejects"
1498
1504
  ]);
1505
+ var RETRYABLE_REFUSALS = /* @__PURE__ */ new Set([
1506
+ "refused:locked",
1507
+ "refused:too_early",
1508
+ "refused:meeting_full",
1509
+ "refused:platform_capacity"
1510
+ ]);
1499
1511
  var NOT_ENDABLE = /* @__PURE__ */ new Set([
1500
1512
  "ended",
1501
1513
  "record",
@@ -1561,7 +1573,7 @@ function nextPhase(from, event) {
1561
1573
  case "link_state":
1562
1574
  return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
1563
1575
  case "join":
1564
- return JOINABLE_FROM.has(from) ? "joining" : null;
1576
+ return JOINABLE_FROM.has(from) || RETRYABLE_REFUSALS.has(from) ? "joining" : null;
1565
1577
  case "auto_rejoin":
1566
1578
  return from === "disconnected" ? "joining" : null;
1567
1579
  case "resumed":
@@ -1986,6 +1998,10 @@ var RPCS = {
1986
1998
  admitAll: "meet_admit_all",
1987
1999
  setLock: "meet_set_lock",
1988
2000
  recordConsent: "meet_record_consent",
2001
+ /** One meeting rule as the gate obeys it (the knob ladder, this meeting's rung included). */
2002
+ meetingRule: "meet_policy",
2003
+ /** A host sets one rule for THIS meeting (the `meet_meeting` rung); refusals come back as `{ok:false}`. */
2004
+ setMeetingRule: "meet_policy_set",
1989
2005
  /*
1990
2006
  * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
1991
2007
  * authorized by the meeting's host or a real iam grant (editor to change,
@@ -2032,7 +2048,7 @@ var INVITEE_COLUMNS = "id,meeting_id,invitee_user_id,email,display_name,role,rsv
2032
2048
  var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
2033
2049
  var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
2034
2050
  var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
2035
- var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,knocked_at,knock_expires_at,is_agent,joined_at,left_at,sessions:metadata->sessions";
2051
+ var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,knocked_at,knock_expires_at,is_agent,joined_at,left_at,sessions:metadata->sessions,knock_flag:metadata->>knock_flag";
2036
2052
  function projectStays(value) {
2037
2053
  if (!Array.isArray(value)) return [];
2038
2054
  const stays = [];
@@ -2432,6 +2448,8 @@ function createMeetRepository(options) {
2432
2448
  admissionState: states.includes(admission) ? admission : "admitted",
2433
2449
  knockedAt: text(row, "knocked_at"),
2434
2450
  knockExpiresAt: text(row, "knock_expires_at"),
2451
+ // The list read selects `metadata->>knock_flag`; a Postgres Changes row carries `metadata`.
2452
+ knockFlag: text(row, "knock_flag") ?? (typeof row["metadata"] === "object" && row["metadata"] !== null ? text(row["metadata"], "knock_flag") : null),
2435
2453
  isAgent: bool(row, "is_agent", false),
2436
2454
  joinedAt: text(row, "joined_at"),
2437
2455
  leftAt: text(row, "left_at"),
@@ -2755,6 +2773,25 @@ function createMeetRepository(options) {
2755
2773
  );
2756
2774
  return projectMeeting(row);
2757
2775
  },
2776
+ async meetingRule(meetingId, key) {
2777
+ const response = await db.rpc(RPCS.meetingRule, { p_meeting_id: meetingId, p_key: key });
2778
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.meetingRule");
2779
+ return response.data;
2780
+ },
2781
+ async setMeetingRule(meetingId, key, value) {
2782
+ const response = await db.rpc(RPCS.setMeetingRule, { p_meeting_id: meetingId, p_key: key, p_value: value });
2783
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.setMeetingRule");
2784
+ const answer = response.data ?? {};
2785
+ if (answer["ok"] !== true) {
2786
+ throw new MeetError(
2787
+ "forbidden",
2788
+ typeof answer["detail"] === "string" ? answer["detail"] : "This meeting's rule was not changed.",
2789
+ "Only the meeting's host or a co-host sets its rules; an organization can lock a rule for everyone.",
2790
+ answer
2791
+ );
2792
+ }
2793
+ return answer["effective_value"];
2794
+ },
2758
2795
  async transcript(meetingId, limit = 1e3) {
2759
2796
  const response = await db.from(TABLES.transcripts).select(SEGMENT_COLUMNS).eq("meeting_id", meetingId).order("started_at", { ascending: true }).order("id", { ascending: true }).limit(limit);
2760
2797
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.transcript");
@@ -3502,11 +3539,13 @@ function consentPending(policy, role, recordingOn, given) {
3502
3539
  }
3503
3540
 
3504
3541
  // src/core/attendance.ts
3542
+ var IDLE_PREVIEW = { state: "idle", unavailable: null };
3505
3543
  var IDLE_MEDIA_REGION = {
3506
3544
  permission: { microphone: "unknown", camera: "unknown" },
3507
3545
  audioOutput: "ok",
3508
3546
  notice: null,
3509
- offer: null
3547
+ offer: null,
3548
+ preview: { microphone: IDLE_PREVIEW, camera: IDLE_PREVIEW }
3510
3549
  };
3511
3550
  var IDLE_TRACK = { enabled: false, unavailable: null };
3512
3551
  var IDLE_MEDIA = {
@@ -4433,6 +4472,47 @@ function gridColumns(count2) {
4433
4472
  return 5;
4434
4473
  }
4435
4474
 
4475
+ // src/core/capabilities.ts
4476
+ var IN_APP = [
4477
+ [/Instagram/i, "Instagram"],
4478
+ [/FBAN|FBAV|FB_IAB/i, "Facebook"],
4479
+ [/LinkedInApp/i, "LinkedIn"],
4480
+ [/MicroMessenger/i, "WeChat"],
4481
+ [/Snapchat/i, "Snapchat"],
4482
+ [/\bLine\//i, "LINE"],
4483
+ [/Twitter|TwitterAndroid/i, "X"],
4484
+ [/TikTok|musical_ly|BytedanceWebview/i, "TikTok"]
4485
+ ];
4486
+ function isMobileUserAgent(userAgent, maxTouchPoints = 0) {
4487
+ if (/iPhone|iPod|Android|Mobile|iPad/i.test(userAgent)) return true;
4488
+ return /Macintosh/.test(userAgent) && maxTouchPoints > 1;
4489
+ }
4490
+ function detectCapabilities(scope = typeof globalThis === "undefined" ? null : globalThis) {
4491
+ if (scope === null || scope.navigator === void 0) {
4492
+ return { canJoin: true, reason: null, inAppBrowser: null, mobile: false, canShareScreen: false };
4493
+ }
4494
+ const ua = scope.navigator.userAgent ?? "";
4495
+ const webrtc = typeof scope.RTCPeerConnection === "function" || typeof scope.webkitRTCPeerConnection === "function";
4496
+ const mobile = isMobileUserAgent(ua, scope.navigator.maxTouchPoints ?? 0);
4497
+ const inAppBrowser = IN_APP.find(([pattern]) => pattern.test(ua))?.[1] ?? null;
4498
+ return {
4499
+ canJoin: webrtc,
4500
+ reason: webrtc ? null : "no_webrtc",
4501
+ inAppBrowser,
4502
+ mobile,
4503
+ canShareScreen: !mobile && typeof scope.navigator.mediaDevices?.getDisplayMedia === "function"
4504
+ };
4505
+ }
4506
+ function unsupportedWords(capabilities) {
4507
+ if (capabilities.inAppBrowser !== null) {
4508
+ return {
4509
+ title: `${capabilities.inAppBrowser}'s browser can't join calls`,
4510
+ body: "Open this link in Chrome, Safari, Edge or Firefox"
4511
+ };
4512
+ }
4513
+ return { title: "This browser can't join calls", body: "Open this link in Chrome, Safari, Edge or Firefox" };
4514
+ }
4515
+
4436
4516
  // src/core/room-driver.ts
4437
4517
  var DATA_TOPICS = {
4438
4518
  reaction: "mx.reaction",
@@ -4587,7 +4667,15 @@ function createLiveKitDriver(handlers, options = {}) {
4587
4667
  created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
4588
4668
  handlers.participantLeft?.(participant.identity, reasonName(reason));
4589
4669
  roster();
4590
- }).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) => {
4670
+ }).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, (publication) => {
4671
+ if (publication.source === Track.Source.ScreenShare) {
4672
+ const ended = () => {
4673
+ void created.localParticipant.setScreenShareEnabled(false).catch(() => void 0);
4674
+ };
4675
+ publication.track?.mediaStreamTrack.addEventListener("ended", ended, { once: true });
4676
+ }
4677
+ roster();
4678
+ }).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
4591
4679
  handlers.roomMetadata?.(metadata ?? "");
4592
4680
  }).on(RoomEvent.AudioPlaybackStatusChanged, (canPlay) => {
4593
4681
  handlers.audioPlayback?.(canPlay);
@@ -4728,7 +4816,10 @@ function createLiveKitDriver(handlers, options = {}) {
4728
4816
  },
4729
4817
  async setScreenShareEnabled(enabled) {
4730
4818
  try {
4731
- await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(enabled);
4819
+ await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(
4820
+ enabled,
4821
+ enabled ? { audio: true, systemAudio: "include" } : void 0
4822
+ );
4732
4823
  } catch (error) {
4733
4824
  throw normalizeScreenShareError(error, "meet.setScreenShareEnabled");
4734
4825
  }
@@ -4767,8 +4858,13 @@ function createLiveKitDriver(handlers, options = {}) {
4767
4858
  supportsVideoEffects() {
4768
4859
  return videoEffectsSupported();
4769
4860
  },
4861
+ screenShareHasAudio() {
4862
+ const target = room;
4863
+ if (target === null) return false;
4864
+ return target.localParticipant.getTrackPublications().some((p) => String(p.source) === "screen_share_audio" && p.track !== void 0);
4865
+ },
4770
4866
  supportsScreenShare() {
4771
- return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getDisplayMedia === "function";
4867
+ return detectCapabilities().canShareScreen;
4772
4868
  },
4773
4869
  attach(identity, kind, element) {
4774
4870
  const target = requireRoom("attach");
@@ -4861,7 +4957,8 @@ function lobbyEntryFrom(attendee, now = Date.now()) {
4861
4957
  knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4862
4958
  knockExpiresAt: attendee.knockExpiresAt ?? null,
4863
4959
  decidedAt: null,
4864
- decidedBy: null
4960
+ decidedBy: null,
4961
+ flag: attendee.knockFlag ?? null
4865
4962
  };
4866
4963
  }
4867
4964
  function deniedEntryFrom(attendee) {
@@ -5393,6 +5490,17 @@ function createMeetingSession(options) {
5393
5490
  async askAgain() {
5394
5491
  await options.rejoin?.();
5395
5492
  },
5493
+ async meetingRule(key) {
5494
+ const meeting = requireMeeting("meet.meetingRule");
5495
+ return repository.meetingRule(meeting.id, key);
5496
+ },
5497
+ async setMeetingRule(key, value) {
5498
+ requireHostUser("meet.setMeetingRule");
5499
+ const meeting = requireMeeting("meet.setMeetingRule");
5500
+ const effective = await repository.setMeetingRule(meeting.id, key, value);
5501
+ hint();
5502
+ return effective;
5503
+ },
5396
5504
  async setLock(locked) {
5397
5505
  const by = requireHostUser("meet.setLock");
5398
5506
  const meeting = requireMeeting("meet.setLock");
@@ -6073,7 +6181,7 @@ function deviceFixSteps(kind, reason, where) {
6073
6181
  case "safari":
6074
6182
  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"];
6075
6183
  default:
6076
- return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
6184
+ return ["Click the site settings icon in the address bar", `Set ${device} to Allow`, "Press Try again"];
6077
6185
  }
6078
6186
  case "blocked_os": {
6079
6187
  const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
@@ -6171,7 +6279,7 @@ function createMediaSupervisor(options) {
6171
6279
  function setRegion(next) {
6172
6280
  const current = region();
6173
6281
  const merged = { ...current, ...next };
6174
- 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) {
6282
+ 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) {
6175
6283
  return;
6176
6284
  }
6177
6285
  store.patch({ mediaRegion: merged });
@@ -6267,7 +6375,7 @@ function createMediaSupervisor(options) {
6267
6375
  if (permission !== "unknown") setPermission(kind, permission);
6268
6376
  return unavailableFrom(normalized, classifyDeviceFailure(normalized, permission));
6269
6377
  }
6270
- return {
6378
+ const api = {
6271
6379
  start() {
6272
6380
  if (started || platform === null) return;
6273
6381
  started = true;
@@ -6289,6 +6397,16 @@ function createMediaSupervisor(options) {
6289
6397
  started = false;
6290
6398
  },
6291
6399
  unavailable,
6400
+ async preview(kind, outcome) {
6401
+ api.start();
6402
+ let next;
6403
+ if (typeof outcome === "string") next = { state: outcome, unavailable: null };
6404
+ else next = { state: "unavailable", unavailable: await unavailable(outcome.error, kind, `meet.${kind}Preview`) };
6405
+ const current = region().preview;
6406
+ const was = current[kind];
6407
+ if (was.state === next.state && was.unavailable?.reason === next.unavailable?.reason && was.unavailable?.message === next.unavailable?.message) return;
6408
+ setRegion({ preview: { ...region().preview, [kind]: next } });
6409
+ },
6292
6410
  onAudioPlayback(canPlay) {
6293
6411
  setRegion({ audioOutput: canPlay ? "ok" : "blocked" });
6294
6412
  },
@@ -6347,6 +6465,7 @@ function createMediaSupervisor(options) {
6347
6465
  setRegion({ notice: null });
6348
6466
  }
6349
6467
  };
6468
+ return api;
6350
6469
  }
6351
6470
 
6352
6471
  // src/core/room.ts
@@ -6508,7 +6627,8 @@ function createRoomEngine(options) {
6508
6627
  message: normalized.message,
6509
6628
  remedy: normalized.remedy,
6510
6629
  reason: normalized.reason,
6511
- retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3
6630
+ retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3,
6631
+ stage: "join"
6512
6632
  }
6513
6633
  });
6514
6634
  store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
@@ -6772,7 +6892,7 @@ function createRoomEngine(options) {
6772
6892
  const { secondDevice: _answered, ...rest } = lastRequest;
6773
6893
  lastRequest = rest;
6774
6894
  }
6775
- options.tabClaim?.inRoom(String(request.roomName), String(token.identity));
6895
+ options.tabClaim?.inRoom(String(request.roomName), String(token.identity), token.secondDeviceRule ?? null);
6776
6896
  current = token;
6777
6897
  lastServerUrl = token.serverUrl;
6778
6898
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
@@ -6899,6 +7019,10 @@ function createRoomEngine(options) {
6899
7019
  }
6900
7020
  departures.clear();
6901
7021
  liveRoster = list;
7022
+ const localNow = list.find((p) => p.isLocal);
7023
+ if (localNow !== void 0 && !localNow.screenSharing && store.snapshot().share.state === "presenting") {
7024
+ store.patch({ share: NO_SHARE });
7025
+ }
6902
7026
  const merged = mergeGhosts(list, ghosts, at);
6903
7027
  ghosts = merged.kept;
6904
7028
  const people = /* @__PURE__ */ new Map();
@@ -7080,10 +7204,12 @@ function createRoomEngine(options) {
7080
7204
  if (claim === void 0 || userId === null) return false;
7081
7205
  const before = store.snapshot();
7082
7206
  if (before.phase !== "prejoin" || before.secondDevice !== null) return false;
7083
- if (!await claim.presentElsewhere(String(roomName), `user:${String(userId)}`)) return false;
7207
+ const person = `user:${String(userId)}`;
7208
+ const found = claim.presenceElsewhere !== void 0 ? await claim.presenceElsewhere(String(roomName), person) : await claim.presentElsewhere(String(roomName), person) ? { rule: null } : null;
7209
+ if (found === null || found.rule === "allow_both") return false;
7084
7210
  const now2 = store.snapshot();
7085
7211
  if (now2.phase !== "prejoin" || now2.secondDevice !== null) return false;
7086
- store.patch({ secondDevice: { rule: "ask_switch_or_join" } });
7212
+ store.patch({ secondDevice: { rule: found.rule ?? "ask_switch_or_join" } });
7087
7213
  report("info", "meet: this browser already has you in this meeting \u2014 offering the choice before Join");
7088
7214
  return true;
7089
7215
  },
@@ -7118,7 +7244,10 @@ function createRoomEngine(options) {
7118
7244
  try {
7119
7245
  await driver.setScreenShareEnabled(enabled);
7120
7246
  ambiguousShareDenials = 0;
7121
- store.patch({ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now() } });
7247
+ const audio = enabled ? driver?.screenShareHasAudio?.() : void 0;
7248
+ store.patch({
7249
+ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now(), ...audio !== void 0 ? { audio } : {} }
7250
+ });
7122
7251
  } catch (error) {
7123
7252
  const kind = classifyScreenShareError(error);
7124
7253
  if (kind === "ambiguous") {
@@ -7185,13 +7314,14 @@ function createRoomEngine(options) {
7185
7314
  return driver?.supportsVideoEffects?.() ?? false;
7186
7315
  },
7187
7316
  supportsScreenShare() {
7188
- return driver?.supportsScreenShare?.() ?? true;
7317
+ return driver?.supportsScreenShare?.() ?? detectCapabilities().canShareScreen;
7189
7318
  },
7190
7319
  async switchDevice(kind, deviceId) {
7191
7320
  if (driver === null) return;
7192
7321
  await driver.switchDevice(kind, deviceId);
7193
7322
  },
7194
7323
  enableAudio: () => media.enableAudio(),
7324
+ previewDevice: (kind, outcome) => media.preview(kind, outcome),
7195
7325
  acceptDeviceOffer: () => media.acceptOffer(),
7196
7326
  dismissDeviceOffer: () => media.dismissOffer(),
7197
7327
  dismissDeviceNotice: () => media.dismissNotice(),
@@ -8222,6 +8352,21 @@ function observedDevice(track, permission = "unknown", held = null) {
8222
8352
  const reason = deviceUnavailableReason(track, permission);
8223
8353
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
8224
8354
  }
8355
+ function observedPreview(preview, permission = "unknown") {
8356
+ if (permission === "denied") return "unavailable:denied";
8357
+ switch (preview.state) {
8358
+ case "on":
8359
+ return "on";
8360
+ case "off":
8361
+ return "muted_by_me";
8362
+ case "prompting":
8363
+ return "prompting";
8364
+ case "unavailable":
8365
+ return `unavailable:${preview.unavailable?.reason ?? "failed"}`;
8366
+ default:
8367
+ return null;
8368
+ }
8369
+ }
8225
8370
  function endedRecord(snapshot, given) {
8226
8371
  const record = snapshot?.meeting ?? given;
8227
8372
  return record !== null && record.endedAt !== null;
@@ -8245,18 +8390,22 @@ function observe(input) {
8245
8390
  const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
8246
8391
  const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
8247
8392
  const deviceInPlay = phase === "joining" || inCall;
8393
+ const region = snapshot?.mediaRegion;
8394
+ const atPrejoin = phase === "prejoin" && region !== void 0;
8248
8395
  return {
8249
8396
  phase,
8250
8397
  reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
8251
8398
  role,
8252
- connection: snapshot?.connection ?? "stable",
8399
+ // `net-blip`: in the call, a drop shorter than `reconnect_banner_delay_ms` is not something the
8400
+ // person sees — the contract says what is on screen, so it reads `stable` until the banner shows.
8401
+ connection: snapshot === null ? "stable" : inCall && snapshot.connection !== "stable" && !snapshot.connectionBanner ? "stable" : snapshot.connection,
8253
8402
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
8254
8403
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
8255
8404
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
8256
8405
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
8257
8406
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
8258
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera, lockedFor(snapshot, "camera", inCall)) : null,
8259
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone, lockedFor(snapshot, "microphone", inCall)) : null
8407
+ 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,
8408
+ 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
8260
8409
  };
8261
8410
  }
8262
8411
  function lockedFor(snapshot, kind, inCall) {
@@ -8336,6 +8485,39 @@ function microphoneNoticeCode(track, permission = "unknown") {
8336
8485
  if (reason === null) return null;
8337
8486
  return deviceNoticeCode("microphone", reason) === "mic-blocked" ? "mic-blocked" : "device-lost";
8338
8487
  }
8488
+
8489
+ // src/core/speaking-detector.ts
8490
+ function createSpeakingWhileMutedDetector(options = {}) {
8491
+ const threshold = options.threshold ?? 0.06;
8492
+ const holdMs = options.holdMs ?? 600;
8493
+ const gapMs = options.gapMs ?? 400;
8494
+ const cooldownMs = options.cooldownMs ?? 2e4;
8495
+ let speechStart = null;
8496
+ let lastLoud = null;
8497
+ let firedAt = null;
8498
+ return {
8499
+ feed(level, at) {
8500
+ if (level >= threshold) {
8501
+ if (speechStart === null || lastLoud === null || at - lastLoud > gapMs) speechStart = at;
8502
+ lastLoud = at;
8503
+ } else if (lastLoud !== null && at - lastLoud > gapMs) {
8504
+ speechStart = null;
8505
+ lastLoud = null;
8506
+ }
8507
+ if (speechStart === null || at - speechStart < holdMs) return false;
8508
+ if (firedAt !== null && at - firedAt < cooldownMs) return false;
8509
+ firedAt = at;
8510
+ speechStart = null;
8511
+ lastLoud = null;
8512
+ return true;
8513
+ },
8514
+ reset() {
8515
+ speechStart = null;
8516
+ lastLoud = null;
8517
+ firedAt = null;
8518
+ }
8519
+ };
8520
+ }
8339
8521
  export {
8340
8522
  ATTENDANCE_CSV_HEADER,
8341
8523
  CALL_INVITE_KIND,
@@ -8453,6 +8635,7 @@ export {
8453
8635
  createReconnectPolicy,
8454
8636
  createRecordingController,
8455
8637
  createRoomEngine,
8638
+ createSpeakingWhileMutedDetector,
8456
8639
  createTokenClient,
8457
8640
  createWebDeviceStorage,
8458
8641
  createWebEffectsStorage,
@@ -8461,6 +8644,7 @@ export {
8461
8644
  createWebRoomTokenStorage,
8462
8645
  defaultCaptionLanguage,
8463
8646
  detectBrowser,
8647
+ detectCapabilities,
8464
8648
  detectOs,
8465
8649
  deviceChipLabel,
8466
8650
  deviceFixSteps,
@@ -8485,6 +8669,7 @@ export {
8485
8669
  isLinkFollower,
8486
8670
  isListenOnly,
8487
8671
  isLivePhase,
8672
+ isMobileUserAgent,
8488
8673
  isNoteTaker,
8489
8674
  isObserverRole,
8490
8675
  isRealName,
@@ -8576,6 +8761,7 @@ export {
8576
8761
  transcriptDocument,
8577
8762
  unavailableFrom,
8578
8763
  uniqueAttendees,
8764
+ unsupportedWords,
8579
8765
  untranslatedLines,
8580
8766
  userTopic,
8581
8767
  wasRefused,