@mentra/engine 3.2.0-dev.221 → 3.2.0-dev.223

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 (69) hide show
  1. package/build/generated/releaseMetadata.js +5 -5
  2. package/build/generated/releaseMetadata.js.map +1 -1
  3. package/build/index.d.ts +2 -0
  4. package/build/index.d.ts.map +1 -1
  5. package/build/index.js +1 -0
  6. package/build/index.js.map +1 -1
  7. package/build/services/AcsMeetingService.d.ts +360 -10
  8. package/build/services/AcsMeetingService.d.ts.map +1 -1
  9. package/build/services/AcsMeetingService.js +836 -19
  10. package/build/services/AcsMeetingService.js.map +1 -1
  11. package/build/services/AudioPlaybackService.d.ts +6 -0
  12. package/build/services/AudioPlaybackService.d.ts.map +1 -1
  13. package/build/services/AudioPlaybackService.js +4 -3
  14. package/build/services/AudioPlaybackService.js.map +1 -1
  15. package/build/services/GlassesMicProbe.d.ts +78 -0
  16. package/build/services/GlassesMicProbe.d.ts.map +1 -0
  17. package/build/services/GlassesMicProbe.js +258 -0
  18. package/build/services/GlassesMicProbe.js.map +1 -0
  19. package/build/services/LocalMiniappRuntime.d.ts +83 -0
  20. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  21. package/build/services/LocalMiniappRuntime.js +714 -39
  22. package/build/services/LocalMiniappRuntime.js.map +1 -1
  23. package/build/services/MentraJSLogPipeline.d.ts +39 -0
  24. package/build/services/MentraJSLogPipeline.d.ts.map +1 -1
  25. package/build/services/MentraJSLogPipeline.js +59 -3
  26. package/build/services/MentraJSLogPipeline.js.map +1 -1
  27. package/build/services/MentraJSRouter.d.ts +1 -1
  28. package/build/services/MentraJSRouter.d.ts.map +1 -1
  29. package/build/services/MentraJSRouter.js +2 -2
  30. package/build/services/MentraJSRouter.js.map +1 -1
  31. package/build/services/MicStateCoordinator.d.ts +22 -0
  32. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  33. package/build/services/MicStateCoordinator.js +34 -3
  34. package/build/services/MicStateCoordinator.js.map +1 -1
  35. package/build/services/PhoneStreamCoordinator.d.ts +8 -0
  36. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  37. package/build/services/PhoneStreamCoordinator.js +2 -0
  38. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  39. package/build/services/SoftapCallTransport.d.ts +365 -0
  40. package/build/services/SoftapCallTransport.d.ts.map +1 -0
  41. package/build/services/SoftapCallTransport.js +722 -0
  42. package/build/services/SoftapCallTransport.js.map +1 -0
  43. package/build/services/SoftapCleanupBarrier.d.ts +45 -0
  44. package/build/services/SoftapCleanupBarrier.d.ts.map +1 -0
  45. package/build/services/SoftapCleanupBarrier.js +51 -0
  46. package/build/services/SoftapCleanupBarrier.js.map +1 -0
  47. package/build/utils/pcm16.d.ts +35 -0
  48. package/build/utils/pcm16.d.ts.map +1 -0
  49. package/build/utils/pcm16.js +85 -0
  50. package/build/utils/pcm16.js.map +1 -0
  51. package/build/utils/softapTrace.d.ts +39 -0
  52. package/build/utils/softapTrace.d.ts.map +1 -0
  53. package/build/utils/softapTrace.js +124 -0
  54. package/build/utils/softapTrace.js.map +1 -0
  55. package/package.json +7 -7
  56. package/src/generated/releaseMetadata.ts +5 -5
  57. package/src/index.ts +2 -0
  58. package/src/services/AcsMeetingService.ts +956 -22
  59. package/src/services/AudioPlaybackService.ts +12 -3
  60. package/src/services/GlassesMicProbe.ts +300 -0
  61. package/src/services/LocalMiniappRuntime.ts +775 -42
  62. package/src/services/MentraJSLogPipeline.ts +70 -3
  63. package/src/services/MentraJSRouter.ts +2 -2
  64. package/src/services/MicStateCoordinator.ts +35 -3
  65. package/src/services/PhoneStreamCoordinator.ts +10 -0
  66. package/src/services/SoftapCallTransport.ts +928 -0
  67. package/src/services/SoftapCleanupBarrier.ts +72 -0
  68. package/src/utils/pcm16.ts +93 -0
  69. package/src/utils/softapTrace.ts +133 -0
