@mentra/engine 3.2.0-dev.116 → 3.2.0-dev.121

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.
Files changed (84) hide show
  1. package/build/engine.d.ts.map +1 -1
  2. package/build/engine.js +7 -2
  3. package/build/engine.js.map +1 -1
  4. package/build/generated/releaseMetadata.js +5 -5
  5. package/build/generated/releaseMetadata.js.map +1 -1
  6. package/build/index.d.ts +1 -0
  7. package/build/index.d.ts.map +1 -1
  8. package/build/index.js +1 -0
  9. package/build/index.js.map +1 -1
  10. package/build/runtime/streamConfig.d.ts +7 -0
  11. package/build/runtime/streamConfig.d.ts.map +1 -1
  12. package/build/runtime/streamConfig.js +7 -0
  13. package/build/runtime/streamConfig.js.map +1 -1
  14. package/build/services/AcsMeetingService.d.ts +154 -0
  15. package/build/services/AcsMeetingService.d.ts.map +1 -0
  16. package/build/services/AcsMeetingService.js +477 -0
  17. package/build/services/AcsMeetingService.js.map +1 -0
  18. package/build/services/AppRegistry.d.ts +9 -0
  19. package/build/services/AppRegistry.d.ts.map +1 -1
  20. package/build/services/AppRegistry.js +22 -0
  21. package/build/services/AppRegistry.js.map +1 -1
  22. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  23. package/build/services/DeviceEventRouter.js +12 -3
  24. package/build/services/DeviceEventRouter.js.map +1 -1
  25. package/build/services/GlassesSettingsSync.d.ts.map +1 -1
  26. package/build/services/GlassesSettingsSync.js +11 -1
  27. package/build/services/GlassesSettingsSync.js.map +1 -1
  28. package/build/services/GlassesStatusProjection.d.ts +12 -0
  29. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  30. package/build/services/GlassesStatusProjection.js +23 -0
  31. package/build/services/GlassesStatusProjection.js.map +1 -1
  32. package/build/services/LocalMiniappRuntime.d.ts +12 -0
  33. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  34. package/build/services/LocalMiniappRuntime.js +184 -2
  35. package/build/services/LocalMiniappRuntime.js.map +1 -1
  36. package/build/services/MentraJSRouter.d.ts.map +1 -1
  37. package/build/services/MentraJSRouter.js +8 -1
  38. package/build/services/MentraJSRouter.js.map +1 -1
  39. package/build/services/MiniappLauncher.d.ts +3 -1
  40. package/build/services/MiniappLauncher.d.ts.map +1 -1
  41. package/build/services/MiniappLauncher.js +66 -50
  42. package/build/services/MiniappLauncher.js.map +1 -1
  43. package/build/services/PhoneCameraFovCoordinator.d.ts +18 -0
  44. package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
  45. package/build/services/PhoneCameraFovCoordinator.js +36 -0
  46. package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
  47. package/build/services/PhoneStreamCoordinator.d.ts +67 -1
  48. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  49. package/build/services/PhoneStreamCoordinator.js +204 -15
  50. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  51. package/build/services/StreamLifecycleController.d.ts +23 -5
  52. package/build/services/StreamLifecycleController.d.ts.map +1 -1
  53. package/build/services/StreamLifecycleController.js +31 -10
  54. package/build/services/StreamLifecycleController.js.map +1 -1
  55. package/build/services/acsAudioSource.d.ts +24 -0
  56. package/build/services/acsAudioSource.d.ts.map +1 -0
  57. package/build/services/acsAudioSource.js +37 -0
  58. package/build/services/acsAudioSource.js.map +1 -0
  59. package/build/utils/devMiniappLaunch.d.ts.map +1 -1
  60. package/build/utils/devMiniappLaunch.js +4 -0
  61. package/build/utils/devMiniappLaunch.js.map +1 -1
  62. package/build/utils/devMiniappSnapshot.d.ts +46 -0
  63. package/build/utils/devMiniappSnapshot.d.ts.map +1 -0
  64. package/build/utils/devMiniappSnapshot.js +94 -0
  65. package/build/utils/devMiniappSnapshot.js.map +1 -0
  66. package/package.json +7 -6
  67. package/src/engine.ts +10 -2
  68. package/src/generated/releaseMetadata.ts +5 -5
  69. package/src/index.ts +4 -0
  70. package/src/runtime/streamConfig.ts +13 -0
  71. package/src/services/AcsMeetingService.ts +570 -0
  72. package/src/services/AppRegistry.ts +23 -0
  73. package/src/services/DeviceEventRouter.ts +12 -3
  74. package/src/services/GlassesSettingsSync.ts +13 -3
  75. package/src/services/GlassesStatusProjection.ts +28 -0
  76. package/src/services/LocalMiniappRuntime.ts +214 -2
  77. package/src/services/MentraJSRouter.ts +7 -1
  78. package/src/services/MiniappLauncher.ts +73 -50
  79. package/src/services/PhoneCameraFovCoordinator.ts +34 -0
  80. package/src/services/PhoneStreamCoordinator.ts +232 -19
  81. package/src/services/StreamLifecycleController.ts +42 -12
  82. package/src/services/acsAudioSource.ts +53 -0
  83. package/src/utils/devMiniappLaunch.ts +4 -0
  84. package/src/utils/devMiniappSnapshot.ts +119 -0
