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
@@ -5,6 +5,12 @@
5
5
  /// These stubs declare `Equatable` for test ergonomics — the generated
6
6
  /// Nitro types do NOT. Shared `ios/` sources must not rely on conformances
7
7
  /// that exist only here: `swift test` compiles, the app build breaks.
8
+ ///
9
+ /// Property names and types ARE checked against `nitrogen/generated/ios/swift`
10
+ /// by `src/nitro-stub-drift.test.ts`, which runs in the JS CI job — neither
11
+ /// native job can see both files at once. Declaring fewer properties than the
12
+ /// real type is fine and deliberate; spelling one differently is what that
13
+ /// test catches. Conformances and default values are still on you.
8
14
 
9
15
  struct ArtworkVariants: Equatable {
10
16
  var light: String
@@ -23,19 +29,20 @@
23
29
  }
24
30
 
25
31
  struct Track: Equatable {
26
- var id: String
27
- var url: String?
32
+ var id: String?
33
+ var path: String?
28
34
  var src: String?
35
+ var artwork: Variant_String_ArtworkVariants?
36
+ var artworkSource: ImageSource?
29
37
  var request: TrackRequest?
30
38
  var title: String = ""
31
39
  var artist: String?
32
- var albumUrl: String?
40
+ var albumPath: String?
33
41
  var album: String?
42
+ var style: TrackStyle?
43
+ var disabled: Bool?
34
44
  var live: Bool?
35
- var artwork: Variant_String_ArtworkVariants?
36
- var artworkSource: ImageSource?
37
- var groupTitle: String?
38
- var imageRow: [ImageRowItem]?
45
+ var icyCharset: String?
39
46
  }
40
47
 
41
48
  struct TrackLoadEvent {
@@ -63,14 +70,24 @@
63
70
  }
64
71
  }
65
72
 
73
+ enum HttpMethod {
74
+ case get
75
+ case post
76
+ case put
77
+ case patch
78
+ case delete
79
+ case head
80
+ case options
81
+ }
82
+
66
83
  struct ImageSource: Equatable {
67
84
  var uri: String
68
- var method: String?
85
+ var method: HttpMethod?
69
86
  var headers: [String: String]?
70
87
  var body: String?
71
88
  }
72
89
 
73
- enum TrackStyle {
90
+ enum StyleDisplay: Equatable {
74
91
  case list
75
92
  case grid
76
93
 
@@ -83,17 +100,105 @@
83
100
  }
84
101
  }
85
102
 
