@mentra/engine 3.2.0-dev.248 → 3.2.0-dev.250
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/AcsMeetingService.d.ts +9 -0
- package/build/services/AcsMeetingService.d.ts.map +1 -1
- package/build/services/AcsMeetingService.js +42 -0
- package/build/services/AcsMeetingService.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +4 -0
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/SoftapCallTransport.d.ts +30 -0
- package/build/services/SoftapCallTransport.d.ts.map +1 -1
- package/build/services/SoftapCallTransport.js +82 -0
- package/build/services/SoftapCallTransport.js.map +1 -1
- package/package.json +8 -8
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/services/AcsMeetingService.ts +40 -0
- package/src/services/LocalMiniappRuntime.ts +4 -0
- package/src/services/SoftapCallTransport.ts +91 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/engine",
|
|
3
|
-
"version": "3.2.0-dev.
|
|
3
|
+
"version": "3.2.0-dev.250",
|
|
4
4
|
"description": "Mentra Engine — miniapp registry, WebView bridge, and miniapp store",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -97,13 +97,13 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@mentra/acs-meeting": "3.2.0-dev.
|
|
101
|
-
"@mentra/glasses-media": "3.2.0-dev.
|
|
102
|
-
"@mentra/bluetooth-sdk": "3.2.0-dev.
|
|
103
|
-
"@mentra/cloud-client": "3.2.0-dev.
|
|
104
|
-
"@mentra/cloud-protocol": "3.2.0-dev.
|
|
105
|
-
"@mentra/crust": "3.2.0-dev.
|
|
106
|
-
"@mentra/miniapp": "3.2.0-dev.
|
|
100
|
+
"@mentra/acs-meeting": "3.2.0-dev.250",
|
|
101
|
+
"@mentra/glasses-media": "3.2.0-dev.250",
|
|
102
|
+
"@mentra/bluetooth-sdk": "3.2.0-dev.250",
|
|
103
|
+
"@mentra/cloud-client": "3.2.0-dev.250",
|
|
104
|
+
"@mentra/cloud-protocol": "3.2.0-dev.250",
|
|
105
|
+
"@mentra/crust": "3.2.0-dev.250",
|
|
106
|
+
"@mentra/miniapp": "3.2.0-dev.250",
|
|
107
107
|
"buffer": "^6.0.3",
|
|
108
108
|
"events": "^3.3.0",
|
|
109
109
|
"react-native-marked": "8.1.1",
|
|
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
|
|
|
12
12
|
export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "3.2.0",
|
|
15
|
-
"releaseIdentity": "3.2.0-dev.
|
|
16
|
-
"releaseSetId": "mentra-3.2.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.2.0-dev.250",
|
|
16
|
+
"releaseSetId": "mentra-3.2.0-dev.250",
|
|
17
|
+
"sourceCommit": "3707f9ec51f41355d71e1783e2783bd0e451c9e6",
|
|
18
|
+
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.250.json",
|
|
19
|
+
"otaManifestSha256": "39fa2567b9b83b52200f266058ed23f44633fa3239a6f3659a1afbee820d4523"
|
|
20
20
|
})
|
|
@@ -569,6 +569,8 @@ class AcsMeetingService {
|
|
|
569
569
|
private lastMediaRestartAt = 0
|
|
570
570
|
/** Callers parked in [waitForFirstFrame], woken by the next `mediaSource` verdict. */
|
|
571
571
|
private readonly firstFrameWaiters = new Set<(error?: Error) => void>()
|
|
572
|
+
/** Mid-call republish waiters: live resolves true, leave/timeout resolves false. Failed is ignored. */
|
|
573
|
+
private readonly mediaLiveWaiters = new Set<(live: boolean) => void>()
|
|
572
574
|
private scopedLostSub: {remove: () => void} | null = null
|
|
573
575
|
private readonly scopedLostListeners = new Set<(error: {code: string; message: string}) => void>()
|
|
574
576
|
/**
|
|
@@ -837,6 +839,42 @@ class AcsMeetingService {
|
|
|
837
839
|
if (mediaSource !== "live" && mediaSource !== "failed") return
|
|
838
840
|
const error = mediaSource === "failed" ? new Error("The glasses video feed failed") : undefined
|
|
839
841
|
for (const settle of [...this.firstFrameWaiters]) settle(error)
|
|
842
|
+
if (mediaSource === "live") this.settleMediaLiveWaiters(true)
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
private settleMediaLiveWaiters(live: boolean): void {
|
|
846
|
+
for (const settle of [...this.mediaLiveWaiters]) settle(live)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Mid-call SoftAP camera recovery. Resolves `true` only when ingest is live again.
|
|
851
|
+
* A standing `failed` is why we republished, so it must not abort the wait the way
|
|
852
|
+
* [waitForFirstFrame] does on join.
|
|
853
|
+
*/
|
|
854
|
+
waitUntilMediaLive(timeoutMs: number): Promise<boolean> {
|
|
855
|
+
if (this.lastState.mediaSource === "live") return Promise.resolve(true)
|
|
856
|
+
const startedAt = Date.now()
|
|
857
|
+
softapTrace("acs_media_live_wait", {
|
|
858
|
+
timeoutMs,
|
|
859
|
+
mediaSource: this.lastState.mediaSource ?? "unknown",
|
|
860
|
+
state: this.lastState.state,
|
|
861
|
+
})
|
|
862
|
+
return new Promise<boolean>((resolve) => {
|
|
863
|
+
const settle = (live: boolean) => {
|
|
864
|
+
if (done) return
|
|
865
|
+
done = true
|
|
866
|
+
clearTimeout(timer)
|
|
867
|
+
this.mediaLiveWaiters.delete(settle)
|
|
868
|
+
softapTrace("acs_media_live_wait_done", {
|
|
869
|
+
waitedMs: Date.now() - startedAt,
|
|
870
|
+
live,
|
|
871
|
+
})
|
|
872
|
+
resolve(live)
|
|
873
|
+
}
|
|
874
|
+
let done = false
|
|
875
|
+
const timer = setTimeout(() => settle(false), timeoutMs)
|
|
876
|
+
this.mediaLiveWaiters.add(settle)
|
|
877
|
+
})
|
|
840
878
|
}
|
|
841
879
|
|
|
842
880
|
/**
|
|
@@ -1164,6 +1202,8 @@ class AcsMeetingService {
|
|
|
1164
1202
|
// rejected, or a leave mid-join leaves the orchestrator waiting out its whole timeout.
|
|
1165
1203
|
for (const settle of [...this.firstFrameWaiters]) settle(new Error("The meeting ended"))
|
|
1166
1204
|
this.firstFrameWaiters.clear()
|
|
1205
|
+
this.settleMediaLiveWaiters(false)
|
|
1206
|
+
this.mediaLiveWaiters.clear()
|
|
1167
1207
|
this.stopGlassesMicUplink()
|
|
1168
1208
|
this.unwatchPhoneNetwork()
|
|
1169
1209
|
await this.stopPcm()
|
|
@@ -3748,6 +3748,9 @@ class LocalMiniappRuntime {
|
|
|
3748
3748
|
type: MiniappResponseType.MEETING_STATE,
|
|
3749
3749
|
...state,
|
|
3750
3750
|
})
|
|
3751
|
+
if (attempt?.ownsResources && attempt.transport?.shouldRepublish(state.mediaSource)) {
|
|
3752
|
+
void attempt.transport.republish(state.mediaSourceReason ?? "mediaSource failed")
|
|
3753
|
+
}
|
|
3751
3754
|
})
|
|
3752
3755
|
}
|
|
3753
3756
|
|
|
@@ -4102,6 +4105,7 @@ class LocalMiniappRuntime {
|
|
|
4102
4105
|
displayName: args.displayName,
|
|
4103
4106
|
video: args.video,
|
|
4104
4107
|
awaitFirstFrame: () => acsMeetingService.waitForFirstFrame(SOFTAP_FIRST_FRAME_MS),
|
|
4108
|
+
waitUntilLive: (timeoutMs) => acsMeetingService.waitUntilMediaLive(timeoutMs),
|
|
4105
4109
|
subsystems: {
|
|
4106
4110
|
setHotspotState: async (enabled) => {
|
|
4107
4111
|
const status = await this.setGlassesHotspotState(enabled)
|
|
@@ -205,6 +205,13 @@ export interface SoftapCallDeps {
|
|
|
205
205
|
* that never delivers a frame reads as healthy behind a frozen tile.
|
|
206
206
|
*/
|
|
207
207
|
awaitFirstFrame(report?: SoftapStepReporter): Promise<void>
|
|
208
|
+
/**
|
|
209
|
+
* Mid-call camera recovery. Resolves `true` only when ingest is live again.
|
|
210
|
+
* A standing `failed` is the reason we are republishing, so it must not abort the wait.
|
|
211
|
+
*/
|
|
212
|
+
waitUntilLive?(timeoutMs: number): Promise<boolean>
|
|
213
|
+
/** Delay after a failed `start_stream` before the next republish attempt. Tests set 0. */
|
|
214
|
+
republishRetryDelayMs?: number
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
export interface SoftapCallOptions {
|
|
@@ -246,6 +253,15 @@ function deferred(): {promise: Promise<void>; resolve: () => void} {
|
|
|
246
253
|
return {promise, resolve}
|
|
247
254
|
}
|
|
248
255
|
|
|
256
|
+
function sleep(ms: number): Promise<void> {
|
|
257
|
+
if (ms <= 0) return Promise.resolve()
|
|
258
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** How long a mid-call republish waits for a frame after re-issuing `start_stream`. */
|
|
262
|
+
export const SOFTAP_REPUBLISH_LIVE_MS = 20_000
|
|
263
|
+
const REPUBLISH_RETRY_DELAY_MS = 2_000
|
|
264
|
+
|
|
249
265
|
/**
|
|
250
266
|
* How a call is being taken down.
|
|
251
267
|
*
|
|
@@ -322,6 +338,9 @@ export class SoftapCallTransport {
|
|
|
322
338
|
private startedAt = 0
|
|
323
339
|
private traceId = ""
|
|
324
340
|
private onProgress: ((progress: SoftapProgress) => void) | undefined
|
|
341
|
+
private republishing: Promise<void> | null = null
|
|
342
|
+
/** Bumped by `stop` so an in-flight republish cannot start_stream after Leave. */
|
|
343
|
+
private republishGeneration = 0
|
|
325
344
|
|
|
326
345
|
constructor(private readonly deps: SoftapCallDeps) {}
|
|
327
346
|
|
|
@@ -329,6 +348,71 @@ export class SoftapCallTransport {
|
|
|
329
348
|
return this.phase
|
|
330
349
|
}
|
|
331
350
|
|
|
351
|
+
/**
|
|
352
|
+
* The glasses camera died after this call was already live. Re-issue `start_stream` at the
|
|
353
|
+
* existing ingest URL — do not rebind the WHIP listener, or the glasses POST to a dead port.
|
|
354
|
+
*/
|
|
355
|
+
shouldRepublish(mediaSource?: string): boolean {
|
|
356
|
+
return this.phase === "live" && !this.terminating && mediaSource === "failed"
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Rebuild the glasses publisher onto the standing SoftAP ingest URL.
|
|
361
|
+
*
|
|
362
|
+
* ACS, BLE mic, and the WHIP listener stay up. The glasses process is what usually vanished
|
|
363
|
+
* (UVC/`system_server` crash); a new `start_stream` is what they need after ASG comes back.
|
|
364
|
+
*/
|
|
365
|
+
republish(reason: string): Promise<void> {
|
|
366
|
+
if (this.phase !== "live" || this.terminating || !this.ingestUrl) return Promise.resolve()
|
|
367
|
+
if (this.republishing) return this.republishing
|
|
368
|
+
const generation = this.republishGeneration
|
|
369
|
+
this.republishing = this.runRepublish(reason, generation).finally(() => {
|
|
370
|
+
if (this.republishGeneration === generation) this.republishing = null
|
|
371
|
+
})
|
|
372
|
+
return this.republishing
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private async runRepublish(reason: string, generation: number): Promise<void> {
|
|
376
|
+
const ingestUrl = this.ingestUrl
|
|
377
|
+
if (!ingestUrl) return
|
|
378
|
+
const retryDelayMs = this.deps.republishRetryDelayMs ?? REPUBLISH_RETRY_DELAY_MS
|
|
379
|
+
let attempt = 0
|
|
380
|
+
softapTrace("glasses_republish_begin", {reason, ingestUrl})
|
|
381
|
+
while (
|
|
382
|
+
generation === this.republishGeneration &&
|
|
383
|
+
!this.terminating &&
|
|
384
|
+
this.phase === "live" &&
|
|
385
|
+
this.ingestUrl === ingestUrl
|
|
386
|
+
) {
|
|
387
|
+
attempt += 1
|
|
388
|
+
try {
|
|
389
|
+
await this.deps.stopPublishing()
|
|
390
|
+
if (generation !== this.republishGeneration || this.terminating) return
|
|
391
|
+
await this.deps.startPublishing({ingestUrl, traceId: this.traceId})
|
|
392
|
+
if (generation !== this.republishGeneration || this.terminating) {
|
|
393
|
+
await this.deps.stopPublishing().catch(() => undefined)
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
softapTrace("glasses_republish_sent", {attempt, ingestUrl})
|
|
397
|
+
if (!this.deps.waitUntilLive) return
|
|
398
|
+
const live = await this.deps.waitUntilLive(SOFTAP_REPUBLISH_LIVE_MS)
|
|
399
|
+
if (generation !== this.republishGeneration || this.terminating) return
|
|
400
|
+
if (live) {
|
|
401
|
+
softapTrace("glasses_republish_live", {attempt})
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
softapTraceFailure("glasses_republish_no_frame", {attempt})
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (generation !== this.republishGeneration || this.terminating) return
|
|
407
|
+
softapTraceFailure("glasses_republish_start_failed", {
|
|
408
|
+
attempt,
|
|
409
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
410
|
+
})
|
|
411
|
+
await sleep(retryDelayMs)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
332
416
|
/**
|
|
333
417
|
* True once a teardown has been decided. Anything that would otherwise report a failure — a lost
|
|
334
418
|
* hotspot, a dropped ACS call — must check this first: after the wearer asks to leave, those are
|
|
@@ -577,6 +661,7 @@ export class SoftapCallTransport {
|
|
|
577
661
|
// Intent before action, always: a watcher must be able to tell a deliberate teardown from a
|
|
578
662
|
// failure even during the very first await below.
|
|
579
663
|
this.terminating = true
|
|
664
|
+
this.republishGeneration++
|
|
580
665
|
if (options.mode) this.teardownMode = options.mode
|
|
581
666
|
if (this.stopping) {
|
|
582
667
|
softapTrace("softap_stop_joined_in_flight", {mode: this.teardownMode})
|
|
@@ -833,6 +918,11 @@ export function createSoftapCallDeps(args: {
|
|
|
833
918
|
video?: {width: number; height: number; fps: number; maxBitrateBps: number}
|
|
834
919
|
/** Resolves when the meeting reports a frame reached ACS; rejects on a failed feed. */
|
|
835
920
|
awaitFirstFrame: () => Promise<void>
|
|
921
|
+
/**
|
|
922
|
+
* Mid-call camera recovery. Resolves `true` only when ingest is live again.
|
|
923
|
+
* A standing `failed` is the reason we are republishing, so it must not abort the wait.
|
|
924
|
+
*/
|
|
925
|
+
waitUntilLive?: (timeoutMs: number) => Promise<boolean>
|
|
836
926
|
subsystems: {
|
|
837
927
|
setHotspotState: (enabled: boolean) => Promise<{state: string; ssid?: string; password?: string; localIp?: string}>
|
|
838
928
|
/** Whether this phone's Wi-Fi radio is on. Optional: only Android hosts can answer it. */
|
|
@@ -1080,5 +1170,6 @@ export function createSoftapCallDeps(args: {
|
|
|
1080
1170
|
},
|
|
1081
1171
|
stopPublishing: () => subsystems.stopPublishing(packageName),
|
|
1082
1172
|
awaitFirstFrame: args.awaitFirstFrame,
|
|
1173
|
+
waitUntilLive: args.waitUntilLive,
|
|
1083
1174
|
}
|
|
1084
1175
|
}
|