react-native-audio-browser 0.2.0-next.3 → 0.2.0-next.30

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 (989) hide show
  1. package/README.md +36 -22
  2. package/android/build.gradle +35 -12
  3. package/android/gradle.properties +2 -2
  4. package/android/src/main/AndroidManifest.xml +3 -1
  5. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +541 -278
  6. package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
  7. package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
  8. package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +52 -0
  9. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
  10. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +27 -9
  11. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +288 -0
  12. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +97 -58
  13. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +70 -29
  14. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
  15. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +20 -7
  16. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  17. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  18. package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
  19. package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
  20. package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
  21. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +12 -12
  22. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
  23. package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
  24. package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
  25. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +509 -230
  26. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
  27. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -25
  28. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +113 -78
  29. package/android/src/main/java/com/audiobrowser/player/Player.kt +81 -38
  30. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  31. package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
  32. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
  33. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  34. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
  35. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
  36. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  37. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
  38. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
  39. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
  40. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +62 -40
  41. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  42. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +110 -0
  43. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +56 -52
  44. package/android/src/main/res/values/strings.xml +25 -5
  45. package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +100 -0
  46. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +119 -33
  47. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  48. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +15 -0
  49. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +346 -8
  50. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +40 -2
  51. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +301 -0
  52. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
  53. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +168 -0
  54. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
  55. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +160 -0
  56. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  57. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  58. package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
  59. package/android/src/test/java/com/audiobrowser/player/BrowseTilesTest.kt +385 -0
  60. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +101 -58
  61. package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
  62. package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
  63. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  64. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
  65. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  66. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
  67. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  68. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
  69. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
  70. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  71. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
  72. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +55 -16
  73. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +162 -0
  74. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  75. package/ios/AVPlayerEngine.swift +330 -0
  76. package/ios/AVPlayerEngineCallbacks.swift +15 -0
  77. package/ios/Browser/BrowseGate.swift +58 -0
  78. package/ios/Browser/BrowserConfig.swift +117 -78
  79. package/ios/Browser/BrowserManager.swift +209 -603
  80. package/ios/Browser/BrowserPathHelper.swift +50 -46
  81. package/ios/Browser/InertStyleDiagnostic.swift +296 -0
  82. package/ios/Browser/JsonModels.swift +264 -106
  83. package/ios/Browser/NavigationErrorState.swift +41 -0
  84. package/ios/Browser/PageResolver.swift +227 -0
  85. package/ios/Browser/RequestLayer.swift +90 -0
  86. package/ios/Browser/{BrowserManager+URLResolution.swift → RequestPipeline.swift} +193 -121
  87. package/ios/Browser/SearchDrillIn.swift +7 -7
  88. package/ios/Browser/SectionScope.swift +58 -24
  89. package/ios/Browser/StyleResolver.swift +58 -0
  90. package/ios/Browser/TabBarEntries.swift +9 -3
  91. package/ios/Browser/TrackIdTemplate.swift +37 -0
  92. package/ios/CarPlay/CPInterfaceController+Safe.swift +100 -43
  93. package/ios/CarPlay/CarPlayArtworkResolver.swift +36 -83
  94. package/ios/CarPlay/CarPlayController.swift +42 -1296
  95. package/ios/CarPlay/CarPlayImageLoader.swift +78 -97
  96. package/ios/CarPlay/CarPlayListItemFactory.swift +303 -92
  97. package/ios/CarPlay/CarPlayNowPlayingManager.swift +129 -136
  98. package/ios/CarPlay/CarPlaySession+Navigator.swift +114 -0
  99. package/ios/CarPlay/CarPlaySession+PageLoader.swift +92 -0
  100. package/ios/CarPlay/CarPlaySession+Refresher.swift +43 -0
  101. package/ios/CarPlay/CarPlaySession+TabBar.swift +193 -0
  102. package/ios/CarPlay/CarPlaySession.swift +209 -0
  103. package/ios/CarPlay/CarPlayTemplates.swift +63 -0
  104. package/ios/CarPlay/HybridAudioBrowser+IntentFavorite.swift +20 -0
  105. package/ios/{Player → CarPlay}/MediaIntentCriteria.swift +17 -9
  106. package/ios/CarPlay/MediaIntentPlayback.swift +74 -0
  107. package/ios/CarPlay/RNABMediaAddHandler.swift +8 -24
  108. package/ios/CarPlay/RNABMediaAffinityHandler.swift +7 -23
  109. package/ios/CarPlay/RNABMediaIntentHandler.swift +18 -19
  110. package/ios/CarPlay/SectionPresentation.swift +77 -0
  111. package/ios/CarPlay/TopTemplateReplacer.swift +96 -0
  112. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  113. package/ios/Extension/NavigationError+Conversion.swift +14 -4
  114. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  115. package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
  116. package/ios/Extension/Section+Copying.swift +29 -0
  117. package/ios/Extension/String+IcyCharset.swift +21 -0
  118. package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
  119. package/ios/Extension/Track+Copying.swift +8 -12
  120. package/ios/Extension/Track+Identity.swift +18 -0
  121. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  122. package/ios/Extension/UIImage+Adaptive.swift +12 -0
  123. package/ios/Extension/URLError+Connectivity.swift +15 -0
  124. package/ios/Http/HttpClient.swift +66 -142
  125. package/ios/HybridAudioBrowser+Browser.swift +153 -0
  126. package/ios/HybridAudioBrowser+Options.swift +144 -0
  127. package/ios/HybridAudioBrowser+Player.swift +397 -0
  128. package/ios/HybridAudioBrowser+PlayerCallbacks.swift +190 -0
  129. package/ios/HybridAudioBrowser+Stubs.swift +46 -0
  130. package/ios/HybridAudioBrowser.swift +153 -1610
  131. package/ios/Model/NitroTypeStubs.swift +146 -24
  132. package/ios/Model/PlayerUpdateOptions.swift +8 -1
  133. package/ios/Model/RemoteCommand.swift +18 -0
  134. package/ios/Model/TrackPlayerError.swift +2 -2
  135. package/ios/NowPlayingInfo/NowPlayingInfoController.swift +18 -53
  136. package/ios/Observer/ItemObserver.swift +90 -0
  137. package/ios/Observer/PlayerObserver.swift +59 -0
  138. package/ios/Option/SessionCategories.swift +42 -92
  139. package/ios/Player/AudioSessionController.swift +177 -0
  140. package/ios/Player/DeferredSeek.swift +70 -0
  141. package/ios/Player/ErrorStatus.swift +27 -0
  142. package/ios/Player/MediaLoader.swift +62 -35
  143. package/ios/Player/NowPlayingUpdater.swift +197 -50
  144. package/ios/Player/PlaybackFailure.swift +125 -0
  145. package/ios/Player/PlaybackPersistence.swift +73 -0
  146. package/ios/Player/PlaybackTimer.swift +8 -2
  147. package/ios/Player/Player.swift +1078 -0
  148. package/ios/Player/{PlaybackCoordinatorCallbacks.swift → PlayerCallbacks.swift} +3 -4
  149. package/ios/Player/PlayerEngine.swift +87 -0
  150. package/ios/Player/PlayerStatusTypes.swift +2 -6
  151. package/ios/Player/QueueManager.swift +93 -67
  152. package/ios/Player/RetryLoop.swift +261 -0
  153. package/ios/Player/ShuffleOrder.swift +33 -0
  154. package/ios/Player/SleepTimerManager.swift +1 -1
  155. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +46 -0
  156. package/ios/RemoteCommand/RemoteCommandController.swift +87 -331
  157. package/ios/Tests/BrowserPathHelperTests.swift +56 -15
  158. package/ios/Tests/CarPlayArtworkResolverTests.swift +60 -373
  159. package/ios/Tests/DeferredSeekTests.swift +70 -0
  160. package/ios/Tests/EmitterTests.swift +29 -0
  161. package/ios/Tests/GenerationMemoTests.swift +121 -0
  162. package/ios/Tests/InertStyleDiagnosticTests.swift +209 -0
  163. package/ios/Tests/JsonModelsTests.swift +164 -1
  164. package/ios/Tests/LRUCacheTests.swift +110 -0
  165. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  166. package/ios/Tests/MediaLoaderTests.swift +12 -48
  167. package/ios/Tests/ObserverTests.swift +88 -0
  168. package/ios/Tests/PlaybackErrorTests.swift +76 -0
  169. package/ios/Tests/PlaybackFailureTests.swift +189 -0
  170. package/ios/Tests/PlaybackPersistenceTests.swift +93 -0
  171. package/ios/Tests/PlayerPolicyTests.swift +420 -0
  172. package/ios/Tests/{PlaybackCoordinatorTests.swift → PlayerTests.swift} +300 -248
  173. package/ios/Tests/PlayingStateTests.swift +42 -0
  174. package/ios/Tests/QueueManagerTests.swift +196 -115
  175. package/ios/Tests/ReadyValueTests.swift +73 -0
  176. package/ios/Tests/RemoteCommandControllerTests.swift +152 -0
  177. package/ios/Tests/RemoteCommandTests.swift +65 -0
  178. package/ios/Tests/RequestLayerTests.swift +112 -0
  179. package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
  180. package/ios/Tests/RetryLoopTests.swift +270 -0
  181. package/ios/Tests/SearchDrillInTests.swift +16 -16
  182. package/ios/Tests/SectionPresentationTests.swift +98 -0
  183. package/ios/Tests/SectionScopeTests.swift +87 -55
  184. package/ios/Tests/StyleResolverTests.swift +132 -0
  185. package/ios/Tests/Support/TestTypes.swift +108 -104
  186. package/ios/Tests/TabBarEntriesTests.swift +19 -19
  187. package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
  188. package/ios/Tests/TopTemplateReplacerTests.swift +172 -0
  189. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  190. package/ios/Tests/TrackIdentityTests.swift +20 -0
  191. package/ios/Tests/TrackSelectorTests.swift +168 -33
  192. package/ios/Tests/URLErrorConnectivityTests.swift +20 -0
  193. package/ios/TrackSelector.swift +46 -31
  194. package/ios/Util/ArtworkImageFetcher.swift +58 -1
  195. package/ios/Util/Emitter.swift +42 -0
  196. package/ios/Util/GenerationMemo.swift +36 -0
  197. package/ios/Util/LRUCache.swift +23 -159
  198. package/ios/Util/NetworkMonitor.swift +43 -26
  199. package/ios/Util/ReadyValue.swift +56 -0
  200. package/ios/Util/SFSymbolRenderer.swift +59 -31
  201. package/ios/Util/SVGProcessor.swift +9 -12
  202. package/lib/commonjs/AudioBrowser.js +8 -8
  203. package/lib/commonjs/AudioBrowser.js.map +1 -1
  204. package/lib/commonjs/CLAUDE.md +52 -0
  205. package/lib/commonjs/features/browser-config.js +12 -8
  206. package/lib/commonjs/features/browser-config.js.map +1 -1
  207. package/lib/commonjs/features/browser.js +11 -3
  208. package/lib/commonjs/features/browser.js.map +1 -1
  209. package/lib/commonjs/features/carConnection.js +1 -1
  210. package/lib/commonjs/features/equalizer.js +1 -1
  211. package/lib/commonjs/features/errors.js +3 -3
  212. package/lib/commonjs/features/favorites.js +14 -11
  213. package/lib/commonjs/features/favorites.js.map +1 -1
  214. package/lib/commonjs/features/gate.js +16 -1
  215. package/lib/commonjs/features/gate.js.map +1 -1
  216. package/lib/commonjs/features/metadata.js +20 -3
  217. package/lib/commonjs/features/metadata.js.map +1 -1
  218. package/lib/commonjs/features/network.js +1 -1
  219. package/lib/commonjs/features/nowPlaying.js +1 -1
  220. package/lib/commonjs/features/output.js +1 -1
  221. package/lib/commonjs/features/playback/controls.js +4 -2
  222. package/lib/commonjs/features/playback/controls.js.map +1 -1
  223. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  224. package/lib/commonjs/features/playback/playing.js +1 -1
  225. package/lib/commonjs/features/playback/progress.js +3 -2
  226. package/lib/commonjs/features/playback/progress.js.map +1 -1
  227. package/lib/commonjs/features/playback/rate.js +8 -1
  228. package/lib/commonjs/features/playback/rate.js.map +1 -1
  229. package/lib/commonjs/features/playback/state.js +3 -4
  230. package/lib/commonjs/features/playback/state.js.map +1 -1
  231. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  232. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  233. package/lib/commonjs/features/playback/volume.js +1 -1
  234. package/lib/commonjs/features/player/options.js +48 -34
  235. package/lib/commonjs/features/player/options.js.map +1 -1
  236. package/lib/commonjs/features/player/setup.js +18 -9
  237. package/lib/commonjs/features/player/setup.js.map +1 -1
  238. package/lib/commonjs/features/player/validateOptions.js +23 -0
  239. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  240. package/lib/commonjs/features/queue/activeTrack.js +15 -4
  241. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  242. package/lib/commonjs/features/queue/queue.js +13 -6
  243. package/lib/commonjs/features/queue/queue.js.map +1 -1
  244. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  245. package/lib/commonjs/features/queue/shuffle.js +1 -1
  246. package/lib/commonjs/features/remoteControls.js +13 -92
  247. package/lib/commonjs/features/remoteControls.js.map +1 -1
  248. package/lib/commonjs/features/sleepTimer.js +13 -1
  249. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  250. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  251. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  252. package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
  253. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  254. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  255. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  256. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  257. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  258. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  259. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  260. package/lib/commonjs/web/NativeAudioBrowser.js +201 -76
  261. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  262. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
  263. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
  264. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  265. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  266. package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
  267. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  268. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +1 -1
  269. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  270. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  271. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
  272. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
  273. package/lib/commonjs/web/browser/BrowserManager.js +175 -100
  274. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  275. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  276. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  277. package/lib/commonjs/web/browser/NavigationErrorManager.js +3 -1
  278. package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
  279. package/lib/commonjs/web/browser/SearchManager.js +18 -23
  280. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  281. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  282. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  283. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  284. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  285. package/lib/commonjs/web/browser/sections.js +89 -0
  286. package/lib/commonjs/web/browser/sections.js.map +1 -0
  287. package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
  288. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  289. package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
  290. package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
  291. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  292. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  293. package/lib/commonjs/web/util/BrowserPathHelper.js +62 -11
  294. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  295. package/lib/module/AudioBrowser.js +2 -1
  296. package/lib/module/AudioBrowser.js.map +1 -1
  297. package/lib/module/CLAUDE.md +52 -0
  298. package/lib/module/features/browser-config.js +12 -8
  299. package/lib/module/features/browser-config.js.map +1 -1
  300. package/lib/module/features/browser.js +11 -3
  301. package/lib/module/features/browser.js.map +1 -1
  302. package/lib/module/features/carConnection.js +1 -1
  303. package/lib/module/features/equalizer.js +1 -1
  304. package/lib/module/features/errors.js +3 -3
  305. package/lib/module/features/favorites.js +14 -11
  306. package/lib/module/features/favorites.js.map +1 -1
  307. package/lib/module/features/gate.js +16 -1
  308. package/lib/module/features/gate.js.map +1 -1
  309. package/lib/module/features/metadata.js +20 -3
  310. package/lib/module/features/metadata.js.map +1 -1
  311. package/lib/module/features/network.js +1 -1
  312. package/lib/module/features/nowPlaying.js +1 -1
  313. package/lib/module/features/output.js +1 -1
  314. package/lib/module/features/playback/controls.js +4 -2
  315. package/lib/module/features/playback/controls.js.map +1 -1
  316. package/lib/module/features/playback/playWhenReady.js +1 -1
  317. package/lib/module/features/playback/playing.js +1 -1
  318. package/lib/module/features/playback/progress.js +3 -2
  319. package/lib/module/features/playback/progress.js.map +1 -1
  320. package/lib/module/features/playback/rate.js +8 -1
  321. package/lib/module/features/playback/rate.js.map +1 -1
  322. package/lib/module/features/playback/state.js +3 -4
  323. package/lib/module/features/playback/state.js.map +1 -1
  324. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  325. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  326. package/lib/module/features/playback/volume.js +1 -1
  327. package/lib/module/features/player/options.js +47 -32
  328. package/lib/module/features/player/options.js.map +1 -1
  329. package/lib/module/features/player/setup.js +17 -8
  330. package/lib/module/features/player/setup.js.map +1 -1
  331. package/lib/module/features/player/validateOptions.js +19 -0
  332. package/lib/module/features/player/validateOptions.js.map +1 -0
  333. package/lib/module/features/queue/activeTrack.js +15 -4
  334. package/lib/module/features/queue/activeTrack.js.map +1 -1
  335. package/lib/module/features/queue/queue.js +13 -6
  336. package/lib/module/features/queue/queue.js.map +1 -1
  337. package/lib/module/features/queue/repeatMode.js +1 -1
  338. package/lib/module/features/queue/shuffle.js +1 -1
  339. package/lib/module/features/remoteControls.js +13 -92
  340. package/lib/module/features/remoteControls.js.map +1 -1
  341. package/lib/module/features/sleepTimer.js +13 -1
  342. package/lib/module/features/sleepTimer.js.map +1 -1
  343. package/lib/module/test-utils/reactNativeStub.js +58 -0
  344. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  345. package/lib/module/utils/LazyNativeEmitter.js +28 -14
  346. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  347. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  348. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  349. package/lib/module/utils/getTrackIdentity.js +20 -0
  350. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  351. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  352. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  353. package/lib/module/web/NativeAudioBrowser.js +201 -76
  354. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  355. package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
  356. package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
  357. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  358. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  359. package/lib/module/web/TrackPlayer/Player.js +254 -39
  360. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  361. package/lib/module/web/TrackPlayer/PlayingStateFactory.js +1 -1
  362. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  363. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  364. package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
  365. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
  366. package/lib/module/web/browser/BrowserManager.js +175 -100
  367. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  368. package/lib/module/web/browser/FavoriteManager.js +31 -21
  369. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  370. package/lib/module/web/browser/NavigationErrorManager.js +3 -1
  371. package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
  372. package/lib/module/web/browser/SearchManager.js +18 -23
  373. package/lib/module/web/browser/SearchManager.js.map +1 -1
  374. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  375. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  376. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  377. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  378. package/lib/module/web/browser/sections.js +83 -0
  379. package/lib/module/web/browser/sections.js.map +1 -0
  380. package/lib/module/web/http/RequestConfigBuilder.js +110 -27
  381. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  382. package/lib/module/web/http/followMediaRedirect.js +215 -0
  383. package/lib/module/web/http/followMediaRedirect.js.map +1 -0
  384. package/lib/module/web/player/NowPlayingManager.js +4 -3
  385. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  386. package/lib/module/web/util/BrowserPathHelper.js +62 -11
  387. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  388. package/lib/typescript/package.json +1 -0
  389. package/lib/typescript/src/AudioBrowser.d.ts +1 -1
  390. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  391. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  392. package/lib/typescript/src/features/browser.d.ts +10 -2
  393. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  394. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  395. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  396. package/lib/typescript/src/features/errors.d.ts +3 -3
  397. package/lib/typescript/src/features/favorites.d.ts +14 -11
  398. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  399. package/lib/typescript/src/features/gate.d.ts +18 -2
  400. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  401. package/lib/typescript/src/features/metadata.d.ts +20 -3
  402. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  403. package/lib/typescript/src/features/network.d.ts +1 -1
  404. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  405. package/lib/typescript/src/features/output.d.ts +1 -1
  406. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  407. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  408. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  409. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  410. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  411. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  412. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  413. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  414. package/lib/typescript/src/features/playback/state.d.ts +3 -4
  415. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  416. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  417. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  418. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  419. package/lib/typescript/src/features/player/options.d.ts +108 -56
  420. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  421. package/lib/typescript/src/features/player/setup.d.ts +14 -4
  422. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  423. package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
  424. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  425. package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
  426. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  427. package/lib/typescript/src/features/queue/queue.d.ts +7 -4
  428. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  429. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  430. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  431. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  432. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  433. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  434. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  435. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  436. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  437. package/lib/typescript/src/types/browser-native.d.ts +3 -2
  438. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  439. package/lib/typescript/src/types/browser-nodes.d.ts +387 -126
  440. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  441. package/lib/typescript/src/types/browser.d.ts +93 -61
  442. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  443. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
  444. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  445. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  446. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  447. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  448. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  449. package/lib/typescript/src/utils/resolveAssetSource.d.ts +1 -2
  450. package/lib/typescript/src/utils/resolveAssetSource.d.ts.map +1 -1
  451. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  452. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  453. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
  454. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +4 -2
  455. package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +2 -2
  456. package/nitrogen/generated/android/c++/JAndroidAudioOffloadSettings.hpp +2 -2
  457. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +12 -12
  458. package/nitrogen/generated/android/c++/JAndroidPlayerWakeMode.hpp +2 -2
  459. package/nitrogen/generated/android/c++/JAppKilledPlaybackBehavior.hpp +2 -2
  460. package/nitrogen/generated/android/c++/JArtworkRendering.hpp +58 -0
  461. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +10 -5
  462. package/nitrogen/generated/android/c++/JArtworkVariants.hpp +2 -2
  463. package/nitrogen/generated/android/c++/JBatteryOptimizationStatus.hpp +2 -2
  464. package/nitrogen/generated/android/c++/JBatteryOptimizationStatusChangedEvent.hpp +2 -2
  465. package/nitrogen/generated/android/c++/JBatteryWarningPendingChangedEvent.hpp +2 -2
  466. package/nitrogen/generated/android/c++/JBrowseError.hpp +2 -2
  467. package/nitrogen/generated/android/c++/JBrowseResult.hpp +19 -7
  468. package/nitrogen/generated/android/c++/JBrowserSourceCallbackParam.hpp +2 -2
  469. package/nitrogen/generated/android/c++/JCarPlayNowPlayingButton.hpp +2 -2
  470. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  471. package/nitrogen/generated/android/c++/JCarPlaySiriListButtonPosition.hpp +2 -2
  472. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  473. package/nitrogen/generated/android/c++/JChapterMetadata.hpp +2 -2
  474. package/nitrogen/generated/android/c++/JEqualizerSettings.hpp +10 -10
  475. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +10 -5
  476. package/nitrogen/generated/android/c++/JFormatNavigationErrorParams.hpp +11 -5
  477. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +10 -5
  478. package/nitrogen/generated/android/c++/JFormattedNavigationError.hpp +2 -2
  479. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.hpp +2 -2
  480. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
  481. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +10 -5
  482. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.hpp +4 -2
  483. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +10 -5
  484. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +10 -5
  485. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.hpp +2 -2
  486. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.hpp +2 -2
  487. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
  488. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +10 -5
  489. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.hpp +2 -2
  490. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +18 -12
  491. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +10 -4
  492. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
  493. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +18 -6
  494. package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -2
  495. package/nitrogen/generated/android/c++/JFunc_void_BatteryOptimizationStatusChangedEvent.hpp +2 -2
  496. package/nitrogen/generated/android/c++/JFunc_void_BatteryWarningPendingChangedEvent.hpp +2 -2
  497. package/nitrogen/generated/android/c++/JFunc_void_EqualizerSettings.hpp +2 -2
  498. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +10 -5
  499. package/nitrogen/generated/android/c++/JFunc_void_GateEvent.hpp +2 -2
  500. package/nitrogen/generated/android/c++/JFunc_void_NavigationErrorEvent.hpp +2 -2
  501. package/nitrogen/generated/android/c++/JFunc_void_NowPlayingMetadata.hpp +2 -2
  502. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +9 -12
  503. package/nitrogen/generated/android/c++/JFunc_void_Output.hpp +2 -2
  504. package/nitrogen/generated/android/c++/JFunc_void_Playback.hpp +2 -2
  505. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +10 -5
  506. package/nitrogen/generated/android/c++/JFunc_void_PlaybackErrorEvent.hpp +2 -2
  507. package/nitrogen/generated/android/c++/JFunc_void_PlaybackPlayWhenReadyChangedEvent.hpp +2 -2
  508. package/nitrogen/generated/android/c++/JFunc_void_PlaybackProgressUpdatedEvent.hpp +2 -2
  509. package/nitrogen/generated/android/c++/JFunc_void_PlaybackQueueEndedEvent.hpp +2 -2
  510. package/nitrogen/generated/android/c++/JFunc_void_PlayingState.hpp +2 -2
  511. package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpBackwardEvent.hpp +2 -2
  512. package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpForwardEvent.hpp +2 -2
  513. package/nitrogen/generated/android/c++/JFunc_void_RemotePlayIdEvent.hpp +2 -2
  514. package/nitrogen/generated/android/c++/JFunc_void_RemotePlaySearchEvent.hpp +2 -2
  515. package/nitrogen/generated/android/c++/JFunc_void_RemoteSeekEvent.hpp +2 -2
  516. package/nitrogen/generated/android/c++/JFunc_void_RemoteSkipEvent.hpp +2 -2
  517. package/nitrogen/generated/android/c++/JFunc_void_RepeatModeChangedEvent.hpp +2 -2
  518. package/nitrogen/generated/android/c++/JFunc_void_TimedMetadata.hpp +2 -2
  519. package/nitrogen/generated/android/c++/JFunc_void_TrackMetadata.hpp +2 -2
  520. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +2 -2
  521. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +2 -2
  522. package/nitrogen/generated/android/c++/JFunc_void_std__optional_FormattedNavigationError_.hpp +2 -2
  523. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +17 -5
  524. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.hpp +2 -2
  525. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +2 -2
  526. package/nitrogen/generated/android/c++/JFunc_void_std__vector_ChapterMetadata_.hpp +16 -16
  527. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +24 -18
  528. package/nitrogen/generated/android/c++/JGate.hpp +2 -2
  529. package/nitrogen/generated/android/c++/JGateDecision.hpp +2 -2
  530. package/nitrogen/generated/android/c++/JGateEvent.hpp +2 -2
  531. package/nitrogen/generated/android/c++/JGateReason.hpp +2 -2
  532. package/nitrogen/generated/android/c++/JGridTile.hpp +64 -0
  533. package/nitrogen/generated/android/c++/JHttpMethod.hpp +2 -2
  534. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +77 -53
  535. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +3 -3
  536. package/nitrogen/generated/android/c++/JIOSCategory.hpp +2 -2
  537. package/nitrogen/generated/android/c++/JIOSCategoryMode.hpp +2 -2
  538. package/nitrogen/generated/android/c++/JIOSCategoryOptions.hpp +2 -2
  539. package/nitrogen/generated/android/c++/JIOSCategoryPolicy.hpp +2 -2
  540. package/nitrogen/generated/android/c++/JIOSOptions.hpp +17 -11
  541. package/nitrogen/generated/android/c++/JImageContext.hpp +2 -2
  542. package/nitrogen/generated/android/c++/JImageQueryParams.hpp +2 -2
  543. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  544. package/nitrogen/generated/android/c++/JImageSource.hpp +2 -2
  545. package/nitrogen/generated/android/c++/JMediaReference.hpp +2 -2
  546. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +10 -5
  547. package/nitrogen/generated/android/c++/JMediaTransformParams.hpp +2 -2
  548. package/nitrogen/generated/android/c++/JNativeAndroidSetupOptions.hpp +2 -2
  549. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +48 -21
  550. package/nitrogen/generated/android/c++/JNativeGateRequest.hpp +2 -2
  551. package/nitrogen/generated/android/c++/JNativeIOSSetupOptions.hpp +10 -10
  552. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +16 -4
  553. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +17 -14
  554. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +9 -12
  555. package/nitrogen/generated/android/c++/JNavigationError.hpp +2 -2
  556. package/nitrogen/generated/android/c++/JNavigationErrorEvent.hpp +2 -2
  557. package/nitrogen/generated/android/c++/JNavigationErrorKind.hpp +58 -0
  558. package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +2 -2
  559. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +12 -12
  560. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +17 -11
  561. package/nitrogen/generated/android/c++/JNowPlayingMetadata.hpp +2 -2
  562. package/nitrogen/generated/android/c++/JNowPlayingUpdate.hpp +2 -2
  563. package/nitrogen/generated/android/c++/JOptions.hpp +9 -12
  564. package/nitrogen/generated/android/c++/JOutput.hpp +2 -2
  565. package/nitrogen/generated/android/c++/JOutputType.hpp +2 -2
  566. package/nitrogen/generated/android/c++/JPlayback.hpp +2 -2
  567. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +10 -5
  568. package/nitrogen/generated/android/c++/JPlaybackError.hpp +2 -2
  569. package/nitrogen/generated/android/c++/JPlaybackErrorEvent.hpp +2 -2
  570. package/nitrogen/generated/android/c++/JPlaybackErrorKind.hpp +2 -2
  571. package/nitrogen/generated/android/c++/JPlaybackPlayWhenReadyChangedEvent.hpp +2 -2
  572. package/nitrogen/generated/android/c++/JPlaybackProgressUpdatedEvent.hpp +2 -2
  573. package/nitrogen/generated/android/c++/JPlaybackQueueEndedEvent.hpp +2 -2
  574. package/nitrogen/generated/android/c++/JPlaybackState.hpp +2 -2
  575. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +7 -13
  576. package/nitrogen/generated/android/c++/JPlayingState.hpp +2 -2
  577. package/nitrogen/generated/android/c++/JProgress.hpp +2 -2
  578. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  579. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  580. package/nitrogen/generated/android/c++/JRemoteJumpBackwardEvent.hpp +2 -2
  581. package/nitrogen/generated/android/c++/JRemoteJumpForwardEvent.hpp +2 -2
  582. package/nitrogen/generated/android/c++/JRemotePlayIdEvent.hpp +2 -2
  583. package/nitrogen/generated/android/c++/JRemotePlaySearchEvent.hpp +2 -2
  584. package/nitrogen/generated/android/c++/JRemoteSeekEvent.hpp +2 -2
  585. package/nitrogen/generated/android/c++/JRemoteSkipEvent.hpp +2 -2
  586. package/nitrogen/generated/android/c++/JRepeatMode.hpp +2 -2
  587. package/nitrogen/generated/android/c++/JRepeatModeChangedEvent.hpp +2 -2
  588. package/nitrogen/generated/android/c++/JRequestConfig.hpp +2 -2
  589. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +67 -59
  590. package/nitrogen/generated/android/c++/JRetryConfig.hpp +2 -2
  591. package/nitrogen/generated/android/c++/JSearchMode.hpp +2 -2
  592. package/nitrogen/generated/android/c++/JSearchParams.hpp +2 -2
  593. package/nitrogen/generated/android/c++/JSection.hpp +116 -0
  594. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  595. package/nitrogen/generated/android/c++/JSleepTimer.hpp +4 -4
  596. package/nitrogen/generated/android/c++/JSleepTimerEndOfTrack.hpp +2 -2
  597. package/nitrogen/generated/android/c++/JSleepTimerTime.hpp +2 -2
  598. package/nitrogen/generated/android/c++/JStallReason.hpp +2 -2
  599. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  600. package/nitrogen/generated/android/c++/JTimedMetadata.hpp +2 -2
  601. package/nitrogen/generated/android/c++/JTrack.hpp +27 -48
  602. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +18 -12
  603. package/nitrogen/generated/android/c++/JTrackMetadata.hpp +2 -2
  604. package/nitrogen/generated/android/c++/JTrackRequest.hpp +2 -2
  605. package/nitrogen/generated/android/c++/JTrackStyle.hpp +47 -19
  606. package/nitrogen/generated/android/c++/JTransformableRequestConfig.hpp +2 -2
  607. package/nitrogen/generated/android/c++/JVariant_Boolean_AndroidAudioOffloadSettings.hpp +3 -3
  608. package/nitrogen/generated/android/c++/JVariant_Boolean_RetryConfig.hpp +3 -3
  609. package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +3 -3
  610. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  611. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  612. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +20 -8
  613. package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.hpp +3 -3
  614. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidAudioOffloadSettings.kt +15 -0
  615. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +20 -3
  616. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  617. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkRequestConfig.kt +37 -0
  618. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkVariants.kt +15 -0
  619. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryOptimizationStatusChangedEvent.kt +13 -0
  620. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryWarningPendingChangedEvent.kt +13 -0
  621. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseError.kt +13 -0
  622. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseResult.kt +16 -7
  623. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowserSourceCallbackParam.kt +15 -0
  624. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  625. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  626. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ChapterMetadata.kt +19 -0
  627. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/EqualizerSettings.kt +27 -0
  628. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteChangedEvent.kt +15 -0
  629. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNavigationErrorParams.kt +25 -3
  630. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNowPlayingParams.kt +21 -0
  631. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormattedNavigationError.kt +15 -0
  632. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.kt +0 -2
  633. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
  634. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___Track.kt +0 -2
  635. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.kt +0 -2
  636. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.kt +0 -2
  637. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_std__string____Track.kt +0 -2
  638. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.kt +0 -2
  639. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.kt +0 -2
  640. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
  641. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.kt +0 -2
  642. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.kt +0 -2
  643. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.kt +0 -2
  644. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.kt +0 -2
  645. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +78 -0
  646. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.kt +0 -2
  647. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void.kt +0 -2
  648. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryOptimizationStatusChangedEvent.kt +0 -2
  649. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryWarningPendingChangedEvent.kt +0 -2
  650. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_EqualizerSettings.kt +0 -2
  651. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_FavoriteChangedEvent.kt +0 -2
  652. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_GateEvent.kt +0 -2
  653. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NavigationErrorEvent.kt +0 -2
  654. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NowPlayingMetadata.kt +0 -2
  655. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Options.kt +0 -2
  656. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Output.kt +0 -2
  657. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Playback.kt +0 -2
  658. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackActiveTrackChangedEvent.kt +0 -2
  659. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackErrorEvent.kt +0 -2
  660. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackPlayWhenReadyChangedEvent.kt +0 -2
  661. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackProgressUpdatedEvent.kt +0 -2
  662. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackQueueEndedEvent.kt +0 -2
  663. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlayingState.kt +0 -2
  664. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpBackwardEvent.kt +0 -2
  665. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpForwardEvent.kt +0 -2
  666. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlayIdEvent.kt +0 -2
  667. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlaySearchEvent.kt +0 -2
  668. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSeekEvent.kt +0 -2
  669. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSkipEvent.kt +0 -2
  670. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RepeatModeChangedEvent.kt +0 -2
  671. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TimedMetadata.kt +0 -2
  672. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TrackMetadata.kt +0 -2
  673. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_bool.kt +0 -2
  674. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_double.kt +0 -2
  675. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_FormattedNavigationError_.kt +0 -2
  676. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_ResolvedTrack_.kt +0 -2
  677. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.kt +0 -2
  678. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__string.kt +0 -2
  679. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_ChapterMetadata_.kt +0 -2
  680. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_Track_.kt +0 -2
  681. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Gate.kt +15 -0
  682. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateDecision.kt +15 -0
  683. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateEvent.kt +13 -0
  684. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +25 -0
  685. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +3 -1
  686. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +25 -3
  687. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageContext.kt +15 -0
  688. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageQueryParams.kt +15 -0
  689. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  690. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageSource.kt +19 -0
  691. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaRequestConfig.kt +35 -0
  692. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaTransformParams.kt +15 -0
  693. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeAndroidSetupOptions.kt +31 -0
  694. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +51 -5
  695. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeGateRequest.kt +17 -0
  696. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeIOSSetupOptions.kt +21 -0
  697. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeRouteEntry.kt +27 -0
  698. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeSetupPlayerOptions.kt +29 -0
  699. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeUpdateOptions.kt +23 -0
  700. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationError.kt +19 -0
  701. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorEvent.kt +13 -0
  702. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorKind.kt +23 -0
  703. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +20 -3
  704. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +25 -3
  705. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingMetadata.kt +29 -0
  706. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingUpdate.kt +17 -0
  707. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Options.kt +23 -0
  708. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Output.kt +17 -0
  709. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Playback.kt +15 -0
  710. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackActiveTrackChangedEvent.kt +21 -0
  711. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackError.kt +21 -0
  712. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackErrorEvent.kt +13 -0
  713. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackPlayWhenReadyChangedEvent.kt +13 -0
  714. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackProgressUpdatedEvent.kt +19 -0
  715. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackQueueEndedEvent.kt +15 -0
  716. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +37 -2
  717. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayingState.kt +15 -0
  718. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Progress.kt +17 -0
  719. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  720. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +61 -0
  721. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpBackwardEvent.kt +13 -0
  722. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpForwardEvent.kt +13 -0
  723. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlayIdEvent.kt +15 -0
  724. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlaySearchEvent.kt +13 -0
  725. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSeekEvent.kt +13 -0
  726. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSkipEvent.kt +13 -0
  727. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RepeatModeChangedEvent.kt +13 -0
  728. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RequestConfig.kt +27 -0
  729. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +67 -15
  730. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RetryConfig.kt +17 -0
  731. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchParams.kt +27 -0
  732. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +66 -0
  733. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +86 -0
  734. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimer.kt +18 -8
  735. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerEndOfTrack.kt +13 -0
  736. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerTime.kt +13 -0
  737. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  738. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TimedMetadata.kt +21 -0
  739. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +55 -12
  740. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackLoadEvent.kt +17 -0
  741. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackMetadata.kt +43 -0
  742. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackRequest.kt +17 -0
  743. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +58 -5
  744. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TransformableRequestConfig.kt +31 -0
  745. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_AndroidAudioOffloadSettings.kt +16 -7
  746. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_RetryConfig.kt +16 -7
  747. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_NullType_Double.kt +16 -7
  748. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +24 -15
  749. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.kt +18 -8
  750. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_String_ArtworkVariants.kt +16 -7
  751. package/nitrogen/generated/ios/AudioBrowser+autolinking.rb +2 -0
  752. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +38 -29
  753. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +779 -665
  754. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +34 -16
  755. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +36 -18
  756. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  757. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  758. package/nitrogen/generated/ios/swift/BrowseResult.swift +12 -0
  759. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  760. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  761. package/nitrogen/generated/ios/swift/FormatNavigationErrorParams.swift +7 -2
  762. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
  763. package/nitrogen/generated/ios/swift/GridTile.swift +48 -0
  764. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
  765. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
  766. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  767. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  768. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
  769. package/nitrogen/generated/ios/swift/NavigationErrorKind.swift +40 -0
  770. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  771. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  772. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  773. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  774. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  775. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
  776. package/nitrogen/generated/ios/swift/Section.swift +82 -0
  777. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  778. package/nitrogen/generated/ios/swift/SleepTimer.swift +13 -0
  779. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  780. package/nitrogen/generated/ios/swift/Track.swift +28 -63
  781. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  782. package/nitrogen/generated/ios/swift/Variant_Bool_AndroidAudioOffloadSettings.swift +12 -0
  783. package/nitrogen/generated/ios/swift/Variant_Bool_RetryConfig.swift +12 -0
  784. package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +12 -0
  785. package/nitrogen/generated/ios/swift/Variant_NullType_RemoteButtonLayout.swift +30 -0
  786. package/nitrogen/generated/ios/swift/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.swift +13 -0
  787. package/nitrogen/generated/ios/swift/Variant_String_ArtworkVariants.swift +12 -0
  788. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  789. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  790. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  791. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  792. package/nitrogen/generated/shared/c++/FormatNavigationErrorParams.hpp +9 -2
  793. package/nitrogen/generated/shared/c++/GridTile.hpp +84 -0
  794. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
  795. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
  796. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  797. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  798. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
  799. package/nitrogen/generated/shared/c++/NavigationErrorKind.hpp +76 -0
  800. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  801. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  802. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  803. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  804. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  805. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
  806. package/nitrogen/generated/shared/c++/Section.hpp +102 -0
  807. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  808. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  809. package/nitrogen/generated/shared/c++/Track.hpp +18 -30
  810. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  811. package/package.json +24 -11
  812. package/src/AudioBrowser.ts +2 -1
  813. package/src/CLAUDE.md +52 -0
  814. package/src/features/battery.test.ts +79 -0
  815. package/src/features/browser-config.test.ts +21 -6
  816. package/src/features/browser-config.ts +12 -8
  817. package/src/features/browser.test.ts +1 -1
  818. package/src/features/browser.ts +11 -3
  819. package/src/features/carConnection.ts +1 -1
  820. package/src/features/equalizer.ts +1 -1
  821. package/src/features/errors.ts +3 -3
  822. package/src/features/favorites.ts +14 -11
  823. package/src/features/gate.ts +16 -1
  824. package/src/features/hook-wiring.test.ts +302 -0
  825. package/src/features/metadata.ts +20 -3
  826. package/src/features/network.ts +1 -1
  827. package/src/features/nowPlaying.ts +1 -1
  828. package/src/features/output.ts +1 -1
  829. package/src/features/playback/controls.ts +4 -2
  830. package/src/features/playback/playWhenReady.ts +1 -1
  831. package/src/features/playback/playing.ts +1 -1
  832. package/src/features/playback/progress.test.ts +169 -0
  833. package/src/features/playback/progress.ts +3 -2
  834. package/src/features/playback/rate.ts +8 -1
  835. package/src/features/playback/state.ts +3 -4
  836. package/src/features/playback/trackPlaybackTime.test.ts +16 -6
  837. package/src/features/playback/trackPlaybackTime.ts +1 -6
  838. package/src/features/playback/volume.ts +1 -1
  839. package/src/features/player/options.ts +126 -76
  840. package/src/features/player/setup.test.ts +24 -7
  841. package/src/features/player/setup.ts +22 -10
  842. package/src/features/player/validateOptions.ts +21 -0
  843. package/src/features/queue/activeTrack.ts +15 -4
  844. package/src/features/queue/queue.ts +13 -6
  845. package/src/features/queue/repeatMode.ts +1 -1
  846. package/src/features/queue/shuffle.ts +1 -1
  847. package/src/features/remoteControls.ts +13 -92
  848. package/src/features/sleepTimer.test.ts +172 -0
  849. package/src/features/sleepTimer.ts +9 -1
  850. package/src/nitro-stub-drift.test.ts +277 -0
  851. package/src/public-surface.test.ts +147 -0
  852. package/src/specs/audio-browser.nitro.ts +5 -4
  853. package/src/style-resolution.test.ts +210 -0
  854. package/src/test-utils/reactNativeStub.ts +60 -0
  855. package/src/types/browser-native.ts +7 -3
  856. package/src/types/browser-nodes.ts +403 -134
  857. package/src/types/browser.ts +97 -63
  858. package/src/utils/LazyNativeEmitter.ts +28 -15
  859. package/src/utils/NativeUpdatedValue.ts +29 -13
  860. package/src/utils/getTrackIdentity.ts +20 -0
  861. package/src/utils/useDebug.test.ts +182 -0
  862. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  863. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  864. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
  865. package/src/web/NativeAudioBrowser.ts +240 -83
  866. package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
  867. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
  868. package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
  869. package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
  870. package/src/web/TrackPlayer/Player.load.test.ts +137 -1
  871. package/src/web/TrackPlayer/Player.ts +284 -37
  872. package/src/web/TrackPlayer/PlayingStateFactory.test.ts +1 -1
  873. package/src/web/TrackPlayer/PlayingStateFactory.ts +1 -1
  874. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  875. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  876. package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
  877. package/src/web/__tests__/lifecycle.test.ts +345 -0
  878. package/src/web/__tests__/lifecycleHarness.ts +351 -0
  879. package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
  880. package/src/web/__tests__/loadSupersession.test.ts +175 -0
  881. package/src/web/__tests__/retry.test.ts +169 -0
  882. package/src/web/browser/BrowserManager.ts +209 -111
  883. package/src/web/browser/FavoriteManager.test.ts +84 -0
  884. package/src/web/browser/FavoriteManager.ts +28 -23
  885. package/src/web/browser/NavigationErrorManager.ts +3 -1
  886. package/src/web/browser/SearchManager.test.ts +75 -0
  887. package/src/web/browser/SearchManager.ts +25 -28
  888. package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
  889. package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
  890. package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
  891. package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
  892. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  893. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  894. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  895. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  896. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  897. package/src/web/browser/sections.test.ts +91 -0
  898. package/src/web/browser/sections.ts +72 -0
  899. package/src/web/http/RequestConfigBuilder.test.ts +241 -1
  900. package/src/web/http/RequestConfigBuilder.ts +140 -33
  901. package/src/web/http/followMediaRedirect.test.ts +289 -0
  902. package/src/web/http/followMediaRedirect.ts +253 -0
  903. package/src/web/player/NowPlayingManager.ts +4 -3
  904. package/src/web/util/BrowserPathHelper.test.ts +68 -0
  905. package/src/web/util/BrowserPathHelper.ts +70 -13
  906. package/website/guide/android-auto.md +114 -0
  907. package/website/guide/android-certificates.md +409 -0
  908. package/website/guide/artwork.md +255 -0
  909. package/website/guide/audio-output.md +135 -0
  910. package/website/guide/automotive.md +86 -0
  911. package/website/guide/basic-usage.md +151 -0
  912. package/website/guide/battery.md +115 -0
  913. package/website/guide/browser.md +587 -0
  914. package/website/guide/carplay.md +650 -0
  915. package/website/guide/configuration.md +173 -0
  916. package/website/guide/equalizer.md +231 -0
  917. package/website/guide/errors.md +310 -0
  918. package/website/guide/favorites.md +154 -0
  919. package/website/guide/gate.md +143 -0
  920. package/website/guide/getting-started.md +46 -0
  921. package/website/guide/hooks.md +238 -0
  922. package/website/guide/live-streams.md +125 -0
  923. package/website/guide/metadata.md +239 -0
  924. package/website/guide/migrating-from-track-player.md +244 -0
  925. package/website/guide/native-callback-fetch.md +127 -0
  926. package/website/guide/network.md +86 -0
  927. package/website/guide/now-playing.md +175 -0
  928. package/website/guide/playback.md +328 -0
  929. package/website/guide/queue.md +300 -0
  930. package/website/guide/remote-controls.md +256 -0
  931. package/website/guide/search.md +255 -0
  932. package/website/guide/sleep-timer.md +268 -0
  933. package/website/guide/track.md +247 -0
  934. package/ios/Bridge.h +0 -8
  935. package/ios/Browser/MediaResolveComposer.swift +0 -114
  936. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  937. package/ios/Model/MediaURL.swift +0 -67
  938. package/ios/NowPlayingInfo/MediaItemProperty.swift +0 -78
  939. package/ios/NowPlayingInfo/NowPlayingInfoKeyValue.swift +0 -6
  940. package/ios/NowPlayingInfo/NowPlayingInfoProperty.swift +0 -220
  941. package/ios/Observer/PlayerItemNotificationObserver.swift +0 -119
  942. package/ios/Observer/PlayerItemPropertyObserver.swift +0 -133
  943. package/ios/Observer/PlayerStateObserver.swift +0 -50
  944. package/ios/Observer/PlayerTimeObserver.swift +0 -55
  945. package/ios/Option/PitchAlgorithms.swift +0 -17
  946. package/ios/Option/TimeEventFrequency.swift +0 -18
  947. package/ios/Player/LoadSeekCoordinator.swift +0 -99
  948. package/ios/Player/MediaLoaderDelegate.swift +0 -11
  949. package/ios/Player/PlaybackCoordinator.swift +0 -754
  950. package/ios/Player/PlaybackEffectHandler.swift +0 -54
  951. package/ios/Player/PlaybackErrorHandler.swift +0 -175
  952. package/ios/Player/PlaybackStateStore.swift +0 -27
  953. package/ios/Player/PlayingStateManager.swift +0 -33
  954. package/ios/Player/RetryManager.swift +0 -322
  955. package/ios/Player/SeekCompletionHandler.swift +0 -3
  956. package/ios/Player/SleepTimerHandling.swift +0 -15
  957. package/ios/Support/RNTrackPlayer-Bridging-Header.h +0 -7
  958. package/ios/Tests/LoadSeekCoordinatorTests.swift +0 -325
  959. package/ios/Tests/MediaResolveTests.swift +0 -126
  960. package/ios/Tests/OnceValueTests.swift +0 -79
  961. package/ios/Tests/PlaybackErrorHandlerTests.swift +0 -547
  962. package/ios/Tests/PlaybackStateStoreTests.swift +0 -48
  963. package/ios/Tests/PlayingStateManagerTests.swift +0 -57
  964. package/ios/Tests/RetryManagerTests.swift +0 -89
  965. package/ios/TrackPlayer.swift +0 -1014
  966. package/ios/TrackPlayerCallbacks.swift +0 -86
  967. package/ios/Util/MetadataAdapter.swift +0 -131
  968. package/ios/Util/OnceValue.swift +0 -104
  969. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  970. package/nitrogen/generated/android/c++/JFavoritesMatchMode.hpp +0 -58
  971. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  972. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  973. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  974. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  975. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  976. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  977. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteConfig.kt +0 -38
  978. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  979. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  980. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  981. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  982. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  983. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  984. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  985. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  986. package/nitrogen/generated/ios/swift/Variant_NullType_NotificationButtonLayout.swift +0 -18
  987. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  988. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
  989. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
