@mentra/engine 3.2.0-dev.233 → 3.2.0-dev.245
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/react/MentraLiveOtaFlow.d.ts +5 -1
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
- package/build/react/MentraLiveOtaFlow.js +31 -3
- package/build/react/MentraLiveOtaFlow.js.map +1 -1
- package/build/react/index.d.ts +1 -1
- package/build/react/index.d.ts.map +1 -1
- package/build/react/index.js +1 -1
- package/build/react/index.js.map +1 -1
- package/build/services/AcsMeetingService.d.ts +70 -0
- package/build/services/AcsMeetingService.d.ts.map +1 -1
- package/build/services/AcsMeetingService.js +97 -0
- package/build/services/AcsMeetingService.js.map +1 -1
- package/build/services/AppRegistry.d.ts +2 -12
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +2 -37
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/CloudClientService.d.ts +1 -1
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/CloudClientService.js +2 -2
- package/build/services/CloudClientService.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +51 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +228 -6
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/PhoneNotificationsSync.d.ts +1 -1
- package/build/services/PhonePhotoCoordinator.d.ts +3 -2
- package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
- package/build/services/PhonePhotoCoordinator.js +19 -14
- package/build/services/PhonePhotoCoordinator.js.map +1 -1
- package/build/services/SoftapCallTransport.d.ts +64 -0
- package/build/services/SoftapCallTransport.d.ts.map +1 -1
- package/build/services/SoftapCallTransport.js +103 -3
- package/build/services/SoftapCallTransport.js.map +1 -1
- package/build/services/manifestPermissions.d.ts +13 -0
- package/build/services/manifestPermissions.d.ts.map +1 -0
- package/build/services/manifestPermissions.js +42 -0
- package/build/services/manifestPermissions.js.map +1 -0
- package/build/types/applet.d.ts +13 -1
- package/build/types/applet.d.ts.map +1 -1
- package/build/types/applet.js.map +1 -1
- package/package.json +8 -8
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/react/MentraLiveOtaFlow.tsx +73 -6
- package/src/react/index.ts +1 -0
- package/src/react/lucide-LICENSE.txt +39 -0
- package/src/services/AcsMeetingService.ts +138 -0
- package/src/services/AppRegistry.ts +3 -40
- package/src/services/CloudClientService.ts +2 -2
- package/src/services/LocalMiniappRuntime.ts +268 -7
- package/src/services/PhonePhotoCoordinator.ts +18 -14
- package/src/services/SoftapCallTransport.ts +151 -16
- package/src/services/manifestPermissions.ts +44 -0
- package/src/types/applet.ts +11 -0
|
@@ -2,7 +2,7 @@ import type { PhoneNotificationEvent } from "@mentra/crust";
|
|
|
2
2
|
export declare function nativeNotificationCapabilities(): {
|
|
3
3
|
supported: boolean;
|
|
4
4
|
source: "phone" | "ancs";
|
|
5
|
-
contentAccess: "
|
|
5
|
+
contentAccess: "unsupported" | "full" | "app_identity";
|
|
6
6
|
perAppFiltering: boolean;
|
|
7
7
|
removal: boolean;
|
|
8
8
|
};
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* short-circuit our long-poll with a typed error (CAMERA_BUSY, BATTERY_LOW,
|
|
18
18
|
* etc.) instead of waiting 30s for cloud's timeout.
|
|
19
19
|
*/
|
|
20
|
+
import { type PhotoCompression } from "@mentra/cloud-protocol/photo-compression";
|
|
20
21
|
import type { PhotoTransferMethod } from "@mentra/bluetooth-sdk/internal";
|
|
21
22
|
export interface PhotoOpts {
|
|
22
23
|
/** Legacy cloud size names are normalized before the native take_photo command. */
|
|
@@ -24,7 +25,7 @@ export interface PhotoOpts {
|
|
|
24
25
|
mode?: "photo" | "text";
|
|
25
26
|
/** Select direct-only, phone-relayed BLE, or the default Wi-Fi/BLE fallback policy. */
|
|
26
27
|
transferMethod?: PhotoTransferMethod;
|
|
27
|
-
compress?:
|
|
28
|
+
compress?: PhotoCompression;
|
|
28
29
|
sound?: boolean;
|
|
29
30
|
saveToGallery?: boolean;
|
|
30
31
|
exposureTimeNs?: number;
|
|
@@ -68,7 +69,7 @@ export declare class PhotoError extends Error {
|
|
|
68
69
|
*/
|
|
69
70
|
export declare const CAPTURE_PIPELINE_TIMEOUT_MS = 45000;
|
|
70
71
|
export declare const CAMERA_WARM_UP_DEFAULT_DURATION_MS = 15000;
|
|
71
|
-
export declare const CAMERA_WARM_UP_MAX_DURATION_MS =
|
|
72
|
+
export declare const CAMERA_WARM_UP_MAX_DURATION_MS = 300000;
|
|
72
73
|
export declare class PhonePhotoCoordinator {
|
|
73
74
|
private readonly activeRequests;
|
|
74
75
|
/** Short BLE correlation id (4-char hex) → full cloud requestId. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PhonePhotoCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/PhonePhotoCoordinator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;
|
|
1
|
+
{"version":3,"file":"PhonePhotoCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/PhonePhotoCoordinator.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* short-circuit our long-poll with a typed error (CAMERA_BUSY, BATTERY_LOW,
|
|
18
18
|
* etc.) instead of waiting 30s for cloud's timeout.
|
|
19
19
|
*/
|
|
20
|
+
import { parsePhotoCompression } from "@mentra/cloud-protocol/photo-compression";
|
|
20
21
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal";
|
|
21
22
|
import { cloudClientService } from "./CloudClientService";
|
|
22
23
|
import { isGlassesConnected } from "./GlassesReadiness";
|
|
@@ -66,7 +67,7 @@ function parsePhotoTransferMethod(value) {
|
|
|
66
67
|
*/
|
|
67
68
|
export const CAPTURE_PIPELINE_TIMEOUT_MS = 45_000;
|
|
68
69
|
export const CAMERA_WARM_UP_DEFAULT_DURATION_MS = 15_000;
|
|
69
|
-
export const CAMERA_WARM_UP_MAX_DURATION_MS =
|
|
70
|
+
export const CAMERA_WARM_UP_MAX_DURATION_MS = 300_000;
|
|
70
71
|
let bleRequestCounter = 0;
|
|
71
72
|
/** Short 4-hex-char correlation id sent over BLE instead of the full cloud
|
|
72
73
|
* requestId UUID (wire v2 keeps BLE JSON small). */
|
|
@@ -74,13 +75,6 @@ function mintBleRequestId() {
|
|
|
74
75
|
bleRequestCounter = (bleRequestCounter + 1) & 0xffff;
|
|
75
76
|
return bleRequestCounter.toString(16).padStart(4, "0");
|
|
76
77
|
}
|
|
77
|
-
function toNativeCompression(compress) {
|
|
78
|
-
if (compress === "high")
|
|
79
|
-
return "heavy";
|
|
80
|
-
if (compress === "low" || compress === "medium")
|
|
81
|
-
return "medium";
|
|
82
|
-
return "none";
|
|
83
|
-
}
|
|
84
78
|
export class PhonePhotoCoordinator {
|
|
85
79
|
// Cloud requestId → in-flight slot. The gated photo_response listener
|
|
86
80
|
// (DeviceEventRouter) resolves short BLE ids via bleIdToCloud before calling
|
|
@@ -98,6 +92,7 @@ export class PhonePhotoCoordinator {
|
|
|
98
92
|
};
|
|
99
93
|
}
|
|
100
94
|
async takePhoto(packageName, opts) {
|
|
95
|
+
const compress = parsePhotoCompression(opts.compress);
|
|
101
96
|
const transferMethod = parsePhotoTransferMethod(opts.transferMethod);
|
|
102
97
|
// Pre-check: if glasses aren't even connected, the BLE photo command
|
|
103
98
|
// would be sent into the void and we'd wait 30s for the cloud long-poll
|
|
@@ -113,8 +108,8 @@ export class PhonePhotoCoordinator {
|
|
|
113
108
|
if (!isGlassesConnected(useGlassesStore.getState().connection)) {
|
|
114
109
|
throw new PhotoError("GLASSES_NOT_CONNECTED", "Glasses are not connected", "command", "ble");
|
|
115
110
|
}
|
|
116
|
-
// Text-mode sensor resolution is owned by ASG constants;
|
|
117
|
-
//
|
|
111
|
+
// Text-mode sensor resolution is owned by ASG constants; the glasses ignore the public
|
|
112
|
+
// size when mode=text.
|
|
118
113
|
const captureSize = opts.size ?? "medium";
|
|
119
114
|
// 1) Presign via the cloud-v2 managed-photo service. Local miniapps use
|
|
120
115
|
// ONLY the cloud-v2 path: the runtime presigns upload+read URLs and the
|
|
@@ -126,9 +121,7 @@ export class PhonePhotoCoordinator {
|
|
|
126
121
|
const flowStarted = performance.now();
|
|
127
122
|
try {
|
|
128
123
|
const presignStarted = performance.now();
|
|
129
|
-
const r = await cloudClientService.startManagedPhoto(
|
|
130
|
-
size: opts.mode === "text" ? "max" : captureSize,
|
|
131
|
-
});
|
|
124
|
+
const r = await cloudClientService.startManagedPhoto();
|
|
132
125
|
const presignMs = Math.round(performance.now() - presignStarted);
|
|
133
126
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
134
127
|
console.debug(`[PhonePhotoCoordinator] presign ${presignMs}ms size=${opts.mode === "text" ? "max" : captureSize} mode=${opts.mode ?? "photo"}`);
|
|
@@ -186,7 +179,7 @@ export class PhonePhotoCoordinator {
|
|
|
186
179
|
webhookUrl: uploadUrl,
|
|
187
180
|
authToken: null,
|
|
188
181
|
...(isLoopbackUpload ? { transferMethod: "ble" } : transferMethod ? { transferMethod } : {}),
|
|
189
|
-
compress
|
|
182
|
+
compress,
|
|
190
183
|
save: opts.saveToGallery ?? false,
|
|
191
184
|
sound: opts.sound ?? true,
|
|
192
185
|
exposureTimeNs: opts.exposureTimeNs ?? null,
|
|
@@ -199,6 +192,18 @@ export class PhonePhotoCoordinator {
|
|
|
199
192
|
...(opts.mfnr != null ? { mfnr: opts.mfnr } : {}),
|
|
200
193
|
ispDigitalGain: opts.ispDigitalGain,
|
|
201
194
|
ispAnalogGain: opts.ispAnalogGain,
|
|
195
|
+
}).then((response) => {
|
|
196
|
+
// Native success is emitted only after the direct/phone-relayed upload
|
|
197
|
+
// completes. It is an independent completion signal when photo.ready
|
|
198
|
+
// cannot reach this phone (for example, a cross-pod WS routing gap).
|
|
199
|
+
// Older native bridges may resolve at dispatch with no response: those
|
|
200
|
+
// must still wait for the cloud push, never return an unuploaded image.
|
|
201
|
+
if (response?.state !== "success" || response.requestId !== bleRequestId)
|
|
202
|
+
return;
|
|
203
|
+
const e = this.activeRequests.get(requestId);
|
|
204
|
+
if (!e || e.abort.signal.aborted)
|
|
205
|
+
return;
|
|
206
|
+
e.resolve({ photoUrl: readUrl, mimeType: "image/jpeg", size: -1 });
|
|
202
207
|
}).catch((err) => {
|
|
203
208
|
const e = this.activeRequests.get(requestId);
|
|
204
209
|
if (!e)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PhonePhotoCoordinator.js","sourceRoot":"","sources":["../../src/services/PhonePhotoCoordinator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,YAAY,MAAM,gCAAgC,CAAA;AAEzD,OAAO,EAAC,kBAAkB,EAAC,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAC,eAAe,EAAC,MAAM,mBAAmB,CAAA;AAQjD,mEAAmE;AACnE,SAAS,kBAAkB,CAAC,KAAc;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAA;IAC9C,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,OAAO;YACV,OAAO,KAAK,CAAA;QACd,KAAK,OAAO;YACV,OAAO,MAAM,CAAA;QACf,KAAK,MAAM;YACT,OAAO,KAAK,CAAA;QACd;YACE,OAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAW,CAAC,QAAQ,CAAC,KAAkB,CAAC,CAAC,CAAC,CAAE,KAAmB,CAAC,CAAC,CAAC,QAAQ,CAAA;IACrH,CAAC;AACH,CAAC;AAsCD,MAAM,OAAO,UAAW,SAAQ,KAAK;IAEjB;IAEA;IACA;IAJlB,YACkB,IAAY,EAC5B,OAAe,EACC,KAAkB,EAClB,SAA0B;QAE1C,KAAK,CAAC,OAAO,CAAC,CAAA;QALE,SAAI,GAAJ,IAAI,CAAQ;QAEZ,UAAK,GAAL,KAAK,CAAa;QAClB,cAAS,GAAT,SAAS,CAAiB;QAG1C,IAAI,CAAC,IAAI,GAAG,YAAY,CAAA;IAC1B,CAAC;CACF;AAED,SAAS,wBAAwB,CAAC,KAAc;IAC9C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACzC,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,KAAK,CAAA;IAC3E,MAAM,IAAI,UAAU,CAClB,kBAAkB,EAClB,0BAA0B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,wCAAwC,CACxF,CAAA;AACH,CAAC;AASD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAA;AACjD,MAAM,CAAC,MAAM,kCAAkC,GAAG,MAAM,CAAA;AACxD,MAAM,CAAC,MAAM,8BAA8B,GAAG,MAAM,CAAA;AAEpD,IAAI,iBAAiB,GAAG,CAAC,CAAA;AAEzB;qDACqD;AACrD,SAAS,gBAAgB;IACvB,iBAAiB,GAAG,CAAC,iBAAiB,GAAG,CAAC,CAAC,GAAG,MAAM,CAAA;IACpD,OAAO,iBAAiB,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AACxD,CAAC;AAED,SAAS,mBAAmB,CAAC,QAA+B;IAC1D,IAAI,QAAQ,KAAK,MAAM;QAAE,OAAO,OAAO,CAAA;IACvC,IAAI,QAAQ,KAAK,KAAK,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAA;IAChE,OAAO,MAAM,CAAA;AACf,CAAC;AAED,MAAM,OAAO,qBAAqB;IAChC,sEAAsE;IACtE,6EAA6E;IAC7E,+BAA+B;IACd,cAAc,GAAG,IAAI,GAAG,EAAyB,CAAA;IAClE,oEAAoE;IACnD,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,aAAa,GAAG,IAAI,GAAG,EAGrC,CAAA;IAEH,sEAAsE;IACtE,qBAAqB;QACnB,OAAO;YACL,aAAa,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE;YAC3G,YAAY,EAAE,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;YACnD,kBAAkB,EAAE,IAAI,CAAC,cAAc,CAAC,IAAI;SAC7C,CAAA;IACH,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,WAAmB,EAAE,IAAe;QAClD,MAAM,cAAc,GAAG,wBAAwB,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QAEpE,qEAAqE;QACrE,wEAAwE;QACxE,6CAA6C;QAC7C,EAAE;QACF,sEAAsE;QACtE,uEAAuE;QACvE,qEAAqE;QACrE,qEAAqE;QACrE,sEAAsE;QACtE,kEAAkE;QAClE,iCAAiC;QACjC,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/D,MAAM,IAAI,UAAU,CAAC,uBAAuB,EAAE,2BAA2B,EAAE,SAAS,EAAE,KAAK,CAAC,CAAA;QAC9F,CAAC;QAED,yFAAyF;QACzF,gFAAgF;QAChF,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAA;QAEzC,wEAAwE;QACxE,2EAA2E;QAC3E,qEAAqE;QACrE,8CAA8C;QAC9C,IAAI,SAAiB,CAAA;QACrB,IAAI,SAAiB,CAAA;QACrB,IAAI,OAAe,CAAA;QACnB,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE,CAAA;QACrC,IAAI,CAAC;YACH,MAAM,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE,CAAA;YACxC,MAAM,CAAC,GAAG,MAAM,kBAAkB,CAAC,iBAAiB,CAAC;gBACnD,IAAI,EAAE,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW;aACjD,CAAC,CAAA;YACF,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,cAAc,CAAC,CAAA;YAChE,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,EAAE,CAAC;gBAC9C,OAAO,CAAC,KAAK,CACX,mCAAmC,SAAS,WAAW,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,SAC/F,IAAI,CAAC,IAAI,IAAI,OACf,EAAE,CACH,CAAA;YACH,CAAC;YACD,SAAS,GAAG,CAAC,CAAC,SAAS,CAAA;YACvB,SAAS,GAAG,CAAC,CAAC,SAAS,CAAA;YACvB,OAAO,GAAG,CAAC,CAAC,OAAO,CAAA;QACrB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,sBAAsB,EAAE,SAAS,EAAE,YAAY,CAAC,CAAA;QAC/E,CAAC;QAED,oEAAoE;QACpE,oEAAoE;QACpE,oEAAoE;QACpE,0DAA0D;QAC1D,mEAAmE;QACnE,gBAAgB;QAChB,4EAA4E;QAC5E,uEAAuE;QACvE,2EAA2E;QAC3E,0EAA0E;QAC1E,iCAAiC;QACjC,MAAM,gBAAgB,GAAG,oDAAoD,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAE7F,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAA;QACnC,MAAM,YAAY,GAAG,gBAAgB,EAAE,CAAA;QACvC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,YAAY,EAAE,SAAS,CAAC,CAAA;QAE9C,MAAM,OAAO,GAAG,IAAI,OAAO,CAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC3D,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,EAAE,EAAC,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAC,CAAC,CAAA;QAC3E,CAAC,CAAC,CAAA;QAEF,2EAA2E;QAC3E,4EAA4E;QAC5E,iEAAiE;QACjE,MAAM,eAAe,GAAG,UAAU,CAAC,GAAG,EAAE;YACtC,MAAM,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;YAC5C,IAAI,CAAC,CAAC;gBAAE,OAAM;YACd,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;YACf,CAAC,CAAC,MAAM,CACN,IAAI,UAAU,CACZ,iBAAiB,EACjB,iHAAiH,EACjH,SAAS,EACT,gBAAgB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAClC,CACF,CAAA;QACH,CAAC,EAAE,2BAA2B,CAAC,CAAA;QAC/B,2EAA2E;QAC3E,0EAA0E;QAC1E,0EAA0E;QAC1E,MAAM,aAAa,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,eAAe,CAAC,CAAA;QACzD,KAAK,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,aAAa,CAAC,CAAA;QAE/C,mEAAmE;QACnE,wEAAwE;QACxE,0EAA0E;QAC1E,qEAAqE;QACrE,IAAI,CAAC;YACH,KAAK,YAAY,CAAC,YAAY,CAAC;gBAC7B,SAAS,EAAE,YAAY;gBACvB,KAAK,EAAE,WAAW;gBAClB,IAAI,EAAE,kBAAkB,CAAC,WAAW,CAAC;gBACrC,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,OAAO;gBAC1B,UAAU,EAAE,SAAS;gBACrB,SAAS,EAAE,IAAI;gBACf,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAC,cAAc,EAAE,KAAc,EAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAC,cAAc,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjG,QAAQ,EAAE,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAC5C,IAAI,EAAE,IAAI,CAAC,aAAa,IAAI,KAAK;gBACjC,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI;gBACzB,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,IAAI;gBAC3C,GAAG,EAAE,IAAI,CAAC,GAAG;gBACb,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;gBACzC,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,cAAc,EAAE,IAAI,CAAC,cAAc;gBACnC,eAAe,EAAE,IAAI,CAAC,eAAe;gBACrC,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,EAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC5C,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,EAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/C,cAAc,EAAE,IAAI,CAAC,cAAc;gBACnC,aAAa,EAAE,IAAI,CAAC,aAAa;aAClC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACf,MAAM,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;gBAC5C,IAAI,CAAC,CAAC;oBAAE,OAAM;gBACd,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;gBACf,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,iBAAiB,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAA;YACvE,CAAC,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;YACrC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;YACtC,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,iBAAiB,EAAE,SAAS,EAAE,KAAK,CAAC,CAAA;QACnE,CAAC;QAED,2EAA2E;QAC3E,yEAAyE;QACzE,YAAY;QACZ,kBAAkB;aACf,sBAAsB,CAAC,SAAS,CAAC;aACjC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;YACZ,MAAM,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;YAC5C,IAAI,CAAC,CAAC;gBAAE,OAAM,CAAC,sCAAsC;YACrD,CAAC,CAAC,OAAO,CAAC,EAAC,QAAQ,EAAE,GAAG,CAAC,OAAO,IAAI,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,EAAC,CAAC,CAAA;QACjF,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACb,MAAM,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;YAC5C,IAAI,CAAC;gBAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;QACtE,CAAC,CAAC,CAAA;QAEJ,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAA;YAC5B,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,EAAE,CAAC;gBAC9C,OAAO,CAAC,KAAK,CACX,8CAA8C,IAAI,CAAC,KAAK,CACtD,WAAW,CAAC,GAAG,EAAE,GAAG,WAAW,CAChC,gBAAgB,SAAS,EAAE,CAC7B,CAAA;YACH,CAAC;YACD,OAAO;gBACL,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,SAAS;aACV,CAAA;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;YACrC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;QACxC,CAAC;IACH,CAAC;IAED,iFAAiF;IACjF,qBAAqB,CAAC,YAAoB;QACxC,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,YAAY,CAAA;IAC5D,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,YAAY,CAChB,WAAmB,EACnB,IAOC;QAED,2EAA2E;QAC3E,oDAAoD;QACpD,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/D,MAAM,IAAI,UAAU,CAAC,uBAAuB,EAAE,2BAA2B,EAAE,SAAS,EAAE,KAAK,CAAC,CAAA;QAC9F,CAAC;QAED,IAAI,KAAuG,CAAA;QAC3G,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAA;YACxC,MAAM,SAAS,GAAG,gBAAgB,EAAE,CAAA;YACpC,MAAM,iBAAiB,GACrB,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC;gBAC5F,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC;gBAC7B,CAAC,CAAC,kCAAkC,CAAA;YACxC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,8BAA8B,CAAC,CAAA;YAC9E,KAAK,GAAG,EAAC,SAAS,EAAE,UAAU,EAAC,CAAA;YAC/B,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,CAAA;YAC1C,MAAM,YAAY,CAAC,YAAY,CAAC;gBAC9B,SAAS;gBACT,IAAI,EAAE,kBAAkB,CAAC,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC;gBAC/C,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,OAAO;gBAC1B,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,IAAI;gBAC3C,UAAU;gBACV,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,EAAC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC5C,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,EAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;aAChD,CAAC,CAAA;YACF,IAAI,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,KAAK,EAAE,CAAC;gBAClD,KAAK,CAAC,WAAW,GAAG,UAAU,CAAC,GAAG,EAAE;oBAClC,IAAI,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,KAAK,EAAE,CAAC;wBAClD,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;oBACxC,CAAC;gBACH,CAAC,EAAE,UAAU,CAAC,CAAA;YAChB,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,KAAK,IAAI,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,KAAK,EAAE,CAAC;gBAC3D,IAAI,KAAK,CAAC,WAAW;oBAAE,YAAY,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;gBACtD,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;YACxC,CAAC;YACD,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,gBAAgB,EAAE,SAAS,EAAE,KAAK,CAAC,CAAA;QAClE,CAAC;IACH,CAAC;IAED,8FAA8F;IAC9F,KAAK,CAAC,gBAAgB,CAAC,WAAmB;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QAClD,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,MAAM,YAAY,CAAC,gBAAgB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QACrD,IAAI,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,MAAM,EAAE,CAAC;YACnD,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;YACtC,IAAI,MAAM,CAAC,WAAW;gBAAE,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;QAC1D,CAAC;IACH,CAAC;IAED,4FAA4F;IAC5F,IAAI,CAAC,SAAiB;QACpB,MAAM,OAAO,GAAG,IAAI,CAAC,qBAAqB,CAAC,SAAS,CAAC,CAAA;QACrD,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IACzC,CAAC;IAED;;;;OAIG;IACH,gBAAgB,CAAC,SAAiB,EAAE,SAAiB,EAAE,YAAoB;QACzE,MAAM,OAAO,GAAG,IAAI,CAAC,qBAAqB,CAAC,SAAS,CAAC,CAAA;QACrD,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAC9C,IAAI,CAAC,KAAK;YAAE,OAAM;QAClB,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QACnC,kEAAkE;QAClE,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QACnB,KAAK,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,SAAS,IAAI,eAAe,EAAE,YAAY,IAAI,eAAe,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAA;QAC7G,yEAAyE;IAC3E,CAAC;IAEO,YAAY,CAAC,GAAY,EAAE,YAAoB,EAAE,KAAkB,EAAE,SAA0B;QACrG,IAAI,GAAG,YAAY,UAAU;YAAE,OAAO,GAAG,CAAA;QACzC,MAAM,IAAI,GAAI,GAAuB,EAAE,IAAI,CAAA;QAC3C,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAChE,OAAO,IAAI,UAAU,CAAC,IAAI,IAAI,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,CAAA;IACxE,CAAC;CACF;AAED,kFAAkF;AAClF,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,qBAAqB,EAAE,CAAA","sourcesContent":["/**\n * PhonePhotoCoordinator — owns local-miniapp takePhoto() end-to-end.\n *\n * Architecture:\n * miniapp → SDK → LocalMiniappRuntime → photo runtime hook\n * → coordinator.takePhoto(packageName, opts)\n * ├── (precheck) glasses connected + hasCamera\n * ├── cloudClientService.startManagedPhoto → {requestId, uploadUrl, readUrl} (cloud-v2 runtime presign)\n * ├── BluetoothSdk.requestPhoto(requestId, size, uploadUrl, compress, sound)\n * └── race:\n * - cloudClientService.awaitManagedPhotoReady(requestId) resolves on photo.ready push\n * - BluetoothSdk.requestPhoto rejects if terminal photo_response is an error\n * - handlePhotoError(requestId, code, message) rejects if MantleManager observes\n * the same BLE photo_response error before the native promise crosses the bridge\n *\n * `activeRequests` lets MantleManager's gated `photo_response` listener\n * short-circuit our long-poll with a typed error (CAMERA_BUSY, BATTERY_LOW,\n * etc.) instead of waiting 30s for cloud's timeout.\n */\n\nimport BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\nimport type {PhotoSize, PhotoTransferMethod} from \"@mentra/bluetooth-sdk/internal\"\nimport {cloudClientService} from \"./CloudClientService\"\nimport {isGlassesConnected} from \"./GlassesReadiness\"\nimport {useGlassesStore} from \"../stores/glasses\"\n\ninterface PhotoResult {\n photoUrl: string\n mimeType: string\n size: number\n}\n\n/** Map legacy/cloud size names onto the native take_photo enum. */\nfunction normalizePhotoSize(value: unknown): PhotoSize {\n if (typeof value !== \"string\") return \"medium\"\n switch (value) {\n case \"small\":\n return \"low\"\n case \"large\":\n return \"high\"\n case \"full\":\n return \"max\"\n default:\n return ([\"low\", \"medium\", \"high\", \"max\"] as const).includes(value as PhotoSize) ? (value as PhotoSize) : \"medium\"\n }\n}\n\nexport interface PhotoOpts {\n /** Legacy cloud size names are normalized before the native take_photo command. */\n size?: \"low\" | \"medium\" | \"high\" | \"max\" | \"small\" | \"large\" | \"full\"\n mode?: \"photo\" | \"text\"\n /** Select direct-only, phone-relayed BLE, or the default Wi-Fi/BLE fallback policy. */\n transferMethod?: PhotoTransferMethod\n compress?: \"none\" | \"low\" | \"medium\" | \"high\"\n sound?: boolean\n saveToGallery?: boolean\n exposureTimeNs?: number\n iso?: number | null\n aeExposureDivisor?: number\n isoCap?: number\n noiseReduction?: boolean\n edgeEnhancement?: boolean\n /** ZSL preview buffering. */\n zsl?: boolean\n /** MFNR still capture. */\n mfnr?: boolean\n ispDigitalGain?: number\n ispAnalogGain?: string\n}\n\nexport interface PhotoTaken {\n photoUrl: string\n mimeType: string\n size: number\n requestId: string\n}\n\n/** Pipeline stage a photo request failed at — surfaced to the miniapp so a dev\n * sees exactly where it broke, not just a flattened message. */\nexport type PhotoStage = \"presign\" | \"command\" | \"capture\" | \"upload\" | \"push\"\n/** Transport in play at the point of failure. */\nexport type PhotoTransport = \"cloud-rest\" | \"ble\" | \"wifi\" | \"ws\"\n\nexport class PhotoError extends Error {\n constructor(\n public readonly code: string,\n message: string,\n public readonly stage?: PhotoStage,\n public readonly transport?: PhotoTransport,\n ) {\n super(message)\n this.name = \"PhotoError\"\n }\n}\n\nfunction parsePhotoTransferMethod(value: unknown): PhotoTransferMethod | undefined {\n if (value === undefined) return undefined\n if (value === \"auto\" || value === \"direct\" || value === \"ble\") return value\n throw new PhotoError(\n \"INVALID_ARGUMENT\",\n `Invalid transferMethod ${JSON.stringify(value)}. Expected \"auto\", \"direct\", or \"ble\".`,\n )\n}\n\ninterface ActiveRequest {\n packageName: string\n abort: AbortController\n resolve: (r: PhotoResult) => void\n reject: (err: Error) => void\n}\n\n/**\n * Last-resort ceiling for the complete capture + encode + transfer + upload\n * pipeline. This must exceed the managed-photo service's 30s ready-push\n * timeout so the cloud can return its more specific failure first.\n *\n * Text mode commonly spends several seconds detecting/cropping the region and\n * can take another 10s+ to transfer over BLE. A 15s ceiling incorrectly\n * rejected successful captures while their upload was still in progress.\n */\nexport const CAPTURE_PIPELINE_TIMEOUT_MS = 45_000\nexport const CAMERA_WARM_UP_DEFAULT_DURATION_MS = 15_000\nexport const CAMERA_WARM_UP_MAX_DURATION_MS = 60_000\n\nlet bleRequestCounter = 0\n\n/** Short 4-hex-char correlation id sent over BLE instead of the full cloud\n * requestId UUID (wire v2 keeps BLE JSON small). */\nfunction mintBleRequestId(): string {\n bleRequestCounter = (bleRequestCounter + 1) & 0xffff\n return bleRequestCounter.toString(16).padStart(4, \"0\")\n}\n\nfunction toNativeCompression(compress: PhotoOpts[\"compress\"]): \"none\" | \"medium\" | \"heavy\" {\n if (compress === \"high\") return \"heavy\"\n if (compress === \"low\" || compress === \"medium\") return \"medium\"\n return \"none\"\n}\n\nexport class PhonePhotoCoordinator {\n // Cloud requestId → in-flight slot. The gated photo_response listener\n // (DeviceEventRouter) resolves short BLE ids via bleIdToCloud before calling\n // owns() / handlePhotoError().\n private readonly activeRequests = new Map<string, ActiveRequest>()\n /** Short BLE correlation id (4-char hex) → full cloud requestId. */\n private readonly bleIdToCloud = new Map<string, string>()\n private readonly activeWarmUps = new Map<\n string,\n {requestId: string; durationMs: number; expiryTimer?: ReturnType<typeof setTimeout>}\n >()\n\n /** Report-safe camera ownership snapshot for incident diagnostics. */\n getDiagnosticSnapshot(): Record<string, unknown> {\n return {\n captureOwners: [...new Set([...this.activeRequests.values()].map((request) => request.packageName))].sort(),\n warmUpOwners: [...this.activeWarmUps.keys()].sort(),\n activeCaptureCount: this.activeRequests.size,\n }\n }\n\n async takePhoto(packageName: string, opts: PhotoOpts): Promise<PhotoTaken> {\n const transferMethod = parsePhotoTransferMethod(opts.transferMethod)\n\n // Pre-check: if glasses aren't even connected, the BLE photo command\n // would be sent into the void and we'd wait 30s for the cloud long-poll\n // to time out. Fail fast with a typed error.\n //\n // We DON'T pre-check `hasCamera` here — the canonical capability data\n // lives in `getModelCapabilities(deviceModel)` from @mentra/engine and\n // pulling that into this file would add a cross-package import. If a\n // cameraless device receives the BLE photo command, the glasses-side\n // handler will return a photo_response error within ~1s and the gated\n // photo_response listener in MantleManager will short-circuit our\n // long-poll. Slower but correct.\n if (!isGlassesConnected(useGlassesStore.getState().connection)) {\n throw new PhotoError(\"GLASSES_NOT_CONNECTED\", \"Glasses are not connected\", \"command\", \"ble\")\n }\n\n // Text-mode sensor resolution is owned by ASG constants; keep cloud metadata on a stable\n // high-capacity tier and let the glasses ignore the public size when mode=text.\n const captureSize = opts.size ?? \"medium\"\n\n // 1) Presign via the cloud-v2 managed-photo service. Local miniapps use\n // ONLY the cloud-v2 path: the runtime presigns upload+read URLs and the\n // phone (as the device controller) delivers the bytes; the legacy\n // backend_url mint is gone from this flow.\n let requestId: string\n let uploadUrl: string\n let readUrl: string\n const flowStarted = performance.now()\n try {\n const presignStarted = performance.now()\n const r = await cloudClientService.startManagedPhoto({\n size: opts.mode === \"text\" ? \"max\" : captureSize,\n })\n const presignMs = Math.round(performance.now() - presignStarted)\n if (typeof __DEV__ !== \"undefined\" && __DEV__) {\n console.debug(\n `[PhonePhotoCoordinator] presign ${presignMs}ms size=${opts.mode === \"text\" ? \"max\" : captureSize} mode=${\n opts.mode ?? \"photo\"\n }`,\n )\n }\n requestId = r.requestId\n uploadUrl = r.uploadUrl\n readUrl = r.readUrl\n } catch (err) {\n throw this.toPhotoError(err, \"PHOTO_REQUEST_FAILED\", \"presign\", \"cloud-rest\")\n }\n\n // 2) Build the outcome Promise FIRST so both resolve+reject handles\n // are wired into activeRequests BEFORE any code path can produce\n // an error. Without this, a fast BLE photo_response (BATTERY_LOW\n // etc.) racing with the Promise constructor could fire\n // handlePhotoError() against a no-op rejectFn and silently drop\n // the error.\n // When the managed-photo upload URL is loopback (the local storage provider\n // reached over `adb reverse`), the glasses cannot reach it over WiFi —\n // localhost on the glasses is the glasses. Force BLE transfer so the phone\n // (which CAN reach the reversed runtime) relays the bytes; a public r2/s3\n // presigned URL stays on \"auto\".\n const isLoopbackUpload = /^https?:\\/\\/(localhost|127\\.0\\.0\\.1|10\\.0\\.2\\.2)\\b/.test(uploadUrl)\n\n const abort = new AbortController()\n const bleRequestId = mintBleRequestId()\n this.bleIdToCloud.set(bleRequestId, requestId)\n\n const outcome = new Promise<PhotoResult>((resolve, reject) => {\n this.activeRequests.set(requestId, {packageName, abort, resolve, reject})\n })\n\n // Last-resort watchdog for a wedged pipeline. The managed-photo ready push\n // normally resolves or rejects first; this only prevents an indefinite hang\n // if both the native terminal response and cloud push disappear.\n const captureWatchdog = setTimeout(() => {\n const e = this.activeRequests.get(requestId)\n if (!e) return\n e.abort.abort()\n e.reject(\n new PhotoError(\n \"CAPTURE_TIMEOUT\",\n \"Photo capture did not complete. The take_photo command, media processing, transfer, or upload may have stalled.\",\n \"capture\",\n isLoopbackUpload ? \"ble\" : \"wifi\",\n ),\n )\n }, CAPTURE_PIPELINE_TIMEOUT_MS)\n // Clear the watchdog on BOTH arms. Not `.finally()`: that would mint a new\n // promise that re-rejects unobserved whenever the photo fails (the caller\n // only awaits `outcome` itself), i.e. an unhandled rejection per failure.\n const clearWatchdog = () => clearTimeout(captureWatchdog)\n void outcome.then(clearWatchdog, clearWatchdog)\n\n // 3) Drive glasses over BLE. requestPhoto now resolves at terminal\n // photo_response success, so run it beside the cloud poll instead of\n // awaiting it before polling. Native defaults transferMethod to \"auto\"\n // (Wi-Fi direct with BLE fallback) unless the miniapp forces BLE.\n try {\n void BluetoothSdk.requestPhoto({\n requestId: bleRequestId,\n appId: packageName,\n size: normalizePhotoSize(captureSize),\n mode: opts.mode ?? \"photo\",\n webhookUrl: uploadUrl,\n authToken: null,\n ...(isLoopbackUpload ? {transferMethod: \"ble\" as const} : transferMethod ? {transferMethod} : {}),\n compress: toNativeCompression(opts.compress),\n save: opts.saveToGallery ?? false,\n sound: opts.sound ?? true,\n exposureTimeNs: opts.exposureTimeNs ?? null,\n iso: opts.iso,\n aeExposureDivisor: opts.aeExposureDivisor,\n isoCap: opts.isoCap,\n noiseReduction: opts.noiseReduction,\n edgeEnhancement: opts.edgeEnhancement,\n ...(opts.zsl != null ? {zsl: opts.zsl} : {}),\n ...(opts.mfnr != null ? {mfnr: opts.mfnr} : {}),\n ispDigitalGain: opts.ispDigitalGain,\n ispAnalogGain: opts.ispAnalogGain,\n }).catch((err) => {\n const e = this.activeRequests.get(requestId)\n if (!e) return\n e.abort.abort()\n e.reject(this.toPhotoError(err, \"BLE_SEND_FAILED\", \"command\", \"ble\"))\n })\n } catch (err) {\n this.activeRequests.delete(requestId)\n this.bleIdToCloud.delete(bleRequestId)\n throw this.toPhotoError(err, \"BLE_SEND_FAILED\", \"command\", \"ble\")\n }\n\n // 4) Await the runtime's photo.ready push (replaces the legacy long-poll).\n // handlePhotoError races against it and uses the same entry to reject\n // first.\n cloudClientService\n .awaitManagedPhotoReady(requestId)\n .then((res) => {\n const e = this.activeRequests.get(requestId)\n if (!e) return // already settled by handlePhotoError\n e.resolve({photoUrl: res.readUrl ?? readUrl, mimeType: \"image/jpeg\", size: -1})\n })\n .catch((err) => {\n const e = this.activeRequests.get(requestId)\n if (e) e.reject(this.toPhotoError(err, \"POLL_FAILED\", \"push\", \"ws\"))\n })\n\n try {\n const result = await outcome\n if (typeof __DEV__ !== \"undefined\" && __DEV__) {\n console.debug(\n `[PhonePhotoCoordinator] takePhoto complete ${Math.round(\n performance.now() - flowStarted,\n )}ms requestId=${requestId}`,\n )\n }\n return {\n photoUrl: result.photoUrl,\n mimeType: result.mimeType,\n size: result.size,\n requestId,\n }\n } finally {\n this.activeRequests.delete(requestId)\n this.bleIdToCloud.delete(bleRequestId)\n }\n }\n\n /** Map a short BLE requestId from photo_response back to the cloud requestId. */\n resolveCloudRequestId(bleOrCloudId: string): string {\n return this.bleIdToCloud.get(bleOrCloudId) ?? bleOrCloudId\n }\n\n /**\n * Pre-warm the glasses camera so the next takePhoto() is near-instant.\n *\n * Pure BLE — NO cloud presign, NO upload, NO long-poll. The phone mints and owns\n * the requestId, sends the warm-up command, and resolves when the camera reports\n * ready (the native promise resolves on the ready status event).\n */\n async warmUpCamera(\n packageName: string,\n opts: {\n size?: \"low\" | \"medium\" | \"high\" | \"max\"\n mode?: \"photo\" | \"text\"\n exposureTimeNs?: number\n durationMs?: number\n zsl?: boolean\n mfnr?: boolean\n },\n ): Promise<void> {\n // Pre-check: if glasses aren't connected, the BLE warm-up command would be\n // sent into the void. Fail fast with a typed error.\n if (!isGlassesConnected(useGlassesStore.getState().connection)) {\n throw new PhotoError(\"GLASSES_NOT_CONNECTED\", \"Glasses are not connected\", \"command\", \"ble\")\n }\n\n let lease: {requestId: string; durationMs: number; expiryTimer?: ReturnType<typeof setTimeout>} | undefined\n try {\n await this.stopWarmUpForApp(packageName)\n const requestId = mintBleRequestId()\n const requestedDuration =\n typeof opts.durationMs === \"number\" && Number.isFinite(opts.durationMs) && opts.durationMs > 0\n ? Math.round(opts.durationMs)\n : CAMERA_WARM_UP_DEFAULT_DURATION_MS\n const durationMs = Math.min(requestedDuration, CAMERA_WARM_UP_MAX_DURATION_MS)\n lease = {requestId, durationMs}\n this.activeWarmUps.set(packageName, lease)\n await BluetoothSdk.warmUpCamera({\n requestId,\n size: normalizePhotoSize(opts.size ?? \"medium\"),\n mode: opts.mode ?? \"photo\",\n exposureTimeNs: opts.exposureTimeNs ?? null,\n durationMs,\n ...(opts.zsl != null ? {zsl: opts.zsl} : {}),\n ...(opts.mfnr != null ? {mfnr: opts.mfnr} : {}),\n })\n if (this.activeWarmUps.get(packageName) === lease) {\n lease.expiryTimer = setTimeout(() => {\n if (this.activeWarmUps.get(packageName) === lease) {\n this.activeWarmUps.delete(packageName)\n }\n }, durationMs)\n }\n } catch (err) {\n if (lease && this.activeWarmUps.get(packageName) === lease) {\n if (lease.expiryTimer) clearTimeout(lease.expiryTimer)\n this.activeWarmUps.delete(packageName)\n }\n throw this.toPhotoError(err, \"WARM_UP_FAILED\", \"command\", \"ble\")\n }\n }\n\n /** Release a miniapp's warm-up even if its original warmUpCamera promise is still opening. */\n async stopWarmUpForApp(packageName: string): Promise<void> {\n const active = this.activeWarmUps.get(packageName)\n if (!active) return\n await BluetoothSdk.stopCameraWarmUp(active.requestId)\n if (this.activeWarmUps.get(packageName) === active) {\n this.activeWarmUps.delete(packageName)\n if (active.expiryTimer) clearTimeout(active.expiryTimer)\n }\n }\n\n /** True iff this requestId (short BLE id or cloud id) is one we're currently waiting on. */\n owns(requestId: string): boolean {\n const cloudId = this.resolveCloudRequestId(requestId)\n return this.activeRequests.has(cloudId)\n }\n\n /**\n * Called by MantleManager's gated photo_response listener when glasses\n * report an error (BATTERY_LOW, CAMERA_BUSY, etc.) for a phone-owned\n * requestId. Rejects the in-flight takePhoto Promise immediately.\n */\n handlePhotoError(requestId: string, errorCode: string, errorMessage: string): void {\n const cloudId = this.resolveCloudRequestId(requestId)\n const entry = this.activeRequests.get(cloudId)\n if (!entry) return\n this.bleIdToCloud.delete(requestId)\n // Abort the in-flight long-poll first so we don't double-resolve.\n entry.abort.abort()\n entry.reject(new PhotoError(errorCode || \"GLASSES_ERROR\", errorMessage || \"Glasses error\", \"capture\", \"ble\"))\n // cloud-v2 pending photo requests TTL out on their own; nothing to free.\n }\n\n private toPhotoError(err: unknown, fallbackCode: string, stage?: PhotoStage, transport?: PhotoTransport): PhotoError {\n if (err instanceof PhotoError) return err\n const code = (err as {code?: string})?.code\n const message = err instanceof Error ? err.message : String(err)\n return new PhotoError(code || fallbackCode, message, stage, transport)\n }\n}\n\n// Singleton — coordinator state is process-wide (mirrors phoneStreamCoordinator).\nexport const phonePhotoCoordinator = new PhonePhotoCoordinator()\n"]}
|
|
1
|
+
{"version":3,"file":"PhonePhotoCoordinator.js","sourceRoot":"","sources":["../../src/services/PhonePhotoCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * PhonePhotoCoordinator — owns local-miniapp takePhoto() end-to-end.\n *\n * Architecture:\n * miniapp → SDK → LocalMiniappRuntime → photo runtime hook\n * → coordinator.takePhoto(packageName, opts)\n * ├── (precheck) glasses connected + hasCamera\n * ├── cloudClientService.startManagedPhoto → {requestId, uploadUrl, readUrl} (cloud-v2 runtime presign)\n * ├── BluetoothSdk.requestPhoto(requestId, size, uploadUrl, compress, sound)\n * └── race:\n * - cloudClientService.awaitManagedPhotoReady(requestId) resolves on photo.ready push\n * - BluetoothSdk.requestPhoto rejects if terminal photo_response is an error\n * - handlePhotoError(requestId, code, message) rejects if MantleManager observes\n * the same BLE photo_response error before the native promise crosses the bridge\n *\n * `activeRequests` lets MantleManager's gated `photo_response` listener\n * short-circuit our long-poll with a typed error (CAMERA_BUSY, BATTERY_LOW,\n * etc.) instead of waiting 30s for cloud's timeout.\n */\n\nimport {parsePhotoCompression, type PhotoCompression} from \"@mentra/cloud-protocol/photo-compression\"\nimport BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\nimport type {PhotoSize, PhotoTransferMethod} from \"@mentra/bluetooth-sdk/internal\"\nimport {cloudClientService} from \"./CloudClientService\"\nimport {isGlassesConnected} from \"./GlassesReadiness\"\nimport {useGlassesStore} from \"../stores/glasses\"\n\ninterface PhotoResult {\n photoUrl: string\n mimeType: string\n size: number\n}\n\n/** Map legacy/cloud size names onto the native take_photo enum. */\nfunction normalizePhotoSize(value: unknown): PhotoSize {\n if (typeof value !== \"string\") return \"medium\"\n switch (value) {\n case \"small\":\n return \"low\"\n case \"large\":\n return \"high\"\n case \"full\":\n return \"max\"\n default:\n return ([\"low\", \"medium\", \"high\", \"max\"] as const).includes(value as PhotoSize) ? (value as PhotoSize) : \"medium\"\n }\n}\n\nexport interface PhotoOpts {\n /** Legacy cloud size names are normalized before the native take_photo command. */\n size?: \"low\" | \"medium\" | \"high\" | \"max\" | \"small\" | \"large\" | \"full\"\n mode?: \"photo\" | \"text\"\n /** Select direct-only, phone-relayed BLE, or the default Wi-Fi/BLE fallback policy. */\n transferMethod?: PhotoTransferMethod\n compress?: PhotoCompression\n sound?: boolean\n saveToGallery?: boolean\n exposureTimeNs?: number\n iso?: number | null\n aeExposureDivisor?: number\n isoCap?: number\n noiseReduction?: boolean\n edgeEnhancement?: boolean\n /** ZSL preview buffering. */\n zsl?: boolean\n /** MFNR still capture. */\n mfnr?: boolean\n ispDigitalGain?: number\n ispAnalogGain?: string\n}\n\nexport interface PhotoTaken {\n photoUrl: string\n mimeType: string\n size: number\n requestId: string\n}\n\n/** Pipeline stage a photo request failed at — surfaced to the miniapp so a dev\n * sees exactly where it broke, not just a flattened message. */\nexport type PhotoStage = \"presign\" | \"command\" | \"capture\" | \"upload\" | \"push\"\n/** Transport in play at the point of failure. */\nexport type PhotoTransport = \"cloud-rest\" | \"ble\" | \"wifi\" | \"ws\"\n\nexport class PhotoError extends Error {\n constructor(\n public readonly code: string,\n message: string,\n public readonly stage?: PhotoStage,\n public readonly transport?: PhotoTransport,\n ) {\n super(message)\n this.name = \"PhotoError\"\n }\n}\n\nfunction parsePhotoTransferMethod(value: unknown): PhotoTransferMethod | undefined {\n if (value === undefined) return undefined\n if (value === \"auto\" || value === \"direct\" || value === \"ble\") return value\n throw new PhotoError(\n \"INVALID_ARGUMENT\",\n `Invalid transferMethod ${JSON.stringify(value)}. Expected \"auto\", \"direct\", or \"ble\".`,\n )\n}\n\ninterface ActiveRequest {\n packageName: string\n abort: AbortController\n resolve: (r: PhotoResult) => void\n reject: (err: Error) => void\n}\n\n/**\n * Last-resort ceiling for the complete capture + encode + transfer + upload\n * pipeline. This must exceed the managed-photo service's 30s ready-push\n * timeout so the cloud can return its more specific failure first.\n *\n * Text mode commonly spends several seconds detecting/cropping the region and\n * can take another 10s+ to transfer over BLE. A 15s ceiling incorrectly\n * rejected successful captures while their upload was still in progress.\n */\nexport const CAPTURE_PIPELINE_TIMEOUT_MS = 45_000\nexport const CAMERA_WARM_UP_DEFAULT_DURATION_MS = 15_000\nexport const CAMERA_WARM_UP_MAX_DURATION_MS = 300_000\n\nlet bleRequestCounter = 0\n\n/** Short 4-hex-char correlation id sent over BLE instead of the full cloud\n * requestId UUID (wire v2 keeps BLE JSON small). */\nfunction mintBleRequestId(): string {\n bleRequestCounter = (bleRequestCounter + 1) & 0xffff\n return bleRequestCounter.toString(16).padStart(4, \"0\")\n}\n\nexport class PhonePhotoCoordinator {\n // Cloud requestId → in-flight slot. The gated photo_response listener\n // (DeviceEventRouter) resolves short BLE ids via bleIdToCloud before calling\n // owns() / handlePhotoError().\n private readonly activeRequests = new Map<string, ActiveRequest>()\n /** Short BLE correlation id (4-char hex) → full cloud requestId. */\n private readonly bleIdToCloud = new Map<string, string>()\n private readonly activeWarmUps = new Map<\n string,\n {requestId: string; durationMs: number; expiryTimer?: ReturnType<typeof setTimeout>}\n >()\n\n /** Report-safe camera ownership snapshot for incident diagnostics. */\n getDiagnosticSnapshot(): Record<string, unknown> {\n return {\n captureOwners: [...new Set([...this.activeRequests.values()].map((request) => request.packageName))].sort(),\n warmUpOwners: [...this.activeWarmUps.keys()].sort(),\n activeCaptureCount: this.activeRequests.size,\n }\n }\n\n async takePhoto(packageName: string, opts: PhotoOpts): Promise<PhotoTaken> {\n const compress = parsePhotoCompression(opts.compress)\n const transferMethod = parsePhotoTransferMethod(opts.transferMethod)\n\n // Pre-check: if glasses aren't even connected, the BLE photo command\n // would be sent into the void and we'd wait 30s for the cloud long-poll\n // to time out. Fail fast with a typed error.\n //\n // We DON'T pre-check `hasCamera` here — the canonical capability data\n // lives in `getModelCapabilities(deviceModel)` from @mentra/engine and\n // pulling that into this file would add a cross-package import. If a\n // cameraless device receives the BLE photo command, the glasses-side\n // handler will return a photo_response error within ~1s and the gated\n // photo_response listener in MantleManager will short-circuit our\n // long-poll. Slower but correct.\n if (!isGlassesConnected(useGlassesStore.getState().connection)) {\n throw new PhotoError(\"GLASSES_NOT_CONNECTED\", \"Glasses are not connected\", \"command\", \"ble\")\n }\n\n // Text-mode sensor resolution is owned by ASG constants; the glasses ignore the public\n // size when mode=text.\n const captureSize = opts.size ?? \"medium\"\n\n // 1) Presign via the cloud-v2 managed-photo service. Local miniapps use\n // ONLY the cloud-v2 path: the runtime presigns upload+read URLs and the\n // phone (as the device controller) delivers the bytes; the legacy\n // backend_url mint is gone from this flow.\n let requestId: string\n let uploadUrl: string\n let readUrl: string\n const flowStarted = performance.now()\n try {\n const presignStarted = performance.now()\n const r = await cloudClientService.startManagedPhoto()\n const presignMs = Math.round(performance.now() - presignStarted)\n if (typeof __DEV__ !== \"undefined\" && __DEV__) {\n console.debug(\n `[PhonePhotoCoordinator] presign ${presignMs}ms size=${opts.mode === \"text\" ? \"max\" : captureSize} mode=${\n opts.mode ?? \"photo\"\n }`,\n )\n }\n requestId = r.requestId\n uploadUrl = r.uploadUrl\n readUrl = r.readUrl\n } catch (err) {\n throw this.toPhotoError(err, \"PHOTO_REQUEST_FAILED\", \"presign\", \"cloud-rest\")\n }\n\n // 2) Build the outcome Promise FIRST so both resolve+reject handles\n // are wired into activeRequests BEFORE any code path can produce\n // an error. Without this, a fast BLE photo_response (BATTERY_LOW\n // etc.) racing with the Promise constructor could fire\n // handlePhotoError() against a no-op rejectFn and silently drop\n // the error.\n // When the managed-photo upload URL is loopback (the local storage provider\n // reached over `adb reverse`), the glasses cannot reach it over WiFi —\n // localhost on the glasses is the glasses. Force BLE transfer so the phone\n // (which CAN reach the reversed runtime) relays the bytes; a public r2/s3\n // presigned URL stays on \"auto\".\n const isLoopbackUpload = /^https?:\\/\\/(localhost|127\\.0\\.0\\.1|10\\.0\\.2\\.2)\\b/.test(uploadUrl)\n\n const abort = new AbortController()\n const bleRequestId = mintBleRequestId()\n this.bleIdToCloud.set(bleRequestId, requestId)\n\n const outcome = new Promise<PhotoResult>((resolve, reject) => {\n this.activeRequests.set(requestId, {packageName, abort, resolve, reject})\n })\n\n // Last-resort watchdog for a wedged pipeline. The managed-photo ready push\n // normally resolves or rejects first; this only prevents an indefinite hang\n // if both the native terminal response and cloud push disappear.\n const captureWatchdog = setTimeout(() => {\n const e = this.activeRequests.get(requestId)\n if (!e) return\n e.abort.abort()\n e.reject(\n new PhotoError(\n \"CAPTURE_TIMEOUT\",\n \"Photo capture did not complete. The take_photo command, media processing, transfer, or upload may have stalled.\",\n \"capture\",\n isLoopbackUpload ? \"ble\" : \"wifi\",\n ),\n )\n }, CAPTURE_PIPELINE_TIMEOUT_MS)\n // Clear the watchdog on BOTH arms. Not `.finally()`: that would mint a new\n // promise that re-rejects unobserved whenever the photo fails (the caller\n // only awaits `outcome` itself), i.e. an unhandled rejection per failure.\n const clearWatchdog = () => clearTimeout(captureWatchdog)\n void outcome.then(clearWatchdog, clearWatchdog)\n\n // 3) Drive glasses over BLE. requestPhoto now resolves at terminal\n // photo_response success, so run it beside the cloud poll instead of\n // awaiting it before polling. Native defaults transferMethod to \"auto\"\n // (Wi-Fi direct with BLE fallback) unless the miniapp forces BLE.\n try {\n void BluetoothSdk.requestPhoto({\n requestId: bleRequestId,\n appId: packageName,\n size: normalizePhotoSize(captureSize),\n mode: opts.mode ?? \"photo\",\n webhookUrl: uploadUrl,\n authToken: null,\n ...(isLoopbackUpload ? {transferMethod: \"ble\" as const} : transferMethod ? {transferMethod} : {}),\n compress,\n save: opts.saveToGallery ?? false,\n sound: opts.sound ?? true,\n exposureTimeNs: opts.exposureTimeNs ?? null,\n iso: opts.iso,\n aeExposureDivisor: opts.aeExposureDivisor,\n isoCap: opts.isoCap,\n noiseReduction: opts.noiseReduction,\n edgeEnhancement: opts.edgeEnhancement,\n ...(opts.zsl != null ? {zsl: opts.zsl} : {}),\n ...(opts.mfnr != null ? {mfnr: opts.mfnr} : {}),\n ispDigitalGain: opts.ispDigitalGain,\n ispAnalogGain: opts.ispAnalogGain,\n }).then((response) => {\n // Native success is emitted only after the direct/phone-relayed upload\n // completes. It is an independent completion signal when photo.ready\n // cannot reach this phone (for example, a cross-pod WS routing gap).\n // Older native bridges may resolve at dispatch with no response: those\n // must still wait for the cloud push, never return an unuploaded image.\n if (response?.state !== \"success\" || response.requestId !== bleRequestId) return\n const e = this.activeRequests.get(requestId)\n if (!e || e.abort.signal.aborted) return\n e.resolve({photoUrl: readUrl, mimeType: \"image/jpeg\", size: -1})\n }).catch((err) => {\n const e = this.activeRequests.get(requestId)\n if (!e) return\n e.abort.abort()\n e.reject(this.toPhotoError(err, \"BLE_SEND_FAILED\", \"command\", \"ble\"))\n })\n } catch (err) {\n this.activeRequests.delete(requestId)\n this.bleIdToCloud.delete(bleRequestId)\n throw this.toPhotoError(err, \"BLE_SEND_FAILED\", \"command\", \"ble\")\n }\n\n // 4) Await the runtime's photo.ready push (replaces the legacy long-poll).\n // handlePhotoError races against it and uses the same entry to reject\n // first.\n cloudClientService\n .awaitManagedPhotoReady(requestId)\n .then((res) => {\n const e = this.activeRequests.get(requestId)\n if (!e) return // already settled by handlePhotoError\n e.resolve({photoUrl: res.readUrl ?? readUrl, mimeType: \"image/jpeg\", size: -1})\n })\n .catch((err) => {\n const e = this.activeRequests.get(requestId)\n if (e) e.reject(this.toPhotoError(err, \"POLL_FAILED\", \"push\", \"ws\"))\n })\n\n try {\n const result = await outcome\n if (typeof __DEV__ !== \"undefined\" && __DEV__) {\n console.debug(\n `[PhonePhotoCoordinator] takePhoto complete ${Math.round(\n performance.now() - flowStarted,\n )}ms requestId=${requestId}`,\n )\n }\n return {\n photoUrl: result.photoUrl,\n mimeType: result.mimeType,\n size: result.size,\n requestId,\n }\n } finally {\n this.activeRequests.delete(requestId)\n this.bleIdToCloud.delete(bleRequestId)\n }\n }\n\n /** Map a short BLE requestId from photo_response back to the cloud requestId. */\n resolveCloudRequestId(bleOrCloudId: string): string {\n return this.bleIdToCloud.get(bleOrCloudId) ?? bleOrCloudId\n }\n\n /**\n * Pre-warm the glasses camera so the next takePhoto() is near-instant.\n *\n * Pure BLE — NO cloud presign, NO upload, NO long-poll. The phone mints and owns\n * the requestId, sends the warm-up command, and resolves when the camera reports\n * ready (the native promise resolves on the ready status event).\n */\n async warmUpCamera(\n packageName: string,\n opts: {\n size?: \"low\" | \"medium\" | \"high\" | \"max\"\n mode?: \"photo\" | \"text\"\n exposureTimeNs?: number\n durationMs?: number\n zsl?: boolean\n mfnr?: boolean\n },\n ): Promise<void> {\n // Pre-check: if glasses aren't connected, the BLE warm-up command would be\n // sent into the void. Fail fast with a typed error.\n if (!isGlassesConnected(useGlassesStore.getState().connection)) {\n throw new PhotoError(\"GLASSES_NOT_CONNECTED\", \"Glasses are not connected\", \"command\", \"ble\")\n }\n\n let lease: {requestId: string; durationMs: number; expiryTimer?: ReturnType<typeof setTimeout>} | undefined\n try {\n await this.stopWarmUpForApp(packageName)\n const requestId = mintBleRequestId()\n const requestedDuration =\n typeof opts.durationMs === \"number\" && Number.isFinite(opts.durationMs) && opts.durationMs > 0\n ? Math.round(opts.durationMs)\n : CAMERA_WARM_UP_DEFAULT_DURATION_MS\n const durationMs = Math.min(requestedDuration, CAMERA_WARM_UP_MAX_DURATION_MS)\n lease = {requestId, durationMs}\n this.activeWarmUps.set(packageName, lease)\n await BluetoothSdk.warmUpCamera({\n requestId,\n size: normalizePhotoSize(opts.size ?? \"medium\"),\n mode: opts.mode ?? \"photo\",\n exposureTimeNs: opts.exposureTimeNs ?? null,\n durationMs,\n ...(opts.zsl != null ? {zsl: opts.zsl} : {}),\n ...(opts.mfnr != null ? {mfnr: opts.mfnr} : {}),\n })\n if (this.activeWarmUps.get(packageName) === lease) {\n lease.expiryTimer = setTimeout(() => {\n if (this.activeWarmUps.get(packageName) === lease) {\n this.activeWarmUps.delete(packageName)\n }\n }, durationMs)\n }\n } catch (err) {\n if (lease && this.activeWarmUps.get(packageName) === lease) {\n if (lease.expiryTimer) clearTimeout(lease.expiryTimer)\n this.activeWarmUps.delete(packageName)\n }\n throw this.toPhotoError(err, \"WARM_UP_FAILED\", \"command\", \"ble\")\n }\n }\n\n /** Release a miniapp's warm-up even if its original warmUpCamera promise is still opening. */\n async stopWarmUpForApp(packageName: string): Promise<void> {\n const active = this.activeWarmUps.get(packageName)\n if (!active) return\n await BluetoothSdk.stopCameraWarmUp(active.requestId)\n if (this.activeWarmUps.get(packageName) === active) {\n this.activeWarmUps.delete(packageName)\n if (active.expiryTimer) clearTimeout(active.expiryTimer)\n }\n }\n\n /** True iff this requestId (short BLE id or cloud id) is one we're currently waiting on. */\n owns(requestId: string): boolean {\n const cloudId = this.resolveCloudRequestId(requestId)\n return this.activeRequests.has(cloudId)\n }\n\n /**\n * Called by MantleManager's gated photo_response listener when glasses\n * report an error (BATTERY_LOW, CAMERA_BUSY, etc.) for a phone-owned\n * requestId. Rejects the in-flight takePhoto Promise immediately.\n */\n handlePhotoError(requestId: string, errorCode: string, errorMessage: string): void {\n const cloudId = this.resolveCloudRequestId(requestId)\n const entry = this.activeRequests.get(cloudId)\n if (!entry) return\n this.bleIdToCloud.delete(requestId)\n // Abort the in-flight long-poll first so we don't double-resolve.\n entry.abort.abort()\n entry.reject(new PhotoError(errorCode || \"GLASSES_ERROR\", errorMessage || \"Glasses error\", \"capture\", \"ble\"))\n // cloud-v2 pending photo requests TTL out on their own; nothing to free.\n }\n\n private toPhotoError(err: unknown, fallbackCode: string, stage?: PhotoStage, transport?: PhotoTransport): PhotoError {\n if (err instanceof PhotoError) return err\n const code = (err as {code?: string})?.code\n const message = err instanceof Error ? err.message : String(err)\n return new PhotoError(code || fallbackCode, message, stage, transport)\n }\n}\n\n// Singleton — coordinator state is process-wide (mirrors phoneStreamCoordinator).\nexport const phonePhotoCoordinator = new PhonePhotoCoordinator()\n"]}
|
|
@@ -38,6 +38,35 @@ export declare class SoftapCallError extends Error {
|
|
|
38
38
|
}
|
|
39
39
|
/** Gallery sync already learned this: glasses report enabled before the SSID is in the phone scan. */
|
|
40
40
|
export declare const HOTSPOT_BROADCAST_WAIT_MS = 3000;
|
|
41
|
+
/**
|
|
42
|
+
* What the glasses→phone hop is worth at 540p over a hotspot with one client and no contention.
|
|
43
|
+
*
|
|
44
|
+
* Not the ACS ceiling: this link is a metre of air between two devices that are already paired,
|
|
45
|
+
* and the profile's `maxBitrateBps` describes the phone→Teams hop, which crosses the internet.
|
|
46
|
+
* Sharing one number between them makes the easy hop obey the hard hop's limits.
|
|
47
|
+
*/
|
|
48
|
+
export declare const GLASSES_PHONE_BITRATE_BPS = 2500000;
|
|
49
|
+
/** Field names are the glasses' `WhipStreamConfig` parser's, where `bitrate` means the maximum. */
|
|
50
|
+
export interface SoftapVideoPolicy {
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
fps: number;
|
|
54
|
+
bitrate: number;
|
|
55
|
+
initialBitrateBps: number;
|
|
56
|
+
minBitrateBps: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The bitrate the glasses are told to hold on the hop to the phone.
|
|
60
|
+
*
|
|
61
|
+
* The hotspot has its own bitrate budget, independent of the phone-to-Teams cap. Above 540p,
|
|
62
|
+
* allow a higher requested ceiling while retaining the hotspot budget for low WAN caps.
|
|
63
|
+
*/
|
|
64
|
+
export declare function softapVideoPolicy(video: {
|
|
65
|
+
width: number;
|
|
66
|
+
height: number;
|
|
67
|
+
fps: number;
|
|
68
|
+
maxBitrateBps: number;
|
|
69
|
+
}): SoftapVideoPolicy;
|
|
41
70
|
export type SoftapStepStatus = "pending" | "running" | "done" | "failed";
|
|
42
71
|
/**
|
|
43
72
|
* One step as the UI should show it. `detail` is the fact the step produced (SSID, phone address,
|
|
@@ -65,6 +94,15 @@ export interface SoftapProgress {
|
|
|
65
94
|
/** Sub-status callback a step can use to narrate what it is doing while it runs. */
|
|
66
95
|
export type SoftapStepReporter = (detail: string) => void;
|
|
67
96
|
export interface SoftapCallDeps {
|
|
97
|
+
/**
|
|
98
|
+
* Is this phone's Wi-Fi radio on? Asked before anything is built.
|
|
99
|
+
*
|
|
100
|
+
* The station radio is what reaches the glasses, and no app can turn it on since Android 10.
|
|
101
|
+
* Finding out at `scopedJoin` means the glasses already raised a hotspot for a join that cannot
|
|
102
|
+
* happen, so the wearer pays a teardown for a condition that was knowable up front. Optional:
|
|
103
|
+
* a host that cannot answer skips the preflight and relies on the native throw at `scopedJoin`.
|
|
104
|
+
*/
|
|
105
|
+
isWifiEnabled?(): Promise<boolean>;
|
|
68
106
|
/** Enable the glasses hotspot and return its credentials. */
|
|
69
107
|
startHotspot(report?: SoftapStepReporter): Promise<{
|
|
70
108
|
ssid: string;
|
|
@@ -228,6 +266,19 @@ export declare class SoftapCallTransport {
|
|
|
228
266
|
* "Cannot start glasses hotspot" followed by a scoped join that never found the SSID.
|
|
229
267
|
*/
|
|
230
268
|
lastTeardownFailures(): SoftapStep[];
|
|
269
|
+
/**
|
|
270
|
+
* Refuse the call while the Wi-Fi radio is off, before the glasses are asked for anything.
|
|
271
|
+
*
|
|
272
|
+
* Ordered ahead of the hotspot step rather than folded into `scopedJoin` because the cost of
|
|
273
|
+
* learning late is paid by the glasses: a hotspot raised, a teardown, and a wearer watching a
|
|
274
|
+
* checklist fail at step 2 for a toggle on their own phone. The native join keeps its own throw
|
|
275
|
+
* for the radio that goes off in between — this is the explainable path, that one is the racy
|
|
276
|
+
* backstop.
|
|
277
|
+
*
|
|
278
|
+
* A probe that throws is treated as "unknown, carry on": an unanswerable question about the
|
|
279
|
+
* radio must not be the thing that stops a call the radio would have carried.
|
|
280
|
+
*/
|
|
281
|
+
private preflightWifi;
|
|
231
282
|
/**
|
|
232
283
|
* Runs the sequence. On any failure the partial sequence is torn down before the error is
|
|
233
284
|
* rethrown, so a failed start never leaves a hotspot up or a publisher running.
|
|
@@ -284,6 +335,16 @@ export declare function createSoftapCallDeps(args: {
|
|
|
284
335
|
meetingUrl: string;
|
|
285
336
|
token: string;
|
|
286
337
|
displayName?: string;
|
|
338
|
+
/**
|
|
339
|
+
* The ACS outgoing profile. Used here only to size the glasses→phone hop; when absent the
|
|
340
|
+
* glasses keep their WHIP defaults, which is what every pre-policy build did.
|
|
341
|
+
*/
|
|
342
|
+
video?: {
|
|
343
|
+
width: number;
|
|
344
|
+
height: number;
|
|
345
|
+
fps: number;
|
|
346
|
+
maxBitrateBps: number;
|
|
347
|
+
};
|
|
287
348
|
/** Resolves when the meeting reports a frame reached ACS; rejects on a failed feed. */
|
|
288
349
|
awaitFirstFrame: () => Promise<void>;
|
|
289
350
|
subsystems: {
|
|
@@ -293,6 +354,8 @@ export declare function createSoftapCallDeps(args: {
|
|
|
293
354
|
password?: string;
|
|
294
355
|
localIp?: string;
|
|
295
356
|
}>;
|
|
357
|
+
/** Whether this phone's Wi-Fi radio is on. Optional: only Android hosts can answer it. */
|
|
358
|
+
isWifiEnabled?: () => Promise<boolean>;
|
|
296
359
|
joinScopedNetwork: (ssid: string, passphrase: string, gateway?: string) => Promise<string | undefined>;
|
|
297
360
|
leaveScopedNetwork: () => Promise<void>;
|
|
298
361
|
cancelScopedNetworkJoin?: () => Promise<void>;
|
|
@@ -318,6 +381,7 @@ export declare function createSoftapCallDeps(args: {
|
|
|
318
381
|
};
|
|
319
382
|
traceId: string;
|
|
320
383
|
captureAudio?: boolean;
|
|
384
|
+
video?: SoftapVideoPolicy;
|
|
321
385
|
}) => Promise<unknown>;
|
|
322
386
|
stopPublishing: (packageName: string) => Promise<void>;
|
|
323
387
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SoftapCallTransport.d.ts","sourceRoot":"","sources":["../../src/services/SoftapCallTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,yDAAyD;AACzD,eAAO,MAAM,YAAY,kEAAmE,CAAA;AAE5F,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;AAE9E,+FAA+F;AAC/F,qBAAa,eAAgB,SAAQ,KAAK;
|
|
1
|
+
{"version":3,"file":"SoftapCallTransport.d.ts","sourceRoot":"","sources":["../../src/services/SoftapCallTransport.ts"],"names":[],"mappings":"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"}
|
|
@@ -37,6 +37,54 @@ export class SoftapCallError extends Error {
|
|
|
37
37
|
}
|
|
38
38
|
/** Gallery sync already learned this: glasses report enabled before the SSID is in the phone scan. */
|
|
39
39
|
export const HOTSPOT_BROADCAST_WAIT_MS = 3_000;
|
|
40
|
+
/**
|
|
41
|
+
* What the glasses→phone hop is worth at 540p over a hotspot with one client and no contention.
|
|
42
|
+
*
|
|
43
|
+
* Not the ACS ceiling: this link is a metre of air between two devices that are already paired,
|
|
44
|
+
* and the profile's `maxBitrateBps` describes the phone→Teams hop, which crosses the internet.
|
|
45
|
+
* Sharing one number between them makes the easy hop obey the hard hop's limits.
|
|
46
|
+
*/
|
|
47
|
+
export const GLASSES_PHONE_BITRATE_BPS = 2_500_000;
|
|
48
|
+
/** Where the encoder starts. High, because the link is good from the first frame. */
|
|
49
|
+
const SOFTAP_START_BITRATE_BPS = 2_000_000;
|
|
50
|
+
/**
|
|
51
|
+
* The floor. This is the whole point of the policy.
|
|
52
|
+
*
|
|
53
|
+
* WHIP's default has none, so after an ICE interruption WebRTC restarts at its own minimum and
|
|
54
|
+
* climbs by probing — which is the "potato for a while" the wearer reports. The link did not get
|
|
55
|
+
* worse; the estimator merely forgot what it knew. A floor says: this hop is a hotspot, not a
|
|
56
|
+
* congested uplink, and it is never worth less than this.
|
|
57
|
+
*
|
|
58
|
+
* That trade is the opposite of Mentra-Call#27's cloud-WHIP review: a 1 Mbps floor on Auto over
|
|
59
|
+
* LTE turns a weak WAN into freezes. This function is SoftAP-only. Cloud WHIP must not call it.
|
|
60
|
+
*/
|
|
61
|
+
const SOFTAP_MIN_BITRATE_BPS = 1_200_000;
|
|
62
|
+
/**
|
|
63
|
+
* The bitrate the glasses are told to hold on the hop to the phone.
|
|
64
|
+
*
|
|
65
|
+
* The hotspot has its own bitrate budget, independent of the phone-to-Teams cap. Above 540p,
|
|
66
|
+
* allow a higher requested ceiling while retaining the hotspot budget for low WAN caps.
|
|
67
|
+
*/
|
|
68
|
+
export function softapVideoPolicy(video) {
|
|
69
|
+
const isSmall = video.width * video.height <= 960 * 540;
|
|
70
|
+
if (!Number.isFinite(video.maxBitrateBps) || video.maxBitrateBps <= 0) {
|
|
71
|
+
throw new Error(`invalid SoftAP video bitrate: ${video.maxBitrateBps}`);
|
|
72
|
+
}
|
|
73
|
+
const max = isSmall ? GLASSES_PHONE_BITRATE_BPS : Math.max(GLASSES_PHONE_BITRATE_BPS, video.maxBitrateBps);
|
|
74
|
+
const initialBitrateBps = Math.min(SOFTAP_START_BITRATE_BPS, max);
|
|
75
|
+
const minBitrateBps = Math.min(SOFTAP_MIN_BITRATE_BPS, initialBitrateBps);
|
|
76
|
+
if (!(minBitrateBps <= initialBitrateBps && initialBitrateBps <= max)) {
|
|
77
|
+
throw new Error(`incoherent SoftAP video policy: min=${minBitrateBps} start=${initialBitrateBps} max=${max}`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
width: video.width,
|
|
81
|
+
height: video.height,
|
|
82
|
+
fps: video.fps,
|
|
83
|
+
bitrate: max,
|
|
84
|
+
initialBitrateBps,
|
|
85
|
+
minBitrateBps,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
40
88
|
/**
|
|
41
89
|
* Android's WifiNetworkSpecifier called onUnavailable. The native message lists three causes
|
|
42
90
|
* because the callback does not say which one happened; on the 18:02 Samsung path the SSID was
|
|
@@ -204,6 +252,41 @@ export class SoftapCallTransport {
|
|
|
204
252
|
}
|
|
205
253
|
return [...this.teardownFailures];
|
|
206
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Refuse the call while the Wi-Fi radio is off, before the glasses are asked for anything.
|
|
257
|
+
*
|
|
258
|
+
* Ordered ahead of the hotspot step rather than folded into `scopedJoin` because the cost of
|
|
259
|
+
* learning late is paid by the glasses: a hotspot raised, a teardown, and a wearer watching a
|
|
260
|
+
* checklist fail at step 2 for a toggle on their own phone. The native join keeps its own throw
|
|
261
|
+
* for the radio that goes off in between — this is the explainable path, that one is the racy
|
|
262
|
+
* backstop.
|
|
263
|
+
*
|
|
264
|
+
* A probe that throws is treated as "unknown, carry on": an unanswerable question about the
|
|
265
|
+
* radio must not be the thing that stops a call the radio would have carried.
|
|
266
|
+
*/
|
|
267
|
+
async preflightWifi() {
|
|
268
|
+
if (!this.deps.isWifiEnabled)
|
|
269
|
+
return;
|
|
270
|
+
let enabled;
|
|
271
|
+
try {
|
|
272
|
+
enabled = await this.deps.isWifiEnabled();
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
softapTraceFailure("softap_wifi_preflight_unknown", {
|
|
276
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
softapTrace("softap_wifi_preflight", { enabled });
|
|
281
|
+
if (enabled)
|
|
282
|
+
return;
|
|
283
|
+
this.setStep("hotspot", { status: "failed", error: "This phone's Wi-Fi is off" });
|
|
284
|
+
this.emitProgress();
|
|
285
|
+
// Code in the message as well as on the error: the miniapp classifies on the message today
|
|
286
|
+
// (the structured code only arrives on the join rejection), and every other SoftAP failure
|
|
287
|
+
// carries its code the same way.
|
|
288
|
+
throw new SoftapCallError("hotspot", "SOFTAP_WIFI_DISABLED", "SOFTAP_WIFI_DISABLED: this phone's Wi-Fi is off, so it cannot reach the glasses.");
|
|
289
|
+
}
|
|
207
290
|
/**
|
|
208
291
|
* Runs the sequence. On any failure the partial sequence is torn down before the error is
|
|
209
292
|
* rethrown, so a failed start never leaves a hotspot up or a publisher running.
|
|
@@ -239,6 +322,7 @@ export class SoftapCallTransport {
|
|
|
239
322
|
softapTrace("softap_call_start", { traceId });
|
|
240
323
|
this.emitProgress();
|
|
241
324
|
try {
|
|
325
|
+
await this.preflightWifi();
|
|
242
326
|
await this.step(generation, "hotspot", "HOTSPOT_FAILED", async (report) => {
|
|
243
327
|
report("Asking the glasses to turn on their hotspot");
|
|
244
328
|
const hotspot = await this.deps.startHotspot(report);
|
|
@@ -571,6 +655,7 @@ export function createSoftapCallDeps(args) {
|
|
|
571
655
|
const hotspotBroadcastWaitMs = args.hotspotBroadcastWaitMs ?? HOTSPOT_BROADCAST_WAIT_MS;
|
|
572
656
|
let gatewayAddress;
|
|
573
657
|
return {
|
|
658
|
+
isWifiEnabled: subsystems.isWifiEnabled ? () => subsystems.isWifiEnabled() : undefined,
|
|
574
659
|
startHotspot: async (report) => {
|
|
575
660
|
const enable = async () => {
|
|
576
661
|
const status = await subsystems.setHotspotState(true);
|
|
@@ -598,7 +683,7 @@ export function createSoftapCallDeps(args) {
|
|
|
598
683
|
report?.("Glasses hotspot did not start; turning it off and trying again");
|
|
599
684
|
await subsystems.setHotspotState(false);
|
|
600
685
|
if (hotspotBroadcastWaitMs > 0) {
|
|
601
|
-
await new Promise(resolve => setTimeout(resolve, Math.min(1_000, hotspotBroadcastWaitMs)));
|
|
686
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(1_000, hotspotBroadcastWaitMs)));
|
|
602
687
|
}
|
|
603
688
|
return await enable();
|
|
604
689
|
}
|
|
@@ -608,7 +693,7 @@ export function createSoftapCallDeps(args) {
|
|
|
608
693
|
return;
|
|
609
694
|
softapTrace("hotspot_broadcast_wait", { ms: hotspotBroadcastWaitMs });
|
|
610
695
|
report?.(`Giving the hotspot ${Math.round(hotspotBroadcastWaitMs / 1000)}s to start broadcasting`);
|
|
611
|
-
await new Promise(resolve => setTimeout(resolve, hotspotBroadcastWaitMs));
|
|
696
|
+
await new Promise((resolve) => setTimeout(resolve, hotspotBroadcastWaitMs));
|
|
612
697
|
},
|
|
613
698
|
stopHotspot: async () => {
|
|
614
699
|
await subsystems.setHotspotState(false);
|
|
@@ -635,7 +720,7 @@ export function createSoftapCallDeps(args) {
|
|
|
635
720
|
gatewayAddress = status.localIp;
|
|
636
721
|
if (hotspotBroadcastWaitMs > 0) {
|
|
637
722
|
report?.(`Giving the hotspot ${Math.round(hotspotBroadcastWaitMs / 1000)}s to start broadcasting`);
|
|
638
|
-
await new Promise(resolve => setTimeout(resolve, hotspotBroadcastWaitMs));
|
|
723
|
+
await new Promise((resolve) => setTimeout(resolve, hotspotBroadcastWaitMs));
|
|
639
724
|
}
|
|
640
725
|
address = await joinOnce(status.ssid, status.password);
|
|
641
726
|
}
|
|
@@ -719,6 +804,20 @@ export function createSoftapCallDeps(args) {
|
|
|
719
804
|
report?.(lc3Uplink
|
|
720
805
|
? "Publishing video only; the wearer's voice comes over Bluetooth LC3"
|
|
721
806
|
: "Publishing video and the glasses microphone");
|
|
807
|
+
const policy = args.video ? softapVideoPolicy(args.video) : undefined;
|
|
808
|
+
if (policy) {
|
|
809
|
+
// Traced at send time rather than read back: the glasses log what they applied in
|
|
810
|
+
// `applyBitrateConstraints`, and the pair of lines is what shows a clamp we did not
|
|
811
|
+
// intend. One line alone can only ever show agreement with itself.
|
|
812
|
+
softapTrace("softap_video_policy", {
|
|
813
|
+
width: policy.width,
|
|
814
|
+
height: policy.height,
|
|
815
|
+
fps: policy.fps,
|
|
816
|
+
min: policy.minBitrateBps,
|
|
817
|
+
start: policy.initialBitrateBps,
|
|
818
|
+
max: policy.bitrate,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
722
821
|
try {
|
|
723
822
|
await subsystems.startPublishing(packageName, {
|
|
724
823
|
streamUrl: ingestUrl,
|
|
@@ -727,6 +826,7 @@ export function createSoftapCallDeps(args) {
|
|
|
727
826
|
ice: { stun: "" },
|
|
728
827
|
traceId,
|
|
729
828
|
captureAudio: !lc3Uplink,
|
|
829
|
+
...(policy ? { video: policy } : {}),
|
|
730
830
|
});
|
|
731
831
|
}
|
|
732
832
|
finally {
|