@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/react.cjs CHANGED
@@ -158,6 +158,7 @@ __export(react_exports, {
158
158
  createReconnectPolicy: () => createReconnectPolicy,
159
159
  createRecordingController: () => createRecordingController,
160
160
  createRoomEngine: () => createRoomEngine,
161
+ createSpeakingWhileMutedDetector: () => createSpeakingWhileMutedDetector,
161
162
  createTokenClient: () => createTokenClient,
162
163
  createWebDeviceStorage: () => createWebDeviceStorage,
163
164
  createWebEffectsStorage: () => createWebEffectsStorage,
@@ -166,6 +167,7 @@ __export(react_exports, {
166
167
  createWebRoomTokenStorage: () => createWebRoomTokenStorage,
167
168
  defaultCaptionLanguage: () => defaultCaptionLanguage,
168
169
  detectBrowser: () => detectBrowser,
170
+ detectCapabilities: () => detectCapabilities,
169
171
  detectOs: () => detectOs,
170
172
  deviceChipLabel: () => deviceChipLabel,
171
173
  deviceFixSteps: () => deviceFixSteps,
@@ -190,6 +192,7 @@ __export(react_exports, {
190
192
  isLinkFollower: () => isLinkFollower,
191
193
  isListenOnly: () => isListenOnly,
192
194
  isLivePhase: () => isLivePhase,
195
+ isMobileUserAgent: () => isMobileUserAgent,
193
196
  isNoteTaker: () => isNoteTaker,
194
197
  isObserverRole: () => isObserverRole,
195
198
  isRealName: () => isRealName,
@@ -282,6 +285,7 @@ __export(react_exports, {
282
285
  transcriptDocument: () => transcriptDocument,
283
286
  unavailableFrom: () => unavailableFrom,
284
287
  uniqueAttendees: () => uniqueAttendees,
288
+ unsupportedWords: () => unsupportedWords,
285
289
  untranslatedLines: () => untranslatedLines,
286
290
  useActivityAttention: () => useActivityAttention,
287
291
  useAudioLevel: () => useAudioLevel,
@@ -354,7 +358,14 @@ function createTabClaim(platform = webTabPlatform()) {
354
358
  if (message.kind === "tab-alive?" && mine !== null && message.tabId === mine && message.from !== self) {
355
359
  platform.bus?.post({ kind: "tab-alive", tabId: mine });
356
360
  } else if (message.kind === "who-is-in?" && current !== null && mine !== null && message.from !== self && message.room === current.room && message.person === current.person) {
357
- platform.bus?.post({ kind: "in-room", room: current.room, person: current.person, tabId: mine, to: message.from });
361
+ platform.bus?.post({
362
+ kind: "in-room",
363
+ room: current.room,
364
+ person: current.person,
365
+ tabId: mine,
366
+ to: message.from,
367
+ rule: current.rule
368
+ });
358
369
  }
359
370
  }) ?? (() => {
360
371
  });
@@ -384,28 +395,32 @@ function createTabClaim(platform = webTabPlatform()) {
384
395
  return candidate;
385
396
  }
386
397
  const tabId = claim();
398
+ async function presence(room, person) {
399
+ const bus = platform.bus;
400
+ if (bus === null) return null;
401
+ let seen = null;
402
+ const stop = bus.subscribe((message) => {
403
+ if (message.kind === "in-room" && message.to === self && message.room === room && message.person === person) {
404
+ seen = { rule: seen?.rule ?? message.rule ?? null };
405
+ }
406
+ });
407
+ bus.post({ kind: "who-is-in?", room, person, from: self });
408
+ await wait(TAB_ANSWER_MS);
409
+ stop();
410
+ return seen;
411
+ }
387
412
  return {
388
413
  tabId,
389
- inRoom(room, person) {
390
- current = { room, person };
414
+ inRoom(room, person, rule) {
415
+ current = { room, person, rule: rule ?? null };
391
416
  },
392
417
  left() {
393
418
  current = null;
394
419
  },
395
420
  async presentElsewhere(room, person) {
396
- const bus = platform.bus;
397
- if (bus === null) return false;
398
- let seen = false;
399
- const stop = bus.subscribe((message) => {
400
- if (message.kind === "in-room" && message.to === self && message.room === room && message.person === person) {
401
- seen = true;
402
- }
403
- });
404
- bus.post({ kind: "who-is-in?", room, person, from: self });
405
- await wait(TAB_ANSWER_MS);
406
- stop();
407
- return seen;
421
+ return await presence(room, person) !== null;
408
422
  },
423
+ presenceElsewhere: (room, person) => presence(room, person),
409
424
  dispose() {
410
425
  unsubscribe();
411
426
  }
@@ -583,7 +598,8 @@ function createWebResumeStorage() {
583
598
  }
584
599
  };
585
600
  }
586
- var CLEARS_RESUME = (phase) => phase === "left" || phase === "removed" || phase === "superseded" || phase === "inactive" || phase === "ended" || phase === "record" || phase === "prejoin" || phase.startsWith("refused:") || phase.startsWith("waiting:");
601
+ 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:");
602
+ var WAITING_RESUMES = (phase) => phase.startsWith("waiting:");
587
603
  function autoRejoinDelayMs(attempt, random = Math.random) {
588
604
  const base = Math.min(15e3, 1e3 * 2 ** Math.max(0, attempt));
589
605
  return Math.round(base * (0.5 + random()));
@@ -715,6 +731,10 @@ function createConnectionSupervisor(options) {
715
731
  rejoinAttempts = 0;
716
732
  stopRejoin();
717
733
  saveMarker(true);
734
+ } else if (WAITING_RESUMES(phase)) {
735
+ stopRejoin();
736
+ driverRegion = null;
737
+ saveMarker(true);
718
738
  } else if (phase === "disconnected") {
719
739
  driverRegion = null;
720
740
  saveMarker(true);
@@ -1235,6 +1255,7 @@ function parseRoomToken(body, roomName) {
1235
1255
  connectionIdentity: readString(payload, "connection_identity") ?? personKey,
1236
1256
  connectionKind: readString(payload, "connection_kind") === "companion" ? "companion" : "main",
1237
1257
  secondDeviceChoice,
1258
+ secondDeviceRule: readString(payload, "second_device_rule"),
1238
1259
  displayName: readString(payload, "display_name") ?? readString(payload, "displayName") ?? identity,
1239
1260
  expiresAt,
1240
1261
  canPublish: readBoolean(payload, "can_publish", true),
@@ -1948,6 +1969,12 @@ var JOINABLE_FROM = /* @__PURE__ */ new Set([
1948
1969
  // allows the press; the server's gate decides whether it knocks or is refused again.
1949
1970
  "refused:lobby_rejects"
1950
1971
  ]);
1972
+ var RETRYABLE_REFUSALS = /* @__PURE__ */ new Set([
1973
+ "refused:locked",
1974
+ "refused:too_early",
1975
+ "refused:meeting_full",
1976
+ "refused:platform_capacity"
1977
+ ]);
1951
1978
  var NOT_ENDABLE = /* @__PURE__ */ new Set([
1952
1979
  "ended",
1953
1980
  "record",
@@ -2013,7 +2040,7 @@ function nextPhase(from, event) {
2013
2040
  case "link_state":
2014
2041
  return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
2015
2042
  case "join":
2016
- return JOINABLE_FROM.has(from) ? "joining" : null;
2043
+ return JOINABLE_FROM.has(from) || RETRYABLE_REFUSALS.has(from) ? "joining" : null;
2017
2044
  case "auto_rejoin":
2018
2045
  return from === "disconnected" ? "joining" : null;
2019
2046
  case "resumed":
@@ -2792,11 +2819,13 @@ function minutesLeft(policy, startedAtMs, nowMs) {
2792
2819
  }
2793
2820
 
2794
2821
  // src/core/attendance.ts
2822
+ var IDLE_PREVIEW = { state: "idle", unavailable: null };
2795
2823
  var IDLE_MEDIA_REGION = {
2796
2824
  permission: { microphone: "unknown", camera: "unknown" },
2797
2825
  audioOutput: "ok",
2798
2826
  notice: null,
2799
- offer: null
2827
+ offer: null,
2828
+ preview: { microphone: IDLE_PREVIEW, camera: IDLE_PREVIEW }
2800
2829
  };
2801
2830
  var IDLE_TRACK = { enabled: false, unavailable: null };
2802
2831
  var IDLE_MEDIA = {
@@ -2853,6 +2882,10 @@ var RPCS = {
2853
2882
  admitAll: "meet_admit_all",
2854
2883
  setLock: "meet_set_lock",
2855
2884
  recordConsent: "meet_record_consent",
2885
+ /** One meeting rule as the gate obeys it (the knob ladder, this meeting's rung included). */
2886
+ meetingRule: "meet_policy",
2887
+ /** A host sets one rule for THIS meeting (the `meet_meeting` rung); refusals come back as `{ok:false}`. */
2888
+ setMeetingRule: "meet_policy_set",
2856
2889
  /*
2857
2890
  * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
2858
2891
  * authorized by the meeting's host or a real iam grant (editor to change,
@@ -2899,7 +2932,7 @@ var INVITEE_COLUMNS = "id,meeting_id,invitee_user_id,email,display_name,role,rsv
2899
2932
  var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
2900
2933
  var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
2901
2934
  var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
2902
- 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";
2935
+ 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";
2903
2936
  function projectStays(value) {
2904
2937
  if (!Array.isArray(value)) return [];
2905
2938
  const stays = [];
@@ -3299,6 +3332,8 @@ function createMeetRepository(options) {
3299
3332
  admissionState: states.includes(admission) ? admission : "admitted",
3300
3333
  knockedAt: text(row, "knocked_at"),
3301
3334
  knockExpiresAt: text(row, "knock_expires_at"),
3335
+ // The list read selects `metadata->>knock_flag`; a Postgres Changes row carries `metadata`.
3336
+ knockFlag: text(row, "knock_flag") ?? (typeof row["metadata"] === "object" && row["metadata"] !== null ? text(row["metadata"], "knock_flag") : null),
3302
3337
  isAgent: bool(row, "is_agent", false),
3303
3338
  joinedAt: text(row, "joined_at"),
3304
3339
  leftAt: text(row, "left_at"),
@@ -3622,6 +3657,25 @@ function createMeetRepository(options) {
3622
3657
  );
3623
3658
  return projectMeeting(row);
3624
3659
  },
3660
+ async meetingRule(meetingId, key) {
3661
+ const response = await db.rpc(RPCS.meetingRule, { p_meeting_id: meetingId, p_key: key });
3662
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.meetingRule");
3663
+ return response.data;
3664
+ },
3665
+ async setMeetingRule(meetingId, key, value) {
3666
+ const response = await db.rpc(RPCS.setMeetingRule, { p_meeting_id: meetingId, p_key: key, p_value: value });
3667
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.setMeetingRule");
3668
+ const answer = response.data ?? {};
3669
+ if (answer["ok"] !== true) {
3670
+ throw new MeetError(
3671
+ "forbidden",
3672
+ typeof answer["detail"] === "string" ? answer["detail"] : "This meeting's rule was not changed.",
3673
+ "Only the meeting's host or a co-host sets its rules; an organization can lock a rule for everyone.",
3674
+ answer
3675
+ );
3676
+ }
3677
+ return answer["effective_value"];
3678
+ },
3625
3679
  async transcript(meetingId, limit = 1e3) {
3626
3680
  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);
3627
3681
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.transcript");
@@ -4516,7 +4570,8 @@ function lobbyEntryFrom(attendee, now = Date.now()) {
4516
4570
  knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4517
4571
  knockExpiresAt: attendee.knockExpiresAt ?? null,
4518
4572
  decidedAt: null,
4519
- decidedBy: null
4573
+ decidedBy: null,
4574
+ flag: attendee.knockFlag ?? null
4520
4575
  };
4521
4576
  }
4522
4577
  function deniedEntryFrom(attendee) {
@@ -5048,6 +5103,17 @@ function createMeetingSession(options) {
5048
5103
  async askAgain() {
5049
5104
  await options.rejoin?.();
5050
5105
  },
5106
+ async meetingRule(key) {
5107
+ const meeting = requireMeeting("meet.meetingRule");
5108
+ return repository.meetingRule(meeting.id, key);
5109
+ },
5110
+ async setMeetingRule(key, value) {
5111
+ requireHostUser("meet.setMeetingRule");
5112
+ const meeting = requireMeeting("meet.setMeetingRule");
5113
+ const effective = await repository.setMeetingRule(meeting.id, key, value);
5114
+ hint();
5115
+ return effective;
5116
+ },
5051
5117
  async setLock(locked) {
5052
5118
  const by = requireHostUser("meet.setLock");
5053
5119
  const meeting = requireMeeting("meet.setLock");
@@ -5151,6 +5217,47 @@ function createMeetSessionIdentity(options) {
5151
5217
  });
5152
5218
  }
5153
5219
 
5220
+ // src/core/capabilities.ts
5221
+ var IN_APP = [
5222
+ [/Instagram/i, "Instagram"],
5223
+ [/FBAN|FBAV|FB_IAB/i, "Facebook"],
5224
+ [/LinkedInApp/i, "LinkedIn"],
5225
+ [/MicroMessenger/i, "WeChat"],
5226
+ [/Snapchat/i, "Snapchat"],
5227
+ [/\bLine\//i, "LINE"],
5228
+ [/Twitter|TwitterAndroid/i, "X"],
5229
+ [/TikTok|musical_ly|BytedanceWebview/i, "TikTok"]
5230
+ ];
5231
+ function isMobileUserAgent(userAgent, maxTouchPoints = 0) {
5232
+ if (/iPhone|iPod|Android|Mobile|iPad/i.test(userAgent)) return true;
5233
+ return /Macintosh/.test(userAgent) && maxTouchPoints > 1;
5234
+ }
5235
+ function detectCapabilities(scope = typeof globalThis === "undefined" ? null : globalThis) {
5236
+ if (scope === null || scope.navigator === void 0) {
5237
+ return { canJoin: true, reason: null, inAppBrowser: null, mobile: false, canShareScreen: false };
5238
+ }
5239
+ const ua = scope.navigator.userAgent ?? "";
5240
+ const webrtc = typeof scope.RTCPeerConnection === "function" || typeof scope.webkitRTCPeerConnection === "function";
5241
+ const mobile = isMobileUserAgent(ua, scope.navigator.maxTouchPoints ?? 0);
5242
+ const inAppBrowser = IN_APP.find(([pattern]) => pattern.test(ua))?.[1] ?? null;
5243
+ return {
5244
+ canJoin: webrtc,
5245
+ reason: webrtc ? null : "no_webrtc",
5246
+ inAppBrowser,
5247
+ mobile,
5248
+ canShareScreen: !mobile && typeof scope.navigator.mediaDevices?.getDisplayMedia === "function"
5249
+ };
5250
+ }
5251
+ function unsupportedWords(capabilities) {
5252
+ if (capabilities.inAppBrowser !== null) {
5253
+ return {
5254
+ title: `${capabilities.inAppBrowser}'s browser can't join calls`,
5255
+ body: "Open this link in Chrome, Safari, Edge or Firefox"
5256
+ };
5257
+ }
5258
+ return { title: "This browser can't join calls", body: "Open this link in Chrome, Safari, Edge or Firefox" };
5259
+ }
5260
+
5154
5261
  // src/core/media-supervisor.ts
5155
5262
  function classifyDeviceFailure(error, permission) {
5156
5263
  if (permission === "denied") return "denied";
@@ -5211,7 +5318,7 @@ function deviceFixSteps(kind, reason, where) {
5211
5318
  case "safari":
5212
5319
  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"];
5213
5320
  default:
5214
- return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
5321
+ return ["Click the site settings icon in the address bar", `Set ${device} to Allow`, "Press Try again"];
5215
5322
  }
5216
5323
  case "blocked_os": {
5217
5324
  const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
@@ -5309,7 +5416,7 @@ function createMediaSupervisor(options) {
5309
5416
  function setRegion(next) {
5310
5417
  const current = region();
5311
5418
  const merged = { ...current, ...next };
5312
- 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) {
5419
+ 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) {
5313
5420
  return;
5314
5421
  }
5315
5422
  store.patch({ mediaRegion: merged });
@@ -5405,7 +5512,7 @@ function createMediaSupervisor(options) {
5405
5512
  if (permission !== "unknown") setPermission(kind, permission);
5406
5513
  return unavailableFrom(normalized, classifyDeviceFailure(normalized, permission));
5407
5514
  }
5408
- return {
5515
+ const api = {
5409
5516
  start() {
5410
5517
  if (started || platform === null) return;
5411
5518
  started = true;
@@ -5427,6 +5534,16 @@ function createMediaSupervisor(options) {
5427
5534
  started = false;
5428
5535
  },
5429
5536
  unavailable,
5537
+ async preview(kind, outcome) {
5538
+ api.start();
5539
+ let next;
5540
+ if (typeof outcome === "string") next = { state: outcome, unavailable: null };
5541
+ else next = { state: "unavailable", unavailable: await unavailable(outcome.error, kind, `meet.${kind}Preview`) };
5542
+ const current = region().preview;
5543
+ const was = current[kind];
5544
+ if (was.state === next.state && was.unavailable?.reason === next.unavailable?.reason && was.unavailable?.message === next.unavailable?.message) return;
5545
+ setRegion({ preview: { ...region().preview, [kind]: next } });
5546
+ },
5430
5547
  onAudioPlayback(canPlay) {
5431
5548
  setRegion({ audioOutput: canPlay ? "ok" : "blocked" });
5432
5549
  },
@@ -5485,6 +5602,7 @@ function createMediaSupervisor(options) {
5485
5602
  setRegion({ notice: null });
5486
5603
  }
5487
5604
  };
5605
+ return api;
5488
5606
  }
5489
5607
 
5490
5608
  // src/core/room-events.ts
@@ -5996,7 +6114,8 @@ function createRoomEngine(options) {
5996
6114
  message: normalized.message,
5997
6115
  remedy: normalized.remedy,
5998
6116
  reason: normalized.reason,
5999
- retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3
6117
+ retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3,
6118
+ stage: "join"
6000
6119
  }
6001
6120
  });
6002
6121
  store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
@@ -6260,7 +6379,7 @@ function createRoomEngine(options) {
6260
6379
  const { secondDevice: _answered, ...rest } = lastRequest;
6261
6380
  lastRequest = rest;
6262
6381
  }
6263
- options.tabClaim?.inRoom(String(request.roomName), String(token.identity));
6382
+ options.tabClaim?.inRoom(String(request.roomName), String(token.identity), token.secondDeviceRule ?? null);
6264
6383
  current = token;
6265
6384
  lastServerUrl = token.serverUrl;
6266
6385
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
@@ -6387,6 +6506,10 @@ function createRoomEngine(options) {
6387
6506
  }
6388
6507
  departures.clear();
6389
6508
  liveRoster = list;
6509
+ const localNow = list.find((p) => p.isLocal);
6510
+ if (localNow !== void 0 && !localNow.screenSharing && store.snapshot().share.state === "presenting") {
6511
+ store.patch({ share: NO_SHARE });
6512
+ }
6390
6513
  const merged = mergeGhosts(list, ghosts, at);
6391
6514
  ghosts = merged.kept;
6392
6515
  const people = /* @__PURE__ */ new Map();
@@ -6568,10 +6691,12 @@ function createRoomEngine(options) {
6568
6691
  if (claim === void 0 || userId === null) return false;
6569
6692
  const before = store.snapshot();
6570
6693
  if (before.phase !== "prejoin" || before.secondDevice !== null) return false;
6571
- if (!await claim.presentElsewhere(String(roomName), `user:${String(userId)}`)) return false;
6694
+ const person = `user:${String(userId)}`;
6695
+ const found = claim.presenceElsewhere !== void 0 ? await claim.presenceElsewhere(String(roomName), person) : await claim.presentElsewhere(String(roomName), person) ? { rule: null } : null;
6696
+ if (found === null || found.rule === "allow_both") return false;
6572
6697
  const now2 = store.snapshot();
6573
6698
  if (now2.phase !== "prejoin" || now2.secondDevice !== null) return false;
6574
- store.patch({ secondDevice: { rule: "ask_switch_or_join" } });
6699
+ store.patch({ secondDevice: { rule: found.rule ?? "ask_switch_or_join" } });
6575
6700
  report("info", "meet: this browser already has you in this meeting \u2014 offering the choice before Join");
6576
6701
  return true;
6577
6702
  },
@@ -6606,7 +6731,10 @@ function createRoomEngine(options) {
6606
6731
  try {
6607
6732
  await driver.setScreenShareEnabled(enabled);
6608
6733
  ambiguousShareDenials = 0;
6609
- store.patch({ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now() } });
6734
+ const audio = enabled ? driver?.screenShareHasAudio?.() : void 0;
6735
+ store.patch({
6736
+ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now(), ...audio !== void 0 ? { audio } : {} }
6737
+ });
6610
6738
  } catch (error) {
6611
6739
  const kind = classifyScreenShareError(error);
6612
6740
  if (kind === "ambiguous") {
@@ -6673,13 +6801,14 @@ function createRoomEngine(options) {
6673
6801
  return driver?.supportsVideoEffects?.() ?? false;
6674
6802
  },
6675
6803
  supportsScreenShare() {
6676
- return driver?.supportsScreenShare?.() ?? true;
6804
+ return driver?.supportsScreenShare?.() ?? detectCapabilities().canShareScreen;
6677
6805
  },
6678
6806
  async switchDevice(kind, deviceId2) {
6679
6807
  if (driver === null) return;
6680
6808
  await driver.switchDevice(kind, deviceId2);
6681
6809
  },
6682
6810
  enableAudio: () => media.enableAudio(),
6811
+ previewDevice: (kind, outcome) => media.preview(kind, outcome),
6683
6812
  acceptDeviceOffer: () => media.acceptOffer(),
6684
6813
  dismissDeviceOffer: () => media.dismissOffer(),
6685
6814
  dismissDeviceNotice: () => media.dismissNotice(),
@@ -7151,7 +7280,15 @@ function createLiveKitDriver(handlers, options = {}) {
7151
7280
  created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
7152
7281
  handlers.participantLeft?.(participant.identity, reasonName(reason));
7153
7282
  roster();
7154
- }).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) => {
7283
+ }).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, (publication) => {
7284
+ if (publication.source === Track.Source.ScreenShare) {
7285
+ const ended = () => {
7286
+ void created.localParticipant.setScreenShareEnabled(false).catch(() => void 0);
7287
+ };
7288
+ publication.track?.mediaStreamTrack.addEventListener("ended", ended, { once: true });
7289
+ }
7290
+ roster();
7291
+ }).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
7155
7292
  handlers.roomMetadata?.(metadata ?? "");
7156
7293
  }).on(RoomEvent.AudioPlaybackStatusChanged, (canPlay) => {
7157
7294
  handlers.audioPlayback?.(canPlay);
@@ -7292,7 +7429,10 @@ function createLiveKitDriver(handlers, options = {}) {
7292
7429
  },
7293
7430
  async setScreenShareEnabled(enabled) {
7294
7431
  try {
7295
- await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(enabled);
7432
+ await requireRoom("setScreenShareEnabled").localParticipant.setScreenShareEnabled(
7433
+ enabled,
7434
+ enabled ? { audio: true, systemAudio: "include" } : void 0
7435
+ );
7296
7436
  } catch (error) {
7297
7437
  throw normalizeScreenShareError(error, "meet.setScreenShareEnabled");
7298
7438
  }
@@ -7331,8 +7471,13 @@ function createLiveKitDriver(handlers, options = {}) {
7331
7471
  supportsVideoEffects() {
7332
7472
  return videoEffectsSupported();
7333
7473
  },
7474
+ screenShareHasAudio() {
7475
+ const target = room;
7476
+ if (target === null) return false;
7477
+ return target.localParticipant.getTrackPublications().some((p) => String(p.source) === "screen_share_audio" && p.track !== void 0);
7478
+ },
7334
7479
  supportsScreenShare() {
7335
- return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getDisplayMedia === "function";
7480
+ return detectCapabilities().canShareScreen;
7336
7481
  },
7337
7482
  attach(identity, kind, element) {
7338
7483
  const target = requireRoom("attach");
@@ -7818,6 +7963,21 @@ function observedDevice(track, permission = "unknown", held = null) {
7818
7963
  const reason = deviceUnavailableReason(track, permission);
7819
7964
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
7820
7965
  }
7966
+ function observedPreview(preview, permission = "unknown") {
7967
+ if (permission === "denied") return "unavailable:denied";
7968
+ switch (preview.state) {
7969
+ case "on":
7970
+ return "on";
7971
+ case "off":
7972
+ return "muted_by_me";
7973
+ case "prompting":
7974
+ return "prompting";
7975
+ case "unavailable":
7976
+ return `unavailable:${preview.unavailable?.reason ?? "failed"}`;
7977
+ default:
7978
+ return null;
7979
+ }
7980
+ }
7821
7981
  function endedRecord(snapshot, given) {
7822
7982
  const record = snapshot?.meeting ?? given;
7823
7983
  return record !== null && record.endedAt !== null;
@@ -7841,18 +8001,22 @@ function observe(input) {
7841
8001
  const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
7842
8002
  const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
7843
8003
  const deviceInPlay = phase === "joining" || inCall;
8004
+ const region = snapshot?.mediaRegion;
8005
+ const atPrejoin = phase === "prejoin" && region !== void 0;
7844
8006
  return {
7845
8007
  phase,
7846
8008
  reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
7847
8009
  role,
7848
- connection: snapshot?.connection ?? "stable",
8010
+ // `net-blip`: in the call, a drop shorter than `reconnect_banner_delay_ms` is not something the
8011
+ // person sees — the contract says what is on screen, so it reads `stable` until the banner shows.
8012
+ connection: snapshot === null ? "stable" : inCall && snapshot.connection !== "stable" && !snapshot.connectionBanner ? "stable" : snapshot.connection,
7849
8013
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
7850
8014
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
7851
8015
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
7852
8016
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
7853
8017
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
7854
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera, lockedFor(snapshot, "camera", inCall)) : null,
7855
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone, lockedFor(snapshot, "microphone", inCall)) : null
8018
+ 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,
8019
+ 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
7856
8020
  };
7857
8021
  }
7858
8022
  function lockedFor(snapshot, kind, inCall) {
@@ -7952,7 +8116,13 @@ function MeetRoot({
7952
8116
  ...options
7953
8117
  }) {
7954
8118
  const snapshot = useMeetSnapshot();
7955
- const observation = useCoreObservation(options);
8119
+ const probed = useCapabilities();
8120
+ const base = useCoreObservation(options);
8121
+ const unsupported = probed !== null && !probed.canJoin;
8122
+ const observation = (0, import_react3.useMemo)(
8123
+ () => unsupported ? { ...staticObservation("unsupported", probed?.reason ?? null) } : base,
8124
+ [unsupported, probed, base]
8125
+ );
7956
8126
  const attrs = (0, import_react3.useMemo)(() => rootAttributes(observation), [observation]);
7957
8127
  const latest = (0, import_react3.useRef)({ observation, storePhase: snapshot?.phase ?? null });
7958
8128
  latest.current = { observation, storePhase: snapshot?.phase ?? null };
@@ -7970,7 +8140,34 @@ function MeetRoot({
7970
8140
  if (target.__matrxMeet === diagnostics) delete target.__matrxMeet;
7971
8141
  };
7972
8142
  }, []);
7973
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ...attrs, style: { display: "contents" }, children });
8143
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ...attrs, style: { display: "contents" }, children: unsupported && probed !== null ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(UnsupportedBrowser, { capabilities: probed }) : children });
8144
+ }
8145
+ function useCapabilities() {
8146
+ const [found, setFound] = (0, import_react3.useState)(null);
8147
+ (0, import_react3.useEffect)(() => {
8148
+ setFound(detectCapabilities());
8149
+ }, []);
8150
+ return found;
8151
+ }
8152
+ function UnsupportedBrowser({ capabilities }) {
8153
+ const words = unsupportedWords(capabilities);
8154
+ const [copied, setCopied] = (0, import_react3.useState)(false);
8155
+ const href = typeof window === "undefined" ? "" : window.location.href;
8156
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mx-meet", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mx-meet__waiting", "data-meet-exit": "unsupported", children: [
8157
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h1", { className: "mx-meet__prejoin-title", children: words.title }),
8158
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mx-meet__hint", role: "status", "data-meet-notice": "browser-unsupported", children: words.body }),
8159
+ href.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
8160
+ "button",
8161
+ {
8162
+ type: "button",
8163
+ className: "mx-meet__primary",
8164
+ onClick: () => {
8165
+ void navigator.clipboard?.writeText(href).then(() => setCopied(true), () => void 0);
8166
+ },
8167
+ children: copied ? "Link copied" : "Copy link"
8168
+ }
8169
+ ) : null
8170
+ ] }) });
7974
8171
  }
7975
8172
 
7976
8173
  // src/react/hooks.ts
@@ -8670,7 +8867,8 @@ function useRoom() {
8670
8867
  failure: snapshot?.failure ?? null,
8671
8868
  secondDevice: snapshot?.secondDevice ?? null,
8672
8869
  local,
8673
- canShareScreen: engine?.supportsScreenShare() ?? true,
8870
+ // An observer (role `viewer`) publishes nothing: no Share screen to press and be refused (MD-16).
8871
+ canShareScreen: (engine?.supportsScreenShare() ?? true) && !isObserverRole(snapshot?.grants.role ?? null),
8674
8872
  join: (0, import_react4.useCallback)(
8675
8873
  async (request) => requireEngine(engine, "join").join(request),
8676
8874
  [engine]
@@ -9149,6 +9347,12 @@ function useMediaSupervisor() {
9149
9347
  retry: (0, import_react4.useCallback)(
9150
9348
  async (kind) => kind === "microphone" ? engine?.setMicrophoneEnabled(true) : engine?.setCameraEnabled(true),
9151
9349
  [engine]
9350
+ ),
9351
+ reportPreview: (0, import_react4.useCallback)(
9352
+ (kind, outcome) => {
9353
+ void engine?.previewDevice(kind, outcome);
9354
+ },
9355
+ [engine]
9152
9356
  )
9153
9357
  };
9154
9358
  }
@@ -10244,6 +10448,39 @@ function useCollabPanelRequest(onRequest) {
10244
10448
  // src/core/reactions.ts
10245
10449
  var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
10246
10450
 
10451
+ // src/core/speaking-detector.ts
10452
+ function createSpeakingWhileMutedDetector(options = {}) {
10453
+ const threshold = options.threshold ?? 0.06;
10454
+ const holdMs = options.holdMs ?? 600;
10455
+ const gapMs = options.gapMs ?? 400;
10456
+ const cooldownMs = options.cooldownMs ?? 2e4;
10457
+ let speechStart = null;
10458
+ let lastLoud = null;
10459
+ let firedAt = null;
10460
+ return {
10461
+ feed(level, at) {
10462
+ if (level >= threshold) {
10463
+ if (speechStart === null || lastLoud === null || at - lastLoud > gapMs) speechStart = at;
10464
+ lastLoud = at;
10465
+ } else if (lastLoud !== null && at - lastLoud > gapMs) {
10466
+ speechStart = null;
10467
+ lastLoud = null;
10468
+ }
10469
+ if (speechStart === null || at - speechStart < holdMs) return false;
10470
+ if (firedAt !== null && at - firedAt < cooldownMs) return false;
10471
+ firedAt = at;
10472
+ speechStart = null;
10473
+ lastLoud = null;
10474
+ return true;
10475
+ },
10476
+ reset() {
10477
+ speechStart = null;
10478
+ lastLoud = null;
10479
+ firedAt = null;
10480
+ }
10481
+ };
10482
+ }
10483
+
10247
10484
  // src/react/facade.ts
10248
10485
  var NO_ACTIONS = {
10249
10486
  join: async () => void 0,
@@ -10264,6 +10501,10 @@ function actionsFor(host) {
10264
10501
  leave: () => engine.leave(),
10265
10502
  backToPrejoin: () => engine.backToPrejoin(),
10266
10503
  retry: () => {
10504
+ if (RETRYABLE_REFUSALS.has(store.snapshot().phase)) {
10505
+ void engine.rejoin().catch(() => void 0);
10506
+ return;
10507
+ }
10267
10508
  store.dispatch({ type: "retry", click: true });
10268
10509
  },
10269
10510
  resolved: () => {
@@ -10547,6 +10788,9 @@ function UndoIcon(props) {
10547
10788
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" })
10548
10789
  ] });
10549
10790
  }
10791
+ function ChevronUpIcon(props) {
10792
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m6 15 6-6 6 6" }) });
10793
+ }
10550
10794
 
10551
10795
  // src/skins/meet/collab.tsx
10552
10796
  var import_jsx_runtime5 = require("react/jsx-runtime");
@@ -11800,6 +12044,7 @@ function ControlBar({ onLeave }) {
11800
12044
  ) }) }) : null,
11801
12045
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BreakoutBanner, {}),
11802
12046
  panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KnockNotice, { onView: () => setPanel("people") }) : null,
12047
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SpeakingWhileMutedNudge, {}),
11803
12048
  controls.notice !== null && panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