@@ -73,6 +73,34 @@ export interface ThrottleOptions {
73
73
  now?: () => number
74
74
  }
75
75
 
76
+ /** A per-package override of the shared budget. Both fields are absolute, not multipliers. */
77
+ export interface PackageLogBudget {
78
+ tokensPerSecond: number
79
+ bucketCapacity: number
80
+ }
81
+
82
+ /**
83
+ * Budgets for miniapps under active diagnosis.
84
+ *
85
+ * The default ceiling protects the host from a miniapp that logs in a render loop, and it is the
86
+ * right default. It is the wrong ceiling for a miniapp someone is actively debugging: a single
87
+ * instrumented call join emits several hundred lines in its first few seconds, which exhausts the
88
+ * burst and then drops everything that follows — and the drops land exactly where the interesting
89
+ * part is. A log with silent holes in the failure window is worse than no log, because it is read
90
+ * as complete.
91
+ *
92
+ * So the ceiling is raised for named packages rather than lowered for everyone. Anything not
93
+ * listed here keeps the protective default.
94
+ */
95
+ export const DIAGNOSTIC_LOG_BUDGETS: Readonly<Record<string, PackageLogBudget>> = {
96
+ // Mentra Call is instrumented end to end — UI, background, and the host call path — so the
97
+ // whole of a join, a cancel, and a teardown can be read back from one capture.
98
+ "com.mentra.call": {tokensPerSecond: 200, bucketCapacity: 5_000},
99
+ }
100
+
101
+ /** Ring-buffer window for a package in {@link DIAGNOSTIC_LOG_BUDGETS}. See `capacityFor`. */
102
+ export const DIAGNOSTIC_RING_CAPACITY = 2_000
103
+
76
104
  interface PackageBucket {
77
105
  tokens: number
78
106
  lastRefillAtMs: number
@@ -85,6 +113,7 @@ export class MentraJSLogThrottle {
85
113
  private readonly bucketCapacity: number
86
114
  private readonly now: () => number
87
115
  private readonly buckets: Map<string, PackageBucket> = new Map()
116
+ private readonly budgets: Map<string, PackageLogBudget> = new Map(Object.entries(DIAGNOSTIC_LOG_BUDGETS))
88
117
 
89
118
  constructor(opts: ThrottleOptions = {}) {
90
119
  this.tokensPerSecond = opts.tokensPerSecond ?? 100 / 60
@@ -92,6 +121,29 @@ export class MentraJSLogThrottle {
92
121
  this.now = opts.now ?? (() => Date.now())
93
122
  }
94
123
 
124
+ /**
125
+ * Raise (or lower) one package's budget at runtime.
126
+ *
127
+ * Exists so a support session can widen the pipe for the miniapp being investigated without a
128
+ * rebuild, and so tests can exercise a budget without depending on the shipped table.
129
+ */
130
+ setPackageBudget(packageName: string, budget: PackageLogBudget | null): void {
131
+ if (budget) this.budgets.set(packageName, budget)
132
+ else this.budgets.delete(packageName)
133
+ // The bucket holds a capacity snapshot, so a stale one would keep enforcing the old ceiling
134
+ // until it happened to refill. Dropping it re-derives from the new budget on the next line.
135
+ this.buckets.delete(packageName)
136
+ }
137
+
138
+ private budgetFor(packageName: string): PackageLogBudget {
139
+ return (
140
+ this.budgets.get(packageName) ?? {
141
+ tokensPerSecond: this.tokensPerSecond,
142
+ bucketCapacity: this.bucketCapacity,
143
+ }
144
+ )
145
+ }
146
+
95
147
  /**
96
148
  * Try to consume one token for the named package. Returns either:
97
149
  * - `{allowed: true}` — caller emits the original log line.
@@ -102,17 +154,18 @@ export class MentraJSLogThrottle {
102
154
  */
103
155
  consume(packageName: string): {allowed: true} | {allowed: false; throttledLine?: string} {
104
156
  const at = this.now()
157
+ const budget = this.budgetFor(packageName)
105
158
  let bucket = this.buckets.get(packageName)
106
159
  if (!bucket) {
107
160
  bucket = {
108
- tokens: this.bucketCapacity,
161
+ tokens: budget.bucketCapacity,
109
162
  lastRefillAtMs: at,
110
163
  pendingDrops: 0,
111
164
  }
112
165
  this.buckets.set(packageName, bucket)
113
166
  } else {
114
167
  const elapsedSec = (at - bucket.lastRefillAtMs) / 1000
115
- bucket.tokens = Math.min(this.bucketCapacity, bucket.tokens + elapsedSec * this.tokensPerSecond)
168
+ bucket.tokens = Math.min(budget.bucketCapacity, bucket.tokens + elapsedSec * budget.tokensPerSecond)
116
169
  bucket.lastRefillAtMs = at
117
170
  }
118
171
  if (bucket.tokens >= 1) {
@@ -146,6 +199,19 @@ export class MentraJSLogRingBuffer {
146
199
  this.capacity = capacityPerPackage
147
200
  }
148
201
 
202
+ /**
203
+ * Capacity for a package under diagnosis.
204
+ *
205
+ * This buffer is the miniapp's last words in a crash report, and 200 lines of a heavily
206
+ * instrumented miniapp is a couple of seconds — so the report would arrive holding the
207
+ * aftermath and none of the cause. A package with a raised log budget needs a window long
208
+ * enough to contain whatever produced the crash.
209
+ */
210
+ private capacityFor(packageName: string): number {
211
+ const budget = DIAGNOSTIC_LOG_BUDGETS[packageName]
212
+ return budget ? Math.max(this.capacity, DIAGNOSTIC_RING_CAPACITY) : this.capacity
213
+ }
214
+
149
215
  push(packageName: string, line: string): void {
150
216
  let buf = this.buffers.get(packageName)
151
217
  if (!buf) {
@@ -153,7 +219,8 @@ export class MentraJSLogRingBuffer {
153
219
  this.buffers.set(packageName, buf)
154
220
  }
155
221
  buf.push(line)
156
- if (buf.length > this.capacity) buf.shift()
222
+ const capacity = this.capacityFor(packageName)
223
+ while (buf.length > capacity) buf.shift()
157
224
  }
158
225
 
159
226
  snapshot(packageName: string): string[] {
@@ -379,9 +379,9 @@ export class MentraJSRouter {
379
379
  * router gates this to registered packages and keeps LocalMiniappView from
380
380
  * reaching into runtime internals.
381
381
  */
382
- probeForegroundLiveness(packageName: string, reason = "foreground-open"): void {
382
+ probeForegroundLiveness(packageName: string, reason = "foreground-open", timeoutMs?: number): void {
383
383
  if (!this.registered.has(packageName)) return
384
- this.runtime.probeForegroundLiveness(packageName, reason)
384
+ this.runtime.probeForegroundLiveness(packageName, reason, timeoutMs)
385
385
  }
386
386
 
387
387
  // ----------------------------------------------------------------
@@ -48,6 +48,14 @@ class MicStateCoordinator {
48
48
  // Local miniapp requirements (set when miniapps subscribe to audio streams)
49
49
  private localWantsPcm = false
50
50
  private localWantsLc3 = false
51
+ /**
52
+ * A live ACS call taking the wearer's voice off the glasses over BLE LC3.
53
+ *
54
+ * Tracked separately from the miniapp requirement because the two have independent lifetimes:
55
+ * the call miniapp does not subscribe to `audio_chunk`, and a captions miniapp that stops mid-call
56
+ * must not take the call's microphone with it.
57
+ */
58
+ private callWantsPcm = false
51
59
  private configuredVad: boolean | undefined
52
60
  private configuredLoudnessGate: boolean | undefined
53
61
  private readonly miniappVadOverrides = new Map<string, GateOverride>()
@@ -75,6 +83,29 @@ class MicStateCoordinator {
75
83
  this.applyUnion()
76
84
  }
77
85
 
86
+ /**
87
+ * Claim or release raw PCM on behalf of an active call.
88
+ *
89
+ * Called by AcsMeetingService around a call whose uplink is the glasses microphone over BLE LC3.
90
+ * Releasing is a claim release, not a mic shutdown: if a captions miniapp still wants PCM the
91
+ * microphone stays on, which is the whole reason this is a separate flag rather than a setter on
92
+ * the local requirement.
93
+ */
94
+ public setCallRequirement(pcm: boolean): void {
95
+ if (this.callWantsPcm === pcm) return
96
+ this.callWantsPcm = pcm
97
+ console.log(`${LOG_TAG}: call requirement updated — pcm=${pcm}`)
98
+ this.applyUnion()
99
+ }
100
+
101
+ /**
102
+ * Whether anything on this device needs a continuous raw-PCM timeline. Also the condition that
103
+ * forces hardware VAD off: a gate that drops silence turns a call into clipped half-words.
104
+ */
105
+ private get wantsRawPcm(): boolean {
106
+ return this.localWantsPcm || this.callWantsPcm
107
+ }
108
+
78
109
  /**
79
110
  * Apply a miniapp-owned gate override without changing the OS preference.
80
111
  * Overrides are lifecycle-scoped and last-live-owner-wins independently for
@@ -159,7 +190,7 @@ class MicStateCoordinator {
159
190
  const vadOverride = this.latestOverride(this.miniappVadOverrides)
160
191
  const loudnessOverride = this.latestOverride(this.miniappLoudnessGateOverrides)
161
192
 
162
- if (this.localWantsPcm) {
193
+ if (this.wantsRawPcm) {
163
194
  runtimeSettings.voice_activity_detection_enabled = false
164
195
  } else if (vadOverride) {
165
196
  runtimeSettings.voice_activity_detection_enabled = vadOverride.enabled
@@ -176,7 +207,7 @@ class MicStateCoordinator {
176
207
  * on-device PCM consumers; cloud audio uses LC3 through AudioCloudUplink.
177
208
  */
178
209
  private applyUnion(): void {
179
- const shouldSendPcm = this.localWantsPcm
210
+ const shouldSendPcm = this.wantsRawPcm
180
211
  const shouldSendLc3 = this.localWantsLc3
181
212
 
182
213
  // console.log(
@@ -225,7 +256,7 @@ class MicStateCoordinator {
225
256
 
226
257
  // Hardware VAD suppresses silence. Raw-audio consumers need a continuous
227
258
  // timeline, so their requirement wins over both OS and miniapp VAD values.
228
- if (this.localWantsPcm) patch.voice_activity_detection_enabled = false
259
+ if (this.wantsRawPcm) patch.voice_activity_detection_enabled = false
229
260
  else if (vadOverride) patch.voice_activity_detection_enabled = vadOverride.enabled
230
261
  else if (this.configuredVad !== undefined) patch.voice_activity_detection_enabled = this.configuredVad
231
262
 
@@ -243,6 +274,7 @@ class MicStateCoordinator {
243
274
  public reset(): void {
244
275
  this.localWantsPcm = false
245
276
  this.localWantsLc3 = false
277
+ this.callWantsPcm = false
246
278
  this.applyUnion()
247
279
  }
248
280
 
@@ -102,6 +102,14 @@ export interface StartUnmanagedOptions {
102
102
  /** Optional Bearer token for WHIP Authorization (custom authenticated endpoints). */
103
103
  authToken?: string
104
104
  captureAudio?: boolean
105
+ /**
106
+ * ICE configuration for the glasses' publisher. SoftAP passes `{stun: ""}`, which puts the
107
+ * glasses in host-only mode: there is no route from the hotspot to a STUN server, so a
108
+ * configured one would only add doomed gathering to every call.
109
+ */
110
+ ice?: StreamStartRequest["ice"]
111
+ /** Correlation id echoed by the glasses into their own logs. See softapTrace. */
112
+ traceId?: string
105
113
  }
106
114
 
107
115
  export interface StartManagedOptions {
@@ -364,6 +372,8 @@ export class PhoneStreamCoordinator {
364
372
  ...(opts.audio !== undefined ? {audio: opts.audio} : {}),
365
373
  ...(opts.authToken ? {authToken: opts.authToken} : {}),
366
374
  ...(typeof opts.captureAudio === "boolean" ? {captureAudio: opts.captureAudio} : {}),
375
+ ...(opts.ice !== undefined ? {ice: opts.ice} : {}),
376
+ ...(opts.traceId ? {traceId: opts.traceId} : {}),
367
377
  })
368
378
  const result = publisherStartResult(streamId, event)
369
379
  this.startLifecycle(streamId)