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
@@ -0,0 +1,94 @@
1
+ package com.audiobrowser.player
2
+
3
+ import okhttp3.HttpUrl.Companion.toHttpUrl
4
+ import okhttp3.Request
5
+ import org.junit.Assert.assertEquals
6
+ import org.junit.Assert.assertFalse
7
+ import org.junit.Assert.assertNull
8
+ import org.junit.Assert.assertTrue
9
+ import org.junit.Test
10
+
11
+ /**
12
+ * OkHttp drops `Authorization` across a cross-origin redirect by itself; these cover the rest,
13
+ * which it forwards and [CrossOriginHeaderStripper.stripCallerHeaders] removes.
14
+ */
15
+ class CrossOriginHeaderStripperTest {
16
+
17
+ private val api = "https://api.example.com/stream/7".toHttpUrl()
18
+
19
+ @Test
20
+ fun `same origin ignores the path`() {
21
+ assertTrue(
22
+ CrossOriginHeaderStripper.sameOrigin(
23
+ api,
24
+ "https://api.example.com/signed/track.mp3".toHttpUrl(),
25
+ )
26
+ )
27
+ }
28
+
29
+ @Test
30
+ fun `a different host, port or scheme is a different origin`() {
31
+ assertFalse(
32
+ CrossOriginHeaderStripper.sameOrigin(api, "https://cdn.example.com/track.mp3".toHttpUrl())
33
+ )
34
+ assertFalse(
35
+ CrossOriginHeaderStripper.sameOrigin(
36
+ api,
37
+ "https://api.example.com:8443/track.mp3".toHttpUrl(),
38
+ )
39
+ )
40
+ assertFalse(
41
+ CrossOriginHeaderStripper.sameOrigin(api, "http://api.example.com/track.mp3".toHttpUrl())
42
+ )
43
+ }
44
+
45
+ @Test
46
+ fun `strips every caller header, whatever its name`() {
47
+ val request =
48
+ Request.Builder()
49
+ .url(api)
50
+ .header("Authorization", "Bearer token")
51
+ .header("X-Api-Key", "k")
52
+ .header("Cookie", "session=abc")
53
+ .build()
54
+
55
+ val stripped = CrossOriginHeaderStripper.stripCallerHeaders(request)
56
+
57
+ assertNull(stripped.header("Authorization"))
58
+ assertNull(stripped.header("X-Api-Key"))
59
+ assertNull(stripped.header("Cookie"))
60
+ }
61
+
62
+ @Test
63
+ fun `keeps the transport headers the hop needs`() {
64
+ val request =
65
+ Request.Builder()
66
+ .url(api)
67
+ .header("Range", "bytes=1024-")
68
+ .header("accept-encoding", "identity")
69
+ .header("User-Agent", "react-native-audio-browser")
70
+ .header("X-Api-Key", "k")
71
+ .build()
72
+
73
+ val stripped = CrossOriginHeaderStripper.stripCallerHeaders(request)
74
+
75
+ // a dropped Range would restart the read from zero on every seek past a redirect
76
+ assertEquals("bytes=1024-", stripped.header("Range"))
77
+ assertEquals("identity", stripped.header("Accept-Encoding"))
78
+ assertEquals("react-native-audio-browser", stripped.header("User-Agent"))
79
+ assertNull(stripped.header("X-Api-Key"))
80
+ }
81
+
82
+ @Test
83
+ fun `keeps media3's ICY metadata request across a hop`() {
84
+ // ProgressiveMediaPeriod asks for Shoutcast metadata on the DataSpec, the same channel the
85
+ // config's headers arrive on. Dropping it costs icy-metaint, so onMetadata never fires again.
86
+ val request =
87
+ Request.Builder().url(api).header("Icy-MetaData", "1").header("X-Api-Key", "k").build()
88
+
89
+ val stripped = CrossOriginHeaderStripper.stripCallerHeaders(request)
90
+
91
+ assertEquals("1", stripped.header("Icy-MetaData"))
92
+ assertNull(stripped.header("X-Api-Key"))
93
+ }
94
+ }
@@ -0,0 +1,45 @@
1
+ package com.audiobrowser.player
2
+
3
+ import androidx.media3.session.CommandButton
4
+ import org.junit.Assert.assertArrayEquals
5
+ import org.junit.Assert.assertTrue
6
+ import org.junit.Test
7
+
8
+ /**
9
+ * Guards the slot preference lists handed to Media3.
10
+ *
11
+ * The invariant that matters is the OVERFLOW fallback: every flattening keeps a button only if it
12
+ * won SLOT_BACK, won SLOT_FORWARD, or names SLOT_OVERFLOW. A slot that stops naming OVERFLOW
13
+ * silently deletes its button from the notification and the car the moment its primary position is
14
+ * unavailable, so the loop below asserts the property for every slot rather than only today's
15
+ * three.
16
+ */
17
+ class MediaSessionSlotMappingTest {
18
+
19
+ @Test
20
+ fun `primary slots are preferred with an overflow fallback`() {
21
+ assertArrayEquals(
22
+ intArrayOf(CommandButton.SLOT_BACK, CommandButton.SLOT_OVERFLOW),
23
+ media3SlotsFor(ButtonSlot.BACK),
24
+ )
25
+ assertArrayEquals(
26
+ intArrayOf(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW),
27
+ media3SlotsFor(ButtonSlot.FORWARD),
28
+ )
29
+ }
30
+
31
+ @Test
32
+ fun `overflow does not repeat itself`() {
33
+ assertArrayEquals(intArrayOf(CommandButton.SLOT_OVERFLOW), media3SlotsFor(ButtonSlot.OVERFLOW))
34
+ }
35
+
36
+ @Test
37
+ fun `every slot names OVERFLOW so nothing can be dropped`() {
38
+ for (slot in ButtonSlot.entries) {
39
+ assertTrue(
40
+ "$slot must name SLOT_OVERFLOW or its button is dropped from the notification and the car",
41
+ media3SlotsFor(slot).contains(CommandButton.SLOT_OVERFLOW),
42
+ )
43
+ }
44
+ }
45
+ }
@@ -377,7 +377,7 @@ class NowPlayingUpdaterTest {
377
377
  }
