react-native-audio-browser 0.2.0-next.2 → 0.2.0-next.21

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 (660) hide show
  1. package/README.md +35 -21
  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 +389 -164
  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 +48 -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 +24 -5
  11. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -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 +56 -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 +108 -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 +87 -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 +38 -0
  51. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -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 +147 -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 +154 -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 +41 -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/Browser/BrowserConfig.swift +102 -5
  76. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  77. package/ios/Browser/BrowserManager.swift +230 -205
  78. package/ios/Browser/BrowserPathHelper.swift +44 -44
  79. package/ios/Browser/InertStyleDiagnostic.swift +279 -0
  80. package/ios/Browser/JsonModels.swift +264 -106
  81. package/ios/Browser/SearchDrillIn.swift +7 -7
  82. package/ios/Browser/SectionPresentation.swift +64 -0
  83. package/ios/Browser/SectionScope.swift +58 -24
  84. package/ios/Browser/StyleResolver.swift +58 -0
  85. package/ios/Browser/TabBarEntries.swift +9 -3
  86. package/ios/Browser/TrackIdTemplate.swift +37 -0
  87. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  88. package/ios/CarPlay/CarPlayController.swift +227 -85
  89. package/ios/CarPlay/CarPlayImageLoader.swift +42 -20
  90. package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
  91. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  92. package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
  93. package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
  94. package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
  95. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  96. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  97. package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
  98. package/ios/Extension/Section+Copying.swift +29 -0
  99. package/ios/Extension/String+IcyCharset.swift +21 -0
  100. package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
  101. package/ios/Extension/Track+Copying.swift +8 -12
  102. package/ios/Extension/Track+Identity.swift +18 -0
  103. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  104. package/ios/Http/HttpClient.swift +8 -2
  105. package/ios/HybridAudioBrowser.swift +253 -100
  106. package/ios/Model/NitroTypeStubs.swift +125 -23
  107. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  108. package/ios/Model/RemoteCommand.swift +18 -0
  109. package/ios/Model/TrackPlayerError.swift +2 -2
  110. package/ios/Option/SessionCategories.swift +3 -2
  111. package/ios/Player/MediaIntentCriteria.swift +17 -9
  112. package/ios/Player/MediaLoader.swift +33 -9
  113. package/ios/Player/NowPlayingUpdater.swift +54 -6
  114. package/ios/Player/PlaybackCoordinator.swift +13 -6
  115. package/ios/Player/PlaybackErrorHandler.swift +4 -4
  116. package/ios/Player/PlaybackTimer.swift +8 -2
  117. package/ios/Player/QueueManager.swift +46 -13
  118. package/ios/Player/RetryManager.swift +80 -18
  119. package/ios/Player/SeekCompletionHandler.swift +3 -1
  120. package/ios/Player/ShuffleOrder.swift +33 -0
  121. package/ios/Player/SleepTimerHandling.swift +13 -0
  122. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
  123. package/ios/RemoteCommand/RemoteCommandController.swift +62 -24
  124. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  125. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  126. package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
  127. package/ios/Tests/JsonModelsTests.swift +150 -1
  128. package/ios/Tests/LRUCacheTests.swift +111 -0
  129. package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
  130. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  131. package/ios/Tests/PlaybackCoordinatorTests.swift +1 -1
  132. package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
  133. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  134. package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
  135. package/ios/Tests/QueueManagerTests.swift +122 -10
  136. package/ios/Tests/RemoteCommandControllerTests.swift +172 -0
  137. package/ios/Tests/RemoteCommandTests.swift +67 -0
  138. package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
  139. package/ios/Tests/RetryManagerTests.swift +89 -0
  140. package/ios/Tests/SearchDrillInTests.swift +16 -16
  141. package/ios/Tests/SectionPresentationTests.swift +90 -0
  142. package/ios/Tests/SectionScopeTests.swift +87 -55
  143. package/ios/Tests/StyleResolverTests.swift +127 -0
  144. package/ios/Tests/Support/TestTypes.swift +8 -1
  145. package/ios/Tests/TabBarEntriesTests.swift +19 -19
  146. package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
  147. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  148. package/ios/Tests/TrackIdentityTests.swift +20 -0
  149. package/ios/Tests/TrackSelectorTests.swift +168 -33
  150. package/ios/TrackPlayer.swift +63 -35
  151. package/ios/TrackPlayerCallbacks.swift +5 -38
  152. package/ios/TrackSelector.swift +45 -30
  153. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  154. package/ios/Util/LRUCache.swift +4 -1
  155. package/ios/Util/NetworkMonitor.swift +43 -26
  156. package/ios/Util/SFSymbolRenderer.swift +24 -2
  157. package/ios/Util/SVGProcessor.swift +9 -2
  158. package/lib/commonjs/AudioBrowser.js +8 -8
  159. package/lib/commonjs/AudioBrowser.js.map +1 -1
  160. package/lib/commonjs/CLAUDE.md +52 -0
  161. package/lib/commonjs/features/browser-config.js +12 -8
  162. package/lib/commonjs/features/browser-config.js.map +1 -1
  163. package/lib/commonjs/features/browser.js +11 -3
  164. package/lib/commonjs/features/browser.js.map +1 -1
  165. package/lib/commonjs/features/carConnection.js +1 -1
  166. package/lib/commonjs/features/equalizer.js +1 -1
  167. package/lib/commonjs/features/errors.js +3 -3
  168. package/lib/commonjs/features/favorites.js +14 -11
  169. package/lib/commonjs/features/favorites.js.map +1 -1
  170. package/lib/commonjs/features/gate.js +16 -1
  171. package/lib/commonjs/features/gate.js.map +1 -1
  172. package/lib/commonjs/features/metadata.js +20 -3
  173. package/lib/commonjs/features/metadata.js.map +1 -1
  174. package/lib/commonjs/features/network.js +1 -1
  175. package/lib/commonjs/features/nowPlaying.js +1 -1
  176. package/lib/commonjs/features/output.js +1 -1
  177. package/lib/commonjs/features/playback/controls.js +4 -2
  178. package/lib/commonjs/features/playback/controls.js.map +1 -1
  179. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  180. package/lib/commonjs/features/playback/playing.js +1 -1
  181. package/lib/commonjs/features/playback/progress.js +3 -2
  182. package/lib/commonjs/features/playback/progress.js.map +1 -1
  183. package/lib/commonjs/features/playback/rate.js +8 -1
  184. package/lib/commonjs/features/playback/rate.js.map +1 -1
  185. package/lib/commonjs/features/playback/state.js +3 -4
  186. package/lib/commonjs/features/playback/state.js.map +1 -1
  187. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  188. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  189. package/lib/commonjs/features/playback/volume.js +1 -1
  190. package/lib/commonjs/features/player/options.js +48 -34
  191. package/lib/commonjs/features/player/options.js.map +1 -1
  192. package/lib/commonjs/features/player/setup.js +18 -9
  193. package/lib/commonjs/features/player/setup.js.map +1 -1
  194. package/lib/commonjs/features/player/validateOptions.js +23 -0
  195. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  196. package/lib/commonjs/features/queue/activeTrack.js +15 -4
  197. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  198. package/lib/commonjs/features/queue/queue.js +13 -6
  199. package/lib/commonjs/features/queue/queue.js.map +1 -1
  200. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  201. package/lib/commonjs/features/queue/shuffle.js +1 -1
  202. package/lib/commonjs/features/remoteControls.js +13 -92
  203. package/lib/commonjs/features/remoteControls.js.map +1 -1
  204. package/lib/commonjs/features/sleepTimer.js +13 -1
  205. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  206. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  207. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  208. package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
  209. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  210. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  211. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  212. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  213. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  214. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  215. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  216. package/lib/commonjs/web/NativeAudioBrowser.js +200 -75
  217. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  218. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
  219. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
  220. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  221. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  222. package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
  223. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  224. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  225. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  226. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
  227. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
  228. package/lib/commonjs/web/browser/BrowserManager.js +175 -100
  229. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  230. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  231. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  232. package/lib/commonjs/web/browser/SearchManager.js +18 -23
  233. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  234. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  235. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  236. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  237. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  238. package/lib/commonjs/web/browser/sections.js +89 -0
  239. package/lib/commonjs/web/browser/sections.js.map +1 -0
  240. package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
  241. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  242. package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
  243. package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
  244. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  245. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  246. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  247. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  248. package/lib/module/AudioBrowser.js +2 -1
  249. package/lib/module/AudioBrowser.js.map +1 -1
  250. package/lib/module/CLAUDE.md +52 -0
  251. package/lib/module/features/browser-config.js +12 -8
  252. package/lib/module/features/browser-config.js.map +1 -1
  253. package/lib/module/features/browser.js +11 -3
  254. package/lib/module/features/browser.js.map +1 -1
  255. package/lib/module/features/carConnection.js +1 -1
  256. package/lib/module/features/equalizer.js +1 -1
  257. package/lib/module/features/errors.js +3 -3
  258. package/lib/module/features/favorites.js +14 -11
  259. package/lib/module/features/favorites.js.map +1 -1
  260. package/lib/module/features/gate.js +16 -1
  261. package/lib/module/features/gate.js.map +1 -1
  262. package/lib/module/features/metadata.js +20 -3
  263. package/lib/module/features/metadata.js.map +1 -1
  264. package/lib/module/features/network.js +1 -1
  265. package/lib/module/features/nowPlaying.js +1 -1
  266. package/lib/module/features/output.js +1 -1
  267. package/lib/module/features/playback/controls.js +4 -2
  268. package/lib/module/features/playback/controls.js.map +1 -1
  269. package/lib/module/features/playback/playWhenReady.js +1 -1
  270. package/lib/module/features/playback/playing.js +1 -1
  271. package/lib/module/features/playback/progress.js +3 -2
  272. package/lib/module/features/playback/progress.js.map +1 -1
  273. package/lib/module/features/playback/rate.js +8 -1
  274. package/lib/module/features/playback/rate.js.map +1 -1
  275. package/lib/module/features/playback/state.js +3 -4
  276. package/lib/module/features/playback/state.js.map +1 -1
  277. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  278. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  279. package/lib/module/features/playback/volume.js +1 -1
  280. package/lib/module/features/player/options.js +47 -32
  281. package/lib/module/features/player/options.js.map +1 -1
  282. package/lib/module/features/player/setup.js +17 -8
  283. package/lib/module/features/player/setup.js.map +1 -1
  284. package/lib/module/features/player/validateOptions.js +19 -0
  285. package/lib/module/features/player/validateOptions.js.map +1 -0
  286. package/lib/module/features/queue/activeTrack.js +15 -4
  287. package/lib/module/features/queue/activeTrack.js.map +1 -1
  288. package/lib/module/features/queue/queue.js +13 -6
  289. package/lib/module/features/queue/queue.js.map +1 -1
  290. package/lib/module/features/queue/repeatMode.js +1 -1
  291. package/lib/module/features/queue/shuffle.js +1 -1
  292. package/lib/module/features/remoteControls.js +13 -92
  293. package/lib/module/features/remoteControls.js.map +1 -1
  294. package/lib/module/features/sleepTimer.js +13 -1
  295. package/lib/module/features/sleepTimer.js.map +1 -1
  296. package/lib/module/test-utils/reactNativeStub.js +58 -0
  297. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  298. package/lib/module/utils/LazyNativeEmitter.js +28 -14
  299. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  300. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  301. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  302. package/lib/module/utils/getTrackIdentity.js +20 -0
  303. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  304. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  305. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  306. package/lib/module/web/NativeAudioBrowser.js +200 -75
  307. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  308. package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
  309. package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
  310. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  311. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  312. package/lib/module/web/TrackPlayer/Player.js +254 -39
  313. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  314. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  315. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  316. package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
  317. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
  318. package/lib/module/web/browser/BrowserManager.js +175 -100
  319. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  320. package/lib/module/web/browser/FavoriteManager.js +31 -21
  321. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  322. package/lib/module/web/browser/SearchManager.js +18 -23
  323. package/lib/module/web/browser/SearchManager.js.map +1 -1
  324. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  325. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  326. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  327. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  328. package/lib/module/web/browser/sections.js +83 -0
  329. package/lib/module/web/browser/sections.js.map +1 -0
  330. package/lib/module/web/http/RequestConfigBuilder.js +110 -27
  331. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  332. package/lib/module/web/http/followMediaRedirect.js +215 -0
  333. package/lib/module/web/http/followMediaRedirect.js.map +1 -0
  334. package/lib/module/web/player/NowPlayingManager.js +4 -3
  335. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  336. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  337. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  338. package/lib/typescript/package.json +1 -0
  339. package/lib/typescript/src/AudioBrowser.d.ts +1 -1
  340. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  341. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  342. package/lib/typescript/src/features/browser.d.ts +10 -2
  343. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  344. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  345. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  346. package/lib/typescript/src/features/errors.d.ts +3 -3
  347. package/lib/typescript/src/features/favorites.d.ts +14 -11
  348. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  349. package/lib/typescript/src/features/gate.d.ts +18 -2
  350. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  351. package/lib/typescript/src/features/metadata.d.ts +20 -3
  352. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  353. package/lib/typescript/src/features/network.d.ts +1 -1
  354. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  355. package/lib/typescript/src/features/output.d.ts +1 -1
  356. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  357. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  358. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  359. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  360. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  361. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  362. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  363. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  364. package/lib/typescript/src/features/playback/state.d.ts +3 -4
  365. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  366. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  367. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  368. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  369. package/lib/typescript/src/features/player/options.d.ts +108 -56
  370. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  371. package/lib/typescript/src/features/player/setup.d.ts +14 -4
  372. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  373. package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
  374. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  375. package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
  376. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  377. package/lib/typescript/src/features/queue/queue.d.ts +7 -4
  378. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  379. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  380. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  381. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  382. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  383. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  384. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  385. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  386. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  387. package/lib/typescript/src/types/browser-native.d.ts +3 -2
  388. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  389. package/lib/typescript/src/types/browser-nodes.d.ts +380 -126
  390. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  391. package/lib/typescript/src/types/browser.d.ts +82 -57
  392. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  393. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
  394. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  395. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  396. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  397. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  398. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  399. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  400. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  401. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
  402. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
  403. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
  404. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  405. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
  406. package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
  407. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  408. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  409. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
  410. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
  411. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
  412. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
  413. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
  414. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
  415. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
  416. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
  417. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
  418. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +16 -4
  419. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
  420. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +7 -10
  421. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
  422. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
  423. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
  424. package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
  425. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +45 -25
  426. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
  427. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  428. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  429. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
  430. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +36 -11
  431. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
  432. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +15 -12
  433. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +7 -10
  434. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
  435. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  436. package/nitrogen/generated/android/c++/JOptions.hpp +7 -10
  437. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
  438. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  439. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  440. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  441. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +57 -49
  442. package/nitrogen/generated/android/c++/JSection.hpp +116 -0
  443. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  444. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  445. package/nitrogen/generated/android/c++/JTrack.hpp +25 -46
  446. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
  447. package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
  448. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  449. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  450. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
  451. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
  452. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  453. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  454. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  455. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +80 -0
  456. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
  457. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
  458. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  459. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  460. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +8 -5
  461. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
  462. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  463. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  464. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  465. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
  466. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -15
  467. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +15 -6
  468. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
  469. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  470. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -12
  471. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
  472. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
  473. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +9 -0
  474. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +244 -133
  475. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +31 -16
  476. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +33 -18
  477. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  478. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  479. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  480. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  481. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
  482. package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
  483. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
  484. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
  485. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  486. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  487. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
  488. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  489. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  490. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  491. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  492. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  493. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
  494. package/nitrogen/generated/ios/swift/Section.swift +82 -0
  495. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  496. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  497. package/nitrogen/generated/ios/swift/Track.swift +28 -63
  498. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  499. package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
  500. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  501. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  502. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  503. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  504. package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
  505. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
  506. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
  507. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  508. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  509. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
  510. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  511. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  512. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  513. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  514. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  515. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
  516. package/nitrogen/generated/shared/c++/Section.hpp +102 -0
  517. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  518. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  519. package/nitrogen/generated/shared/c++/Track.hpp +18 -30
  520. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  521. package/package.json +24 -11
  522. package/src/AudioBrowser.ts +2 -1
  523. package/src/CLAUDE.md +52 -0
  524. package/src/features/battery.test.ts +79 -0
  525. package/src/features/browser-config.test.ts +21 -6
  526. package/src/features/browser-config.ts +12 -8
  527. package/src/features/browser.test.ts +1 -1
  528. package/src/features/browser.ts +11 -3
  529. package/src/features/carConnection.ts +1 -1
  530. package/src/features/equalizer.ts +1 -1
  531. package/src/features/errors.ts +3 -3
  532. package/src/features/favorites.ts +14 -11
  533. package/src/features/gate.ts +16 -1
  534. package/src/features/hook-wiring.test.ts +302 -0
  535. package/src/features/metadata.ts +20 -3
  536. package/src/features/network.ts +1 -1
  537. package/src/features/nowPlaying.ts +1 -1
  538. package/src/features/output.ts +1 -1
  539. package/src/features/playback/controls.ts +4 -2
  540. package/src/features/playback/playWhenReady.ts +1 -1
  541. package/src/features/playback/playing.ts +1 -1
  542. package/src/features/playback/progress.test.ts +169 -0
  543. package/src/features/playback/progress.ts +3 -2
  544. package/src/features/playback/rate.ts +8 -1
  545. package/src/features/playback/state.ts +3 -4
  546. package/src/features/playback/trackPlaybackTime.test.ts +16 -6
  547. package/src/features/playback/trackPlaybackTime.ts +1 -6
  548. package/src/features/playback/volume.ts +1 -1
  549. package/src/features/player/options.ts +126 -76
  550. package/src/features/player/setup.test.ts +24 -7
  551. package/src/features/player/setup.ts +22 -10
  552. package/src/features/player/validateOptions.ts +21 -0
  553. package/src/features/queue/activeTrack.ts +15 -4
  554. package/src/features/queue/queue.ts +13 -6
  555. package/src/features/queue/repeatMode.ts +1 -1
  556. package/src/features/queue/shuffle.ts +1 -1
  557. package/src/features/remoteControls.ts +13 -92
  558. package/src/features/sleepTimer.test.ts +172 -0
  559. package/src/features/sleepTimer.ts +9 -1
  560. package/src/nitro-stub-drift.test.ts +277 -0
  561. package/src/public-surface.test.ts +147 -0
  562. package/src/specs/audio-browser.nitro.ts +5 -4
  563. package/src/style-resolution.test.ts +210 -0
  564. package/src/test-utils/reactNativeStub.ts +60 -0
  565. package/src/types/browser-native.ts +7 -3
  566. package/src/types/browser-nodes.ts +396 -134
  567. package/src/types/browser.ts +85 -59
  568. package/src/utils/LazyNativeEmitter.ts +28 -15
  569. package/src/utils/NativeUpdatedValue.ts +29 -13
  570. package/src/utils/getTrackIdentity.ts +20 -0
  571. package/src/utils/useDebug.test.ts +182 -0
  572. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  573. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  574. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
  575. package/src/web/NativeAudioBrowser.ts +239 -82
  576. package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
  577. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
  578. package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
  579. package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
  580. package/src/web/TrackPlayer/Player.load.test.ts +137 -1
  581. package/src/web/TrackPlayer/Player.ts +284 -37
  582. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  583. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  584. package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
  585. package/src/web/__tests__/lifecycle.test.ts +345 -0
  586. package/src/web/__tests__/lifecycleHarness.ts +351 -0
  587. package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
  588. package/src/web/__tests__/loadSupersession.test.ts +175 -0
  589. package/src/web/__tests__/retry.test.ts +169 -0
  590. package/src/web/browser/BrowserManager.ts +209 -111
  591. package/src/web/browser/FavoriteManager.test.ts +84 -0
  592. package/src/web/browser/FavoriteManager.ts +28 -23
  593. package/src/web/browser/SearchManager.test.ts +75 -0
  594. package/src/web/browser/SearchManager.ts +25 -28
  595. package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
  596. package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
  597. package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
  598. package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
  599. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  600. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  601. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  602. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  603. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  604. package/src/web/browser/sections.test.ts +91 -0
  605. package/src/web/browser/sections.ts +72 -0
  606. package/src/web/http/RequestConfigBuilder.test.ts +241 -1
  607. package/src/web/http/RequestConfigBuilder.ts +140 -33
  608. package/src/web/http/followMediaRedirect.test.ts +289 -0
  609. package/src/web/http/followMediaRedirect.ts +253 -0
  610. package/src/web/player/NowPlayingManager.ts +4 -3
  611. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  612. package/src/web/util/BrowserPathHelper.ts +61 -13
  613. package/website/guide/android-auto.md +114 -0
  614. package/website/guide/android-certificates.md +409 -0
  615. package/website/guide/artwork.md +255 -0
  616. package/website/guide/audio-output.md +135 -0
  617. package/website/guide/automotive.md +86 -0
  618. package/website/guide/basic-usage.md +151 -0
  619. package/website/guide/battery.md +115 -0
  620. package/website/guide/browser.md +581 -0
  621. package/website/guide/carplay.md +620 -0
  622. package/website/guide/configuration.md +173 -0
  623. package/website/guide/equalizer.md +231 -0
  624. package/website/guide/errors.md +310 -0
  625. package/website/guide/favorites.md +154 -0
  626. package/website/guide/gate.md +143 -0
  627. package/website/guide/getting-started.md +46 -0
  628. package/website/guide/hooks.md +238 -0
  629. package/website/guide/live-streams.md +125 -0
  630. package/website/guide/metadata.md +239 -0
  631. package/website/guide/migrating-from-track-player.md +244 -0
  632. package/website/guide/native-callback-fetch.md +127 -0
  633. package/website/guide/network.md +86 -0
  634. package/website/guide/now-playing.md +175 -0
  635. package/website/guide/playback.md +328 -0
  636. package/website/guide/queue.md +300 -0
  637. package/website/guide/remote-controls.md +256 -0
  638. package/website/guide/search.md +255 -0
  639. package/website/guide/sleep-timer.md +268 -0
  640. package/website/guide/track.md +247 -0
  641. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  642. package/ios/Model/MediaURL.swift +0 -67
  643. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  644. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  645. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  646. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  647. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  648. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  649. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  650. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  651. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  652. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  653. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  654. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  655. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  656. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  657. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  658. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  659. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
  660. 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,105 @@ 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