@@ -20,9 +20,11 @@ import com.audiobrowser.browser.CallbackException
20
20
  import com.audiobrowser.browser.ContentNotFoundException
21
21
  import com.audiobrowser.browser.HttpStatusException
22
22
  import com.audiobrowser.browser.NetworkException
23
+ import com.audiobrowser.browser.flattenedChildren
23
24
  import com.audiobrowser.browser.handleTrackLoad
24
25
  import com.audiobrowser.browser.resolveMediaUrl
25
26
  import com.audiobrowser.extension.NumberExt.Companion.toSeconds
27
+ import com.audiobrowser.extension.indexOfTappedTrack
26
28
  import com.audiobrowser.model.PlayerSetupOptions
27
29
  import com.audiobrowser.model.PlayerUpdateOptions
28
30
  import com.audiobrowser.player.Player
@@ -35,6 +37,7 @@ import com.audiobrowser.util.SystemVolumeMonitor
35
37
  import com.facebook.proguard.annotations.DoNotStrip
36
38
  import com.google.common.util.concurrent.ListenableFuture
37
39
  import com.margelo.nitro.NitroModules
40
+ import com.margelo.nitro.audiobrowser.ArtworkRequestConfig
38
41
  import com.margelo.nitro.audiobrowser.BatteryOptimizationStatus
