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
@@ -16,6 +16,10 @@ import os.log
16
16
  /// Only safe when the caller blocks until the MainActor work completes (DispatchQueue.main.sync).
17
17
  private struct UncheckedSendableBox<T>: @unchecked Sendable { let value: T }
18
18
 
19
+ /// `@unchecked Sendable` because it must be: the spec's requirements are all
20
+ /// nonisolated and Nitro calls them synchronously on the JS thread, so the class
21
+ /// can't be `@MainActor`. Isolation is therefore per-property, and `@unchecked`
22
+ /// means the compiler won't tell you when a new one needs it.
19
23
  public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
20
24
  private let logger = Logger(subsystem: "com.audiobrowser", category: "AudioBrowser")
21
25
 
@@ -30,8 +34,28 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
30
34
  /// silent.
31
35
  static let instanceChangedEmitter = Emitter<Void>()
32
36
 
37
+ /// Installed once, on first instance creation. React Native posts this
38
+ /// notification (from `RCTReloadCommand`) when a JS reload — dev or
39
+ /// OTA-update restart — is triggered, *before* the old runtime is destroyed:
40
+ /// the earliest signal that this instance's JS callbacks are about to go
41
+ /// stale. `init` of the replacement instance drops them too, but only after
42
+ /// the new runtime is up; this observer closes the window in between.
43
+ /// Observed by name so the library needs no React-Core dependency (and
44
+ /// stays inert under SPM tests, where nothing posts it).
45
+ @MainActor private static let reloadObserver: NSObjectProtocol = NotificationCenter.default.addObserver(
46
+ forName: Notification.Name("RCTTriggerReloadCommandNotification"),
47
+ object: nil,
48
+ queue: .main,
49
+ ) { _ in
50
+ MainActor.assumeIsolated {
51
+ HybridAudioBrowser.shared?.dropStaleJSCallbacks()
52
+ }
53
+ }
54
+
33
55
  // MARK: - Private Properties
34
56
 
57
+ /// Main-confined by convention, not annotation: `deinit` reads it as a
58
+ /// safety net and can't hop. Reach it from `onMainActor` / `@MainActor`.
35
59
  private var player: TrackPlayer?
36
60
  private let networkMonitor = NetworkMonitor()
37
61
  private let playbackStateStore = PlaybackStateStore()
@@ -43,27 +67,35 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
43
67
  private var mediaServicesResetObserver: NSObjectProtocol?
44
68
  /// Re-applies our resolved audio-session category. Captured at setup so a
45
69
  /// media-services reset (which clears the category) can restore it.
46
- private var applyAudioSessionCategory: () -> Void = {}
47
- private var nowPlayingOverride: NowPlayingUpdate?
70
+ @MainActor private var applyAudioSessionCategory: () -> Void = {}
71
+ @MainActor private var nowPlayingOverride: NowPlayingUpdate?
48
72
  /// When false, the now-playing surface uses the raw track fields (override + formatter ignored).
49
- private var nowPlayingMetadataEnabled = true
73
+ @MainActor private var nowPlayingMetadataEnabled = true
50
74
  /// Customizes the now-playing title/subtitle from the track + live timed metadata + playback state.
51
- private var nowPlayingMetadataFormatter: ((_ params: FormatNowPlayingParams) -> Promise<NowPlayingUpdate?>)?
52
- /// Keep the media session controllable through a terminal error (see `keepSessionAliveOnError`).
53
- private var keepSessionAliveOnError = true
75
+ @MainActor private var nowPlayingMetadataFormatter: ((_ params: FormatNowPlayingParams) -> Promise<NowPlayingUpdate?>)?
54
76
  /// Initial player state staged before the player exists — from setup options or the imperative
55
77
  /// setters called pre-setup. Strict last-write-wins; consumed when the player comes up.
56
- private var pendingPlayWhenReady: Bool?
57
- private var pendingRepeatMode: RepeatMode?
78
+ @MainActor private var pendingPlayWhenReady: Bool?
79
+ @MainActor private var pendingRepeatMode: RepeatMode?
58
80
  /// Latest live timed (ICY/ID3) metadata, passed to the formatter. Cleared on track change.
59
- private var latestTimedMetadata: TimedMetadata?
60
- private let playerOptions = PlayerUpdateOptions()
81
+ @MainActor private var latestTimedMetadata: TimedMetadata?
82
+ @MainActor private let playerOptions = PlayerUpdateOptions()
61
83
 
62
84
  /// Configured playback rates for the playback-rate capability (for CarPlay rate cycling)