85
90
  import com.margelo.nitro.core.Promise
86
91
  import java.util.concurrent.TimeUnit
92
+ import java.util.concurrent.atomic.AtomicReference
87
93
  import kotlin.coroutines.resume
88
94
  import kotlin.coroutines.resumeWithException
89
95
  import kotlin.coroutines.suspendCoroutine
96
+ import kotlin.time.Duration.Companion.seconds
90
97
  import kotlinx.coroutines.CancellationException
98
+ import kotlinx.coroutines.CoroutineScope
91
99
  import kotlinx.coroutines.Job
92
100
  import kotlinx.coroutines.MainScope
101
+ import kotlinx.coroutines.async
102
+ import kotlinx.coroutines.cancel
93
103
  import kotlinx.coroutines.cancelChildren
94
- import kotlinx.coroutines.delay
95
104
  import kotlinx.coroutines.launch
96
105
  import kotlinx.coroutines.runBlocking
106
+ import kotlinx.coroutines.withTimeoutOrNull
97
107
  import timber.log.Timber
98
108
 
99
109
  @Keep
100
110
  @DoNotStrip
101
111
  class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
102
112
 
103
- /** Called by Nitro when the JS object is destroyed (incl. JS runtime reloads). */
113
+ /**
114
+ * Nitro calls this only when JS calls `dispose()` explicitly — never on runtime teardown, which
115
+ * is what [AudioBrowserLifecycleModule] is for. Both routes land here.
116
+ *
117
+ * The Service outlives this object, so everything registered in `init` or on connect has to be
118
+ * undone. Left alone, the running [Player] goes on invoking callbacks into a dead JSI runtime,
119
+ * and every reload strands another ServiceConnection and lifecycle observer, each retaining a
120
+ * BrowserManager.
121
+ *
122
+ * Runs the teardown on main: the state below is written from main-thread coroutines
123
+ * ([onServiceConnected], [onServiceDisconnected]), while dispose itself arrives on the JS thread
124
+ * or the teardown callback's thread.
125
+ */
104
126
  override fun dispose() {
127
+ current.compareAndSet(this, null)
105
128
  systemVolumeMonitor.destroy()
106
129
  outputMonitor.destroy()
130
+ // Only when still ours: on a reload the replacement may already own the slot.
131
+ carConnectionTarget.compareAndSet(onCarConnectedChanged, null)
132
+
133
+ // The runtime behind the config's JS functions is going away, but anything still holding
134
+ // this instance (an in-flight browse coroutine, the running player) can invoke them after
135
+ // it's gone — each invoke then throws instead of answering, so browse takes its error path
136
+ // rather than its fallback.
137
+ dropStaleJSCallbacks()
138
+
139
+ handler.post {
140
+ connectedService?.let { service ->
141
+ // Identity-guarded: both instances bind to the same singleton Service, so a late dispose
142
+ // must not strip the wiring a replacement has already installed.
143
+ if (service.player.getCallbacks() === callbacks) service.player.setCallbacks(null)
144
+ if (service.player.browser === this) {
145
+ service.player.browser = null
146
+ service.player.forgetBrowserRegistration()
147
+ // The formatter wraps a JS callback from this instance's runtime (installed by our
148
+ // setup(); browser still being ours means no replacement has run setup yet). Without
149
+ // this, every track change until the next setup() throws into the updater's fallback.
150
+ service.player.nowPlaying.formatter = null
151
+ }
152
+ if (service.onBatteryWarningPendingChanged === onBatteryWarningPending) {
153
+ service.onBatteryWarningPendingChanged = null
154
+ }
155
+ }
156
+ connectedService = null
157
+
158
+ // Dropping the reference disconnects nothing — the controller stays in the session's
159
+ // connected list, holding a binder and a listener.
160
+ mediaBrowserFuture?.let { MediaBrowser.releaseFuture(it) }
161
+ mediaBrowserFuture = null
162
+ setupPromise = null
163
+
164
+ serviceBinding.unbind(this)
165
+
166
+ ProcessLifecycleOwner.get().lifecycle.removeObserver(lifecycleObserver)
167
+ // After the observer removal, so this post is not the one dropped.
168
+ handler.removeCallbacksAndMessages(null)
169
+ mainScope.cancel()
170
+ }
107
171
  super.dispose()
108
172
  }