11804
12049
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__controls", children: [
11805
12050
  live.micLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -11811,6 +12056,7 @@ function ControlBar({ onLeave }) {
11811
12056
  children: room.local?.micEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, {})
11812
12057
  }
11813
12058
  ),
12059
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AudioSettingsControl, {}),
11814
12060
  live.cameraLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11815
12061
  ControlButton,
11816
12062
  {
@@ -11926,6 +12172,13 @@ function ControlBar({ onLeave }) {
11926
12172
  ]
11927
12173
  }
11928
12174
  ),
12175
+ !room.canShareScreen && room.local?.screenSharing !== true ? (
12176
+ // `share-unsupported-mobile`: the control is absent; the menu says where it lives.
12177
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__menu-note", role: "note", "data-meet-notice": "share-unsupported", children: [
12178
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
12179
+ " Present from a computer"
12180
+ ] })
12181
+ ) : null,
11929
12182
  controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11930
12183
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__menu-sep", role: "separator" }),
11931
12184
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -11971,6 +12224,7 @@ function ControlBar({ onLeave }) {
11971
12224
  ]
11972
12225
  }
11973
12226
  ),
12227
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(WaitingRoomRule, {}),
11974
12228
  queue.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11975
12229
  MenuItem,
11976
12230
  {
@@ -12190,6 +12444,58 @@ function MenuItem({
12190
12444
  }
12191
12445
  );
12192
12446
  }
