@mentra/engine 3.2.0-dev.217 → 3.2.0-dev.222

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 (87) 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 +3 -1
  4. package/build/index.d.ts.map +1 -1
  5. package/build/index.js +2 -1
  6. package/build/index.js.map +1 -1
  7. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
  8. package/build/react/MentraLiveOtaFlow.js +19 -3
  9. package/build/react/MentraLiveOtaFlow.js.map +1 -1
  10. package/build/react/useMentraLiveOta.d.ts +12 -0
  11. package/build/react/useMentraLiveOta.d.ts.map +1 -1
  12. package/build/react/useMentraLiveOta.js +31 -5
  13. package/build/react/useMentraLiveOta.js.map +1 -1
  14. package/build/services/AcsMeetingService.d.ts +360 -10
  15. package/build/services/AcsMeetingService.d.ts.map +1 -1
  16. package/build/services/AcsMeetingService.js +836 -19
  17. package/build/services/AcsMeetingService.js.map +1 -1
  18. package/build/services/AudioPlaybackService.d.ts +6 -0
  19. package/build/services/AudioPlaybackService.d.ts.map +1 -1
  20. package/build/services/AudioPlaybackService.js +4 -3
  21. package/build/services/AudioPlaybackService.js.map +1 -1
  22. package/build/services/GlassesMicProbe.d.ts +78 -0
  23. package/build/services/GlassesMicProbe.d.ts.map +1 -0
  24. package/build/services/GlassesMicProbe.js +258 -0
  25. package/build/services/GlassesMicProbe.js.map +1 -0
  26. package/build/services/LocalMiniappRuntime.d.ts +83 -0
  27. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  28. package/build/services/LocalMiniappRuntime.js +714 -39
  29. package/build/services/LocalMiniappRuntime.js.map +1 -1
  30. package/build/services/MentraJSLogPipeline.d.ts +39 -0
  31. package/build/services/MentraJSLogPipeline.d.ts.map +1 -1
  32. package/build/services/MentraJSLogPipeline.js +59 -3
  33. package/build/services/MentraJSLogPipeline.js.map +1 -1
  34. package/build/services/MentraJSRouter.d.ts +1 -1
  35. package/build/services/MentraJSRouter.d.ts.map +1 -1
  36. package/build/services/MentraJSRouter.js +2 -2
  37. package/build/services/MentraJSRouter.js.map +1 -1
  38. package/build/services/MicStateCoordinator.d.ts +22 -0
  39. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  40. package/build/services/MicStateCoordinator.js +34 -3
  41. package/build/services/MicStateCoordinator.js.map +1 -1
  42. package/build/services/OtaErrorMapping.d.ts +25 -2
  43. package/build/services/OtaErrorMapping.d.ts.map +1 -1
  44. package/build/services/OtaErrorMapping.js +68 -23
  45. package/build/services/OtaErrorMapping.js.map +1 -1
  46. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  47. package/build/services/OtaInstallCoordinator.js +6 -1
  48. package/build/services/OtaInstallCoordinator.js.map +1 -1
  49. package/build/services/PhoneStreamCoordinator.d.ts +8 -0
  50. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  51. package/build/services/PhoneStreamCoordinator.js +2 -0
  52. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  53. package/build/services/SoftapCallTransport.d.ts +365 -0
  54. package/build/services/SoftapCallTransport.d.ts.map +1 -0
  55. package/build/services/SoftapCallTransport.js +722 -0
  56. package/build/services/SoftapCallTransport.js.map +1 -0
  57. package/build/services/SoftapCleanupBarrier.d.ts +45 -0
  58. package/build/services/SoftapCleanupBarrier.d.ts.map +1 -0
  59. package/build/services/SoftapCleanupBarrier.js +51 -0
  60. package/build/services/SoftapCleanupBarrier.js.map +1 -0
  61. package/build/utils/pcm16.d.ts +35 -0
  62. package/build/utils/pcm16.d.ts.map +1 -0
  63. package/build/utils/pcm16.js +85 -0
  64. package/build/utils/pcm16.js.map +1 -0
  65. package/build/utils/softapTrace.d.ts +39 -0
  66. package/build/utils/softapTrace.d.ts.map +1 -0
  67. package/build/utils/softapTrace.js +124 -0
  68. package/build/utils/softapTrace.js.map +1 -0
  69. package/package.json +7 -7
  70. package/src/generated/releaseMetadata.ts +5 -5
  71. package/src/index.ts +8 -0
  72. package/src/react/MentraLiveOtaFlow.tsx +22 -3
  73. package/src/react/useMentraLiveOta.ts +44 -6
  74. package/src/services/AcsMeetingService.ts +956 -22
  75. package/src/services/AudioPlaybackService.ts +12 -3
  76. package/src/services/GlassesMicProbe.ts +300 -0
  77. package/src/services/LocalMiniappRuntime.ts +775 -42
  78. package/src/services/MentraJSLogPipeline.ts +70 -3
  79. package/src/services/MentraJSRouter.ts +2 -2
  80. package/src/services/MicStateCoordinator.ts +35 -3
  81. package/src/services/OtaErrorMapping.ts +77 -25
  82. package/src/services/OtaInstallCoordinator.ts +6 -1
  83. package/src/services/PhoneStreamCoordinator.ts +10 -0
  84. package/src/services/SoftapCallTransport.ts +928 -0
  85. package/src/services/SoftapCleanupBarrier.ts +72 -0
  86. package/src/utils/pcm16.ts +93 -0
  87. 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
 
