@mentra/engine 0.1.0-beta.0 → 0.1.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -1
- package/build/engine.d.ts +11 -3
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +7 -2
- package/build/engine.js.map +1 -1
- package/build/facades/dev.d.ts.map +1 -1
- package/build/facades/dev.js +9 -5
- package/build/facades/dev.js.map +1 -1
- package/build/facades/gallery.js +1 -1
- package/build/facades/gallery.js.map +1 -1
- package/build/facades/glasses.d.ts +1 -0
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +19 -4
- package/build/facades/glasses.js.map +1 -1
- package/build/facades/glassesSettings.d.ts.map +1 -1
- package/build/facades/glassesSettings.js +1 -0
- package/build/facades/glassesSettings.js.map +1 -1
- package/build/facades/miniapps.d.ts +4 -0
- package/build/facades/miniapps.d.ts.map +1 -1
- package/build/facades/miniapps.js +5 -0
- package/build/facades/miniapps.js.map +1 -1
- package/build/facades/ota.d.ts +11 -2
- package/build/facades/ota.d.ts.map +1 -1
- package/build/facades/ota.js +19 -4
- package/build/facades/ota.js.map +1 -1
- package/build/facades/pairing.d.ts +4 -0
- package/build/facades/pairing.d.ts.map +1 -1
- package/build/facades/pairing.js +10 -8
- package/build/facades/pairing.js.map +1 -1
- package/build/facades/phoneNotifications.d.ts +0 -4
- package/build/facades/phoneNotifications.d.ts.map +1 -1
- package/build/facades/phoneNotifications.js +4 -14
- package/build/facades/phoneNotifications.js.map +1 -1
- package/build/index.d.ts +6 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -2
- package/build/index.js.map +1 -1
- package/build/internal.d.ts +2 -2
- package/build/internal.d.ts.map +1 -1
- package/build/internal.js +1 -1
- package/build/internal.js.map +1 -1
- package/build/react/MentraLiveOtaFlow.d.ts +38 -0
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
- package/build/react/MentraLiveOtaFlow.js +528 -0
- package/build/react/MentraLiveOtaFlow.js.map +1 -0
- package/build/react/index.d.ts +2 -0
- package/build/react/index.d.ts.map +1 -0
- package/build/react/index.js +2 -0
- package/build/react/index.js.map +1 -0
- package/build/react/useEngineSnapshot.d.ts +2 -0
- package/build/react/useEngineSnapshot.d.ts.map +1 -0
- package/build/react/useEngineSnapshot.js +14 -0
- package/build/react/useEngineSnapshot.js.map +1 -0
- package/build/runtime/bootstrap.d.ts +24 -1
- package/build/runtime/bootstrap.d.ts.map +1 -1
- package/build/runtime/bootstrap.js +17 -0
- package/build/runtime/bootstrap.js.map +1 -1
- package/build/runtime/scanQrSeam.d.ts +7 -0
- package/build/runtime/scanQrSeam.d.ts.map +1 -0
- package/build/runtime/scanQrSeam.js +16 -0
- package/build/runtime/scanQrSeam.js.map +1 -0
- package/build/services/AppRegistry.d.ts +18 -8
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +44 -45
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/AudioCloudUplink.d.ts +9 -3
- package/build/services/AudioCloudUplink.d.ts.map +1 -1
- package/build/services/AudioCloudUplink.js +9 -3
- package/build/services/AudioCloudUplink.js.map +1 -1
- package/build/services/AudioPlaybackService.d.ts +9 -1
- package/build/services/AudioPlaybackService.d.ts.map +1 -1
- package/build/services/AudioPlaybackService.js +77 -32
- package/build/services/AudioPlaybackService.js.map +1 -1
- package/build/services/BlobStore.d.ts +2 -1
- package/build/services/BlobStore.d.ts.map +1 -1
- package/build/services/BlobStore.js +17 -9
- package/build/services/BlobStore.js.map +1 -1
- package/build/services/CloudAudioSubscriptionSync.d.ts.map +1 -1
- package/build/services/CloudAudioSubscriptionSync.js +13 -1
- package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
- package/build/services/CloudClientService.d.ts +15 -0
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/CloudClientService.js +75 -0
- package/build/services/CloudClientService.js.map +1 -1
- package/build/services/ForegroundLocationPermission.d.ts +13 -0
- package/build/services/ForegroundLocationPermission.d.ts.map +1 -0
- package/build/services/ForegroundLocationPermission.js +12 -0
- package/build/services/ForegroundLocationPermission.js.map +1 -0
- package/build/services/GlassesSettingsSync.d.ts.map +1 -1
- package/build/services/GlassesSettingsSync.js +9 -3
- package/build/services/GlassesSettingsSync.js.map +1 -1
- package/build/services/GlassesStatusProjection.d.ts +2 -1
- package/build/services/GlassesStatusProjection.d.ts.map +1 -1
- package/build/services/GlassesStatusProjection.js +15 -8
- package/build/services/GlassesStatusProjection.js.map +1 -1
- package/build/services/HotspotOtaTransport.d.ts +29 -0
- package/build/services/HotspotOtaTransport.d.ts.map +1 -0
- package/build/services/HotspotOtaTransport.js +146 -0
- package/build/services/HotspotOtaTransport.js.map +1 -0
- package/build/services/HotspotShutdown.d.ts +17 -0
- package/build/services/HotspotShutdown.d.ts.map +1 -0
- package/build/services/HotspotShutdown.js +29 -0
- package/build/services/HotspotShutdown.js.map +1 -0
- package/build/services/LocalDisplayManager.d.ts +15 -0
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +106 -20
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +67 -1
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +486 -126
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/LocalMiniappStorage.d.ts +32 -0
- package/build/services/LocalMiniappStorage.d.ts.map +1 -0
- package/build/services/LocalMiniappStorage.js +66 -0
- package/build/services/LocalMiniappStorage.js.map +1 -0
- package/build/services/LocalMiniappUserIdentity.d.ts +20 -0
- package/build/services/LocalMiniappUserIdentity.d.ts.map +1 -0
- package/build/services/LocalMiniappUserIdentity.js +43 -0
- package/build/services/LocalMiniappUserIdentity.js.map +1 -0
- package/build/services/MicStateCoordinator.d.ts +42 -1
- package/build/services/MicStateCoordinator.d.ts.map +1 -1
- package/build/services/MicStateCoordinator.js +141 -3
- package/build/services/MicStateCoordinator.js.map +1 -1
- package/build/services/MiniappLauncher.d.ts.map +1 -1
- package/build/services/MiniappLauncher.js +23 -3
- package/build/services/MiniappLauncher.js.map +1 -1
- package/build/services/MiniappLiveness.d.ts +12 -0
- package/build/services/MiniappLiveness.d.ts.map +1 -0
- package/build/services/MiniappLiveness.js +16 -0
- package/build/services/MiniappLiveness.js.map +1 -0
- package/build/services/OtaArtifactDownloader.d.ts +65 -0
- package/build/services/OtaArtifactDownloader.d.ts.map +1 -0
- package/build/services/OtaArtifactDownloader.js +226 -0
- package/build/services/OtaArtifactDownloader.js.map +1 -0
- package/build/services/OtaAutoChain.d.ts +34 -0
- package/build/services/OtaAutoChain.d.ts.map +1 -0
- package/build/services/OtaAutoChain.js +83 -0
- package/build/services/OtaAutoChain.js.map +1 -0
- package/build/services/OtaErrorMapping.d.ts +18 -0
- package/build/services/OtaErrorMapping.d.ts.map +1 -0
- package/build/services/OtaErrorMapping.js +63 -0
- package/build/services/OtaErrorMapping.js.map +1 -0
- package/build/services/OtaInstallCoordinator.d.ts +94 -14
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
- package/build/services/OtaInstallCoordinator.js +574 -130
- package/build/services/OtaInstallCoordinator.js.map +1 -1
- package/build/services/OtaService.d.ts.map +1 -1
- package/build/services/OtaService.js +8 -1
- package/build/services/OtaService.js.map +1 -1
- package/build/services/OtaUpdateCheckService.d.ts +38 -3
- package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
- package/build/services/OtaUpdateCheckService.js +142 -22
- package/build/services/OtaUpdateCheckService.js.map +1 -1
- package/build/services/PhoneCalendarService.d.ts.map +1 -1
- package/build/services/PhoneCalendarService.js +22 -1
- package/build/services/PhoneCalendarService.js.map +1 -1
- package/build/services/PhoneCameraFovCoordinator.d.ts +2 -0
- package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
- package/build/services/PhoneCameraFovCoordinator.js +14 -0
- package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
- package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
- package/build/services/PhoneNotificationsSync.js +12 -10
- package/build/services/PhoneNotificationsSync.js.map +1 -1
- package/build/services/PhonePhotoCoordinator.d.ts +10 -3
- package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
- package/build/services/PhonePhotoCoordinator.js +21 -3
- package/build/services/PhonePhotoCoordinator.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts +8 -1
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +125 -13
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/PhoneVideoCoordinator.d.ts +2 -0
- package/build/services/PhoneVideoCoordinator.d.ts.map +1 -1
- package/build/services/PhoneVideoCoordinator.js +10 -0
- package/build/services/PhoneVideoCoordinator.js.map +1 -1
- package/build/services/SceneRenderer.d.ts.map +1 -1
- package/build/services/SceneRenderer.js +15 -0
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/services/SupportProfileSync.d.ts +22 -0
- package/build/services/SupportProfileSync.d.ts.map +1 -0
- package/build/services/SupportProfileSync.js +213 -0
- package/build/services/SupportProfileSync.js.map +1 -0
- package/build/services/TtsTextSanitizer.d.ts +5 -0
- package/build/services/TtsTextSanitizer.d.ts.map +1 -0
- package/build/services/TtsTextSanitizer.js +61 -0
- package/build/services/TtsTextSanitizer.js.map +1 -0
- package/build/services/WifiAdbRequest.d.ts +17 -0
- package/build/services/WifiAdbRequest.d.ts.map +1 -0
- package/build/services/WifiAdbRequest.js +35 -0
- package/build/services/WifiAdbRequest.js.map +1 -0
- package/build/services/asg/asgCameraApi.d.ts +3 -0
- package/build/services/asg/asgCameraApi.d.ts.map +1 -1
- package/build/services/asg/asgCameraApi.js +72 -2
- package/build/services/asg/asgCameraApi.js.map +1 -1
- package/build/services/asg/gallerySyncService.d.ts.map +1 -1
- package/build/services/asg/gallerySyncService.js +26 -14
- package/build/services/asg/gallerySyncService.js.map +1 -1
- package/build/services/asg/localNetworkTransport.d.ts +2 -2
- package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
- package/build/services/asg/localNetworkTransport.js +6 -6
- package/build/services/asg/localNetworkTransport.js.map +1 -1
- package/build/services/audioPlaybackAssets.d.ts +8 -0
- package/build/services/audioPlaybackAssets.d.ts.map +1 -0
- package/build/services/audioPlaybackAssets.js +7 -0
- package/build/services/audioPlaybackAssets.js.map +1 -0
- package/build/services/glassesClockSync.js +2 -2
- package/build/services/glassesClockSync.js.map +1 -1
- package/build/services/manifestActions.d.ts +7 -0
- package/build/services/manifestActions.d.ts.map +1 -0
- package/build/services/manifestActions.js +25 -0
- package/build/services/manifestActions.js.map +1 -0
- package/build/services/otaDisplayState.d.ts +22 -11
- package/build/services/otaDisplayState.d.ts.map +1 -1
- package/build/services/otaDisplayState.js +36 -14
- package/build/services/otaDisplayState.js.map +1 -1
- package/build/services/otaInstallPolicy.d.ts +15 -3
- package/build/services/otaInstallPolicy.d.ts.map +1 -1
- package/build/services/otaInstallPolicy.js +24 -3
- package/build/services/otaInstallPolicy.js.map +1 -1
- package/build/services/otaManifestUrl.d.ts +2 -0
- package/build/services/otaManifestUrl.d.ts.map +1 -0
- package/build/services/{asgOtaVersionUrl.js → otaManifestUrl.js} +15 -5
- package/build/services/otaManifestUrl.js.map +1 -0
- package/build/services/slimStreamStatus.d.ts +11 -0
- package/build/services/slimStreamStatus.d.ts.map +1 -1
- package/build/services/slimStreamStatus.js +14 -4
- package/build/services/slimStreamStatus.js.map +1 -1
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +11 -0
- package/build/stores/apps.js.map +1 -1
- package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
- package/build/stores/bluetoothSettingKeys.js +2 -0
- package/build/stores/bluetoothSettingKeys.js.map +1 -1
- package/build/stores/gallerySync.d.ts +2 -0
- package/build/stores/gallerySync.d.ts.map +1 -1
- package/build/stores/gallerySync.js +2 -0
- package/build/stores/gallerySync.js.map +1 -1
- package/build/stores/glasses.d.ts.map +1 -1
- package/build/stores/glasses.js +3 -0
- package/build/stores/glasses.js.map +1 -1
- package/build/stores/settings.d.ts +17 -0
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +152 -19
- package/build/stores/settings.js.map +1 -1
- package/build/types/applet.d.ts +2 -0
- package/build/types/applet.d.ts.map +1 -1
- package/build/types/applet.js.map +1 -1
- package/build/types/enums.d.ts +1 -0
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +1 -0
- package/build/types/enums.js.map +1 -1
- package/build/types/hardware.d.ts +1 -0
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js +14 -0
- package/build/types/hardware.js.map +1 -1
- package/build/utils/configuredDevHost.d.ts +6 -0
- package/build/utils/configuredDevHost.d.ts.map +1 -0
- package/build/utils/configuredDevHost.js +19 -0
- package/build/utils/configuredDevHost.js.map +1 -0
- package/build/utils/devMiniappLaunch.d.ts +19 -8
- package/build/utils/devMiniappLaunch.d.ts.map +1 -1
- package/build/utils/devMiniappLaunch.js +144 -21
- package/build/utils/devMiniappLaunch.js.map +1 -1
- package/build/utils/diagnosticContext.d.ts.map +1 -1
- package/build/utils/diagnosticContext.js +15 -17
- package/build/utils/diagnosticContext.js.map +1 -1
- package/build/utils/hardware/hardware.d.ts.map +1 -1
- package/build/utils/hardware/hardware.js +7 -3
- package/build/utils/hardware/hardware.js.map +1 -1
- package/build/utils/miniappDiagnostics.d.ts +64 -0
- package/build/utils/miniappDiagnostics.d.ts.map +1 -0
- package/build/utils/miniappDiagnostics.js +173 -0
- package/build/utils/miniappDiagnostics.js.map +1 -0
- package/build/utils/timers/timers.d.ts.map +1 -1
- package/build/utils/timers/timers.js +55 -18
- package/build/utils/timers/timers.js.map +1 -1
- package/package.json +27 -18
- package/src/assets/silence.wav +0 -0
- package/src/engine.ts +7 -2
- package/src/facades/dev.ts +9 -5
- package/src/facades/gallery.ts +1 -1
- package/src/facades/glasses.ts +17 -4
- package/src/facades/glassesSettings.ts +5 -0
- package/src/facades/miniapps.ts +7 -0
- package/src/facades/ota.ts +19 -4
- package/src/facades/pairing.ts +11 -7
- package/src/facades/phoneNotifications.ts +4 -13
- package/src/index.ts +26 -2
- package/src/internal.ts +2 -1
- package/src/react/MentraLiveOtaFlow.tsx +856 -0
- package/src/react/index.ts +7 -0
- package/src/react/useEngineSnapshot.ts +16 -0
- package/src/runtime/bootstrap.ts +33 -1
- package/src/runtime/scanQrSeam.ts +20 -0
- package/src/services/AppRegistry.ts +72 -48
- package/src/services/AudioCloudUplink.ts +9 -3
- package/src/services/AudioPlaybackService.ts +94 -34
- package/src/services/BlobStore.ts +22 -9
- package/src/services/CloudAudioSubscriptionSync.ts +13 -1
- package/src/services/CloudClientService.ts +74 -0
- package/src/services/ForegroundLocationPermission.ts +24 -0
- package/src/services/GlassesSettingsSync.ts +9 -3
- package/src/services/GlassesStatusProjection.ts +16 -8
- package/src/services/HotspotOtaTransport.ts +184 -0
- package/src/services/HotspotShutdown.ts +41 -0
- package/src/services/LocalDisplayManager.ts +112 -21
- package/src/services/LocalMiniappRuntime.ts +576 -130
- package/src/services/LocalMiniappStorage.ts +82 -0
- package/src/services/LocalMiniappUserIdentity.ts +46 -0
- package/src/services/MicStateCoordinator.ts +169 -4
- package/src/services/MiniappLauncher.ts +30 -3
- package/src/services/MiniappLiveness.ts +24 -0
- package/src/services/OtaArtifactDownloader.ts +312 -0
- package/src/services/OtaAutoChain.ts +106 -0
- package/src/services/OtaErrorMapping.ts +80 -0
- package/src/services/OtaInstallCoordinator.ts +645 -143
- package/src/services/OtaService.ts +11 -5
- package/src/services/OtaUpdateCheckService.ts +180 -20
- package/src/services/PhoneCalendarService.ts +27 -4
- package/src/services/PhoneCameraFovCoordinator.ts +15 -0
- package/src/services/PhoneNotificationsSync.ts +12 -10
- package/src/services/PhonePhotoCoordinator.ts +33 -7
- package/src/services/PhoneStreamCoordinator.ts +139 -25
- package/src/services/PhoneVideoCoordinator.ts +11 -0
- package/src/services/SceneRenderer.ts +16 -0
- package/src/services/SupportProfileSync.ts +218 -0
- package/src/services/TtsTextSanitizer.ts +66 -0
- package/src/services/WifiAdbRequest.ts +53 -0
- package/src/services/asg/asgCameraApi.ts +99 -8
- package/src/services/asg/gallerySyncService.ts +28 -19
- package/src/services/asg/localNetworkTransport.ts +7 -11
- package/src/services/audioPlaybackAssets.ts +8 -0
- package/src/services/glassesClockSync.ts +2 -2
- package/src/services/manifestActions.ts +30 -0
- package/src/services/otaDisplayState.ts +57 -16
- package/src/services/otaInstallPolicy.ts +29 -3
- package/src/services/{asgOtaVersionUrl.ts → otaManifestUrl.ts} +16 -4
- package/src/services/slimStreamStatus.ts +16 -5
- package/src/stores/apps.ts +12 -0
- package/src/stores/bluetoothSettingKeys.ts +2 -0
- package/src/stores/gallerySync.ts +4 -0
- package/src/stores/glasses.ts +3 -0
- package/src/stores/settings.ts +174 -21
- package/src/types/applet.ts +2 -0
- package/src/types/enums.ts +2 -1
- package/src/types/hardware.ts +17 -0
- package/src/utils/configuredDevHost.ts +17 -0
- package/src/utils/devMiniappLaunch.ts +151 -23
- package/src/utils/diagnosticContext.ts +17 -17
- package/src/utils/hardware/hardware.ts +7 -9
- package/src/utils/miniappDiagnostics.ts +226 -0
- package/src/utils/timers/timers.ts +57 -21
- package/build/services/asgOtaVersionUrl.d.ts +0 -2
- package/build/services/asgOtaVersionUrl.d.ts.map +0 -1
- package/build/services/asgOtaVersionUrl.js.map +0 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {useEffect, useRef, useState} from "react"
|
|
2
|
+
|
|
3
|
+
export function useEngineSnapshot<T>(getSnapshot: () => T, subscribe: (onChange: () => void) => () => void): T {
|
|
4
|
+
const getSnapshotRef = useRef(getSnapshot)
|
|
5
|
+
const subscribeRef = useRef(subscribe)
|
|
6
|
+
getSnapshotRef.current = getSnapshot
|
|
7
|
+
subscribeRef.current = subscribe
|
|
8
|
+
const [snapshot, setSnapshot] = useState(() => getSnapshot())
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
setSnapshot(getSnapshotRef.current())
|
|
12
|
+
return subscribeRef.current(() => setSnapshot(getSnapshotRef.current()))
|
|
13
|
+
}, [])
|
|
14
|
+
|
|
15
|
+
return snapshot
|
|
16
|
+
}
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -50,9 +50,22 @@ export interface IslandUiSeams {
|
|
|
50
50
|
* `session.glasses.requestWifiSetup` — open the host's glasses Wi-Fi setup
|
|
51
51
|
* flow. Absent ⇒ miniapps get NOT_IMPLEMENTED for the request.
|
|
52
52
|
*/
|
|
53
|
-
requestWifiSetup?: (reason?: string) => Promise<void> | void
|
|
53
|
+
requestWifiSetup?: (reason?: string, packageName?: string) => Promise<void> | void
|
|
54
|
+
/**
|
|
55
|
+
* `session.system.scanQr` — open a phone-camera QR scanner overlay. Must NOT
|
|
56
|
+
* clear miniapp foreground: UI_CLOSE on a live call hangs it up. Absent ⇒
|
|
57
|
+
* miniapps get NOT_IMPLEMENTED.
|
|
58
|
+
*/
|
|
59
|
+
scanQr?: (options?: ScanQrOptions) => Promise<ScanQrResult>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ScanQrOptions {
|
|
63
|
+
title?: string
|
|
64
|
+
hint?: string
|
|
54
65
|
}
|
|
55
66
|
|
|
67
|
+
export type ScanQrResult = {data: string} | {cancelled: true}
|
|
68
|
+
|
|
56
69
|
export interface IslandConfigureOptions {
|
|
57
70
|
/** REQUIRED — the only must-have seam. The host owns login; engine owns the rest. */
|
|
58
71
|
auth: IslandAuth
|
|
@@ -81,6 +94,19 @@ export function configure(opts: IslandConfigureOptions): void {
|
|
|
81
94
|
options = opts
|
|
82
95
|
}
|
|
83
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Merge host-UI seams after configure/start. UI capabilities (scan overlay,
|
|
99
|
+
* wifi setup) are looked up per request, so this is safe on a running runtime.
|
|
100
|
+
* Used so Metro reloads can attach seams that missed the original configure().
|
|
101
|
+
*/
|
|
102
|
+
export function updateUiSeams(ui: IslandUiSeams): void {
|
|
103
|
+
if (!options) {
|
|
104
|
+
console.warn("engine.updateUiSeams() called before engine.configure(); ignored")
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
options = {...options, ui: {...options.ui, ...ui}}
|
|
108
|
+
}
|
|
109
|
+
|
|
84
110
|
/** Mark the runtime started. Idempotent. */
|
|
85
111
|
export async function start(): Promise<void> {
|
|
86
112
|
if (started) return
|
|
@@ -99,6 +125,12 @@ export function isStarted(): boolean {
|
|
|
99
125
|
return started
|
|
100
126
|
}
|
|
101
127
|
|
|
128
|
+
/** Test-only: wipe configure state so suites can start from a cold host. */
|
|
129
|
+
export function resetForTests(): void {
|
|
130
|
+
options = null
|
|
131
|
+
started = false
|
|
132
|
+
}
|
|
133
|
+
|
|
102
134
|
/** The host-supplied auth provider, or null if not configured yet. */
|
|
103
135
|
export function getAuth(): IslandAuth | null {
|
|
104
136
|
return options?.auth ?? null
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {ScanQrOptions, ScanQrResult} from "./bootstrap"
|
|
2
|
+
|
|
3
|
+
export class ScanQrNotConfiguredError extends Error {
|
|
4
|
+
readonly code = "NOT_IMPLEMENTED"
|
|
5
|
+
constructor() {
|
|
6
|
+
super("QR scanning is not configured on this host. Reload the Mentra App and try again.")
|
|
7
|
+
this.name = "ScanQrNotConfiguredError"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function invokeScanQrSeam(
|
|
12
|
+
scanQr: ((options?: ScanQrOptions) => Promise<ScanQrResult>) | undefined,
|
|
13
|
+
payload: Record<string, unknown> = {},
|
|
14
|
+
): Promise<ScanQrResult> {
|
|
15
|
+
if (!scanQr) throw new ScanQrNotConfiguredError()
|
|
16
|
+
return scanQr({
|
|
17
|
+
title: typeof payload.title === "string" ? payload.title : undefined,
|
|
18
|
+
hint: typeof payload.hint === "string" ? payload.hint : undefined,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
@@ -22,14 +22,17 @@ import {unzip} from "react-native-zip-archive"
|
|
|
22
22
|
import semver from "semver"
|
|
23
23
|
import {AsyncResult, Result, result as Res} from "typesafe-ts"
|
|
24
24
|
|
|
25
|
-
import type {AppletPermission, AppPermissionType, AppletType, ClientApp
|
|
25
|
+
import type {AppletPermission, AppPermissionType, AppletType, ClientApp} from "../types/applet"
|
|
26
26
|
import {HardwareRequirement, HardwareRequirementLevel, HardwareType} from "../types"
|
|
27
|
-
import {
|
|
27
|
+
import {configuredDevHost} from "../utils/configuredDevHost"
|
|
28
28
|
import {storage} from "../utils/storage/storage"
|
|
29
29
|
import {printDirectory} from "../utils/storage/zip"
|
|
30
30
|
import {checkManifestVersions} from "./manifestVersionGate"
|
|
31
|
+
import {normalizeManifestActions} from "./manifestActions"
|
|
31
32
|
import {miniappRunningRegistry} from "./MiniappRunningRegistry"
|
|
32
33
|
|
|
34
|
+
export {normalizeManifestActions} from "./manifestActions"
|
|
35
|
+
|
|
33
36
|
const ALLOWED_PERMISSION_TYPES: ReadonlySet<AppPermissionType> = new Set<AppPermissionType>([
|
|
34
37
|
"MICROPHONE",
|
|
35
38
|
"CAMERA",
|
|
@@ -73,29 +76,6 @@ function normalizeManifestType(raw: unknown): AppletType {
|
|
|
73
76
|
return raw === "background" || raw === "system_dashboard" || raw === "standard" ? raw : "standard"
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
/**
|
|
77
|
-
* Normalize a manifest's `actions` into DeclaredAction[]. Defensive — keeps only
|
|
78
|
-
* well-formed `{id, description}` entries (installed/dev bundles may be
|
|
79
|
-
* malformed). Shared by installed (disk) and dev-sideload projection so both
|
|
80
|
-
* surface declared actions to session.miniapps.list + the invoke gate.
|
|
81
|
-
*/
|
|
82
|
-
export function normalizeManifestActions(raw: unknown): DeclaredAction[] {
|
|
83
|
-
if (!Array.isArray(raw)) return []
|
|
84
|
-
const out: DeclaredAction[] = []
|
|
85
|
-
for (const a of raw as Array<{id?: unknown; description?: unknown; parameters?: unknown}>) {
|
|
86
|
-
if (a && typeof a.id === "string" && typeof a.description === "string") {
|
|
87
|
-
out.push({
|
|
88
|
-
id: a.id,
|
|
89
|
-
description: a.description,
|
|
90
|
-
...(a.parameters && typeof a.parameters === "object"
|
|
91
|
-
? {parameters: a.parameters as Record<string, unknown>}
|
|
92
|
-
: {}),
|
|
93
|
-
})
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return out
|
|
97
|
-
}
|
|
98
|
-
|
|
99
79
|
/**
|
|
100
80
|
* Convert declared hardwareRequirements from a miniapp.json manifest into
|
|
101
81
|
* runtime `HardwareRequirement[]`, always appending `{EXIST, REQUIRED}` so
|
|
@@ -159,6 +139,17 @@ interface InstalledLma {
|
|
|
159
139
|
versions: Record<string, InstalledInfo>
|
|
160
140
|
}
|
|
161
141
|
|
|
142
|
+
export interface MiniappReleaseIdentity {
|
|
143
|
+
source: "direct_download" | "bundled_asset" | "preinstalled_registry" | "dev_snapshot"
|
|
144
|
+
releaseId?: string
|
|
145
|
+
bundleSha256?: string
|
|
146
|
+
channel?: string
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function releaseIdentityKey(packageName: string, version: string): string {
|
|
150
|
+
return `miniapp_release_identity:${packageName}:${version}`
|
|
151
|
+
}
|
|
152
|
+
|
|
162
153
|
/**
|
|
163
154
|
* Download a miniapp zip from `url` into the cache and return its local path.
|
|
164
155
|
*
|
|
@@ -451,11 +442,18 @@ class AppRegistry {
|
|
|
451
442
|
* of `manifest.version`. The dev caching path uses `dev-<ms>` so multiple
|
|
452
443
|
* snapshots can coexist alongside semver-installed versions.
|
|
453
444
|
*/
|
|
454
|
-
public installFromUrl(
|
|
445
|
+
public installFromUrl(
|
|
446
|
+
url: string,
|
|
447
|
+
opts?: {versionOverride?: string; releaseIdentity?: MiniappReleaseIdentity},
|
|
448
|
+
): AsyncResult<void, Error> {
|
|
455
449
|
return Res.try_async(async () => {
|
|
456
450
|
const {packageName, version} = await downloadAndInstallMiniApp(url, opts?.versionOverride)
|
|
457
451
|
console.log("APP_REGISTRY: Downloaded and installed mini app")
|
|
458
|
-
this.finalizeInstall(
|
|
452
|
+
this.finalizeInstall(
|
|
453
|
+
packageName,
|
|
454
|
+
version,
|
|
455
|
+
opts?.releaseIdentity ?? {source: version.startsWith("dev-") ? "dev_snapshot" : "direct_download"},
|
|
456
|
+
)
|
|
459
457
|
})
|
|
460
458
|
}
|
|
461
459
|
|
|
@@ -468,12 +466,12 @@ class AppRegistry {
|
|
|
468
466
|
*/
|
|
469
467
|
public installFromLocalZip(
|
|
470
468
|
zipPath: string,
|
|
471
|
-
opts?: {versionOverride?: string},
|
|
469
|
+
opts?: {versionOverride?: string; releaseIdentity?: MiniappReleaseIdentity},
|
|
472
470
|
): AsyncResult<{packageName: string; version: string}, Error> {
|
|
473
471
|
return Res.try_async(async () => {
|
|
474
472
|
const {packageName, version} = await unpackMiniApp(zipPath, opts?.versionOverride)
|
|
475
473
|
console.log("APP_REGISTRY: Installed mini app from local zip")
|
|
476
|
-
this.finalizeInstall(packageName, version)
|
|
474
|
+
this.finalizeInstall(packageName, version, opts?.releaseIdentity ?? {source: "bundled_asset"})
|
|
477
475
|
return {packageName, version}
|
|
478
476
|
})
|
|
479
477
|
}
|
|
@@ -483,7 +481,7 @@ class AppRegistry {
|
|
|
483
481
|
* artifacts on release installs, point the active-version at the just-
|
|
484
482
|
* installed bundle, and notify subscribers to refresh.
|
|
485
483
|
*/
|
|
486
|
-
private finalizeInstall(packageName: string, version: string): void {
|
|
484
|
+
private finalizeInstall(packageName: string, version: string, releaseIdentity: MiniappReleaseIdentity): void {
|
|
487
485
|
// If this is a release install (semver, not dev-*) of a package that
|
|
488
486
|
// currently has dev-* snapshots, clear the dev state so the swap to
|
|
489
487
|
// "released" is clean. Otherwise the dev version would keep winning
|
|
@@ -495,10 +493,16 @@ class AppRegistry {
|
|
|
495
493
|
}
|
|
496
494
|
|
|
497
495
|
this.setActiveVersion(packageName, version)
|
|
496
|
+
storage.save(releaseIdentityKey(packageName, version), releaseIdentity)
|
|
498
497
|
this.refreshNeeded = true
|
|
499
498
|
this.notify()
|
|
500
499
|
}
|
|
501
500
|
|
|
501
|
+
public getReleaseIdentity(packageName: string, version: string): MiniappReleaseIdentity | null {
|
|
502
|
+
const result = storage.load<MiniappReleaseIdentity>(releaseIdentityKey(packageName, version))
|
|
503
|
+
return result.is_ok() ? result.value : null
|
|
504
|
+
}
|
|
505
|
+
|
|
502
506
|
public installFromJsonUrl(baseUrl: string): AsyncResult<{packageName: string; version: string; name: string}, Error> {
|
|
503
507
|
return Res.try_async(async () => {
|
|
504
508
|
const trimmed = baseUrl.replace(/\/$/, "")
|
|
@@ -534,6 +538,7 @@ class AppRegistry {
|
|
|
534
538
|
for (const item of pkgDir.list()) {
|
|
535
539
|
if (item instanceof Directory && item.name.startsWith("dev-")) {
|
|
536
540
|
try {
|
|
541
|
+
storage.remove(releaseIdentityKey(packageName, item.name))
|
|
537
542
|
item.delete()
|
|
538
543
|
} catch (e) {
|
|
539
544
|
console.warn(`APP_REGISTRY: failed to delete ${item.name}:`, e)
|
|
@@ -546,6 +551,7 @@ class AppRegistry {
|
|
|
546
551
|
}
|
|
547
552
|
storage.remove(`${packageName}_dev_url`)
|
|
548
553
|
storage.remove(`${packageName}_dev_port`)
|
|
554
|
+
storage.remove(`${packageName}_dev_mdns`)
|
|
549
555
|
storage.remove(`${packageName}_dev_last_reachable`)
|
|
550
556
|
// HTTP-direct dev miniapps are indexed by their real package name. A
|
|
551
557
|
// release install/uninstall of that same package should remove only its
|
|
@@ -588,12 +594,16 @@ class AppRegistry {
|
|
|
588
594
|
// Guard exists: a dev miniapp loads over HTTP and has no on-disk dir,
|
|
589
595
|
// so an unconditional delete() would throw and abort the cleanup below.
|
|
590
596
|
if (lmaDir.exists) lmaDir.delete()
|
|
597
|
+
storage.remove(releaseIdentityKey(packageName, version))
|
|
591
598
|
console.log("APP_REGISTRY: Uninstalled mini app version", version)
|
|
592
599
|
const packageDir = new Directory(Paths.document, "lmas", packageName)
|
|
593
600
|
if (packageDir.exists && packageDir.list().length === 0) {
|
|
594
601
|
packageDir.delete()
|
|
595
602
|
}
|
|
596
603
|
} else {
|
|
604
|
+
for (const installedVersion of this.getInstalledVersions(packageName)) {
|
|
605
|
+
storage.remove(releaseIdentityKey(packageName, installedVersion))
|
|
606
|
+
}
|
|
597
607
|
const packageDir = new Directory(Paths.document, "lmas", packageName)
|
|
598
608
|
if (packageDir.exists) {
|
|
599
609
|
packageDir.delete()
|
|
@@ -732,7 +742,7 @@ class AppRegistry {
|
|
|
732
742
|
permissions?: Array<string | {type: string; required?: boolean; description?: string}>
|
|
733
743
|
hardwareRequirements?: Array<{type: string; level: string; description?: string}>
|
|
734
744
|
type?: string
|
|
735
|
-
actions?: Array<{id?: unknown; description?: unknown; parameters?: unknown}>
|
|
745
|
+
actions?: Array<{id?: unknown; description?: unknown; parameters?: unknown; outputSchema?: unknown}>
|
|
736
746
|
} | null
|
|
737
747
|
|
|
738
748
|
const permissions = normalizeManifestPermissions(manifest?.permissions)
|
|
@@ -930,11 +940,17 @@ export interface DevAppRecord {
|
|
|
930
940
|
iconUrl: string
|
|
931
941
|
devUrl: string
|
|
932
942
|
devPort?: number
|
|
943
|
+
/**
|
|
944
|
+
* Bonjour / mDNS hostname from the QR (`ComputerName.local`). Used as a
|
|
945
|
+
* failover host when the raw LAN IP in `devUrl` goes stale after a Wi-Fi
|
|
946
|
+
* change — phones that resolve `.local` can relaunch without re-scanning.
|
|
947
|
+
*/
|
|
948
|
+
mdnsHost?: string
|
|
933
949
|
type?: AppletType
|
|
934
950
|
permissions?: Array<string | {type: string; required?: boolean; description?: string}>
|
|
935
951
|
hardwareRequirements?: Array<{type: string; level: string; description?: string}>
|
|
936
952
|
/** Manifest-declared actions — so dev-sideloaded miniapps can be invoked too. */
|
|
937
|
-
actions?: Array<{id?: unknown; description?: unknown; parameters?: unknown}>
|
|
953
|
+
actions?: Array<{id?: unknown; description?: unknown; parameters?: unknown; outputSchema?: unknown}>
|
|
938
954
|
/** Legacy single-slot migration field. New records use the real packageName directly. */
|
|
939
955
|
sourcePackageName?: string
|
|
940
956
|
/**
|
|
@@ -948,22 +964,6 @@ export interface DevAppRecord {
|
|
|
948
964
|
const DEV_APPS_INDEX_KEY = "dev_apps_index"
|
|
949
965
|
const DEV_APP_ICONS_DIR = "dev-miniapp-icons"
|
|
950
966
|
|
|
951
|
-
function configuredDevHost(): string | undefined {
|
|
952
|
-
// Explicit escape hatch first; otherwise the host-injected Metro host (the
|
|
953
|
-
// address this dev bundle was served from — always current for the network
|
|
954
|
-
// the phone is on). Deliberately NOT the EXPO_PUBLIC_CLOUD_* URLs: those are
|
|
955
|
-
// cloud endpoints, a different machine entirely from the laptop running the
|
|
956
|
-
// miniapp dev server, and rewriting a dev URL to a cloud host would break it.
|
|
957
|
-
const explicit = process.env.EXPO_PUBLIC_LOCAL_MINIAPP_HOST
|
|
958
|
-
if (explicit) {
|
|
959
|
-
try {
|
|
960
|
-
return new URL(explicit).hostname
|
|
961
|
-
} catch {
|
|
962
|
-
if (/^[\w.-]+$/.test(explicit)) return explicit
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
return getConfigValues().devServerHost?.()
|
|
966
|
-
}
|
|
967
967
|
|
|
968
968
|
function isPrivateLanHost(hostname: string): boolean {
|
|
969
969
|
return (
|
|
@@ -1054,6 +1054,7 @@ function removeDevRecordKeys(packageName: string): void {
|
|
|
1054
1054
|
storage.remove(`${packageName}_dev_meta`)
|
|
1055
1055
|
storage.remove(`${packageName}_dev_url`)
|
|
1056
1056
|
storage.remove(`${packageName}_dev_port`)
|
|
1057
|
+
storage.remove(`${packageName}_dev_mdns`)
|
|
1057
1058
|
storage.remove(`${packageName}_dev_last_reachable`)
|
|
1058
1059
|
}
|
|
1059
1060
|
|
|
@@ -1092,16 +1093,28 @@ function migrateLegacyDevSlot(): void {
|
|
|
1092
1093
|
const port = migrated.devPort ?? (legacyPort.is_ok() ? legacyPort.value : undefined)
|
|
1093
1094
|
if (typeof port === "number" && Number.isFinite(port)) storage.save(`${targetPackage}_dev_port`, port)
|
|
1094
1095
|
}
|
|
1096
|
+
const legacyMdns = storage.load<string>(`${DEV_APP_PACKAGE_NAME}_dev_mdns`)
|
|
1097
|
+
const targetMdns = storage.load<string>(`${targetPackage}_dev_mdns`)
|
|
1098
|
+
if (!targetMdns.is_ok() && legacyMdns.is_ok() && legacyMdns.value.trim()) {
|
|
1099
|
+
storage.save(`${targetPackage}_dev_mdns`, legacyMdns.value.trim())
|
|
1100
|
+
}
|
|
1095
1101
|
|
|
1096
1102
|
storage.remove(`${DEV_APP_PACKAGE_NAME}_dev_meta`)
|
|
1097
1103
|
storage.remove(`${DEV_APP_PACKAGE_NAME}_dev_url`)
|
|
1098
1104
|
storage.remove(`${DEV_APP_PACKAGE_NAME}_dev_port`)
|
|
1105
|
+
storage.remove(`${DEV_APP_PACKAGE_NAME}_dev_mdns`)
|
|
1099
1106
|
storage.remove(`${DEV_APP_PACKAGE_NAME}_dev_last_reachable`)
|
|
1100
1107
|
|
|
1101
1108
|
const index = Array.from(new Set(getDevAppIndex().map((pkg) => (pkg === DEV_APP_PACKAGE_NAME ? targetPackage : pkg))))
|
|
1102
1109
|
storage.save(DEV_APPS_INDEX_KEY, JSON.stringify(index))
|
|
1103
1110
|
}
|
|
1104
1111
|
|
|
1112
|
+
function readStoredMdnsHost(packageName: string): string | undefined {
|
|
1113
|
+
const res = storage.load<string>(`${packageName}_dev_mdns`)
|
|
1114
|
+
if (!res.is_ok()) return undefined
|
|
1115
|
+
return res.value.trim() || undefined
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1105
1118
|
/**
|
|
1106
1119
|
* Register or update one dev miniapp under its real manifest package. Different
|
|
1107
1120
|
* package names coexist; rescanning the same package updates that package only.
|
|
@@ -1112,12 +1125,18 @@ export async function registerDevApp(record: DevAppRecord): Promise<void> {
|
|
|
1112
1125
|
if (!packageName) throw new Error("Dev miniapp manifest is missing packageName")
|
|
1113
1126
|
|
|
1114
1127
|
const iconUrl = await cacheDevAppIcon(packageName, record.iconUrl)
|
|
1128
|
+
// Relaunch paths (developer-URL screen, loadDevMiniapp) omit mdnsHost, so an
|
|
1129
|
+
// omitted field keeps whatever the QR scan stored instead of wiping the
|
|
1130
|
+
// `.local` failover host.
|
|
1131
|
+
const mdnsHost =
|
|
1132
|
+
record.mdnsHost !== undefined ? record.mdnsHost.trim() || undefined : readStoredMdnsHost(packageName)
|
|
1115
1133
|
const devRecord: DevAppRecord = {
|
|
1116
1134
|
...record,
|
|
1117
1135
|
packageName,
|
|
1118
1136
|
sourcePackageName: packageName,
|
|
1119
1137
|
name: record.name || DEV_APP_NAME,
|
|
1120
1138
|
iconUrl,
|
|
1139
|
+
mdnsHost,
|
|
1121
1140
|
}
|
|
1122
1141
|
storage.save(`${packageName}_dev_meta`, JSON.stringify(devRecord))
|
|
1123
1142
|
storage.save(`${packageName}_dev_url`, record.devUrl)
|
|
@@ -1126,6 +1145,11 @@ export async function registerDevApp(record: DevAppRecord): Promise<void> {
|
|
|
1126
1145
|
} else {
|
|
1127
1146
|
storage.remove(`${packageName}_dev_port`)
|
|
1128
1147
|
}
|
|
1148
|
+
if (mdnsHost) {
|
|
1149
|
+
storage.save(`${packageName}_dev_mdns`, mdnsHost)
|
|
1150
|
+
} else {
|
|
1151
|
+
storage.remove(`${packageName}_dev_mdns`)
|
|
1152
|
+
}
|
|
1129
1153
|
const idx = getDevAppIndex()
|
|
1130
1154
|
if (!idx.includes(packageName)) {
|
|
1131
1155
|
idx.push(packageName)
|
|
@@ -21,9 +21,15 @@ let sub: {remove: () => void} | null = null
|
|
|
21
21
|
const suppressionSources = new Set<string>()
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Keep the glasses LC3 transport alive while
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Keep the glasses LC3 transport alive while dropping mic audio before cloud
|
|
25
|
+
* STT. Sources are reference-counted by id so overlapping playback cannot
|
|
26
|
+
* reopen the uplink until every active source has released.
|
|
27
|
+
*
|
|
28
|
+
* Suppression is global to the uplink: one source silences STT for every app.
|
|
29
|
+
* That is why no platform path arms it any more (spoken TTS used to, which made
|
|
30
|
+
* every assistant reply punch a hole in Live Translation and Captions). The
|
|
31
|
+
* mechanism stays because it is the right primitive for an explicit,
|
|
32
|
+
* caller-scoped mute; see OS-1741 for the per-app opt-in that will use it.
|
|
27
33
|
*/
|
|
28
34
|
export function setAudioCloudUplinkSuppressed(sourceId: string, suppressed: boolean): void {
|
|
29
35
|
if (suppressed) suppressionSources.add(sourceId)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {createAudioPlayer, AudioPlayer, AudioStatus, setAudioModeAsync} from "expo-audio"
|
|
2
2
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
3
|
-
import {Platform} from "react-native"
|
|
3
|
+
import {AppState, Platform} from "react-native"
|
|
4
4
|
import {BgTimer} from "../utils/timers"
|
|
5
5
|
import {setAudioCloudUplinkSuppressed} from "./AudioCloudUplink"
|
|
6
|
+
import {SILENT_AUDIO_SOURCE} from "./audioPlaybackAssets"
|
|
6
7
|
|
|
7
8
|
const RESTORE_GLASSES_VOLUME_AFTER_PLAYBACK = false
|
|
8
9
|
|
|
@@ -12,7 +13,12 @@ interface AudioPlayRequest {
|
|
|
12
13
|
appId?: string
|
|
13
14
|
volume?: number
|
|
14
15
|
stopOtherAudio?: boolean
|
|
15
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Suppress microphone audio sent to cloud STT while this audio is audible.
|
|
18
|
+
* Opt-in and off by default: the suppression is global to the uplink, so a
|
|
19
|
+
* caller that sets this silences STT for every running app, not just itself.
|
|
20
|
+
* No platform path sets it today (see AudioCloudUplink and OS-1741).
|
|
21
|
+
*/
|
|
16
22
|
suppressCloudUplink?: boolean
|
|
17
23
|
}
|
|
18
24
|
|
|
@@ -28,6 +34,7 @@ type AudioPlaybackCompletion = (
|
|
|
28
34
|
|
|
29
35
|
interface PlaybackState {
|
|
30
36
|
requestId: string
|
|
37
|
+
audioUrl: string
|
|
31
38
|
uplinkSuppressionId: string
|
|
32
39
|
appId?: string
|
|
33
40
|
startTime: number
|
|
@@ -38,6 +45,9 @@ interface PlaybackState {
|
|
|
38
45
|
|
|
39
46
|
interface PendingPlaybackState {
|
|
40
47
|
cancelled: boolean
|
|
48
|
+
audioUrl: string
|
|
49
|
+
appId?: string
|
|
50
|
+
createdAt: number
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
/** Open request for a live PCM output stream (miniapp speaker.createStream). */
|
|
@@ -91,9 +101,9 @@ class AudioPlaybackService {
|
|
|
91
101
|
// window after actual audio so we only pre-roll the first sound after idle.
|
|
92
102
|
private audioRouteWarmUntil = 0
|
|
93
103
|
private audioRouteWarmupPromise: Promise<void> | null = null
|
|
94
|
-
//
|
|
95
|
-
// Track those completed sources so a
|
|
96
|
-
// listening immediately instead of inheriting
|
|
104
|
+
// Playback that opted into suppression stays suppressed briefly while its
|
|
105
|
+
// A2DP tail drains. Track those completed sources so a later unsuppressed
|
|
106
|
+
// sound can resume listening immediately instead of inheriting that gate.
|
|
97
107
|
private tailUplinkSuppressions = new Set<string>()
|
|
98
108
|
// Original glasses media volume captured before we bump it for A2DP playback.
|
|
99
109
|
private glassesVolumeRestoreLevel: number | null = null
|
|
@@ -102,13 +112,15 @@ class AudioPlaybackService {
|
|
|
102
112
|
private static readonly AUDIO_ROUTE_WARM_WINDOW_MS = 1500
|
|
103
113
|
private static readonly AUDIO_ROUTE_PREWARM_MS = 200
|
|
104
114
|
private static readonly AUDIO_ROUTE_PREWARM_SAMPLE_RATE = 16_000
|
|
115
|
+
/** Break accidental same-app replay storms without affecting deliberate later replays. */
|
|
116
|
+
private static readonly DUPLICATE_PLAY_WINDOW_MS = 750
|
|
105
117
|
private static readonly AUDIO_ROUTE_PREWARM_PCM_BASE64 =
|
|
106
|
-
"AAAA".repeat(
|
|
107
|
-
(
|
|
108
|
-
AudioPlaybackService.AUDIO_ROUTE_PREWARM_SAMPLE_RATE *
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
118
|
+
"AAAA".repeat(
|
|
119
|
+
Math.floor(
|
|
120
|
+
(AudioPlaybackService.AUDIO_ROUTE_PREWARM_MS * AudioPlaybackService.AUDIO_ROUTE_PREWARM_SAMPLE_RATE * 2) /
|
|
121
|
+
(1000 * 3),
|
|
122
|
+
),
|
|
123
|
+
) + "AA=="
|
|
112
124
|
/** If glasses report step volume at or below this, bump to FLOOR before A2DP playback. */
|
|
113
125
|
private static readonly GLASSES_VOLUME_LOW_THRESHOLD = 2
|
|
114
126
|
private static readonly GLASSES_VOLUME_FLOOR = 9
|
|
@@ -189,6 +201,14 @@ class AudioPlaybackService {
|
|
|
189
201
|
*/
|
|
190
202
|
private async prewarmAudioRouteIfCold(): Promise<void> {
|
|
191
203
|
if (Platform.OS !== "android" || this.isAudioRouteWarm()) return
|
|
204
|
+
// This silent PCM pre-roll is only an anti-clipping optimization. Some
|
|
205
|
+
// Android devices defer opening its AudioTrack until the host Activity
|
|
206
|
+
// resumes even though the real URL player supports background playback.
|
|
207
|
+
// Never let that optional pre-roll block a glasses-initiated prompt.
|
|
208
|
+
if (AppState.currentState !== "active") {
|
|
209
|
+
console.log(`AUDIO: Skipping cold-route prewarm while app is ${AppState.currentState}`)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
192
212
|
if (this.audioRouteWarmupPromise) return this.audioRouteWarmupPromise
|
|
193
213
|
|
|
194
214
|
const streamId = `audio-route-prewarm-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
@@ -321,6 +341,34 @@ class AudioPlaybackService {
|
|
|
321
341
|
}
|
|
322
342
|
}
|
|
323
343
|
|
|
344
|
+
private unloadPlaybackSource(playback: PlaybackState, reason: string): void {
|
|
345
|
+
if (this.loadedPlayback !== playback) return
|
|
346
|
+
const player = this.player
|
|
347
|
+
if (!player) {
|
|
348
|
+
this.loadedPlayback = null
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
player.pause()
|
|
354
|
+
} catch (e) {
|
|
355
|
+
console.warn(`AUDIO: Error pausing ${playback.requestId} during ${reason}:`, e)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
// replace(null) fails native argument casting on iOS, while remove()
|
|
360
|
+
// destroys the ExoPlayer that Android must reuse to avoid AudioTrack
|
|
361
|
+
// exhaustion. Replacing with bundled silence detaches the previous media
|
|
362
|
+
// item on both platforms and leaves the native player alive.
|
|
363
|
+
player.replace(SILENT_AUDIO_SOURCE)
|
|
364
|
+
console.log(`AUDIO: Cleared source for ${playback.requestId} during ${reason}`)
|
|
365
|
+
} catch (e) {
|
|
366
|
+
console.warn(`AUDIO: Error clearing ${playback.requestId} during ${reason}:`, e)
|
|
367
|
+
} finally {
|
|
368
|
+
if (this.loadedPlayback === playback) this.loadedPlayback = null
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
324
372
|
private clearFinishedPlayerAfterTail(playback: PlaybackState): void {
|
|
325
373
|
if (playback.suppressCloudUplink) {
|
|
326
374
|
this.tailUplinkSuppressions.add(playback.uplinkSuppressionId)
|
|
@@ -332,18 +380,7 @@ class AudioPlaybackService {
|
|
|
332
380
|
if (playback.suppressCloudUplink && this.tailUplinkSuppressions.delete(playback.uplinkSuppressionId)) {
|
|
333
381
|
setAudioCloudUplinkSuppressed(playback.uplinkSuppressionId, false)
|
|
334
382
|
}
|
|
335
|
-
|
|
336
|
-
if (this.loadedPlayback !== playback) return
|
|
337
|
-
try {
|
|
338
|
-
this.player.pause()
|
|
339
|
-
this.player.replace(null)
|
|
340
|
-
this.loadedPlayback = null
|
|
341
|
-
console.log(
|
|
342
|
-
`AUDIO: Cleared finished player for ${playback.requestId} after ${AudioPlaybackService.A2DP_TAIL_DRAIN_MS}ms tail drain`,
|
|
343
|
-
)
|
|
344
|
-
} catch (e) {
|
|
345
|
-
console.warn("AUDIO: Error clearing player after tail drain:", e)
|
|
346
|
-
}
|
|
383
|
+
this.unloadPlaybackSource(playback, `${AudioPlaybackService.A2DP_TAIL_DRAIN_MS}ms A2DP tail drain`)
|
|
347
384
|
}, AudioPlaybackService.A2DP_TAIL_DRAIN_MS)
|
|
348
385
|
}
|
|
349
386
|
|
|
@@ -360,7 +397,27 @@ class AudioPlaybackService {
|
|
|
360
397
|
*/
|
|
361
398
|
public async play(request: AudioPlayRequest, onComplete: AudioPlaybackCompletion): Promise<void> {
|
|
362
399
|
const {requestId, audioUrl, appId, volume = 1.0, stopOtherAudio = true, suppressCloudUplink = false} = request
|
|
363
|
-
const
|
|
400
|
+
const now = Date.now()
|
|
401
|
+
const activeDuplicate =
|
|
402
|
+
this.currentPlayback &&
|
|
403
|
+
!this.currentPlayback.completed &&
|
|
404
|
+
this.currentPlayback.appId === appId &&
|
|
405
|
+
this.currentPlayback.audioUrl === audioUrl &&
|
|
406
|
+
now - this.currentPlayback.startTime < AudioPlaybackService.DUPLICATE_PLAY_WINDOW_MS
|
|
407
|
+
const pendingDuplicate = [...this.pendingPlaybacks.values()].some(
|
|
408
|
+
(candidate) =>
|
|
409
|
+
!candidate.cancelled &&
|
|
410
|
+
candidate.appId === appId &&
|
|
411
|
+
candidate.audioUrl === audioUrl &&
|
|
412
|
+
now - candidate.createdAt < AudioPlaybackService.DUPLICATE_PLAY_WINDOW_MS,
|
|
413
|
+
)
|
|
414
|
+
if (activeDuplicate || pendingDuplicate) {
|
|
415
|
+
console.warn(`AUDIO: Suppressed duplicate play request ${requestId}${appId ? ` from ${appId}` : ""}`)
|
|
416
|
+
onComplete(requestId, false, "Duplicate audio request suppressed", 0, "error")
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const pending: PendingPlaybackState = {cancelled: false, audioUrl, appId, createdAt: now}
|
|
364
421
|
this.pendingPlaybacks.set(requestId, pending)
|
|
365
422
|
|
|
366
423
|
console.log(`AUDIO: Play request ${requestId}${appId ? ` from ${appId}` : ""}: ${audioUrl}`)
|
|
@@ -408,6 +465,7 @@ class AudioPlaybackService {
|
|
|
408
465
|
// Store the new playback state
|
|
409
466
|
const playback: PlaybackState = {
|
|
410
467
|
requestId,
|
|
468
|
+
audioUrl,
|
|
411
469
|
uplinkSuppressionId: `url:${requestId}`,
|
|
412
470
|
appId,
|
|
413
471
|
startTime: Date.now(),
|
|
@@ -450,9 +508,11 @@ class AudioPlaybackService {
|
|
|
450
508
|
if (suppressCloudUplink) {
|
|
451
509
|
setAudioCloudUplinkSuppressed(`url:${requestId}`, false)
|
|
452
510
|
}
|
|
453
|
-
|
|
454
|
-
|
|
511
|
+
const failedPlayback = this.currentPlayback?.requestId === requestId ? this.currentPlayback : null
|
|
512
|
+
if (failedPlayback) {
|
|
513
|
+
failedPlayback.completed = true
|
|
455
514
|
this.currentPlayback = null
|
|
515
|
+
this.unloadPlaybackSource(failedPlayback, "playback start failure")
|
|
456
516
|
}
|
|
457
517
|
if (pending.cancelled) {
|
|
458
518
|
onComplete(requestId, true, null, 0, "interrupted")
|
|
@@ -497,14 +557,9 @@ class AudioPlaybackService {
|
|
|
497
557
|
}
|
|
498
558
|
this.markAudioRouteWarm()
|
|
499
559
|
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
this.player.pause()
|
|
504
|
-
} catch (error) {
|
|
505
|
-
console.error("AUDIO: Error pausing player:", error)
|
|
506
|
-
}
|
|
507
|
-
}
|
|
560
|
+
// Pausing alone leaves the URI available to OS/Bluetooth media Play.
|
|
561
|
+
// Unload it unless a newer request has already replaced this source.
|
|
562
|
+
this.unloadPlaybackSource(playback, "interruption")
|
|
508
563
|
|
|
509
564
|
// Notify native that our app stopped playing audio (debounced)
|
|
510
565
|
this.notifyAudioStopDebounced()
|
|
@@ -563,6 +618,7 @@ class AudioPlaybackService {
|
|
|
563
618
|
if (playback.suppressCloudUplink) {
|
|
564
619
|
setAudioCloudUplinkSuppressed(playback.uplinkSuppressionId, false)
|
|
565
620
|
}
|
|
621
|
+
this.unloadPlaybackSource(playback, "playback failure")
|
|
566
622
|
playback.onComplete(playback.requestId, false, "Playback failed (player went idle)", null, "error")
|
|
567
623
|
this.markAudioRouteWarm()
|
|
568
624
|
this.notifyAudioStopDebounced()
|
|
@@ -585,6 +641,10 @@ class AudioPlaybackService {
|
|
|
585
641
|
// Uses BgTimer to work reliably when app is backgrounded on Android
|
|
586
642
|
this.audioStopDebounceTimer = BgTimer.setTimeout(() => {
|
|
587
643
|
this.audioStopDebounceTimer = null
|
|
644
|
+
// URL playback and live PCM streams may overlap when stopOtherAudio is false.
|
|
645
|
+
// A URL that ends must not clear the native playback signal while another
|
|
646
|
+
// source is still active, or the mic watchdog can interrupt that source.
|
|
647
|
+
if (this.isPlaying()) return
|
|
588
648
|
BluetoothSdk.setOwnAppAudioPlaying(false).catch((e) => {
|
|
589
649
|
console.warn("AUDIO: Failed to notify native of audio stop:", e)
|
|
590
650
|
})
|