39
42
  import com.margelo.nitro.audiobrowser.BatteryOptimizationStatusChangedEvent
40
43
  import com.margelo.nitro.audiobrowser.BatteryWarningPendingChangedEvent
@@ -50,6 +53,7 @@ import com.margelo.nitro.audiobrowser.HybridAudioBrowserSpec
50
53
  import com.margelo.nitro.audiobrowser.MediaRequestConfig
51
54
  import com.margelo.nitro.audiobrowser.NativeBrowserConfiguration
52
55
  import com.margelo.nitro.audiobrowser.NativeGateRequest
56
+ import com.margelo.nitro.audiobrowser.NativeRouteEntry
53
57
  import com.margelo.nitro.audiobrowser.NativeSetupPlayerOptions
54
58
  import com.margelo.nitro.audiobrowser.NativeUpdateOptions
55
59
  import com.margelo.nitro.audiobrowser.NavigationError
@@ -82,31 +86,106 @@ import com.margelo.nitro.audiobrowser.SleepTimer
82
86
  import com.margelo.nitro.audiobrowser.TimedMetadata
83
87
  import com.margelo.nitro.audiobrowser.Track
84
88
  import com.margelo.nitro.audiobrowser.TrackMetadata
89
+ import com.margelo.nitro.audiobrowser.TransformableRequestConfig
90
+ import com.margelo.nitro.core.NullType
85
91
  import com.margelo.nitro.core.Promise