12447
+ var LOBBY_BYPASS_CHOICES = [
12448
+ { value: "everyone", label: "Everyone" },
12449
+ { value: "org_and_invited", label: "Your organization and invited people" },
12450
+ { value: "org", label: "Your organization" },
12451
+ { value: "invited", label: "Invited people" },
12452
+ { value: "host_only", label: "Only hosts" }
12453
+ ];
12454
+ function WaitingRoomRule() {
12455
+ const host = useRequiredMeetHost();
12456
+ const [open, setOpen] = (0, import_react10.useState)(false);
12457
+ const [value, setValue] = (0, import_react10.useState)(null);
12458
+ const [notice, setNotice] = (0, import_react10.useState)(null);
12459
+ (0, import_react10.useEffect)(() => {
12460
+ let live = true;
12461
+ void host.meetings.meetingRule("lobby_bypass").then((rule) => {
12462
+ if (live) setValue(typeof rule === "string" ? rule : null);
12463
+ }).catch(() => void 0);
12464
+ return () => {
12465
+ live = false;
12466
+ };
12467
+ }, [host]);
12468
+ const current = LOBBY_BYPASS_CHOICES.find((choice) => choice.value === value)?.label ?? null;
12469
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
12470
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(MenuItem, { onSelect: () => setOpen(!open), children: [
12471
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(LockIcon, { width: 16, height: 16 }),
12472
+ " Who can skip the waiting room",
12473
+ current !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__menu-value", children: current }) : null
12474
+ ] }),
12475
+ open ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { role: "group", "aria-label": "Who can skip the waiting room", "data-meet-rule": "lobby_bypass", children: [
12476
+ LOBBY_BYPASS_CHOICES.map((choice) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
12477
+ "button",
12478
+ {
12479
+ type: "button",
12480
+ role: "menuitemradio",
12481
+ "aria-checked": choice.value === value,
12482
+ className: "mx-meet__menu-item mx-meet__menu-item--sub",
12483
+ onClick: () => {
12484
+ setNotice(null);
12485
+ void host.meetings.setMeetingRule("lobby_bypass", choice.value).then(() => setValue(choice.value)).catch((error) => setNotice(error instanceof Error ? error.message : String(error)));
12486
+ },
12487
+ children: [
12488
+ choice.value === value ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CheckIcon, { width: 14, height: 14 }) : null,
12489
+ " ",
12490
+ choice.label
12491
+ ]
12492
+ },
12493
+ choice.value
12494
+ )),
12495
+ notice !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__menu-note", role: "status", children: notice }) : null
12496
+ ] }) : null
12497
+ ] });
12498
+ }
12193
12499
  function Toast({ text: text3, failed, onDone }) {
12194
12500
  (0, import_react10.useEffect)(() => {
12195
12501
  const timer = setTimeout(onDone, failed ? 7e3 : 4e3);
@@ -12200,6 +12506,94 @@ function Toast({ text: text3, failed, onDone }) {
12200
12506
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: onDone, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, { width: 14, height: 14 }) })
12201
12507
  ] });
