@mentra/engine 3.2.0-dev.116 → 3.2.0-dev.121
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/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/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.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/AppRegistry.d.ts +9 -0
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +22 -0
- package/build/services/AppRegistry.js.map +1 -1
- 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 +12 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +184 -2
- 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/MiniappLauncher.d.ts +3 -1
- package/build/services/MiniappLauncher.d.ts.map +1 -1
- package/build/services/MiniappLauncher.js +66 -50
- package/build/services/MiniappLauncher.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/build/utils/devMiniappSnapshot.d.ts +46 -0
- package/build/utils/devMiniappSnapshot.d.ts.map +1 -0
- package/build/utils/devMiniappSnapshot.js +94 -0
- package/build/utils/devMiniappSnapshot.js.map +1 -0
- package/package.json +7 -6
- package/src/engine.ts +10 -2
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +4 -0
- package/src/runtime/streamConfig.ts +13 -0
- package/src/services/AcsMeetingService.ts +570 -0
- package/src/services/AppRegistry.ts +23 -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 +214 -2
- package/src/services/MentraJSRouter.ts +7 -1
- package/src/services/MiniappLauncher.ts +73 -50
- 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
- package/src/utils/devMiniappSnapshot.ts +119 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PhoneCameraFovCoordinator.js","sourceRoot":"","sources":["../../src/services/PhoneCameraFovCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["import BluetoothSdk, {type CameraFovRequest, type CameraFovResult} from \"@mentra/bluetooth-sdk/internal\"\n\nexport const CAMERA_FOV_OVERRIDE_TTL_MS = 300_000\nexport const CAMERA_FOV_OVERRIDE_REFRESH_MS = 120_000\n// Legacy ASG clients restart the camera HAL for an FOV update but cannot acknowledge\n// when it is usable again. The ASG cooldown is 5s; retain a small settling margin so\n// miniapps cannot submit a photo against the stale camera service.\nconst LEGACY_CAMERA_RESTART_SETTLE_MS = 7_000\n\ninterface OverrideEntry {\n leaseId: string\n fov: number\n roiPosition: \"center\" | \"bottom\" | \"top\"\n order: number\n}\n\nlet leaseCounter = 0\n\nfunction mintLeaseId(): string {\n leaseCounter = (leaseCounter + 1) & 0xffff\n return `fov-${leaseCounter.toString(16).padStart(4, \"0\")}`\n}\n\nfunction isLegacyFovCompatibilityError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error)\n return /timed out waiting for glasses response|not available|unknown method/i.test(message)\n}\n\n/**\n * Serializes miniapp-owned FOV/ROI overrides. The last live owner wins; releasing it applies the\n * previous owner or asks ASG to restore its persistent base. ASG refreshes are no-restart TTL\n * extensions for crash safety.\n */\nexport class PhoneCameraFovCoordinator {\n private readonly overrides = new Map<string, OverrideEntry>()\n private effectiveLeaseId: string | undefined\n private sequence = 0\n private queue: Promise<unknown> = Promise.resolve()\n private refreshTimer: ReturnType<typeof setTimeout> | undefined\n private legacyMode = false\n\n constructor(private readonly legacyRestartSettleMs = LEGACY_CAMERA_RESTART_SETTLE_MS) {}\n\n /** Report-safe FOV ownership snapshot for incident diagnostics. */\n getDiagnosticSnapshot(): Record<string, unknown> {\n return {\n owners: [...this.overrides.entries()]\n .sort(([, a], [, b]) => b.order - a.order)\n .map(([packageName, entry]) => ({\n packageName,\n effective: entry.leaseId === this.effectiveLeaseId,\n fov: entry.fov,\n roiPosition: entry.roiPosition,\n })),\n legacyMode: this.legacyMode,\n }\n }\n\n setOverride(packageName: string, request: CameraFovRequest): Promise<CameraFovResult> {\n return this.enqueue(async () => {\n const previous = this.overrides.get(packageName)\n const leaseId = previous?.leaseId ?? mintLeaseId()\n const order = ++this.sequence\n try {\n if (this.legacyMode) return this.setLegacyOverride(packageName, request, leaseId, order)\n const result = await BluetoothSdk.setCameraFovOverride({\n ...request,\n leaseId,\n ttlMs: CAMERA_FOV_OVERRIDE_TTL_MS,\n })\n this.overrides.set(packageName, {\n leaseId,\n fov: result.fov,\n roiPosition: result.roiPosition,\n order,\n })\n this.effectiveLeaseId = leaseId\n this.scheduleRefresh()\n return result\n } catch (error) {\n if (!isLegacyFovCompatibilityError(error)) throw error\n console.warn(\"[PhoneCameraFovCoordinator] override unavailable; using legacy FOV command\", error)\n const result = await this.setLegacyOverride(packageName, request, leaseId, order)\n // Only cache compatibility mode after the fallback itself succeeds. A\n // disconnect or missing native bridge must not permanently downgrade\n // future requests on a healthy/newer glasses connection.\n this.legacyMode = true\n return result\n }\n })\n }\n\n releaseForApp(packageName: string): Promise<void> {\n return this.enqueue(async () => {\n const removed = this.overrides.get(packageName)\n if (!removed) return\n if (removed.leaseId !== this.effectiveLeaseId) {\n this.overrides.delete(packageName)\n return\n }\n\n const next = [...this.overrides.entries()]\n .filter(([candidatePackage]) => candidatePackage !== packageName)\n .map(([, entry]) => entry)\n .sort((a, b) => b.order - a.order)[0]\n if (this.legacyMode) {\n if (next) {\n await this.applyLegacyFov({fov: next.fov, roiPosition: next.roiPosition})\n this.overrides.delete(packageName)\n this.effectiveLeaseId = next.leaseId\n } else {\n // Legacy commands are one-way and have no lease-release protocol.\n // Ask native to replay its unchanged persistent base setting, then\n // probe the modern override path again for the next ownership cycle.\n await BluetoothSdk.restoreLegacyCameraFov()\n await this.waitForLegacyCameraRestart()\n this.overrides.delete(packageName)\n this.clearRefresh()\n this.effectiveLeaseId = undefined\n this.legacyMode = false\n }\n return\n }\n if (next) {\n await BluetoothSdk.setCameraFovOverride({\n leaseId: next.leaseId,\n fov: next.fov,\n roiPosition: next.roiPosition,\n ttlMs: CAMERA_FOV_OVERRIDE_TTL_MS,\n })\n // Commit phone-side ownership only after ASG accepted the replacement. On failure the\n // closing app remains effective here, allowing unregister/reconnect cleanup to retry.\n this.overrides.delete(packageName)\n this.effectiveLeaseId = next.leaseId\n this.scheduleRefresh()\n } else {\n await BluetoothSdk.releaseCameraFovOverride(removed.leaseId)\n this.overrides.delete(packageName)\n this.clearRefresh()\n this.effectiveLeaseId = undefined\n }\n })\n }\n\n private scheduleRefresh(): void {\n if (this.legacyMode) return\n this.clearRefresh()\n const leaseId = this.effectiveLeaseId\n if (!leaseId) return\n this.refreshTimer = setTimeout(() => {\n void this.enqueue(async () => {\n if (this.effectiveLeaseId !== leaseId) return\n const entry = [...this.overrides.values()].find((candidate) => candidate.leaseId === leaseId)\n if (!entry) return\n await BluetoothSdk.setCameraFovOverride({\n leaseId,\n fov: entry.fov,\n roiPosition: entry.roiPosition,\n ttlMs: CAMERA_FOV_OVERRIDE_TTL_MS,\n })\n this.scheduleRefresh()\n }).catch((error) => console.warn(\"[PhoneCameraFovCoordinator] failed to refresh override\", error))\n }, CAMERA_FOV_OVERRIDE_REFRESH_MS)\n }\n\n private clearRefresh(): void {\n if (this.refreshTimer) clearTimeout(this.refreshTimer)\n this.refreshTimer = undefined\n }\n\n private async setLegacyOverride(\n packageName: string,\n request: CameraFovRequest,\n leaseId: string,\n order: number,\n ): Promise<CameraFovResult> {\n const result = await this.applyLegacyFov(request)\n this.overrides.set(packageName, {\n leaseId,\n fov: result.fov,\n roiPosition: result.roiPosition,\n order,\n })\n this.effectiveLeaseId = leaseId\n this.clearRefresh()\n return result\n }\n\n private async applyLegacyFov(request: CameraFovRequest): Promise<CameraFovResult> {\n const result = await BluetoothSdk.setLegacyCameraFov(request)\n await this.waitForLegacyCameraRestart()\n return result\n }\n\n private waitForLegacyCameraRestart(): Promise<void> {\n if (this.legacyRestartSettleMs <= 0) return Promise.resolve()\n return new Promise<void>((resolve) => setTimeout(resolve, this.legacyRestartSettleMs))\n }\n\n private enqueue<T>(operation: () => Promise<T>): Promise<T> {\n const result = this.queue.then(operation, operation)\n this.queue = result.then(\n () => undefined,\n () => undefined,\n )\n return result\n }\n}\n\nexport const phoneCameraFovCoordinator = new PhoneCameraFovCoordinator()\n"]}
|
|
1
|
+
{"version":3,"file":"PhoneCameraFovCoordinator.js","sourceRoot":"","sources":["../../src/services/PhoneCameraFovCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["import BluetoothSdk, {type CameraFovRequest, type CameraFovResult} from \"@mentra/bluetooth-sdk/internal\"\n\nexport const CAMERA_FOV_OVERRIDE_TTL_MS = 300_000\nexport const CAMERA_FOV_OVERRIDE_REFRESH_MS = 120_000\n// Legacy ASG clients restart the camera HAL for an FOV update but cannot acknowledge\n// when it is usable again. The ASG cooldown is 5s; retain a small settling margin so\n// miniapps cannot submit a photo against the stale camera service.\nconst LEGACY_CAMERA_RESTART_SETTLE_MS = 7_000\n\ninterface OverrideEntry {\n leaseId: string\n fov: number\n roiPosition: \"center\" | \"bottom\" | \"top\"\n order: number\n}\n\nlet leaseCounter = 0\n\nfunction mintLeaseId(): string {\n leaseCounter = (leaseCounter + 1) & 0xffff\n return `fov-${leaseCounter.toString(16).padStart(4, \"0\")}`\n}\n\nfunction isLegacyFovCompatibilityError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error)\n return /timed out waiting for glasses response|not available|unknown method/i.test(message)\n}\n\n/**\n * Serializes miniapp-owned FOV/ROI overrides. The last live owner wins; releasing it applies the\n * previous owner or asks ASG to restore its persistent base. ASG refreshes are no-restart TTL\n * extensions for crash safety.\n */\nexport class PhoneCameraFovCoordinator {\n private readonly overrides = new Map<string, OverrideEntry>()\n private effectiveLeaseId: string | undefined\n private sequence = 0\n private queue: Promise<unknown> = Promise.resolve()\n private refreshTimer: ReturnType<typeof setTimeout> | undefined\n private legacyMode = false\n\n constructor(private readonly legacyRestartSettleMs = LEGACY_CAMERA_RESTART_SETTLE_MS) {}\n\n /** Report-safe FOV ownership snapshot for incident diagnostics. */\n getDiagnosticSnapshot(): Record<string, unknown> {\n return {\n owners: [...this.overrides.entries()]\n .sort(([, a], [, b]) => b.order - a.order)\n .map(([packageName, entry]) => ({\n packageName,\n effective: entry.leaseId === this.effectiveLeaseId,\n fov: entry.fov,\n roiPosition: entry.roiPosition,\n })),\n legacyMode: this.legacyMode,\n }\n }\n\n setOverride(packageName: string, request: CameraFovRequest): Promise<CameraFovResult> {\n return this.enqueue(async () => {\n const previous = this.overrides.get(packageName)\n const leaseId = previous?.leaseId ?? mintLeaseId()\n const order = ++this.sequence\n try {\n if (this.legacyMode) return this.setLegacyOverride(packageName, request, leaseId, order)\n const result = await BluetoothSdk.setCameraFovOverride({\n ...request,\n leaseId,\n ttlMs: CAMERA_FOV_OVERRIDE_TTL_MS,\n })\n this.overrides.set(packageName, {\n leaseId,\n fov: result.fov,\n roiPosition: result.roiPosition,\n order,\n })\n this.effectiveLeaseId = leaseId\n this.scheduleRefresh()\n return result\n } catch (error) {\n if (!isLegacyFovCompatibilityError(error)) throw error\n console.warn(\"[PhoneCameraFovCoordinator] override unavailable; using legacy FOV command\", error)\n const result = await this.setLegacyOverride(packageName, request, leaseId, order)\n // Only cache compatibility mode after the fallback itself succeeds. A\n // disconnect or missing native bridge must not permanently downgrade\n // future requests on a healthy/newer glasses connection.\n this.legacyMode = true\n return result\n }\n })\n }\n\n /**\n * Re-assert the current override on the glasses.\n *\n * The on-connect device-settings push replays the persistent `camera_fov`,\n * so glasses that connect mid-call come up at the user's saved value and\n * silently drop a miniapp's override — a Mentra Call wearer loses the 102°\n * bottom crop by connecting their glasses after joining.\n *\n * The effective lease is recomputed inside [enqueue], not captured at call\n * time: a `setOverride` or `releaseForApp` already queued ahead of this must\n * win, and it is this coordinator that has to be the authoritative last\n * writer. Sends the complete override — `fov` *and* `roiPosition` — because\n * the scalar alone leaves the ROI wherever the persistent setting put it.\n *\n * No-op in legacy mode: those commands restart the camera HAL, which is far\n * too expensive to fire on every reconnect.\n */\n reapplyEffectiveOverride(): Promise<void> {\n return this.enqueue(async () => {\n if (this.legacyMode) return\n const leaseId = this.effectiveLeaseId\n if (!leaseId) return\n const entry = [...this.overrides.values()].find((candidate) => candidate.leaseId === leaseId)\n if (!entry) return\n await BluetoothSdk.setCameraFovOverride({\n leaseId,\n fov: entry.fov,\n roiPosition: entry.roiPosition,\n ttlMs: CAMERA_FOV_OVERRIDE_TTL_MS,\n })\n this.scheduleRefresh()\n })\n }\n\n releaseForApp(packageName: string): Promise<void> {\n return this.enqueue(async () => {\n const removed = this.overrides.get(packageName)\n if (!removed) return\n if (removed.leaseId !== this.effectiveLeaseId) {\n this.overrides.delete(packageName)\n return\n }\n\n const next = [...this.overrides.entries()]\n .filter(([candidatePackage]) => candidatePackage !== packageName)\n .map(([, entry]) => entry)\n .sort((a, b) => b.order - a.order)[0]\n if (this.legacyMode) {\n if (next) {\n await this.applyLegacyFov({fov: next.fov, roiPosition: next.roiPosition})\n this.overrides.delete(packageName)\n this.effectiveLeaseId = next.leaseId\n } else {\n // Legacy commands are one-way and have no lease-release protocol.\n // Ask native to replay its unchanged persistent base setting, then\n // probe the modern override path again for the next ownership cycle.\n await BluetoothSdk.restoreLegacyCameraFov()\n await this.waitForLegacyCameraRestart()\n this.overrides.delete(packageName)\n this.clearRefresh()\n this.effectiveLeaseId = undefined\n this.legacyMode = false\n }\n return\n }\n if (next) {\n await BluetoothSdk.setCameraFovOverride({\n leaseId: next.leaseId,\n fov: next.fov,\n roiPosition: next.roiPosition,\n ttlMs: CAMERA_FOV_OVERRIDE_TTL_MS,\n })\n // Commit phone-side ownership only after ASG accepted the replacement. On failure the\n // closing app remains effective here, allowing unregister/reconnect cleanup to retry.\n this.overrides.delete(packageName)\n this.effectiveLeaseId = next.leaseId\n this.scheduleRefresh()\n } else {\n await BluetoothSdk.releaseCameraFovOverride(removed.leaseId)\n this.overrides.delete(packageName)\n this.clearRefresh()\n this.effectiveLeaseId = undefined\n }\n })\n }\n\n private scheduleRefresh(): void {\n if (this.legacyMode) return\n this.clearRefresh()\n const leaseId = this.effectiveLeaseId\n if (!leaseId) return\n this.refreshTimer = setTimeout(() => {\n void this.enqueue(async () => {\n if (this.effectiveLeaseId !== leaseId) return\n const entry = [...this.overrides.values()].find((candidate) => candidate.leaseId === leaseId)\n if (!entry) return\n await BluetoothSdk.setCameraFovOverride({\n leaseId,\n fov: entry.fov,\n roiPosition: entry.roiPosition,\n ttlMs: CAMERA_FOV_OVERRIDE_TTL_MS,\n })\n this.scheduleRefresh()\n }).catch((error) => console.warn(\"[PhoneCameraFovCoordinator] failed to refresh override\", error))\n }, CAMERA_FOV_OVERRIDE_REFRESH_MS)\n }\n\n private clearRefresh(): void {\n if (this.refreshTimer) clearTimeout(this.refreshTimer)\n this.refreshTimer = undefined\n }\n\n private async setLegacyOverride(\n packageName: string,\n request: CameraFovRequest,\n leaseId: string,\n order: number,\n ): Promise<CameraFovResult> {\n const result = await this.applyLegacyFov(request)\n this.overrides.set(packageName, {\n leaseId,\n fov: result.fov,\n roiPosition: result.roiPosition,\n order,\n })\n this.effectiveLeaseId = leaseId\n this.clearRefresh()\n return result\n }\n\n private async applyLegacyFov(request: CameraFovRequest): Promise<CameraFovResult> {\n const result = await BluetoothSdk.setLegacyCameraFov(request)\n await this.waitForLegacyCameraRestart()\n return result\n }\n\n private waitForLegacyCameraRestart(): Promise<void> {\n if (this.legacyRestartSettleMs <= 0) return Promise.resolve()\n return new Promise<void>((resolve) => setTimeout(resolve, this.legacyRestartSettleMs))\n }\n\n private enqueue<T>(operation: () => Promise<T>): Promise<T> {\n const result = this.queue.then(operation, operation)\n this.queue = result.then(\n () => undefined,\n () => undefined,\n )\n return result\n }\n}\n\nexport const phoneCameraFovCoordinator = new PhoneCameraFovCoordinator()\n"]}
|
|
@@ -26,6 +26,18 @@
|
|
|
26
26
|
* streams. We use a `phone-` prefix so they're trivially distinguishable from
|
|
27
27
|
* cloud-minted IDs in logs and from cloud-SDK app streams that flow through
|
|
28
28
|
* the legacy path.
|
|
29
|
+
*
|
|
30
|
+
* BLE link loss is a SUSPENDED state, not a failure:
|
|
31
|
+
* The glasses publisher keeps pushing over Wi-Fi when the phone's Bluetooth
|
|
32
|
+
* link drops; only our keep-alives stop reaching it. While the store says
|
|
33
|
+
* the glasses are disconnected we pause the heartbeat (so we don't count
|
|
34
|
+
* misses we caused ourselves), fan out `suspended`, and give the link
|
|
35
|
+
* `glassesGraceMs` to come back. On reconnect we resume, heartbeat at once,
|
|
36
|
+
* and fan out `resumed` with the SAME streamId — no re-provision. The grace
|
|
37
|
+
* ends early when Cloudflare reports the publisher gone for two probes in
|
|
38
|
+
* a row (glasses powered off, not a BLE hiccup). If the grace expires the
|
|
39
|
+
* stream is torn down with reason `glasses_disconnected`, and the BLE stop
|
|
40
|
+
* the glasses never received is sent on the next reconnect.
|
|
29
41
|
*/
|
|
30
42
|
import type { KeepAliveAckEvent, StreamResolvedConfig, StreamStartRequest, StreamStatusEvent } from "@mentra/bluetooth-sdk/internal";
|
|
31
43
|
import { type RestreamDestinationInput } from "./cloudStreamApi";
|
|
@@ -42,11 +54,28 @@ declare const DEFAULT_TIMINGS: {
|
|
|
42
54
|
readonly hlsReadinessInitialDelayMs: 5000;
|
|
43
55
|
readonly hlsReadinessPollMs: 2000;
|
|
44
56
|
readonly hlsReadinessMaxAttempts: 30;
|
|
57
|
+
readonly glassesGraceMs: 45000;
|
|
58
|
+
readonly suspendedPublisherGoneProbes: 2;
|
|
45
59
|
};
|
|
46
60
|
type TimingConfig = {
|
|
47
61
|
[K in keyof typeof DEFAULT_TIMINGS]: number;
|
|
48
62
|
};
|
|
49
63
|
export type CoordinatorTimings = Partial<TimingConfig>;
|
|
64
|
+
/**
|
|
65
|
+
* Where the coordinator learns whether the phone↔glasses BLE link is up.
|
|
66
|
+
* Injected so tests can drive link transitions without the zustand store.
|
|
67
|
+
*/
|
|
68
|
+
export interface GlassesLinkSource {
|
|
69
|
+
isConnected(): boolean;
|
|
70
|
+
/** Fires on every connected↔disconnected transition. */
|
|
71
|
+
subscribe(listener: (connected: boolean) => void): () => void;
|
|
72
|
+
}
|
|
73
|
+
/** Reasons carried on coordinator-sourced `stream_status` fanouts for link events. */
|
|
74
|
+
export declare const LINK_STATUS: {
|
|
75
|
+
readonly suspended: "suspended";
|
|
76
|
+
readonly resumed: "resumed";
|
|
77
|
+
readonly reason: "glasses_disconnected";
|
|
78
|
+
};
|
|
50
79
|
export interface StartUnmanagedOptions {
|
|
51
80
|
streamUrl: string;
|
|
52
81
|
video?: StreamStartRequest["video"];
|
|
@@ -54,6 +83,7 @@ export interface StartUnmanagedOptions {
|
|
|
54
83
|
sound?: boolean;
|
|
55
84
|
/** Optional Bearer token for WHIP Authorization (custom authenticated endpoints). */
|
|
56
85
|
authToken?: string;
|
|
86
|
+
captureAudio?: boolean;
|
|
57
87
|
}
|
|
58
88
|
export interface StartManagedOptions {
|
|
59
89
|
restreamDestinations?: RestreamDestinationInput[];
|
|
@@ -71,6 +101,8 @@ export interface StartManagedOptions {
|
|
|
71
101
|
* that drop WHIP/SRT UDP.
|
|
72
102
|
*/
|
|
73
103
|
ingest?: "srt" | "whip" | "rtmp";
|
|
104
|
+
/** When false, glasses skip encoding their mic for this WHIP session. */
|
|
105
|
+
captureAudio?: boolean;
|
|
74
106
|
}
|
|
75
107
|
export interface StreamPublisherStartResult {
|
|
76
108
|
streamId: string;
|
|
@@ -112,6 +144,16 @@ export declare class PhoneStreamCoordinator {
|
|
|
112
144
|
private statusSubscriber;
|
|
113
145
|
private idCounter;
|
|
114
146
|
private readonly timings;
|
|
147
|
+
private readonly linkSource;
|
|
148
|
+
private unsubscribeLink;
|
|
149
|
+
private suspended;
|
|
150
|
+
/**
|
|
151
|
+
* A stream was torn down while the BLE link was down, so the glasses never
|
|
152
|
+
* got `stopStream`. Sent on the next reconnect (if no new stream has claimed
|
|
153
|
+
* the slot) so a publisher that outlived its input does not keep pushing
|
|
154
|
+
* until its own watchdog fires.
|
|
155
|
+
*/
|
|
156
|
+
private pendingBleStop;
|
|
115
157
|
/**
|
|
116
158
|
* Serializes state transitions (start, stop, teardown). Without it, a
|
|
117
159
|
* second `start*` racing with the first can pass the `this.current === null`
|
|
@@ -125,7 +167,17 @@ export declare class PhoneStreamCoordinator {
|
|
|
125
167
|
private lastFanoutSignature;
|
|
126
168
|
/** Send full resolvedConfig only once per stream session. */
|
|
127
169
|
private resolvedConfigForwarded;
|
|
128
|
-
constructor(timings?: CoordinatorTimings
|
|
170
|
+
constructor(timings?: CoordinatorTimings, deps?: {
|
|
171
|
+
linkSource?: GlassesLinkSource;
|
|
172
|
+
});
|
|
173
|
+
/**
|
|
174
|
+
* Fail fast if glasses aren't connected — BEFORE provisioning. Without this a
|
|
175
|
+
* managed start would create a provider live input, fail the BLE command, and
|
|
176
|
+
* tear the input down again: a slow, billable no-op with a confusing error.
|
|
177
|
+
*/
|
|
178
|
+
private assertGlassesConnected;
|
|
179
|
+
/** True while the active stream is parked on a dropped BLE link. */
|
|
180
|
+
isSuspended(): boolean;
|
|
129
181
|
/**
|
|
130
182
|
* Run `work` under the transition lock. Each call awaits the previous
|
|
131
183
|
* transition before re-evaluating preconditions, so e.g. two concurrent
|
|
@@ -156,7 +208,21 @@ export declare class PhoneStreamCoordinator {
|
|
|
156
208
|
handleGlassesStatus(event: StreamStatusEvent): void;
|
|
157
209
|
/** Called by MantleManager when a phone-owned keep_alive_ack arrives. */
|
|
158
210
|
handleKeepAliveAck(event: KeepAliveAckEvent): void;
|
|
211
|
+
private attachLink;
|
|
212
|
+
private detachLinkIfIdle;
|
|
213
|
+
private handleLinkChange;
|
|
214
|
+
private suspend;
|
|
215
|
+
private resumeLocked;
|
|
216
|
+
private onGraceExpired;
|
|
217
|
+
/**
|
|
218
|
+
* End a suspended stream. `publisherGone` distinguishes "glasses are off and
|
|
219
|
+
* Cloudflare confirms nothing is publishing" from "grace ran out with the
|
|
220
|
+
* publisher possibly still alive" — miniapps word the two differently.
|
|
221
|
+
*/
|
|
222
|
+
private failSuspended;
|
|
223
|
+
private flushPendingBleStop;
|
|
159
224
|
private mintId;
|
|
225
|
+
/** BLE keep-alives. StreamLifecycleController uses BgTimer so they survive MentraOS backgrounding. */
|
|
160
226
|
private startLifecycle;
|
|
161
227
|
private startCloudflareStatusPoll;
|
|
162
228
|
private startHlsReadinessPoll;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PhoneStreamCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/PhoneStreamCoordinator.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"PhoneStreamCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/PhoneStreamCoordinator.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,6 +26,18 @@
|
|
|
26
26
|
* streams. We use a `phone-` prefix so they're trivially distinguishable from
|
|
27
27
|
* cloud-minted IDs in logs and from cloud-SDK app streams that flow through
|
|
28
28
|
* the legacy path.
|
|
29
|
+
*
|
|
30
|
+
* BLE link loss is a SUSPENDED state, not a failure:
|
|
31
|
+
* The glasses publisher keeps pushing over Wi-Fi when the phone's Bluetooth
|
|
32
|
+
* link drops; only our keep-alives stop reaching it. While the store says
|
|
33
|
+
* the glasses are disconnected we pause the heartbeat (so we don't count
|
|
34
|
+
* misses we caused ourselves), fan out `suspended`, and give the link
|
|
35
|
+
* `glassesGraceMs` to come back. On reconnect we resume, heartbeat at once,
|
|
36
|
+
* and fan out `resumed` with the SAME streamId — no re-provision. The grace
|
|
37
|
+
* ends early when Cloudflare reports the publisher gone for two probes in
|
|
38
|
+
* a row (glasses powered off, not a BLE hiccup). If the grace expires the
|
|
39
|
+
* stream is torn down with reason `glasses_disconnected`, and the BLE stop
|
|
40
|
+
* the glasses never received is sent on the next reconnect.
|
|
29
41
|
*/
|
|
30
42
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal";
|
|
31
43
|
import { isGlassesConnected } from "./GlassesReadiness";
|
|
@@ -49,6 +61,23 @@ const DEFAULT_TIMINGS = {
|
|
|
49
61
|
hlsReadinessInitialDelayMs: 5_000,
|
|
50
62
|
hlsReadinessPollMs: 2_000,
|
|
51
63
|
hlsReadinessMaxAttempts: 30,
|
|
64
|
+
// How long a stream survives a BLE link drop before we give up on it. Must
|
|
65
|
+
// stay under the glasses publisher's own 60s no-keep-alive watchdog with
|
|
66
|
+
// margin for the resume heartbeat's round trip.
|
|
67
|
+
glassesGraceMs: 45_000,
|
|
68
|
+
// Consecutive Cloudflare "publisher disconnected" probes while suspended
|
|
69
|
+
// before we conclude the glasses are off (not just out of BLE range).
|
|
70
|
+
suspendedPublisherGoneProbes: 2,
|
|
71
|
+
};
|
|
72
|
+
const storeLinkSource = {
|
|
73
|
+
isConnected: () => isGlassesConnected(useGlassesStore.getState().connection),
|
|
74
|
+
subscribe: (listener) => useGlassesStore.subscribe((s) => isGlassesConnected(s.connection), listener),
|
|
75
|
+
};
|
|
76
|
+
/** Reasons carried on coordinator-sourced `stream_status` fanouts for link events. */
|
|
77
|
+
export const LINK_STATUS = {
|
|
78
|
+
suspended: "suspended",
|
|
79
|
+
resumed: "resumed",
|
|
80
|
+
reason: "glasses_disconnected",
|
|
52
81
|
};
|
|
53
82
|
// Console-backed minimal logger; replaces pino on the phone.
|
|
54
83
|
const consoleLogger = {
|
|
@@ -78,22 +107,22 @@ export class StreamConflictError extends Error {
|
|
|
78
107
|
this.name = "StreamConflictError";
|
|
79
108
|
}
|
|
80
109
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Fail fast if glasses aren't connected — BEFORE provisioning. Without this a
|
|
83
|
-
* managed start would create a provider live input, fail the BLE command, and
|
|
84
|
-
* tear the input down again: a slow, billable no-op with a confusing error.
|
|
85
|
-
*/
|
|
86
|
-
function assertGlassesConnected() {
|
|
87
|
-
if (!isGlassesConnected(useGlassesStore.getState().connection)) {
|
|
88
|
-
throw new StreamConflictError("GLASSES_NOT_CONNECTED", "Glasses are not connected", "command", "ble");
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
110
|
export class PhoneStreamCoordinator {
|
|
92
111
|
current = null;
|
|
93
112
|
lifecycle = null;
|
|
94
113
|
statusSubscriber = null;
|
|
95
114
|
idCounter = 0;
|
|
96
115
|
timings;
|
|
116
|
+
linkSource;
|
|
117
|
+
unsubscribeLink = null;
|
|
118
|
+
suspended = null;
|
|
119
|
+
/**
|
|
120
|
+
* A stream was torn down while the BLE link was down, so the glasses never
|
|
121
|
+
* got `stopStream`. Sent on the next reconnect (if no new stream has claimed
|
|
122
|
+
* the slot) so a publisher that outlived its input does not keep pushing
|
|
123
|
+
* until its own watchdog fires.
|
|
124
|
+
*/
|
|
125
|
+
pendingBleStop = null;
|
|
97
126
|
/**
|
|
98
127
|
* Serializes state transitions (start, stop, teardown). Without it, a
|
|
99
128
|
* second `start*` racing with the first can pass the `this.current === null`
|
|
@@ -107,8 +136,23 @@ export class PhoneStreamCoordinator {
|
|
|
107
136
|
lastFanoutSignature = null;
|
|
108
137
|
/** Send full resolvedConfig only once per stream session. */
|
|
109
138
|
resolvedConfigForwarded = false;
|
|
110
|
-
constructor(timings = {}) {
|
|
139
|
+
constructor(timings = {}, deps = {}) {
|
|
111
140
|
this.timings = { ...DEFAULT_TIMINGS, ...timings };
|
|
141
|
+
this.linkSource = deps.linkSource ?? storeLinkSource;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Fail fast if glasses aren't connected — BEFORE provisioning. Without this a
|
|
145
|
+
* managed start would create a provider live input, fail the BLE command, and
|
|
146
|
+
* tear the input down again: a slow, billable no-op with a confusing error.
|
|
147
|
+
*/
|
|
148
|
+
assertGlassesConnected() {
|
|
149
|
+
if (!this.linkSource.isConnected()) {
|
|
150
|
+
throw new StreamConflictError("GLASSES_NOT_CONNECTED", "Glasses are not connected", "command", "ble");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** True while the active stream is parked on a dropped BLE link. */
|
|
154
|
+
isSuspended() {
|
|
155
|
+
return this.suspended !== null;
|
|
112
156
|
}
|
|
113
157
|
/**
|
|
114
158
|
* Run `work` under the transition lock. Each call awaits the previous
|
|
@@ -146,7 +190,11 @@ export class PhoneStreamCoordinator {
|
|
|
146
190
|
/** Report-safe stream ownership snapshot for incident diagnostics. */
|
|
147
191
|
getDiagnosticSnapshot() {
|
|
148
192
|
if (!this.current)
|
|
149
|
-
return { active: false };
|
|
193
|
+
return { active: false, pendingBleStop: this.pendingBleStop?.streamId ?? null };
|
|
194
|
+
const link = {
|
|
195
|
+
suspended: this.suspended !== null,
|
|
196
|
+
...(this.suspended ? { suspendedForMs: Date.now() - this.suspended.since } : {}),
|
|
197
|
+
};
|
|
150
198
|
return this.current.kind === "managed"
|
|
151
199
|
? {
|
|
152
200
|
active: true,
|
|
@@ -155,12 +203,14 @@ export class PhoneStreamCoordinator {
|
|
|
155
203
|
subscribers: [...this.current.subscribers].sort(),
|
|
156
204
|
mode: this.current.mode,
|
|
157
205
|
playbackReady: this.current.hlsReady,
|
|
206
|
+
...link,
|
|
158
207
|
}
|
|
159
208
|
: {
|
|
160
209
|
active: true,
|
|
161
210
|
kind: this.current.kind,
|
|
162
211
|
streamId: this.current.streamId,
|
|
163
212
|
ownerPackageName: this.current.packageName,
|
|
213
|
+
...link,
|
|
164
214
|
};
|
|
165
215
|
}
|
|
166
216
|
async startUnmanaged(packageName, opts) {
|
|
@@ -169,7 +219,7 @@ export class PhoneStreamCoordinator {
|
|
|
169
219
|
if (!opts.streamUrl || typeof opts.streamUrl !== "string") {
|
|
170
220
|
throw new StreamConflictError("STREAM_URL_REQUIRED", "streamUrl is required");
|
|
171
221
|
}
|
|
172
|
-
assertGlassesConnected();
|
|
222
|
+
this.assertGlassesConnected();
|
|
173
223
|
return this.runExclusive(async () => {
|
|
174
224
|
if (this.current) {
|
|
175
225
|
throw new StreamConflictError("STREAM_ALREADY_ACTIVE", `A ${this.current.kind} stream is already active. Stop it before starting a new one.`);
|
|
@@ -195,6 +245,7 @@ export class PhoneStreamCoordinator {
|
|
|
195
245
|
...(opts.video !== undefined ? { video: opts.video } : {}),
|
|
196
246
|
...(opts.audio !== undefined ? { audio: opts.audio } : {}),
|
|
197
247
|
...(opts.authToken ? { authToken: opts.authToken } : {}),
|
|
248
|
+
...(typeof opts.captureAudio === "boolean" ? { captureAudio: opts.captureAudio } : {}),
|
|
198
249
|
});
|
|
199
250
|
const result = publisherStartResult(streamId, event);
|
|
200
251
|
this.startLifecycle(streamId);
|
|
@@ -219,7 +270,7 @@ export class PhoneStreamCoordinator {
|
|
|
219
270
|
// Two-phase: the entry-claim runs under the transition lock; the wait for
|
|
220
271
|
// HLS readiness happens AFTER the lock releases so a long warm-up doesn't
|
|
221
272
|
// block subsequent start/stop transitions on this coordinator.
|
|
222
|
-
assertGlassesConnected();
|
|
273
|
+
this.assertGlassesConnected();
|
|
223
274
|
const decision = await this.runExclusive(async () => {
|
|
224
275
|
if (this.current && this.current.kind === "unmanaged") {
|
|
225
276
|
throw new StreamConflictError("STREAM_ALREADY_ACTIVE", "An unmanaged stream is already active. Stop it before starting a managed stream.");
|
|
@@ -279,6 +330,7 @@ export class PhoneStreamCoordinator {
|
|
|
279
330
|
// See startUnmanaged: the native bridge rejects explicit `undefined`.
|
|
280
331
|
...(opts.video !== undefined ? { video: opts.video } : {}),
|
|
281
332
|
...(opts.audio !== undefined ? { audio: opts.audio } : {}),
|
|
333
|
+
...(typeof opts.captureAudio === "boolean" ? { captureAudio: opts.captureAudio } : {}),
|
|
282
334
|
});
|
|
283
335
|
entry.publisherStart = publisherStartResult(streamId, event);
|
|
284
336
|
console.info("[STREAM_STARTUP]", {
|
|
@@ -396,13 +448,120 @@ export class PhoneStreamCoordinator {
|
|
|
396
448
|
this.lifecycle?.handleAck(event.ackId);
|
|
397
449
|
}
|
|
398
450
|
// ===========================================================================
|
|
451
|
+
// BLE link suspension
|
|
452
|
+
// ===========================================================================
|
|
453
|
+
attachLink() {
|
|
454
|
+
if (this.unsubscribeLink)
|
|
455
|
+
return;
|
|
456
|
+
this.unsubscribeLink = this.linkSource.subscribe((connected) => this.handleLinkChange(connected));
|
|
457
|
+
}
|
|
458
|
+
detachLinkIfIdle() {
|
|
459
|
+
if (this.current || this.pendingBleStop || !this.unsubscribeLink)
|
|
460
|
+
return;
|
|
461
|
+
this.unsubscribeLink();
|
|
462
|
+
this.unsubscribeLink = null;
|
|
463
|
+
}
|
|
464
|
+
handleLinkChange(connected) {
|
|
465
|
+
if (connected) {
|
|
466
|
+
if (this.current && this.suspended) {
|
|
467
|
+
this.resumeLocked();
|
|
468
|
+
}
|
|
469
|
+
else if (!this.current && this.pendingBleStop) {
|
|
470
|
+
this.flushPendingBleStop();
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (this.current && !this.suspended)
|
|
475
|
+
this.suspend();
|
|
476
|
+
}
|
|
477
|
+
suspend() {
|
|
478
|
+
const entry = this.current;
|
|
479
|
+
if (!entry)
|
|
480
|
+
return;
|
|
481
|
+
const since = Date.now();
|
|
482
|
+
// Pause the heartbeat: misses now would be our own fault, not the
|
|
483
|
+
// publisher's, and counting them would reap a stream that is still live.
|
|
484
|
+
this.lifecycle?.setActive(false);
|
|
485
|
+
const graceTimer = setTimeout(() => this.onGraceExpired(entry.streamId), this.timings.glassesGraceMs);
|
|
486
|
+
this.suspended = { since, graceTimer, publisherGoneProbes: 0 };
|
|
487
|
+
console.warn("[STREAM] BLE link lost; stream suspended", {
|
|
488
|
+
streamId: entry.streamId,
|
|
489
|
+
graceMs: this.timings.glassesGraceMs,
|
|
490
|
+
});
|
|
491
|
+
this.fanout({
|
|
492
|
+
streamId: entry.streamId,
|
|
493
|
+
source: "coordinator",
|
|
494
|
+
status: LINK_STATUS.suspended,
|
|
495
|
+
data: { reason: LINK_STATUS.reason, graceMs: this.timings.glassesGraceMs, since },
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
resumeLocked() {
|
|
499
|
+
const entry = this.current;
|
|
500
|
+
const suspended = this.suspended;
|
|
501
|
+
if (!entry || !suspended)
|
|
502
|
+
return;
|
|
503
|
+
clearTimeout(suspended.graceTimer);
|
|
504
|
+
this.suspended = null;
|
|
505
|
+
const suspendedMs = Date.now() - suspended.since;
|
|
506
|
+
console.info("[STREAM] BLE link back; stream resumed", { streamId: entry.streamId, suspendedMs });
|
|
507
|
+
// Resume and heartbeat immediately: the glasses watchdog has been running
|
|
508
|
+
// the whole time, so the first keep-alive must not wait another interval.
|
|
509
|
+
this.lifecycle?.setActive(true);
|
|
510
|
+
this.lifecycle?.tickNow();
|
|
511
|
+
// A resumed session is a fresh status baseline for subscribers.
|
|
512
|
+
this.lastFanoutSignature = null;
|
|
513
|
+
this.fanout({
|
|
514
|
+
streamId: entry.streamId,
|
|
515
|
+
source: "coordinator",
|
|
516
|
+
status: LINK_STATUS.resumed,
|
|
517
|
+
data: { reason: LINK_STATUS.reason, suspendedMs },
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
onGraceExpired(streamId) {
|
|
521
|
+
if (this.current?.streamId !== streamId || !this.suspended)
|
|
522
|
+
return;
|
|
523
|
+
this.failSuspended(streamId, "glasses_disconnected", { publisherGone: false });
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* End a suspended stream. `publisherGone` distinguishes "glasses are off and
|
|
527
|
+
* Cloudflare confirms nothing is publishing" from "grace ran out with the
|
|
528
|
+
* publisher possibly still alive" — miniapps word the two differently.
|
|
529
|
+
*/
|
|
530
|
+
failSuspended(streamId, reason, detail) {
|
|
531
|
+
this.fanout({
|
|
532
|
+
streamId,
|
|
533
|
+
source: "coordinator",
|
|
534
|
+
status: "error",
|
|
535
|
+
data: { reason: LINK_STATUS.reason, teardownReason: reason, ...detail },
|
|
536
|
+
});
|
|
537
|
+
void this.runExclusive(async () => {
|
|
538
|
+
if (this.current?.streamId !== streamId)
|
|
539
|
+
return;
|
|
540
|
+
await this.teardownLocked(reason);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
flushPendingBleStop() {
|
|
544
|
+
const pending = this.pendingBleStop;
|
|
545
|
+
if (!pending)
|
|
546
|
+
return;
|
|
547
|
+
this.pendingBleStop = null;
|
|
548
|
+
console.info("[STREAM] BLE link back; sending deferred stopStream", pending);
|
|
549
|
+
void BluetoothSdk.stopStream()
|
|
550
|
+
.catch((err) => console.warn("[STREAM] deferred stopStream failed:", err))
|
|
551
|
+
.finally(() => this.detachLinkIfIdle());
|
|
552
|
+
}
|
|
553
|
+
// ===========================================================================
|
|
399
554
|
// Internal
|
|
400
555
|
// ===========================================================================
|
|
401
556
|
mintId(prefix) {
|
|
402
557
|
this.idCounter += 1;
|
|
403
558
|
return `phone-${prefix}-${Date.now().toString(36)}-${this.idCounter}`;
|
|
404
559
|
}
|
|
560
|
+
/** BLE keep-alives. StreamLifecycleController uses BgTimer so they survive MentraOS backgrounding. */
|
|
405
561
|
startLifecycle(streamId) {
|
|
562
|
+
// A new stream supersedes any deferred stop for the previous one.
|
|
563
|
+
this.pendingBleStop = null;
|
|
564
|
+
this.attachLink();
|
|
406
565
|
this.lifecycle?.dispose();
|
|
407
566
|
const ctrl = new StreamLifecycleController({
|
|
408
567
|
logger: consoleLogger,
|
|
@@ -475,6 +634,21 @@ export class PhoneStreamCoordinator {
|
|
|
475
634
|
status: status.isConnected ? "connected" : "disconnected",
|
|
476
635
|
data: status,
|
|
477
636
|
});
|
|
637
|
+
// While the BLE link is down, Cloudflare is the only witness to the
|
|
638
|
+
// publisher. Two consecutive "nobody is publishing" probes mean the
|
|
639
|
+
// glasses are off, not merely out of Bluetooth range — stop waiting.
|
|
640
|
+
if (this.suspended) {
|
|
641
|
+
if (status.isConnected) {
|
|
642
|
+
this.suspended.publisherGoneProbes = 0;
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
this.suspended.publisherGoneProbes += 1;
|
|
646
|
+
if (this.suspended.publisherGoneProbes >= this.timings.suspendedPublisherGoneProbes) {
|
|
647
|
+
this.failSuspended(entry.streamId, "glasses_disconnected_publisher_gone", { publisherGone: true });
|
|
648
|
+
keepPolling = false;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
478
652
|
// webrtc mode readiness: first "connected" means WHEP playback is
|
|
479
653
|
// available (WebRTC playback follows the ingest directly; there is no
|
|
480
654
|
// manifest to probe).
|
|
@@ -652,11 +826,25 @@ export class PhoneStreamCoordinator {
|
|
|
652
826
|
const sendBleStop = options.sendBleStop !== false;
|
|
653
827
|
this.lastFanoutSignature = null;
|
|
654
828
|
this.resolvedConfigForwarded = false;
|
|
829
|
+
if (this.suspended) {
|
|
830
|
+
clearTimeout(this.suspended.graceTimer);
|
|
831
|
+
this.suspended = null;
|
|
832
|
+
}
|
|
655
833
|
// Dispose the lifecycle controller immediately so it doesn't fire one
|
|
656
834
|
// more keep-alive against a stream we're tearing down. The transition
|
|
657
835
|
// lock guarantees no new lifecycle is started concurrently.
|
|
658
836
|
this.lifecycle?.dispose();
|
|
659
837
|
this.lifecycle = null;
|
|
838
|
+
// With the link down a BLE write can only fail (and hold the transition
|
|
839
|
+
// lock for the native timeout). Defer it to the next reconnect instead.
|
|
840
|
+
const linkUp = this.linkSource.isConnected();
|
|
841
|
+
if (sendBleStop && !linkUp) {
|
|
842
|
+
this.pendingBleStop = { streamId: entry.streamId };
|
|
843
|
+
console.warn("[STREAM] BLE link down during teardown; stopStream deferred", {
|
|
844
|
+
streamId: entry.streamId,
|
|
845
|
+
reason,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
660
848
|
if (entry.kind === "managed") {
|
|
661
849
|
if (entry.cloudflareTimer)
|
|
662
850
|
clearTimeout(entry.cloudflareTimer);
|
|
@@ -670,7 +858,7 @@ export class PhoneStreamCoordinator {
|
|
|
670
858
|
entry.hlsReadyRejecters = [];
|
|
671
859
|
}
|
|
672
860
|
try {
|
|
673
|
-
if (sendBleStop) {
|
|
861
|
+
if (sendBleStop && linkUp) {
|
|
674
862
|
await BluetoothSdk.stopStream();
|
|
675
863
|
}
|
|
676
864
|
}
|
|
@@ -684,6 +872,7 @@ export class PhoneStreamCoordinator {
|
|
|
684
872
|
// serializes us, so this should always be true).
|
|
685
873
|
if (this.current === entry)
|
|
686
874
|
this.current = null;
|
|
875
|
+
this.detachLinkIfIdle();
|
|
687
876
|
if (entry.kind === "managed") {
|
|
688
877
|
// Start remote cleanup only after the publisher has stopped, but do not
|
|
689
878
|
// hold the local transition lock on an unbounded network request. The
|