86
92
  import java.util.concurrent.TimeUnit
93
+ import java.util.concurrent.atomic.AtomicReference
87
94
  import kotlin.coroutines.resume
88
95
  import kotlin.coroutines.resumeWithException
89
96
  import kotlin.coroutines.suspendCoroutine
97
+ import kotlin.time.Duration.Companion.seconds
90
98
  import kotlinx.coroutines.CancellationException
99
+ import kotlinx.coroutines.CoroutineScope
91
100
  import kotlinx.coroutines.Job
92
101
  import kotlinx.coroutines.MainScope
102
+ import kotlinx.coroutines.async
103
+ import kotlinx.coroutines.cancel
93
104
  import kotlinx.coroutines.cancelChildren
94
- import kotlinx.coroutines.delay
95
105
  import kotlinx.coroutines.launch
96
106
  import kotlinx.coroutines.runBlocking
107
+ import kotlinx.coroutines.withTimeoutOrNull
97
108
  import timber.log.Timber
98
109
 
99
110
  @Keep
100
111
  @DoNotStrip
101
112
  class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
102
113
 
103
- /** Called by Nitro when the JS object is destroyed (incl. JS runtime reloads). */
114
+ /**
115
+ * Nitro calls this only when JS calls `dispose()` explicitly — never on runtime teardown, which
116
+ * is what [AudioBrowserLifecycleModule] is for. Both routes land here.
117
+ *
118
+ * The Service outlives this object, so everything registered in `init` or on connect has to be
119
+ * undone. Left alone, the running [Player] goes on invoking callbacks into a dead JSI runtime,
120
+ * and every reload strands another ServiceConnection and lifecycle observer, each retaining a
121
+ * BrowserManager.
122
+ *
123
+ * Runs the teardown on main: the state below is written from main-thread coroutines
124
+ * ([onServiceConnected], [onServiceDisconnected]), while dispose itself arrives on the JS thread
125
+ * or the teardown callback's thread.
126
+ */
104
127
  override fun dispose() {
128
+ current.compareAndSet(this, null)
105
129
  systemVolumeMonitor.destroy()
106
130
  outputMonitor.destroy()
131
+ // Only when still ours: on a reload the replacement may already own the slot.
132
+ carConnectionTarget.compareAndSet(onCarConnectedChanged, null)
133
+
134
+ // The runtime behind the config's JS functions is going away, but anything still holding
135
+ // this instance (an in-flight browse coroutine, the running player) can invoke them after
136
+ // it's gone — each invoke then throws instead of answering, so browse takes its error path
137
+ // rather than its fallback.
138
+ dropStaleJSCallbacks()
139
+
140
+ handler.post {
141
+ connectedService?.let { service ->
142
+ // Identity-guarded: both instances bind to the same singleton Service, so a late dispose
143
+ // must not strip the wiring a replacement has already installed.
144
+ if (service.player.getCallbacks() === callbacks) service.player.setCallbacks(null)
145
+ if (service.player.browser === this) {
146
+ service.player.browser = null
147
+ service.player.forgetBrowserRegistration()
148
+ // The formatter wraps a JS callback from this instance's runtime (installed by our
149
+ // setup(); browser still being ours means no replacement has run setup yet). Without
150
+ // this, every track change until the next setup() throws into the updater's fallback.
151
+ service.player.nowPlaying.formatter = null
152
+ }
153
+ if (service.onBatteryWarningPendingChanged === onBatteryWarningPending) {
154
+ service.onBatteryWarningPendingChanged = null
155
+ }
156
+ }
157
+ connectedService = null
158
+
159
+ // Dropping the reference disconnects nothing — the controller stays in the session's
160
+ // connected list, holding a binder and a listener.
161
+ mediaBrowserFuture?.let { MediaBrowser.releaseFuture(it) }
162
+ mediaBrowserFuture = null
163
+ setupPromise = null
164
+
165
+ serviceBinding.unbind(this)
166
+
167
+ ProcessLifecycleOwner.get().lifecycle.removeObserver(lifecycleObserver)
168
+ // After the observer removal, so this post is not the one dropped.
169
+ handler.removeCallbacksAndMessages(null)
170
+ mainScope.cancel()
171
+ }
107
172
  super.dispose()
108
173
  }
