@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.
Files changed (355) hide show
  1. package/README.md +42 -1
  2. package/build/engine.d.ts +11 -3
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +7 -2
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/dev.d.ts.map +1 -1
  7. package/build/facades/dev.js +9 -5
  8. package/build/facades/dev.js.map +1 -1
  9. package/build/facades/gallery.js +1 -1
  10. package/build/facades/gallery.js.map +1 -1
  11. package/build/facades/glasses.d.ts +1 -0
  12. package/build/facades/glasses.d.ts.map +1 -1
  13. package/build/facades/glasses.js +19 -4
  14. package/build/facades/glasses.js.map +1 -1
  15. package/build/facades/glassesSettings.d.ts.map +1 -1
  16. package/build/facades/glassesSettings.js +1 -0
  17. package/build/facades/glassesSettings.js.map +1 -1
  18. package/build/facades/miniapps.d.ts +4 -0
  19. package/build/facades/miniapps.d.ts.map +1 -1
  20. package/build/facades/miniapps.js +5 -0
  21. package/build/facades/miniapps.js.map +1 -1
  22. package/build/facades/ota.d.ts +11 -2
  23. package/build/facades/ota.d.ts.map +1 -1
  24. package/build/facades/ota.js +19 -4
  25. package/build/facades/ota.js.map +1 -1
  26. package/build/facades/pairing.d.ts +4 -0
  27. package/build/facades/pairing.d.ts.map +1 -1
  28. package/build/facades/pairing.js +10 -8
  29. package/build/facades/pairing.js.map +1 -1
  30. package/build/facades/phoneNotifications.d.ts +0 -4
  31. package/build/facades/phoneNotifications.d.ts.map +1 -1
  32. package/build/facades/phoneNotifications.js +4 -14
  33. package/build/facades/phoneNotifications.js.map +1 -1
  34. package/build/index.d.ts +6 -3
  35. package/build/index.d.ts.map +1 -1
  36. package/build/index.js +4 -2
  37. package/build/index.js.map +1 -1
  38. package/build/internal.d.ts +2 -2
  39. package/build/internal.d.ts.map +1 -1
  40. package/build/internal.js +1 -1
  41. package/build/internal.js.map +1 -1
  42. package/build/react/MentraLiveOtaFlow.d.ts +38 -0
  43. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
  44. package/build/react/MentraLiveOtaFlow.js +528 -0
  45. package/build/react/MentraLiveOtaFlow.js.map +1 -0
  46. package/build/react/index.d.ts +2 -0
  47. package/build/react/index.d.ts.map +1 -0
  48. package/build/react/index.js +2 -0
  49. package/build/react/index.js.map +1 -0
  50. package/build/react/useEngineSnapshot.d.ts +2 -0
  51. package/build/react/useEngineSnapshot.d.ts.map +1 -0
  52. package/build/react/useEngineSnapshot.js +14 -0
  53. package/build/react/useEngineSnapshot.js.map +1 -0
  54. package/build/runtime/bootstrap.d.ts +24 -1
  55. package/build/runtime/bootstrap.d.ts.map +1 -1
  56. package/build/runtime/bootstrap.js +17 -0
  57. package/build/runtime/bootstrap.js.map +1 -1
  58. package/build/runtime/scanQrSeam.d.ts +7 -0
  59. package/build/runtime/scanQrSeam.d.ts.map +1 -0
  60. package/build/runtime/scanQrSeam.js +16 -0
  61. package/build/runtime/scanQrSeam.js.map +1 -0
  62. package/build/services/AppRegistry.d.ts +18 -8
  63. package/build/services/AppRegistry.d.ts.map +1 -1
  64. package/build/services/AppRegistry.js +44 -45
  65. package/build/services/AppRegistry.js.map +1 -1
  66. package/build/services/AudioCloudUplink.d.ts +9 -3
  67. package/build/services/AudioCloudUplink.d.ts.map +1 -1
  68. package/build/services/AudioCloudUplink.js +9 -3
  69. package/build/services/AudioCloudUplink.js.map +1 -1
  70. package/build/services/AudioPlaybackService.d.ts +9 -1
  71. package/build/services/AudioPlaybackService.d.ts.map +1 -1
  72. package/build/services/AudioPlaybackService.js +77 -32
  73. package/build/services/AudioPlaybackService.js.map +1 -1
  74. package/build/services/BlobStore.d.ts +2 -1
  75. package/build/services/BlobStore.d.ts.map +1 -1
  76. package/build/services/BlobStore.js +17 -9
  77. package/build/services/BlobStore.js.map +1 -1
  78. package/build/services/CloudAudioSubscriptionSync.d.ts.map +1 -1
  79. package/build/services/CloudAudioSubscriptionSync.js +13 -1
  80. package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
  81. package/build/services/CloudClientService.d.ts +15 -0
  82. package/build/services/CloudClientService.d.ts.map +1 -1
  83. package/build/services/CloudClientService.js +75 -0
  84. package/build/services/CloudClientService.js.map +1 -1
  85. package/build/services/ForegroundLocationPermission.d.ts +13 -0
  86. package/build/services/ForegroundLocationPermission.d.ts.map +1 -0
  87. package/build/services/ForegroundLocationPermission.js +12 -0
  88. package/build/services/ForegroundLocationPermission.js.map +1 -0
  89. package/build/services/GlassesSettingsSync.d.ts.map +1 -1
  90. package/build/services/GlassesSettingsSync.js +9 -3
  91. package/build/services/GlassesSettingsSync.js.map +1 -1
  92. package/build/services/GlassesStatusProjection.d.ts +2 -1
  93. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  94. package/build/services/GlassesStatusProjection.js +15 -8
  95. package/build/services/GlassesStatusProjection.js.map +1 -1
  96. package/build/services/HotspotOtaTransport.d.ts +29 -0
  97. package/build/services/HotspotOtaTransport.d.ts.map +1 -0
  98. package/build/services/HotspotOtaTransport.js +146 -0
  99. package/build/services/HotspotOtaTransport.js.map +1 -0
  100. package/build/services/HotspotShutdown.d.ts +17 -0
  101. package/build/services/HotspotShutdown.d.ts.map +1 -0
  102. package/build/services/HotspotShutdown.js +29 -0
  103. package/build/services/HotspotShutdown.js.map +1 -0
  104. package/build/services/LocalDisplayManager.d.ts +15 -0
  105. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  106. package/build/services/LocalDisplayManager.js +106 -20
  107. package/build/services/LocalDisplayManager.js.map +1 -1
  108. package/build/services/LocalMiniappRuntime.d.ts +67 -1
  109. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  110. package/build/services/LocalMiniappRuntime.js +486 -126
  111. package/build/services/LocalMiniappRuntime.js.map +1 -1
  112. package/build/services/LocalMiniappStorage.d.ts +32 -0
  113. package/build/services/LocalMiniappStorage.d.ts.map +1 -0
  114. package/build/services/LocalMiniappStorage.js +66 -0
  115. package/build/services/LocalMiniappStorage.js.map +1 -0
  116. package/build/services/LocalMiniappUserIdentity.d.ts +20 -0
  117. package/build/services/LocalMiniappUserIdentity.d.ts.map +1 -0
  118. package/build/services/LocalMiniappUserIdentity.js +43 -0
  119. package/build/services/LocalMiniappUserIdentity.js.map +1 -0
  120. package/build/services/MicStateCoordinator.d.ts +42 -1
  121. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  122. package/build/services/MicStateCoordinator.js +141 -3
  123. package/build/services/MicStateCoordinator.js.map +1 -1
  124. package/build/services/MiniappLauncher.d.ts.map +1 -1
  125. package/build/services/MiniappLauncher.js +23 -3
  126. package/build/services/MiniappLauncher.js.map +1 -1
  127. package/build/services/MiniappLiveness.d.ts +12 -0
  128. package/build/services/MiniappLiveness.d.ts.map +1 -0
  129. package/build/services/MiniappLiveness.js +16 -0
  130. package/build/services/MiniappLiveness.js.map +1 -0
  131. package/build/services/OtaArtifactDownloader.d.ts +65 -0
  132. package/build/services/OtaArtifactDownloader.d.ts.map +1 -0
  133. package/build/services/OtaArtifactDownloader.js +226 -0
  134. package/build/services/OtaArtifactDownloader.js.map +1 -0
  135. package/build/services/OtaAutoChain.d.ts +34 -0
  136. package/build/services/OtaAutoChain.d.ts.map +1 -0
  137. package/build/services/OtaAutoChain.js +83 -0
  138. package/build/services/OtaAutoChain.js.map +1 -0
  139. package/build/services/OtaErrorMapping.d.ts +18 -0
  140. package/build/services/OtaErrorMapping.d.ts.map +1 -0
  141. package/build/services/OtaErrorMapping.js +63 -0
  142. package/build/services/OtaErrorMapping.js.map +1 -0
  143. package/build/services/OtaInstallCoordinator.d.ts +94 -14
  144. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  145. package/build/services/OtaInstallCoordinator.js +574 -130
  146. package/build/services/OtaInstallCoordinator.js.map +1 -1
  147. package/build/services/OtaService.d.ts.map +1 -1
  148. package/build/services/OtaService.js +8 -1
  149. package/build/services/OtaService.js.map +1 -1
  150. package/build/services/OtaUpdateCheckService.d.ts +38 -3
  151. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  152. package/build/services/OtaUpdateCheckService.js +142 -22
  153. package/build/services/OtaUpdateCheckService.js.map +1 -1
  154. package/build/services/PhoneCalendarService.d.ts.map +1 -1
  155. package/build/services/PhoneCalendarService.js +22 -1
  156. package/build/services/PhoneCalendarService.js.map +1 -1
  157. package/build/services/PhoneCameraFovCoordinator.d.ts +2 -0
  158. package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
  159. package/build/services/PhoneCameraFovCoordinator.js +14 -0
  160. package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
  161. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  162. package/build/services/PhoneNotificationsSync.js +12 -10
  163. package/build/services/PhoneNotificationsSync.js.map +1 -1
  164. package/build/services/PhonePhotoCoordinator.d.ts +10 -3
  165. package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
  166. package/build/services/PhonePhotoCoordinator.js +21 -3
  167. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  168. package/build/services/PhoneStreamCoordinator.d.ts +8 -1
  169. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  170. package/build/services/PhoneStreamCoordinator.js +125 -13
  171. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  172. package/build/services/PhoneVideoCoordinator.d.ts +2 -0
  173. package/build/services/PhoneVideoCoordinator.d.ts.map +1 -1
  174. package/build/services/PhoneVideoCoordinator.js +10 -0
  175. package/build/services/PhoneVideoCoordinator.js.map +1 -1
  176. package/build/services/SceneRenderer.d.ts.map +1 -1
  177. package/build/services/SceneRenderer.js +15 -0
  178. package/build/services/SceneRenderer.js.map +1 -1
  179. package/build/services/SupportProfileSync.d.ts +22 -0
  180. package/build/services/SupportProfileSync.d.ts.map +1 -0
  181. package/build/services/SupportProfileSync.js +213 -0
  182. package/build/services/SupportProfileSync.js.map +1 -0
  183. package/build/services/TtsTextSanitizer.d.ts +5 -0
  184. package/build/services/TtsTextSanitizer.d.ts.map +1 -0
  185. package/build/services/TtsTextSanitizer.js +61 -0
  186. package/build/services/TtsTextSanitizer.js.map +1 -0
  187. package/build/services/WifiAdbRequest.d.ts +17 -0
  188. package/build/services/WifiAdbRequest.d.ts.map +1 -0
  189. package/build/services/WifiAdbRequest.js +35 -0
  190. package/build/services/WifiAdbRequest.js.map +1 -0
  191. package/build/services/asg/asgCameraApi.d.ts +3 -0
  192. package/build/services/asg/asgCameraApi.d.ts.map +1 -1
  193. package/build/services/asg/asgCameraApi.js +72 -2
  194. package/build/services/asg/asgCameraApi.js.map +1 -1
  195. package/build/services/asg/gallerySyncService.d.ts.map +1 -1
  196. package/build/services/asg/gallerySyncService.js +26 -14
  197. package/build/services/asg/gallerySyncService.js.map +1 -1
  198. package/build/services/asg/localNetworkTransport.d.ts +2 -2
  199. package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
  200. package/build/services/asg/localNetworkTransport.js +6 -6
  201. package/build/services/asg/localNetworkTransport.js.map +1 -1
  202. package/build/services/audioPlaybackAssets.d.ts +8 -0
  203. package/build/services/audioPlaybackAssets.d.ts.map +1 -0
  204. package/build/services/audioPlaybackAssets.js +7 -0
  205. package/build/services/audioPlaybackAssets.js.map +1 -0
  206. package/build/services/glassesClockSync.js +2 -2
  207. package/build/services/glassesClockSync.js.map +1 -1
  208. package/build/services/manifestActions.d.ts +7 -0
  209. package/build/services/manifestActions.d.ts.map +1 -0
  210. package/build/services/manifestActions.js +25 -0
  211. package/build/services/manifestActions.js.map +1 -0
  212. package/build/services/otaDisplayState.d.ts +22 -11
  213. package/build/services/otaDisplayState.d.ts.map +1 -1
  214. package/build/services/otaDisplayState.js +36 -14
  215. package/build/services/otaDisplayState.js.map +1 -1
  216. package/build/services/otaInstallPolicy.d.ts +15 -3
  217. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  218. package/build/services/otaInstallPolicy.js +24 -3
  219. package/build/services/otaInstallPolicy.js.map +1 -1
  220. package/build/services/otaManifestUrl.d.ts +2 -0
  221. package/build/services/otaManifestUrl.d.ts.map +1 -0
  222. package/build/services/{asgOtaVersionUrl.js → otaManifestUrl.js} +15 -5
  223. package/build/services/otaManifestUrl.js.map +1 -0
  224. package/build/services/slimStreamStatus.d.ts +11 -0
  225. package/build/services/slimStreamStatus.d.ts.map +1 -1
  226. package/build/services/slimStreamStatus.js +14 -4
  227. package/build/services/slimStreamStatus.js.map +1 -1
  228. package/build/stores/apps.d.ts.map +1 -1
  229. package/build/stores/apps.js +11 -0
  230. package/build/stores/apps.js.map +1 -1
  231. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  232. package/build/stores/bluetoothSettingKeys.js +2 -0
  233. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  234. package/build/stores/gallerySync.d.ts +2 -0
  235. package/build/stores/gallerySync.d.ts.map +1 -1
  236. package/build/stores/gallerySync.js +2 -0
  237. package/build/stores/gallerySync.js.map +1 -1
  238. package/build/stores/glasses.d.ts.map +1 -1
  239. package/build/stores/glasses.js +3 -0
  240. package/build/stores/glasses.js.map +1 -1
  241. package/build/stores/settings.d.ts +17 -0
  242. package/build/stores/settings.d.ts.map +1 -1
  243. package/build/stores/settings.js +152 -19
  244. package/build/stores/settings.js.map +1 -1
  245. package/build/types/applet.d.ts +2 -0
  246. package/build/types/applet.d.ts.map +1 -1
  247. package/build/types/applet.js.map +1 -1
  248. package/build/types/enums.d.ts +1 -0
  249. package/build/types/enums.d.ts.map +1 -1
  250. package/build/types/enums.js +1 -0
  251. package/build/types/enums.js.map +1 -1
  252. package/build/types/hardware.d.ts +1 -0
  253. package/build/types/hardware.d.ts.map +1 -1
  254. package/build/types/hardware.js +14 -0
  255. package/build/types/hardware.js.map +1 -1
  256. package/build/utils/configuredDevHost.d.ts +6 -0
  257. package/build/utils/configuredDevHost.d.ts.map +1 -0
  258. package/build/utils/configuredDevHost.js +19 -0
  259. package/build/utils/configuredDevHost.js.map +1 -0
  260. package/build/utils/devMiniappLaunch.d.ts +19 -8
  261. package/build/utils/devMiniappLaunch.d.ts.map +1 -1
  262. package/build/utils/devMiniappLaunch.js +144 -21
  263. package/build/utils/devMiniappLaunch.js.map +1 -1
  264. package/build/utils/diagnosticContext.d.ts.map +1 -1
  265. package/build/utils/diagnosticContext.js +15 -17
  266. package/build/utils/diagnosticContext.js.map +1 -1
  267. package/build/utils/hardware/hardware.d.ts.map +1 -1
  268. package/build/utils/hardware/hardware.js +7 -3
  269. package/build/utils/hardware/hardware.js.map +1 -1
  270. package/build/utils/miniappDiagnostics.d.ts +64 -0
  271. package/build/utils/miniappDiagnostics.d.ts.map +1 -0
  272. package/build/utils/miniappDiagnostics.js +173 -0
  273. package/build/utils/miniappDiagnostics.js.map +1 -0
  274. package/build/utils/timers/timers.d.ts.map +1 -1
  275. package/build/utils/timers/timers.js +55 -18
  276. package/build/utils/timers/timers.js.map +1 -1
  277. package/package.json +27 -18
  278. package/src/assets/silence.wav +0 -0
  279. package/src/engine.ts +7 -2
  280. package/src/facades/dev.ts +9 -5
  281. package/src/facades/gallery.ts +1 -1
  282. package/src/facades/glasses.ts +17 -4
  283. package/src/facades/glassesSettings.ts +5 -0
  284. package/src/facades/miniapps.ts +7 -0
  285. package/src/facades/ota.ts +19 -4
  286. package/src/facades/pairing.ts +11 -7
  287. package/src/facades/phoneNotifications.ts +4 -13
  288. package/src/index.ts +26 -2
  289. package/src/internal.ts +2 -1
  290. package/src/react/MentraLiveOtaFlow.tsx +856 -0
  291. package/src/react/index.ts +7 -0
  292. package/src/react/useEngineSnapshot.ts +16 -0
  293. package/src/runtime/bootstrap.ts +33 -1
  294. package/src/runtime/scanQrSeam.ts +20 -0
  295. package/src/services/AppRegistry.ts +72 -48
  296. package/src/services/AudioCloudUplink.ts +9 -3
  297. package/src/services/AudioPlaybackService.ts +94 -34
  298. package/src/services/BlobStore.ts +22 -9
  299. package/src/services/CloudAudioSubscriptionSync.ts +13 -1
  300. package/src/services/CloudClientService.ts +74 -0
  301. package/src/services/ForegroundLocationPermission.ts +24 -0
  302. package/src/services/GlassesSettingsSync.ts +9 -3
  303. package/src/services/GlassesStatusProjection.ts +16 -8
  304. package/src/services/HotspotOtaTransport.ts +184 -0
  305. package/src/services/HotspotShutdown.ts +41 -0
  306. package/src/services/LocalDisplayManager.ts +112 -21
  307. package/src/services/LocalMiniappRuntime.ts +576 -130
  308. package/src/services/LocalMiniappStorage.ts +82 -0
  309. package/src/services/LocalMiniappUserIdentity.ts +46 -0
  310. package/src/services/MicStateCoordinator.ts +169 -4
  311. package/src/services/MiniappLauncher.ts +30 -3
  312. package/src/services/MiniappLiveness.ts +24 -0
  313. package/src/services/OtaArtifactDownloader.ts +312 -0
  314. package/src/services/OtaAutoChain.ts +106 -0
  315. package/src/services/OtaErrorMapping.ts +80 -0
  316. package/src/services/OtaInstallCoordinator.ts +645 -143
  317. package/src/services/OtaService.ts +11 -5
  318. package/src/services/OtaUpdateCheckService.ts +180 -20
  319. package/src/services/PhoneCalendarService.ts +27 -4
  320. package/src/services/PhoneCameraFovCoordinator.ts +15 -0
  321. package/src/services/PhoneNotificationsSync.ts +12 -10
  322. package/src/services/PhonePhotoCoordinator.ts +33 -7
  323. package/src/services/PhoneStreamCoordinator.ts +139 -25
  324. package/src/services/PhoneVideoCoordinator.ts +11 -0
  325. package/src/services/SceneRenderer.ts +16 -0
  326. package/src/services/SupportProfileSync.ts +218 -0
  327. package/src/services/TtsTextSanitizer.ts +66 -0
  328. package/src/services/WifiAdbRequest.ts +53 -0
  329. package/src/services/asg/asgCameraApi.ts +99 -8
  330. package/src/services/asg/gallerySyncService.ts +28 -19
  331. package/src/services/asg/localNetworkTransport.ts +7 -11
  332. package/src/services/audioPlaybackAssets.ts +8 -0
  333. package/src/services/glassesClockSync.ts +2 -2
  334. package/src/services/manifestActions.ts +30 -0
  335. package/src/services/otaDisplayState.ts +57 -16
  336. package/src/services/otaInstallPolicy.ts +29 -3
  337. package/src/services/{asgOtaVersionUrl.ts → otaManifestUrl.ts} +16 -4
  338. package/src/services/slimStreamStatus.ts +16 -5
  339. package/src/stores/apps.ts +12 -0
  340. package/src/stores/bluetoothSettingKeys.ts +2 -0
  341. package/src/stores/gallerySync.ts +4 -0
  342. package/src/stores/glasses.ts +3 -0
  343. package/src/stores/settings.ts +174 -21
  344. package/src/types/applet.ts +2 -0
  345. package/src/types/enums.ts +2 -1
  346. package/src/types/hardware.ts +17 -0
  347. package/src/utils/configuredDevHost.ts +17 -0
  348. package/src/utils/devMiniappLaunch.ts +151 -23
  349. package/src/utils/diagnosticContext.ts +17 -17
  350. package/src/utils/hardware/hardware.ts +7 -9
  351. package/src/utils/miniappDiagnostics.ts +226 -0
  352. package/src/utils/timers/timers.ts +57 -21
  353. package/build/services/asgOtaVersionUrl.d.ts +0 -2
  354. package/build/services/asgOtaVersionUrl.d.ts.map +0 -1
  355. package/build/services/asgOtaVersionUrl.js.map +0 -1
