@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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog — `@ai-matrx/meet`
2
2
 
3
+ ## 0.11.45
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/meet/v0.11.44..npm/meet/v0.11.45 -- apps/shared/meet`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.11.44
10
+
11
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
12
+ `git diff npm/meet/v0.11.43..npm/meet/v0.11.44 -- apps/shared/meet`).
13
+ No source changes intended and no consumer action required.
14
+
3
15
  ## 0.11.43
4
16
 
5
17
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -107,6 +107,11 @@ interface LobbyEntry {
107
107
  readonly knockExpiresAt?: string | null | undefined;
108
108
  readonly decidedAt: string | null;
109
109
  readonly decidedBy: UserId | null;
110
+ /**
111
+ * The server's flag on this knock (`mod-removed-bypass`): `previously_removed` when its coarse
112
+ * signals match someone a host removed from this meeting. Advice for the host, never a block.
113
+ */
114
+ readonly flag?: string | null | undefined;
110
115
  }
111
116
  type ConnectionQuality = "excellent" | "good" | "poor" | "lost" | "unknown";
112
117
  /** One person in the room, as the UI needs them. Media state is a FACT here. */
@@ -403,6 +408,8 @@ interface MeetingAttendee {
403
408
  /** When this person knocked, and when an unanswered knock lapses (null = never). */
404
409
  readonly knockedAt?: string | null | undefined;
405
410
  readonly knockExpiresAt?: string | null | undefined;
411
+ /** The server's flag on a knock (`metadata.knock_flag`), e.g. `previously_removed`. */
412
+ readonly knockFlag?: string | null | undefined;
406
413
  readonly isAgent: boolean;
407
414
  /** FIRST arrival in the room — a rejoin never moves it. */
408
415
  readonly joinedAt: string | null;
@@ -107,6 +107,11 @@ interface LobbyEntry {
107
107
  readonly knockExpiresAt?: string | null | undefined;
108
108
  readonly decidedAt: string | null;
109
109
  readonly decidedBy: UserId | null;
110
+ /**
111
+ * The server's flag on this knock (`mod-removed-bypass`): `previously_removed` when its coarse
112
+ * signals match someone a host removed from this meeting. Advice for the host, never a block.
113
+ */
114
+ readonly flag?: string | null | undefined;
110
115
  }
111
116
  type ConnectionQuality = "excellent" | "good" | "poor" | "lost" | "unknown";
112
117
  /** One person in the room, as the UI needs them. Media state is a FACT here. */
@@ -403,6 +408,8 @@ interface MeetingAttendee {
403
408
  /** When this person knocked, and when an unanswered knock lapses (null = never). */
404
409
  readonly knockedAt?: string | null | undefined;
405
410
  readonly knockExpiresAt?: string | null | undefined;
411
+ /** The server's flag on a knock (`metadata.knock_flag`), e.g. `previously_removed`. */
412
+ readonly knockFlag?: string | null | undefined;
406
413
  readonly isAgent: boolean;
407
414
  /** FIRST arrival in the room — a rejoin never moves it. */
408
415
  readonly joinedAt: string | null;
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { M as MeetingRecord, P as ParticipantIdentity, R as RoomName, a as MeetParticipant } from './collab-BNJNgXFB.js';
2
+ import { M as MeetingRecord, P as ParticipantIdentity, R as RoomName, a as MeetParticipant } from './collab-D1TX5uf9.js';
3
3
 
4
4
  /**
5
5
  * THE PRODUCT UI (R6/R7). Not a headless kernel with a note telling the host to
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { M as MeetingRecord, P as ParticipantIdentity, R as RoomName, a as MeetParticipant } from './collab-BNJNgXFB.cjs';
2
+ import { M as MeetingRecord, P as ParticipantIdentity, R as RoomName, a as MeetParticipant } from './collab-D1TX5uf9.cjs';
3
3
 
4
4
  /**
5
5
  * THE PRODUCT UI (R6/R7). Not a headless kernel with a note telling the host to
package/dist/core.cjs CHANGED
@@ -146,6 +146,7 @@ __export(core_exports, {
146
146
  createReconnectPolicy: () => createReconnectPolicy,
147
147
  createRecordingController: () => createRecordingController,
148
148
  createRoomEngine: () => createRoomEngine,
149
+ createSpeakingWhileMutedDetector: () => createSpeakingWhileMutedDetector,
149
150
  createTokenClient: () => createTokenClient,
150
151
  createWebDeviceStorage: () => createWebDeviceStorage,
151
152
  createWebEffectsStorage: () => createWebEffectsStorage,
@@ -154,6 +155,7 @@ __export(core_exports, {
154
155
  createWebRoomTokenStorage: () => createWebRoomTokenStorage,
155
156
  defaultCaptionLanguage: () => defaultCaptionLanguage,
156
157
  detectBrowser: () => detectBrowser,
158
+ detectCapabilities: () => detectCapabilities,
157
159
  detectOs: () => detectOs,
158
160
  deviceChipLabel: () => deviceChipLabel,
159
161
  deviceFixSteps: () => deviceFixSteps,
@@ -178,6 +180,7 @@ __export(core_exports, {
178
180
  isLinkFollower: () => isLinkFollower,
179
181
  isListenOnly: () => isListenOnly,
180
182
  isLivePhase: () => isLivePhase,
183
+ isMobileUserAgent: () => isMobileUserAgent,
181
184
  isNoteTaker: () => isNoteTaker,
182
185
  isObserverRole: () => isObserverRole,
183
186
  isRealName: () => isRealName,
@@ -269,6 +272,7 @@ __export(core_exports, {
269
272
  transcriptDocument: () => transcriptDocument,
270
273
  unavailableFrom: () => unavailableFrom,
271
274
  uniqueAttendees: () => uniqueAttendees,
275
+ unsupportedWords: () => unsupportedWords,
272
276
  untranslatedLines: () => untranslatedLines,
273
277
  userTopic: () => userTopic,
274
278
  wasRefused: () => wasRefused,
@@ -603,7 +607,8 @@ function createWebResumeStorage() {
603
607
  }
604
608
  };
605
609
  }
606
- var CLEARS_RESUME = (phase) => phase === "left" || phase === "removed" || phase === "superseded" || phase === "inactive" || phase === "ended" || phase === "record" || phase === "prejoin" || phase.startsWith("refused:") || phase.startsWith("waiting:");
610
+ 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:");
611
+ var WAITING_RESUMES = (phase) => phase.startsWith("waiting:");
607
612
  function autoRejoinDelayMs(attempt, random = Math.random) {
608
613
  const base = Math.min(15e3, 1e3 * 2 ** Math.max(0, attempt));
609
614
  return Math.round(base * (0.5 + random()));
@@ -735,6 +740,10 @@ function createConnectionSupervisor(options) {
735
740
  rejoinAttempts = 0;
736
741
  stopRejoin();
737
742
  saveMarker(true);
743
+ } else if (WAITING_RESUMES(phase)) {
744
+ stopRejoin();
745
+ driverRegion = null;
746
+ saveMarker(true);
738
747
  } else if (phase === "disconnected") {
739
748
  driverRegion = null;
740
749
  saveMarker(true);
@@ -1038,6 +1047,7 @@ function parseRoomToken(body, roomName) {
1038
1047
  connectionIdentity: readString(payload, "connection_identity") ?? personKey,
1039
1048
  connectionKind: readString(payload, "connection_kind") === "companion" ? "companion" : "main",
1040
1049
  secondDeviceChoice,
1050
+ secondDeviceRule: readString(payload, "second_device_rule"),
1041
1051
  displayName: readString(payload, "display_name") ?? readString(payload, "displayName") ?? identity,
1042
1052
  expiresAt,
1043
1053
  canPublish: readBoolean(payload, "can_publish", true),
@@ -1776,6 +1786,12 @@ var JOINABLE_FROM = /* @__PURE__ */ new Set([
1776
1786
  // allows the press; the server's gate decides whether it knocks or is refused again.
1777
1787
  "refused:lobby_rejects"
1778
1788
  ]);
1789
+ var RETRYABLE_REFUSALS = /* @__PURE__ */ new Set([
1790
+ "refused:locked",
1791
+ "refused:too_early",
1792
+ "refused:meeting_full",
1793
+ "refused:platform_capacity"
1794
+ ]);
1779
1795
  var NOT_ENDABLE = /* @__PURE__ */ new Set([
1780
1796
  "ended",
1781
1797
  "record",
@@ -1841,7 +1857,7 @@ function nextPhase(from, event) {
1841
1857
  case "link_state":
1842
1858
  return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
1843
1859
  case "join":
1844
- return JOINABLE_FROM.has(from) ? "joining" : null;
1860
+ return JOINABLE_FROM.has(from) || RETRYABLE_REFUSALS.has(from) ? "joining" : null;
1845
1861
  case "auto_rejoin":
1846
1862
  return from === "disconnected" ? "joining" : null;
1847
1863
  case "resumed":
@@ -2266,6 +2282,10 @@ var RPCS = {
2266
2282
  admitAll: "meet_admit_all",
2267
2283
  setLock: "meet_set_lock",
2268
2284
  recordConsent: "meet_record_consent",
2285
+ /** One meeting rule as the gate obeys it (the knob ladder, this meeting's rung included). */
2286
+ meetingRule: "meet_policy",
2287
+ /** A host sets one rule for THIS meeting (the `meet_meeting` rung); refusals come back as `{ok:false}`. */
2288
+ setMeetingRule: "meet_policy_set",
2269
2289
  /*
2270
2290
  * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
2271
2291
  * authorized by the meeting's host or a real iam grant (editor to change,
@@ -2312,7 +2332,7 @@ var INVITEE_COLUMNS = "id,meeting_id,invitee_user_id,email,display_name,role,rsv
2312
2332
  var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
2313
2333
  var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
2314
2334
  var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
2315
- 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";
2335
+ 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";
2316
2336
  function projectStays(value) {
2317
2337
  if (!Array.isArray(value)) return [];
2318
2338
  const stays = [];
@@ -2712,6 +2732,8 @@ function createMeetRepository(options) {
2712
2732
  admissionState: states.includes(admission) ? admission : "admitted",
2713
2733
  knockedAt: text(row, "knocked_at"),
2714
2734
  knockExpiresAt: text(row, "knock_expires_at"),
2735
+ // The list read selects `metadata->>knock_flag`; a Postgres Changes row carries `metadata`.
2736
+ knockFlag: text(row, "knock_flag") ?? (typeof row["metadata"] === "object" && row["metadata"] !== null ? text(row["metadata"], "knock_flag") : null),
2715
2737
  isAgent: bool(row, "is_agent", false),
2716
2738
  joinedAt: text(row, "joined_at"),
2717
2739
  leftAt: text(row, "left_at"),
@@ -3035,6 +3057,25 @@ function createMeetRepository(options) {
3035
3057
  );
3036
3058
  return projectMeeting(row);
3037
3059
  },
3060
+ async meetingRule(meetingId, key) {
3061
+ const response = await db.rpc(RPCS.meetingRule, { p_meeting_id: meetingId, p_key: key });
3062
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.meetingRule");
3063
+ return response.data;
3064
+ },
3065
+ async setMeetingRule(meetingId, key, value) {
3066
+ const response = await db.rpc(RPCS.setMeetingRule, { p_meeting_id: meetingId, p_key: key, p_value: value });
3067
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.setMeetingRule");
3068
+ const answer = response.data ?? {};
3069
+ if (answer["ok"] !== true) {
3070
+ throw new MeetError(
3071
+ "forbidden",
3072
+ typeof answer["detail"] === "string" ? answer["detail"] : "This meeting's rule was not changed.",
3073
+ "Only the meeting's host or a co-host sets its rules; an organization can lock a rule for everyone.",
3074
+ answer
3075
+ );
3076
+ }
3077
+ return answer["effective_value"];
3078
+ },
3038
3079
  async transcript(meetingId, limit = 1e3) {
3039
3080
  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);
3040
3081
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.transcript");
@@ -3782,11 +3823,13 @@ function consentPending(policy, role, recordingOn, given) {
3782
3823
  }
3783
3824
 
3784
3825
  // src/core/attendance.ts
3826
+ var IDLE_PREVIEW = { state: "idle", unavailable: null };
3785
3827
  var IDLE_MEDIA_REGION = {
3786
3828
  permission: { microphone: "unknown", camera: "unknown" },
3787
3829
  audioOutput: "ok",
3788
3830
  notice: null,
3789
- offer: null
3831
+ offer: null,
3832
+ preview: { microphone: IDLE_PREVIEW, camera: IDLE_PREVIEW }
3790
3833
  };
3791
3834
  var IDLE_TRACK = { enabled: false, unavailable: null };
3792
3835
  var IDLE_MEDIA = {
@@ -4713,6 +4756,47 @@ function gridColumns(count2) {
4713
4756
  return 5;
4714
4757
  }
4715
4758
 
4759
+ // src/core/capabilities.ts
4760
+ var IN_APP = [
4761
+ [/Instagram/i, "Instagram"],
4762
+ [/FBAN|FBAV|FB_IAB/i, "Facebook"],
4763
+ [/LinkedInApp/i, "LinkedIn"],
4764
+ [/MicroMessenger/i, "WeChat"],
4765
+ [/Snapchat/i, "Snapchat"],
4766
+ [/\bLine\//i, "LINE"],
4767
+ [/Twitter|TwitterAndroid/i, "X"],
4768
+ [/TikTok|musical_ly|BytedanceWebview/i, "TikTok"]
4769
+ ];
4770
+ function isMobileUserAgent(userAgent, maxTouchPoints = 0) {
4771
+ if (/iPhone|iPod|Android|Mobile|iPad/i.test(userAgent)) return true;
4772
+ return /Macintosh/.test(userAgent) && maxTouchPoints > 1;
4773
+ }
4774
+ function detectCapabilities(scope = typeof globalThis === "undefined" ? null : globalThis) {
4775
+ if (scope === null || scope.navigator === void 0) {
4776
+ return { canJoin: true, reason: null, inAppBrowser: null, mobile: false, canShareScreen: false };
4777
+ }
4778
+ const ua = scope.navigator.userAgent ?? "";
4779
+ const webrtc = typeof scope.RTCPeerConnection === "function" || typeof scope.webkitRTCPeerConnection === "function";
4780
+ const mobile = isMobileUserAgent(ua, scope.navigator.maxTouchPoints ?? 0);
4781
+ const inAppBrowser = IN_APP.find(([pattern]) => pattern.test(ua))?.[1] ?? null;
4782
+ return {
4783
+ canJoin: webrtc,
4784
+ reason: webrtc ? null : "no_webrtc",
4785
+ inAppBrowser,
4786
+ mobile,
4787
+ canShareScreen: !mobile && typeof scope.navigator.mediaDevices?.getDisplayMedia === "function"
4788
+ };
4789
+ }
4790
+ function unsupportedWords(capabilities) {
4791
+ if (capabilities.inAppBrowser !== null) {
4792
+ return {
4793
+ title: `${capabilities.inAppBrowser}'s browser can't join calls`,
4794
+ body: "Open this link in Chrome, Safari, Edge or Firefox"
4795
+ };
4796
+ }
4797
+ return { title: "This browser can't join calls", body: "Open this link in Chrome, Safari, Edge or Firefox" };
4798
+ }
4799
+
4716
4800
  // src/core/room-driver.ts
4717
4801
  var DATA_TOPICS = {
4718
4802
  reaction: "mx.reaction",
@@ -4867,7 +4951,15 @@ function createLiveKitDriver(handlers, options = {}) {
4867
4951
  created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
4868
4952
  handlers.participantLeft?.(participant.identity, reasonName(reason));
4869
4953
  roster();
4870
- }).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) => {
4954
+ }).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, (publication) => {
4955
+ if (publication.source === Track.Source.ScreenShare) {
4956
+ const ended = () => {
4957
+ void created.localParticipant.setScreenShareEnabled(false).catch(() => void 0);
4958
+ };
4959
+ publication.track?.mediaStreamTrack.addEventListener("ended", ended, { once: true });
4960
+ }
4961
+ roster();
4962
+ }).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
4871
4963
  handlers.roomMetadata?.(metadata ?? "");
4872
4964
  }).on(RoomEvent.AudioPlaybackStatusChanged, (canPlay) => {
4873
4965
  handlers.audioPlayback?.(canPlay);
@@ -5008,7 +5100,10 @@ function createLiveKitDriver(handlers, options = {}) {
5008
5100
  },
5009
5101
  async setScreenShareEnabled(enabled) {
5010
5102
  try {
5011
- await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(enabled);
5103
+ await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(
5104
+ enabled,
5105
+ enabled ? { audio: true, systemAudio: "include" } : void 0
5106
+ );
5012
5107
  } catch (error) {
5013
5108
  throw normalizeScreenShareError(error, "meet.setScreenShareEnabled");
5014
5109
  }
@@ -5047,8 +5142,13 @@ function createLiveKitDriver(handlers, options = {}) {
5047
5142
  supportsVideoEffects() {
5048
5143
  return videoEffectsSupported();
5049
5144
  },
5145
+ screenShareHasAudio() {
5146
+ const target = room;
5147
+ if (target === null) return false;
5148
+ return target.localParticipant.getTrackPublications().some((p) => String(p.source) === "screen_share_audio" && p.track !== void 0);
5149
+ },
5050
5150
  supportsScreenShare() {
5051
- return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getDisplayMedia === "function";
5151
+ return detectCapabilities().canShareScreen;
5052
5152
  },
5053
5153
  attach(identity, kind, element) {
5054
5154
  const target = requireRoom("attach");
@@ -5141,7 +5241,8 @@ function lobbyEntryFrom(attendee, now = Date.now()) {
5141
5241
  knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5142
5242
  knockExpiresAt: attendee.knockExpiresAt ?? null,
5143
5243
  decidedAt: null,
5144
- decidedBy: null
5244
+ decidedBy: null,
5245
+ flag: attendee.knockFlag ?? null
5145
5246
  };
5146
5247
  }
5147
5248
  function deniedEntryFrom(attendee) {
@@ -5673,6 +5774,17 @@ function createMeetingSession(options) {
5673
5774
  async askAgain() {
5674
5775
  await options.rejoin?.();
5675
5776
  },
5777
+ async meetingRule(key) {
5778
+ const meeting = requireMeeting("meet.meetingRule");
5779
+ return repository.meetingRule(meeting.id, key);
5780
+ },
5781
+ async setMeetingRule(key, value) {
5782
+ requireHostUser("meet.setMeetingRule");
5783
+ const meeting = requireMeeting("meet.setMeetingRule");
5784
+ const effective = await repository.setMeetingRule(meeting.id, key, value);
5785
+ hint();
5786
+ return effective;
5787
+ },
5676
5788
  async setLock(locked) {
5677
5789
  const by = requireHostUser("meet.setLock");
5678
5790
  const meeting = requireMeeting("meet.setLock");
@@ -6353,7 +6465,7 @@ function deviceFixSteps(kind, reason, where) {
6353
6465
  case "safari":
6354
6466
  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"];
6355
6467
  default:
6356
- return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
6468
+ return ["Click the site settings icon in the address bar", `Set ${device} to Allow`, "Press Try again"];
6357
6469
  }
6358
6470
  case "blocked_os": {
6359
6471
  const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
@@ -6451,7 +6563,7 @@ function createMediaSupervisor(options) {
6451
6563
  function setRegion(next) {
6452
6564
  const current = region();
6453
6565
  const merged = { ...current, ...next };
6454
- 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) {
6566
+ 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) {
6455
6567
  return;
6456
6568
  }
6457
6569
  store.patch({ mediaRegion: merged });
@@ -6547,7 +6659,7 @@ function createMediaSupervisor(options) {
6547
6659
  if (permission !== "unknown") setPermission(kind, permission);
6548
6660
  return unavailableFrom(normalized, classifyDeviceFailure(normalized, permission));
6549
6661
  }
6550
- return {
6662
+ const api = {
6551
6663
  start() {
6552
6664
  if (started || platform === null) return;
6553
6665
  started = true;
@@ -6569,6 +6681,16 @@ function createMediaSupervisor(options) {
6569
6681
  started = false;
6570
6682
  },
6571
6683
  unavailable,
6684
+ async preview(kind, outcome) {
6685
+ api.start();
6686
+ let next;
6687
+ if (typeof outcome === "string") next = { state: outcome, unavailable: null };
6688
+ else next = { state: "unavailable", unavailable: await unavailable(outcome.error, kind, `meet.${kind}Preview`) };
6689
+ const current = region().preview;
6690
+ const was = current[kind];
6691
+ if (was.state === next.state && was.unavailable?.reason === next.unavailable?.reason && was.unavailable?.message === next.unavailable?.message) return;
6692
+ setRegion({ preview: { ...region().preview, [kind]: next } });
6693
+ },
6572
6694
  onAudioPlayback(canPlay) {
6573
6695
  setRegion({ audioOutput: canPlay ? "ok" : "blocked" });
6574
6696
  },
@@ -6627,6 +6749,7 @@ function createMediaSupervisor(options) {
6627
6749
  setRegion({ notice: null });
6628
6750
  }
6629
6751
  };
6752
+ return api;
6630
6753
  }
6631
6754
 
6632
6755
  // src/core/room.ts
@@ -6788,7 +6911,8 @@ function createRoomEngine(options) {
6788
6911
  message: normalized.message,
6789
6912
  remedy: normalized.remedy,
6790
6913
  reason: normalized.reason,
6791
- retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3
6914
+ retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3,
6915
+ stage: "join"
6792
6916
  }
6793
6917
  });
6794
6918
  store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
@@ -7052,7 +7176,7 @@ function createRoomEngine(options) {
7052
7176
  const { secondDevice: _answered, ...rest } = lastRequest;
7053
7177
  lastRequest = rest;
7054
7178
  }
7055
- options.tabClaim?.inRoom(String(request.roomName), String(token.identity));
7179
+ options.tabClaim?.inRoom(String(request.roomName), String(token.identity), token.secondDeviceRule ?? null);
7056
7180
  current = token;
7057
7181
  lastServerUrl = token.serverUrl;
7058
7182
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
@@ -7179,6 +7303,10 @@ function createRoomEngine(options) {
7179
7303
  }
7180
7304
  departures.clear();
7181
7305
  liveRoster = list;
7306
+ const localNow = list.find((p) => p.isLocal);
7307
+ if (localNow !== void 0 && !localNow.screenSharing && store.snapshot().share.state === "presenting") {
7308
+ store.patch({ share: NO_SHARE });
7309
+ }
7182
7310
  const merged = mergeGhosts(list, ghosts, at);
7183
7311
  ghosts = merged.kept;
7184
7312
  const people = /* @__PURE__ */ new Map();
@@ -7360,10 +7488,12 @@ function createRoomEngine(options) {
7360
7488
  if (claim === void 0 || userId === null) return false;
7361
7489
  const before = store.snapshot();
7362
7490
  if (before.phase !== "prejoin" || before.secondDevice !== null) return false;
7363
- if (!await claim.presentElsewhere(String(roomName), `user:${String(userId)}`)) return false;
7491
+ const person = `user:${String(userId)}`;
7492
+ const found = claim.presenceElsewhere !== void 0 ? await claim.presenceElsewhere(String(roomName), person) : await claim.presentElsewhere(String(roomName), person) ? { rule: null } : null;
7493
+ if (found === null || found.rule === "allow_both") return false;
7364
7494
  const now2 = store.snapshot();
7365
7495
  if (now2.phase !== "prejoin" || now2.secondDevice !== null) return false;
7366
- store.patch({ secondDevice: { rule: "ask_switch_or_join" } });
7496
+ store.patch({ secondDevice: { rule: found.rule ?? "ask_switch_or_join" } });
7367
7497
  report("info", "meet: this browser already has you in this meeting \u2014 offering the choice before Join");
7368
7498
  return true;
7369
7499
  },
@@ -7398,7 +7528,10 @@ function createRoomEngine(options) {
7398
7528
  try {
7399
7529
  await driver.setScreenShareEnabled(enabled);
7400
7530
  ambiguousShareDenials = 0;
7401
- store.patch({ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now() } });
7531
+ const audio = enabled ? driver?.screenShareHasAudio?.() : void 0;
7532
+ store.patch({
7533
+ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now(), ...audio !== void 0 ? { audio } : {} }
7534
+ });
7402
7535
  } catch (error) {
7403
7536
  const kind = classifyScreenShareError(error);
7404
7537
  if (kind === "ambiguous") {
@@ -7465,13 +7598,14 @@ function createRoomEngine(options) {
7465
7598
  return driver?.supportsVideoEffects?.() ?? false;
7466
7599
  },
7467
7600
  supportsScreenShare() {
7468
- return driver?.supportsScreenShare?.() ?? true;
7601
+ return driver?.supportsScreenShare?.() ?? detectCapabilities().canShareScreen;
7469
7602
  },
7470
7603
  async switchDevice(kind, deviceId) {
7471
7604
  if (driver === null) return;
7472
7605
  await driver.switchDevice(kind, deviceId);
7473
7606
  },
7474
7607
  enableAudio: () => media.enableAudio(),
7608
+ previewDevice: (kind, outcome) => media.preview(kind, outcome),
7475
7609
  acceptDeviceOffer: () => media.acceptOffer(),
7476
7610
  dismissDeviceOffer: () => media.dismissOffer(),
7477
7611
  dismissDeviceNotice: () => media.dismissNotice(),
@@ -8502,6 +8636,21 @@ function observedDevice(track, permission = "unknown", held = null) {
8502
8636
  const reason = deviceUnavailableReason(track, permission);
8503
8637
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
8504
8638
  }
8639
+ function observedPreview(preview, permission = "unknown") {
8640
+ if (permission === "denied") return "unavailable:denied";
8641
+ switch (preview.state) {
8642
+ case "on":
8643
+ return "on";
8644
+ case "off":
8645
+ return "muted_by_me";
8646
+ case "prompting":
8647
+ return "prompting";
8648
+ case "unavailable":
8649
+ return `unavailable:${preview.unavailable?.reason ?? "failed"}`;
8650
+ default:
8651
+ return null;
8652
+ }
8653
+ }
8505
8654
  function endedRecord(snapshot, given) {
8506
8655
  const record = snapshot?.meeting ?? given;
8507
8656
  return record !== null && record.endedAt !== null;
@@ -8525,18 +8674,22 @@ function observe(input) {
8525
8674
  const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
8526
8675
  const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
8527
8676
  const deviceInPlay = phase === "joining" || inCall;
8677
+ const region = snapshot?.mediaRegion;
8678
+ const atPrejoin = phase === "prejoin" && region !== void 0;
8528
8679
  return {
8529
8680
  phase,
8530
8681
  reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
8531
8682
  role,
8532
- connection: snapshot?.connection ?? "stable",
8683
+ // `net-blip`: in the call, a drop shorter than `reconnect_banner_delay_ms` is not something the
8684
+ // person sees — the contract says what is on screen, so it reads `stable` until the banner shows.
8685
+ connection: snapshot === null ? "stable" : inCall && snapshot.connection !== "stable" && !snapshot.connectionBanner ? "stable" : snapshot.connection,
8533
8686
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
8534
8687
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
8535
8688
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
8536
8689
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
8537
8690
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
8538
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera, lockedFor(snapshot, "camera", inCall)) : null,
8539
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone, lockedFor(snapshot, "microphone", inCall)) : null
8691
+ 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,
8692
+ 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
8540
8693
  };
8541
8694
  }
8542
8695
  function lockedFor(snapshot, kind, inCall) {
@@ -8616,4 +8769,37 @@ function microphoneNoticeCode(track, permission = "unknown") {
8616
8769
  if (reason === null) return null;
8617
8770
  return deviceNoticeCode("microphone", reason) === "mic-blocked" ? "mic-blocked" : "device-lost";
8618
8771
  }
8772
+
8773
+ // src/core/speaking-detector.ts
8774
+ function createSpeakingWhileMutedDetector(options = {}) {
8775
+ const threshold = options.threshold ?? 0.06;
8776
+ const holdMs = options.holdMs ?? 600;
8777
+ const gapMs = options.gapMs ?? 400;
8778
+ const cooldownMs = options.cooldownMs ?? 2e4;
8779
+ let speechStart = null;
8780
+ let lastLoud = null;
8781
+ let firedAt = null;
8782
+ return {
8783
+ feed(level, at) {
8784
+ if (level >= threshold) {
8785
+ if (speechStart === null || lastLoud === null || at - lastLoud > gapMs) speechStart = at;
8786
+ lastLoud = at;
8787
+ } else if (lastLoud !== null && at - lastLoud > gapMs) {
8788
+ speechStart = null;
8789
+ lastLoud = null;
8790
+ }
8791
+ if (speechStart === null || at - speechStart < holdMs) return false;
8792
+ if (firedAt !== null && at - firedAt < cooldownMs) return false;
8793
+ firedAt = at;
8794
+ speechStart = null;
8795
+ lastLoud = null;
8796
+ return true;
8797
+ },
8798
+ reset() {
8799
+ speechStart = null;
8800
+ lastLoud = null;
8801
+ firedAt = null;
8802
+ }
8803
+ };
8804
+ }
8619
8805
  //# sourceMappingURL=core.cjs.map