109
173
 
174
+ /**
175
+ * Demotes the config to data-only (deep strip of every JS function field) and reverts the JS
176
+ * callbacks stored outside the config tree ([resolveGate]; the now-playing formatter is cleared
177
+ * in [dispose]'s service block, identity-guarded). Stale paths then degrade the same way as a
178
+ * config that never had callbacks — static request/browse layers, default error copy, fail-closed
179
+ * gate — instead of throwing into the error path. Mirrors iOS `dropStaleJSCallbacks`, where the
180
+ * same invoke is fatal rather than a throw.
181
+ */
182
+ private fun dropStaleJSCallbacks() {
183
+ _configuration = _configuration.strippingJSCallbacks()
184
+ browserManager.config = buildConfig()
185
+ resolveGate = defaultResolveGate
186
+ }
187
+
110
188
  private val mainScope = MainScope()
111
189
  private var navigationJob: Job? = null
112
190
  private val handler = Handler(Looper.getMainLooper())
@@ -129,8 +207,9 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
129
207
  singleTrack = null,
130
208
  handleTrackLoad = null,
131
209
  androidControllerOfflineError = null,
210
+ viewMoreTitle = null,
132
211
  carPlayLoadingTitle = null,
133
- resolveAlbumUrl = null,
212
+ resolveAlbumPath = null,
134
213
  formatNavigationError = null,