@@ -0,0 +1,477 @@
1
+ /**
2
+ * Host-side ACS meeting controller. One active meeting. Native module owns
3
+ * WHEP + ACS; this service maps that into miniapp envelopes and pipes
4
+ * incoming PCM into AudioPlaybackService (A2DP / PcmStreamPlayer).
5
+ */
6
+ import audioPlaybackService from "./AudioPlaybackService";
7
+ import { SETTINGS, useSettingsStore } from "../stores/settings";
8
+ import { ACS_CALL_MIC } from "./acsAudioSource";
9
+ export { ACS_CALL_MIC };
10
+ /**
11
+ * The call microphone is [ACS_CALL_MIC]. Flip that constant to `"glasses"`
12
+ * to restore glasses WHIP → Cloudflare → WHEP PCM. Do not use ACS
13
+ * LocalOutgoingAudioStream for phone: that path makes ACS own the route
14
+ * (MODE_IN_COMMUNICATION + forced speaker) and opens an echo loop.
15
+ * preferred_mic governs MentraOS STT capture, not this call.
16
+ */
17
+ export function resolveAcsAudioSource() {
18
+ return { source: ACS_CALL_MIC, reason: "explicit" };
19
+ }
20
+ const PARTICIPANT_STATES = new Set([
21
+ "idle",
22
+ "connecting",
23
+ "connected",
24
+ "lobby",
25
+ "hold",
26
+ "disconnected",
27
+ ]);
28
+ export function parseMeetingParticipants(raw) {
29
+ if (!Array.isArray(raw))
30
+ return undefined;
31
+ const result = [];
32
+ for (const entry of raw) {
33
+ if (!entry || typeof entry !== "object")
34
+ continue;
35
+ const value = entry;
36
+ if (typeof value.id !== "string" || !value.id)
37
+ continue;
38
+ const state = PARTICIPANT_STATES.has(value.state)
39
+ ? value.state
40
+ : "idle";
41
+ result.push({
42
+ id: value.id,
43
+ displayName: typeof value.displayName === "string" && value.displayName ? value.displayName : null,
44
+ state,
45
+ isMuted: Boolean(value.isMuted),
46
+ isSpeaking: Boolean(value.isSpeaking),
47
+ });
48
+ }
49
+ return result;
50
+ }
51
+ /** ACS VirtualOutgoingVideoStream documented 16:9 sizes. P540 is 960×540, not 540×960. */
52
+ const ACS_VIRTUAL_CAMERA_SIZES = new Set(["1280x720", "960x540"]);
53
+ export function parseAcsOutgoingVideo(raw) {
54
+ if (raw == null)
55
+ return undefined;
56
+ if (typeof raw !== "object")
57
+ throw new Error("video must be an object");
58
+ const value = raw;
59
+ const width = Number(value.width);
60
+ const height = Number(value.height);
61
+ const fps = Number(value.fps);
62
+ const maxBitrateBps = Number(value.maxBitrateBps);
63
+ if (![width, height, fps, maxBitrateBps].every(Number.isFinite)) {
64
+ throw new Error("video requires width, height, fps, and maxBitrateBps");
65
+ }
66
+ if (!ACS_VIRTUAL_CAMERA_SIZES.has(`${width}x${height}`) || fps < 1 || fps > 30 || maxBitrateBps <= 0) {
67
+ throw new Error(`unsupported ACS video ${width}x${height}@${fps}`);
68
+ }
69
+ return { width, height, fps, maxBitrateBps };
70
+ }
71
+ let nativeModule;
72
+ function getNative() {
73
+ if (nativeModule !== undefined)
74
+ return nativeModule;
75
+ try {
76
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
77
+ nativeModule = require("@mentra/acs-meeting").default;
78
+ }
79
+ catch {
80
+ nativeModule = null;
81
+ }
82
+ return nativeModule;
83
+ }
84
+ /** Test seam: skip the native require and inject a fake ACS module. */
85
+ export function setAcsMeetingNativeForTests(mod) {
86
+ nativeModule = mod;
87
+ }
88
+ let phoneNetwork;
89
+ function getPhoneNetwork() {
90
+ if (phoneNetwork !== undefined)
91
+ return phoneNetwork;
92
+ try {
93
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
94
+ const netInfo = require("@react-native-community/netinfo");
95
+ phoneNetwork = netInfo.default ?? netInfo;
96
+ }
97
+ catch {
98
+ phoneNetwork = null;
99
+ }
100
+ return phoneNetwork;
101
+ }
102
+ /** Test seam: inject a fake phone-network feed (or `null` to disable it). */
103
+ export function setAcsMeetingPhoneNetworkForTests(source) {
104
+ phoneNetwork = source;
105
+ }
106
+ function parseActiveStream(value) {
107
+ if (value === "none" || value === "virtual" || value === "local")
108
+ return value;
109
+ return undefined;
110
+ }
111
+ function parseAudioSafety(value) {
112
+ if (value === "safe" || value === "degraded" || value === "unsafe")
113
+ return value;
114
+ return undefined;
115
+ }
116
+ function parseMediaSource(value) {
117
+ if (value === "idle" || value === "connecting" || value === "live" || value === "failed")
118
+ return value;
119
+ return undefined;
120
+ }
121
+ /** Meeting phases during which the WHEP feed should be alive and is worth restarting. */
122
+ const MEDIA_ACTIVE_PHASES = new Set(["connecting", "lobby", "connected"]);
123
+ /** Floor between WHEP restarts we trigger from the host, so a flapping network cannot thrash native. */
124
+ const MEDIA_RESTART_MIN_INTERVAL_MS = 3000;
125
+ /** Formats the native PcmStreamPlayer accepts (mono only). */
126
+ const PCM_SAMPLE_RATES = new Set([16000, 24000, 48000]);
127
+ const PCM_BACKLOG_WARN_MS = 600;
128
+ class AcsMeetingService {
129
+ owner = null;
130
+ pcmStreamId = null;
131
+ pcmFormat = null;
132
+ pcmReopen = null;
133
+ pcmWriteChain = Promise.resolve();
134
+ lastBacklogWarnAt = 0;
135
+ subscriptions = [];
136
+ lastState = { state: "idle", muted: false };
137
+ onState = null;
138
+ /** WHEP URL native is (or should be) subscribed to; what a host-triggered restart re-feeds. */
139
+ whepUrl = null;
140
+ phoneNetworkUnsub = null;
141
+ lastPhoneNetworkKey = null;
142
+ lastMediaRestartAt = 0;
143
+ setStateHandler(handler) {
144
+ this.onState = handler;
145
+ }
146
+ getState() {
147
+ return { ...this.lastState };
148
+ }
149
+ ownerPackage() {
150
+ return this.owner;
151
+ }
152
+ async join(packageName, args) {
153
+ const native = getNative();
154
+ if (!native) {
155
+ console.warn("[AcsMeeting] phase=join-native nativeLoaded=false");
156
+ throw new Error("ACS meeting module is not available on this host");
157
+ }
158
+ if (this.owner && this.owner !== packageName) {
159
+ throw new Error("Another miniapp already has an active meeting");
160
+ }
161
+ // Validate before claiming ownership so a bad request cannot leave the slot taken.
162
+ const video = args.video ? parseAcsOutgoingVideo(args.video) : undefined;
163
+ const resolved = resolveAcsAudioSource();
164
+ this.owner = packageName;
165
+ this.whepUrl = args.whepUrl;
166
+ this.bindNative(native, packageName);
167
+ console.log("[AcsMeeting] phase=join-native", {
168
+ packageName,
169
+ nativeLoaded: true,
170
+ hasToken: Boolean(args.token),
171
+ hasWhep: Boolean(args.whepUrl),
172
+ audioSource: resolved.source,
173
+ audioSourceReason: resolved.reason,
174
+ preferredMic: useSettingsStore.getState().getSetting(SETTINGS.preferred_mic.key),
175
+ });
176
+ try {
177
+ const state = await native.join({
178
+ meetingUrl: args.meetingUrl,
179
+ token: args.token,
180
+ whepUrl: args.whepUrl,
181
+ displayName: args.displayName,
182
+ audioSource: resolved.source,
183
+ ...(video ? { video } : {}),
184
+ });
185
+ this.lastState = {
186
+ ...state,
187
+ audioSource: resolved.source,
188
+ audioSourceReason: resolved.reason,
189
+ };
190
+ console.log("[AcsMeeting] phase=join-native-ok", { state: state.state, muted: state.muted });
191
+ }
192
+ catch (error) {
193
+ // Native never joined (or is unwinding). Release the slot so the same or another
194
+ // miniapp can retry, and make sure nothing half-joined lingers in Teams.
195
+ console.warn("[AcsMeeting] phase=join-native-failed", {
196
+ packageName,
197
+ error: error instanceof Error ? error.message : String(error),
198
+ });
199
+ await native.leave().catch((leaveError) => {
200
+ console.warn("[AcsMeeting] native leave after failed join also failed", leaveError);
201
+ });
202
+ await this.releaseHostState();
203
+ throw error;
204
+ }
205
+ this.watchPhoneNetwork(native);
206
+ // Return audio is additive: the wearer is already in the meeting with camera and
207
+ // mic. A playback failure must not reject the join — that would leave the miniapp
208
+ // believing it never joined while native keeps the wearer in Teams.
209
+ try {
210
+ await this.ensurePcmPlayback(packageName);
211
+ console.log("[AcsMeeting] phase=pcm-playback", { streamId: this.pcmStreamId });
212
+ }
213
+ catch (error) {
214
+ console.warn("[AcsMeeting] phase=pcm-playback-failed; continuing without return audio", {
215
+ error: error instanceof Error ? error.message : String(error),
216
+ });
217
+ }
218
+ return this.lastState;
219
+ }
220
+ async leave(packageName) {
221
+ if (this.owner && this.owner !== packageName)
222
+ return;
223
+ const native = getNative();
224
+ try {
225
+ await native?.leave();
226
+ }
227
+ finally {
228
+ await this.releaseHostState();
229
+ }
230
+ }
231
+ /**
232
+ * Drop everything the host set up around a session: the network watcher, return
233
+ * audio, native listeners, ownership. Runs after native has been told to leave
234
+ * (or after a join that never produced a native call).
235
+ */
236
+ async releaseHostState() {
237
+ this.unwatchPhoneNetwork();
238
+ await this.stopPcm();
239
+ this.unbindNative();
240
+ this.owner = null;
241
+ this.whepUrl = null;
242
+ this.lastMediaRestartAt = 0;
243
+ this.lastState = { state: "idle", muted: false };
244
+ }
245
+ async setMuted(packageName, muted) {
246
+ this.assertOwner(packageName);
247
+ const native = getNative();
248
+ if (!native)
249
+ throw new Error("ACS meeting module is not available on this host");
250
+ const state = await native.setMuted(muted);
251
+ this.lastState = {
252
+ ...this.lastState,
253
+ ...state,
254
+ audioSourceReason: this.lastState.audioSourceReason,
255
+ };
256
+ return this.lastState;
257
+ }
258
+ async updateVideoSource(packageName, whepUrl) {
259
+ this.assertOwner(packageName);
260
+ const native = getNative();
261
+ if (!native)
262
+ throw new Error("ACS meeting module is not available on this host");
263
+ this.whepUrl = whepUrl;
264
+ await native.updateVideoSource(whepUrl);
265
+ }
266
+ async readState(packageName) {
267
+ if (this.owner && this.owner !== packageName) {
268
+ return { state: "idle", muted: false };
269
+ }
270
+ const native = getNative();
271
+ if (!native)
272
+ return { state: "idle", muted: false };
273
+ const state = await native.getState();
274
+ this.lastState = state;
275
+ return state;
276
+ }
277
+ async leaveIfOwner(packageName) {
278
+ if (this.owner === packageName)
279
+ await this.leave(packageName);
280
+ }
281
+ assertOwner(packageName) {
282
+ if (this.owner !== packageName) {
283
+ throw new Error(this.owner ? "This miniapp does not own the active meeting" : "No active meeting; call meeting.join first");
284
+ }
285
+ }
286
+ unbindNative() {
287
+ this.subscriptions.forEach((sub) => sub.remove());
288
+ this.subscriptions = [];
289
+ }
290
+ bindNative(native, packageName) {
291
+ this.unbindNative();
292
+ this.subscriptions = [
293
+ native.addListener("onState", (event) => {
294
+ const audioSafety = parseAudioSafety(event.audioSafety);
295
+ if (audioSafety === "unsafe") {
296
+ console.error("[AcsMeeting] phase=audio-unsafe", {
297
+ activeStream: event.activeStream,
298
+ audioSource: event.audioSource,
299
+ });
300
+ }
301
+ const participants = parseMeetingParticipants(event.participants);
302
+ const mediaSource = parseMediaSource(event.mediaSource);
303
+ const state = {
304
+ state: event.state ?? "idle",
305
+ muted: Boolean(event.muted),
306
+ error: event.error,
307
+ meetingUrl: event.meetingUrl,
308
+ provider: "acs-teams",
309
+ audioSource: event.audioSource === "phone" ? "phone" : "glasses",
310
+ audioSourceReason: this.lastState.audioSourceReason,
311
+ activeStream: parseActiveStream(event.activeStream),
312
+ audioSafety,
313
+ ...(mediaSource ? { mediaSource } : {}),
314
+ ...(participants ? { participants } : {}),
315
+ };
316
+ this.lastState = state;
317
+ console.log("[AcsMeeting] phase=native-state", {
318
+ state: state.state,
319
+ muted: state.muted,
320
+ error: state.error,
321
+ audioSource: state.audioSource,
322
+ activeStream: state.activeStream,
323
+ audioSafety: state.audioSafety,
324
+ mediaSource: state.mediaSource,
325
+ participants: participants?.length,
326
+ });
327
+ this.onState?.(packageName, state);
328
+ }),
329
+ native.addListener("onIncomingPcm", (event) => {
330
+ const base64 = event.base64;
331
+ if (!base64 || !this.owner)
332
+ return;
333
+ // Serialize: native async calls may complete out of order, and PCM
334
+ // chunks written out of order are audible as garbling.
335
+ this.pcmWriteChain = this.pcmWriteChain
336
+ .then(() => this.writeIncomingPcm(packageName, base64, event.sampleRate, event.channels))
337
+ .catch(() => undefined);
338
+ }),
339
+ ];
340
+ }
341
+ /**
342
+ * The WHEP PeerConnection does not survive the phone changing networks
343
+ * (Wi-Fi↔cellular, or a different Wi-Fi): ICE fails and native parks the source
344
+ * as `failed` while the ACS call itself reconnects on its own. Nudge native to
345
+ * rebuild the subscription whenever the phone's network identity changes during
346
+ * a live meeting. Native ignores the request when the source is healthy on the
347
+ * same URL, so a spurious event costs nothing.
348
+ */
349
+ watchPhoneNetwork(native) {
350
+ this.unwatchPhoneNetwork();
351
+ const source = getPhoneNetwork();
352
+ if (!source)
353
+ return;
354
+ this.lastPhoneNetworkKey = null;
355
+ try {
356
+ this.phoneNetworkUnsub = source.addEventListener((info) => {
357
+ const key = `${info.type}:${info.isConnected === false ? "offline" : "online"}`;
358
+ const previous = this.lastPhoneNetworkKey;
359
+ this.lastPhoneNetworkKey = key;
360
+ // First callback is NetInfo replaying the current state, not a change.
361
+ if (previous === null || previous === key)
362
+ return;
363
+ if (info.isConnected === false)
364
+ return;
365
+ void this.restartMediaSource(native, `phone network ${previous} → ${key}`);
366
+ });
367
+ }
368
+ catch (error) {
369
+ console.warn("[AcsMeeting] phone network watch unavailable", error);
370
+ this.phoneNetworkUnsub = null;
371
+ }
372
+ }
373
+ unwatchPhoneNetwork() {
374
+ this.phoneNetworkUnsub?.();
375
+ this.phoneNetworkUnsub = null;
376
+ this.lastPhoneNetworkKey = null;
377
+ }
378
+ async restartMediaSource(native, reason) {
379
+ const whepUrl = this.whepUrl;
380
+ if (!this.owner || !whepUrl || !MEDIA_ACTIVE_PHASES.has(this.lastState.state))
381
+ return;
382
+ const now = Date.now();
383
+ if (now - this.lastMediaRestartAt < MEDIA_RESTART_MIN_INTERVAL_MS)
384
+ return;
385
+ this.lastMediaRestartAt = now;
386
+ console.log("[AcsMeeting] phase=media-restart", { reason, mediaSource: this.lastState.mediaSource });
387
+ try {
388
+ // A same-URL updateVideoSource is a no-op while native still believes the
389
+ // peer is healthy; after a network switch that belief is exactly what is wrong.
390
+ if (native.restartVideoSource)
391
+ await native.restartVideoSource();
392
+ else
393
+ await native.updateVideoSource(whepUrl);
394
+ }
395
+ catch (error) {
396
+ console.warn("[AcsMeeting] media restart failed", { reason, error });
397
+ }
398
+ }
399
+ /**
400
+ * Native normalizes to 16 kHz mono, so this is normally a straight write.
401
+ * If the format ever differs, reopen the player to match rather than play
402
+ * the bytes at the wrong rate (that is what slows and deepens the audio).
403
+ */
404
+ async writeIncomingPcm(packageName, base64, rawRate, rawChannels) {
405
+ const sampleRate = Number(rawRate) || 16000;
406
+ const channels = Number(rawChannels) || 1;
407
+ if (!PCM_SAMPLE_RATES.has(sampleRate) || channels !== 1) {
408
+ console.warn("[AcsMeeting] incoming PCM format unsupported; dropping chunk", { sampleRate, channels });
409
+ return;
410
+ }
411
+ if (this.pcmFormat && (this.pcmFormat.sampleRate !== sampleRate || this.pcmFormat.channels !== channels)) {
412
+ if (!this.pcmReopen) {
413
+ console.warn("[AcsMeeting] incoming PCM format changed; reopening player", {
414
+ from: this.pcmFormat,
415
+ to: { sampleRate, channels },
416
+ });
417
+ this.pcmReopen = this.stopPcm()
418
+ .then(() => this.ensurePcmPlayback(packageName, sampleRate, channels))
419
+ .finally(() => {
420
+ this.pcmReopen = null;
421
+ });
422
+ }
423
+ await this.pcmReopen;
424
+ }
425
+ else if (!this.pcmStreamId) {
426
+ await this.ensurePcmPlayback(packageName, sampleRate, channels);
427
+ }
428
+ const streamId = this.pcmStreamId;
429
+ if (!streamId)
430
+ return;
431
+ try {
432
+ const result = await audioPlaybackService.writeStreamChunk(streamId, base64);
433
+ const bufferedMs = result?.bufferedMs;
434
+ if (typeof bufferedMs === "number" && bufferedMs > PCM_BACKLOG_WARN_MS) {
435
+ const now = Date.now();
436
+ if (now - this.lastBacklogWarnAt > 5000) {
437
+ this.lastBacklogWarnAt = now;
438
+ console.warn("[AcsMeeting] incoming PCM backlog high", { bufferedMs });
439
+ }
440
+ }
441
+ }
442
+ catch (error) {
443
+ console.warn("[AcsMeeting] incoming PCM write failed", error);
444
+ }
445
+ }
446
+ async ensurePcmPlayback(packageName, sampleRate = 16000, channels = 1) {
447
+ if (this.pcmStreamId)
448
+ return;
449
+ const streamId = `acs-in-${packageName}-${Date.now()}`;
450
+ await audioPlaybackService.openStream({
451
+ streamId,
452
+ appId: packageName,
453
+ sampleRate,
454
+ channels,
455
+ stopOtherAudio: true,
456
+ onEnded: () => {
457
+ if (this.pcmStreamId === streamId) {
458
+ this.pcmStreamId = null;
459
+ this.pcmFormat = null;
460
+ }
461
+ },
462
+ });
463
+ this.pcmStreamId = streamId;
464
+ this.pcmFormat = { sampleRate, channels };
465
+ }
466
+ async stopPcm() {
467
+ const streamId = this.pcmStreamId;
468
+ this.pcmStreamId = null;
469
+ this.pcmFormat = null;
470
+ if (!streamId)
471
+ return;
472
+ await audioPlaybackService.abortStream(streamId).catch(() => undefined);
473
+ }
474
+ }
475
+ const acsMeetingService = new AcsMeetingService();
476
+ export default acsMeetingService;
477
+ //# sourceMappingURL=AcsMeetingService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AcsMeetingService.js","sourceRoot":"","sources":["../../src/services/AcsMeetingService.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,oBAAoB,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAC,QAAQ,EAAE,gBAAgB,EAAC,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EAAC,YAAY,EAA8C,MAAM,kBAAkB,CAAA;AAE1F,OAAO,EAAC,YAAY,EAAC,CAAA;AAsCrB;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,EAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAC,CAAA;AACnD,CAAC;AAED,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAA0B;IAC1D,MAAM;IACN,YAAY;IACZ,WAAW;IACX,OAAO;IACP,MAAM;IACN,cAAc;CACf,CAAC,CAAA;AAEF,MAAM,UAAU,wBAAwB,CAAC,GAAY;IACnD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IACzC,MAAM,MAAM,GAAyB,EAAE,CAAA;IACvC,KAAK,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QACxB,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,SAAQ;QACjD,MAAM,KAAK,GAAG,KAAgC,CAAA;QAC9C,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,EAAE;YAAE,SAAQ;QACvD,MAAM,KAAK,GAAG,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,KAAgC,CAAC;YAC1E,CAAC,CAAE,KAAK,CAAC,KAAiC;YAC1C,CAAC,CAAC,MAAM,CAAA;QACV,MAAM,CAAC,IAAI,CAAC;YACV,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,WAAW,EAAE,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI;YAClG,KAAK;YACL,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;YAC/B,UAAU,EAAE,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC;SACtC,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AASD,0FAA0F;AAC1F,MAAM,wBAAwB,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,CAAA;AAEjE,MAAM,UAAU,qBAAqB,CAAC,GAAY;IAChD,IAAI,GAAG,IAAI,IAAI;QAAE,OAAO,SAAS,CAAA;IACjC,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAA;IACvE,MAAM,KAAK,GAAG,GAA8B,CAAA;IAC5C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IACjC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IACnC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC7B,MAAM,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,CAAA;IACjD,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAA;IACzE,CAAC;IACD,IAAI,CAAC,wBAAwB,CAAC,GAAG,CAAC,GAAG,KAAK,IAAI,MAAM,EAAE,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,EAAE,IAAI,aAAa,IAAI,CAAC,EAAE,CAAC;QACrG,MAAM,IAAI,KAAK,CAAC,yBAAyB,KAAK,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC,CAAA;IACpE,CAAC;IACD,OAAO,EAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,EAAC,CAAA;AAC5C,CAAC;AAsBD,IAAI,YAA6C,CAAA;AAEjD,SAAS,SAAS;IAChB,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO,YAAY,CAAA;IACnD,IAAI,CAAC;QACH,iEAAiE;QACjE,YAAY,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC,OAAuB,CAAA;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,YAAY,GAAG,IAAI,CAAA;IACrB,CAAC;IACD,OAAO,YAAY,CAAA;AACrB,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,2BAA2B,CAAC,GAAoC;IAC9E,YAAY,GAAG,GAAG,CAAA;AACpB,CAAC;AAaD,IAAI,YAAmD,CAAA;AAEvD,SAAS,eAAe;IACtB,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO,YAAY,CAAA;IACnD,IAAI,CAAC;QACH,iEAAiE;QACjE,MAAM,OAAO,GAAG,OAAO,CAAC,iCAAiC,CAAwD,CAAA;QACjH,YAAY,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,CAAA;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,YAAY,GAAG,IAAI,CAAA;IACrB,CAAC;IACD,OAAO,YAAY,CAAA;AACrB,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,iCAAiC,CAAC,MAA6C;IAC7F,YAAY,GAAG,MAAM,CAAA;AACvB,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAc;IACvC,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,OAAO;QAAE,OAAO,KAAK,CAAA;IAC9E,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IACtC,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,UAAU,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAChF,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IACtC,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,YAAY,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IACtG,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,yFAAyF;AACzF,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAe,CAAC,YAAY,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAA;AACvF,wGAAwG;AACxG,MAAM,6BAA6B,GAAG,IAAI,CAAA;AAE1C,8DAA8D;AAC9D,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;AACvD,MAAM,mBAAmB,GAAG,GAAG,CAAA;AAE/B,MAAM,iBAAiB;IACb,KAAK,GAAkB,IAAI,CAAA;IAC3B,WAAW,GAAkB,IAAI,CAAA;IACjC,SAAS,GAAkD,IAAI,CAAA;IAC/D,SAAS,GAAyB,IAAI,CAAA;IACtC,aAAa,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAA;IAChD,iBAAiB,GAAG,CAAC,CAAA;IACrB,aAAa,GAAgC,EAAE,CAAA;IAC/C,SAAS,GAAiB,EAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAC,CAAA;IACvD,OAAO,GAAgE,IAAI,CAAA;IACnF,+FAA+F;IACvF,OAAO,GAAkB,IAAI,CAAA;IAC7B,iBAAiB,GAAwB,IAAI,CAAA;IAC7C,mBAAmB,GAAkB,IAAI,CAAA;IACzC,kBAAkB,GAAG,CAAC,CAAA;IAE9B,eAAe,CAAC,OAA2D;QACzE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;IAED,QAAQ;QACN,OAAO,EAAC,GAAG,IAAI,CAAC,SAAS,EAAC,CAAA;IAC5B,CAAC;IAED,YAAY;QACV,OAAO,IAAI,CAAC,KAAK,CAAA;IACnB,CAAC;IAED,KAAK,CAAC,IAAI,CACR,WAAmB,EACnB,IAMC;QAED,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;QAC1B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,mDAAmD,CAAC,CAAA;YACjE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;QACrE,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;YAC7C,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;QAClE,CAAC;QACD,mFAAmF;QACnF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QACxE,MAAM,QAAQ,GAAG,qBAAqB,EAAE,CAAA;QACxC,IAAI,CAAC,KAAK,GAAG,WAAW,CAAA;QACxB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;QAC3B,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;QACpC,OAAO,CAAC,GAAG,CAAC,gCAAgC,EAAE;YAC5C,WAAW;YACX,YAAY,EAAE,IAAI;YAClB,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;YAC7B,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;YAC9B,WAAW,EAAE,QAAQ,CAAC,MAAM;YAC5B,iBAAiB,EAAE,QAAQ,CAAC,MAAM;YAClC,YAAY,EAAE,gBAAgB,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC;SACjF,CAAC,CAAA;QACF,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC;gBAC9B,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,WAAW,EAAE,QAAQ,CAAC,MAAM;gBAC5B,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAC,KAAK,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;aAC1B,CAAC,CAAA;YACF,IAAI,CAAC,SAAS,GAAG;gBACf,GAAG,KAAK;gBACR,WAAW,EAAE,QAAQ,CAAC,MAAM;gBAC5B,iBAAiB,EAAE,QAAQ,CAAC,MAAM;aACnC,CAAA;YACD,OAAO,CAAC,GAAG,CAAC,mCAAmC,EAAE,EAAC,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAC,CAAC,CAAA;QAC5F,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,iFAAiF;YACjF,yEAAyE;YACzE,OAAO,CAAC,IAAI,CAAC,uCAAuC,EAAE;gBACpD,WAAW;gBACX,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;aAC9D,CAAC,CAAA;YACF,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,UAAU,EAAE,EAAE;gBACxC,OAAO,CAAC,IAAI,CAAC,yDAAyD,EAAE,UAAU,CAAC,CAAA;YACrF,CAAC,CAAC,CAAA;YACF,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAA;YAC7B,MAAM,KAAK,CAAA;QACb,CAAC;QACD,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAA;QAC9B,iFAAiF;QACjF,kFAAkF;QAClF,oEAAoE;QACpE,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAA;YACzC,OAAO,CAAC,GAAG,CAAC,iCAAiC,EAAE,EAAC,QAAQ,EAAE,IAAI,CAAC,WAAW,EAAC,CAAC,CAAA;QAC9E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,yEAAyE,EAAE;gBACtF,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;aAC9D,CAAC,CAAA;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAA;IACvB,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,WAAmB;QAC7B,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW;YAAE,OAAM;QACpD,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;QAC1B,IAAI,CAAC;YACH,MAAM,MAAM,EAAE,KAAK,EAAE,CAAA;QACvB,CAAC;gBAAS,CAAC;YACT,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAA;QAC/B,CAAC;IACH,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,gBAAgB;QAC5B,IAAI,CAAC,mBAAmB,EAAE,CAAA;QAC1B,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;QACpB,IAAI,CAAC,YAAY,EAAE,CAAA;QACnB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QACjB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;QACnB,IAAI,CAAC,kBAAkB,GAAG,CAAC,CAAA;QAC3B,IAAI,CAAC,SAAS,GAAG,EAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAC,CAAA;IAChD,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,WAAmB,EAAE,KAAc;QAChD,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,CAAA;QAC7B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;QAC1B,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;QAChF,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QAC1C,IAAI,CAAC,SAAS,GAAG;YACf,GAAG,IAAI,CAAC,SAAS;YACjB,GAAG,KAAK;YACR,iBAAiB,EAAE,IAAI,CAAC,SAAS,CAAC,iBAAiB;SACpD,CAAA;QACD,OAAO,IAAI,CAAC,SAAS,CAAA;IACvB,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,WAAmB,EAAE,OAAe;QAC1D,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,CAAA;QAC7B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;QAC1B,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAA;QAChF,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,MAAM,MAAM,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAA;IACzC,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,WAAmB;QACjC,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;YAC7C,OAAO,EAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAC,CAAA;QACtC,CAAC;QACD,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;QAC1B,IAAI,CAAC,MAAM;YAAE,OAAO,EAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAC,CAAA;QACjD,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAA;QACrC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAA;QACtB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,WAAmB;QACpC,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW;YAAE,MAAM,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;IAC/D,CAAC;IAEO,WAAW,CAAC,WAAmB;QACrC,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CACb,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,8CAA8C,CAAC,CAAC,CAAC,4CAA4C,CAC3G,CAAA;QACH,CAAC;IACH,CAAC;IAEO,YAAY;QAClB,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;QACjD,IAAI,CAAC,aAAa,GAAG,EAAE,CAAA;IACzB,CAAC;IAEO,UAAU,CAAC,MAAoB,EAAE,WAAmB;QAC1D,IAAI,CAAC,YAAY,EAAE,CAAA;QACnB,IAAI,CAAC,aAAa,GAAG;YACnB,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE;gBACtC,MAAM,WAAW,GAAG,gBAAgB,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;gBACvD,IAAI,WAAW,KAAK,QAAQ,EAAE,CAAC;oBAC7B,OAAO,CAAC,KAAK,CAAC,iCAAiC,EAAE;wBAC/C,YAAY,EAAE,KAAK,CAAC,YAAY;wBAChC,WAAW,EAAE,KAAK,CAAC,WAAW;qBAC/B,CAAC,CAAA;gBACJ,CAAC;gBACD,MAAM,YAAY,GAAG,wBAAwB,CAAC,KAAK,CAAC,YAAY,CAAC,CAAA;gBACjE,MAAM,WAAW,GAAG,gBAAgB,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;gBACvD,MAAM,KAAK,GAAiB;oBAC1B,KAAK,EAAG,KAAK,CAAC,KAAsB,IAAI,MAAM;oBAC9C,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;oBAC3B,KAAK,EAAE,KAAK,CAAC,KAA2B;oBACxC,UAAU,EAAE,KAAK,CAAC,UAAgC;oBAClD,QAAQ,EAAE,WAAW;oBACrB,WAAW,EAAE,KAAK,CAAC,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;oBAChE,iBAAiB,EAAE,IAAI,CAAC,SAAS,CAAC,iBAAiB;oBACnD,YAAY,EAAE,iBAAiB,CAAC,KAAK,CAAC,YAAY,CAAC;oBACnD,WAAW;oBACX,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAC,WAAW,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;oBACrC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAC,YAAY,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;iBACxC,CAAA;gBACD,IAAI,CAAC,SAAS,GAAG,KAAK,CAAA;gBACtB,OAAO,CAAC,GAAG,CAAC,iCAAiC,EAAE;oBAC7C,KAAK,EAAE,KAAK,CAAC,KAAK;oBAClB,KAAK,EAAE,KAAK,CAAC,KAAK;oBAClB,KAAK,EAAE,KAAK,CAAC,KAAK;oBAClB,WAAW,EAAE,KAAK,CAAC,WAAW;oBAC9B,YAAY,EAAE,KAAK,CAAC,YAAY;oBAChC,WAAW,EAAE,KAAK,CAAC,WAAW;oBAC9B,WAAW,EAAE,KAAK,CAAC,WAAW;oBAC9B,YAAY,EAAE,YAAY,EAAE,MAAM;iBACnC,CAAC,CAAA;gBACF,IAAI,CAAC,OAAO,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,CAAA;YACpC,CAAC,CAAC;YACF,MAAM,CAAC,WAAW,CAAC,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC5C,MAAM,MAAM,GAAG,KAAK,CAAC,MAA4B,CAAA;gBACjD,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK;oBAAE,OAAM;gBAClC,mEAAmE;gBACnE,uDAAuD;gBACvD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa;qBACpC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;qBACxF,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;YAC3B,CAAC,CAAC;SACH,CAAA;IACH,CAAC;IAED;;;;;;;OAOG;IACK,iBAAiB,CAAC,MAAoB;QAC5C,IAAI,CAAC,mBAAmB,EAAE,CAAA;QAC1B,MAAM,MAAM,GAAG,eAAe,EAAE,CAAA;QAChC,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAA;QAC/B,IAAI,CAAC;YACH,IAAI,CAAC,iBAAiB,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC,IAAI,EAAE,EAAE;gBACxD,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;gBAC/E,MAAM,QAAQ,GAAG,IAAI,CAAC,mBAAmB,CAAA;gBACzC,IAAI,CAAC,mBAAmB,GAAG,GAAG,CAAA;gBAC9B,uEAAuE;gBACvE,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,GAAG;oBAAE,OAAM;gBACjD,IAAI,IAAI,CAAC,WAAW,KAAK,KAAK;oBAAE,OAAM;gBACtC,KAAK,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,iBAAiB,QAAQ,MAAM,GAAG,EAAE,CAAC,CAAA;YAC5E,CAAC,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,8CAA8C,EAAE,KAAK,CAAC,CAAA;YACnE,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAA;QAC/B,CAAC;IACH,CAAC;IAEO,mBAAmB;QACzB,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAA;QAC1B,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAA;QAC7B,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAA;IACjC,CAAC;IAEO,KAAK,CAAC,kBAAkB,CAAC,MAAoB,EAAE,MAAc;QACnE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;QAC5B,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;YAAE,OAAM;QACrF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QACtB,IAAI,GAAG,GAAG,IAAI,CAAC,kBAAkB,GAAG,6BAA6B;YAAE,OAAM;QACzE,IAAI,CAAC,kBAAkB,GAAG,GAAG,CAAA;QAC7B,OAAO,CAAC,GAAG,CAAC,kCAAkC,EAAE,EAAC,MAAM,EAAE,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,EAAC,CAAC,CAAA;QAClG,IAAI,CAAC;YACH,0EAA0E;YAC1E,gFAAgF;YAChF,IAAI,MAAM,CAAC,kBAAkB;gBAAE,MAAM,MAAM,CAAC,kBAAkB,EAAE,CAAA;;gBAC3D,MAAM,MAAM,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAA;QAC9C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,mCAAmC,EAAE,EAAC,MAAM,EAAE,KAAK,EAAC,CAAC,CAAA;QACpE,CAAC;IACH,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,gBAAgB,CAC5B,WAAmB,EACnB,MAAc,EACd,OAAgB,EAChB,WAAoB;QAEpB,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,KAAK,CAAA;QAC3C,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;QACzC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;YACxD,OAAO,CAAC,IAAI,CAAC,8DAA8D,EAAE,EAAC,UAAU,EAAE,QAAQ,EAAC,CAAC,CAAA;YACpG,OAAM;QACR,CAAC;QACD,IAAI,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,UAAU,KAAK,UAAU,IAAI,IAAI,CAAC,SAAS,CAAC,QAAQ,KAAK,QAAQ,CAAC,EAAE,CAAC;YACzG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;gBACpB,OAAO,CAAC,IAAI,CAAC,4DAA4D,EAAE;oBACzE,IAAI,EAAE,IAAI,CAAC,SAAS;oBACpB,EAAE,EAAE,EAAC,UAAU,EAAE,QAAQ,EAAC;iBAC3B,CAAC,CAAA;gBACF,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE;qBAC5B,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,WAAW,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;qBACrE,OAAO,CAAC,GAAG,EAAE;oBACZ,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;gBACvB,CAAC,CAAC,CAAA;YACN,CAAC;YACD,MAAM,IAAI,CAAC,SAAS,CAAA;QACtB,CAAC;aAAM,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YAC7B,MAAM,IAAI,CAAC,iBAAiB,CAAC,WAAW,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAA;QACjE,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAA;QACjC,IAAI,CAAC,QAAQ;YAAE,OAAM;QACrB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,oBAAoB,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;YAC5E,MAAM,UAAU,GAAG,MAAM,EAAE,UAAU,CAAA;YACrC,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,GAAG,mBAAmB,EAAE,CAAC;gBACvE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;gBACtB,IAAI,GAAG,GAAG,IAAI,CAAC,iBAAiB,GAAG,IAAI,EAAE,CAAC;oBACxC,IAAI,CAAC,iBAAiB,GAAG,GAAG,CAAA;oBAC5B,OAAO,CAAC,IAAI,CAAC,wCAAwC,EAAE,EAAC,UAAU,EAAC,CAAC,CAAA;gBACtE,CAAC;YACH,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,wCAAwC,EAAE,KAAK,CAAC,CAAA;QAC/D,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAAC,WAAmB,EAAE,UAAU,GAAG,KAAK,EAAE,QAAQ,GAAG,CAAC;QACnF,IAAI,IAAI,CAAC,WAAW;YAAE,OAAM;QAC5B,MAAM,QAAQ,GAAG,UAAU,WAAW,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAA;QACtD,MAAM,oBAAoB,CAAC,UAAU,CAAC;YACpC,QAAQ;YACR,KAAK,EAAE,WAAW;YAClB,UAAU;YACV,QAAQ;YACR,cAAc,EAAE,IAAI;YACpB,OAAO,EAAE,GAAG,EAAE;gBACZ,IAAI,IAAI,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;oBAClC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;oBACvB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;gBACvB,CAAC;YACH,CAAC;SACF,CAAC,CAAA;QACF,IAAI,CAAC,WAAW,GAAG,QAAQ,CAAA;QAC3B,IAAI,CAAC,SAAS,GAAG,EAAC,UAAU,EAAE,QAAQ,EAAC,CAAA;IACzC,CAAC;IAEO,KAAK,CAAC,OAAO;QACnB,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAA;QACjC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAA;QACvB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;QACrB,IAAI,CAAC,QAAQ;YAAE,OAAM;QACrB,MAAM,oBAAoB,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IACzE,CAAC;CACF;AAED,MAAM,iBAAiB,GAAG,IAAI,iBAAiB,EAAE,CAAA;AACjD,eAAe,iBAAiB,CAAA","sourcesContent":["/**\n * Host-side ACS meeting controller. One active meeting. Native module owns\n * WHEP + ACS; this service maps that into miniapp envelopes and pipes\n * incoming PCM into AudioPlaybackService (A2DP / PcmStreamPlayer).\n */\n\nimport audioPlaybackService from \"./AudioPlaybackService\"\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport {ACS_CALL_MIC, type ResolvedAudioSource, type SourceReason} from \"./acsAudioSource\"\n\nexport {ACS_CALL_MIC}\nexport type {ResolvedAudioSource, SourceReason}\n\ntype MeetingPhase = \"idle\" | \"connecting\" | \"lobby\" | \"connected\" | \"disconnected\" | \"error\"\n\nexport type AudioSafety = \"safe\" | \"degraded\" | \"unsafe\"\nexport type ActiveStream = \"none\" | \"virtual\" | \"local\"\n/**\n * Health of the glasses WHEP subscription that feeds the call. `failed` means ICE\n * dropped or the WHEP endpoint went away (glasses stopped publishing, phone changed\n * networks); the ACS call itself may still be `connected` with a frozen last frame.\n */\nexport type MediaSourceState = \"idle\" | \"connecting\" | \"live\" | \"failed\"\n\nexport type MeetingParticipantState = \"idle\" | \"connecting\" | \"connected\" | \"lobby\" | \"hold\" | \"disconnected\"\n\nexport interface MeetingParticipant {\n id: string\n displayName: string | null\n state: MeetingParticipantState\n isMuted: boolean\n isSpeaking: boolean\n}\n\nexport interface MeetingState {\n state: MeetingPhase\n muted: boolean\n error?: string\n meetingUrl?: string\n provider?: \"acs-teams\"\n audioSource?: \"glasses\" | \"phone\"\n audioSourceReason?: SourceReason\n activeStream?: ActiveStream\n audioSafety?: AudioSafety\n mediaSource?: MediaSourceState\n participants?: MeetingParticipant[]\n}\n\n/**\n * The call microphone is [ACS_CALL_MIC]. Flip that constant to `\"glasses\"`\n * to restore glasses WHIP → Cloudflare → WHEP PCM. Do not use ACS\n * LocalOutgoingAudioStream for phone: that path makes ACS own the route\n * (MODE_IN_COMMUNICATION + forced speaker) and opens an echo loop.\n * preferred_mic governs MentraOS STT capture, not this call.\n */\nexport function resolveAcsAudioSource(): ResolvedAudioSource {\n return {source: ACS_CALL_MIC, reason: \"explicit\"}\n}\n\nconst PARTICIPANT_STATES = new Set<MeetingParticipantState>([\n \"idle\",\n \"connecting\",\n \"connected\",\n \"lobby\",\n \"hold\",\n \"disconnected\",\n])\n\nexport function parseMeetingParticipants(raw: unknown): MeetingParticipant[] | undefined {\n if (!Array.isArray(raw)) return undefined\n const result: MeetingParticipant[] = []\n for (const entry of raw) {\n if (!entry || typeof entry !== \"object\") continue\n const value = entry as Record<string, unknown>\n if (typeof value.id !== \"string\" || !value.id) continue\n const state = PARTICIPANT_STATES.has(value.state as MeetingParticipantState)\n ? (value.state as MeetingParticipantState)\n : \"idle\"\n result.push({\n id: value.id,\n displayName: typeof value.displayName === \"string\" && value.displayName ? value.displayName : null,\n state,\n isMuted: Boolean(value.isMuted),\n isSpeaking: Boolean(value.isSpeaking),\n })\n }\n return result\n}\n\nexport type AcsOutgoingVideo = {\n width: number\n height: number\n fps: number\n maxBitrateBps: number\n}\n\n/** ACS VirtualOutgoingVideoStream documented 16:9 sizes. P540 is 960×540, not 540×960. */\nconst ACS_VIRTUAL_CAMERA_SIZES = new Set([\"1280x720\", \"960x540\"])\n\nexport function parseAcsOutgoingVideo(raw: unknown): AcsOutgoingVideo | undefined {\n if (raw == null) return undefined\n if (typeof raw !== \"object\") throw new Error(\"video must be an object\")\n const value = raw as Record<string, unknown>\n const width = Number(value.width)\n const height = Number(value.height)\n const fps = Number(value.fps)\n const maxBitrateBps = Number(value.maxBitrateBps)\n if (![width, height, fps, maxBitrateBps].every(Number.isFinite)) {\n throw new Error(\"video requires width, height, fps, and maxBitrateBps\")\n }\n if (!ACS_VIRTUAL_CAMERA_SIZES.has(`${width}x${height}`) || fps < 1 || fps > 30 || maxBitrateBps <= 0) {\n throw new Error(`unsupported ACS video ${width}x${height}@${fps}`)\n }\n return {width, height, fps, maxBitrateBps}\n}\n\ntype NativeModule = {\n join(options: {\n meetingUrl: string\n token: string\n whepUrl: string\n displayName?: string\n dumpPcmWav?: boolean\n audioSource?: \"glasses\" | \"phone\"\n video?: AcsOutgoingVideo\n }): Promise<MeetingState>\n leave(): Promise<void>\n setMuted(muted: boolean): Promise<MeetingState>\n setAudioSource(source: \"glasses\" | \"phone\"): Promise<MeetingState>\n updateVideoSource(whepUrl: string): Promise<void>\n /** Force a WHEP rebuild on the current URL. Absent on natives that predate it. */\n restartVideoSource?(): Promise<void>\n getState(): Promise<MeetingState>\n addListener(event: string, listener: (event: Record<string, unknown>) => void): {remove: () => void}\n}\n\nlet nativeModule: NativeModule | null | undefined\n\nfunction getNative(): NativeModule | null {\n if (nativeModule !== undefined) return nativeModule\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n nativeModule = require(\"@mentra/acs-meeting\").default as NativeModule\n } catch {\n nativeModule = null\n }\n return nativeModule\n}\n\n/** Test seam: skip the native require and inject a fake ACS module. */\nexport function setAcsMeetingNativeForTests(mod: NativeModule | null | undefined): void {\n nativeModule = mod\n}\n\n/**\n * Phone connectivity feed. Only the subset of `@react-native-community/netinfo`\n * this service needs, so tests can inject a fake and hosts without the package\n * (or a bare test runtime) degrade to \"no phone-network awareness\" instead of\n * failing to load the meeting service.\n */\nexport type PhoneNetworkInfo = {type: string; isConnected: boolean | null}\ntype PhoneNetworkSource = {\n addEventListener(listener: (state: PhoneNetworkInfo) => void): () => void\n}\n\nlet phoneNetwork: PhoneNetworkSource | null | undefined\n\nfunction getPhoneNetwork(): PhoneNetworkSource | null {\n if (phoneNetwork !== undefined) return phoneNetwork\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const netInfo = require(\"@react-native-community/netinfo\") as {default?: PhoneNetworkSource} & PhoneNetworkSource\n phoneNetwork = netInfo.default ?? netInfo\n } catch {\n phoneNetwork = null\n }\n return phoneNetwork\n}\n\n/** Test seam: inject a fake phone-network feed (or `null` to disable it). */\nexport function setAcsMeetingPhoneNetworkForTests(source: PhoneNetworkSource | null | undefined): void {\n phoneNetwork = source\n}\n\nfunction parseActiveStream(value: unknown): ActiveStream | undefined {\n if (value === \"none\" || value === \"virtual\" || value === \"local\") return value\n return undefined\n}\n\nfunction parseAudioSafety(value: unknown): AudioSafety | undefined {\n if (value === \"safe\" || value === \"degraded\" || value === \"unsafe\") return value\n return undefined\n}\n\nfunction parseMediaSource(value: unknown): MediaSourceState | undefined {\n if (value === \"idle\" || value === \"connecting\" || value === \"live\" || value === \"failed\") return value\n return undefined\n}\n\n/** Meeting phases during which the WHEP feed should be alive and is worth restarting. */\nconst MEDIA_ACTIVE_PHASES = new Set<MeetingPhase>([\"connecting\", \"lobby\", \"connected\"])\n/** Floor between WHEP restarts we trigger from the host, so a flapping network cannot thrash native. */\nconst MEDIA_RESTART_MIN_INTERVAL_MS = 3000\n\n/** Formats the native PcmStreamPlayer accepts (mono only). */\nconst PCM_SAMPLE_RATES = new Set([16000, 24000, 48000])\nconst PCM_BACKLOG_WARN_MS = 600\n\nclass AcsMeetingService {\n private owner: string | null = null\n private pcmStreamId: string | null = null\n private pcmFormat: {sampleRate: number; channels: number} | null = null\n private pcmReopen: Promise<void> | null = null\n private pcmWriteChain: Promise<void> = Promise.resolve()\n private lastBacklogWarnAt = 0\n private subscriptions: Array<{remove: () => void}> = []\n private lastState: MeetingState = {state: \"idle\", muted: false}\n private onState: ((packageName: string, state: MeetingState) => void) | null = null\n /** WHEP URL native is (or should be) subscribed to; what a host-triggered restart re-feeds. */\n private whepUrl: string | null = null\n private phoneNetworkUnsub: (() => void) | null = null\n private lastPhoneNetworkKey: string | null = null\n private lastMediaRestartAt = 0\n\n setStateHandler(handler: (packageName: string, state: MeetingState) => void): void {\n this.onState = handler\n }\n\n getState(): MeetingState {\n return {...this.lastState}\n }\n\n ownerPackage(): string | null {\n return this.owner\n }\n\n async join(\n packageName: string,\n args: {\n meetingUrl: string\n token: string\n whepUrl: string\n displayName?: string\n video?: AcsOutgoingVideo\n },\n ): Promise<MeetingState> {\n const native = getNative()\n if (!native) {\n console.warn(\"[AcsMeeting] phase=join-native nativeLoaded=false\")\n throw new Error(\"ACS meeting module is not available on this host\")\n }\n if (this.owner && this.owner !== packageName) {\n throw new Error(\"Another miniapp already has an active meeting\")\n }\n // Validate before claiming ownership so a bad request cannot leave the slot taken.\n const video = args.video ? parseAcsOutgoingVideo(args.video) : undefined\n const resolved = resolveAcsAudioSource()\n this.owner = packageName\n this.whepUrl = args.whepUrl\n this.bindNative(native, packageName)\n console.log(\"[AcsMeeting] phase=join-native\", {\n packageName,\n nativeLoaded: true,\n hasToken: Boolean(args.token),\n hasWhep: Boolean(args.whepUrl),\n audioSource: resolved.source,\n audioSourceReason: resolved.reason,\n preferredMic: useSettingsStore.getState().getSetting(SETTINGS.preferred_mic.key),\n })\n try {\n const state = await native.join({\n meetingUrl: args.meetingUrl,\n token: args.token,\n whepUrl: args.whepUrl,\n displayName: args.displayName,\n audioSource: resolved.source,\n ...(video ? {video} : {}),\n })\n this.lastState = {\n ...state,\n audioSource: resolved.source,\n audioSourceReason: resolved.reason,\n }\n console.log(\"[AcsMeeting] phase=join-native-ok\", {state: state.state, muted: state.muted})\n } catch (error) {\n // Native never joined (or is unwinding). Release the slot so the same or another\n // miniapp can retry, and make sure nothing half-joined lingers in Teams.\n console.warn(\"[AcsMeeting] phase=join-native-failed\", {\n packageName,\n error: error instanceof Error ? error.message : String(error),\n })\n await native.leave().catch((leaveError) => {\n console.warn(\"[AcsMeeting] native leave after failed join also failed\", leaveError)\n })\n await this.releaseHostState()\n throw error\n }\n this.watchPhoneNetwork(native)\n // Return audio is additive: the wearer is already in the meeting with camera and\n // mic. A playback failure must not reject the join — that would leave the miniapp\n // believing it never joined while native keeps the wearer in Teams.\n try {\n await this.ensurePcmPlayback(packageName)\n console.log(\"[AcsMeeting] phase=pcm-playback\", {streamId: this.pcmStreamId})\n } catch (error) {\n console.warn(\"[AcsMeeting] phase=pcm-playback-failed; continuing without return audio\", {\n error: error instanceof Error ? error.message : String(error),\n })\n }\n return this.lastState\n }\n\n async leave(packageName: string): Promise<void> {\n if (this.owner && this.owner !== packageName) return\n const native = getNative()\n try {\n await native?.leave()\n } finally {\n await this.releaseHostState()\n }\n }\n\n /**\n * Drop everything the host set up around a session: the network watcher, return\n * audio, native listeners, ownership. Runs after native has been told to leave\n * (or after a join that never produced a native call).\n */\n private async releaseHostState(): Promise<void> {\n this.unwatchPhoneNetwork()\n await this.stopPcm()\n this.unbindNative()\n this.owner = null\n this.whepUrl = null\n this.lastMediaRestartAt = 0\n this.lastState = {state: \"idle\", muted: false}\n }\n\n async setMuted(packageName: string, muted: boolean): Promise<MeetingState> {\n this.assertOwner(packageName)\n const native = getNative()\n if (!native) throw new Error(\"ACS meeting module is not available on this host\")\n const state = await native.setMuted(muted)\n this.lastState = {\n ...this.lastState,\n ...state,\n audioSourceReason: this.lastState.audioSourceReason,\n }\n return this.lastState\n }\n\n async updateVideoSource(packageName: string, whepUrl: string): Promise<void> {\n this.assertOwner(packageName)\n const native = getNative()\n if (!native) throw new Error(\"ACS meeting module is not available on this host\")\n this.whepUrl = whepUrl\n await native.updateVideoSource(whepUrl)\n }\n\n async readState(packageName: string): Promise<MeetingState> {\n if (this.owner && this.owner !== packageName) {\n return {state: \"idle\", muted: false}\n }\n const native = getNative()\n if (!native) return {state: \"idle\", muted: false}\n const state = await native.getState()\n this.lastState = state\n return state\n }\n\n async leaveIfOwner(packageName: string): Promise<void> {\n if (this.owner === packageName) await this.leave(packageName)\n }\n\n private assertOwner(packageName: string): void {\n if (this.owner !== packageName) {\n throw new Error(\n this.owner ? \"This miniapp does not own the active meeting\" : \"No active meeting; call meeting.join first\",\n )\n }\n }\n\n private unbindNative(): void {\n this.subscriptions.forEach((sub) => sub.remove())\n this.subscriptions = []\n }\n\n private bindNative(native: NativeModule, packageName: string): void {\n this.unbindNative()\n this.subscriptions = [\n native.addListener(\"onState\", (event) => {\n const audioSafety = parseAudioSafety(event.audioSafety)\n if (audioSafety === \"unsafe\") {\n console.error(\"[AcsMeeting] phase=audio-unsafe\", {\n activeStream: event.activeStream,\n audioSource: event.audioSource,\n })\n }\n const participants = parseMeetingParticipants(event.participants)\n const mediaSource = parseMediaSource(event.mediaSource)\n const state: MeetingState = {\n state: (event.state as MeetingPhase) ?? \"idle\",\n muted: Boolean(event.muted),\n error: event.error as string | undefined,\n meetingUrl: event.meetingUrl as string | undefined,\n provider: \"acs-teams\",\n audioSource: event.audioSource === \"phone\" ? \"phone\" : \"glasses\",\n audioSourceReason: this.lastState.audioSourceReason,\n activeStream: parseActiveStream(event.activeStream),\n audioSafety,\n ...(mediaSource ? {mediaSource} : {}),\n ...(participants ? {participants} : {}),\n }\n this.lastState = state\n console.log(\"[AcsMeeting] phase=native-state\", {\n state: state.state,\n muted: state.muted,\n error: state.error,\n audioSource: state.audioSource,\n activeStream: state.activeStream,\n audioSafety: state.audioSafety,\n mediaSource: state.mediaSource,\n participants: participants?.length,\n })\n this.onState?.(packageName, state)\n }),\n native.addListener(\"onIncomingPcm\", (event) => {\n const base64 = event.base64 as string | undefined\n if (!base64 || !this.owner) return\n // Serialize: native async calls may complete out of order, and PCM\n // chunks written out of order are audible as garbling.\n this.pcmWriteChain = this.pcmWriteChain\n .then(() => this.writeIncomingPcm(packageName, base64, event.sampleRate, event.channels))\n .catch(() => undefined)\n }),\n ]\n }\n\n /**\n * The WHEP PeerConnection does not survive the phone changing networks\n * (Wi-Fi↔cellular, or a different Wi-Fi): ICE fails and native parks the source\n * as `failed` while the ACS call itself reconnects on its own. Nudge native to\n * rebuild the subscription whenever the phone's network identity changes during\n * a live meeting. Native ignores the request when the source is healthy on the\n * same URL, so a spurious event costs nothing.\n */\n private watchPhoneNetwork(native: NativeModule): void {\n this.unwatchPhoneNetwork()\n const source = getPhoneNetwork()\n if (!source) return\n this.lastPhoneNetworkKey = null\n try {\n this.phoneNetworkUnsub = source.addEventListener((info) => {\n const key = `${info.type}:${info.isConnected === false ? \"offline\" : \"online\"}`\n const previous = this.lastPhoneNetworkKey\n this.lastPhoneNetworkKey = key\n // First callback is NetInfo replaying the current state, not a change.\n if (previous === null || previous === key) return\n if (info.isConnected === false) return\n void this.restartMediaSource(native, `phone network ${previous} → ${key}`)\n })\n } catch (error) {\n console.warn(\"[AcsMeeting] phone network watch unavailable\", error)\n this.phoneNetworkUnsub = null\n }\n }\n\n private unwatchPhoneNetwork(): void {\n this.phoneNetworkUnsub?.()\n this.phoneNetworkUnsub = null\n this.lastPhoneNetworkKey = null\n }\n\n private async restartMediaSource(native: NativeModule, reason: string): Promise<void> {\n const whepUrl = this.whepUrl\n if (!this.owner || !whepUrl || !MEDIA_ACTIVE_PHASES.has(this.lastState.state)) return\n const now = Date.now()\n if (now - this.lastMediaRestartAt < MEDIA_RESTART_MIN_INTERVAL_MS) return\n this.lastMediaRestartAt = now\n console.log(\"[AcsMeeting] phase=media-restart\", {reason, mediaSource: this.lastState.mediaSource})\n try {\n // A same-URL updateVideoSource is a no-op while native still believes the\n // peer is healthy; after a network switch that belief is exactly what is wrong.\n if (native.restartVideoSource) await native.restartVideoSource()\n else await native.updateVideoSource(whepUrl)\n } catch (error) {\n console.warn(\"[AcsMeeting] media restart failed\", {reason, error})\n }\n }\n\n /**\n * Native normalizes to 16 kHz mono, so this is normally a straight write.\n * If the format ever differs, reopen the player to match rather than play\n * the bytes at the wrong rate (that is what slows and deepens the audio).\n */\n private async writeIncomingPcm(\n packageName: string,\n base64: string,\n rawRate: unknown,\n rawChannels: unknown,\n ): Promise<void> {\n const sampleRate = Number(rawRate) || 16000\n const channels = Number(rawChannels) || 1\n if (!PCM_SAMPLE_RATES.has(sampleRate) || channels !== 1) {\n console.warn(\"[AcsMeeting] incoming PCM format unsupported; dropping chunk\", {sampleRate, channels})\n return\n }\n if (this.pcmFormat && (this.pcmFormat.sampleRate !== sampleRate || this.pcmFormat.channels !== channels)) {\n if (!this.pcmReopen) {\n console.warn(\"[AcsMeeting] incoming PCM format changed; reopening player\", {\n from: this.pcmFormat,\n to: {sampleRate, channels},\n })\n this.pcmReopen = this.stopPcm()\n .then(() => this.ensurePcmPlayback(packageName, sampleRate, channels))\n .finally(() => {\n this.pcmReopen = null\n })\n }\n await this.pcmReopen\n } else if (!this.pcmStreamId) {\n await this.ensurePcmPlayback(packageName, sampleRate, channels)\n }\n const streamId = this.pcmStreamId\n if (!streamId) return\n try {\n const result = await audioPlaybackService.writeStreamChunk(streamId, base64)\n const bufferedMs = result?.bufferedMs\n if (typeof bufferedMs === \"number\" && bufferedMs > PCM_BACKLOG_WARN_MS) {\n const now = Date.now()\n if (now - this.lastBacklogWarnAt > 5000) {\n this.lastBacklogWarnAt = now\n console.warn(\"[AcsMeeting] incoming PCM backlog high\", {bufferedMs})\n }\n }\n } catch (error) {\n console.warn(\"[AcsMeeting] incoming PCM write failed\", error)\n }\n }\n\n private async ensurePcmPlayback(packageName: string, sampleRate = 16000, channels = 1): Promise<void> {\n if (this.pcmStreamId) return\n const streamId = `acs-in-${packageName}-${Date.now()}`\n await audioPlaybackService.openStream({\n streamId,\n appId: packageName,\n sampleRate,\n channels,\n stopOtherAudio: true,\n onEnded: () => {\n if (this.pcmStreamId === streamId) {\n this.pcmStreamId = null\n this.pcmFormat = null\n }\n },\n })\n this.pcmStreamId = streamId\n this.pcmFormat = {sampleRate, channels}\n }\n\n private async stopPcm(): Promise<void> {\n const streamId = this.pcmStreamId\n this.pcmStreamId = null\n this.pcmFormat = null\n if (!streamId) return\n await audioPlaybackService.abortStream(streamId).catch(() => undefined)\n }\n}\n\nconst acsMeetingService = new AcsMeetingService()\nexport default acsMeetingService\n"]}
@@ -13,6 +13,7 @@
13
13
  * - uninstall(packageName, version?) remove one or all versions
