@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.
package/dist/react.js CHANGED
@@ -2183,13 +2183,19 @@ function recordingLabel(state) {
2183
2183
  }
2184
2184
 
2185
2185
  // src/core/attendance.ts
2186
+ var IDLE_MEDIA_REGION = {
2187
+ permission: { microphone: "unknown", camera: "unknown" },
2188
+ audioOutput: "ok",
2189
+ notice: null,
2190
+ offer: null
2191
+ };
2186
2192
  var IDLE_TRACK = { enabled: false, unavailable: null };
2187
2193
  var IDLE_MEDIA = {
2188
2194
  microphone: IDLE_TRACK,
2189
2195
  camera: IDLE_TRACK
2190
2196
  };
2191
- function unavailableFrom(error) {
2192
- return { code: error.code, message: error.message, remedy: error.remedy };
2197
+ function unavailableFrom(error, reason) {
2198
+ return reason === void 0 ? { code: error.code, message: error.message, remedy: error.remedy } : { code: error.code, message: error.message, remedy: error.remedy, reason };
2193
2199
  }
2194
2200
  var LISTEN_ONLY_NOTICE = "You're attending without a microphone \u2014 others can't hear you";
2195
2201
  function isListenOnly(media) {
@@ -3637,6 +3643,7 @@ var EMPTY = {
3637
3643
  wrapUp: null,
3638
3644
  noteTakerRefusal: null,
3639
3645
  media: IDLE_MEDIA,
3646
+ mediaRegion: IDLE_MEDIA_REGION,
3640
3647
  failure: null,
3641
3648
  collab: EMPTY_COLLAB,
3642
3649
  breakoutRoomId: null
@@ -3854,7 +3861,7 @@ function createMeetStore(initial = {}) {
3854
3861
  },
3855
3862
  setLocalTrack(kind, next) {
3856
3863
  const current = state.media[kind];
3857
- if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.message === next.unavailable?.message) {
3864
+ if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.reason === next.unavailable?.reason && current.unavailable?.message === next.unavailable?.message) {
3858
3865
  return;
3859
3866
  }
3860
3867
  commit({ ...state, media: { ...state.media, [kind]: next } });
@@ -4441,6 +4448,342 @@ function createMeetSessionIdentity(options) {
4441
4448
  });
4442
4449
  }
4443
4450
 
4451
+ // src/core/media-supervisor.ts
4452
+ function classifyDeviceFailure(error, permission) {
4453
+ if (permission === "denied") return "denied";
4454
+ switch (error.code) {
4455
+ case "permission-denied":
4456
+ if (/by system/i.test(error.message) || permission === "granted") return "blocked_os";
4457
+ return "denied";
4458
+ case "device-not-found":
4459
+ return "none";
4460
+ case "device-in-use":
4461
+ return "busy";
4462
+ case "insecure-context":
4463
+ return "blocked_site";
4464
+ default:
4465
+ return "failed";
4466
+ }
4467
+ }
4468
+ function deviceNoticeCode(kind, reason) {
4469
+ if (reason === "denied" || reason === "blocked_site" || reason === "blocked_os") {
4470
+ return kind === "microphone" ? "mic-blocked" : "camera-blocked";
4471
+ }
4472
+ return "device-lost";
4473
+ }
4474
+ function deviceUnavailableReason(track, permission) {
4475
+ if (track.enabled) return null;
4476
+ if (permission === "denied") return "denied";
4477
+ const refused = track.unavailable;
4478
+ if (refused === null) return null;
4479
+ return refused.reason ?? classifyDeviceFailure({ code: refused.code, message: refused.message }, permission);
4480
+ }
4481
+ function deviceChipLabel(kind, reason) {
4482
+ const what = kind === "microphone" ? "Mic" : "Camera";
4483
+ switch (reason) {
4484
+ case "denied":
4485
+ case "blocked_site":
4486
+ case "blocked_os":
4487
+ return `${what} blocked`;
4488
+ case "none":
4489
+ return kind === "microphone" ? "No microphone found" : "No camera found";
4490
+ case "busy":
4491
+ return `${what} in use by another app`;
4492
+ case "revoked":
4493
+ return `${what} access was turned off`;
4494
+ default:
4495
+ return `${what} didn't start`;
4496
+ }
4497
+ }
4498
+ function deviceFixSteps(kind, reason, where) {
4499
+ const device = kind === "microphone" ? "Microphone" : "Camera";
4500
+ const lower = device.toLowerCase();
4501
+ switch (reason) {
4502
+ case "denied":
4503
+ case "revoked":
4504
+ case "blocked_site":
4505
+ switch (where.browser) {
4506
+ case "firefox":
4507
+ return [`Click the crossed-out ${lower} icon in the address bar`, "Clear the Blocked setting", "Press Try again and choose Allow"];
4508
+ case "safari":
4509
+ 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"];
4510
+ default:
4511
+ return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
4512
+ }
4513
+ case "blocked_os": {
4514
+ const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
4515
+ if (where.os === "mac") return [`System Settings \u203A Privacy & Security \u203A ${device}`, `Turn on ${app}`, "Reopen the browser if asked, then Try again"];
4516
+ if (where.os === "windows") return [`Settings \u203A Privacy & security \u203A ${device}`, `Turn on ${device} access for apps`, "Press Try again"];
4517
+ return [`Allow ${lower} access for ${app} in your system settings`, "Press Try again"];
4518
+ }
4519
+ case "busy":
4520
+ return [`Close other apps using the ${lower} (Zoom, Teams, another tab)`, "Press Try again"];
4521
+ case "none":
4522
+ return [`Connect a ${lower}`, "Press Try again"];
4523
+ default:
4524
+ return [`Pick another ${lower} in Settings`, "Press Try again"];
4525
+ }
4526
+ }
4527
+ function detectBrowser(userAgent) {
4528
+ if (/Edg\//.test(userAgent)) return "edge";
4529
+ if (/Firefox\//.test(userAgent)) return "firefox";
4530
+ if (/Chrome\/|Chromium\/|CriOS\//.test(userAgent)) return "chrome";
4531
+ if (/Safari\//.test(userAgent)) return "safari";
4532
+ return "other";
4533
+ }
4534
+ function detectOs(userAgent, maxTouchPoints = 0) {
4535
+ if (/iPhone|iPad|iPod/.test(userAgent)) return "ios";
4536
+ if (/Macintosh/.test(userAgent)) return maxTouchPoints > 1 ? "ios" : "mac";
4537
+ if (/Android/.test(userAgent)) return "android";
4538
+ if (/Windows/.test(userAgent)) return "windows";
4539
+ if (/Linux|CrOS/.test(userAgent)) return "linux";
4540
+ return "other";
4541
+ }
4542
+ function webMediaPlatform() {
4543
+ const scope = globalThis;
4544
+ const nav = scope.navigator;
4545
+ if (nav === void 0) return null;
4546
+ const ua = nav.userAgent ?? "";
4547
+ const reading = (state) => state === "granted" || state === "denied" || state === "prompt" ? state : "unknown";
4548
+ const query = async (kind) => {
4549
+ try {
4550
+ return await nav.permissions?.query({ name: kind }) ?? null;
4551
+ } catch {
4552
+ return null;
4553
+ }
4554
+ };
4555
+ return {
4556
+ browser: detectBrowser(ua),
4557
+ os: detectOs(ua, nav.maxTouchPoints ?? 0),
4558
+ async queryPermission(kind) {
4559
+ return reading((await query(kind))?.state);
4560
+ },
4561
+ watchPermission(kind, onChange) {
4562
+ let status = null;
4563
+ let stopped = false;
4564
+ void query(kind).then((found) => {
4565
+ if (stopped || found === null) return;
4566
+ status = found;
4567
+ found.onchange = () => onChange(reading(found.state));
4568
+ });
4569
+ return () => {
4570
+ stopped = true;
4571
+ if (status !== null) status.onchange = null;
4572
+ };
4573
+ },
4574
+ async listDevices() {
4575
+ try {
4576
+ return await nav.mediaDevices?.enumerateDevices() ?? [];
4577
+ } catch {
4578
+ return [];
4579
+ }
4580
+ },
4581
+ onVisible(callback) {
4582
+ const doc = scope.document;
4583
+ if (doc === void 0) return () => void 0;
4584
+ const handler = () => {
4585
+ if (doc.visibilityState === "visible") callback();
4586
+ };
4587
+ doc.addEventListener("visibilitychange", handler);
4588
+ return () => doc.removeEventListener("visibilitychange", handler);
4589
+ }
4590
+ };
4591
+ }
4592
+ var INPUT_KIND = { microphone: "audioinput", camera: "videoinput" };
4593
+ function isAlias(deviceId2) {
4594
+ return deviceId2 === "default" || deviceId2 === "communications" || deviceId2 === "";
4595
+ }
4596
+ function createMediaSupervisor(options) {
4597
+ const { store, platform } = options;
4598
+ let unsubscribers = [];
4599
+ let started = false;
4600
+ let noticeId = 0;
4601
+ let known = null;
4602
+ let inUse = { audioinput: null, videoinput: null };
4603
+ function region() {
4604
+ return store.snapshot().mediaRegion;
4605
+ }
4606
+ function setRegion(next) {
4607
+ const current = region();
4608
+ const merged = { ...current, ...next };
4609
+ 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) {
4610
+ return;
4611
+ }
4612
+ store.patch({ mediaRegion: merged });
4613
+ }
4614
+ function setPermission(kind, reading) {
4615
+ const current = region().permission;
4616
+ if (current[kind] === reading) return;
4617
+ setRegion({ permission: { ...current, [kind]: reading } });
4618
+ const track = store.snapshot().media[kind];
4619
+ if (reading === "granted" && !track.enabled && track.unavailable?.reason === "denied") {
4620
+ store.setLocalTrack(kind, {
4621
+ enabled: false,
4622
+ unavailable: { ...track.unavailable, reason: "failed", remedy: "Access is allowed now. Press Try again." }
4623
+ });
4624
+ }
4625
+ }
4626
+ async function snapshotDevices() {
4627
+ const list = platform === null ? [] : await platform.listDevices();
4628
+ const out = { audioinput: /* @__PURE__ */ new Map(), videoinput: /* @__PURE__ */ new Map() };
4629
+ for (const device of list) {
4630
+ if (device.kind === "audioinput" || device.kind === "videoinput") out[device.kind].set(device.deviceId, device.label);
4631
+ }
4632
+ return out;
4633
+ }
4634
+ function readInUse() {
4635
+ const driver = options.driver();
4636
+ if (driver === null) return;
4637
+ const active = driver.activeDevices();
4638
+ inUse = { audioinput: active.audioinput, videoinput: active.videoinput };
4639
+ }
4640
+ async function handleDevicesChanged() {
4641
+ if (platform === null) return;
4642
+ const before = known;
4643
+ const now = await snapshotDevices();
4644
+ known = now;
4645
+ const driver = options.driver();
4646
+ if (driver === null || before === null) {
4647
+ readInUse();
4648
+ return;
4649
+ }
4650
+ for (const kind of ["microphone", "camera"]) {
4651
+ const deviceKind = INPUT_KIND[kind];
4652
+ const previous = inUse[deviceKind];
4653
+ const track = store.snapshot().media[kind];
4654
+ const gone = previous !== null && !isAlias(previous) && before[deviceKind].has(previous) && !now[deviceKind].has(previous);
4655
+ if (gone) {
4656
+ const from = before[deviceKind].get(previous) ?? "";
4657
+ const remaining = [...now[deviceKind].entries()].filter(([id]) => !isAlias(id));
4658
+ if (remaining.length === 0) {
4659
+ store.setLocalTrack(kind, {
4660
+ enabled: false,
4661
+ unavailable: {
4662
+ code: "device-not-found",
4663
+ message: `meet.${kind}: the ${kind} was disconnected and no other is connected`,
4664
+ remedy: `Connect a ${kind === "microphone" ? "microphone" : "camera"}, then press Try again.`,
4665
+ reason: "none"
4666
+ }
4667
+ });
4668
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "lost", from, to: null } });
4669
+ continue;
4670
+ }
4671
+ if (kind === "camera") {
4672
+ const [nextId, nextLabel] = remaining[0];
4673
+ try {
4674
+ await driver.switchDevice("videoinput", nextId);
4675
+ if (track.enabled || track.unavailable !== null) {
4676
+ await driver.setCameraEnabled(true);
4677
+ store.setLocalTrack("camera", { enabled: true, unavailable: null });
4678
+ }
4679
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "switched", from, to: nextLabel } });
4680
+ } catch (error) {
4681
+ const refused = await unavailable(error, "camera", "meet.camera(fallback)");
4682
+ store.setLocalTrack("camera", { enabled: false, unavailable: refused });
4683
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "lost", from, to: null } });
4684
+ }
4685
+ } else {
4686
+ const active = driver.activeDevices().audioinput;
4687
+ const to = (active !== null ? now.audioinput.get(active) : void 0) ?? now.audioinput.get("default") ?? remaining[0][1];
4688
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "switched", from, to } });
4689
+ }
4690
+ continue;
4691
+ }
4692
+ const added = [...now[deviceKind].entries()].find(([id]) => !isAlias(id) && !before[deviceKind].has(id));
4693
+ if (added !== void 0 && added[0] !== driver.activeDevices()[deviceKind]) {
4694
+ setRegion({ offer: { kind, deviceId: added[0], label: added[1] } });
4695
+ }
4696
+ }
4697
+ readInUse();
4698
+ }
4699
+ async function unavailable(error, kind, operation) {
4700
+ const normalized = normalizeDeviceError(error, operation);
4701
+ const permission = platform === null ? "unknown" : await platform.queryPermission(kind);
4702
+ if (permission !== "unknown") setPermission(kind, permission);
4703
+ return unavailableFrom(normalized, classifyDeviceFailure(normalized, permission));
4704
+ }
4705
+ return {
4706
+ start() {
4707
+ if (started || platform === null) return;
4708
+ started = true;
4709
+ for (const kind of ["microphone", "camera"]) {
4710
+ void platform.queryPermission(kind).then((reading) => setPermission(kind, reading));
4711
+ unsubscribers.push(platform.watchPermission(kind, (reading) => setPermission(kind, reading)));
4712
+ }
4713
+ unsubscribers.push(
4714
+ platform.onVisible(() => {
4715
+ const driver = options.driver();
4716
+ if (driver?.startAudio === void 0 || region().audioOutput !== "blocked") return;
4717
+ driver.startAudio().catch(() => void 0);
4718
+ })
4719
+ );
4720
+ },
4721
+ stop() {
4722
+ for (const off of unsubscribers) off();
4723
+ unsubscribers = [];
4724
+ started = false;
4725
+ },
4726
+ unavailable,
4727
+ onAudioPlayback(canPlay) {
4728
+ setRegion({ audioOutput: canPlay ? "ok" : "blocked" });
4729
+ },
4730
+ onDevicesChanged() {
4731
+ void handleDevicesChanged().catch((error) => {
4732
+ options.report("warn", `meet: device change could not be handled: ${String(error)}`, "Pick the device in Settings.");
4733
+ });
4734
+ },
4735
+ onDeviceError(error, deviceKind) {
4736
+ if (deviceKind === null) return;
4737
+ const kind = deviceKind === "audioinput" ? "microphone" : "camera";
4738
+ void unavailable(error, kind, `meet.${kind}`).then((refused) => {
4739
+ const track = store.snapshot().media[kind];
4740
+ if (track.enabled) return;
4741
+ store.setLocalTrack(kind, { enabled: false, unavailable: refused });
4742
+ });
4743
+ },
4744
+ onConnected() {
4745
+ void snapshotDevices().then((now) => {
4746
+ known = now;
4747
+ readInUse();
4748
+ });
4749
+ },
4750
+ onDisconnected() {
4751
+ known = null;
4752
+ inUse = { audioinput: null, videoinput: null };
4753
+ setRegion({ audioOutput: "ok", notice: null, offer: null });
4754
+ },
4755
+ async enableAudio() {
4756
+ const driver = options.driver();
4757
+ if (driver?.startAudio === void 0) return;
4758
+ try {
4759
+ await driver.startAudio();
4760
+ setRegion({ audioOutput: "ok" });
4761
+ } catch (error) {
4762
+ options.report("warn", `meet: sound could not be enabled: ${String(error)}`, "Click Enable sound again, or check the tab is not muted.");
4763
+ }
4764
+ },
4765
+ async acceptOffer() {
4766
+ const offer = region().offer;
4767
+ const driver = options.driver();
4768
+ setRegion({ offer: null });
4769
+ if (offer === null || driver === null) return;
4770
+ try {
4771
+ await driver.switchDevice(INPUT_KIND[offer.kind], offer.deviceId);
4772
+ readInUse();
4773
+ } catch (error) {
4774
+ const refused = await unavailable(error, offer.kind, "meet.switchDevice");
4775
+ options.report("warn", refused.message, refused.remedy);
4776
+ }
4777
+ },
4778
+ dismissOffer() {
4779
+ setRegion({ offer: null });
4780
+ },
4781
+ dismissNotice() {
4782
+ setRegion({ notice: null });
4783
+ }
4784
+ };
4785
+ }
4786
+
4444
4787
  // src/core/room-events.ts
