@mentra/engine 0.1.0-beta.0
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 +79 -0
- package/build/devtools.d.ts +11 -0
- package/build/devtools.d.ts.map +1 -0
- package/build/devtools.js +13 -0
- package/build/devtools.js.map +1 -0
- package/build/engine.d.ts +261 -0
- package/build/engine.d.ts.map +1 -0
- package/build/engine.js +170 -0
- package/build/engine.js.map +1 -0
- package/build/facades/dev.d.ts +58 -0
- package/build/facades/dev.d.ts.map +1 -0
- package/build/facades/dev.js +189 -0
- package/build/facades/dev.js.map +1 -0
- package/build/facades/display.d.ts +21 -0
- package/build/facades/display.d.ts.map +1 -0
- package/build/facades/display.js +17 -0
- package/build/facades/display.js.map +1 -0
- package/build/facades/displayMirror.d.ts +15 -0
- package/build/facades/displayMirror.d.ts.map +1 -0
- package/build/facades/displayMirror.js +29 -0
- package/build/facades/displayMirror.js.map +1 -0
- package/build/facades/gallery.d.ts +62 -0
- package/build/facades/gallery.d.ts.map +1 -0
- package/build/facades/gallery.js +61 -0
- package/build/facades/gallery.js.map +1 -0
- package/build/facades/glasses.d.ts +141 -0
- package/build/facades/glasses.d.ts.map +1 -0
- package/build/facades/glasses.js +199 -0
- package/build/facades/glasses.js.map +1 -0
- package/build/facades/glassesSettings.d.ts +13 -0
- package/build/facades/glassesSettings.d.ts.map +1 -0
- package/build/facades/glassesSettings.js +54 -0
- package/build/facades/glassesSettings.js.map +1 -0
- package/build/facades/glassesWifi.d.ts +25 -0
- package/build/facades/glassesWifi.d.ts.map +1 -0
- package/build/facades/glassesWifi.js +57 -0
- package/build/facades/glassesWifi.js.map +1 -0
- package/build/facades/miniapps.d.ts +43 -0
- package/build/facades/miniapps.d.ts.map +1 -0
- package/build/facades/miniapps.js +49 -0
- package/build/facades/miniapps.js.map +1 -0
- package/build/facades/notifications.d.ts +13 -0
- package/build/facades/notifications.d.ts.map +1 -0
- package/build/facades/notifications.js +12 -0
- package/build/facades/notifications.js.map +1 -0
- package/build/facades/ota.d.ts +103 -0
- package/build/facades/ota.d.ts.map +1 -0
- package/build/facades/ota.js +156 -0
- package/build/facades/ota.js.map +1 -0
- package/build/facades/pairing.d.ts +114 -0
- package/build/facades/pairing.d.ts.map +1 -0
- package/build/facades/pairing.js +275 -0
- package/build/facades/pairing.js.map +1 -0
- package/build/facades/permissions.d.ts +33 -0
- package/build/facades/permissions.d.ts.map +1 -0
- package/build/facades/permissions.js +155 -0
- package/build/facades/permissions.js.map +1 -0
- package/build/facades/phoneNotifications.d.ts +21 -0
- package/build/facades/phoneNotifications.d.ts.map +1 -0
- package/build/facades/phoneNotifications.js +51 -0
- package/build/facades/phoneNotifications.js.map +1 -0
- package/build/facades/reports.d.ts +36 -0
- package/build/facades/reports.d.ts.map +1 -0
- package/build/facades/reports.js +134 -0
- package/build/facades/reports.js.map +1 -0
- package/build/facades/session.d.ts +21 -0
- package/build/facades/session.d.ts.map +1 -0
- package/build/facades/session.js +52 -0
- package/build/facades/session.js.map +1 -0
- package/build/facades/settings.d.ts +37 -0
- package/build/facades/settings.d.ts.map +1 -0
- package/build/facades/settings.js +55 -0
- package/build/facades/settings.js.map +1 -0
- package/build/facades/speech.d.ts +30 -0
- package/build/facades/speech.d.ts.map +1 -0
- package/build/facades/speech.js +37 -0
- package/build/facades/speech.js.map +1 -0
- package/build/index.d.ts +43 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +45 -0
- package/build/index.js.map +1 -0
- package/build/internal.d.ts +67 -0
- package/build/internal.d.ts.map +1 -0
- package/build/internal.js +90 -0
- package/build/internal.js.map +1 -0
- package/build/runtime/bootstrap.d.ts +83 -0
- package/build/runtime/bootstrap.d.ts.map +1 -0
- package/build/runtime/bootstrap.js +56 -0
- package/build/runtime/bootstrap.js.map +1 -0
- package/build/runtime/config.d.ts +146 -0
- package/build/runtime/config.d.ts.map +1 -0
- package/build/runtime/config.js +18 -0
- package/build/runtime/config.js.map +1 -0
- package/build/runtime/streamConfig.d.ts +4 -0
- package/build/runtime/streamConfig.d.ts.map +1 -0
- package/build/runtime/streamConfig.js +38 -0
- package/build/runtime/streamConfig.js.map +1 -0
- package/build/services/AppRegistry.d.ts +269 -0
- package/build/services/AppRegistry.d.ts.map +1 -0
- package/build/services/AppRegistry.js +1079 -0
- package/build/services/AppRegistry.js.map +1 -0
- package/build/services/AudioCloudUplink.d.ts +9 -0
- package/build/services/AudioCloudUplink.d.ts.map +1 -0
- package/build/services/AudioCloudUplink.js +48 -0
- package/build/services/AudioCloudUplink.js.map +1 -0
- package/build/services/AudioPlaybackService.d.ts +175 -0
- package/build/services/AudioPlaybackService.d.ts.map +1 -0
- package/build/services/AudioPlaybackService.js +705 -0
- package/build/services/AudioPlaybackService.js.map +1 -0
- package/build/services/AutomaticReportResult.d.ts +20 -0
- package/build/services/AutomaticReportResult.d.ts.map +1 -0
- package/build/services/AutomaticReportResult.js +29 -0
- package/build/services/AutomaticReportResult.js.map +1 -0
- package/build/services/BlobStore.d.ts +91 -0
- package/build/services/BlobStore.d.ts.map +1 -0
- package/build/services/BlobStore.js +668 -0
- package/build/services/BlobStore.js.map +1 -0
- package/build/services/CaptionsTesterReportService.d.ts +4 -0
- package/build/services/CaptionsTesterReportService.d.ts.map +1 -0
- package/build/services/CaptionsTesterReportService.js +80 -0
- package/build/services/CaptionsTesterReportService.js.map +1 -0
- package/build/services/CloudAudioSubscriptionSync.d.ts +24 -0
- package/build/services/CloudAudioSubscriptionSync.d.ts.map +1 -0
- package/build/services/CloudAudioSubscriptionSync.js +39 -0
- package/build/services/CloudAudioSubscriptionSync.js.map +1 -0
- package/build/services/CloudClientService.d.ts +143 -0
- package/build/services/CloudClientService.d.ts.map +1 -0
- package/build/services/CloudClientService.js +644 -0
- package/build/services/CloudClientService.js.map +1 -0
- package/build/services/CloudTranscriptE2EMetrics.d.ts +4 -0
- package/build/services/CloudTranscriptE2EMetrics.d.ts.map +1 -0
- package/build/services/CloudTranscriptE2EMetrics.js +24 -0
- package/build/services/CloudTranscriptE2EMetrics.js.map +1 -0
- package/build/services/ConnectionCoordinator.d.ts +51 -0
- package/build/services/ConnectionCoordinator.d.ts.map +1 -0
- package/build/services/ConnectionCoordinator.js +34 -0
- package/build/services/ConnectionCoordinator.js.map +1 -0
- package/build/services/DevServerBridge.d.ts +63 -0
- package/build/services/DevServerBridge.d.ts.map +1 -0
- package/build/services/DevServerBridge.js +294 -0
- package/build/services/DevServerBridge.js.map +1 -0
- package/build/services/DeviceEventRouter.d.ts +3 -0
- package/build/services/DeviceEventRouter.d.ts.map +1 -0
- package/build/services/DeviceEventRouter.js +190 -0
- package/build/services/DeviceEventRouter.js.map +1 -0
- package/build/services/DeviceStoreHydration.d.ts +38 -0
- package/build/services/DeviceStoreHydration.d.ts.map +1 -0
- package/build/services/DeviceStoreHydration.js +124 -0
- package/build/services/DeviceStoreHydration.js.map +1 -0
- package/build/services/DisplayProcessor.d.ts +223 -0
- package/build/services/DisplayProcessor.d.ts.map +1 -0
- package/build/services/DisplayProcessor.js +552 -0
- package/build/services/DisplayProcessor.js.map +1 -0
- package/build/services/GlassesReadiness.d.ts +41 -0
- package/build/services/GlassesReadiness.d.ts.map +1 -0
- package/build/services/GlassesReadiness.js +82 -0
- package/build/services/GlassesReadiness.js.map +1 -0
- package/build/services/GlassesSettingsSync.d.ts +33 -0
- package/build/services/GlassesSettingsSync.d.ts.map +1 -0
- package/build/services/GlassesSettingsSync.js +121 -0
- package/build/services/GlassesSettingsSync.js.map +1 -0
- package/build/services/GlassesStatusProjection.d.ts +3 -0
- package/build/services/GlassesStatusProjection.d.ts.map +1 -0
- package/build/services/GlassesStatusProjection.js +65 -0
- package/build/services/GlassesStatusProjection.js.map +1 -0
- package/build/services/HeadingService.d.ts +27 -0
- package/build/services/HeadingService.d.ts.map +1 -0
- package/build/services/HeadingService.js +96 -0
- package/build/services/HeadingService.js.map +1 -0
- package/build/services/LocalDisplayManager.d.ts +139 -0
- package/build/services/LocalDisplayManager.d.ts.map +1 -0
- package/build/services/LocalDisplayManager.js +579 -0
- package/build/services/LocalDisplayManager.js.map +1 -0
- package/build/services/LocalMiniappRuntime.d.ts +439 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -0
- package/build/services/LocalMiniappRuntime.js +3695 -0
- package/build/services/LocalMiniappRuntime.js.map +1 -0
- package/build/services/LocalSttFallbackCoordinator.d.ts +57 -0
- package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -0
- package/build/services/LocalSttFallbackCoordinator.js +137 -0
- package/build/services/LocalSttFallbackCoordinator.js.map +1 -0
- package/build/services/MentraJSCrashController.d.ts +97 -0
- package/build/services/MentraJSCrashController.d.ts.map +1 -0
- package/build/services/MentraJSCrashController.js +138 -0
- package/build/services/MentraJSCrashController.js.map +1 -0
- package/build/services/MentraJSCrashloopReportService.d.ts +9 -0
- package/build/services/MentraJSCrashloopReportService.d.ts.map +1 -0
- package/build/services/MentraJSCrashloopReportService.js +54 -0
- package/build/services/MentraJSCrashloopReportService.js.map +1 -0
- package/build/services/MentraJSLogPipeline.d.ts +87 -0
- package/build/services/MentraJSLogPipeline.d.ts.map +1 -0
- package/build/services/MentraJSLogPipeline.js +137 -0
- package/build/services/MentraJSLogPipeline.js.map +1 -0
- package/build/services/MentraJSRouter.d.ts +203 -0
- package/build/services/MentraJSRouter.d.ts.map +1 -0
- package/build/services/MentraJSRouter.js +480 -0
- package/build/services/MentraJSRouter.js.map +1 -0
- package/build/services/MentraUIRouter.d.ts +137 -0
- package/build/services/MentraUIRouter.d.ts.map +1 -0
- package/build/services/MentraUIRouter.js +210 -0
- package/build/services/MentraUIRouter.js.map +1 -0
- package/build/services/MicStateCoordinator.d.ts +37 -0
- package/build/services/MicStateCoordinator.d.ts.map +1 -0
- package/build/services/MicStateCoordinator.js +81 -0
- package/build/services/MicStateCoordinator.js.map +1 -0
- package/build/services/MiniappEngine.d.ts +31 -0
- package/build/services/MiniappEngine.d.ts.map +1 -0
- package/build/services/MiniappEngine.js +119 -0
- package/build/services/MiniappEngine.js.map +1 -0
- package/build/services/MiniappLauncher.d.ts +129 -0
- package/build/services/MiniappLauncher.d.ts.map +1 -0
- package/build/services/MiniappLauncher.js +291 -0
- package/build/services/MiniappLauncher.js.map +1 -0
- package/build/services/MiniappRunningRegistry.d.ts +24 -0
- package/build/services/MiniappRunningRegistry.d.ts.map +1 -0
- package/build/services/MiniappRunningRegistry.js +52 -0
- package/build/services/MiniappRunningRegistry.js.map +1 -0
- package/build/services/NativeHttpResponse.d.ts +3 -0
- package/build/services/NativeHttpResponse.d.ts.map +1 -0
- package/build/services/NativeHttpResponse.js +6 -0
- package/build/services/NativeHttpResponse.js.map +1 -0
- package/build/services/NavigationHandlers.d.ts +90 -0
- package/build/services/NavigationHandlers.d.ts.map +1 -0
- package/build/services/NavigationHandlers.js +379 -0
- package/build/services/NavigationHandlers.js.map +1 -0
- package/build/services/NavigationService.d.ts +254 -0
- package/build/services/NavigationService.d.ts.map +1 -0
- package/build/services/NavigationService.js +553 -0
- package/build/services/NavigationService.js.map +1 -0
- package/build/services/NotificationsEmitter.d.ts +28 -0
- package/build/services/NotificationsEmitter.d.ts.map +1 -0
- package/build/services/NotificationsEmitter.js +22 -0
- package/build/services/NotificationsEmitter.js.map +1 -0
- package/build/services/OfflineSpeechModelService.d.ts +33 -0
- package/build/services/OfflineSpeechModelService.d.ts.map +1 -0
- package/build/services/OfflineSpeechModelService.js +136 -0
- package/build/services/OfflineSpeechModelService.js.map +1 -0
- package/build/services/OtaInstallCoordinator.d.ts +195 -0
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -0
- package/build/services/OtaInstallCoordinator.js +1047 -0
- package/build/services/OtaInstallCoordinator.js.map +1 -0
- package/build/services/OtaService.d.ts +3 -0
- package/build/services/OtaService.d.ts.map +1 -0
- package/build/services/OtaService.js +65 -0
- package/build/services/OtaService.js.map +1 -0
- package/build/services/OtaUpdateCheckService.d.ts +65 -0
- package/build/services/OtaUpdateCheckService.d.ts.map +1 -0
- package/build/services/OtaUpdateCheckService.js +260 -0
- package/build/services/OtaUpdateCheckService.js.map +1 -0
- package/build/services/PairingIdentity.d.ts +80 -0
- package/build/services/PairingIdentity.d.ts.map +1 -0
- package/build/services/PairingIdentity.js +145 -0
- package/build/services/PairingIdentity.js.map +1 -0
- package/build/services/PhoneCalendarHelpers.d.ts +32 -0
- package/build/services/PhoneCalendarHelpers.d.ts.map +1 -0
- package/build/services/PhoneCalendarHelpers.js +157 -0
- package/build/services/PhoneCalendarHelpers.js.map +1 -0
- package/build/services/PhoneCalendarService.d.ts +5 -0
- package/build/services/PhoneCalendarService.d.ts.map +1 -0
- package/build/services/PhoneCalendarService.js +24 -0
- package/build/services/PhoneCalendarService.js.map +1 -0
- package/build/services/PhoneCameraFovCoordinator.d.ts +28 -0
- package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -0
- package/build/services/PhoneCameraFovCoordinator.js +180 -0
- package/build/services/PhoneCameraFovCoordinator.js.map +1 -0
- package/build/services/PhoneLocationService.d.ts +36 -0
- package/build/services/PhoneLocationService.d.ts.map +1 -0
- package/build/services/PhoneLocationService.js +115 -0
- package/build/services/PhoneLocationService.js.map +1 -0
- package/build/services/PhoneNotificationsSync.d.ts +3 -0
- package/build/services/PhoneNotificationsSync.d.ts.map +1 -0
- package/build/services/PhoneNotificationsSync.js +35 -0
- package/build/services/PhoneNotificationsSync.js.map +1 -0
- package/build/services/PhonePhotoCoordinator.d.ts +103 -0
- package/build/services/PhonePhotoCoordinator.d.ts.map +1 -0
- package/build/services/PhonePhotoCoordinator.js +326 -0
- package/build/services/PhonePhotoCoordinator.js.map +1 -0
- package/build/services/PhoneStreamCoordinator.d.ts +168 -0
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -0
- package/build/services/PhoneStreamCoordinator.js +632 -0
- package/build/services/PhoneStreamCoordinator.js.map +1 -0
- package/build/services/PhoneVideoCoordinator.d.ts +50 -0
- package/build/services/PhoneVideoCoordinator.d.ts.map +1 -0
- package/build/services/PhoneVideoCoordinator.js +147 -0
- package/build/services/PhoneVideoCoordinator.js.map +1 -0
- package/build/services/STTModelManager.d.ts +65 -0
- package/build/services/STTModelManager.d.ts.map +1 -0
- package/build/services/STTModelManager.js +292 -0
- package/build/services/STTModelManager.js.map +1 -0
- package/build/services/SceneRenderer.d.ts +90 -0
- package/build/services/SceneRenderer.d.ts.map +1 -0
- package/build/services/SceneRenderer.js +248 -0
- package/build/services/SceneRenderer.js.map +1 -0
- package/build/services/SentenceTtsPipeline.d.ts +31 -0
- package/build/services/SentenceTtsPipeline.d.ts.map +1 -0
- package/build/services/SentenceTtsPipeline.js +67 -0
- package/build/services/SentenceTtsPipeline.js.map +1 -0
- package/build/services/StreamLifecycleController.d.ts +67 -0
- package/build/services/StreamLifecycleController.d.ts.map +1 -0
- package/build/services/StreamLifecycleController.js +152 -0
- package/build/services/StreamLifecycleController.js.map +1 -0
- package/build/services/TTSModelManager.d.ts +90 -0
- package/build/services/TTSModelManager.d.ts.map +1 -0
- package/build/services/TTSModelManager.js +366 -0
- package/build/services/TTSModelManager.js.map +1 -0
- package/build/services/TranscriptionRouting.d.ts +12 -0
- package/build/services/TranscriptionRouting.d.ts.map +1 -0
- package/build/services/TranscriptionRouting.js +24 -0
- package/build/services/TranscriptionRouting.js.map +1 -0
- package/build/services/WebviewBridge.d.ts +22 -0
- package/build/services/WebviewBridge.d.ts.map +1 -0
- package/build/services/WebviewBridge.js +47 -0
- package/build/services/WebviewBridge.js.map +1 -0
- package/build/services/asg/GalleryMediaIntegrityReportService.d.ts +17 -0
- package/build/services/asg/GalleryMediaIntegrityReportService.d.ts.map +1 -0
- package/build/services/asg/GalleryMediaIntegrityReportService.js +43 -0
- package/build/services/asg/GalleryMediaIntegrityReportService.js.map +1 -0
- package/build/services/asg/asgCameraApi.d.ts +205 -0
- package/build/services/asg/asgCameraApi.d.ts.map +1 -0
- package/build/services/asg/asgCameraApi.js +1410 -0
- package/build/services/asg/asgCameraApi.js.map +1 -0
- package/build/services/asg/cameraRollExportCoordinator.d.ts +60 -0
- package/build/services/asg/cameraRollExportCoordinator.d.ts.map +1 -0
- package/build/services/asg/cameraRollExportCoordinator.js +618 -0
- package/build/services/asg/cameraRollExportCoordinator.js.map +1 -0
- package/build/services/asg/cameraRollExportLedger.d.ts +44 -0
- package/build/services/asg/cameraRollExportLedger.d.ts.map +1 -0
- package/build/services/asg/cameraRollExportLedger.js +78 -0
- package/build/services/asg/cameraRollExportLedger.js.map +1 -0
- package/build/services/asg/galleryMediaValidation.d.ts +29 -0
- package/build/services/asg/galleryMediaValidation.d.ts.map +1 -0
- package/build/services/asg/galleryMediaValidation.js +149 -0
- package/build/services/asg/galleryMediaValidation.js.map +1 -0
- package/build/services/asg/galleryNotices.d.ts +33 -0
- package/build/services/asg/galleryNotices.d.ts.map +1 -0
- package/build/services/asg/galleryNotices.js +29 -0
- package/build/services/asg/galleryNotices.js.map +1 -0
- package/build/services/asg/galleryPathMigration.d.ts +3 -0
- package/build/services/asg/galleryPathMigration.d.ts.map +1 -0
- package/build/services/asg/galleryPathMigration.js +26 -0
- package/build/services/asg/galleryPathMigration.js.map +1 -0
- package/build/services/asg/gallerySettingsService.d.ts +20 -0
- package/build/services/asg/gallerySettingsService.d.ts.map +1 -0
- package/build/services/asg/gallerySettingsService.js +53 -0
- package/build/services/asg/gallerySettingsService.js.map +1 -0
- package/build/services/asg/gallerySyncNotifications.d.ts +56 -0
- package/build/services/asg/gallerySyncNotifications.d.ts.map +1 -0
- package/build/services/asg/gallerySyncNotifications.js +198 -0
- package/build/services/asg/gallerySyncNotifications.js.map +1 -0
- package/build/services/asg/gallerySyncService.d.ts +137 -0
- package/build/services/asg/gallerySyncService.d.ts.map +1 -0
- package/build/services/asg/gallerySyncService.js +1933 -0
- package/build/services/asg/gallerySyncService.js.map +1 -0
- package/build/services/asg/galleryTransferLedger.d.ts +58 -0
- package/build/services/asg/galleryTransferLedger.d.ts.map +1 -0
- package/build/services/asg/galleryTransferLedger.js +225 -0
- package/build/services/asg/galleryTransferLedger.js.map +1 -0
- package/build/services/asg/localNetworkTransport.d.ts +15 -0
- package/build/services/asg/localNetworkTransport.d.ts.map +1 -0
- package/build/services/asg/localNetworkTransport.js +144 -0
- package/build/services/asg/localNetworkTransport.js.map +1 -0
- package/build/services/asg/localStorageService.d.ts +146 -0
- package/build/services/asg/localStorageService.d.ts.map +1 -0
- package/build/services/asg/localStorageService.js +572 -0
- package/build/services/asg/localStorageService.js.map +1 -0
- package/build/services/asg/mediaProcessingQueue.d.ts +70 -0
- package/build/services/asg/mediaProcessingQueue.d.ts.map +1 -0
- package/build/services/asg/mediaProcessingQueue.js +451 -0
- package/build/services/asg/mediaProcessingQueue.js.map +1 -0
- package/build/services/asgOtaVersionUrl.d.ts +2 -0
- package/build/services/asgOtaVersionUrl.d.ts.map +1 -0
- package/build/services/asgOtaVersionUrl.js +36 -0
- package/build/services/asgOtaVersionUrl.js.map +1 -0
- package/build/services/blobPaths.d.ts +31 -0
- package/build/services/blobPaths.d.ts.map +1 -0
- package/build/services/blobPaths.js +86 -0
- package/build/services/blobPaths.js.map +1 -0
- package/build/services/cloudStreamApi.d.ts +59 -0
- package/build/services/cloudStreamApi.d.ts.map +1 -0
- package/build/services/cloudStreamApi.js +81 -0
- package/build/services/cloudStreamApi.js.map +1 -0
- package/build/services/gallerySyncClock.d.ts +17 -0
- package/build/services/gallerySyncClock.d.ts.map +1 -0
- package/build/services/gallerySyncClock.js +21 -0
- package/build/services/gallerySyncClock.js.map +1 -0
- package/build/services/glassesClockSync.d.ts +20 -0
- package/build/services/glassesClockSync.d.ts.map +1 -0
- package/build/services/glassesClockSync.js +97 -0
- package/build/services/glassesClockSync.js.map +1 -0
- package/build/services/manifestVersionGate.d.ts +28 -0
- package/build/services/manifestVersionGate.d.ts.map +1 -0
- package/build/services/manifestVersionGate.js +41 -0
- package/build/services/manifestVersionGate.js.map +1 -0
- package/build/services/mentraUiShim.d.ts +38 -0
- package/build/services/mentraUiShim.d.ts.map +1 -0
- package/build/services/mentraUiShim.js +360 -0
- package/build/services/mentraUiShim.js.map +1 -0
- package/build/services/navigation/roadNameResolver.d.ts +67 -0
- package/build/services/navigation/roadNameResolver.d.ts.map +1 -0
- package/build/services/navigation/roadNameResolver.js +96 -0
- package/build/services/navigation/roadNameResolver.js.map +1 -0
- package/build/services/navigation/routesApiCodec.d.ts +38 -0
- package/build/services/navigation/routesApiCodec.d.ts.map +1 -0
- package/build/services/navigation/routesApiCodec.js +68 -0
- package/build/services/navigation/routesApiCodec.js.map +1 -0
- package/build/services/otaDisplayState.d.ts +42 -0
- package/build/services/otaDisplayState.d.ts.map +1 -0
- package/build/services/otaDisplayState.js +92 -0
- package/build/services/otaDisplayState.js.map +1 -0
- package/build/services/otaInstallPolicy.d.ts +81 -0
- package/build/services/otaInstallPolicy.d.ts.map +1 -0
- package/build/services/otaInstallPolicy.js +96 -0
- package/build/services/otaInstallPolicy.js.map +1 -0
- package/build/services/otaLegacyMapping.d.ts +22 -0
- package/build/services/otaLegacyMapping.d.ts.map +1 -0
- package/build/services/otaLegacyMapping.js +56 -0
- package/build/services/otaLegacyMapping.js.map +1 -0
- package/build/services/slimStreamStatus.d.ts +7 -0
- package/build/services/slimStreamStatus.d.ts.map +1 -0
- package/build/services/slimStreamStatus.js +26 -0
- package/build/services/slimStreamStatus.js.map +1 -0
- package/build/stores/apps.d.ts +104 -0
- package/build/stores/apps.d.ts.map +1 -0
- package/build/stores/apps.js +492 -0
- package/build/stores/apps.js.map +1 -0
- package/build/stores/bluetoothSettingKeys.d.ts +4 -0
- package/build/stores/bluetoothSettingKeys.d.ts.map +1 -0
- package/build/stores/bluetoothSettingKeys.js +31 -0
- package/build/stores/bluetoothSettingKeys.js.map +1 -0
- package/build/stores/cloudClientStatus.d.ts +9 -0
- package/build/stores/cloudClientStatus.d.ts.map +1 -0
- package/build/stores/cloudClientStatus.js +24 -0
- package/build/stores/cloudClientStatus.js.map +1 -0
- package/build/stores/connection.d.ts +22 -0
- package/build/stores/connection.d.ts.map +1 -0
- package/build/stores/connection.js +43 -0
- package/build/stores/connection.js.map +1 -0
- package/build/stores/core.d.ts +15 -0
- package/build/stores/core.d.ts.map +1 -0
- package/build/stores/core.js +21 -0
- package/build/stores/core.js.map +1 -0
- package/build/stores/display.d.ts +24 -0
- package/build/stores/display.d.ts.map +1 -0
- package/build/stores/display.js +96 -0
- package/build/stores/display.js.map +1 -0
- package/build/stores/gallerySync.d.ts +101 -0
- package/build/stores/gallerySync.d.ts.map +1 -0
- package/build/stores/gallerySync.js +217 -0
- package/build/stores/gallerySync.js.map +1 -0
- package/build/stores/glasses.d.ts +62 -0
- package/build/stores/glasses.d.ts.map +1 -0
- package/build/stores/glasses.js +203 -0
- package/build/stores/glasses.js.map +1 -0
- package/build/stores/settings.d.ts +37 -0
- package/build/stores/settings.d.ts.map +1 -0
- package/build/stores/settings.js +922 -0
- package/build/stores/settings.js.map +1 -0
- package/build/types/applet.d.ts +96 -0
- package/build/types/applet.d.ts.map +1 -0
- package/build/types/applet.js +5 -0
- package/build/types/applet.js.map +1 -0
- package/build/types/asg.d.ts +53 -0
- package/build/types/asg.d.ts.map +1 -0
- package/build/types/asg.js +2 -0
- package/build/types/asg.js.map +1 -0
- package/build/types/capabilities/even-realities-g1.d.ts +12 -0
- package/build/types/capabilities/even-realities-g1.d.ts.map +1 -0
- package/build/types/capabilities/even-realities-g1.js +55 -0
- package/build/types/capabilities/even-realities-g1.js.map +1 -0
- package/build/types/capabilities/even-realities-g2.d.ts +12 -0
- package/build/types/capabilities/even-realities-g2.d.ts.map +1 -0
- package/build/types/capabilities/even-realities-g2.js +75 -0
- package/build/types/capabilities/even-realities-g2.js.map +1 -0
- package/build/types/capabilities/mentra-display.d.ts +12 -0
- package/build/types/capabilities/mentra-display.d.ts.map +1 -0
- package/build/types/capabilities/mentra-display.js +63 -0
- package/build/types/capabilities/mentra-display.js.map +1 -0
- package/build/types/capabilities/mentra-live.d.ts +12 -0
- package/build/types/capabilities/mentra-live.d.ts.map +1 -0
- package/build/types/capabilities/mentra-live.js +92 -0
- package/build/types/capabilities/mentra-live.js.map +1 -0
- package/build/types/capabilities/none.d.ts +13 -0
- package/build/types/capabilities/none.d.ts.map +1 -0
- package/build/types/capabilities/none.js +65 -0
- package/build/types/capabilities/none.js.map +1 -0
- package/build/types/capabilities/simulated-glasses.d.ts +13 -0
- package/build/types/capabilities/simulated-glasses.d.ts.map +1 -0
- package/build/types/capabilities/simulated-glasses.js +65 -0
- package/build/types/capabilities/simulated-glasses.js.map +1 -0
- package/build/types/capabilities/vuzix-z100.d.ts +12 -0
- package/build/types/capabilities/vuzix-z100.d.ts.map +1 -0
- package/build/types/capabilities/vuzix-z100.js +52 -0
- package/build/types/capabilities/vuzix-z100.js.map +1 -0
- package/build/types/enums.d.ts +40 -0
- package/build/types/enums.d.ts.map +1 -0
- package/build/types/enums.js +44 -0
- package/build/types/enums.js.map +1 -0
- package/build/types/hardware.d.ts +160 -0
- package/build/types/hardware.d.ts.map +1 -0
- package/build/types/hardware.js +36 -0
- package/build/types/hardware.js.map +1 -0
- package/build/types/index.d.ts +11 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/index.js +10 -0
- package/build/types/index.js.map +1 -0
- package/build/utils/GlobalEventEmitter.d.ts +10 -0
- package/build/utils/GlobalEventEmitter.d.ts.map +1 -0
- package/build/utils/GlobalEventEmitter.js +13 -0
- package/build/utils/GlobalEventEmitter.js.map +1 -0
- package/build/utils/cloudClient/RnUdpAdapter.d.ts +3 -0
- package/build/utils/cloudClient/RnUdpAdapter.d.ts.map +1 -0
- package/build/utils/cloudClient/RnUdpAdapter.js +41 -0
- package/build/utils/cloudClient/RnUdpAdapter.js.map +1 -0
- package/build/utils/cloudClient/cloudSecureStore.d.ts +3 -0
- package/build/utils/cloudClient/cloudSecureStore.d.ts.map +1 -0
- package/build/utils/cloudClient/cloudSecureStore.js +26 -0
- package/build/utils/cloudClient/cloudSecureStore.js.map +1 -0
- package/build/utils/debouncedPatch.d.ts +6 -0
- package/build/utils/debouncedPatch.d.ts.map +1 -0
- package/build/utils/debouncedPatch.js +25 -0
- package/build/utils/debouncedPatch.js.map +1 -0
- package/build/utils/devLogging.d.ts +26 -0
- package/build/utils/devLogging.d.ts.map +1 -0
- package/build/utils/devLogging.js +116 -0
- package/build/utils/devLogging.js.map +1 -0
- package/build/utils/devMiniappLaunch.d.ts +44 -0
- package/build/utils/devMiniappLaunch.d.ts.map +1 -0
- package/build/utils/devMiniappLaunch.js +52 -0
- package/build/utils/devMiniappLaunch.js.map +1 -0
- package/build/utils/diagnosticContext.d.ts +3 -0
- package/build/utils/diagnosticContext.d.ts.map +1 -0
- package/build/utils/diagnosticContext.js +119 -0
- package/build/utils/diagnosticContext.js.map +1 -0
- package/build/utils/display/composer/ColumnComposer.d.ts +131 -0
- package/build/utils/display/composer/ColumnComposer.d.ts.map +1 -0
- package/build/utils/display/composer/ColumnComposer.js +204 -0
- package/build/utils/display/composer/ColumnComposer.js.map +1 -0
- package/build/utils/display/composer/index.d.ts +9 -0
- package/build/utils/display/composer/index.d.ts.map +1 -0
- package/build/utils/display/composer/index.js +9 -0
- package/build/utils/display/composer/index.js.map +1 -0
- package/build/utils/display/helpers/DisplayHelpers.d.ts +165 -0
- package/build/utils/display/helpers/DisplayHelpers.d.ts.map +1 -0
- package/build/utils/display/helpers/DisplayHelpers.js +312 -0
- package/build/utils/display/helpers/DisplayHelpers.js.map +1 -0
- package/build/utils/display/helpers/ScrollView.d.ts +183 -0
- package/build/utils/display/helpers/ScrollView.d.ts.map +1 -0
- package/build/utils/display/helpers/ScrollView.js +257 -0
- package/build/utils/display/helpers/ScrollView.js.map +1 -0
- package/build/utils/display/helpers/index.d.ts +11 -0
- package/build/utils/display/helpers/index.d.ts.map +1 -0
- package/build/utils/display/helpers/index.js +10 -0
- package/build/utils/display/helpers/index.js.map +1 -0
- package/build/utils/display/index.d.ts +174 -0
- package/build/utils/display/index.d.ts.map +1 -0
- package/build/utils/display/index.js +185 -0
- package/build/utils/display/index.js.map +1 -0
- package/build/utils/display/measurer/TextMeasurer.d.ts +160 -0
- package/build/utils/display/measurer/TextMeasurer.d.ts.map +1 -0
- package/build/utils/display/measurer/TextMeasurer.js +273 -0
- package/build/utils/display/measurer/TextMeasurer.js.map +1 -0
- package/build/utils/display/measurer/index.d.ts +10 -0
- package/build/utils/display/measurer/index.d.ts.map +1 -0
- package/build/utils/display/measurer/index.js +11 -0
- package/build/utils/display/measurer/index.js.map +1 -0
- package/build/utils/display/measurer/script-detection.d.ts +53 -0
- package/build/utils/display/measurer/script-detection.d.ts.map +1 -0
- package/build/utils/display/measurer/script-detection.js +184 -0
- package/build/utils/display/measurer/script-detection.js.map +1 -0
- package/build/utils/display/profiles/g1.d.ts +33 -0
- package/build/utils/display/profiles/g1.d.ts.map +1 -0
- package/build/utils/display/profiles/g1.js +213 -0
- package/build/utils/display/profiles/g1.js.map +1 -0
- package/build/utils/display/profiles/g2.d.ts +13 -0
- package/build/utils/display/profiles/g2.d.ts.map +1 -0
- package/build/utils/display/profiles/g2.js +34 -0
- package/build/utils/display/profiles/g2.js.map +1 -0
- package/build/utils/display/profiles/index.d.ts +12 -0
- package/build/utils/display/profiles/index.d.ts.map +1 -0
- package/build/utils/display/profiles/index.js +15 -0
- package/build/utils/display/profiles/index.js.map +1 -0
- package/build/utils/display/profiles/nex.d.ts +10 -0
- package/build/utils/display/profiles/nex.d.ts.map +1 -0
- package/build/utils/display/profiles/nex.js +150 -0
- package/build/utils/display/profiles/nex.js.map +1 -0
- package/build/utils/display/profiles/types.d.ts +103 -0
- package/build/utils/display/profiles/types.d.ts.map +1 -0
- package/build/utils/display/profiles/types.js +2 -0
- package/build/utils/display/profiles/types.js.map +1 -0
- package/build/utils/display/profiles/z100.d.ts +33 -0
- package/build/utils/display/profiles/z100.d.ts.map +1 -0
- package/build/utils/display/profiles/z100.js +182 -0
- package/build/utils/display/profiles/z100.js.map +1 -0
- package/build/utils/display/scene/degrade.d.ts +25 -0
- package/build/utils/display/scene/degrade.d.ts.map +1 -0
- package/build/utils/display/scene/degrade.js +64 -0
- package/build/utils/display/scene/degrade.js.map +1 -0
- package/build/utils/display/scene/differ.d.ts +38 -0
- package/build/utils/display/scene/differ.d.ts.map +1 -0
- package/build/utils/display/scene/differ.js +107 -0
- package/build/utils/display/scene/differ.js.map +1 -0
- package/build/utils/display/scene/index.d.ts +10 -0
- package/build/utils/display/scene/index.d.ts.map +1 -0
- package/build/utils/display/scene/index.js +6 -0
- package/build/utils/display/scene/index.js.map +1 -0
- package/build/utils/display/scene/process.d.ts +35 -0
- package/build/utils/display/scene/process.d.ts.map +1 -0
- package/build/utils/display/scene/process.js +187 -0
- package/build/utils/display/scene/process.js.map +1 -0
- package/build/utils/display/scene/store.d.ts +48 -0
- package/build/utils/display/scene/store.d.ts.map +1 -0
- package/build/utils/display/scene/store.js +0 -0
- package/build/utils/display/scene/store.js.map +1 -0
- package/build/utils/display/scene/types.d.ts +120 -0
- package/build/utils/display/scene/types.d.ts.map +1 -0
- package/build/utils/display/scene/types.js +0 -0
- package/build/utils/display/scene/types.js.map +1 -0
- package/build/utils/display/wrapper/TextWrapper.d.ts +102 -0
- package/build/utils/display/wrapper/TextWrapper.d.ts.map +1 -0
- package/build/utils/display/wrapper/TextWrapper.js +471 -0
- package/build/utils/display/wrapper/TextWrapper.js.map +1 -0
- package/build/utils/display/wrapper/index.d.ts +12 -0
- package/build/utils/display/wrapper/index.d.ts.map +1 -0
- package/build/utils/display/wrapper/index.js +12 -0
- package/build/utils/display/wrapper/index.js.map +1 -0
- package/build/utils/display/wrapper/types.d.ts +75 -0
- package/build/utils/display/wrapper/types.d.ts.map +1 -0
- package/build/utils/display/wrapper/types.js +11 -0
- package/build/utils/display/wrapper/types.js.map +1 -0
- package/build/utils/e2eMetrics.d.ts +6 -0
- package/build/utils/e2eMetrics.d.ts.map +1 -0
- package/build/utils/e2eMetrics.js +27 -0
- package/build/utils/e2eMetrics.js.map +1 -0
- package/build/utils/hardware/hardware.d.ts +42 -0
- package/build/utils/hardware/hardware.d.ts.map +1 -0
- package/build/utils/hardware/hardware.js +127 -0
- package/build/utils/hardware/hardware.js.map +1 -0
- package/build/utils/hardware/index.d.ts +2 -0
- package/build/utils/hardware/index.d.ts.map +1 -0
- package/build/utils/hardware/index.js +2 -0
- package/build/utils/hardware/index.js.map +1 -0
- package/build/utils/miniappGlobals.d.ts +60 -0
- package/build/utils/miniappGlobals.d.ts.map +1 -0
- package/build/utils/miniappGlobals.js +224 -0
- package/build/utils/miniappGlobals.js.map +1 -0
- package/build/utils/permissions/MediaLibraryPermissions.d.ts +45 -0
- package/build/utils/permissions/MediaLibraryPermissions.d.ts.map +1 -0
- package/build/utils/permissions/MediaLibraryPermissions.js +145 -0
- package/build/utils/permissions/MediaLibraryPermissions.js.map +1 -0
- package/build/utils/permissions/galleryDisplayName.d.ts +8 -0
- package/build/utils/permissions/galleryDisplayName.d.ts.map +1 -0
- package/build/utils/permissions/galleryDisplayName.js +25 -0
- package/build/utils/permissions/galleryDisplayName.js.map +1 -0
- package/build/utils/storage/index.d.ts +3 -0
- package/build/utils/storage/index.d.ts.map +1 -0
- package/build/utils/storage/index.js +3 -0
- package/build/utils/storage/index.js.map +1 -0
- package/build/utils/storage/storage.d.ts +24 -0
- package/build/utils/storage/storage.d.ts.map +1 -0
- package/build/utils/storage/storage.js +85 -0
- package/build/utils/storage/storage.js.map +1 -0
- package/build/utils/storage/zip.d.ts +3 -0
- package/build/utils/storage/zip.d.ts.map +1 -0
- package/build/utils/storage/zip.js +14 -0
- package/build/utils/storage/zip.js.map +1 -0
- package/build/utils/timers/index.d.ts +3 -0
- package/build/utils/timers/index.d.ts.map +1 -0
- package/build/utils/timers/index.js +3 -0
- package/build/utils/timers/index.js.map +1 -0
- package/build/utils/timers/timers.d.ts +9 -0
- package/build/utils/timers/timers.d.ts.map +1 -0
- package/build/utils/timers/timers.js +105 -0
- package/build/utils/timers/timers.js.map +1 -0
- package/package.json +120 -0
- package/src/devtools.ts +13 -0
- package/src/engine.ts +173 -0
- package/src/facades/dev.ts +198 -0
- package/src/facades/display.ts +17 -0
- package/src/facades/displayMirror.ts +33 -0
- package/src/facades/gallery.ts +69 -0
- package/src/facades/glasses.ts +206 -0
- package/src/facades/glassesSettings.ts +56 -0
- package/src/facades/glassesWifi.ts +68 -0
- package/src/facades/miniapps.ts +55 -0
- package/src/facades/notifications.ts +15 -0
- package/src/facades/ota.ts +174 -0
- package/src/facades/pairing.ts +329 -0
- package/src/facades/permissions.ts +165 -0
- package/src/facades/phoneNotifications.ts +50 -0
- package/src/facades/reports.ts +185 -0
- package/src/facades/session.ts +55 -0
- package/src/facades/settings.ts +57 -0
- package/src/facades/speech.ts +38 -0
- package/src/index.ts +191 -0
- package/src/internal.ts +159 -0
- package/src/runtime/bootstrap.ts +118 -0
- package/src/runtime/config.ts +147 -0
- package/src/runtime/streamConfig.ts +34 -0
- package/src/services/AppRegistry.ts +1190 -0
- package/src/services/AudioCloudUplink.ts +50 -0
- package/src/services/AudioPlaybackService.ts +813 -0
- package/src/services/AutomaticReportResult.ts +48 -0
- package/src/services/BlobStore.ts +746 -0
- package/src/services/CaptionsTesterReportService.ts +107 -0
- package/src/services/CloudAudioSubscriptionSync.ts +41 -0
- package/src/services/CloudClientService.ts +689 -0
- package/src/services/CloudTranscriptE2EMetrics.ts +27 -0
- package/src/services/ConnectionCoordinator.ts +65 -0
- package/src/services/DevServerBridge.ts +332 -0
- package/src/services/DeviceEventRouter.ts +238 -0
- package/src/services/DeviceStoreHydration.ts +130 -0
- package/src/services/DisplayProcessor.ts +775 -0
- package/src/services/GlassesReadiness.ts +103 -0
- package/src/services/GlassesSettingsSync.ts +134 -0
- package/src/services/GlassesStatusProjection.ts +73 -0
- package/src/services/HeadingService.ts +98 -0
- package/src/services/LocalDisplayManager.ts +703 -0
- package/src/services/LocalMiniappRuntime.ts +4213 -0
- package/src/services/LocalSttFallbackCoordinator.ts +147 -0
- package/src/services/MentraJSCrashController.ts +189 -0
- package/src/services/MentraJSCrashloopReportService.ts +66 -0
- package/src/services/MentraJSLogPipeline.ts +166 -0
- package/src/services/MentraJSRouter.ts +557 -0
- package/src/services/MentraUIRouter.ts +244 -0
- package/src/services/MicStateCoordinator.ts +94 -0
- package/src/services/MiniappEngine.ts +143 -0
- package/src/services/MiniappLauncher.ts +340 -0
- package/src/services/MiniappRunningRegistry.ts +57 -0
- package/src/services/NativeHttpResponse.ts +6 -0
- package/src/services/NavigationHandlers.ts +423 -0
- package/src/services/NavigationService.ts +731 -0
- package/src/services/NotificationsEmitter.ts +48 -0
- package/src/services/OfflineSpeechModelService.ts +168 -0
- package/src/services/OtaInstallCoordinator.ts +1205 -0
- package/src/services/OtaService.ts +83 -0
- package/src/services/OtaUpdateCheckService.ts +386 -0
- package/src/services/PairingIdentity.ts +154 -0
- package/src/services/PhoneCalendarHelpers.ts +212 -0
- package/src/services/PhoneCalendarService.ts +42 -0
- package/src/services/PhoneCameraFovCoordinator.ts +195 -0
- package/src/services/PhoneLocationService.ts +122 -0
- package/src/services/PhoneNotificationsSync.ts +44 -0
- package/src/services/PhonePhotoCoordinator.ts +409 -0
- package/src/services/PhoneStreamCoordinator.ts +777 -0
- package/src/services/PhoneVideoCoordinator.ts +189 -0
- package/src/services/STTModelManager.ts +360 -0
- package/src/services/SceneRenderer.ts +292 -0
- package/src/services/SentenceTtsPipeline.ts +106 -0
- package/src/services/StreamLifecycleController.ts +213 -0
- package/src/services/TTSModelManager.ts +466 -0
- package/src/services/TranscriptionRouting.ts +37 -0
- package/src/services/WebviewBridge.ts +50 -0
- package/src/services/asg/GalleryMediaIntegrityReportService.ts +70 -0
- package/src/services/asg/asgCameraApi.ts +1695 -0
- package/src/services/asg/cameraRollExportCoordinator.ts +627 -0
- package/src/services/asg/cameraRollExportLedger.ts +122 -0
- package/src/services/asg/galleryMediaValidation.ts +183 -0
- package/src/services/asg/galleryNotices.ts +61 -0
- package/src/services/asg/galleryPathMigration.ts +22 -0
- package/src/services/asg/gallerySettingsService.ts +68 -0
- package/src/services/asg/gallerySyncNotifications.ts +236 -0
- package/src/services/asg/gallerySyncService.ts +2269 -0
- package/src/services/asg/galleryTransferLedger.ts +312 -0
- package/src/services/asg/localNetworkTransport.ts +173 -0
- package/src/services/asg/localStorageService.ts +652 -0
- package/src/services/asg/mediaProcessingQueue.ts +518 -0
- package/src/services/asgOtaVersionUrl.ts +41 -0
- package/src/services/blobPaths.ts +86 -0
- package/src/services/cloudStreamApi.ts +154 -0
- package/src/services/gallerySyncClock.ts +39 -0
- package/src/services/glassesClockSync.ts +115 -0
- package/src/services/manifestVersionGate.ts +43 -0
- package/src/services/mentraUiShim.ts +366 -0
- package/src/services/navigation/roadNameResolver.ts +112 -0
- package/src/services/navigation/routesApiCodec.ts +69 -0
- package/src/services/otaDisplayState.ts +115 -0
- package/src/services/otaInstallPolicy.ts +106 -0
- package/src/services/otaLegacyMapping.ts +71 -0
- package/src/services/slimStreamStatus.ts +28 -0
- package/src/stores/apps.ts +614 -0
- package/src/stores/bluetoothSettingKeys.ts +36 -0
- package/src/stores/cloudClientStatus.ts +37 -0
- package/src/stores/connection.ts +69 -0
- package/src/stores/core.ts +32 -0
- package/src/stores/display.ts +121 -0
- package/src/stores/gallerySync.ts +380 -0
- package/src/stores/glasses.ts +309 -0
- package/src/stores/settings.ts +1001 -0
- package/src/types/applet.ts +112 -0
- package/src/types/asg.ts +58 -0
- package/src/types/capabilities/even-realities-g1.ts +67 -0
- package/src/types/capabilities/even-realities-g2.ts +88 -0
- package/src/types/capabilities/mentra-display.ts +75 -0
- package/src/types/capabilities/mentra-live.ts +103 -0
- package/src/types/capabilities/none.ts +76 -0
- package/src/types/capabilities/simulated-glasses.ts +76 -0
- package/src/types/capabilities/vuzix-z100.ts +64 -0
- package/src/types/enums.ts +43 -0
- package/src/types/hardware.ts +203 -0
- package/src/types/index.ts +37 -0
- package/src/utils/GlobalEventEmitter.ts +14 -0
- package/src/utils/cloudClient/RnUdpAdapter.ts +47 -0
- package/src/utils/cloudClient/cloudSecureStore.ts +28 -0
- package/src/utils/debouncedPatch.ts +28 -0
- package/src/utils/devLogging.ts +140 -0
- package/src/utils/devMiniappLaunch.ts +68 -0
- package/src/utils/diagnosticContext.ts +155 -0
- package/src/utils/display/composer/ColumnComposer.ts +285 -0
- package/src/utils/display/composer/index.ts +15 -0
- package/src/utils/display/helpers/DisplayHelpers.ts +413 -0
- package/src/utils/display/helpers/ScrollView.ts +337 -0
- package/src/utils/display/helpers/index.ts +14 -0
- package/src/utils/display/index.ts +273 -0
- package/src/utils/display/measurer/TextMeasurer.ts +346 -0
- package/src/utils/display/measurer/index.ts +21 -0
- package/src/utils/display/measurer/script-detection.ts +218 -0
- package/src/utils/display/profiles/g1.ts +236 -0
- package/src/utils/display/profiles/g2.ts +39 -0
- package/src/utils/display/profiles/index.ts +28 -0
- package/src/utils/display/profiles/nex.ts +166 -0
- package/src/utils/display/profiles/types.ts +132 -0
- package/src/utils/display/profiles/z100.ts +198 -0
- package/src/utils/display/scene/degrade.ts +80 -0
- package/src/utils/display/scene/differ.ts +128 -0
- package/src/utils/display/scene/index.ts +21 -0
- package/src/utils/display/scene/process.ts +214 -0
- package/src/utils/display/scene/store.ts +0 -0
- package/src/utils/display/scene/types.ts +0 -0
- package/src/utils/display/wrapper/TextWrapper.ts +546 -0
- package/src/utils/display/wrapper/index.ts +15 -0
- package/src/utils/display/wrapper/types.ts +102 -0
- package/src/utils/e2eMetrics.ts +30 -0
- package/src/utils/hardware/hardware.ts +166 -0
- package/src/utils/hardware/index.ts +1 -0
- package/src/utils/miniappGlobals.ts +260 -0
- package/src/utils/permissions/MediaLibraryPermissions.ts +161 -0
- package/src/utils/permissions/galleryDisplayName.ts +28 -0
- package/src/utils/storage/index.ts +2 -0
- package/src/utils/storage/storage.ts +101 -0
- package/src/utils/storage/zip.ts +13 -0
- package/src/utils/timers/index.ts +2 -0
- package/src/utils/timers/timers.ts +139 -0
|
@@ -0,0 +1,1695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASG Camera Server API Client
|
|
3
|
+
* Provides methods to interact with the AsgCameraServer Java APIs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as RNFS from "@dr.pogodin/react-native-fs"
|
|
7
|
+
|
|
8
|
+
import {PhotoInfo, CaptureGroup, GalleryResponse, ServerStatus, HealthResponse} from "../../types/asg"
|
|
9
|
+
import {BgTimer} from "../../utils/timers"
|
|
10
|
+
|
|
11
|
+
import {localStorageService} from "./localStorageService"
|
|
12
|
+
import {validateDownloadedMediaFile} from "./galleryMediaValidation"
|
|
13
|
+
import {reportInvalidGalleryMedia} from "./GalleryMediaIntegrityReportService"
|
|
14
|
+
import {galleryTransferLedger} from "./galleryTransferLedger"
|
|
15
|
+
import {localNetworkTransport} from "./localNetworkTransport"
|
|
16
|
+
|
|
17
|
+
export interface GalleryCapabilities {
|
|
18
|
+
api_version: number
|
|
19
|
+
range_downloads: boolean
|
|
20
|
+
fixed_length_downloads: boolean
|
|
21
|
+
etag: boolean
|
|
22
|
+
sha256: boolean
|
|
23
|
+
recoverable_trash: boolean
|
|
24
|
+
idempotent_ack: boolean
|
|
25
|
+
selected_capture?: boolean
|
|
26
|
+
recommended_segment_bytes?: number
|
|
27
|
+
max_manifest_page_size?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type V3ManifestResponse = {
|
|
31
|
+
status: string
|
|
32
|
+
data: {
|
|
33
|
+
api_version: 3
|
|
34
|
+
captures: CaptureGroup[]
|
|
35
|
+
has_more: boolean
|
|
36
|
+
next_cursor?: string | null
|
|
37
|
+
total_count: number
|
|
38
|
+
server_time: number
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class AsgCameraApiClient {
|
|
43
|
+
private baseUrl: string
|
|
44
|
+
private port: number
|
|
45
|
+
private lastRequestTime: number = 0
|
|
46
|
+
private galleryCapabilities: GalleryCapabilities | null | undefined
|
|
47
|
+
|
|
48
|
+
constructor(serverUrl?: string, port: number = 8089) {
|
|
49
|
+
this.port = port
|
|
50
|
+
this.baseUrl = serverUrl || `http://localhost:${port}`
|
|
51
|
+
console.log(`[ASG Camera API] Client initialized with server: ${this.baseUrl}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private createTimeoutSignal(timeoutMs: number): AbortSignal {
|
|
55
|
+
const controller = new AbortController()
|
|
56
|
+
BgTimer.setTimeout(() => controller.abort(), timeoutMs)
|
|
57
|
+
return controller.signal
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Set the server URL and port
|
|
62
|
+
*/
|
|
63
|
+
setServer(serverUrl: string, port?: number) {
|
|
64
|
+
console.log(`[ASG Camera API] setServer called with serverUrl: ${serverUrl}, port: ${port}`)
|
|
65
|
+
const newPort = port || this.port
|
|
66
|
+
const newUrl = `http://${serverUrl.replace(/^https?:\/\//, "")}:${newPort}`
|
|
67
|
+
|
|
68
|
+
console.log(`[ASG Camera API] Constructed newUrl: ${newUrl}`)
|
|
69
|
+
// console.log(`[ASG Camera API] Current baseUrl: ${this.baseUrl}`)
|
|
70
|
+
|
|
71
|
+
// Only update if the URL actually changed
|
|
72
|
+
if (this.baseUrl !== newUrl) {
|
|
73
|
+
this.baseUrl = newUrl
|
|
74
|
+
this.port = newPort
|
|
75
|
+
this.galleryCapabilities = undefined
|
|
76
|
+
// console.log(`[ASG Camera API] Server changed from ${oldUrl} to ${this.baseUrl}`)
|
|
77
|
+
} else {
|
|
78
|
+
// console.log(`[ASG Camera API] Server URL unchanged: ${this.baseUrl}`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get the current server URL
|
|
84
|
+
*/
|
|
85
|
+
getServerUrl(): string {
|
|
86
|
+
return this.baseUrl
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Rate limiting helper - ensures minimum delay between requests
|
|
91
|
+
*/
|
|
92
|
+
private async rateLimit(minDelay: number = 500): Promise<void> {
|
|
93
|
+
const now = Date.now()
|
|
94
|
+
const timeSinceLastRequest = now - this.lastRequestTime
|
|
95
|
+
|
|
96
|
+
if (timeSinceLastRequest < minDelay) {
|
|
97
|
+
const delay = minDelay - timeSinceLastRequest
|
|
98
|
+
console.log(`[ASG Camera API] Rate limiting: waiting ${delay}ms`)
|
|
99
|
+
await new Promise<void>((resolve) => BgTimer.setTimeout(() => resolve(), delay))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.lastRequestTime = Date.now()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Make a request to the ASG Camera Server with rate limiting and retry logic
|
|
107
|
+
*/
|
|
108
|
+
private async makeRequest<T>(
|
|
109
|
+
endpoint: string,
|
|
110
|
+
options?: RequestInit,
|
|
111
|
+
retries: number = 5,
|
|
112
|
+
writeIntervalMs: number = 500,
|
|
113
|
+
): Promise<T> {
|
|
114
|
+
const url = `${this.baseUrl}${endpoint}`
|
|
115
|
+
const method = options?.method || "GET"
|
|
116
|
+
|
|
117
|
+
console.log(`[ASG Camera API] makeRequest called with endpoint: ${endpoint}`)
|
|
118
|
+
console.log(`[ASG Camera API] Current baseUrl: ${this.baseUrl}`)
|
|
119
|
+
console.log(`[ASG Camera API] Full URL: ${url}`)
|
|
120
|
+
console.log(`[ASG Camera API] Method: ${method}`)
|
|
121
|
+
console.log(`[ASG Camera API] Retries remaining: ${retries}`)
|
|
122
|
+
console.log(`[ASG Camera API] Request options:`, {
|
|
123
|
+
method,
|
|
124
|
+
headers: options?.headers,
|
|
125
|
+
body: options?.body ? "Present" : "None",
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const startTime = Date.now()
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// Apply rate limiting only for non-GET requests
|
|
132
|
+
if (method !== "GET" && writeIntervalMs > 0) {
|
|
133
|
+
await this.rateLimit(writeIntervalMs)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Prepare headers - don't set Content-Type for GET requests
|
|
137
|
+
const headers: Record<string, string> = {
|
|
138
|
+
"Accept": "application/json",
|
|
139
|
+
"User-Agent": "MentraOS-Mobile/1.0",
|
|
140
|
+
}
|
|
141
|
+
if (method !== "GET") {
|
|
142
|
+
headers["Content-Type"] = "application/json"
|
|
143
|
+
}
|
|
144
|
+
if (options?.headers) {
|
|
145
|
+
Object.assign(headers, options.headers)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(`[ASG Camera API] Making fetch request to: ${url}`)
|
|
149
|
+
console.log(`[ASG Camera API] Headers being sent:`, headers)
|
|
150
|
+
|
|
151
|
+
// N4: Add 30s timeout to all fetch calls in makeRequest
|
|
152
|
+
const response = await localNetworkTransport.fetch(url, {
|
|
153
|
+
headers,
|
|
154
|
+
...options,
|
|
155
|
+
signal: options?.signal || this.createTimeoutSignal(30000),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const duration = Date.now() - startTime
|
|
159
|
+
console.log(`[ASG Camera API] Response received in ${duration}ms:`, {
|
|
160
|
+
status: response.status,
|
|
161
|
+
statusText: response.statusText,
|
|
162
|
+
contentType: response.headers.get("content-type"),
|
|
163
|
+
contentLength: response.headers.get("content-length"),
|
|
164
|
+
url: response.url,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
console.error(`[ASG Camera API] HTTP Error ${response.status}: ${response.statusText}`)
|
|
169
|
+
|
|
170
|
+
// Handle rate limiting with retry
|
|
171
|
+
if (response.status === 429 && retries > 0) {
|
|
172
|
+
// N3: Cap individual retry delay at 10s
|
|
173
|
+
const retryDelay = Math.min(Math.pow(2, 6 - retries) * 1000, 10000)
|
|
174
|
+
console.log(`[ASG Camera API] Rate limited, retrying in ${retryDelay}ms (${retries} retries left)`)
|
|
175
|
+
await new Promise<void>((resolve) => BgTimer.setTimeout(() => resolve(), retryDelay))
|
|
176
|
+
return this.makeRequest<T>(endpoint, options, retries - 1, writeIntervalMs)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Handle different response types
|
|
183
|
+
const contentType = response.headers.get("content-type")
|
|
184
|
+
console.log(`[ASG Camera API] Response content-type: ${contentType}`)
|
|
185
|
+
|
|
186
|
+
if (contentType?.includes("application/json")) {
|
|
187
|
+
const data = await response.json()
|
|
188
|
+
console.log(`[ASG Camera API] JSON response received`, {
|
|
189
|
+
endpoint,
|
|
190
|
+
status: data?.status,
|
|
191
|
+
apiVersion: data?.data?.api_version,
|
|
192
|
+
captureCount: data?.data?.captures?.length,
|
|
193
|
+
})
|
|
194
|
+
return data
|
|
195
|
+
} else if (contentType?.includes("image/") || contentType?.includes("application/octet-stream")) {
|
|
196
|
+
// For image responses and binary data (including AVIF), return the blob
|
|
197
|
+
const blob = await response.blob()
|
|
198
|
+
console.log(`[ASG Camera API] Binary/Image Response received:`, {
|
|
199
|
+
size: blob.size,
|
|
200
|
+
type: blob.type,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// Quick check if this might be an AVIF file
|
|
204
|
+
if (contentType?.includes("application/octet-stream") && blob.size > 12) {
|
|
205
|
+
const arrayBuffer = await blob.arrayBuffer()
|
|
206
|
+
const bytes = new Uint8Array(arrayBuffer.slice(4, 12))
|
|
207
|
+
const ftypSignature = String.fromCharCode(...bytes)
|
|
208
|
+
if (ftypSignature === "ftypavif") {
|
|
209
|
+
console.log(`[ASG Camera API] Detected AVIF file in response`)
|
|
210
|
+
}
|
|
211
|
+
// Return a new blob since we consumed the original
|
|
212
|
+
return new Blob([arrayBuffer], {type: blob.type}) as T
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return blob as T
|
|
216
|
+
} else {
|
|
217
|
+
// For text responses
|
|
218
|
+
const text = await response.text()
|
|
219
|
+
console.log(
|
|
220
|
+
`[ASG Camera API] Text Response received:`,
|
|
221
|
+
text.substring(0, 200) + (text.length > 200 ? "..." : ""),
|
|
222
|
+
)
|
|
223
|
+
return text as T
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
const duration = Date.now() - startTime
|
|
227
|
+
console.error(`[ASG Camera API] Error (${endpoint}) after ${duration}ms:`, error)
|
|
228
|
+
console.error(`[ASG Camera API] Error details:`, {
|
|
229
|
+
endpoint,
|
|
230
|
+
url,
|
|
231
|
+
method,
|
|
232
|
+
duration,
|
|
233
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
234
|
+
errorStack: error instanceof Error ? error.stack : undefined,
|
|
235
|
+
})
|
|
236
|
+
throw error
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Take a picture using the ASG camera
|
|
242
|
+
*/
|
|
243
|
+
async takePicture(): Promise<{message: string}> {
|
|
244
|
+
console.log(`[ASG Camera API] Taking picture...`)
|
|
245
|
+
return this.makeRequest<{message: string}>("/api/take-picture", {
|
|
246
|
+
method: "POST",
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get the latest photo as a blob
|
|
252
|
+
*/
|
|
253
|
+
async getLatestPhoto(): Promise<Blob> {
|
|
254
|
+
console.log(`[ASG Camera API] Getting latest photo...`)
|
|
255
|
+
return this.makeRequest<Blob>("/api/latest-photo")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get the latest photo as a data URL
|
|
260
|
+
*/
|
|
261
|
+
async getLatestPhotoAsDataUrl(): Promise<string> {
|
|
262
|
+
console.log(`[ASG Camera API] Getting latest photo as data URL...`)
|
|
263
|
+
const blob = await this.getLatestPhoto()
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
const reader = new FileReader()
|
|
266
|
+
reader.onload = () => resolve(reader.result as string)
|
|
267
|
+
reader.onerror = reject
|
|
268
|
+
reader.readAsDataURL(blob)
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get gallery photos from the server with optional pagination
|
|
274
|
+
*/
|
|
275
|
+
async getGallery(limit?: number, offset?: number): Promise<GalleryResponse> {
|
|
276
|
+
console.log(`[ASG Camera API] getGallery called with limit=${limit}, offset=${offset}`)
|
|
277
|
+
// console.log(`[ASG Camera API] Current baseUrl: ${this.baseUrl}`)
|
|
278
|
+
|
|
279
|
+
// Build URL with optional query parameters
|
|
280
|
+
let galleryUrl = `${this.baseUrl}/api/gallery`
|
|
281
|
+
const params = new URLSearchParams()
|
|
282
|
+
if (limit !== undefined) params.append("limit", limit.toString())
|
|
283
|
+
if (offset !== undefined) params.append("offset", offset.toString())
|
|
284
|
+
if (params.toString()) galleryUrl += `?${params.toString()}`
|
|
285
|
+
|
|
286
|
+
console.log(`[ASG Camera API] Full gallery URL: ${galleryUrl}`)
|
|
287
|
+
|
|
288
|
+
// Use browser-like headers since we know the browser works
|
|
289
|
+
try {
|
|
290
|
+
console.log(`[ASG Camera API] Making direct fetch to gallery endpoint`)
|
|
291
|
+
const response = await localNetworkTransport.fetch(galleryUrl, {
|
|
292
|
+
method: "GET",
|
|
293
|
+
headers: {
|
|
294
|
+
"Accept": "application/json",
|
|
295
|
+
"User-Agent": "MentraOS-Mobile/1.0",
|
|
296
|
+
},
|
|
297
|
+
signal: this.createTimeoutSignal(10000), // 10 second timeout
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
console.log(`[ASG Camera API] Response status: ${response.status}`)
|
|
301
|
+
|
|
302
|
+
if (!response.ok) {
|
|
303
|
+
throw new Error(`Gallery endpoint returned: ${response.status}`)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const responseText = await response.text()
|
|
307
|
+
console.log(`[ASG Camera API] Raw response:`, responseText.substring(0, 1000))
|
|
308
|
+
|
|
309
|
+
let data: any
|
|
310
|
+
try {
|
|
311
|
+
data = JSON.parse(responseText)
|
|
312
|
+
} catch (parseError) {
|
|
313
|
+
console.log(`[ASG Camera API] Failed to parse JSON:`, parseError)
|
|
314
|
+
throw new Error("Invalid JSON response from gallery endpoint")
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Handle the exact response format we see from browser
|
|
318
|
+
if (data && data.status === "success" && data.data?.photos) {
|
|
319
|
+
console.log(`[ASG Camera API] Found ${data.data.photos.length} photos (total: ${data.data.total_count})`)
|
|
320
|
+
|
|
321
|
+
// Map photos to ensure proper URL construction
|
|
322
|
+
const photos = data.data.photos.map((photo: any) => ({
|
|
323
|
+
...photo,
|
|
324
|
+
url: this.constructPhotoUrl(photo.url || photo.name),
|
|
325
|
+
download: this.constructDownloadUrl(photo.download || photo.name),
|
|
326
|
+
}))
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
status: "success",
|
|
330
|
+
data: {
|
|
331
|
+
photos,
|
|
332
|
+
total_count: data.data.total_count,
|
|
333
|
+
returned_count: data.data.returned_count,
|
|
334
|
+
has_more: data.data.has_more,
|
|
335
|
+
offset: data.data.offset,
|
|
336
|
+
limit: data.data.limit,
|
|
337
|
+
},
|
|
338
|
+
} as GalleryResponse
|
|
339
|
+
} else {
|
|
340
|
+
console.log(`[ASG Camera API] Invalid response structure:`, data)
|
|
341
|
+
throw new Error("Invalid response structure from gallery endpoint")
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.log(`[ASG Camera API] Gallery request failed:`, error)
|
|
345
|
+
throw error
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async deleteGalleryPhoto(photoId: string): Promise<any> {
|
|
350
|
+
const response = await this.makeRequest<any>(`/api/gallery/${photoId}`, {
|
|
351
|
+
method: "DELETE",
|
|
352
|
+
})
|
|
353
|
+
console.log("Photo deleted successfully:", photoId)
|
|
354
|
+
return response
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get the gallery photos array with proper URL construction
|
|
359
|
+
*/
|
|
360
|
+
async getGalleryPhotos(
|
|
361
|
+
limit?: number,
|
|
362
|
+
offset?: number,
|
|
363
|
+
): Promise<{
|
|
364
|
+
photos: PhotoInfo[]
|
|
365
|
+
hasMore: boolean
|
|
366
|
+
totalCount: number
|
|
367
|
+
}> {
|
|
368
|
+
console.log(`[ASG Camera API] Getting gallery photos with limit=${limit}, offset=${offset}...`)
|
|
369
|
+
try {
|
|
370
|
+
const response = await this.getGallery(limit, offset)
|
|
371
|
+
console.log(`[ASG Camera API] Gallery response:`, response)
|
|
372
|
+
|
|
373
|
+
if (!response.data || !response.data.photos) {
|
|
374
|
+
console.warn(`[ASG Camera API] Invalid gallery response structure:`, response)
|
|
375
|
+
return {photos: [], hasMore: false, totalCount: 0}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const photos = response.data.photos
|
|
379
|
+
console.log(`[ASG Camera API] Found ${photos.length} photos (total: ${(response.data as any).total_count})`)
|
|
380
|
+
|
|
381
|
+
// Ensure each photo has proper URLs and detect AVIF files
|
|
382
|
+
const processedPhotos = photos.map((photo) => {
|
|
383
|
+
// Check if filename suggests AVIF (no extension or .avif)
|
|
384
|
+
const mightBeAvif = !photo.name.includes(".") || photo.name.match(/\.(avif|avifs)$/i)
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
...photo,
|
|
388
|
+
url: this.constructPhotoUrl(photo.name),
|
|
389
|
+
download: this.constructDownloadUrl(photo.name),
|
|
390
|
+
mime_type: photo.mime_type || (mightBeAvif ? "image/avif" : undefined),
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
console.log(`[ASG Camera API] Processed photos:`, processedPhotos)
|
|
395
|
+
return {
|
|
396
|
+
photos: processedPhotos,
|
|
397
|
+
hasMore: (response.data as any).has_more || false,
|
|
398
|
+
totalCount: (response.data as any).total_count || photos.length,
|
|
399
|
+
}
|
|
400
|
+
} catch (error) {
|
|
401
|
+
console.error(`[ASG Camera API] Error getting gallery photos:`, error)
|
|
402
|
+
throw error
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Discover available endpoints on the server
|
|
408
|
+
*/
|
|
409
|
+
async discoverEndpoints(): Promise<string[]> {
|
|
410
|
+
const availableEndpoints: string[] = []
|
|
411
|
+
const testEndpoints = [
|
|
412
|
+
"/",
|
|
413
|
+
"/api",
|
|
414
|
+
"/api/health",
|
|
415
|
+
"/api/status",
|
|
416
|
+
"/api/gallery",
|
|
417
|
+
"/gallery",
|
|
418
|
+
"/api/photos",
|
|
419
|
+
"/photos",
|
|
420
|
+
"/api/images",
|
|
421
|
+
"/images",
|
|
422
|
+
"/api/take-picture",
|
|
423
|
+
"/api/latest-photo",
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
console.log(`[ASG Camera API] Discovering available endpoints...`)
|
|
427
|
+
|
|
428
|
+
for (const endpoint of testEndpoints) {
|
|
429
|
+
try {
|
|
430
|
+
console.log(`[ASG Camera API] Testing endpoint: ${endpoint}`)
|
|
431
|
+
const response = await localNetworkTransport.fetch(`${this.baseUrl}${endpoint}`, {
|
|
432
|
+
method: "HEAD",
|
|
433
|
+
headers: {
|
|
434
|
+
"Accept": "*/*",
|
|
435
|
+
"User-Agent": "MentraOS-Mobile/1.0",
|
|
436
|
+
},
|
|
437
|
+
signal: this.createTimeoutSignal(5000),
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
if (response.ok) {
|
|
441
|
+
availableEndpoints.push(endpoint)
|
|
442
|
+
console.log(`[ASG Camera API] Found endpoint: ${endpoint} (${response.status})`)
|
|
443
|
+
} else {
|
|
444
|
+
console.log(`[ASG Camera API] Endpoint ${endpoint} returned: ${response.status}`)
|
|
445
|
+
}
|
|
446
|
+
} catch (error) {
|
|
447
|
+
console.log(`[ASG Camera API] Endpoint ${endpoint} failed:`, error)
|
|
448
|
+
// For /api/gallery specifically, let's try a GET request to see if it's a HEAD request issue
|
|
449
|
+
if (endpoint === "/api/gallery") {
|
|
450
|
+
try {
|
|
451
|
+
console.log(`[ASG Camera API] Trying GET request for /api/gallery...`)
|
|
452
|
+
const getResponse = await localNetworkTransport.fetch(`${this.baseUrl}${endpoint}`, {
|
|
453
|
+
method: "GET",
|
|
454
|
+
headers: {
|
|
455
|
+
"Accept": "application/json",
|
|
456
|
+
"User-Agent": "MentraOS-Mobile/1.0",
|
|
457
|
+
},
|
|
458
|
+
signal: this.createTimeoutSignal(5000),
|
|
459
|
+
})
|
|
460
|
+
console.log(`[ASG Camera API] GET /api/gallery status: ${getResponse.status}`)
|
|
461
|
+
if (getResponse.ok) {
|
|
462
|
+
console.log(`[ASG Camera API] GET /api/gallery works! Adding to available endpoints`)
|
|
463
|
+
availableEndpoints.push(endpoint)
|
|
464
|
+
}
|
|
465
|
+
} catch (getError) {
|
|
466
|
+
console.log(`[ASG Camera API] GET /api/gallery also failed:`, getError)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
console.log(`[ASG Camera API] Available endpoints:`, availableEndpoints)
|
|
473
|
+
return availableEndpoints
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Construct a photo URL for a given filename
|
|
478
|
+
*/
|
|
479
|
+
private constructPhotoUrl(filename: string): string {
|
|
480
|
+
return `${this.baseUrl}/api/photo?file=${encodeURIComponent(filename)}`
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Construct a download URL for a given filename
|
|
485
|
+
*/
|
|
486
|
+
private constructDownloadUrl(filename: string): string {
|
|
487
|
+
return `${this.baseUrl}/api/download?file=${encodeURIComponent(filename)}`
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Get a specific photo by filename
|
|
492
|
+
*/
|
|
493
|
+
async getPhoto(filename: string): Promise<Blob> {
|
|
494
|
+
console.log(`[ASG Camera API] Getting photo: ${filename}`)
|
|
495
|
+
return this.makeRequest<Blob>(`/api/photo?file=${encodeURIComponent(filename)}`)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Get a specific photo as a data URL
|
|
500
|
+
*/
|
|
501
|
+
async getPhotoAsDataUrl(filename: string): Promise<string> {
|
|
502
|
+
console.log(`[ASG Camera API] Getting photo as data URL: ${filename}`)
|
|
503
|
+
const blob = await this.getPhoto(filename)
|
|
504
|
+
return new Promise((resolve, reject) => {
|
|
505
|
+
const reader = new FileReader()
|
|
506
|
+
reader.onload = () => resolve(reader.result as string)
|
|
507
|
+
reader.onerror = reject
|
|
508
|
+
reader.readAsDataURL(blob)
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Download a photo (returns download URL)
|
|
514
|
+
*/
|
|
515
|
+
async downloadPhoto(filename: string): Promise<string> {
|
|
516
|
+
console.log(`[ASG Camera API] Downloading photo: ${filename}`)
|
|
517
|
+
const downloadUrl = `${this.baseUrl}/api/download?file=${encodeURIComponent(filename)}`
|
|
518
|
+
console.log(`[ASG Camera API] Download URL: ${downloadUrl}`)
|
|
519
|
+
return downloadUrl
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Get server status information
|
|
524
|
+
*/
|
|
525
|
+
async getStatus(): Promise<ServerStatus> {
|
|
526
|
+
console.log(`[ASG Camera API] Getting server status...`)
|
|
527
|
+
return this.makeRequest<ServerStatus>("/api/status")
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Get server health check
|
|
532
|
+
*/
|
|
533
|
+
async getHealth(): Promise<HealthResponse> {
|
|
534
|
+
console.log(`[ASG Camera API] Getting server health...`)
|
|
535
|
+
return this.makeRequest<HealthResponse>("/api/health")
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Get the index page (for testing)
|
|
540
|
+
*/
|
|
541
|
+
async getIndexPage(): Promise<string> {
|
|
542
|
+
console.log(`[ASG Camera API] Getting index page...`)
|
|
543
|
+
return this.makeRequest<string>("/")
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Check if the server is reachable (simple ping)
|
|
548
|
+
*/
|
|
549
|
+
async isServerReachable(): Promise<boolean> {
|
|
550
|
+
try {
|
|
551
|
+
console.log(`[ASG Camera API] Checking server reachability...`)
|
|
552
|
+
// Use a simple HEAD request to check reachability
|
|
553
|
+
const controller = new AbortController()
|
|
554
|
+
const timeoutId = BgTimer.setTimeout(() => controller.abort(), 3000) // 3 second timeout
|
|
555
|
+
|
|
556
|
+
const response = await localNetworkTransport.fetch(`${this.baseUrl}/api/health`, {
|
|
557
|
+
method: "HEAD",
|
|
558
|
+
signal: controller.signal,
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
BgTimer.clearTimeout(timeoutId)
|
|
562
|
+
console.log(`[ASG Camera API] Server is reachable`)
|
|
563
|
+
return response.ok
|
|
564
|
+
} catch (error) {
|
|
565
|
+
console.log(`[ASG Camera API] Server is not reachable:`, error)
|
|
566
|
+
return false
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Get comprehensive server information
|
|
572
|
+
*/
|
|
573
|
+
async getServerInfo(): Promise<{
|
|
574
|
+
reachable: boolean
|
|
575
|
+
status?: ServerStatus
|
|
576
|
+
health?: HealthResponse
|
|
577
|
+
error?: string
|
|
578
|
+
}> {
|
|
579
|
+
try {
|
|
580
|
+
const [status, health] = await Promise.all([this.getStatus(), this.getHealth()])
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
reachable: true,
|
|
584
|
+
status,
|
|
585
|
+
health,
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
return {
|
|
589
|
+
reachable: false,
|
|
590
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Sync with server to get changed files since last sync
|
|
597
|
+
*/
|
|
598
|
+
async syncWithServer(
|
|
599
|
+
clientId: string,
|
|
600
|
+
lastSyncTime?: number,
|
|
601
|
+
includeThumbnails: boolean = false,
|
|
602
|
+
): Promise<{
|
|
603
|
+
status: string
|
|
604
|
+
data: {
|
|
605
|
+
api_version?: number
|
|
606
|
+
client_id: string
|
|
607
|
+
captures?: CaptureGroup[]
|
|
608
|
+
changed_files: PhotoInfo[]
|
|
609
|
+
deleted_files: string[]
|
|
610
|
+
server_time: number
|
|
611
|
+
total_changed: number
|
|
612
|
+
total_size: number
|
|
613
|
+
}
|
|
614
|
+
}> {
|
|
615
|
+
const params = new URLSearchParams({
|
|
616
|
+
client_id: clientId,
|
|
617
|
+
include_thumbnails: includeThumbnails.toString(),
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
if (lastSyncTime) {
|
|
621
|
+
params.append("last_sync_time", lastSyncTime.toString())
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const response = await this.makeRequest(`/api/sync?${params.toString()}`, {
|
|
625
|
+
method: "GET",
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
return response as {
|
|
629
|
+
status: string
|
|
630
|
+
data: {
|
|
631
|
+
api_version?: number
|
|
632
|
+
client_id: string
|
|
633
|
+
captures?: CaptureGroup[]
|
|
634
|
+
changed_files: PhotoInfo[]
|
|
635
|
+
deleted_files: string[]
|
|
636
|
+
server_time: number
|
|
637
|
+
total_changed: number
|
|
638
|
+
total_size: number
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Probe the additive gallery protocol. A 404 cleanly falls back to legacy v2. */
|
|
644
|
+
async getGalleryCapabilities(): Promise<GalleryCapabilities | null> {
|
|
645
|
+
if (this.galleryCapabilities !== undefined) return this.galleryCapabilities
|
|
646
|
+
try {
|
|
647
|
+
const response = await localNetworkTransport.fetch(`${this.baseUrl}/api/v3/capabilities`, {
|
|
648
|
+
headers: {"Accept": "application/json", "User-Agent": "MentraOS-Mobile/1.0"},
|
|
649
|
+
signal: this.createTimeoutSignal(5000),
|
|
650
|
+
})
|
|
651
|
+
if (response.status === 404) {
|
|
652
|
+
this.galleryCapabilities = null
|
|
653
|
+
return null
|
|
654
|
+
}
|
|
655
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
656
|
+
const payload = await response.json()
|
|
657
|
+
const capabilities = (payload.data || payload) as GalleryCapabilities
|
|
658
|
+
this.galleryCapabilities = capabilities.api_version >= 3 ? capabilities : null
|
|
659
|
+
return this.galleryCapabilities
|
|
660
|
+
} catch (error) {
|
|
661
|
+
// A failed probe must not break old firmware. Do not permanently cache transient failures.
|
|
662
|
+
console.warn("[ASG Camera API] v3 capability probe failed; using legacy sync for this attempt", error)
|
|
663
|
+
return null
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Fetch all lightweight v3 manifest pages, newest first. */
|
|
668
|
+
async getV3Manifest(): Promise<V3ManifestResponse["data"] | null> {
|
|
669
|
+
const capabilities = await this.getGalleryCapabilities()
|
|
670
|
+
if (!capabilities) return null
|
|
671
|
+
|
|
672
|
+
const captures: CaptureGroup[] = []
|
|
673
|
+
let cursor: string | undefined
|
|
674
|
+
let serverTime = Date.now()
|
|
675
|
+
let totalCount = 0
|
|
676
|
+
do {
|
|
677
|
+
const params = new URLSearchParams({limit: String(capabilities.max_manifest_page_size || 100)})
|
|
678
|
+
if (cursor) params.set("cursor", cursor)
|
|
679
|
+
const response = await this.makeRequest<V3ManifestResponse>(`/api/v3/manifest?${params.toString()}`)
|
|
680
|
+
const data = response.data || (response as unknown as V3ManifestResponse["data"])
|
|
681
|
+
captures.push(...(data.captures || []))
|
|
682
|
+
serverTime = data.server_time || serverTime
|
|
683
|
+
totalCount = data.total_count || captures.length
|
|
684
|
+
cursor = data.has_more && data.next_cursor ? data.next_cursor : undefined
|
|
685
|
+
} while (cursor)
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
api_version: 3,
|
|
689
|
+
captures,
|
|
690
|
+
has_more: false,
|
|
691
|
+
next_cursor: null,
|
|
692
|
+
total_count: totalCount,
|
|
693
|
+
server_time: serverTime,
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async getV3FileHash(fileName: string): Promise<{sha256: string; etag: string; size: number}> {
|
|
698
|
+
const response = await localNetworkTransport.fetch(
|
|
699
|
+
`${this.baseUrl}/api/v3/hash?file=${encodeURIComponent(fileName)}`,
|
|
700
|
+
{
|
|
701
|
+
headers: {"Accept": "application/json", "User-Agent": "MentraOS-Mobile/1.0"},
|
|
702
|
+
signal: this.createTimeoutSignal(10 * 60 * 1000),
|
|
703
|
+
},
|
|
704
|
+
10 * 60 * 1000,
|
|
705
|
+
)
|
|
706
|
+
if (!response.ok) throw new Error(`Hash request failed: HTTP ${response.status}`)
|
|
707
|
+
const payload = await response.json()
|
|
708
|
+
return (payload.data || payload) as {sha256: string; etag: string; size: number}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async acknowledgeCapture(captureId: string, ackId: string): Promise<void> {
|
|
712
|
+
const response = await this.makeRequest<any>(
|
|
713
|
+
"/api/v3/ack",
|
|
714
|
+
{
|
|
715
|
+
method: "POST",
|
|
716
|
+
body: JSON.stringify({capture_id: captureId, ack_id: ackId}),
|
|
717
|
+
},
|
|
718
|
+
5,
|
|
719
|
+
0,
|
|
720
|
+
)
|
|
721
|
+
const data = response.data || response
|
|
722
|
+
if (!data.success && !data.already_trashed) {
|
|
723
|
+
throw new Error(data.message || `Glasses did not acknowledge ${captureId}`)
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async restoreCapture(captureId: string): Promise<void> {
|
|
728
|
+
const response = await this.makeRequest<any>(
|
|
729
|
+
"/api/v3/restore",
|
|
730
|
+
{
|
|
731
|
+
method: "POST",
|
|
732
|
+
body: JSON.stringify({capture_id: captureId}),
|
|
733
|
+
},
|
|
734
|
+
5,
|
|
735
|
+
0,
|
|
736
|
+
)
|
|
737
|
+
const data = response.data || response
|
|
738
|
+
if (!data.success) throw new Error(data.message || `Glasses did not restore ${captureId}`)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Batch sync files from server with controlled concurrency.
|
|
743
|
+
* Used by the legacy executeDownload path for old asg_client firmware
|
|
744
|
+
* that doesn't send api_version=2.
|
|
745
|
+
*/
|
|
746
|
+
async batchSyncFiles(
|
|
747
|
+
files: PhotoInfo[],
|
|
748
|
+
includeThumbnails: boolean = false,
|
|
749
|
+
onProgress?: (
|
|
750
|
+
current: number,
|
|
751
|
+
total: number,
|
|
752
|
+
fileName: string,
|
|
753
|
+
fileProgress?: number,
|
|
754
|
+
downloadedFile?: PhotoInfo,
|
|
755
|
+
) => void,
|
|
756
|
+
abortSignal?: AbortSignal,
|
|
757
|
+
): Promise<{
|
|
758
|
+
downloaded: PhotoInfo[]
|
|
759
|
+
failed: string[]
|
|
760
|
+
total_size: number
|
|
761
|
+
}> {
|
|
762
|
+
const results = {
|
|
763
|
+
downloaded: [] as PhotoInfo[],
|
|
764
|
+
failed: [] as string[],
|
|
765
|
+
total_size: 0,
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Process files in parallel batches for better performance
|
|
769
|
+
// Use controlled concurrency to avoid overwhelming the network
|
|
770
|
+
const CONCURRENCY_LIMIT = 1
|
|
771
|
+
|
|
772
|
+
// Process files in batches
|
|
773
|
+
for (let i = 0; i < files.length; i += CONCURRENCY_LIMIT) {
|
|
774
|
+
const batch = files.slice(i, i + CONCURRENCY_LIMIT)
|
|
775
|
+
console.log(
|
|
776
|
+
`[ASG Camera API] Processing batch ${Math.floor(i / CONCURRENCY_LIMIT) + 1}/${Math.ceil(
|
|
777
|
+
files.length / CONCURRENCY_LIMIT,
|
|
778
|
+
)}: ${batch.length} files`,
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
// Process batch in parallel
|
|
782
|
+
const batchPromises = batch.map(async (file, batchIndex) => {
|
|
783
|
+
const globalIndex = i + batchIndex
|
|
784
|
+
|
|
785
|
+
// Report progress if callback provided - start of this file (0%)
|
|
786
|
+
if (onProgress) {
|
|
787
|
+
onProgress(globalIndex + 1, files.length, file.name, 0)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
try {
|
|
791
|
+
console.log(`[ASG Camera API] Downloading file ${globalIndex + 1}/${files.length}: ${file.name}`)
|
|
792
|
+
|
|
793
|
+
// Download file with progress tracking
|
|
794
|
+
const fileData = await this.downloadFile(
|
|
795
|
+
file.name,
|
|
796
|
+
includeThumbnails,
|
|
797
|
+
(fileProgress) => {
|
|
798
|
+
if (onProgress) {
|
|
799
|
+
onProgress(globalIndex + 1, files.length, file.name, fileProgress)
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
abortSignal,
|
|
803
|
+
file.size, // Pass expected size for validation when Content-Length is missing
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
// Combine file info with downloaded file paths
|
|
807
|
+
const downloadedFile = {
|
|
808
|
+
...file,
|
|
809
|
+
filePath: fileData.filePath,
|
|
810
|
+
thumbnailPath: fileData.thumbnailPath,
|
|
811
|
+
mime_type: fileData.mime_type || file.mime_type,
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
console.log(`[ASG Camera API] Successfully downloaded: ${file.name}`)
|
|
815
|
+
|
|
816
|
+
// Notify progress callback that this file is complete with file info
|
|
817
|
+
if (onProgress) {
|
|
818
|
+
onProgress(globalIndex + 1, files.length, file.name, 100, downloadedFile)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Don't delete from glasses here — deletion is deferred until after
|
|
822
|
+
// processing completes (in mediaProcessingQueue) to avoid data loss on crash.
|
|
823
|
+
|
|
824
|
+
return {downloadedFile, fileSize: file.size}
|
|
825
|
+
} catch (error: any) {
|
|
826
|
+
// Re-throw cancellation so it terminates the entire batch
|
|
827
|
+
if (error?.message === "Sync cancelled") {
|
|
828
|
+
throw error
|
|
829
|
+
}
|
|
830
|
+
console.error(`[ASG Camera API] Failed to download ${file.name}:`, error)
|
|
831
|
+
return {error: file.name}
|
|
832
|
+
}
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
// Wait for batch to complete
|
|
836
|
+
const batchResults = await Promise.all(batchPromises)
|
|
837
|
+
|
|
838
|
+
// Process batch results
|
|
839
|
+
for (const result of batchResults) {
|
|
840
|
+
if ("downloadedFile" in result && result.downloadedFile) {
|
|
841
|
+
results.downloaded.push(result.downloadedFile)
|
|
842
|
+
results.total_size += result.fileSize || 0
|
|
843
|
+
} else if ("error" in result) {
|
|
844
|
+
results.failed.push(result.error)
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
console.log(
|
|
850
|
+
`[ASG Camera API] Batch sync completed: ${results.downloaded.length} downloaded, ${results.failed.length} failed`,
|
|
851
|
+
)
|
|
852
|
+
return results
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Download all files in a capture group sequentially.
|
|
857
|
+
* Reports aggregate byte progress across all files in the group.
|
|
858
|
+
*/
|
|
859
|
+
private async downloadResumableCaptureFile(
|
|
860
|
+
captureId: string,
|
|
861
|
+
file: CaptureGroup["files"][number],
|
|
862
|
+
localFilePath: string,
|
|
863
|
+
onProgress?: (bytesDownloaded: number) => void,
|
|
864
|
+
abortSignal?: AbortSignal,
|
|
865
|
+
): Promise<void> {
|
|
866
|
+
const segmentBytes = 16 * 1024 * 1024
|
|
867
|
+
const segmentCount = Math.max(1, Math.ceil(file.size / segmentBytes))
|
|
868
|
+
const generation = (file.etag || `${file.size}`).replace(/[^A-Za-z0-9_-]/g, "").slice(0, 48) || "legacy"
|
|
869
|
+
const partPath = (index: number) => `${localFilePath}.partial.${generation}.${index}`
|
|
870
|
+
const assemblingPath = `${localFilePath}.assembling.${generation}`
|
|
871
|
+
const transferStartedAt = Date.now()
|
|
872
|
+
let networkDurationMs = 0
|
|
873
|
+
let networkBytes = 0
|
|
874
|
+
|
|
875
|
+
if (await RNFS.exists(localFilePath)) {
|
|
876
|
+
const existing = await RNFS.stat(localFilePath)
|
|
877
|
+
if (existing.size === file.size) {
|
|
878
|
+
if (file.etag) {
|
|
879
|
+
const remoteHash = file.sha256 ? {sha256: file.sha256, etag: file.etag} : await this.getV3FileHash(file.name)
|
|
880
|
+
if (remoteHash.etag && remoteHash.etag !== file.etag) {
|
|
881
|
+
throw new Error("Source generation changed before existing-file verification")
|
|
882
|
+
}
|
|
883
|
+
const localHash = await RNFS.hash(localFilePath, "sha256")
|
|
884
|
+
if (localHash.toLowerCase() === remoteHash.sha256.toLowerCase()) {
|
|
885
|
+
galleryTransferLedger.markFileHash(captureId, file.name, localHash.toLowerCase())
|
|
886
|
+
onProgress?.(file.size)
|
|
887
|
+
return
|
|
888
|
+
}
|
|
889
|
+
console.warn(`[ASG Camera API] Existing file failed SHA-256 verification; replacing ${file.name}`)
|
|
890
|
+
} else {
|
|
891
|
+
// Legacy servers have no generation/hash endpoint. Preserve old-client
|
|
892
|
+
// compatibility and let the media validator below reject corrupt data.
|
|
893
|
+
onProgress?.(file.size)
|
|
894
|
+
return
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
await RNFS.moveFile(localFilePath, `${localFilePath}.recovery.${Date.now()}`).catch(() => {})
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
let completedBytes = 0
|
|
901
|
+
let resumedSegmentCount = 0
|
|
902
|
+
for (let index = 0; index < segmentCount; index++) {
|
|
903
|
+
const start = index * segmentBytes
|
|
904
|
+
const expectedLength = Math.min(segmentBytes, file.size - start)
|
|
905
|
+
if (await RNFS.exists(partPath(index))) {
|
|
906
|
+
const stat = await RNFS.stat(partPath(index))
|
|
907
|
+
if (stat.size === expectedLength) {
|
|
908
|
+
completedBytes += expectedLength
|
|
909
|
+
resumedSegmentCount++
|
|
910
|
+
galleryTransferLedger.markSegmentComplete(captureId, file.name, index)
|
|
911
|
+
continue
|
|
912
|
+
}
|
|
913
|
+
await RNFS.unlink(partPath(index)).catch(() => {})
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
let completed = false
|
|
917
|
+
let lastError: unknown
|
|
918
|
+
for (let attempt = 0; attempt < 5 && !completed; attempt++) {
|
|
919
|
+
if (abortSignal?.aborted) throw new Error("Sync cancelled")
|
|
920
|
+
await RNFS.unlink(partPath(index)).catch(() => {})
|
|
921
|
+
const end = start + expectedLength - 1
|
|
922
|
+
let responseHeaders: Record<string, string> = {}
|
|
923
|
+
const {jobId, promise} = localNetworkTransport.downloadFile({
|
|
924
|
+
fromUrl: `${this.baseUrl}/api/download?file=${encodeURIComponent(file.name)}`,
|
|
925
|
+
toFile: partPath(index),
|
|
926
|
+
headers: {
|
|
927
|
+
"Accept": "*/*",
|
|
928
|
+
"User-Agent": "MentraOS-Mobile/1.0",
|
|
929
|
+
"Range": `bytes=${start}-${end}`,
|
|
930
|
+
...(file.etag ? {"If-Range": file.etag} : {}),
|
|
931
|
+
},
|
|
932
|
+
connectionTimeout: 300000,
|
|
933
|
+
readTimeout: 300000,
|
|
934
|
+
backgroundTimeout: 24 * 60 * 60 * 1000,
|
|
935
|
+
progressDivider: 2,
|
|
936
|
+
progressInterval: 250,
|
|
937
|
+
begin: (response) => {
|
|
938
|
+
responseHeaders = response.headers || {}
|
|
939
|
+
},
|
|
940
|
+
progress: (progress) => onProgress?.(completedBytes + (progress.bytesWritten || 0)),
|
|
941
|
+
})
|
|
942
|
+
|
|
943
|
+
let abortPollTimer: number | undefined
|
|
944
|
+
if (abortSignal) {
|
|
945
|
+
abortPollTimer = BgTimer.setInterval(() => {
|
|
946
|
+
if (abortSignal.aborted) localNetworkTransport.stopDownload(jobId)
|
|
947
|
+
}, 500)
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const attemptStartedAt = Date.now()
|
|
951
|
+
try {
|
|
952
|
+
const result = await promise
|
|
953
|
+
const attemptDurationMs = Math.max(1, Date.now() - attemptStartedAt)
|
|
954
|
+
networkDurationMs += attemptDurationMs
|
|
955
|
+
networkBytes += result.bytesWritten || 0
|
|
956
|
+
if (abortSignal?.aborted) throw new Error("Sync cancelled")
|
|
957
|
+
const header = (name: string): string | undefined => {
|
|
958
|
+
const key = Object.keys(responseHeaders).find((candidate) => candidate.toLowerCase() === name.toLowerCase())
|
|
959
|
+
return key ? responseHeaders[key] : undefined
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (result.statusCode === 200 && index === 0 && result.bytesWritten === file.size) {
|
|
963
|
+
// Older servers ignore Range. A full fixed/chunked response is still compatible;
|
|
964
|
+
// commit it through the same staging path and skip the remaining segments.
|
|
965
|
+
await RNFS.unlink(assemblingPath).catch(() => {})
|
|
966
|
+
await RNFS.moveFile(partPath(index), assemblingPath)
|
|
967
|
+
completedBytes = file.size
|
|
968
|
+
completed = true
|
|
969
|
+
break
|
|
970
|
+
}
|
|
971
|
+
if (result.statusCode !== 206) throw new Error(`HTTP ${result.statusCode}; expected 206`)
|
|
972
|
+
if (result.bytesWritten !== expectedLength) {
|
|
973
|
+
throw new Error(`Segment ${index} truncated: ${result.bytesWritten}/${expectedLength} bytes`)
|
|
974
|
+
}
|
|
975
|
+
const contentRange = header("content-range")
|
|
976
|
+
if (contentRange !== `bytes ${start}-${end}/${file.size}`) {
|
|
977
|
+
throw new Error(`Invalid Content-Range: ${contentRange || "missing"}`)
|
|
978
|
+
}
|
|
979
|
+
const responseEtag = header("etag")
|
|
980
|
+
if (file.etag && responseEtag && responseEtag !== file.etag) {
|
|
981
|
+
throw new Error("Source generation changed during transfer")
|
|
982
|
+
}
|
|
983
|
+
galleryTransferLedger.markSegmentComplete(captureId, file.name, index)
|
|
984
|
+
completedBytes += expectedLength
|
|
985
|
+
completed = true
|
|
986
|
+
console.log("[ASG Camera API] Gallery segment complete", {
|
|
987
|
+
captureId,
|
|
988
|
+
file: file.name,
|
|
989
|
+
segment: index,
|
|
990
|
+
segmentCount,
|
|
991
|
+
attempt: attempt + 1,
|
|
992
|
+
status: result.statusCode,
|
|
993
|
+
bytes: result.bytesWritten,
|
|
994
|
+
durationMs: attemptDurationMs,
|
|
995
|
+
throughputMbps: (((result.bytesWritten || 0) * 8) / attemptDurationMs / 1000).toFixed(2),
|
|
996
|
+
etag: file.etag,
|
|
997
|
+
})
|
|
998
|
+
} catch (error) {
|
|
999
|
+
lastError = error
|
|
1000
|
+
if (error instanceof Error && error.message === "Sync cancelled") throw error
|
|
1001
|
+
console.warn("[ASG Camera API] Gallery segment attempt failed", {
|
|
1002
|
+
captureId,
|
|
1003
|
+
file: file.name,
|
|
1004
|
+
segment: index,
|
|
1005
|
+
segmentCount,
|
|
1006
|
+
attempt: attempt + 1,
|
|
1007
|
+
rangeStart: start,
|
|
1008
|
+
rangeEnd: end,
|
|
1009
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1010
|
+
})
|
|
1011
|
+
await RNFS.unlink(partPath(index)).catch(() => {})
|
|
1012
|
+
if (attempt < 4) {
|
|
1013
|
+
const delay = Math.min(1000 * 2 ** attempt, 10000) + Math.floor(Math.random() * 500)
|
|
1014
|
+
await new Promise<void>((resolve) => BgTimer.setTimeout(resolve, delay))
|
|
1015
|
+
}
|
|
1016
|
+
} finally {
|
|
1017
|
+
if (abortPollTimer !== undefined) BgTimer.clearInterval(abortPollTimer)
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (!completed) throw lastError || new Error(`Segment ${index} failed`)
|
|
1021
|
+
if (completedBytes === file.size && (await RNFS.exists(assemblingPath))) break
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (!(await RNFS.exists(assemblingPath))) {
|
|
1025
|
+
await RNFS.unlink(assemblingPath).catch(() => {})
|
|
1026
|
+
await RNFS.writeFile(assemblingPath, "", "utf8")
|
|
1027
|
+
for (let index = 0; index < segmentCount; index++) {
|
|
1028
|
+
const data = await RNFS.readFile(partPath(index), "base64")
|
|
1029
|
+
await RNFS.appendFile(assemblingPath, data, "base64")
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const assembledStat = await RNFS.stat(assemblingPath)
|
|
1034
|
+
if (assembledStat.size !== file.size) {
|
|
1035
|
+
await RNFS.moveFile(assemblingPath, `${assemblingPath}.invalid.${Date.now()}`).catch(() => {})
|
|
1036
|
+
throw new Error(`Assembled file has ${assembledStat.size}/${file.size} bytes`)
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const verificationStartedAt = Date.now()
|
|
1040
|
+
const capabilities = await this.getGalleryCapabilities()
|
|
1041
|
+
if (capabilities?.sha256) {
|
|
1042
|
+
const remoteHash = file.sha256 ? {sha256: file.sha256, etag: file.etag} : await this.getV3FileHash(file.name)
|
|
1043
|
+
if (file.etag && remoteHash.etag && file.etag !== remoteHash.etag) {
|
|
1044
|
+
throw new Error("Source generation changed before integrity verification")
|
|
1045
|
+
}
|
|
1046
|
+
const localHash = await RNFS.hash(assemblingPath, "sha256")
|
|
1047
|
+
if (localHash.toLowerCase() !== remoteHash.sha256.toLowerCase()) {
|
|
1048
|
+
await RNFS.moveFile(assemblingPath, `${assemblingPath}.invalid.${Date.now()}`).catch(() => {})
|
|
1049
|
+
throw new Error(`SHA-256 mismatch for ${file.name}`)
|
|
1050
|
+
}
|
|
1051
|
+
galleryTransferLedger.markFileHash(captureId, file.name, localHash.toLowerCase())
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
await RNFS.moveFile(assemblingPath, localFilePath)
|
|
1055
|
+
for (let index = 0; index < segmentCount; index++) await RNFS.unlink(partPath(index)).catch(() => {})
|
|
1056
|
+
console.log("[ASG Camera API] Gallery file committed", {
|
|
1057
|
+
captureId,
|
|
1058
|
+
file: file.name,
|
|
1059
|
+
bytes: file.size,
|
|
1060
|
+
segments: segmentCount,
|
|
1061
|
+
networkDurationMs,
|
|
1062
|
+
networkMbps: networkDurationMs > 0 ? ((networkBytes * 8) / networkDurationMs / 1000).toFixed(2) : "0.00",
|
|
1063
|
+
verificationDurationMs: Date.now() - verificationStartedAt,
|
|
1064
|
+
totalDurationMs: Date.now() - transferStartedAt,
|
|
1065
|
+
resumedSegments: resumedSegmentCount,
|
|
1066
|
+
})
|
|
1067
|
+
onProgress?.(file.size)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async downloadCapture(
|
|
1071
|
+
capture: CaptureGroup,
|
|
1072
|
+
onProgress?: (bytesDownloaded: number, totalBytes: number) => void,
|
|
1073
|
+
abortSignal?: AbortSignal,
|
|
1074
|
+
): Promise<{captureDir: string; primaryPath: string; bracketPaths: string[]; sidecarPath?: string}> {
|
|
1075
|
+
if (capture.files.some((file) => !file.size || file.size <= 0)) {
|
|
1076
|
+
// Preserve support for malformed manifests from pre-v2 development firmware.
|
|
1077
|
+
return this.downloadCaptureLegacyUnsafe(capture, onProgress, abortSignal)
|
|
1078
|
+
}
|
|
1079
|
+
const captureDir = localStorageService.getPhotoFilePath(capture.capture_id)
|
|
1080
|
+
if (!(await RNFS.exists(captureDir))) await RNFS.mkdir(captureDir)
|
|
1081
|
+
const capabilities = await this.getGalleryCapabilities()
|
|
1082
|
+
galleryTransferLedger.ensureCapture(capture, capabilities?.api_version || 2)
|
|
1083
|
+
galleryTransferLedger.transition(capture.capture_id, "TRANSFERRING")
|
|
1084
|
+
|
|
1085
|
+
const sortedFiles = [...capture.files].sort((a, b) => {
|
|
1086
|
+
const order = {bracket: 0, primary: 1, sidecar: 2}
|
|
1087
|
+
return (order[a.role] ?? 1) - (order[b.role] ?? 1)
|
|
1088
|
+
})
|
|
1089
|
+
let completedCaptureBytes = 0
|
|
1090
|
+
let primaryPath = ""
|
|
1091
|
+
const bracketPaths: string[] = []
|
|
1092
|
+
let sidecarPath: string | undefined
|
|
1093
|
+
|
|
1094
|
+
try {
|
|
1095
|
+
for (const file of sortedFiles) {
|
|
1096
|
+
const leafName = file.name.includes("/") ? file.name.split("/").pop()! : file.name
|
|
1097
|
+
const localFilePath = `${captureDir}/${leafName}`
|
|
1098
|
+
await this.downloadResumableCaptureFile(
|
|
1099
|
+
capture.capture_id,
|
|
1100
|
+
file,
|
|
1101
|
+
localFilePath,
|
|
1102
|
+
(fileBytes) => onProgress?.(completedCaptureBytes + fileBytes, capture.total_size),
|
|
1103
|
+
abortSignal,
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
const isVideo = !!file.name.match(/\.(mp4|mov|avi|webm|mkv)$/i)
|
|
1107
|
+
const mediaKind = file.role === "sidecar" ? "unknown" : isVideo ? "video" : "photo"
|
|
1108
|
+
try {
|
|
1109
|
+
await validateDownloadedMediaFile({
|
|
1110
|
+
path: localFilePath,
|
|
1111
|
+
name: file.name,
|
|
1112
|
+
expectedSize: file.size,
|
|
1113
|
+
mediaKind,
|
|
1114
|
+
})
|
|
1115
|
+
} catch (validationError: any) {
|
|
1116
|
+
reportInvalidGalleryMedia({
|
|
1117
|
+
name: file.name,
|
|
1118
|
+
path: localFilePath,
|
|
1119
|
+
mediaKind,
|
|
1120
|
+
stage: "download_capture_validation",
|
|
1121
|
+
reason: validationError?.message || String(validationError),
|
|
1122
|
+
expectedSize: file.size,
|
|
1123
|
+
captureId: capture.capture_id,
|
|
1124
|
+
duration: capture.duration,
|
|
1125
|
+
})
|
|
1126
|
+
await RNFS.moveFile(localFilePath, `${localFilePath}.invalid.${Date.now()}`).catch(() => {})
|
|
1127
|
+
throw validationError
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
completedCaptureBytes += file.size
|
|
1131
|
+
if (file.role === "primary") primaryPath = localFilePath
|
|
1132
|
+
else if (file.role === "bracket") bracketPaths.push(localFilePath)
|
|
1133
|
+
else if (file.role === "sidecar") sidecarPath = localFilePath
|
|
1134
|
+
}
|
|
1135
|
+
if (!primaryPath && sortedFiles.length > 0) {
|
|
1136
|
+
const leafName = sortedFiles[0].name.includes("/") ? sortedFiles[0].name.split("/").pop()! : sortedFiles[0].name
|
|
1137
|
+
primaryPath = `${captureDir}/${leafName}`
|
|
1138
|
+
}
|
|
1139
|
+
galleryTransferLedger.transition(capture.capture_id, "VERIFIED")
|
|
1140
|
+
onProgress?.(capture.total_size, capture.total_size)
|
|
1141
|
+
return {captureDir, primaryPath, bracketPaths, sidecarPath}
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
galleryTransferLedger.recordFailure(capture.capture_id, error)
|
|
1144
|
+
throw error
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/** Previous direct-to-final implementation kept for binary-source compatibility only. */
|
|
1149
|
+
private async downloadCaptureLegacyUnsafe(
|
|
1150
|
+
capture: CaptureGroup,
|
|
1151
|
+
onProgress?: (bytesDownloaded: number, totalBytes: number) => void,
|
|
1152
|
+
abortSignal?: AbortSignal,
|
|
1153
|
+
): Promise<{captureDir: string; primaryPath: string; bracketPaths: string[]; sidecarPath?: string}> {
|
|
1154
|
+
const captureDir = localStorageService.getPhotoFilePath(capture.capture_id)
|
|
1155
|
+
console.log(
|
|
1156
|
+
`[ASG Camera API] downloadCapture: ${capture.capture_id} (${capture.files.length} files) -> ${captureDir}`,
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
// Ensure capture directory exists
|
|
1160
|
+
const dirExists = await RNFS.exists(captureDir)
|
|
1161
|
+
if (!dirExists) {
|
|
1162
|
+
await RNFS.mkdir(captureDir)
|
|
1163
|
+
console.log(`[ASG Camera API] downloadCapture: created dir ${captureDir}`)
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
let primaryPath = ""
|
|
1167
|
+
const bracketPaths: string[] = []
|
|
1168
|
+
let sidecarPath: string | undefined
|
|
1169
|
+
|
|
1170
|
+
let totalBytesDownloaded = 0
|
|
1171
|
+
const totalBytes = capture.total_size
|
|
1172
|
+
|
|
1173
|
+
// Sort files: brackets first, then primary, then sidecar
|
|
1174
|
+
// This ensures brackets are available before merge runs on the primary
|
|
1175
|
+
const sortedFiles = [...capture.files].sort((a, b) => {
|
|
1176
|
+
const order = {bracket: 0, primary: 1, sidecar: 2}
|
|
1177
|
+
return (order[a.role] ?? 1) - (order[b.role] ?? 1)
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
for (const file of sortedFiles) {
|
|
1181
|
+
// Derive local filename: use leaf of path if folder-based, otherwise full name
|
|
1182
|
+
const leafName = file.name.includes("/") ? file.name.split("/").pop()! : file.name
|
|
1183
|
+
const localFilePath = `${captureDir}/${leafName}`
|
|
1184
|
+
|
|
1185
|
+
const isVideo = file.name.match(/\.(mp4|mov|avi|webm|mkv)$/i)
|
|
1186
|
+
const downloadEndpoint = isVideo ? "download" : "photo"
|
|
1187
|
+
const downloadUrl = `${this.baseUrl}/api/${downloadEndpoint}?file=${encodeURIComponent(file.name)}`
|
|
1188
|
+
|
|
1189
|
+
console.log(`[ASG Camera API] downloadCapture: downloading ${file.name} (${file.role}) -> ${localFilePath}`)
|
|
1190
|
+
|
|
1191
|
+
try {
|
|
1192
|
+
// Throttle progress: only fire when bytesWritten changes meaningfully
|
|
1193
|
+
let lastReportedBytes = -1
|
|
1194
|
+
const {jobId, promise: dlPromise} = localNetworkTransport.downloadFile({
|
|
1195
|
+
fromUrl: downloadUrl,
|
|
1196
|
+
toFile: localFilePath,
|
|
1197
|
+
headers: {
|
|
1198
|
+
"Accept": "*/*",
|
|
1199
|
+
"User-Agent": "MentraOS-Mobile/1.0",
|
|
1200
|
+
},
|
|
1201
|
+
connectionTimeout: 300000,
|
|
1202
|
+
readTimeout: 300000,
|
|
1203
|
+
backgroundTimeout: 600000,
|
|
1204
|
+
progressDivider: 5,
|
|
1205
|
+
progressInterval: 250,
|
|
1206
|
+
progress: (res: {bytesWritten: number}) => {
|
|
1207
|
+
const currentBytes = totalBytesDownloaded + (res.bytesWritten || 0)
|
|
1208
|
+
if (onProgress && currentBytes !== lastReportedBytes) {
|
|
1209
|
+
lastReportedBytes = currentBytes
|
|
1210
|
+
onProgress(currentBytes, totalBytes)
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
})
|
|
1214
|
+
|
|
1215
|
+
// Wire up abort signal via polling (safe for all Hermes versions)
|
|
1216
|
+
let abortPollTimer: number | undefined
|
|
1217
|
+
if (abortSignal) {
|
|
1218
|
+
if (abortSignal.aborted) {
|
|
1219
|
+
localNetworkTransport.stopDownload(jobId)
|
|
1220
|
+
throw new Error("Sync cancelled")
|
|
1221
|
+
}
|
|
1222
|
+
abortPollTimer = BgTimer.setInterval(() => {
|
|
1223
|
+
if (abortSignal.aborted) {
|
|
1224
|
+
localNetworkTransport.stopDownload(jobId)
|
|
1225
|
+
}
|
|
1226
|
+
}, 500)
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
let downloadResult
|
|
1230
|
+
try {
|
|
1231
|
+
downloadResult = await dlPromise
|
|
1232
|
+
} finally {
|
|
1233
|
+
if (abortPollTimer !== undefined) BgTimer.clearInterval(abortPollTimer)
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (downloadResult.statusCode !== 200) {
|
|
1237
|
+
await RNFS.unlink(localFilePath).catch(() => {})
|
|
1238
|
+
throw new Error(`HTTP ${downloadResult.statusCode}`)
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// Check if aborted after completion
|
|
1242
|
+
if (abortSignal?.aborted) {
|
|
1243
|
+
await RNFS.unlink(localFilePath).catch(() => {})
|
|
1244
|
+
throw new Error("Sync cancelled")
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const mediaKind = file.role === "sidecar" ? "unknown" : isVideo ? "video" : "photo"
|
|
1248
|
+
try {
|
|
1249
|
+
await validateDownloadedMediaFile({
|
|
1250
|
+
path: localFilePath,
|
|
1251
|
+
name: file.name,
|
|
1252
|
+
expectedSize: file.size,
|
|
1253
|
+
mediaKind,
|
|
1254
|
+
})
|
|
1255
|
+
} catch (validationErr: any) {
|
|
1256
|
+
const reason = validationErr?.message || validationErr?.toString?.() || JSON.stringify(validationErr)
|
|
1257
|
+
reportInvalidGalleryMedia({
|
|
1258
|
+
name: file.name,
|
|
1259
|
+
path: localFilePath,
|
|
1260
|
+
mediaKind,
|
|
1261
|
+
stage: "download_capture_validation",
|
|
1262
|
+
reason,
|
|
1263
|
+
expectedSize: file.size,
|
|
1264
|
+
captureId: capture.capture_id,
|
|
1265
|
+
duration: capture.duration,
|
|
1266
|
+
})
|
|
1267
|
+
await RNFS.unlink(localFilePath).catch(() => {})
|
|
1268
|
+
throw validationErr
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
console.log(`[ASG Camera API] downloadCapture: completed ${file.name} (${file.size} bytes)`)
|
|
1272
|
+
} catch (dlErr: any) {
|
|
1273
|
+
// S2: Clean up partial file on failure
|
|
1274
|
+
await RNFS.unlink(localFilePath).catch(() => {})
|
|
1275
|
+
const errMsg = dlErr?.message || dlErr?.toString?.() || JSON.stringify(dlErr)
|
|
1276
|
+
console.error(`[ASG Camera API] downloadCapture: FAILED ${file.name}: ${errMsg}`)
|
|
1277
|
+
throw new Error(`Failed to download ${file.name}: ${errMsg}`)
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
totalBytesDownloaded += file.size
|
|
1281
|
+
|
|
1282
|
+
if (file.role === "primary") {
|
|
1283
|
+
primaryPath = localFilePath
|
|
1284
|
+
} else if (file.role === "bracket") {
|
|
1285
|
+
bracketPaths.push(localFilePath)
|
|
1286
|
+
} else if (file.role === "sidecar") {
|
|
1287
|
+
sidecarPath = localFilePath
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// C3: If no file was marked as primary, fall back to first downloaded file
|
|
1292
|
+
if (primaryPath === "" && sortedFiles.length > 0) {
|
|
1293
|
+
const leafName = sortedFiles[0].name.includes("/") ? sortedFiles[0].name.split("/").pop()! : sortedFiles[0].name
|
|
1294
|
+
primaryPath = `${captureDir}/${leafName}`
|
|
1295
|
+
console.warn(`[ASG Camera API] downloadCapture: No primary file found, falling back to ${primaryPath}`)
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Report final progress
|
|
1299
|
+
if (onProgress) {
|
|
1300
|
+
onProgress(totalBytes, totalBytes)
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
return {captureDir, primaryPath, bracketPaths, sidecarPath}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Delete files from server
|
|
1308
|
+
*/
|
|
1309
|
+
async deleteFilesFromServer(fileNames: string[]): Promise<{
|
|
1310
|
+
deleted: string[]
|
|
1311
|
+
failed: string[]
|
|
1312
|
+
}> {
|
|
1313
|
+
if (fileNames.length === 0) {
|
|
1314
|
+
return {deleted: [], failed: []}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
try {
|
|
1318
|
+
const response = await this.makeRequest(
|
|
1319
|
+
"/api/delete-files",
|
|
1320
|
+
{
|
|
1321
|
+
method: "POST",
|
|
1322
|
+
body: JSON.stringify({files: fileNames}),
|
|
1323
|
+
},
|
|
1324
|
+
5,
|
|
1325
|
+
0,
|
|
1326
|
+
)
|
|
1327
|
+
|
|
1328
|
+
// Parse the response format from the ASG server
|
|
1329
|
+
const responseData = response as any
|
|
1330
|
+
if (responseData.data && responseData.data.results) {
|
|
1331
|
+
const deleted: string[] = []
|
|
1332
|
+
const failed: string[] = []
|
|
1333
|
+
|
|
1334
|
+
for (const result of responseData.data.results) {
|
|
1335
|
+
if (result.success) {
|
|
1336
|
+
deleted.push(result.file)
|
|
1337
|
+
} else {
|
|
1338
|
+
failed.push(result.file)
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
console.log(`[ASG Camera API] Delete results: ${deleted.length} deleted, ${failed.length} failed`)
|
|
1343
|
+
return {deleted, failed}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
return response as {
|
|
1347
|
+
deleted: string[]
|
|
1348
|
+
failed: string[]
|
|
1349
|
+
}
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
console.error("Failed to delete files from server:", error)
|
|
1352
|
+
return {deleted: [], failed: fileNames}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Get sync status from server
|
|
1358
|
+
*/
|
|
1359
|
+
async getSyncStatus(): Promise<{
|
|
1360
|
+
total_files: number
|
|
1361
|
+
total_size: number
|
|
1362
|
+
last_modified: number
|
|
1363
|
+
}> {
|
|
1364
|
+
const response = await this.makeRequest("/sync/status", {
|
|
1365
|
+
method: "GET",
|
|
1366
|
+
})
|
|
1367
|
+
|
|
1368
|
+
return response as {
|
|
1369
|
+
total_files: number
|
|
1370
|
+
total_size: number
|
|
1371
|
+
last_modified: number
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* Download a file from the server and save to filesystem
|
|
1377
|
+
*/
|
|
1378
|
+
async downloadFile(
|
|
1379
|
+
filename: string,
|
|
1380
|
+
includeThumbnail: boolean = false,
|
|
1381
|
+
onProgress?: (progress: number) => void,
|
|
1382
|
+
abortSignal?: AbortSignal,
|
|
1383
|
+
expectedSize?: number,
|
|
1384
|
+
): Promise<{filePath: string; thumbnailPath?: string; mime_type: string}> {
|
|
1385
|
+
// The legacy public shape did not require a size. Keep that rare call compatible while all
|
|
1386
|
+
// sync manifests use the verified resumable path below.
|
|
1387
|
+
if (!expectedSize || expectedSize <= 0) {
|
|
1388
|
+
return this.downloadFileLegacyUnsafe(filename, includeThumbnail, onProgress, abortSignal, expectedSize)
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const localFilePath = localStorageService.getPhotoFilePath(filename)
|
|
1392
|
+
const parentDir = localFilePath.substring(0, localFilePath.lastIndexOf("/"))
|
|
1393
|
+
if (!(await RNFS.exists(parentDir))) await RNFS.mkdir(parentDir)
|
|
1394
|
+
const lowerName = filename.toLowerCase()
|
|
1395
|
+
const isVideo = /\.(mp4|mov|avi|webm|mkv)$/.test(lowerName)
|
|
1396
|
+
const captureId = filename.includes("/")
|
|
1397
|
+
? filename.substring(0, filename.indexOf("/"))
|
|
1398
|
+
: filename.replace(/\.imu\.json$/i, "").replace(/\.[^.]+$/, "")
|
|
1399
|
+
const capture: CaptureGroup = {
|
|
1400
|
+
capture_id: captureId,
|
|
1401
|
+
type: isVideo ? "video" : "photo",
|
|
1402
|
+
timestamp: Date.now(),
|
|
1403
|
+
total_size: expectedSize,
|
|
1404
|
+
files: [{name: filename, size: expectedSize, role: "primary"}],
|
|
1405
|
+
}
|
|
1406
|
+
const capabilities = await this.getGalleryCapabilities()
|
|
1407
|
+
galleryTransferLedger.ensureCapture(capture, capabilities?.api_version || 1)
|
|
1408
|
+
galleryTransferLedger.transition(captureId, "TRANSFERRING")
|
|
1409
|
+
try {
|
|
1410
|
+
await this.downloadResumableCaptureFile(
|
|
1411
|
+
captureId,
|
|
1412
|
+
capture.files[0],
|
|
1413
|
+
localFilePath,
|
|
1414
|
+
(bytes) => onProgress?.(Math.min(100, Math.round((bytes / expectedSize) * 100))),
|
|
1415
|
+
abortSignal,
|
|
1416
|
+
)
|
|
1417
|
+
await validateDownloadedMediaFile({
|
|
1418
|
+
path: localFilePath,
|
|
1419
|
+
name: filename,
|
|
1420
|
+
expectedSize,
|
|
1421
|
+
mediaKind: isVideo ? "video" : "photo",
|
|
1422
|
+
})
|
|
1423
|
+
galleryTransferLedger.transition(captureId, "VERIFIED")
|
|
1424
|
+
return {
|
|
1425
|
+
filePath: localFilePath,
|
|
1426
|
+
mime_type: isVideo
|
|
1427
|
+
? "video/mp4"
|
|
1428
|
+
: lowerName.endsWith(".png")
|
|
1429
|
+
? "image/png"
|
|
1430
|
+
: lowerName.endsWith(".avif")
|
|
1431
|
+
? "image/avif"
|
|
1432
|
+
: "image/jpeg",
|
|
1433
|
+
}
|
|
1434
|
+
} catch (error) {
|
|
1435
|
+
galleryTransferLedger.recordFailure(captureId, error)
|
|
1436
|
+
throw error
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
private async downloadFileLegacyUnsafe(
|
|
1441
|
+
filename: string,
|
|
1442
|
+
includeThumbnail: boolean = false,
|
|
1443
|
+
onProgress?: (progress: number) => void,
|
|
1444
|
+
abortSignal?: AbortSignal,
|
|
1445
|
+
expectedSize?: number,
|
|
1446
|
+
): Promise<{
|
|
1447
|
+
filePath: string
|
|
1448
|
+
thumbnailPath?: string
|
|
1449
|
+
mime_type: string
|
|
1450
|
+
}> {
|
|
1451
|
+
console.log(`[ASG Camera API] Downloading file: ${filename}`)
|
|
1452
|
+
|
|
1453
|
+
try {
|
|
1454
|
+
// Get the local file path where we'll save this
|
|
1455
|
+
const localFilePath = localStorageService.getPhotoFilePath(filename)
|
|
1456
|
+
const localThumbnailPath = includeThumbnail ? localStorageService.getThumbnailFilePath(filename) : undefined
|
|
1457
|
+
|
|
1458
|
+
// Ensure parent directory exists (for folder-based capture paths like IMG_xxx/base.jpg)
|
|
1459
|
+
if (filename.includes("/")) {
|
|
1460
|
+
const parentDir = localFilePath.substring(0, localFilePath.lastIndexOf("/"))
|
|
1461
|
+
const parentExists = await RNFS.exists(parentDir)
|
|
1462
|
+
if (!parentExists) {
|
|
1463
|
+
await RNFS.mkdir(parentDir)
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// Determine if this is a video file based on extension
|
|
1468
|
+
const isVideo = filename.match(/\.(mp4|mov|avi|webm|mkv)$/i)
|
|
1469
|
+
|
|
1470
|
+
// Use /api/download for videos (full file) and /api/photo for images
|
|
1471
|
+
const downloadEndpoint = isVideo ? "download" : "photo"
|
|
1472
|
+
const downloadUrl = `${this.baseUrl}/api/${downloadEndpoint}?file=${encodeURIComponent(filename)}`
|
|
1473
|
+
|
|
1474
|
+
// Download the file directly to filesystem
|
|
1475
|
+
console.log(`[ASG Camera API] Downloading ${isVideo ? "video" : "photo"} from: ${downloadUrl}`)
|
|
1476
|
+
console.log(`[ASG Camera API] Saving to: ${localFilePath}`)
|
|
1477
|
+
|
|
1478
|
+
// Track content length from begin callback for post-download validation
|
|
1479
|
+
let expectedContentLength = 0
|
|
1480
|
+
// Throttle progress: only call onProgress when percentage actually changes
|
|
1481
|
+
let lastReportedProgress = -1
|
|
1482
|
+
|
|
1483
|
+
const {jobId, promise: downloadPromise} = localNetworkTransport.downloadFile({
|
|
1484
|
+
fromUrl: downloadUrl,
|
|
1485
|
+
toFile: localFilePath,
|
|
1486
|
+
headers: {
|
|
1487
|
+
"Accept": "*/*",
|
|
1488
|
+
"User-Agent": "MentraOS-Mobile/1.0",
|
|
1489
|
+
},
|
|
1490
|
+
connectionTimeout: 300000, // 5 minutes for connection establishment
|
|
1491
|
+
readTimeout: 300000, // 5 minutes for data reading
|
|
1492
|
+
backgroundTimeout: 600000, // 10 minutes for background downloads (iOS)
|
|
1493
|
+
progressDivider: 5, // Fire progress every 5% to reduce event frequency
|
|
1494
|
+
progressInterval: 250, // Update progress every 250ms max
|
|
1495
|
+
begin: (res) => {
|
|
1496
|
+
expectedContentLength = res.contentLength || 0
|
|
1497
|
+
console.log(`[ASG Camera API] Download started for ${filename}, size: ${res.contentLength}`)
|
|
1498
|
+
},
|
|
1499
|
+
progress: (res) => {
|
|
1500
|
+
// Validate progress data to prevent negative percentages
|
|
1501
|
+
const contentLength = res.contentLength || 0
|
|
1502
|
+
const bytesWritten = res.bytesWritten || 0
|
|
1503
|
+
|
|
1504
|
+
let percentage = 0
|
|
1505
|
+
if (contentLength > 0 && bytesWritten >= 0) {
|
|
1506
|
+
percentage = Math.round((bytesWritten / contentLength) * 100)
|
|
1507
|
+
// Clamp percentage between 0 and 100
|
|
1508
|
+
percentage = Math.max(0, Math.min(100, percentage))
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// Only call onProgress when percentage actually changes (throttle)
|
|
1512
|
+
if (onProgress && percentage !== lastReportedProgress) {
|
|
1513
|
+
lastReportedProgress = percentage
|
|
1514
|
+
onProgress(percentage)
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Log every 10%
|
|
1518
|
+
if (percentage % 10 === 0) {
|
|
1519
|
+
console.log(`[ASG Camera API] Download progress ${filename}: ${percentage}%`)
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1522
|
+
})
|
|
1523
|
+
|
|
1524
|
+
// Wire up abort signal to stop download via polling (safe for all Hermes versions)
|
|
1525
|
+
let abortPollTimer: number | undefined
|
|
1526
|
+
if (abortSignal) {
|
|
1527
|
+
if (abortSignal.aborted) {
|
|
1528
|
+
localNetworkTransport.stopDownload(jobId)
|
|
1529
|
+
throw new Error("Sync cancelled")
|
|
1530
|
+
}
|
|
1531
|
+
abortPollTimer = BgTimer.setInterval(() => {
|
|
1532
|
+
if (abortSignal.aborted) {
|
|
1533
|
+
localNetworkTransport.stopDownload(jobId)
|
|
1534
|
+
}
|
|
1535
|
+
}, 500)
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
let downloadResult
|
|
1539
|
+
try {
|
|
1540
|
+
downloadResult = await downloadPromise
|
|
1541
|
+
} finally {
|
|
1542
|
+
if (abortPollTimer !== undefined) BgTimer.clearInterval(abortPollTimer)
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
if (downloadResult.statusCode !== 200) {
|
|
1546
|
+
// S2: Clean up partial file on HTTP error
|
|
1547
|
+
await RNFS.unlink(localFilePath).catch(() => {})
|
|
1548
|
+
throw new Error(`Failed to download ${filename}: HTTP ${downloadResult.statusCode}`)
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// Check if download was aborted after completion
|
|
1552
|
+
if (abortSignal?.aborted) {
|
|
1553
|
+
await RNFS.unlink(localFilePath).catch(() => {})
|
|
1554
|
+
throw new Error("Sync cancelled")
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// N1: Validate file size after download.
|
|
1558
|
+
// Use Content-Length from the HTTP response if available, otherwise fall back
|
|
1559
|
+
// to expectedSize from the sync response metadata. This catches truncated
|
|
1560
|
+
// downloads even when the server uses chunked transfer encoding (no Content-Length).
|
|
1561
|
+
const sizeToCheck =
|
|
1562
|
+
expectedContentLength > 0 ? expectedContentLength : expectedSize && expectedSize > 0 ? expectedSize : 0
|
|
1563
|
+
|
|
1564
|
+
try {
|
|
1565
|
+
await validateDownloadedMediaFile({
|
|
1566
|
+
path: localFilePath,
|
|
1567
|
+
name: filename,
|
|
1568
|
+
expectedSize: sizeToCheck,
|
|
1569
|
+
mediaKind: isVideo ? "video" : "photo",
|
|
1570
|
+
})
|
|
1571
|
+
} catch (validationErr: any) {
|
|
1572
|
+
const reason = validationErr?.message || validationErr?.toString?.() || JSON.stringify(validationErr)
|
|
1573
|
+
reportInvalidGalleryMedia({
|
|
1574
|
+
name: filename,
|
|
1575
|
+
path: localFilePath,
|
|
1576
|
+
mediaKind: isVideo ? "video" : "photo",
|
|
1577
|
+
stage: "download_validation",
|
|
1578
|
+
reason,
|
|
1579
|
+
expectedSize: sizeToCheck,
|
|
1580
|
+
})
|
|
1581
|
+
await RNFS.unlink(localFilePath).catch(() => {})
|
|
1582
|
+
throw validationErr
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
console.log(`[ASG Camera API] Successfully downloaded ${filename} to filesystem`)
|
|
1586
|
+
|
|
1587
|
+
// Detect MIME type by checking file signature
|
|
1588
|
+
let mimeType = "application/octet-stream"
|
|
1589
|
+
try {
|
|
1590
|
+
// Read first 20 bytes to check file signature
|
|
1591
|
+
const firstBytes = await RNFS.read(localFilePath, 20, 0, "base64")
|
|
1592
|
+
const decodedBytes = atob(firstBytes)
|
|
1593
|
+
|
|
1594
|
+
// Check for AVIF signature
|
|
1595
|
+
if (decodedBytes.length > 11) {
|
|
1596
|
+
const ftypSignature = decodedBytes.substring(4, 12)
|
|
1597
|
+
if (ftypSignature === "ftypavif") {
|
|
1598
|
+
mimeType = "image/avif"
|
|
1599
|
+
console.log(`[ASG Camera API] Detected AVIF file: ${filename}`)
|
|
1600
|
+
} else if (decodedBytes.substring(0, 2) === "\xFF\xD8") {
|
|
1601
|
+
mimeType = "image/jpeg"
|
|
1602
|
+
} else if (decodedBytes.substring(0, 8) === "\x89PNG\r\n\x1a\n") {
|
|
1603
|
+
mimeType = "image/png"
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// Also check by extension
|
|
1608
|
+
if (mimeType === "application/octet-stream") {
|
|
1609
|
+
if (filename.toLowerCase().endsWith(".jpg") || filename.toLowerCase().endsWith(".jpeg")) {
|
|
1610
|
+
mimeType = "image/jpeg"
|
|
1611
|
+
} else if (filename.toLowerCase().endsWith(".png")) {
|
|
1612
|
+
mimeType = "image/png"
|
|
1613
|
+
} else if (filename.toLowerCase().endsWith(".mp4")) {
|
|
1614
|
+
mimeType = "video/mp4"
|
|
1615
|
+
} else if (!filename.includes(".")) {
|
|
1616
|
+
// Files without extension are likely AVIF
|
|
1617
|
+
mimeType = "image/avif"
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
} catch (e) {
|
|
1621
|
+
console.warn(`[ASG Camera API] Could not detect MIME type for ${filename}:`, e)
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Download thumbnail if requested and it's a video
|
|
1625
|
+
let thumbnailPath: string | undefined
|
|
1626
|
+
if (includeThumbnail && filename.toLowerCase().match(/\.(mp4|mov|avi|mkv|webm)$/)) {
|
|
1627
|
+
try {
|
|
1628
|
+
console.log(`[ASG Camera API] Downloading thumbnail for ${filename}`)
|
|
1629
|
+
console.log(`[ASG Camera API] Using /api/photo endpoint for video thumbnail`)
|
|
1630
|
+
|
|
1631
|
+
// The server's /api/photo endpoint serves thumbnails for video files
|
|
1632
|
+
// It detects video files and automatically generates/serves thumbnails instead of the full video
|
|
1633
|
+
const thumbResult = await localNetworkTransport.downloadFile({
|
|
1634
|
+
fromUrl: `${this.baseUrl}/api/photo?file=${encodeURIComponent(filename)}`,
|
|
1635
|
+
toFile: localThumbnailPath as string,
|
|
1636
|
+
headers: {
|
|
1637
|
+
"Accept": "image/*",
|
|
1638
|
+
"User-Agent": "MentraOS-Mobile/1.0",
|
|
1639
|
+
},
|
|
1640
|
+
connectionTimeout: 60000, // 1 minute for thumbnails (smaller files)
|
|
1641
|
+
readTimeout: 60000, // 1 minute for thumbnails
|
|
1642
|
+
progressDivider: 1, // Get all progress updates for thumbnails too
|
|
1643
|
+
begin: (res) => {
|
|
1644
|
+
console.log(`[ASG Camera API] Thumbnail download started for ${filename}, size: ${res.contentLength}`)
|
|
1645
|
+
},
|
|
1646
|
+
progress: (res) => {
|
|
1647
|
+
const percentage = Math.round((res.bytesWritten / res.contentLength) * 100)
|
|
1648
|
+
if (percentage % 25 === 0) {
|
|
1649
|
+
console.log(`[ASG Camera API] Thumbnail download progress ${filename}: ${percentage}%`)
|
|
1650
|
+
}
|
|
1651
|
+
},
|
|
1652
|
+
}).promise
|
|
1653
|
+
|
|
1654
|
+
console.log(
|
|
1655
|
+
`[ASG Camera API] Thumbnail download result for ${filename}: status=${thumbResult.statusCode}, bytesWritten=${thumbResult.bytesWritten}`,
|
|
1656
|
+
)
|
|
1657
|
+
|
|
1658
|
+
if (thumbResult.statusCode === 200) {
|
|
1659
|
+
thumbnailPath = localThumbnailPath as string
|
|
1660
|
+
console.log(`[ASG Camera API] Successfully downloaded thumbnail to: ${thumbnailPath}`)
|
|
1661
|
+
|
|
1662
|
+
// Verify the file exists
|
|
1663
|
+
const exists = await RNFS.exists(thumbnailPath)
|
|
1664
|
+
console.log(`[ASG Camera API] Thumbnail file exists: ${exists}`)
|
|
1665
|
+
} else {
|
|
1666
|
+
console.warn(`[ASG Camera API] Thumbnail download failed with status: ${thumbResult.statusCode}`)
|
|
1667
|
+
}
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
console.warn(`[ASG Camera API] Failed to download thumbnail for ${filename}:`, error)
|
|
1670
|
+
}
|
|
1671
|
+
} else {
|
|
1672
|
+
console.log(
|
|
1673
|
+
`[ASG Camera API] Skipping thumbnail download - includeThumbnail: ${includeThumbnail}, filename: ${filename}, is video extension: ${
|
|
1674
|
+
filename.toLowerCase().match(/\.(mp4|mov|avi|mkv|webm)$/) ? "yes" : "no"
|
|
1675
|
+
}`,
|
|
1676
|
+
)
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
return {
|
|
1680
|
+
filePath: localFilePath,
|
|
1681
|
+
thumbnailPath: thumbnailPath,
|
|
1682
|
+
mime_type: mimeType,
|
|
1683
|
+
}
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
console.error(`[ASG Camera API] Error downloading file ${filename}:`, error)
|
|
1686
|
+
// S2: Clean up partial file on any failure
|
|
1687
|
+
const localFilePath = localStorageService.getPhotoFilePath(filename)
|
|
1688
|
+
await RNFS.unlink(localFilePath).catch(() => {})
|
|
1689
|
+
throw error
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// Export a default instance - will be initialized with proper IP when used
|
|
1695
|
+
export const asgCameraApi = new AsgCameraApiClient()
|