@ai-matrx/meet 0.11.43 → 0.11.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,6 +48,7 @@ __export(meet_exports, {
48
48
  ChatIcon: () => ChatIcon,
49
49
  ChatPanel: () => ChatPanel,
50
50
  CheckIcon: () => CheckIcon,
51
+ ChevronUpIcon: () => ChevronUpIcon,
51
52
  CloseIcon: () => CloseIcon,
52
53
  ConnectionDot: () => ConnectionDot,
53
54
  ConsentNotice: () => ConsentNotice,
@@ -390,11 +391,13 @@ function minutesLeft(policy, startedAtMs, nowMs) {
390
391
  }
391
392
 
392
393
  // src/core/attendance.ts
394
+ var IDLE_PREVIEW = { state: "idle", unavailable: null };
393
395
  var IDLE_MEDIA_REGION = {
394
396
  permission: { microphone: "unknown", camera: "unknown" },
395
397
  audioOutput: "ok",
396
398
  notice: null,
397
- offer: null
399
+ offer: null,
400
+ preview: { microphone: IDLE_PREVIEW, camera: IDLE_PREVIEW }
398
401
  };
399
402
  var IDLE_TRACK = { enabled: false, unavailable: null };
400
403
  var IDLE_MEDIA = {
@@ -615,7 +618,7 @@ function deviceFixSteps(kind, reason, where) {
615
618
  case "safari":
616
619
  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"];
617
620
  default:
618
- return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
621
+ return ["Click the site settings icon in the address bar", `Set ${device} to Allow`, "Press Try again"];
619
622
  }
620
623
  case "blocked_os": {
621
624
  const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
@@ -709,6 +712,21 @@ function observedDevice(track, permission = "unknown", held = null) {
709
712
  const reason = deviceUnavailableReason(track, permission);
710
713
  return reason === null ? "muted_by_me" : `unavailable:${reason}`;
711
714
  }
715
+ function observedPreview(preview, permission = "unknown") {
716
+ if (permission === "denied") return "unavailable:denied";
717
+ switch (preview.state) {
718
+ case "on":
719
+ return "on";
720
+ case "off":
721
+ return "muted_by_me";
722
+ case "prompting":
723
+ return "prompting";
724
+ case "unavailable":
725
+ return `unavailable:${preview.unavailable?.reason ?? "failed"}`;
726
+ default:
727
+ return null;
728
+ }
729
+ }
712
730
  function endedRecord(snapshot, given) {
713
731
  const record = snapshot?.meeting ?? given;
714
732
  return record !== null && record.endedAt !== null;
@@ -732,18 +750,22 @@ function observe(input) {
732
750
  const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
733
751
  const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
734
752
  const deviceInPlay = phase === "joining" || inCall;
753
+ const region = snapshot?.mediaRegion;
754
+ const atPrejoin = phase === "prejoin" && region !== void 0;
735
755
  return {
736
756
  phase,
737
757
  reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
738
758
  role,
739
- connection: snapshot?.connection ?? "stable",
759
+ // `net-blip`: in the call, a drop shorter than `reconnect_banner_delay_ms` is not something the
760
+ // person sees — the contract says what is on screen, so it reads `stable` until the banner shows.
761
+ connection: snapshot === null ? "stable" : inCall && snapshot.connection !== "stable" && !snapshot.connectionBanner ? "stable" : snapshot.connection,
740
762
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
741
763
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
742
764
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
743
765
  // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
744
766
  audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
745
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera, lockedFor(snapshot, "camera", inCall)) : null,
746
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone, lockedFor(snapshot, "microphone", inCall)) : null
767
+ 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,
768
+ 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
747
769
  };
748
770
  }
749
771
  function lockedFor(snapshot, kind, inCall) {
@@ -1267,7 +1289,8 @@ function useRoom() {
1267
1289
  failure: snapshot?.failure ?? null,
1268
1290
  secondDevice: snapshot?.secondDevice ?? null,
1269
1291
  local,
1270
- canShareScreen: engine?.supportsScreenShare() ?? true,
1292
+ // An observer (role `viewer`) publishes nothing: no Share screen to press and be refused (MD-16).
1293
+ canShareScreen: (engine?.supportsScreenShare() ?? true) && !isObserverRole(snapshot?.grants.role ?? null),
1271
1294
  join: (0, import_react5.useCallback)(
1272
1295
  async (request) => requireEngine(engine, "join").join(request),
1273
1296
  [engine]
@@ -1693,6 +1716,12 @@ function useMediaSupervisor() {
1693
1716
  retry: (0, import_react5.useCallback)(
1694
1717
  async (kind) => kind === "microphone" ? engine?.setMicrophoneEnabled(true) : engine?.setCameraEnabled(true),
1695
1718
  [engine]
1719
+ ),
1720
+ reportPreview: (0, import_react5.useCallback)(
1721
+ (kind, outcome) => {
1722
+ void engine?.previewDevice(kind, outcome);
1723
+ },
1724
+ [engine]
1696
1725
  )
1697
1726
  };
1698
1727
  }
@@ -2013,6 +2042,39 @@ function useCollabPanelRequest(onRequest) {
2013
2042
  // src/core/reactions.ts
2014
2043
  var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
2015
2044
 
2045
+ // src/core/speaking-detector.ts
2046
+ function createSpeakingWhileMutedDetector(options = {}) {
2047
+ const threshold = options.threshold ?? 0.06;
2048
+ const holdMs = options.holdMs ?? 600;
2049
+ const gapMs = options.gapMs ?? 400;
2050
+ const cooldownMs = options.cooldownMs ?? 2e4;
2051
+ let speechStart = null;
2052
+ let lastLoud = null;
2053
+ let firedAt = null;
2054
+ return {
2055
+ feed(level, at) {
2056
+ if (level >= threshold) {
2057
+ if (speechStart === null || lastLoud === null || at - lastLoud > gapMs) speechStart = at;
2058
+ lastLoud = at;
2059
+ } else if (lastLoud !== null && at - lastLoud > gapMs) {
2060
+ speechStart = null;
2061
+ lastLoud = null;
2062
+ }
2063
+ if (speechStart === null || at - speechStart < holdMs) return false;
2064
+ if (firedAt !== null && at - firedAt < cooldownMs) return false;
2065
+ firedAt = at;
2066
+ speechStart = null;
2067
+ lastLoud = null;
2068
+ return true;
2069
+ },
2070
+ reset() {
2071
+ speechStart = null;
2072
+ lastLoud = null;
2073
+ firedAt = null;
2074
+ }
2075
+ };
2076
+ }
2077
+
2016
2078
  // src/react/facade.ts
2017
2079
  var NO_ACTIONS = {
2018
2080
  join: async () => void 0,
@@ -2328,6 +2390,9 @@ function UndoIcon(props) {
2328
2390
  /* @__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" })
2329
2391
  ] });
2330
2392
  }
2393
+ function ChevronUpIcon(props) {
2394
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Icon, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m6 15 6-6 6 6" }) });
2395
+ }
2331
2396
 
2332
2397
  // src/skins/meet/collab.tsx
2333
2398
  var import_jsx_runtime5 = require("react/jsx-runtime");
@@ -3585,6 +3650,7 @@ function ControlBar({ onLeave }) {
3585
3650
  ) }) }) : null,
3586
3651
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BreakoutBanner, {}),
3587
3652
  panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KnockNotice, { onView: () => setPanel("people") }) : null,
3653
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SpeakingWhileMutedNudge, {}),
3588
3654
  controls.notice !== null && panel !== "people" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Toast, { text: controls.notice.text, failed: controls.notice.failed, onDone: controls.clearNotice }) : null,
3589
3655
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__controls", children: [
3590
3656
  live.micLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -3596,6 +3662,7 @@ function ControlBar({ onLeave }) {
3596
3662
  children: room.local?.micEnabled === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, {})
3597
3663
  }
3598
3664
  ),
3665
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AudioSettingsControl, {}),
3599
3666
  live.cameraLocked ? null : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3600
3667
  ControlButton,
3601
3668
  {
@@ -3711,6 +3778,13 @@ function ControlBar({ onLeave }) {
3711
3778
  ]
3712
3779
  }
3713
3780
  ),
3781
+ !room.canShareScreen && room.local?.screenSharing !== true ? (
3782
+ // `share-unsupported-mobile`: the control is absent; the menu says where it lives.
3783
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__menu-note", role: "note", "data-meet-notice": "share-unsupported", children: [
3784
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ScreenShareIcon, { width: 16, height: 16 }),
3785
+ " Present from a computer"
3786
+ ] })
3787
+ ) : null,
3714
3788
  controls.isHost ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3715
3789
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-meet__menu-sep", role: "separator" }),
3716
3790
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -3985,6 +4059,94 @@ function Toast({ text, failed, onDone }) {
3985
4059
  /* @__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 }) })
3986
4060
  ] });
3987
4061
  }
4062
+ function AudioSettingsControl() {
4063
+ const host = useMeetHost();
4064
+ const devices = useDeviceSnapshot();
4065
+ const [open, setOpen] = (0, import_react10.useState)(false);
4066
+ const list = (devices?.devices.audioinput ?? []).filter((device) => device.deviceId.length > 0);
4067
+ if (host === null || list.length < 2) return null;
4068
+ const current = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
4069
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__anchor", children: [
4070
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4071
+ ControlButton,
4072
+ {
4073
+ label: "Audio settings",
4074
+ active: open,
4075
+ onClick: () => {
4076
+ if (!open) void host.devices.refresh();
4077
+ setOpen(!open);
4078
+ },
4079
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChevronUpIcon, {})
4080
+ }
4081
+ ),
4082
+ 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)(
4083
+ "button",
4084
+ {
4085
+ type: "button",
4086
+ role: "menuitemradio",
4087
+ "aria-checked": device.deviceId === current,
4088
+ className: "mx-meet__menu-item",
4089
+ onClick: () => {
4090
+ setOpen(false);
4091
+ host.devices.select("audioinput", device.deviceId);
4092
+ void host.engine.switchDevice("audioinput", device.deviceId).catch(() => void 0);
4093
+ },
4094
+ children: [
4095
+ 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" }),
4096
+ device.label.length > 0 ? device.label : "Microphone"
4097
+ ]
4098
+ },
4099
+ device.deviceId
4100
+ )) }) }) : null
4101
+ ] });
4102
+ }
4103
+ function SpeakingWhileMutedNudge() {
4104
+ const room = useRoom();
4105
+ const host = useMeetHost();
4106
+ const devices = useDeviceSnapshot();
4107
+ const microphone = useLocalMedia().microphone;
4108
+ const mutedByMe = host !== null && room.local != null && room.local.micEnabled !== true && microphone.unavailable === null;
4109
+ const mic = devices?.selected.audioinput ?? host?.devices.resolve("audioinput") ?? null;
4110
+ const stream = useMicrophoneStream(mutedByMe ? mic : "off");
4111
+ const level = useAudioLevel(stream);
4112
+ const latest = (0, import_react10.useRef)(0);
4113
+ latest.current = level;
4114
+ const [shown, setShown] = (0, import_react10.useState)(false);
4115
+ const [detector] = (0, import_react10.useState)(() => createSpeakingWhileMutedDetector());
4116
+ (0, import_react10.useEffect)(() => {
4117
+ if (!mutedByMe) {
4118
+ detector.reset();
4119
+ setShown(false);
4120
+ return void 0;
4121
+ }
4122
+ const timer = setInterval(() => {
4123
+ if (detector.feed(latest.current, Date.now())) setShown(true);
4124
+ }, 150);
4125
+ return () => clearInterval(timer);
4126
+ }, [mutedByMe, detector]);
4127
+ (0, import_react10.useEffect)(() => {
4128
+ if (!shown) return void 0;
4129
+ const timer = setTimeout(() => setShown(false), 8e3);
4130
+ return () => clearTimeout(timer);
4131
+ }, [shown]);
4132
+ if (!shown || !mutedByMe) return null;
4133
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__notice-bar", role: "status", "data-meet-notice": "speaking-while-muted", children: [
4134
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 16, height: 16 }),
4135
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "You're muted" }),
4136
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4137
+ "button",
4138
+ {
4139
+ type: "button",
4140
+ className: "mx-meet__primary mx-meet__primary--small",
4141
+ onClick: () => {
4142
+ setShown(false);
4143
+ void room.toggleMicrophone();
4144
+ },
4145
+ children: "Unmute"
4146
+ }
4147
+ )
4148
+ ] });
4149
+ }
3988
4150
  function MediaNotices() {
3989
4151
  const media = useLocalMedia();
3990
4152
  const { region, where, enableAudio, acceptOffer, dismissOffer, dismissNotice, retry } = useMediaSupervisor();
@@ -4834,14 +4996,20 @@ async function playTestSound(sinkId) {
4834
4996
  audio.pause();
4835
4997
  await context.close();
4836
4998
  }
4837
- function useMicrophoneStream(deviceId) {
4999
+ function useMicrophoneStream(deviceId, report, attempt = 0) {
4838
5000
  const [stream, setStream] = (0, import_react10.useState)(null);
5001
+ const reportRef = (0, import_react10.useRef)(report);
5002
+ reportRef.current = report;
4839
5003
  (0, import_react10.useEffect)(() => {
4840
- if (deviceId === "off") return void 0;
5004
+ if (deviceId === "off") {
5005
+ reportRef.current?.("off");
5006
+ return void 0;
5007
+ }
4841
5008
  let live = true;
4842
5009
  let opened = null;
4843
5010
  const media = globalThis.navigator?.mediaDevices;
4844
5011
  if (media === void 0) return void 0;
5012
+ reportRef.current?.("prompting");
4845
5013
  void media.getUserMedia({ audio: deviceId === null ? true : { deviceId: { exact: deviceId } } }).then((next) => {
4846
5014
  if (!live) {
4847
5015
  next.getTracks().forEach((track) => track.stop());
@@ -4849,15 +5017,49 @@ function useMicrophoneStream(deviceId) {
4849
5017
  }
4850
5018
  opened = next;
4851
5019
  setStream(next);
4852
- }).catch(() => setStream(null));
5020
+ reportRef.current?.("on");
5021
+ }).catch((thrown) => {
5022
+ setStream(null);
5023
+ if (live) reportRef.current?.({ error: thrown });
5024
+ });
4853
5025
  return () => {
4854
5026
  live = false;
4855
5027
  opened?.getTracks().forEach((track) => track.stop());
4856
5028
  setStream(null);
4857
5029
  };
4858
- }, [deviceId]);
5030
+ }, [deviceId, attempt]);
4859
5031
  return stream;
4860
5032
  }
5033
+ function PreviewStatus({ onRetry }) {
5034
+ const { region, where } = useMediaSupervisor();
5035
+ const [waited, setWaited] = (0, import_react10.useState)(false);
5036
+ const prompting = region.preview.microphone.state === "prompting" || region.preview.camera.state === "prompting";
5037
+ (0, import_react10.useEffect)(() => {
5038
+ if (!prompting) {
5039
+ setWaited(false);
5040
+ return void 0;
5041
+ }
5042
+ const timer = setTimeout(() => setWaited(true), 800);
5043
+ return () => clearTimeout(timer);
5044
+ }, [prompting]);
5045
+ const refused = ["microphone", "camera"].flatMap((kind) => {
5046
+ const preview = region.preview[kind];
5047
+ const reason = region.permission[kind] === "denied" ? "denied" : preview.state === "unavailable" ? preview.unavailable?.reason ?? "failed" : null;
5048
+ return reason === null ? [] : [{ kind, reason }];
5049
+ });
5050
+ if (refused.length === 0) {
5051
+ if (!prompting || !waited) return null;
5052
+ 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" });
5053
+ }
5054
+ const first = refused[0];
5055
+ const both = refused.length === 2 && refused[0].reason === refused[1].reason;
5056
+ const label = both ? deviceChipLabel("camera", first.reason).replace(/^Camera/, "Camera and mic") : refused.map((r) => deviceChipLabel(r.kind, r.reason)).join(" \xB7 ");
5057
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__warning", role: "status", "data-meet-notice": deviceNoticeCode(first.kind, first.reason), children: [
5058
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { children: label }),
5059
+ /* @__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)) }),
5060
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", variant: "outline", size: "sm", onClick: onRetry, children: "Try again" })
5061
+ ] });
5062
+ }
4861
5063
  function KnockNotice({ onView }) {
4862
5064
  const { knocker, others, admit, dismiss } = useKnockNotice();
4863
5065
  if (knocker === null) return null;
@@ -5187,8 +5389,15 @@ function PreJoinBody({
5187
5389
  const [joining, setJoining] = (0, import_react10.useState)(false);
5188
5390
  const camera = devices?.selected.videoinput ?? host.devices.resolve("videoinput");
5189
5391
  const mic = devices?.selected.audioinput ?? host.devices.resolve("audioinput");
5190
- const preview = useCameraPreview(mode === "video" ? camera : void 0, effects.effects);
5191
- const micStream = useMicrophoneStream(microphone ? mic : "off");
5392
+ const { reportPreview } = useMediaSupervisor();
5393
+ const [attempt, setAttempt] = (0, import_react10.useState)(0);
5394
+ const preview = useCameraPreview(
5395
+ mode === "video" ? camera : void 0,
5396
+ effects.effects,
5397
+ (outcome) => reportPreview("camera", outcome),
5398
+ attempt
5399
+ );
5400
+ const micStream = useMicrophoneStream(microphone ? mic : "off", (outcome) => reportPreview("microphone", outcome), attempt);
5192
5401
  const level = useAudioLevel(micStream);
5193
5402
  const [speakerTesting, setSpeakerTesting] = (0, import_react10.useState)(false);
5194
5403
  (0, import_react10.useEffect)(() => {
@@ -5232,6 +5441,7 @@ function PreJoinBody({
5232
5441
  ] }),
5233
5442
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__prejoin-panel", children: [
5234
5443
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h1", { className: "mx-meet__prejoin-title", children: "Ready to join?" }),
5444
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PreviewStatus, { onRetry: () => setAttempt((n) => n + 1) }),
5235
5445
  notice?.kind === "removed" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__warning", children: notice.text }) : null,
5236
5446
  devices?.labelsHidden === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__panel-note", children: [
5237
5447
  "Your browser is hiding device names until you allow camera and microphone access.",
@@ -5334,16 +5544,22 @@ function PreJoinBody({
5334
5544
  ] })
5335
5545
  ] });
5336
5546
  }
5337
- function useCameraPreview(deviceId, effects) {
5547
+ function useCameraPreview(deviceId, effects, report, attempt = 0) {
5548
+ const reportRef = (0, import_react10.useRef)(report);
5549
+ reportRef.current = report;
5338
5550
  const [preview, setPreview] = (0, import_react10.useState)(null);
5339
5551
  const [error, setError] = (0, import_react10.useState)(null);
5340
5552
  const current = (0, import_react10.useRef)(null);
5341
5553
  const initialEffect = (0, import_react10.useRef)(toVideoEffect(effects));
5342
5554
  initialEffect.current = toVideoEffect(effects);
5343
5555
  (0, import_react10.useEffect)(() => {
5344
- if (deviceId === void 0) return void 0;
5556
+ if (deviceId === void 0) {
5557
+ reportRef.current?.("off");
5558
+ return void 0;
5559
+ }
5345
5560
  let live = true;
5346
5561
  setError(null);
5562
+ reportRef.current?.("prompting");
5347
5563
  void createCameraPreview(deviceId, initialEffect.current).then((created) => {
5348
5564
  if (!live) {
5349
5565
  created.stop();
@@ -5351,8 +5567,10 @@ function useCameraPreview(deviceId, effects) {
5351
5567
  }
5352
5568
  current.current = created;
5353
5569
  setPreview(created);
5570
+ reportRef.current?.("on");
5354
5571
  }).catch((thrown) => {
5355
5572
  if (!live) return;
5573
+ reportRef.current?.({ error: thrown });
5356
5574
  const failure = thrown;
5357
5575
  const message = (failure.message ?? "").replace(/^meet\.[A-Za-z.()]+:\s*/, "");
5358
5576
  const sentence = message.length > 0 ? message.charAt(0).toUpperCase() + message.slice(1) : "";
@@ -5364,7 +5582,7 @@ function useCameraPreview(deviceId, effects) {
5364
5582
  current.current = null;
5365
5583
  setPreview(null);
5366
5584
  };
5367
- }, [deviceId]);
5585
+ }, [deviceId, attempt]);
5368
5586
  return {
5369
5587
  preview,
5370
5588
  error,
@@ -5504,6 +5722,14 @@ function MeetSkinRoom({
5504
5722
  }
5505
5723
  function ShareNotice() {
5506
5724
  const { share, actions } = useMeetCore();
5725
+ const [soundHintClosed, setSoundHintClosed] = (0, import_react10.useState)(null);
5726
+ if (share.state === "presenting" && share.audio === false && soundHintClosed !== share.at) {
5727
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-audio-missing", children: [
5728
+ "Sound not shared",
5729
+ /* @__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" }),
5730
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__link", onClick: () => setSoundHintClosed(share.at), children: "Dismiss" })
5731
+ ] });
5732
+ }
5507
5733
  if (share.state !== "refused") return null;
5508
5734
  const text = 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";
5509
5735
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": "share-unavailable", children: [
@@ -5616,6 +5842,27 @@ function ExitScreen({ onLeave }) {
5616
5842
  ] });
5617
5843
  break;
5618
5844
  case "disconnected":
5845
+ if (failure?.stage === "join" && (failure.reason ?? null) === null) {
5846
+ title = "Can't connect to the meeting";
5847
+ line = connection === "offline" ? "You're offline. Check your internet connection." : "Check your network. A VPN, firewall or proxy may be blocking the call.";
5848
+ notice = "cannot-connect";
5849
+ actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5850
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5851
+ import_design_system.Button,
5852
+ {
5853
+ type: "button",
5854
+ disabled: busy,
5855
+ onClick: () => {
5856
+ setBusy(true);
5857
+ void actions.rejoin().catch(() => void 0).finally(() => setBusy(false));
5858
+ },
5859
+ children: "Try again"
5860
+ }
5861
+ ),
5862
+ home
5863
+ ] });
5864
+ break;
5865
+ }
5619
5866
  title = "You lost your connection to the meeting";
5620
5867
  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.";
5621
5868
  notice = "disconnected";