@mentra/engine 0.1.0-beta.0 → 0.1.0-dev.1
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/README.md +42 -1
- package/build/engine.d.ts +11 -3
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +7 -2
- package/build/engine.js.map +1 -1
- package/build/facades/dev.d.ts.map +1 -1
- package/build/facades/dev.js +9 -5
- package/build/facades/dev.js.map +1 -1
- package/build/facades/gallery.js +1 -1
- package/build/facades/gallery.js.map +1 -1
- package/build/facades/glasses.d.ts +1 -0
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +19 -4
- package/build/facades/glasses.js.map +1 -1
- package/build/facades/glassesSettings.d.ts.map +1 -1
- package/build/facades/glassesSettings.js +1 -0
- package/build/facades/glassesSettings.js.map +1 -1
- package/build/facades/miniapps.d.ts +4 -0
- package/build/facades/miniapps.d.ts.map +1 -1
- package/build/facades/miniapps.js +5 -0
- package/build/facades/miniapps.js.map +1 -1
- package/build/facades/ota.d.ts +11 -2
- package/build/facades/ota.d.ts.map +1 -1
- package/build/facades/ota.js +19 -4
- package/build/facades/ota.js.map +1 -1
- package/build/facades/pairing.d.ts +4 -0
- package/build/facades/pairing.d.ts.map +1 -1
- package/build/facades/pairing.js +10 -8
- package/build/facades/pairing.js.map +1 -1
- package/build/facades/phoneNotifications.d.ts +0 -4
- package/build/facades/phoneNotifications.d.ts.map +1 -1
- package/build/facades/phoneNotifications.js +4 -14
- package/build/facades/phoneNotifications.js.map +1 -1
- package/build/index.d.ts +6 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -2
- package/build/index.js.map +1 -1
- package/build/internal.d.ts +2 -2
- package/build/internal.d.ts.map +1 -1
- package/build/internal.js +1 -1
- package/build/internal.js.map +1 -1
- package/build/react/MentraLiveOtaFlow.d.ts +38 -0
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
- package/build/react/MentraLiveOtaFlow.js +528 -0
- package/build/react/MentraLiveOtaFlow.js.map +1 -0
- package/build/react/index.d.ts +2 -0
- package/build/react/index.d.ts.map +1 -0
- package/build/react/index.js +2 -0
- package/build/react/index.js.map +1 -0
- package/build/react/useEngineSnapshot.d.ts +2 -0
- package/build/react/useEngineSnapshot.d.ts.map +1 -0
- package/build/react/useEngineSnapshot.js +14 -0
- package/build/react/useEngineSnapshot.js.map +1 -0
- package/build/runtime/bootstrap.d.ts +24 -1
- package/build/runtime/bootstrap.d.ts.map +1 -1
- package/build/runtime/bootstrap.js +17 -0
- package/build/runtime/bootstrap.js.map +1 -1
- package/build/runtime/scanQrSeam.d.ts +7 -0
- package/build/runtime/scanQrSeam.d.ts.map +1 -0
- package/build/runtime/scanQrSeam.js +16 -0
- package/build/runtime/scanQrSeam.js.map +1 -0
- package/build/services/AppRegistry.d.ts +18 -8
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +44 -45
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/AudioCloudUplink.d.ts +9 -3
- package/build/services/AudioCloudUplink.d.ts.map +1 -1
- package/build/services/AudioCloudUplink.js +9 -3
- package/build/services/AudioCloudUplink.js.map +1 -1
- package/build/services/AudioPlaybackService.d.ts +9 -1
- package/build/services/AudioPlaybackService.d.ts.map +1 -1
- package/build/services/AudioPlaybackService.js +77 -32
- package/build/services/AudioPlaybackService.js.map +1 -1
- package/build/services/BlobStore.d.ts +2 -1
- package/build/services/BlobStore.d.ts.map +1 -1
- package/build/services/BlobStore.js +17 -9
- package/build/services/BlobStore.js.map +1 -1
- package/build/services/CloudAudioSubscriptionSync.d.ts.map +1 -1
- package/build/services/CloudAudioSubscriptionSync.js +13 -1
- package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
- package/build/services/CloudClientService.d.ts +15 -0
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/CloudClientService.js +75 -0
- package/build/services/CloudClientService.js.map +1 -1
- package/build/services/ForegroundLocationPermission.d.ts +13 -0
- package/build/services/ForegroundLocationPermission.d.ts.map +1 -0
- package/build/services/ForegroundLocationPermission.js +12 -0
- package/build/services/ForegroundLocationPermission.js.map +1 -0
- package/build/services/GlassesSettingsSync.d.ts.map +1 -1
- package/build/services/GlassesSettingsSync.js +9 -3
- package/build/services/GlassesSettingsSync.js.map +1 -1
- package/build/services/GlassesStatusProjection.d.ts +2 -1
- package/build/services/GlassesStatusProjection.d.ts.map +1 -1
- package/build/services/GlassesStatusProjection.js +15 -8
- package/build/services/GlassesStatusProjection.js.map +1 -1
- package/build/services/HotspotOtaTransport.d.ts +29 -0
- package/build/services/HotspotOtaTransport.d.ts.map +1 -0
- package/build/services/HotspotOtaTransport.js +146 -0
- package/build/services/HotspotOtaTransport.js.map +1 -0
- package/build/services/HotspotShutdown.d.ts +17 -0
- package/build/services/HotspotShutdown.d.ts.map +1 -0
- package/build/services/HotspotShutdown.js +29 -0
- package/build/services/HotspotShutdown.js.map +1 -0
- package/build/services/LocalDisplayManager.d.ts +15 -0
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +106 -20
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +67 -1
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +486 -126
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/LocalMiniappStorage.d.ts +32 -0
- package/build/services/LocalMiniappStorage.d.ts.map +1 -0
- package/build/services/LocalMiniappStorage.js +66 -0
- package/build/services/LocalMiniappStorage.js.map +1 -0
- package/build/services/LocalMiniappUserIdentity.d.ts +20 -0
- package/build/services/LocalMiniappUserIdentity.d.ts.map +1 -0
- package/build/services/LocalMiniappUserIdentity.js +43 -0
- package/build/services/LocalMiniappUserIdentity.js.map +1 -0
- package/build/services/MicStateCoordinator.d.ts +42 -1
- package/build/services/MicStateCoordinator.d.ts.map +1 -1
- package/build/services/MicStateCoordinator.js +141 -3
- package/build/services/MicStateCoordinator.js.map +1 -1
- package/build/services/MiniappLauncher.d.ts.map +1 -1
- package/build/services/MiniappLauncher.js +23 -3
- package/build/services/MiniappLauncher.js.map +1 -1
- package/build/services/MiniappLiveness.d.ts +12 -0
- package/build/services/MiniappLiveness.d.ts.map +1 -0
- package/build/services/MiniappLiveness.js +16 -0
- package/build/services/MiniappLiveness.js.map +1 -0
- package/build/services/OtaArtifactDownloader.d.ts +65 -0
- package/build/services/OtaArtifactDownloader.d.ts.map +1 -0
- package/build/services/OtaArtifactDownloader.js +226 -0
- package/build/services/OtaArtifactDownloader.js.map +1 -0
- package/build/services/OtaAutoChain.d.ts +34 -0
- package/build/services/OtaAutoChain.d.ts.map +1 -0
- package/build/services/OtaAutoChain.js +83 -0
- package/build/services/OtaAutoChain.js.map +1 -0
- package/build/services/OtaErrorMapping.d.ts +18 -0
- package/build/services/OtaErrorMapping.d.ts.map +1 -0
- package/build/services/OtaErrorMapping.js +63 -0
- package/build/services/OtaErrorMapping.js.map +1 -0
- package/build/services/OtaInstallCoordinator.d.ts +94 -14
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
- package/build/services/OtaInstallCoordinator.js +574 -130
- package/build/services/OtaInstallCoordinator.js.map +1 -1
- package/build/services/OtaService.d.ts.map +1 -1
- package/build/services/OtaService.js +8 -1
- package/build/services/OtaService.js.map +1 -1
- package/build/services/OtaUpdateCheckService.d.ts +38 -3
- package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
- package/build/services/OtaUpdateCheckService.js +142 -22
- package/build/services/OtaUpdateCheckService.js.map +1 -1
- package/build/services/PhoneCalendarService.d.ts.map +1 -1
- package/build/services/PhoneCalendarService.js +22 -1
- package/build/services/PhoneCalendarService.js.map +1 -1
- package/build/services/PhoneCameraFovCoordinator.d.ts +2 -0
- package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
- package/build/services/PhoneCameraFovCoordinator.js +14 -0
- package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
- package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
- package/build/services/PhoneNotificationsSync.js +12 -10
- package/build/services/PhoneNotificationsSync.js.map +1 -1
- package/build/services/PhonePhotoCoordinator.d.ts +10 -3
- package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
- package/build/services/PhonePhotoCoordinator.js +21 -3
- package/build/services/PhonePhotoCoordinator.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts +8 -1
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +125 -13
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/PhoneVideoCoordinator.d.ts +2 -0
- package/build/services/PhoneVideoCoordinator.d.ts.map +1 -1
- package/build/services/PhoneVideoCoordinator.js +10 -0
- package/build/services/PhoneVideoCoordinator.js.map +1 -1
- package/build/services/SceneRenderer.d.ts.map +1 -1
- package/build/services/SceneRenderer.js +15 -0
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/services/SupportProfileSync.d.ts +22 -0
- package/build/services/SupportProfileSync.d.ts.map +1 -0
- package/build/services/SupportProfileSync.js +213 -0
- package/build/services/SupportProfileSync.js.map +1 -0
- package/build/services/TtsTextSanitizer.d.ts +5 -0
- package/build/services/TtsTextSanitizer.d.ts.map +1 -0
- package/build/services/TtsTextSanitizer.js +61 -0
- package/build/services/TtsTextSanitizer.js.map +1 -0
- package/build/services/WifiAdbRequest.d.ts +17 -0
- package/build/services/WifiAdbRequest.d.ts.map +1 -0
- package/build/services/WifiAdbRequest.js +35 -0
- package/build/services/WifiAdbRequest.js.map +1 -0
- package/build/services/asg/asgCameraApi.d.ts +3 -0
- package/build/services/asg/asgCameraApi.d.ts.map +1 -1
- package/build/services/asg/asgCameraApi.js +72 -2
- package/build/services/asg/asgCameraApi.js.map +1 -1
- package/build/services/asg/gallerySyncService.d.ts.map +1 -1
- package/build/services/asg/gallerySyncService.js +26 -14
- package/build/services/asg/gallerySyncService.js.map +1 -1
- package/build/services/asg/localNetworkTransport.d.ts +2 -2
- package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
- package/build/services/asg/localNetworkTransport.js +6 -6
- package/build/services/asg/localNetworkTransport.js.map +1 -1
- package/build/services/audioPlaybackAssets.d.ts +8 -0
- package/build/services/audioPlaybackAssets.d.ts.map +1 -0
- package/build/services/audioPlaybackAssets.js +7 -0
- package/build/services/audioPlaybackAssets.js.map +1 -0
- package/build/services/glassesClockSync.js +2 -2
- package/build/services/glassesClockSync.js.map +1 -1
- package/build/services/manifestActions.d.ts +7 -0
- package/build/services/manifestActions.d.ts.map +1 -0
- package/build/services/manifestActions.js +25 -0
- package/build/services/manifestActions.js.map +1 -0
- package/build/services/otaDisplayState.d.ts +22 -11
- package/build/services/otaDisplayState.d.ts.map +1 -1
- package/build/services/otaDisplayState.js +36 -14
- package/build/services/otaDisplayState.js.map +1 -1
- package/build/services/otaInstallPolicy.d.ts +15 -3
- package/build/services/otaInstallPolicy.d.ts.map +1 -1
- package/build/services/otaInstallPolicy.js +24 -3
- package/build/services/otaInstallPolicy.js.map +1 -1
- package/build/services/otaManifestUrl.d.ts +2 -0
- package/build/services/otaManifestUrl.d.ts.map +1 -0
- package/build/services/{asgOtaVersionUrl.js → otaManifestUrl.js} +15 -5
- package/build/services/otaManifestUrl.js.map +1 -0
- package/build/services/slimStreamStatus.d.ts +11 -0
- package/build/services/slimStreamStatus.d.ts.map +1 -1
- package/build/services/slimStreamStatus.js +14 -4
- package/build/services/slimStreamStatus.js.map +1 -1
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +11 -0
- package/build/stores/apps.js.map +1 -1
- package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
- package/build/stores/bluetoothSettingKeys.js +2 -0
- package/build/stores/bluetoothSettingKeys.js.map +1 -1
- package/build/stores/gallerySync.d.ts +2 -0
- package/build/stores/gallerySync.d.ts.map +1 -1
- package/build/stores/gallerySync.js +2 -0
- package/build/stores/gallerySync.js.map +1 -1
- package/build/stores/glasses.d.ts.map +1 -1
- package/build/stores/glasses.js +3 -0
- package/build/stores/glasses.js.map +1 -1
- package/build/stores/settings.d.ts +17 -0
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +152 -19
- package/build/stores/settings.js.map +1 -1
- package/build/types/applet.d.ts +2 -0
- package/build/types/applet.d.ts.map +1 -1
- package/build/types/applet.js.map +1 -1
- package/build/types/enums.d.ts +1 -0
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +1 -0
- package/build/types/enums.js.map +1 -1
- package/build/types/hardware.d.ts +1 -0
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js +14 -0
- package/build/types/hardware.js.map +1 -1
- package/build/utils/configuredDevHost.d.ts +6 -0
- package/build/utils/configuredDevHost.d.ts.map +1 -0
- package/build/utils/configuredDevHost.js +19 -0
- package/build/utils/configuredDevHost.js.map +1 -0
- package/build/utils/devMiniappLaunch.d.ts +19 -8
- package/build/utils/devMiniappLaunch.d.ts.map +1 -1
- package/build/utils/devMiniappLaunch.js +144 -21
- package/build/utils/devMiniappLaunch.js.map +1 -1
- package/build/utils/diagnosticContext.d.ts.map +1 -1
- package/build/utils/diagnosticContext.js +15 -17
- package/build/utils/diagnosticContext.js.map +1 -1
- package/build/utils/hardware/hardware.d.ts.map +1 -1
- package/build/utils/hardware/hardware.js +7 -3
- package/build/utils/hardware/hardware.js.map +1 -1
- package/build/utils/miniappDiagnostics.d.ts +64 -0
- package/build/utils/miniappDiagnostics.d.ts.map +1 -0
- package/build/utils/miniappDiagnostics.js +173 -0
- package/build/utils/miniappDiagnostics.js.map +1 -0
- package/build/utils/timers/timers.d.ts.map +1 -1
- package/build/utils/timers/timers.js +55 -18
- package/build/utils/timers/timers.js.map +1 -1
- package/package.json +27 -18
- package/src/assets/silence.wav +0 -0
- package/src/engine.ts +7 -2
- package/src/facades/dev.ts +9 -5
- package/src/facades/gallery.ts +1 -1
- package/src/facades/glasses.ts +17 -4
- package/src/facades/glassesSettings.ts +5 -0
- package/src/facades/miniapps.ts +7 -0
- package/src/facades/ota.ts +19 -4
- package/src/facades/pairing.ts +11 -7
- package/src/facades/phoneNotifications.ts +4 -13
- package/src/index.ts +26 -2
- package/src/internal.ts +2 -1
- package/src/react/MentraLiveOtaFlow.tsx +856 -0
- package/src/react/index.ts +7 -0
- package/src/react/useEngineSnapshot.ts +16 -0
- package/src/runtime/bootstrap.ts +33 -1
- package/src/runtime/scanQrSeam.ts +20 -0
- package/src/services/AppRegistry.ts +72 -48
- package/src/services/AudioCloudUplink.ts +9 -3
- package/src/services/AudioPlaybackService.ts +94 -34
- package/src/services/BlobStore.ts +22 -9
- package/src/services/CloudAudioSubscriptionSync.ts +13 -1
- package/src/services/CloudClientService.ts +74 -0
- package/src/services/ForegroundLocationPermission.ts +24 -0
- package/src/services/GlassesSettingsSync.ts +9 -3
- package/src/services/GlassesStatusProjection.ts +16 -8
- package/src/services/HotspotOtaTransport.ts +184 -0
- package/src/services/HotspotShutdown.ts +41 -0
- package/src/services/LocalDisplayManager.ts +112 -21
- package/src/services/LocalMiniappRuntime.ts +576 -130
- package/src/services/LocalMiniappStorage.ts +82 -0
- package/src/services/LocalMiniappUserIdentity.ts +46 -0
- package/src/services/MicStateCoordinator.ts +169 -4
- package/src/services/MiniappLauncher.ts +30 -3
- package/src/services/MiniappLiveness.ts +24 -0
- package/src/services/OtaArtifactDownloader.ts +312 -0
- package/src/services/OtaAutoChain.ts +106 -0
- package/src/services/OtaErrorMapping.ts +80 -0
- package/src/services/OtaInstallCoordinator.ts +645 -143
- package/src/services/OtaService.ts +11 -5
- package/src/services/OtaUpdateCheckService.ts +180 -20
- package/src/services/PhoneCalendarService.ts +27 -4
- package/src/services/PhoneCameraFovCoordinator.ts +15 -0
- package/src/services/PhoneNotificationsSync.ts +12 -10
- package/src/services/PhonePhotoCoordinator.ts +33 -7
- package/src/services/PhoneStreamCoordinator.ts +139 -25
- package/src/services/PhoneVideoCoordinator.ts +11 -0
- package/src/services/SceneRenderer.ts +16 -0
- package/src/services/SupportProfileSync.ts +218 -0
- package/src/services/TtsTextSanitizer.ts +66 -0
- package/src/services/WifiAdbRequest.ts +53 -0
- package/src/services/asg/asgCameraApi.ts +99 -8
- package/src/services/asg/gallerySyncService.ts +28 -19
- package/src/services/asg/localNetworkTransport.ts +7 -11
- package/src/services/audioPlaybackAssets.ts +8 -0
- package/src/services/glassesClockSync.ts +2 -2
- package/src/services/manifestActions.ts +30 -0
- package/src/services/otaDisplayState.ts +57 -16
- package/src/services/otaInstallPolicy.ts +29 -3
- package/src/services/{asgOtaVersionUrl.ts → otaManifestUrl.ts} +16 -4
- package/src/services/slimStreamStatus.ts +16 -5
- package/src/stores/apps.ts +12 -0
- package/src/stores/bluetoothSettingKeys.ts +2 -0
- package/src/stores/gallerySync.ts +4 -0
- package/src/stores/glasses.ts +3 -0
- package/src/stores/settings.ts +174 -21
- package/src/types/applet.ts +2 -0
- package/src/types/enums.ts +2 -1
- package/src/types/hardware.ts +17 -0
- package/src/utils/configuredDevHost.ts +17 -0
- package/src/utils/devMiniappLaunch.ts +151 -23
- package/src/utils/diagnosticContext.ts +17 -17
- package/src/utils/hardware/hardware.ts +7 -9
- package/src/utils/miniappDiagnostics.ts +226 -0
- package/src/utils/timers/timers.ts +57 -21
- package/build/services/asgOtaVersionUrl.d.ts +0 -2
- package/build/services/asgOtaVersionUrl.d.ts.map +0 -1
- package/build/services/asgOtaVersionUrl.js.map +0 -1
|
@@ -58,6 +58,9 @@ const DEFAULT_TIMINGS = {
|
|
|
58
58
|
ackTimeoutMs: 10_000,
|
|
59
59
|
maxMissedAcks: 3,
|
|
60
60
|
cloudflareStatusPollMs: 5_000,
|
|
61
|
+
// During WHIP startup, probe quickly so readiness is not quantized to the
|
|
62
|
+
// steady-state 5s monitoring cadence. The delay backs off on each miss.
|
|
63
|
+
cloudflareStartupPollInitialMs: 500,
|
|
61
64
|
// Cloudflare typically needs ~5-10s after first frame before HLS is live.
|
|
62
65
|
hlsReadinessInitialDelayMs: 5_000,
|
|
63
66
|
hlsReadinessPollMs: 2_000,
|
|
@@ -85,6 +88,8 @@ export interface StartUnmanagedOptions {
|
|
|
85
88
|
video?: StreamStartRequest["video"]
|
|
86
89
|
audio?: StreamStartRequest["audio"]
|
|
87
90
|
sound?: boolean
|
|
91
|
+
/** Optional Bearer token for WHIP Authorization (custom authenticated endpoints). */
|
|
92
|
+
authToken?: string
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
export interface StartManagedOptions {
|
|
@@ -99,8 +104,10 @@ export interface StartManagedOptions {
|
|
|
99
104
|
* - "whip": WebRTC ingest -> WHEP playback (<1s glass-to-screen), but NO
|
|
100
105
|
* HLS/DASH playback and NO recording (Cloudflare limitation; the returned
|
|
101
106
|
* hlsUrl will serve 204 forever).
|
|
107
|
+
* - "rtmp": RTMPS ingest over TCP 443 -> HLS playback. Survives networks
|
|
108
|
+
* that drop WHIP/SRT UDP.
|
|
102
109
|
*/
|
|
103
|
-
ingest?: "srt" | "whip"
|
|
110
|
+
ingest?: "srt" | "whip" | "rtmp"
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
export interface StreamPublisherStartResult {
|
|
@@ -152,11 +159,13 @@ interface ManagedEntry {
|
|
|
152
159
|
hlsReady: boolean
|
|
153
160
|
hlsReadyResolvers: Array<(result: ManagedStartResult) => void>
|
|
154
161
|
hlsReadyRejecters: Array<(err: Error) => void>
|
|
155
|
-
cloudflareTimer?: ReturnType<typeof
|
|
162
|
+
cloudflareTimer?: ReturnType<typeof setTimeout>
|
|
156
163
|
hlsTimer?: ReturnType<typeof setInterval>
|
|
157
164
|
hlsAttempts: number
|
|
158
|
-
/**
|
|
159
|
-
|
|
165
|
+
/** Cloudflare status probes made during this stream session. */
|
|
166
|
+
cloudflareAttempts: number
|
|
167
|
+
/** Wall-clock origin for end-to-end managed startup diagnostics. */
|
|
168
|
+
startupStartedAtMs: number
|
|
160
169
|
}
|
|
161
170
|
|
|
162
171
|
type Entry = UnmanagedEntry | ManagedEntry
|
|
@@ -250,6 +259,26 @@ export class PhoneStreamCoordinator {
|
|
|
250
259
|
return this.current !== null && this.current.streamId === streamId
|
|
251
260
|
}
|
|
252
261
|
|
|
262
|
+
/** Report-safe stream ownership snapshot for incident diagnostics. */
|
|
263
|
+
getDiagnosticSnapshot(): Record<string, unknown> {
|
|
264
|
+
if (!this.current) return {active: false}
|
|
265
|
+
return this.current.kind === "managed"
|
|
266
|
+
? {
|
|
267
|
+
active: true,
|
|
268
|
+
kind: this.current.kind,
|
|
269
|
+
streamId: this.current.streamId,
|
|
270
|
+
subscribers: [...this.current.subscribers].sort(),
|
|
271
|
+
mode: this.current.mode,
|
|
272
|
+
playbackReady: this.current.hlsReady,
|
|
273
|
+
}
|
|
274
|
+
: {
|
|
275
|
+
active: true,
|
|
276
|
+
kind: this.current.kind,
|
|
277
|
+
streamId: this.current.streamId,
|
|
278
|
+
ownerPackageName: this.current.packageName,
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
253
282
|
async startUnmanaged(
|
|
254
283
|
packageName: string,
|
|
255
284
|
opts: StartUnmanagedOptions,
|
|
@@ -289,6 +318,7 @@ export class PhoneStreamCoordinator {
|
|
|
289
318
|
// undefined, expected an Object") — only include what was provided.
|
|
290
319
|
...(opts.video !== undefined ? {video: opts.video} : {}),
|
|
291
320
|
...(opts.audio !== undefined ? {audio: opts.audio} : {}),
|
|
321
|
+
...(opts.authToken ? {authToken: opts.authToken} : {}),
|
|
292
322
|
})
|
|
293
323
|
const result = publisherStartResult(streamId, event)
|
|
294
324
|
this.startLifecycle(streamId)
|
|
@@ -304,6 +334,15 @@ export class PhoneStreamCoordinator {
|
|
|
304
334
|
packageName: string,
|
|
305
335
|
opts: StartManagedOptions,
|
|
306
336
|
): Promise<ManagedStartResult> {
|
|
337
|
+
const startupStartedAtMs = Date.now()
|
|
338
|
+
// streamId doesn't exist yet — it's minted a few lines below, once we
|
|
339
|
+
// know this is a fresh provision rather than a join onto an existing one.
|
|
340
|
+
console.info("[STREAM_STARTUP]", {
|
|
341
|
+
stage: "requested",
|
|
342
|
+
packageName,
|
|
343
|
+
ingest: opts.ingest,
|
|
344
|
+
elapsedMs: 0,
|
|
345
|
+
})
|
|
307
346
|
// Two-phase: the entry-claim runs under the transition lock; the wait for
|
|
308
347
|
// HLS readiness happens AFTER the lock releases so a long warm-up doesn't
|
|
309
348
|
// block subsequent start/stop transitions on this coordinator.
|
|
@@ -362,10 +401,18 @@ export class PhoneStreamCoordinator {
|
|
|
362
401
|
hlsReadyResolvers: [],
|
|
363
402
|
hlsReadyRejecters: [],
|
|
364
403
|
hlsAttempts: 0,
|
|
365
|
-
|
|
404
|
+
cloudflareAttempts: 0,
|
|
405
|
+
startupStartedAtMs,
|
|
366
406
|
}
|
|
367
407
|
this.current = entry
|
|
368
408
|
|
|
409
|
+
console.info("[STREAM_STARTUP]", {
|
|
410
|
+
streamId,
|
|
411
|
+
stage: "provisioned",
|
|
412
|
+
mode,
|
|
413
|
+
elapsedMs: Date.now() - startupStartedAtMs,
|
|
414
|
+
})
|
|
415
|
+
|
|
369
416
|
try {
|
|
370
417
|
const event = await BluetoothSdk.startExternallyManagedStream({
|
|
371
418
|
type: "start_stream",
|
|
@@ -377,6 +424,12 @@ export class PhoneStreamCoordinator {
|
|
|
377
424
|
...(opts.audio !== undefined ? {audio: opts.audio} : {}),
|
|
378
425
|
})
|
|
379
426
|
entry.publisherStart = publisherStartResult(streamId, event)
|
|
427
|
+
console.info("[STREAM_STARTUP]", {
|
|
428
|
+
streamId,
|
|
429
|
+
stage: "publisher_ready",
|
|
430
|
+
mode,
|
|
431
|
+
elapsedMs: Date.now() - startupStartedAtMs,
|
|
432
|
+
})
|
|
380
433
|
} catch (err) {
|
|
381
434
|
this.current = null
|
|
382
435
|
await teardownManagedStream(provision.liveInputId).catch(() => undefined)
|
|
@@ -398,6 +451,13 @@ export class PhoneStreamCoordinator {
|
|
|
398
451
|
return decision.immediate
|
|
399
452
|
}
|
|
400
453
|
|
|
454
|
+
// The first Cloudflare probe runs immediately and can complete before the
|
|
455
|
+
// transition lock releases. Avoid stranding this caller after readiness
|
|
456
|
+
// resolvers have already been drained.
|
|
457
|
+
if (decision.entry.hlsReady) {
|
|
458
|
+
return managedStartResult(decision.entry)
|
|
459
|
+
}
|
|
460
|
+
|
|
401
461
|
// Wait for playback readiness OUTSIDE the lock — readiness can take ~10s
|
|
402
462
|
// and we don't want to block other transitions for that long.
|
|
403
463
|
return new Promise<ManagedStartResult>((resolve, reject) => {
|
|
@@ -530,20 +590,41 @@ export class PhoneStreamCoordinator {
|
|
|
530
590
|
}
|
|
531
591
|
|
|
532
592
|
private startCloudflareStatusPoll(entry: ManagedEntry): void {
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
)
|
|
542
|
-
|
|
593
|
+
// Keep the existing ~60s readiness budget while decoupling it from probe
|
|
594
|
+
// count. Startup probes begin immediately and back off to the normal 5s
|
|
595
|
+
// monitoring cadence, so a just-connected publisher is noticed quickly
|
|
596
|
+
// without increasing steady-state traffic.
|
|
597
|
+
const connectTimeoutMs = Math.max(1, this.timings.hlsReadinessMaxAttempts * this.timings.hlsReadinessPollMs)
|
|
598
|
+
const pollingStartedAtMs = Date.now()
|
|
599
|
+
|
|
600
|
+
const scheduleNext = () => {
|
|
601
|
+
if (this.current !== entry) return
|
|
602
|
+
const waitingForWebRtc = entry.mode === "webrtc" && !entry.hlsReady
|
|
603
|
+
const elapsedMs = Date.now() - pollingStartedAtMs
|
|
604
|
+
const remainingMs = Math.max(0, connectTimeoutMs - elapsedMs)
|
|
605
|
+
const startupDelayMs = Math.min(
|
|
606
|
+
this.timings.cloudflareStatusPollMs,
|
|
607
|
+
this.timings.cloudflareStartupPollInitialMs * 2 ** Math.min(Math.max(0, entry.cloudflareAttempts - 1), 10),
|
|
608
|
+
)
|
|
609
|
+
const delayMs = waitingForWebRtc ? Math.min(startupDelayMs, remainingMs) : this.timings.cloudflareStatusPollMs
|
|
610
|
+
entry.cloudflareTimer = setTimeout(() => void poll(), delayMs)
|
|
611
|
+
}
|
|
612
|
+
|
|
543
613
|
const poll = async () => {
|
|
544
614
|
if (this.current !== entry) return
|
|
615
|
+
const requestStartedAtMs = Date.now()
|
|
616
|
+
let keepPolling = true
|
|
617
|
+
entry.cloudflareAttempts += 1
|
|
545
618
|
try {
|
|
546
619
|
const status: CloudflareStatus = await getManagedStreamStatus(entry.liveInputId)
|
|
620
|
+
console.debug("[STREAM_STARTUP]", {
|
|
621
|
+
streamId: entry.streamId,
|
|
622
|
+
stage: "cloudflare_probe",
|
|
623
|
+
connected: status.isConnected,
|
|
624
|
+
attempt: entry.cloudflareAttempts,
|
|
625
|
+
requestMs: Date.now() - requestStartedAtMs,
|
|
626
|
+
elapsedMs: Date.now() - entry.startupStartedAtMs,
|
|
627
|
+
})
|
|
547
628
|
this.fanout({
|
|
548
629
|
streamId: entry.streamId,
|
|
549
630
|
source: "cloudflare",
|
|
@@ -556,6 +637,13 @@ export class PhoneStreamCoordinator {
|
|
|
556
637
|
if (entry.mode === "webrtc" && !entry.hlsReady) {
|
|
557
638
|
if (status.isConnected) {
|
|
558
639
|
entry.hlsReady = true
|
|
640
|
+
console.info("[STREAM_STARTUP]", {
|
|
641
|
+
streamId: entry.streamId,
|
|
642
|
+
stage: "playback_ready",
|
|
643
|
+
mode: entry.mode,
|
|
644
|
+
probes: entry.cloudflareAttempts,
|
|
645
|
+
elapsedMs: Date.now() - entry.startupStartedAtMs,
|
|
646
|
+
})
|
|
559
647
|
const result = managedStartResult(entry)
|
|
560
648
|
for (const r of entry.hlsReadyResolvers) r(result)
|
|
561
649
|
entry.hlsReadyResolvers = []
|
|
@@ -567,11 +655,8 @@ export class PhoneStreamCoordinator {
|
|
|
567
655
|
data: result as unknown as Record<string, unknown>,
|
|
568
656
|
})
|
|
569
657
|
} else {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const err = new Error(
|
|
573
|
-
`WebRTC ingest never reached Cloudflare after ${entry.connectAttempts} status polls`,
|
|
574
|
-
)
|
|
658
|
+
if (Date.now() - pollingStartedAtMs >= connectTimeoutMs) {
|
|
659
|
+
const err = new Error(`WebRTC ingest never reached Cloudflare after ${connectTimeoutMs}ms`)
|
|
575
660
|
for (const reject of entry.hlsReadyRejecters) reject(err)
|
|
576
661
|
entry.hlsReadyResolvers = []
|
|
577
662
|
entry.hlsReadyRejecters = []
|
|
@@ -586,15 +671,32 @@ export class PhoneStreamCoordinator {
|
|
|
586
671
|
if (this.current?.streamId !== targetStreamId) return
|
|
587
672
|
await this.teardownLocked("webrtc_not_connected")
|
|
588
673
|
})
|
|
674
|
+
keepPolling = false
|
|
589
675
|
}
|
|
590
676
|
}
|
|
591
677
|
}
|
|
592
678
|
} catch (err) {
|
|
593
679
|
console.warn("[STREAM] cloudflare status poll failed:", err)
|
|
680
|
+
if (entry.mode === "webrtc" && !entry.hlsReady && Date.now() - pollingStartedAtMs >= connectTimeoutMs) {
|
|
681
|
+
const timeoutErr = new Error(`WebRTC ingest status could not be confirmed after ${connectTimeoutMs}ms`)
|
|
682
|
+
for (const reject of entry.hlsReadyRejecters) reject(timeoutErr)
|
|
683
|
+
entry.hlsReadyResolvers = []
|
|
684
|
+
entry.hlsReadyRejecters = []
|
|
685
|
+
const targetStreamId = entry.streamId
|
|
686
|
+
void this.runExclusive(async () => {
|
|
687
|
+
if (this.current?.streamId !== targetStreamId) return
|
|
688
|
+
await this.teardownLocked("webrtc_status_unavailable")
|
|
689
|
+
})
|
|
690
|
+
keepPolling = false
|
|
691
|
+
}
|
|
692
|
+
} finally {
|
|
693
|
+
if (keepPolling) scheduleNext()
|
|
594
694
|
}
|
|
595
695
|
}
|
|
596
|
-
|
|
597
|
-
//
|
|
696
|
+
|
|
697
|
+
// The glasses publisher has already reported streaming, so the first
|
|
698
|
+
// status request is useful now. Most starts avoid the old blind 5s wait.
|
|
699
|
+
void poll()
|
|
598
700
|
}
|
|
599
701
|
|
|
600
702
|
private startHlsReadinessPoll(entry: ManagedEntry): void {
|
|
@@ -610,6 +712,13 @@ export class PhoneStreamCoordinator {
|
|
|
610
712
|
const res = await fetch(entry.hlsUrl, {method: "HEAD"})
|
|
611
713
|
if (res.status === 200) {
|
|
612
714
|
entry.hlsReady = true
|
|
715
|
+
console.info("[STREAM_STARTUP]", {
|
|
716
|
+
streamId: entry.streamId,
|
|
717
|
+
stage: "playback_ready",
|
|
718
|
+
mode: entry.mode,
|
|
719
|
+
probes: entry.hlsAttempts,
|
|
720
|
+
elapsedMs: Date.now() - entry.startupStartedAtMs,
|
|
721
|
+
})
|
|
613
722
|
if (entry.hlsTimer) {
|
|
614
723
|
clearInterval(entry.hlsTimer)
|
|
615
724
|
entry.hlsTimer = undefined
|
|
@@ -697,7 +806,7 @@ export class PhoneStreamCoordinator {
|
|
|
697
806
|
this.lifecycle = null
|
|
698
807
|
|
|
699
808
|
if (entry.kind === "managed") {
|
|
700
|
-
if (entry.cloudflareTimer)
|
|
809
|
+
if (entry.cloudflareTimer) clearTimeout(entry.cloudflareTimer)
|
|
701
810
|
if (entry.hlsTimer) clearInterval(entry.hlsTimer)
|
|
702
811
|
// Reject any still-pending HLS readiness waiters.
|
|
703
812
|
const pendingErr = new Error(`Stream torn down: ${reason}`)
|
|
@@ -728,7 +837,7 @@ export class PhoneStreamCoordinator {
|
|
|
728
837
|
}
|
|
729
838
|
}
|
|
730
839
|
|
|
731
|
-
function pickIngestUrl(p: ProvisionResult, preference?: "srt" | "whip"): string {
|
|
840
|
+
function pickIngestUrl(p: ProvisionResult, preference?: "srt" | "whip" | "rtmp"): string {
|
|
732
841
|
// Glasses' StreamCommandHandler detects protocol from URL prefix.
|
|
733
842
|
//
|
|
734
843
|
// Default priority: SRT > RTMP > WHIP. SRT first: Cloudflare's WebRTC (WHIP)
|
|
@@ -739,13 +848,18 @@ function pickIngestUrl(p: ProvisionResult, preference?: "srt" | "whip"): string
|
|
|
739
848
|
//
|
|
740
849
|
// "whip" preference flips the trade: sub-second WHEP playback for
|
|
741
850
|
// live-monitor use cases, accepting no HLS and no recording.
|
|
851
|
+
// "rtmp" prefers TCP 443 ingest so networks that drop WHIP/SRT UDP still
|
|
852
|
+
// get HLS playback. Callers must request it explicitly; WHIP starts do not
|
|
853
|
+
// fall back to RTMP.
|
|
742
854
|
//
|
|
743
855
|
// Throw if none resolved so the caller's Promise rejects with a clear
|
|
744
856
|
// message rather than the glasses' "unknown protocol" error.
|
|
745
857
|
const url =
|
|
746
858
|
preference === "whip"
|
|
747
859
|
? p.webrtcPublishUrl || p.srtUrl || p.rtmpUrl
|
|
748
|
-
:
|
|
860
|
+
: preference === "rtmp"
|
|
861
|
+
? p.rtmpUrl || p.srtUrl || p.webrtcPublishUrl
|
|
862
|
+
: p.srtUrl || p.rtmpUrl || p.webrtcPublishUrl
|
|
749
863
|
if (!url) {
|
|
750
864
|
throw new Error("Cloudflare provision returned no usable ingest URL")
|
|
751
865
|
}
|
|
@@ -55,6 +55,17 @@ export class PhoneVideoCoordinator {
|
|
|
55
55
|
// second start that races the first is still rejected.
|
|
56
56
|
private starting = false
|
|
57
57
|
|
|
58
|
+
/** Report-safe recording ownership snapshot for incident diagnostics. */
|
|
59
|
+
getDiagnosticSnapshot(): Record<string, unknown> {
|
|
60
|
+
return {
|
|
61
|
+
recordingOwners: [
|
|
62
|
+
...new Set([...this.activeRecordings.values()].map((recording) => recording.packageName)),
|
|
63
|
+
].sort(),
|
|
64
|
+
activeRecordingCount: this.activeRecordings.size,
|
|
65
|
+
startInProgress: this.starting,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
async startRecording(packageName: string, opts: VideoRecordingOpts): Promise<VideoRecordingStarted> {
|
|
59
70
|
// Fail fast if glasses aren't connected — the BLE command would otherwise
|
|
60
71
|
// be sent into the void.
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import displayProcessor from "./DisplayProcessor"
|
|
15
15
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
16
|
+
import {useDisplayStore} from "../stores/display"
|
|
16
17
|
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
17
18
|
import {useGlassesStore} from "../stores/glasses"
|
|
18
19
|
import {DeviceTypes, getModelCapabilities} from "../types"
|
|
@@ -269,6 +270,21 @@ class SceneRenderer {
|
|
|
269
270
|
void Promise.resolve(BluetoothSdk.displayEvent({view: frame.view, scene: frame})).catch((err) => {
|
|
270
271
|
console.error(`${LOG_TAG}: native scene send failed:`, err)
|
|
271
272
|
})
|
|
273
|
+
|
|
274
|
+
// Keep the phone-side glasses mirror on the same frame as native. Legacy
|
|
275
|
+
// layouts update this store in LocalDisplayManager.sendToNative; scene
|
|
276
|
+
// frames bypass that method, so without an explicit write every
|
|
277
|
+
// positioning device (including G2) leaves the mirror blank/stale.
|
|
278
|
+
const caps = this.currentCapabilities()
|
|
279
|
+
useDisplayStore.getState().setDisplayEvent({
|
|
280
|
+
view: frame.view,
|
|
281
|
+
layout: {
|
|
282
|
+
layoutType: "scene",
|
|
283
|
+
width: caps?.width,
|
|
284
|
+
height: caps?.height,
|
|
285
|
+
elements: frame.elements,
|
|
286
|
+
},
|
|
287
|
+
})
|
|
272
288
|
} catch (err) {
|
|
273
289
|
console.error(`${LOG_TAG}: native scene send failed:`, err)
|
|
274
290
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import Constants from "expo-constants"
|
|
2
|
+
import * as Device from "expo-device"
|
|
3
|
+
import {Platform} from "react-native"
|
|
4
|
+
import type {SupportStateInput} from "@mentra/cloud-client"
|
|
5
|
+
|
|
6
|
+
import {useGlassesStore} from "../stores/glasses"
|
|
7
|
+
import {BgTimer} from "../utils/timers"
|
|
8
|
+
import {cloudClientService} from "./CloudClientService"
|
|
9
|
+
|
|
10
|
+
const ENGINE_VERSION = (require("../../package.json") as {version?: string}).version
|
|
11
|
+
const BLUETOOTH_SDK_VERSION = (require("@mentra/bluetooth-sdk/package.json") as {version?: string}).version
|
|
12
|
+
const DEBOUNCE_MS = 2_000
|
|
13
|
+
const RETRY_MS = 30_000
|
|
14
|
+
const HEARTBEAT_MS = 6 * 60 * 60_000
|
|
15
|
+
|
|
16
|
+
let unsubscribe: (() => void) | null = null
|
|
17
|
+
let debounceTimer: number | null = null
|
|
18
|
+
let retryTimer: number | null = null
|
|
19
|
+
let heartbeatTimer: number | null = null
|
|
20
|
+
let lastSentFingerprint: string | null = null
|
|
21
|
+
let queuedFingerprint: string | null = null
|
|
22
|
+
let sendingGeneration: number | null = null
|
|
23
|
+
let syncGeneration = 0
|
|
24
|
+
let consecutiveSendFailures = 0
|
|
25
|
+
let lastFailure: {code: string; stage: string} | null = null
|
|
26
|
+
let lastConnectionState: string | null = null
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Keep Cloud V2's canonical support snapshot current without creating an event
|
|
30
|
+
* stream from noisy store updates. Only the explicitly selected fields below
|
|
31
|
+
* leave the phone; Wi-Fi, Bluetooth addresses, location and diagnostics do not.
|
|
32
|
+
*/
|
|
33
|
+
export function startSupportProfileSync(): void {
|
|
34
|
+
if (unsubscribe) return
|
|
35
|
+
const generation = ++syncGeneration
|
|
36
|
+
lastConnectionState = useGlassesStore.getState().connection.state
|
|
37
|
+
unsubscribe = useGlassesStore.subscribe(() => handleGlassesStoreChange(generation))
|
|
38
|
+
heartbeatTimer = BgTimer.setInterval(() => void sendCurrentSnapshot(true, generation), HEARTBEAT_MS)
|
|
39
|
+
void sendCurrentSnapshot(true, generation)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function stopSupportProfileSync(): void {
|
|
43
|
+
syncGeneration += 1
|
|
44
|
+
unsubscribe?.()
|
|
45
|
+
unsubscribe = null
|
|
46
|
+
if (debounceTimer !== null) BgTimer.clearTimeout(debounceTimer)
|
|
47
|
+
if (retryTimer !== null) BgTimer.clearTimeout(retryTimer)
|
|
48
|
+
if (heartbeatTimer !== null) BgTimer.clearInterval(heartbeatTimer)
|
|
49
|
+
debounceTimer = null
|
|
50
|
+
retryTimer = null
|
|
51
|
+
heartbeatTimer = null
|
|
52
|
+
lastSentFingerprint = null
|
|
53
|
+
queuedFingerprint = null
|
|
54
|
+
sendingGeneration = null
|
|
55
|
+
consecutiveSendFailures = 0
|
|
56
|
+
lastFailure = null
|
|
57
|
+
lastConnectionState = null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleGlassesStoreChange(generation: number): void {
|
|
61
|
+
if (!unsubscribe || generation !== syncGeneration) return
|
|
62
|
+
const connectionState = useGlassesStore.getState().connection.state
|
|
63
|
+
if (connectionState === "connected" && lastConnectionState !== "connected") lastFailure = null
|
|
64
|
+
lastConnectionState = connectionState
|
|
65
|
+
scheduleMeaningfulUpdate(generation)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function scheduleMeaningfulUpdate(generation = syncGeneration): void {
|
|
69
|
+
if (!unsubscribe || generation !== syncGeneration) return
|
|
70
|
+
const fingerprint = snapshotFingerprint(buildSnapshot())
|
|
71
|
+
if (fingerprint === lastSentFingerprint) return
|
|
72
|
+
queuedFingerprint = fingerprint
|
|
73
|
+
armDebounce(generation)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function armDebounce(generation: number): void {
|
|
77
|
+
if (debounceTimer !== null) BgTimer.clearTimeout(debounceTimer)
|
|
78
|
+
debounceTimer = BgTimer.setTimeout(() => {
|
|
79
|
+
debounceTimer = null
|
|
80
|
+
void sendCurrentSnapshot(false, generation)
|
|
81
|
+
}, DEBOUNCE_MS)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function sendCurrentSnapshot(force: boolean, generation = syncGeneration): Promise<void> {
|
|
85
|
+
if (!unsubscribe || generation !== syncGeneration) return
|
|
86
|
+
if (sendingGeneration === generation) {
|
|
87
|
+
const fingerprint = snapshotFingerprint(buildSnapshot())
|
|
88
|
+
if (fingerprint !== lastSentFingerprint) queuedFingerprint = fingerprint
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
const snapshot = buildSnapshot()
|
|
92
|
+
const fingerprint = snapshotFingerprint(snapshot)
|
|
93
|
+
if (!force && fingerprint === lastSentFingerprint) return
|
|
94
|
+
|
|
95
|
+
sendingGeneration = generation
|
|
96
|
+
let retryDelayMs: number | null = null
|
|
97
|
+
try {
|
|
98
|
+
const result = await cloudClientService.core.supportProfile.update(snapshot)
|
|
99
|
+
if (generation !== syncGeneration || !unsubscribe) return
|
|
100
|
+
consecutiveSendFailures = 0
|
|
101
|
+
retryDelayMs = retryDelayForSupportProfileResult(result)
|
|
102
|
+
if (retryDelayMs === null) {
|
|
103
|
+
lastSentFingerprint = fingerprint
|
|
104
|
+
if (queuedFingerprint === fingerprint) queuedFingerprint = null
|
|
105
|
+
if (retryTimer !== null) BgTimer.clearTimeout(retryTimer)
|
|
106
|
+
retryTimer = null
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (generation !== syncGeneration || !unsubscribe) return
|
|
110
|
+
consecutiveSendFailures += 1
|
|
111
|
+
retryDelayMs = retryDelayForSendFailure(consecutiveSendFailures)
|
|
112
|
+
console.warn(
|
|
113
|
+
`supportProfile: Cloud V2 update failed (retry in ${Math.round(retryDelayMs / 1_000)}s):`,
|
|
114
|
+
error instanceof Error ? error.message : error,
|
|
115
|
+
)
|
|
116
|
+
} finally {
|
|
117
|
+
const generationIsActive = generation === syncGeneration && sendingGeneration === generation && Boolean(unsubscribe)
|
|
118
|
+
if (generationIsActive) {
|
|
119
|
+
sendingGeneration = null
|
|
120
|
+
if (retryDelayMs !== null) {
|
|
121
|
+
if (retryTimer !== null) BgTimer.clearTimeout(retryTimer)
|
|
122
|
+
retryTimer = BgTimer.setTimeout(() => {
|
|
123
|
+
retryTimer = null
|
|
124
|
+
void sendCurrentSnapshot(true, generation)
|
|
125
|
+
}, retryDelayMs)
|
|
126
|
+
} else {
|
|
127
|
+
const currentFingerprint = snapshotFingerprint(buildSnapshot())
|
|
128
|
+
if (currentFingerprint !== lastSentFingerprint) {
|
|
129
|
+
queuedFingerprint = currentFingerprint
|
|
130
|
+
if (retryTimer === null) armDebounce(generation)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function retryDelayForSupportProfileResult(result: {
|
|
138
|
+
status: "accepted" | "deduplicated" | "stale"
|
|
139
|
+
}): number | null {
|
|
140
|
+
if (result.status === "accepted" || result.status === "deduplicated") return null
|
|
141
|
+
return RETRY_MS
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Support freshness is not worth an aggressive retry loop. A snapshot the
|
|
146
|
+
* server keeps rejecting (skewed clock, deleted account, signed-out host)
|
|
147
|
+
* backs off exponentially until it converges on the heartbeat cadence.
|
|
148
|
+
*/
|
|
149
|
+
export function retryDelayForSendFailure(consecutiveFailures: number): number {
|
|
150
|
+
return Math.min(HEARTBEAT_MS, RETRY_MS * 2 ** Math.min(Math.max(consecutiveFailures, 1) - 1, 10))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function buildSnapshot(observedAt = new Date()): SupportStateInput {
|
|
154
|
+
const glasses = useGlassesStore.getState()
|
|
155
|
+
const device = {
|
|
156
|
+
hardwareId: optional(glasses.serialNumber),
|
|
157
|
+
model: optional(glasses.deviceModel),
|
|
158
|
+
androidVersion: optional(glasses.androidVersion),
|
|
159
|
+
firmwareVersion: optional(glasses.firmwareVersion),
|
|
160
|
+
mtkFirmwareVersion: optional(glasses.mtkFirmwareVersion),
|
|
161
|
+
besFirmwareVersion: optional(glasses.besFirmwareVersion),
|
|
162
|
+
appVersion: optional(glasses.appVersion),
|
|
163
|
+
buildNumber: optional(glasses.buildNumber),
|
|
164
|
+
}
|
|
165
|
+
const hasDevice = Object.values(device).some(Boolean)
|
|
166
|
+
const constants = Constants as typeof Constants & {
|
|
167
|
+
nativeAppVersion?: string | null
|
|
168
|
+
nativeBuildVersion?: string | null
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
observedAt: observedAt.toISOString(),
|
|
172
|
+
host: {
|
|
173
|
+
appVersion: optional(constants.nativeAppVersion ?? Constants.expoConfig?.version),
|
|
174
|
+
appBuild: optional(constants.nativeBuildVersion),
|
|
175
|
+
engineVersion: optional(ENGINE_VERSION),
|
|
176
|
+
bluetoothSdkVersion: optional(BLUETOOTH_SDK_VERSION),
|
|
177
|
+
phonePlatform: Platform.OS === "ios" || Platform.OS === "android" ? Platform.OS : "unknown",
|
|
178
|
+
phoneModel: optional(Device.modelName),
|
|
179
|
+
phoneOsVersion: optional(
|
|
180
|
+
Platform.OS === "android"
|
|
181
|
+
? String((Platform.constants as {Release?: string} | undefined)?.Release ?? Platform.Version)
|
|
182
|
+
: String(Platform.Version),
|
|
183
|
+
),
|
|
184
|
+
connectionState: glasses.connection.state,
|
|
185
|
+
...(lastFailure ? {failureCode: lastFailure.code, failureStage: lastFailure.stage} : {}),
|
|
186
|
+
},
|
|
187
|
+
...(hasDevice ? {device} : {}),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Record only normalized connection failures; arbitrary exception messages never leave the phone. */
|
|
192
|
+
export function recordSupportProfileConnectionFailure(error: unknown, stage: string): void {
|
|
193
|
+
const rawCode =
|
|
194
|
+
typeof error === "object" && error && "code" in error ? String((error as {code?: unknown}).code ?? "") : ""
|
|
195
|
+
const message = error instanceof Error ? `${error.name} ${error.message}`.toLowerCase() : ""
|
|
196
|
+
const code = /^[a-zA-Z0-9_.-]{1,64}$/.test(rawCode)
|
|
197
|
+
? rawCode.toLowerCase()
|
|
198
|
+
: message.includes("permission")
|
|
199
|
+
? "permission_denied"
|
|
200
|
+
: message.includes("bluetooth") && (message.includes("off") || message.includes("unavailable"))
|
|
201
|
+
? "bluetooth_unavailable"
|
|
202
|
+
: message.includes("not found") || message.includes("no default")
|
|
203
|
+
? "device_not_found"
|
|
204
|
+
: message.includes("timeout")
|
|
205
|
+
? "connect_timeout"
|
|
206
|
+
: "connect_failed"
|
|
207
|
+
lastFailure = {code, stage: stage.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 64)}
|
|
208
|
+
scheduleMeaningfulUpdate()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function snapshotFingerprint(snapshot: SupportStateInput): string {
|
|
212
|
+
return JSON.stringify({host: snapshot.host, device: snapshot.device})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function optional(value: string | null | undefined): string | undefined {
|
|
216
|
+
const trimmed = value?.trim()
|
|
217
|
+
return trimmed ? trimmed.slice(0, 128) : undefined
|
|
218
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const HTML_ENTITIES: Record<string, string> = {
|
|
2
|
+
"&": "&",
|
|
3
|
+
"<": "<",
|
|
4
|
+
">": ">",
|
|
5
|
+
""": '"',
|
|
6
|
+
"'": "'",
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Convert display-oriented text into plain words before TTS synthesis. */
|
|
10
|
+
export function sanitizeTtsText(text: string): string {
|
|
11
|
+
let speech = text.replace(/&(amp|lt|gt|quot|#39);/gi, (entity) => HTML_ENTITIES[entity.toLowerCase()] ?? entity)
|
|
12
|
+
|
|
13
|
+
speech = speech
|
|
14
|
+
.replace(/<say-as\b[^>]*>(.*?)<\/say-as>/gis, "$1")
|
|
15
|
+
.replace(/<\/?[a-z][a-z0-9-]*(?:\s[^<>]*?)?\s*\/?>/gi, " ")
|
|
16
|
+
.replace(/\[([^\]]+)]\([^)]+\)/g, "$1")
|
|
17
|
+
.replace(/`([^`]*)`/g, "$1")
|
|
18
|
+
.replace(/(-?\d+(?:[.,]\d+)?)\s*(?:°\s*f\b|℉)/gi, "$1 degrees Fahrenheit")
|
|
19
|
+
.replace(/(-?\d+(?:[.,]\d+)?)\s*(?:°\s*c\b|℃)/gi, "$1 degrees Celsius")
|
|
20
|
+
.replace(/(-?\d+(?:[.,]\d+)?)\s*°/g, "$1 degrees")
|
|
21
|
+
.replace(/<=/g, " less than or equal to ")
|
|
22
|
+
.replace(/>=/g, " greater than or equal to ")
|
|
23
|
+
.replace(/</g, " less than ")
|
|
24
|
+
.replace(/>/g, " greater than ")
|
|
25
|
+
.replace(/[[\]{}()]/g, " ")
|
|
26
|
+
.replace(/(degrees Fahrenheit)\s+(-?\d+(?:[.,]\d+)? degrees Celsius)\b/gi, "$1 or $2")
|
|
27
|
+
.replace(/(degrees Celsius)\s+(-?\d+(?:[.,]\d+)? degrees Fahrenheit)\b/gi, "$1 or $2")
|
|
28
|
+
.replace(/&/g, " and ")
|
|
29
|
+
.replace(/\+/g, " plus ")
|
|
30
|
+
.replace(/=/g, " equals ")
|
|
31
|
+
.replace(/#/g, " hash ")
|
|
32
|
+
.replace(/@/g, " at ")
|
|
33
|
+
.replace(/%/g, " percent ")
|
|
34
|
+
.replace(/\\/g, " backslash ")
|
|
35
|
+
.replace(/\//g, (slash, offset, source) => {
|
|
36
|
+
const numericSeparator = /\d/.test(source[offset - 1] ?? "") && /\d/.test(source[offset + 1] ?? "")
|
|
37
|
+
return numericSeparator ? slash : " slash "
|
|
38
|
+
})
|
|
39
|
+
.replace(/\*/g, " star ")
|
|
40
|
+
|
|
41
|
+
return speech
|
|
42
|
+
.replace(/\s+([,.;:!?])/g, "$1")
|
|
43
|
+
.replace(/\s+/g, " ")
|
|
44
|
+
.trim()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Prepare an already-trimmed sentence list while preserving the exact-text opt-out. */
|
|
48
|
+
export function prepareTtsSentences(sentences: string[], enableSanitization = true): string[] {
|
|
49
|
+
if (!enableSanitization) return sentences
|
|
50
|
+
|
|
51
|
+
const sanitized = sentences.map((sentence) => sanitizeTtsText(sentence)).filter(Boolean)
|
|
52
|
+
|
|
53
|
+
return sanitized.map((sentence, index) => {
|
|
54
|
+
const previous = sanitized[index - 1]
|
|
55
|
+
if (!previous) return sentence
|
|
56
|
+
|
|
57
|
+
const followsFahrenheit = /degrees Fahrenheit[,.;:!?]?$/i.test(previous)
|
|
58
|
+
const followsCelsius = /degrees Celsius[,.;:!?]?$/i.test(previous)
|
|
59
|
+
const startsWithFahrenheit = /^-?\d+(?:[.,]\d+)? degrees Fahrenheit\b/i.test(sentence)
|
|
60
|
+
const startsWithCelsius = /^-?\d+(?:[.,]\d+)? degrees Celsius\b/i.test(sentence)
|
|
61
|
+
|
|
62
|
+
return (followsFahrenheit && startsWithCelsius) || (followsCelsius && startsWithFahrenheit)
|
|
63
|
+
? `or ${sentence}`
|
|
64
|
+
: sentence
|
|
65
|
+
})
|
|
66
|
+
}
|