@@ -1,7 +1,79 @@
1
1
  import type {OtaProgress, OtaStatus} from "../facades/ota"
2
2
 
3
- export const BES_INSTALL_RESTART_MESSAGE =
4
- "Restart your glasses to safely exit firmware update mode before trying again"
3
+ /**
4
+ * Copy keys for OTA failures the glasses report by code (`ota_status.error`), plus the
5
+ * phone-side BES restart instruction. Hosts translate these keys; the engine's own
6
+ * English copy for each key lives in `OTA_ERROR_ENGLISH_COPY`.
7
+ *
8
+ * The glasses-side producers are `sendProgressToPhone(..., "FAILED", errorCode)` in the
9
+ * ASG client's OtaHelper (download classification, install failures, and the downgrade
10
+ * handoff to the recovery worker). Keep this table in sync with those codes; anything
11
+ * else falls back to `OTA_ERROR_UNKNOWN_GLASSES_COPY_KEY` and the raw code is shown in a
12
+ * secondary line so support can still identify it.
13
+ */
14
+ export const OTA_GLASSES_ERROR_COPY_KEYS: Readonly<Record<string, string>> = {
15
+ no_internet: "ota:errorNoInternet",
16
+ clock_skew: "ota:errorClockSkew",
17
+ ssl_error: "ota:errorSslError",
18
+ download_failed: "ota:errorDownloadFailed",
19
+ insufficient_storage: "ota:errorInsufficientStorage",
20
+ firmware_too_large: "ota:errorFirmwareTooLarge",
21
+ firmware_verify_failed: "ota:errorFirmwareVerifyFailed",
22
+ apk_verify_failed: "ota:errorApkVerifyFailed",
23
+ install_failed: "ota:errorInstallFailed",
24
+ apk_restart_guard_not_persisted: "ota:errorApkRestartGuardNotPersisted",
25
+ downgrade_handoff_refused: "ota:errorDowngradeHandoffRefused",
26
+ downgrade_handoff_failed: "ota:errorDowngradeHandoffFailed",
27
+ downgrade_transaction_stalled: "ota:errorDowngradeTransactionStalled",
28
+ }
29
+
30
+ export const OTA_ERROR_UNKNOWN_GLASSES_COPY_KEY = "ota:errorGlassesUnknown"
31
+ export const OTA_ERROR_GENERIC_COPY_KEY = "ota:errorGeneric"
32
+ export const OTA_ERROR_BES_RESTART_REQUIRED_COPY_KEY = "ota:errorBesRestartRequired"
33
+
34
+ /** English copy for every OTA error key above. Mirrored in the Mentra App's `ota` i18n namespace. */
35
+ export const OTA_ERROR_ENGLISH_COPY: Readonly<Record<string, string>> = {
36
+ "ota:errorNoInternet": "Glasses WiFi has no internet connection",
37
+ "ota:errorClockSkew": "Glasses clock is wrong — syncing time from your phone, then retrying update check",
38
+ "ota:errorSslError": "Secure connection failed — try a different WiFi network",
39
+ "ota:errorDownloadFailed": "Download failed — check glasses WiFi connection",
40
+ "ota:errorInsufficientStorage": "Not enough storage on your glasses — free up space before trying again",
41
+ "ota:errorFirmwareTooLarge": "Firmware file is unexpectedly large — please contact support",
42
+ "ota:errorFirmwareVerifyFailed": "Firmware verification failed — please try again or contact support",
43
+ "ota:errorApkVerifyFailed": "Update verification failed — please try again or contact support",
44
+ "ota:errorInstallFailed": "Install failed — please try again",
45
+ "ota:errorApkRestartGuardNotPersisted":
46
+ "Your glasses could not save the update before restarting. Restart your glasses and try again.",
47
+ "ota:errorDowngradeHandoffRefused":
48
+ "Your glasses could not start the version change. Restart your glasses and try again.",
49
+ "ota:errorDowngradeHandoffFailed":
50
+ "The recovery service on your glasses did not respond. Restart your glasses and try again.",
51
+ "ota:errorDowngradeTransactionStalled":
52
+ "The version change on your glasses did not finish. Restart your glasses and try again.",
53
+ "ota:errorGlassesUnknown": "Your glasses reported an unexpected error. Restart your glasses and try again.",
54
+ "ota:errorGeneric": "Update failed",
55
+ "ota:errorBesRestartRequired": "Restart your glasses to safely exit firmware update mode before trying again",
56
+ "ota:errorCode": "Error code: {{code}}",
57
+ }
58
+
59
+ export const BES_INSTALL_RESTART_MESSAGE = OTA_ERROR_ENGLISH_COPY[OTA_ERROR_BES_RESTART_REQUIRED_COPY_KEY]
60
+
61
+ /**
62
+ * Copy key for a glasses-reported failure code. Unknown codes get the generic
63
+ * glasses-error copy; a missing code gets the plain generic copy.
64
+ */
65
+ export function otaErrorCopyKey(error?: string | null): string {
66
+ if (!error) return OTA_ERROR_GENERIC_COPY_KEY
67
+ // Own-property lookup: a code that happens to name an inherited Object member
68
+ // ("constructor", "toString", ...) is unknown, not a mapped key.
69
+ return hasOwn(OTA_GLASSES_ERROR_COPY_KEYS, error)
70
+ ? OTA_GLASSES_ERROR_COPY_KEYS[error]
71
+ : OTA_ERROR_UNKNOWN_GLASSES_COPY_KEY
72
+ }
73
+
74
+ function hasOwn(table: Readonly<Record<string, string>>, key: string): boolean {
75
+ return Object.prototype.hasOwnProperty.call(table, key)
76
+ }
5
77
 
