@mentra/engine 3.2.0-dev.115 → 3.2.0-dev.120

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 (65) hide show
  1. package/LICENSE +201 -0
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/engine.js +7 -2
  4. package/build/engine.js.map +1 -1
  5. package/build/generated/releaseMetadata.js +5 -5
  6. package/build/generated/releaseMetadata.js.map +1 -1
  7. package/build/runtime/streamConfig.d.ts +7 -0
  8. package/build/runtime/streamConfig.d.ts.map +1 -1
  9. package/build/runtime/streamConfig.js +7 -0
  10. package/build/runtime/streamConfig.js.map +1 -1
  11. package/build/services/AcsMeetingService.d.ts +154 -0
  12. package/build/services/AcsMeetingService.d.ts.map +1 -0
  13. package/build/services/AcsMeetingService.js +477 -0
  14. package/build/services/AcsMeetingService.js.map +1 -0
  15. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  16. package/build/services/DeviceEventRouter.js +12 -3
  17. package/build/services/DeviceEventRouter.js.map +1 -1
  18. package/build/services/GlassesSettingsSync.d.ts.map +1 -1
  19. package/build/services/GlassesSettingsSync.js +11 -1
  20. package/build/services/GlassesSettingsSync.js.map +1 -1
  21. package/build/services/GlassesStatusProjection.d.ts +12 -0
  22. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  23. package/build/services/GlassesStatusProjection.js +23 -0
  24. package/build/services/GlassesStatusProjection.js.map +1 -1
  25. package/build/services/LocalMiniappRuntime.d.ts +6 -0
  26. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  27. package/build/services/LocalMiniappRuntime.js +130 -1
  28. package/build/services/LocalMiniappRuntime.js.map +1 -1
  29. package/build/services/MentraJSRouter.d.ts.map +1 -1
  30. package/build/services/MentraJSRouter.js +8 -1
  31. package/build/services/MentraJSRouter.js.map +1 -1
  32. package/build/services/PhoneCameraFovCoordinator.d.ts +18 -0
  33. package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
  34. package/build/services/PhoneCameraFovCoordinator.js +36 -0
  35. package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
  36. package/build/services/PhoneStreamCoordinator.d.ts +67 -1
  37. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  38. package/build/services/PhoneStreamCoordinator.js +204 -15
  39. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  40. package/build/services/StreamLifecycleController.d.ts +23 -5
  41. package/build/services/StreamLifecycleController.d.ts.map +1 -1
  42. package/build/services/StreamLifecycleController.js +31 -10
  43. package/build/services/StreamLifecycleController.js.map +1 -1
  44. package/build/services/acsAudioSource.d.ts +24 -0
  45. package/build/services/acsAudioSource.d.ts.map +1 -0
  46. package/build/services/acsAudioSource.js +37 -0
  47. package/build/services/acsAudioSource.js.map +1 -0
  48. package/build/utils/devMiniappLaunch.d.ts.map +1 -1
  49. package/build/utils/devMiniappLaunch.js +4 -0
  50. package/build/utils/devMiniappLaunch.js.map +1 -1
  51. package/package.json +8 -7
  52. package/src/engine.ts +10 -2
  53. package/src/generated/releaseMetadata.ts +5 -5
  54. package/src/runtime/streamConfig.ts +13 -0
  55. package/src/services/AcsMeetingService.ts +570 -0
  56. package/src/services/DeviceEventRouter.ts +12 -3
  57. package/src/services/GlassesSettingsSync.ts +13 -3
  58. package/src/services/GlassesStatusProjection.ts +28 -0
  59. package/src/services/LocalMiniappRuntime.ts +142 -1
  60. package/src/services/MentraJSRouter.ts +7 -1
  61. package/src/services/PhoneCameraFovCoordinator.ts +34 -0
  62. package/src/services/PhoneStreamCoordinator.ts +232 -19
  63. package/src/services/StreamLifecycleController.ts +42 -12
  64. package/src/services/acsAudioSource.ts +53 -0
  65. package/src/utils/devMiniappLaunch.ts +4 -0
@@ -1,11 +1,15 @@
1
1
  /**
2
- * Drives RTMP stream liveliness checks via a keep-alive heartbeat:
2
+ * Drives stream liveliness checks via a keep-alive heartbeat:
3
3
  * `setActive(true)` starts the timer; each tick sends a keep-alive with a
4
4
  * fresh ackId; `maxMissedAcks` consecutive timeouts fire `onTimeout`.
5
5
  *
6
6
  * Phone-owned successor to the retired cloud stream lifecycle controller.
7
7
  * It uses the local `LifecycleLogger` interface because pino is not a phone
8
8
  * dependency.
9
+ *
10
+ * Timers MUST go through BgTimer. React Native pauses plain `setInterval`
11
+ * while MentraOS is backgrounded; glasses WHIP then auto-stops after 60s
12
+ * without `keep_stream_alive`, and Mentra Call recovers by restarting ingest.
9
13
  */