135
214
  )
136
215
 
@@ -149,7 +228,18 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
149
228
  private var updateOptions: PlayerUpdateOptions = PlayerUpdateOptions()
150
229
  private var mediaBrowserFuture: ListenableFuture<MediaBrowser>? = null
151
230
  private var setupOptions = PlayerSetupOptions()
152
- private var connectedService: Service? = null
231
+ // Written only on the main thread (ServiceConnection callbacks, dispose); read from main or,
232
+ // in a few places, straight from the JS thread. @Volatile gives those reads visibility — they
233
+ // must still tolerate staleness in both directions (a just-detached service, or null while
234
+ // onServiceConnected is pending). Keep writes on main; no read-modify-write.
235
+ @Volatile private var connectedService: Service? = null
236
+
237
+ private val serviceBinding = ServiceBinding(context)
238
+
239
+ /** Held rather than inlined so [dispose] can tell its own wiring from a replacement's. */
240
+ private val onBatteryWarningPending: (Boolean) -> Unit = { pending ->
241
+ post { onBatteryWarningPendingChanged(BatteryWarningPendingChangedEvent(pending)) }
242
+ }
153
243
  private var setupPromise: ((Unit) -> Unit)? = null
154
244
 
155
245
  // Initial player state staged before the player exists — from setup options or the imperative