12202
12508
  }
12509
+ function AudioSettingsControl() {
12510
+ const host = useMeetHost();
12511
+ const devices = useDeviceSnapshot();
12512
+ const [open, setOpen] = (0, import_react10.useState)(false);
12513
+ const list = (devices?.devices.audioinput ?? []).filter((device) => device.deviceId.length > 0);
12514
+ if (host === null || list.length < 2) return null;
12515
+ const current = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
12516
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__anchor", children: [
12517
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12518
+ ControlButton,
12519
+ {
12520
+ label: "Audio settings",
12521
+ active: open,
12522
+ onClick: () => {
12523
+ if (!open) void host.devices.refresh();
12524
+ setOpen(!open);
12525
+ },
12526
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChevronUpIcon, {})
12527
+ }
12528
+ ),
12529
+ open ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Popover, { label: "Microphone", onClose: () => setOpen(false), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__menu", role: "menu", "aria-label": "Microphone", children: list.map((device) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
12530
+ "button",
12531
+ {
12532
+ type: "button",
12533
+ role: "menuitemradio",
12534
+ "aria-checked": device.deviceId === current,
12535
+ className: "mx-meet__menu-item",
12536
+ onClick: () => {
12537
+ setOpen(false);
12538
+ host.devices.select("audioinput", device.deviceId);
12539
+ void host.engine.switchDevice("audioinput", device.deviceId).catch(() => void 0);
12540
+ },
12541
+ children: [
12542
+ device.deviceId === current ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CheckIcon, { width: 16, height: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__menu-gap" }),
12543
+ device.label.length > 0 ? device.label : "Microphone"
12544
+ ]
12545
+ },
12546
+ device.deviceId
12547
+ )) }) }) : null
12548
+ ] });
12549
+ }
12550
+ function SpeakingWhileMutedNudge() {
12551
+ const room = useRoom();
12552
+ const host = useMeetHost();
12553
+ const devices = useDeviceSnapshot();
12554
+ const microphone = useLocalMedia().microphone;
12555
+ const mutedByMe = host !== null && room.local != null && room.local.micEnabled !== true && microphone.unavailable === null;
12556
+ const mic = devices?.selected.audioinput ?? host?.devices.resolve("audioinput") ?? null;
12557
+ const stream = useMicrophoneStream(mutedByMe ? mic : "off");
12558
+ const level = useAudioLevel(stream);
12559
+ const latest = (0, import_react10.useRef)(0);
12560
+ latest.current = level;
12561
+ const [shown, setShown] = (0, import_react10.useState)(false);
12562
+ const [detector] = (0, import_react10.useState)(() => createSpeakingWhileMutedDetector());
12563
+ (0, import_react10.useEffect)(() => {
12564
+ if (!mutedByMe) {
12565
+ detector.reset();
12566
+ setShown(false);
12567
+ return void 0;
12568
+ }
12569
+ const timer = setInterval(() => {
12570
+ if (detector.feed(latest.current, Date.now())) setShown(true);
12571
+ }, 150);
12572
+ return () => clearInterval(timer);
12573
+ }, [mutedByMe, detector]);
12574
+ (0, import_react10.useEffect)(() => {
12575
+ if (!shown) return void 0;
12576
+ const timer = setTimeout(() => setShown(false), 8e3);
12577
+ return () => clearTimeout(timer);
12578
+ }, [shown]);
12579
+ if (!shown || !mutedByMe) return null;
12580
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "speaking-while-muted", children: [
12581
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 16, height: 16 }),
12582
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "You're muted" }),
12583
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
12584
+ "button",
12585
+ {
12586
+ type: "button",
12587
+ className: "mx-meet__primary mx-meet__primary--small",
12588
+ onClick: () => {
12589
+ setShown(false);
12590
+ void room.toggleMicrophone();
12591
+ },
12592
+ children: "Unmute"
12593
+ }
12594
+ )
12595
+ ] });
12596
+ }
12203
12597
  function MediaNotices() {
12204
12598
  const media = useLocalMedia();
12205
12599
  const { region, where, enableAudio, acceptOffer, dismissOffer, dismissNotice, retry } = useMediaSupervisor();
@@ -13049,14 +13443,20 @@ async function playTestSound(sinkId) {
13049
13443
  audio.pause();
13050
13444
  await context.close();
13051
13445
  }
13052
- function useMicrophoneStream(deviceId2) {
13446
+ function useMicrophoneStream(deviceId2, report, attempt = 0) {
13053
13447
  const [stream, setStream] = (0, import_react10.useState)(null);
13448
+ const reportRef = (0, import_react10.useRef)(report);
13449
+ reportRef.current = report;
13054
13450
  (0, import_react10.useEffect)(() => {
13055
- if (deviceId2 === "off") return void 0;
13451
+ if (deviceId2 === "off") {
13452
+ reportRef.current?.("off");
13453
+ return void 0;
13454
+ }
13056
13455
  let live = true;
13057
13456
  let opened = null;
13058
13457
  const media = globalThis.navigator?.mediaDevices;
13059
13458
  if (media === void 0) return void 0;
13459
+ reportRef.current?.("prompting");
13060
13460
  void media.getUserMedia({ audio: deviceId2 === null ? true : { deviceId: { exact: deviceId2 } } }).then((next) => {
13061
13461
  if (!live) {
13062
13462
  next.getTracks().forEach((track) => track.stop());
@@ -13064,15 +13464,49 @@ function useMicrophoneStream(deviceId2) {
13064
13464
  }
13065
13465
  opened = next;
13066
13466
  setStream(next);
13067
- }).catch(() => setStream(null));
13467
+ reportRef.current?.("on");
13468
+ }).catch((thrown) => {
13469
+ setStream(null);
13470
+ if (live) reportRef.current?.({ error: thrown });
13471
+ });
13068
13472
  return () => {
13069
13473
  live = false;
13070
13474
  opened?.getTracks().forEach((track) => track.stop());
13071
13475
  setStream(null);
13072
13476
  };
13073
- }, [deviceId2]);
13477
+ }, [deviceId2, attempt]);
13074
13478
  return stream;
