@mentra/engine 3.2.0-dev.227 → 3.2.0-dev.233

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.
@@ -998,13 +998,14 @@ class LocalMiniappRuntime {
998
998
  // remain active for another subscriber; a rejected FOV release then stops
999
999
  // renewing the departed owner's lease and ASG restores it once idle.
1000
1000
  const cameraCleanup = Promise.allSettled([
1001
+ this.leaveMeetingForApp(packageName),
1001
1002
  phonePhotoCoordinator.stopWarmUpForApp(packageName),
1002
1003
  phoneStreamCoordinator.stop(packageName),
1003
1004
  phoneVideoCoordinator.stopForApp(packageName),
1004
1005
  ]).then((results) => {
1005
1006
  for (const result of results) {
1006
1007
  if (result.status === "rejected") {
1007
- console.warn(`${LOG_TAG}: failed to stop camera consumer for ${packageName} on unregister`, result.reason)
1008
+ console.warn(`${LOG_TAG}: failed to stop miniapp resource for ${packageName} on unregister`, result.reason)
1008
1009
  }
1009
1010
  }
1010
1011
  })
@@ -1038,10 +1039,6 @@ class LocalMiniappRuntime {
1038
1039
  // so a crashed/closed miniapp doesn't leak partial files or file handles.
1039
1040
  this.blobStore.onAppGone(packageName)
1040
1041
 
1041
- void acsMeetingService.leaveIfOwner(packageName).catch((error) => {
1042
- console.warn(`${LOG_TAG}: failed to leave ACS meeting for ${packageName} on unregister`, error)
1043
- })
1044
-
1045
1042
  // Detach the per-app nav event forwarder but leave the native nav session
1046
1043
  // running. The user may have just closed the mini-app UI and will reopen
1047
1044
  // it; stopping the session here would kill an active trip mid-route.
@@ -3668,6 +3665,9 @@ class LocalMiniappRuntime {
3668
3665
 
3669
3666
  private ensureMeetingStateBridge(): void {
3670
3667
  acsMeetingService.setStateHandler((owner, state) => {
3668
+ const attempt = this.softapAttempt
3669
+ // While a replacement waits for cleanup, native events still describe its predecessor.
3670
+ if (attempt?.packageName === owner && !attempt.ownsResources) return
3671
3671
  this.sendToMiniapp(owner, {
3672
3672
  type: MiniappResponseType.MEETING_STATE,
3673
3673
  ...state,
@@ -3675,6 +3675,16 @@ class LocalMiniappRuntime {
3675
3675
  })
3676
3676
  }
3677
3677
 
3678
+ /** Startup owns the hotspot before ACS has an owner. Closing the app must retire both. */
3679
+ private async leaveMeetingForApp(packageName: string): Promise<void> {
3680
+ const attempt = this.softapAttempt
3681
+ if (attempt?.packageName === packageName) {
3682
+ await this.retireSoftapAttempt({attempt})
3683
+ } else {
3684
+ await acsMeetingService.leaveIfOwner(packageName)
3685
+ }
3686
+ }
3687
+
3678
3688
  private async handleMeetingJoin(
3679
3689
  packageName: string,
3680
3690
  payload: Record<string, unknown>,
@@ -3903,7 +3913,8 @@ class LocalMiniappRuntime {
3903
3913
  return
3904
3914
  }
3905
3915
  attempt.progress = progress
3906
- const current = acsMeetingService.getState()
3916
+ // A reservation waiting behind another call must not inherit that call's connected state.
3917
+ const current = attempt.transport ? acsMeetingService.getState() : {state: "connecting", muted: false}
3907
3918
  this.sendToMiniapp(attempt.packageName, {
3908
3919
  type: MiniappResponseType.MEETING_STATE,
3909
3920
  ...current,
@@ -3937,6 +3948,7 @@ class LocalMiniappRuntime {
3937
3948
  ): Promise<MeetingState> {
3938
3949
  const packageName = attempt.packageName
3939
3950
  if (previous) {
3951
+ this.narrateSoftapPreflight(attempt, "Finishing the previous call’s cleanup…")
3940
3952
  softapTrace("softap_join_superseding", {
3941
3953
  previousAttempt: previous.id,
3942
3954
  previousAgeMs: Date.now() - previous.startedAt,
@@ -4000,6 +4012,7 @@ class LocalMiniappRuntime {
4000
4012
  joinScopedNetwork: (ssid, passphrase, gateway) =>
4001
4013
  acsMeetingService.joinScopedNetwork(ssid, passphrase, gateway),
4002
4014
  leaveScopedNetwork: () => acsMeetingService.leaveScopedNetwork(),
4015
+ cancelScopedNetworkJoin: () => acsMeetingService.cancelScopedNetworkJoin(),
4003
4016
  probeGateway: async () => {
4004
4017
  const verdict = await acsMeetingService.probeScopedGateway()
4005
4018
  // No native probe means no verdict, and the orchestrator must not read that as "down".
@@ -4251,7 +4264,7 @@ class LocalMiniappRuntime {
4251
4264
  // Teams configuration problem.
4252
4265
  const defaultNetworkStartedAt = Date.now()
4253
4266
  try {
4254
- const network = await acsMeetingService.awaitValidatedDefaultNetwork()
4267
+ const network = await acsMeetingService.awaitDefaultNetworkAfterHotspot()
4255
4268
  softapTrace("softap_teardown_step", {
4256
4269
  attempt: attempt.id,
4257
4270
  step: "awaitValidatedDefaultNetwork",
@@ -107,6 +107,8 @@ export interface SoftapCallDeps {
107
107
  /** Join the hotspot without taking the phone's default route. Resolves to the phone's own IPv4. */
108
108
  joinScopedNetwork(ssid: string, passphrase: string, report?: SoftapStepReporter): Promise<string | undefined>
109
109
  leaveScopedNetwork(): Promise<void>
110
+ /** Interrupt a pending native join while retaining its cleanup barrier. Unsupported hosts wait. */
111
+ cancelScopedNetworkJoin?(): Promise<void>
110
112
  /**
111
113
  * Join the meeting. This is what binds the local WHIP listener and arms the ACS raw outputs, so
112
114
  * it must resolve before the glasses are told to publish.
@@ -411,7 +413,7 @@ export class SoftapCallTransport {
411
413
  throw new Error("the meeting reported no ingest URL")
412
414
  }
413
415
  this.ingestUrl = ingestUrl
414
- softapTrace("acs_joined", {ingestUrl})
416
+ softapTrace("acs_receiver_ready", {ingestUrl})
415
417
  report(`Receiver ready at ${ingestUrl}`)
416
418
  })
417
419
 
@@ -424,10 +426,10 @@ export class SoftapCallTransport {
424
426
  })
425
427
 
426
428
  await this.step(generation, "live", "NO_FIRST_FRAME", async (report) => {
427
- report("Waiting for the first video frame to reach Teams")
429
+ report("Waiting for the first glasses video frame on this phone")
428
430
  await this.deps.awaitFirstFrame(report)
429
- softapTrace("first_frame_in_acs")
430
- report("Video is live in the meeting")
431
+ softapTrace("first_glasses_frame_received")
432
+ report("Glasses video is reaching this phone")
431
433
  })
432
434
 
433
435
  if (generation !== this.generation) {
@@ -500,9 +502,21 @@ export class SoftapCallTransport {
500
502
  // this call is still coming". Deliberately unbounded: a native call that never returns must
501
503
  // hold the next call back, never let it race this one's cleanup.
502
504
  if (running) {
505
+ const cancellation =
506
+ running.step === "scopedJoin" && this.deps.cancelScopedNetworkJoin
507
+ ? Promise.resolve()
508
+ .then(() => this.deps.cancelScopedNetworkJoin!())
509
+ .catch((error) => {
510
+ if (!this.teardownFailures.includes("scopedJoin")) this.teardownFailures.push("scopedJoin")
511
+ softapTraceFailure("softap_join_cancel_failed", {
512
+ reason: error instanceof Error ? error.message : String(error),
513
+ })
514
+ })
515
+ : undefined
503
516
  softapTrace("softap_stop_waiting_for_step", {step: running.step})
504
517
  const waitStartedAt = Date.now()
505
518
  await running.settled
519
+ await cancellation
506
520
  // This wait is unbounded by design, so its duration is the difference between "the leave
507
521
  // was slow" and "the leave was held by a native call that had not returned".
508
522
  softapTrace("softap_stop_step_settled", {step: running.step, waitedMs: Date.now() - waitStartedAt})
@@ -710,6 +724,7 @@ export function createSoftapCallDeps(args: {
710
724
  setHotspotState: (enabled: boolean) => Promise<{state: string; ssid?: string; password?: string; localIp?: string}>
711
725
  joinScopedNetwork: (ssid: string, passphrase: string, gateway?: string) => Promise<string | undefined>
712
726
  leaveScopedNetwork: () => Promise<void>
727
+ cancelScopedNetworkJoin?: () => Promise<void>
713
728
  joinMeeting: (
714
729
  packageName: string,
715
730
  options: {
@@ -853,6 +868,7 @@ export function createSoftapCallDeps(args: {
853
868
  return address
854
869
  },
855
870
  leaveScopedNetwork: () => subsystems.leaveScopedNetwork(),
871
+ cancelScopedNetworkJoin: subsystems.cancelScopedNetworkJoin,
856
872
  joinMeeting: async ({ssid, passphrase, bindAddress}, report) => {
857
873
  // The hotspot join just took this phone off Wi-Fi, so the route Teams needs is whatever
858
874
  // Android promoted in its place. Waiting for it to validate is what stopped the ACS join