86
- struct ImageRowItem: Equatable {
87
- var id: String?
88
- var url: String?
89
- var src: String?
90
- var artwork: String?
91
- var artworkSource: ImageSource?
92
- var title: String
103
+ enum ArtworkRendering: Equatable {
104
+ case original
105
+ case stencil
106
+
107
+ init?(fromString string: String) {
108
+ switch string {
109
+ case "original": self = .original
110
+ case "stencil": self = .stencil
111
+ default: return nil
112
+ }
113
+ }
114
+ }
115
+
116
+ enum ImageShape: Equatable {
117
+ case circular
118
+ case roundedRectangle
119
+
120
+ init?(fromString string: String) {
121
+ switch string {
122
+ case "circular": self = .circular
123
+ case "rounded-rectangle": self = .roundedRectangle
124
+ default: return nil
125
+ }
126
+ }
127
+ }
128
+
129
+ enum GridTile: Equatable {
130
+ case plain
131
+ case card
132
+ case condensed
133
+
134
+ init?(fromString string: String) {
135
+ switch string {
136
+ case "plain": self = .plain
137
+ case "card": self = .card
138
+ case "condensed": self = .condensed
139
+ default: return nil
140
+ }
141
+ }
142
+
143
+ // Mirrors the generated extension (nitrogen emits `stringValue` beside
144
+ // `init?(fromString:)` for every string-backed enum).
145
+ var stringValue: String {
146
+ switch self {
147
+ case .plain: "plain"
148
+ case .card: "card"
149
+ case .condensed: "condensed"
150
+ }
151
+ }
152
+ }
153
+
154
+ enum CardImage: Equatable {
155
+ case normal
156
+ case background
157
+
158
+ init?(fromString string: String) {
159
+ switch string {
160
+ case "normal": self = .normal
161
+ case "background": self = .background
162
+ default: return nil
163
+ }
164
+ }
165
+ }
166
+
167
+ struct TrackStyle: Equatable {
168
+ var display: StyleDisplay?
169
+ var artworkRendering: ArtworkRendering?
170
+ var imageShape: ImageShape?
171
+ var accessorySymbol: String?
172
+ var cardTint: String?
173
+ var cardImage: CardImage?
174
+ }
175
+
176
+ // Member order matches the generated init (own properties before inherited
177
+ // ones — Nitro flattens `extends` own-props-first): the memberwise init this
178
+ // produces must accept the same argument order as the real type, or
179
+ // `swift test` passes while the app build breaks on argument order.
180
+ struct SectionStyle: Equatable {
181
+ var gridWrap: Bool?
182
+ var gridTile: GridTile?
183
+ var display: StyleDisplay?
184
+ var artworkRendering: ArtworkRendering?
185
+ var imageShape: ImageShape?
186
+ var accessorySymbol: String?
187
+ var cardTint: String?
188
+ var cardImage: CardImage?
189
+ }
190
+
191
+ struct Section: Equatable {
192
+ var title: String?
193
+ var style: SectionStyle?
194
+ var path: String?
195
+ var children: [Track]
93
196
  }
94
197
 
95
198
  struct ResolvedTrack: Equatable {
96
- var url: String
199
+ var path: String
200
+ var style: SectionStyle?
201
+ var sections: [Section]?
97
202
  var children: [Track]?
98
203
  var carPlaySiriListButton: CarPlaySiriListButtonPosition?
99
204
  var id: String?
@@ -101,21 +206,18 @@
101
206
  var artwork: Variant_String_ArtworkVariants?
102
207
  var artworkSource: ImageSource?
103
208
  var request: TrackRequest?
104
- var artworkCarPlayTinted: Bool?
105
209
  var title: String
106
210
  var subtitle: String?
107
211
  var artist: String?
108
- var albumUrl: String?
212
+ var albumPath: String?
109
213
  var album: String?
110
214
  var description: String?
111
215
  var genre: String?
112
216
  var duration: Double?
113
- var style: TrackStyle?
114
- var childrenStyle: TrackStyle?
217
+ var disabled: Bool?
115
218
  var favorited: Bool?
116
- var groupTitle: String?
117
219
  var live: Bool?
118
- var imageRow: [ImageRowItem]?
220
+ var icyCharset: String?
119
221
  }
120
222
 
121
223
  enum PlaybackState: Equatable {
@@ -32,6 +32,9 @@ class PlayerUpdateOptions {
32
32
  /// Custom buttons for the CarPlay Now Playing screen
33
33
  var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] = []
34
34
 
35
+ /// Where the now-playing indicator draws on CarPlay list rows
36
+ var carPlayPlayingIndicatorLocation: CarPlayPlayingIndicatorLocation = .leading
37
+
35
38
  // MARK: - Initialization
36
39
 
37
40
  init() {}
@@ -74,6 +77,9 @@ class PlayerUpdateOptions {
74
77
  if let buttons = ios.carPlayNowPlayingButtons {
75
78
  carPlayNowPlayingButtons = buttons
76
79
  }
80
+ if let location = ios.carPlayPlayingIndicatorLocation {
81
+ carPlayPlayingIndicatorLocation = location
82
+ }
77
83
  }
78
84
  }
79
85
 
@@ -92,6 +98,7 @@ class PlayerUpdateOptions {
92
98
  playbackRates: playbackRates,
93
99
  carPlayUpNextButton: carPlayUpNextButton,
94
100
  carPlayNowPlayingButtons: carPlayNowPlayingButtons,
101
+ carPlayPlayingIndicatorLocation: carPlayPlayingIndicatorLocation,
95
102
  ),
96
103
  )
97
104
  }