13075
13479
  }
13480
+ function PreviewStatus({ onRetry }) {
13481
+ const { region, where } = useMediaSupervisor();
13482
+ const [waited, setWaited] = (0, import_react10.useState)(false);
13483
+ const prompting = region.preview.microphone.state === "prompting" || region.preview.camera.state === "prompting";
13484
+ (0, import_react10.useEffect)(() => {
13485
+ if (!prompting) {
13486
+ setWaited(false);
13487
+ return void 0;
13488
+ }
13489
+ const timer = setTimeout(() => setWaited(true), 800);
13490
+ return () => clearTimeout(timer);
13491
+ }, [prompting]);
13492
+ const refused = ["microphone", "camera"].flatMap((kind) => {
13493
+ const preview = region.preview[kind];
13494
+ const reason = region.permission[kind] === "denied" ? "denied" : preview.state === "unavailable" ? preview.unavailable?.reason ?? "failed" : null;
13495
+ return reason === null ? [] : [{ kind, reason }];
13496
+ });
13497
+ if (refused.length === 0) {
13498
+ if (!prompting || !waited) return null;
13499
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__panel-note", role: "status", "data-meet-notice": "permission-prompt", children: "Allow camera and microphone when your browser asks" });
13500
+ }
13501
+ const first = refused[0];
13502
+ const both = refused.length === 2 && refused[0].reason === refused[1].reason;
13503
+ const label = both ? deviceChipLabel("camera", first.reason).replace(/^Camera/, "Camera and mic") : refused.map((r) => deviceChipLabel(r.kind, r.reason)).join(" \xB7 ");
13504
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__warning", role: "status", "data-meet-notice": deviceNoticeCode(first.kind, first.reason), children: [
13505
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { children: label }),
13506
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ol", { className: "mx-meet__fix-steps", children: deviceFixSteps(first.kind, first.reason, where).map((step) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("li", { children: step }, step)) }),
13507
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", variant: "outline", size: "sm", onClick: onRetry, children: "Try again" })
13508
+ ] });
13509
+ }
13076
13510
  function KnockNotice({ onView }) {
13077
13511
  const { knocker, others, admit, dismiss } = useKnockNotice();
13078
13512
  if (knocker === null) return null;
@@ -13150,7 +13584,11 @@ function WaitingList({ lobby }) {
13150
13584
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Avatar, { name: entry.displayName, url: entry.avatarUrl, size: 32 }),
13151
13585
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__person-name", children: [
13152
13586
  entry.displayName,
13153
- entry.userId === null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Guest" }) : null
13587
+ entry.userId === null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Guest" }) : null,
13588
+ entry.flag === "previously_removed" ? (
13589
+ // `mod-removed-bypass`: advice for the host, never a block — Admit stays.
13590
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge mx-meet__badge--warn", "data-meet-notice": "knock-flagged", children: "Previously removed" })
13591
+ ) : null
13154
13592
  ] }),
