@ai-matrx/meet 0.11.29 → 0.11.30

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.
@@ -330,6 +330,12 @@ function recordingLabel(state) {
330
330
  }
331
331
 
332
332
  // src/core/attendance.ts
333
+ var IDLE_MEDIA_REGION = {
334
+ permission: { microphone: "unknown", camera: "unknown" },
335
+ audioOutput: "ok",
336
+ notice: null,
337
+ offer: null
338
+ };
333
339
  var IDLE_TRACK = { enabled: false, unavailable: null };
334
340
  var IDLE_MEDIA = {
335
341
  microphone: IDLE_TRACK,
@@ -481,28 +487,158 @@ var REACTION_TTL_MS = 5e3;
481
487
  // src/react/root.tsx
482
488
  var import_react3 = require("react");
483
489
 
484
- // src/core/observation.ts
485
- function observedRole(role, standing) {
486
- if (role === "host" || role === "cohost") return role;
487
- return isLinkFollower(standing) ? "guest" : "participant";
488
- }
489
- function observedDevice(track) {
490
- if (track.enabled) return "on";
491
- const refused = track.unavailable;
492
- if (refused === null) return "muted_by_me";
493
- switch (refused.code) {
490
+ // src/core/media-supervisor.ts
491
+ function classifyDeviceFailure(error, permission) {
492
+ if (permission === "denied") return "denied";
493
+ switch (error.code) {
494
494
  case "permission-denied":
495
- return "unavailable:denied";
495
+ if (/by system/i.test(error.message) || permission === "granted") return "blocked_os";
496
+ return "denied";
496
497
  case "device-not-found":
497
- return "unavailable:none";
498
+ return "none";
498
499
  case "device-in-use":
499
- return "unavailable:busy";
500
+ return "busy";
500
501
  case "insecure-context":
501
- return "unavailable:blocked_site";
502
+ return "blocked_site";
503
+ default:
504
+ return "failed";
505
+ }
506
+ }
507
+ function deviceNoticeCode(kind, reason) {
508
+ if (reason === "denied" || reason === "blocked_site" || reason === "blocked_os") {
509
+ return kind === "microphone" ? "mic-blocked" : "camera-blocked";
510
+ }
511
+ return "device-lost";
512
+ }
513
+ function deviceUnavailableReason(track, permission) {
514
+ if (track.enabled) return null;
515
+ if (permission === "denied") return "denied";
516
+ const refused = track.unavailable;
517
+ if (refused === null) return null;
518
+ return refused.reason ?? classifyDeviceFailure({ code: refused.code, message: refused.message }, permission);
519
+ }
520
+ function deviceChipLabel(kind, reason) {
521
+ const what = kind === "microphone" ? "Mic" : "Camera";
522
+ switch (reason) {
523
+ case "denied":
524
+ case "blocked_site":
525
+ case "blocked_os":
526
+ return `${what} blocked`;
527
+ case "none":
528
+ return kind === "microphone" ? "No microphone found" : "No camera found";
529
+ case "busy":
530
+ return `${what} in use by another app`;
531
+ case "revoked":
532
+ return `${what} access was turned off`;
533
+ default:
534
+ return `${what} didn't start`;
535
+ }
536
+ }
537
+ function deviceFixSteps(kind, reason, where) {
538
+ const device = kind === "microphone" ? "Microphone" : "Camera";
539
+ const lower = device.toLowerCase();
540
+ switch (reason) {
541
+ case "denied":
542
+ case "revoked":
543
+ case "blocked_site":
544
+ switch (where.browser) {
545
+ case "firefox":
546
+ return [`Click the crossed-out ${lower} icon in the address bar`, "Clear the Blocked setting", "Press Try again and choose Allow"];
547
+ case "safari":
548
+ 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"];
549
+ default:
550
+ return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
551
+ }
552
+ case "blocked_os": {
553
+ const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
554
+ if (where.os === "mac") return [`System Settings \u203A Privacy & Security \u203A ${device}`, `Turn on ${app}`, "Reopen the browser if asked, then Try again"];
555
+ if (where.os === "windows") return [`Settings \u203A Privacy & security \u203A ${device}`, `Turn on ${device} access for apps`, "Press Try again"];
556
+ return [`Allow ${lower} access for ${app} in your system settings`, "Press Try again"];
557
+ }
558
+ case "busy":
559
+ return [`Close other apps using the ${lower} (Zoom, Teams, another tab)`, "Press Try again"];
560
+ case "none":
561
+ return [`Connect a ${lower}`, "Press Try again"];
502
562
  default:
503
- return "unavailable:failed";
563
+ return [`Pick another ${lower} in Settings`, "Press Try again"];
504
564
  }
505
565
  }
566
+ function detectBrowser(userAgent) {
567
+ if (/Edg\//.test(userAgent)) return "edge";
568
+ if (/Firefox\//.test(userAgent)) return "firefox";
569
+ if (/Chrome\/|Chromium\/|CriOS\//.test(userAgent)) return "chrome";
570
+ if (/Safari\//.test(userAgent)) return "safari";
571
+ return "other";
572
+ }
573
+ function detectOs(userAgent, maxTouchPoints = 0) {
574
+ if (/iPhone|iPad|iPod/.test(userAgent)) return "ios";
575
+ if (/Macintosh/.test(userAgent)) return maxTouchPoints > 1 ? "ios" : "mac";
576
+ if (/Android/.test(userAgent)) return "android";
577
+ if (/Windows/.test(userAgent)) return "windows";
578
+ if (/Linux|CrOS/.test(userAgent)) return "linux";
579
+ return "other";
580
+ }
581
+ function webMediaPlatform() {
582
+ const scope = globalThis;
583
+ const nav = scope.navigator;
584
+ if (nav === void 0) return null;
585
+ const ua = nav.userAgent ?? "";
586
+ const reading = (state) => state === "granted" || state === "denied" || state === "prompt" ? state : "unknown";
587
+ const query = async (kind) => {
588
+ try {
589
+ return await nav.permissions?.query({ name: kind }) ?? null;
590
+ } catch {
591
+ return null;
592
+ }
593
+ };
594
+ return {
595
+ browser: detectBrowser(ua),
596
+ os: detectOs(ua, nav.maxTouchPoints ?? 0),
597
+ async queryPermission(kind) {
598
+ return reading((await query(kind))?.state);
599
+ },
600
+ watchPermission(kind, onChange) {
601
+ let status = null;
602
+ let stopped = false;
603
+ void query(kind).then((found) => {
604
+ if (stopped || found === null) return;
605
+ status = found;
606
+ found.onchange = () => onChange(reading(found.state));
607
+ });
608
+ return () => {
609
+ stopped = true;
610
+ if (status !== null) status.onchange = null;
611
+ };
612
+ },
613
+ async listDevices() {
614
+ try {
615
+ return await nav.mediaDevices?.enumerateDevices() ?? [];
616
+ } catch {
617
+ return [];
618
+ }
619
+ },
620
+ onVisible(callback) {
621
+ const doc = scope.document;
622
+ if (doc === void 0) return () => void 0;
623
+ const handler = () => {
624
+ if (doc.visibilityState === "visible") callback();
625
+ };
626
+ doc.addEventListener("visibilitychange", handler);
627
+ return () => doc.removeEventListener("visibilitychange", handler);
628
+ }
629
+ };
630
+ }
631
+
632
+ // src/core/observation.ts
633
+ function observedRole(role, standing) {
634
+ if (role === "host" || role === "cohost") return role;
635
+ return isLinkFollower(standing) ? "guest" : "participant";
636
+ }
637
+ function observedDevice(track, permission = "unknown") {
638
+ if (track.enabled) return "on";
639
+ const reason = deviceUnavailableReason(track, permission);
640
+ return reason === null ? "muted_by_me" : `unavailable:${reason}`;
641
+ }
506
642
  function endedRecord(snapshot, given) {
507
643
  const record = snapshot?.meeting ?? given;
508
644
  return record !== null && record.endedAt !== null;
@@ -534,9 +670,10 @@ function observe(input) {
534
670
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
535
671
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
536
672
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
537
- audioBlocked: null,
538
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera) : null,
539
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone) : null
673
+ // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
674
+ audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
675
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
676
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
540
677
  };
541
678
  }
542
679
  function staticObservation(phase, reason = null) {
@@ -578,10 +715,10 @@ function participantAttributes(p) {
578
715
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
579
716
  return attrs;
580
717
  }
581
- function microphoneNoticeCode(track) {
582
- const refused = track.unavailable;
583
- if (refused === null) return null;
584
- return refused.code === "permission-denied" || refused.code === "insecure-context" ? "mic-blocked" : "device-lost";
718
+ function microphoneNoticeCode(track, permission = "unknown") {
719
+ const reason = deviceUnavailableReason(track, permission);
720
+ if (reason === null) return null;
721
+ return deviceNoticeCode("microphone", reason) === "mic-blocked" ? "mic-blocked" : "device-lost";
585
722
  }
586
723
 
587
724
  // src/react/provider.tsx
@@ -1358,6 +1495,26 @@ function useConsentNotice() {
1358
1495
  if (agent === null) return null;
1359
1496
  return `${agent.displayName} is in this meeting: it is transcribing what is said and writing notes everyone here can read.`;
1360
1497
  }
1498
+ function useMediaSupervisor() {
1499
+ const host = useMeetHost();
1500
+ const snapshot = useMeetSnapshot();
1501
+ const engine = host?.engine ?? null;
1502
+ return {
1503
+ region: snapshot?.mediaRegion ?? IDLE_MEDIA_REGION,
1504
+ where: (0, import_react5.useMemo)(() => {
1505
+ const platform = webMediaPlatform();
1506
+ return { browser: platform?.browser ?? "other", os: platform?.os ?? "other" };
1507
+ }, []),
1508
+ enableAudio: (0, import_react5.useCallback)(async () => engine?.enableAudio(), [engine]),
1509
+ acceptOffer: (0, import_react5.useCallback)(async () => engine?.acceptDeviceOffer(), [engine]),
1510
+ dismissOffer: (0, import_react5.useCallback)(() => engine?.dismissDeviceOffer(), [engine]),
1511
+ dismissNotice: (0, import_react5.useCallback)(() => engine?.dismissDeviceNotice(), [engine]),
1512
+ retry: (0, import_react5.useCallback)(
1513
+ async (kind) => kind === "microphone" ? engine?.setMicrophoneEnabled(true) : engine?.setCameraEnabled(true),
1514
+ [engine]
1515
+ )
1516
+ };
1517
+ }
1361
1518
  function useLocalMedia() {
1362
1519
  const snapshot = useMeetSnapshot();
1363
1520
  return snapshot?.media ?? IDLE_MEDIA;
@@ -3447,12 +3604,83 @@ function Toast({ text, failed, onDone }) {
3447
3604
  /* @__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 }) })
3448
3605
  ] });
3449
3606
  }
3607
+ function MediaNotices() {
3608
+ const media = useLocalMedia();
3609
+ const { region, where, enableAudio, acceptOffer, dismissOffer, dismissNotice, retry } = useMediaSupervisor();
3610
+ const [fixing, setFixing] = (0, import_react10.useState)(null);
3611
+ const chips = ["microphone", "camera"].flatMap((kind) => {
3612
+ const reason = deviceUnavailableReason(media[kind], region.permission[kind]);
3613
+ return reason === null ? [] : [{ kind, reason }];
3614
+ });
3615
+ const fixReason = fixing === null ? null : chips.find((chip) => chip.kind === fixing)?.reason ?? null;
3616
+ const notice = region.notice;
3617
+ const offer = region.offer;
3618
+ const name = (kind) => kind === "microphone" ? "Microphone" : "Camera";
3619
+ if (chips.length === 0 && region.audioOutput !== "blocked" && notice === null && offer === null) return null;
3620
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-notices", children: [
3621
+ region.audioOutput === "blocked" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-pill", role: "alert", "data-meet-notice": "audio-blocked", children: [
3622
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "Sound is paused in this tab" }),
3623
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void enableAudio(), children: "Enable sound" })
3624
+ ] }) : null,
3625
+ chips.map(({ kind, reason }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-pill", role: "status", "data-meet-notice": deviceNoticeCode(kind, reason), children: [
3626
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: deviceChipLabel(kind, reason) }),
3627
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3628
+ "button",
3629
+ {
3630
+ type: "button",
3631
+ className: "mx-meet__primary mx-meet__primary--small",
3632
+ "aria-label": kind === "microphone" ? "Fix microphone" : "Fix camera",
3633
+ "aria-expanded": fixing === kind,
3634
+ onClick: () => setFixing(fixing === kind ? null : kind),
3635
+ children: "Fix"
3636
+ }
3637
+ )
3638
+ ] }, kind)),
3639
+ fixing !== null && fixReason !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__popover mx-meet__fix-sheet", role: "dialog", "aria-label": `Fix ${name(fixing).toLowerCase()}`, children: [
3640
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ol", { className: "mx-meet__fix-steps", children: deviceFixSteps(fixing, fixReason, where).map((step) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("li", { children: step }, step)) }),
3641
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__fix-actions", children: [
3642
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3643
+ "button",
3644
+ {
3645
+ type: "button",
3646
+ className: "mx-meet__primary mx-meet__primary--small",
3647
+ onClick: () => {
3648
+ const kind = fixing;
3649
+ setFixing(null);
3650
+ void retry(kind);
3651
+ },
3652
+ children: "Try again"
3653
+ }
3654
+ ),
3655
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Close", onClick: () => setFixing(null), children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, { width: 14, height: 14 }) })
3656
+ ] })
3657
+ ] }) : null,
3658
+ notice !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3659
+ "div",
3660
+ {
3661
+ className: "mx-meet__media-pill",
3662
+ role: "status",
3663
+ ...notice.outcome === "lost" ? { "data-meet-notice": "device-lost" } : {},
3664
+ children: [
3665
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: notice.outcome === "switched" ? `${notice.from || name(notice.kind)} disconnected \xB7 now using ${notice.to || "the default"}` : `${notice.from || name(notice.kind)} disconnected` }),
3666
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismissNotice, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, { width: 14, height: 14 }) })
3667
+ ]
3668
+ }
3669
+ ) : null,
3670
+ offer !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-pill", role: "status", children: [
3671
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: `New ${offer.kind}: ${offer.label || name(offer.kind)}` }),
3672
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void acceptOffer(), children: "Use it" }),
3673
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismissOffer, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, { width: 14, height: 14 }) })
3674
+ ] }) : null
3675
+ ] });
3676
+ }
3450
3677
  function MeetingOverlays() {
3451
3678
  const reactions = useReactions();
3452
3679
  const { notice, dismiss } = useModerationNotice();
3453
3680
  const room = useRoom();
3454
3681
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3455
3682
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ReactionOverlay, { reactions }),
3683
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MediaNotices, {}),
3456
3684
  notice !== null && notice.kind !== "removed" ? (
3457
3685
  // A host's request ("the host is asking you to unmute"), not an error:
3458
3686
  // an assertive status, so it is announced at once without being filed