@@ -0,0 +1,7 @@
1
+ export {
2
+ MentraLiveOtaFlow,
3
+ type MentraLiveOtaFlowPage,
4
+ type MentraLiveOtaFlowProps,
5
+ type MentraLiveOtaFlowTheme,
6
+ type MentraLiveOtaFlowTranslate,
7
+ } from "./MentraLiveOtaFlow"
@@ -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
+ }
@@ -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, DeclaredAction} from "../types/applet"
25
+ import type {AppletPermission, AppPermissionType, AppletType, ClientApp} from "../types/applet"
26
26
  import {HardwareRequirement, HardwareRequirementLevel, HardwareType} from "../types"
27
- import {getConfigValues} from "../runtime/bootstrap"
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(url: string, opts?: {versionOverride?: string}): AsyncResult<void, Error> {
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(packageName, version)
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 preventing spoken TTS from being
25
- * ingested by cloud STT. Sources are reference-counted by id so overlapping
26
- * speech playback cannot reopen the uplink until every active prompt has ended.
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
- /** Suppress microphone audio sent to cloud STT while this audio is audible. */
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
- // Spoken playback remains suppressed briefly while its A2DP tail drains.
95
- // Track those completed sources so a new non-spoken sound can resume
96
- // listening immediately instead of inheriting the previous TTS gate.
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(Math.floor(
107
- (AudioPlaybackService.AUDIO_ROUTE_PREWARM_MS *
108
- AudioPlaybackService.AUDIO_ROUTE_PREWARM_SAMPLE_RATE *
109
- 2) /
110
- (1000 * 3),
111
- )) + "AA=="
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
- if (!this.player) return
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 pending: PendingPlaybackState = {cancelled: false}
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
- if (this.currentPlayback?.requestId === requestId) {
454
- this.currentPlayback.completed = true
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
- // Stop the player
501
- if (this.player) {
502
- try {
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
  })