10
14
 
11
15
  export interface LifecycleLogger {
@@ -23,6 +27,13 @@ interface StreamLifecycleCallbacks {
23
27
  onKeepAliveMissed?: (ackId: string, ageMs: number, missedCount: number) => void
24
28
  }
25
29
 
30
+ export interface StreamLifecycleTimerApi {
31
+ setInterval: (callback: () => void, delay: number) => number
32
+ clearInterval: (intervalId: number) => void
33
+ setTimeout: (callback: () => void, delay: number) => number
34
+ clearTimeout: (timeoutId: number) => void
35
+ }
36
+
26
37
  export interface StreamLifecycleOptions {
27
38
  logger: LifecycleLogger
28
39
  streamId: string
@@ -31,21 +42,27 @@ export interface StreamLifecycleOptions {
31
42
  maxMissedAcks: number
32
43
  shouldSendKeepAlive?: () => boolean
33
44
  now?: () => number
45
+ timers?: StreamLifecycleTimerApi
34
46
  }
35
47
 
36
48
  interface PendingAckInfo {
37
49
  sentAt: number
38
- timeout: ReturnType<typeof setTimeout>
50
+ timeout: number
51
+ }
52
+
53
+ function productionTimers(): StreamLifecycleTimerApi {
54
+ // Lazy so unit tests that inject timers never parse react-native.
55
+ const {BgTimer} = require("../utils/timers") as {BgTimer: StreamLifecycleTimerApi}
56
+ return BgTimer
39
57
  }
40
58
 
41
59
  /**
42
- * Drives RTMP stream liveliness checks. Activate to start the heartbeat;
43
- * each tick sends a keep-alive with a fresh ackId and arms a timeout. When
44
- * `maxMissedAcks` consecutive timeouts fire, `onTimeout` is invoked and the
45
- * caller is expected to tear the stream down.
60
+ * Keep-alive heartbeat. Activate to start ticks; each tick sends keep-alive
61
+ * with a fresh ackId and arms a timeout. `maxMissedAcks` consecutive misses
62
+ * fire `onTimeout`, and the caller is expected to tear the stream down.
46
63
  */
47
64
  export class StreamLifecycleController {
48
- private keepAliveTimer?: ReturnType<typeof setInterval>
65
+ private keepAliveTimer?: number
49
66
  private pendingAcks: Map<string, PendingAckInfo> = new Map()
50
67
  private missedAcks = 0
51
68
  private lastActivityMs: number
@@ -59,6 +76,7 @@ export class StreamLifecycleController {
59
76
  private readonly maxMissedAcks: number
60
77
  private readonly shouldSendKeepAlive?: () => boolean
61
78
  private readonly now: () => number
79
+ private readonly timers: StreamLifecycleTimerApi
62
80
 
63
81
  constructor(
64
82
  options: StreamLifecycleOptions,
@@ -71,6 +89,7 @@ export class StreamLifecycleController {
71
89
  this.maxMissedAcks = options.maxMissedAcks
72
90
  this.shouldSendKeepAlive = options.shouldSendKeepAlive
73
91
  this.now = options.now ?? (() => Date.now())
92
+ this.timers = options.timers ?? productionTimers()
74
93
  this.lastActivityMs = this.now()
75
94
  }
76
95
 
@@ -94,6 +113,17 @@ export class StreamLifecycleController {
94
113
  this.missedAcks = 0
95
114
  }
96
115
 
116
+ /**
117
+ * Send one keep-alive immediately instead of waiting for the next interval.
118
+ * Used when the BLE link comes back after a suspension: the glasses
119
+ * publisher's 60s watchdog has been running the whole time, so the first
120
+ * heartbeat after resume must not wait up to another full interval.
121
+ */
122
+ tickNow(): void {
123
+ if (this.disposed || !this.active) return
124
+ void this.tick()
125
+ }
126
+
97
127
  handleAck(ackId: string): void {
98
128
  if (this.disposed) return
99
129
 
@@ -103,7 +133,7 @@ export class StreamLifecycleController {
103
133
  return
104
134
  }
105
135
 
106
- clearTimeout(ackInfo.timeout)
136
+ this.timers.clearTimeout(ackInfo.timeout)
107
137
  this.pendingAcks.delete(ackId)
108
138
  this.recordActivity()
109
139
 
@@ -125,7 +155,7 @@ export class StreamLifecycleController {
125
155
 
126
156
  private startTimer(): void {
127
157
  if (this.keepAliveTimer) return
128
- this.keepAliveTimer = setInterval(() => {
158
+ this.keepAliveTimer = this.timers.setInterval(() => {
129
159
  void this.tick()
130
160
  }, this.keepAliveIntervalMs)
131
161
  this.logger.debug({streamId: this.streamId}, "Keep-alive timer started")
@@ -133,7 +163,7 @@ export class StreamLifecycleController {
133
163
 
134
164
  private stopTimer(): void {
135
165
  if (this.keepAliveTimer) {
136
- clearInterval(this.keepAliveTimer)
166
+ this.timers.clearInterval(this.keepAliveTimer)
137
167
  this.keepAliveTimer = undefined
138
168
  this.logger.debug({streamId: this.streamId}, "Keep-alive timer stopped")
139
169
  }
@@ -153,7 +183,7 @@ export class StreamLifecycleController {
153
183
  const ackId = this.createAckId()
154
184
  const sentAt = this.now()
155
185
 
156
- const timeout = setTimeout(() => {
186
+ const timeout = this.timers.setTimeout(() => {
157
187
  this.onAckTimeout(ackId, sentAt)
158
188
  }, this.ackTimeoutMs)
159
189
 
@@ -202,7 +232,7 @@ export class StreamLifecycleController {
202
232
 
203
233
  private clearPendingAcks(): void {
204
234
  for (const {timeout} of this.pendingAcks.values()) {
205
- clearTimeout(timeout)
235
+ this.timers.clearTimeout(timeout)
206
236
  }
207
237
  this.pendingAcks.clear()
208
238
  }
@@ -0,0 +1,53 @@
1
+ export type SourceReason =
2
+ | "explicit"
3
+ | "current-mic"
4
+ | "ranking"
5
+ | "fallback-glasses-connected"
6
+ | "fallback-no-glasses"
7
+
8
+ export type AcsAudioSource = "glasses" | "phone"
9
+
10
+ /**
11
+ * Single switch for Mentra Call ACS uplink.
12
+ * `"phone"` = AudioRecord / iOS input tap → RawOutgoingAudioStream (no Cloudflare audio).
13
+ * `"glasses"` = today's glasses WHIP → Cloudflare → WHEP PCM → ACS path.
14
+ * preferred_mic does not govern this call.
15
+ */
16
+ export const ACS_CALL_MIC: AcsAudioSource = "glasses"
17
+
18
+ export type ResolvedAudioSource = {
19
+ source: AcsAudioSource
20
+ reason: SourceReason
21
+ }
22
+
23
+ function mapMic(value: string | null | undefined): AcsAudioSource | null {
24
+ if (value === "phone" || value === "bluetooth" || value === "bluetoothClassic") return "phone"
25
+ if (value === "glasses") return "glasses"
26
+ return null
27
+ }
28
+
29
+ /**
30
+ * Pure preferred_mic → ACS source mapping. Store reads stay in the thin wrapper
31
+ * so this table can be unit tested without zustand.
32
+ */
33
+ export function resolveAudioSource(input: {
34
+ preferred: string
35
+ currentMic: string | null
36
+ micRanking: string[]
37
+ glassesConnected: boolean
38
+ }): ResolvedAudioSource {
39
+ const preferred = input.preferred || "auto"
40
+ if (preferred === "phone" || preferred === "bluetooth") {
41
+ return {source: "phone", reason: "explicit"}
42
+ }
43
+ if (preferred === "glasses") {
44
+ return {source: "glasses", reason: "explicit"}
45
+ }
46
+ const fromCurrent = mapMic(input.currentMic)
47
+ if (fromCurrent) return {source: fromCurrent, reason: "current-mic"}
48
+ const fromRank = mapMic(input.micRanking[0])
49
+ if (fromRank) return {source: fromRank, reason: "ranking"}
50
+ return input.glassesConnected
51
+ ? {source: "glasses", reason: "fallback-glasses-connected"}
52
+ : {source: "phone", reason: "fallback-no-glasses"}
53
+ }
@@ -121,6 +121,10 @@ function collectAlternateHosts(packageName: string, extra?: string[]): string[]
121
121
  }
122
122
 
123
123
  for (const h of extra ?? []) push(h)
124
+ // USB `adb reverse` publishes the laptop on the phone's loopback. Mentra
125
+ // Call's QR still advertises the Wi-Fi IP, which AP isolation drops. Try
126
+ // loopback last so LAN/mDNS keep winning when they work.
127
+ push("127.0.0.1")
124
128
  return hosts
125
129
  }
126
130