react-native-audio-browser 0.2.0-next.3 → 0.2.0-next.30

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 (989) hide show
  1. package/README.md +36 -22
  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 +541 -278
  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 +52 -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 +27 -9
  11. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +288 -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 +62 -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 +110 -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 +100 -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 +40 -2
  51. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +301 -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 +168 -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 +160 -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 +55 -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/AVPlayerEngine.swift +330 -0
  76. package/ios/AVPlayerEngineCallbacks.swift +15 -0
  77. package/ios/Browser/BrowseGate.swift +58 -0
  78. package/ios/Browser/BrowserConfig.swift +117 -78
  79. package/ios/Browser/BrowserManager.swift +209 -603
  80. package/ios/Browser/BrowserPathHelper.swift +50 -46
  81. package/ios/Browser/InertStyleDiagnostic.swift +296 -0
  82. package/ios/Browser/JsonModels.swift +264 -106
  83. package/ios/Browser/NavigationErrorState.swift +41 -0
  84. package/ios/Browser/PageResolver.swift +227 -0
  85. package/ios/Browser/RequestLayer.swift +90 -0
  86. package/ios/Browser/{BrowserManager+URLResolution.swift → RequestPipeline.swift} +193 -121
  87. package/ios/Browser/SearchDrillIn.swift +7 -7
  88. package/ios/Browser/SectionScope.swift +58 -24
  89. package/ios/Browser/StyleResolver.swift +58 -0
  90. package/ios/Browser/TabBarEntries.swift +9 -3
  91. package/ios/Browser/TrackIdTemplate.swift +37 -0
  92. package/ios/CarPlay/CPInterfaceController+Safe.swift +100 -43
  93. package/ios/CarPlay/CarPlayArtworkResolver.swift +36 -83
  94. package/ios/CarPlay/CarPlayController.swift +42 -1296
  95. package/ios/CarPlay/CarPlayImageLoader.swift +78 -97
  96. package/ios/CarPlay/CarPlayListItemFactory.swift +303 -92
  97. package/ios/CarPlay/CarPlayNowPlayingManager.swift +129 -136
  98. package/ios/CarPlay/CarPlaySession+Navigator.swift +114 -0
  99. package/ios/CarPlay/CarPlaySession+PageLoader.swift +92 -0
  100. package/ios/CarPlay/CarPlaySession+Refresher.swift +43 -0
  101. package/ios/CarPlay/CarPlaySession+TabBar.swift +193 -0
  102. package/ios/CarPlay/CarPlaySession.swift +209 -0
  103. package/ios/CarPlay/CarPlayTemplates.swift +63 -0
  104. package/ios/CarPlay/HybridAudioBrowser+IntentFavorite.swift +20 -0
  105. package/ios/{Player → CarPlay}/MediaIntentCriteria.swift +17 -9
  106. package/ios/CarPlay/MediaIntentPlayback.swift +74 -0
  107. package/ios/CarPlay/RNABMediaAddHandler.swift +8 -24
  108. package/ios/CarPlay/RNABMediaAffinityHandler.swift +7 -23
  109. package/ios/CarPlay/RNABMediaIntentHandler.swift +18 -19
  110. package/ios/CarPlay/SectionPresentation.swift +77 -0
  111. package/ios/CarPlay/TopTemplateReplacer.swift +96 -0
  112. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  113. package/ios/Extension/NavigationError+Conversion.swift +14 -4
  114. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  115. package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
  116. package/ios/Extension/Section+Copying.swift +29 -0
  117. package/ios/Extension/String+IcyCharset.swift +21 -0
  118. package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
  119. package/ios/Extension/Track+Copying.swift +8 -12
  120. package/ios/Extension/Track+Identity.swift +18 -0
  121. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  122. package/ios/Extension/UIImage+Adaptive.swift +12 -0
  123. package/ios/Extension/URLError+Connectivity.swift +15 -0
  124. package/ios/Http/HttpClient.swift +66 -142
  125. package/ios/HybridAudioBrowser+Browser.swift +153 -0
  126. package/ios/HybridAudioBrowser+Options.swift +144 -0
  127. package/ios/HybridAudioBrowser+Player.swift +397 -0
  128. package/ios/HybridAudioBrowser+PlayerCallbacks.swift +190 -0
  129. package/ios/HybridAudioBrowser+Stubs.swift +46 -0
  130. package/ios/HybridAudioBrowser.swift +153 -1610
  131. package/ios/Model/NitroTypeStubs.swift +146 -24
  132. package/ios/Model/PlayerUpdateOptions.swift +8 -1
  133. package/ios/Model/RemoteCommand.swift +18 -0
  134. package/ios/Model/TrackPlayerError.swift +2 -2
  135. package/ios/NowPlayingInfo/NowPlayingInfoController.swift +18 -53
  136. package/ios/Observer/ItemObserver.swift +90 -0
  137. package/ios/Observer/PlayerObserver.swift +59 -0
  138. package/ios/Option/SessionCategories.swift +42 -92
  139. package/ios/Player/AudioSessionController.swift +177 -0
  140. package/ios/Player/DeferredSeek.swift +70 -0
  141. package/ios/Player/ErrorStatus.swift +27 -0
  142. package/ios/Player/MediaLoader.swift +62 -35
  143. package/ios/Player/NowPlayingUpdater.swift +197 -50
  144. package/ios/Player/PlaybackFailure.swift +125 -0
  145. package/ios/Player/PlaybackPersistence.swift +73 -0
  146. package/ios/Player/PlaybackTimer.swift +8 -2
  147. package/ios/Player/Player.swift +1078 -0
  148. package/ios/Player/{PlaybackCoordinatorCallbacks.swift → PlayerCallbacks.swift} +3 -4
  149. package/ios/Player/PlayerEngine.swift +87 -0
  150. package/ios/Player/PlayerStatusTypes.swift +2 -6
  151. package/ios/Player/QueueManager.swift +93 -67
  152. package/ios/Player/RetryLoop.swift +261 -0
  153. package/ios/Player/ShuffleOrder.swift +33 -0
  154. package/ios/Player/SleepTimerManager.swift +1 -1
  155. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +46 -0
  156. package/ios/RemoteCommand/RemoteCommandController.swift +87 -331
  157. package/ios/Tests/BrowserPathHelperTests.swift +56 -15
  158. package/ios/Tests/CarPlayArtworkResolverTests.swift +60 -373
  159. package/ios/Tests/DeferredSeekTests.swift +70 -0
  160. package/ios/Tests/EmitterTests.swift +29 -0
  161. package/ios/Tests/GenerationMemoTests.swift +121 -0
  162. package/ios/Tests/InertStyleDiagnosticTests.swift +209 -0
  163. package/ios/Tests/JsonModelsTests.swift +164 -1
  164. package/ios/Tests/LRUCacheTests.swift +110 -0
  165. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  166. package/ios/Tests/MediaLoaderTests.swift +12 -48
  167. package/ios/Tests/ObserverTests.swift +88 -0
  168. package/ios/Tests/PlaybackErrorTests.swift +76 -0
  169. package/ios/Tests/PlaybackFailureTests.swift +189 -0
  170. package/ios/Tests/PlaybackPersistenceTests.swift +93 -0
  171. package/ios/Tests/PlayerPolicyTests.swift +420 -0
  172. package/ios/Tests/{PlaybackCoordinatorTests.swift → PlayerTests.swift} +300 -248
  173. package/ios/Tests/PlayingStateTests.swift +42 -0
  174. package/ios/Tests/QueueManagerTests.swift +196 -115
  175. package/ios/Tests/ReadyValueTests.swift +73 -0
  176. package/ios/Tests/RemoteCommandControllerTests.swift +152 -0
  177. package/ios/Tests/RemoteCommandTests.swift +65 -0
  178. package/ios/Tests/RequestLayerTests.swift +112 -0
  179. package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
  180. package/ios/Tests/RetryLoopTests.swift +270 -0
  181. package/ios/Tests/SearchDrillInTests.swift +16 -16
  182. package/ios/Tests/SectionPresentationTests.swift +98 -0
  183. package/ios/Tests/SectionScopeTests.swift +87 -55
  184. package/ios/Tests/StyleResolverTests.swift +132 -0
  185. package/ios/Tests/Support/TestTypes.swift +108 -104
  186. package/ios/Tests/TabBarEntriesTests.swift +19 -19
  187. package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
  188. package/ios/Tests/TopTemplateReplacerTests.swift +172 -0
  189. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  190. package/ios/Tests/TrackIdentityTests.swift +20 -0
  191. package/ios/Tests/TrackSelectorTests.swift +168 -33
  192. package/ios/Tests/URLErrorConnectivityTests.swift +20 -0
  193. package/ios/TrackSelector.swift +46 -31
  194. package/ios/Util/ArtworkImageFetcher.swift +58 -1
  195. package/ios/Util/Emitter.swift +42 -0
  196. package/ios/Util/GenerationMemo.swift +36 -0
  197. package/ios/Util/LRUCache.swift +23 -159
  198. package/ios/Util/NetworkMonitor.swift +43 -26
  199. package/ios/Util/ReadyValue.swift +56 -0
  200. package/ios/Util/SFSymbolRenderer.swift +59 -31
  201. package/ios/Util/SVGProcessor.swift +9 -12
  202. package/lib/commonjs/AudioBrowser.js +8 -8
  203. package/lib/commonjs/AudioBrowser.js.map +1 -1
  204. package/lib/commonjs/CLAUDE.md +52 -0
  205. package/lib/commonjs/features/browser-config.js +12 -8
  206. package/lib/commonjs/features/browser-config.js.map +1 -1
  207. package/lib/commonjs/features/browser.js +11 -3
  208. package/lib/commonjs/features/browser.js.map +1 -1
  209. package/lib/commonjs/features/carConnection.js +1 -1
  210. package/lib/commonjs/features/equalizer.js +1 -1
  211. package/lib/commonjs/features/errors.js +3 -3
  212. package/lib/commonjs/features/favorites.js +14 -11
  213. package/lib/commonjs/features/favorites.js.map +1 -1
  214. package/lib/commonjs/features/gate.js +16 -1
  215. package/lib/commonjs/features/gate.js.map +1 -1
  216. package/lib/commonjs/features/metadata.js +20 -3
  217. package/lib/commonjs/features/metadata.js.map +1 -1
  218. package/lib/commonjs/features/network.js +1 -1
  219. package/lib/commonjs/features/nowPlaying.js +1 -1
  220. package/lib/commonjs/features/output.js +1 -1
  221. package/lib/commonjs/features/playback/controls.js +4 -2
  222. package/lib/commonjs/features/playback/controls.js.map +1 -1
  223. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  224. package/lib/commonjs/features/playback/playing.js +1 -1
  225. package/lib/commonjs/features/playback/progress.js +3 -2
  226. package/lib/commonjs/features/playback/progress.js.map +1 -1
  227. package/lib/commonjs/features/playback/rate.js +8 -1
  228. package/lib/commonjs/features/playback/rate.js.map +1 -1
  229. package/lib/commonjs/features/playback/state.js +3 -4
  230. package/lib/commonjs/features/playback/state.js.map +1 -1
  231. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  232. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  233. package/lib/commonjs/features/playback/volume.js +1 -1
  234. package/lib/commonjs/features/player/options.js +48 -34
  235. package/lib/commonjs/features/player/options.js.map +1 -1
  236. package/lib/commonjs/features/player/setup.js +18 -9
  237. package/lib/commonjs/features/player/setup.js.map +1 -1
  238. package/lib/commonjs/features/player/validateOptions.js +23 -0
  239. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  240. package/lib/commonjs/features/queue/activeTrack.js +15 -4
  241. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  242. package/lib/commonjs/features/queue/queue.js +13 -6
  243. package/lib/commonjs/features/queue/queue.js.map +1 -1
  244. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  245. package/lib/commonjs/features/queue/shuffle.js +1 -1
  246. package/lib/commonjs/features/remoteControls.js +13 -92
  247. package/lib/commonjs/features/remoteControls.js.map +1 -1
  248. package/lib/commonjs/features/sleepTimer.js +13 -1
  249. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  250. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  251. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  252. package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
  253. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  254. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  255. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  256. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  257. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  258. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  259. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  260. package/lib/commonjs/web/NativeAudioBrowser.js +201 -76
  261. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  262. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
  263. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
  264. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  265. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  266. package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
  267. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  268. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +1 -1
  269. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  270. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  271. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
  272. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
  273. package/lib/commonjs/web/browser/BrowserManager.js +175 -100
  274. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  275. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  276. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  277. package/lib/commonjs/web/browser/NavigationErrorManager.js +3 -1
  278. package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
  279. package/lib/commonjs/web/browser/SearchManager.js +18 -23
  280. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  281. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  282. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  283. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  284. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  285. package/lib/commonjs/web/browser/sections.js +89 -0
  286. package/lib/commonjs/web/browser/sections.js.map +1 -0
  287. package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
  288. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  289. package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
  290. package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
  291. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  292. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  293. package/lib/commonjs/web/util/BrowserPathHelper.js +62 -11
  294. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  295. package/lib/module/AudioBrowser.js +2 -1
  296. package/lib/module/AudioBrowser.js.map +1 -1
  297. package/lib/module/CLAUDE.md +52 -0
  298. package/lib/module/features/browser-config.js +12 -8
  299. package/lib/module/features/browser-config.js.map +1 -1
  300. package/lib/module/features/browser.js +11 -3
  301. package/lib/module/features/browser.js.map +1 -1
  302. package/lib/module/features/carConnection.js +1 -1
  303. package/lib/module/features/equalizer.js +1 -1
  304. package/lib/module/features/errors.js +3 -3
  305. package/lib/module/features/favorites.js +14 -11
  306. package/lib/module/features/favorites.js.map +1 -1
  307. package/lib/module/features/gate.js +16 -1
  308. package/lib/module/features/gate.js.map +1 -1
  309. package/lib/module/features/metadata.js +20 -3
  310. package/lib/module/features/metadata.js.map +1 -1
  311. package/lib/module/features/network.js +1 -1
  312. package/lib/module/features/nowPlaying.js +1 -1
  313. package/lib/module/features/output.js +1 -1
  314. package/lib/module/features/playback/controls.js +4 -2
  315. package/lib/module/features/playback/controls.js.map +1 -1
  316. package/lib/module/features/playback/playWhenReady.js +1 -1
  317. package/lib/module/features/playback/playing.js +1 -1
  318. package/lib/module/features/playback/progress.js +3 -2
  319. package/lib/module/features/playback/progress.js.map +1 -1
  320. package/lib/module/features/playback/rate.js +8 -1
  321. package/lib/module/features/playback/rate.js.map +1 -1
  322. package/lib/module/features/playback/state.js +3 -4
  323. package/lib/module/features/playback/state.js.map +1 -1
  324. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  325. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  326. package/lib/module/features/playback/volume.js +1 -1
  327. package/lib/module/features/player/options.js +47 -32
  328. package/lib/module/features/player/options.js.map +1 -1
  329. package/lib/module/features/player/setup.js +17 -8
  330. package/lib/module/features/player/setup.js.map +1 -1
  331. package/lib/module/features/player/validateOptions.js +19 -0
  332. package/lib/module/features/player/validateOptions.js.map +1 -0
  333. package/lib/module/features/queue/activeTrack.js +15 -4
  334. package/lib/module/features/queue/activeTrack.js.map +1 -1
  335. package/lib/module/features/queue/queue.js +13 -6
  336. package/lib/module/features/queue/queue.js.map +1 -1
  337. package/lib/module/features/queue/repeatMode.js +1 -1
  338. package/lib/module/features/queue/shuffle.js +1 -1
  339. package/lib/module/features/remoteControls.js +13 -92
  340. package/lib/module/features/remoteControls.js.map +1 -1
  341. package/lib/module/features/sleepTimer.js +13 -1
  342. package/lib/module/features/sleepTimer.js.map +1 -1
  343. package/lib/module/test-utils/reactNativeStub.js +58 -0
  344. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  345. package/lib/module/utils/LazyNativeEmitter.js +28 -14
  346. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  347. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  348. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  349. package/lib/module/utils/getTrackIdentity.js +20 -0
  350. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  351. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  352. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  353. package/lib/module/web/NativeAudioBrowser.js +201 -76
  354. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  355. package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
  356. package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
  357. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  358. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  359. package/lib/module/web/TrackPlayer/Player.js +254 -39
  360. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  361. package/lib/module/web/TrackPlayer/PlayingStateFactory.js +1 -1
  362. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  363. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  364. package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
  365. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
  366. package/lib/module/web/browser/BrowserManager.js +175 -100
  367. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  368. package/lib/module/web/browser/FavoriteManager.js +31 -21
  369. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  370. package/lib/module/web/browser/NavigationErrorManager.js +3 -1
  371. package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
  372. package/lib/module/web/browser/SearchManager.js +18 -23
  373. package/lib/module/web/browser/SearchManager.js.map +1 -1
  374. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  375. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  376. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  377. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  378. package/lib/module/web/browser/sections.js +83 -0
  379. package/lib/module/web/browser/sections.js.map +1 -0
  380. package/lib/module/web/http/RequestConfigBuilder.js +110 -27
  381. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  382. package/lib/module/web/http/followMediaRedirect.js +215 -0
  383. package/lib/module/web/http/followMediaRedirect.js.map +1 -0
  384. package/lib/module/web/player/NowPlayingManager.js +4 -3
  385. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  386. package/lib/module/web/util/BrowserPathHelper.js +62 -11
  387. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  388. package/lib/typescript/package.json +1 -0
  389. package/lib/typescript/src/AudioBrowser.d.ts +1 -1
  390. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  391. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  392. package/lib/typescript/src/features/browser.d.ts +10 -2
  393. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  394. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  395. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  396. package/lib/typescript/src/features/errors.d.ts +3 -3
  397. package/lib/typescript/src/features/favorites.d.ts +14 -11
  398. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  399. package/lib/typescript/src/features/gate.d.ts +18 -2
  400. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  401. package/lib/typescript/src/features/metadata.d.ts +20 -3
  402. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  403. package/lib/typescript/src/features/network.d.ts +1 -1
  404. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  405. package/lib/typescript/src/features/output.d.ts +1 -1
  406. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  407. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  408. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  409. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  410. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  411. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  412. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  413. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  414. package/lib/typescript/src/features/playback/state.d.ts +3 -4
  415. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  416. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  417. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  418. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  419. package/lib/typescript/src/features/player/options.d.ts +108 -56
  420. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  421. package/lib/typescript/src/features/player/setup.d.ts +14 -4
  422. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  423. package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
  424. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  425. package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
  426. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  427. package/lib/typescript/src/features/queue/queue.d.ts +7 -4
  428. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  429. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  430. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  431. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  432. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  433. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  434. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  435. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  436. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  437. package/lib/typescript/src/types/browser-native.d.ts +3 -2
  438. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  439. package/lib/typescript/src/types/browser-nodes.d.ts +387 -126
  440. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  441. package/lib/typescript/src/types/browser.d.ts +93 -61
  442. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  443. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
  444. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  445. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  446. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  447. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  448. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  449. package/lib/typescript/src/utils/resolveAssetSource.d.ts +1 -2
  450. package/lib/typescript/src/utils/resolveAssetSource.d.ts.map +1 -1
  451. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  452. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  453. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
  454. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +4 -2
  455. package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +2 -2
  456. package/nitrogen/generated/android/c++/JAndroidAudioOffloadSettings.hpp +2 -2
  457. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +12 -12
  458. package/nitrogen/generated/android/c++/JAndroidPlayerWakeMode.hpp +2 -2
  459. package/nitrogen/generated/android/c++/JAppKilledPlaybackBehavior.hpp +2 -2
  460. package/nitrogen/generated/android/c++/JArtworkRendering.hpp +58 -0
  461. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +10 -5
  462. package/nitrogen/generated/android/c++/JArtworkVariants.hpp +2 -2
  463. package/nitrogen/generated/android/c++/JBatteryOptimizationStatus.hpp +2 -2
  464. package/nitrogen/generated/android/c++/JBatteryOptimizationStatusChangedEvent.hpp +2 -2
  465. package/nitrogen/generated/android/c++/JBatteryWarningPendingChangedEvent.hpp +2 -2
  466. package/nitrogen/generated/android/c++/JBrowseError.hpp +2 -2
  467. package/nitrogen/generated/android/c++/JBrowseResult.hpp +19 -7
  468. package/nitrogen/generated/android/c++/JBrowserSourceCallbackParam.hpp +2 -2
  469. package/nitrogen/generated/android/c++/JCarPlayNowPlayingButton.hpp +2 -2
  470. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  471. package/nitrogen/generated/android/c++/JCarPlaySiriListButtonPosition.hpp +2 -2
  472. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  473. package/nitrogen/generated/android/c++/JChapterMetadata.hpp +2 -2
  474. package/nitrogen/generated/android/c++/JEqualizerSettings.hpp +10 -10
  475. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +10 -5
  476. package/nitrogen/generated/android/c++/JFormatNavigationErrorParams.hpp +11 -5
  477. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +10 -5
  478. package/nitrogen/generated/android/c++/JFormattedNavigationError.hpp +2 -2
  479. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.hpp +2 -2
  480. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
  481. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +10 -5
  482. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.hpp +4 -2
  483. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +10 -5
  484. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +10 -5
  485. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.hpp +2 -2
  486. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.hpp +2 -2
  487. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
  488. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +10 -5
  489. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.hpp +2 -2
  490. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +18 -12
  491. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +10 -4
  492. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
  493. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +18 -6
  494. package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -2
  495. package/nitrogen/generated/android/c++/JFunc_void_BatteryOptimizationStatusChangedEvent.hpp +2 -2
  496. package/nitrogen/generated/android/c++/JFunc_void_BatteryWarningPendingChangedEvent.hpp +2 -2
  497. package/nitrogen/generated/android/c++/JFunc_void_EqualizerSettings.hpp +2 -2
  498. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +10 -5
  499. package/nitrogen/generated/android/c++/JFunc_void_GateEvent.hpp +2 -2
  500. package/nitrogen/generated/android/c++/JFunc_void_NavigationErrorEvent.hpp +2 -2
  501. package/nitrogen/generated/android/c++/JFunc_void_NowPlayingMetadata.hpp +2 -2
  502. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +9 -12
  503. package/nitrogen/generated/android/c++/JFunc_void_Output.hpp +2 -2
  504. package/nitrogen/generated/android/c++/JFunc_void_Playback.hpp +2 -2
  505. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +10 -5
  506. package/nitrogen/generated/android/c++/JFunc_void_PlaybackErrorEvent.hpp +2 -2
  507. package/nitrogen/generated/android/c++/JFunc_void_PlaybackPlayWhenReadyChangedEvent.hpp +2 -2
  508. package/nitrogen/generated/android/c++/JFunc_void_PlaybackProgressUpdatedEvent.hpp +2 -2
  509. package/nitrogen/generated/android/c++/JFunc_void_PlaybackQueueEndedEvent.hpp +2 -2
  510. package/nitrogen/generated/android/c++/JFunc_void_PlayingState.hpp +2 -2
  511. package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpBackwardEvent.hpp +2 -2
  512. package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpForwardEvent.hpp +2 -2
  513. package/nitrogen/generated/android/c++/JFunc_void_RemotePlayIdEvent.hpp +2 -2
  514. package/nitrogen/generated/android/c++/JFunc_void_RemotePlaySearchEvent.hpp +2 -2
  515. package/nitrogen/generated/android/c++/JFunc_void_RemoteSeekEvent.hpp +2 -2
  516. package/nitrogen/generated/android/c++/JFunc_void_RemoteSkipEvent.hpp +2 -2
  517. package/nitrogen/generated/android/c++/JFunc_void_RepeatModeChangedEvent.hpp +2 -2
  518. package/nitrogen/generated/android/c++/JFunc_void_TimedMetadata.hpp +2 -2
  519. package/nitrogen/generated/android/c++/JFunc_void_TrackMetadata.hpp +2 -2
  520. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +2 -2
  521. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +2 -2
  522. package/nitrogen/generated/android/c++/JFunc_void_std__optional_FormattedNavigationError_.hpp +2 -2
  523. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +17 -5
  524. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.hpp +2 -2
  525. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +2 -2
  526. package/nitrogen/generated/android/c++/JFunc_void_std__vector_ChapterMetadata_.hpp +16 -16
  527. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +24 -18
  528. package/nitrogen/generated/android/c++/JGate.hpp +2 -2
  529. package/nitrogen/generated/android/c++/JGateDecision.hpp +2 -2
  530. package/nitrogen/generated/android/c++/JGateEvent.hpp +2 -2
  531. package/nitrogen/generated/android/c++/JGateReason.hpp +2 -2
  532. package/nitrogen/generated/android/c++/JGridTile.hpp +64 -0
  533. package/nitrogen/generated/android/c++/JHttpMethod.hpp +2 -2
  534. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +77 -53
  535. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +3 -3
  536. package/nitrogen/generated/android/c++/JIOSCategory.hpp +2 -2
  537. package/nitrogen/generated/android/c++/JIOSCategoryMode.hpp +2 -2
  538. package/nitrogen/generated/android/c++/JIOSCategoryOptions.hpp +2 -2
  539. package/nitrogen/generated/android/c++/JIOSCategoryPolicy.hpp +2 -2
  540. package/nitrogen/generated/android/c++/JIOSOptions.hpp +17 -11
  541. package/nitrogen/generated/android/c++/JImageContext.hpp +2 -2
  542. package/nitrogen/generated/android/c++/JImageQueryParams.hpp +2 -2
  543. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  544. package/nitrogen/generated/android/c++/JImageSource.hpp +2 -2
  545. package/nitrogen/generated/android/c++/JMediaReference.hpp +2 -2
  546. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +10 -5
  547. package/nitrogen/generated/android/c++/JMediaTransformParams.hpp +2 -2
  548. package/nitrogen/generated/android/c++/JNativeAndroidSetupOptions.hpp +2 -2
  549. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +48 -21
  550. package/nitrogen/generated/android/c++/JNativeGateRequest.hpp +2 -2
  551. package/nitrogen/generated/android/c++/JNativeIOSSetupOptions.hpp +10 -10
  552. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +16 -4
  553. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +17 -14
  554. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +9 -12
  555. package/nitrogen/generated/android/c++/JNavigationError.hpp +2 -2
  556. package/nitrogen/generated/android/c++/JNavigationErrorEvent.hpp +2 -2
  557. package/nitrogen/generated/android/c++/JNavigationErrorKind.hpp +58 -0
  558. package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +2 -2
  559. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +12 -12
  560. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +17 -11
  561. package/nitrogen/generated/android/c++/JNowPlayingMetadata.hpp +2 -2
  562. package/nitrogen/generated/android/c++/JNowPlayingUpdate.hpp +2 -2
  563. package/nitrogen/generated/android/c++/JOptions.hpp +9 -12
  564. package/nitrogen/generated/android/c++/JOutput.hpp +2 -2
  565. package/nitrogen/generated/android/c++/JOutputType.hpp +2 -2
  566. package/nitrogen/generated/android/c++/JPlayback.hpp +2 -2
  567. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +10 -5
  568. package/nitrogen/generated/android/c++/JPlaybackError.hpp +2 -2
  569. package/nitrogen/generated/android/c++/JPlaybackErrorEvent.hpp +2 -2
  570. package/nitrogen/generated/android/c++/JPlaybackErrorKind.hpp +2 -2
  571. package/nitrogen/generated/android/c++/JPlaybackPlayWhenReadyChangedEvent.hpp +2 -2
  572. package/nitrogen/generated/android/c++/JPlaybackProgressUpdatedEvent.hpp +2 -2
  573. package/nitrogen/generated/android/c++/JPlaybackQueueEndedEvent.hpp +2 -2
  574. package/nitrogen/generated/android/c++/JPlaybackState.hpp +2 -2
  575. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +7 -13
  576. package/nitrogen/generated/android/c++/JPlayingState.hpp +2 -2
  577. package/nitrogen/generated/android/c++/JProgress.hpp +2 -2
  578. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  579. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  580. package/nitrogen/generated/android/c++/JRemoteJumpBackwardEvent.hpp +2 -2
  581. package/nitrogen/generated/android/c++/JRemoteJumpForwardEvent.hpp +2 -2
  582. package/nitrogen/generated/android/c++/JRemotePlayIdEvent.hpp +2 -2
  583. package/nitrogen/generated/android/c++/JRemotePlaySearchEvent.hpp +2 -2
  584. package/nitrogen/generated/android/c++/JRemoteSeekEvent.hpp +2 -2
  585. package/nitrogen/generated/android/c++/JRemoteSkipEvent.hpp +2 -2
  586. package/nitrogen/generated/android/c++/JRepeatMode.hpp +2 -2
  587. package/nitrogen/generated/android/c++/JRepeatModeChangedEvent.hpp +2 -2
  588. package/nitrogen/generated/android/c++/JRequestConfig.hpp +2 -2
  589. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +67 -59
  590. package/nitrogen/generated/android/c++/JRetryConfig.hpp +2 -2
  591. package/nitrogen/generated/android/c++/JSearchMode.hpp +2 -2
  592. package/nitrogen/generated/android/c++/JSearchParams.hpp +2 -2
  593. package/nitrogen/generated/android/c++/JSection.hpp +116 -0
  594. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  595. package/nitrogen/generated/android/c++/JSleepTimer.hpp +4 -4
  596. package/nitrogen/generated/android/c++/JSleepTimerEndOfTrack.hpp +2 -2
  597. package/nitrogen/generated/android/c++/JSleepTimerTime.hpp +2 -2
  598. package/nitrogen/generated/android/c++/JStallReason.hpp +2 -2
  599. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  600. package/nitrogen/generated/android/c++/JTimedMetadata.hpp +2 -2
  601. package/nitrogen/generated/android/c++/JTrack.hpp +27 -48
  602. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +18 -12
  603. package/nitrogen/generated/android/c++/JTrackMetadata.hpp +2 -2
  604. package/nitrogen/generated/android/c++/JTrackRequest.hpp +2 -2
  605. package/nitrogen/generated/android/c++/JTrackStyle.hpp +47 -19
  606. package/nitrogen/generated/android/c++/JTransformableRequestConfig.hpp +2 -2
  607. package/nitrogen/generated/android/c++/JVariant_Boolean_AndroidAudioOffloadSettings.hpp +3 -3
  608. package/nitrogen/generated/android/c++/JVariant_Boolean_RetryConfig.hpp +3 -3
  609. package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +3 -3
  610. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  611. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  612. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +20 -8
  613. package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.hpp +3 -3
  614. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidAudioOffloadSettings.kt +15 -0
  615. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +20 -3
  616. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  617. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkRequestConfig.kt +37 -0
  618. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkVariants.kt +15 -0
  619. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryOptimizationStatusChangedEvent.kt +13 -0
  620. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryWarningPendingChangedEvent.kt +13 -0
  621. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseError.kt +13 -0
  622. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseResult.kt +16 -7
  623. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowserSourceCallbackParam.kt +15 -0
  624. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  625. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  626. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ChapterMetadata.kt +19 -0
  627. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/EqualizerSettings.kt +27 -0
  628. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteChangedEvent.kt +15 -0
  629. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNavigationErrorParams.kt +25 -3
  630. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNowPlayingParams.kt +21 -0
  631. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormattedNavigationError.kt +15 -0
  632. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.kt +0 -2
  633. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
  634. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___Track.kt +0 -2
  635. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.kt +0 -2
  636. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.kt +0 -2
  637. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_std__string____Track.kt +0 -2
  638. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.kt +0 -2
  639. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.kt +0 -2
  640. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
  641. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.kt +0 -2
  642. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.kt +0 -2
  643. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.kt +0 -2
  644. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.kt +0 -2
  645. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +78 -0
  646. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.kt +0 -2
  647. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void.kt +0 -2
  648. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryOptimizationStatusChangedEvent.kt +0 -2
  649. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryWarningPendingChangedEvent.kt +0 -2
  650. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_EqualizerSettings.kt +0 -2
  651. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_FavoriteChangedEvent.kt +0 -2
  652. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_GateEvent.kt +0 -2
  653. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NavigationErrorEvent.kt +0 -2
  654. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NowPlayingMetadata.kt +0 -2
  655. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Options.kt +0 -2
  656. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Output.kt +0 -2
  657. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Playback.kt +0 -2
  658. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackActiveTrackChangedEvent.kt +0 -2
  659. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackErrorEvent.kt +0 -2
  660. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackPlayWhenReadyChangedEvent.kt +0 -2
  661. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackProgressUpdatedEvent.kt +0 -2
  662. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackQueueEndedEvent.kt +0 -2
  663. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlayingState.kt +0 -2
  664. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpBackwardEvent.kt +0 -2
  665. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpForwardEvent.kt +0 -2
  666. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlayIdEvent.kt +0 -2
  667. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlaySearchEvent.kt +0 -2
  668. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSeekEvent.kt +0 -2
  669. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSkipEvent.kt +0 -2
  670. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RepeatModeChangedEvent.kt +0 -2
  671. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TimedMetadata.kt +0 -2
  672. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TrackMetadata.kt +0 -2
  673. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_bool.kt +0 -2
  674. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_double.kt +0 -2
  675. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_FormattedNavigationError_.kt +0 -2
  676. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_ResolvedTrack_.kt +0 -2
  677. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.kt +0 -2
  678. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__string.kt +0 -2
  679. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_ChapterMetadata_.kt +0 -2
  680. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_Track_.kt +0 -2
  681. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Gate.kt +15 -0
  682. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateDecision.kt +15 -0
  683. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateEvent.kt +13 -0
  684. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +25 -0
  685. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +3 -1
  686. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +25 -3
  687. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageContext.kt +15 -0
  688. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageQueryParams.kt +15 -0
  689. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  690. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageSource.kt +19 -0
  691. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaRequestConfig.kt +35 -0
  692. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaTransformParams.kt +15 -0
  693. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeAndroidSetupOptions.kt +31 -0
  694. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +51 -5
  695. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeGateRequest.kt +17 -0
  696. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeIOSSetupOptions.kt +21 -0
  697. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeRouteEntry.kt +27 -0
  698. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeSetupPlayerOptions.kt +29 -0
  699. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeUpdateOptions.kt +23 -0
  700. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationError.kt +19 -0
  701. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorEvent.kt +13 -0
  702. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorKind.kt +23 -0
  703. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +20 -3
  704. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +25 -3
  705. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingMetadata.kt +29 -0
  706. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingUpdate.kt +17 -0
  707. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Options.kt +23 -0
  708. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Output.kt +17 -0
  709. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Playback.kt +15 -0
  710. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackActiveTrackChangedEvent.kt +21 -0
  711. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackError.kt +21 -0
  712. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackErrorEvent.kt +13 -0
  713. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackPlayWhenReadyChangedEvent.kt +13 -0
  714. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackProgressUpdatedEvent.kt +19 -0
  715. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackQueueEndedEvent.kt +15 -0
  716. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +37 -2
  717. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayingState.kt +15 -0
  718. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Progress.kt +17 -0
  719. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  720. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +61 -0
  721. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpBackwardEvent.kt +13 -0
  722. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpForwardEvent.kt +13 -0
  723. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlayIdEvent.kt +15 -0
  724. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlaySearchEvent.kt +13 -0
  725. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSeekEvent.kt +13 -0
  726. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSkipEvent.kt +13 -0
  727. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RepeatModeChangedEvent.kt +13 -0
  728. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RequestConfig.kt +27 -0
  729. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +67 -15
  730. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RetryConfig.kt +17 -0
  731. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchParams.kt +27 -0
  732. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +66 -0
  733. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +86 -0
  734. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimer.kt +18 -8
  735. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerEndOfTrack.kt +13 -0
  736. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerTime.kt +13 -0
  737. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  738. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TimedMetadata.kt +21 -0
  739. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +55 -12
  740. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackLoadEvent.kt +17 -0
  741. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackMetadata.kt +43 -0
  742. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackRequest.kt +17 -0
  743. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +58 -5
  744. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TransformableRequestConfig.kt +31 -0
  745. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_AndroidAudioOffloadSettings.kt +16 -7
  746. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_RetryConfig.kt +16 -7
  747. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_NullType_Double.kt +16 -7
  748. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +24 -15
  749. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.kt +18 -8
  750. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_String_ArtworkVariants.kt +16 -7
  751. package/nitrogen/generated/ios/AudioBrowser+autolinking.rb +2 -0
  752. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +38 -29
  753. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +779 -665
  754. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +34 -16
  755. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +36 -18
  756. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  757. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  758. package/nitrogen/generated/ios/swift/BrowseResult.swift +12 -0
  759. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  760. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  761. package/nitrogen/generated/ios/swift/FormatNavigationErrorParams.swift +7 -2
  762. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
  763. package/nitrogen/generated/ios/swift/GridTile.swift +48 -0
  764. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
  765. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
  766. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  767. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  768. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
  769. package/nitrogen/generated/ios/swift/NavigationErrorKind.swift +40 -0
  770. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  771. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  772. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  773. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  774. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  775. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
  776. package/nitrogen/generated/ios/swift/Section.swift +82 -0
  777. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  778. package/nitrogen/generated/ios/swift/SleepTimer.swift +13 -0
  779. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  780. package/nitrogen/generated/ios/swift/Track.swift +28 -63
  781. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  782. package/nitrogen/generated/ios/swift/Variant_Bool_AndroidAudioOffloadSettings.swift +12 -0
  783. package/nitrogen/generated/ios/swift/Variant_Bool_RetryConfig.swift +12 -0
  784. package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +12 -0
  785. package/nitrogen/generated/ios/swift/Variant_NullType_RemoteButtonLayout.swift +30 -0
  786. package/nitrogen/generated/ios/swift/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.swift +13 -0
  787. package/nitrogen/generated/ios/swift/Variant_String_ArtworkVariants.swift +12 -0
  788. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  789. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  790. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  791. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  792. package/nitrogen/generated/shared/c++/FormatNavigationErrorParams.hpp +9 -2
  793. package/nitrogen/generated/shared/c++/GridTile.hpp +84 -0
  794. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
  795. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
  796. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  797. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  798. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
  799. package/nitrogen/generated/shared/c++/NavigationErrorKind.hpp +76 -0
  800. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  801. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  802. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  803. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  804. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  805. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
  806. package/nitrogen/generated/shared/c++/Section.hpp +102 -0
  807. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  808. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  809. package/nitrogen/generated/shared/c++/Track.hpp +18 -30
  810. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  811. package/package.json +24 -11
  812. package/src/AudioBrowser.ts +2 -1
  813. package/src/CLAUDE.md +52 -0
  814. package/src/features/battery.test.ts +79 -0
  815. package/src/features/browser-config.test.ts +21 -6
  816. package/src/features/browser-config.ts +12 -8
  817. package/src/features/browser.test.ts +1 -1
  818. package/src/features/browser.ts +11 -3
  819. package/src/features/carConnection.ts +1 -1
  820. package/src/features/equalizer.ts +1 -1
  821. package/src/features/errors.ts +3 -3
  822. package/src/features/favorites.ts +14 -11
  823. package/src/features/gate.ts +16 -1
  824. package/src/features/hook-wiring.test.ts +302 -0
  825. package/src/features/metadata.ts +20 -3
  826. package/src/features/network.ts +1 -1
  827. package/src/features/nowPlaying.ts +1 -1
  828. package/src/features/output.ts +1 -1
  829. package/src/features/playback/controls.ts +4 -2
  830. package/src/features/playback/playWhenReady.ts +1 -1
  831. package/src/features/playback/playing.ts +1 -1
  832. package/src/features/playback/progress.test.ts +169 -0
  833. package/src/features/playback/progress.ts +3 -2
  834. package/src/features/playback/rate.ts +8 -1
  835. package/src/features/playback/state.ts +3 -4
  836. package/src/features/playback/trackPlaybackTime.test.ts +16 -6
  837. package/src/features/playback/trackPlaybackTime.ts +1 -6
  838. package/src/features/playback/volume.ts +1 -1
  839. package/src/features/player/options.ts +126 -76
  840. package/src/features/player/setup.test.ts +24 -7
  841. package/src/features/player/setup.ts +22 -10
  842. package/src/features/player/validateOptions.ts +21 -0
  843. package/src/features/queue/activeTrack.ts +15 -4
  844. package/src/features/queue/queue.ts +13 -6
  845. package/src/features/queue/repeatMode.ts +1 -1
  846. package/src/features/queue/shuffle.ts +1 -1
  847. package/src/features/remoteControls.ts +13 -92
  848. package/src/features/sleepTimer.test.ts +172 -0
  849. package/src/features/sleepTimer.ts +9 -1
  850. package/src/nitro-stub-drift.test.ts +277 -0
  851. package/src/public-surface.test.ts +147 -0
  852. package/src/specs/audio-browser.nitro.ts +5 -4
  853. package/src/style-resolution.test.ts +210 -0
  854. package/src/test-utils/reactNativeStub.ts +60 -0
  855. package/src/types/browser-native.ts +7 -3
  856. package/src/types/browser-nodes.ts +403 -134
  857. package/src/types/browser.ts +97 -63
  858. package/src/utils/LazyNativeEmitter.ts +28 -15
  859. package/src/utils/NativeUpdatedValue.ts +29 -13
  860. package/src/utils/getTrackIdentity.ts +20 -0
  861. package/src/utils/useDebug.test.ts +182 -0
  862. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  863. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  864. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
  865. package/src/web/NativeAudioBrowser.ts +240 -83
  866. package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
  867. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
  868. package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
  869. package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
  870. package/src/web/TrackPlayer/Player.load.test.ts +137 -1
  871. package/src/web/TrackPlayer/Player.ts +284 -37
  872. package/src/web/TrackPlayer/PlayingStateFactory.test.ts +1 -1
  873. package/src/web/TrackPlayer/PlayingStateFactory.ts +1 -1
  874. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  875. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  876. package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
  877. package/src/web/__tests__/lifecycle.test.ts +345 -0
  878. package/src/web/__tests__/lifecycleHarness.ts +351 -0
  879. package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
  880. package/src/web/__tests__/loadSupersession.test.ts +175 -0
  881. package/src/web/__tests__/retry.test.ts +169 -0
  882. package/src/web/browser/BrowserManager.ts +209 -111
  883. package/src/web/browser/FavoriteManager.test.ts +84 -0
  884. package/src/web/browser/FavoriteManager.ts +28 -23
  885. package/src/web/browser/NavigationErrorManager.ts +3 -1
  886. package/src/web/browser/SearchManager.test.ts +75 -0
  887. package/src/web/browser/SearchManager.ts +25 -28
  888. package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
  889. package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
  890. package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
  891. package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
  892. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  893. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  894. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  895. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  896. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  897. package/src/web/browser/sections.test.ts +91 -0
  898. package/src/web/browser/sections.ts +72 -0
  899. package/src/web/http/RequestConfigBuilder.test.ts +241 -1
  900. package/src/web/http/RequestConfigBuilder.ts +140 -33
  901. package/src/web/http/followMediaRedirect.test.ts +289 -0
  902. package/src/web/http/followMediaRedirect.ts +253 -0
  903. package/src/web/player/NowPlayingManager.ts +4 -3
  904. package/src/web/util/BrowserPathHelper.test.ts +68 -0
  905. package/src/web/util/BrowserPathHelper.ts +70 -13
  906. package/website/guide/android-auto.md +114 -0
  907. package/website/guide/android-certificates.md +409 -0
  908. package/website/guide/artwork.md +255 -0
  909. package/website/guide/audio-output.md +135 -0
  910. package/website/guide/automotive.md +86 -0
  911. package/website/guide/basic-usage.md +151 -0
  912. package/website/guide/battery.md +115 -0
  913. package/website/guide/browser.md +587 -0
  914. package/website/guide/carplay.md +650 -0
  915. package/website/guide/configuration.md +173 -0
  916. package/website/guide/equalizer.md +231 -0
  917. package/website/guide/errors.md +310 -0
  918. package/website/guide/favorites.md +154 -0
  919. package/website/guide/gate.md +143 -0
  920. package/website/guide/getting-started.md +46 -0
  921. package/website/guide/hooks.md +238 -0
  922. package/website/guide/live-streams.md +125 -0
  923. package/website/guide/metadata.md +239 -0
  924. package/website/guide/migrating-from-track-player.md +244 -0
  925. package/website/guide/native-callback-fetch.md +127 -0
  926. package/website/guide/network.md +86 -0
  927. package/website/guide/now-playing.md +175 -0
  928. package/website/guide/playback.md +328 -0
  929. package/website/guide/queue.md +300 -0
  930. package/website/guide/remote-controls.md +256 -0
  931. package/website/guide/search.md +255 -0
  932. package/website/guide/sleep-timer.md +268 -0
  933. package/website/guide/track.md +247 -0
  934. package/ios/Bridge.h +0 -8
  935. package/ios/Browser/MediaResolveComposer.swift +0 -114
  936. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  937. package/ios/Model/MediaURL.swift +0 -67
  938. package/ios/NowPlayingInfo/MediaItemProperty.swift +0 -78
  939. package/ios/NowPlayingInfo/NowPlayingInfoKeyValue.swift +0 -6
  940. package/ios/NowPlayingInfo/NowPlayingInfoProperty.swift +0 -220
  941. package/ios/Observer/PlayerItemNotificationObserver.swift +0 -119
  942. package/ios/Observer/PlayerItemPropertyObserver.swift +0 -133
  943. package/ios/Observer/PlayerStateObserver.swift +0 -50
  944. package/ios/Observer/PlayerTimeObserver.swift +0 -55
  945. package/ios/Option/PitchAlgorithms.swift +0 -17
  946. package/ios/Option/TimeEventFrequency.swift +0 -18
  947. package/ios/Player/LoadSeekCoordinator.swift +0 -99
  948. package/ios/Player/MediaLoaderDelegate.swift +0 -11
  949. package/ios/Player/PlaybackCoordinator.swift +0 -754
  950. package/ios/Player/PlaybackEffectHandler.swift +0 -54
  951. package/ios/Player/PlaybackErrorHandler.swift +0 -175
  952. package/ios/Player/PlaybackStateStore.swift +0 -27
  953. package/ios/Player/PlayingStateManager.swift +0 -33
  954. package/ios/Player/RetryManager.swift +0 -322
  955. package/ios/Player/SeekCompletionHandler.swift +0 -3
  956. package/ios/Player/SleepTimerHandling.swift +0 -15
  957. package/ios/Support/RNTrackPlayer-Bridging-Header.h +0 -7
  958. package/ios/Tests/LoadSeekCoordinatorTests.swift +0 -325
  959. package/ios/Tests/MediaResolveTests.swift +0 -126
  960. package/ios/Tests/OnceValueTests.swift +0 -79
  961. package/ios/Tests/PlaybackErrorHandlerTests.swift +0 -547
  962. package/ios/Tests/PlaybackStateStoreTests.swift +0 -48
  963. package/ios/Tests/PlayingStateManagerTests.swift +0 -57
  964. package/ios/Tests/RetryManagerTests.swift +0 -89
  965. package/ios/TrackPlayer.swift +0 -1014
  966. package/ios/TrackPlayerCallbacks.swift +0 -86
  967. package/ios/Util/MetadataAdapter.swift +0 -131
  968. package/ios/Util/OnceValue.swift +0 -104
  969. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  970. package/nitrogen/generated/android/c++/JFavoritesMatchMode.hpp +0 -58
  971. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  972. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  973. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  974. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  975. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  976. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  977. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteConfig.kt +0 -38
  978. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  979. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  980. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  981. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  982. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  983. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  984. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  985. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  986. package/nitrogen/generated/ios/swift/Variant_NullType_NotificationButtonLayout.swift +0 -18
  987. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  988. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
  989. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