109
174
 
175
+ /**
176
+ * Demotes the config to data-only (deep strip of every JS function field) and reverts the JS
177
+ * callbacks stored outside the config tree ([resolveGate]; the now-playing formatter is cleared
178
+ * in [dispose]'s service block, identity-guarded). Stale paths then degrade the same way as a
179
+ * config that never had callbacks — static request/browse layers, default error copy, fail-closed
180
+ * gate — instead of throwing into the error path. Mirrors iOS `dropStaleJSCallbacks`, where the
181
+ * same invoke is fatal rather than a throw.
182
+ */
183
+ private fun dropStaleJSCallbacks() {
184
+ _configuration = _configuration.strippingJSCallbacks()
185
+ browserManager.config = buildConfig()
186
+ resolveGate = defaultResolveGate
187
+ }
188
+
110
189
  private val mainScope = MainScope()
111
190
  private var navigationJob: Job? = null
112
191
  private val handler = Handler(Looper.getMainLooper())
@@ -129,8 +208,9 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
129
208
  singleTrack = null,
130
209
  handleTrackLoad = null,
131
210
  androidControllerOfflineError = null,
211
+ viewMoreTitle = null,
132
212
  carPlayLoadingTitle = null,
133
- resolveAlbumUrl = null,
213
+ resolveAlbumPath = null,
134
214
  formatNavigationError = null,
135
215
  )
136
216
 
@@ -149,7 +229,18 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
149
229
  private var updateOptions: PlayerUpdateOptions = PlayerUpdateOptions()
150
230
  private var mediaBrowserFuture: ListenableFuture<MediaBrowser>? = null
151
231
  private var setupOptions = PlayerSetupOptions()
152
- private var connectedService: Service? = null
232
+ // Written only on the main thread (ServiceConnection callbacks, dispose); read from main or,
233
+ // in a few places, straight from the JS thread. @Volatile gives those reads visibility — they
234
+ // must still tolerate staleness in both directions (a just-detached service, or null while
235
+ // onServiceConnected is pending). Keep writes on main; no read-modify-write.
236
+ @Volatile private var connectedService: Service? = null
237
+
238
+ private val serviceBinding = ServiceBinding(context)
239
+
240
+ /** Held rather than inlined so [dispose] can tell its own wiring from a replacement's. */
241
+ private val onBatteryWarningPending: (Boolean) -> Unit = { pending ->
242
+ post { onBatteryWarningPendingChanged(BatteryWarningPendingChangedEvent(pending)) }
243
+ }
153
244
  private var setupPromise: ((Unit) -> Unit)? = null
154
245
 
155
246
  // Initial player state staged before the player exists — from setup options or the imperative
@@ -168,14 +259,14 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
168
259
  override var onFormattedNavigationError: (FormattedNavigationError?) -> Unit = {}
169
260
 
170
261
  // MARK: Gate callbacks (native→JS; set from JS, native CALLS them)
171
- // Default resolver: only reachable in the init window where the native side has recorded
172
- // hasResolver=true but JS hasn't yet assigned the real `resolveGate` slot. It DENIES by default
262
+ // Default resolver: only reachable while hasResolver=true but no live runtime has the real
263
+ // `resolveGate` bound — the init window before JS assigns it, or after [dispose] reverts a dead
264
+ // runtime's resolver. It DENIES by default
173
265
  // (gated=true) so an active gate never serves content during that window — same fail-closed
174
266
  // direction as the resolver-error path in [gateDecision]. The static-gate fast path skips it
175
267
  // entirely once a resolver-less gate is set.
176
- override var resolveGate: (request: NativeGateRequest) -> Promise<Promise<GateDecision>> = {
177
- Promise.resolved(Promise.resolved(GateDecision(gated = true, gate = null)))
178
- }
268
+ override var resolveGate: (request: NativeGateRequest) -> Promise<Promise<GateDecision>> =
269
+ defaultResolveGate
179
270
  override var onGate: (event: GateEvent) -> Unit = {}
180
271
 
181
272
  // MARK: Player callbacks
@@ -239,12 +330,14 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
239
330
  }
240
331
 