378
378
 
379
379
  @Test
380
- fun `artwork re-resolves for a new track id and skips without an id`() = runTest {
380
+ fun `artwork re-resolves for a new track identity including id-less tracks`() = runTest {
381
381
  val surface =
382
382
  FakeSurface().apply {
383
383
  currentTrack = track("A", id = "a")
@@ -393,10 +393,31 @@ class NowPlayingUpdaterTest {
393
393
  runCurrent()
394
394
  assertEquals(2, surface.artworkResolveCount)
395
395
 
396
+ // The config runs for id-less tracks too — no blanket skip.
396
397
  surface.currentTrack = track("C", id = null, src = "https://s/c.mp3")
397
398
  updater.render()
398
399
  runCurrent()
399
- assertEquals(2, surface.artworkResolveCount) // no id → no resolve
400
+ assertEquals(3, surface.artworkResolveCount)
401
+ assertEquals(0 to "https://img/a.png", surface.artworkStamps.last())
402
+ }
403
+
404
+ @Test
405
+ fun `artwork for an id-less track resolves once keyed on src identity`() = runTest {
406
+ val surface =
407
+ FakeSurface().apply {
408
+ currentTrack = track("C", id = null, src = "https://s/c.mp3")
409
+ hasNowPlayingArtworkConfig = true
410
+ artworkResult = "https://img/c.png"
411
+ }
412
+ val updater = NowPlayingUpdater(surface, backgroundScope)
413
+
414
+ updater.render()
415
+ runCurrent()
416
+ updater.updateNowPlaying(update(title = "Song")) // re-render, same track
417
+ runCurrent()
418
+
419
+ assertEquals(1, surface.artworkResolveCount)
420
+ assertEquals(listOf(0 to "https://img/c.png"), surface.artworkStamps)
400
421
  }
401
422
 
402
423
  @Test
@@ -439,4 +460,23 @@ class NowPlayingUpdaterTest {
439
460
 
440
461
  assertTrue(surface.artworkStamps.isEmpty())
441
462
  }
463
+
464
+ @Test
465
+ fun `a stale artwork result for an id-less track is not stamped after a track change`() =
466
+ runTest {
467
+ val surface =
468
+ FakeSurface().apply {
469
+ currentTrack = track("A", id = null, src = "https://s/a.mp3")
470
+ hasNowPlayingArtworkConfig = true
471
+ artworkResult = "https://img/a.png"
472
+ }
473
+ val updater = NowPlayingUpdater(surface, backgroundScope)
474
+
475
+ updater.render() // schedules the artwork resolve
476
+ // Skip to another id-less track before it lands: identity (src) keying must catch it.
477
+ surface.currentTrack = track("B", id = null, src = "https://s/b.mp3")
478
+ runCurrent()
479
+
480
+ assertTrue(surface.artworkStamps.isEmpty())
481
+ }
442
482
  }
@@ -132,8 +132,8 @@ class PlaybackErrorClassifierTest {
132
132
 
133
133
  /**
134
134
  * Load errors are raw IOExceptions from the transport layer; the policy only retries network
135
- * ones, so the advisory classification is UNREACHABLE unless the device is offline or the
136
- * server actually answered.
135
+ * ones, so the advisory classification is UNREACHABLE unless the device is offline or the server
136
+ * actually answered.
137
137
  */
138
138
  @Test
139
139
  fun `retrying transport failure is unreachable`() {
@@ -158,8 +158,7 @@ class PlaybackErrorClassifierTest {
158
158
  /** The HTTP status is dug out of the cause chain and mapped through the shared status table. */
159
159
  @Test
160
160
  fun `retrying HTTP failure carries the status`() {
161
- val error =
162
- PlaybackErrorClassifier.retryingLoadError(httpException(503), online = true)
161
+ val error = PlaybackErrorClassifier.retryingLoadError(httpException(503), online = true)
163
162
  assertEquals(PlaybackErrorKind.SERVER_ERROR, error.kind)
164
163
  assertEquals(503.0, error.statusCode!!, 0.0)
165
164
  assertEquals(true, error.retrying)
@@ -0,0 +1,55 @@
1
+ package com.audiobrowser.player
2
+
3
+ import com.audiobrowser.TestFixtures
4
+ import com.audiobrowser.TestFixtures.trackStyle
5
+ import com.margelo.nitro.audiobrowser.ArtworkRendering
6
+ import com.margelo.nitro.audiobrowser.StyleDisplay
7
+ import org.junit.Assert.assertEquals
8
+ import org.junit.Assert.assertNull
9
+ import org.junit.Test
10
+ import org.junit.runner.RunWith
11
+ import org.robolectric.RobolectricTestRunner
12
+
13
+ /**
14
+ * The bespoke org.json persistence of the resumption track. ADR 0011: the style block and
15
+ * `disabled` survive process death (a restored queue keeps its visible fidelity), and a snapshot
16
+ * written before the block model — `style` as an enum-name string — restores without a declaration
17
+ * rather than failing.
18
+ */
19
+ @RunWith(RobolectricTestRunner::class)
20
+ class PlaybackStateStoreTrackJsonTest {
21
+
22
+ @Test
23
+ fun `style and disabled survive the round-trip`() {
24
+ val track =
25
+ TestFixtures.track(title = "Night Mix", src = "https://s/a.mp3", disabled = true)
26
+ .copy(
27
+ style =
28
+ trackStyle(display = StyleDisplay.GRID, artworkRendering = ArtworkRendering.STENCIL)
29
+ )
30
+
31
+ val restored = PlaybackStateStore.trackFromJson(PlaybackStateStore.trackToJson(track))
32
+
33
+ assertEquals(StyleDisplay.GRID, restored?.style?.display)
34
+ assertEquals(ArtworkRendering.STENCIL, restored?.style?.artworkRendering)
35
+ assertEquals(true, restored?.disabled)
36
+ }
37
+
38
+ @Test
39
+ fun `a track without style round-trips without one`() {
40
+ val track = TestFixtures.track(title = "Plain", src = "https://s/a.mp3")
41
+
42
+ val restored = PlaybackStateStore.trackFromJson(PlaybackStateStore.trackToJson(track))
43
+
44
+ assertNull(restored?.style)
45
+ assertNull(restored?.disabled)
46
+ }
47
+
48
+ @Test
49
+ fun `a pre-block snapshot with a string style restores without a declaration`() {
50
+ val restored = PlaybackStateStore.trackFromJson("""{"title":"X","src":"s","style":"LIST"}""")
51
+
52
+ assertEquals("X", restored?.title)
53
+ assertNull(restored?.style)
54
+ }
55
+ }
@@ -201,9 +201,9 @@ class RetryLoadErrorHandlingPolicyBudgetTest {
201
201
  }
202
202
 
203
203
  /**
204
- * Offline failures must not burn the first-connect budget (a station tapped in a tunnel parks
205
- * for connectivity); the budget's clock starts at the first error observed online, so a dead
206
- * station still gets a fast verdict after restoration.
204
+ * Offline failures must not burn the first-connect budget (a station tapped in a tunnel parks for
205
+ * connectivity); the budget's clock starts at the first error observed online, so a dead station
206
+ * still gets a fast verdict after restoration.
207
207
  */
208
208
  @Test
209
209
  fun `offline time does not burn the first-connect budget`() {
@@ -0,0 +1,79 @@
1
+ package com.audiobrowser.player
2
+
3
+ import androidx.media3.common.C
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Assert.assertNotEquals
6
+ import org.junit.Assert.assertTrue
7
+ import org.junit.Test
8
+
9
+ /**
10
+ * Media3's `ShuffleOrder.cloneAndSet(insertionCount, startIndex)` discards the start index (the
11
+ * interface default clears and re-inserts from 0, and `DefaultShuffleOrder` does not override it),
12
+ * so `setMediaItems(items, startIndex, …)` leaves the starting track at a random shuffle position —
13
+ * last, for a 1-in-N share of queues, which reads as end-of-queue from the very first track.
14
+ */
15
+ class ShuffleOrderLedByTest {
16
+ @Test
17
+ fun `the start index leads the order`() {
18
+ for (start in 0 until 6) {
19
+ val order = shuffleOrderLedBy(startIndex = start, count = 6)
20
+ assertEquals("start index must be at shuffle position 0", start, order.firstIndex)
21
+ assertNotEquals(
22
+ "a queue of 6 must have somewhere to advance to from its start track",
23
+ C.INDEX_UNSET.toLong(),
24
+ order.getNextIndex(start).toLong(),
25
+ )
26
+ }
27
+ }
28
+
29
+ @Test
30
+ fun `every index appears exactly once`() {
31
+ val order = shuffleOrderLedBy(startIndex = 3, count = 8)
32
+ assertEquals(8, order.length)
33
+ val seen = mutableListOf<Int>()
34
+ var index = order.firstIndex
35
+ while (index != C.INDEX_UNSET) {
36
+ seen.add(index)
37
+ index = order.getNextIndex(index)
38
+ }
39
+ assertEquals((0 until 8).toList(), seen.sorted())
40
+ }
41
+
42
+ @Test
43
+ fun `a single-track queue leads with its only track and ends there`() {
44
+ val order = shuffleOrderLedBy(startIndex = 0, count = 1)
45
+ assertEquals(0, order.firstIndex)
46
+ assertEquals(C.INDEX_UNSET, order.getNextIndex(0))
47
+ }
48
+
49
+ @Test
50
+ fun `an out-of-range start index is clamped rather than throwing`() {
51
+ assertEquals(4, shuffleOrderLedBy(startIndex = 99, count = 5).firstIndex)
52
+ assertEquals(0, shuffleOrderLedBy(startIndex = -3, count = 5).firstIndex)
53
+ }
54
+
55
+ @Test
56
+ fun `an empty queue produces an empty order`() {
57
+ assertEquals(0, shuffleOrderLedBy(startIndex = 0, count = 0).length)
58
+ }
59
+
60
+ @Test
61
+ fun `the tail is actually shuffled`() {
62
+ // Not a strict guarantee of any single draw — but across many draws an
63
+ // unshuffled tail would be impossible to miss.
64
+ val tails =
65
+ (0 until 50)
66
+ .map { shuffleOrderLedBy(startIndex = 0, count = 8) }
67
+ .map { order ->
68
+ buildList {
69
+ var index = order.getNextIndex(order.firstIndex)
70
+ while (index != C.INDEX_UNSET) {
71
+ add(index)
72
+ index = order.getNextIndex(index)
73
+ }
74
+ }
75
+ }
76
+ .toSet()
77
+ assertTrue("50 draws produced one identical tail — the order is not shuffled", tails.size > 1)
78
+ }
79
+ }
@@ -7,12 +7,98 @@ import java.security.cert.PKIXParameters
7
7
  import java.security.cert.TrustAnchor
8
8
  import java.security.cert.X509Certificate
9
9
  import java.util.Date
10
+ import javax.security.auth.x500.X500Principal
10
11
  import org.junit.Assert.assertEquals
11
12
  import org.junit.Assert.assertThrows
12
13
  import org.junit.Test
14
+ import org.mockito.Mockito.mock
15
+ import org.mockito.Mockito.`when`
13
16
 
14
17
  class AiaCertChaserTest {
15
18
 
19
+ /**
20
+ * A certificate whose AIA extension value is exactly [extensionValue].
21
+ *
22
+ * Subject and issuer are stubbed to distinct principals on purpose: an unstubbed mock answers
23
+ * null to both, which `completeChain` would read as a self-signed root and stop on before
24
+ * fetching anything.
25
+ */
26
+ private fun certWithAia(extensionValue: ByteArray): X509Certificate =
27
+ mock(X509Certificate::class.java).also {
28
+ `when`(it.getExtensionValue("1.3.6.1.5.5.7.1.1")).thenReturn(extensionValue)
29
+ `when`(it.subjectX500Principal).thenReturn(X500Principal("CN=leaf"))
30
+ `when`(it.issuerX500Principal).thenReturn(X500Principal("CN=issuer"))
31
+ }
32
+
33
+ /** A DER tag-length-value, with the length in whichever form fits. */
34
+ private fun der(tag: Int, content: ByteArray): ByteArray {
35
+ val length =
36
+ when (val n = content.size) {
37
+ in 0..0x7F -> byteArrayOf(n.toByte())
38
+ in 0x80..0xFF -> byteArrayOf(0x81.toByte(), n.toByte())
39
+ in 0x100..0xFFFF -> byteArrayOf(0x82.toByte(), (n shr 8).toByte(), n.toByte())
40
+ else -> byteArrayOf(0x83.toByte(), (n shr 16).toByte(), (n shr 8).toByte(), n.toByte())
41
+ }
42
+ return byteArrayOf(tag.toByte()) + length + content
43
+ }
44
+
45
+ /** A well-formed AIA extension value listing [urls] as caIssuers locations. */
46
+ private fun aiaExtension(urls: List<String>): ByteArray {
47
+ val caIssuersOid = byteArrayOf(0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02)
48
+ val descriptions =
49
+ urls.fold(ByteArray(0)) { acc, url ->
50
+ acc + der(0x30, der(0x06, caIssuersOid) + der(0x86, url.toByteArray(Charsets.US_ASCII)))
51
+ }
52
+ return der(0x04, der(0x30, descriptions))
53
+ }
54
+
55
+ @Test
56
+ fun `the AIA fixture builder round-trips through the parser`() {
57
+ // Guards the two tests below: if this builder were malformed they would pass by parsing
58
+ // nothing at all, rather than by the cap doing its job.
59
+ val urls = listOf("http://ca1.example/i.crt", "http://ca2.example/i.crt")
60
+
61
+ assertEquals(urls, AiaCertChaser.extractCaIssuerUrls(certWithAia(aiaExtension(urls))))
62
+ }
63
+
64
+ @Test(timeout = 5_000)
65
+ fun `a negative long-form length does not hang the parser`() {
66
+ // OCTET STRING { SEQUENCE { tag 0x00, long-form length 0xFFFFFFFA = -6 } }.
67
+ // The length's four bytes shift into a negative Int, so `pos += len` rewinds the
68
+ // reader by exactly the six bytes it just consumed: the tag is not a SEQUENCE, the
69
+ // loop `continue`s, and the reader is back where it started with more input to read.
70
+ val aia =
71
+ byteArrayOf(
72
+ 0x04,
73
+ 0x08,
74
+ 0x30,
75
+ 0x06,
76
+ 0x00,
77
+ 0x84.toByte(),
78
+ 0xFF.toByte(),
79
+ 0xFF.toByte(),
80
+ 0xFF.toByte(),
81
+ 0xFA.toByte(),
82
+ )
83
+
84
+ assertEquals(emptyList<String>(), AiaCertChaser.extractCaIssuerUrls(certWithAia(aia)))
85
+ }
86
+
87
+ @Test(timeout = 5_000)
88
+ fun `a length running past the buffer is treated as no AIA`() {
89
+ // SEQUENCE claiming 0x7F content bytes inside a 2-byte buffer.
90
+ val aia = byteArrayOf(0x04, 0x04, 0x30, 0x02, 0x30, 0x7F)
91
+
92
+ assertEquals(emptyList<String>(), AiaCertChaser.extractCaIssuerUrls(certWithAia(aia)))
93
+ }
94
+
95
+ @Test(timeout = 5_000)
96
+ fun `truncated DER is treated as no AIA`() {
97
+ val aia = byteArrayOf(0x04, 0x08, 0x30)
98
+
99
+ assertEquals(emptyList<String>(), AiaCertChaser.extractCaIssuerUrls(certWithAia(aia)))
100
+ }
101
+
16
102
  @Test
17
103
  fun `extracts CA Issuers URL from real leaf certificate AIA extension`() {
18
104
  val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
@@ -27,7 +113,7 @@ class AiaCertChaserTest {
27
113
  val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
28
114
  val r13 = CertFixtures.cert("letsencrypt-r13.pem")
29
115
  // Fetcher knows only the R13 URL (the leaf's AIA pointer); empty for the root URL.
30
- val fetch = { url: String ->
116
+ val fetch = { url: String, _: Long ->
31
117
  if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
32
118
  }
33
119
 
@@ -44,8 +130,8 @@ class AiaCertChaserTest {
44
130
 
45
131
  // A PKCS#7-style bundle where the genuine issuer is not first must still be selected.
46
132
  val completed =
47
- AiaCertChaser.completeChain(listOf(leaf)) {
48
- if (it == "http://r13.i.lencr.org/") listOf(unrelated, r13) else emptyList()
133
+ AiaCertChaser.completeChain(listOf(leaf)) { url, _ ->
134
+ if (url == "http://r13.i.lencr.org/") listOf(unrelated, r13) else emptyList()
49
135
  }
50
136
 
51
137
  assertEquals(listOf(leaf, r13), completed)
@@ -57,7 +143,7 @@ class AiaCertChaserTest {
57
143
  var fetches = 0
58
144
 
59
145
  val completed =
60
- AiaCertChaser.completeChain(listOf(root)) {
146
+ AiaCertChaser.completeChain(listOf(root)) { _, _ ->
61
147
  fetches++
62
148
  emptyList()
63
149
  }
@@ -66,11 +152,115 @@ class AiaCertChaserTest {
66
152
  assertEquals(0, fetches)
67
153
  }
68
154
 
155
+ @Test
156
+ fun `completeChain stops at a chain that already reaches a trust anchor`() {
157
+ val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
158
+ val r13 = CertFixtures.cert("letsencrypt-r13.pem")
159
+ val root = CertFixtures.cert("isrg-root-x1.pem")
160
+ val fetched = mutableListOf<String>()
161
+
162
+ // R13's own AIA points at the ISRG root, so without knowing the anchors this walks one hop
163
+ // too far and pays for a second blocking fetch on the ordinary rescue path.
164
+ val completed =
165
+ AiaCertChaser.completeChain(
166
+ listOf(leaf),
167
+ anchorSubjects = setOf(root.subjectX500Principal),
168
+ ) { url, _ ->
169
+ fetched.add(url)
170
+ if (url == "http://r13.i.lencr.org/") listOf(r13) else listOf(root)
171
+ }
172
+
173
+ assertEquals(listOf(leaf, r13), completed)
174
+ assertEquals(listOf("http://r13.i.lencr.org/"), fetched)
175
+ }
176
+
177
+ @Test
178
+ fun `completeChain fetches nothing when the presented chain is already anchored`() {
179
+ val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
180
+ val r13 = CertFixtures.cert("letsencrypt-r13.pem")
181
+ val root = CertFixtures.cert("isrg-root-x1.pem")
182
+ var fetches = 0
183
+
184
+ // The shape of every non-path failure — expiry, hostname mismatch, CT — where the server
185
+ // sent a complete chain and validation failed for some other reason.
186
+ val completed =
187
+ AiaCertChaser.completeChain(
188
+ listOf(leaf, r13),
189
+ anchorSubjects = setOf(root.subjectX500Principal),
190
+ ) { _, _ ->
191
+ fetches++
192
+ emptyList()
193
+ }
194
+
195
+ assertEquals(listOf(leaf, r13), completed)
196
+ assertEquals(0, fetches)
197
+ }
198
+
199
+ @Test
200
+ fun `completeChain tries only a few of a certificate's CA-Issuers URLs`() {
201
+ // The extension is authored by the server that just failed validation, and nothing limits
202
+ // how many URLs it lists. Each one costs a blocking connect on the handshake thread, so
203
+ // hundreds of them are a denial of service whatever each individual fetch costs.
204
+ val manyUrls = (1..500).map { "http://ca$it.example/issuer.crt" }
205
+ val cert = certWithAia(aiaExtension(manyUrls))
206
+ val fetched = mutableListOf<String>()
207
+
208
+ AiaCertChaser.completeChain(listOf(cert)) { url, _ ->
209
+ fetched.add(url)
210
+ emptyList()
211
+ }
212
+
213
+ assertEquals(AiaCertChaser.MAX_URLS_PER_CERT, fetched.size)
214
+ }
215
+
216
+ @Test
217
+ fun `duplicate CA-Issuers URLs do not consume the URL cap`() {
218
+ // 500 copies of one dead URL followed by a distinct one: the cap must buy distinct hosts,
219
+ // not three visits to the same dead one.
220
+ val urls = List(500) { "http://ca.example/issuer.crt" } + "http://ca-b.example/issuer.crt"
221
+ val cert = certWithAia(aiaExtension(urls))
222
+ val fetched = mutableListOf<String>()
223
+
224
+ AiaCertChaser.completeChain(listOf(cert)) { url, _ ->
225
+ fetched.add(url)
226
+ emptyList()
227
+ }
228
+
229
+ assertEquals(listOf("http://ca.example/issuer.crt", "http://ca-b.example/issuer.crt"), fetched)
230
+ }
231
+
232
+ @Test
233
+ fun `completeChain stops fetching once its wall-clock budget is spent`() {
234
+ val cert = certWithAia(aiaExtension((1..500).map { "http://ca$it.example/issuer.crt" }))
235
+ var fetches = 0
236
+ var nanos = 0L
237
+ val deadlines = mutableListOf<Long>()
238
+
239
+ // Each fetch "takes" 8s against a 20s budget, so the walk must stop after a handful even
240
+ // though the URL cap alone would have allowed more.
241
+ AiaCertChaser.completeChain(
242
+ listOf(cert),
243
+ maxUrlsPerCert = 500,
244
+ budgetMs = 20_000,
245
+ nowNanos = { nanos },
246
+ ) { _, deadline ->
247
+ fetches++
248
+ deadlines.add(deadline)
249
+ nanos += 8_000_000_000
250
+ emptyList()
251
+ }
252
+
253
+ assertEquals(3, fetches)
254
+ // Every fetch is handed the same walk-wide deadline, so it can clip its own I/O to it —
255
+ // the budget bounds the fetches themselves, not just how many of them start.
256
+ assertEquals(listOf(20_000_000_000L, 20_000_000_000L, 20_000_000_000L), deadlines)
257
+ }
258
+
69
259
  @Test
70
260
  fun `completeChain stops when the issuer cannot be fetched`() {
71
261
  val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
72
262
 
73
- val completed = AiaCertChaser.completeChain(listOf(leaf)) { emptyList() }
263
+ val completed = AiaCertChaser.completeChain(listOf(leaf)) { _, _ -> emptyList() }
74
264
 
75
265
  assertEquals(listOf(leaf), completed)
76
266
  }
@@ -88,8 +278,8 @@ class AiaCertChaserTest {
88
278
 
89
279
  // After AIA chasing splices in R13, the chain validates to the trusted root.
90
280
  val completed =
91
- AiaCertChaser.completeChain(listOf(leaf)) {
92
- if (it == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
281
+ AiaCertChaser.completeChain(listOf(leaf)) { url, _ ->
282
+ if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
93
283
  }
94
284
  validatePath(completed, root, date) // must not throw
95
285
  }