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

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,289 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { followMediaRedirect } from './followMediaRedirect'
3
+
4
+ const res = (over: Partial<Response> = {}): Response =>
5
+ ({ ok: true, url: '', ...over }) as Response
6
+
7
+ describe('followMediaRedirect', () => {
8
+ const url = 'https://api.example.com/stream/content/7'
9
+ const signed = 'https://cdn.example.com/track.mp3?sig=abc'
10
+ const headers = { Authorization: 'Bearer token' }
11
+
12
+ it('returns the url the redirect chain lands on', async () => {
13
+ const fetchImpl = vi.fn().mockResolvedValue(res({ url: signed }))
14
+
15
+ const out = await followMediaRedirect(url, headers, { fetchImpl })
16
+
17
+ expect(out.src).toBe(signed)
18
+ // the credential was for the authenticating host, not the one it sent us to
19
+ expect(out.headers).toBeUndefined()
20
+ expect(fetchImpl).toHaveBeenCalledWith(url, {
21
+ method: 'HEAD',
22
+ headers,
23
+ redirect: 'follow',
24
+ signal: expect.any(AbortSignal)
25
+ })
26
+ })
27
+
28
+ it('does not fetch when there are no headers to apply', async () => {
29
+ const fetchImpl = vi.fn()
30
+
31
+ expect((await followMediaRedirect(url, undefined, { fetchImpl })).src).toBe(
32
+ url
33
+ )
34
+ expect((await followMediaRedirect(url, {}, { fetchImpl })).src).toBe(url)
35
+ expect(fetchImpl).not.toHaveBeenCalled()
36
+ })
37
+
38
+ it('does not fetch for shared headers that are not a credential', async () => {
39
+ const fetchImpl = vi.fn()
40
+
41
+ // an X-Client-Version that also rides browse requests: probing here would
42
+ // cost a preflight on every play, at every stream host, for nothing
43
+ const out = await followMediaRedirect(
44
+ url,
45
+ { 'X-Client-Version': '1.2.3' },
46
+ { fetchImpl }
47
+ )
48
+
49
+ expect(out.src).toBe(url)
50
+ expect(fetchImpl).not.toHaveBeenCalled()
51
+ })
52
+
53
+ it('fetches for a non-Authorization header the media layer set', async () => {
54
+ const fetchImpl = vi.fn().mockResolvedValue(res({ url: signed }))
55
+
56
+ const out = await followMediaRedirect(
57
+ url,
58
+ { 'X-Api-Key': 'k' },
59
+ { fetchImpl, mediaLayerHeaders: true }
60
+ )
61
+
62
+ expect(out.src).toBe(signed)
63
+ })
64
+
65
+ it('keeps the signed url when the final hop refuses the HEAD', async () => {
66
+ // presigned S3/GCS sign the method, so a URL signed for GET 403s a HEAD —
67
+ // but hop one authenticated, so the signed url it handed back is good
68
+ const fetchImpl = vi
69
+ .fn()
70
+ .mockResolvedValue(res({ ok: false, status: 403, url: signed }))
71
+
72
+ const out = await followMediaRedirect(url, headers, { fetchImpl })
73
+
74
+ expect(out.src).toBe(signed)
75
+ expect(out.headers).toBeUndefined()
76
+ })
77
+
78
+ /**
79
+ * A manifest is played through Shaka's networking engine, where the request
80
+ * filter carries the headers to the manifest, its segments and its keys.
81
+ * Probing one is a wasted round trip at best, and at worst clears the headers
82
+ * on a cross-origin redirect so the segments Android still authenticates go
83
+ * out bare — which is the divergence this skip closes.
84
+ */
85
+ describe('manifests are not probed', () => {
86
+ it.each([
87
+ 'https://api.example.com/stream/7/playlist.m3u8',
88
+ 'https://api.example.com/stream/7/manifest.mpd',
89
+ 'https://api.example.com/stream/7/Manifest.ISM',
90
+ 'https://api.example.com/stream/7/playlist.m3u8?token=abc'
91
+ ])('skips %s', async (manifest) => {
92
+ const fetchImpl = vi.fn()
93
+
94
+ const out = await followMediaRedirect(manifest, headers, { fetchImpl })
95
+
96
+ expect(out.src).toBe(manifest)
97
+ expect(out.headers).toEqual(headers)
98
+ expect(fetchImpl).not.toHaveBeenCalled()
99
+ })
100
+
101
+ it('still probes a progressive file', async () => {
102
+ const fetchImpl = vi.fn().mockResolvedValue(res({ url: signed }))
103
+
104
+ await followMediaRedirect('https://api.example.com/7.mp3', headers, {
105
+ fetchImpl
106
+ })
107
+
108
+ expect(fetchImpl).toHaveBeenCalled()
109
+ })
110
+
111
+ it('still probes an extensionless url', async () => {
112
+ // Shaka cannot type these from the extension either, so they stay on the
113
+ // probe path rather than being guessed either way
114
+ const fetchImpl = vi.fn().mockResolvedValue(res({ url: signed }))
115
+
116
+ await followMediaRedirect(url, headers, { fetchImpl })
117
+
118
+ expect(fetchImpl).toHaveBeenCalled()
119
+ })
120
+ })
121
+
122
+ it('does not fetch local urls', async () => {
123
+ const fetchImpl = vi.fn()
124
+ const file = 'file:///var/media/track.mp3'
125
+
126
+ expect((await followMediaRedirect(file, headers, { fetchImpl })).src).toBe(
127
+ file
128
+ )
129
+ expect(fetchImpl).not.toHaveBeenCalled()
130
+ })
131
+
132
+ it('resolves a url that does not redirect to itself', async () => {
133
+ const fetchImpl = vi.fn().mockResolvedValue(res({ url }))
134
+
135
+ const out = await followMediaRedirect(url, headers, { fetchImpl })
136
+ expect(out.src).toBe(url)
137
+ // same origin, so the headers are still ours to send
138
+ expect(out.headers).toEqual(headers)
139
+ })
140
+
141
+ it('keeps the headers when the redirect stays on the same origin', async () => {
142
+ const sameOrigin = 'https://api.example.com/signed/track.mp3'
143
+ const fetchImpl = vi.fn().mockResolvedValue(res({ url: sameOrigin }))
144
+
145
+ const out = await followMediaRedirect(url, headers, { fetchImpl })
146
+
147
+ expect(out.src).toBe(sameOrigin)
148
+ expect(out.headers).toEqual(headers)
149
+ })
150
+
151
+ it('keeps the headers for a relative src served from the page origin', async () => {
152
+ // `fetch('/api/authed/7')` goes to the page's own origin, so the absolute
153
+ // url the response reports is the same origin — not a hop. Comparing the
154
+ // raw relative string instead dropped the credential on every such load.
155
+ vi.stubGlobal('location', { href: 'https://app.example.com/player' })
156
+ try {
157
+ const fetchImpl = vi
158
+ .fn()
159
+ .mockResolvedValue(res({ url: 'https://app.example.com/api/authed/7' }))
160
+
161
+ const out = await followMediaRedirect('/api/authed/7', headers, {
162
+ fetchImpl
163
+ })
164
+
165
+ expect(out.headers).toEqual(headers)
166
+ } finally {
167
+ vi.unstubAllGlobals()
168
+ }
169
+ })
170
+
171
+ it('clears the headers when a relative src redirects off the page origin', async () => {
172
+ vi.stubGlobal('location', { href: 'https://app.example.com/player' })
173
+ try {
174
+ const fetchImpl = vi.fn().mockResolvedValue(res({ url: signed }))
175
+
176
+ const out = await followMediaRedirect('/api/authed/7', headers, {
177
+ fetchImpl
178
+ })
179
+
180
+ expect(out.src).toBe(signed)
181
+ expect(out.headers).toBeUndefined()
182
+ } finally {
183
+ vi.unstubAllGlobals()
184
+ }
185
+ })
186
+
187
+ it('keeps the headers when the origin cannot be determined', async () => {
188
+ // no document base, so a relative src has no origin — unknown is not
189
+ // evidence of a hop, and guessing cross-origin loses the credential
190
+ const fetchImpl = vi
191
+ .fn()
192
+ .mockResolvedValue(res({ url: 'https://app.example.com/api/authed/7' }))
193
+
194
+ const out = await followMediaRedirect('/api/authed/7', headers, {
195
+ fetchImpl
196
+ })
197
+
198
+ expect(out.headers).toEqual(headers)
199
+ })
200
+
201
+ it('treats a default port as the same origin', async () => {
202
+ // Android compares HttpUrl.port, which resolves to the scheme default, so
203
+ // web must normalize too or it strips headers Android keeps
204
+ const explicitPort = 'https://api.example.com:443/signed/track.mp3'
205
+ const fetchImpl = vi.fn().mockResolvedValue(res({ url: explicitPort }))
206
+
207
+ const out = await followMediaRedirect(url, headers, { fetchImpl })
208
+
209
+ expect(out.src).toBe(explicitPort)
210
+ expect(out.headers).toEqual(headers)
211
+ })
212
+
213
+ it('ignores userinfo when comparing origins', async () => {
214
+ const withUser = 'https://user@api.example.com/signed/track.mp3'
215
+ const fetchImpl = vi.fn().mockResolvedValue(res({ url: withUser }))
216
+
217
+ const out = await followMediaRedirect(url, headers, { fetchImpl })
218
+
219
+ expect(out.headers).toEqual(headers)
220
+ })
221
+
222
+ it('clears the headers on a real cross-origin hop', async () => {
223
+ const otherPort = 'https://api.example.com:8443/signed/track.mp3'
224
+ const fetchImpl = vi.fn().mockResolvedValue(res({ url: otherPort }))
225
+
226
+ expect(
227
+ (await followMediaRedirect(url, headers, { fetchImpl })).headers
228
+ ).toBeUndefined()
229
+ })
230
+
231
+ it('falls back to the original url when the credential is refused', async () => {
232
+ // no redirect, so the 401 is the authenticating endpoint's own answer
233
+ const fetchImpl = vi
234
+ .fn()
235
+ .mockResolvedValue(res({ ok: false, status: 401, url }))
236
+
237
+ expect((await followMediaRedirect(url, headers, { fetchImpl })).src).toBe(
238
+ url
239
+ )
240
+ })
241
+
242
+ it('falls back to the original url when the probe outlives its deadline', async () => {
243
+ vi.useFakeTimers()
244
+ try {
245
+ // a host that accepts the connection and then says nothing: without a
246
+ // deadline the load waits on this forever, with no audio and no error
247
+ const fetchImpl = vi.fn(() => new Promise<Response>(() => {}))
248
+ const pending = followMediaRedirect(url, headers, {
249
+ fetchImpl,
250
+ timeoutMs: 1000
251
+ })
252
+
253
+ await vi.advanceTimersByTimeAsync(1000)
254
+ const out = await pending
255
+
256
+ expect(out.src).toBe(url)
257
+ expect(out.headers).toEqual(headers)
258
+ } finally {
259
+ vi.useRealTimers()
260
+ }
261
+ })
262
+
263
+ it('passes an abort signal so a dead host does not hold the connection', async () => {
264
+ const fetchImpl = vi.fn().mockResolvedValue(res({ url: signed }))
265
+
266
+ await followMediaRedirect(url, headers, { fetchImpl })
267
+
268
+ expect(fetchImpl.mock.calls[0]?.[1]?.signal).toBeInstanceOf(AbortSignal)
269
+ })
270
+
271
+ it('falls back to the original url when the fetch throws', async () => {
272
+ const fetchImpl = vi.fn().mockRejectedValue(new Error('CORS'))
273
+
274
+ expect((await followMediaRedirect(url, headers, { fetchImpl })).src).toBe(
275
+ url
276
+ )
277
+ })
278
+
279
+ it('falls back to the original url when no fetch is available', async () => {
280
+ const globalFetch = globalThis.fetch
281
+ // @ts-expect-error — exercising a runtime without fetch
282
+ globalThis.fetch = undefined
283
+ try {
284
+ expect((await followMediaRedirect(url, headers)).src).toBe(url)
285
+ } finally {
286
+ globalThis.fetch = globalFetch
287
+ }
288
+ })
289
+ })
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Resolves a media URL that is authenticated by a header.
3
+ *
4
+ * Shaka only routes manifest requests through its networking engine, where a
5
+ * request filter can attach headers. A progressive file is played by assigning
6
+ * `mediaElement.src`, and a media element cannot send custom headers — so such
7
+ * a URL is unplayable that way. Following the redirect here yields the signed,
8
+ * public URL these endpoints hand back, which needs no auth of its own.
9
+ *
10
+ * Best effort and bounded: anything that fails — including taking too long, see
11
+ * {@link PROBE_TIMEOUT_MS} — falls back to the original URL, so the load fails
12
+ * or succeeds on its own terms rather than ours. It must never be the reason a
13
+ * load hangs: `load()` awaits this before Shaka sees the URL at all.
14
+ *
15
+ * Gated, because the probe is not free: a HEAD at a third-party stream host
16
+ * costs a CORS preflight, and most will refuse it, so an app that never
17
+ * authenticates its media would pay a round trip on every play for a result it
18
+ * always discards. It runs only for headers that look like media credentials
19
+ * ({@link isMediaAuth}), on media a media element actually has to play
20
+ * ({@link isManifestUrl}), and never on a URL the browser resolves locally.
21
+ */
22
+
23
+ /** URLs the browser resolves locally; there is no redirect to follow. */
24
+ const isLocalUrl = (url: string): boolean =>
25
+ /^(file|blob|data|mediasource):/i.test(url)
26
+
27
+ /**
28
+ * Extensions Shaka types as a manifest, from its own `EXTENSIONS_TO_MIME_TYPES_`
29
+ * map: HLS, DASH and Smooth Streaming.
30
+ */
31
+ const MANIFEST_EXTENSIONS = new Set(['m3u8', 'mpd', 'ism'])
32
+
33
+ /**
34
+ * Whether Shaka will parse this as a manifest rather than hand it to the media
35
+ * element — in which case the probe has nothing to offer and something to cost.
36
+ *
37
+ * A manifest goes through Shaka's networking engine, where the request filter
38
+ * attaches the headers to the manifest, its segments and any key requests. The
39
+ * probe cannot improve on that, and it actively breaks it: on a cross-origin
40
+ * redirect the probe clears the headers, so the segments that Android still
41
+ * authenticates would go out bare and 401. So this is a correctness fix as much
42
+ * as it is one fewer round trip.
43
+ *
44
+ * Extension-based, and therefore partial — Shaka guesses the same way
45
+ * (`NetworkingUtils.getExtension`), but plenty of HLS endpoints have no
46
+ * extension at all. Those still get probed, which is the status quo rather than
47
+ * a regression. Nothing on `Track` carries a MIME type to do better with.
48
+ */
49
+ const isManifestUrl = (url: string): boolean => {
50
+ const path = url.split(/[?#]/)[0] ?? ''
51
+ const filename = path.slice(path.lastIndexOf('/') + 1)
52
+ const pieces = filename.split('.')
53
+ return (
54
+ pieces.length > 1 &&
55
+ MANIFEST_EXTENSIONS.has(pieces[pieces.length - 1]!.toLowerCase())
56
+ )
57
+ }
58
+
59
+ export interface FollowMediaRedirectOptions {
60
+ /** Injected for testing; defaults to the global `fetch`. */
61
+ fetchImpl?: typeof fetch
62
+ /**
63
+ * Whether the `media` layer contributed any of these headers, rather than
64
+ * their all arriving from the shared `request` layer.
65
+ */
66
+ mediaLayerHeaders?: boolean
67
+ /** Milliseconds before the probe gives up. Defaults to {@link PROBE_TIMEOUT_MS}. */
68
+ timeoutMs?: number
69
+ }
70
+
71
+ /**
72
+ * How long the probe may take before the original URL is played instead.
73
+ *
74
+ * The same 8s the Android media client allows a connect or a read
75
+ * (`MediaFactory.mediaHttpClient`), which is generous for a HEAD but errs the
76
+ * safe way: a slow-but-working auth endpoint should not be abandoned, since
77
+ * giving up means playing a URL that will 401.
78
+ */
79
+ const PROBE_TIMEOUT_MS = 8_000
80
+
81
+ /**
82
+ * `AbortSignal.timeout` where it exists (Safari 16+, Chrome 103+, Node 17.3+),
83
+ * else a hand-rolled equivalent, else nothing — an environment with neither
84
+ * still gets the deadline below, just without cancelling the request.
85
+ */
86
+ const timeoutSignal = (ms: number): AbortSignal | undefined => {
87
+ if (typeof AbortSignal === 'undefined') return undefined
88
+ if (typeof AbortSignal.timeout === 'function') return AbortSignal.timeout(ms)
89
+ if (typeof AbortController === 'undefined') return undefined
90
+ const controller = new AbortController()
91
+ setTimeout(() => controller.abort(), ms)
92
+ return controller.signal
93
+ }
94
+
95
+ /**
96
+ * The probe, with a deadline. `load()` awaits this before Shaka is asked to
97
+ * play anything, so an unreachable host would otherwise stall the load
98
+ * indefinitely — no audio, and no error to show for it.
99
+ *
100
+ * Two mechanisms, each with its own job: the signal cancels the request, so a
101
+ * dead host does not hold a connection open behind us, and the race bounds *us*
102
+ * even where the fetch implementation ignores the signal. Whichever fires, the
103
+ * caller treats it as any other probe failure and plays the original URL.
104
+ */
105
+ const fetchWithDeadline = async (
106
+ doFetch: typeof fetch,
107
+ url: string,
108
+ headers: Record<string, string>,
109
+ timeoutMs: number
110
+ ): Promise<Response> => {
111
+ let timer: ReturnType<typeof setTimeout> | undefined
112
+ try {
113
+ return await Promise.race([
114
+ doFetch(url, {
115
+ method: 'HEAD',
116
+ headers,
117
+ redirect: 'follow',
118
+ signal: timeoutSignal(timeoutMs)
119
+ }),
120
+ new Promise<never>((_resolve, reject) => {
121
+ timer = setTimeout(
122
+ () =>
123
+ reject(
124
+ new Error(`Media redirect probe timed out (${timeoutMs}ms)`)
125
+ ),
126
+ timeoutMs
127
+ )
128
+ })
129
+ ])
130
+ } finally {
131
+ if (timer !== undefined) clearTimeout(timer)
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Whether these headers look like a media credential — the only thing the
137
+ * probe can fix, and the only thing worth a preflight.
138
+ *
139
+ * A header the `media` layer set is media-specific by construction. Otherwise
140
+ * only `Authorization` counts: an app authenticating one API for browse and
141
+ * media alike puts it on the shared `request` layer, and that is the common
142
+ * shape. Shared plumbing that also rides browse requests — an `X-Client-Version`
143
+ * — is not a credential and must not trigger the probe.
144
+ *
145
+ * The residual case is a shared layer carrying a custom auth header, say
146
+ * `X-Api-Key`, for media that needs it: move it to the `media` layer and the
147
+ * probe runs.
148
+ */
149
+ const isMediaAuth = (
150
+ headers: Record<string, string>,
151
+ mediaLayerHeaders: boolean
152
+ ): boolean =>
153
+ mediaLayerHeaders ||
154
+ Object.keys(headers).some((key) => key.toLowerCase() === 'authorization')
155
+
156
+ export interface ResolvedMedia {
157
+ /** The URL to play. */
158
+ src: string
159
+ /**
160
+ * The headers still safe to send. Cleared once the redirect leaves the
161
+ * origin: the credential was addressed to the authenticating host, and
162
+ * browsers strip `Authorization` on that hop for the same reason.
163
+ *
164
+ * Only for what we send *after* the probe. The probe's own hop is the
165
+ * browser's to make: `redirect: 'follow'` drops `Authorization` across
166
+ * origins but re-sends every other header, so a custom-named credential can
167
+ * reach the redirect target if that target's preflight admits it. There is no
168
+ * way around it from JS — `redirect: 'manual'` yields an opaque response in
169
+ * CORS mode, with no `Location` to walk by hand — so a custom-named media
170
+ * credential is only as private as the hosts it may be redirected to.
171
+ * Android has no such limit: [CrossOriginHeaderStripper] strips the hop
172
+ * there.
173
+ */
174
+ headers?: Record<string, string>
175
+ }
176
+
177
+ /**
178
+ * Origin of an http(s) URL, or undefined when it cannot be determined.
179
+ *
180
+ * Relative URLs are resolved against the document, because that is what the
181
+ * probe's own `fetch` does — a `src` of `/api/authed/7` is a request to the
182
+ * page's origin, so the credential went there. Without this a relative `src`
183
+ * has no origin, never matches the absolute URL the response reports, and the
184
+ * headers are dropped from every load that uses one.
185
+ *
186
+ * Parsed rather than pattern-matched so the comparison normalizes the way
187
+ * Android's `CrossOriginHeaderStripper.sameOrigin` does: `URL.host` omits a
188
+ * default port, so `https://a.com` and `https://a.com:443` are one origin, and
189
+ * it excludes userinfo, so `https://user@a.com` is not a third.
190
+ */
191
+ const originOf = (url: string): string | undefined => {
192
+ try {
193
+ const base = typeof location === 'undefined' ? undefined : location.href
194
+ const { protocol, host } = new URL(url, base)
195
+ if (protocol !== 'http:' && protocol !== 'https:') return undefined
196
+ return `${protocol}//${host}`
197
+ } catch {
198
+ return undefined
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Whether the redirect left the origin the credential was addressed to.
204
+ *
205
+ * Only when both origins are known and differ. An origin we could not work out
206
+ * — a relative URL with no document base, outside a browser — is not evidence
207
+ * of a hop, and treating it as one is how the headers went missing before.
208
+ */
209
+ const leftTheOrigin = (from: string, to: string): boolean => {
210
+ const origin = originOf(from)
211
+ return origin !== undefined && originOf(to) !== origin
212
+ }
213
+
214
+ export async function followMediaRedirect(
215
+ url: string,
216
+ headers: Record<string, string> | undefined,
217
+ {
218
+ fetchImpl,
219
+ mediaLayerHeaders = false,
220
+ timeoutMs = PROBE_TIMEOUT_MS
221
+ }: FollowMediaRedirectOptions = {}
222
+ ): Promise<ResolvedMedia> {
223
+ if (!headers || Object.keys(headers).length === 0)
224
+ return { src: url, headers }
225
+ if (!isMediaAuth(headers, mediaLayerHeaders)) return { src: url, headers }
226
+ if (isManifestUrl(url)) return { src: url, headers }
227
+ if (isLocalUrl(url)) return { src: url, headers }
228
+
229
+ const doFetch = fetchImpl ?? globalThis.fetch
230
+ if (typeof doFetch !== 'function') return { src: url, headers }
231
+
232
+ try {
233
+ // HEAD walks the chain without tripping download counting, which keys off
234
+ // ranged GETs. Deadlined, because the load waits on it.
235
+ const response = await fetchWithDeadline(doFetch, url, headers, timeoutMs)
236
+
237
+ const src = response.url || url
238
+
239
+ // The status belongs to the *last* hop, so it only speaks for the
240
+ // authenticating endpoint when there was no redirect. Once one has
241
+ // happened, hop one authenticated and handed us its target, and a refusal
242
+ // past that is routinely about the method rather than the credential:
243
+ // presigned S3 and GCS URLs sign it — SigV4's canonical request opens with
244
+ // HTTPMethod, GCS V4's with HTTP_VERB — so a URL signed for GET answers a
245
+ // HEAD with 403. Keep the signed URL, which is the whole point of the walk,
246
+ // and let a real error surface through the player.
247
+ if (!response.ok && src === url) return { src: url, headers }
248
+
249
+ return { src, headers: leftTheOrigin(url, src) ? undefined : headers }
250
+ } catch {
251
+ return { src: url, headers }
252
+ }
253
+ }
@@ -1,6 +1,7 @@
1
1
  import type { NowPlayingUpdate, NowPlayingMetadata } from '../../features'
2
2
  import type { Track } from '../../types'
3
3
  import { artworkUrl } from '../../utils/artwork'
4
+ import { getTrackIdentity } from '../../utils/getTrackIdentity'
4
5
 
5
6
  /**
6
7
  * Manages now playing metadata overrides.
@@ -37,7 +38,7 @@ export class NowPlayingManager {
37
38
  album: update.album ?? currentTrack.album,
38
39
  artwork: artworkUrl(currentTrack.artwork),
39
40
  description: currentTrack.description,
40
- mediaId: currentTrack.src ?? currentTrack.url,
41
+ mediaId: getTrackIdentity(currentTrack) ?? currentTrack.path,
41
42
  genre: currentTrack.genre,
42
43
  duration
43
44
  }
@@ -47,7 +48,7 @@ export class NowPlayingManager {
47
48
  album: currentTrack.album,
48
49
  artwork: artworkUrl(currentTrack.artwork),
49
50
  description: currentTrack.description,
50
- mediaId: currentTrack.src ?? currentTrack.url,
51
+ mediaId: getTrackIdentity(currentTrack) ?? currentTrack.path,
51
52
  genre: currentTrack.genre,
52
53
  duration
53
54
  }
@@ -74,7 +75,7 @@ export class NowPlayingManager {
74
75
  album: this.override?.album ?? currentTrack.album,
75
76
  artwork: artworkUrl(currentTrack.artwork),
76
77
  description: currentTrack.description,
77
- mediaId: currentTrack.src ?? currentTrack.url,
78
+ mediaId: getTrackIdentity(currentTrack) ?? currentTrack.path,
78
79
  genre: currentTrack.genre,
79
80
  duration
80
81
  }
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { BrowserPathHelper } from './BrowserPathHelper'
3
+
4
+ describe('BrowserPathHelper contextual URL round-trip', () => {
5
+ it('round-trips a src carrying its own query params', () => {
6
+ // A src carrying its own query string (signed CDN URL) must survive the
7
+ // build → extract/strip round-trip: an unescaped `&` would split the src
8
+ // into stray query params, truncating the trackId and polluting the
9
+ // parent path.
10
+ const src = 'https://cdn.example.com/stream.mp3?token=abc&exp=1699999999'
11
+ const url = BrowserPathHelper.build('/library', src)
12
+ expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
13
+ expect(BrowserPathHelper.stripTrackId(url)).toBe('/library')
14
+ })
15
+
16
+ it('round-trips a src with equals and plus', () => {
17
+ const src = 'https://cdn.example.com/a+b.mp3?sig=x=y'
18
+ const url = BrowserPathHelper.build('/library', src)
19
+ expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
20
+ expect(BrowserPathHelper.stripTrackId(url)).toBe('/library')
21
+ })
22
+
23
+ it('preserves unrelated parent query params when stripping', () => {
24
+ const src = 'https://cdn.example.com/stream.mp3?token=abc&exp=1'
25
+ const url = BrowserPathHelper.build('/search?q=jazz', src)
26
+ expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
27
+ expect(BrowserPathHelper.stripTrackId(url)).toBe('/search?q=jazz')
28
+ })
29
+
30
+ it('round-trips the stamped index alongside the trackId', () => {
31
+ const src = 'https://cdn.example.com/stream.mp3?token=abc&exp=1'
32
+ const url = BrowserPathHelper.build('/library', src, 3)
33
+ expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
34
+ expect(BrowserPathHelper.extractIndex(url)).toBe(3)
35
+ expect(BrowserPathHelper.stripTrackId(url)).toBe('/library')
36
+ })
37
+
38
+ it('extracts no index from an index-less contextual URL', () => {
39
+ const url = BrowserPathHelper.build('/library', 'song.mp3')
40
+ expect(BrowserPathHelper.extractIndex(url)).toBeUndefined()
41
+ })
42
+
43
+ it('extracts no index from a non-contextual or malformed-index URL', () => {
44
+ expect(BrowserPathHelper.extractIndex('/library')).toBeUndefined()
45
+ expect(
46
+ BrowserPathHelper.extractIndex('/library?__trackId=song.mp3&__index=x')
47
+ ).toBeUndefined()
48
+ expect(
49
+ BrowserPathHelper.extractIndex('/library?__trackId=song.mp3&__index=-1')
50
+ ).toBeUndefined()
51
+ })
52
+ })