241
332
  init {
333
+ current.set(this)
334
+
242
335
  // Auto-bind to service if it's already running
243
336
  launchInScope {
244
337
  try {
245
338
  Timber.d("Attempting to auto-bind to existing AudioBrowserService from AudioBrowser")
246
339
  val intent = Intent(context, Service::class.java)
247
- val bound = context.bindService(intent, this@AudioBrowser, Context.BIND_AUTO_CREATE)
340
+ val bound = serviceBinding.bind(intent, this@AudioBrowser)
248
341
  Timber.d("Auto-bind result: $bound")
249
342
 
250
343
  if (!bound) {
@@ -286,6 +379,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
286
379
  routes = _configuration.routes,
287
380
  singleTrack = _configuration.singleTrack ?: false,
288
381
  androidControllerOfflineError = _configuration.androidControllerOfflineError ?: true,
382
+ viewMoreTitle = _configuration.viewMoreTitle,
289
383
  )
290
384
  }
291
385
 
@@ -319,8 +413,8 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
319
413
  browserManager.config = buildConfig()
320
414
  val tabs = browserManager.queryTabs()
321
415
  if (tabs.isNotEmpty()) {
322
- Timber.d("Using first tab as default path: ${tabs[0].url}")
323
- tabs[0].url
416
+ Timber.d("Using first tab as default path: ${tabs[0].path}")
417
+ tabs[0].path
324
418
  } else {
325
419
  Timber.d("Using root path as default: /")
326
420
  "/"
@@ -375,8 +469,14 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
375
469
  browserManager.config = buildConfig()
376
470
 
377
471
  // Notify player that browser configuration is ready (routes/tabs available)
378
- // This allows Android Auto to start browsing content
379
- connectedService?.player?.notifyBrowserConfigurationReady()
472
+ // This allows Android Auto to start browsing content.
473
+ // This reaches MediaSession.getConnectedControllers, which asserts the
474
+ // main thread, and this setter runs on the JS thread. Hop without
475
+ // blocking -- runBlockingOnMain would deadlock here for the same reason
476
+ // described for the navigation below (JS callbacks can't run while the
477
+ // JS thread waits). Ordering with navigationJob is preserved: both are
478
+ // queued on mainScope.
479
+ launchInScope { connectedService?.player?.notifyBrowserConfigurationReady() }
380
480
 
381
481
  clearNavigationError()
382
482
 
@@ -408,121 +508,56 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
408
508
 
409
509
  override fun getFormattedNavigationError(): FormattedNavigationError? = formattedNavigationError
410
510
 
411
- /** Creates a default formatted error from a NavigationError */
412
- private fun defaultFormattedError(error: NavigationError): FormattedNavigationError {
413
- val title =
414
- when (error.code) {
415
- NavigationErrorType.CONTENT_NOT_FOUND -> "Content Not Found"
416
- NavigationErrorType.NETWORK_ERROR -> "Network Error"
417
- NavigationErrorType.HTTP_ERROR -> {
418
- // Use system-localized HTTP status text (e.g., "Not Found", "Service Unavailable")
419
- error.statusCode?.let { httpStatusText(it.toInt()) } ?: "Server Error"
420
- }
421
- NavigationErrorType.CALLBACK_ERROR -> "Error"
422
- NavigationErrorType.UNKNOWN_ERROR -> "Error"
423
- // Not a failure — a container that resolved with no children. Neutral copy. See ADR 0001.
424
- NavigationErrorType.EMPTY_CONTENT -> "Nothing here"
425
- NavigationErrorType.TIMEOUT -> "Couldn't load"
426
- }
427
- // Omit an empty message so it renders as title-only (e.g. the empty-content case).
428
- return FormattedNavigationError(title, error.message.takeIf { it.isNotEmpty() })
429
- }
430
-
431
- /** Returns localized HTTP status text for the given status code */
432
- private fun httpStatusText(statusCode: Int): String {
433
- return when (statusCode) {
434
- 400 -> "Bad Request"
435
- 401 -> "Unauthorized"
436
- 403 -> "Forbidden"
437
- 404 -> "Not Found"
438
- 405 -> "Method Not Allowed"
439
- 408 -> "Request Timeout"
440
- 429 -> "Too Many Requests"
441
- 500 -> "Internal Server Error"
442
- 502 -> "Bad Gateway"
443
- 503 -> "Service Unavailable"
444
- 504 -> "Gateway Timeout"
445
- else -> "Server Error"
511
+ /**
512
+ * Resolves a navigation error to its display form: the app's `formatNavigationError` for the
513
+ * given [path] when one is configured, else the built-in default. Shared by [setNavigationError]
514
+ * and the Android Auto empty tile so in-app and in-car copy cannot drift (ADR 0001).
515
+ */
516
+ internal suspend fun resolveFormattedError(
517
+ error: NavigationError,
518
+ path: String,
519
+ ): FormattedNavigationError {
520
+ val default = defaultFormattedError(error)
521
+ val formatter = _configuration.formatNavigationError ?: return default
522
+ return formattedOrDefault(mainScope, default) {
523
+ formatter(
524
+ FormatNavigationErrorParams(
525
+ error,
526
+ default,
527
+ path,
528
+ BrowserPathHelper.navigationErrorKind(path),
529
+ )
530
+ )
531
+ .await()
446
532
  }
447
533
  }
448
534
 
449
- private fun setNavigationError(
450
- code: NavigationErrorType,
451
- message: String,
452
- path: String,
453
- statusCode: Double? = null,
454
- statusCodeSuccess: Boolean? = null,
455
- ) {
456
- val navError = NavigationError(code, message, statusCode, statusCodeSuccess)
535
+ private fun setNavigationError(navError: NavigationError, path: String) {
457
536
  navigationError = navError
458
537
  onNavigationError(NavigationErrorEvent(navigationError))
459
538
 
460
- // Format the error (async if using JS callback, sync for defaults)
461
- val defaultFormatted = defaultFormattedError(navError)
462
- val formatter = _configuration.formatNavigationError
463
- if (formatter != null) {
464
- mainScope.launch {
465
- try {
466
- val params = FormatNavigationErrorParams(navError, defaultFormatted, path)
467
- val customFormatted = formatter(params).await()
468
- formattedNavigationError = customFormatted ?: defaultFormatted
469
- } catch (e: Exception) {
470
- formattedNavigationError = defaultFormatted
471
- }
472
- onFormattedNavigationError(formattedNavigationError)
473
- }
474
- } else {
475
- formattedNavigationError = defaultFormatted
476
- onFormattedNavigationError(formattedNavigationError)
539
+ // No formatter means no hop, and the field and event land in this turn: a same-turn
540
+ // getFormattedNavigationError, or a clearNavigationError right after, must not race a
541
+ // launch that has yet to run.
542
+ if (_configuration.formatNavigationError == null) {
543
+ val default = defaultFormattedError(navError)
544
+ formattedNavigationError = default
545
+ onFormattedNavigationError(default)
546
+ return
547
+ }
548
+
549
+ mainScope.launch {
550
+ val formatted = resolveFormattedError(navError, path)
551
+ formattedNavigationError = formatted
552
+ onFormattedNavigationError(formatted)
477
553
  }
478
554
  }
479
555
 
480
- /** Maps common browser exceptions to navigation errors */
556
+ /** Raises the navigation error [e] maps to, for a failed navigation to [path]. */
481
557
  private fun handleBrowserException(e: Exception, path: String, logContext: String) {
482
- when (e) {
483
- is HttpStatusException -> {
484
- Timber.e(e, "HTTP error $logContext")
485
- setNavigationError(
486
- NavigationErrorType.HTTP_ERROR,
487
- e.message ?: "Server error",
488
- path,
489
- e.statusCode.toDouble(),
490
- e.statusCode in 200..299,
491
- )
492
- }
493
- is NetworkException -> {
494
- Timber.e(e, "Network error $logContext")
495
- setNavigationError(
496
- NavigationErrorType.NETWORK_ERROR,
497
- e.message ?: "Network request failed",
498
- path,
499
- )
500
- }
501
- is ContentNotFoundException -> {
502
- Timber.e(e, "Content not found $logContext")
503
- setNavigationError(
504
- NavigationErrorType.CONTENT_NOT_FOUND,
505
- e.message ?: "Content not found",
506
- path,
507
- )
508
- }
509
- is CallbackException -> {
510
- Timber.e(e, "Callback error $logContext")
511
- setNavigationError(
512
- NavigationErrorType.CALLBACK_ERROR,
513
- e.message ?: "An error occurred",
514
- path,
515
- )
516
- }
517
- else -> {
518
- Timber.e(e, "Unexpected error $logContext")
519
- setNavigationError(
520
- NavigationErrorType.UNKNOWN_ERROR,
521
- e.message ?: "An unexpected error occurred",
522
- path,
523
- )
524
- }
525
- }
558
+ val navError = navigationErrorFor(e)
559
+ Timber.e(e, "${navError.code} $logContext")
560
+ setNavigationError(navError, path)
526
561
  }
527
562
 
528
563
  private fun clearNavigationError() {
@@ -558,26 +593,38 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
558
593
  }
559
594
 
560
595
  override fun navigateTrack(track: Track) {
596
+ // A disabled track is unavailable — it never plays, whichever surface or
597
+ // stale resume path delivered the selection (Track.disabled; mirrors iOS
598
+ // TrackSelector.select). Refused before any state changes: an inert tap
599
+ // must not clear an unrelated navigation error or cancel an in-flight
600
+ // navigation.
601
+ if (track.disabled == true) {
602
+ Timber.d("Ignoring selection of disabled track: ${track.title}")
603
+ return
604
+ }
605
+
561
606
  clearNavigationError()
562
607
 
563
608
  // Cancel previous navigation to avoid race conditions
564
609
  navigationJob?.cancel()
565
610
 
566
- val url = track.url
611
+ val path = track.path
567
612
  navigationJob =
568
613
  mainScope.launch {
569
614
  try {
570
615
  when {
571
- // Check if this is a contextual URL (playable-only track with queue context)
572
- url != null && BrowserPathHelper.isContextual(url) -> {
573
- Timber.d("Navigating to contextual track URL: $url")
616
+ // Check if this is a contextual path (playable-only track with queue context)
617
+ path != null && BrowserPathHelper.isContextual(path) -> {
618
+ Timber.d("Navigating to contextual track path: $path")
574
619
 
575
- val parentPath = BrowserPathHelper.stripTrackId(url)
576
- val trackId = BrowserPathHelper.extractTrackId(url)
620
+ val parentPath = BrowserPathHelper.stripTrackId(path)
621
+ val trackId = BrowserPathHelper.extractTrackId(path)
577
622
 
578
- // Check if queue already came from this parent path - just skip to the track
623
+ // Check if queue already came from this parent path - just skip
624
+ // to the tapped surface (exact path first, identity for
625
+ // index-less paths — see indexOfTappedTrack)
579
626
  if (trackId != null && parentPath == player.queueSourcePath) {
580
- val index = player.tracks.indexOfFirst { it.src == trackId }
627
+ val index = player.tracks.indexOfTappedTrack(path, trackId)
581
628
  if (index >= 0) {
582
629
  Timber.d("Queue already from $parentPath, skipping to index $index")
583
630
  handleTrackLoad(
@@ -595,8 +642,8 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
595
642
  }
596
643
  }
597
644
 
598
- // Expand the queue from the contextual URL
599
- val expanded = browserManager.expandQueueFromContextualUrl(url)
645
+ // Expand the queue from the contextual path
646
+ val expanded = browserManager.expandQueueFromContextualPath(path)
600
647
 
601
648
  if (expanded != null) {
602
649
  val (tracks, startIndex) = expanded
@@ -631,9 +678,9 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
631
678
  }
632
679
  }
633
680
  // Navigate to browsable track to show browsing UI
634
- url != null -> {
635
- Timber.d("Navigating to browsable track: $url")
636
- browserManager.navigate(url)
681
+ path != null -> {
682
+ Timber.d("Navigating to browsable track: $path")
683
+ browserManager.navigate(path)
637
684
  }
638
685
  // If track is playable (has src), load it into player
639
686
  track.src != null -> {
@@ -648,22 +695,36 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
648
695
  )
649
696
  }
650
697
  else -> {
651
- throw IllegalArgumentException("Track must have either an 'url' or an 'src' property")
698
+ throw IllegalArgumentException("Track must have either a 'path' or an 'src' property")
652
699
  }
653
700
  }
654
701
  } catch (e: CancellationException) {
655
702
  throw e // Rethrow to properly cancel
656
703
  } catch (e: Exception) {
657
- handleBrowserException(e, url ?: track.src ?: "", "navigating to track: ${track.title}")
704
+ handleBrowserException(e, path ?: track.src ?: "", "navigating to track: ${track.title}")
658
705
  }
659
706
  }
660
707
  }
661
708
 
662
- override fun onSearch(query: String): Promise<Array<Track>> {
709
+ override fun search(query: String): Promise<Array<Track>> {
663
710
  return Promise.async(mainScope) {
664
711
  Timber.d("Searching for: $query")
665
- val searchResults = browserManager.search(query)
666
- searchResults.children ?: emptyArray()
712
+ // Rejects with the error the browse surface would raise, but leaves the navigation error
713
+ // itself unset: a search is not a navigation, and its failure must not replace the
714
+ // displayed page's state (mirrors HybridAudioBrowser.search).
715
+ val searchResults =
716
+ try {
717
+ browserManager.search(query)
718
+ } catch (e: CancellationException) {
719
+ throw e
720
+ } catch (e: Exception) {
721
+ Timber.e(e, "Search failed for query: $query")
722
+ throw NavigationErrorException(navigationErrorFor(e))
723
+ }
724
+ // Flattened, not `children`: a resolved page carries its rows in
725
+ // `sections` (ADR 0010) and search builds exactly one untitled section,
726
+ // so `children` is always null here and this returned nothing at all.
727
+ searchResults.flattenedChildren?.toTypedArray() ?: emptyArray()
667
728
  }
668
729
  }
669
730
 
@@ -677,8 +738,9 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
677
738
  // Invalidate cached content so future navigations fetch fresh data
678
739
  browserManager.invalidateContentCache(path)
679
740
 
680
- // Notify external media controllers (Android Auto)
681
- connectedService?.player?.notifyContentChanged(path)
741
+ // Notify external media controllers (Android Auto). Reaches MediaSession,
742
+ // which asserts the main thread; this override runs on the JS thread.
743
+ launchInScope { connectedService?.player?.notifyContentChanged(path) }
682
744
 
683
745
  // If we're currently viewing this path, refresh the content
684
746
  if (browserManager.getPath() == path) {
@@ -692,8 +754,10 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
692
754
  // Clear all cached content so every path re-fetches fresh data
693
755
  browserManager.clearContentCache()
694
756
 
695
- // Notify external media controllers (Android Auto) to refresh subscribed paths
696
- connectedService?.player?.invalidateAllContent()
757
+ // Notify external media controllers (Android Auto) to refresh subscribed paths.
758
+ // Reaches MediaSession, which asserts the main thread; this override runs on
759
+ // the JS thread.
760
+ launchInScope { connectedService?.player?.invalidateAllContent() }
697
761
 
698
762
  // Refresh whatever the browser is currently viewing
699
763
  mainScope.launch { browserManager.refresh() }
@@ -734,14 +798,16 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
734
798
  // Re-query every subscribed parent so a connected controller (Android
735
799
  // Auto) swaps its lists for the gate tile without reconnecting. Notify
736
800
  // only — the content cache stays warm for when the gate clears.
737
- connectedService?.player?.invalidateAllContent()
801
+ // MediaSession asserts the main thread; this override runs on the JS thread.
802
+ launchInScope { connectedService?.player?.invalidateAllContent() }
738
803
  }
739
804
 
740
805
  override fun clearGate() {
741
806
  if (!gateState.active) return
742
807
  // Single atomic assignment — readers never see a half-cleared triple.
743
808
  gateState = GateState(active = false, chrome = null, hasResolver = false)
744
- connectedService?.player?.invalidateAllContent()
809
+ // MediaSession asserts the main thread; this override runs on the JS thread.
810
+ launchInScope { connectedService?.player?.invalidateAllContent() }
745
811
  }
746
812
 
747
813
  /**
@@ -781,7 +847,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
781
847
  override var onCarConnectedChanged: (Boolean) -> Unit = {}
782
848
  set(value) {
783
849
  field = value
784
- carConnectionTarget = value
850
+ carConnectionTarget.set(value)
785
851
  // Immediately notify current state (the car may have connected before
786
852
  // this JS runtime subscribed) — mirrors the iOS behavior.
787
853
  value(carConnected)
@@ -811,12 +877,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
811
877
  // Service not connected yet, bind to service
812
878
  suspendCoroutine<Unit> { continuation ->
813
879
  Timber.d("Binding to AudioBrowserService")
814
- val bound =
815
- context.bindService(
816
- Intent(context, Service::class.java),
817
- this@AudioBrowser,
818
- Context.BIND_AUTO_CREATE,
819
- )
880
+ val bound = serviceBinding.bind(Intent(context, Service::class.java), this@AudioBrowser)
820
881
 
821
882
  if (!bound) {
822
883
  continuation.resumeWithException(
@@ -832,8 +893,17 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
832
893
 
833
894
  override fun updateOptions(options: NativeUpdateOptions) {
834
895
  updateOptions.updateFromBridge(options)
835
- // Only update the options if the service is around
836
- connectedService?.let { player.applyOptions(updateOptions) }
896
+ // Only update the options if the service is around.
897
+ // applyOptions touches the Media3 player, which asserts it is only ever
898
+ // driven from the main thread ("Player callback method is called from a
899
+ // wrong thread"). Nitro invokes this from the JS thread, so hop.
900
+ //
901
+ // The hop blocks rather than launching because `updateOptions` is a single
902
+ // mutable holder: updateFromBridge mutates it in place on the JS thread and
903
+ // applyOptions reads it on main. Blocking is what keeps the two ordered.
904
+ connectedService?.let { service ->
905
+ runBlockingOnMain { service.player.applyOptions(updateOptions) }
906
+ }
837
907
  }
838
908
 
839
909
  override fun getOptions(): Options {
@@ -847,25 +917,27 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
847
917
  // ============================================================================
848
918
 
849
919
  override fun load(track: Track) {
920
+ // Throws without a player, as on iOS: a track asked for and silently dropped hides a mistake.
921
+ val player = player
850
922
  launchInScope { player.load(track) }
851
923
  }
852
924
 
853
- override fun reset() = runBlockingOnMain {
854
- player.stop()
855
- delay(300) // Allow playback to stop
856
- player.clear()
857
- // clear() while IDLE emits no engine event, so the state machine never
858
- // leaves STOPPED — a reset queue is "nothing loaded".
859
- player.setPlaybackState(PlaybackState.NONE)
860
- }
925
+ override fun reset() =
926
+ commandPlayer("reset") { player ->
927
+ player.stop()
928
+ player.clear()
929
+ // clear() while IDLE emits no engine event, so the state machine never
930
+ // leaves STOPPED — a reset queue is "nothing loaded".
931
+ player.setPlaybackState(PlaybackState.NONE)
932
+ }
861
933
 
862
- override fun play() = runBlockingOnMain { player.play() }
934
+ override fun play() = commandPlayer("play") { it.play() }
863
935
 
864
- override fun pause() = runBlockingOnMain { player.pause() }
936
+ override fun pause() = commandPlayer("pause") { it.pause() }
865
937
 
866
- override fun togglePlayback() = runBlockingOnMain { player.togglePlayback() }
938
+ override fun togglePlayback() = commandPlayer("togglePlayback") { it.togglePlayback() }
867
939
 
868
- override fun stop() = runBlockingOnMain { player.stop() }
940
+ override fun stop() = commandPlayer("stop") { it.stop() }
869
941
 
870
942
  override fun setPlayWhenReady(playWhenReady: Boolean) = runBlockingOnMain {
871
943
  // Pre-setup this stages the intent for the player to come up with — never throws.
@@ -877,35 +949,41 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
877
949
  connectedService?.player?.playWhenReady ?: pendingPlayWhenReady ?: false
878
950
  }
879
951
 
880
- override fun seekTo(position: Double) = runBlockingOnMain {
881
- player.seekTo((position * 1000).toLong(), TimeUnit.MILLISECONDS)
882
- }
952
+ override fun seekTo(position: Double) =
953
+ commandPlayer("seekTo") { player ->
954
+ player.seekTo((position * 1000).toLong(), TimeUnit.MILLISECONDS)
955
+ }
883
956
 
884
- override fun seekBy(offset: Double) = runBlockingOnMain {
885
- player.seekBy((offset * 1000).toLong(), TimeUnit.MILLISECONDS)
886
- }
957
+ override fun seekBy(offset: Double) =
958
+ commandPlayer("seekBy") { player ->
959
+ player.seekBy((offset * 1000).toLong(), TimeUnit.MILLISECONDS)
960
+ }
887
961
 
888
- override fun seekToLiveEdge() = runBlockingOnMain { player.seekToLiveEdge() }
962
+ override fun seekToLiveEdge() = commandPlayer("seekToLiveEdge") { it.seekToLiveEdge() }
889
963
 
890
- override fun setVolume(level: Double) = runBlockingOnMain { player.volume = level.toFloat() }
964
+ override fun setVolume(level: Double) = commandPlayer("setVolume") { it.volume = level.toFloat() }
891
965
 
892
- override fun getVolume(): Double = runBlockingOnMain { player.volume.toDouble() }
966
+ override fun getVolume(): Double = readPlayer(1.0) { it.volume.toDouble() }
893
967
 
894
- override fun setRate(rate: Double) = runBlockingOnMain { player.playbackSpeed = rate.toFloat() }
968
+ override fun setRate(rate: Double) =
969
+ commandPlayer("setRate") { it.playbackSpeed = rate.toFloat() }
895
970
 
896
- override fun getRate(): Double = runBlockingOnMain { player.playbackSpeed.toDouble() }
971
+ override fun getRate(): Double = readPlayer(1.0) { it.playbackSpeed.toDouble() }
897
972
 
898
- override fun getProgress(): Progress = runBlockingOnMain {
899
- Progress(
900
- duration = player.duration.toSeconds(),
901
- position = player.position.toSeconds(),
902
- buffered = player.bufferedPosition.toSeconds(),
903
- )
904
- }
973
+ override fun getProgress(): Progress =
974
+ readPlayer(Progress(duration = 0.0, position = 0.0, buffered = 0.0)) { player ->
975
+ Progress(
976
+ duration = player.duration.toSeconds(),
977
+ position = player.position.toSeconds(),
978
+ buffered = player.bufferedPosition.toSeconds(),
979
+ )
980
+ }
905
981
 
906
- override fun getPlayback(): Playback = runBlockingOnMain { player.getPlayback() }
982
+ override fun getPlayback(): Playback =
983
+ readPlayer(Playback(PlaybackState.NONE, null)) { it.getPlayback() }
907
984
 
908
- override fun getPlayingState(): PlayingState = runBlockingOnMain { player.getPlayingState() }
985
+ override fun getPlayingState(): PlayingState =
986
+ readPlayer(PlayingState(playing = false, buffering = false)) { it.getPlayingState() }
909
987
 
910
988
  override fun getRepeatMode(): RepeatMode = runBlockingOnMain {
911
989
  connectedService?.player?.repeatMode ?: pendingRepeatMode ?: RepeatMode.OFF
@@ -916,118 +994,121 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
916
994
  connectedService?.player?.let { it.repeatMode = mode } ?: run { pendingRepeatMode = mode }
917
995
  }
918
996
 
919
- override fun getShuffleEnabled(): Boolean = runBlockingOnMain { player.shuffleMode }
997
+ override fun getShuffleEnabled(): Boolean = readPlayer(false) { it.shuffleMode }
920
998
 
921
- override fun setShuffleEnabled(enabled: Boolean) = runBlockingOnMain {
922
- player.shuffleMode = enabled
923
- }
999
+ override fun setShuffleEnabled(enabled: Boolean) =
1000
+ commandPlayer("setShuffleEnabled") { player -> player.shuffleMode = enabled }
924
1001
 
925
- override fun setPlaybackIntervalEnabled(enabled: Boolean) = runBlockingOnMain {
926
- player.setPlaybackIntervalEnabled(enabled)
927
- }
1002
+ override fun setPlaybackIntervalEnabled(enabled: Boolean) =
1003
+ commandPlayer("setPlaybackIntervalEnabled") { player ->
1004
+ player.setPlaybackIntervalEnabled(enabled)
1005
+ }
928
1006
 
929
- override fun getPlaybackError(): PlaybackError? = runBlockingOnMain { player.playbackError }
1007
+ override fun getPlaybackError(): PlaybackError? = readPlayer(null) { it.playbackError }
930
1008
 
931
- override fun retry() = runBlockingOnMain { player.prepare() }
1009
+ override fun retry() = commandPlayer("retry") { it.prepare() }
932
1010
 
933
1011
  // ============================================================================
934
1012
  // MARK: Queue Management
935
1013
  // ============================================================================
936
1014
 
937
- override fun add(tracks: Array<Track>, insertBeforeIndex: Double?) = runBlockingOnMain {
938
- val inputIndex = insertBeforeIndex?.toInt() ?: -1
939
- player.add(tracks, inputIndex)
940
- }
1015
+ override fun add(tracks: Array<Track>, insertBeforeIndex: Double?) =
1016
+ commandPlayer("add") { player ->
1017
+ val inputIndex = insertBeforeIndex?.toInt() ?: -1
1018
+ player.add(tracks, inputIndex)
1019
+ }
941
1020
 
942
- override fun move(fromIndex: Double, toIndex: Double) = runBlockingOnMain {
943
- player.move(fromIndex.toInt(), toIndex.toInt())
944
- }
1021
+ override fun move(fromIndex: Double, toIndex: Double) =
1022
+ commandPlayer("move") { player -> player.move(fromIndex.toInt(), toIndex.toInt()) }
945
1023
 
946
- override fun remove(indexes: DoubleArray) = runBlockingOnMain {
947
- val indexList = indexes.map { it.toInt() }
948
- player.remove(indexList)
949
- }
1024
+ override fun remove(indexes: DoubleArray) =
1025
+ commandPlayer("remove") { player ->
1026
+ val indexList = indexes.map { it.toInt() }
1027
+ player.remove(indexList)
1028
+ }
950
1029
 
951
- override fun removeUpcomingTracks() = runBlockingOnMain { player.removeUpcomingTracks() }
1030
+ override fun removeUpcomingTracks() =
1031
+ commandPlayer("removeUpcomingTracks") { it.removeUpcomingTracks() }
952
1032
 
953
- override fun skip(index: Double, initialPosition: Double?) = runBlockingOnMain {
954
- player.skipTo(index.toInt())
1033
+ override fun skip(index: Double, initialPosition: Double?) =
1034
+ commandPlayer("skip") { player ->
1035
+ player.skipTo(index.toInt())
955
1036
 
956
- if (initialPosition != null && initialPosition >= 0) {
957
- player.seekTo((initialPosition * 1000).toLong(), TimeUnit.MILLISECONDS)
1037
+ if (initialPosition != null && initialPosition >= 0) {
1038
+ player.seekTo((initialPosition * 1000).toLong(), TimeUnit.MILLISECONDS)
1039
+ }
958
1040
  }
959
- }
960
1041
 
961
- override fun skipToNext(initialPosition: Double?) = runBlockingOnMain {
962
- player.next()
1042
+ override fun skipToNext(initialPosition: Double?) =
1043
+ commandPlayer("skipToNext") { player ->
1044
+ player.next()
963
1045
 
964
- if (initialPosition != null && initialPosition >= 0) {
965
- player.seekTo((initialPosition * 1000).toLong(), TimeUnit.MILLISECONDS)
1046
+ if (initialPosition != null && initialPosition >= 0) {
1047
+ player.seekTo((initialPosition * 1000).toLong(), TimeUnit.MILLISECONDS)
1048
+ }
966
1049
  }
967
- }
968
1050
 
969
- override fun skipToPrevious(initialPosition: Double?) = runBlockingOnMain {
970
- player.previous()
1051
+ override fun skipToPrevious(initialPosition: Double?) =
1052
+ commandPlayer("skipToPrevious") { player ->
1053
+ player.previous()
971
1054
 
972
- if (initialPosition != null && initialPosition >= 0) {
973
- player.seekTo((initialPosition * 1000).toLong(), TimeUnit.MILLISECONDS)
1055
+ if (initialPosition != null && initialPosition >= 0) {
1056
+ player.seekTo((initialPosition * 1000).toLong(), TimeUnit.MILLISECONDS)
1057
+ }
974
1058
  }
975
- }
976
1059
 
977
1060
  override fun setQueue(tracks: Array<Track>, startIndex: Double?, startPosition: Double?) =
978
- runBlockingOnMain {
1061
+ commandPlayer("setQueue") { player ->
979
1062
  player.setQueue(tracks, startIndex?.toInt() ?: 0, ((startPosition ?: 0.0) * 1000).toLong())
980
1063
  }
981
1064
 
982
- override fun setActiveTrackFavorited(favorited: Boolean) = runBlockingOnMain {
983
- player.setActiveTrackFavorited(favorited)
984
- }
1065
+ override fun setActiveTrackFavorited(favorited: Boolean): Unit =
1066
+ commandPlayer("setActiveTrackFavorited") { player ->
1067
+ // Applied-result is for MediaSession honesty; the JS API stays fire-and-forget.
1068
+ player.setActiveTrackFavorited(favorited)
1069
+ }
985
1070
 
986
- override fun toggleActiveTrackFavorited() = runBlockingOnMain {
987
- player.toggleActiveTrackFavorited()
988
- }
1071
+ override fun toggleActiveTrackFavorited(): Unit =
1072
+ commandPlayer("toggleActiveTrackFavorited") { it.toggleActiveTrackFavorited() }
989
1073
 
990
- override fun getQueue(): Array<Track> = runBlockingOnMain { player.tracks }
1074
+ override fun getQueue(): Array<Track> = readPlayer(emptyArray()) { it.tracks }
991
1075
 
992
- override fun getTrack(index: Double): Track? = runBlockingOnMain {
993
- try {
994
- player.getTrack(index.toInt())
995
- } catch (e: IllegalArgumentException) {
996
- null
1076
+ override fun getTrack(index: Double): Track? =
1077
+ readPlayer(null) { player ->
1078
+ try {
1079
+ player.getTrack(index.toInt())
1080
+ } catch (e: IllegalArgumentException) {
1081
+ null
1082
+ }
997
1083
  }
998
- }
999
1084
 
1000
- override fun getActiveTrackIndex(): Double? = runBlockingOnMain {
1001
- player.currentIndex?.toDouble()
1002
- }
1085
+ override fun getActiveTrackIndex(): Double? = readPlayer(null) { it.currentIndex?.toDouble() }
1003
1086
 
1004
- override fun getActiveTrack(): Track? = runBlockingOnMain { player.currentTrack }
1087
+ override fun getActiveTrack(): Track? = readPlayer(null) { it.currentTrack }
1005
1088
 
1006
1089
  // ============================================================================
1007
1090
  // MARK: Now Playing Metadata
1008
1091
  // ============================================================================
1009
1092
 
1010
- override fun updateNowPlaying(update: NowPlayingUpdate?) = runBlockingOnMain {
1011
- player.nowPlaying.updateNowPlaying(update)
1012
- }
1093
+ override fun updateNowPlaying(update: NowPlayingUpdate?) =
1094
+ commandPlayer("updateNowPlaying") { player -> player.nowPlaying.updateNowPlaying(update) }
1013
1095
 
1014
- override fun flashNowPlaying(update: NowPlayingUpdate, durationMs: Double) = runBlockingOnMain {
1015
- player.nowPlaying.flashNowPlaying(update, durationMs)
1016
- }
1096
+ override fun flashNowPlaying(update: NowPlayingUpdate, durationMs: Double) =
1097
+ commandPlayer("flashNowPlaying") { player ->
1098
+ player.nowPlaying.flashNowPlaying(update, durationMs)
1099
+ }
1017
1100
 
1018
- override fun clearNowPlayingFlash() = runBlockingOnMain {
1019
- player.nowPlaying.clearNowPlayingFlash()
1020
- }
1101
+ override fun clearNowPlayingFlash() =
1102
+ commandPlayer("clearNowPlayingFlash") { player -> player.nowPlaying.clearNowPlayingFlash() }
1021
1103
 
1022
- override fun getNowPlaying(): NowPlayingMetadata? = runBlockingOnMain {
1023
- player.nowPlaying.getNowPlaying()
1024
- }
1104
+ override fun getNowPlaying(): NowPlayingMetadata? =
1105
+ readPlayer(null) { player -> player.nowPlaying.getNowPlaying() }
1025
1106
 
1026
1107
  // ============================================================================
1027
1108
  // MARK: Network Connectivity
1028
1109
  // ============================================================================
1029
1110
 
1030
- override fun getOnline(): Boolean = runBlockingOnMain { player.getOnline() }
1111
+ override fun getOnline(): Boolean = readPlayer(true) { it.getOnline() }
1031
1112
 
1032
1113
  // ============================================================================
1033
1114
  // MARK: System Volume
@@ -1053,35 +1134,32 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
1053
1134
  // MARK: Equalizer (Android only)
1054
1135
  // ============================================================================
1055
1136
 
1056
- override fun getEqualizerSettings(): EqualizerSettings? = runBlockingOnMain {
1057
- player.equalizer.getSettings()
1058
- }
1137
+ override fun getEqualizerSettings(): EqualizerSettings? =
1138
+ readPlayer(null) { player -> player.equalizer.getSettings() }
1059
1139
 
1060
- override fun setEqualizerEnabled(enabled: Boolean) = runBlockingOnMain {
1061
- player.equalizer.setEnabled(enabled)
1062
- }
1140
+ override fun setEqualizerEnabled(enabled: Boolean) =
1141
+ commandPlayer("setEqualizerEnabled") { player -> player.equalizer.setEnabled(enabled) }
1063
1142
 
1064
- override fun setEqualizerPreset(preset: String) = runBlockingOnMain {
1065
- player.equalizer.setPreset(preset)
1066
- }
1143
+ override fun setEqualizerPreset(preset: String) =
1144
+ commandPlayer("setEqualizerPreset") { player -> player.equalizer.setPreset(preset) }
1067
1145
 
1068
- override fun setEqualizerLevels(levels: DoubleArray) = runBlockingOnMain {
1069
- player.equalizer.setLevels(levels)
1070
- }
1146
+ override fun setEqualizerLevels(levels: DoubleArray) =
1147
+ commandPlayer("setEqualizerLevels") { player -> player.equalizer.setLevels(levels) }
1071
1148
 
1072
1149
  // ============================================================================
1073
1150
  // MARK: Sleep Timer
1074
1151
  // ============================================================================
1075
1152
 
1076
- override fun getSleepTimer(): SleepTimer = runBlockingOnMain { player.sleepTimer.get() }
1153
+ override fun getSleepTimer(): SleepTimer =
1154
+ readPlayer(SleepTimer.create(NullType.NULL)) { it.sleepTimer.get() }
1077
1155
 
1078
- override fun setSleepTimer(seconds: Double, fadeDuration: Double?) = runBlockingOnMain {
1079
- player.sleepTimer.setAfter(seconds, fadeDuration)
1080
- }
1156
+ override fun setSleepTimer(seconds: Double, fadeDuration: Double?) =
1157
+ commandPlayer("setSleepTimer") { player -> player.sleepTimer.setAfter(seconds, fadeDuration) }
1081
1158
 
1082
- override fun setSleepTimerToEndOfTrack() = runBlockingOnMain { player.sleepTimer.setEndOfTrack() }
1159
+ override fun setSleepTimerToEndOfTrack() =
1160
+ commandPlayer("setSleepTimerToEndOfTrack") { it.sleepTimer.setEndOfTrack() }
1083
1161
 
1084
- override fun clearSleepTimer(): Boolean = runBlockingOnMain { player.sleepTimer.clear() }
1162
+ override fun clearSleepTimer(): Boolean = readPlayer(false) { it.sleepTimer.clear() }
1085
1163
 
1086
1164
  // ============================================================================
1087
1165
  // MARK: Battery Optimization (Android only)
@@ -1175,13 +1253,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
1175
1253
  }
1176
1254
 
1177
1255
  // Wire up battery warning callback from service
1178
- connectedService?.onBatteryWarningPendingChanged = { pending ->
1179
- post {
1180
- this@AudioBrowser.onBatteryWarningPendingChanged(
1181
- BatteryWarningPendingChangedEvent(pending)
1182
- )
1183
- }
1184
- }
1256
+ connectedService?.onBatteryWarningPendingChanged = onBatteryWarningPending
1185
1257
 
1186
1258
  val sessionToken = SessionToken(context, ComponentName(context, Service::class.java))
1187
1259
  mediaBrowserFuture = MediaBrowser.Builder(context, sessionToken).buildAsync()
@@ -1228,6 +1300,29 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
1228
1300
  private val service: Service
1229
1301
  get() = connectedService ?: throw Exception("Player not initialized")
1230
1302
 
1303
+ /**
1304
+ * The player while its service is connected: not before setupPlayer, nor while the service
1305
+ * restarts.
1306
+ */
1307
+ private val connectedPlayer: Player?
1308
+ get() = connectedService?.player
1309
+
1310
+ /** What [read] gets from the player, or without one [fallback], the answer iOS gives then. */
1311
+ private fun <T> readPlayer(fallback: T, read: suspend (Player) -> T): T = runBlockingOnMain {
1312
+ val player = connectedPlayer ?: return@runBlockingOnMain fallback
1313
+ read(player)
1314
+ }
1315
+
1316
+ /** Runs [command] on the player; without one it is dropped with a warning, as iOS drops it. */
1317
+ private fun commandPlayer(name: String, command: suspend (Player) -> Unit) = runBlockingOnMain {
1318
+ val player = connectedPlayer
1319
+ if (player == null) {
1320
+ Timber.w("$name before the player is set up, or while its service restarts: ignored")
1321
+ } else {
1322
+ command(player)
1323
+ }
1324
+ }
1325
+
1231
1326
  internal val player
1232
1327
  get() = service.player
1233
1328
 
@@ -1436,9 +1531,27 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
1436
1531
  // callback registers as the single notification target (mirrors the iOS
1437
1532
  // static + shared-instance pattern).
1438
1533
  @Volatile private var carConnected = false
1439
- @Volatile private var carConnectionTarget: ((Boolean) -> Unit)? = null
1534
+ // Compare-and-set, not @Volatile: a disposing instance clears the slot only if it still owns
1535
+ // it, and a replacement may be claiming it at the same moment.
1536
+ private val carConnectionTarget = AtomicReference<((Boolean) -> Unit)?>(null)
1440
1537
  private var carConnectionObserverStarted = false
1441
1538
 
1539
+ /**
1540
+ * The instance the current JS runtime is using. Nitro hands out no registry, and the teardown
1541
+ * callback has nothing to dispose without one.
1542
+ */
1543
+ private val current = AtomicReference<AudioBrowser?>(null)
1544
+
1545
+ /** Called by [AudioBrowserLifecycleModule] when the React instance is torn down. */
1546
+ internal fun disposeCurrent() {
1547
+ current.getAndSet(null)?.dispose()
1548
+ }
1549
+
1550
+ /** Shared by the [resolveGate] initializer and [dispose], which reverts to it. */
1551
+ private val defaultResolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = {
1552
+ Promise.resolved(Promise.resolved(GateDecision(gated = true, gate = null)))
1553
+ }
1554
+
1442
1555
  private fun startCarConnectionObserver(context: Context, handler: Handler) {
1443
1556
  // Posted to main: LiveData observation is main-thread only, and the
1444
1557
  // single-start guard is then only ever touched from one thread.
@@ -1449,9 +1562,159 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
1449
1562
  val connected = type != CarConnection.CONNECTION_TYPE_NOT_CONNECTED
1450
1563
  if (connected == carConnected) return@observeForever
1451
1564
  carConnected = connected
1452
- carConnectionTarget?.invoke(connected)
1565
+ carConnectionTarget.get()?.invoke(connected)
1453
1566
  }
1454
1567
  }
1455
1568
  }
1456
1569
  }
1457
1570
  }
1571
+
1572
+ // MARK: Deep JS-callback stripping (used by dispose)
1573
+ // Functions live at every level of the config, not just the top; each type's strip nils its own.
1574
+
1575
+ private fun NativeBrowserConfiguration.strippingJSCallbacks() =
1576
+ copy(
1577
+ requestResolver = null,
1578
+ browseResolver = null,
1579
+ handleTrackLoad = null,
1580
+ viewMoreTitle = null,
1581
+ resolveAlbumPath = null,
1582
+ formatNavigationError = null,
1583
+ request = request?.strippingJSCallbacks(),
1584
+ browse = browse?.strippingJSCallbacks(),
1585
+ media = media?.strippingJSCallbacks(),
1586
+ artwork = artwork?.strippingJSCallbacks(),
1587
+ nowPlayingArtwork = nowPlayingArtwork?.strippingJSCallbacks(),
1588
+ routes = routes?.map { it.strippingJSCallbacks() }?.toTypedArray(),
1589
+ )
1590
+
1591
+ private fun TransformableRequestConfig.strippingJSCallbacks() =
1592
+ copy(transform = null, transformSync = null)
1593
+
1594
+ private fun MediaRequestConfig.strippingJSCallbacks() =
1595
+ copy(resolve = null, resolveSync = null, transform = null, transformSync = null)
1596
+
1597
+ private fun ArtworkRequestConfig.strippingJSCallbacks() =
1598
+ copy(resolve = null, resolveSync = null, transform = null, transformSync = null)
1599
+
1600
+ private fun NativeRouteEntry.strippingJSCallbacks() =
1601
+ copy(
1602
+ browseCallback = null,
1603
+ browseConfig = browseConfig?.strippingJSCallbacks(),
1604
+ searchCallback = null,
1605
+ searchConfig = searchConfig?.strippingJSCallbacks(),
1606
+ media = media?.strippingJSCallbacks(),
1607
+ artwork = artwork?.strippingJSCallbacks(),
1608
+ )
1609
+
1610
+ /**
1611
+ * Ceiling on the app's `formatNavigationError` hop. Same budget [Player.awaitBrowser] gives the JS
1612
+ * side, the only other JS wait a browse serve already blocks on.
1613
+ */
1614
+ private val FORMATTER_TIMEOUT = 10.seconds
1615
+
1616
+ /**
1617
+ * The navigation error an exception raises. Shared by [AudioBrowser]'s failed navigations, the
1618
+ * rejection [AudioBrowser.search] hands JS, and the Android Auto browse-error tile, so one failure
1619
+ * is named one way wherever it surfaces.
1620
+ */
1621
+ internal fun navigationErrorFor(e: Exception): NavigationError =
1622
+ when (e) {
1623
+ is HttpStatusException ->
1624
+ NavigationError(
1625
+ NavigationErrorType.HTTP_ERROR,
1626
+ e.message ?: "Server error",
1627
+ e.statusCode.toDouble(),
1628
+ e.statusCode in 200..299,
1629
+ )
1630
+ is NetworkException ->
1631
+ NavigationError(
1632
+ NavigationErrorType.NETWORK_ERROR,
1633
+ e.message ?: "Network request failed",
1634
+ null,
1635
+ null,
1636
+ )
1637
+ is ContentNotFoundException ->
1638
+ NavigationError(
1639
+ NavigationErrorType.CONTENT_NOT_FOUND,
1640
+ e.message ?: "Content not found",
1641
+ null,
1642
+ null,
1643
+ )
1644
+ is CallbackException ->
1645
+ NavigationError(
1646
+ NavigationErrorType.CALLBACK_ERROR,
1647
+ e.message ?: "An error occurred",
1648
+ null,
1649
+ null,
1650
+ )
1651
+ else ->
1652
+ NavigationError(
1653
+ NavigationErrorType.UNKNOWN_ERROR,
1654
+ e.message ?: "An unexpected error occurred",
1655
+ null,
1656
+ null,
1657
+ )
1658
+ }
1659
+
1660
+ /** Carries a [NavigationError] across a rejected Promise, whose JS error keeps its message. */
1661
+ class NavigationErrorException(val error: NavigationError) : Exception(error.message)
1662
+
1663
+ /**
1664
+ * The built-in display copy for a navigation error, and the fallback when the app's is unusable.
1665
+ */
1666
+ internal fun defaultFormattedError(error: NavigationError): FormattedNavigationError {
1667
+ val title =
1668
+ when (error.code) {
1669
+ NavigationErrorType.CONTENT_NOT_FOUND -> "Content Not Found"
1670
+ NavigationErrorType.NETWORK_ERROR -> "Network Error"
1671
+ NavigationErrorType.HTTP_ERROR -> {
1672
+ // Use system-localized HTTP status text (e.g., "Not Found", "Service Unavailable")
1673
+ error.statusCode?.let { httpStatusText(it.toInt()) } ?: "Server Error"
1674
+ }
1675
+ NavigationErrorType.CALLBACK_ERROR -> "Error"
1676
+ NavigationErrorType.UNKNOWN_ERROR -> "Error"
1677
+ // Not a failure — a container that resolved with no children. Neutral copy. See ADR 0001.
1678
+ NavigationErrorType.EMPTY_CONTENT -> "Nothing here"
1679
+ NavigationErrorType.TIMEOUT -> "Couldn't load"
1680
+ }
1681
+ // Omit an empty message so it renders as title-only (e.g. the empty-content case).
1682
+ return FormattedNavigationError(title, error.message.takeIf { it.isNotEmpty() })
1683
+ }
1684
+
1685
+ /**
1686
+ * The formatter hop's fallback rule: the app's answer when it produces one, [default] when it
1687
+ * answers null, throws, or outruns [FORMATTER_TIMEOUT]. Kept off the JS edge so the rule is
1688
+ * exercisable without a Nitro Promise.
1689
+ *
1690
+ * The hop runs in [scope] and is awaited through its [kotlinx.coroutines.Deferred], because Nitro's
1691
+ * `Promise.await` parks in a plain `suspendCoroutine`: cancelling it does not unpark it. Awaiting
1692
+ * the Deferred is cancellable, so the timeout leaves the orphan to finish unobserved instead of
1693
+ * holding the caller open until JS answers — which for a browse serve would be forever.
1694
+ */
1695
+ internal suspend fun formattedOrDefault(
1696
+ scope: CoroutineScope,
1697
+ default: FormattedNavigationError,
1698
+ format: suspend () -> FormattedNavigationError?,
1699
+ ): FormattedNavigationError {
1700
+ val hop = scope.async { runCatching { format() }.getOrNull() }
1701
+ return withTimeoutOrNull(FORMATTER_TIMEOUT) { hop.await() } ?: default
1702
+ }
1703
+
1704
+ /** Returns localized HTTP status text for the given status code */
1705
+ private fun httpStatusText(statusCode: Int): String {
1706
+ return when (statusCode) {
1707
+ 400 -> "Bad Request"
1708
+ 401 -> "Unauthorized"
1709
+ 403 -> "Forbidden"
1710
+ 404 -> "Not Found"
1711
+ 405 -> "Method Not Allowed"
1712
+ 408 -> "Request Timeout"
1713
+ 429 -> "Too Many Requests"
1714
+ 500 -> "Internal Server Error"
1715
+ 502 -> "Bad Gateway"
1716
+ 503 -> "Service Unavailable"
1717
+ 504 -> "Gateway Timeout"
1718
+ else -> "Server Error"
1719
+ }
1720
+ }