@@ -168,14 +258,14 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
168
258
  override var onFormattedNavigationError: (FormattedNavigationError?) -> Unit = {}
169
259
 
170
260
  // 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
261
+ // Default resolver: only reachable while hasResolver=true but no live runtime has the real
262
+ // `resolveGate` bound — the init window before JS assigns it, or after [dispose] reverts a dead
263
+ // runtime's resolver. It DENIES by default
173
264
  // (gated=true) so an active gate never serves content during that window — same fail-closed
174
265
  // direction as the resolver-error path in [gateDecision]. The static-gate fast path skips it
175
266
  // 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
- }
267
+ override var resolveGate: (request: NativeGateRequest) -> Promise<Promise<GateDecision>> =
268
+ defaultResolveGate
179
269
  override var onGate: (event: GateEvent) -> Unit = {}
180
270
 
181
271
  // MARK: Player callbacks
@@ -239,12 +329,14 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
239
329
  }
240
330
 
241
331
  init {
332
+ current.set(this)
333
+
242
334
  // Auto-bind to service if it's already running
243
335
  launchInScope {
244
336
  try {
245
337
  Timber.d("Attempting to auto-bind to existing AudioBrowserService from AudioBrowser")
246
338
  val intent = Intent(context, Service::class.java)
247
- val bound = context.bindService(intent, this@AudioBrowser, Context.BIND_AUTO_CREATE)
339
+ val bound = serviceBinding.bind(intent, this@AudioBrowser)
248
340
  Timber.d("Auto-bind result: $bound")
249
341
 
250
342
  if (!bound) {
@@ -286,6 +378,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
286
378
  routes = _configuration.routes,
287
379
  singleTrack = _configuration.singleTrack ?: false,
288
380
  androidControllerOfflineError = _configuration.androidControllerOfflineError ?: true,
381
+ viewMoreTitle = _configuration.viewMoreTitle,
289
382
  )
290
383
  }
291
384
 
@@ -319,8 +412,8 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
319
412
  browserManager.config = buildConfig()
320
413
  val tabs = browserManager.queryTabs()
321
414
  if (tabs.isNotEmpty()) {
322
- Timber.d("Using first tab as default path: ${tabs[0].url}")
323
- tabs[0].url
415
+ Timber.d("Using first tab as default path: ${tabs[0].path}")
416
+ tabs[0].path
324
417
  } else {
325
418
  Timber.d("Using root path as default: /")
326
419
  "/"
@@ -375,8 +468,14 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
375
468
  browserManager.config = buildConfig()
376
469
 
377
470
  // Notify player that browser configuration is ready (routes/tabs available)
378
- // This allows Android Auto to start browsing content
379
- connectedService?.player?.notifyBrowserConfigurationReady()
471
+ // This allows Android Auto to start browsing content.
472
+ // This reaches MediaSession.getConnectedControllers, which asserts the
473
+ // main thread, and this setter runs on the JS thread. Hop without
474
+ // blocking -- runBlockingOnMain would deadlock here for the same reason
475
+ // described for the navigation below (JS callbacks can't run while the
476
+ // JS thread waits). Ordering with navigationJob is preserved: both are
477
+ // queued on mainScope.
478
+ launchInScope { connectedService?.player?.notifyBrowserConfigurationReady() }
380
479
 
381
480
  clearNavigationError()
382
481
 
@@ -408,121 +507,48 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
408
507
 
409
508
  override fun getFormattedNavigationError(): FormattedNavigationError? = formattedNavigationError
410
509
 
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"
510
+ /**
511
+ * Resolves a navigation error to its display form: the app's `formatNavigationError` for the
512
+ * given [path] when one is configured, else the built-in default. Shared by [setNavigationError]
513
+ * and the Android Auto empty tile so in-app and in-car copy cannot drift (ADR 0001).
514
+ */
515
+ internal suspend fun resolveFormattedError(
516
+ error: NavigationError,
517
+ path: String,
518
+ ): FormattedNavigationError {
519
+ val default = defaultFormattedError(error)
520
+ val formatter = _configuration.formatNavigationError ?: return default
521
+ return formattedOrDefault(mainScope, default) {
522
+ formatter(FormatNavigationErrorParams(error, default, path)).await()
446
523
  }
447
524
  }
448
525
 
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)
526
+ private fun setNavigationError(navError: NavigationError, path: String) {
457
527
  navigationError = navError
458
528
  onNavigationError(NavigationErrorEvent(navigationError))
459
529
 
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)
530
+ // No formatter means no hop, and the field and event land in this turn: a same-turn
531
+ // getFormattedNavigationError, or a clearNavigationError right after, must not race a
532
+ // launch that has yet to run.
533
+ if (_configuration.formatNavigationError == null) {
534
+ val default = defaultFormattedError(navError)
535
+ formattedNavigationError = default
536
+ onFormattedNavigationError(default)
537
+ return
538
+ }
539
+
540
+ mainScope.launch {
541
+ val formatted = resolveFormattedError(navError, path)
542
+ formattedNavigationError = formatted
543
+ onFormattedNavigationError(formatted)
477
544
  }
478
545
  }
479
546
 
480
- /** Maps common browser exceptions to navigation errors */
547
+ /** Raises the navigation error [e] maps to, for a failed navigation to [path]. */
481
548
  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
- }
549
+ val navError = navigationErrorFor(e)
550
+ Timber.e(e, "${navError.code} $logContext")
551
+ setNavigationError(navError, path)
526
552
  }