@@ -48,6 +48,24 @@ enum RemoteCommand: CustomStringConvertible, Equatable {
48
48
 
49
49
  var key: String { description }
50
50
 
51
+ /**
52
+ Commands in `self` that are absent from `next` and so should be disabled.
53
+
54
+ Compared by `key`, not by `==`. `RemoteCommand` is `Equatable` including its
55
+ associated values, but every case with associated values maps onto a single
56
+ `MPRemoteCommand` addressed by `key`: `.skipForward([15])` and
57
+ `.skipForward([30])` are two different values of one command. Diffing by `==`
58
+ would report the old interval as removed and disable the command that the new
59
+ interval had just re-enabled.
60
+ */
61
+ static func commandsToDisable(
62
+ enabled: [RemoteCommand],
63
+ replacedBy next: [RemoteCommand],
64
+ ) -> [RemoteCommand] {
65
+ let nextKeys = Set(next.map(\.key))
66
+ return enabled.filter { !nextKeys.contains($0.key) }
67
+ }
68
+
51
69
  /**
52
70
  All values in an array for convenience.
53
71
  Don't use for associated values.
@@ -103,10 +103,10 @@ public extension TrackPlayerError.PlaybackError {
103
103
  static func kind(forHttpStatus status: Int) -> PlaybackErrorKind {
104
104
  switch status {
105
105
  case 404, 410: .notFound
106
- case 500...599: .serverError
106
+ case 500 ... 599: .serverError
107
107
  // Every other 4xx is the server refusing us — auth, geo-blocking, a
108
108
  // rate limit. All of them mean "you can't have this stream", not "retry".
109
- case 400...499: .rejected
109
+ case 400 ... 499: .rejected
110
110
  default: .unknown
111
111
  }
112
112
  }
@@ -99,7 +99,7 @@ extension NativeIOSSetupOptions {
99
99
  category: AVAudioSession.Category,
100
100
  mode: AVAudioSession.Mode,
101
101
  policy: AVAudioSession.RouteSharingPolicy,
102
- options: AVAudioSession.CategoryOptions
102
+ options: AVAudioSession.CategoryOptions,
103
103
  ) {
104
104
  let category = self.category
105
105
  .flatMap { SessionCategory(rawValue: $0.stringValue) }?
@@ -113,7 +113,8 @@ extension NativeIOSSetupOptions {
113
113
  let options = (self.categoryOptions ?? [])
114
114
  .reduce(into: AVAudioSession.CategoryOptions()) { acc, option in
115
115
  if let mapped = SessionCategoryOptions(rawValue: option.stringValue)?
116
- .mapConfigToAVAudioSessionCategoryOptions() {
116
+ .mapConfigToAVAudioSessionCategoryOptions()
117
+ {
117
118
  acc.insert(mapped)
118
119
  }
119
120
  }
@@ -43,7 +43,7 @@ public struct MediaIntentCriteria: Sendable {
43
43
  artist: String? = nil,
44
44
  album: String? = nil,
45
45
  title: String? = nil,
46
- playlist: String? = nil
46
+ playlist: String? = nil,
47
47
  ) {
48
48
  self.query = query
49
49
  self.reference = reference
@@ -90,7 +90,7 @@ public struct MediaIntentCriteria: Sendable {
90
90
  mediaTypeMode: String?,
91
91
  reference: Reference,
92
92
  hasMediaType: Bool,
93
- appName: String?
93
+ appName: String?,
94
94
  ) -> MediaIntentCriteria {
95
95
  let name = (mediaName ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
96
96
  let trimmedNonEmpty: (String?) -> String? = {
@@ -115,25 +115,33 @@ public struct MediaIntentCriteria: Sendable {
115
115
  hasGenres: !genreNames.isEmpty,
116
116
  hasMediaType: hasMediaType,
117
117
  matchesAppName: queryMatchesAppName(query, appName: appName),
118
- searchMode: mediaTypeMode, // mode is the vertical, verbatim — no derivation
118
+ searchMode: mediaTypeMode, // mode is the vertical, verbatim — no derivation
119
119
  genre: genre,
120
120
  artist: artist,
121
121
  album: album,
122
122
  title: title,
123
- playlist: playlist
123
+ playlist: playlist,
124
124
  )
125
125
  }
126
126
 
127
127
  /// Whether `query` is effectively the host app's own name. Case- and
128
128
  /// diacritic-insensitive.
129
+ ///
130
+ /// Whole-word subset match in either direction. Siri may deliver only part of
131
+ /// the name ("Name" for «App Name»), so a partial match must count — but a
132
+ /// substring match swallowed any query appearing inside the app name, making
133
+ /// one-word stations unreachable by voice.
129
134
  private static func queryMatchesAppName(_ query: String, appName: String?) -> Bool {
130
135
  guard let appName else { return false }
131
- let normalize: (String) -> String = {
132
- $0.folding(options: [.diacriticInsensitive, .caseInsensitive], locale: nil)
133
- .trimmingCharacters(in: .whitespacesAndNewlines)
136
+ let tokens: (String) -> Set<String> = {
137
+ Set(
138
+ $0.folding(options: [.diacriticInsensitive, .caseInsensitive], locale: nil)
139
+ .split(whereSeparator: { !$0.isLetter && !$0.isNumber })
140
+ .map(String.init),
141
+ )
134
142
  }
135
- let q = normalize(query), a = normalize(appName)
143
+ let q = tokens(query), a = tokens(appName)
136
144
  guard !q.isEmpty, !a.isEmpty else { return false }
137
- return a.contains(q) || q.contains(a)
145
+ return q.isSubset(of: a) || a.isSubset(of: q)
138
146
  }
139
147
  }
@@ -38,19 +38,28 @@ final class MediaLoader {
38
38
  func resolveAndLoad(src: String, track: Track? = nil) {
39
39
  if let resolver = mediaUrlResolver {
40
40
  mediaResolverTask?.cancel()
41
- mediaResolverTask = Task {
42
- self.logger.debug("resolveAndLoad: starting resolution for \(src)")
41
+ // `[weak self]` like the sibling load tasks below: the resolver is
42
+ // consumer-supplied and `cancelAll()` cannot force an in-flight `await`
43
+ // to return, so a strong capture let a resolver that never settles pin
44
+ // this loader (which also retains the task) indefinitely.
45
+ // `Logger` is a value type, so capturing it directly keeps the logging
46
+ // off `self` — and `self` weak, so a consumer resolver that never settles
47
+ // can't pin the loader (`cancelAll()` can't force an in-flight `await` to
48
+ // return).
49
+ let logger = logger
50
+ mediaResolverTask = Task { [weak self] in
51
+ logger.debug("resolveAndLoad: starting resolution for \(src)")
43
52
 
44
53
  guard !Task.isCancelled else {
45
- self.logger.debug("resolveAndLoad: cancelled before start")
54
+ logger.debug("resolveAndLoad: cancelled before start")
46
55
  return
47
56
  }
48
57
 
49
- self.logger.debug("resolveAndLoad: calling resolver...")
58
+ logger.debug("resolveAndLoad: calling resolver...")
50
59
  let resolved = await resolver(src, track)
51
60
 
52
- guard !Task.isCancelled else {
53
- self.logger.debug("resolveAndLoad: cancelled after resolver returned")
61
+ guard !Task.isCancelled, let self else {
62
+ logger.debug("resolveAndLoad: cancelled after resolver returned")
54
63
  return
55
64
  }
56
65
 
@@ -96,7 +105,7 @@ final class MediaLoader {
96
105
  .availableMetadataFormats,
97
106
  ) else { return }
98
107
 
99
- guard !Task.isCancelled, pendingAsset == asset else { return }
108
+ guard isCurrent(pendingAsset) else { return }
100
109
 
101
110
  if !commonMetadata.isEmpty {
102
111
  delegate?.mediaLoaderDidReceiveCommonMetadata(commonMetadata)
@@ -104,19 +113,21 @@ final class MediaLoader {
104
113
 
105
114
  if !chapterLocales.isEmpty {
106
115
  for locale in chapterLocales {
107
- guard !Task.isCancelled else { return }
116
+ guard isCurrent(pendingAsset) else { return }
108
117
  if let chapters = try? await pendingAsset.loadChapterMetadataGroups(
109
118
  withTitleLocale: locale,
110
119
  containingItemsWithCommonKeys: [],
111
120
  ) {
121
+ guard isCurrent(pendingAsset) else { return }
112
122
  delegate?.mediaLoaderDidReceiveChapterMetadata(chapters)
113
123
  }
114
124
  }
115
125
  } else {
116
126
  let duration = await (try? pendingAsset.load(.duration)) ?? .zero
117
127
  for format in metadataFormats {
118
- guard !Task.isCancelled else { return }
128
+ guard isCurrent(pendingAsset) else { return }
119
129
  if let metadata = try? await pendingAsset.loadMetadata(for: format) {
130
+ guard isCurrent(pendingAsset) else { return }
120
131
  let timeRange = CMTimeRange(
121
132
  start: CMTime(seconds: 0, preferredTimescale: 1000),
122
133
  end: duration,
@@ -159,6 +170,19 @@ final class MediaLoader {
159
170
  }
160
171
  }
161
172
 
173
+ /// Whether `candidate` is still the load this loader cares about — checked
174
+ /// after EVERY await, not just once before a loop.
175
+ ///
176
+ /// Cancellation alone is not enough: it only sets a flag, and can't abort a
177
+ /// load already in flight inside AVFoundation (only `cancelLoading()` does,
178
+ /// and the track-change path deliberately doesn't call it — the old asset
179
+ /// still backs the playing item at that point). So a load that lands after
180
+ /// the track changed would otherwise attribute the old asset's metadata to
181
+ /// the newly loaded track.
182
+ private func isCurrent(_ candidate: AVURLAsset) -> Bool {
183
+ !Task.isCancelled && candidate == asset
184
+ }
185
+
162
186
  func cancelAll() {
163
187
  mediaResolverTask?.cancel()
164
188
  mediaResolverTask = nil
@@ -30,17 +30,51 @@ final class NowPlayingUpdater {
30
30
  let album: String?
31
31
  }
32
32
 
33
+ /// Dedupe baseline for `applyFields`. Only meaningful while it describes the
34
+ /// controller's current info dict, so the two are cleared together in
35
+ /// `clearNowPlaying()` — never separately.
33
36
  private var lastPublished: Published?
34
37
 
38
+ /// Empties the now-playing surface and the dedupe baseline that describes it.
39
+ /// One call, because clearing the dict without the baseline makes the next
40
+ /// publish of the SAME track dedupe against a dict that no longer holds it —
41
+ /// artwork lands (never deduped) but the text doesn't, and `onChanged`
42
+ /// never fires.
43
+ func clearNowPlaying() {
44
+ nowPlayingInfoController.clear()
45
+ lastPublished = nil
46
+ formattedTrackId = nil
47
+ }
48
+
35
49
  /// Bumped on every `render`; an async formatter result applies only if its render is still the
36
50
  /// latest (latest-render-wins — drops a result whose track skipped or whose state moved on while
37
51
  /// the formatter was in flight).
38
52
  private var renderGeneration: UInt = 0
39
53
 
54
+ /// The track whose *formatted* line is currently on the surface. While this
55
+ /// matches the rendering track, `render` skips its interim default write — see
56
+ /// the comment at that site. Cleared whenever the surface stops showing a
57
+ /// formatter result (a flash overwrites it, `clearNowPlaying` empties it).
58
+ private var formattedTrackId: String?
59
+
40
60
  init(nowPlayingInfoController: NowPlayingInfoController) {
41
61
  self.nowPlayingInfoController = nowPlayingInfoController
42
62
  }
43
63
 
64
+ /// Pixel width to request now-playing artwork at: the active scene's screen
65
+ /// width in pixels, capped at `maxArtworkPixelWidth`. With no foreground
66
+ /// scene (headless CarPlay) the cap stands in — every current device exceeds
67
+ /// it anyway, and this only sizes a CDN request.
68
+ private static var artworkPixelWidth: CGFloat {
69
+ guard let screen = UIApplication.shared.activeWindowScene?.screen else {
70
+ return maxArtworkPixelWidth
71
+ }
72
+ return min(screen.bounds.width * screen.scale, maxArtworkPixelWidth)
73
+ }
74
+
75
+ /// Mirrored by the Kotlin twin's `artworkSizePx`.
76
+ private static let maxArtworkPixelWidth: CGFloat = 1200
77
+
44
78
  /// Publishes the static, per-track fields + artwork on a track change. The title/artist/album
45
79
  /// lines are owned by `render` (routed through `applyFields` here so it dedupes against later
46
80
  /// renders).
@@ -75,6 +109,8 @@ final class NowPlayingUpdater {
75
109
  let defaultSecondary = override?.artist ?? track.artist
76
110
  let defaultAlbum = override?.album ?? track.album
77
111
 
112
+ let trackId = track.identity ?? track.path
113
+
78
114
  if let flash {
79
115
  applyFields(
80
116
  track: track,
@@ -82,10 +118,22 @@ final class NowPlayingUpdater {
82
118
  artist: flash.artist ?? defaultSecondary,
83
119
  album: flash.album ?? defaultAlbum,
84
120
  )
121
+ // The flash replaced the formatter's line, so the surface no longer shows
122
+ // one: the render that follows the flash reverting must publish the default
123
+ // immediately rather than sit on stale flash text awaiting the formatter.
124
+ formattedTrackId = nil
85
125
  return
86
126
  }
87
127
 
88
- applyFields(track: track, title: defaultTitle, artist: defaultSecondary, album: defaultAlbum)
128
+ // Publish the raw default only while no formatter result is on screen for this
129
+ // track. `render` runs on EVERY playback transition (state change, playing/
130
+ // buffering flip, timed-metadata tick, connectivity change); writing the
131
+ // default each time would flash the track title over the formatter's line
132
+ // until the async result lands, since the two differ by construction on a live
133
+ // stream (station name vs. song line).
134
+ if formatter == nil || formattedTrackId != trackId {
135
+ applyFields(track: track, title: defaultTitle, artist: defaultSecondary, album: defaultAlbum)
136
+ }
89
137
 
90
138
  guard let formatter else { return }
91
139
  let params = FormatNowPlayingParams(
@@ -111,6 +159,9 @@ final class NowPlayingUpdater {
111
159
  artist: formatted.artist ?? defaultSecondary,
112
160
  album: formatted.album ?? defaultAlbum,
113
161
  )
162
+ // A formatter result now owns the surface for this track, so later
163
+ // renders skip the interim default write.
164
+ self.formattedTrackId = trackId
114
165
  }
115
166
  }
116
167
  .catch { @Sendable [weak self] error in
@@ -130,7 +181,7 @@ final class NowPlayingUpdater {
130
181
  let resolvedArtist = artist ?? track.artist
131
182
  let resolvedAlbum = album ?? track.album
132
183
  let published = Published(
133
- trackId: track.src ?? track.url,
184
+ trackId: track.identity ?? track.path,
134
185
  title: resolvedTitle,
135
186
  artist: resolvedArtist,
136
187
  album: resolvedAlbum,
@@ -152,10 +203,7 @@ final class NowPlayingUpdater {
152
203
  let artworkUrl = track.artworkSource?.uri ?? track.artwork?.url
153
204
  logger.debug("loadArtwork: \(track.title), artworkUrl: \(artworkUrl ?? "nil")")
154
205
 
155
- // Now Playing artwork: use screen width in pixels, capped at 1200px
156
- let screenScale = UIScreen.main.scale
157
- let screenWidth = UIScreen.main.bounds.width * screenScale
158
- let artworkSize = min(screenWidth, 1200)
206
+ let artworkSize = Self.artworkPixelWidth
159
207
  let nowPlayingSize = ImageContext(width: artworkSize, height: artworkSize)
160
208
 
161
209
  artworkLoadTask?.cancel()
@@ -526,14 +526,21 @@ class PlaybackCoordinator {
526
526
  let changed = queue.add([track], initialIndex: 0)
527
527
  if changed { handleCurrentTrackChanged() }
528
528
  } else {
529
- queue.replace(queue.currentIndex, track)
530
- handleCurrentTrackChanged()
529
+ // `currentIndex` is invariantly -1 or in bounds, and -1 took the branch
530
+ // above, so this cannot fail — but surface it rather than reloading the
531
+ // old track if that invariant is ever broken.
532
+ do {
533
+ try queue.replace(queue.currentIndex, track)
534
+ handleCurrentTrackChanged()
535
+ } catch {
536
+ logger.error("load: replace at \(self.queue.currentIndex) failed: \(error)")
537
+ }
531
538
  }
532
539
  }
533
540
  }
534
541
 
535
- func replace(_ index: Int, _ track: Track) {
536
- queue.replace(index, track)
542
+ func replace(_ index: Int, _ track: Track) throws {
543
+ try queue.replace(index, track)
537
544
  }
538
545
 
539
546
  func setQueue(
@@ -691,14 +698,14 @@ class PlaybackCoordinator {
691
698
  guard let src = currentTrack.src else {
692
699
  logger.error("Failed to load track - no src")
693
700
  logger.error(" track.title: \(currentTrack.title)")
694
- logger.error(" track.url: \(currentTrack.url ?? "nil")")
701
+ logger.error(" track.path: \(currentTrack.path ?? "nil")")
695
702
  effectHandler?.clearCurrentItem()
696
703
  transition(.errorOccurred(.invalidSourceUrl("nil")))
697
704
  return
698
705
  }
699
706
 
700
707
  logger.debug("Loading track: \(currentTrack.title)")
701
- logger.debug(" track.url: \(currentTrack.url ?? "nil")")
708
+ logger.debug(" track.path: \(currentTrack.path ?? "nil")")
702
709
  logger.debug(" track.src: \(src)")
703
710
 
704
711
  effectHandler?.loadTrack(src: src, track: currentTrack)
@@ -8,7 +8,7 @@ import os.log
8
8
  var isEnabled: Bool { get }
9
9
  /// Whether the current load has produced audio — selects the retry budget.
10
10
  var hasPlayed: Bool { get set }
11
- func isRetryable(_ error: Error?) -> Bool
11
+ func isRetryable(_ error: Error?, httpStatusCode: Int?) -> Bool
12
12
  func attemptRetry(startFromCurrentTime: Bool) async -> Bool
13
13
  func reset()
14
14
  }
@@ -62,7 +62,7 @@ public enum PlaybackErrorContext {
62
62
  logger.error("[\(String(describing: context))] failure with nil error")
63
63
  }
64
64
 
65
- if retryHandler.isEnabled, retryHandler.isRetryable(error) {
65
+ if retryHandler.isEnabled, retryHandler.isRetryable(error, httpStatusCode: httpStatusCode) {
66
66
  onRetryingError?(PlaybackErrorHandler.classify(
67
67
  error: error, fallback: context.fallbackError, httpStatusCode: httpStatusCode,
68
68
  ))
@@ -138,7 +138,7 @@ public enum PlaybackErrorContext {
138
138
  }
139
139
  }
140
140
 
141
- if let httpStatusCode, !(200...299).contains(httpStatusCode) {
141
+ if let httpStatusCode, !(200 ... 299).contains(httpStatusCode) {
142
142
  return .httpStatus(httpStatusCode)
143
143
  }
144
144
 
@@ -155,7 +155,7 @@ public enum PlaybackErrorContext {
155
155
  /// with HTTP statuses — so only in-range values count, and the newest wins:
156
156
  /// a live stream reconnects, and older entries describe earlier attempts.
157
157
  public static func httpStatusCode(fromErrorStatusCodes codes: [Int]) -> Int? {
158
- codes.last { (100...599).contains($0) }
158
+ codes.last { (100 ... 599).contains($0) }
159
159
  }
160
160
 
161
161
  /// The stream was fetched but cannot be decoded — unknown container,
@@ -48,10 +48,16 @@ final class PlaybackTimer {
48
48
 
49
49
  private func start(_ interval: TimeInterval) {
50
50
  guard timer == nil else { return }
51
- timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) {
52
- [weak self] _ in
51
+ // `.common`, not the `.default` mode `Timer.scheduledTimer` installs: UIKit
52
+ // runs the main run loop in tracking mode while a list is being dragged, and
53
+ // a default-mode timer does not fire there. Missed repeats are dropped rather
54
+ // than replayed, so progress events would freeze and interval ticks would be
55
+ // lost for as long as the user scrolls. (VolumeFader already does this.)
56
+ let timer = Timer(timeInterval: interval, repeats: true) { [weak self] _ in
53
57
  MainActor.assumeIsolated { self?.onTick() }
54
58
  }
59
+ self.timer = timer
60
+ RunLoop.main.add(timer, forMode: .common)
55
61
  }
56
62
 
57
63
  private func stop() {