@mentra/engine 3.2.0-dev.213 → 3.2.0-dev.214
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/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +15 -22
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/PhoneCameraFovCoordinator.d.ts +4 -1
- package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
- package/build/services/PhoneCameraFovCoordinator.js +63 -43
- package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts +2 -0
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +11 -0
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/slimStreamStatus.d.ts.map +1 -1
- package/build/services/slimStreamStatus.js +2 -0
- package/build/services/slimStreamStatus.js.map +1 -1
- package/package.json +7 -7
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/services/LocalMiniappRuntime.ts +17 -25
- package/src/services/PhoneCameraFovCoordinator.ts +65 -41
- package/src/services/PhoneStreamCoordinator.ts +15 -1
- package/src/services/slimStreamStatus.ts +1 -0
|
@@ -12,6 +12,7 @@ interface OverrideEntry {
|
|
|
12
12
|
fov: number
|
|
13
13
|
roiPosition: "center" | "bottom" | "top"
|
|
14
14
|
order: number
|
|
15
|
+
releasing?: boolean
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
let leaseCounter = 0
|
|
@@ -41,6 +42,11 @@ export class PhoneCameraFovCoordinator {
|
|
|
41
42
|
|
|
42
43
|
constructor(private readonly legacyRestartSettleMs = LEGACY_CAMERA_RESTART_SETTLE_MS) {}
|
|
43
44
|
|
|
45
|
+
/** Capture a barrier for FOV work already submitted, excluding future requests. */
|
|
46
|
+
whenSettled(): Promise<void> {
|
|
47
|
+
return this.queue.then(() => undefined)
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
/** Report-safe FOV ownership snapshot for incident diagnostics. */
|
|
45
51
|
getDiagnosticSnapshot(): Record<string, unknown> {
|
|
46
52
|
return {
|
|
@@ -114,6 +120,11 @@ export class PhoneCameraFovCoordinator {
|
|
|
114
120
|
if (!leaseId) return
|
|
115
121
|
const entry = [...this.overrides.values()].find((candidate) => candidate.leaseId === leaseId)
|
|
116
122
|
if (!entry) return
|
|
123
|
+
if (entry.releasing) {
|
|
124
|
+
const owner = [...this.overrides.entries()].find(([, candidate]) => candidate === entry)![0]
|
|
125
|
+
await this.releaseOverride(owner)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
117
128
|
await BluetoothSdk.setCameraFovOverride({
|
|
118
129
|
leaseId,
|
|
119
130
|
fov: entry.fov,
|
|
@@ -124,56 +135,69 @@ export class PhoneCameraFovCoordinator {
|
|
|
124
135
|
})
|
|
125
136
|
}
|
|
126
137
|
|
|
127
|
-
releaseForApp(packageName: string): Promise<void> {
|
|
138
|
+
releaseForApp(packageName: string, cameraCleanup: Promise<void> = Promise.resolve()): Promise<void> {
|
|
139
|
+
// Reserve the queue slot now, before waiting for capture teardown. A respawn
|
|
140
|
+
// must not install a new crop that this old incarnation's cleanup releases.
|
|
128
141
|
return this.enqueue(async () => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return
|
|
134
|
-
}
|
|
142
|
+
await cameraCleanup
|
|
143
|
+
await this.releaseOverride(packageName)
|
|
144
|
+
})
|
|
145
|
+
}
|
|
135
146
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
return
|
|
157
|
-
}
|
|
147
|
+
private async releaseOverride(packageName: string): Promise<void> {
|
|
148
|
+
const removed = this.overrides.get(packageName)
|
|
149
|
+
if (!removed) return
|
|
150
|
+
if (removed.leaseId !== this.effectiveLeaseId) {
|
|
151
|
+
this.overrides.delete(packageName)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// A failed release must not renew the closing owner's lease forever. Keep
|
|
156
|
+
// enough identity to retry on reconnect, but let ASG's existing TTL restore
|
|
157
|
+
// the saved crop if the phone cannot complete cleanup (including camera_busy).
|
|
158
|
+
removed.releasing = true
|
|
159
|
+
this.clearRefresh()
|
|
160
|
+
|
|
161
|
+
const next = [...this.overrides.entries()]
|
|
162
|
+
.filter(([candidatePackage, entry]) => candidatePackage !== packageName && !entry.releasing)
|
|
163
|
+
.map(([, entry]) => entry)
|
|
164
|
+
.sort((a, b) => b.order - a.order)[0]
|
|
165
|
+
if (this.legacyMode) {
|
|
158
166
|
if (next) {
|
|
159
|
-
await
|
|
160
|
-
leaseId: next.leaseId,
|
|
161
|
-
fov: next.fov,
|
|
162
|
-
roiPosition: next.roiPosition,
|
|
163
|
-
ttlMs: CAMERA_FOV_OVERRIDE_TTL_MS,
|
|
164
|
-
})
|
|
165
|
-
// Commit phone-side ownership only after ASG accepted the replacement. On failure the
|
|
166
|
-
// closing app remains effective here, allowing unregister/reconnect cleanup to retry.
|
|
167
|
+
await this.applyLegacyFov({fov: next.fov, roiPosition: next.roiPosition})
|
|
167
168
|
this.overrides.delete(packageName)
|
|
168
169
|
this.effectiveLeaseId = next.leaseId
|
|
169
|
-
this.scheduleRefresh()
|
|
170
170
|
} else {
|
|
171
|
-
|
|
171
|
+
// Legacy commands are one-way and have no lease-release protocol.
|
|
172
|
+
// Ask native to replay its unchanged persistent base setting, then
|
|
173
|
+
// probe the modern override path again for the next ownership cycle.
|
|
174
|
+
await BluetoothSdk.restoreLegacyCameraFov()
|
|
175
|
+
await this.waitForLegacyCameraRestart()
|
|
172
176
|
this.overrides.delete(packageName)
|
|
173
177
|
this.clearRefresh()
|
|
174
178
|
this.effectiveLeaseId = undefined
|
|
179
|
+
this.legacyMode = false
|
|
175
180
|
}
|
|
176
|
-
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
if (next) {
|
|
184
|
+
await BluetoothSdk.setCameraFovOverride({
|
|
185
|
+
leaseId: next.leaseId,
|
|
186
|
+
fov: next.fov,
|
|
187
|
+
roiPosition: next.roiPosition,
|
|
188
|
+
ttlMs: CAMERA_FOV_OVERRIDE_TTL_MS,
|
|
189
|
+
})
|
|
190
|
+
// Commit phone-side ownership only after ASG accepted the replacement. On failure the
|
|
191
|
+
// closing app remains effective here, allowing unregister/reconnect cleanup to retry.
|
|
192
|
+
this.overrides.delete(packageName)
|
|
193
|
+
this.effectiveLeaseId = next.leaseId
|
|
194
|
+
this.scheduleRefresh()
|
|
195
|
+
} else {
|
|
196
|
+
await BluetoothSdk.releaseCameraFovOverride(removed.leaseId)
|
|
197
|
+
this.overrides.delete(packageName)
|
|
198
|
+
this.clearRefresh()
|
|
199
|
+
this.effectiveLeaseId = undefined
|
|
200
|
+
}
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
private scheduleRefresh(): void {
|
|
@@ -185,7 +209,7 @@ export class PhoneCameraFovCoordinator {
|
|
|
185
209
|
void this.enqueue(async () => {
|
|
186
210
|
if (this.effectiveLeaseId !== leaseId) return
|
|
187
211
|
const entry = [...this.overrides.values()].find((candidate) => candidate.leaseId === leaseId)
|
|
188
|
-
if (!entry) return
|
|
212
|
+
if (!entry || entry.releasing) return
|
|
189
213
|
await BluetoothSdk.setCameraFovOverride({
|
|
190
214
|
leaseId,
|
|
191
215
|
fov: entry.fov,
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
37
37
|
import type {StreamResolvedConfig, StreamStartRequest, StreamStatusEvent} from "@mentra/bluetooth-sdk/internal"
|
|
38
38
|
import {isGlassesConnected} from "./GlassesReadiness"
|
|
39
|
+
import {phoneCameraFovCoordinator} from "./PhoneCameraFovCoordinator"
|
|
39
40
|
import {useGlassesStore} from "../stores/glasses"
|
|
40
41
|
|
|
41
42
|
import {slimStreamStatusEvent, streamStatusSignature} from "./slimStreamStatus"
|
|
@@ -210,6 +211,7 @@ export class PhoneStreamCoordinator {
|
|
|
210
211
|
private idCounter = 0
|
|
211
212
|
private readonly timings: TimingConfig
|
|
212
213
|
private readonly linkSource: GlassesLinkSource
|
|
214
|
+
private readonly pendingCameraChanges: () => Promise<void>
|
|
213
215
|
private unsubscribeLink: (() => void) | null = null
|
|
214
216
|
private suspended: SuspendedState | null = null
|
|
215
217
|
/**
|
|
@@ -233,9 +235,13 @@ export class PhoneStreamCoordinator {
|
|
|
233
235
|
/** Send full resolvedConfig only once per stream session. */
|
|
234
236
|
private resolvedConfigForwarded = false
|
|
235
237
|
|
|
236
|
-
constructor(
|
|
238
|
+
constructor(
|
|
239
|
+
timings: CoordinatorTimings = {},
|
|
240
|
+
deps: {linkSource?: GlassesLinkSource; pendingCameraChanges?: () => Promise<void>} = {},
|
|
241
|
+
) {
|
|
237
242
|
this.timings = {...DEFAULT_TIMINGS, ...timings}
|
|
238
243
|
this.linkSource = deps.linkSource ?? storeLinkSource
|
|
244
|
+
this.pendingCameraChanges = deps.pendingCameraChanges ?? (() => phoneCameraFovCoordinator.whenSettled())
|
|
239
245
|
}
|
|
240
246
|
|
|
241
247
|
/**
|
|
@@ -322,7 +328,12 @@ export class PhoneStreamCoordinator {
|
|
|
322
328
|
throw new StreamConflictError("STREAM_URL_REQUIRED", "streamUrl is required")
|
|
323
329
|
}
|
|
324
330
|
this.assertGlassesConnected()
|
|
331
|
+
// Capture before entering the stream queue: a later FOV release may itself
|
|
332
|
+
// await stop(), and must not become a circular dependency of this start.
|
|
333
|
+
const cameraReady = this.pendingCameraChanges()
|
|
325
334
|
return this.runExclusive(async () => {
|
|
335
|
+
await cameraReady
|
|
336
|
+
this.assertGlassesConnected()
|
|
326
337
|
if (this.current) {
|
|
327
338
|
throw new StreamConflictError(
|
|
328
339
|
"STREAM_ALREADY_ACTIVE",
|
|
@@ -365,6 +376,7 @@ export class PhoneStreamCoordinator {
|
|
|
365
376
|
}
|
|
366
377
|
|
|
367
378
|
async startManaged(packageName: string, opts: StartManagedOptions): Promise<ManagedStartResult> {
|
|
379
|
+
const cameraReady = this.pendingCameraChanges()
|
|
368
380
|
const startupStartedAtMs = Date.now()
|
|
369
381
|
// streamId doesn't exist yet — it's minted a few lines below, once we
|
|
370
382
|
// know this is a fresh provision rather than a join onto an existing one.
|
|
@@ -383,6 +395,8 @@ export class PhoneStreamCoordinator {
|
|
|
383
395
|
| {kind: "fresh"; entry: ManagedEntry}
|
|
384
396
|
|
|
385
397
|
const decision = await this.runExclusive(async (): Promise<JoinDecision> => {
|
|
398
|
+
await cameraReady
|
|
399
|
+
this.assertGlassesConnected()
|
|
386
400
|
if (this.current && this.current.kind === "unmanaged") {
|
|
387
401
|
throw new StreamConflictError(
|
|
388
402
|
"STREAM_ALREADY_ACTIVE",
|
|
@@ -26,6 +26,7 @@ export function slimStreamStatusEvent(
|
|
|
26
26
|
if (event.sid) slim.sid = event.sid
|
|
27
27
|
if (typeof event.revision === "number") slim.revision = event.revision
|
|
28
28
|
if (typeof event.terminal === "boolean") slim.terminal = event.terminal
|
|
29
|
+
if ("willRetry" in event && typeof event.willRetry === "boolean") slim.willRetry = event.willRetry
|
|
29
30
|
if (event.errorDetails) slim.errorDetails = event.errorDetails
|
|
30
31
|
if (event.kind === "reconnect") {
|
|
31
32
|
if ("reason" in event) slim.reason = event.reason
|