527
553
 
528
554
  private fun clearNavigationError() {
@@ -558,26 +584,38 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
558
584
  }
559
585
 
560
586
  override fun navigateTrack(track: Track) {
587
+ // A disabled track is unavailable — it never plays, whichever surface or
588
+ // stale resume path delivered the selection (Track.disabled; mirrors iOS
589
+ // TrackSelector.select). Refused before any state changes: an inert tap
590
+ // must not clear an unrelated navigation error or cancel an in-flight
591
+ // navigation.
592
+ if (track.disabled == true) {
593
+ Timber.d("Ignoring selection of disabled track: ${track.title}")
594
+ return
595
+ }
596
+
561
597
  clearNavigationError()
562
598
 
563
599
  // Cancel previous navigation to avoid race conditions
564
600
  navigationJob?.cancel()
565
601
 
566
- val url = track.url
602
+ val path = track.path
567
603
  navigationJob =
568
604
  mainScope.launch {
569
605
  try {
570
606
  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")
607
+ // Check if this is a contextual path (playable-only track with queue context)
608
+ path != null && BrowserPathHelper.isContextual(path) -> {
609
+ Timber.d("Navigating to contextual track path: $path")
574
610
 
575
- val parentPath = BrowserPathHelper.stripTrackId(url)
576
- val trackId = BrowserPathHelper.extractTrackId(url)
611
+ val parentPath = BrowserPathHelper.stripTrackId(path)
612
+ val trackId = BrowserPathHelper.extractTrackId(path)
577
613
 
578
- // Check if queue already came from this parent path - just skip to the track
614
+ // Check if queue already came from this parent path - just skip
615
+ // to the tapped surface (exact path first, identity for
616
+ // index-less paths — see indexOfTappedTrack)
579
617
  if (trackId != null && parentPath == player.queueSourcePath) {
580
- val index = player.tracks.indexOfFirst { it.src == trackId }
618
+ val index = player.tracks.indexOfTappedTrack(path, trackId)
581
619
  if (index >= 0) {
582
620
  Timber.d("Queue already from $parentPath, skipping to index $index")
583
621
  handleTrackLoad(
@@ -595,8 +633,8 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
595
633
  }
596
634
  }
597
635
 
598
- // Expand the queue from the contextual URL
599
- val expanded = browserManager.expandQueueFromContextualUrl(url)
636
+ // Expand the queue from the contextual path
637
+ val expanded = browserManager.expandQueueFromContextualPath(path)
600
638
 
601
639
  if (expanded != null) {
602
640
  val (tracks, startIndex) = expanded
@@ -631,9 +669,9 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
631
669
  }
632
670
  }
633
671
  // Navigate to browsable track to show browsing UI
634
- url != null -> {
635
- Timber.d("Navigating to browsable track: $url")
636
- browserManager.navigate(url)
672
+ path != null -> {
673
+ Timber.d("Navigating to browsable track: $path")
674
+ browserManager.navigate(path)
637
675
  }
638
676
  // If track is playable (has src), load it into player
639
677
  track.src != null -> {
@@ -648,22 +686,36 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
648
686
  )
649
687
  }
650
688
  else -> {
651
- throw IllegalArgumentException("Track must have either an 'url' or an 'src' property")
689
+ throw IllegalArgumentException("Track must have either a 'path' or an 'src' property")
652
690
  }
653
691
  }
654
692
  } catch (e: CancellationException) {
655
693
  throw e // Rethrow to properly cancel
656
694
  } catch (e: Exception) {
657
- handleBrowserException(e, url ?: track.src ?: "", "navigating to track: ${track.title}")
695
+ handleBrowserException(e, path ?: track.src ?: "", "navigating to track: ${track.title}")
658
696
  }
659
697
  }
660
698
  }
661
699
 
