@mentra/engine 0.1.0-dev.0 → 3.1.0-dev.10
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 +55 -18
- package/build/bluetooth-sdk/debug.d.ts +2 -0
- package/build/bluetooth-sdk/debug.d.ts.map +1 -0
- package/build/bluetooth-sdk/debug.js +2 -0
- package/build/bluetooth-sdk/debug.js.map +1 -0
- package/build/bluetooth-sdk/index.d.ts +3 -0
- package/build/bluetooth-sdk/index.d.ts.map +1 -0
- package/build/bluetooth-sdk/index.js +3 -0
- package/build/bluetooth-sdk/index.js.map +1 -0
- package/build/bluetooth-sdk/photo-receiver.d.ts +3 -0
- package/build/bluetooth-sdk/photo-receiver.d.ts.map +1 -0
- package/build/bluetooth-sdk/photo-receiver.js +3 -0
- package/build/bluetooth-sdk/photo-receiver.js.map +1 -0
- package/build/bluetooth-sdk/react.d.ts +2 -0
- package/build/bluetooth-sdk/react.d.ts.map +1 -0
- package/build/bluetooth-sdk/react.js +2 -0
- package/build/bluetooth-sdk/react.js.map +1 -0
- package/build/bluetooth-sdk/types.d.ts +2 -0
- package/build/bluetooth-sdk/types.d.ts.map +1 -0
- package/build/bluetooth-sdk/types.js +2 -0
- package/build/bluetooth-sdk/types.js.map +1 -0
- package/build/engine.d.ts +24 -16
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +9 -4
- 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/display.d.ts +1 -1
- package/build/facades/display.d.ts.map +1 -1
- package/build/facades/display.js +1 -1
- package/build/facades/display.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 +4 -13
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +21 -6
- 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 +14 -16
- package/build/facades/ota.d.ts.map +1 -1
- package/build/facades/ota.js +22 -7
- package/build/facades/ota.js.map +1 -1
- package/build/facades/pairing.d.ts +8 -5
- package/build/facades/pairing.d.ts.map +1 -1
- package/build/facades/pairing.js +12 -10
- 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/facades/speech.d.ts +7 -6
- package/build/facades/speech.d.ts.map +1 -1
- package/build/facades/speech.js +1 -1
- package/build/facades/speech.js.map +1 -1
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +10 -8
- package/build/index.d.ts.map +1 -1
- package/build/index.js +6 -5
- package/build/index.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 +41 -57
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +208 -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 +95 -15
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
- package/build/services/OtaInstallCoordinator.js +577 -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 +39 -4
- package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
- package/build/services/OtaUpdateCheckService.js +138 -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.d.ts.map +1 -1
- package/build/services/glassesClockSync.js +6 -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 +23 -12
- 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 +16 -4
- 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/otaLegacyMapping.d.ts +1 -1
- package/build/services/otaLegacyMapping.d.ts.map +1 -1
- package/build/services/otaLegacyMapping.js.map +1 -1
- package/build/services/otaManifestPolicy.d.ts +14 -0
- package/build/services/otaManifestPolicy.d.ts.map +1 -0
- package/build/services/otaManifestPolicy.js +19 -0
- package/build/services/otaManifestPolicy.js.map +1 -0
- package/build/services/otaManifestUrl.d.ts +3 -0
- package/build/services/otaManifestUrl.d.ts.map +1 -0
- package/build/services/otaManifestUrl.js +38 -0
- 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 +2 -25
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +2 -27
- package/build/types/enums.js.map +1 -1
- package/build/types/hardware.d.ts +4 -13
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js +15 -3
- package/build/types/hardware.js.map +1 -1
- package/build/types/index.d.ts +2 -6
- package/build/types/index.d.ts.map +1 -1
- package/build/types/index.js +2 -6
- package/build/types/index.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 +59 -31
- package/src/assets/silence.wav +0 -0
- package/src/bluetooth-sdk/debug.ts +1 -0
- package/src/bluetooth-sdk/index.ts +2 -0
- package/src/bluetooth-sdk/photo-receiver.ts +2 -0
- package/src/bluetooth-sdk/react.ts +1 -0
- package/src/bluetooth-sdk/types.ts +1 -0
- package/src/engine.ts +9 -4
- package/src/facades/dev.ts +17 -12
- package/src/facades/display.ts +1 -1
- package/src/facades/gallery.ts +1 -1
- package/src/facades/glasses.ts +20 -7
- package/src/facades/glassesSettings.ts +5 -0
- package/src/facades/miniapps.ts +7 -0
- package/src/facades/ota.ts +23 -8
- package/src/facades/pairing.ts +15 -11
- package/src/facades/permissions.ts +14 -0
- package/src/facades/phoneNotifications.ts +4 -13
- package/src/facades/speech.ts +21 -7
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +39 -13
- 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 +240 -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 +649 -144
- package/src/services/OtaService.ts +11 -5
- package/src/services/OtaUpdateCheckService.ts +181 -21
- 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 +6 -2
- package/src/services/manifestActions.ts +30 -0
- package/src/services/otaDisplayState.ts +58 -17
- package/src/services/otaInstallPolicy.ts +30 -4
- package/src/services/otaLegacyMapping.ts +1 -1
- package/src/services/otaManifestPolicy.ts +42 -0
- package/src/services/otaManifestUrl.ts +44 -0
- 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 +3 -28
- package/src/types/hardware.ts +20 -13
- package/src/types/index.ts +2 -6
- 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/devtools.d.ts +0 -11
- package/build/devtools.d.ts.map +0 -1
- package/build/devtools.js +0 -13
- package/build/devtools.js.map +0 -1
- package/build/internal.d.ts +0 -66
- package/build/internal.d.ts.map +0 -1
- package/build/internal.js +0 -89
- package/build/internal.js.map +0 -1
- package/build/services/asgOtaVersionUrl.d.ts +0 -2
- package/build/services/asgOtaVersionUrl.d.ts.map +0 -1
- package/build/services/asgOtaVersionUrl.js +0 -36
- package/build/services/asgOtaVersionUrl.js.map +0 -1
- package/src/devtools.ts +0 -13
- package/src/internal.ts +0 -158
- package/src/services/asgOtaVersionUrl.ts +0 -41
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* management (connect/disconnect/ping).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {Linking} from "react-native"
|
|
12
|
+
import {AppState, Linking} from "react-native"
|
|
13
13
|
import Share from "react-native-share"
|
|
14
14
|
import * as Battery from "expo-battery"
|
|
15
15
|
import * as Clipboard from "expo-clipboard"
|
|
@@ -45,9 +45,14 @@ import localSttFallbackCoordinator from "./LocalSttFallbackCoordinator"
|
|
|
45
45
|
import micStateCoordinator from "./MicStateCoordinator"
|
|
46
46
|
import {BlobStore} from "./BlobStore"
|
|
47
47
|
import {CloudAudioSubscriptionSync} from "./CloudAudioSubscriptionSync"
|
|
48
|
+
import {phoneCameraFovCoordinator} from "./PhoneCameraFovCoordinator"
|
|
48
49
|
import {phonePhotoCoordinator} from "./PhonePhotoCoordinator"
|
|
49
50
|
import {phoneStreamCoordinator} from "./PhoneStreamCoordinator"
|
|
50
51
|
import {phoneVideoCoordinator} from "./PhoneVideoCoordinator"
|
|
52
|
+
import {runSentenceTtsPipeline} from "./SentenceTtsPipeline"
|
|
53
|
+
import {summarizeTranscriptionRoutes, transcriptionDeliveryRoute} from "./TranscriptionRouting"
|
|
54
|
+
import {prepareTtsSentences} from "./TtsTextSanitizer"
|
|
55
|
+
import {handleWifiAdbRequest} from "./WifiAdbRequest"
|
|
51
56
|
import {cloudClientService} from "./CloudClientService"
|
|
52
57
|
import {miniappLauncher} from "./MiniappLauncher"
|
|
53
58
|
import {
|
|
@@ -61,39 +66,51 @@ import {
|
|
|
61
66
|
type TtsSynthesisResult,
|
|
62
67
|
} from "../runtime/config"
|
|
63
68
|
import {getAnalytics, getUiSeams} from "../runtime/bootstrap"
|
|
69
|
+
import {invokeScanQrSeam} from "../runtime/scanQrSeam"
|
|
64
70
|
import {normalizeStreamAudioConfig, normalizeStreamVideoConfig} from "../runtime/streamConfig"
|
|
65
|
-
import
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
TranscriptionData,
|
|
69
|
-
TranslationData,
|
|
70
|
-
} from "@mentra/cloud-protocol"
|
|
71
|
+
import {toLanguageHint} from "@mentra/cloud-protocol/languages"
|
|
72
|
+
import type {AudioSubscription, LanguageSource, TranscriptionData, TranslationData} from "@mentra/cloud-protocol"
|
|
73
|
+
import {buildMiniappManifestSnapshot, type MiniappRuntimeDiagnosticSnapshot} from "../utils/miniappDiagnostics"
|
|
71
74
|
import ttsModelManager from "./TTSModelManager"
|
|
72
75
|
import {NavigationHandlers} from "./NavigationHandlers"
|
|
73
76
|
import type {ClientApp} from "../types/applet"
|
|
74
77
|
import {useAppStatusStore} from "../stores/apps"
|
|
75
|
-
import {
|
|
78
|
+
import {getDevAppAttestation, getDevAppSourcePackage} from "./AppRegistry"
|
|
79
|
+
import {resolveForegroundLocationPermission} from "./ForegroundLocationPermission"
|
|
80
|
+
import {advanceMiniappPingLiveness} from "./MiniappLiveness"
|
|
81
|
+
import {listPhoneCalendarEvents, PhoneCalendarError} from "./PhoneCalendarService"
|
|
82
|
+
import {LocalMiniappStorage} from "./LocalMiniappStorage"
|
|
76
83
|
|
|
77
84
|
// =============================================================================
|
|
78
85
|
// Types
|
|
79
86
|
// =============================================================================
|
|
80
87
|
|
|
81
88
|
export interface InstalledMiniappManifest {
|
|
89
|
+
packageName?: string
|
|
82
90
|
/** Human-facing app name (from miniapp.json `name`). Shown in the "Starting
|
|
83
91
|
* <name>…" boot message; falls back to the package name when absent. */
|
|
84
92
|
name?: string
|
|
93
|
+
version?: string
|
|
94
|
+
sdkVersion?: string
|
|
95
|
+
minHostVersion?: string
|
|
96
|
+
type?: string
|
|
97
|
+
entry?: {background?: string; ui?: string}
|
|
85
98
|
permissions?: Array<{type: string; required?: boolean; description?: string}>
|
|
86
99
|
hardwareRequirements?: Array<{type: string; level: string; description?: string}>
|
|
100
|
+
actions?: Array<{id?: unknown; description?: unknown; parameters?: unknown; outputSchema?: unknown}>
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
type SpeakerStateValue = "idle" | "loading" | "playing" | "stopped" | "error"
|
|
90
104
|
|
|
91
105
|
interface ConnectedMiniapp {
|
|
92
106
|
subscriptions: Set<string>
|
|
107
|
+
/** Transcription streams this app explicitly requires to stay on-device. */
|
|
108
|
+
forceLocalTranscriptionStreams: Set<string>
|
|
109
|
+
/** Transcription streams with at least one listener using the default/cloud route. */
|
|
110
|
+
cloudTranscriptionStreams: Set<string>
|
|
93
111
|
sendMessage: (raw: string) => void
|
|
94
112
|
lastPongAt: number
|
|
95
|
-
|
|
96
|
-
bgcapLastPingSentAt?: number
|
|
113
|
+
unansweredPingRounds: number
|
|
97
114
|
installedManifest?: InstalledMiniappManifest
|
|
98
115
|
authRefreshTimerId: number | null
|
|
99
116
|
/**
|
|
@@ -122,6 +139,15 @@ interface ConnectedMiniapp {
|
|
|
122
139
|
requestedLocationRate: string | null
|
|
123
140
|
}
|
|
124
141
|
|
|
142
|
+
interface SpeechRun {
|
|
143
|
+
id: string
|
|
144
|
+
cancelled: boolean
|
|
145
|
+
playbackRequestId?: string
|
|
146
|
+
onCancelled?: () => void
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
type TranscriptionEventSource = "cloud" | "local"
|
|
150
|
+
|
|
125
151
|
/**
|
|
126
152
|
* Strictness ordering for `location_stream` rate values. Higher index =
|
|
127
153
|
* higher accuracy and higher power cost. The aggregate tier applied to
|
|
@@ -158,6 +184,16 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | nul
|
|
|
158
184
|
}
|
|
159
185
|
|
|
160
186
|
const LOG_TAG = "LOCAL_MINIAPP"
|
|
187
|
+
const DIAGNOSTIC_MAX_LIST_ITEMS = 100
|
|
188
|
+
const DIAGNOSTIC_MAX_STRING_LENGTH = 512
|
|
189
|
+
|
|
190
|
+
function diagnosticStringList(values: Iterable<string>): string[] {
|
|
191
|
+
return [...values]
|
|
192
|
+
.map((value) => value.slice(0, DIAGNOSTIC_MAX_STRING_LENGTH))
|
|
193
|
+
.sort()
|
|
194
|
+
.slice(0, DIAGNOSTIC_MAX_LIST_ITEMS)
|
|
195
|
+
}
|
|
196
|
+
|
|
161
197
|
const SYSTEM_MINIAPP_PACKAGES = new Set([
|
|
162
198
|
"com.mentra.camera",
|
|
163
199
|
"com.mentra.gallery",
|
|
@@ -189,81 +225,34 @@ const REQUEST_WIFI_SETUP_TYPE = "miniapp_request_wifi_setup"
|
|
|
189
225
|
// comes back automatically — this threshold only bounds how long that takes.
|
|
190
226
|
const PING_TIMEOUT_THRESHOLD = 6 // ~30s
|
|
191
227
|
|
|
192
|
-
// ===========================================================================
|
|
193
|
-
// BGCAP: temporary background-caption telemetry (DIAGNOSTIC — remove after the
|
|
194
|
-
// "captions fall behind then flood in waves" investigation lands a fix).
|
|
195
|
-
//
|
|
196
|
-
// Captions now run as a local miniapp inside the per-context JS engine
|
|
197
|
-
// (Crust: QuickJS on Android / JSC on iOS), driven off a single serial
|
|
198
|
-
// queue/executor with NO background awareness. The hypothesis is that the OS
|
|
199
|
-
// starves/freezes that queue when the screen is off (iOS app suspension /
|
|
200
|
-
// Android Doze), so inbound transcript deliveries pile up and drain in a burst
|
|
201
|
-
// on resume. These logs measure exactly that. All lines are prefixed "BGCAP:"
|
|
202
|
-
// so they're trivial to grep in incident logs and to delete. Cross-platform —
|
|
203
|
-
// host-side only, no native changes.
|
|
204
|
-
// ===========================================================================
|
|
205
|
-
const BGCAP_TELEMETRY = false
|
|
206
228
|
const TRANSCRIPT_TIMING_TELEMETRY = (globalThis as {__DEV__?: boolean}).__DEV__ === true
|
|
207
|
-
let bgcapHbTimer: ReturnType<typeof setInterval> | null = null
|
|
208
|
-
let bgcapHbLast = 0
|
|
209
|
-
let bgcapTxCount = 0
|
|
210
|
-
let bgcapTxLastFinalAt = 0
|
|
211
|
-
let bgcapTxLastLogAt = 0
|
|
212
|
-
|
|
213
|
-
function bgcapStartHeartbeat(): void {
|
|
214
|
-
if (!BGCAP_TELEMETRY || bgcapHbTimer !== null) return
|
|
215
|
-
bgcapHbLast = Date.now()
|
|
216
|
-
// PLAIN setInterval (NOT BgTimer): a plain JS timer only fires when the RN
|
|
217
|
-
// JS thread is actually executing. A gap >> 1s between ticks therefore proves
|
|
218
|
-
// the JS thread was suspended/throttled (iOS suspend / Android Doze) for that
|
|
219
|
-
// long — the smoking gun for the freeze. Silent when healthy; logs the stall
|
|
220
|
-
// duration when it happens. Pair with the "App state changed to:" lines.
|
|
221
|
-
bgcapHbTimer = setInterval(() => {
|
|
222
|
-
const now = Date.now()
|
|
223
|
-
const gap = now - bgcapHbLast
|
|
224
|
-
bgcapHbLast = now
|
|
225
|
-
if (gap > 2000) {
|
|
226
|
-
console.log(`BGCAP: host-js stall — no tick for ${gap}ms (RN JS thread frozen/throttled)`)
|
|
227
|
-
}
|
|
228
|
-
}, 1000)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function bgcapStopHeartbeat(): void {
|
|
232
|
-
if (bgcapHbTimer !== null) {
|
|
233
|
-
clearInterval(bgcapHbTimer)
|
|
234
|
-
bgcapHbTimer = null
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Rate-limited (1/s) summary of transcripts being FORWARDED to miniapps. Shows
|
|
239
|
-
// whether transcripts keep arriving + being handed to the captions miniapp
|
|
240
|
-
// during background. Compare its rate against the native "MAN: displayEvent"
|
|
241
|
-
// (render) rate: fwd keeps flowing but render lags => miniapp executor is
|
|
242
|
-
// starved; fwd itself stops => upstream (cloud delivery / RN thread) is the gap.
|
|
243
|
-
function bgcapNoteTranscriptForward(isFinal: boolean): void {
|
|
244
|
-
if (!BGCAP_TELEMETRY) return
|
|
245
|
-
bgcapTxCount++
|
|
246
|
-
if (isFinal) bgcapTxLastFinalAt = Date.now()
|
|
247
|
-
const now = Date.now()
|
|
248
|
-
if (now - bgcapTxLastLogAt >= 1000) {
|
|
249
|
-
console.log(
|
|
250
|
-
`BGCAP: tx forwarded=${bgcapTxCount} in last ${now - bgcapTxLastLogAt}ms (lastFinal ${
|
|
251
|
-
bgcapTxLastFinalAt ? now - bgcapTxLastFinalAt : -1
|
|
252
|
-
}ms ago)`,
|
|
253
|
-
)
|
|
254
|
-
bgcapTxCount = 0
|
|
255
|
-
bgcapTxLastLogAt = now
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
229
|
|
|
259
230
|
const RGB_LED_ACTIONS = new Set<RgbLedAction>(["on", "off"])
|
|
260
231
|
const RGB_LED_COLORS = new Set<RgbLedColor>(["red", "green", "blue", "orange", "white"])
|
|
261
232
|
const CAMERA_FOV_MIN = 62
|
|
262
233
|
const CAMERA_FOV_MAX = 118
|
|
263
|
-
const CAMERA_FOV_DEFAULT =
|
|
264
|
-
const CAMERA_FOV_PRESETS: Record<CameraFovPreset, number> = {narrow: 82, standard:
|
|
234
|
+
const CAMERA_FOV_DEFAULT = 118
|
|
235
|
+
const CAMERA_FOV_PRESETS: Record<CameraFovPreset, number> = {narrow: 82, standard: 102, wide: 118}
|
|
265
236
|
const CAMERA_ROI_POSITION_BY_NAME: Record<string, CameraRoiPosition> = {center: "center", bottom: "bottom", top: "top"}
|
|
266
|
-
|
|
237
|
+
|
|
238
|
+
const localMiniappStorageBackend = {
|
|
239
|
+
get(key: string): unknown | null {
|
|
240
|
+
const result = mmkvStorage.load<unknown>(key)
|
|
241
|
+
return result.is_ok() ? result.value : null
|
|
242
|
+
},
|
|
243
|
+
has(key: string): boolean {
|
|
244
|
+
return mmkvStorage.load<unknown>(key).is_ok()
|
|
245
|
+
},
|
|
246
|
+
set(key: string, value: unknown): void {
|
|
247
|
+
mmkvStorage.save(key, value)
|
|
248
|
+
},
|
|
249
|
+
remove(key: string): void {
|
|
250
|
+
mmkvStorage.remove(key)
|
|
251
|
+
},
|
|
252
|
+
keys(): string[] {
|
|
253
|
+
return mmkvStorage.getAllKeys()
|
|
254
|
+
},
|
|
255
|
+
}
|
|
267
256
|
|
|
268
257
|
// =============================================================================
|
|
269
258
|
// Declared-permission record helper (for CONNECT_ACK / PERMISSIONS_UPDATE)
|
|
@@ -406,11 +395,22 @@ class LocalMiniappRuntime {
|
|
|
406
395
|
/** Ref-counted stream subscriptions: stream → set of packageNames. */
|
|
407
396
|
private streamSubscribers: Map<string, Set<string>> = new Map()
|
|
408
397
|
|
|
398
|
+
/** Host observers for the Mentra Live button-capture policy. */
|
|
399
|
+
private buttonPressSubscriberListeners = new Set<(packageNames: string[]) => void>()
|
|
400
|
+
|
|
409
401
|
/** Guards one-time wiring of the cloud transcript/translation fan-out. */
|
|
410
402
|
private cloudResultsWired = false
|
|
411
403
|
private cloudStatusWired = false
|
|
412
404
|
private cloudAudioSubscriptionSync = new CloudAudioSubscriptionSync()
|
|
413
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Per-miniapp language hints from TRANSCRIPTION_CONFIG (issue 021 WP3).
|
|
408
|
+
* Bare ISO 639-1 codes, already registry-validated by the SDK and
|
|
409
|
+
* re-normalized here (defense in depth). Merged (union) into the auto-mode
|
|
410
|
+
* cloud transcription subscription in buildCloudAudioSubscriptions.
|
|
411
|
+
*/
|
|
412
|
+
private transcriptionHintsByApp = new Map<string, string[]>()
|
|
413
|
+
|
|
414
414
|
/** Ping interval handle. */
|
|
415
415
|
private pingIntervalId: number | null = null
|
|
416
416
|
private foregroundProbeTimers: Map<string, number> = new Map()
|
|
@@ -427,6 +427,7 @@ class LocalMiniappRuntime {
|
|
|
427
427
|
|
|
428
428
|
/** Pending cloud requests: requestId → packageName that originated the request. */
|
|
429
429
|
private pendingCloudRequests: Map<string, {packageName: string; envelopeRequestId?: string}> = new Map()
|
|
430
|
+
private speechRuns = new Map<string, SpeechRun>()
|
|
430
431
|
|
|
431
432
|
// Browser fallback token auth — HMAC-signed blob with a phone-local
|
|
432
433
|
// secret. Both issuer and verifier are the same process, so the
|
|
@@ -436,6 +437,99 @@ class LocalMiniappRuntime {
|
|
|
436
437
|
|
|
437
438
|
private constructor() {}
|
|
438
439
|
|
|
440
|
+
/** Snapshot of miniapps actively subscribed to hardware button events. */
|
|
441
|
+
public getButtonPressSubscribers(): string[] {
|
|
442
|
+
return [...(this.streamSubscribers.get(MiniappStreamType.BUTTON_PRESS) ?? [])].sort()
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Observe active hardware-button subscriptions. */
|
|
446
|
+
public onButtonPressSubscribersChanged(listener: (packageNames: string[]) => void): () => void {
|
|
447
|
+
this.buttonPressSubscriberListeners.add(listener)
|
|
448
|
+
return () => this.buttonPressSubscriberListeners.delete(listener)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Report-safe live miniapp/subscription/resource snapshot. */
|
|
452
|
+
public getDiagnosticSnapshot(nowMs = Date.now()): MiniappRuntimeDiagnosticSnapshot {
|
|
453
|
+
const connectedApps = [...this.connectedApps.entries()]
|
|
454
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
455
|
+
.slice(0, DIAGNOSTIC_MAX_LIST_ITEMS)
|
|
456
|
+
.map(([packageName, app]) => ({
|
|
457
|
+
packageName: packageName.slice(0, DIAGNOSTIC_MAX_STRING_LENGTH),
|
|
458
|
+
handshakeComplete: this.handshookApps.has(packageName),
|
|
459
|
+
subscriptions: diagnosticStringList(app.subscriptions),
|
|
460
|
+
lastMessageAgeMs: Math.max(0, nowMs - app.lastPongAt),
|
|
461
|
+
requestedLocationRate: app.requestedLocationRate,
|
|
462
|
+
hasActiveSpeakerStream: app.activeSpeakerStreamId !== undefined,
|
|
463
|
+
...(app.installedManifest
|
|
464
|
+
? {
|
|
465
|
+
manifest: buildMiniappManifestSnapshot(app.installedManifest, {
|
|
466
|
+
packageName,
|
|
467
|
+
name: app.installedManifest.name,
|
|
468
|
+
version: app.installedManifest.version,
|
|
469
|
+
type: app.installedManifest.type,
|
|
470
|
+
}),
|
|
471
|
+
}
|
|
472
|
+
: {}),
|
|
473
|
+
}))
|
|
474
|
+
const subscriptionsByStream = Object.fromEntries(
|
|
475
|
+
[...this.streamSubscribers.entries()]
|
|
476
|
+
.filter(([, packageNames]) => packageNames.size > 0)
|
|
477
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
478
|
+
.slice(0, DIAGNOSTIC_MAX_LIST_ITEMS)
|
|
479
|
+
.map(([stream, packageNames]) => [
|
|
480
|
+
stream.slice(0, DIAGNOSTIC_MAX_STRING_LENGTH),
|
|
481
|
+
diagnosticStringList(packageNames),
|
|
482
|
+
]),
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
connectedApps,
|
|
487
|
+
subscriptionsByStream,
|
|
488
|
+
resources: {
|
|
489
|
+
display: localDisplayManager.getDiagnosticSnapshot(),
|
|
490
|
+
camera: phonePhotoCoordinator.getDiagnosticSnapshot(),
|
|
491
|
+
video: phoneVideoCoordinator.getDiagnosticSnapshot(),
|
|
492
|
+
stream: phoneStreamCoordinator.getDiagnosticSnapshot(),
|
|
493
|
+
cameraFov: phoneCameraFovCoordinator.getDiagnosticSnapshot(),
|
|
494
|
+
},
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private replaceStreamSubscribers(
|
|
499
|
+
packageName: string,
|
|
500
|
+
previousStreams: Iterable<string>,
|
|
501
|
+
nextStreams: Iterable<string>,
|
|
502
|
+
) {
|
|
503
|
+
const previousButtonSubscribers = this.getButtonPressSubscribers().join("\u0000")
|
|
504
|
+
|
|
505
|
+
for (const stream of previousStreams) {
|
|
506
|
+
const subscribers = this.streamSubscribers.get(stream)
|
|
507
|
+
if (!subscribers) continue
|
|
508
|
+
subscribers.delete(packageName)
|
|
509
|
+
if (subscribers.size === 0) this.streamSubscribers.delete(stream)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
for (const stream of nextStreams) {
|
|
513
|
+
let subscribers = this.streamSubscribers.get(stream)
|
|
514
|
+
if (!subscribers) {
|
|
515
|
+
subscribers = new Set()
|
|
516
|
+
this.streamSubscribers.set(stream, subscribers)
|
|
517
|
+
}
|
|
518
|
+
subscribers.add(packageName)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const nextButtonSubscribers = this.getButtonPressSubscribers()
|
|
522
|
+
if (previousButtonSubscribers !== nextButtonSubscribers.join("\u0000")) {
|
|
523
|
+
for (const listener of this.buttonPressSubscriberListeners) {
|
|
524
|
+
try {
|
|
525
|
+
listener(nextButtonSubscribers)
|
|
526
|
+
} catch (error) {
|
|
527
|
+
console.warn(`${LOG_TAG}: button press subscriber listener threw:`, error)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
439
533
|
/**
|
|
440
534
|
* Generate an HMAC-signed local session token for browser fallback auth.
|
|
441
535
|
* Token format: base64(JSON({userId, packageName, exp})).base64(HMAC-SHA256(payload, secret))
|
|
@@ -640,6 +734,7 @@ class LocalMiniappRuntime {
|
|
|
640
734
|
// dangling references. The WebView will re-subscribe after its CONNECT.
|
|
641
735
|
if (this.connectedApps.has(packageName)) {
|
|
642
736
|
const existing = this.connectedApps.get(packageName)!
|
|
737
|
+
this.cancelSpeech(packageName)
|
|
643
738
|
// A crash respawn replaces the ConnectedMiniapp without going through
|
|
644
739
|
// unregisterApp(). Abort the old incarnation's native PCM player before
|
|
645
740
|
// its only stream id is lost. Target the exact stream rather than using
|
|
@@ -649,10 +744,7 @@ class LocalMiniappRuntime {
|
|
|
649
744
|
if (existingSpeakerStreamId) {
|
|
650
745
|
existing.activeSpeakerStreamId = undefined
|
|
651
746
|
void audioPlaybackService.abortStream(existingSpeakerStreamId).catch((error) => {
|
|
652
|
-
console.warn(
|
|
653
|
-
`${LOG_TAG}: failed to abort speaker stream while replacing ${packageName}`,
|
|
654
|
-
error,
|
|
655
|
-
)
|
|
747
|
+
console.warn(`${LOG_TAG}: failed to abort speaker stream while replacing ${packageName}`, error)
|
|
656
748
|
})
|
|
657
749
|
}
|
|
658
750
|
if (existing.authRefreshTimerId !== null) {
|
|
@@ -663,20 +755,17 @@ class LocalMiniappRuntime {
|
|
|
663
755
|
BgTimer.clearTimeout(existing.authRetryTimerId)
|
|
664
756
|
existing.authRetryTimerId = null
|
|
665
757
|
}
|
|
666
|
-
|
|
667
|
-
const subs = this.streamSubscribers.get(stream)
|
|
668
|
-
if (subs) {
|
|
669
|
-
subs.delete(packageName)
|
|
670
|
-
if (subs.size === 0) this.streamSubscribers.delete(stream)
|
|
671
|
-
}
|
|
672
|
-
}
|
|
758
|
+
this.replaceStreamSubscribers(packageName, existing.subscriptions, [])
|
|
673
759
|
this.recomputeMicRequirements()
|
|
674
760
|
this.updateCloudSubscriptions()
|
|
675
761
|
}
|
|
676
762
|
this.connectedApps.set(packageName, {
|
|
677
763
|
subscriptions: new Set(),
|
|
764
|
+
forceLocalTranscriptionStreams: new Set(),
|
|
765
|
+
cloudTranscriptionStreams: new Set(),
|
|
678
766
|
sendMessage: sendFn,
|
|
679
767
|
lastPongAt: Date.now(),
|
|
768
|
+
unansweredPingRounds: 0,
|
|
680
769
|
installedManifest,
|
|
681
770
|
authRefreshTimerId: null,
|
|
682
771
|
authRetryTimerId: null,
|
|
@@ -797,6 +886,7 @@ class LocalMiniappRuntime {
|
|
|
797
886
|
|
|
798
887
|
public unregisterApp(packageName: string): void {
|
|
799
888
|
console.log(`${LOG_TAG}: unregisterApp(${packageName})`)
|
|
889
|
+
const releasedMicGateOverride = micStateCoordinator.clearMiniappGateOverrides(packageName)
|
|
800
890
|
this.clearForegroundProbe(packageName)
|
|
801
891
|
this.clearMiniappAuthRefresh(packageName)
|
|
802
892
|
this.clearMiniappAuthDeliveryRetry(packageName)
|
|
@@ -805,23 +895,44 @@ class LocalMiniappRuntime {
|
|
|
805
895
|
// the early-return so it runs even if the connectedApps entry is already gone.
|
|
806
896
|
this.handshookApps.delete(packageName)
|
|
807
897
|
this.flushConnectWaiters(packageName, new Error(`${packageName} unregistered before connect`))
|
|
898
|
+
// Drop this app's transcription hints; the cloud subscription rebuild
|
|
899
|
+
// below (via replaceStreamSubscribers) re-derives the hint union without it.
|
|
900
|
+
this.transcriptionHintsByApp.delete(packageName)
|
|
901
|
+
|
|
902
|
+
// Warm-up is a request-owned camera lease. Cancel it even if the app record was already
|
|
903
|
+
// removed so a close racing the camera-open promise cannot leave the sensor running.
|
|
904
|
+
// Keep camera teardown ordered: restoring FOV can restart the HAL, so the warm-up lease must
|
|
905
|
+
// finish closing the sensor before FOV reconciliation begins. Continue to FOV cleanup even if
|
|
906
|
+
// warm-up cancellation fails; phone ownership remains retryable until the ASG lease TTL.
|
|
907
|
+
void phonePhotoCoordinator
|
|
908
|
+
.stopWarmUpForApp(packageName)
|
|
909
|
+
.catch((error) => {
|
|
910
|
+
console.warn(`${LOG_TAG}: failed to stop camera warm-up for ${packageName} on unregister`, error)
|
|
911
|
+
})
|
|
912
|
+
.then(() => phoneCameraFovCoordinator.releaseForApp(packageName))
|
|
913
|
+
.catch((error) => {
|
|
914
|
+
console.warn(`${LOG_TAG}: failed to release camera FOV override for ${packageName} on unregister`, error)
|
|
915
|
+
})
|
|
808
916
|
const app = this.connectedApps.get(packageName)
|
|
809
|
-
if (!app)
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
if (subs) {
|
|
815
|
-
subs.delete(packageName)
|
|
816
|
-
if (subs.size === 0) {
|
|
817
|
-
this.streamSubscribers.delete(stream)
|
|
818
|
-
}
|
|
917
|
+
if (!app) {
|
|
918
|
+
if (releasedMicGateOverride) {
|
|
919
|
+
void micStateCoordinator.syncEffectiveGatePolicy(this.getConfiguredMicGates()).catch((error) => {
|
|
920
|
+
console.warn(`${LOG_TAG}: failed to restore mic gate policy for ${packageName}`, error)
|
|
921
|
+
})
|
|
819
922
|
}
|
|
923
|
+
return
|
|
820
924
|
}
|
|
821
925
|
|
|
926
|
+
// Remove from all stream subscriber sets
|
|
927
|
+
this.replaceStreamSubscribers(packageName, app.subscriptions, [])
|
|
928
|
+
|
|
822
929
|
// Reset per-session warning dedup so a relaunch surfaces issues again.
|
|
823
930
|
resetPermissionWarnings(packageName)
|
|
824
931
|
|
|
932
|
+
// Cancel lookahead/in-flight synthesis before stopping the current file so
|
|
933
|
+
// an interrupted sentence cannot advance and reclaim playback.
|
|
934
|
+
this.cancelSpeech(packageName)
|
|
935
|
+
|
|
825
936
|
// Stop audio for this app
|
|
826
937
|
audioPlaybackService.stopForApp(packageName)
|
|
827
938
|
|
|
@@ -880,6 +991,11 @@ class LocalMiniappRuntime {
|
|
|
880
991
|
|
|
881
992
|
this.connectedApps.delete(packageName)
|
|
882
993
|
this.recomputeMicRequirements()
|
|
994
|
+
if (releasedMicGateOverride) {
|
|
995
|
+
void micStateCoordinator.syncEffectiveGatePolicy(this.getConfiguredMicGates()).catch((error) => {
|
|
996
|
+
console.warn(`${LOG_TAG}: failed to restore mic gate policy for ${packageName}`, error)
|
|
997
|
+
})
|
|
998
|
+
}
|
|
883
999
|
this.updateCloudSubscriptions()
|
|
884
1000
|
|
|
885
1001
|
if (this.connectedApps.size === 0) {
|
|
@@ -984,6 +1100,9 @@ class LocalMiniappRuntime {
|
|
|
984
1100
|
case MiniappRequestType.LOCATION_POLL:
|
|
985
1101
|
this.handleLocationPoll(packageName, requestId)
|
|
986
1102
|
break
|
|
1103
|
+
case MiniappRequestType.CALENDAR_LIST_EVENTS:
|
|
1104
|
+
void this.handleCalendarListEvents(packageName, payload, requestId)
|
|
1105
|
+
break
|
|
987
1106
|
case MiniappRequestType.NAVIGATION_START:
|
|
988
1107
|
this.navigationHandlers.handleStart(packageName, payload, requestId)
|
|
989
1108
|
break
|
|
@@ -1050,9 +1169,18 @@ class LocalMiniappRuntime {
|
|
|
1050
1169
|
case MiniappRequestType.CAMERA_FOV:
|
|
1051
1170
|
void this.handleCameraFov(packageName, payload, requestId)
|
|
1052
1171
|
break
|
|
1172
|
+
case MiniappRequestType.SET_WIFI_ADB_STATE:
|
|
1173
|
+
void this.handleSetWifiAdbState(packageName, payload, requestId)
|
|
1174
|
+
break
|
|
1053
1175
|
case MiniappRequestType.IMU_SET_ENABLED:
|
|
1054
1176
|
this.handleImuSetEnabled(packageName, payload, requestId)
|
|
1055
1177
|
break
|
|
1178
|
+
case MiniappRequestType.MIC_SET_VAD_ENABLED:
|
|
1179
|
+
void this.handleMicSetVadEnabled(packageName, payload, requestId)
|
|
1180
|
+
break
|
|
1181
|
+
case MiniappRequestType.MIC_SET_LOUDNESS_GATE_ENABLED:
|
|
1182
|
+
void this.handleMicSetLoudnessGateEnabled(packageName, payload, requestId)
|
|
1183
|
+
break
|
|
1056
1184
|
case MiniappRequestType.PING:
|
|
1057
1185
|
// SDK should handle this itself; reply PONG just in case
|
|
1058
1186
|
this.sendToMiniapp(packageName, {type: MiniappResponseType.PONG}, requestId)
|
|
@@ -1060,23 +1188,6 @@ class LocalMiniappRuntime {
|
|
|
1060
1188
|
case MiniappResponseType.PONG: {
|
|
1061
1189
|
// Liveness already touched above for every inbound message; the
|
|
1062
1190
|
// PONG carries no other action.
|
|
1063
|
-
// BGCAP: measure PING→PONG round-trip ONLY here (a real pong), not in
|
|
1064
|
-
// handlePong (which fires for every inbound envelope). RTT = how long
|
|
1065
|
-
// the miniapp's own JS engine (Crust serial queue) took to answer — a
|
|
1066
|
-
// large value means that queue was starved (the transcript→display
|
|
1067
|
-
// layer). bgcapLastPingSentAt holds the OLDEST outstanding ping (see
|
|
1068
|
-
// doPingRound), so a multi-round background stall reports its full
|
|
1069
|
-
// duration on the first pong after resume.
|
|
1070
|
-
if (BGCAP_TELEMETRY) {
|
|
1071
|
-
const app = this.connectedApps.get(packageName)
|
|
1072
|
-
if (app?.bgcapLastPingSentAt != null) {
|
|
1073
|
-
const rtt = Date.now() - app.bgcapLastPingSentAt
|
|
1074
|
-
if (rtt > 1500) {
|
|
1075
|
-
console.log(`BGCAP: ${packageName} pong rtt=${rtt}ms (miniapp JS engine was starved)`)
|
|
1076
|
-
}
|
|
1077
|
-
app.bgcapLastPingSentAt = undefined
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
1191
|
break
|
|
1081
1192
|
}
|
|
1082
1193
|
|
|
@@ -1092,52 +1203,83 @@ class LocalMiniappRuntime {
|
|
|
1092
1203
|
case MiniappRequestType.DOWNLOAD:
|
|
1093
1204
|
this.handleDownload(packageName, payload, requestId)
|
|
1094
1205
|
break
|
|
1206
|
+
case MiniappRequestType.SCAN_QR:
|
|
1207
|
+
void this.handleScanQr(packageName, payload, requestId)
|
|
1208
|
+
break
|
|
1095
1209
|
|
|
1096
1210
|
// Persistent binary blob storage (session.blob)
|
|
1097
1211
|
case MiniappRequestType.BLOB_CREATE:
|
|
1098
|
-
this.
|
|
1212
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1213
|
+
this.blobStore.handleCreate(packageName, payload, requestId),
|
|
1214
|
+
)
|
|
1099
1215
|
break
|
|
1100
1216
|
case MiniappRequestType.BLOB_WRITE:
|
|
1101
|
-
this.
|
|
1217
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1218
|
+
this.blobStore.handleWrite(packageName, payload, requestId),
|
|
1219
|
+
)
|
|
1102
1220
|
break
|
|
1103
1221
|
case MiniappRequestType.BLOB_COMMIT:
|
|
1104
|
-
this.
|
|
1222
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1223
|
+
this.blobStore.handleCommit(packageName, payload, requestId),
|
|
1224
|
+
)
|
|
1105
1225
|
break
|
|
1106
1226
|
case MiniappRequestType.BLOB_ABORT:
|
|
1107
|
-
this.
|
|
1227
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1228
|
+
this.blobStore.handleAbort(packageName, payload, requestId),
|
|
1229
|
+
)
|
|
1108
1230
|
break
|
|
1109
1231
|
case MiniappRequestType.BLOB_SET_FROM_URL:
|
|
1110
|
-
|
|
1232
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1233
|
+
this.blobStore.handleSetFromUrl(packageName, payload, requestId),
|
|
1234
|
+
)
|
|
1111
1235
|
break
|
|
1112
1236
|
case MiniappRequestType.BLOB_IMPORT:
|
|
1113
|
-
|
|
1237
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1238
|
+
this.blobStore.handleImport(packageName, payload, requestId),
|
|
1239
|
+
)
|
|
1114
1240
|
break
|
|
1115
1241
|
case MiniappRequestType.BLOB_GET:
|
|
1116
|
-
this.blobStore.handleGet(packageName, payload, requestId)
|
|
1242
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleGet(packageName, payload, requestId))
|
|
1117
1243
|
break
|
|
1118
1244
|
case MiniappRequestType.BLOB_LIST:
|
|
1119
|
-
this.
|
|
1245
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1246
|
+
this.blobStore.handleList(packageName, payload, requestId),
|
|
1247
|
+
)
|
|
1120
1248
|
break
|
|
1121
1249
|
case MiniappRequestType.BLOB_USAGE:
|
|
1122
|
-
this.
|
|
1250
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1251
|
+
this.blobStore.handleUsage(packageName, payload, requestId),
|
|
1252
|
+
)
|
|
1123
1253
|
break
|
|
1124
1254
|
case MiniappRequestType.BLOB_DELETE:
|
|
1125
|
-
this.
|
|
1255
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1256
|
+
this.blobStore.handleDelete(packageName, payload, requestId),
|
|
1257
|
+
)
|
|
1126
1258
|
break
|
|
1127
1259
|
case MiniappRequestType.BLOB_CLEAR:
|
|
1128
|
-
this.
|
|
1260
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1261
|
+
this.blobStore.handleClear(packageName, payload, requestId),
|
|
1262
|
+
)
|
|
1129
1263
|
break
|
|
1130
1264
|
case MiniappRequestType.BLOB_OPEN_READ:
|
|
1131
|
-
this.
|
|
1265
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1266
|
+
this.blobStore.handleOpenRead(packageName, payload, requestId),
|
|
1267
|
+
)
|
|
1132
1268
|
break
|
|
1133
1269
|
case MiniappRequestType.BLOB_READ:
|
|
1134
|
-
this.
|
|
1270
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1271
|
+
this.blobStore.handleRead(packageName, payload, requestId),
|
|
1272
|
+
)
|
|
1135
1273
|
break
|
|
1136
1274
|
case MiniappRequestType.BLOB_CLOSE_READ:
|
|
1137
|
-
this.
|
|
1275
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1276
|
+
this.blobStore.handleCloseRead(packageName, payload, requestId),
|
|
1277
|
+
)
|
|
1138
1278
|
break
|
|
1139
1279
|
case MiniappRequestType.BLOB_SHARE:
|
|
1140
|
-
|
|
1280
|
+
this.enqueueBlobRequest(packageName, requestId, () =>
|
|
1281
|
+
this.blobStore.handleShare(packageName, payload, requestId),
|
|
1282
|
+
)
|
|
1141
1283
|
break
|
|
1142
1284
|
|
|
1143
1285
|
// Cloud-coordinated features
|
|
@@ -1194,8 +1336,20 @@ class LocalMiniappRuntime {
|
|
|
1194
1336
|
})
|
|
1195
1337
|
break
|
|
1196
1338
|
|
|
1339
|
+
case MiniappRequestType.TRANSCRIPTION_CONFIG:
|
|
1340
|
+
this.handleTranscriptionConfig(packageName, payload, requestId)
|
|
1341
|
+
break
|
|
1342
|
+
|
|
1197
1343
|
default:
|
|
1344
|
+
// NACK instead of silent drop (issue 021 S3): an SDK that sends a
|
|
1345
|
+
// request this runtime doesn't implement must get a rejected promise,
|
|
1346
|
+
// not a permanently-pending silent gap. One-shots (no requestId) can
|
|
1347
|
+
// only be logged.
|
|
1198
1348
|
console.warn(`${LOG_TAG}: Unknown request type from ${packageName}: ${requestType}`)
|
|
1349
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
1350
|
+
code: MiniappErrorCode.NOT_IMPLEMENTED,
|
|
1351
|
+
message: `Unknown or unimplemented request type: ${requestType}`,
|
|
1352
|
+
})
|
|
1199
1353
|
break
|
|
1200
1354
|
}
|
|
1201
1355
|
}
|
|
@@ -1220,6 +1374,7 @@ class LocalMiniappRuntime {
|
|
|
1220
1374
|
|
|
1221
1375
|
// Update lastPongAt so it doesn't time out right away
|
|
1222
1376
|
existing.lastPongAt = Date.now()
|
|
1377
|
+
existing.unansweredPingRounds = 0
|
|
1223
1378
|
|
|
1224
1379
|
// Read current glasses capabilities from the settings store
|
|
1225
1380
|
const defaultWearable = useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.defaultWearable) as
|
|
@@ -1276,9 +1431,12 @@ class LocalMiniappRuntime {
|
|
|
1276
1431
|
}
|
|
1277
1432
|
|
|
1278
1433
|
private async requestMiniappAuth(packageName: string, opts?: {minTtlMs?: number}): Promise<MiniappAuthToken | null> {
|
|
1279
|
-
const authPackageName =
|
|
1280
|
-
|
|
1281
|
-
return cloudClientService.getMiniappAuthToken(authPackageName,
|
|
1434
|
+
const authPackageName = getDevAppSourcePackage(packageName) ?? packageName
|
|
1435
|
+
const devAttestation = getDevAppAttestation(packageName) ?? undefined
|
|
1436
|
+
return cloudClientService.getMiniappAuthToken(authPackageName, {
|
|
1437
|
+
...opts,
|
|
1438
|
+
...(devAttestation ? {devAttestation} : {}),
|
|
1439
|
+
})
|
|
1282
1440
|
}
|
|
1283
1441
|
|
|
1284
1442
|
private async handleAuthRefresh(
|
|
@@ -1424,18 +1582,65 @@ class LocalMiniappRuntime {
|
|
|
1424
1582
|
}
|
|
1425
1583
|
}
|
|
1426
1584
|
|
|
1585
|
+
/**
|
|
1586
|
+
* TRANSCRIPTION_CONFIG (issue 021 WP3): store the miniapp's language hints
|
|
1587
|
+
* and rebuild the cloud subscription set so the auto-mode subscription
|
|
1588
|
+
* carries them. Replies with a REQUEST_RESULT so the SDK's configure()
|
|
1589
|
+
* promise settles: unknown hint values are an error, not a silent drop.
|
|
1590
|
+
*/
|
|
1591
|
+
private handleTranscriptionConfig(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
|
|
1592
|
+
const app = this.connectedApps.get(packageName)
|
|
1593
|
+
if (!app) return
|
|
1594
|
+
|
|
1595
|
+
const config = (payload.config ?? {}) as Record<string, unknown>
|
|
1596
|
+
const rawHints = config.languageHints
|
|
1597
|
+
if (rawHints !== undefined && !Array.isArray(rawHints)) {
|
|
1598
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
1599
|
+
code: MiniappErrorCode.INTERNAL,
|
|
1600
|
+
message: "languageHints must be an array of language codes",
|
|
1601
|
+
})
|
|
1602
|
+
return
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
if (rawHints === undefined || rawHints.length === 0) {
|
|
1606
|
+
this.transcriptionHintsByApp.delete(packageName)
|
|
1607
|
+
} else {
|
|
1608
|
+
// Defense in depth: the SDK already validated, but the wire is untyped.
|
|
1609
|
+
const hints: string[] = []
|
|
1610
|
+
const invalid: string[] = []
|
|
1611
|
+
for (const raw of rawHints as unknown[]) {
|
|
1612
|
+
const hint = typeof raw === "string" ? toLanguageHint(raw) : null
|
|
1613
|
+
if (hint) hints.push(hint)
|
|
1614
|
+
else invalid.push(String(raw))
|
|
1615
|
+
}
|
|
1616
|
+
if (invalid.length > 0) {
|
|
1617
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
1618
|
+
code: MiniappErrorCode.INTERNAL,
|
|
1619
|
+
message: `Unknown language hint(s): ${invalid.join(", ")}`,
|
|
1620
|
+
})
|
|
1621
|
+
return
|
|
1622
|
+
}
|
|
1623
|
+
this.transcriptionHintsByApp.set(packageName, [...new Set(hints)].sort())
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
this.updateCloudSubscriptions()
|
|
1627
|
+
this.sendResult(packageName, requestId, true, {applied: true})
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1427
1630
|
private handleSubscribe(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
|
|
1428
1631
|
const app = this.connectedApps.get(packageName)
|
|
1429
1632
|
if (!app) return
|
|
1430
1633
|
|
|
1431
1634
|
const rawStreams = (payload.subscriptions ?? payload.streams) as
|
|
1432
|
-
| (string | {stream: string; rate?: string})[]
|
|
1635
|
+
| (string | {stream: string; rate?: string; forceLocal?: boolean; includeCloud?: boolean})[]
|
|
1433
1636
|
| undefined
|
|
1434
1637
|
// Normalize: objects like {stream: "location_stream", rate: "realtime"} → extract stream name
|
|
1435
1638
|
// "location_stream" is the rate-bearing alias for "location_update".
|
|
1436
1639
|
// The strictest rate wins when multiple `location_stream` entries appear
|
|
1437
1640
|
// in one SUBSCRIBE — the same rule we apply across apps.
|
|
1438
1641
|
let requestedLocationRate: string | null = null
|
|
1642
|
+
const forceLocalTranscriptionStreams = new Set<string>()
|
|
1643
|
+
const cloudTranscriptionStreams = new Set<string>()
|
|
1439
1644
|
const streams = rawStreams?.map((s) => {
|
|
1440
1645
|
if (typeof s === "object" && s !== null) {
|
|
1441
1646
|
if (s.stream === "location_stream") {
|
|
@@ -1444,8 +1649,15 @@ class LocalMiniappRuntime {
|
|
|
1444
1649
|
}
|
|
1445
1650
|
return "location_update"
|
|
1446
1651
|
}
|
|
1652
|
+
if (s.forceLocal === true && s.stream.startsWith("transcription:")) {
|
|
1653
|
+
forceLocalTranscriptionStreams.add(s.stream)
|
|
1654
|
+
}
|
|
1655
|
+
if (s.stream.startsWith("transcription:") && (s.forceLocal !== true || s.includeCloud === true)) {
|
|
1656
|
+
cloudTranscriptionStreams.add(s.stream)
|
|
1657
|
+
}
|
|
1447
1658
|
return s.stream
|
|
1448
1659
|
}
|
|
1660
|
+
if (s.startsWith("transcription:")) cloudTranscriptionStreams.add(s)
|
|
1449
1661
|
return s
|
|
1450
1662
|
}) as string[] | undefined
|
|
1451
1663
|
if (!Array.isArray(streams)) {
|
|
@@ -1469,7 +1681,6 @@ class LocalMiniappRuntime {
|
|
|
1469
1681
|
if (s === "location_update") return "LOCATION"
|
|
1470
1682
|
if (s === "phone_notification") return "READ_NOTIFICATIONS"
|
|
1471
1683
|
if (s === "phone_notification_dismissed") return "READ_NOTIFICATIONS"
|
|
1472
|
-
if (s === "calendar_event") return "CALENDAR"
|
|
1473
1684
|
return null
|
|
1474
1685
|
}
|
|
1475
1686
|
|
|
@@ -1489,27 +1700,16 @@ class LocalMiniappRuntime {
|
|
|
1489
1700
|
}
|
|
1490
1701
|
}
|
|
1491
1702
|
|
|
1492
|
-
// Remove old subscriptions for this app
|
|
1493
|
-
for (const oldStream of app.subscriptions) {
|
|
1494
|
-
const subs = this.streamSubscribers.get(oldStream)
|
|
1495
|
-
if (subs) {
|
|
1496
|
-
subs.delete(packageName)
|
|
1497
|
-
if (subs.size === 0) {
|
|
1498
|
-
this.streamSubscribers.delete(oldStream)
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
1703
|
// Add new subscriptions
|
|
1704
|
+
const previousSubscriptions = app.subscriptions
|
|
1504
1705
|
app.subscriptions = new Set(streams)
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
}
|
|
1706
|
+
app.forceLocalTranscriptionStreams = new Set(
|
|
1707
|
+
[...forceLocalTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)),
|
|
1708
|
+
)
|
|
1709
|
+
app.cloudTranscriptionStreams = new Set(
|
|
1710
|
+
[...cloudTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)),
|
|
1711
|
+
)
|
|
1712
|
+
this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions)
|
|
1513
1713
|
|
|
1514
1714
|
this.recomputeMicRequirements()
|
|
1515
1715
|
this.updateCloudSubscriptions()
|
|
@@ -1527,6 +1727,37 @@ class LocalMiniappRuntime {
|
|
|
1527
1727
|
this.emitInitialSnapshots(packageName, streams)
|
|
1528
1728
|
}
|
|
1529
1729
|
|
|
1730
|
+
private async handleCalendarListEvents(
|
|
1731
|
+
packageName: string,
|
|
1732
|
+
payload: Record<string, unknown>,
|
|
1733
|
+
requestId?: string,
|
|
1734
|
+
): Promise<void> {
|
|
1735
|
+
const app = this.connectedApps.get(packageName)
|
|
1736
|
+
const declared = app?.installedManifest?.permissions?.some(
|
|
1737
|
+
(permission) => permission.type?.toUpperCase() === "CALENDAR",
|
|
1738
|
+
)
|
|
1739
|
+
if (!declared) {
|
|
1740
|
+
logPermissionNotDeclared(packageName, "CALENDAR", "to list calendar events", '{"type": "CALENDAR"}')
|
|
1741
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
1742
|
+
code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
|
|
1743
|
+
message: 'CALENDAR permission not declared in miniapp.json. Add {"type": "CALENDAR"} to the permissions array.',
|
|
1744
|
+
permission: "CALENDAR",
|
|
1745
|
+
})
|
|
1746
|
+
return
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
try {
|
|
1750
|
+
const result = await listPhoneCalendarEvents(payload)
|
|
1751
|
+
this.sendResult(packageName, requestId, true, result)
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
const known = error instanceof PhoneCalendarError
|
|
1754
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
1755
|
+
code: known ? error.code : MiniappErrorCode.INTERNAL,
|
|
1756
|
+
message: error instanceof Error ? error.message : "Failed to read phone calendar events",
|
|
1757
|
+
})
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1530
1761
|
/**
|
|
1531
1762
|
* For state-bearing streams (battery, connection), deliver the current value
|
|
1532
1763
|
* immediately on subscribe. Other streams (button press, transcription, etc.)
|
|
@@ -1725,6 +1956,7 @@ class LocalMiniappRuntime {
|
|
|
1725
1956
|
const volume = typeof payload.volume === "number" ? payload.volume : 1.0
|
|
1726
1957
|
const stopOtherAudio = payload.stopOtherAudio !== false
|
|
1727
1958
|
|
|
1959
|
+
this.cancelSpeech(packageName)
|
|
1728
1960
|
this.setSpeakerState(packageName, "loading")
|
|
1729
1961
|
audioPlaybackService.play(
|
|
1730
1962
|
{requestId: audioRequestId, audioUrl, appId: packageName, volume, stopOtherAudio},
|
|
@@ -1760,6 +1992,7 @@ class LocalMiniappRuntime {
|
|
|
1760
1992
|
}
|
|
1761
1993
|
|
|
1762
1994
|
private handleStopAudio(packageName: string, _payload: Record<string, unknown>, requestId?: string): void {
|
|
1995
|
+
this.cancelSpeech(packageName)
|
|
1763
1996
|
audioPlaybackService.stopForApp(packageName)
|
|
1764
1997
|
this.setSpeakerState(packageName, "stopped")
|
|
1765
1998
|
this.sendResult(packageName, requestId, true)
|
|
@@ -1965,108 +2198,221 @@ class LocalMiniappRuntime {
|
|
|
1965
2198
|
}
|
|
1966
2199
|
|
|
1967
2200
|
private async handleSpeak(packageName: string, payload: Record<string, unknown>, requestId?: string): Promise<void> {
|
|
1968
|
-
const
|
|
1969
|
-
|
|
2201
|
+
const rawText = payload.text
|
|
2202
|
+
const rawSentences =
|
|
2203
|
+
typeof rawText === "string"
|
|
2204
|
+
? [rawText.trim()].filter(Boolean)
|
|
2205
|
+
: Array.isArray(rawText)
|
|
2206
|
+
? rawText
|
|
2207
|
+
.filter((sentence): sentence is string => typeof sentence === "string")
|
|
2208
|
+
.map((sentence) => sentence.trim())
|
|
2209
|
+
.filter(Boolean)
|
|
2210
|
+
: []
|
|
2211
|
+
const enableSanitization = payload.enableSanitization !== false
|
|
2212
|
+
const sentences = prepareTtsSentences(rawSentences, enableSanitization)
|
|
2213
|
+
if (sentences.length === 0) {
|
|
1970
2214
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
1971
2215
|
code: MiniappErrorCode.INTERNAL,
|
|
1972
|
-
message: "speak requires text",
|
|
2216
|
+
message: "speak requires text or a non-empty sentence list with speakable content",
|
|
1973
2217
|
})
|
|
1974
2218
|
return
|
|
1975
2219
|
}
|
|
2220
|
+
const cloudText = sentences.join(" ")
|
|
1976
2221
|
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
payload.voice_settings
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
const offlineSupportsVoice =
|
|
1992
|
-
!voiceExplicit || voice === "default" || ttsModelManager.getAvailableLanguages().some((l) => l.code === voice)
|
|
1993
|
-
|
|
1994
|
-
const modelId = typeof payload.model_id === "string" ? payload.model_id : undefined
|
|
1995
|
-
const cloudConnected = cloudClientService.isConnected()
|
|
1996
|
-
console.log(
|
|
1997
|
-
`${LOG_TAG}: TTS decision for ${packageName}: cloudConnected=${cloudConnected}, runtimeTts=yes, offlineVoice=${offlineSupportsVoice}`,
|
|
1998
|
-
)
|
|
2222
|
+
const voice = ((payload.voice_id ?? payload.voice) as string) || "default"
|
|
2223
|
+
const audioRequestId = requestId || `tts_${Date.now()}`
|
|
2224
|
+
const volume = typeof payload.volume === "number" ? payload.volume : 1.0
|
|
2225
|
+
const stopOtherAudio = payload.stopOtherAudio !== false
|
|
2226
|
+
const voiceSettings =
|
|
2227
|
+
payload.voice_settings && typeof payload.voice_settings === "object"
|
|
2228
|
+
? (payload.voice_settings as Record<string, unknown>)
|
|
2229
|
+
: undefined
|
|
2230
|
+
const speed = typeof voiceSettings?.speed === "number" ? voiceSettings.speed : undefined
|
|
2231
|
+
const run: SpeechRun = {id: audioRequestId, cancelled: false}
|
|
2232
|
+
|
|
2233
|
+
this.cancelSpeech(packageName)
|
|
2234
|
+
this.speechRuns.set(packageName, run)
|
|
2235
|
+
this.setSpeakerState(packageName, "loading")
|
|
1999
2236
|
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2237
|
+
let terminalSent = false
|
|
2238
|
+
const sendPlaybackResult = (
|
|
2239
|
+
success: boolean,
|
|
2240
|
+
error: string | null,
|
|
2241
|
+
duration: number | null,
|
|
2242
|
+
fallbackErrorMessage: string,
|
|
2243
|
+
completed = success,
|
|
2244
|
+
errorCode: MiniappErrorCode = MiniappErrorCode.TTS_UPSTREAM_ERROR,
|
|
2245
|
+
) => {
|
|
2246
|
+
if (terminalSent) return
|
|
2247
|
+
terminalSent = true
|
|
2248
|
+
|
|
2249
|
+
// A superseded run still owns its bridge request, but it no longer owns
|
|
2250
|
+
// the package's shared speaker state.
|
|
2251
|
+
const ownsSpeakerState = this.speechRuns.get(packageName) === run
|
|
2252
|
+
if (ownsSpeakerState) this.speechRuns.delete(packageName)
|
|
2253
|
+
|
|
2254
|
+
if (ownsSpeakerState) {
|
|
2009
2255
|
if (success) {
|
|
2010
2256
|
this.setSpeakerState(packageName, "stopped", {durationMs: duration ?? undefined})
|
|
2011
2257
|
} else {
|
|
2012
2258
|
this.setSpeakerState(packageName, "error", {
|
|
2013
|
-
errorCode
|
|
2259
|
+
errorCode,
|
|
2014
2260
|
errorMessage: error ?? fallbackErrorMessage,
|
|
2015
2261
|
durationMs: duration ?? undefined,
|
|
2016
2262
|
})
|
|
2017
2263
|
}
|
|
2018
|
-
this.sendResult(
|
|
2019
|
-
packageName,
|
|
2020
|
-
requestId,
|
|
2021
|
-
success,
|
|
2022
|
-
{completed: success, duration},
|
|
2023
|
-
error
|
|
2024
|
-
? {
|
|
2025
|
-
code: MiniappErrorCode.TTS_UPSTREAM_ERROR,
|
|
2026
|
-
message: error,
|
|
2027
|
-
}
|
|
2028
|
-
: undefined,
|
|
2029
|
-
)
|
|
2030
2264
|
}
|
|
2031
2265
|
|
|
2266
|
+
this.sendResult(
|
|
2267
|
+
packageName,
|
|
2268
|
+
requestId,
|
|
2269
|
+
success,
|
|
2270
|
+
{completed, duration},
|
|
2271
|
+
error
|
|
2272
|
+
? {
|
|
2273
|
+
code: errorCode,
|
|
2274
|
+
message: error,
|
|
2275
|
+
}
|
|
2276
|
+
: undefined,
|
|
2277
|
+
)
|
|
2278
|
+
}
|
|
2279
|
+
run.onCancelled = () => sendPlaybackResult(true, null, null, "tts playback stopped", false)
|
|
2280
|
+
|
|
2281
|
+
try {
|
|
2282
|
+
const voiceExplicit = payload.voice_id !== undefined || payload.voice !== undefined
|
|
2283
|
+
const offlineSupportsVoice =
|
|
2284
|
+
!voiceExplicit || voice === "default" || ttsModelManager.getAvailableLanguages().some((l) => l.code === voice)
|
|
2285
|
+
|
|
2286
|
+
const modelId = typeof payload.model_id === "string" ? payload.model_id : undefined
|
|
2287
|
+
const forceLocal = payload.forceLocal === true
|
|
2288
|
+
const cloudConnected = cloudClientService.isConnected()
|
|
2289
|
+
console.log(
|
|
2290
|
+
`${LOG_TAG}: TTS decision for ${packageName}: cloudConnected=${cloudConnected}, runtimeTts=yes, offlineVoice=${offlineSupportsVoice}, forceLocal=${forceLocal}`,
|
|
2291
|
+
)
|
|
2292
|
+
|
|
2032
2293
|
const playOfflineTts = async (reason?: string): Promise<boolean> => {
|
|
2294
|
+
if (run.cancelled) return true
|
|
2033
2295
|
if (!offlineSupportsVoice || !(await ttsModelManager.isModelAvailable())) {
|
|
2034
2296
|
return false
|
|
2035
2297
|
}
|
|
2298
|
+
if (run.cancelled) return true
|
|
2299
|
+
|
|
2300
|
+
const languageCode = ttsModelManager.getAvailableLanguages().some((l) => l.code === voice) ? voice : undefined
|
|
2301
|
+
const offlineSentences = sentences
|
|
2302
|
+
|
|
2303
|
+
if (offlineSentences.length > 1) {
|
|
2304
|
+
let firstSentence: TtsSynthesisResult
|
|
2305
|
+
try {
|
|
2306
|
+
firstSentence = await ttsModelManager.synthesizeToFile(offlineSentences[0], {languageCode, speed})
|
|
2307
|
+
} catch (offlineErr) {
|
|
2308
|
+
if (run.cancelled) return true
|
|
2309
|
+
console.warn(
|
|
2310
|
+
`${LOG_TAG}: offline sentence TTS synthesize failed${reason ? ` after ${reason}` : ""}:`,
|
|
2311
|
+
offlineErr,
|
|
2312
|
+
)
|
|
2313
|
+
return false
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
if (run.cancelled) {
|
|
2317
|
+
await Promise.resolve(firstSentence.cleanup?.())
|
|
2318
|
+
return true
|
|
2319
|
+
}
|
|
2320
|
+
void runSentenceTtsPipeline({
|
|
2321
|
+
sentences: offlineSentences,
|
|
2322
|
+
firstAudio: firstSentence,
|
|
2323
|
+
run,
|
|
2324
|
+
synthesize: (sentence) => ttsModelManager.synthesizeToFile(sentence, {languageCode, speed}),
|
|
2325
|
+
play: (audio, index) =>
|
|
2326
|
+
new Promise((resolve) => {
|
|
2327
|
+
const sentenceRequestId = `${audioRequestId}_sentence_${index}`
|
|
2328
|
+
run.playbackRequestId = sentenceRequestId
|
|
2329
|
+
audioPlaybackService.play(
|
|
2330
|
+
{
|
|
2331
|
+
requestId: sentenceRequestId,
|
|
2332
|
+
audioUrl: audio.audioUrl,
|
|
2333
|
+
appId: packageName,
|
|
2334
|
+
volume,
|
|
2335
|
+
stopOtherAudio,
|
|
2336
|
+
},
|
|
2337
|
+
(_responseId, success, error, duration, completionReason) => {
|
|
2338
|
+
if (run.playbackRequestId === sentenceRequestId) run.playbackRequestId = undefined
|
|
2339
|
+
resolve({success, completed: completionReason === "completed", error, duration})
|
|
2340
|
+
},
|
|
2341
|
+
)
|
|
2342
|
+
}),
|
|
2343
|
+
onCleanupError: (cleanupError) => {
|
|
2344
|
+
console.warn(`${LOG_TAG}: sentence TTS cleanup failed`, cleanupError)
|
|
2345
|
+
},
|
|
2346
|
+
}).then(
|
|
2347
|
+
({success, completed, error, duration}) => {
|
|
2348
|
+
sendPlaybackResult(success, error, duration, "offline sentence tts playback failed", completed)
|
|
2349
|
+
},
|
|
2350
|
+
(pipelineError: unknown) => {
|
|
2351
|
+
const message = pipelineError instanceof Error ? pipelineError.message : String(pipelineError)
|
|
2352
|
+
sendPlaybackResult(false, message, null, "offline sentence tts playback failed", false)
|
|
2353
|
+
},
|
|
2354
|
+
)
|
|
2355
|
+
queueMicrotask(() => {
|
|
2356
|
+
if (this.speechRuns.get(packageName) === run) this.setSpeakerState(packageName, "playing")
|
|
2357
|
+
})
|
|
2358
|
+
return true
|
|
2359
|
+
}
|
|
2036
2360
|
|
|
2037
2361
|
let offlineGenerated: TtsSynthesisResult | undefined
|
|
2038
2362
|
|
|
2039
2363
|
try {
|
|
2040
|
-
|
|
2041
|
-
offlineGenerated = await ttsModelManager.synthesizeToFile(text, {languageCode, speed})
|
|
2364
|
+
offlineGenerated = await ttsModelManager.synthesizeToFile(offlineSentences[0], {languageCode, speed})
|
|
2042
2365
|
} catch (offlineErr) {
|
|
2366
|
+
if (run.cancelled) return true
|
|
2043
2367
|
console.warn(`${LOG_TAG}: offline TTS synthesize failed${reason ? ` after ${reason}` : ""}:`, offlineErr)
|
|
2044
2368
|
return false
|
|
2045
2369
|
}
|
|
2046
2370
|
|
|
2047
2371
|
const generated = offlineGenerated
|
|
2372
|
+
if (run.cancelled) {
|
|
2373
|
+
await Promise.resolve(generated.cleanup?.())
|
|
2374
|
+
return true
|
|
2375
|
+
}
|
|
2376
|
+
run.playbackRequestId = audioRequestId
|
|
2048
2377
|
audioPlaybackService.play(
|
|
2049
|
-
{
|
|
2050
|
-
|
|
2378
|
+
{
|
|
2379
|
+
requestId: audioRequestId,
|
|
2380
|
+
audioUrl: generated.audioUrl,
|
|
2381
|
+
appId: packageName,
|
|
2382
|
+
volume,
|
|
2383
|
+
stopOtherAudio,
|
|
2384
|
+
},
|
|
2385
|
+
(_respId, success, error, duration, completionReason) => {
|
|
2386
|
+
if (run.playbackRequestId === audioRequestId) run.playbackRequestId = undefined
|
|
2051
2387
|
void Promise.resolve(generated.cleanup?.()).catch((cleanupError) => {
|
|
2052
2388
|
console.warn(`${LOG_TAG}: offline TTS cleanup failed`, cleanupError)
|
|
2053
2389
|
})
|
|
2054
|
-
sendPlaybackResult(
|
|
2390
|
+
sendPlaybackResult(
|
|
2391
|
+
success,
|
|
2392
|
+
error,
|
|
2393
|
+
duration,
|
|
2394
|
+
"offline tts playback failed",
|
|
2395
|
+
completionReason === "completed",
|
|
2396
|
+
)
|
|
2055
2397
|
},
|
|
2056
2398
|
)
|
|
2057
|
-
queueMicrotask(() =>
|
|
2399
|
+
queueMicrotask(() => {
|
|
2400
|
+
if (this.speechRuns.get(packageName) === run) this.setSpeakerState(packageName, "playing")
|
|
2401
|
+
})
|
|
2058
2402
|
return true
|
|
2059
2403
|
}
|
|
2060
2404
|
|
|
2061
2405
|
const playCloudTts = async (fallbackToOffline: boolean): Promise<boolean> => {
|
|
2406
|
+
if (run.cancelled) return true
|
|
2062
2407
|
let source: Awaited<ReturnType<typeof cloudClientService.tts.speak>>
|
|
2063
2408
|
try {
|
|
2064
|
-
source = await cloudClientService.tts.speak(
|
|
2409
|
+
source = await cloudClientService.tts.speak(cloudText, {
|
|
2065
2410
|
...(voiceExplicit && voice !== "default" ? {voice_id: voice} : {}),
|
|
2066
2411
|
...(modelId ? {model_id: modelId} : {}),
|
|
2067
2412
|
...(voiceSettings ? {voice_settings: voiceSettings} : {}),
|
|
2068
2413
|
})
|
|
2069
2414
|
} catch (cloudErr) {
|
|
2415
|
+
if (run.cancelled) return true
|
|
2070
2416
|
const error = cloudErr instanceof Error ? cloudErr.message : String(cloudErr)
|
|
2071
2417
|
console.warn(`${LOG_TAG}: cloud TTS source failed: ${error}`)
|
|
2072
2418
|
if (fallbackToOffline && (await playOfflineTts("cloud tts source failed"))) {
|
|
@@ -2076,10 +2422,20 @@ class LocalMiniappRuntime {
|
|
|
2076
2422
|
return true
|
|
2077
2423
|
}
|
|
2078
2424
|
|
|
2425
|
+
if (run.cancelled) return true
|
|
2426
|
+
|
|
2427
|
+
run.playbackRequestId = audioRequestId
|
|
2079
2428
|
await Promise.resolve(
|
|
2080
2429
|
audioPlaybackService.play(
|
|
2081
|
-
{
|
|
2082
|
-
|
|
2430
|
+
{
|
|
2431
|
+
requestId: audioRequestId,
|
|
2432
|
+
audioUrl: source.audioUrl,
|
|
2433
|
+
appId: packageName,
|
|
2434
|
+
volume,
|
|
2435
|
+
stopOtherAudio,
|
|
2436
|
+
},
|
|
2437
|
+
(_respId, success, error, duration, completionReason) => {
|
|
2438
|
+
if (run.playbackRequestId === audioRequestId) run.playbackRequestId = undefined
|
|
2083
2439
|
if (!success && fallbackToOffline) {
|
|
2084
2440
|
void playOfflineTts("cloud tts playback failed").then((started) => {
|
|
2085
2441
|
if (!started) {
|
|
@@ -2088,14 +2444,26 @@ class LocalMiniappRuntime {
|
|
|
2088
2444
|
})
|
|
2089
2445
|
return
|
|
2090
2446
|
}
|
|
2091
|
-
sendPlaybackResult(success, error, duration, "tts failed")
|
|
2447
|
+
sendPlaybackResult(success, error, duration, "tts failed", completionReason === "completed")
|
|
2092
2448
|
},
|
|
2093
2449
|
),
|
|
2094
2450
|
)
|
|
2095
|
-
queueMicrotask(() =>
|
|
2451
|
+
queueMicrotask(() => {
|
|
2452
|
+
if (this.speechRuns.get(packageName) === run) this.setSpeakerState(packageName, "playing")
|
|
2453
|
+
})
|
|
2096
2454
|
return true
|
|
2097
2455
|
}
|
|
2098
2456
|
|
|
2457
|
+
if (forceLocal) {
|
|
2458
|
+
if (await playOfflineTts()) {
|
|
2459
|
+
return
|
|
2460
|
+
}
|
|
2461
|
+
if (run.cancelled) return
|
|
2462
|
+
const message = "tts unavailable: forceLocal requested but offline TTS model is not available"
|
|
2463
|
+
sendPlaybackResult(false, message, null, message, false, MiniappErrorCode.TTS_LOCAL_UNAVAILABLE)
|
|
2464
|
+
return
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2099
2467
|
if (cloudConnected && (await playCloudTts(true))) {
|
|
2100
2468
|
return
|
|
2101
2469
|
}
|
|
@@ -2111,28 +2479,23 @@ class LocalMiniappRuntime {
|
|
|
2111
2479
|
}
|
|
2112
2480
|
|
|
2113
2481
|
const message = "tts unavailable: no cloud TTS URL or offline TTS model"
|
|
2114
|
-
|
|
2115
|
-
errorCode: MiniappErrorCode.TTS_UPSTREAM_ERROR,
|
|
2116
|
-
errorMessage: message,
|
|
2117
|
-
})
|
|
2118
|
-
this.sendResult(packageName, requestId, false, undefined, {
|
|
2119
|
-
code: MiniappErrorCode.TTS_UPSTREAM_ERROR,
|
|
2120
|
-
message,
|
|
2121
|
-
})
|
|
2482
|
+
sendPlaybackResult(false, message, null, message, false)
|
|
2122
2483
|
} catch (err) {
|
|
2123
2484
|
console.error(`${LOG_TAG}: speak error:`, err)
|
|
2124
2485
|
const message = err instanceof Error ? err.message : "TTS error"
|
|
2125
|
-
|
|
2126
|
-
errorCode: MiniappErrorCode.TTS_UPSTREAM_ERROR,
|
|
2127
|
-
errorMessage: message,
|
|
2128
|
-
})
|
|
2129
|
-
this.sendResult(packageName, requestId, false, undefined, {
|
|
2130
|
-
code: MiniappErrorCode.TTS_UPSTREAM_ERROR,
|
|
2131
|
-
message,
|
|
2132
|
-
})
|
|
2486
|
+
sendPlaybackResult(false, message, null, "TTS error", false)
|
|
2133
2487
|
}
|
|
2134
2488
|
}
|
|
2135
2489
|
|
|
2490
|
+
private cancelSpeech(packageName: string): void {
|
|
2491
|
+
const run = this.speechRuns.get(packageName)
|
|
2492
|
+
if (!run) return
|
|
2493
|
+
run.cancelled = true
|
|
2494
|
+
this.speechRuns.delete(packageName)
|
|
2495
|
+
if (run.playbackRequestId) audioPlaybackService.cancelPlayback(run.playbackRequestId)
|
|
2496
|
+
run.onCancelled?.()
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2136
2499
|
private async handleRgbLed(packageName: string, payload: Record<string, unknown>, requestId?: string): Promise<void> {
|
|
2137
2500
|
const coerceNumber = (value: unknown, fallback: number): number => {
|
|
2138
2501
|
const coerced = Number(value)
|
|
@@ -2164,7 +2527,17 @@ class LocalMiniappRuntime {
|
|
|
2164
2527
|
|
|
2165
2528
|
private async handleLocationPoll(packageName: string, requestId?: string): Promise<void> {
|
|
2166
2529
|
try {
|
|
2167
|
-
|
|
2530
|
+
// Expo's Android requestForegroundPermissionsAsync always delegates to
|
|
2531
|
+
// Activity.requestPermissions(), even when permission is already granted.
|
|
2532
|
+
// Calling it after the user presses Home therefore waits until the Activity
|
|
2533
|
+
// resumes. Read the current grant first, and only enter the prompt flow
|
|
2534
|
+
// while the Activity is actually foregrounded.
|
|
2535
|
+
const permissionStartedAt = Date.now()
|
|
2536
|
+
const permission = await resolveForegroundLocationPermission(Location, () => AppState.currentState)
|
|
2537
|
+
const {status} = permission
|
|
2538
|
+
console.log(
|
|
2539
|
+
`${LOG_TAG}: location poll permission status=${status} appState=${AppState.currentState} elapsed=${Date.now() - permissionStartedAt}ms`,
|
|
2540
|
+
)
|
|
2168
2541
|
if (status !== "granted") {
|
|
2169
2542
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
2170
2543
|
code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
|
|
@@ -2177,8 +2550,11 @@ class LocalMiniappRuntime {
|
|
|
2177
2550
|
// a fresh fix if the cache is empty. Use Low accuracy on the fresh
|
|
2178
2551
|
// path so we get a cell/wifi-tower fix in ~1s instead of waiting
|
|
2179
2552
|
// for full GPS warm-up.
|
|
2553
|
+
const locationStartedAt = Date.now()
|
|
2180
2554
|
const cached = await Location.getLastKnownPositionAsync({maxAge: 60_000})
|
|
2555
|
+
console.log(`${LOG_TAG}: location poll cache hit=${cached !== null} elapsed=${Date.now() - locationStartedAt}ms`)
|
|
2181
2556
|
const location = cached ?? (await Location.getCurrentPositionAsync({accuracy: Location.Accuracy.Low}))
|
|
2557
|
+
console.log(`${LOG_TAG}: location poll resolved elapsed=${Date.now() - locationStartedAt}ms`)
|
|
2182
2558
|
|
|
2183
2559
|
this.sendResult(packageName, requestId, true, {
|
|
2184
2560
|
lat: location.coords.latitude,
|
|
@@ -2220,10 +2596,70 @@ class LocalMiniappRuntime {
|
|
|
2220
2596
|
private readonly blobStore = new BlobStore({
|
|
2221
2597
|
sendResult: (packageName, requestId, ok, result, error) =>
|
|
2222
2598
|
this.sendResult(packageName, requestId, ok, result, error),
|
|
2223
|
-
getUserId: () =>
|
|
2224
|
-
|
|
2599
|
+
getUserId: () => cloudClientService.getMentraUserId(),
|
|
2600
|
+
})
|
|
2601
|
+
private readonly blobRequestQueues = new Map<string, Promise<void>>()
|
|
2602
|
+
|
|
2603
|
+
private readonly simpleStorage = new LocalMiniappStorage({
|
|
2604
|
+
backend: localMiniappStorageBackend,
|
|
2605
|
+
getUserId: () => cloudClientService.resolveMentraUserId(),
|
|
2225
2606
|
})
|
|
2226
2607
|
|
|
2608
|
+
/**
|
|
2609
|
+
* BlobStore's file helpers are synchronous once a user namespace is selected,
|
|
2610
|
+
* but first-boot Core auth is not. Queue requests per app behind stable
|
|
2611
|
+
* identity resolution so CREATE/WRITE/COMMIT ordering survives that wait.
|
|
2612
|
+
*/
|
|
2613
|
+
private enqueueBlobRequest(
|
|
2614
|
+
packageName: string,
|
|
2615
|
+
requestId: string | undefined,
|
|
2616
|
+
operation: () => Promise<void> | void,
|
|
2617
|
+
): void {
|
|
2618
|
+
const app = this.connectedApps.get(packageName)
|
|
2619
|
+
const previous = this.blobRequestQueues.get(packageName) ?? Promise.resolve()
|
|
2620
|
+
const queued = previous.then(async () => {
|
|
2621
|
+
try {
|
|
2622
|
+
await cloudClientService.resolveMentraUserId()
|
|
2623
|
+
} catch (err) {
|
|
2624
|
+
console.error(`${LOG_TAG}: blob identity error:`, err)
|
|
2625
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
2626
|
+
code: MiniappErrorCode.NOT_CONNECTED,
|
|
2627
|
+
message: "Blob storage is unavailable before sign-in completes",
|
|
2628
|
+
})
|
|
2629
|
+
return
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
// unregisterApp() or a crash-respawn may have replaced this app while
|
|
2633
|
+
// identity resolution was pending. Never replay an old context's request
|
|
2634
|
+
// against its replacement.
|
|
2635
|
+
if (!app || this.connectedApps.get(packageName) !== app) return
|
|
2636
|
+
|
|
2637
|
+
// Invoke in request order, but do not keep the queue occupied for the
|
|
2638
|
+
// lifetime of an async picker, download, or share sheet. CREATE/WRITE/
|
|
2639
|
+
// COMMIT are synchronous and therefore still dispatch strictly in order.
|
|
2640
|
+
try {
|
|
2641
|
+
const pending = operation()
|
|
2642
|
+
if (pending) {
|
|
2643
|
+
void pending.catch((err) => this.handleBlobRequestError(packageName, requestId, err))
|
|
2644
|
+
}
|
|
2645
|
+
} catch (err) {
|
|
2646
|
+
this.handleBlobRequestError(packageName, requestId, err)
|
|
2647
|
+
}
|
|
2648
|
+
})
|
|
2649
|
+
this.blobRequestQueues.set(packageName, queued)
|
|
2650
|
+
void queued.finally(() => {
|
|
2651
|
+
if (this.blobRequestQueues.get(packageName) === queued) this.blobRequestQueues.delete(packageName)
|
|
2652
|
+
})
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
private handleBlobRequestError(packageName: string, requestId: string | undefined, err: unknown): void {
|
|
2656
|
+
console.error(`${LOG_TAG}: blob request error:`, err)
|
|
2657
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
2658
|
+
code: MiniappErrorCode.INTERNAL,
|
|
2659
|
+
message: err instanceof Error ? err.message : "Blob storage error",
|
|
2660
|
+
})
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2227
2663
|
/**
|
|
2228
2664
|
* Heading is a sensor stream — start the native compass when any mini
|
|
2229
2665
|
* app is subscribed to "heading_update", stop it when none are.
|
|
@@ -2299,12 +2735,6 @@ class LocalMiniappRuntime {
|
|
|
2299
2735
|
// Storage helpers
|
|
2300
2736
|
// ---------------------------------------------------------------------------
|
|
2301
2737
|
|
|
2302
|
-
private getStorageKeyPrefix(packageName: string): string {
|
|
2303
|
-
const userId =
|
|
2304
|
-
(useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.coreToken) as string | undefined) || "anonymous"
|
|
2305
|
-
return `mentraos_localstorage_${userId}_${packageName}_`
|
|
2306
|
-
}
|
|
2307
|
-
|
|
2308
2738
|
private async handleStorageGet(
|
|
2309
2739
|
packageName: string,
|
|
2310
2740
|
payload: Record<string, unknown>,
|
|
@@ -2319,9 +2749,8 @@ class LocalMiniappRuntime {
|
|
|
2319
2749
|
})
|
|
2320
2750
|
return
|
|
2321
2751
|
}
|
|
2322
|
-
const
|
|
2323
|
-
|
|
2324
|
-
this.sendResult(packageName, requestId, true, {key, value: result.is_ok() ? result.value : null})
|
|
2752
|
+
const value = await this.simpleStorage.get(packageName, key)
|
|
2753
|
+
this.sendResult(packageName, requestId, true, {key, value})
|
|
2325
2754
|
} catch (err) {
|
|
2326
2755
|
console.error(`${LOG_TAG}: storage_get error:`, err)
|
|
2327
2756
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
@@ -2345,8 +2774,7 @@ class LocalMiniappRuntime {
|
|
|
2345
2774
|
})
|
|
2346
2775
|
return
|
|
2347
2776
|
}
|
|
2348
|
-
|
|
2349
|
-
mmkvStorage.save(fullKey, payload.value ?? null)
|
|
2777
|
+
await this.simpleStorage.set(packageName, key, payload.value ?? null)
|
|
2350
2778
|
this.sendResult(packageName, requestId, true)
|
|
2351
2779
|
} catch (err) {
|
|
2352
2780
|
console.error(`${LOG_TAG}: storage_set error:`, err)
|
|
@@ -2371,8 +2799,7 @@ class LocalMiniappRuntime {
|
|
|
2371
2799
|
})
|
|
2372
2800
|
return
|
|
2373
2801
|
}
|
|
2374
|
-
|
|
2375
|
-
mmkvStorage.remove(fullKey)
|
|
2802
|
+
await this.simpleStorage.delete(packageName, key)
|
|
2376
2803
|
this.sendResult(packageName, requestId, true)
|
|
2377
2804
|
} catch (err) {
|
|
2378
2805
|
console.error(`${LOG_TAG}: storage_delete error:`, err)
|
|
@@ -2389,9 +2816,7 @@ class LocalMiniappRuntime {
|
|
|
2389
2816
|
requestId?: string,
|
|
2390
2817
|
): Promise<void> {
|
|
2391
2818
|
try {
|
|
2392
|
-
const
|
|
2393
|
-
const allKeys = mmkvStorage.getAllKeys()
|
|
2394
|
-
const keys = allKeys.filter((k) => k.startsWith(prefix)).map((k) => k.slice(prefix.length))
|
|
2819
|
+
const keys = await this.simpleStorage.keys(packageName)
|
|
2395
2820
|
this.sendResult(packageName, requestId, true, {keys})
|
|
2396
2821
|
} catch (err) {
|
|
2397
2822
|
console.error(`${LOG_TAG}: storage_list error:`, err)
|
|
@@ -2404,11 +2829,7 @@ class LocalMiniappRuntime {
|
|
|
2404
2829
|
|
|
2405
2830
|
private async handleStorageClear(packageName: string, requestId?: string): Promise<void> {
|
|
2406
2831
|
try {
|
|
2407
|
-
|
|
2408
|
-
const allKeys = mmkvStorage.getAllKeys()
|
|
2409
|
-
for (const k of allKeys) {
|
|
2410
|
-
if (k.startsWith(prefix)) mmkvStorage.remove(k)
|
|
2411
|
-
}
|
|
2832
|
+
await this.simpleStorage.clear(packageName)
|
|
2412
2833
|
this.sendResult(packageName, requestId, true)
|
|
2413
2834
|
} catch (err) {
|
|
2414
2835
|
console.error(`${LOG_TAG}: storage_clear error:`, err)
|
|
@@ -2433,9 +2854,8 @@ class LocalMiniappRuntime {
|
|
|
2433
2854
|
})
|
|
2434
2855
|
return
|
|
2435
2856
|
}
|
|
2436
|
-
const
|
|
2437
|
-
|
|
2438
|
-
this.sendResult(packageName, requestId, true, {has: result.is_ok()})
|
|
2857
|
+
const has = await this.simpleStorage.has(packageName, key)
|
|
2858
|
+
this.sendResult(packageName, requestId, true, {has})
|
|
2439
2859
|
} catch (err) {
|
|
2440
2860
|
console.error(`${LOG_TAG}: storage_has error:`, err)
|
|
2441
2861
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
@@ -2447,17 +2867,7 @@ class LocalMiniappRuntime {
|
|
|
2447
2867
|
|
|
2448
2868
|
private async handleStorageGetAll(packageName: string, requestId?: string): Promise<void> {
|
|
2449
2869
|
try {
|
|
2450
|
-
const
|
|
2451
|
-
const allKeys = mmkvStorage.getAllKeys()
|
|
2452
|
-
const values: Record<string, string> = {}
|
|
2453
|
-
for (const k of allKeys) {
|
|
2454
|
-
if (!k.startsWith(prefix)) continue
|
|
2455
|
-
const r = mmkvStorage.load<unknown>(k)
|
|
2456
|
-
if (r.is_ok()) {
|
|
2457
|
-
const v = r.value
|
|
2458
|
-
values[k.slice(prefix.length)] = typeof v === "string" ? v : String(v ?? "")
|
|
2459
|
-
}
|
|
2460
|
-
}
|
|
2870
|
+
const values = await this.simpleStorage.getAll(packageName)
|
|
2461
2871
|
this.sendResult(packageName, requestId, true, {values})
|
|
2462
2872
|
} catch (err) {
|
|
2463
2873
|
console.error(`${LOG_TAG}: storage_get_all error:`, err)
|
|
@@ -2482,10 +2892,7 @@ class LocalMiniappRuntime {
|
|
|
2482
2892
|
})
|
|
2483
2893
|
return
|
|
2484
2894
|
}
|
|
2485
|
-
|
|
2486
|
-
for (const [key, value] of Object.entries(values)) {
|
|
2487
|
-
mmkvStorage.save(prefix + key, value ?? null)
|
|
2488
|
-
}
|
|
2895
|
+
await this.simpleStorage.setMultiple(packageName, values)
|
|
2489
2896
|
this.sendResult(packageName, requestId, true)
|
|
2490
2897
|
} catch (err) {
|
|
2491
2898
|
console.error(`${LOG_TAG}: storage_set_multiple error:`, err)
|
|
@@ -2496,6 +2903,36 @@ class LocalMiniappRuntime {
|
|
|
2496
2903
|
}
|
|
2497
2904
|
}
|
|
2498
2905
|
|
|
2906
|
+
// ---------------------------------------------------------------------------
|
|
2907
|
+
// Wi-Fi ADB
|
|
2908
|
+
// ---------------------------------------------------------------------------
|
|
2909
|
+
|
|
2910
|
+
private async handleSetWifiAdbState(
|
|
2911
|
+
packageName: string,
|
|
2912
|
+
payload: Record<string, unknown>,
|
|
2913
|
+
requestId?: string,
|
|
2914
|
+
): Promise<void> {
|
|
2915
|
+
const result = await handleWifiAdbRequest(packageName, payload, {
|
|
2916
|
+
isSystemPackage: (candidate) => this.isSystemPackage(candidate),
|
|
2917
|
+
setWifiAdbState: async (enabled) => {
|
|
2918
|
+
console.log(`${LOG_TAG}: set_wifi_adb_state enabled=${enabled} from ${packageName}`)
|
|
2919
|
+
try {
|
|
2920
|
+
await BluetoothSdk.setWifiAdbState(enabled)
|
|
2921
|
+
} catch (error) {
|
|
2922
|
+
console.error(`${LOG_TAG}: set_wifi_adb_state error:`, error)
|
|
2923
|
+
throw error
|
|
2924
|
+
}
|
|
2925
|
+
},
|
|
2926
|
+
})
|
|
2927
|
+
|
|
2928
|
+
if (result.ok) {
|
|
2929
|
+
this.sendResult(packageName, requestId, true)
|
|
2930
|
+
return
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
this.sendResult(packageName, requestId, false, undefined, result.error)
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2499
2936
|
// ---------------------------------------------------------------------------
|
|
2500
2937
|
// Camera FOV
|
|
2501
2938
|
// ---------------------------------------------------------------------------
|
|
@@ -2524,17 +2961,7 @@ class LocalMiniappRuntime {
|
|
|
2524
2961
|
"preset" in request ? `preset=${request.preset}` : `fov=${request.fov} roi=${request.roiPosition ?? "center"}`
|
|
2525
2962
|
console.log(`${LOG_TAG}: camera_fov_set ${description}`)
|
|
2526
2963
|
|
|
2527
|
-
|
|
2528
|
-
// runtime hook — a pure BluetoothSdk.setCameraFov passthrough with no host coupling).
|
|
2529
|
-
const result = await BluetoothSdk.setCameraFov(request)
|
|
2530
|
-
|
|
2531
|
-
useSettingsStore
|
|
2532
|
-
.getState()
|
|
2533
|
-
.setSetting(
|
|
2534
|
-
ISLAND_SETTINGS_KEYS.cameraFov,
|
|
2535
|
-
{fov: result.fov, roi_position: CAMERA_ROI_POSITION_VALUES[result.roiPosition]},
|
|
2536
|
-
false,
|
|
2537
|
-
)
|
|
2964
|
+
const result = await phoneCameraFovCoordinator.setOverride(packageName, request)
|
|
2538
2965
|
this.sendResult(packageName, requestId, true, result)
|
|
2539
2966
|
} catch (err) {
|
|
2540
2967
|
console.error(`${LOG_TAG}: camera_fov error:`, err)
|
|
@@ -2571,6 +2998,124 @@ class LocalMiniappRuntime {
|
|
|
2571
2998
|
}
|
|
2572
2999
|
}
|
|
2573
3000
|
|
|
3001
|
+
/**
|
|
3002
|
+
* Lifecycle-scoped glasses-side VAD override from a miniapp
|
|
3003
|
+
* (session.mic.setVoiceActivityDetectionEnabled). Requires MICROPHONE in the
|
|
3004
|
+
* miniapp manifest. Mentra Live only today — other models no-op via DeviceStore.
|
|
3005
|
+
*/
|
|
3006
|
+
private async handleMicSetVadEnabled(
|
|
3007
|
+
packageName: string,
|
|
3008
|
+
payload: Record<string, unknown>,
|
|
3009
|
+
requestId?: string,
|
|
3010
|
+
): Promise<void> {
|
|
3011
|
+
const app = this.connectedApps.get(packageName)
|
|
3012
|
+
const hasMicPermission = app?.installedManifest?.permissions?.some((p) => p.type === "MICROPHONE")
|
|
3013
|
+
if (!hasMicPermission) {
|
|
3014
|
+
logPermissionNotDeclared(packageName, "MICROPHONE", "to set VAD enabled", `{"type": "MICROPHONE"}`)
|
|
3015
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
3016
|
+
code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
|
|
3017
|
+
message: `MICROPHONE permission not declared in miniapp.json. Add {"type": "MICROPHONE"} to the "permissions" array.`,
|
|
3018
|
+
permission: "MICROPHONE",
|
|
3019
|
+
operation: MiniappRequestType.MIC_SET_VAD_ENABLED,
|
|
3020
|
+
})
|
|
3021
|
+
return
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
if (typeof payload.enabled !== "boolean") {
|
|
3025
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
3026
|
+
code: MiniappErrorCode.INVALID_ARGUMENT,
|
|
3027
|
+
message: "enabled must be a boolean",
|
|
3028
|
+
})
|
|
3029
|
+
return
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
try {
|
|
3033
|
+
const enabled = payload.enabled
|
|
3034
|
+
console.log(`${LOG_TAG}: mic_set_vad_enabled ${enabled} (by ${packageName})`)
|
|
3035
|
+
await micStateCoordinator.setMiniappGateOverride(packageName, "vad", enabled, this.getConfiguredMicGates())
|
|
3036
|
+
this.sendResult(packageName, requestId, true)
|
|
3037
|
+
} catch (err) {
|
|
3038
|
+
console.error(`${LOG_TAG}: mic_set_vad_enabled error:`, err)
|
|
3039
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
3040
|
+
code: MiniappErrorCode.INTERNAL,
|
|
3041
|
+
message: err instanceof Error ? err.message : "VAD set-enabled error",
|
|
3042
|
+
})
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
/**
|
|
3047
|
+
* Lifecycle-scoped center-mic loudness gate ("Barrier") override from a miniapp
|
|
3048
|
+
* (session.mic.setLoudnessGateEnabled). Requires MICROPHONE in the miniapp
|
|
3049
|
+
* manifest. Mentra Live only today — other models no-op via DeviceStore.
|
|
3050
|
+
*/
|
|
3051
|
+
private async handleMicSetLoudnessGateEnabled(
|
|
3052
|
+
packageName: string,
|
|
3053
|
+
payload: Record<string, unknown>,
|
|
3054
|
+
requestId?: string,
|
|
3055
|
+
): Promise<void> {
|
|
3056
|
+
const app = this.connectedApps.get(packageName)
|
|
3057
|
+
const hasMicPermission = app?.installedManifest?.permissions?.some((p) => p.type === "MICROPHONE")
|
|
3058
|
+
if (!hasMicPermission) {
|
|
3059
|
+
logPermissionNotDeclared(packageName, "MICROPHONE", "to set loudness gate enabled", `{"type": "MICROPHONE"}`)
|
|
3060
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
3061
|
+
code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
|
|
3062
|
+
message: `MICROPHONE permission not declared in miniapp.json. Add {"type": "MICROPHONE"} to the "permissions" array.`,
|
|
3063
|
+
permission: "MICROPHONE",
|
|
3064
|
+
operation: MiniappRequestType.MIC_SET_LOUDNESS_GATE_ENABLED,
|
|
3065
|
+
})
|
|
3066
|
+
return
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
if (typeof payload.enabled !== "boolean") {
|
|
3070
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
3071
|
+
code: MiniappErrorCode.INVALID_ARGUMENT,
|
|
3072
|
+
message: "enabled must be a boolean",
|
|
3073
|
+
})
|
|
3074
|
+
return
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
try {
|
|
3078
|
+
const enabled = payload.enabled
|
|
3079
|
+
console.log(`${LOG_TAG}: mic_set_loudness_gate_enabled ${enabled} (by ${packageName})`)
|
|
3080
|
+
await micStateCoordinator.setMiniappGateOverride(
|
|
3081
|
+
packageName,
|
|
3082
|
+
"loudnessGate",
|
|
3083
|
+
enabled,
|
|
3084
|
+
this.getConfiguredMicGates(),
|
|
3085
|
+
)
|
|
3086
|
+
this.sendResult(packageName, requestId, true)
|
|
3087
|
+
} catch (err) {
|
|
3088
|
+
console.error(`${LOG_TAG}: mic_set_loudness_gate_enabled error:`, err)
|
|
3089
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
3090
|
+
code: MiniappErrorCode.INTERNAL,
|
|
3091
|
+
message: err instanceof Error ? err.message : "Loudness gate set-enabled error",
|
|
3092
|
+
})
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
/**
|
|
3097
|
+
* session.system.scanQr — host camera overlay. Must not clear miniapp
|
|
3098
|
+
* foreground; the host seam is responsible for presenting a Modal on top.
|
|
3099
|
+
*/
|
|
3100
|
+
private async handleScanQr(
|
|
3101
|
+
packageName: string,
|
|
3102
|
+
payload: Record<string, unknown>,
|
|
3103
|
+
requestId?: string,
|
|
3104
|
+
): Promise<void> {
|
|
3105
|
+
try {
|
|
3106
|
+
const result = await invokeScanQrSeam(getUiSeams().scanQr, payload)
|
|
3107
|
+
this.sendResult(packageName, requestId, true, result)
|
|
3108
|
+
} catch (err) {
|
|
3109
|
+
if (!getUiSeams().scanQr) {
|
|
3110
|
+
console.warn(`${LOG_TAG}: scanQr seam missing — QR overlay is not registered`)
|
|
3111
|
+
}
|
|
3112
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
3113
|
+
code: (err as {code?: string} | null | undefined)?.code || MiniappErrorCode.INTERNAL,
|
|
3114
|
+
message: err instanceof Error ? err.message : "QR scan failed",
|
|
3115
|
+
})
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
|
|
2574
3119
|
// ---------------------------------------------------------------------------
|
|
2575
3120
|
// System utilities (share, open URL, clipboard, download)
|
|
2576
3121
|
// ---------------------------------------------------------------------------
|
|
@@ -2715,10 +3260,20 @@ class LocalMiniappRuntime {
|
|
|
2715
3260
|
const result = await phonePhotoCoordinator.takePhoto(packageName, {
|
|
2716
3261
|
size: payload.size as "low" | "medium" | "high" | "max" | "small" | "large" | "full" | undefined,
|
|
2717
3262
|
mode: payload.mode as "photo" | "text" | undefined,
|
|
3263
|
+
transferMethod: payload.transferMethod as "auto" | "direct" | "ble" | undefined,
|
|
2718
3264
|
compress: payload.compress as "none" | "low" | "medium" | "high" | undefined,
|
|
2719
3265
|
sound: payload.sound as boolean | undefined,
|
|
2720
3266
|
saveToGallery: payload.saveToGallery as boolean | undefined,
|
|
2721
3267
|
exposureTimeNs: payload.exposureTimeNs as number | undefined,
|
|
3268
|
+
iso: payload.iso as number | null | undefined,
|
|
3269
|
+
aeExposureDivisor: payload.aeExposureDivisor as number | undefined,
|
|
3270
|
+
isoCap: payload.isoCap as number | undefined,
|
|
3271
|
+
noiseReduction: payload.noiseReduction as boolean | undefined,
|
|
3272
|
+
edgeEnhancement: payload.edgeEnhancement as boolean | undefined,
|
|
3273
|
+
zsl: payload.zsl as boolean | undefined,
|
|
3274
|
+
mfnr: payload.mfnr as boolean | undefined,
|
|
3275
|
+
ispDigitalGain: payload.ispDigitalGain as number | undefined,
|
|
3276
|
+
ispAnalogGain: payload.ispAnalogGain as string | undefined,
|
|
2722
3277
|
})
|
|
2723
3278
|
this.sendResult(packageName, requestId, true, result)
|
|
2724
3279
|
} catch (err) {
|
|
@@ -2753,8 +3308,11 @@ class LocalMiniappRuntime {
|
|
|
2753
3308
|
try {
|
|
2754
3309
|
await phonePhotoCoordinator.warmUpCamera(packageName, {
|
|
2755
3310
|
size: payload.size as "low" | "medium" | "high" | "max" | undefined,
|
|
3311
|
+
mode: payload.mode as "photo" | "text" | undefined,
|
|
2756
3312
|
exposureTimeNs: payload.exposureTimeNs as number | undefined,
|
|
2757
3313
|
durationMs: payload.durationMs as number | undefined,
|
|
3314
|
+
zsl: payload.zsl as boolean | undefined,
|
|
3315
|
+
mfnr: payload.mfnr as boolean | undefined,
|
|
2758
3316
|
})
|
|
2759
3317
|
this.sendResult(packageName, requestId, true)
|
|
2760
3318
|
} catch (err) {
|
|
@@ -2847,6 +3405,7 @@ class LocalMiniappRuntime {
|
|
|
2847
3405
|
video: normalizeStreamVideoConfig(payload.video),
|
|
2848
3406
|
audio: normalizeStreamAudioConfig(payload.audio),
|
|
2849
3407
|
sound: payload.sound as boolean | undefined,
|
|
3408
|
+
authToken: typeof payload.authToken === "string" ? payload.authToken : undefined,
|
|
2850
3409
|
})
|
|
2851
3410
|
this.sendResult(packageName, requestId, true, result)
|
|
2852
3411
|
} catch (err) {
|
|
@@ -2942,7 +3501,7 @@ class LocalMiniappRuntime {
|
|
|
2942
3501
|
return
|
|
2943
3502
|
}
|
|
2944
3503
|
try {
|
|
2945
|
-
await requestWifiSetup(payload.reason as string | undefined)
|
|
3504
|
+
await requestWifiSetup(payload.reason as string | undefined, packageName)
|
|
2946
3505
|
this.sendResult(packageName, requestId, true)
|
|
2947
3506
|
} catch (err) {
|
|
2948
3507
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
@@ -3006,27 +3565,12 @@ class LocalMiniappRuntime {
|
|
|
3006
3565
|
}
|
|
3007
3566
|
}
|
|
3008
3567
|
|
|
3009
|
-
// Remove old subscriptions for this app
|
|
3010
|
-
for (const oldStream of app.subscriptions) {
|
|
3011
|
-
const subs = this.streamSubscribers.get(oldStream)
|
|
3012
|
-
if (subs) {
|
|
3013
|
-
subs.delete(packageName)
|
|
3014
|
-
if (subs.size === 0) {
|
|
3015
|
-
this.streamSubscribers.delete(oldStream)
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
3568
|
// Add new subscriptions
|
|
3569
|
+
const previousSubscriptions = app.subscriptions
|
|
3021
3570
|
app.subscriptions = new Set(streams)
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
subs = new Set()
|
|
3026
|
-
this.streamSubscribers.set(stream, subs)
|
|
3027
|
-
}
|
|
3028
|
-
subs.add(packageName)
|
|
3029
|
-
}
|
|
3571
|
+
app.forceLocalTranscriptionStreams.clear()
|
|
3572
|
+
app.cloudTranscriptionStreams = new Set(streams.filter((stream) => stream.startsWith("transcription:")))
|
|
3573
|
+
this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions)
|
|
3030
3574
|
|
|
3031
3575
|
this.recomputeMicRequirements()
|
|
3032
3576
|
this.updateCloudSubscriptions()
|
|
@@ -3037,6 +3581,16 @@ class LocalMiniappRuntime {
|
|
|
3037
3581
|
// Mic requirements
|
|
3038
3582
|
// ===========================================================================
|
|
3039
3583
|
|
|
3584
|
+
private getConfiguredMicGates(): {vadEnabled: boolean | null; loudnessGateEnabled: boolean | null} {
|
|
3585
|
+
const bluetoothSettings = useSettingsStore.getState().getBluetoothSettings()
|
|
3586
|
+
const configuredVad = bluetoothSettings.voice_activity_detection_enabled
|
|
3587
|
+
const configuredLoudnessGate = bluetoothSettings.loudness_gate_enabled
|
|
3588
|
+
return {
|
|
3589
|
+
vadEnabled: typeof configuredVad === "boolean" ? configuredVad : null,
|
|
3590
|
+
loudnessGateEnabled: typeof configuredLoudnessGate === "boolean" ? configuredLoudnessGate : null,
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3040
3594
|
private recomputeMicRequirements(): void {
|
|
3041
3595
|
let anyPcm = false
|
|
3042
3596
|
let anyLc3 = false
|
|
@@ -3045,7 +3599,11 @@ class LocalMiniappRuntime {
|
|
|
3045
3599
|
if (stream === "audio_chunk") anyPcm = true
|
|
3046
3600
|
if (stream.startsWith("transcription:") || stream.startsWith("translation:") || stream === "vad") anyLc3 = true
|
|
3047
3601
|
}
|
|
3048
|
-
micStateCoordinator.setLocalRequirements({
|
|
3602
|
+
micStateCoordinator.setLocalRequirements({
|
|
3603
|
+
pcm: anyPcm,
|
|
3604
|
+
lc3: anyLc3,
|
|
3605
|
+
...this.getConfiguredMicGates(),
|
|
3606
|
+
})
|
|
3049
3607
|
}
|
|
3050
3608
|
|
|
3051
3609
|
/**
|
|
@@ -3058,19 +3616,34 @@ class LocalMiniappRuntime {
|
|
|
3058
3616
|
private updateCloudSubscriptions(): void {
|
|
3059
3617
|
const cloudStreams = new Set<string>()
|
|
3060
3618
|
let transcriptionLang: string | null = null
|
|
3619
|
+
let forceLocalTranscriptionLang: string | null = null
|
|
3620
|
+
let hasForceLocalTranscription = false
|
|
3061
3621
|
for (const [stream, subscribers] of this.streamSubscribers) {
|
|
3062
3622
|
if (subscribers.size === 0) continue
|
|
3063
|
-
// Only transcription / translation need cloud delivery. Location
|
|
3064
|
-
// notifications
|
|
3065
|
-
|
|
3066
|
-
if (stream.startsWith("transcription:") || stream.startsWith("translation:")) {
|
|
3623
|
+
// Only transcription / translation need cloud delivery. Location and
|
|
3624
|
+
// notifications are sourced natively on the phone — no cloud hop.
|
|
3625
|
+
if (stream.startsWith("translation:")) {
|
|
3067
3626
|
cloudStreams.add(stream)
|
|
3068
3627
|
}
|
|
3069
|
-
if (stream.startsWith("transcription:")
|
|
3070
|
-
|
|
3628
|
+
if (stream.startsWith("transcription:")) {
|
|
3629
|
+
const language = stream.substring("transcription:".length)
|
|
3630
|
+
if (transcriptionLang === null) transcriptionLang = language
|
|
3631
|
+
|
|
3632
|
+
const routes = summarizeTranscriptionRoutes(subscribers, (packageName) =>
|
|
3633
|
+
this.appTranscriptionRoutesForSubscription(packageName, stream),
|
|
3634
|
+
)
|
|
3635
|
+
if (routes.hasForceLocalSubscriber) {
|
|
3636
|
+
hasForceLocalTranscription = true
|
|
3637
|
+
if (forceLocalTranscriptionLang === null) forceLocalTranscriptionLang = language
|
|
3638
|
+
}
|
|
3639
|
+
if (routes.hasCloudSubscriber) cloudStreams.add(stream)
|
|
3071
3640
|
}
|
|
3072
3641
|
}
|
|
3073
|
-
localSttFallbackCoordinator.onSubscriptionChange(
|
|
3642
|
+
localSttFallbackCoordinator.onSubscriptionChange(
|
|
3643
|
+
transcriptionLang !== null,
|
|
3644
|
+
forceLocalTranscriptionLang ?? transcriptionLang,
|
|
3645
|
+
hasForceLocalTranscription,
|
|
3646
|
+
)
|
|
3074
3647
|
|
|
3075
3648
|
// Mirror the same set as typed AudioSubscription[] and push it to the cloud
|
|
3076
3649
|
// runtime.
|
|
@@ -3108,12 +3681,21 @@ class LocalMiniappRuntime {
|
|
|
3108
3681
|
*/
|
|
3109
3682
|
private buildCloudAudioSubscriptions(cloudStreams: Set<string>): AudioSubscription[] {
|
|
3110
3683
|
const subs: AudioSubscription[] = []
|
|
3684
|
+
// Union of every connected app's TRANSCRIPTION_CONFIG hints. Hints only
|
|
3685
|
+
// apply to the auto-mode TRANSCRIPTION subscription (the wire schema has
|
|
3686
|
+
// no hints field on specific-language sources — the language IS the hint
|
|
3687
|
+
// there), and are deliberately NOT attached to translation sources.
|
|
3688
|
+
const hintUnion = [...new Set([...this.transcriptionHintsByApp.values()].flat())].sort()
|
|
3111
3689
|
const langSource = (code: string): LanguageSource =>
|
|
3112
3690
|
code === "auto" || code === "*" ? {mode: "auto"} : {mode: "specific", code}
|
|
3113
3691
|
for (const stream of cloudStreams) {
|
|
3114
3692
|
if (stream.startsWith("transcription:")) {
|
|
3115
3693
|
const lang = stream.substring("transcription:".length)
|
|
3116
|
-
|
|
3694
|
+
const source = langSource(lang)
|
|
3695
|
+
subs.push({
|
|
3696
|
+
kind: "transcription",
|
|
3697
|
+
language: source.mode === "auto" && hintUnion.length > 0 ? {mode: "auto", hints: hintUnion} : source,
|
|
3698
|
+
})
|
|
3117
3699
|
} else if (stream.startsWith("translation:")) {
|
|
3118
3700
|
const parts = stream.split(":")
|
|
3119
3701
|
// Only `translation:<source>:<target>` maps cleanly; a concrete target
|
|
@@ -3168,21 +3750,25 @@ class LocalMiniappRuntime {
|
|
|
3168
3750
|
} text="${d.text.slice(0, 48)}"`,
|
|
3169
3751
|
)
|
|
3170
3752
|
}
|
|
3171
|
-
this.forwardEvent(
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3753
|
+
this.forwardEvent(
|
|
3754
|
+
`transcription:${d.resolvedLanguage}`,
|
|
3755
|
+
{
|
|
3756
|
+
type: "transcription",
|
|
3757
|
+
text: d.text,
|
|
3758
|
+
isFinal: d.isFinal,
|
|
3759
|
+
utteranceId: d.utteranceId,
|
|
3760
|
+
transcribeLanguage: d.resolvedLanguage,
|
|
3761
|
+
detectedLanguage: d.languageDetected ? d.resolvedLanguage : undefined,
|
|
3762
|
+
startTime: d.startMs,
|
|
3763
|
+
endTime: d.endMs,
|
|
3764
|
+
speakerId: d.speakerId,
|
|
3765
|
+
duration: d.durationMs,
|
|
3766
|
+
provider: d.provider,
|
|
3767
|
+
confidence: d.confidence,
|
|
3768
|
+
__hostReceivedAt: receivedAt,
|
|
3769
|
+
},
|
|
3770
|
+
"cloud",
|
|
3771
|
+
)
|
|
3186
3772
|
})
|
|
3187
3773
|
|
|
3188
3774
|
cloudClientService.onTranslation((d: TranslationData) => {
|
|
@@ -3265,7 +3851,7 @@ class LocalMiniappRuntime {
|
|
|
3265
3851
|
* - Cloud sends "head_up" → miniapp protocol uses "head_position" (HEAD_POSITION)
|
|
3266
3852
|
* - Cloud sends "VAD" (uppercase) → miniapp protocol uses "vad" (lowercase)
|
|
3267
3853
|
*/
|
|
3268
|
-
public forwardEvent(streamType: string, data: unknown): void {
|
|
3854
|
+
public forwardEvent(streamType: string, data: unknown, transcriptionSource?: TranscriptionEventSource): void {
|
|
3269
3855
|
// Translate cloud event names to miniapp protocol stream types
|
|
3270
3856
|
const normalizedStream = this.normalizeStreamType(streamType)
|
|
3271
3857
|
|
|
@@ -3340,16 +3926,14 @@ class LocalMiniappRuntime {
|
|
|
3340
3926
|
|
|
3341
3927
|
if (matchedSubs.size === 0 && !perGestureStream) return
|
|
3342
3928
|
|
|
3343
|
-
// BGCAP: count transcripts only once we know they're actually being handed
|
|
3344
|
-
// to a subscriber. If the captions miniapp is unregistered/respawning (or
|
|
3345
|
-
// hasn't subscribed yet) the cloud may still push transcripts that match no
|
|
3346
|
-
// one — counting those would falsely show "forwarded" during the exact gap
|
|
3347
|
-
// the diagnostic is meant to expose.
|
|
3348
|
-
if (BGCAP_TELEMETRY && normalizedStream.startsWith("transcription:")) {
|
|
3349
|
-
bgcapNoteTranscriptForward(!!(data as {isFinal?: boolean} | null)?.isFinal)
|
|
3350
|
-
}
|
|
3351
|
-
|
|
3352
3929
|
for (const packageName of matchedSubs) {
|
|
3930
|
+
let transcriptionRoute: "default" | "forceLocal" | "all" | undefined
|
|
3931
|
+
if (normalizedStream.startsWith("transcription:") && transcriptionSource) {
|
|
3932
|
+
const routes = this.appTranscriptionRoutesForEvent(packageName, normalizedStream)
|
|
3933
|
+
transcriptionRoute =
|
|
3934
|
+
transcriptionDeliveryRoute(transcriptionSource, routes, cloudClientService.isConnected()) ?? undefined
|
|
3935
|
+
if (!transcriptionRoute) continue
|
|
3936
|
+
}
|
|
3353
3937
|
if (TRANSCRIPT_TIMING_TELEMETRY && normalizedStream.startsWith("transcription:")) {
|
|
3354
3938
|
const transcript = data as {text?: string; isFinal?: boolean; __hostReceivedAt?: number} | null
|
|
3355
3939
|
const now = Date.now()
|
|
@@ -3371,6 +3955,7 @@ class LocalMiniappRuntime {
|
|
|
3371
3955
|
type: MiniappResponseType.EVENT,
|
|
3372
3956
|
streamType: normalizedStream,
|
|
3373
3957
|
data: outboundData,
|
|
3958
|
+
...(transcriptionRoute ? {transcriptionRoute} : {}),
|
|
3374
3959
|
})
|
|
3375
3960
|
}
|
|
3376
3961
|
|
|
@@ -3390,6 +3975,38 @@ class LocalMiniappRuntime {
|
|
|
3390
3975
|
}
|
|
3391
3976
|
}
|
|
3392
3977
|
|
|
3978
|
+
private appTranscriptionRoutesForSubscription(
|
|
3979
|
+
packageName: string,
|
|
3980
|
+
stream: string,
|
|
3981
|
+
): {
|
|
3982
|
+
cloud: boolean
|
|
3983
|
+
forceLocal: boolean
|
|
3984
|
+
} {
|
|
3985
|
+
const app = this.connectedApps.get(packageName)
|
|
3986
|
+
return {
|
|
3987
|
+
cloud: app?.cloudTranscriptionStreams.has(stream) === true,
|
|
3988
|
+
forceLocal: app?.forceLocalTranscriptionStreams.has(stream) === true,
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
private appTranscriptionRoutesForEvent(
|
|
3993
|
+
packageName: string,
|
|
3994
|
+
stream: string,
|
|
3995
|
+
): {
|
|
3996
|
+
cloud: boolean
|
|
3997
|
+
forceLocal: boolean
|
|
3998
|
+
} {
|
|
3999
|
+
const app = this.connectedApps.get(packageName)
|
|
4000
|
+
const autoStream = `${MiniappStreamType.TRANSCRIPTION}:auto`
|
|
4001
|
+
return {
|
|
4002
|
+
cloud:
|
|
4003
|
+
app?.cloudTranscriptionStreams.has(stream) === true || app?.cloudTranscriptionStreams.has(autoStream) === true,
|
|
4004
|
+
forceLocal:
|
|
4005
|
+
app?.forceLocalTranscriptionStreams.has(stream) === true ||
|
|
4006
|
+
app?.forceLocalTranscriptionStreams.has(autoStream) === true,
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
|
|
3393
4010
|
/**
|
|
3394
4011
|
* Translate cloud event names to miniapp stream type values.
|
|
3395
4012
|
*/
|
|
@@ -3934,7 +4551,6 @@ class LocalMiniappRuntime {
|
|
|
3934
4551
|
// ===========================================================================
|
|
3935
4552
|
|
|
3936
4553
|
private ensurePingLoop(): void {
|
|
3937
|
-
bgcapStartHeartbeat() // BGCAP diagnostic
|
|
3938
4554
|
if (this.pingIntervalId !== null) return
|
|
3939
4555
|
|
|
3940
4556
|
this.pingIntervalId = BgTimer.setInterval(() => {
|
|
@@ -3943,7 +4559,6 @@ class LocalMiniappRuntime {
|
|
|
3943
4559
|
}
|
|
3944
4560
|
|
|
3945
4561
|
private stopPingLoop(): void {
|
|
3946
|
-
bgcapStopHeartbeat() // BGCAP diagnostic
|
|
3947
4562
|
if (this.pingIntervalId !== null) {
|
|
3948
4563
|
BgTimer.clearInterval(this.pingIntervalId)
|
|
3949
4564
|
this.pingIntervalId = null
|
|
@@ -3951,23 +4566,18 @@ class LocalMiniappRuntime {
|
|
|
3951
4566
|
}
|
|
3952
4567
|
|
|
3953
4568
|
private doPingRound(): void {
|
|
3954
|
-
const now = Date.now()
|
|
3955
|
-
const staleThreshold = PING_INTERVAL_MS * PING_TIMEOUT_THRESHOLD
|
|
3956
|
-
|
|
3957
4569
|
const toRemove: string[] = []
|
|
3958
4570
|
|
|
3959
4571
|
for (const [packageName, app] of this.connectedApps) {
|
|
3960
|
-
|
|
4572
|
+
const liveness = advanceMiniappPingLiveness(app.unansweredPingRounds, PING_TIMEOUT_THRESHOLD)
|
|
4573
|
+
if (liveness.shouldUnregister) {
|
|
3961
4574
|
console.warn(`${LOG_TAG}: ${packageName} missed ${PING_TIMEOUT_THRESHOLD} pings, unregistering`)
|
|
3962
4575
|
toRemove.push(packageName)
|
|
3963
4576
|
continue
|
|
3964
4577
|
}
|
|
4578
|
+
app.unansweredPingRounds = liveness.unansweredPingRounds
|
|
3965
4579
|
|
|
3966
4580
|
// Send PING — SDK auto-replies with PONG
|
|
3967
|
-
// BGCAP: stamp the OLDEST outstanding ping only — don't overwrite, or a
|
|
3968
|
-
// background stall (pings keep firing on the native BgTimer while the
|
|
3969
|
-
// miniapp engine is frozen) would reset the clock and hide its duration.
|
|
3970
|
-
if (BGCAP_TELEMETRY && app.bgcapLastPingSentAt == null) app.bgcapLastPingSentAt = now
|
|
3971
4581
|
this.sendToMiniapp(packageName, {
|
|
3972
4582
|
type: MiniappRequestType.PING,
|
|
3973
4583
|
})
|
|
@@ -3989,6 +4599,7 @@ class LocalMiniappRuntime {
|
|
|
3989
4599
|
const app = this.connectedApps.get(packageName)
|
|
3990
4600
|
if (app) {
|
|
3991
4601
|
app.lastPongAt = Date.now()
|
|
4602
|
+
app.unansweredPingRounds = 0
|
|
3992
4603
|
this.clearForegroundProbe(packageName)
|
|
3993
4604
|
}
|
|
3994
4605
|
}
|
|
@@ -4016,7 +4627,17 @@ class LocalMiniappRuntime {
|
|
|
4016
4627
|
|
|
4017
4628
|
// Belt-and-suspenders: clear any remaining state
|
|
4018
4629
|
this.pendingCloudRequests.clear()
|
|
4630
|
+
const hadButtonPressSubscribers = this.getButtonPressSubscribers().length > 0
|
|
4019
4631
|
this.streamSubscribers.clear()
|
|
4632
|
+
if (hadButtonPressSubscribers) {
|
|
4633
|
+
for (const listener of this.buttonPressSubscriberListeners) {
|
|
4634
|
+
try {
|
|
4635
|
+
listener([])
|
|
4636
|
+
} catch (error) {
|
|
4637
|
+
console.warn(`${LOG_TAG}: button press subscriber listener threw during cleanup:`, error)
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4020
4641
|
this.connectedApps.clear()
|
|
4021
4642
|
for (const timerId of this.foregroundProbeTimers.values()) {
|
|
4022
4643
|
BgTimer.clearTimeout(timerId)
|