@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.
- package/LICENSE +201 -0
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +7 -2
- package/build/engine.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/runtime/streamConfig.d.ts +7 -0
- package/build/runtime/streamConfig.d.ts.map +1 -1
- package/build/runtime/streamConfig.js +7 -0
- package/build/runtime/streamConfig.js.map +1 -1
- package/build/services/AcsMeetingService.d.ts +154 -0
- package/build/services/AcsMeetingService.d.ts.map +1 -0
- package/build/services/AcsMeetingService.js +477 -0
- package/build/services/AcsMeetingService.js.map +1 -0
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +12 -3
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/GlassesSettingsSync.d.ts.map +1 -1
- package/build/services/GlassesSettingsSync.js +11 -1
- package/build/services/GlassesSettingsSync.js.map +1 -1
- package/build/services/GlassesStatusProjection.d.ts +12 -0
- package/build/services/GlassesStatusProjection.d.ts.map +1 -1
- package/build/services/GlassesStatusProjection.js +23 -0
- package/build/services/GlassesStatusProjection.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +6 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +130 -1
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MentraJSRouter.d.ts.map +1 -1
- package/build/services/MentraJSRouter.js +8 -1
- package/build/services/MentraJSRouter.js.map +1 -1
- package/build/services/PhoneCameraFovCoordinator.d.ts +18 -0
- package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
- package/build/services/PhoneCameraFovCoordinator.js +36 -0
- package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts +67 -1
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +204 -15
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/StreamLifecycleController.d.ts +23 -5
- package/build/services/StreamLifecycleController.d.ts.map +1 -1
- package/build/services/StreamLifecycleController.js +31 -10
- package/build/services/StreamLifecycleController.js.map +1 -1
- package/build/services/acsAudioSource.d.ts +24 -0
- package/build/services/acsAudioSource.d.ts.map +1 -0
- package/build/services/acsAudioSource.js +37 -0
- package/build/services/acsAudioSource.js.map +1 -0
- package/build/utils/devMiniappLaunch.d.ts.map +1 -1
- package/build/utils/devMiniappLaunch.js +4 -0
- package/build/utils/devMiniappLaunch.js.map +1 -1
- package/package.json +8 -7
- package/src/engine.ts +10 -2
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/runtime/streamConfig.ts +13 -0
- package/src/services/AcsMeetingService.ts +570 -0
- package/src/services/DeviceEventRouter.ts +12 -3
- package/src/services/GlassesSettingsSync.ts +13 -3
- package/src/services/GlassesStatusProjection.ts +28 -0
- package/src/services/LocalMiniappRuntime.ts +142 -1
- package/src/services/MentraJSRouter.ts +7 -1
- package/src/services/PhoneCameraFovCoordinator.ts +34 -0
- package/src/services/PhoneStreamCoordinator.ts +232 -19
- package/src/services/StreamLifecycleController.ts +42 -12
- package/src/services/acsAudioSource.ts +53 -0
- package/src/utils/devMiniappLaunch.ts +4 -0
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Drives
|
|
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:
|
|
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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* `
|
|
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?:
|
|
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
|
|