662
- override fun onSearch(query: String): Promise<Array<Track>> {
700
+ override fun search(query: String): Promise<Array<Track>> {
663
701
  return Promise.async(mainScope) {
664
702
  Timber.d("Searching for: $query")
665
- val searchResults = browserManager.search(query)
666
- searchResults.children ?: emptyArray()
703
+ // Rejects with the error the browse surface would raise, but leaves the navigation error
704
+ // itself unset: a search is not a navigation, and its failure must not replace the
705
+ // displayed page's state (mirrors HybridAudioBrowser.search).
706
+ val searchResults =
707
+ try {
708
+ browserManager.search(query)
709
+ } catch (e: CancellationException) {
710
+ throw e
711
+ } catch (e: Exception) {
712
+ Timber.e(e, "Search failed for query: $query")
713
+ throw NavigationErrorException(navigationErrorFor(e))
714
+ }
715
+ // Flattened, not `children`: a resolved page carries its rows in
716
+ // `sections` (ADR 0010) and search builds exactly one untitled section,
717
+ // so `children` is always null here and this returned nothing at all.
718
+ searchResults.flattenedChildren?.toTypedArray() ?: emptyArray()
667
719
  }
668
720
  }
669
721
 
@@ -677,8 +729,9 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
677
729
  // Invalidate cached content so future navigations fetch fresh data
678
730
  browserManager.invalidateContentCache(path)
679
731
 
680
- // Notify external media controllers (Android Auto)
681
- connectedService?.player?.notifyContentChanged(path)
732
+ // Notify external media controllers (Android Auto). Reaches MediaSession,
733
+ // which asserts the main thread; this override runs on the JS thread.
734
+ launchInScope { connectedService?.player?.notifyContentChanged(path) }
682
735
 
683
736
  // If we're currently viewing this path, refresh the content
684
737
  if (browserManager.getPath() == path) {
@@ -692,8 +745,10 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
692
745
  // Clear all cached content so every path re-fetches fresh data
693
746
  browserManager.clearContentCache()
694
747
 
695
- // Notify external media controllers (Android Auto) to refresh subscribed paths
696
- connectedService?.player?.invalidateAllContent()
748
+ // Notify external media controllers (Android Auto) to refresh subscribed paths.
749
+ // Reaches MediaSession, which asserts the main thread; this override runs on
750
+ // the JS thread.
751
+ launchInScope { connectedService?.player?.invalidateAllContent() }
697
752
 
698
753
  // Refresh whatever the browser is currently viewing
699
754
  mainScope.launch { browserManager.refresh() }
@@ -734,14 +789,16 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
734
789
  // Re-query every subscribed parent so a connected controller (Android
735
790
  // Auto) swaps its lists for the gate tile without reconnecting. Notify
736
791
  // only — the content cache stays warm for when the gate clears.
737
- connectedService?.player?.invalidateAllContent()
792
+ // MediaSession asserts the main thread; this override runs on the JS thread.
793
+ launchInScope { connectedService?.player?.invalidateAllContent() }
738
794
  }
739
795
 
740
796
  override fun clearGate() {
741
797
  if (!gateState.active) return
742
798
  // Single atomic assignment — readers never see a half-cleared triple.
743
799
  gateState = GateState(active = false, chrome = null, hasResolver = false)
744
- connectedService?.player?.invalidateAllContent()
800
+ // MediaSession asserts the main thread; this override runs on the JS thread.
801
+ launchInScope { connectedService?.player?.invalidateAllContent() }
745
802
  }
746
803
 
747
804
  /**
@@ -781,7 +838,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
781
838
  override var onCarConnectedChanged: (Boolean) -> Unit = {}
782
839
  set(value) {
783
840
  field = value
784
- carConnectionTarget = value
841
+ carConnectionTarget.set(value)
785
842
  // Immediately notify current state (the car may have connected before
786
843
  // this JS runtime subscribed) — mirrors the iOS behavior.
787
844
  value(carConnected)
@@ -811,12 +868,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
811
868
  // Service not connected yet, bind to service
812
869
  suspendCoroutine<Unit> { continuation ->
813
870
  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
- )
871
+ val bound = serviceBinding.bind(Intent(context, Service::class.java), this@AudioBrowser)
820
872
 
821
873
  if (!bound) {
822
874
  continuation.resumeWithException(
@@ -832,8 +884,17 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
832
884
 
833
885
  override fun updateOptions(options: NativeUpdateOptions) {
834
886
  updateOptions.updateFromBridge(options)
835
- // Only update the options if the service is around
836
- connectedService?.let { player.applyOptions(updateOptions) }
887
+ // Only update the options if the service is around.
888
+ // applyOptions touches the Media3 player, which asserts it is only ever
889
+ // driven from the main thread ("Player callback method is called from a
890
+ // wrong thread"). Nitro invokes this from the JS thread, so hop.
891
+ //
892
+ // The hop blocks rather than launching because `updateOptions` is a single
893
+ // mutable holder: updateFromBridge mutates it in place on the JS thread and
894
+ // applyOptions reads it on main. Blocking is what keeps the two ordered.
895
+ connectedService?.let { service ->
896
+ runBlockingOnMain { service.player.applyOptions(updateOptions) }
897
+ }
837
898
  }
838
899
 
839
900
  override fun getOptions(): Options {
@@ -852,7 +913,6 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
852
913
 
853
914
  override fun reset() = runBlockingOnMain {
854
915
  player.stop()
855
- delay(300) // Allow playback to stop
856
916
  player.clear()
857
917
  // clear() while IDLE emits no engine event, so the state machine never
858
918
  // leaves STOPPED — a reset queue is "nothing loaded".
@@ -979,12 +1039,15 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
979
1039
  player.setQueue(tracks, startIndex?.toInt() ?: 0, ((startPosition ?: 0.0) * 1000).toLong())
980
1040
  }
981
1041
 
982
- override fun setActiveTrackFavorited(favorited: Boolean) = runBlockingOnMain {
1042
+ override fun setActiveTrackFavorited(favorited: Boolean): Unit = runBlockingOnMain {
1043
+ // Applied-result is for MediaSession honesty; the JS API stays fire-and-forget.
983
1044
  player.setActiveTrackFavorited(favorited)
1045
+ Unit
984
1046
  }
985
1047
 
986
- override fun toggleActiveTrackFavorited() = runBlockingOnMain {
1048
+ override fun toggleActiveTrackFavorited(): Unit = runBlockingOnMain {
987
1049
  player.toggleActiveTrackFavorited()
1050
+ Unit
988
1051
  }
989
1052
 
990
1053
  override fun getQueue(): Array<Track> = runBlockingOnMain { player.tracks }
@@ -1175,13 +1238,7 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
1175
1238
  }
1176
1239
 
1177
1240
  // Wire up battery warning callback from service
1178
- connectedService?.onBatteryWarningPendingChanged = { pending ->
1179
- post {
1180
- this@AudioBrowser.onBatteryWarningPendingChanged(
1181
- BatteryWarningPendingChangedEvent(pending)
1182
- )
1183
- }
1184
- }
1241
+ connectedService?.onBatteryWarningPendingChanged = onBatteryWarningPending
1185
1242
 
1186
1243
  val sessionToken = SessionToken(context, ComponentName(context, Service::class.java))
1187
1244
  mediaBrowserFuture = MediaBrowser.Builder(context, sessionToken).buildAsync()
@@ -1436,9 +1493,27 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
1436
1493
  // callback registers as the single notification target (mirrors the iOS
1437
1494
  // static + shared-instance pattern).
1438
1495
  @Volatile private var carConnected = false
1439
- @Volatile private var carConnectionTarget: ((Boolean) -> Unit)? = null
1496
+ // Compare-and-set, not @Volatile: a disposing instance clears the slot only if it still owns
1497
+ // it, and a replacement may be claiming it at the same moment.
1498
+ private val carConnectionTarget = AtomicReference<((Boolean) -> Unit)?>(null)
1440
1499
  private var carConnectionObserverStarted = false
1441
1500
 
1501
+ /**
1502
+ * The instance the current JS runtime is using. Nitro hands out no registry, and the teardown
1503
+ * callback has nothing to dispose without one.
1504
+ */
1505
+ private val current = AtomicReference<AudioBrowser?>(null)
1506
+
1507
+ /** Called by [AudioBrowserLifecycleModule] when the React instance is torn down. */
1508
+ internal fun disposeCurrent() {
1509
+ current.getAndSet(null)?.dispose()
1510
+ }
1511
+
1512
+ /** Shared by the [resolveGate] initializer and [dispose], which reverts to it. */
1513
+ private val defaultResolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = {
1514
+ Promise.resolved(Promise.resolved(GateDecision(gated = true, gate = null)))
1515
+ }
1516
+
1442
1517
  private fun startCarConnectionObserver(context: Context, handler: Handler) {
1443
1518
  // Posted to main: LiveData observation is main-thread only, and the
1444
1519
  // single-start guard is then only ever touched from one thread.
@@ -1449,9 +1524,159 @@ class AudioBrowser : HybridAudioBrowserSpec(), ServiceConnection {
1449
1524
  val connected = type != CarConnection.CONNECTION_TYPE_NOT_CONNECTED
1450
1525
  if (connected == carConnected) return@observeForever
1451
1526
  carConnected = connected
1452
- carConnectionTarget?.invoke(connected)
1527
+ carConnectionTarget.get()?.invoke(connected)
1453
1528
  }
1454
1529
  }
1455
1530
  }
1456
1531
  }
1457
1532
  }