@@ -0,0 +1,1078 @@
1
+ import Foundation
2
+ import os.log
3
+
4
+ #if canImport(NitroModules)
5
+ import NitroModules
6
+ #endif
7
+
8
+ /// The player: the playback state machine, the queue, retries, the sleep timer, persistence,
9
+ /// and every decision about loading, seeking and recovering. The media stack under it is a
10
+ /// `PlayerEngine` (AVPlayerEngine in production, a mock in tests), which carries the decisions out
11
+ /// and reports what it observes.
12
+ @MainActor
13
+ final class Player {
14
+ let logger = Logger(subsystem: "com.audiobrowser", category: "Player")
15
+
16
+ // MARK: - Dependencies
17
+
18
+ let engine: any PlayerEngine
19
+ weak var callbacks: PlayerCallbacks?
20
+
21
+ let queue = QueueManager()
22
+ let retry: RetryLoop
23
+ let sleepTimerManager: SleepTimerManager
24
+ var deferredSeek = DeferredSeek()
25
+
26
+ lazy var volumeFader = VolumeFader(
27
+ getVolume: { [weak self] in self?.engine.volume ?? 1 },
28
+ setVolume: { [weak self] level in self?.engine.volume = level },
29
+ )
30
+
31
+ private lazy var progressTimer = PlaybackTimer(
32
+ isActive: { $0 == .loading || $0 == .buffering || $0 == .playing },
33
+ ) { [weak self] in
34
+ guard let self, currentIndex >= 0 else { return }
35
+ let progressEvent = PlaybackProgressUpdatedEvent(
36
+ track: Double(currentIndex),
37
+ position: engine.currentTime,
38
+ duration: engine.duration,
39
+ buffered: engine.bufferedPosition,
40
+ )
41
+ callbacks?.playerDidUpdateProgress(progressEvent)
42
+ }
43
+
44
+ private lazy var intervalTimer = PlaybackTimer(
45
+ isActive: { $0 == .playing },
46
+ ) { [weak self] in
47
+ self?.callbacks?.playerDidFirePlaybackInterval()
48
+ }
49
+
50
+ // MARK: - State
51
+
52
+ private(set) var state: PlaybackState = .none
53
+ private(set) var errorStatus: ErrorStatus = .none
54
+
55
+ /// One track's playback session, which automatic retry reloads keep (glossary: Load).
56
+ struct Load: Equatable {
57
+ let id: UInt
58
+ /// Audio has rendered: retries get the recovery budget, not the first-connect one (ADR 0004).
59
+ var hasPlayed = false
60
+ /// The silent primer has run for this load's first failure (ADR 0005).
61
+ var primedNowPlaying = false
62
+ /// One of this load's items is in the engine; until then it reports the previous one's position.
63
+ var itemPrepared = false
64
+ }
65
+
66
+ private(set) var load: Load?
67
+ private var loadCount: UInt = 0
68
+
69
+ /// Starts a Load: a track becoming current, or a restart of this one from a terminal error.
70
+ /// Fresh retry budgets, no pending seek, unproven playback.
71
+ func beginLoad() {
72
+ loadCount &+= 1
73
+ load = Load(id: loadCount)
74
+ retry.reset()
75
+ deferredSeek.reset()
76
+ }
77
+
78
+ /// True the first time this load asks, so the silent primer runs once per load.
79
+ func claimNowPlayingPrime() -> Bool {
80
+ guard load?.primedNowPlaying == false else { return false }
81
+ load?.primedNowPlaying = true
82
+ return true
83
+ }
84
+
85
+ /// Whether playback should resume when the current audio-session interruption
86
+ /// ends — set to the play intent captured at interruption start, so we only
87
+ /// auto-resume if we were actually playing when interrupted.
88
+ private var shouldResumeAfterInterruption = false
89
+
90
+ /// Seconds of continuous audible playback after which the retry budget is
91
+ /// refilled — proof the stream actually delivered audio, not a brief blip
92
+ /// (mirrors Android's HEALTHY_PLAYBACK_MS). Without the refill a long-lived
93
+ /// stream permanently loses retry after its first recovered error, because
94
+ /// the window/attempt counters otherwise reset only on track change.
95
+ /// Overridable in tests.
96
+ var healthyPlaybackDuration: TimeInterval = 20
97
+
98
+ /// The pending refill. Readable so tests can await the window instead of
99
+ /// sleeping past it; only this type arms and cancels it.
100
+ private(set) var healthyPlaybackTask: Task<Void, Never>?
101
+
102
+ /// Playback rate (1.0 = normal speed). A non-positive rate would leave the player unable to
103
+ /// play, so it is refused here, where every source (JS, remote command, CarPlay, a restored
104
+ /// snapshot) funnels through. Speed and play intent are independent, as on Android and web.
105
+ var rate: Float = 1.0 {
106
+ didSet {
107
+ guard rate > 0 else {
108
+ rate = oldValue
109
+ return
110
+ }
111
+ engine.setRate(rate, playing: playWhenReady)
112
+ persistence.save()
113
+ }
114
+ }
115
+
116
+ private let persistenceDefaults: UserDefaults
117
+ private let persistenceSaveInterval: Duration
118
+ private lazy var persistence = PlaybackPersistence(
119
+ defaults: persistenceDefaults, saveInterval: persistenceSaveInterval,
120
+ ) { [unowned self] in
121
+ currentTrack.map {
122
+ PlaybackPersistence.snapshot(
123
+ of: $0, state: state, position: enginePositionIsCurrent ? engine.currentTime : deferredSeek.target ?? 0,
124
+ repeatMode: repeatMode,
125
+ shuffleEnabled: shuffleEnabled, rate: rate,
126
+ )
127
+ }
128
+ }
129
+
130
+ /// Whether the engine reports this Load's position: not before one of its items is prepared,
131
+ /// nor while one reloads.
132
+ private var enginePositionIsCurrent: Bool {
133
+ state != .loading && load?.itemPrepared == true
134
+ }
135
+
136
+ /// Where a reload resumes this Load.
137
+ private var resumePosition: TimeInterval? {
138
+ enginePositionIsCurrent ? engine.resumePosition : deferredSeek.target
139
+ }
140
+
141
+ // MARK: - Track State (for active track changed events)
142
+
143
+ var lastIndex: Int = -1
144
+ var lastTrack: Track?
145
+
146
+ // MARK: - Queue Forwarding Properties
147
+
148
+ var tracks: [Track] { queue.tracks }
149
+ var currentIndex: Int { queue.currentIndex }
150
+ var currentTrack: Track? { queue.currentTrack }
151
+ var queueSourcePath: String? { queue.queueSourcePath }
152
+ var nextTracks: [Track] { queue.nextTracks }
153
+ var previousTracks: [Track] { queue.previousTracks }
154
+ var isLastInPlaybackOrder: Bool { queue.isLastInPlaybackOrder }
155
+ var canNext: Bool { queue.canNext }
156
+ var currentTime: Double { engine.currentTime }
157
+ var nitroPlaybackError: PlaybackError? { errorStatus.nitroError }
158
+ var sleepTimer: SleepTimerState? { sleepTimerManager.get() }
159
+ var canPrevious: Bool { queue.canPrevious }
160
+
161
+ /// The repeat mode for the queue player.
162
+ var repeatMode: RepeatMode {
163
+ get { queue.repeatMode }
164
+ set {
165
+ guard queue.repeatMode != newValue else { return }
166
+ queue.repeatMode = newValue
167
+ engine.updateRemoteRepeatMode(newValue)
168
+ // Persisted on each change, or a mode toggled while paused would miss the resume snapshot.
169
+ persistence.save()
170
+ pushSkipAvailability() // repeat-all wrap changes next/previous availability
171
+ callbacks?.playerDidChangeRepeatMode(
172
+ RepeatModeChangedEvent(repeatMode: newValue),
173
+ )
174
+ }
175
+ }
176
+
177
+ /// Whether shuffle mode is enabled.
178
+ var shuffleEnabled: Bool {
179
+ get { queue.shuffleEnabled }
180
+ set {
181
+ guard queue.shuffleEnabled != newValue else { return }
182
+ queue.shuffleEnabled = newValue
183
+ engine.updateRemoteShuffleMode(newValue)
184
+ persistence.save()
185
+ pushSkipAvailability() // shuffle reorders playback → boundary changes
186
+ callbacks?.playerDidChangeShuffleEnabled(newValue)
187
+ }
188
+ }
189
+
190
+ /// Pushes current next/previous availability to the remote command center so
191
+ /// CarPlay / lock-screen grey out the buttons when there's nowhere to skip.
192
+ private func pushSkipAvailability() {
193
+ engine.updateSkipAvailability(canNext: queue.canNext, canPrevious: queue.canPrevious)
194
+ }
195
+
196
+ // MARK: - playWhenReady
197
+
198
+ /// The intent to play, applied through `setPlayWhenReady`.
199
+ private(set) var playWhenReady = false
200
+
201
+ func setPlayWhenReady(_ newValue: Bool) {
202
+ let changed = newValue != playWhenReady
203
+ playWhenReady = newValue
204
+ // A play during an interruption takes ownership of the intent: the user's later pause must
205
+ // not be overridden by auto-resume. (handleInterruptionEnded consumes its flag first.)
206
+ if newValue { shouldResumeAfterInterruption = false }
207
+
208
+ // Terminal states need a reload, not a bare play(): the item is gone (.stopped/.error) or
209
+ // parked at its end (.ended), where startPlayback() is a silent no-op. Only .error resumes
210
+ // from the last position.
211
+ if newValue, state == .error || state == .stopped || state == .ended {
212
+ // Re-resolve rather than replay the cached URL: play-from-.error typically comes long
213
+ // after the failure, when a signed URL has expired.
214
+ reloadResolving(startFromCurrentTime: state == .error)
215
+ }
216
+ if state != .loading {
217
+ if newValue { engine.play() } else { engine.pause() }
218
+ }
219
+
220
+ guard changed else { return }
221
+ // An explicit pause during the sleep fade is the timer's goal arriving early: clear the
222
+ // timer (restores the pre-fade volume via onFadeCancel).
223
+ if !newValue, volumeFader.isActive { sleepTimerManager.clear() }
224
+ publish()
225
+ evaluateSessionRelease()
226
+ }
227
+
228
+ /// Playing with intent, in a state that can play; buffering while loading or rebuffering.
229
+ static func playingState(playWhenReady: Bool, state: PlaybackState) -> PlayingState {
230
+ PlayingState(
231
+ playing: playWhenReady && ![.error, .ended, .none, .stopped].contains(state),
232
+ buffering: playWhenReady && (state == .loading || state == .buffering),
233
+ )
234
+ }
235
+
236
+ var playingState: PlayingState {
237
+ Self.playingState(playWhenReady: playWhenReady, state: state)
238
+ }
239
+
240
+ var playbackActive: Bool {
241
+ switch state {
242
+ case .none, .stopped, .ended, .error:
243
+ false
244
+ default: true
245
+ }
246
+ }
247
+
248
+ // MARK: - Init
249
+
250
+ /// - Parameters:
251
+ /// - persistenceDefaults: Where the resume snapshot is kept.
252
+ /// - persistenceSaveInterval: How often the snapshot is saved while the play intent is active.
253
+ init(
254
+ engine: any PlayerEngine,
255
+ retry: RetryLoop = RetryLoop(),
256
+ sleepTimerManager: SleepTimerManager = SleepTimerManager(),
257
+ persistenceDefaults: UserDefaults = PlaybackPersistence.standardDefaults,
258
+ persistenceSaveInterval: Duration = .seconds(5),
259
+ ) {
260
+ self.engine = engine
261
+ self.retry = retry
262
+ self.sleepTimerManager = sleepTimerManager
263
+ self.persistenceDefaults = persistenceDefaults
264
+ self.persistenceSaveInterval = persistenceSaveInterval
265
+ engine.onEvent = { [weak self] in self?.engineDidEmit($0) }
266
+
267
+ // Configure sleep timer
268
+ sleepTimerManager.onComplete = { [weak self] in
269
+ guard let self else { return }
270
+ self.volumeFader.resolve { self.pause() }
271
+ }
272
+ sleepTimerManager.onFadeStart = { [weak self] duration in
273
+ self?.volumeFader.start(duration: duration)
274
+ }
275
+ sleepTimerManager.onFadeCancel = { [weak self] in
276
+ self?.volumeFader.cancel(restoringVolume: true)
277
+ }
278
+ sleepTimerManager.onChanged = { [weak self] in self?.callbacks?.playerDidChangeSleepTimer($0) }
279
+
280
+ retry.onError = { [weak self] error in
281
+ self?.transition(.errorOccurred(error))
282
+ }
283
+ retry.onRetryingError = { [weak self] error in
284
+ self?.reportRetryingError(error)
285
+ }
286
+ retry.onRetry = { [weak self] startFromCurrentTime in
287
+ // Re-resolve: a retry may be recovering from an expired short-lived URL or token.
288
+ self?.reloadResolving(startFromCurrentTime: startFromCurrentTime)
289
+ }
290
+ retry.shouldRetry = { [weak self] in self?.playWhenReady ?? false }
291
+ retry.hasPlayed = { [weak self] in self?.load?.hasPlayed ?? false }
292
+ }
293
+
294
+ /// Surfaces a failure the retry loop is still working on: attached to the
295
+ /// current (non-terminal) state so UIs can show the cause over the spinner.
296
+ /// Not a state transition — the state machine never sees it — and not a
297
+ /// `playerDidError`, which stays reserved for terminal errors.
298
+ private func reportRetryingError(_ error: TrackPlayerError.PlaybackError) {
299
+ // Only attach while the failure is what the listener is looking at (matches
300
+ // Android). During .playing buffered audio is still audibly fine — the
301
+ // advisory would replace the song line over working sound — and a
302
+ // terminal/stopped state must not regain an advisory error.
303
+ guard state == .loading || state == .buffering || state == .paused else { return }
304
+ // Each failed attempt re-reports; identical repeats add nothing.
305
+ errorStatus = .retrying(error)
306
+ publish()
307
+ }
308
+
309
+ // MARK: - Playback State Machine
310
+
311
+ func transition(_ event: PlaybackEvent) {
312
+ guard let newState = nextPlaybackState(from: state, on: event) else { return }
313
+
314
+ // Allow error-to-error transitions to update the error and emit callbacks,
315
+ // even though the state enum value doesn't change.
316
+ if newState == state, case let .errorOccurred(error) = event {
317
+ errorStatus = .terminal(error)
318
+ published.errorStatus = .none // each terminal report reaches JS, a repeat included
319
+ publish()
320
+ return
321
+ }
322
+
323
+ guard newState != state else { return }
324
+ let oldState = state
325
+ state = newState
326
+ applySideEffects(old: oldState, new: newState, event: event)
327
+ publish()
328
+ // While nothing plays, no periodic save runs: every transition rewrites the snapshot, so it
329
+ // never keeps a position from before a track change or a seek.
330
+ if !(playbackActive && playWhenReady) { persistence.save() }
331
+ // Repeat modes never end the queue (matches web's endsQueue()): a repeat-track pause at the
332
+ // track boundary (e.g. the end-of-track sleep timer) must not read as "playlist over".
333
+ if newState == .ended, isLastInPlaybackOrder, repeatMode == .off {
334
+ callbacks?.playerDidEndQueue(
335
+ PlaybackQueueEndedEvent(track: Double(currentIndex), position: engine.currentTime),
336
+ )
337
+ }
338
+ }
339
+
340
+ private struct Published: Equatable {
341
+ var state = PlaybackState.none
342
+ var errorStatus = ErrorStatus.none
343
+ var playWhenReady = false
344
+ var playing = false
345
+ var buffering = false
346
+ var playIntentActive = false
347
+ }
348
+
349
+ private var published = Published()
350
+
351
+ /// Emits what changed since the last publish, in order: the play intent, the playing state,
352
+ /// the playback (state and error), the terminal error, the active play intent. The one place
353
+ /// these leave the Player, so each goes out once per change.
354
+ private func publish() {
355
+ let playing = playingState
356
+ let now = Published(
357
+ state: state, errorStatus: errorStatus, playWhenReady: playWhenReady,
358
+ playing: playing.playing, buffering: playing.buffering,
359
+ playIntentActive: playbackActive && playWhenReady,
360
+ )
361
+ let old = published
362
+ guard now != old else { return }
363
+ published = now
364
+ if now.playWhenReady != old.playWhenReady {
365
+ callbacks?.playerDidChangePlayWhenReady(now.playWhenReady)
366
+ }
367
+ if now.playing != old.playing || now.buffering != old.buffering {
368
+ callbacks?.playerDidChangePlayingState(playing)
369
+ }
370
+ if now.state != old.state || now.errorStatus != old.errorStatus {
371
+ callbacks?.playerDidChangePlayback(Playback(state: now.state, error: now.errorStatus.nitroError))
372
+ }
373
+ let erred = now.state == .error, wasErred = old.state == .error
374
+ if erred != wasErred || (erred && now.errorStatus != old.errorStatus) {
375
+ callbacks?.playerDidError(PlaybackErrorEvent(error: now.errorStatus.nitroError))
376
+ }
377
+ if now.playIntentActive != old.playIntentActive {
378
+ persistence.playIntentDidChange(active: now.playIntentActive)
379
+ }
380
+ }
381
+
382
+ private func applySideEffects(old: PlaybackState, new: PlaybackState, event: PlaybackEvent) {
383
+ // Error lifecycle. A retrying error rides along through loading/buffering, but any state
384
+ // that proves recovery (.ready/.playing: data flowed) or abandons the attempt
385
+ // (.stopped/.none/.ended) clears it. Runs before publish, so the emitted Playback
386
+ // is already clean.
387
+ if case let .errorOccurred(error) = event {
388
+ errorStatus = .terminal(error)
389
+ } else if old == .error || (errorStatus.isRetrying && new != .loading && new != .buffering && new != .paused) {
390
+ errorStatus = .none
391
+ }
392
+
393
+ // Retry-budget refill: sustained audible playback proves the stream
394
+ // recovered, so the next error starts a fresh retry window. Leaving
395
+ // .playing before the threshold cancels the pending refill.
396
+ if new == .playing {
397
+ healthyPlaybackTask?.cancel()
398
+ healthyPlaybackTask = Task { [weak self] in
399
+ guard let self else { return }
400
+ try? await Task.sleep(nanoseconds: UInt64(healthyPlaybackDuration * 1_000_000_000))
401
+ guard !Task.isCancelled else { return }
402
+ // Refill only between episodes: reset() cancels a pending retry,
403
+ // which would silently kill an in-flight reconnect.
404
+ guard retry.pendingTask == nil else { return }
405
+ retry.reset()
406
+ }
407
+ } else if old == .playing {
408
+ healthyPlaybackTask?.cancel()
409
+ healthyPlaybackTask = nil
410
+ }
411
+
412
+ // State-specific effects
413
+ switch new {
414
+ case .ready:
415
+ engine.applyTimePitchAlgorithm()
416
+ if playWhenReady { engine.play() }
417
+ case .loading:
418
+ engine.applyTimePitchAlgorithm()
419
+ case .ended:
420
+ // A natural end exhausts the play intent — nothing is left to play.
421
+ // Keeping playWhenReady true would invert the play/pause toggle, hold
422
+ // the audio session forever (release is intent-gated), and arm
423
+ // interruption auto-resume into silence. The setter also requests the
424
+ // session release.
425
+ setPlayWhenReady(false)
426
+ default: break
427
+ }
428
+
429
+ progressTimer.onPlaybackStateChanged(new)
430
+ intervalTimer.onPlaybackStateChanged(new)
431
+ }
432
+
433
+ // MARK: - Observer Context Guards
434
+
435
+ func avPlayerDidChangeTimeControlStatus(_ status: PlayerTimeControlStatus) {
436
+ // During loading, ignore stale timeControlStatus changes from old items.
437
+ if state == .loading { return }
438
+
439
+ switch status {
440
+ case .paused:
441
+ let currentState = state
442
+ let currentTime = engine.currentTime
443
+ let duration = engine.duration
444
+ // Ignore pauses when near track end
445
+ let nearTrackEnd = currentTime >= duration - 0.5 && duration > 0
446
+
447
+ if nearTrackEnd {
448
+ // Ignore - track completion will be handled by handleTrackDidPlayToEndTime
449
+ } else if !engine.hasLoadedAsset, currentState != .stopped {
450
+ transition(.avPlayerPaused(hasAsset: false))
451
+ } else if currentState != .error, currentState != .stopped {
452
+ if !playWhenReady {
453
+ transition(.avPlayerPaused(hasAsset: true))
454
+ }
455
+ }
456
+ case .waitingToPlayAtSpecifiedRate:
457
+ if engine.hasLoadedAsset {
458
+ transition(.avPlayerWaiting)
459
+ }
460
+ case .playing:
461
+ transition(.avPlayerPlaying)
462
+ }
463
+ }
464
+
465
+ func audioDidStart() {
466
+ load?.hasPlayed = true
467
+ // Don't override loading state
468
+ if state == .loading { return }
469
+ transition(.audioFrameDecoded)
470
+ }
471
+
472
+ func avItemDidUpdatePlaybackLikelyToKeepUp(_ playbackLikelyToKeepUp: Bool) {
473
+ guard playbackLikelyToKeepUp else { return }
474
+ guard engine.hasLoadedAsset else { return }
475
+
476
+ if !deferredSeek.defersReady, state != .playing {
477
+ logger.debug("avItemDidUpdatePlaybackLikelyToKeepUp → .ready")
478
+ transition(.bufferingSufficient)
479
+ }
480
+ }
481
+
482
+ // MARK: - Player Actions
483
+
484
+ func play() {
485
+ setPlayWhenReady(true)
486
+ }
487
+
488
+ func pause() {
489
+ setPlayWhenReady(false)
490
+ }
491
+
492
+ func togglePlayback() {
493
+ setPlayWhenReady(!playWhenReady)
494
+ }
495
+
496
+ func stop() {
497
+ // Cancel before the intent drop: a surviving retry would surface .error
498
+ // over the stop, and a pending task blocks the session release below.
499
+ retry.cancel()
500
+ // A stop is terminal — interruption-end must not resume over it.
501
+ shouldResumeAfterInterruption = false
502
+ transition(.stopped)
503
+ setPlayWhenReady(false)
504
+ if currentTrack?.live != true { seek(to: 0) }
505
+ }
506
+
507
+ // MARK: - Audio Session Interruptions
508
+
509
+ /// The active output route disappeared (headphones unplugged, Bluetooth
510
+ /// speaker off). A deliberate output loss: drop the intent and any pending
511
+ /// interruption auto-resume, so a call ending can't blast the built-in
512
+ /// speaker.
513
+ func handleRouteDisconnected() {
514
+ shouldResumeAfterInterruption = false
515
+ pause()
516
+ }
517
+
518
+ /// An audio-session interruption began (a phone call, or another app such as
519
+ /// Music/Spotify taking over playback). iOS has already paused our AVPlayer,
520
+ /// but that pause arrives as a `timeControlStatus` change that's swallowed
521
+ /// while `playWhenReady` is still true — so the state would otherwise stay
522
+ /// `.playing` and any UI driven by it stays stuck. Drop the play intent and
523
+ /// force the paused state, independent of notification delivery order.
524
+ func handleInterruptionBegan() {
525
+ // OR: a nested interruption's second .began sees the already-paused
526
+ // intent and must not wipe the flag the first one captured.
527
+ shouldResumeAfterInterruption = shouldResumeAfterInterruption || playWhenReady
528
+ pause()
529
+ if playbackActive, state != .loading {
530
+ transition(.avPlayerPaused(hasAsset: engine.hasLoadedAsset))
531
+ }
532
+ }
533
+
534
+ /// Whether an interruption-end will resume playback — the intent captured at
535
+ /// interruption start. Lets the host gate side effects that must precede the
536
+ /// resume (audio-session reactivation) on whether one will actually happen.
537
+ var willResumeAfterInterruption: Bool { shouldResumeAfterInterruption }
538
+
539
+ /// An audio-session interruption ended. Resume only if we were playing when it
540
+ /// began and the system indicates resumption is appropriate (`shouldResume`).
541
+ func handleInterruptionEnded(shouldResume: Bool) {
542
+ guard shouldResumeAfterInterruption else { return }
543
+ shouldResumeAfterInterruption = false
544
+ if shouldResume {
545
+ play()
546
+ } else {
547
+ // Interruption ended and we're staying paused: the session can be released now.
548
+ evaluateSessionRelease()
549
+ }
550
+ }
551
+
552
+ /// Requests an audio-session release when playback is deliberately stopped: play intent is off,
553
+ /// it isn't an interruption-driven pause (those resume — `shouldResumeAfterInterruption` is set
554
+ /// before the interruption's `pause()`, so this skips it), and no retry is pending (a live-stream
555
+ /// reconnect keeps `playWhenReady` true and must keep the session). The host debounces and cancels
556
+ /// on the next activation, so a quick pause→resume never actually releases.
557
+ private func evaluateSessionRelease() {
558
+ guard !playWhenReady,
559
+ !shouldResumeAfterInterruption,
560
+ retry.pendingTask == nil
561
+ else { return }
562
+ callbacks?.playerShouldReleaseSession()
563
+ }
564
+
565
+ func getPlayback() -> Playback {
566
+ Playback(state: state, error: errorStatus.nitroError)
567
+ }
568
+
569
+ /// Handles the `playWhenReady` setting while executing a given action.
570
+ func handlePlayWhenReady(_ playWhenReady: Bool?, action: () throws -> Void) rethrows {
571
+ if playWhenReady == false { setPlayWhenReady(false) }
572
+ try action()
573
+ if playWhenReady == true { setPlayWhenReady(true) }
574
+ }
575
+
576
+ // MARK: - Queue Methods
577
+
578
+ func load(_ track: Track, playWhenReady: Bool? = nil) {
579
+ handlePlayWhenReady(playWhenReady) {
580
+ if queue.currentIndex == -1 {
581
+ afterQueueMutation(queue.add([track], initialIndex: 0))
582
+ } else {
583
+ // `currentIndex` is invariantly -1 or in bounds, and -1 took the branch
584
+ // above, so this cannot fail — but surface it rather than reloading the
585
+ // old track if that invariant is ever broken.
586
+ do {
587
+ try queue.replace(queue.currentIndex, track)
588
+ afterQueueMutation(.tracksAndCurrent)
589
+ } catch {
590
+ logger.error("load: replace at \(self.queue.currentIndex) failed: \(error)")
591
+ }
592
+ }
593
+ }
594
+ }
595
+
596
+ func replace(_ index: Int, _ track: Track) throws {
597
+ try afterQueueMutation(queue.replace(index, track))
598
+ }
599
+
600
+ func setQueue(
601
+ _ newTracks: [Track],
602
+ initialIndex: Int = 0,
603
+ startPositionMs: Double? = nil,
604
+ playWhenReady: Bool? = nil,
605
+ sourcePath: String? = nil,
606
+ ) {
607
+ guard !newTracks.isEmpty else {
608
+ clear()
609
+ return
610
+ }
611
+ handlePlayWhenReady(playWhenReady) {
612
+ afterQueueMutation(
613
+ queue.setQueue(newTracks, initialIndex: initialIndex, sourcePath: sourcePath),
614
+ startingAt: startPositionMs.flatMap { $0 > 0 ? $0 / 1000 : nil },
615
+ )
616
+ }
617
+ }
618
+
619
+ func add(_ tracks: [Track], initialIndex: Int? = nil, playWhenReady: Bool? = nil) {
620
+ handlePlayWhenReady(playWhenReady) {
621
+ afterQueueMutation(queue.add(tracks, initialIndex: initialIndex ?? 0))
622
+ }
623
+ }
624
+
625
+ func add(_ tracks: [Track], at index: Int) throws {
626
+ try afterQueueMutation(queue.addAt(tracks, at: index))
627
+ }
628
+
629
+ func next() {
630
+ let result = queue.next()
631
+ switch result {
632
+ case .trackChanged: handleCurrentTrackChanged()
633
+ case .sameTrackReplay: if playWhenReady { replay() }
634
+ case .noChange: break
635
+ }
636
+ }
637
+
638
+ func previous() {
639
+ let result = queue.previous()
640
+ switch result {
641
+ case .trackChanged: handleCurrentTrackChanged()
642
+ case .sameTrackReplay: if playWhenReady { replay() }
643
+ case .noChange: break
644
+ }
645
+ }
646
+
647
+ func remove(_ index: Int) throws {
648
+ try afterQueueMutation(queue.remove(index))
649
+ }
650
+
651
+ func skipTo(_ index: Int, playWhenReady: Bool? = nil) throws {
652
+ try handlePlayWhenReady(playWhenReady) {
653
+ if index == queue.currentIndex {
654
+ seek(to: 0)
655
+ } else {
656
+ try queue.skipTo(index)
657
+ handleCurrentTrackChanged()
658
+ }
659
+ }
660
+ }
661
+
662
+ func move(fromIndex: Int, toIndex: Int) throws {
663
+ try afterQueueMutation(queue.move(fromIndex: fromIndex, toIndex: toIndex))
664
+ }
665
+
666
+ func removeUpcomingTracks() {
667
+ afterQueueMutation(queue.removeUpcomingTracks())
668
+ }
669
+
670
+ func replay() {
671
+ seek(to: 0) { [weak self] succeeded in
672
+ if succeeded { self?.play() }
673
+ }
674
+ }
675
+
676
+ /// The one place a settled queue mutation is published: the new track list, then the
677
+ /// current-track change, then next/previous availability.
678
+ private func afterQueueMutation(_ change: QueueChange, startingAt start: TimeInterval? = nil) {
679
+ if change.tracks { callbacks?.playerDidChangeQueue(queue.tracks) }
680
+ if change.currentTrack {
681
+ handleCurrentTrackChanged(startingAt: start)
682
+ } else if change.tracks {
683
+ pushSkipAvailability()
684
+ }
685
+ }
686
+
687
+ func clear() {
688
+ afterQueueMutation(queue.clear())
689
+ unload()
690
+ engine.clearNowPlaying()
691
+ }
692
+
693
+ // MARK: - Track Loading
694
+
695
+ func handleTrackDidPlayToEndTime() {
696
+ sleepTimerManager.onTrackPlayedToEnd()
697
+
698
+ if repeatMode == .track {
699
+ // The end-of-track sleep pause lands just above and clears the intent;
700
+ // without it, settle in .ended (where play() reloads) instead of
701
+ // replaying past the sleep timer.
702
+ if playWhenReady {
703
+ replay()
704
+ } else {
705
+ transition(.trackEndedNaturally)
706
+ }
707
+ } else if repeatMode == .queue || !isLastInPlaybackOrder {
708
+ next()
709
+ } else {
710
+ transition(.trackEndedNaturally)
711
+ }
712
+ }
713
+
714
+ /// Loads the new current track. `start` is held as the deferred seek, so the ready transition
715
+ /// waits for it (no flash at 0) and the snapshot saved on the way records it.
716
+ func handleCurrentTrackChanged(startingAt start: TimeInterval? = nil) {
717
+ defer { pushSkipAvailability() }
718
+ sleepTimerManager.onTrackChanged()
719
+
720
+ engine.cancelLoading()
721
+ beginLoad()
722
+ if let start { deferredSeek.capture(start) }
723
+
724
+ if errorStatus != .none {
725
+ errorStatus = .none
726
+ // Already .loading, the .trackLoading transition below won't fire: publish the clear here,
727
+ // or the old track's error lingers over the new one's spinner. In any other state the
728
+ // transition publishes it, with the state it belongs to.
729
+ if state == .loading { publish() }
730
+ }
731
+
732
+ let lastPosition = engine.currentTime
733
+ let shouldContinuePlayback = playWhenReady
734
+ if let currentTrack {
735
+ engine.stopObservingItem()
736
+ engine.pause()
737
+
738
+ // Set loading state before playWhenReady so the setter's guard
739
+ // prevents a no-op startPlayback() on the now-nil item.
740
+ transition(.trackLoading)
741
+
742
+ // Ensure playWhenReady is set before loading to preserve playback state
743
+ setPlayWhenReady(shouldContinuePlayback)
744
+
745
+ engine.loadNowPlayingMetadata(for: currentTrack)
746
+ persistence.save()
747
+
748
+ // Validate source URL before handing off to MediaLoader
749
+ guard let src = currentTrack.src else {
750
+ logger.error("Failed to load track - no src")
751
+ logger.error(" track.title: \(currentTrack.title)")
752
+ logger.error(" track.path: \(currentTrack.path ?? "nil")")
753
+ clearItem()
754
+ transition(.errorOccurred(.invalidSourceUrl("nil")))
755
+ return
756
+ }
757
+
758
+ logger.debug("Loading track: \(currentTrack.title)")
759
+ logger.debug(" track.path: \(currentTrack.path ?? "nil")")
760
+ logger.debug(" track.src: \(src)")
761
+
762
+ engine.resolveAndLoad(src: src, track: currentTrack)
763
+ } else {
764
+ unload()
765
+ engine.clearNowPlaying()
766
+ }
767
+
768
+ let eventData = PlaybackActiveTrackChangedEvent(
769
+ lastIndex: lastIndex == -1 ? nil : Double(lastIndex),
770
+ lastTrack: lastTrack,
771
+ lastPosition: lastPosition,
772
+ index: currentIndex == -1 ? nil : Double(currentIndex),
773
+ track: currentTrack,
774
+ )
775
+ callbacks?.playerDidChangeActiveTrack(eventData)
776
+ lastTrack = currentTrack
777
+ lastIndex = currentIndex
778
+ }
779
+
780
+ // MARK: - Seeking
781
+
782
+ /// Seeks the current item. While loading, the target is held until the item is ready and the
783
+ /// ready transition waits for it (`DeferredSeek`); `completion` then reports false at once.
784
+ func seek(to seconds: TimeInterval, completion: @escaping @MainActor (Bool) -> Void = { _ in }) {
785
+ if state == .loading {
786
+ deferredSeek.capture(seconds)
787
+ completion(false)
788
+ } else if engine.hasItem {
789
+ seekItem(to: seconds, completion: completion)
790
+ } else {
791
+ completion(false)
792
+ }
793
+ }
794
+
795
+ func seek(by offset: TimeInterval) {
796
+ if state == .loading {
797
+ seek(to: (deferredSeek.target ?? 0) + offset)
798
+ } else if engine.hasItem {
799
+ seek(to: engine.currentTime + offset)
800
+ }
801
+ }
802
+
803
+ /// Jumps to the live edge: the end of the seekable window when there is one; without one
804
+ /// (ICY) there is nothing to seek within, so it reconnects. No-op for a track that isn't live.
805
+ func seekToLiveEdge() {
806
+ guard currentTrack?.live == true, engine.hasItem else { return }
807
+ if let edge = engine.liveEdge {
808
+ engine.seek(to: edge) { _ in }
809
+ } else {
810
+ reloadResolving(startFromCurrentTime: false)
811
+ }
812
+ }
813
+
814
+ /// A completion arriving after a track change belongs to the old item, and reports false
815
+ /// without touching the new one's deferred seek.
816
+ private func seekItem(to seconds: TimeInterval, completion: @escaping @MainActor (Bool) -> Void) {
817
+ let generation = deferredSeek.generation
818
+ engine.seek(to: seconds) { [weak self] finished in
819
+ guard let self, deferredSeek.generation == generation else {
820
+ completion(false)
821
+ return
822
+ }
823
+ seekDidLand(at: seconds, finished: finished)
824
+ completion(finished)
825
+ }
826
+ }
827
+
828
+ /// Issues the deferred seek if one is pending; true when it did, so ready waits for it.
829
+ private func beginDeferredSeek() -> Bool {
830
+ guard let time = deferredSeek.begin() else { return false }
831
+ seekItem(to: time) { _ in }
832
+ return true
833
+ }
834
+
835
+ private func seekDidLand(at seconds: TimeInterval, finished: Bool) {
836
+ switch deferredSeek.land() {
837
+ case .done:
838
+ if state == .loading {
839
+ logger.debug("[loadSeek] seek landed at \(seconds)s (finished=\(finished)) → .ready")
840
+ transition(.loadSeekCompleted)
841
+ }
842
+ case let .reseek(time):
843
+ seekItem(to: time) { _ in }
844
+ case .none:
845
+ break
846
+ }
847
+ }
848
+
849
+ // MARK: - Reloading
850
+
851
+ /// Loads the current track again, re-running the media-URL resolver, so a consumer whose
852
+ /// resolver mints short-lived URLs or tokens recovers once they expire. A track without a `src`
853
+ /// fails as an invalid source; with no current track, nothing loads.
854
+ func reloadResolving(startFromCurrentTime: Bool) {
855
+ guard let track = currentTrack else { return }
856
+ guard let src = track.src else {
857
+ transition(.errorOccurred(.invalidSourceUrl("nil")))
858
+ return
859
+ }
860
+ prepareForReload(resumingAt: startFromCurrentTime ? resumePosition : nil)
861
+ engine.resolveAndLoad(src: src, track: track)
862
+ }
863
+
864
+ /// A restart from a terminal error begins a new Load on a new AVPlayer; automatic retries
865
+ /// never get here in .error, since the retry loop keeps the state non-terminal. `time` is held
866
+ /// as the deferred seek before the loading transition saves the snapshot.
867
+ private func prepareForReload(resumingAt time: TimeInterval?) {
868
+ if state == .error {
869
+ beginLoad()
870
+ clearItem()
871
+ engine.recreate()
872
+ } else {
873
+ engine.cancelLoading()
874
+ engine.dropAsset()
875
+ engine.pause()
876
+ }
877
+ if let time { deferredSeek.capture(time) }
878
+ transition(.trackLoading)
879
+ }
880
+
881
+ private func clearItem() {
882
+ engine.clearItem()
883
+ deferredSeek.reset()
884
+ }
885
+
886
+ private func unload() {
887
+ clearItem()
888
+ transition(.trackUnloaded)
889
+ }
890
+
891
+ /// Recovers from a media-services reset: every AVPlayer and session handle is invalid, so a
892
+ /// new player reloads the current track, keeping the play intent, so a live stream reconnects
893
+ /// instead of going silent. No-op when nothing is loaded.
894
+ func mediaServicesWereReset() {
895
+ guard currentTrack != nil else { return }
896
+ logger.info("Media services were reset — recreating player and reloading current track")
897
+ clearItem()
898
+ engine.recreate()
899
+ reloadResolving(startFromCurrentTime: false)
900
+ }
901
+
902
+ // MARK: - Engine Events
903
+
904
+ private func engineDidEmit(_ event: EngineEvent) {
905
+ switch event {
906
+ case let .itemPrepared(likelyToKeepUp):
907
+ load?.itemPrepared = true
908
+ if playWhenReady { engine.play() }
909
+ if !beginDeferredSeek(), likelyToKeepUp { avItemDidUpdatePlaybackLikelyToKeepUp(true) }
910
+ case let .loadFailed(error):
911
+ primeNowPlayingIfNeeded()
912
+ retry.handleError(error, context: .mediaLoad)
913
+ case .unplayable:
914
+ primeNowPlayingIfNeeded()
915
+ transition(.errorOccurred(.trackWasUnplayable))
916
+ case let .invalidSource(url):
917
+ primeNowPlayingIfNeeded()
918
+ transition(.errorOccurred(.invalidSourceUrl(url)))
919
+ case let .failed(error, httpStatusCode):
920
+ retry.handleError(error, context: .playback, httpStatusCode: httpStatusCode)
921
+ case let .likelyToKeepUp(isLikely):
922
+ guard isLikely, engine.hasItem else { return }
923
+ if beginDeferredSeek() { return }
924
+ avItemDidUpdatePlaybackLikelyToKeepUp(isLikely)
925
+ case let .timeControlStatus(status, waitingForNoItem):
926
+ recoverIfAirPlayStalled(transitioningTo: status, waitingForNoItem: waitingForNoItem)
927
+ avPlayerDidChangeTimeControlStatus(status)
928
+ case .audioDidStart:
929
+ audioDidStart()
930
+ case .playedToEnd:
931
+ itemDidPlayToEnd()
932
+ case .stalled:
933
+ // Nudge, don't reconnect: a genuine drop is recovered by the retry and network paths.
934
+ recoverFromStall(reconnectIfLive: false)
935
+ }
936
+ }
937
+
938
+ /// A failed load renders no audio, and without it iOS never elects the app onto the
939
+ /// now-playing surfaces (ADR 0005). Once per load: one burst either elects it or never will.
940
+ private func primeNowPlayingIfNeeded() {
941
+ guard playWhenReady, claimNowPlayingPrime() else { return }
942
+ engine.primeNowPlaying()
943
+ }
944
+
945
+ /// A genuine end advances the queue; a dropped live stream or an underrun reported as an end
946
+ /// is recovered instead (see `EndOfTrackJudgement`).
947
+ private func itemDidPlayToEnd() {
948
+ let judgement = EndOfTrackJudgement(
949
+ isLive: currentTrack?.live == true,
950
+ currentTime: engine.currentTime,
951
+ duration: engine.duration,
952
+ )
953
+ switch judgement.outcome {
954
+ case .ended: handleTrackDidPlayToEndTime()
955
+ case .stalled: recoverFromStall(reconnectIfLive: true)
956
+ }
957
+ }
958
+
959
+ /// Recovers from a stall while the play intent holds: a live stream that ran out of data
960
+ /// (`reconnectIfLive`) rejoins the edge; otherwise play() un-parks AVPlayer from waiting.
961
+ private func recoverFromStall(reconnectIfLive: Bool) {
962
+ guard playWhenReady else { return }
963
+ if reconnectIfLive, currentTrack?.live == true {
964
+ seekToLiveEdge()
965
+ } else {
966
+ engine.play()
967
+ }
968
+ }
969
+
970
+ private var previousTimeControlStatus: PlayerTimeControlStatus?
971
+ private var previousWaitingForNoItem = false
972
+
973
+ /// AVPlayer can strand itself in .paused after a `.noItemToPlay` wait over AirPlay while the
974
+ /// intent is to play: play() is re-issued on exactly that transition.
975
+ private func recoverIfAirPlayStalled(transitioningTo current: PlayerTimeControlStatus, waitingForNoItem: Bool) {
976
+ let stalled = AirPlayStallJudgement(
977
+ previous: previousTimeControlStatus,
978
+ current: current,
979
+ previousWaitingReasonWasNoItemToPlay: previousWaitingForNoItem,
980
+ isAirPlay: engine.isAirPlayRoute,
981
+ playWhenReady: playWhenReady,
982
+ ).shouldNudge
983
+ previousTimeControlStatus = current
984
+ previousWaitingForNoItem = current == .waitingToPlayAtSpecifiedRate && waitingForNoItem
985
+ if stalled {
986
+ logger.info("AirPlay waiting→paused stall detected — nudging play()")
987
+ engine.play()
988
+ }
989
+ }
990
+
991
+ // MARK: - Connectivity
992
+
993
+ /// Re-establishes a stalled stream when connectivity returns. AVPlayer doesn't reliably report
994
+ /// a live stream's mid-stream drop as a failure: it waits with no error, so the retry loop
995
+ /// never engages. Only while stalled (.buffering) with play intent, so a pause or a retry that
996
+ /// gave up isn't resumed; and not while the retry loop is itself waiting for the network,
997
+ /// which owns that reload.
998
+ func networkDidReturn() {
999
+ guard playWhenReady, state == .buffering, !retry.isWaitingForNetwork else { return }
1000
+ logger.info("Connectivity restored while stalled — re-resolving to reconnect")
1001
+ reloadResolving(startFromCurrentTime: true)
1002
+ }
1003
+
1004
+ /// Time playback must have progressed before a buffer dip counts as a stall, so the initial
1005
+ /// connect or a seek doesn't read as one.
1006
+ private static let stallGraceSeconds: TimeInterval = 0.5
1007
+
1008
+ /// Ongoing playback is waiting for data. Mirrors Android's `stalled`, which ExoPlayer
1009
+ /// distinguishes natively; iOS has no rebuffer-vs-initial signal.
1010
+ var isStalled: Bool {
1011
+ guard engine.hasItem else { return false }
1012
+ return engine.currentTime > Self.stallGraceSeconds && playWhenReady && !engine.isPlaybackLikelyToKeepUp
1013
+ }
1014
+
1015
+ // MARK: - Resume
1016
+
1017
+ /// The last persisted session; nil when nothing was persisted, or its track has since become
1018
+ /// `disabled`, which never plays.
1019
+ func persistedSession() -> PersistedPlaybackState? {
1020
+ guard let snapshot = persistence.load() else {
1021
+ logger.error("resume: nothing persisted")
1022
+ return nil
1023
+ }
1024
+ guard snapshot.track.disabled != true else {
1025
+ logger.info("resume: persisted track is disabled — refusing")
1026
+ return nil
1027
+ }
1028
+ return snapshot
1029
+ }
1030
+
1031
+ /// Plays a persisted session (a cold-start resume): its settings, then its queue — `expanded`,
1032
+ /// the expansion of the track's contextual path, or the track alone.
1033
+ func resume(_ snapshot: PersistedPlaybackState, queue expanded: (tracks: [Track], selectedIndex: Int)?) {
1034
+ let track = snapshot.track.toNitro()
1035
+ logger.info("resume: cold — restoring persisted track")
1036
+ // Settings first: `setQueue` builds the shuffle order, and the mode events then precede the
1037
+ // queue change, as on Android.
1038
+ repeatMode = RepeatMode(fromString: snapshot.repeatMode) ?? .off
1039
+ shuffleEnabled = snapshot.shuffleEnabled
1040
+ rate = snapshot.playbackSpeed
1041
+ let startMs = track.live == true ? nil : snapshot.positionMs
1042
+ if let expanded {
1043
+ setQueue(expanded.tracks, initialIndex: expanded.selectedIndex, startPositionMs: startMs, playWhenReady: true)
1044
+ } else {
1045
+ setQueue([track], initialIndex: 0, startPositionMs: startMs, playWhenReady: true)
1046
+ }
1047
+ }
1048
+
1049
+ /// Plays what `TrackSelector` resolved a picked track to.
1050
+ func execute(_ intent: TrackSelector.PlaybackIntent) {
1051
+ switch intent {
1052
+ case let .skipTo(index):
1053
+ do {
1054
+ try skipTo(index, playWhenReady: true)
1055
+ } catch {
1056
+ logger.error("Failed to skip to track at index \(index): \(error)")
1057
+ }
1058
+ case let .setQueue(tracks, startIndex, sourcePath):
1059
+ setQueue(tracks, initialIndex: startIndex, playWhenReady: true, sourcePath: sourcePath)
1060
+ case let .loadTrack(track):
1061
+ load(track, playWhenReady: true)
1062
+ }
1063
+ }
1064
+
1065
+ // MARK: - Progress Updates
1066
+
1067
+ func setProgressUpdateInterval(_ interval: TimeInterval?) {
1068
+ progressTimer.setInterval(interval)
1069
+ }
1070
+
1071
+ func setPlaybackIntervalEnabled(_ enabled: Bool) {
1072
+ intervalTimer.setInterval(enabled ? 1 : nil)
1073
+ }
1074
+ }
1075
+
1076
+ // MARK: - TrackSelectionPlayer
1077
+
1078
+ extension Player: TrackSelectionPlayer {}