4445
4788
  var RECORDING_STATES = [
4446
4789
  "idle",
@@ -4895,9 +5238,9 @@ function createRoomEngine(options) {
4895
5238
  const departures = /* @__PURE__ */ new Map();
4896
5239
  function requestAsNow() {
4897
5240
  if (lastRequest === null) return null;
4898
- const media = store.snapshot().media;
5241
+ const media2 = store.snapshot().media;
4899
5242
  if (store.snapshot().phase !== "in_call") return lastRequest;
4900
- return { ...lastRequest, mode: media.camera.enabled ? "video" : "audio", microphone: media.microphone.enabled };
5243
+ return { ...lastRequest, mode: media2.camera.enabled ? "video" : "audio", microphone: media2.microphone.enabled };
4901
5244
  }
4902
5245
  async function dropRoom() {
4903
5246
  const target = driver;
@@ -4908,6 +5251,7 @@ function createRoomEngine(options) {
4908
5251
  }
4909
5252
  store.setLocalTrack("microphone", IDLE_TRACK);
4910
5253
  store.setLocalTrack("camera", IDLE_TRACK);
5254
+ media.onDisconnected();
4911
5255
  store.patch({ share: NO_SHARE, connection: "stable" });
4912
5256
  clearGhosts();
4913
5257
  await target?.disconnect();
@@ -4973,7 +5317,22 @@ function createRoomEngine(options) {
4973
5317
  report("error", normalized.message, normalized.remedy);
4974
5318
  return normalized;
4975
5319
  }
5320
+ const media = createMediaSupervisor({
5321
+ store,
5322
+ platform: options.mediaPlatform === void 0 ? webMediaPlatform() : options.mediaPlatform,
5323
+ driver: () => driver,
5324
+ report
5325
+ });
4976
5326
  const handlers = {
5327
+ audioPlayback(canPlay) {
5328
+ media.onAudioPlayback(canPlay);
5329
+ },
5330
+ devicesChanged() {
5331
+ media.onDevicesChanged();
5332
+ },
5333
+ mediaDeviceError(error, kind) {
5334
+ media.onDeviceError(error, kind);
5335
+ },
4977
5336
  participants(list) {
4978
5337
  const joinedAt = new Date(now()).toISOString();
4979
5338
  const before = new Set(store.snapshot().participants.map((p) => p.identity));
@@ -5239,6 +5598,7 @@ function createRoomEngine(options) {
5239
5598
  store.dispatch({ type: "connected" });
5240
5599
  store.setLocalTrack("microphone", IDLE_TRACK);
5241
5600
  store.setLocalTrack("camera", IDLE_TRACK);
5601
+ media.start();
5242
5602
  const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
5243
5603
  store.patch({ effects });
5244
5604
  if (!effects.noiseSuppression || effects.background !== "none") {
@@ -5263,6 +5623,7 @@ function createRoomEngine(options) {
5263
5623
  if (request.microphone !== false) {
5264
5624
  await enableTrack(created, "microphone", true);
5265
5625
  }
5626
+ media.onConnected();
5266
5627
  }
5267
5628
  async function enableTrack(target, kind, enabled) {
5268
5629
  try {
@@ -5272,10 +5633,10 @@ function createRoomEngine(options) {
5272
5633
  store.setLocalTrack(kind, state);
5273
5634
  return state;
5274
5635
  } catch (error) {
5275
- const normalized = normalizeDeviceError(error, `meet.${kind}`);
5276
- const state = { enabled: false, unavailable: unavailableFrom(normalized) };
5636
+ const refused = await media.unavailable(error, kind, `meet.${kind}`);
5637
+ const state = { enabled: false, unavailable: refused };
5277
5638
  store.setLocalTrack(kind, state);
5278
- report("warn", normalized.message, normalized.remedy);
5639
+ report("warn", refused.message, refused.remedy);
5279
5640
  return state;
5280
5641
  }
5281
5642
  }
@@ -5584,6 +5945,10 @@ function createRoomEngine(options) {
5584
5945
  if (driver === null) return;
5585
5946
  await driver.switchDevice(kind, deviceId2);
5586
5947
  },
5948
+ enableAudio: () => media.enableAudio(),
5949
+ acceptDeviceOffer: () => media.acceptOffer(),
5950
+ dismissDeviceOffer: () => media.dismissOffer(),
5951
+ dismissDeviceNotice: () => media.dismissNotice(),
5587
5952
  pin(who) {
5588
5953
  store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
5589
5954
  },
@@ -5598,6 +5963,7 @@ function createRoomEngine(options) {
5598
5963
  async dispose() {
5599
5964
  disposed = true;
5600
5965
  supervisor.dispose();
5966
+ media.stop();
5601
5967
  clearGhosts();
5602
5968
  if (driver !== null) {
5603
5969
  const target = driver;
@@ -6031,6 +6397,12 @@ function createLiveKitDriver(handlers, options = {}) {
6031
6397
  roster();
6032
6398
  }).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) => {
6033
6399
  handlers.roomMetadata?.(metadata ?? "");
6400
+ }).on(RoomEvent.AudioPlaybackStatusChanged, (canPlay) => {
6401
+ handlers.audioPlayback?.(canPlay);
6402
+ }).on(RoomEvent.MediaDevicesChanged, () => {
6403
+ handlers.devicesChanged?.();
6404
+ }).on(RoomEvent.MediaDevicesError, (error, kind) => {
6405
+ handlers.mediaDeviceError?.(error, kind === "audioinput" || kind === "videoinput" ? kind : null);
6034
6406
  }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
6035
6407
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
6036
6408
  }).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
@@ -6127,6 +6499,7 @@ function createLiveKitDriver(handlers, options = {}) {
6127
6499
  }
6128
6500
  projectRoster?.();
6129
6501
  handlers.roomMetadata?.(target.metadata ?? "");
6502
+ handlers.audioPlayback?.(target.canPlaybackAudio);
6130
6503
  },
6131
6504
  async disconnect() {
6132
6505
  if (room === null) return;
@@ -6253,6 +6626,11 @@ function createLiveKitDriver(handlers, options = {}) {
6253
6626
  } catch (error) {
6254
6627
  throw normalizeDeviceError(error, "meet.switchDevice");
6255
6628
  }
6629
+ },
6630
+ async startAudio() {
6631
+ const target = room;
6632
+ if (target === null) return;
6633
+ await target.startAudio();
6256
6634
  }
6257
6635
  };
6258
6636
  }
@@ -6660,22 +7038,10 @@ function observedRole(role, standing) {
6660
7038
  if (role === "host" || role === "cohost") return role;
6661
7039
  return isLinkFollower(standing) ? "guest" : "participant";
6662
7040
  }
6663
- function observedDevice(track) {
7041
+ function observedDevice(track, permission = "unknown") {
6664
7042
  if (track.enabled) return "on";
6665
- const refused = track.unavailable;
6666
- if (refused === null) return "muted_by_me";
6667
- switch (refused.code) {
6668
- case "permission-denied":
6669
- return "unavailable:denied";
6670
- case "device-not-found":
6671
- return "unavailable:none";
6672
- case "device-in-use":
6673
- return "unavailable:busy";
6674
- case "insecure-context":
6675
- return "unavailable:blocked_site";
6676
- default:
6677
- return "unavailable:failed";
6678
- }
7043
+ const reason = deviceUnavailableReason(track, permission);
7044
+ return reason === null ? "muted_by_me" : `unavailable:${reason}`;
6679
7045
  }
6680
7046
  function endedRecord(snapshot, given) {
6681
7047
  const record = snapshot?.meeting ?? given;
@@ -6708,9 +7074,10 @@ function observe(input) {
6708
7074
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
6709
7075
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
6710
7076
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
6711
- audioBlocked: null,
6712
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera) : null,
6713
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone) : null
7077
+ // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
7078
+ audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
7079
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
7080
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
6714
7081
  };
6715
7082
  }
6716
7083
  function staticObservation(phase, reason = null) {
@@ -6775,10 +7142,10 @@ function participantAttributes(p) {
6775
7142
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
6776
7143
  return attrs;
6777
7144
  }
6778
- function microphoneNoticeCode(track) {
6779
- const refused = track.unavailable;
6780
- if (refused === null) return null;
6781
- return refused.code === "permission-denied" || refused.code === "insecure-context" ? "mic-blocked" : "device-lost";
7145
+ function microphoneNoticeCode(track, permission = "unknown") {
7146
+ const reason = deviceUnavailableReason(track, permission);
7147
+ if (reason === null) return null;
7148
+ return deviceNoticeCode("microphone", reason) === "mic-blocked" ? "mic-blocked" : "device-lost";
6782
7149
  }
6783
7150
 
6784
7151
  // src/react/root.tsx
@@ -7879,6 +8246,26 @@ function useConsentNotice() {
7879
8246
  if (agent === null) return null;
7880
8247
  return `${agent.displayName} is in this meeting: it is transcribing what is said and writing notes everyone here can read.`;
7881
8248
  }
8249
+ function useMediaSupervisor() {
8250
+ const host = useMeetHost();
8251
+ const snapshot = useMeetSnapshot();
8252
+ const engine = host?.engine ?? null;
8253
+ return {
8254
+ region: snapshot?.mediaRegion ?? IDLE_MEDIA_REGION,
8255
+ where: useMemo3(() => {
8256
+ const platform = webMediaPlatform();
8257
+ return { browser: platform?.browser ?? "other", os: platform?.os ?? "other" };
8258
+ }, []),
8259
+ enableAudio: useCallback2(async () => engine?.enableAudio(), [engine]),
8260
+ acceptOffer: useCallback2(async () => engine?.acceptDeviceOffer(), [engine]),
8261
+ dismissOffer: useCallback2(() => engine?.dismissDeviceOffer(), [engine]),
8262
+ dismissNotice: useCallback2(() => engine?.dismissDeviceNotice(), [engine]),
8263
+ retry: useCallback2(
8264
+ async (kind) => kind === "microphone" ? engine?.setMicrophoneEnabled(true) : engine?.setCameraEnabled(true),
8265
+ [engine]
8266
+ )
8267
+ };
8268
+ }
7882
8269
  function useLocalMedia() {
7883
8270
  const snapshot = useMeetSnapshot();
7884
8271
  return snapshot?.media ?? IDLE_MEDIA;
@@ -10727,12 +11114,83 @@ function Toast({ text: text3, failed, onDone }) {
10727
11114
  /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: onDone, children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
10728
11115
  ] });
10729
11116
  }
11117
+ function MediaNotices() {
11118
+ const media = useLocalMedia();
11119
+ const { region, where, enableAudio, acceptOffer, dismissOffer, dismissNotice, retry } = useMediaSupervisor();
11120
+ const [fixing, setFixing] = useState4(null);
11121
+ const chips = ["microphone", "camera"].flatMap((kind) => {
11122
+ const reason = deviceUnavailableReason(media[kind], region.permission[kind]);
11123
+ return reason === null ? [] : [{ kind, reason }];
11124
+ });
11125
+ const fixReason = fixing === null ? null : chips.find((chip) => chip.kind === fixing)?.reason ?? null;
11126
+ const notice = region.notice;
11127
+ const offer = region.offer;
11128
+ const name = (kind) => kind === "microphone" ? "Microphone" : "Camera";
11129
+ if (chips.length === 0 && region.audioOutput !== "blocked" && notice === null && offer === null) return null;
11130
+ return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-notices", children: [
11131
+ region.audioOutput === "blocked" ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-pill", role: "alert", "data-meet-notice": "audio-blocked", children: [
11132
+ /* @__PURE__ */ jsx6("span", { children: "Sound is paused in this tab" }),
11133
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void enableAudio(), children: "Enable sound" })
11134
+ ] }) : null,
11135
+ chips.map(({ kind, reason }) => /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-pill", role: "status", "data-meet-notice": deviceNoticeCode(kind, reason), children: [
11136
+ /* @__PURE__ */ jsx6("span", { children: deviceChipLabel(kind, reason) }),
11137
+ /* @__PURE__ */ jsx6(
11138
+ "button",
11139
+ {
11140
+ type: "button",
11141
+ className: "mx-meet__primary mx-meet__primary--small",
11142
+ "aria-label": kind === "microphone" ? "Fix microphone" : "Fix camera",
11143
+ "aria-expanded": fixing === kind,
11144
+ onClick: () => setFixing(fixing === kind ? null : kind),
11145
+ children: "Fix"
11146
+ }
11147
+ )
11148
+ ] }, kind)),
11149
+ fixing !== null && fixReason !== null ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__popover mx-meet__fix-sheet", role: "dialog", "aria-label": `Fix ${name(fixing).toLowerCase()}`, children: [
11150
+ /* @__PURE__ */ jsx6("ol", { className: "mx-meet__fix-steps", children: deviceFixSteps(fixing, fixReason, where).map((step) => /* @__PURE__ */ jsx6("li", { children: step }, step)) }),
11151
+ /* @__PURE__ */ jsxs3("div", { className: "mx-meet__fix-actions", children: [
11152
+ /* @__PURE__ */ jsx6(
11153
+ "button",
11154
+ {
11155
+ type: "button",
11156
+ className: "mx-meet__primary mx-meet__primary--small",
11157
+ onClick: () => {
11158
+ const kind = fixing;
11159
+ setFixing(null);
11160
+ void retry(kind);
11161
+ },
11162
+ children: "Try again"
11163
+ }
11164
+ ),
11165
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Close", onClick: () => setFixing(null), children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
11166
+ ] })
11167
+ ] }) : null,
11168
+ notice !== null ? /* @__PURE__ */ jsxs3(
11169
+ "div",
11170
+ {
11171
+ className: "mx-meet__media-pill",
11172
+ role: "status",
11173
+ ...notice.outcome === "lost" ? { "data-meet-notice": "device-lost" } : {},
11174
+ children: [
11175
+ /* @__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` }),
11176
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismissNotice, children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
11177
+ ]
11178
+ }
11179
+ ) : null,
11180
+ offer !== null ? /* @__PURE__ */ jsxs3("div", { className: "mx-meet__media-pill", role: "status", children: [
11181
+ /* @__PURE__ */ jsx6("span", { children: `New ${offer.kind}: ${offer.label || name(offer.kind)}` }),
11182
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void acceptOffer(), children: "Use it" }),
11183
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "mx-meet__toast-close", "aria-label": "Dismiss", onClick: dismissOffer, children: /* @__PURE__ */ jsx6(CloseIcon, { width: 14, height: 14 }) })
11184
+ ] }) : null
11185
+ ] });
11186
+ }
10730
11187
  function MeetingOverlays() {
10731
11188
  const reactions = useReactions();
10732
11189
  const { notice, dismiss } = useModerationNotice();
10733
11190
  const room = useRoom();
10734
11191
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
10735
11192
  /* @__PURE__ */ jsx6(ReactionOverlay, { reactions }),
11193
+ /* @__PURE__ */ jsx6(MediaNotices, {}),
10736
11194
  notice !== null && notice.kind !== "removed" ? (
10737
11195
  // A host's request ("the host is asking you to unmute"), not an error:
10738
11196
  // an assertive status, so it is announced at once without being filed
@@ -13011,6 +13469,7 @@ export {
13011
13469
  GUEST_RECORD_NOT_SHARED,
13012
13470
  GuestRecordAccessControl,
13013
13471
  IDLE_MEDIA,
13472
+ IDLE_MEDIA_REGION,
13014
13473
  IDLE_PHASES,
13015
13474
  IDLE_TRACK,
13016
13475
  IncomingCallCard,
@@ -13090,6 +13549,7 @@ export {
13090
13549
  captionSegment,
13091
13550
  captureHeldOf,
13092
13551
  changesPatch,
13552
+ classifyDeviceFailure,
13093
13553
  clockPosition,
13094
13554
  collabFromState,
13095
13555
  computeStage,
@@ -13101,6 +13561,7 @@ export {
13101
13561
  createDeviceManager,
13102
13562
  createInRoomClient,
13103
13563
  createLiveKitDriver,
13564
+ createMediaSupervisor,
13104
13565
  createMeetAi,
13105
13566
  createMeetApi,
13106
13567
  createMeetRepository,
@@ -13120,6 +13581,12 @@ export {
13120
13581
  createWebResumeStorage,
13121
13582
  createWebRoomTokenStorage,
13122
13583
  defaultCaptionLanguage,
13584
+ detectBrowser,
13585
+ detectOs,
13586
+ deviceChipLabel,
13587
+ deviceFixSteps,
13588
+ deviceNoticeCode,
13589
+ deviceUnavailableReason,
13123
13590
  displayNameFor,
13124
13591
  fileDownloadPath,
13125
13592
  focusFromRow,
@@ -13276,6 +13743,7 @@ export {
13276
13743
  useTrack,
13277
13744
  userTopic,
13278
13745
  wasRefused,
13746
+ webMediaPlatform,
13279
13747
  withItem,
13280
13748
  wrapUpStatus
13281
13749
  };