13155
13593
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ControlButton, { label: `Admit ${entry.displayName}`, onClick: () => void lobby.admit(entry), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CheckIcon, { width: 16, height: 16 }) }),
13156
13594
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -13402,8 +13840,15 @@ function PreJoinBody({
13402
13840
  const [joining, setJoining] = (0, import_react10.useState)(false);
13403
13841
  const camera = devices?.selected.videoinput ?? host.devices.resolve("videoinput");
13404
13842
  const mic = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
13405
- const preview = useCameraPreview(mode === "video" ? camera : void 0, effects.effects);
13406
- const micStream = useMicrophoneStream(microphone ? mic : "off");
13843
+ const { reportPreview } = useMediaSupervisor();
13844
+ const [attempt, setAttempt] = (0, import_react10.useState)(0);
13845
+ const preview = useCameraPreview(
13846
+ mode === "video" ? camera : void 0,
13847
+ effects.effects,
13848
+ (outcome) => reportPreview("camera", outcome),
13849
+ attempt
13850
+ );
13851
+ const micStream = useMicrophoneStream(microphone ? mic : "off", (outcome) => reportPreview("microphone", outcome), attempt);
13407
13852
  const level = useAudioLevel(micStream);
13408
13853
  const [speakerTesting, setSpeakerTesting] = (0, import_react10.useState)(false);
13409
13854
  (0, import_react10.useEffect)(() => {
@@ -13447,6 +13892,7 @@ function PreJoinBody({
13447
13892
  ] }),
13448
13893
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__prejoin-panel", children: [
13449
13894
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h1", { className: "mx-meet__prejoin-title", children: "Ready to join?" }),
13895
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PreviewStatus, { onRetry: () => setAttempt((n) => n + 1) }),
13450
13896
  notice?.kind === "removed" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__warning", children: notice.text }) : null,
13451
13897
  devices?.labelsHidden === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__panel-note", children: [
13452
13898
  "Your browser is hiding device names until you allow camera and microphone access.",
@@ -13549,16 +13995,22 @@ function PreJoinBody({
13549
13995
  ] })
13550
13996
  ] });