63
- var playbackRates: [Double] { playerOptions.playbackRates }
64
- var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
65
- var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] { playerOptions.carPlayNowPlayingButtons }
66
- private var lastNavigationError: NavigationError? {
85
+ @MainActor var playbackRates: [Double] { playerOptions.playbackRates }
86
+ @MainActor var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
87
+ @MainActor var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] {
88
+ playerOptions.carPlayNowPlayingButtons
89
+ }
90
+
91
+ @MainActor var carPlayPlayingIndicatorLocation: CarPlayPlayingIndicatorLocation {
92
+ playerOptions.carPlayPlayingIndicatorLocation
93
+ }
94
+
95
+ /// Written from the JS thread, the cooperative pool and MainActor, with a
96
+ /// `didSet` that calls back into Nitro. MainActor-isolated so the compiler
97
+ /// requires an `onMainActor` hop at every access.
98
+ @MainActor private var lastNavigationError: NavigationError? {
67
99
  didSet {
68
100
  // Skip if both nil (no real change)
69
101
  guard oldValue != nil || lastNavigationError != nil else { return }
@@ -71,7 +103,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
71
103
  }
72
104
  }
73
105
 
74
- private var lastFormattedNavigationError: FormattedNavigationError? {
106
+ /// Isolated for the same reason as ``lastNavigationError``.
107
+ @MainActor private var lastFormattedNavigationError: FormattedNavigationError? {
75
108
  didSet {
76
109
  // Skip if both nil (no real change)
77
110
  guard oldValue != nil || lastFormattedNavigationError != nil else { return }
@@ -153,9 +186,14 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
153
186
  public var configuration: NativeBrowserConfiguration = .init(
154
187
  path: nil, request: nil, requestResolver: nil, browse: nil, browseResolver: nil, media: nil, artwork: nil, nowPlayingArtwork: nil, routes: nil,
155
188
  singleTrack: nil, handleTrackLoad: nil,
156
- androidControllerOfflineError: nil, carPlayLoadingTitle: nil, resolveAlbumUrl: nil, formatNavigationError: nil,
189
+ androidControllerOfflineError: nil, viewMoreTitle: nil, carPlayLoadingTitle: nil, resolveAlbumPath: nil,
190
+ formatNavigationError: nil,
157
191
  ) {
158
192
  didSet {
193
+ // Copied here, on the setter's thread: the Task below outlives this call,
194
+ // and Nitro can reassign `configuration` — a C++ struct holding
195
+ // std::string/std::function — from the JS thread while it runs.
196
+ let configuredPath = configuration.path
159
197
  onMainActor { browserManager.config = BrowserConfig(from: configuration) }
160
198
 
161
199
  // Query tabs and navigate to initial path after config is set (matches Kotlin behavior)
@@ -166,12 +204,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
166
204
  Task {
167
205
  let tabs = try? await browserManager.queryTabs()
168
206
  // Navigate to configured path, first tab, or "/"
169
- let initialPath = configuration.path ?? tabs?.first?.url ?? "/"
207
+ let initialPath = configuredPath ?? tabs?.first?.path ?? "/"
170
208
  // Clear error before navigation (matches Kotlin clearNavigationError())
171
- await MainActor.run {
172
- lastNavigationError = nil
173
- lastFormattedNavigationError = nil
174
- }
209
+ clearNavigationError()
175
210
  do {
176
211
  try await browserManager.navigate(initialPath)
177
212
  } catch {
@@ -201,8 +236,14 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
201
236
  /// state, e.g. CarPlay re-seeding `isGated` at scene connect). Serving content
202
237
  /// in that window would be a fail-open leak — same direction as the
203
238
  /// resolver-error path in `gateDecision`. Matches Android's default.
204
- public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = { _ in
205
- Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
239
+ public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = HybridAudioBrowser.defaultResolveGate
240
+
241
+ // Computed, not stored: a nonisolated static holding a non-Sendable closure
242
+ // is rejected under Swift 6 concurrency checking.
243
+ static var defaultResolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> {
244
+ { _ in
245
+ Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
246
+ }
206
247
  }
207
248
 
208
249
  /// Fired when a request is gated (served the gate). Set by JS.
@@ -294,7 +335,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
294
335
  // playerAndConfiguredBrowser OnceValue, so we must explicitly stop
295
336
  // its player to prevent two audio streams running simultaneously.
296
337
  onMainActor {
338
+ // Statics are lazy — touch the observer so it's installed from the first
339
+ // instance onward.
340
+ _ = HybridAudioBrowser.reloadObserver
297
341
  HybridAudioBrowser.shared?.player?.destroy()
342
+ HybridAudioBrowser.shared?.dropStaleJSCallbacks()
298
343
  playerAndConfiguredBrowser.reset()
299
344
  HybridAudioBrowser.shared = self
300
345
  // Tell connected external controllers (CarPlay) to re-subscribe against
@@ -307,7 +352,52 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
307
352
  setupBrowserCallbacks()
308
353
  }
309
354
 
355
+ /// Drops every JS-provided callback that native code can invoke on its own
356
+ /// initiative (browse resolvers, `handleTrackLoad`, `resolveAlbumPath`,
357
+ /// `formatNavigationError`, `resolveGate`, the now-playing formatter).
358
+ ///
359
+ /// Called on the *previous* instance when a new one takes over: its JS runtime
360
+ /// is gone, and invoking a callback from a dead runtime aborts the process —
361
+ /// Nitro's `AsyncJSCallback` throws on a destroyed Dispatcher, and the throw
362
+ /// crosses the generated bridge wrapper's `noexcept` boundary. Native code
363
+ /// (e.g. a CarPlay controller still bound to this instance mid-reload) keeps
364
+ /// working off the data-only config with built-in fallbacks; `resolveGate`
365
+ /// reverts to the fail-closed default it documents for exactly this window.
366
+ /// One-shot: on a normal reload both triggers hit the same dying instance
367
+ /// (the reload notification, then the replacement's `init`), and the second
368
+ /// strip would re-fire the config-setter cascade — another `onConfigChanged`
369
+ /// → CarPlay now-playing button rebuild — for nothing.
370
+ @MainActor private var didDropJSCallbacks = false
371
+
372
+ @MainActor
373
+ private func dropStaleJSCallbacks() {
374
+ guard !didDropJSCallbacks else { return }
375
+ didDropJSCallbacks = true
376
+ // Guarded: assigning `config` marks the browser configured (the setter's
377
+ // didSet), and a never-configured browser must keep cold-start waiters
378
+ // (CarPlay, Siri) blocked on the readiness gate rather than resolve them
379
+ // against an empty config.
380
+ if browserManager.isConfigured {
381
+ browserManager.config = browserManager.config.strippingJSCallbacks()
382
+ }
383
+ resolveGate = Self.defaultResolveGate
384
+ nowPlayingMetadataFormatter = nil
385
+ }
386
+
310
387
  deinit {
388
+ // Block-based notification observers are retained by the center until an
389
+ // explicit removeObserver — observer death isn't detected — so without this
390
+ // every JS reload strands another token + closure. They capture `self`
391
+ // weakly, so a stranded one no-ops rather than misbehaving; what accumulates
392
+ // is the tokens and a wasted main-queue dispatch per interruption / route
393
+ // change / media-services reset. (`volumeObservation` needs no counterpart:
394
+ // NSKeyValueObservation invalidates itself when this instance releases it.)
395
+ for observer in [interruptionObserver, routeChangeObserver, mediaServicesResetObserver]
396
+ .compactMap(\.self)
397
+ {
398
+ NotificationCenter.default.removeObserver(observer)
399
+ }
400
+
311
401
  // Safety net: ensure the AVPlayer is stopped if this instance is deallocated.
312
402
  let player = self.player
313
403
  if Thread.isMainThread {
@@ -370,7 +460,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
370
460
  private func handleNavigationError(_ error: Error, path: String) {
371
461
  let navError = NavigationError.from(error)
372
462
 
373
- lastNavigationError = navError
463
+ onMainActor { lastNavigationError = navError }
374
464
 
375
465
  // Format the error (async if using JS callback, sync for defaults)
376
466
  let defaultFormatted = navError.defaultFormatted()
@@ -380,27 +470,40 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
380
470
  // The result callbacks are @Sendable: a plain closure would inherit MainActor
381
471
  // isolation from this block, and Nitro resolves its promises synchronously on the
382
472
  // JS thread — Swift 6.2's dynamic isolation check then traps on closure entry.
383
- // The property is JS-thread-touched elsewhere too, so no hop back is needed.
473
+ // They therefore hop back explicitly, like every other writer.
384
474
  DispatchQueue.main.async { [weak self] in
385
475
  formatter(params)
386
476
  .then { @Sendable [weak self] customDisplay in
387
- self?.lastFormattedNavigationError = customDisplay ?? defaultFormatted
477
+ self?.setFormattedNavigationError(customDisplay ?? defaultFormatted)
388
478
  }
389
479
  .catch { @Sendable [weak self] _ in
390
- self?.lastFormattedNavigationError = defaultFormatted
480
+ self?.setFormattedNavigationError(defaultFormatted)
391
481
  }
392
482
  }
393
483
  } else {
394
- lastFormattedNavigationError = defaultFormatted
484
+ setFormattedNavigationError(defaultFormatted)
485
+ }
486
+ }
487
+
488
+ /// Callable from any thread — the Nitro promise callbacks below resolve on
489
+ /// the JS thread.
490
+ private func setFormattedNavigationError(_ formatted: FormattedNavigationError?) {
491
+ onMainActor { lastFormattedNavigationError = formatted }
492
+ }
493
+
494
+ /// Synchronous on purpose: the `didSet`s notify JS, which has to happen
495
+ /// before the navigation that may set them again.
496
+ private func clearNavigationError() {
497
+ onMainActor {
498
+ lastNavigationError = nil
499
+ lastFormattedNavigationError = nil
395
500
  }
396
501
  }
397
502
 
398
503
  // MARK: - Browser Methods
399
504
 
400
505
  public func navigatePath(path: String) throws {
401
- // Clear error synchronously before starting navigation (didSet notifies JS)
402
- lastNavigationError = nil
403
- lastFormattedNavigationError = nil
506
+ clearNavigationError()
404
507
  Task {
405
508
  do {
406
509
  try await browserManager.navigate(path)
@@ -411,7 +514,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
411
514
  }
412
515
 
413
516
  public func navigateTrack(track: Track) throws {
414
- Task {
517
+ // @MainActor: `player` is main-confined, and `trackSelector.select` is
518
+ // MainActor anyway — a bare Task read the reference off the cooperative
519
+ // pool, racing the assignment in `setupPlayer`.
520
+ Task { @MainActor in
415
521
  guard let player else { return }
416
522
  let result = await trackSelector.select(track: track, player: player)
417
523
  switch result {
@@ -419,8 +525,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
419
525
  executePlayback(intent)
420
526
  case .intercepted:
421
527
  break
422
- case let .browse(url):
423
- navigateToBrowsableUrl(url)
528
+ case let .browse(path):
529
+ navigateToBrowsablePath(path)
424
530
  case .none:
425
531
  break
426
532
  }
@@ -444,24 +550,22 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
444
550
  }
445
551
  }
446
552
 
447
- private func navigateToBrowsableUrl(_ url: String) {
448
- // Clear error synchronously before starting navigation (didSet notifies JS)
449
- lastNavigationError = nil
450
- lastFormattedNavigationError = nil
553
+ private func navigateToBrowsablePath(_ path: String) {
554
+ clearNavigationError()
451
555
  Task {
452
556
  do {
453
- try await browserManager.navigate(url)
557
+ try await browserManager.navigate(path)
454
558
  } catch {
455
- handleNavigationError(error, path: url)
559
+ handleNavigationError(error, path: path)
456
560
  }
457
561
  }
458
562
  }
459
563
 
460
- public func onSearch(query: String) throws -> Promise<[Track]> {
564
+ public func search(query: String) throws -> Promise<[Track]> {
461
565
  Promise.async { [weak self] in
462
566
  guard let self else { return [] }
463
567
  let resolved = try await browserManager.search(query)
464
- return resolved.children ?? []
568
+ return resolved.flattenedChildren ?? []
465
569
  }
466
570
  }
467
571
 
@@ -470,11 +574,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
470
574
  }
471
575
 
472
576
  public func getNavigationError() throws -> NavigationError? {
473
- lastNavigationError
577
+ onMainActor { lastNavigationError }
474
578
  }
475
579
 
476
580
  public func getFormattedNavigationError() throws -> FormattedNavigationError? {
477
- lastFormattedNavigationError
581
+ onMainActor { lastFormattedNavigationError }
478
582
  }
479
583
 
480
584
  /// Internal signal sent on `externalContentChangedEmitter` to tell CarPlay to
@@ -484,7 +588,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
484
588
  static let invalidateAllSentinel = "__rnab_invalidate_all__"
485
589
 
486
590
  public func notifyContentChanged(path: String) throws {
487
- onMainActor { browserManager.invalidateContentCache(path) }
591
+ // Throws on a contextual path (matching Android's `require`) — the caller
592
+ // must pass the container path, or the invalidation silently hits nothing.
593
+ try onMainActor { try browserManager.invalidateContentCache(path) }
488
594
 
489
595
  // Notify external controllers (CarPlay) that content changed
490
596
  externalContentChangedEmitter.emit(path)
@@ -601,7 +707,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
601
707
  // built-in so the gate page still renders rather than leaking content.
602
708
  return GateOutcome(gated: true, chrome: defaultChrome ?? Self.builtInGate)
603
709
  }
604
- guard decision.gated else { return GateOutcome(gated: false, chrome: nil) } // explicit allow
710
+ guard decision.gated else { return GateOutcome(gated: false, chrome: nil) } // explicit allow
605
711
  return GateOutcome(gated: true, chrome: decision.gate ?? defaultChrome ?? Self.builtInGate)
606
712
  }
607
713
 
@@ -649,7 +755,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
649
755
  category: AVAudioSession.Category,
650
756
  mode: AVAudioSession.Mode,
651
757
  policy: AVAudioSession.RouteSharingPolicy,
652
- options: AVAudioSession.CategoryOptions
758
+ options: AVAudioSession.CategoryOptions,
653
759
  ) = options.ios?.resolveAudioSessionConfig()
654
760
  ?? (category: .playback, mode: .default, policy: .default, options: [])
655
761
  // Capture category application once so a media-services reset (which
@@ -664,6 +770,16 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
664
770
  guard let self else { return }
665
771
  self.applyAudioSessionCategory = applyCategory
666
772
 
773
+ // A re-setup REPLACES the player, so the outgoing one has to be torn down
774
+ // and un-published first. `TrackPlayer` has no deinit: dropping the
775
+ // reference alone would leave its AVPlayer and MPNowPlayingSession alive.
776
+ // And `playerAndConfiguredBrowser` holds the old player strongly — its
777
+ // `resolve` is a one-shot, so without the reset the gate would keep
778
+ // handing the dead player to the CarPlay controller and Siri intents,
779
+ // which both await it. No-op on first setup.
780
+ player?.destroy()
781
+ playerAndConfiguredBrowser.reset()
782
+
667
783
  // Create player with self as callbacks delegate
668
784
  player = TrackPlayer(callbacks: self)
669
785
 
@@ -713,11 +829,16 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
713
829
  return await browserManager.resolveMediaUrl(src, track: track)
714
830
  }
715
831
 
716
- // Configure artwork URL resolver for Now Playing (with size context)
832
+ // Configure artwork URL resolver for Now Playing (with size context). The
833
+ // `nowPlayingArtwork` config runs for EVERY playing track, id or not — an id-less track
834
+ // leaves any `{id}` token unfilled (resolveArtworkUrl warns and the request visibly
835
+ // 404s) rather than skipping the config.
717
836
  player?.nowPlayingUpdater.artworkUrlResolver = { [weak self] track, imageContext in
718
837
  guard let self else { return nil }
719
- let perRoute = (track.id?.isEmpty == false) ? browserManager.config.nowPlayingArtwork : nil
720
- return await browserManager.resolveArtworkUrl(track: track, perRouteConfig: perRoute, imageContext: imageContext)
838
+ return await browserManager.resolveArtworkUrl(
839
+ track: track, perRouteConfig: browserManager.config.nowPlayingArtwork,
840
+ imageContext: imageContext,
841
+ )
721
842
  }
722
843
 
723
844
  // Emit the JS `onNowPlayingChanged` event when the rendered text lines change. The updater
@@ -733,16 +854,16 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
733
854
  self?.onSleepTimerChanged(state)
734
855
  }
735
856
 
736
- // Now-playing: whether to manage metadata, the optional formatter that owns the rendered
737
- // lines, and whether to keep the session alive through a terminal error.
857
+ // Now-playing: whether to manage metadata, and the optional formatter that owns the
858
+ // rendered lines.
738
859
  nowPlayingMetadataEnabled = options.autoUpdateNowPlayingMetadata ?? true
739
860
  nowPlayingMetadataFormatter = options.nowPlayingMetadataFormatter
740
- // Stored for API symmetry, but iOS needs no masking: a terminal error resolves the playback
741
- // state to *paused* (PlaybackCoordinator.applySideEffects), the now-playing info is retained
742
- // (never cleared on error), and next/previous stay enabled (gated on queue position, not
743
- // state). So the session stays controllable through errors by default — the behavior Android
744
- // achieves via InterceptingPlayer is already the iOS norm.
745
- keepSessionAliveOnError = options.keepSessionAliveOnError ?? true
861
+ // `options.keepSessionAliveOnError` is deliberately ignored here: iOS needs no masking.
862
+ // A terminal error resolves the playback state to *paused*
863
+ // (PlaybackCoordinator.applySideEffects), the now-playing info is retained (never cleared
864
+ // on error), and next/previous stay enabled (gated on queue position, not state). So the
865
+ // session stays controllable through errors by default — the behavior Android achieves via
866
+ // InterceptingPlayer is already the iOS norm.
746
867
 
747
868
  // The bundled runtime options and initial state are part of the atomic launch
748
869
  // description. Stage the state first so setup options and earlier pre-setup setter
@@ -758,6 +879,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
758
879
  if let intent = options.playWhenReady { pendingPlayWhenReady = intent }
759
880
  applyPendingPlayerState()
760
881
 
882
+ // Push the resolved event cadence unconditionally. `updateOptions` applies
883
+ // the interval only when it *differs* from the stored one, but the store
884
+ // outlives the player — so on a re-setup with unchanged options the new
885
+ // player would come up with no progress timer at all.
886
+ player?.setProgressUpdateInterval(playerOptions.progressUpdateEventInterval)
887
+ player?.setPlaybackIntervalEnabled(playbackIntervalEnabled)
888
+
761
889
  // Notify listeners that player is ready (e.g., CarPlay)
762
890
  playerAndConfiguredBrowser.check()
763
891
  }
@@ -787,9 +915,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
787
915
  // Update stored options
788
916
  playerOptions.update(from: options)
789
917
 
790
- // Propagate the favorite match mode to the browser so it can hydrate
918
+ // Propagate the favorite flag to the browser so it can hydrate
791
919
  // row hearts (the `favorite` capability is the single favoriting switch).
792
- browserManager.setFavoriteMatch(playerOptions.capabilities.favoriteMatch)
920
+ browserManager.setFavoriteEnabled(playerOptions.capabilities.favoriteEnabled)
793
921
 
794
922
  // Apply remote commands to player
795
923
  applyRemoteCommands()
@@ -819,11 +947,19 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
819
947
  }
820
948
 
821
949
  public func getOptions() throws -> Options {
822
- playerOptions.toOptions()
950
+ onMainActor { playerOptions.toOptions() }
823
951
  }
824
952
 
953
+ /// Stored so a re-setup can re-apply it: unlike the other runtime options this
954
+ /// one has no home in `playerOptions`, and the player it was pushed to does not
955
+ /// survive `setupPlayer`.
956
+ @MainActor private var playbackIntervalEnabled = false
957
+
825
958
  public func setPlaybackIntervalEnabled(enabled: Bool) {
826
- onMainActor { player?.setPlaybackIntervalEnabled(enabled) }
959
+ onMainActor {
960
+ playbackIntervalEnabled = enabled
961
+ player?.setPlaybackIntervalEnabled(enabled)
962
+ }
827
963
  }
828
964
 
829
965
  // MARK: - Playback Control
@@ -1037,24 +1173,25 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1037
1173
  }
1038
1174
 
1039
1175
  public func setActiveTrackFavorited(favorited: Bool) throws {
1040
- onMainActor {
1041
- guard let track = player?.currentTrack, let src = track.src else { return }
1176
+ try onMainActor {
1177
+ guard let track = player?.currentTrack, let identity = track.identity else { return }
1042
1178
  guard let index = player?.currentIndex, index >= 0 else { return }
1043
- // Optimistically reflect in the authoritative match set so the now-playing
1044
- // heart (hydrated from it in getActiveTrack) flips immediately; native
1045
- // reconciles to its canonical ids on the next setFavorites.
1046
- browserManager.setFavorited(src: src, favorited: favorited)
1179
+ // Optimistically reflect in the authoritative favorite set so the
1180
+ // now-playing heart (hydrated from it in getActiveTrack) flips
1181
+ // immediately; the consumer reconciles to its canonical identities on
1182
+ // the next setFavorites.
1183
+ browserManager.setFavorited(identity: identity, favorited: favorited)
1047
1184
  let updatedTrack = track.copying(favorited: favorited)
1185
+ // Hydration never overrides a caller-set `favorited`, so the queued
1186
+ // track must itself carry the latest explicit value (as Android's tag
1187
+ // swap and web's replaceTrack do).
1188
+ try player?.replace(index, updatedTrack)
1189
+ // The ONLY emit: a favorite toggle is an in-place mutation of the active
1190
+ // track, not a transition — onPlaybackActiveTrackChanged stays
1191
+ // transition-only, and the useActiveTrack / useQueue hooks subscribe to
1192
+ // onFavoriteChanged themselves. Native surfaces (CarPlay heart) observe
1193
+ // favoriteChangedEmitter / onFavoritesChanged directly.
1048
1194
  favoriteChangedEmitter.emit(FavoriteChangedEvent(track: updatedTrack, favorited: favorited))
1049
- // Fire active track changed so the now-playing heart + useActiveTrack() refresh.
1050
- let position = player?.currentTime ?? 0
1051
- activeTrackChangedEmitter.emit(PlaybackActiveTrackChangedEvent(
1052
- lastIndex: Double(index),
1053
- lastTrack: track,
1054
- lastPosition: position,
1055
- index: Double(index),
1056
- track: updatedTrack,
1057
- ))
1058
1195
  }
1059
1196
  }
1060
1197
 
@@ -1121,11 +1258,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1121
1258
  /// metadata can't stomp it mid-flash. Reverted by a NATIVE timer — JS
1122
1259
  /// timers pause with a backgrounded host on Android, and the lock screen
1123
1260
  /// is exactly the backgrounded case — and cleared early on track change.
1124
- private var nowPlayingFlash: NowPlayingUpdate?
1125
- private var nowPlayingFlashRevert: DispatchWorkItem?
1261
+ @MainActor private var nowPlayingFlash: NowPlayingUpdate?
1262
+ @MainActor private var nowPlayingFlashRevert: DispatchWorkItem?
1126
1263
 
1127
1264
  /// Pending debounced audio-session release (see `playerShouldReleaseSession`).
1128
- private var sessionReleaseWork: DispatchWorkItem?
1265
+ @MainActor private var sessionReleaseWork: DispatchWorkItem?
1129
1266
 
1130
1267
  public func flashNowPlaying(update: NowPlayingUpdate, durationMs: Double) throws {
1131
1268
  onMainActor {
@@ -1151,6 +1288,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1151
1288
  }
1152
1289
  }
1153
1290
 
1291
+ @MainActor
1154
1292
  private func cancelNowPlayingFlash() {
1155
1293
  nowPlayingFlashRevert?.cancel()
1156
1294
  nowPlayingFlashRevert = nil
@@ -1193,7 +1331,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1193
1331
  duration: track.duration,
1194
1332
  artwork: nowPlayingArtwork(for: track),
1195
1333
  description: track.description,
1196
- mediaId: track.src ?? track.url,
1334
+ mediaId: track.src ?? track.path,
1197
1335
  genre: track.genre,
1198
1336
  )
1199
1337
  }
@@ -1206,7 +1344,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1206
1344
  // Classify a stall by connectivity so the formatter can show "No internet connection" vs
1207
1345
  // "Reconnecting…" without a separate read. nil while not stalled.
1208
1346
  let stalled: StallReason? =
1209
- player.isStalled ? (networkMonitor.getOnline() ? .buffering : .offline) : nil
1347
+ player.isStalled ? (networkMonitor.isOnline ? .buffering : .offline) : nil
1210
1348
  player.nowPlayingUpdater.render(
1211
1349
  track: track,
1212
1350
  timedMetadata: latestTimedMetadata,
@@ -1222,7 +1360,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1222
1360
  // MARK: - Network
1223
1361
 
1224
1362
  public func getOnline() throws -> Bool {
1225
- networkMonitor.getOnline()
1363
+ // No `onMainActor` hop: `isOnline` is lock-protected, so this stays a
1364
+ // cheap read on the JS thread instead of blocking on the main queue.
1365
+ networkMonitor.isOnline
1226
1366
  }
1227
1367
 
1228
1368
  // MARK: - System Volume
@@ -1354,8 +1494,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1354
1494
  guard let self else { return }
1355
1495
  // Re-apply our category (the reset cleared it), then have the player
1356
1496
  // recreate itself and reconnect the current stream.
1357
- self.applyAudioSessionCategory()
1358
- self.onMainActor { self.player?.handleMediaServicesReset() }
1497
+ self.onMainActor {
1498
+ self.applyAudioSessionCategory()
1499
+ self.player?.handleMediaServicesReset()
1500
+ }
1359
1501
  }
1360
1502
  }
1361
1503
 
@@ -1394,17 +1536,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1394
1536
 
1395
1537
  public func supportsOutputSwitcher() throws -> Bool {
1396
1538
  // iOS can always present the system route picker (AVRoutePickerView).
1397
- return true
1539
+ true
1398
1540
  }
1399
1541
 
1400
1542
  public func openOutputPicker() throws {
1401
1543
  DispatchQueue.main.async { [weak self] in
1402
1544
  guard let self else { return }
1403
- guard let windowScene = UIApplication.shared.connectedScenes
1404
- .compactMap({ $0 as? UIWindowScene })
1405
- .first(where: { $0.activationState == .foregroundActive }),
1406
- let window = windowScene.windows.first(where: { $0.isKeyWindow }),
1407
- var topController = window.rootViewController
1545
+ guard let windowScene = UIApplication.shared.activeWindowScene,
1546
+ let window = windowScene.windows.first(where: { $0.isKeyWindow }),
1547
+ var topController = window.rootViewController
1408
1548
  else {
1409
1549
  self.logger.error("openOutputPicker: no active window scene / root view controller")
1410
1550
  return
@@ -1586,6 +1726,7 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1586
1726
  DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: release)
1587
1727
  }
1588
1728
 
1729
+ @MainActor
1589
1730
  private func cancelSessionRelease() {
1590
1731
  sessionReleaseWork?.cancel()
1591
1732
  sessionReleaseWork = nil
@@ -1619,7 +1760,7 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1619
1760
 
1620
1761
  public func playerDidReceiveTimedMetadata(_ groups: [AVTimedMetadataGroup]) {
1621
1762
  for group in groups {
1622
- if let metadata = TimedMetadata.from(items: group.items) {
1763
+ if let metadata = TimedMetadata.from(items: group.items, icyCharset: player?.currentTrack?.icyCharset) {
1623
1764
  latestTimedMetadata = metadata
1624
1765
  onTimedMetadata(metadata)
1625
1766
  // Re-render the now-playing so the formatter can reflect the live song.
@@ -1779,14 +1920,26 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1779
1920
  browser.showNowPlayingRequestedEmitter.emit(())
1780
1921
  completion(true)
1781
1922
  } else if let state = browser.playbackStateStore.load() {
1782
- browser.logger.info("resume: cold — restoring persisted track")
1783
1923
  let track = state.track.toNitro()
1924
+ // The persisted track can carry `disabled` — it became unavailable
1925
+ // after it was saved. A disabled track never plays, resumption
1926
+ // included (Track.disabled).
1927
+ if track.disabled == true {
1928
+ browser.logger.info("resume: persisted track is disabled — refusing")
1929
+ completion(false)
1930
+ return
1931
+ }
1932
+ browser.logger.info("resume: cold — restoring persisted track")
1933
+ // Repeat / shuffle / speed come back before the queue is built — see
1934
+ // `restorePlaybackSettings(from:)` for why that ordering matters.
1935
+ player.restorePlaybackSettings(from: state)
1784
1936
  let startMs = (track.live == true) ? nil : state.positionMs
1785
1937
  // Match Android resume: re-expand the full queue from the track's contextual
1786
- // URL (parent container → siblings + selected index). Fall back to the single
1787
- // track when the url isn't contextual or expansion fails.
1788
- if let url = state.track.url,
1789
- let expanded = try? await browser.browserManager.expandQueueFromContextualUrl(url) {
1938
+ // path (parent container → siblings + selected index). Fall back to the single
1939
+ // track when the path isn't contextual or expansion fails.
1940
+ if let path = state.track.path,
1941
+ let expanded = try? await browser.browserManager.expandQueueFromContextualPath(path)
1942
+ {
1790
1943
  player.setQueue(expanded.tracks, initialIndex: expanded.selectedIndex, startPositionMs: startMs, playWhenReady: true)
1791
1944
  } else {
1792
1945
  player.setQueue([track], initialIndex: 0, startPositionMs: startMs, playWhenReady: true)
@@ -1795,7 +1948,7 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1795
1948
  completion(true)
1796
1949
  } else {
1797
1950
  browser.logger.error("resume: nothing playing and nothing persisted → no-op")
1798
- completion(false) // nothing playing and nothing persisted
1951
+ completion(false) // nothing playing and nothing persisted
1799
1952
  }
1800
1953
  return
1801
1954
  }
@@ -1812,14 +1965,14 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1812
1965
  title: criteria.title,
1813
1966
  playlist: criteria.playlist,
1814
1967
  // .currentlyPlaying can't reach here — isResume routed it to resume.
1815
- reference: criteria.reference == .my ? .my : .unknown
1968
+ reference: criteria.reference == .my ? .my : .unknown,
1816
1969
  )
1817
1970
 
1818
1971
  // Search is *finding* new content — refused when the gate decides this
1819
1972
  // request is gated (resume above is unaffected: the gate never blocks
1820
1973
  // hearing, and the gate check stays after the resume check).
1821
1974
  let outcome = await browser.gateDecision(
1822
- for: NativeGateRequest(reason: .search, path: nil, search: params)
1975
+ for: NativeGateRequest(reason: .search, path: nil, search: params),
1823
1976
  )
1824
1977
  guard !outcome.gated else {
1825
1978
  browser.logger.info("handlePlayMediaIntent: search refused — gated")