@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.
@@ -225,6 +225,12 @@ function recordingLabel(state) {
225
225
  }
226
226
 
227
227
  // src/core/attendance.ts
228
+ var IDLE_MEDIA_REGION = {
229
+ permission: { microphone: "unknown", camera: "unknown" },
230
+ audioOutput: "ok",
231
+ notice: null,
232
+ offer: null
233
+ };
228
234
  var IDLE_TRACK = { enabled: false, unavailable: null };
229
235
  var IDLE_MEDIA = {
230
236
  microphone: IDLE_TRACK,
@@ -376,28 +382,158 @@ var REACTION_TTL_MS = 5e3;
376
382
  // src/react/root.tsx
377
383
  import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from "react";
378
384
 
379
- // src/core/observation.ts
380
- function observedRole(role, standing) {
381
- if (role === "host" || role === "cohost") return role;
382
- return isLinkFollower(standing) ? "guest" : "participant";
383
- }
384
- function observedDevice(track) {
385
- if (track.enabled) return "on";
386
- const refused = track.unavailable;
387
- if (refused === null) return "muted_by_me";
388
- switch (refused.code) {
385
+ // src/core/media-supervisor.ts
386
+ function classifyDeviceFailure(error, permission) {
387
+ if (permission === "denied") return "denied";
388
+ switch (error.code) {
389
389
  case "permission-denied":
390
- return "unavailable:denied";
390
+ if (/by system/i.test(error.message) || permission === "granted") return "blocked_os";
391
+ return "denied";
391
392
  case "device-not-found":
392
- return "unavailable:none";
393
+ return "none";
393
394
  case "device-in-use":
394
- return "unavailable:busy";
395
+ return "busy";
395
396
  case "insecure-context":
396
- return "unavailable:blocked_site";
397
+ return "blocked_site";
398
+ default:
399
+ return "failed";
400
+ }
401
+ }
402
+ function deviceNoticeCode(kind, reason) {
403
+ if (reason === "denied" || reason === "blocked_site" || reason === "blocked_os") {
404
+ return kind === "microphone" ? "mic-blocked" : "camera-blocked";
405
+ }
406
+ return "device-lost";
407
+ }
408
+ function deviceUnavailableReason(track, permission) {
409
+ if (track.enabled) return null;
410
+ if (permission === "denied") return "denied";
411
+ const refused = track.unavailable;
412
+ if (refused === null) return null;
413
+ return refused.reason ?? classifyDeviceFailure({ code: refused.code, message: refused.message }, permission);
414
+ }
415
+ function deviceChipLabel(kind, reason) {
416
+ const what = kind === "microphone" ? "Mic" : "Camera";
417
+ switch (reason) {
418
+ case "denied":
419
+ case "blocked_site":
420
+ case "blocked_os":
421
+ return `${what} blocked`;
422
+ case "none":
423
+ return kind === "microphone" ? "No microphone found" : "No camera found";
424
+ case "busy":
425
+ return `${what} in use by another app`;
426
+ case "revoked":
427
+ return `${what} access was turned off`;
428
+ default:
429
+ return `${what} didn't start`;
430
+ }
431
+ }
432
+ function deviceFixSteps(kind, reason, where) {
433
+ const device = kind === "microphone" ? "Microphone" : "Camera";
434
+ const lower = device.toLowerCase();
435
+ switch (reason) {
436
+ case "denied":
437
+ case "revoked":
438
+ case "blocked_site":
439
+ switch (where.browser) {
440
+ case "firefox":
441
+ return [`Click the crossed-out ${lower} icon in the address bar`, "Clear the Blocked setting", "Press Try again and choose Allow"];
442
+ case "safari":
443
+ 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"];
444
+ default:
445
+ return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
446
+ }
447
+ case "blocked_os": {
448
+ const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
449
+ if (where.os === "mac") return [`System Settings \u203A Privacy & Security \u203A ${device}`, `Turn on ${app}`, "Reopen the browser if asked, then Try again"];
450
+ if (where.os === "windows") return [`Settings \u203A Privacy & security \u203A ${device}`, `Turn on ${device} access for apps`, "Press Try again"];
451
+ return [`Allow ${lower} access for ${app} in your system settings`, "Press Try again"];
452
+ }
453
+ case "busy":
454
+ return [`Close other apps using the ${lower} (Zoom, Teams, another tab)`, "Press Try again"];
455
+ case "none":
456
+ return [`Connect a ${lower}`, "Press Try again"];
397
457
  default:
398
- return "unavailable:failed";
458
+ return [`Pick another ${lower} in Settings`, "Press Try again"];
399
459
  }
400
460
  }
461
+ function detectBrowser(userAgent) {
462
+ if (/Edg\//.test(userAgent)) return "edge";
463
+ if (/Firefox\//.test(userAgent)) return "firefox";
464
+ if (/Chrome\/|Chromium\/|CriOS\//.test(userAgent)) return "chrome";
465
+ if (/Safari\//.test(userAgent)) return "safari";
466
+ return "other";
467
+ }
468
+ function detectOs(userAgent, maxTouchPoints = 0) {
469
+ if (/iPhone|iPad|iPod/.test(userAgent)) return "ios";
470
+ if (/Macintosh/.test(userAgent)) return maxTouchPoints > 1 ? "ios" : "mac";
471
+ if (/Android/.test(userAgent)) return "android";
472
+ if (/Windows/.test(userAgent)) return "windows";
473
+ if (/Linux|CrOS/.test(userAgent)) return "linux";
474
+ return "other";
475
+ }
476
+ function webMediaPlatform() {
477
+ const scope = globalThis;
478
+ const nav = scope.navigator;
479
+ if (nav === void 0) return null;
480
+ const ua = nav.userAgent ?? "";
481
+ const reading = (state) => state === "granted" || state === "denied" || state === "prompt" ? state : "unknown";
482
+ const query = async (kind) => {
483
+ try {
484
+ return await nav.permissions?.query({ name: kind }) ?? null;
485
+ } catch {
486
+ return null;
487
+ }
488
+ };
489
+ return {
490
+ browser: detectBrowser(ua),
491
+ os: detectOs(ua, nav.maxTouchPoints ?? 0),
492
+ async queryPermission(kind) {
493
+ return reading((await query(kind))?.state);
494
+ },
495
+ watchPermission(kind, onChange) {
496
+ let status = null;
497
+ let stopped = false;
498
+ void query(kind).then((found) => {
499
+ if (stopped || found === null) return;
500
+ status = found;
501
+ found.onchange = () => onChange(reading(found.state));
502
+ });
503
+ return () => {
504
+ stopped = true;
505
+ if (status !== null) status.onchange = null;
506
+ };
507
+ },
508
+ async listDevices() {
509
+ try {
510
+ return await nav.mediaDevices?.enumerateDevices() ?? [];
511
+ } catch {
512
+ return [];
513
+ }
514
+ },
515
+ onVisible(callback) {
516
+ const doc = scope.document;
517
+ if (doc === void 0) return () => void 0;
518
+ const handler = () => {
519
+ if (doc.visibilityState === "visible") callback();
520
+ };
521
+ doc.addEventListener("visibilitychange", handler);
522
+ return () => doc.removeEventListener("visibilitychange", handler);
523
+ }
524
+ };
525
+ }
526
+
527
+ // src/core/observation.ts
528
+ function observedRole(role, standing) {
529
+ if (role === "host" || role === "cohost") return role;
530
+ return isLinkFollower(standing) ? "guest" : "participant";
531
+ }
532
+ function observedDevice(track, permission = "unknown") {
533
+ if (track.enabled) return "on";
534
+ const reason = deviceUnavailableReason(track, permission);
535
+ return reason === null ? "muted_by_me" : `unavailable:${reason}`;
536
+ }
401
537
  function endedRecord(snapshot, given) {
402
538
  const record = snapshot?.meeting ?? given;
403
539
  return record !== null && record.endedAt !== null;
@@ -429,9 +565,10 @@ function observe(input) {
429
565
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
430
566
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
431
567
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
432
- audioBlocked: null,
433
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera) : null,
434
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone) : null
568
+ // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
569
+ audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
570
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
571
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
435
572
  };
436
573
  }
437
574
  function staticObservation(phase, reason = null) {
@@ -473,10 +610,10 @@ function participantAttributes(p) {
473
610
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
474
611
  return attrs;
475
612
  }
476
- function microphoneNoticeCode(track) {
477
- const refused = track.unavailable;
478
- if (refused === null) return null;
479
- return refused.code === "permission-denied" || refused.code === "insecure-context" ? "mic-blocked" : "device-lost";
613
+ function microphoneNoticeCode(track, permission = "unknown") {
614
+ const reason = deviceUnavailableReason(track, permission);
615
+ if (reason === null) return null;
616
+ return deviceNoticeCode("microphone", reason) === "mic-blocked" ? "mic-blocked" : "device-lost";
480
617
  }
481
618
 
482
619
  // src/react/provider.tsx
@@ -1266,6 +1403,26 @@ function useConsentNotice() {
1266
1403
  if (agent === null) return null;
1267
1404
  return `${agent.displayName} is in this meeting: it is transcribing what is said and writing notes everyone here can read.`;
1268
1405
  }
1406
+ function useMediaSupervisor() {
1407
+ const host = useMeetHost();
1408
+ const snapshot = useMeetSnapshot();
1409
+ const engine = host?.engine ?? null;
1410
+ return {
1411
+ region: snapshot?.mediaRegion ?? IDLE_MEDIA_REGION,
1412
+ where: useMemo4(() => {
1413
+ const platform = webMediaPlatform();
1414
+ return { browser: platform?.browser ?? "other", os: platform?.os ?? "other" };
1415
+ }, []),
1416
+ enableAudio: useCallback2(async () => engine?.enableAudio(), [engine]),
1417
+ acceptOffer: useCallback2(async () => engine?.acceptDeviceOffer(), [engine]),
1418
+ dismissOffer: useCallback2(() => engine?.dismissDeviceOffer(), [engine]),
1419
+ dismissNotice: useCallback2(() => engine?.dismissDeviceNotice(), [engine]),
1420
+ retry: useCallback2(
1421
+ async (kind) => kind === "microphone" ? engine?.setMicrophoneEnabled(true) : engine?.setCameraEnabled(true),
1422
+ [engine]
1423
+ )
1424
+ };
1425
+ }
1269
1426
  function useLocalMedia() {
1270
1427
  const snapshot = useMeetSnapshot();
1271
1428
  return snapshot?.media ?? IDLE_MEDIA;
@@ -3355,12 +3512,83 @@ function Toast({ text, failed, onDone }) {
3355
3512
  /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: onDone, children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
3356
3513
  ] });
3357
3514
  }
3515
+ function MediaNotices() {
3516
+ const media = useLocalMedia();
3517
+ const { region, where, enableAudio, acceptOffer, dismissOffer, dismissNotice, retry } = useMediaSupervisor();
3518
+ const [fixing, setFixing] = useState4(null);
3519
+ const chips = ["microphone", "camera"].flatMap((kind) => {
3520
+ const reason = deviceUnavailableReason(media[kind], region.permission[kind]);
3521
+ return reason === null ? [] : [{ kind, reason }];
3522
+ });
3523
+ const fixReason = fixing === null ? null : chips.find((chip) => chip.kind === fixing)?.reason ?? null;
3524
+ const notice = region.notice;
3525
+ const offer = region.offer;
3526
+ const name = (kind) => kind === "microphone" ? "Microphone" : "Camera";
3527
+ if (chips.length === 0 && region.audioOutput !== "blocked" && notice === null && offer === null) return null;
3528
+ return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-notices", children: [
3529
+ region.audioOutput === "blocked" ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-pill", role: "alert", "data-meet-notice": "audio-blocked", children: [
3530
+ /* @__PURE__ */ jsx6("span", { children: "Sound is paused in this tab" }),
3531
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void enableAudio(), children: "Enable sound" })
3532
+ ] }) : null,
3533
+ chips.map(({ kind, reason }) => /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-pill", role: "status", "data-meet-notice": deviceNoticeCode(kind, reason), children: [
3534
+ /* @__PURE__ */ jsx6("span", { children: deviceChipLabel(kind, reason) }),
3535
+ /* @__PURE__ */ jsx6(
3536
+ "button",
3537
+ {
3538
+ type: "button",
3539
+ className: "mx-meet__primary mx-meet__primary--small",
3540
+ "aria-label": kind === "microphone" ? "Fix microphone" : "Fix camera",
3541
+ "aria-expanded": fixing === kind,
3542
+ onClick: () => setFixing(fixing === kind ? null : kind),
3543
+ children: "Fix"
3544
+ }
3545
+ )
3546
+ ] }, kind)),
3547
+ fixing !== null && fixReason !== null ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__popover mx-meet__fix-sheet", role: "dialog", "aria-label": `Fix ${name(fixing).toLowerCase()}`, children: [
3548
+ /* @__PURE__ */ jsx6("ol", { className: "mx-meet__fix-steps", children: deviceFixSteps(fixing, fixReason, where).map((step) => /* @__PURE__ */ jsx6("li", { children: step }, step)) }),
3549
+ /* @__PURE__ */ jsxs3("div", { className: "mx-meet__fix-actions", children: [
3550
+ /* @__PURE__ */ jsx6(
3551
+ "button",
3552
+ {
3553
+ type: "button",
3554
+ className: "mx-meet__primary mx-meet__primary--small",
3555
+ onClick: () => {
3556
+ const kind = fixing;
3557
+ setFixing(null);
3558
+ void retry(kind);
3559
+ },
3560
+ children: "Try again"
3561
+ }
3562
+ ),
3563
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Close", onClick: () => setFixing(null), children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
3564
+ ] })
3565
+ ] }) : null,
3566
+ notice !== null ? /* @__PURE__ */ jsxs3(
3567
+ "div",
3568
+ {
3569
+ className: "mx-meet__media-pill",
3570
+ role: "status",
3571
+ ...notice.outcome === "lost" ? { "data-meet-notice": "device-lost" } : {},
3572
+ children: [
3573
+ /* @__PURE__ */ jsx6("span", { children: notice.outcome === "switched" ? `${notice.from || name(notice.kind)} disconnected \xB7 now using ${notice.to || "the default"}` : `${notice.from || name(notice.kind)} disconnected` }),
3574
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismissNotice, children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
3575
+ ]
3576
+ }
3577
+ ) : null,
3578
+ offer !== null ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-pill", role: "status", children: [
3579
+ /* @__PURE__ */ jsx6("span", { children: `New ${offer.kind}: ${offer.label || name(offer.kind)}` }),
3580
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void acceptOffer(), children: "Use it" }),
3581
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismissOffer, children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
3582
+ ] }) : null
3583
+ ] });
3584
+ }
3358
3585
  function MeetingOverlays() {
3359
3586
  const reactions = useReactions();
3360
3587
  const { notice, dismiss } = useModerationNotice();
3361
3588
  const room = useRoom();
3362
3589
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
3363
3590
  /* @__PURE__ */ jsx6(ReactionOverlay, { reactions }),
3591
+ /* @__PURE__ */ jsx6(MediaNotices, {}),
3364
3592
  notice !== null && notice.kind !== "removed" ? (
3365
3593
  // A host's request ("the host is asking you to unmute"), not an error:
3366
3594
  // an assertive status, so it is announced at once without being filed