1533
+
1534
+ // MARK: Deep JS-callback stripping (used by dispose)
1535
+ // Functions live at every level of the config, not just the top; each type's strip nils its own.
1536
+
1537
+ private fun NativeBrowserConfiguration.strippingJSCallbacks() =
1538
+ copy(
1539
+ requestResolver = null,
1540
+ browseResolver = null,
1541
+ handleTrackLoad = null,
1542
+ viewMoreTitle = null,
1543
+ resolveAlbumPath = null,
1544
+ formatNavigationError = null,
1545
+ request = request?.strippingJSCallbacks(),
1546
+ browse = browse?.strippingJSCallbacks(),
1547
+ media = media?.strippingJSCallbacks(),
1548
+ artwork = artwork?.strippingJSCallbacks(),
1549
+ nowPlayingArtwork = nowPlayingArtwork?.strippingJSCallbacks(),
1550
+ routes = routes?.map { it.strippingJSCallbacks() }?.toTypedArray(),
1551
+ )
1552
+
1553
+ private fun TransformableRequestConfig.strippingJSCallbacks() =
1554
+ copy(transform = null, transformSync = null)
1555
+
1556
+ private fun MediaRequestConfig.strippingJSCallbacks() =
1557
+ copy(resolve = null, resolveSync = null, transform = null, transformSync = null)
1558
+
1559
+ private fun ArtworkRequestConfig.strippingJSCallbacks() =
1560
+ copy(resolve = null, resolveSync = null, transform = null, transformSync = null)
1561
+
1562
+ private fun NativeRouteEntry.strippingJSCallbacks() =
1563
+ copy(
1564
+ browseCallback = null,
1565
+ browseConfig = browseConfig?.strippingJSCallbacks(),
1566
+ searchCallback = null,
1567
+ searchConfig = searchConfig?.strippingJSCallbacks(),
1568
+ media = media?.strippingJSCallbacks(),
1569
+ artwork = artwork?.strippingJSCallbacks(),
1570
+ )
1571
+
1572
+ /**
1573
+ * Ceiling on the app's `formatNavigationError` hop. Same budget [Player.awaitBrowser] gives the JS
1574
+ * side, the only other JS wait a browse serve already blocks on.
1575
+ */
1576
+ private val FORMATTER_TIMEOUT = 10.seconds
1577
+
1578
+ /**
1579
+ * The navigation error an exception raises. Shared by [AudioBrowser]'s failed navigations, the
1580
+ * rejection [AudioBrowser.search] hands JS, and the Android Auto browse-error tile, so one failure
1581
+ * is named one way wherever it surfaces.
1582
+ */
1583
+ internal fun navigationErrorFor(e: Exception): NavigationError =
1584
+ when (e) {
1585
+ is HttpStatusException ->
1586
+ NavigationError(
1587
+ NavigationErrorType.HTTP_ERROR,
1588
+ e.message ?: "Server error",
1589
+ e.statusCode.toDouble(),
1590
+ e.statusCode in 200..299,
1591
+ )
1592
+ is NetworkException ->
1593
+ NavigationError(
1594
+ NavigationErrorType.NETWORK_ERROR,
1595
+ e.message ?: "Network request failed",
1596
+ null,
1597
+ null,
1598
+ )
1599
+ is ContentNotFoundException ->
1600
+ NavigationError(
1601
+ NavigationErrorType.CONTENT_NOT_FOUND,
1602
+ e.message ?: "Content not found",
1603
+ null,
1604
+ null,
1605
+ )
1606
+ is CallbackException ->
1607
+ NavigationError(
1608
+ NavigationErrorType.CALLBACK_ERROR,
1609
+ e.message ?: "An error occurred",
1610
+ null,
1611
+ null,
1612
+ )
1613
+ else ->
1614
+ NavigationError(
1615
+ NavigationErrorType.UNKNOWN_ERROR,
1616
+ e.message ?: "An unexpected error occurred",
1617
+ null,
1618
+ null,
1619
+ )
1620
+ }
1621
+
1622
+ /** Carries a [NavigationError] across a rejected Promise, whose JS error keeps its message. */
1623
+ class NavigationErrorException(val error: NavigationError) : Exception(error.message)
1624
+
1625
+ /**
1626
+ * The built-in display copy for a navigation error, and the fallback when the app's is unusable.
1627
+ */
1628
+ internal fun defaultFormattedError(error: NavigationError): FormattedNavigationError {
1629
+ val title =
1630
+ when (error.code) {
1631
+ NavigationErrorType.CONTENT_NOT_FOUND -> "Content Not Found"
1632
+ NavigationErrorType.NETWORK_ERROR -> "Network Error"
1633
+ NavigationErrorType.HTTP_ERROR -> {
1634
+ // Use system-localized HTTP status text (e.g., "Not Found", "Service Unavailable")
1635
+ error.statusCode?.let { httpStatusText(it.toInt()) } ?: "Server Error"
1636
+ }
1637
+ NavigationErrorType.CALLBACK_ERROR -> "Error"
1638
+ NavigationErrorType.UNKNOWN_ERROR -> "Error"
1639
+ // Not a failure — a container that resolved with no children. Neutral copy. See ADR 0001.
1640
+ NavigationErrorType.EMPTY_CONTENT -> "Nothing here"
1641
+ NavigationErrorType.TIMEOUT -> "Couldn't load"
1642
+ }
1643
+ // Omit an empty message so it renders as title-only (e.g. the empty-content case).
1644
+ return FormattedNavigationError(title, error.message.takeIf { it.isNotEmpty() })
1645
+ }
1646
+
1647
+ /**
1648
+ * The formatter hop's fallback rule: the app's answer when it produces one, [default] when it
1649
+ * answers null, throws, or outruns [FORMATTER_TIMEOUT]. Kept off the JS edge so the rule is
1650
+ * exercisable without a Nitro Promise.
1651
+ *
1652
+ * The hop runs in [scope] and is awaited through its [kotlinx.coroutines.Deferred], because Nitro's
1653
+ * `Promise.await` parks in a plain `suspendCoroutine`: cancelling it does not unpark it. Awaiting
1654
+ * the Deferred is cancellable, so the timeout leaves the orphan to finish unobserved instead of
1655
+ * holding the caller open until JS answers — which for a browse serve would be forever.
1656
+ */
1657
+ internal suspend fun formattedOrDefault(
1658
+ scope: CoroutineScope,
1659
+ default: FormattedNavigationError,
1660
+ format: suspend () -> FormattedNavigationError?,
1661
+ ): FormattedNavigationError {
1662
+ val hop = scope.async { runCatching { format() }.getOrNull() }
1663
+ return withTimeoutOrNull(FORMATTER_TIMEOUT) { hop.await() } ?: default
1664
+ }
1665
+
1666
+ /** Returns localized HTTP status text for the given status code */
1667
+ private fun httpStatusText(statusCode: Int): String {
1668
+ return when (statusCode) {
1669
+ 400 -> "Bad Request"
1670
+ 401 -> "Unauthorized"
1671
+ 403 -> "Forbidden"
1672
+ 404 -> "Not Found"
1673
+ 405 -> "Method Not Allowed"
1674
+ 408 -> "Request Timeout"
1675
+ 429 -> "Too Many Requests"
1676
+ 500 -> "Internal Server Error"
1677
+ 502 -> "Bad Gateway"
1678
+ 503 -> "Service Unavailable"
1679
+ 504 -> "Gateway Timeout"
1680
+ else -> "Server Error"
1681
+ }
1682
+ }