@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.cjs CHANGED
@@ -50,6 +50,7 @@ __export(react_exports, {
50
50
  GUEST_RECORD_NOT_SHARED: () => GUEST_RECORD_NOT_SHARED,
51
51
  GuestRecordAccessControl: () => GuestRecordAccessControl,
52
52
  IDLE_MEDIA: () => IDLE_MEDIA,
53
+ IDLE_MEDIA_REGION: () => IDLE_MEDIA_REGION,
53
54
  IDLE_PHASES: () => IDLE_PHASES,
54
55
  IDLE_TRACK: () => IDLE_TRACK,
55
56
  IncomingCallCard: () => IncomingCallCard,
@@ -129,6 +130,7 @@ __export(react_exports, {
129
130
  captionSegment: () => captionSegment,
130
131
  captureHeldOf: () => captureHeldOf,
131
132
  changesPatch: () => changesPatch,
133
+ classifyDeviceFailure: () => classifyDeviceFailure,
132
134
  clockPosition: () => clockPosition,
133
135
  collabFromState: () => collabFromState,
134
136
  computeStage: () => computeStage,
@@ -140,6 +142,7 @@ __export(react_exports, {
140
142
  createDeviceManager: () => createDeviceManager,
141
143
  createInRoomClient: () => createInRoomClient,
142
144
  createLiveKitDriver: () => createLiveKitDriver,
145
+ createMediaSupervisor: () => createMediaSupervisor,
143
146
  createMeetAi: () => createMeetAi,
144
147
  createMeetApi: () => createMeetApi,
145
148
  createMeetRepository: () => createMeetRepository,
@@ -159,6 +162,12 @@ __export(react_exports, {
159
162
  createWebResumeStorage: () => createWebResumeStorage,
160
163
  createWebRoomTokenStorage: () => createWebRoomTokenStorage,
161
164
  defaultCaptionLanguage: () => defaultCaptionLanguage,
165
+ detectBrowser: () => detectBrowser,
166
+ detectOs: () => detectOs,
167
+ deviceChipLabel: () => deviceChipLabel,
168
+ deviceFixSteps: () => deviceFixSteps,
169
+ deviceNoticeCode: () => deviceNoticeCode,
170
+ deviceUnavailableReason: () => deviceUnavailableReason,
162
171
  displayNameFor: () => displayNameFor,
163
172
  fileDownloadPath: () => fileDownloadPath,
164
173
  focusFromRow: () => focusFromRow,
@@ -315,6 +324,7 @@ __export(react_exports, {
315
324
  useTrack: () => useTrack,
316
325
  userTopic: () => userTopic,
317
326
  wasRefused: () => wasRefused,
327
+ webMediaPlatform: () => webMediaPlatform,
318
328
  withItem: () => withItem,
319
329
  wrapUpStatus: () => wrapUpStatus
320
330
  });
@@ -2489,13 +2499,19 @@ function recordingLabel(state) {
2489
2499
  }
2490
2500
 
2491
2501
  // src/core/attendance.ts
2502
+ var IDLE_MEDIA_REGION = {
2503
+ permission: { microphone: "unknown", camera: "unknown" },
2504
+ audioOutput: "ok",
2505
+ notice: null,
2506
+ offer: null
2507
+ };
2492
2508
  var IDLE_TRACK = { enabled: false, unavailable: null };
2493
2509
  var IDLE_MEDIA = {
2494
2510
  microphone: IDLE_TRACK,
2495
2511
  camera: IDLE_TRACK
2496
2512
  };
2497
- function unavailableFrom(error) {
2498
- return { code: error.code, message: error.message, remedy: error.remedy };
2513
+ function unavailableFrom(error, reason) {
2514
+ return reason === void 0 ? { code: error.code, message: error.message, remedy: error.remedy } : { code: error.code, message: error.message, remedy: error.remedy, reason };
2499
2515
  }
2500
2516
  var LISTEN_ONLY_NOTICE = "You're attending without a microphone \u2014 others can't hear you";
2501
2517
  function isListenOnly(media) {
@@ -3943,6 +3959,7 @@ var EMPTY = {
3943
3959
  wrapUp: null,
3944
3960
  noteTakerRefusal: null,
3945
3961
  media: IDLE_MEDIA,
3962
+ mediaRegion: IDLE_MEDIA_REGION,
3946
3963
  failure: null,
3947
3964
  collab: EMPTY_COLLAB,
3948
3965
  breakoutRoomId: null
@@ -4160,7 +4177,7 @@ function createMeetStore(initial = {}) {
4160
4177
  },
4161
4178
  setLocalTrack(kind, next) {
4162
4179
  const current = state.media[kind];
4163
- if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.message === next.unavailable?.message) {
4180
+ if (current.enabled === next.enabled && current.unavailable?.code === next.unavailable?.code && current.unavailable?.reason === next.unavailable?.reason && current.unavailable?.message === next.unavailable?.message) {
4164
4181
  return;
4165
4182
  }
4166
4183
  commit({ ...state, media: { ...state.media, [kind]: next } });
@@ -4745,6 +4762,342 @@ function createMeetSessionIdentity(options) {
4745
4762
  });
4746
4763
  }
4747
4764
 
4765
+ // src/core/media-supervisor.ts
4766
+ function classifyDeviceFailure(error, permission) {
4767
+ if (permission === "denied") return "denied";
4768
+ switch (error.code) {
4769
+ case "permission-denied":
4770
+ if (/by system/i.test(error.message) || permission === "granted") return "blocked_os";
4771
+ return "denied";
4772
+ case "device-not-found":
4773
+ return "none";
4774
+ case "device-in-use":
4775
+ return "busy";
4776
+ case "insecure-context":
4777
+ return "blocked_site";
4778
+ default:
4779
+ return "failed";
4780
+ }
4781
+ }
4782
+ function deviceNoticeCode(kind, reason) {
4783
+ if (reason === "denied" || reason === "blocked_site" || reason === "blocked_os") {
4784
+ return kind === "microphone" ? "mic-blocked" : "camera-blocked";
4785
+ }
4786
+ return "device-lost";
4787
+ }
4788
+ function deviceUnavailableReason(track, permission) {
4789
+ if (track.enabled) return null;
4790
+ if (permission === "denied") return "denied";
4791
+ const refused = track.unavailable;
4792
+ if (refused === null) return null;
4793
+ return refused.reason ?? classifyDeviceFailure({ code: refused.code, message: refused.message }, permission);
4794
+ }
4795
+ function deviceChipLabel(kind, reason) {
4796
+ const what = kind === "microphone" ? "Mic" : "Camera";
4797
+ switch (reason) {
4798
+ case "denied":
4799
+ case "blocked_site":
4800
+ case "blocked_os":
4801
+ return `${what} blocked`;
4802
+ case "none":
4803
+ return kind === "microphone" ? "No microphone found" : "No camera found";
4804
+ case "busy":
4805
+ return `${what} in use by another app`;
4806
+ case "revoked":
4807
+ return `${what} access was turned off`;
4808
+ default:
4809
+ return `${what} didn't start`;
4810
+ }
4811
+ }
4812
+ function deviceFixSteps(kind, reason, where) {
4813
+ const device = kind === "microphone" ? "Microphone" : "Camera";
4814
+ const lower = device.toLowerCase();
4815
+ switch (reason) {
4816
+ case "denied":
4817
+ case "revoked":
4818
+ case "blocked_site":
4819
+ switch (where.browser) {
4820
+ case "firefox":
4821
+ return [`Click the crossed-out ${lower} icon in the address bar`, "Clear the Blocked setting", "Press Try again and choose Allow"];
4822
+ case "safari":
4823
+ 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"];
4824
+ default:
4825
+ return ["Click the icon left of the web address", `Set ${device} to Allow`, "Press Try again"];
4826
+ }
4827
+ case "blocked_os": {
4828
+ const app = where.browser === "chrome" ? "Google Chrome" : where.browser === "edge" ? "Microsoft Edge" : where.browser === "firefox" ? "Firefox" : "your browser";
4829
+ if (where.os === "mac") return [`System Settings \u203A Privacy & Security \u203A ${device}`, `Turn on ${app}`, "Reopen the browser if asked, then Try again"];
4830
+ if (where.os === "windows") return [`Settings \u203A Privacy & security \u203A ${device}`, `Turn on ${device} access for apps`, "Press Try again"];
4831
+ return [`Allow ${lower} access for ${app} in your system settings`, "Press Try again"];
4832
+ }
4833
+ case "busy":
4834
+ return [`Close other apps using the ${lower} (Zoom, Teams, another tab)`, "Press Try again"];
4835
+ case "none":
4836
+ return [`Connect a ${lower}`, "Press Try again"];
4837
+ default:
4838
+ return [`Pick another ${lower} in Settings`, "Press Try again"];
4839
+ }
4840
+ }
4841
+ function detectBrowser(userAgent) {
4842
+ if (/Edg\//.test(userAgent)) return "edge";
4843
+ if (/Firefox\//.test(userAgent)) return "firefox";
4844
+ if (/Chrome\/|Chromium\/|CriOS\//.test(userAgent)) return "chrome";
4845
+ if (/Safari\//.test(userAgent)) return "safari";
4846
+ return "other";
4847
+ }
4848
+ function detectOs(userAgent, maxTouchPoints = 0) {
4849
+ if (/iPhone|iPad|iPod/.test(userAgent)) return "ios";
4850
+ if (/Macintosh/.test(userAgent)) return maxTouchPoints > 1 ? "ios" : "mac";
4851
+ if (/Android/.test(userAgent)) return "android";
4852
+ if (/Windows/.test(userAgent)) return "windows";
4853
+ if (/Linux|CrOS/.test(userAgent)) return "linux";
4854
+ return "other";
4855
+ }
4856
+ function webMediaPlatform() {
4857
+ const scope = globalThis;
4858
+ const nav = scope.navigator;
4859
+ if (nav === void 0) return null;
4860
+ const ua = nav.userAgent ?? "";
4861
+ const reading = (state) => state === "granted" || state === "denied" || state === "prompt" ? state : "unknown";
4862
+ const query = async (kind) => {
4863
+ try {
4864
+ return await nav.permissions?.query({ name: kind }) ?? null;
4865
+ } catch {
4866
+ return null;
4867
+ }
4868
+ };
4869
+ return {
4870
+ browser: detectBrowser(ua),
4871
+ os: detectOs(ua, nav.maxTouchPoints ?? 0),
4872
+ async queryPermission(kind) {
4873
+ return reading((await query(kind))?.state);
4874
+ },
4875
+ watchPermission(kind, onChange) {
4876
+ let status = null;
4877
+ let stopped = false;
4878
+ void query(kind).then((found) => {
4879
+ if (stopped || found === null) return;
4880
+ status = found;
4881
+ found.onchange = () => onChange(reading(found.state));
4882
+ });
4883
+ return () => {
4884
+ stopped = true;
4885
+ if (status !== null) status.onchange = null;
4886
+ };
4887
+ },
4888
+ async listDevices() {
4889
+ try {
4890
+ return await nav.mediaDevices?.enumerateDevices() ?? [];
4891
+ } catch {
4892
+ return [];
4893
+ }
4894
+ },
4895
+ onVisible(callback) {
4896
+ const doc = scope.document;
4897
+ if (doc === void 0) return () => void 0;
4898
+ const handler = () => {
4899
+ if (doc.visibilityState === "visible") callback();
4900
+ };
4901
+ doc.addEventListener("visibilitychange", handler);
4902
+ return () => doc.removeEventListener("visibilitychange", handler);
4903
+ }
4904
+ };
4905
+ }
4906
+ var INPUT_KIND = { microphone: "audioinput", camera: "videoinput" };
4907
+ function isAlias(deviceId2) {
4908
+ return deviceId2 === "default" || deviceId2 === "communications" || deviceId2 === "";
4909
+ }
4910
+ function createMediaSupervisor(options) {
4911
+ const { store, platform } = options;
4912
+ let unsubscribers = [];
4913
+ let started = false;
4914
+ let noticeId = 0;
4915
+ let known = null;
4916
+ let inUse = { audioinput: null, videoinput: null };
4917
+ function region() {
4918
+ return store.snapshot().mediaRegion;
4919
+ }
4920
+ function setRegion(next) {
4921
+ const current = region();
4922
+ const merged = { ...current, ...next };
4923
+ 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) {
4924
+ return;
4925
+ }
4926
+ store.patch({ mediaRegion: merged });
4927
+ }
4928
+ function setPermission(kind, reading) {
4929
+ const current = region().permission;
4930
+ if (current[kind] === reading) return;
4931
+ setRegion({ permission: { ...current, [kind]: reading } });
4932
+ const track = store.snapshot().media[kind];
4933
+ if (reading === "granted" && !track.enabled && track.unavailable?.reason === "denied") {
4934
+ store.setLocalTrack(kind, {
4935
+ enabled: false,
4936
+ unavailable: { ...track.unavailable, reason: "failed", remedy: "Access is allowed now. Press Try again." }
4937
+ });
4938
+ }
4939
+ }
4940
+ async function snapshotDevices() {
4941
+ const list = platform === null ? [] : await platform.listDevices();
4942
+ const out = { audioinput: /* @__PURE__ */ new Map(), videoinput: /* @__PURE__ */ new Map() };
4943
+ for (const device of list) {
4944
+ if (device.kind === "audioinput" || device.kind === "videoinput") out[device.kind].set(device.deviceId, device.label);
4945
+ }
4946
+ return out;
4947
+ }
4948
+ function readInUse() {
4949
+ const driver = options.driver();
4950
+ if (driver === null) return;
4951
+ const active = driver.activeDevices();
4952
+ inUse = { audioinput: active.audioinput, videoinput: active.videoinput };
4953
+ }
4954
+ async function handleDevicesChanged() {
4955
+ if (platform === null) return;
4956
+ const before = known;
4957
+ const now = await snapshotDevices();
4958
+ known = now;
4959
+ const driver = options.driver();
4960
+ if (driver === null || before === null) {
4961
+ readInUse();
4962
+ return;
4963
+ }
4964
+ for (const kind of ["microphone", "camera"]) {
4965
+ const deviceKind = INPUT_KIND[kind];
4966
+ const previous = inUse[deviceKind];
4967
+ const track = store.snapshot().media[kind];
4968
+ const gone = previous !== null && !isAlias(previous) && before[deviceKind].has(previous) && !now[deviceKind].has(previous);
4969
+ if (gone) {
4970
+ const from = before[deviceKind].get(previous) ?? "";
4971
+ const remaining = [...now[deviceKind].entries()].filter(([id]) => !isAlias(id));
4972
+ if (remaining.length === 0) {
4973
+ store.setLocalTrack(kind, {
4974
+ enabled: false,
4975
+ unavailable: {
4976
+ code: "device-not-found",
4977
+ message: `meet.${kind}: the ${kind} was disconnected and no other is connected`,
4978
+ remedy: `Connect a ${kind === "microphone" ? "microphone" : "camera"}, then press Try again.`,
4979
+ reason: "none"
4980
+ }
4981
+ });
4982
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "lost", from, to: null } });
4983
+ continue;
4984
+ }
4985
+ if (kind === "camera") {
4986
+ const [nextId, nextLabel] = remaining[0];
4987
+ try {
4988
+ await driver.switchDevice("videoinput", nextId);
4989
+ if (track.enabled || track.unavailable !== null) {
4990
+ await driver.setCameraEnabled(true);
4991
+ store.setLocalTrack("camera", { enabled: true, unavailable: null });
4992
+ }
4993
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "switched", from, to: nextLabel } });
4994
+ } catch (error) {
4995
+ const refused = await unavailable(error, "camera", "meet.camera(fallback)");
4996
+ store.setLocalTrack("camera", { enabled: false, unavailable: refused });
4997
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "lost", from, to: null } });
4998
+ }
4999
+ } else {
5000
+ const active = driver.activeDevices().audioinput;
5001
+ const to = (active !== null ? now.audioinput.get(active) : void 0) ?? now.audioinput.get("default") ?? remaining[0][1];
5002
+ setRegion({ notice: { id: ++noticeId, kind, outcome: "switched", from, to } });
5003
+ }
5004
+ continue;
5005
+ }
5006
+ const added = [...now[deviceKind].entries()].find(([id]) => !isAlias(id) && !before[deviceKind].has(id));
5007
+ if (added !== void 0 && added[0] !== driver.activeDevices()[deviceKind]) {
5008
+ setRegion({ offer: { kind, deviceId: added[0], label: added[1] } });
5009
+ }
5010
+ }
5011
+ readInUse();
5012
+ }
5013
+ async function unavailable(error, kind, operation) {
5014
+ const normalized = normalizeDeviceError(error, operation);
5015
+ const permission = platform === null ? "unknown" : await platform.queryPermission(kind);
5016
+ if (permission !== "unknown") setPermission(kind, permission);
5017
+ return unavailableFrom(normalized, classifyDeviceFailure(normalized, permission));
5018
+ }
5019
+ return {
5020
+ start() {
5021
+ if (started || platform === null) return;
5022
+ started = true;
5023
+ for (const kind of ["microphone", "camera"]) {
5024
+ void platform.queryPermission(kind).then((reading) => setPermission(kind, reading));
5025
+ unsubscribers.push(platform.watchPermission(kind, (reading) => setPermission(kind, reading)));
5026
+ }
5027
+ unsubscribers.push(
5028
+ platform.onVisible(() => {
5029
+ const driver = options.driver();
5030
+ if (driver?.startAudio === void 0 || region().audioOutput !== "blocked") return;
5031
+ driver.startAudio().catch(() => void 0);
5032
+ })
5033
+ );
5034
+ },
5035
+ stop() {
5036
+ for (const off of unsubscribers) off();
5037
+ unsubscribers = [];
5038
+ started = false;
5039
+ },
5040
+ unavailable,
5041
+ onAudioPlayback(canPlay) {
5042
+ setRegion({ audioOutput: canPlay ? "ok" : "blocked" });
5043
+ },
5044
+ onDevicesChanged() {
5045
+ void handleDevicesChanged().catch((error) => {
5046
+ options.report("warn", `meet: device change could not be handled: ${String(error)}`, "Pick the device in Settings.");
5047
+ });
5048
+ },
5049
+ onDeviceError(error, deviceKind) {
5050
+ if (deviceKind === null) return;
5051
+ const kind = deviceKind === "audioinput" ? "microphone" : "camera";
5052
+ void unavailable(error, kind, `meet.${kind}`).then((refused) => {
5053
+ const track = store.snapshot().media[kind];
5054
+ if (track.enabled) return;
5055
+ store.setLocalTrack(kind, { enabled: false, unavailable: refused });
5056
+ });
5057
+ },
5058
+ onConnected() {
5059
+ void snapshotDevices().then((now) => {
5060
+ known = now;
5061
+ readInUse();
5062
+ });
5063
+ },
5064
+ onDisconnected() {
5065
+ known = null;
5066
+ inUse = { audioinput: null, videoinput: null };
5067
+ setRegion({ audioOutput: "ok", notice: null, offer: null });
5068
+ },
5069
+ async enableAudio() {
5070
+ const driver = options.driver();
5071
+ if (driver?.startAudio === void 0) return;
5072
+ try {
5073
+ await driver.startAudio();
5074
+ setRegion({ audioOutput: "ok" });
5075
+ } catch (error) {
5076
+ options.report("warn", `meet: sound could not be enabled: ${String(error)}`, "Click Enable sound again, or check the tab is not muted.");
5077
+ }
5078
+ },
5079
+ async acceptOffer() {
5080
+ const offer = region().offer;
5081
+ const driver = options.driver();
5082
+ setRegion({ offer: null });
5083
+ if (offer === null || driver === null) return;
5084
+ try {
5085
+ await driver.switchDevice(INPUT_KIND[offer.kind], offer.deviceId);
5086
+ readInUse();
5087
+ } catch (error) {
5088
+ const refused = await unavailable(error, offer.kind, "meet.switchDevice");
5089
+ options.report("warn", refused.message, refused.remedy);
5090
+ }
5091
+ },
5092
+ dismissOffer() {
5093
+ setRegion({ offer: null });
5094
+ },
5095
+ dismissNotice() {
5096
+ setRegion({ notice: null });
5097
+ }
5098
+ };
5099
+ }
5100
+
4748
5101
  // src/core/room-events.ts
4749
5102
  var RECORDING_STATES = [
4750
5103
  "idle",
@@ -5199,9 +5552,9 @@ function createRoomEngine(options) {
5199
5552
  const departures = /* @__PURE__ */ new Map();
5200
5553
  function requestAsNow() {
5201
5554
  if (lastRequest === null) return null;
5202
- const media = store.snapshot().media;
5555
+ const media2 = store.snapshot().media;
5203
5556
  if (store.snapshot().phase !== "in_call") return lastRequest;
5204
- return { ...lastRequest, mode: media.camera.enabled ? "video" : "audio", microphone: media.microphone.enabled };
5557
+ return { ...lastRequest, mode: media2.camera.enabled ? "video" : "audio", microphone: media2.microphone.enabled };
5205
5558
  }
5206
5559
  async function dropRoom() {
5207
5560
  const target = driver;
@@ -5212,6 +5565,7 @@ function createRoomEngine(options) {
5212
5565
  }
5213
5566
  store.setLocalTrack("microphone", IDLE_TRACK);
5214
5567
  store.setLocalTrack("camera", IDLE_TRACK);
5568
+ media.onDisconnected();
5215
5569
  store.patch({ share: NO_SHARE, connection: "stable" });
5216
5570
  clearGhosts();
5217
5571
  await target?.disconnect();
@@ -5277,7 +5631,22 @@ function createRoomEngine(options) {
5277
5631
  report("error", normalized.message, normalized.remedy);
5278
5632
  return normalized;
5279
5633
  }
5634
+ const media = createMediaSupervisor({
5635
+ store,
5636
+ platform: options.mediaPlatform === void 0 ? webMediaPlatform() : options.mediaPlatform,
5637
+ driver: () => driver,
5638
+ report
5639
+ });
5280
5640
  const handlers = {
5641
+ audioPlayback(canPlay) {
5642
+ media.onAudioPlayback(canPlay);
5643
+ },
5644
+ devicesChanged() {
5645
+ media.onDevicesChanged();
5646
+ },
5647
+ mediaDeviceError(error, kind) {
5648
+ media.onDeviceError(error, kind);
5649
+ },
5281
5650
  participants(list) {
5282
5651
  const joinedAt = new Date(now()).toISOString();
5283
5652
  const before = new Set(store.snapshot().participants.map((p) => p.identity));
@@ -5543,6 +5912,7 @@ function createRoomEngine(options) {
5543
5912
  store.dispatch({ type: "connected" });
5544
5913
  store.setLocalTrack("microphone", IDLE_TRACK);
5545
5914
  store.setLocalTrack("camera", IDLE_TRACK);
5915
+ media.start();
5546
5916
  const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
5547
5917
  store.patch({ effects });
5548
5918
  if (!effects.noiseSuppression || effects.background !== "none") {
@@ -5567,6 +5937,7 @@ function createRoomEngine(options) {
5567
5937
  if (request.microphone !== false) {
5568
5938
  await enableTrack(created, "microphone", true);
5569
5939
  }
5940
+ media.onConnected();
5570
5941
  }
5571
5942
  async function enableTrack(target, kind, enabled) {
5572
5943
  try {
@@ -5576,10 +5947,10 @@ function createRoomEngine(options) {
5576
5947
  store.setLocalTrack(kind, state);
5577
5948
  return state;
5578
5949
  } catch (error) {
5579
- const normalized = normalizeDeviceError(error, `meet.${kind}`);
5580
- const state = { enabled: false, unavailable: unavailableFrom(normalized) };
5950
+ const refused = await media.unavailable(error, kind, `meet.${kind}`);
5951
+ const state = { enabled: false, unavailable: refused };
5581
5952
  store.setLocalTrack(kind, state);
5582
- report("warn", normalized.message, normalized.remedy);
5953
+ report("warn", refused.message, refused.remedy);
5583
5954
  return state;
5584
5955
  }
5585
5956
  }
@@ -5888,6 +6259,10 @@ function createRoomEngine(options) {
5888
6259
  if (driver === null) return;
5889
6260
  await driver.switchDevice(kind, deviceId2);
5890
6261
  },
6262
+ enableAudio: () => media.enableAudio(),
6263
+ acceptDeviceOffer: () => media.acceptOffer(),
6264
+ dismissDeviceOffer: () => media.dismissOffer(),
6265
+ dismissDeviceNotice: () => media.dismissNotice(),
5891
6266
  pin(who) {
5892
6267
  store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
5893
6268
  },
@@ -5902,6 +6277,7 @@ function createRoomEngine(options) {
5902
6277
  async dispose() {
5903
6278
  disposed = true;
5904
6279
  supervisor.dispose();
6280
+ media.stop();
5905
6281
  clearGhosts();
5906
6282
  if (driver !== null) {
5907
6283
  const target = driver;
@@ -6335,6 +6711,12 @@ function createLiveKitDriver(handlers, options = {}) {
6335
6711
  roster();
6336
6712
  }).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) => {
6337
6713
  handlers.roomMetadata?.(metadata ?? "");
6714
+ }).on(RoomEvent.AudioPlaybackStatusChanged, (canPlay) => {
6715
+ handlers.audioPlayback?.(canPlay);
6716
+ }).on(RoomEvent.MediaDevicesChanged, () => {
6717
+ handlers.devicesChanged?.();
6718
+ }).on(RoomEvent.MediaDevicesError, (error, kind) => {
6719
+ handlers.mediaDeviceError?.(error, kind === "audioinput" || kind === "videoinput" ? kind : null);
6338
6720
  }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
6339
6721
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
6340
6722
  }).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
@@ -6431,6 +6813,7 @@ function createLiveKitDriver(handlers, options = {}) {
6431
6813
  }
6432
6814
  projectRoster?.();
6433
6815
  handlers.roomMetadata?.(target.metadata ?? "");
6816
+ handlers.audioPlayback?.(target.canPlaybackAudio);
6434
6817
  },
6435
6818
  async disconnect() {
6436
6819
  if (room === null) return;
@@ -6557,6 +6940,11 @@ function createLiveKitDriver(handlers, options = {}) {
6557
6940
  } catch (error) {
6558
6941
  throw normalizeDeviceError(error, "meet.switchDevice");
6559
6942
  }
6943
+ },
6944
+ async startAudio() {
6945
+ const target = room;
6946
+ if (target === null) return;
6947
+ await target.startAudio();
6560
6948
  }
6561
6949
  };
6562
6950
  }
@@ -6964,22 +7352,10 @@ function observedRole(role, standing) {
6964
7352
  if (role === "host" || role === "cohost") return role;
6965
7353
  return isLinkFollower(standing) ? "guest" : "participant";
6966
7354
  }
6967
- function observedDevice(track) {
7355
+ function observedDevice(track, permission = "unknown") {
6968
7356
  if (track.enabled) return "on";
6969
- const refused = track.unavailable;
6970
- if (refused === null) return "muted_by_me";
6971
- switch (refused.code) {
6972
- case "permission-denied":
6973
- return "unavailable:denied";
6974
- case "device-not-found":
6975
- return "unavailable:none";
6976
- case "device-in-use":
6977
- return "unavailable:busy";
6978
- case "insecure-context":
6979
- return "unavailable:blocked_site";
6980
- default:
6981
- return "unavailable:failed";
6982
- }
7357
+ const reason = deviceUnavailableReason(track, permission);
7358
+ return reason === null ? "muted_by_me" : `unavailable:${reason}`;
6983
7359
  }
6984
7360
  function endedRecord(snapshot, given) {
6985
7361
  const record = snapshot?.meeting ?? given;
@@ -7012,9 +7388,10 @@ function observe(input) {
7012
7388
  hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
7013
7389
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
7014
7390
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
7015
- audioBlocked: null,
7016
- camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera) : null,
7017
- microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone) : null
7391
+ // S6c: the autoplay region is known only inside the room (no remote audio exists elsewhere).
7392
+ audioBlocked: inCall && snapshot !== null ? snapshot.mediaRegion.audioOutput === "blocked" : null,
7393
+ camera: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.camera, snapshot.mediaRegion.permission.camera) : null,
7394
+ microphone: deviceInPlay && snapshot !== null ? observedDevice(snapshot.media.microphone, snapshot.mediaRegion.permission.microphone) : null
7018
7395
  };
7019
7396
  }
7020
7397
  function staticObservation(phase, reason = null) {
@@ -7079,10 +7456,10 @@ function participantAttributes(p) {
7079
7456
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
7080
7457
  return attrs;
7081
7458
  }
7082
- function microphoneNoticeCode(track) {
7083
- const refused = track.unavailable;
7084
- if (refused === null) return null;
7085
- return refused.code === "permission-denied" || refused.code === "insecure-context" ? "mic-blocked" : "device-lost";
7459
+ function microphoneNoticeCode(track, permission = "unknown") {
7460
+ const reason = deviceUnavailableReason(track, permission);
7461
+ if (reason === null) return null;
7462
+ return deviceNoticeCode("microphone", reason) === "mic-blocked" ? "mic-blocked" : "device-lost";
7086
7463
  }
7087
7464
 
7088
7465
  // src/react/root.tsx
@@ -8183,6 +8560,26 @@ function useConsentNotice() {
8183
8560
  if (agent === null) return null;
8184
8561
  return `${agent.displayName} is in this meeting: it is transcribing what is said and writing notes everyone here can read.`;
8185
8562
  }
8563
+ function useMediaSupervisor() {
8564
+ const host = useMeetHost();
8565
+ const snapshot = useMeetSnapshot();
8566
+ const engine = host?.engine ?? null;
8567
+ return {
8568
+ region: snapshot?.mediaRegion ?? IDLE_MEDIA_REGION,
8569
+ where: (0, import_react4.useMemo)(() => {
8570
+ const platform = webMediaPlatform();
8571
+ return { browser: platform?.browser ?? "other", os: platform?.os ?? "other" };
8572
+ }, []),
8573
+ enableAudio: (0, import_react4.useCallback)(async () => engine?.enableAudio(), [engine]),
8574
+ acceptOffer: (0, import_react4.useCallback)(async () => engine?.acceptDeviceOffer(), [engine]),
8575
+ dismissOffer: (0, import_react4.useCallback)(() => engine?.dismissDeviceOffer(), [engine]),
8576
+ dismissNotice: (0, import_react4.useCallback)(() => engine?.dismissDeviceNotice(), [engine]),
8577
+ retry: (0, import_react4.useCallback)(
8578
+ async (kind) => kind === "microphone" ? engine?.setMicrophoneEnabled(true) : engine?.setCameraEnabled(true),
8579
+ [engine]
8580
+ )
8581
+ };
8582
+ }
8186
8583
  function useLocalMedia() {
8187
8584
  const snapshot = useMeetSnapshot();
8188
8585
  return snapshot?.media ?? IDLE_MEDIA;
@@ -11031,12 +11428,83 @@ function Toast({ text: text3, failed, onDone }) {
11031
11428
  /* @__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 }) })
11032
11429
  ] });
11033
11430
  }
11431
+ function MediaNotices() {
11432
+ const media = useLocalMedia();
11433
+ const { region, where, enableAudio, acceptOffer, dismissOffer, dismissNotice, retry } = useMediaSupervisor();
11434
+ const [fixing, setFixing] = (0, import_react10.useState)(null);
11435
+ const chips = ["microphone", "camera"].flatMap((kind) => {
11436
+ const reason = deviceUnavailableReason(media[kind], region.permission[kind]);
11437
+ return reason === null ? [] : [{ kind, reason }];
11438
+ });
11439
+ const fixReason = fixing === null ? null : chips.find((chip) => chip.kind === fixing)?.reason ?? null;
11440
+ const notice = region.notice;
11441
+ const offer = region.offer;
11442
+ const name = (kind) => kind === "microphone" ? "Microphone" : "Camera";
11443
+ if (chips.length === 0 && region.audioOutput !== "blocked" && notice === null && offer === null) return null;
11444
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-notices", children: [
11445
+ region.audioOutput === "blocked" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-pill", role: "alert", "data-meet-notice": "audio-blocked", children: [
11446
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "Sound is paused in this tab" }),
11447
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void enableAudio(), children: "Enable sound" })
11448
+ ] }) : null,
11449
+ 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: [
11450
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: deviceChipLabel(kind, reason) }),
11451
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11452
+ "button",
11453
+ {
11454
+ type: "button",
11455
+ className: "mx-meet__primary mx-meet__primary--small",
11456
+ "aria-label": kind === "microphone" ? "Fix microphone" : "Fix camera",
11457
+ "aria-expanded": fixing === kind,
11458
+ onClick: () => setFixing(fixing === kind ? null : kind),
11459
+ children: "Fix"
11460
+ }
11461
+ )
11462
+ ] }, kind)),
11463
+ 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: [
11464
+ /* @__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)) }),
11465
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__fix-actions", children: [
11466
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
11467
+ "button",
11468
+ {
11469
+ type: "button",
11470
+ className: "mx-meet__primary mx-meet__primary--small",
11471
+ onClick: () => {
11472
+ const kind = fixing;
11473
+ setFixing(null);
11474
+ void retry(kind);
11475
+ },
11476
+ children: "Try again"
11477
+ }
11478
+ ),
11479
+ /* @__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 }) })
11480
+ ] })
11481
+ ] }) : null,
11482
+ notice !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
11483
+ "div",
11484
+ {
11485
+ className: "mx-meet__media-pill",
11486
+ role: "status",
11487
+ ...notice.outcome === "lost" ? { "data-meet-notice": "device-lost" } : {},
11488
+ children: [
11489
+ /* @__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` }),
11490
+ /* @__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 }) })
11491
+ ]
11492
+ }
11493
+ ) : null,
11494
+ offer !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mx-meet__media-pill", role: "status", children: [
11495
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: `New ${offer.kind}: ${offer.label || name(offer.kind)}` }),
11496
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "mx-meet__primary mx-meet__primary--small", onClick: () => void acceptOffer(), children: "Use it" }),
11497
+ /* @__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 }) })
11498
+ ] }) : null
11499
+ ] });
11500
+ }
11034
11501
  function MeetingOverlays() {
11035
11502
  const reactions = useReactions();
11036
11503
  const { notice, dismiss } = useModerationNotice();
11037
11504
  const room = useRoom();
11038
11505
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11039
11506
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ReactionOverlay, { reactions }),
11507
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MediaNotices, {}),
11040
11508
  notice !== null && notice.kind !== "removed" ? (
11041
11509
  // A host's request ("the host is asking you to unmute"), not an error:
11042
11510
  // an assertive status, so it is announced at once without being filed