13551
13997
  }
13552
- function useCameraPreview(deviceId2, effects) {
13998
+ function useCameraPreview(deviceId2, effects, report, attempt = 0) {
13999
+ const reportRef = (0, import_react10.useRef)(report);
14000
+ reportRef.current = report;
13553
14001
  const [preview, setPreview] = (0, import_react10.useState)(null);
13554
14002
  const [error, setError] = (0, import_react10.useState)(null);
13555
14003
  const current = (0, import_react10.useRef)(null);
13556
14004
  const initialEffect = (0, import_react10.useRef)(toVideoEffect(effects));
13557
14005
  initialEffect.current = toVideoEffect(effects);
13558
14006
  (0, import_react10.useEffect)(() => {
13559
- if (deviceId2 === void 0) return void 0;
14007
+ if (deviceId2 === void 0) {
14008
+ reportRef.current?.("off");
14009
+ return void 0;
14010
+ }
13560
14011
  let live = true;
13561
14012
  setError(null);
14013
+ reportRef.current?.("prompting");
13562
14014
  void createCameraPreview(deviceId2, initialEffect.current).then((created) => {
13563
14015
  if (!live) {
13564
14016
  created.stop();
@@ -13566,8 +14018,10 @@ function useCameraPreview(deviceId2, effects) {
13566
14018
  }
13567
14019
  current.current = created;
13568
14020
  setPreview(created);
14021
+ reportRef.current?.("on");
13569
14022
  }).catch((thrown) => {
13570
14023
  if (!live) return;
14024
+ reportRef.current?.({ error: thrown });
13571
14025
  const failure = thrown;
13572
14026
  const message = (failure.message ?? "").replace(/^meet\.[A-Za-z.()]+:\s*/, "");
13573
14027
  const sentence = message.length > 0 ? message.charAt(0).toUpperCase() + message.slice(1) : "";
@@ -13579,7 +14033,7 @@ function useCameraPreview(deviceId2, effects) {
13579
14033
  current.current = null;
13580
14034
  setPreview(null);
13581
14035
  };
13582
- }, [deviceId2]);
14036
+ }, [deviceId2, attempt]);
13583
14037
  return {
13584
14038
  preview,
13585
14039
  error,
@@ -13719,6 +14173,14 @@ function MeetSkinRoom({
13719
14173
  }
13720
14174
  function ShareNotice() {
13721
14175
  const { share, actions } = useMeetCore();
14176
+ const [soundHintClosed, setSoundHintClosed] = (0, import_react10.useState)(null);
14177
+ if (share.state === "presenting" && share.audio === false && soundHintClosed !== share.at) {
14178
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-audio-missing", children: [
14179
+ "Sound not shared",
14180
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__remedy", children: "To include sound, share a tab and turn on Also share tab audio" }),
14181
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setSoundHintClosed(share.at), children: "Dismiss" })
14182
+ ] });
14183
+ }
13722
14184
  if (share.state !== "refused") return null;
13723
14185
  const text3 = share.reason === "unsupported" ? "You can't share your screen from this browser" : share.reason === "blocked" ? "Screen sharing is blocked" : "Couldn't start sharing your screen";
13724
14186
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-unavailable", children: [
@@ -13751,7 +14213,20 @@ function ExitScreen({ onLeave }) {
13751
14213
  const { phase, reason, host, snapshot, actions, call, connection } = useMeetCore();
13752
14214
  const [busy, setBusy] = (0, import_react10.useState)(false);
13753
14215
  const failure = snapshot?.failure ?? null;
13754
- const retryLeft = useCountdown(phase === "refused:lobby_rejects" ? failure?.retryAt ?? null : null);
14216
+ const retryLeft = useCountdown(
14217
+ phase === "refused:lobby_rejects" || phase === "refused:too_early" ? failure?.retryAt ?? null : null
14218
+ );
14219
+ const reaskedAtZero = (0, import_react10.useRef)(false);
14220
+ (0, import_react10.useEffect)(() => {
14221
+ if (phase !== "refused:too_early") {
14222
+ reaskedAtZero.current = false;
14223
+ return;
14224
+ }
14225
+ if (retryLeft === 0 && !reaskedAtZero.current) {
14226
+ reaskedAtZero.current = true;
14227
+ actions.retry();
14228
+ }
14229
+ }, [phase, retryLeft, actions]);
13755
14230
  const rejoin = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
13756
14231
  import_design_system.Button,
13757
14232
  {
@@ -13789,11 +14264,19 @@ function ExitScreen({ onLeave }) {
13789
14264
  if (phase.startsWith("refused:")) {
13790
14265
  const code = phase.slice("refused:".length);
13791
14266
  title = REFUSED_TITLES[code] ?? "You can't join this meeting";
13792
- line = code === "too_early" || code === "registration_required" || code === "cancelled" ? failure?.remedy ?? null : null;
14267
+ line = code === "too_early" || code === "registration_required" || code === "cancelled" || code === "wrong_account" ? failure?.remedy ?? null : null;
14268
+ if (code === "too_early" && retryLeft !== null && retryLeft > 0) line = `Opens in ${clockText(retryLeft)}`;
13793
14269
  actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
13794
14270
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", variant: "outline", onClick: actions.retry, children: "Try again" }),
13795
14271
  home
13796
14272
  ] });
14273
+ if (code === "sign_in_required" || code === "wrong_account") {
14274
+ const back = typeof window === "undefined" ? "/" : `${window.location.pathname}${window.location.search}`;
14275
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
14276
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("a", { href: `/login?redirectTo=${encodeURIComponent(back)}`, children: code === "sign_in_required" ? "Sign in" : "Switch account" }) }),
14277
+ home
14278
+ ] });
14279
+ }
13797
14280
  if (code === "lobby_rejects") {
13798
14281
  if (retryLeft !== null && retryLeft > 0) {
13799
14282
  line = `You can ask again in ${clockText(retryLeft)}`;
@@ -13831,6 +14314,27 @@ function ExitScreen({ onLeave }) {
13831
14314
  ] });
13832
14315
  break;
13833
14316
  case "disconnected":
14317
+ if (failure?.stage === "join" && (failure.reason ?? null) === null) {
14318
+ title = "Can't connect to the meeting";
14319
+ line = connection === "offline" ? "You're offline. Check your internet connection." : "Check your network. A VPN, firewall or proxy may be blocking the call.";
14320
+ notice = "cannot-connect";
14321
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
14322
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
14323
+ import_design_system.Button,
14324
+ {
14325
+ type: "button",
14326
+ disabled: busy,
14327
+ onClick: () => {
14328
+ setBusy(true);
14329
+ void actions.rejoin().catch(() => void 0).finally(() => setBusy(false));
14330
+ },
14331
+ children: "Try again"
14332
+ }
14333
+ ),
14334
+ home
14335
+ ] });
14336
+ break;
14337
+ }
13834
14338
  title = "You lost your connection to the meeting";
13835
14339
  line = connection === "offline" ? "You're offline. You'll rejoin automatically when your internet is back." : connection === "server_unreachable" ? "Can't reach the meeting server. You'll rejoin automatically when it answers." : failure?.remedy ?? "Check your internet connection, then rejoin.";
13836
14340
  notice = "disconnected";