14
14
  * - getInstalledMiniapps() ClientApp[] derived from disk
15
15
  * - getActiveVersion(packageName) active version string for a package
16
+ * - getLatestDevSnapshotVersion(pkg) newest on-disk `dev-*` snapshot
16
17
  * - getBundleDir / getMiniappManifest filesystem helpers used by hosts
17
18
  * - subscribe(fn) register a refresh listener
18
19
  */
@@ -173,6 +174,14 @@ declare class AppRegistry {
173
174
  getInstalledVersions(packageName: string): string[];
174
175
  getActiveVersion(packageName: string): Promise<string>;
175
176
  setActiveVersion(packageName: string, version: string): Result<void, Error>;
177
+ /**
178
+ * Newest `dev-*` snapshot that still has a resolvable UI or background
179
+ * entry. Semver store installs are ignored — a live-dev tile must not
180
+ * silently fall back to a released store bundle when the laptop drops.
181
+ */
182
+ getLatestDevSnapshotVersion(packageName: string): string | null;
183
+ /** True iff {@link getLatestDevSnapshotVersion} finds a usable snapshot. */
184
+ hasDevSnapshot(packageName: string): boolean;
176
185
  getMetadata(packageName: string, version: string): InstalledInfo;
177
186
  getInstalledInfo(): InstalledLma[];
178
187
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"AppRegistry.d.ts","sourceRoot":"","sources":["../../src/services/AppRegistry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AppRegistry.d.ts","sourceRoot":"","sources":["../../src/services/AppRegistry.ts"],"names":[],"mappings":"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"}
@@ -13,6 +13,7 @@
13
13
  * - uninstall(packageName, version?) remove one or all versions
14
14
  * - getInstalledMiniapps() ClientApp[] derived from disk
15
15
  * - getActiveVersion(packageName) active version string for a package
16
+ * - getLatestDevSnapshotVersion(pkg) newest on-disk `dev-*` snapshot
16
17
  * - getBundleDir / getMiniappManifest filesystem helpers used by hosts
17
18
  * - subscribe(fn) register a refresh listener
18
19
  */
@@ -608,6 +609,27 @@ class AppRegistry {
608
609
  setActiveVersion(packageName, version) {
609
610
  return storage.save(`${packageName}_active_version`, version);
610
611
  }
612
+ /**
613
+ * Newest `dev-*` snapshot that still has a resolvable UI or background
614
+ * entry. Semver store installs are ignored — a live-dev tile must not
615
+ * silently fall back to a released store bundle when the laptop drops.
616
+ */
617
+ getLatestDevSnapshotVersion(packageName) {
618
+ const versions = this.getInstalledVersions(packageName)
619
+ .filter((v) => v.startsWith("dev-"))
620
+ .sort()
621
+ .reverse();
622
+ for (const version of versions) {
623
+ const paths = this.getMiniappEntryPaths(packageName, version);
624
+ if (paths?.background || paths?.ui)
625
+ return version;
626
+ }
627
+ return null;
628
+ }
629
+ /** True iff {@link getLatestDevSnapshotVersion} finds a usable snapshot. */
630
+ hasDevSnapshot(packageName) {
631
+ return this.getLatestDevSnapshotVersion(packageName) != null;
632
+ }
611
633
  getMetadata(packageName, version) {
612
634
  try {
613
635
  const lmaDir = new Directory(Paths.document, "lmas", packageName, version);