6
78
  function isDownloadPhaseSnapshot(
7
79
  otaStatus: OtaStatus | null | undefined,
@@ -43,29 +115,9 @@ export function shouldShowChangeWifiForOtaDownloadFailure(
43
115
  return false
44
116
  }
45
117
 
46
- export function getOtaErrorMessage(error?: string): string {
47
- switch (error) {
48
- case "no_internet":
49
- return "Glasses WiFi has no internet connection"
50
- case "clock_skew":
51
- return "Glasses clock is wrong — syncing time from your phone, then retrying update check"
52
- case "ssl_error":
53
- return "Secure connection failed — try a different WiFi network"
54
- case "download_failed":
55
- return "Download failed — check glasses WiFi connection"
56
- case "insufficient_storage":
57
- return "Not enough storage on your glasses — free up space before trying again"
58
- case "firmware_too_large":
59
- return "Firmware file is unexpectedly large — please contact support"
60
- case "firmware_verify_failed":
61
- return "Firmware verification failed — please try again or contact support"
62
- case "apk_verify_failed":
63
- return "Update verification failed — please try again or contact support"
64
- case "install_failed":
65
- return "Install failed — please try again"
66
- default:
67
- return error || "Update failed"
68
- }
118
+ /** English message for a glasses-reported failure code. Never echoes the raw code. */
119
+ export function getOtaErrorMessage(error?: string | null): string {
120
+ return OTA_ERROR_ENGLISH_COPY[otaErrorCopyKey(error)]
69
121
  }
70
122
 
71
123
  /**
@@ -853,7 +853,12 @@ class OtaInstallCoordinator {
853
853
  // suppress ota_start and hang the fresh downgrade until the global timeout).
854
854
  this.otaStartOwnership?.outcome === "acknowledged" &&
855
855
  otaStatus?.stepType === "apk" &&
856
- otaStatus.phase === "install"
856
+ otaStatus.phase === "install" &&
857
+ // Only a live install arms the latch. A terminal failed status keeps stepType/phase
858
+ // apk/install and stays in the store, so without this gate every later pass (any
859
+ // unrelated store change) would re-arm the latch and the non-ownership block below
860
+ // would release it again, flooding the log with paired enter/release lines.
861
+ otaStatus.status === "in_progress"
857
862
  ) {
858
863
  console.log("[OTA_PROGRESS] version-change: apk install started — entering detour wait")
859
864
  this.versionChangeInstallStarted = true
@@ -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)