@mentra/engine 0.1.0-dev.0 → 0.1.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -1
- package/build/engine.d.ts +11 -3
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +7 -2
- package/build/engine.js.map +1 -1
- package/build/facades/dev.d.ts +3 -2
- package/build/facades/dev.d.ts.map +1 -1
- package/build/facades/dev.js +18 -12
- package/build/facades/dev.js.map +1 -1
- package/build/facades/gallery.js +1 -1
- package/build/facades/gallery.js.map +1 -1
- package/build/facades/glasses.d.ts +1 -0
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +19 -4
- package/build/facades/glasses.js.map +1 -1
- package/build/facades/glassesSettings.d.ts.map +1 -1
- package/build/facades/glassesSettings.js +1 -0
- package/build/facades/glassesSettings.js.map +1 -1
- package/build/facades/miniapps.d.ts +4 -0
- package/build/facades/miniapps.d.ts.map +1 -1
- package/build/facades/miniapps.js +5 -0
- package/build/facades/miniapps.js.map +1 -1
- package/build/facades/ota.d.ts +11 -2
- package/build/facades/ota.d.ts.map +1 -1
- package/build/facades/ota.js +19 -4
- package/build/facades/ota.js.map +1 -1
- package/build/facades/pairing.d.ts +4 -0
- package/build/facades/pairing.d.ts.map +1 -1
- package/build/facades/pairing.js +10 -8
- package/build/facades/pairing.js.map +1 -1
- package/build/facades/permissions.d.ts +10 -0
- package/build/facades/permissions.d.ts.map +1 -1
- package/build/facades/permissions.js +10 -0
- package/build/facades/permissions.js.map +1 -1
- package/build/facades/phoneNotifications.d.ts +0 -4
- package/build/facades/phoneNotifications.d.ts.map +1 -1
- package/build/facades/phoneNotifications.js +4 -14
- package/build/facades/phoneNotifications.js.map +1 -1
- package/build/index.d.ts +6 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -2
- package/build/index.js.map +1 -1
- package/build/internal.d.ts +3 -2
- package/build/internal.d.ts.map +1 -1
- package/build/internal.js +2 -1
- package/build/internal.js.map +1 -1
- package/build/react/MentraLiveOtaFlow.d.ts +38 -0
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
- package/build/react/MentraLiveOtaFlow.js +528 -0
- package/build/react/MentraLiveOtaFlow.js.map +1 -0
- package/build/react/index.d.ts +2 -0
- package/build/react/index.d.ts.map +1 -0
- package/build/react/index.js +2 -0
- package/build/react/index.js.map +1 -0
- package/build/react/useEngineSnapshot.d.ts +2 -0
- package/build/react/useEngineSnapshot.d.ts.map +1 -0
- package/build/react/useEngineSnapshot.js +14 -0
- package/build/react/useEngineSnapshot.js.map +1 -0
- package/build/runtime/bootstrap.d.ts +24 -1
- package/build/runtime/bootstrap.d.ts.map +1 -1
- package/build/runtime/bootstrap.js +17 -0
- package/build/runtime/bootstrap.js.map +1 -1
- package/build/runtime/scanQrSeam.d.ts +7 -0
- package/build/runtime/scanQrSeam.d.ts.map +1 -0
- package/build/runtime/scanQrSeam.js +16 -0
- package/build/runtime/scanQrSeam.js.map +1 -0
- package/build/services/AppRegistry.d.ts +33 -57
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +193 -144
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/AudioCloudUplink.d.ts +12 -0
- package/build/services/AudioCloudUplink.d.ts.map +1 -1
- package/build/services/AudioCloudUplink.js +22 -1
- package/build/services/AudioCloudUplink.js.map +1 -1
- package/build/services/AudioPlaybackService.d.ts +41 -2
- package/build/services/AudioPlaybackService.d.ts.map +1 -1
- package/build/services/AudioPlaybackService.js +250 -33
- package/build/services/AudioPlaybackService.js.map +1 -1
- package/build/services/BlobStore.d.ts +2 -1
- package/build/services/BlobStore.d.ts.map +1 -1
- package/build/services/BlobStore.js +17 -9
- package/build/services/BlobStore.js.map +1 -1
- package/build/services/CloudAudioSubscriptionSync.d.ts.map +1 -1
- package/build/services/CloudAudioSubscriptionSync.js +13 -1
- package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
- package/build/services/CloudClientService.d.ts +15 -0
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/CloudClientService.js +93 -1
- package/build/services/CloudClientService.js.map +1 -1
- package/build/services/DeviceEventRouter.js +5 -5
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/ForegroundLocationPermission.d.ts +13 -0
- package/build/services/ForegroundLocationPermission.d.ts.map +1 -0
- package/build/services/ForegroundLocationPermission.js +12 -0
- package/build/services/ForegroundLocationPermission.js.map +1 -0
- package/build/services/GlassesSettingsSync.d.ts.map +1 -1
- package/build/services/GlassesSettingsSync.js +9 -3
- package/build/services/GlassesSettingsSync.js.map +1 -1
- package/build/services/GlassesStatusProjection.d.ts +2 -1
- package/build/services/GlassesStatusProjection.d.ts.map +1 -1
- package/build/services/GlassesStatusProjection.js +15 -8
- package/build/services/GlassesStatusProjection.js.map +1 -1
- package/build/services/HotspotOtaTransport.d.ts +29 -0
- package/build/services/HotspotOtaTransport.d.ts.map +1 -0
- package/build/services/HotspotOtaTransport.js +146 -0
- package/build/services/HotspotOtaTransport.js.map +1 -0
- package/build/services/HotspotShutdown.d.ts +17 -0
- package/build/services/HotspotShutdown.d.ts.map +1 -0
- package/build/services/HotspotShutdown.js +29 -0
- package/build/services/HotspotShutdown.js.map +1 -0
- package/build/services/LocalDisplayManager.d.ts +15 -0
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +106 -20
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +74 -2
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +785 -291
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/LocalMiniappStorage.d.ts +32 -0
- package/build/services/LocalMiniappStorage.d.ts.map +1 -0
- package/build/services/LocalMiniappStorage.js +66 -0
- package/build/services/LocalMiniappStorage.js.map +1 -0
- package/build/services/LocalMiniappUserIdentity.d.ts +20 -0
- package/build/services/LocalMiniappUserIdentity.d.ts.map +1 -0
- package/build/services/LocalMiniappUserIdentity.js +43 -0
- package/build/services/LocalMiniappUserIdentity.js.map +1 -0
- package/build/services/LocalSttFallbackCoordinator.d.ts +9 -6
- package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
- package/build/services/LocalSttFallbackCoordinator.js +18 -12
- package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
- package/build/services/MicStateCoordinator.d.ts +42 -1
- package/build/services/MicStateCoordinator.d.ts.map +1 -1
- package/build/services/MicStateCoordinator.js +141 -3
- package/build/services/MicStateCoordinator.js.map +1 -1
- package/build/services/MiniappLauncher.d.ts.map +1 -1
- package/build/services/MiniappLauncher.js +23 -3
- package/build/services/MiniappLauncher.js.map +1 -1
- package/build/services/MiniappLiveness.d.ts +12 -0
- package/build/services/MiniappLiveness.d.ts.map +1 -0
- package/build/services/MiniappLiveness.js +16 -0
- package/build/services/MiniappLiveness.js.map +1 -0
- package/build/services/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/OtaArtifactDownloader.d.ts +65 -0
- package/build/services/OtaArtifactDownloader.d.ts.map +1 -0
- package/build/services/OtaArtifactDownloader.js +226 -0
- package/build/services/OtaArtifactDownloader.js.map +1 -0
- package/build/services/OtaAutoChain.d.ts +34 -0
- package/build/services/OtaAutoChain.d.ts.map +1 -0
- package/build/services/OtaAutoChain.js +83 -0
- package/build/services/OtaAutoChain.js.map +1 -0
- package/build/services/OtaErrorMapping.d.ts +18 -0
- package/build/services/OtaErrorMapping.d.ts.map +1 -0
- package/build/services/OtaErrorMapping.js +63 -0
- package/build/services/OtaErrorMapping.js.map +1 -0
- package/build/services/OtaInstallCoordinator.d.ts +94 -14
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
- package/build/services/OtaInstallCoordinator.js +574 -130
- package/build/services/OtaInstallCoordinator.js.map +1 -1
- package/build/services/OtaService.d.ts.map +1 -1
- package/build/services/OtaService.js +8 -1
- package/build/services/OtaService.js.map +1 -1
- package/build/services/OtaUpdateCheckService.d.ts +38 -3
- package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
- package/build/services/OtaUpdateCheckService.js +142 -22
- package/build/services/OtaUpdateCheckService.js.map +1 -1
- package/build/services/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 +45 -0
- package/build/services/PhoneCalendarService.js.map +1 -0
- package/build/services/PhoneCameraFovCoordinator.d.ts +30 -0
- package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -0
- package/build/services/PhoneCameraFovCoordinator.js +194 -0
- package/build/services/PhoneCameraFovCoordinator.js.map +1 -0
- package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
- package/build/services/PhoneNotificationsSync.js +12 -10
- package/build/services/PhoneNotificationsSync.js.map +1 -1
- package/build/services/PhonePhotoCoordinator.d.ts +26 -2
- package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
- package/build/services/PhonePhotoCoordinator.js +79 -9
- package/build/services/PhonePhotoCoordinator.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts +8 -1
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +125 -13
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/PhoneVideoCoordinator.d.ts +2 -0
- package/build/services/PhoneVideoCoordinator.d.ts.map +1 -1
- package/build/services/PhoneVideoCoordinator.js +10 -0
- package/build/services/PhoneVideoCoordinator.js.map +1 -1
- package/build/services/SceneRenderer.d.ts.map +1 -1
- package/build/services/SceneRenderer.js +15 -0
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/services/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/SupportProfileSync.d.ts +22 -0
- package/build/services/SupportProfileSync.d.ts.map +1 -0
- package/build/services/SupportProfileSync.js +213 -0
- package/build/services/SupportProfileSync.js.map +1 -0
- package/build/services/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/TtsTextSanitizer.d.ts +5 -0
- package/build/services/TtsTextSanitizer.d.ts.map +1 -0
- package/build/services/TtsTextSanitizer.js +61 -0
- package/build/services/TtsTextSanitizer.js.map +1 -0
- package/build/services/WifiAdbRequest.d.ts +17 -0
- package/build/services/WifiAdbRequest.d.ts.map +1 -0
- package/build/services/WifiAdbRequest.js +35 -0
- package/build/services/WifiAdbRequest.js.map +1 -0
- package/build/services/asg/asgCameraApi.d.ts +43 -0
- package/build/services/asg/asgCameraApi.d.ts.map +1 -1
- package/build/services/asg/asgCameraApi.js +509 -26
- package/build/services/asg/asgCameraApi.js.map +1 -1
- 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/galleryNotices.d.ts +1 -1
- package/build/services/asg/galleryNotices.d.ts.map +1 -1
- package/build/services/asg/galleryNotices.js.map +1 -1
- 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 +1 -0
- package/build/services/asg/gallerySettingsService.d.ts.map +1 -1
- package/build/services/asg/gallerySettingsService.js +2 -0
- package/build/services/asg/gallerySettingsService.js.map +1 -1
- package/build/services/asg/gallerySyncService.d.ts.map +1 -1
- package/build/services/asg/gallerySyncService.js +162 -56
- package/build/services/asg/gallerySyncService.js.map +1 -1
- 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 +18 -0
- package/build/services/asg/localStorageService.d.ts.map +1 -1
- package/build/services/asg/localStorageService.js +168 -19
- package/build/services/asg/localStorageService.js.map +1 -1
- package/build/services/asg/mediaProcessingQueue.d.ts +10 -0
- package/build/services/asg/mediaProcessingQueue.d.ts.map +1 -1
- package/build/services/asg/mediaProcessingQueue.js +193 -62
- package/build/services/asg/mediaProcessingQueue.js.map +1 -1
- package/build/services/audioPlaybackAssets.d.ts +8 -0
- package/build/services/audioPlaybackAssets.d.ts.map +1 -0
- package/build/services/audioPlaybackAssets.js +7 -0
- package/build/services/audioPlaybackAssets.js.map +1 -0
- package/build/services/gallerySyncClock.js +1 -1
- package/build/services/gallerySyncClock.js.map +1 -1
- package/build/services/glassesClockSync.js +2 -2
- package/build/services/glassesClockSync.js.map +1 -1
- package/build/services/manifestActions.d.ts +7 -0
- package/build/services/manifestActions.d.ts.map +1 -0
- package/build/services/manifestActions.js +25 -0
- package/build/services/manifestActions.js.map +1 -0
- package/build/services/otaDisplayState.d.ts +22 -11
- package/build/services/otaDisplayState.d.ts.map +1 -1
- package/build/services/otaDisplayState.js +36 -14
- package/build/services/otaDisplayState.js.map +1 -1
- package/build/services/otaInstallPolicy.d.ts +15 -3
- package/build/services/otaInstallPolicy.d.ts.map +1 -1
- package/build/services/otaInstallPolicy.js +24 -3
- package/build/services/otaInstallPolicy.js.map +1 -1
- package/build/services/otaManifestUrl.d.ts +2 -0
- package/build/services/otaManifestUrl.d.ts.map +1 -0
- package/build/services/{asgOtaVersionUrl.js → otaManifestUrl.js} +15 -5
- package/build/services/otaManifestUrl.js.map +1 -0
- package/build/services/slimStreamStatus.d.ts +11 -0
- package/build/services/slimStreamStatus.d.ts.map +1 -1
- package/build/services/slimStreamStatus.js +14 -4
- package/build/services/slimStreamStatus.js.map +1 -1
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +11 -0
- package/build/stores/apps.js.map +1 -1
- package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
- package/build/stores/bluetoothSettingKeys.js +2 -0
- package/build/stores/bluetoothSettingKeys.js.map +1 -1
- package/build/stores/gallerySync.d.ts +2 -0
- package/build/stores/gallerySync.d.ts.map +1 -1
- package/build/stores/gallerySync.js +2 -0
- package/build/stores/gallerySync.js.map +1 -1
- package/build/stores/glasses.d.ts.map +1 -1
- package/build/stores/glasses.js +3 -0
- package/build/stores/glasses.js.map +1 -1
- package/build/stores/settings.d.ts +17 -0
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +157 -17
- package/build/stores/settings.js.map +1 -1
- package/build/types/applet.d.ts +2 -0
- package/build/types/applet.d.ts.map +1 -1
- package/build/types/applet.js.map +1 -1
- package/build/types/asg.d.ts +7 -0
- package/build/types/asg.d.ts.map +1 -1
- package/build/types/asg.js.map +1 -1
- package/build/types/enums.d.ts +1 -0
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +1 -0
- package/build/types/enums.js.map +1 -1
- package/build/types/hardware.d.ts +1 -0
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js +14 -0
- package/build/types/hardware.js.map +1 -1
- package/build/utils/configuredDevHost.d.ts +6 -0
- package/build/utils/configuredDevHost.d.ts.map +1 -0
- package/build/utils/configuredDevHost.js +19 -0
- package/build/utils/configuredDevHost.js.map +1 -0
- package/build/utils/devMiniappLaunch.d.ts +19 -8
- package/build/utils/devMiniappLaunch.d.ts.map +1 -1
- package/build/utils/devMiniappLaunch.js +144 -21
- package/build/utils/devMiniappLaunch.js.map +1 -1
- package/build/utils/diagnosticContext.d.ts.map +1 -1
- package/build/utils/diagnosticContext.js +15 -17
- package/build/utils/diagnosticContext.js.map +1 -1
- package/build/utils/hardware/hardware.d.ts.map +1 -1
- package/build/utils/hardware/hardware.js +7 -3
- package/build/utils/hardware/hardware.js.map +1 -1
- package/build/utils/miniappDiagnostics.d.ts +64 -0
- package/build/utils/miniappDiagnostics.d.ts.map +1 -0
- package/build/utils/miniappDiagnostics.js +173 -0
- package/build/utils/miniappDiagnostics.js.map +1 -0
- package/build/utils/permissions/MediaLibraryPermissions.d.ts +11 -0
- package/build/utils/permissions/MediaLibraryPermissions.d.ts.map +1 -1
- package/build/utils/permissions/MediaLibraryPermissions.js +27 -4
- package/build/utils/permissions/MediaLibraryPermissions.js.map +1 -1
- package/build/utils/timers/timers.d.ts.map +1 -1
- package/build/utils/timers/timers.js +55 -18
- package/build/utils/timers/timers.js.map +1 -1
- package/package.json +25 -15
- package/src/assets/silence.wav +0 -0
- package/src/engine.ts +7 -2
- package/src/facades/dev.ts +17 -12
- package/src/facades/gallery.ts +1 -1
- package/src/facades/glasses.ts +17 -4
- package/src/facades/glassesSettings.ts +5 -0
- package/src/facades/miniapps.ts +7 -0
- package/src/facades/ota.ts +19 -4
- package/src/facades/pairing.ts +11 -7
- package/src/facades/permissions.ts +14 -0
- package/src/facades/phoneNotifications.ts +4 -13
- package/src/index.ts +26 -2
- package/src/internal.ts +3 -1
- package/src/react/MentraLiveOtaFlow.tsx +856 -0
- package/src/react/index.ts +7 -0
- package/src/react/useEngineSnapshot.ts +16 -0
- package/src/runtime/bootstrap.ts +33 -1
- package/src/runtime/scanQrSeam.ts +20 -0
- package/src/services/AppRegistry.ts +224 -149
- package/src/services/AudioCloudUplink.ts +23 -1
- package/src/services/AudioPlaybackService.ts +287 -38
- package/src/services/BlobStore.ts +22 -9
- package/src/services/CloudAudioSubscriptionSync.ts +13 -1
- package/src/services/CloudClientService.ts +96 -1
- package/src/services/DeviceEventRouter.ts +5 -5
- package/src/services/ForegroundLocationPermission.ts +24 -0
- package/src/services/GlassesSettingsSync.ts +9 -3
- package/src/services/GlassesStatusProjection.ts +16 -8
- package/src/services/HotspotOtaTransport.ts +184 -0
- package/src/services/HotspotShutdown.ts +41 -0
- package/src/services/LocalDisplayManager.ts +112 -21
- package/src/services/LocalMiniappRuntime.ts +967 -346
- package/src/services/LocalMiniappStorage.ts +82 -0
- package/src/services/LocalMiniappUserIdentity.ts +46 -0
- package/src/services/LocalSttFallbackCoordinator.ts +19 -12
- package/src/services/MicStateCoordinator.ts +169 -4
- package/src/services/MiniappLauncher.ts +30 -3
- package/src/services/MiniappLiveness.ts +24 -0
- package/src/services/NativeHttpResponse.ts +6 -0
- package/src/services/OtaArtifactDownloader.ts +312 -0
- package/src/services/OtaAutoChain.ts +106 -0
- package/src/services/OtaErrorMapping.ts +80 -0
- package/src/services/OtaInstallCoordinator.ts +645 -143
- package/src/services/OtaService.ts +11 -5
- package/src/services/OtaUpdateCheckService.ts +180 -20
- package/src/services/PhoneCalendarHelpers.ts +212 -0
- package/src/services/PhoneCalendarService.ts +65 -0
- package/src/services/PhoneCameraFovCoordinator.ts +210 -0
- package/src/services/PhoneNotificationsSync.ts +12 -10
- package/src/services/PhonePhotoCoordinator.ts +116 -18
- package/src/services/PhoneStreamCoordinator.ts +139 -25
- package/src/services/PhoneVideoCoordinator.ts +11 -0
- package/src/services/SceneRenderer.ts +16 -0
- package/src/services/SentenceTtsPipeline.ts +106 -0
- package/src/services/SupportProfileSync.ts +218 -0
- package/src/services/TranscriptionRouting.ts +37 -0
- package/src/services/TtsTextSanitizer.ts +66 -0
- package/src/services/WifiAdbRequest.ts +53 -0
- package/src/services/asg/asgCameraApi.ts +615 -33
- package/src/services/asg/cameraRollExportCoordinator.ts +627 -0
- package/src/services/asg/cameraRollExportLedger.ts +122 -0
- package/src/services/asg/galleryNotices.ts +1 -0
- package/src/services/asg/galleryPathMigration.ts +22 -0
- package/src/services/asg/gallerySettingsService.ts +3 -0
- package/src/services/asg/gallerySyncService.ts +184 -73
- package/src/services/asg/galleryTransferLedger.ts +312 -0
- package/src/services/asg/localNetworkTransport.ts +169 -0
- package/src/services/asg/localStorageService.ts +179 -24
- package/src/services/asg/mediaProcessingQueue.ts +209 -62
- package/src/services/audioPlaybackAssets.ts +8 -0
- package/src/services/gallerySyncClock.ts +1 -1
- package/src/services/glassesClockSync.ts +2 -2
- package/src/services/manifestActions.ts +30 -0
- package/src/services/otaDisplayState.ts +57 -16
- package/src/services/otaInstallPolicy.ts +29 -3
- package/src/services/{asgOtaVersionUrl.ts → otaManifestUrl.ts} +16 -4
- package/src/services/slimStreamStatus.ts +16 -5
- package/src/stores/apps.ts +12 -0
- package/src/stores/bluetoothSettingKeys.ts +2 -0
- package/src/stores/gallerySync.ts +4 -0
- package/src/stores/glasses.ts +3 -0
- package/src/stores/settings.ts +179 -18
- package/src/types/applet.ts +2 -0
- package/src/types/asg.ts +7 -0
- package/src/types/enums.ts +2 -1
- package/src/types/hardware.ts +17 -0
- package/src/utils/configuredDevHost.ts +17 -0
- package/src/utils/devMiniappLaunch.ts +151 -23
- package/src/utils/diagnosticContext.ts +17 -17
- package/src/utils/hardware/hardware.ts +7 -9
- package/src/utils/miniappDiagnostics.ts +226 -0
- package/src/utils/permissions/MediaLibraryPermissions.ts +35 -4
- package/src/utils/timers/timers.ts +57 -21
- package/build/services/asgOtaVersionUrl.d.ts +0 -2
- package/build/services/asgOtaVersionUrl.d.ts.map +0 -1
- package/build/services/asgOtaVersionUrl.js.map +0 -1
|
@@ -11,11 +11,39 @@ import {BgTimer} from "../../utils/timers"
|
|
|
11
11
|
import {localStorageService} from "./localStorageService"
|
|
12
12
|
import {validateDownloadedMediaFile} from "./galleryMediaValidation"
|
|
13
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
|
+
}
|
|
14
41
|
|
|
15
42
|
export class AsgCameraApiClient {
|
|
16
43
|
private baseUrl: string
|
|
17
44
|
private port: number
|
|
18
45
|
private lastRequestTime: number = 0
|
|
46
|
+
private galleryCapabilities: GalleryCapabilities | null | undefined
|
|
19
47
|
|
|
20
48
|
constructor(serverUrl?: string, port: number = 8089) {
|
|
21
49
|
this.port = port
|
|
@@ -44,6 +72,7 @@ export class AsgCameraApiClient {
|
|
|
44
72
|
if (this.baseUrl !== newUrl) {
|
|
45
73
|
this.baseUrl = newUrl
|
|
46
74
|
this.port = newPort
|
|
75
|
+
this.galleryCapabilities = undefined
|
|
47
76
|
// console.log(`[ASG Camera API] Server changed from ${oldUrl} to ${this.baseUrl}`)
|
|
48
77
|
} else {
|
|
49
78
|
// console.log(`[ASG Camera API] Server URL unchanged: ${this.baseUrl}`)
|
|
@@ -60,10 +89,9 @@ export class AsgCameraApiClient {
|
|
|
60
89
|
/**
|
|
61
90
|
* Rate limiting helper - ensures minimum delay between requests
|
|
62
91
|
*/
|
|
63
|
-
private async rateLimit(): Promise<void> {
|
|
92
|
+
private async rateLimit(minDelay: number = 500): Promise<void> {
|
|
64
93
|
const now = Date.now()
|
|
65
94
|
const timeSinceLastRequest = now - this.lastRequestTime
|
|
66
|
-
const minDelay = 500 // 500ms minimum delay between requests
|
|
67
95
|
|
|
68
96
|
if (timeSinceLastRequest < minDelay) {
|
|
69
97
|
const delay = minDelay - timeSinceLastRequest
|
|
@@ -77,7 +105,12 @@ export class AsgCameraApiClient {
|
|
|
77
105
|
/**
|
|
78
106
|
* Make a request to the ASG Camera Server with rate limiting and retry logic
|
|
79
107
|
*/
|
|
80
|
-
private async makeRequest<T>(
|
|
108
|
+
private async makeRequest<T>(
|
|
109
|
+
endpoint: string,
|
|
110
|
+
options?: RequestInit,
|
|
111
|
+
retries: number = 5,
|
|
112
|
+
writeIntervalMs: number = 500,
|
|
113
|
+
): Promise<T> {
|
|
81
114
|
const url = `${this.baseUrl}${endpoint}`
|
|
82
115
|
const method = options?.method || "GET"
|
|
83
116
|
|
|
@@ -96,8 +129,8 @@ export class AsgCameraApiClient {
|
|
|
96
129
|
|
|
97
130
|
try {
|
|
98
131
|
// Apply rate limiting only for non-GET requests
|
|
99
|
-
if (method !== "GET") {
|
|
100
|
-
await this.rateLimit()
|
|
132
|
+
if (method !== "GET" && writeIntervalMs > 0) {
|
|
133
|
+
await this.rateLimit(writeIntervalMs)
|
|
101
134
|
}
|
|
102
135
|
|
|
103
136
|
// Prepare headers - don't set Content-Type for GET requests
|
|
@@ -116,7 +149,7 @@ export class AsgCameraApiClient {
|
|
|
116
149
|
console.log(`[ASG Camera API] Headers being sent:`, headers)
|
|
117
150
|
|
|
118
151
|
// N4: Add 30s timeout to all fetch calls in makeRequest
|
|
119
|
-
const response = await fetch(url, {
|
|
152
|
+
const response = await localNetworkTransport.fetch(url, {
|
|
120
153
|
headers,
|
|
121
154
|
...options,
|
|
122
155
|
signal: options?.signal || this.createTimeoutSignal(30000),
|
|
@@ -140,7 +173,7 @@ export class AsgCameraApiClient {
|
|
|
140
173
|
const retryDelay = Math.min(Math.pow(2, 6 - retries) * 1000, 10000)
|
|
141
174
|
console.log(`[ASG Camera API] Rate limited, retrying in ${retryDelay}ms (${retries} retries left)`)
|
|
142
175
|
await new Promise<void>((resolve) => BgTimer.setTimeout(() => resolve(), retryDelay))
|
|
143
|
-
return this.makeRequest<T>(endpoint, options, retries - 1)
|
|
176
|
+
return this.makeRequest<T>(endpoint, options, retries - 1, writeIntervalMs)
|
|
144
177
|
}
|
|
145
178
|
|
|
146
179
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
@@ -152,7 +185,12 @@ export class AsgCameraApiClient {
|
|
|
152
185
|
|
|
153
186
|
if (contentType?.includes("application/json")) {
|
|
154
187
|
const data = await response.json()
|
|
155
|
-
console.log(`[ASG Camera API] 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
|
+
})
|
|
156
194
|
return data
|
|
157
195
|
} else if (contentType?.includes("image/") || contentType?.includes("application/octet-stream")) {
|
|
158
196
|
// For image responses and binary data (including AVIF), return the blob
|
|
@@ -250,7 +288,7 @@ export class AsgCameraApiClient {
|
|
|
250
288
|
// Use browser-like headers since we know the browser works
|
|
251
289
|
try {
|
|
252
290
|
console.log(`[ASG Camera API] Making direct fetch to gallery endpoint`)
|
|
253
|
-
const response = await fetch(galleryUrl, {
|
|
291
|
+
const response = await localNetworkTransport.fetch(galleryUrl, {
|
|
254
292
|
method: "GET",
|
|
255
293
|
headers: {
|
|
256
294
|
"Accept": "application/json",
|
|
@@ -390,7 +428,7 @@ export class AsgCameraApiClient {
|
|
|
390
428
|
for (const endpoint of testEndpoints) {
|
|
391
429
|
try {
|
|
392
430
|
console.log(`[ASG Camera API] Testing endpoint: ${endpoint}`)
|
|
393
|
-
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
|
431
|
+
const response = await localNetworkTransport.fetch(`${this.baseUrl}${endpoint}`, {
|
|
394
432
|
method: "HEAD",
|
|
395
433
|
headers: {
|
|
396
434
|
"Accept": "*/*",
|
|
@@ -411,7 +449,7 @@ export class AsgCameraApiClient {
|
|
|
411
449
|
if (endpoint === "/api/gallery") {
|
|
412
450
|
try {
|
|
413
451
|
console.log(`[ASG Camera API] Trying GET request for /api/gallery...`)
|
|
414
|
-
const getResponse = await fetch(`${this.baseUrl}${endpoint}`, {
|
|
452
|
+
const getResponse = await localNetworkTransport.fetch(`${this.baseUrl}${endpoint}`, {
|
|
415
453
|
method: "GET",
|
|
416
454
|
headers: {
|
|
417
455
|
"Accept": "application/json",
|
|
@@ -515,7 +553,7 @@ export class AsgCameraApiClient {
|
|
|
515
553
|
const controller = new AbortController()
|
|
516
554
|
const timeoutId = BgTimer.setTimeout(() => controller.abort(), 3000) // 3 second timeout
|
|
517
555
|
|
|
518
|
-
const response = await fetch(`${this.baseUrl}/api/health`, {
|
|
556
|
+
const response = await localNetworkTransport.fetch(`${this.baseUrl}/api/health`, {
|
|
519
557
|
method: "HEAD",
|
|
520
558
|
signal: controller.signal,
|
|
521
559
|
})
|
|
@@ -602,6 +640,104 @@ export class AsgCameraApiClient {
|
|
|
602
640
|
}
|
|
603
641
|
}
|
|
604
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
|
+
|
|
605
741
|
/**
|
|
606
742
|
* Batch sync files from server with controlled concurrency.
|
|
607
743
|
* Used by the legacy executeDownload path for old asg_client firmware
|
|
@@ -637,7 +773,9 @@ export class AsgCameraApiClient {
|
|
|
637
773
|
for (let i = 0; i < files.length; i += CONCURRENCY_LIMIT) {
|
|
638
774
|
const batch = files.slice(i, i + CONCURRENCY_LIMIT)
|
|
639
775
|
console.log(
|
|
640
|
-
`[ASG Camera API] Processing batch ${Math.floor(i / CONCURRENCY_LIMIT) + 1}/${Math.ceil(
|
|
776
|
+
`[ASG Camera API] Processing batch ${Math.floor(i / CONCURRENCY_LIMIT) + 1}/${Math.ceil(
|
|
777
|
+
files.length / CONCURRENCY_LIMIT,
|
|
778
|
+
)}: ${batch.length} files`,
|
|
641
779
|
)
|
|
642
780
|
|
|
643
781
|
// Process batch in parallel
|
|
@@ -706,12 +844,6 @@ export class AsgCameraApiClient {
|
|
|
706
844
|
results.failed.push(result.error)
|
|
707
845
|
}
|
|
708
846
|
}
|
|
709
|
-
|
|
710
|
-
// Small delay between batches to prevent overwhelming the server
|
|
711
|
-
if (i + CONCURRENCY_LIMIT < files.length) {
|
|
712
|
-
console.log(`[ASG Camera API] Waiting 300ms between batches`)
|
|
713
|
-
await new Promise<void>((resolve) => BgTimer.setTimeout(() => resolve(), 300))
|
|
714
|
-
}
|
|
715
847
|
}
|
|
716
848
|
|
|
717
849
|
console.log(
|
|
@@ -724,11 +856,318 @@ export class AsgCameraApiClient {
|
|
|
724
856
|
* Download all files in a capture group sequentially.
|
|
725
857
|
* Reports aggregate byte progress across all files in the group.
|
|
726
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
|
+
|
|
727
1070
|
async downloadCapture(
|
|
728
1071
|
capture: CaptureGroup,
|
|
729
1072
|
onProgress?: (bytesDownloaded: number, totalBytes: number) => void,
|
|
730
1073
|
abortSignal?: AbortSignal,
|
|
731
|
-
): Promise<{
|
|
1074
|
+
): Promise<{
|
|
1075
|
+
captureDir: string
|
|
1076
|
+
primaryPath: string
|
|
1077
|
+
bracketPaths: string[]
|
|
1078
|
+
sidecarPath?: string
|
|
1079
|
+
thumbnailPath?: string
|
|
1080
|
+
}> {
|
|
1081
|
+
if (capture.files.some((file) => !file.size || file.size <= 0)) {
|
|
1082
|
+
// Preserve support for malformed manifests from pre-v2 development firmware.
|
|
1083
|
+
return this.downloadCaptureLegacyUnsafe(capture, onProgress, abortSignal)
|
|
1084
|
+
}
|
|
1085
|
+
const captureDir = localStorageService.getPhotoFilePath(capture.capture_id)
|
|
1086
|
+
if (!(await RNFS.exists(captureDir))) await RNFS.mkdir(captureDir)
|
|
1087
|
+
const capabilities = await this.getGalleryCapabilities()
|
|
1088
|
+
galleryTransferLedger.ensureCapture(capture, capabilities?.api_version || 2)
|
|
1089
|
+
galleryTransferLedger.transition(capture.capture_id, "TRANSFERRING")
|
|
1090
|
+
|
|
1091
|
+
const sortedFiles = [...capture.files].sort((a, b) => {
|
|
1092
|
+
const order = {bracket: 0, primary: 1, sidecar: 2}
|
|
1093
|
+
return (order[a.role] ?? 1) - (order[b.role] ?? 1)
|
|
1094
|
+
})
|
|
1095
|
+
let completedCaptureBytes = 0
|
|
1096
|
+
let primaryPath = ""
|
|
1097
|
+
const bracketPaths: string[] = []
|
|
1098
|
+
let sidecarPath: string | undefined
|
|
1099
|
+
|
|
1100
|
+
try {
|
|
1101
|
+
for (const file of sortedFiles) {
|
|
1102
|
+
const leafName = file.name.includes("/") ? file.name.split("/").pop()! : file.name
|
|
1103
|
+
const localFilePath = `${captureDir}/${leafName}`
|
|
1104
|
+
await this.downloadResumableCaptureFile(
|
|
1105
|
+
capture.capture_id,
|
|
1106
|
+
file,
|
|
1107
|
+
localFilePath,
|
|
1108
|
+
(fileBytes) => onProgress?.(completedCaptureBytes + fileBytes, capture.total_size),
|
|
1109
|
+
abortSignal,
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
const isVideo = !!file.name.match(/\.(mp4|mov|avi|webm|mkv)$/i)
|
|
1113
|
+
const mediaKind = file.role === "sidecar" ? "unknown" : isVideo ? "video" : "photo"
|
|
1114
|
+
try {
|
|
1115
|
+
await validateDownloadedMediaFile({
|
|
1116
|
+
path: localFilePath,
|
|
1117
|
+
name: file.name,
|
|
1118
|
+
expectedSize: file.size,
|
|
1119
|
+
mediaKind,
|
|
1120
|
+
})
|
|
1121
|
+
} catch (validationError: any) {
|
|
1122
|
+
reportInvalidGalleryMedia({
|
|
1123
|
+
name: file.name,
|
|
1124
|
+
path: localFilePath,
|
|
1125
|
+
mediaKind,
|
|
1126
|
+
stage: "download_capture_validation",
|
|
1127
|
+
reason: validationError?.message || String(validationError),
|
|
1128
|
+
expectedSize: file.size,
|
|
1129
|
+
captureId: capture.capture_id,
|
|
1130
|
+
duration: capture.duration,
|
|
1131
|
+
})
|
|
1132
|
+
await RNFS.moveFile(localFilePath, `${localFilePath}.invalid.${Date.now()}`).catch(() => {})
|
|
1133
|
+
throw validationError
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
completedCaptureBytes += file.size
|
|
1137
|
+
if (file.role === "primary") primaryPath = localFilePath
|
|
1138
|
+
else if (file.role === "bracket") bracketPaths.push(localFilePath)
|
|
1139
|
+
else if (file.role === "sidecar") sidecarPath = localFilePath
|
|
1140
|
+
}
|
|
1141
|
+
if (!primaryPath && sortedFiles.length > 0) {
|
|
1142
|
+
const leafName = sortedFiles[0].name.includes("/") ? sortedFiles[0].name.split("/").pop()! : sortedFiles[0].name
|
|
1143
|
+
primaryPath = `${captureDir}/${leafName}`
|
|
1144
|
+
}
|
|
1145
|
+
const primaryFile = sortedFiles.find((file) => file.role === "primary") || sortedFiles[0]
|
|
1146
|
+
const thumbnailPath =
|
|
1147
|
+
capture.type === "video" && primaryFile
|
|
1148
|
+
? await this.downloadVideoThumbnail(primaryFile.name, `${captureDir}/.thumb.jpg`, abortSignal)
|
|
1149
|
+
: undefined
|
|
1150
|
+
galleryTransferLedger.transition(capture.capture_id, "VERIFIED")
|
|
1151
|
+
onProgress?.(capture.total_size, capture.total_size)
|
|
1152
|
+
return {captureDir, primaryPath, bracketPaths, sidecarPath, thumbnailPath}
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
galleryTransferLedger.recordFailure(capture.capture_id, error)
|
|
1155
|
+
throw error
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/** Previous direct-to-final implementation kept for binary-source compatibility only. */
|
|
1160
|
+
private async downloadCaptureLegacyUnsafe(
|
|
1161
|
+
capture: CaptureGroup,
|
|
1162
|
+
onProgress?: (bytesDownloaded: number, totalBytes: number) => void,
|
|
1163
|
+
abortSignal?: AbortSignal,
|
|
1164
|
+
): Promise<{
|
|
1165
|
+
captureDir: string
|
|
1166
|
+
primaryPath: string
|
|
1167
|
+
bracketPaths: string[]
|
|
1168
|
+
sidecarPath?: string
|
|
1169
|
+
thumbnailPath?: string
|
|
1170
|
+
}> {
|
|
732
1171
|
const captureDir = localStorageService.getPhotoFilePath(capture.capture_id)
|
|
733
1172
|
console.log(
|
|
734
1173
|
`[ASG Camera API] downloadCapture: ${capture.capture_id} (${capture.files.length} files) -> ${captureDir}`,
|
|
@@ -769,7 +1208,7 @@ export class AsgCameraApiClient {
|
|
|
769
1208
|
try {
|
|
770
1209
|
// Throttle progress: only fire when bytesWritten changes meaningfully
|
|
771
1210
|
let lastReportedBytes = -1
|
|
772
|
-
const {jobId, promise: dlPromise} =
|
|
1211
|
+
const {jobId, promise: dlPromise} = localNetworkTransport.downloadFile({
|
|
773
1212
|
fromUrl: downloadUrl,
|
|
774
1213
|
toFile: localFilePath,
|
|
775
1214
|
headers: {
|
|
@@ -794,12 +1233,12 @@ export class AsgCameraApiClient {
|
|
|
794
1233
|
let abortPollTimer: number | undefined
|
|
795
1234
|
if (abortSignal) {
|
|
796
1235
|
if (abortSignal.aborted) {
|
|
797
|
-
|
|
1236
|
+
localNetworkTransport.stopDownload(jobId)
|
|
798
1237
|
throw new Error("Sync cancelled")
|
|
799
1238
|
}
|
|
800
1239
|
abortPollTimer = BgTimer.setInterval(() => {
|
|
801
1240
|
if (abortSignal.aborted) {
|
|
802
|
-
|
|
1241
|
+
localNetworkTransport.stopDownload(jobId)
|
|
803
1242
|
}
|
|
804
1243
|
}, 500)
|
|
805
1244
|
}
|
|
@@ -878,7 +1317,76 @@ export class AsgCameraApiClient {
|
|
|
878
1317
|
onProgress(totalBytes, totalBytes)
|
|
879
1318
|
}
|
|
880
1319
|
|
|
881
|
-
|
|
1320
|
+
const primaryFile = sortedFiles.find((file) => file.role === "primary") || sortedFiles[0]
|
|
1321
|
+
const thumbnailPath =
|
|
1322
|
+
capture.type === "video" && primaryFile
|
|
1323
|
+
? await this.downloadVideoThumbnail(primaryFile.name, `${captureDir}/.thumb.jpg`, abortSignal)
|
|
1324
|
+
: undefined
|
|
1325
|
+
|
|
1326
|
+
return {captureDir, primaryPath, bracketPaths, sidecarPath, thumbnailPath}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/** Download an on-demand video thumbnail while the source capture is still on the glasses. */
|
|
1330
|
+
private async downloadVideoThumbnail(
|
|
1331
|
+
sourceFileName: string,
|
|
1332
|
+
thumbnailPath: string,
|
|
1333
|
+
abortSignal?: AbortSignal,
|
|
1334
|
+
): Promise<string | undefined> {
|
|
1335
|
+
const partialPath = `${thumbnailPath}.partial`
|
|
1336
|
+
const parentDir = thumbnailPath.substring(0, thumbnailPath.lastIndexOf("/"))
|
|
1337
|
+
|
|
1338
|
+
try {
|
|
1339
|
+
if (parentDir && !(await RNFS.exists(parentDir))) {
|
|
1340
|
+
await RNFS.mkdir(parentDir)
|
|
1341
|
+
}
|
|
1342
|
+
await RNFS.unlink(partialPath).catch(() => {})
|
|
1343
|
+
const {jobId, promise} = localNetworkTransport.downloadFile({
|
|
1344
|
+
fromUrl: `${this.baseUrl}/api/photo?file=${encodeURIComponent(sourceFileName)}`,
|
|
1345
|
+
toFile: partialPath,
|
|
1346
|
+
headers: {
|
|
1347
|
+
"Accept": "image/jpeg",
|
|
1348
|
+
"User-Agent": "MentraOS-Mobile/1.0",
|
|
1349
|
+
},
|
|
1350
|
+
connectionTimeout: 60000,
|
|
1351
|
+
readTimeout: 60000,
|
|
1352
|
+
progressDivider: 10,
|
|
1353
|
+
})
|
|
1354
|
+
|
|
1355
|
+
let abortPollTimer: number | undefined
|
|
1356
|
+
if (abortSignal) {
|
|
1357
|
+
if (abortSignal.aborted) {
|
|
1358
|
+
localNetworkTransport.stopDownload(jobId)
|
|
1359
|
+
throw new Error("Sync cancelled")
|
|
1360
|
+
}
|
|
1361
|
+
abortPollTimer = BgTimer.setInterval(() => {
|
|
1362
|
+
if (abortSignal.aborted) localNetworkTransport.stopDownload(jobId)
|
|
1363
|
+
}, 500)
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
let result
|
|
1367
|
+
try {
|
|
1368
|
+
result = await promise
|
|
1369
|
+
} finally {
|
|
1370
|
+
if (abortPollTimer !== undefined) BgTimer.clearInterval(abortPollTimer)
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if (abortSignal?.aborted) throw new Error("Sync cancelled")
|
|
1374
|
+
if (result.statusCode !== 200 || result.bytesWritten <= 0) {
|
|
1375
|
+
throw new Error(`HTTP ${result.statusCode}, ${result.bytesWritten} bytes`)
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
await RNFS.unlink(thumbnailPath).catch(() => {})
|
|
1379
|
+
await RNFS.moveFile(partialPath, thumbnailPath)
|
|
1380
|
+
console.log(`[ASG Camera API] Downloaded video thumbnail for ${sourceFileName}`)
|
|
1381
|
+
return thumbnailPath
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
await RNFS.unlink(partialPath).catch(() => {})
|
|
1384
|
+
if (abortSignal?.aborted || (error instanceof Error && error.message === "Sync cancelled")) {
|
|
1385
|
+
throw new Error("Sync cancelled")
|
|
1386
|
+
}
|
|
1387
|
+
console.warn(`[ASG Camera API] Failed to download video thumbnail for ${sourceFileName}:`, error)
|
|
1388
|
+
return undefined
|
|
1389
|
+
}
|
|
882
1390
|
}
|
|
883
1391
|
|
|
884
1392
|
/**
|
|
@@ -893,10 +1401,15 @@ export class AsgCameraApiClient {
|
|
|
893
1401
|
}
|
|
894
1402
|
|
|
895
1403
|
try {
|
|
896
|
-
const response = await this.makeRequest(
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1404
|
+
const response = await this.makeRequest(
|
|
1405
|
+
"/api/delete-files",
|
|
1406
|
+
{
|
|
1407
|
+
method: "POST",
|
|
1408
|
+
body: JSON.stringify({files: fileNames}),
|
|
1409
|
+
},
|
|
1410
|
+
5,
|
|
1411
|
+
0,
|
|
1412
|
+
)
|
|
900
1413
|
|
|
901
1414
|
// Parse the response format from the ASG server
|
|
902
1415
|
const responseData = response as any
|
|
@@ -954,6 +1467,73 @@ export class AsgCameraApiClient {
|
|
|
954
1467
|
onProgress?: (progress: number) => void,
|
|
955
1468
|
abortSignal?: AbortSignal,
|
|
956
1469
|
expectedSize?: number,
|
|
1470
|
+
): Promise<{filePath: string; thumbnailPath?: string; mime_type: string}> {
|
|
1471
|
+
// The legacy public shape did not require a size. Keep that rare call compatible while all
|
|
1472
|
+
// sync manifests use the verified resumable path below.
|
|
1473
|
+
if (!expectedSize || expectedSize <= 0) {
|
|
1474
|
+
return this.downloadFileLegacyUnsafe(filename, includeThumbnail, onProgress, abortSignal, expectedSize)
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const localFilePath = localStorageService.getPhotoFilePath(filename)
|
|
1478
|
+
const parentDir = localFilePath.substring(0, localFilePath.lastIndexOf("/"))
|
|
1479
|
+
if (!(await RNFS.exists(parentDir))) await RNFS.mkdir(parentDir)
|
|
1480
|
+
const lowerName = filename.toLowerCase()
|
|
1481
|
+
const isVideo = /\.(mp4|mov|avi|webm|mkv)$/.test(lowerName)
|
|
1482
|
+
const captureId = filename.includes("/")
|
|
1483
|
+
? filename.substring(0, filename.indexOf("/"))
|
|
1484
|
+
: filename.replace(/\.imu\.json$/i, "").replace(/\.[^.]+$/, "")
|
|
1485
|
+
const capture: CaptureGroup = {
|
|
1486
|
+
capture_id: captureId,
|
|
1487
|
+
type: isVideo ? "video" : "photo",
|
|
1488
|
+
timestamp: Date.now(),
|
|
1489
|
+
total_size: expectedSize,
|
|
1490
|
+
files: [{name: filename, size: expectedSize, role: "primary"}],
|
|
1491
|
+
}
|
|
1492
|
+
const capabilities = await this.getGalleryCapabilities()
|
|
1493
|
+
galleryTransferLedger.ensureCapture(capture, capabilities?.api_version || 1)
|
|
1494
|
+
galleryTransferLedger.transition(captureId, "TRANSFERRING")
|
|
1495
|
+
try {
|
|
1496
|
+
await this.downloadResumableCaptureFile(
|
|
1497
|
+
captureId,
|
|
1498
|
+
capture.files[0],
|
|
1499
|
+
localFilePath,
|
|
1500
|
+
(bytes) => onProgress?.(Math.min(100, Math.round((bytes / expectedSize) * 100))),
|
|
1501
|
+
abortSignal,
|
|
1502
|
+
)
|
|
1503
|
+
await validateDownloadedMediaFile({
|
|
1504
|
+
path: localFilePath,
|
|
1505
|
+
name: filename,
|
|
1506
|
+
expectedSize,
|
|
1507
|
+
mediaKind: isVideo ? "video" : "photo",
|
|
1508
|
+
})
|
|
1509
|
+
galleryTransferLedger.transition(captureId, "VERIFIED")
|
|
1510
|
+
const thumbnailPath =
|
|
1511
|
+
includeThumbnail && isVideo
|
|
1512
|
+
? await this.downloadVideoThumbnail(filename, localStorageService.getThumbnailFilePath(filename), abortSignal)
|
|
1513
|
+
: undefined
|
|
1514
|
+
return {
|
|
1515
|
+
filePath: localFilePath,
|
|
1516
|
+
thumbnailPath,
|
|
1517
|
+
mime_type: isVideo
|
|
1518
|
+
? "video/mp4"
|
|
1519
|
+
: lowerName.endsWith(".png")
|
|
1520
|
+
? "image/png"
|
|
1521
|
+
: lowerName.endsWith(".avif")
|
|
1522
|
+
? "image/avif"
|
|
1523
|
+
: "image/jpeg",
|
|
1524
|
+
}
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
galleryTransferLedger.recordFailure(captureId, error)
|
|
1527
|
+
throw error
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
private async downloadFileLegacyUnsafe(
|
|
1532
|
+
filename: string,
|
|
1533
|
+
includeThumbnail: boolean = false,
|
|
1534
|
+
onProgress?: (progress: number) => void,
|
|
1535
|
+
abortSignal?: AbortSignal,
|
|
1536
|
+
expectedSize?: number,
|
|
957
1537
|
): Promise<{
|
|
958
1538
|
filePath: string
|
|
959
1539
|
thumbnailPath?: string
|
|
@@ -991,7 +1571,7 @@ export class AsgCameraApiClient {
|
|
|
991
1571
|
// Throttle progress: only call onProgress when percentage actually changes
|
|
992
1572
|
let lastReportedProgress = -1
|
|
993
1573
|
|
|
994
|
-
const {jobId, promise: downloadPromise} =
|
|
1574
|
+
const {jobId, promise: downloadPromise} = localNetworkTransport.downloadFile({
|
|
995
1575
|
fromUrl: downloadUrl,
|
|
996
1576
|
toFile: localFilePath,
|
|
997
1577
|
headers: {
|
|
@@ -1036,12 +1616,12 @@ export class AsgCameraApiClient {
|
|
|
1036
1616
|
let abortPollTimer: number | undefined
|
|
1037
1617
|
if (abortSignal) {
|
|
1038
1618
|
if (abortSignal.aborted) {
|
|
1039
|
-
|
|
1619
|
+
localNetworkTransport.stopDownload(jobId)
|
|
1040
1620
|
throw new Error("Sync cancelled")
|
|
1041
1621
|
}
|
|
1042
1622
|
abortPollTimer = BgTimer.setInterval(() => {
|
|
1043
1623
|
if (abortSignal.aborted) {
|
|
1044
|
-
|
|
1624
|
+
localNetworkTransport.stopDownload(jobId)
|
|
1045
1625
|
}
|
|
1046
1626
|
}, 500)
|
|
1047
1627
|
}
|
|
@@ -1141,7 +1721,7 @@ export class AsgCameraApiClient {
|
|
|
1141
1721
|
|
|
1142
1722
|
// The server's /api/photo endpoint serves thumbnails for video files
|
|
1143
1723
|
// It detects video files and automatically generates/serves thumbnails instead of the full video
|
|
1144
|
-
const thumbResult = await
|
|
1724
|
+
const thumbResult = await localNetworkTransport.downloadFile({
|
|
1145
1725
|
fromUrl: `${this.baseUrl}/api/photo?file=${encodeURIComponent(filename)}`,
|
|
1146
1726
|
toFile: localThumbnailPath as string,
|
|
1147
1727
|
headers: {
|
|
@@ -1181,7 +1761,9 @@ export class AsgCameraApiClient {
|
|
|
1181
1761
|
}
|
|
1182
1762
|
} else {
|
|
1183
1763
|
console.log(
|
|
1184
|
-
`[ASG Camera API] Skipping thumbnail download - includeThumbnail: ${includeThumbnail}, filename: ${filename}, is video extension: ${
|
|
1764
|
+
`[ASG Camera API] Skipping thumbnail download - includeThumbnail: ${includeThumbnail}, filename: ${filename}, is video extension: ${
|
|
1765
|
+
filename.toLowerCase().match(/\.(mp4|mov|avi|mkv|webm)$/) ? "yes" : "no"
|
|
1766
|
+
}`,
|
|
1185
1767
|
)
|
|
1186
1768
|
}
|
|
1187
1769
|
|