react-native-audio-browser 0.1.1 → 0.2.0-next.10

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 (1000) hide show
  1. package/AudioBrowser.podspec +1 -1
  2. package/README.md +184 -2
  3. package/android/build.gradle +17 -5
  4. package/android/gradle.properties +2 -2
  5. package/android/src/main/AndroidManifest.xml +18 -1
  6. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +473 -158
  7. package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
  8. package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
  9. package/android/src/main/java/com/audiobrowser/Callbacks.kt +4 -6
  10. package/android/src/main/java/com/audiobrowser/Service.kt +67 -18
  11. package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
  12. package/android/src/main/java/com/audiobrowser/browser/ArtworkResolutionRegistry.kt +37 -0
  13. package/android/src/main/java/com/audiobrowser/browser/BrowseArtworkRegistry.kt +34 -0
  14. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +442 -178
  15. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +299 -0
  16. package/android/src/main/java/com/audiobrowser/browser/JsonArtwork.kt +79 -0
  17. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +41 -4
  18. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +42 -0
  19. package/android/src/main/java/com/audiobrowser/browser/TrackLoadHandler.kt +4 -4
  20. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +35 -0
  21. package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
  22. package/android/src/main/java/com/audiobrowser/http/RequestConfigBuilder.kt +109 -97
  23. package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +6 -10
  24. package/android/src/main/java/com/audiobrowser/model/PlayerSetupOptions.kt +40 -8
  25. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +40 -46
  26. package/android/src/main/java/com/audiobrowser/player/AutomaticBufferManager.kt +4 -1
  27. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +172 -0
  28. package/android/src/main/java/com/audiobrowser/player/DynamicLoadControl.kt +18 -1
  29. package/android/src/main/java/com/audiobrowser/player/EqualizerController.kt +153 -0
  30. package/android/src/main/java/com/audiobrowser/player/InterceptingPlayer.kt +141 -0
  31. package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +15 -3
  32. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +422 -134
  33. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +248 -404
  34. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +347 -0
  35. package/android/src/main/java/com/audiobrowser/player/PlaybackErrorClassifier.kt +129 -0
  36. package/android/src/main/java/com/audiobrowser/player/PlaybackStateMachine.kt +85 -0
  37. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +25 -5
  38. package/android/src/main/java/com/audiobrowser/player/PlaybackTimer.kt +56 -0
  39. package/android/src/main/java/com/audiobrowser/player/Player.kt +475 -577
  40. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +20 -0
  41. package/android/src/main/java/com/audiobrowser/player/PlayerEngineFactory.kt +130 -0
  42. package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +190 -67
  43. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +118 -53
  44. package/android/src/main/java/com/audiobrowser/player/SleepTimer.kt +37 -3
  45. package/android/src/main/java/com/audiobrowser/player/SleepTimerManager.kt +80 -0
  46. package/android/src/main/java/com/audiobrowser/player/StuckRecoveryPolicy.kt +50 -0
  47. package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +85 -40
  48. package/android/src/main/java/com/audiobrowser/player/VolumeFader.kt +81 -0
  49. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +156 -0
  50. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +109 -0
  51. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +37 -0
  52. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +55 -0
  53. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +265 -0
  54. package/android/src/main/java/com/audiobrowser/util/ArtworkContentProvider.kt +161 -0
  55. package/android/src/main/java/com/audiobrowser/util/ArtworkUris.kt +28 -0
  56. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +24 -0
  57. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +24 -0
  58. package/android/src/main/java/com/audiobrowser/util/CoilArtworkLoader.kt +49 -0
  59. package/android/src/main/java/com/audiobrowser/util/CoilArtworkLoaderHolder.kt +36 -0
  60. package/android/src/main/java/com/audiobrowser/util/CoilBitmapLoader.kt +23 -300
  61. package/android/src/main/java/com/audiobrowser/util/EqualizerManager.kt +13 -23
  62. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +2 -2
  63. package/android/src/main/java/com/audiobrowser/util/MetadataAdapter.kt +4 -3
  64. package/android/src/main/java/com/audiobrowser/util/OutputMonitor.kt +211 -0
  65. package/android/src/main/java/com/audiobrowser/util/OutputSwitcher.kt +58 -0
  66. package/android/src/main/java/com/audiobrowser/util/PlayingStateFactory.kt +2 -1
  67. package/android/src/main/java/com/audiobrowser/util/RatingFavorites.kt +14 -0
  68. package/android/src/main/java/com/audiobrowser/util/SvgArtworkRenderer.kt +6 -111
  69. package/android/src/main/java/com/audiobrowser/util/SystemVolumeMonitor.kt +22 -8
  70. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +112 -80
  71. package/android/src/main/res/values/strings.xml +4 -0
  72. package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
  73. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +165 -0
  74. package/android/src/test/java/com/audiobrowser/browser/ArtworkResolutionRegistryTest.kt +46 -0
  75. package/android/src/test/java/com/audiobrowser/browser/BrowseArtworkRegistryTest.kt +39 -0
  76. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerBuildApiRequestTest.kt +114 -0
  77. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +141 -0
  78. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +4 -0
  79. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +177 -0
  80. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +55 -0
  81. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +94 -0
  82. package/android/src/test/java/com/audiobrowser/browser/SimpleRouterTest.kt +1 -2
  83. package/android/src/test/java/com/audiobrowser/http/RequestConfigBuilderTest.kt +72 -0
  84. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +203 -0
  85. package/android/src/test/java/com/audiobrowser/player/DynamicLoadControlTest.kt +56 -0
  86. package/android/src/test/java/com/audiobrowser/player/EqualizerControllerTest.kt +216 -0
  87. package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
  88. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +442 -0
  89. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +190 -0
  90. package/android/src/test/java/com/audiobrowser/player/PlaybackStateMachineTest.kt +198 -0
  91. package/android/src/test/java/com/audiobrowser/player/PlaybackTimerTest.kt +137 -0
  92. package/android/src/test/java/com/audiobrowser/player/PlayerEngineFactoryTest.kt +36 -0
  93. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +268 -0
  94. package/android/src/test/java/com/audiobrowser/player/SessionErrorMaskTest.kt +68 -0
  95. package/android/src/test/java/com/audiobrowser/player/StuckRecoveryPolicyTest.kt +40 -0
  96. package/android/src/test/java/com/audiobrowser/player/TransformingDataSourceTest.kt +193 -0
  97. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +153 -0
  98. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +186 -0
  99. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +158 -0
  100. package/android/src/test/java/com/audiobrowser/tls/CertFixtures.kt +18 -0
  101. package/android/src/test/java/com/audiobrowser/util/ArtworkContentProviderTest.kt +163 -0
  102. package/android/src/test/java/com/audiobrowser/util/ArtworkUrisTest.kt +34 -0
  103. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +28 -0
  104. package/android/src/test/java/com/audiobrowser/util/CoilArtworkLoaderHolderTest.kt +53 -0
  105. package/android/src/test/java/com/audiobrowser/util/CoilArtworkLoaderTest.kt +58 -0
  106. package/android/src/test/java/com/audiobrowser/util/FakeImageLoader.kt +57 -0
  107. package/android/src/test/java/com/audiobrowser/util/PlayingStateFactoryTest.kt +40 -0
  108. package/android/src/test/java/com/audiobrowser/util/RatingFavoritesTest.kt +34 -0
  109. package/android/src/test/java/com/audiobrowser/util/TrackFactoryBrowseTest.kt +50 -0
  110. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +63 -0
  111. package/android/src/test/java/com/audiobrowser/util/TrackFactoryRatingTest.kt +35 -0
  112. package/android/src/test/resources/certs/isrg-root-x1.pem +31 -0
  113. package/android/src/test/resources/certs/letsencrypt-r13.der +0 -0
  114. package/android/src/test/resources/certs/letsencrypt-r13.pem +29 -0
  115. package/android/src/test/resources/certs/stationplaylist-leaf.pem +34 -0
  116. package/ios/Browser/BrowserConfig.swift +41 -11
  117. package/ios/Browser/BrowserManager+URLResolution.swift +213 -93
  118. package/ios/Browser/BrowserManager.swift +303 -135
  119. package/ios/Browser/BrowserPathHelper.swift +32 -3
  120. package/ios/Browser/JsonModels.swift +287 -66
  121. package/ios/Browser/MediaResolveComposer.swift +114 -0
  122. package/ios/Browser/SearchDrillIn.swift +44 -0
  123. package/ios/Browser/SectionScope.swift +35 -0
  124. package/ios/Browser/TabBarEntries.swift +15 -0
  125. package/ios/CarPlay/CPInterfaceController+Safe.swift +126 -0
  126. package/ios/CarPlay/CarPlayArtworkResolver.swift +6 -6
  127. package/ios/CarPlay/CarPlayController.swift +739 -149
  128. package/ios/CarPlay/CarPlayImageLoader.swift +57 -8
  129. package/ios/CarPlay/CarPlayListItemFactory.swift +44 -35
  130. package/ios/CarPlay/CarPlayNowPlayingManager.swift +180 -36
  131. package/ios/CarPlay/ImageRowItem+CarPlay.swift +19 -0
  132. package/ios/CarPlay/RNABAudioBrowser.h +15 -10
  133. package/ios/CarPlay/RNABAudioBrowser.m +15 -14
  134. package/ios/CarPlay/RNABCarPlaySceneDelegate.m +2 -0
  135. package/ios/CarPlay/RNABMediaAddHandler.swift +75 -0
  136. package/ios/CarPlay/RNABMediaAffinityHandler.swift +69 -0
  137. package/ios/CarPlay/RNABMediaIntentHandler.swift +53 -43
  138. package/ios/Extension/Capability+RemoteCommand.swift +7 -9
  139. package/ios/Extension/ChapterMetadata+AVFoundation.swift +1 -1
  140. package/ios/Extension/NavigationError+Conversion.swift +8 -1
  141. package/ios/Extension/PlayerCapabilities+Favorite.swift +20 -0
  142. package/ios/Extension/RequestConfig+Copying.swift +31 -0
  143. package/ios/Extension/ResolvedTrack+Copying.swift +10 -4
  144. package/ios/Extension/TimedMetadata+AVFoundation.swift +1 -1
  145. package/ios/Extension/Track+AVPlayer.swift +4 -4
  146. package/ios/Extension/Track+Copying.swift +9 -3
  147. package/ios/Extension/TrackMetadata+AVFoundation.swift +1 -1
  148. package/ios/Extension/Variant+Artwork.swift +49 -0
  149. package/ios/HybridAudioBrowser.swift +805 -197
  150. package/ios/Model/NitroTypeStubs.swift +299 -224
  151. package/ios/Model/PlayerUpdateOptions.swift +23 -23
  152. package/ios/Model/RemoteCommand.swift +24 -6
  153. package/ios/Model/TrackPlayerError.swift +82 -18
  154. package/ios/NowPlayingInfo/NowPlayingInfoController.swift +99 -121
  155. package/ios/NowPlayingInfo/SilentAudioPrimer.swift +73 -0
  156. package/ios/Observer/PlayerItemNotificationObserver.swift +45 -5
  157. package/ios/Observer/PlayerItemPropertyObserver.swift +27 -4
  158. package/ios/Observer/PlayerTimeObserver.swift +3 -53
  159. package/ios/Option/SessionCategories.swift +34 -11
  160. package/ios/Player/AirPlayStallJudgement.swift +25 -0
  161. package/ios/Player/EndOfTrackJudgement.swift +32 -0
  162. package/ios/Player/LoadSeekCoordinator.swift +16 -1
  163. package/ios/Player/MediaIntentCriteria.swift +139 -0
  164. package/ios/Player/MediaLoader.swift +31 -11
  165. package/ios/Player/NowPlayingUpdater.swift +119 -26
  166. package/ios/Player/PersistedPlaybackState.swift +11 -0
  167. package/ios/Player/PlaybackCoordinator.swift +285 -51
  168. package/ios/Player/PlaybackCoordinatorCallbacks.swift +6 -1
  169. package/ios/Player/PlaybackEffectHandler.swift +19 -9
  170. package/ios/Player/PlaybackErrorHandler.swift +98 -14
  171. package/ios/Player/PlaybackStateMachine.swift +18 -13
  172. package/ios/Player/PlaybackStateStore.swift +27 -0
  173. package/ios/Player/PlaybackTimer.swift +61 -0
  174. package/ios/Player/PlayingStateManager.swift +3 -2
  175. package/ios/Player/QueueManager.swift +77 -64
  176. package/ios/Player/RetryManager.swift +175 -38
  177. package/ios/Player/ShuffleOrder.swift +10 -0
  178. package/ios/Player/SleepTimerHandling.swift +8 -0
  179. package/ios/Player/SleepTimerManager.swift +72 -11
  180. package/ios/Player/VolumeFader.swift +77 -0
  181. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
  182. package/ios/RemoteCommand/RemoteCommandController.swift +40 -21
  183. package/ios/Tests/AirPlayStallJudgementTests.swift +52 -0
  184. package/ios/Tests/BrowserPathHelperTests.swift +28 -0
  185. package/ios/Tests/CarPlayArtworkResolverTests.swift +39 -39
  186. package/ios/Tests/ChapterMetadataAVFoundationTests.swift +2 -2
  187. package/ios/Tests/EndOfTrackJudgementTests.swift +35 -0
  188. package/ios/Tests/JsonModelsTests.swift +58 -0
  189. package/ios/Tests/LRUCacheTests.swift +111 -0
  190. package/ios/Tests/LoadSeekCoordinatorTests.swift +40 -7
  191. package/ios/Tests/MediaIntentCriteriaTests.swift +170 -0
  192. package/ios/Tests/MediaLoaderAssetOptionsTests.swift +46 -0
  193. package/ios/Tests/MediaLoaderTests.swift +32 -20
  194. package/ios/Tests/MediaResolveTests.swift +126 -0
  195. package/ios/Tests/OnceValueTests.swift +79 -0
  196. package/ios/Tests/PlaybackCoordinatorTests.swift +787 -13
  197. package/ios/Tests/PlaybackErrorHandlerTests.swift +383 -47
  198. package/ios/Tests/PlaybackStateMachineTests.swift +33 -8
  199. package/ios/Tests/PlaybackStateStoreTests.swift +48 -0
  200. package/ios/Tests/PlaybackTimerTests.swift +55 -0
  201. package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
  202. package/ios/Tests/PlayingStateManagerTests.swift +57 -0
  203. package/ios/Tests/QueueManagerTests.swift +195 -18
  204. package/ios/Tests/RemoteCommandControllerTests.swift +156 -0
  205. package/ios/Tests/RemoteCommandTests.swift +67 -0
  206. package/ios/Tests/ResolvedTrackCopyingTests.swift +15 -8
  207. package/ios/Tests/RetryManagerTests.swift +178 -0
  208. package/ios/Tests/SearchDrillInTests.swift +63 -0
  209. package/ios/Tests/SectionScopeTests.swift +82 -0
  210. package/ios/Tests/SleepTimerManagerTests.swift +107 -0
  211. package/ios/Tests/Support/ManualSleepTimerScheduler.swift +47 -0
  212. package/ios/Tests/Support/TestTypes.swift +61 -20
  213. package/ios/Tests/TabBarEntriesTests.swift +71 -0
  214. package/ios/Tests/TrackSelectorTests.swift +15 -15
  215. package/ios/TrackPlayer.swift +385 -58
  216. package/ios/TrackPlayerCallbacks.swift +5 -44
  217. package/ios/TrackSelector.swift +5 -5
  218. package/ios/Util/ArtworkImageFetcher.swift +4 -2
  219. package/ios/Util/LRUCache.swift +4 -1
  220. package/ios/Util/NetworkMonitor.swift +7 -3
  221. package/ios/Util/OnceValue.swift +34 -0
  222. package/lib/commonjs/AudioBrowser.js +0 -11
  223. package/lib/commonjs/AudioBrowser.js.map +1 -1
  224. package/lib/commonjs/CLAUDE.md +52 -0
  225. package/lib/commonjs/features/browser-config.js +227 -0
  226. package/lib/commonjs/features/browser-config.js.map +1 -0
  227. package/lib/commonjs/features/browser.js +60 -159
  228. package/lib/commonjs/features/browser.js.map +1 -1
  229. package/lib/commonjs/features/carConnection.js +42 -0
  230. package/lib/commonjs/features/carConnection.js.map +1 -0
  231. package/lib/commonjs/features/equalizer.js +1 -1
  232. package/lib/commonjs/features/errors.js +37 -5
  233. package/lib/commonjs/features/errors.js.map +1 -1
  234. package/lib/commonjs/features/favorites.js +1 -1
  235. package/lib/commonjs/features/gate.js +164 -0
  236. package/lib/commonjs/features/gate.js.map +1 -0
  237. package/lib/commonjs/features/index.js +22 -0
  238. package/lib/commonjs/features/index.js.map +1 -1
  239. package/lib/commonjs/features/metadata.js +20 -3
  240. package/lib/commonjs/features/metadata.js.map +1 -1
  241. package/lib/commonjs/features/network.js +1 -1
  242. package/lib/commonjs/features/nowPlaying.js +36 -1
  243. package/lib/commonjs/features/nowPlaying.js.map +1 -1
  244. package/lib/commonjs/features/output.js +37 -23
  245. package/lib/commonjs/features/output.js.map +1 -1
  246. package/lib/commonjs/features/playback/controls.js +9 -0
  247. package/lib/commonjs/features/playback/controls.js.map +1 -1
  248. package/lib/commonjs/features/playback/index.js +11 -0
  249. package/lib/commonjs/features/playback/index.js.map +1 -1
  250. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  251. package/lib/commonjs/features/playback/playing.js +1 -1
  252. package/lib/commonjs/features/playback/progress.js +29 -13
  253. package/lib/commonjs/features/playback/progress.js.map +1 -1
  254. package/lib/commonjs/features/playback/state.js +1 -1
  255. package/lib/commonjs/features/playback/trackPlaybackTime.js +86 -0
  256. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -0
  257. package/lib/commonjs/features/playback/volume.js +2 -2
  258. package/lib/commonjs/features/player/options.js +79 -29
  259. package/lib/commonjs/features/player/options.js.map +1 -1
  260. package/lib/commonjs/features/player/setup.js +256 -3
  261. package/lib/commonjs/features/player/setup.js.map +1 -1
  262. package/lib/commonjs/features/player/validateOptions.js +21 -0
  263. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  264. package/lib/commonjs/features/queue/activeTrack.js +1 -1
  265. package/lib/commonjs/features/queue/queue.js +12 -5
  266. package/lib/commonjs/features/queue/queue.js.map +1 -1
  267. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  268. package/lib/commonjs/features/queue/shuffle.js +1 -1
  269. package/lib/commonjs/features/remoteControls.js +14 -104
  270. package/lib/commonjs/features/remoteControls.js.map +1 -1
  271. package/lib/commonjs/features/sleepTimer.js +22 -5
  272. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  273. package/lib/commonjs/native.js +5 -0
  274. package/lib/commonjs/native.js.map +1 -1
  275. package/lib/commonjs/native.web.js +4 -1
  276. package/lib/commonjs/native.web.js.map +1 -1
  277. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  278. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  279. package/lib/commonjs/utils/LazyNativeEmitter.js +8 -8
  280. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  281. package/lib/commonjs/utils/NativeUpdatedValue.js +0 -3
  282. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  283. package/lib/commonjs/utils/artwork.js +31 -0
  284. package/lib/commonjs/utils/artwork.js.map +1 -0
  285. package/lib/commonjs/web/NativeAudioBrowser.js +302 -125
  286. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  287. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +71 -0
  288. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -0
  289. package/lib/commonjs/web/TrackPlayer/PlaybackTimer.js +34 -0
  290. package/lib/commonjs/web/TrackPlayer/PlaybackTimer.js.map +1 -0
  291. package/lib/commonjs/web/TrackPlayer/Player.js +90 -26
  292. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  293. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +21 -0
  294. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js.map +1 -0
  295. package/lib/commonjs/web/TrackPlayer/QueueManager.js +273 -0
  296. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -0
  297. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +158 -0
  298. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -0
  299. package/lib/commonjs/web/TrackPlayer/RepeatMode.js +1 -1
  300. package/lib/commonjs/web/TrackPlayer/RepeatMode.js.map +1 -1
  301. package/lib/commonjs/web/TrackPlayer/SleepTimer.js +34 -3
  302. package/lib/commonjs/web/TrackPlayer/SleepTimer.js.map +1 -1
  303. package/lib/commonjs/web/TrackPlayer/VolumeFader.js +75 -0
  304. package/lib/commonjs/web/TrackPlayer/VolumeFader.js.map +1 -0
  305. package/lib/commonjs/web/TrackPlayer/index.js +13 -5
  306. package/lib/commonjs/web/TrackPlayer/index.js.map +1 -1
  307. package/lib/commonjs/web/browser/BrowserManager.js +176 -50
  308. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  309. package/lib/commonjs/web/browser/NavigationErrorManager.js +3 -1
  310. package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
  311. package/lib/commonjs/web/browser/SearchManager.js +11 -9
  312. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  313. package/lib/commonjs/web/browser/assertBrowsePageShape.js +22 -0
  314. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -0
  315. package/lib/commonjs/web/browser/classifyTrackNavigation.js +41 -0
  316. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -0
  317. package/lib/commonjs/web/browser/parseSearchResponse.js +20 -0
  318. package/lib/commonjs/web/browser/parseSearchResponse.js.map +1 -0
  319. package/lib/commonjs/web/http/HttpClient.js +4 -23
  320. package/lib/commonjs/web/http/HttpClient.js.map +1 -1
  321. package/lib/commonjs/web/http/RequestConfigBuilder.js +107 -58
  322. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  323. package/lib/commonjs/web/playbackErrorKind.js +57 -0
  324. package/lib/commonjs/web/playbackErrorKind.js.map +1 -0
  325. package/lib/commonjs/web/player/MediaSessionManager.js +121 -0
  326. package/lib/commonjs/web/player/MediaSessionManager.js.map +1 -0
  327. package/lib/commonjs/web/player/NowPlayingManager.js +6 -5
  328. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  329. package/lib/commonjs/web/player/OptionsManager.js +9 -17
  330. package/lib/commonjs/web/player/OptionsManager.js.map +1 -1
  331. package/lib/commonjs/web/player/RemoteCommandController.js +96 -0
  332. package/lib/commonjs/web/player/RemoteCommandController.js.map +1 -0
  333. package/lib/module/AudioBrowser.js +1 -1
  334. package/lib/module/AudioBrowser.js.map +1 -1
  335. package/lib/module/CLAUDE.md +52 -0
  336. package/lib/module/features/browser-config.js +221 -0
  337. package/lib/module/features/browser-config.js.map +1 -0
  338. package/lib/module/features/browser.js +54 -157
  339. package/lib/module/features/browser.js.map +1 -1
  340. package/lib/module/features/carConnection.js +37 -0
  341. package/lib/module/features/carConnection.js.map +1 -0
  342. package/lib/module/features/equalizer.js +1 -1
  343. package/lib/module/features/errors.js +37 -5
  344. package/lib/module/features/errors.js.map +1 -1
  345. package/lib/module/features/favorites.js +1 -1
  346. package/lib/module/features/gate.js +159 -0
  347. package/lib/module/features/gate.js.map +1 -0
  348. package/lib/module/features/index.js +2 -0
  349. package/lib/module/features/index.js.map +1 -1
  350. package/lib/module/features/metadata.js +20 -3
  351. package/lib/module/features/metadata.js.map +1 -1
  352. package/lib/module/features/network.js +1 -1
  353. package/lib/module/features/nowPlaying.js +34 -1
  354. package/lib/module/features/nowPlaying.js.map +1 -1
  355. package/lib/module/features/output.js +32 -19
  356. package/lib/module/features/output.js.map +1 -1
  357. package/lib/module/features/playback/controls.js +8 -0
  358. package/lib/module/features/playback/controls.js.map +1 -1
  359. package/lib/module/features/playback/index.js +1 -0
  360. package/lib/module/features/playback/index.js.map +1 -1
  361. package/lib/module/features/playback/playWhenReady.js +1 -1
  362. package/lib/module/features/playback/playing.js +1 -1
  363. package/lib/module/features/playback/progress.js +29 -13
  364. package/lib/module/features/playback/progress.js.map +1 -1
  365. package/lib/module/features/playback/state.js +1 -1
  366. package/lib/module/features/playback/trackPlaybackTime.js +83 -0
  367. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -0
  368. package/lib/module/features/playback/volume.js +2 -2
  369. package/lib/module/features/player/options.js +79 -29
  370. package/lib/module/features/player/options.js.map +1 -1
  371. package/lib/module/features/player/setup.js +256 -3
  372. package/lib/module/features/player/setup.js.map +1 -1
  373. package/lib/module/features/player/validateOptions.js +17 -0
  374. package/lib/module/features/player/validateOptions.js.map +1 -0
  375. package/lib/module/features/queue/activeTrack.js +1 -1
  376. package/lib/module/features/queue/queue.js +12 -5
  377. package/lib/module/features/queue/queue.js.map +1 -1
  378. package/lib/module/features/queue/repeatMode.js +1 -1
  379. package/lib/module/features/queue/shuffle.js +1 -1
  380. package/lib/module/features/remoteControls.js +13 -103
  381. package/lib/module/features/remoteControls.js.map +1 -1
  382. package/lib/module/features/sleepTimer.js +22 -5
  383. package/lib/module/features/sleepTimer.js.map +1 -1
  384. package/lib/module/native.js +5 -0
  385. package/lib/module/native.js.map +1 -1
  386. package/lib/module/native.web.js +4 -1
  387. package/lib/module/native.web.js.map +1 -1
  388. package/lib/module/test-utils/reactNativeStub.js +58 -0
  389. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  390. package/lib/module/utils/LazyNativeEmitter.js +8 -8
  391. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  392. package/lib/module/utils/NativeUpdatedValue.js +0 -3
  393. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  394. package/lib/module/utils/artwork.js +26 -0
  395. package/lib/module/utils/artwork.js.map +1 -0
  396. package/lib/module/web/NativeAudioBrowser.js +303 -126
  397. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  398. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +67 -0
  399. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -0
  400. package/lib/module/web/TrackPlayer/PlaybackTimer.js +29 -0
  401. package/lib/module/web/TrackPlayer/PlaybackTimer.js.map +1 -0
  402. package/lib/module/web/TrackPlayer/Player.js +90 -26
  403. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  404. package/lib/module/web/TrackPlayer/PlayingStateFactory.js +17 -0
  405. package/lib/module/web/TrackPlayer/PlayingStateFactory.js.map +1 -0
  406. package/lib/module/web/TrackPlayer/QueueManager.js +269 -0
  407. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -0
  408. package/lib/module/web/TrackPlayer/QueuePlayer.js +154 -0
  409. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -0
  410. package/lib/module/web/TrackPlayer/RepeatMode.js +1 -1
  411. package/lib/module/web/TrackPlayer/RepeatMode.js.map +1 -1
  412. package/lib/module/web/TrackPlayer/SleepTimer.js +34 -3
  413. package/lib/module/web/TrackPlayer/SleepTimer.js.map +1 -1
  414. package/lib/module/web/TrackPlayer/VolumeFader.js +70 -0
  415. package/lib/module/web/TrackPlayer/VolumeFader.js.map +1 -0
  416. package/lib/module/web/TrackPlayer/index.js +2 -1
  417. package/lib/module/web/TrackPlayer/index.js.map +1 -1
  418. package/lib/module/web/browser/BrowserManager.js +176 -50
  419. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  420. package/lib/module/web/browser/NavigationErrorManager.js +3 -1
  421. package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
  422. package/lib/module/web/browser/SearchManager.js +11 -9
  423. package/lib/module/web/browser/SearchManager.js.map +1 -1
  424. package/lib/module/web/browser/assertBrowsePageShape.js +18 -0
  425. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -0
  426. package/lib/module/web/browser/classifyTrackNavigation.js +38 -0
  427. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -0
  428. package/lib/module/web/browser/parseSearchResponse.js +16 -0
  429. package/lib/module/web/browser/parseSearchResponse.js.map +1 -0
  430. package/lib/module/web/http/HttpClient.js +4 -23
  431. package/lib/module/web/http/HttpClient.js.map +1 -1
  432. package/lib/module/web/http/RequestConfigBuilder.js +107 -58
  433. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  434. package/lib/module/web/playbackErrorKind.js +52 -0
  435. package/lib/module/web/playbackErrorKind.js.map +1 -0
  436. package/lib/module/web/player/MediaSessionManager.js +116 -0
  437. package/lib/module/web/player/MediaSessionManager.js.map +1 -0
  438. package/lib/module/web/player/NowPlayingManager.js +7 -5
  439. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  440. package/lib/module/web/player/OptionsManager.js +9 -17
  441. package/lib/module/web/player/OptionsManager.js.map +1 -1
  442. package/lib/module/web/player/RemoteCommandController.js +92 -0
  443. package/lib/module/web/player/RemoteCommandController.js.map +1 -0
  444. package/lib/typescript/src/AudioBrowser.d.ts +0 -1
  445. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  446. package/lib/typescript/src/features/browser-config.d.ts +12 -0
  447. package/lib/typescript/src/features/browser-config.d.ts.map +1 -0
  448. package/lib/typescript/src/features/browser.d.ts +40 -1
  449. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  450. package/lib/typescript/src/features/carConnection.d.ts +20 -0
  451. package/lib/typescript/src/features/carConnection.d.ts.map +1 -0
  452. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  453. package/lib/typescript/src/features/errors.d.ts +61 -6
  454. package/lib/typescript/src/features/errors.d.ts.map +1 -1
  455. package/lib/typescript/src/features/favorites.d.ts +1 -1
  456. package/lib/typescript/src/features/gate.d.ts +93 -0
  457. package/lib/typescript/src/features/gate.d.ts.map +1 -0
  458. package/lib/typescript/src/features/index.d.ts +2 -0
  459. package/lib/typescript/src/features/index.d.ts.map +1 -1
  460. package/lib/typescript/src/features/metadata.d.ts +26 -7
  461. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  462. package/lib/typescript/src/features/network.d.ts +1 -1
  463. package/lib/typescript/src/features/nowPlaying.d.ts +28 -1
  464. package/lib/typescript/src/features/nowPlaying.d.ts.map +1 -1
  465. package/lib/typescript/src/features/output.d.ts +28 -18
  466. package/lib/typescript/src/features/output.d.ts.map +1 -1
  467. package/lib/typescript/src/features/playback/controls.d.ts +5 -0
  468. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  469. package/lib/typescript/src/features/playback/index.d.ts +1 -0
  470. package/lib/typescript/src/features/playback/index.d.ts.map +1 -1
  471. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  472. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  473. package/lib/typescript/src/features/playback/progress.d.ts +4 -1
  474. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  475. package/lib/typescript/src/features/playback/state.d.ts +1 -1
  476. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +34 -0
  477. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -0
  478. package/lib/typescript/src/features/playback/volume.d.ts +2 -2
  479. package/lib/typescript/src/features/player/options.d.ts +176 -127
  480. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  481. package/lib/typescript/src/features/player/setup.d.ts +216 -167
  482. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  483. package/lib/typescript/src/features/player/validateOptions.d.ts +9 -0
  484. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  485. package/lib/typescript/src/features/queue/activeTrack.d.ts +1 -1
  486. package/lib/typescript/src/features/queue/queue.d.ts +11 -4
  487. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  488. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  489. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  490. package/lib/typescript/src/features/remoteControls.d.ts +23 -35
  491. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  492. package/lib/typescript/src/features/sleepTimer.d.ts +19 -4
  493. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  494. package/lib/typescript/src/index.d.ts.map +1 -1
  495. package/lib/typescript/src/native.d.ts +1 -2
  496. package/lib/typescript/src/native.d.ts.map +1 -1
  497. package/lib/typescript/src/specs/audio-browser.nitro.d.ts +17 -225
  498. package/lib/typescript/src/specs/audio-browser.nitro.d.ts.map +1 -1
  499. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  500. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  501. package/lib/typescript/src/types/browser-native.d.ts +9 -3
  502. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  503. package/lib/typescript/src/types/browser-nodes.d.ts +145 -13
  504. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  505. package/lib/typescript/src/types/browser.d.ts +349 -85
  506. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  507. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +8 -6
  508. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  509. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +0 -2
  510. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  511. package/lib/typescript/src/utils/artwork.d.ts +19 -0
  512. package/lib/typescript/src/utils/artwork.d.ts.map +1 -0
  513. package/lib/typescript/src/utils/resolveAssetSource.d.ts.map +1 -1
  514. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +4 -2
  515. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +24 -10
  516. package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +6 -6
  517. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -20
  518. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +33 -1
  519. package/nitrogen/generated/android/c++/JArtworkVariants.hpp +61 -0
  520. package/nitrogen/generated/android/c++/JBrowseResult.hpp +5 -0
  521. package/nitrogen/generated/android/c++/JCarPlayNowPlayingButton.hpp +3 -3
  522. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +6 -0
  523. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +58 -0
  524. package/nitrogen/generated/android/c++/JFavoritesMatchMode.hpp +58 -0
  525. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +100 -0
  526. package/nitrogen/generated/android/c++/JFormattedNavigationError.hpp +3 -2
  527. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.hpp +112 -0
  528. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +121 -0
  529. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +123 -0
  530. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +133 -0
  531. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +121 -0
  532. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.hpp +139 -0
  533. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +6 -0
  534. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.hpp +134 -0
  535. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -0
  536. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +6 -0
  537. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +131 -0
  538. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +6 -0
  539. package/nitrogen/generated/android/c++/JFunc_void_GateEvent.hpp +79 -0
  540. package/nitrogen/generated/android/c++/JFunc_void_NowPlayingMetadata.hpp +0 -10
  541. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +14 -9
  542. package/nitrogen/generated/android/c++/JFunc_void_Output.hpp +80 -0
  543. package/nitrogen/generated/android/c++/JFunc_void_Playback.hpp +2 -0
  544. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +6 -0
  545. package/nitrogen/generated/android/c++/JFunc_void_PlaybackErrorEvent.hpp +2 -0
  546. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +10 -10
  547. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +6 -0
  548. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +6 -0
  549. package/nitrogen/generated/android/c++/JGate.hpp +62 -0
  550. package/nitrogen/generated/android/c++/JGateDecision.hpp +64 -0
  551. package/nitrogen/generated/android/c++/{JThumbsRating.hpp → JGateEvent.hpp} +16 -15
  552. package/nitrogen/generated/android/c++/JGateReason.hpp +58 -0
  553. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +247 -223
  554. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +26 -23
  555. package/nitrogen/generated/android/c++/JIOSOptions.hpp +95 -0
  556. package/nitrogen/generated/android/c++/JImageRowItem.hpp +33 -3
  557. package/nitrogen/generated/android/c++/JMediaReference.hpp +58 -0
  558. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +33 -1
  559. package/nitrogen/generated/android/c++/{JPartialAndroidSetupPlayerOptions.hpp → JNativeAndroidSetupOptions.hpp} +10 -10
  560. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +61 -28
  561. package/nitrogen/generated/android/c++/JNativeGateRequest.hpp +74 -0
  562. package/nitrogen/generated/android/c++/{JPartialIOSSetupPlayerOptions.hpp → JNativeIOSSetupOptions.hpp} +10 -10
  563. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +20 -9
  564. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +182 -0
  565. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +21 -24
  566. package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +6 -0
  567. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -20
  568. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +96 -0
  569. package/nitrogen/generated/android/c++/JNowPlayingMetadata.hpp +3 -17
  570. package/nitrogen/generated/android/c++/JNowPlayingUpdate.hpp +7 -3
  571. package/nitrogen/generated/android/c++/JOptions.hpp +19 -14
  572. package/nitrogen/generated/android/c++/{JIosOutput.hpp → JOutput.hpp} +15 -15
  573. package/nitrogen/generated/android/c++/JOutputType.hpp +82 -0
  574. package/nitrogen/generated/android/c++/JPlayback.hpp +2 -0
  575. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +6 -0
  576. package/nitrogen/generated/android/c++/JPlaybackError.hpp +18 -3
  577. package/nitrogen/generated/android/c++/JPlaybackErrorEvent.hpp +2 -0
  578. package/nitrogen/generated/android/c++/JPlaybackErrorKind.hpp +76 -0
  579. package/nitrogen/generated/android/c++/JPlaybackState.hpp +3 -3
  580. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +11 -5
  581. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +20 -20
  582. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  583. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +23 -5
  584. package/nitrogen/generated/android/c++/JRetryConfig.hpp +11 -7
  585. package/nitrogen/generated/android/c++/JSearchMode.hpp +27 -9
  586. package/nitrogen/generated/android/c++/JSearchParams.hpp +9 -3
  587. package/nitrogen/generated/android/c++/JStallReason.hpp +58 -0
  588. package/nitrogen/generated/android/c++/JTrack.hpp +23 -5
  589. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +6 -0
  590. package/nitrogen/generated/android/c++/JTrackRequest.hpp +93 -0
  591. package/nitrogen/generated/android/c++/JTransformableRequestConfig.hpp +14 -1
  592. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +26 -0
  593. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +71 -0
  594. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  595. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  596. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.cpp +41 -0
  597. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +121 -0
  598. package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.cpp +26 -0
  599. package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.hpp +70 -0
  600. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidAudioContentType.kt +4 -4
  601. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -9
  602. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkRequestConfig.kt +10 -4
  603. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkVariants.kt +41 -0
  604. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayNowPlayingButton.kt +3 -3
  605. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{HeartRating.kt → FavoriteConfig.kt} +6 -6
  606. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{RatingType.kt → FavoritesMatchMode.kt} +5 -10
  607. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNowPlayingParams.kt +50 -0
  608. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormattedNavigationError.kt +2 -2
  609. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.kt +80 -0
  610. 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 +80 -0
  611. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___Track.kt +80 -0
  612. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.kt +80 -0
  613. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_std__string____Track.kt +80 -0
  614. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.kt +80 -0
  615. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.kt +80 -0
  616. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.kt → Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.kt} +12 -12
  617. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_void_RemoteSetRatingEvent.kt → Func_void_GateEvent.kt} +9 -9
  618. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_void_IosOutput.kt → Func_void_Output.kt} +9 -9
  619. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_bool.kt +9 -9
  620. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{ThumbsRating.kt → Gate.kt} +9 -6
  621. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PercentageRating.kt → GateDecision.kt} +9 -6
  622. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{StarRating.kt → GateEvent.kt} +6 -6
  623. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → GateReason.kt} +5 -8
  624. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +104 -110
  625. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +44 -0
  626. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +24 -3
  627. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaReference.kt +23 -0
  628. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaRequestConfig.kt +10 -4
  629. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PartialAndroidSetupPlayerOptions.kt → NativeAndroidSetupOptions.kt} +5 -5
  630. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +18 -6
  631. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{RemoteSetRatingEvent.kt → NativeGateRequest.kt} +12 -6
  632. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PartialIOSSetupPlayerOptions.kt → NativeIOSSetupOptions.kt} +5 -5
  633. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeRouteEntry.kt +4 -4
  634. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeSetupPlayerOptions.kt +66 -0
  635. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeUpdateOptions.kt +6 -6
  636. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorType.kt +3 -1
  637. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -9
  638. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +44 -0
  639. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingMetadata.kt +3 -6
  640. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingUpdate.kt +6 -3
  641. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Options.kt +3 -3
  642. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{IosOutput.kt → Output.kt} +6 -6
  643. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/OutputType.kt +31 -0
  644. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackError.kt +12 -3
  645. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackErrorKind.kt +29 -0
  646. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackState.kt +6 -6
  647. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  648. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButton.kt +26 -0
  649. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
  650. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -3
  651. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RetryConfig.kt +7 -4
  652. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchMode.kt +12 -6
  653. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchParams.kt +6 -3
  654. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StallReason.kt +23 -0
  655. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +12 -3
  656. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackRequest.kt +44 -0
  657. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TransformableRequestConfig.kt +7 -4
  658. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +53 -0
  659. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
  660. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.kt +65 -0
  661. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_String_ArtworkVariants.kt +53 -0
  662. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +139 -28
  663. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +1126 -490
  664. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +66 -45
  665. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +178 -120
  666. package/nitrogen/generated/ios/swift/AndroidAudioContentType.swift +8 -8
  667. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -22
  668. package/nitrogen/generated/ios/swift/ArtworkRequestConfig.swift +95 -1
  669. package/nitrogen/generated/ios/swift/ArtworkVariants.swift +34 -0
  670. package/nitrogen/generated/ios/swift/CarPlayNowPlayingButton.swift +4 -4
  671. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +29 -0
  672. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +40 -0
  673. package/nitrogen/generated/ios/swift/FormatNowPlayingParams.swift +67 -0
  674. package/nitrogen/generated/ios/swift/FormattedNavigationError.swift +17 -4
  675. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.swift +54 -0
  676. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.swift +69 -0
  677. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___Track.swift +54 -0
  678. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.swift +60 -0
  679. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__optional_std__string____Track.swift +60 -0
  680. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.swift +61 -0
  681. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.swift +61 -0
  682. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.swift +77 -0
  683. package/nitrogen/generated/ios/swift/Func_void_GateDecision.swift +46 -0
  684. package/nitrogen/generated/ios/swift/{Func_void_RemoteSetRatingEvent.swift → Func_void_GateEvent.swift} +10 -10
  685. package/nitrogen/generated/ios/swift/{Func_void_IosOutput.swift → Func_void_Output.swift} +10 -10
  686. package/nitrogen/generated/ios/swift/Func_void_TransformableRequestConfig.swift +46 -0
  687. package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
  688. package/nitrogen/generated/ios/swift/Func_void_std__optional_NowPlayingUpdate_.swift +46 -0
  689. package/nitrogen/generated/ios/swift/Func_void_std__optional_std__string_.swift +53 -0
  690. package/nitrogen/generated/ios/swift/{Func_void_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError___.swift → Func_void_std__shared_ptr_Promise_GateDecision__.swift} +17 -17
  691. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_Promise_TransformableRequestConfig__.swift +66 -0
  692. package/nitrogen/generated/ios/swift/Func_void_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError____.swift +81 -0
  693. package/nitrogen/generated/ios/swift/Gate.swift +47 -0
  694. package/nitrogen/generated/ios/swift/GateDecision.swift +40 -0
  695. package/nitrogen/generated/ios/swift/GateEvent.swift +29 -0
  696. package/nitrogen/generated/ios/swift/GateReason.swift +40 -0
  697. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +21 -15
  698. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +232 -199
  699. package/nitrogen/generated/ios/swift/IOSOptions.swift +51 -0
  700. package/nitrogen/generated/ios/swift/ImageRowItem.swift +121 -2
  701. package/nitrogen/generated/ios/swift/MediaReference.swift +40 -0
  702. package/nitrogen/generated/ios/swift/MediaRequestConfig.swift +107 -1
  703. package/nitrogen/generated/ios/swift/{PartialAndroidSetupPlayerOptions.swift → NativeAndroidSetupOptions.swift} +5 -5
  704. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +161 -22
  705. package/nitrogen/generated/ios/swift/NativeGateRequest.swift +58 -0
  706. package/nitrogen/generated/ios/swift/{PartialIOSSetupPlayerOptions.swift → NativeIOSSetupOptions.swift} +5 -5
  707. package/nitrogen/generated/ios/swift/NativeRouteEntry.swift +19 -19
  708. package/nitrogen/generated/ios/swift/NativeSetupPlayerOptions.swift +206 -0
  709. package/nitrogen/generated/ios/swift/NativeUpdateOptions.swift +12 -25
  710. package/nitrogen/generated/ios/swift/NavigationErrorType.swift +8 -0
  711. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -41
  712. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +90 -0
  713. package/nitrogen/generated/ios/swift/NowPlayingMetadata.swift +1 -48
  714. package/nitrogen/generated/ios/swift/NowPlayingUpdate.swift +19 -1
  715. package/nitrogen/generated/ios/swift/Options.swift +10 -4
  716. package/nitrogen/generated/ios/swift/{IosOutput.swift → Output.swift} +7 -7
  717. package/nitrogen/generated/ios/swift/OutputType.swift +72 -0
  718. package/nitrogen/generated/ios/swift/PlaybackError.swift +43 -2
  719. package/nitrogen/generated/ios/swift/PlaybackErrorKind.swift +64 -0
  720. package/nitrogen/generated/ios/swift/PlaybackState.swift +4 -4
  721. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +27 -8
  722. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +10 -10
  723. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  724. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +74 -8
  725. package/nitrogen/generated/ios/swift/RetryConfig.swift +35 -4
  726. package/nitrogen/generated/ios/swift/SearchMode.swift +36 -12
  727. package/nitrogen/generated/ios/swift/SearchParams.swift +7 -2
  728. package/nitrogen/generated/ios/swift/StallReason.swift +40 -0
  729. package/nitrogen/generated/ios/swift/Track.swift +74 -8
  730. package/nitrogen/generated/ios/swift/TrackRequest.swift +106 -0
  731. package/nitrogen/generated/ios/swift/TransformableRequestConfig.swift +60 -1
  732. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +18 -0
  733. package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
  734. package/nitrogen/generated/ios/swift/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.swift +19 -0
  735. package/nitrogen/generated/ios/swift/Variant_String_ArtworkVariants.swift +18 -0
  736. package/nitrogen/generated/shared/c++/AndroidAudioContentType.hpp +10 -10
  737. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -19
  738. package/nitrogen/generated/shared/c++/ArtworkRequestConfig.hpp +10 -2
  739. package/nitrogen/generated/shared/c++/ArtworkVariants.hpp +87 -0
  740. package/nitrogen/generated/shared/c++/CarPlayNowPlayingButton.hpp +6 -6
  741. package/nitrogen/generated/shared/c++/EqualizerSettings.hpp +0 -2
  742. package/nitrogen/generated/shared/c++/{StarRating.hpp → FavoriteConfig.hpp} +18 -17
  743. package/nitrogen/generated/shared/c++/FavoritesMatchMode.hpp +76 -0
  744. package/nitrogen/generated/shared/c++/FormatNowPlayingParams.hpp +110 -0
  745. package/nitrogen/generated/shared/c++/FormattedNavigationError.hpp +6 -5
  746. package/nitrogen/generated/shared/c++/{PercentageRating.hpp → Gate.hpp} +21 -16
  747. package/nitrogen/generated/shared/c++/{HeartRating.hpp → GateDecision.hpp} +23 -17
  748. package/nitrogen/generated/shared/c++/{ThumbsRating.hpp → GateEvent.hpp} +18 -17
  749. package/nitrogen/generated/shared/c++/GateReason.hpp +76 -0
  750. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +22 -19
  751. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +45 -36
  752. package/nitrogen/generated/shared/c++/IOSOptions.hpp +91 -0
  753. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +33 -2
  754. package/nitrogen/generated/shared/c++/MediaReference.hpp +76 -0
  755. package/nitrogen/generated/shared/c++/MediaRequestConfig.hpp +10 -2
  756. package/nitrogen/generated/shared/c++/{PartialAndroidSetupPlayerOptions.hpp → NativeAndroidSetupOptions.hpp} +11 -11
  757. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +31 -15
  758. package/nitrogen/generated/shared/c++/NativeGateRequest.hpp +97 -0
  759. package/nitrogen/generated/shared/c++/{PartialIOSSetupPlayerOptions.hpp → NativeIOSSetupOptions.hpp} +11 -11
  760. package/nitrogen/generated/shared/c++/NativeRouteEntry.hpp +5 -5
  761. package/nitrogen/generated/shared/c++/NativeSetupPlayerOptions.hpp +138 -0
  762. package/nitrogen/generated/shared/c++/NativeUpdateOptions.hpp +9 -7
  763. package/nitrogen/generated/shared/c++/NavigationErrorType.hpp +8 -0
  764. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -19
  765. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +92 -0
  766. package/nitrogen/generated/shared/c++/NowPlayingMetadata.hpp +3 -19
  767. package/nitrogen/generated/shared/c++/NowPlayingUpdate.hpp +6 -2
  768. package/nitrogen/generated/shared/c++/Options.hpp +8 -8
  769. package/nitrogen/generated/shared/c++/{IosOutput.hpp → Output.hpp} +18 -18
  770. package/nitrogen/generated/shared/c++/OutputType.hpp +108 -0
  771. package/nitrogen/generated/shared/c++/PlaybackError.hpp +18 -3
  772. package/nitrogen/generated/shared/c++/PlaybackErrorKind.hpp +100 -0
  773. package/nitrogen/generated/shared/c++/PlaybackState.hpp +9 -9
  774. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +9 -6
  775. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +25 -25
  776. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  777. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +24 -5
  778. package/nitrogen/generated/shared/c++/RetryConfig.hpp +10 -6
  779. package/nitrogen/generated/shared/c++/SearchMode.hpp +39 -15
  780. package/nitrogen/generated/shared/c++/SearchParams.hpp +9 -2
  781. package/nitrogen/generated/shared/c++/StallReason.hpp +76 -0
  782. package/nitrogen/generated/shared/c++/Track.hpp +24 -5
  783. package/nitrogen/generated/shared/c++/TrackRequest.hpp +93 -0
  784. package/nitrogen/generated/shared/c++/TransformableRequestConfig.hpp +6 -2
  785. package/package.json +20 -8
  786. package/src/AudioBrowser.ts +1 -1
  787. package/src/CLAUDE.md +52 -0
  788. package/src/codegen-no-config-variant.test.ts +70 -0
  789. package/src/features/battery.test.ts +79 -0
  790. package/src/features/browser-config.test.ts +110 -0
  791. package/src/features/browser-config.ts +293 -0
  792. package/src/features/browser.test.ts +19 -0
  793. package/src/features/browser.ts +59 -202
  794. package/src/features/carConnection.ts +36 -0
  795. package/src/features/equalizer.ts +1 -1
  796. package/src/features/errors.ts +71 -5
  797. package/src/features/favorites.ts +1 -1
  798. package/src/features/gate.test.ts +72 -0
  799. package/src/features/gate.ts +170 -0
  800. package/src/features/hook-wiring.test.ts +254 -0
  801. package/src/features/index.ts +2 -0
  802. package/src/features/metadata.ts +26 -15
  803. package/src/features/network.ts +1 -1
  804. package/src/features/nowPlaying.ts +37 -1
  805. package/src/features/output.ts +35 -22
  806. package/src/features/playback/controls.ts +8 -0
  807. package/src/features/playback/index.ts +1 -0
  808. package/src/features/playback/playWhenReady.ts +1 -1
  809. package/src/features/playback/playing.ts +1 -1
  810. package/src/features/playback/progress.test.ts +169 -0
  811. package/src/features/playback/progress.ts +32 -13
  812. package/src/features/playback/state.ts +1 -1
  813. package/src/features/playback/trackPlaybackTime.test.ts +76 -0
  814. package/src/features/playback/trackPlaybackTime.ts +101 -0
  815. package/src/features/playback/volume.ts +2 -2
  816. package/src/features/player/options.test.ts +38 -0
  817. package/src/features/player/options.ts +217 -110
  818. package/src/features/player/setup.test.ts +125 -0
  819. package/src/features/player/setup.ts +400 -175
  820. package/src/features/player/validateOptions.ts +19 -0
  821. package/src/features/queue/activeTrack.ts +1 -1
  822. package/src/features/queue/queue.test.ts +36 -0
  823. package/src/features/queue/queue.ts +17 -6
  824. package/src/features/queue/repeatMode.ts +1 -1
  825. package/src/features/queue/shuffle.ts +1 -1
  826. package/src/features/remoteControls.ts +13 -115
  827. package/src/features/sleepTimer.test.ts +172 -0
  828. package/src/features/sleepTimer.ts +32 -5
  829. package/src/native.ts +5 -0
  830. package/src/native.web.ts +4 -1
  831. package/src/public-surface.test.ts +107 -0
  832. package/src/specs/audio-browser.nitro.ts +132 -42
  833. package/src/test-utils/reactNativeStub.ts +60 -0
  834. package/src/types/browser-native.ts +19 -6
  835. package/src/types/browser-nodes.ts +160 -13
  836. package/src/types/browser.ts +377 -89
  837. package/src/utils/LazyNativeEmitter.ts +9 -8
  838. package/src/utils/NativeUpdatedValue.ts +0 -2
  839. package/src/utils/artwork.ts +30 -0
  840. package/src/utils/useDebug.test.ts +182 -0
  841. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  842. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +301 -0
  843. package/src/web/NativeAudioBrowser.ts +366 -159
  844. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +139 -0
  845. package/src/web/TrackPlayer/PlaybackStateMachine.ts +82 -0
  846. package/src/web/TrackPlayer/PlaybackTimer.test.ts +67 -0
  847. package/src/web/TrackPlayer/PlaybackTimer.ts +33 -0
  848. package/src/web/TrackPlayer/Player.dispatch.test.ts +229 -0
  849. package/src/web/TrackPlayer/Player.load.test.ts +86 -0
  850. package/src/web/TrackPlayer/Player.ts +113 -39
  851. package/src/web/TrackPlayer/PlayingStateFactory.test.ts +51 -0
  852. package/src/web/TrackPlayer/PlayingStateFactory.ts +24 -0
  853. package/src/web/TrackPlayer/QueueManager.test.ts +289 -0
  854. package/src/web/TrackPlayer/QueueManager.ts +302 -0
  855. package/src/web/TrackPlayer/QueuePlayer.ts +188 -0
  856. package/src/web/TrackPlayer/RepeatMode.ts +1 -1
  857. package/src/web/TrackPlayer/SleepTimer.ts +38 -3
  858. package/src/web/TrackPlayer/VolumeFader.ts +74 -0
  859. package/src/web/TrackPlayer/index.ts +2 -1
  860. package/src/web/browser/BrowserManager.ts +231 -62
  861. package/src/web/browser/NavigationErrorManager.ts +3 -1
  862. package/src/web/browser/SearchManager.ts +11 -15
  863. package/src/web/browser/__tests__/layer-resolver.test.ts +69 -0
  864. package/src/web/browser/assertBrowsePageShape.test.ts +27 -0
  865. package/src/web/browser/assertBrowsePageShape.ts +27 -0
  866. package/src/web/browser/classifyTrackNavigation.test.ts +41 -0
  867. package/src/web/browser/classifyTrackNavigation.ts +35 -0
  868. package/src/web/browser/parseSearchResponse.test.ts +25 -0
  869. package/src/web/browser/parseSearchResponse.ts +15 -0
  870. package/src/web/http/HttpClient.ts +5 -35
  871. package/src/web/http/RequestConfigBuilder.test.ts +59 -0
  872. package/src/web/http/RequestConfigBuilder.ts +120 -63
  873. package/src/web/playbackErrorKind.test.ts +55 -0
  874. package/src/web/playbackErrorKind.ts +56 -0
  875. package/src/web/player/MediaSessionManager.test.ts +220 -0
  876. package/src/web/player/MediaSessionManager.ts +151 -0
  877. package/src/web/player/NowPlayingManager.test.ts +46 -0
  878. package/src/web/player/NowPlayingManager.ts +6 -5
  879. package/src/web/player/OptionsManager.ts +12 -18
  880. package/src/web/player/RemoteCommandController.test.ts +171 -0
  881. package/src/web/player/RemoteCommandController.ts +166 -0
  882. package/android/src/main/java/com/audiobrowser/player/PlaybackProgressUpdateManager.kt +0 -59
  883. package/android/src/main/java/com/audiobrowser/util/RatingFactory.kt +0 -72
  884. package/android/src/main/java/com/audiobrowser/util/ResolvedTrackFactory.kt +0 -36
  885. package/android/src/test/java/com/audiobrowser/player/PlaybackProgressUpdateManagerTest.kt +0 -146
  886. package/ios/CarPlay/Track+CarPlay.swift +0 -63
  887. package/ios/Model/MediaURL.swift +0 -67
  888. package/ios/NowPlayingInfo/NowPlayingInfoCenter.swift +0 -9
  889. package/ios/Player/PlaybackProgressUpdateManager.swift +0 -68
  890. package/ios/Tests/PlaybackProgressUpdateManagerTests.swift +0 -166
  891. package/lib/commonjs/features/rating.js +0 -2
  892. package/lib/commonjs/features/rating.js.map +0 -1
  893. package/lib/commonjs/types/player.js +0 -2
  894. package/lib/commonjs/types/player.js.map +0 -1
  895. package/lib/commonjs/web/SimpleRouter.test.js +0 -174
  896. package/lib/commonjs/web/SimpleRouter.test.js.map +0 -1
  897. package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js +0 -293
  898. package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js.map +0 -1
  899. package/lib/module/features/rating.js +0 -2
  900. package/lib/module/features/rating.js.map +0 -1
  901. package/lib/module/types/player.js +0 -2
  902. package/lib/module/types/player.js.map +0 -1
  903. package/lib/module/web/SimpleRouter.test.js +0 -174
  904. package/lib/module/web/SimpleRouter.test.js.map +0 -1
  905. package/lib/module/web/TrackPlayer/PlaylistPlayer.js +0 -288
  906. package/lib/module/web/TrackPlayer/PlaylistPlayer.js.map +0 -1
  907. package/lib/typescript/src/features/rating.d.ts +0 -14
  908. package/lib/typescript/src/features/rating.d.ts.map +0 -1
  909. package/lib/typescript/src/native.web.d.ts +0 -3
  910. package/lib/typescript/src/native.web.d.ts.map +0 -1
  911. package/lib/typescript/src/types/player.d.ts +0 -64
  912. package/lib/typescript/src/types/player.d.ts.map +0 -1
  913. package/lib/typescript/src/web/NativeAudioBrowser.d.ts +0 -188
  914. package/lib/typescript/src/web/NativeAudioBrowser.d.ts.map +0 -1
  915. package/lib/typescript/src/web/SimpleRouter.d.ts +0 -40
  916. package/lib/typescript/src/web/SimpleRouter.d.ts.map +0 -1
  917. package/lib/typescript/src/web/SimpleRouter.test.d.ts +0 -2
  918. package/lib/typescript/src/web/SimpleRouter.test.d.ts.map +0 -1
  919. package/lib/typescript/src/web/TrackPlayer/Event.d.ts +0 -11
  920. package/lib/typescript/src/web/TrackPlayer/Event.d.ts.map +0 -1
  921. package/lib/typescript/src/web/TrackPlayer/Player.d.ts +0 -59
  922. package/lib/typescript/src/web/TrackPlayer/Player.d.ts.map +0 -1
  923. package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts +0 -39
  924. package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts.map +0 -1
  925. package/lib/typescript/src/web/TrackPlayer/RepeatMode.d.ts +0 -6
  926. package/lib/typescript/src/web/TrackPlayer/RepeatMode.d.ts.map +0 -1
  927. package/lib/typescript/src/web/TrackPlayer/SetupNotCalledError.d.ts +0 -4
  928. package/lib/typescript/src/web/TrackPlayer/SetupNotCalledError.d.ts.map +0 -1
  929. package/lib/typescript/src/web/TrackPlayer/SleepTimer.d.ts +0 -34
  930. package/lib/typescript/src/web/TrackPlayer/SleepTimer.d.ts.map +0 -1
  931. package/lib/typescript/src/web/TrackPlayer/State.d.ts +0 -13
  932. package/lib/typescript/src/web/TrackPlayer/State.d.ts.map +0 -1
  933. package/lib/typescript/src/web/TrackPlayer/index.d.ts +0 -5
  934. package/lib/typescript/src/web/TrackPlayer/index.d.ts.map +0 -1
  935. package/lib/typescript/src/web/browser/BrowserManager.d.ts +0 -154
  936. package/lib/typescript/src/web/browser/BrowserManager.d.ts.map +0 -1
  937. package/lib/typescript/src/web/browser/FavoriteManager.d.ts +0 -42
  938. package/lib/typescript/src/web/browser/FavoriteManager.d.ts.map +0 -1
  939. package/lib/typescript/src/web/browser/NavigationErrorManager.d.ts +0 -40
  940. package/lib/typescript/src/web/browser/NavigationErrorManager.d.ts.map +0 -1
  941. package/lib/typescript/src/web/browser/SearchManager.d.ts +0 -22
  942. package/lib/typescript/src/web/browser/SearchManager.d.ts.map +0 -1
  943. package/lib/typescript/src/web/http/HttpClient.d.ts +0 -25
  944. package/lib/typescript/src/web/http/HttpClient.d.ts.map +0 -1
  945. package/lib/typescript/src/web/http/RequestConfigBuilder.d.ts +0 -98
  946. package/lib/typescript/src/web/http/RequestConfigBuilder.d.ts.map +0 -1
  947. package/lib/typescript/src/web/player/NowPlayingManager.d.ts +0 -34
  948. package/lib/typescript/src/web/player/NowPlayingManager.d.ts.map +0 -1
  949. package/lib/typescript/src/web/player/OptionsManager.d.ts +0 -32
  950. package/lib/typescript/src/web/player/OptionsManager.d.ts.map +0 -1
  951. package/lib/typescript/src/web/util/BrowserPathHelper.d.ts +0 -69
  952. package/lib/typescript/src/web/util/BrowserPathHelper.d.ts.map +0 -1
  953. package/lib/typescript/src/web/util/shuffle.d.ts +0 -8
  954. package/lib/typescript/src/web/util/shuffle.d.ts.map +0 -1
  955. package/nitrogen/generated/android/c++/JAndroidUpdateOptions.hpp +0 -86
  956. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.hpp +0 -147
  957. package/nitrogen/generated/android/c++/JFunc_void_IosOutput.hpp +0 -80
  958. package/nitrogen/generated/android/c++/JFunc_void_RemoteSetRatingEvent.hpp +0 -87
  959. package/nitrogen/generated/android/c++/JHeartRating.hpp +0 -57
  960. package/nitrogen/generated/android/c++/JIosOutputType.hpp +0 -85
  961. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  962. package/nitrogen/generated/android/c++/JPartialSetupPlayerOptions.hpp +0 -96
  963. package/nitrogen/generated/android/c++/JPercentageRating.hpp +0 -57
  964. package/nitrogen/generated/android/c++/JRatingType.hpp +0 -73
  965. package/nitrogen/generated/android/c++/JRemoteSetRatingEvent.hpp +0 -66
  966. package/nitrogen/generated/android/c++/JStarRating.hpp +0 -57
  967. package/nitrogen/generated/android/c++/JUpdateOptions.hpp +0 -105
  968. package/nitrogen/generated/android/c++/JVariant_HeartRating_ThumbsRating_StarRating_PercentageRating.cpp +0 -34
  969. package/nitrogen/generated/android/c++/JVariant_HeartRating_ThumbsRating_StarRating_PercentageRating.hpp +0 -105
  970. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  971. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  972. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidUpdateOptions.kt +0 -50
  973. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IosOutputType.kt +0 -32
  974. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  975. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PartialSetupPlayerOptions.kt +0 -47
  976. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/UpdateOptions.kt +0 -53
  977. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_HeartRating_ThumbsRating_StarRating_PercentageRating.kt +0 -77
  978. package/nitrogen/generated/ios/swift/AndroidUpdateOptions.swift +0 -119
  979. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.swift +0 -68
  980. package/nitrogen/generated/ios/swift/HeartRating.swift +0 -29
  981. package/nitrogen/generated/ios/swift/IosOutputType.swift +0 -76
  982. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  983. package/nitrogen/generated/ios/swift/PartialSetupPlayerOptions.swift +0 -101
  984. package/nitrogen/generated/ios/swift/PercentageRating.swift +0 -29
  985. package/nitrogen/generated/ios/swift/RatingType.swift +0 -60
  986. package/nitrogen/generated/ios/swift/RemoteSetRatingEvent.swift +0 -58
  987. package/nitrogen/generated/ios/swift/StarRating.swift +0 -29
  988. package/nitrogen/generated/ios/swift/ThumbsRating.swift +0 -29
  989. package/nitrogen/generated/ios/swift/UpdateOptions.swift +0 -143
  990. package/nitrogen/generated/ios/swift/Variant_HeartRating_ThumbsRating_StarRating_PercentageRating.swift +0 -20
  991. package/nitrogen/generated/shared/c++/AndroidUpdateOptions.hpp +0 -109
  992. package/nitrogen/generated/shared/c++/IosOutputType.hpp +0 -112
  993. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
  994. package/nitrogen/generated/shared/c++/PartialSetupPlayerOptions.hpp +0 -104
  995. package/nitrogen/generated/shared/c++/RatingType.hpp +0 -96
  996. package/nitrogen/generated/shared/c++/RemoteSetRatingEvent.hpp +0 -94
  997. package/nitrogen/generated/shared/c++/UpdateOptions.hpp +0 -111
  998. package/src/features/rating.ts +0 -17
  999. package/src/types/player.ts +0 -69
  1000. package/src/web/TrackPlayer/PlaylistPlayer.ts +0 -346
@@ -2,61 +2,59 @@ package com.audiobrowser.player
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
- import androidx.media3.common.AudioAttributes
5
+ import android.os.Handler
6
+ import android.os.Looper
7
+ import androidx.core.net.toUri
6
8
  import androidx.media3.common.C
7
- import androidx.media3.common.ForwardingPlayer
8
9
  import androidx.media3.common.HeartRating
9
- import androidx.media3.common.MediaItem
10
- import androidx.media3.common.TrackSelectionParameters
10
+ import androidx.media3.common.MediaMetadata
11
11
  import androidx.media3.database.StandaloneDatabaseProvider
12
12
  import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
13
13
  import androidx.media3.datasource.cache.SimpleCache
14
- import androidx.media3.exoplayer.DefaultRenderersFactory
15
14
  import androidx.media3.exoplayer.ExoPlayer
16
- import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
17
15
  import androidx.media3.session.MediaLibraryService
18
16
  import androidx.media3.session.MediaSession
19
17
  import com.audiobrowser.AudioBrowser
20
18
  import com.audiobrowser.Callbacks
19
+ import com.audiobrowser.browser.displayArtworkSource
20
+ import com.audiobrowser.browser.resolveArtworkUrl
21
+ import com.audiobrowser.browser.unattributedArtworkSource
21
22
  import com.audiobrowser.extension.NumberExt.Companion.toSeconds
22
- import com.margelo.nitro.audiobrowser.ImageContext
23
23
  import com.audiobrowser.model.PlayerSetupOptions
24
24
  import com.audiobrowser.model.PlayerUpdateOptions
25
- import com.audiobrowser.util.AndroidAudioContentTypeFactory
26
25
  import com.audiobrowser.util.CoilBitmapLoader
27
- import com.audiobrowser.util.EqualizerManager
28
26
  import com.audiobrowser.util.NetworkConnectivityMonitor
29
27
  import com.audiobrowser.util.PlayingStateFactory
30
28
  import com.audiobrowser.util.RepeatModeFactory
31
29
  import com.audiobrowser.util.TrackFactory
32
- import com.margelo.nitro.audiobrowser.AndroidPlayerWakeMode
33
30
  import com.margelo.nitro.audiobrowser.AppKilledPlaybackBehavior
34
31
  import com.margelo.nitro.audiobrowser.FavoriteChangedEvent
32
+ import com.margelo.nitro.audiobrowser.GateEvent
33
+ import com.margelo.nitro.audiobrowser.GateReason
34
+ import com.margelo.nitro.audiobrowser.ImageContext
35
+ import com.margelo.nitro.audiobrowser.ImageSource
36
+ import com.margelo.nitro.audiobrowser.NativeGateRequest
35
37
  import com.margelo.nitro.audiobrowser.NowPlayingMetadata
36
- import com.margelo.nitro.audiobrowser.NowPlayingUpdate
37
38
  import com.margelo.nitro.audiobrowser.Playback
38
39
  import com.margelo.nitro.audiobrowser.PlaybackActiveTrackChangedEvent
39
40
  import com.margelo.nitro.audiobrowser.PlaybackError
41
+ import com.margelo.nitro.audiobrowser.PlaybackPlayWhenReadyChangedEvent
40
42
  import com.margelo.nitro.audiobrowser.PlaybackProgressUpdatedEvent
41
43
  import com.margelo.nitro.audiobrowser.PlaybackQueueEndedEvent
42
44
  import com.margelo.nitro.audiobrowser.PlaybackState
43
45
  import com.margelo.nitro.audiobrowser.PlayingState
44
- import com.margelo.nitro.audiobrowser.RatingType
45
- import com.margelo.nitro.audiobrowser.RemoteJumpBackwardEvent
46
- import com.margelo.nitro.audiobrowser.RemoteJumpForwardEvent
47
- import com.margelo.nitro.audiobrowser.RemoteSeekEvent
48
46
  import com.margelo.nitro.audiobrowser.RepeatMode
49
47
  import com.margelo.nitro.audiobrowser.SearchParams
50
- import com.margelo.nitro.audiobrowser.SleepTimer as NitroSleepTimer
51
- import com.margelo.nitro.audiobrowser.SleepTimerEndOfTrack
52
- import com.margelo.nitro.audiobrowser.SleepTimerTime
53
48
  import com.margelo.nitro.audiobrowser.Track
54
- import com.margelo.nitro.core.NullType
55
49
  import java.io.File
50
+ import java.io.IOException
56
51
  import java.util.concurrent.TimeUnit
57
52
  import kotlin.time.Duration.Companion.seconds
58
53
  import kotlinx.coroutines.CompletableDeferred
54
+ import kotlinx.coroutines.Dispatchers
55
+ import kotlinx.coroutines.MainScope
59
56
  import kotlinx.coroutines.TimeoutCancellationException
57
+ import kotlinx.coroutines.withContext
60
58
  import kotlinx.coroutines.withTimeout
61
59
  import timber.log.Timber
62
60
 
@@ -66,25 +64,50 @@ class Player(internal val context: Context) {
66
64
  get() = options.appKilledPlaybackBehavior
67
65
 
68
66
  private var options = PlayerUpdateOptions()
67
+ private val mainHandler = Handler(Looper.getMainLooper())
69
68
  internal var callbacks: Callbacks? = null
70
69
  private lateinit var mediaSession: MediaSession
71
70
  val networkMonitor: NetworkConnectivityMonitor = NetworkConnectivityMonitor(context)
72
- private var equalizerManager: EqualizerManager? = null
71
+ internal val equalizer =
72
+ EqualizerController(onSettingsChanged = { settings -> callbacks?.onEqualizerChanged(settings) })
73
73
  private val mediaSessionCallback = MediaSessionCallback(this)
74
74
  internal val playbackStateStore = PlaybackStateStore(this)
75
- private val sleepTimer =
76
- object : SleepTimer() {
77
- override fun onComplete() {
78
- Timber.d("Sleep timer completed, stopping playback")
79
- stop()
80
- callbacks?.onSleepTimerChanged(NitroSleepTimer.create(NullType.NULL))
81
- }
82
- }
75
+ internal val volumeFader = VolumeFader(getVolume = { volume }, setVolume = { volume = it })
76
+ internal val sleepTimer =
77
+ SleepTimerManager(
78
+ volumeFader,
79
+ pause = { pause() },
80
+ onChanged = { callbacks?.onSleepTimerChanged(it) },
81
+ )
83
82
 
84
83
  lateinit var exoPlayer: ExoPlayer
85
84
  lateinit var forwardingPlayer: androidx.media3.common.Player
86
85
  /** Thread-safe cache of playWhenReady for access from non-main threads (e.g., retry policy) */
87
86
  @Volatile internal var playWhenReadyCache = false
87
+
88
+ /**
89
+ * Whether the current load has ever produced audio — selects between the retry policy's
90
+ * first-connect and recovery budgets. Volatile: read from ExoPlayer's loader thread. Set when
91
+ * playback starts, cleared on media-item transition (a new load starts unproven); retry reloads
92
+ * of the same load keep it.
93
+ */
94
+ @Volatile internal var hasPlayedCache = false
95
+
96
+ /** Last playWhenReady value emitted to JS — see [emitPlayWhenReadyChanged]. */
97
+ private var lastEmittedPlayWhenReady: Boolean? = null
98
+
99
+ /**
100
+ * Emits the playWhenReady change to JS exactly once per value. Two producers exist: the
101
+ * synchronous clear at ENDED in [setPlaybackState] (ordered before the state / queue-ended
102
+ * callbacks, matching iOS) and the async media3 listener (authoritative for changes ExoPlayer
103
+ * makes on its own, e.g. audio focus); the guard deduplicates their overlap.
104
+ */
105
+ internal fun emitPlayWhenReadyChanged(value: Boolean) {
106
+ if (lastEmittedPlayWhenReady == value) return
107
+ lastEmittedPlayWhenReady = value
108
+ callbacks?.onPlaybackPlayWhenReadyChanged(PlaybackPlayWhenReadyChangedEvent(value))
109
+ }
110
+
88
111
  private lateinit var mediaFactory: MediaFactory
89
112
  private lateinit var loadControl: DynamicLoadControl
90
113
  private var automaticBufferManager: AutomaticBufferManager? = null
@@ -95,49 +118,39 @@ class Player(internal val context: Context) {
95
118
  */
96
119
  @Volatile private var pendingNetworkRetry = false
97
120
 
98
- private var _browser: AudioBrowser? = null
99
121
  private var browserRegistered = CompletableDeferred<AudioBrowser>()
100
122
  private var _coilBitmapLoader: CoilBitmapLoader? = null
101
123
 
102
124
  /**
103
- * Coil ImageLoader for SVG pre-rendering in Android Auto browse items.
104
- * Set by Service after creation.
125
+ * Coil ImageLoader for SVG pre-rendering in Android Auto browse items. Set by Service after
126
+ * creation.
105
127
  */
106
128
  var imageLoader: coil3.ImageLoader? = null
107
129
 
130
+ /** Registry that maps content:// tokens to artwork URLs for the browse content provider. */
131
+ val browseArtworkRegistry = com.audiobrowser.browser.BrowseArtworkRegistry()
132
+
108
133
  /**
109
- * Set the CoilBitmapLoader for artwork URL transformation. Called from Service after creation.
134
+ * Set the CoilBitmapLoader for display-time bitmap loading. Called from Service after creation.
110
135
  */
111
136
  var coilBitmapLoader: CoilBitmapLoader?
112
137
  get() = _coilBitmapLoader
113
138
  set(value) {
114
139
  _coilBitmapLoader = value
115
- // If browser is already set, wire up the resolver
116
- _browser?.let { audioBrowser -> wireUpArtworkResolver(audioBrowser, value) }
117
140
  }
118
141
 
119
- var browser: AudioBrowser?
120
- get() = _browser
121
- set(value) {
122
- _browser = value
123
- value?.let { audioBrowser ->
124
- // Wire up artwork URL resolver
125
- // This ensures the resolver is available when onGetChildren resumes
126
- wireUpArtworkResolver(audioBrowser, _coilBitmapLoader)
127
-
128
- // NOTE: We do NOT complete browserRegistered here.
129
- // Configuration (routes/tabs) must be set before Android Auto can browse.
130
- // Call notifyBrowserConfigurationReady() after configuration is set.
131
- }
132
- }
142
+ // NOTE: setting the browser does NOT complete browserRegistered.
143
+ // Configuration (routes/tabs) must be set before Android Auto can browse.
144
+ // Call notifyBrowserConfigurationReady() after configuration is set.
145
+ var browser: AudioBrowser? = null
133
146
 
134
147
  /**
135
- * Notifies that the browser configuration is ready (routes/tabs are configured).
136
- * This should be called after setting the browser AND its configuration.
137
- * Only then will Android Auto be able to browse content.
148
+ * Notifies that the browser configuration is ready (routes/tabs are configured). This should be
149
+ * called after setting the browser AND its configuration. Only then will Android Auto be able to
150
+ * browse content.
138
151
  */
139
152
  fun notifyBrowserConfigurationReady() {
140
- val audioBrowser = _browser ?: return
153
+ val audioBrowser = browser ?: return
141
154
 
142
155
  if (!browserRegistered.isCompleted) {
143
156
  Timber.d("Browser configuration ready - completing deferred")
@@ -155,31 +168,23 @@ class Player(internal val context: Context) {
155
168
  mediaSessionCallback.updateMediaSession(
156
169
  mediaSession,
157
170
  options.capabilities,
158
- options.notificationButtons,
171
+ options.remoteButtonLayout,
159
172
  searchAvailable,
173
+ options.forwardJumpInterval,
174
+ options.backwardJumpInterval,
160
175
  )
161
176
  }
162
177
  }
163
178
  }
164
179
 
165
180
  /**
166
- * Wires up the artwork URL resolver between BrowserManager and CoilBitmapLoader. This enables
167
- * artwork URL transformation during browse-time (Phase 2).
181
+ * Drops a completed browser registration so [awaitBrowser] waits for the replacement instead of
182
+ * handing out the disposed browser. A [CompletableDeferred] completes once, so it would otherwise
183
+ * pin the first browser for the life of the process.
168
184
  */
169
- private fun wireUpArtworkResolver(audioBrowser: AudioBrowser, loader: CoilBitmapLoader?) {
170
- val browserManager = audioBrowser.browserManager
171
- if (loader != null) {
172
- Timber.d("Wiring up artwork URL resolver")
173
- browserManager.artworkUrlResolver = { track, perRouteConfig, imageContext ->
174
- loader.transformArtworkUrlForTrack(track, perRouteConfig, imageContext)
175
- }
176
- // Clear content cache so that content resolved before the resolver was wired up
177
- // will be re-fetched with artwork transformation applied
178
- browserManager.clearContentCache()
179
- Timber.d("Cleared content cache after wiring artwork resolver")
180
- } else {
181
- Timber.d("CoilBitmapLoader not available - artwork URL transformation disabled")
182
- browserManager.artworkUrlResolver = null
185
+ fun forgetBrowserRegistration() {
186
+ if (browserRegistered.isCompleted) {
187
+ browserRegistered = CompletableDeferred()
183
188
  }
184
189
  }
185
190
 
@@ -200,133 +205,16 @@ class Player(internal val context: Context) {
200
205
  throw e
201
206
  }
202
207
 
203
- /**
204
- * ForwardingPlayer that intercepts external player actions and dispatches them to callbacks.
205
- *
206
- * This class blocks all external media item modifications to delegate control to RNAB. These
207
- * overrides prevent media controllers (like Android Auto, notifications) from directly modifying
208
- * the queue, ensuring all queue changes go through the RNAB API for proper state management and
209
- * event handling.
210
- */
211
- private inner class InterceptingPlayer(player: ExoPlayer) : ForwardingPlayer(player) {
212
-
213
- override fun setMediaItems(mediaItems: MutableList<MediaItem>, resetPosition: Boolean) {
214
- return super.setMediaItems(mediaItems, resetPosition)
215
- }
216
-
217
- override fun addMediaItems(mediaItems: MutableList<MediaItem>) {
218
- return super.addMediaItems(mediaItems)
219
- }
220
-
221
- override fun addMediaItems(index: Int, mediaItems: MutableList<MediaItem>) {
222
- return super.addMediaItems(index, mediaItems)
223
- }
224
-
225
- override fun setMediaItems(
226
- mediaItems: MutableList<MediaItem>,
227
- startIndex: Int,
228
- startPositionMs: Long,
229
- ) {
230
- return super.setMediaItems(mediaItems, startIndex, startPositionMs)
231
- }
232
-
233
- override fun setMediaItems(mediaItems: MutableList<MediaItem>) {
234
- return super.setMediaItems(mediaItems)
235
- }
236
-
237
- // Intercept playback controls and dispatch to callbacks or fall back to default behavior
238
- override fun play() {
239
- if (callbacks?.handleRemotePlay() != true) {
240
- super.play()
241
- }
242
- }
243
-
244
- override fun pause() {
245
- if (callbacks?.handleRemotePause() != true) {
246
- super.pause()
247
- }
248
- }
249
-
250
- override fun seekToNext() {
251
- Timber.Forest.d("InterceptingPlayer.seekToNext() called")
252
- if (callbacks?.handleRemoteNext() != true) {
253
- super.seekToNext()
254
- }
255
- }
256
-
257
- override fun seekToNextMediaItem() {
258
- Timber.Forest.d("InterceptingPlayer.seekToNextMediaItem() called")
259
- if (callbacks?.handleRemoteNext() != true) {
260
- super.seekToNextMediaItem()
261
- }
262
- }
263
-
264
- override fun seekToPrevious() {
265
- Timber.Forest.d("InterceptingPlayer.seekToPrevious() called")
266
- if (callbacks?.handleRemotePrevious() != true) {
267
- super.seekToPrevious()
268
- }
269
- }
270
-
271
- override fun seekToPreviousMediaItem() {
272
- Timber.Forest.d("InterceptingPlayer.seekToPreviousMediaItem() called")
273
- if (callbacks?.handleRemotePrevious() != true) {
274
- super.seekToPreviousMediaItem()
275
- }
276
- }
277
-
278
- override fun seekForward() {
279
- Timber.Forest.d("InterceptingPlayer.seekForward() called")
280
- if (
281
- callbacks?.handleRemoteJumpForward(
282
- RemoteJumpForwardEvent(interval = options.forwardJumpInterval)
283
- ) != true
284
- ) {
285
- super.seekForward()
286
- }
287
- }
288
-
289
- override fun seekBack() {
290
- if (
291
- callbacks?.handleRemoteJumpBackward(
292
- RemoteJumpBackwardEvent(interval = options.backwardJumpInterval)
293
- ) != true
294
- ) {
295
- super.seekBack()
296
- }
297
- }
298
-
299
- override fun stop() {
300
- if (callbacks?.handleRemoteStop() != true) {
301
- super.stop()
302
- }
303
- }
304
-
305
- override fun seekTo(mediaItemIndex: Int, positionMs: Long) {
306
- if (
307
- callbacks?.handleRemoteSeek(RemoteSeekEvent(position = positionMs.toDouble() / 1000.0)) !=
308
- true
309
- ) {
310
- super.seekTo(mediaItemIndex, positionMs)
311
- }
312
- }
313
-
314
- override fun seekTo(positionMs: Long) {
315
- if (
316
- callbacks?.handleRemoteSeek(RemoteSeekEvent(position = positionMs.toDouble() / 1000.0)) !=
317
- true
318
- ) {
319
- super.seekTo(positionMs)
320
- }
321
- }
322
- }
323
-
324
208
  private lateinit var playerListener: PlayerListener
325
209
  private var cache: SimpleCache? = null
326
210
 
327
- private val progressUpdateManager: PlaybackProgressUpdateManager by lazy {
328
- PlaybackProgressUpdateManager {
329
- val index = currentIndex ?: return@PlaybackProgressUpdateManager
211
+ private val progressTimer: PlaybackTimer by lazy {
212
+ PlaybackTimer(
213
+ isActive = {
214
+ it == PlaybackState.LOADING || it == PlaybackState.BUFFERING || it == PlaybackState.PLAYING
215
+ }
216
+ ) {
217
+ val index = currentIndex ?: return@PlaybackTimer
330
218
  val event =
331
219
  PlaybackProgressUpdatedEvent(
332
220
  position = position.toSeconds(),
@@ -338,7 +226,12 @@ class Player(internal val context: Context) {
338
226
  }
339
227
  }
340
228
 
229
+ private val intervalTimer: PlaybackTimer by lazy {
230
+ PlaybackTimer(isActive = { it == PlaybackState.PLAYING }) { callbacks?.onPlaybackInterval() }
231
+ }
232
+
341
233
  internal var playingState: PlayingState = PlayingState(false, false)
234
+ private set
342
235
 
343
236
  val currentTrack: Track?
344
237
  get() = exoPlayer.currentMediaItem?.let { TrackFactory.fromMedia3(it) }
@@ -352,9 +245,6 @@ class Player(internal val context: Context) {
352
245
  internal var playbackState: PlaybackState = PlaybackState.NONE
353
246
  private set
354
247
 
355
- /** Current now playing metadata override (null = use track metadata) */
356
- private var nowPlayingOverride: NowPlayingUpdate? = null
357
-
358
248
  fun getPlayback(): Playback {
359
249
  return Playback(playbackState, playbackError)
360
250
  }
@@ -363,9 +253,33 @@ class Player(internal val context: Context) {
363
253
  return playingState
364
254
  }
365
255
 
256
+ /**
257
+ * Re-derives [playingState] from the current `playWhenReady` + [playbackState] and emits
258
+ * `onPlaybackPlayingState` when it changed. The only writer of [playingState] — called from the
259
+ * state machine ([setPlaybackState]) and from [PlayerListener.onPlayWhenReadyChanged], the change
260
+ * points of its two inputs.
261
+ */
262
+ internal fun refreshPlayingState() {
263
+ val newPlayingState = PlayingStateFactory.derive(playWhenReady, playbackState)
264
+ if (newPlayingState == playingState) return
265
+ Timber.d(
266
+ "PlayingState changed: playing=${newPlayingState.playing}, buffering=${newPlayingState.buffering}"
267
+ )
268
+ playingState = newPlayingState
269
+ callbacks?.onPlaybackPlayingState(playingState)
270
+ }
271
+
366
272
  var playWhenReady: Boolean
367
273
  get() = exoPlayer.playWhenReady
368
274
  set(value) {
275
+ // Raising the intent at STATE_ENDED must replay, not silently no-op:
276
+ // ExoPlayer's setPlayWhenReady(true) never restarts an ended player (the
277
+ // ENDED→seekToDefaultPosition replay lives in media3's media-button
278
+ // path, not the Player API). In the setter so every writer gets it —
279
+ // play(), togglePlayback() and JS setPlayWhenReady alike.
280
+ if (value && exoPlayer.playbackState == ExoPlayer.STATE_ENDED) {
281
+ exoPlayer.seekToDefaultPosition()
282
+ }
369
283
  playWhenReadyCache = value
370
284
  exoPlayer.playWhenReady = value
371
285
  }
@@ -398,8 +312,6 @@ class Player(internal val context: Context) {
398
312
  val isPlaying
399
313
  get() = exoPlayer.isPlaying
400
314
 
401
- var ratingType: RatingType = RatingType.NONE
402
-
403
315
  var repeatMode: RepeatMode
404
316
  get() = RepeatModeFactory.fromMedia3(exoPlayer.repeatMode)
405
317
  internal set(value) {
@@ -429,8 +341,14 @@ class Player(internal val context: Context) {
429
341
  .map { index -> TrackFactory.fromMedia3(exoPlayer.getMediaItemAt(index)) }
430
342
  .toTypedArray()
431
343
 
432
- val isLastTrack: Boolean
433
- get() = exoPlayer.currentMediaItemIndex == exoPlayer.mediaItemCount - 1
344
+ /**
345
+ * Whether nothing follows the current item in *playback order* — shuffle- and repeat-aware via
346
+ * media3's own order computation (the linear `currentMediaItemIndex == count - 1` check missed a
347
+ * shuffled queue whose playback order ends on a different linear index). Matches iOS's
348
+ * `isLastInPlaybackOrder`.
349
+ */
350
+ val isLastInPlaybackOrder: Boolean
351
+ get() = exoPlayer.nextMediaItemIndex == C.INDEX_UNSET
434
352
 
435
353
  /**
436
354
  * The source path from which the current queue was expanded (e.g., from a contextual URL). Used
@@ -467,6 +385,12 @@ class Player(internal val context: Context) {
467
385
  fun setup(setupOptions: PlayerSetupOptions) {
468
386
  Timber.Forest.d("Setting up player with new options")
469
387
 
388
+ nowPlaying.enabled = setupOptions.autoUpdateNowPlayingMetadata
389
+ // Wrap the Nitro Func into a plain suspend call so the updater (and its tests) never touch
390
+ // bridge types; the double-await depth is unchanged (invoke -> bridge Promise -> JS promise).
391
+ nowPlaying.formatter =
392
+ setupOptions.nowPlayingMetadataFormatter?.let { f -> { params -> f.invoke(params).await() } }
393
+
470
394
  val isInitialSetup = !::exoPlayer.isInitialized
471
395
 
472
396
  if (!isInitialSetup) {
@@ -476,6 +400,9 @@ class Player(internal val context: Context) {
476
400
  }
477
401
 
478
402
  if (setupOptions.maxCacheSize > 0) {
403
+ // SimpleCache locks its folder — re-setup must release the previous
404
+ // instance first or the constructor throws.
405
+ cache?.release()
479
406
  cache =
480
407
  SimpleCache(
481
408
  File(context.cacheDir, "RNAB"),
@@ -489,94 +416,46 @@ class Player(internal val context: Context) {
489
416
  cache = null
490
417
  }
491
418
 
492
- // Recreate ExoPlayer with new setup options
493
- val renderer = DefaultRenderersFactory(context)
494
- renderer.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER)
495
-
496
- // Create bandwidth meter for adaptive bitrate selection in HLS/DASH
497
- val bandwidthMeter = DefaultBandwidthMeter.Builder(context).build()
498
-
499
- loadControl = run {
500
- val minBuffer = setupOptions.minBuffer.toInt()
501
- val maxBuffer = setupOptions.maxBuffer.toInt()
502
- val playBuffer = setupOptions.playBuffer.toInt()
503
- // When automatic (rebufferBuffer is null), start at playBuffer and let AutomaticBufferManager
504
- // adjust
505
- // When fixed (rebufferBuffer is set), use that value
506
- val playAfterRebuffer = setupOptions.rebufferBuffer?.toInt() ?: playBuffer
507
- val backBuffer = setupOptions.backBuffer.toInt()
508
- val config =
509
- BufferConfig(
510
- minBufferMs = minBuffer,
511
- maxBufferMs = maxBuffer,
512
- bufferForPlaybackMs = playBuffer,
513
- bufferForPlaybackAfterRebufferMs = playAfterRebuffer,
514
- backBufferMs = backBuffer,
515
- )
516
- DynamicLoadControl(initialConfig = config)
517
- }
518
- // Create MediaFactory with reference to browser for media URL transformation
519
- // shouldRetry checks playWhenReady to avoid retrying when paused (e.g., another app took audio
520
- // focus). Uses thread-safe cache since this is called from ExoPlayer's playback thread.
521
- // isOnline and onRetryPending enable network-aware retry acceleration.
522
- mediaFactory =
523
- MediaFactory(
419
+ // One engine generation: load control, media factory, ExoPlayer (pure construction —
420
+ // see buildPlayerEngine; lifecycle and wiring stay here).
421
+ val engine =
422
+ buildPlayerEngine(
524
423
  context,
424
+ setupOptions,
525
425
  cache,
526
- setupOptions.retryPolicy,
527
426
  shouldRetry = { playWhenReadyCache },
528
427
  isOnline = { networkMonitor.getOnline() },
529
- onRetryPending = { isNetworkError ->
428
+ hasPlayed = { hasPlayedCache },
429
+ onRetryPending = { exception, isNetworkError ->
530
430
  // Track pending network retries for acceleration when connectivity returns
531
431
  pendingNetworkRetry = isNetworkError
532
432
  if (isNetworkError) {
533
433
  Timber.d("Network retry pending, will accelerate on connectivity restoration")
534
434
  }
435
+ reportRetryingError(exception)
535
436
  },
536
- transferListener = bandwidthMeter,
537
- ) { url ->
538
- browser?.getMediaRequestConfig(url)
539
- }
540
-
541
- exoPlayer =
542
- ExoPlayer.Builder(context)
543
- .setRenderersFactory(renderer)
544
- .setBandwidthMeter(bandwidthMeter)
545
- .setHandleAudioBecomingNoisy(setupOptions.handleAudioBecomingNoisy)
546
- .setMediaSourceFactory(mediaFactory)
547
- .setWakeMode(
548
- when (setupOptions.wakeMode) {
549
- AndroidPlayerWakeMode.NONE -> C.WAKE_MODE_NONE
550
- AndroidPlayerWakeMode.LOCAL -> C.WAKE_MODE_LOCAL
551
- AndroidPlayerWakeMode.NETWORK -> C.WAKE_MODE_NETWORK
552
- }
553
- )
554
- .setLoadControl(loadControl)
555
- .setName("AudioBrowser")
556
- .build()
557
- exoPlayer.setAudioAttributes(
558
- AudioAttributes.Builder()
559
- .setUsage(C.USAGE_MEDIA)
560
- .setContentType(AndroidAudioContentTypeFactory.toMedia3(setupOptions.audioContentType))
561
- .build(),
562
- true, // handle audio focus
563
- )
437
+ resolveMediaConfig = { url -> browser?.getMediaRequestConfig(url) },
438
+ )
439
+ loadControl = engine.loadControl
440
+ mediaFactory = engine.mediaFactory
441
+ exoPlayer = engine.exoPlayer
564
442
 
565
- // Apply setup-specific options
566
- setupOptions.audioOffload?.let {
567
- val audioOffloadPreferences =
568
- TrackSelectionParameters.AudioOffloadPreferences.Builder()
569
- .setAudioOffloadMode(
570
- TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED
571
- )
572
- .setIsGaplessSupportRequired(it.gaplessSupportRequired)
573
- .setIsSpeedChangeSupportRequired(it.rateChangeSupportRequired)
574
- .build()
575
- exoPlayer.trackSelectionParameters.audioOffloadPreferences = audioOffloadPreferences
443
+ // A rebuilt engine starts paused and fires no change event — sync the
444
+ // eager cache and tell JS, or the stale true sticks (and the emit dedupe
445
+ // would swallow the next real rise).
446
+ if (!isInitialSetup) {
447
+ playWhenReadyCache = false
448
+ emitPlayWhenReadyChanged(false)
576
449
  }
577
450
 
578
451
  // Recreate forwarding player with new ExoPlayer
579
- forwardingPlayer = InterceptingPlayer(exoPlayer)
452
+ forwardingPlayer =
453
+ InterceptingPlayer(
454
+ exoPlayer,
455
+ { callbacks },
456
+ { options },
457
+ keepSessionAliveOnError = setupOptions.keepSessionAliveOnError,
458
+ )
580
459
 
581
460
  if (isInitialSetup) {
582
461
  // Initial setup - create player listener and emit initial state
@@ -585,7 +464,7 @@ class Player(internal val context: Context) {
585
464
  callbacks?.onPlaybackChanged(Playback(PlaybackState.NONE, null))
586
465
 
587
466
  // Initialize equalizer with audio session ID
588
- initializeEqualizer()
467
+ equalizer.initialize(exoPlayer.audioSessionId)
589
468
  } else {
590
469
  // Re-setup - re-add listener and update MediaSession
591
470
  forwardingPlayer.addListener(playerListener)
@@ -620,8 +499,8 @@ class Player(internal val context: Context) {
620
499
  }
621
500
 
622
501
  /**
623
- * Starts observing network connectivity changes and invokes the callback when state changes.
624
- * Also handles accelerating pending network retries when connectivity is restored.
502
+ * Starts observing network connectivity changes and invokes the callback when state changes. Also
503
+ * handles accelerating pending network retries when connectivity is restored.
625
504
  *
626
505
  * @param scope The coroutine scope to use for observation
627
506
  */
@@ -629,6 +508,10 @@ class Player(internal val context: Context) {
629
508
  networkMonitor.observeOnline(scope) { isOnline ->
630
509
  callbacks?.onOnlineChanged(isOnline)
631
510
 
511
+ // Re-render now-playing so the formatter's stall classification (buffering vs offline) tracks
512
+ // connectivity immediately, not only on the next playback transition.
513
+ nowPlaying.render()
514
+
632
515
  // Accelerate pending network retry when connectivity is restored
633
516
  if (isOnline && pendingNetworkRetry) {
634
517
  Timber.d("Network restored with pending retry, triggering immediate retry")
@@ -804,36 +687,18 @@ class Player(internal val context: Context) {
804
687
  * controllers without interrupting playback.
805
688
  */
806
689
  fun setActiveTrackFavorited(favorited: Boolean) {
807
- val index = exoPlayer.currentMediaItemIndex
808
- if (index == C.INDEX_UNSET) return
809
-
810
690
  val currentTrack = this.currentTrack ?: return
811
691
 
812
- // Update native favorites cache (only tracks with src can be favorited)
692
+ // Persist to the native favorites cache regardless of player state — the
693
+ // user's gesture is durable even when there's no active media item to
694
+ // update (queue tear-down, between-track gaps, etc.).
813
695
  currentTrack.src?.let { src -> browser?.browserManager?.updateFavorite(src, favorited) }
814
696
 
697
+ val index = exoPlayer.currentMediaItemIndex
698
+ if (index == C.INDEX_UNSET) return
699
+
815
700
  // Create updated Track with new favorited state
816
- val updatedTrack =
817
- Track(
818
- url = currentTrack.url,
819
- src = currentTrack.src,
820
- artwork = currentTrack.artwork,
821
- artworkSource = currentTrack.artworkSource,
822
- artworkCarPlayTinted = currentTrack.artworkCarPlayTinted,
823
- title = currentTrack.title,
824
- subtitle = currentTrack.subtitle,
825
- artist = currentTrack.artist,
826
- album = currentTrack.album,
827
- description = currentTrack.description,
828
- genre = currentTrack.genre,
829
- duration = currentTrack.duration,
830
- style = currentTrack.style,
831
- childrenStyle = currentTrack.childrenStyle,
832
- favorited = favorited,
833
- groupTitle = currentTrack.groupTitle,
834
- live = currentTrack.live,
835
- imageRow = currentTrack.imageRow,
836
- )
701
+ val updatedTrack = currentTrack.copy(favorited = favorited)
837
702
 
838
703
  // Use buildUpon() on the existing MediaItem to update only the metadata
839
704
  // This preserves internal references and avoids playback interruption
@@ -881,78 +746,152 @@ class Player(internal val context: Context) {
881
746
  // MARK: - Now Playing Metadata
882
747
 
883
748
  /**
884
- * Updates the now playing notification metadata. Pass null to clear overrides and revert to track
885
- * metadata.
749
+ * Now Playing rendering (flash/override/formatter precedence, dedupe, stale guards, artwork
750
+ * keying) lives in [NowPlayingUpdater]; this object is its platform surface — current playback
751
+ * reads plus MediaItem stamping via [replaceMediaItem], and artwork resolution via the browser.
886
752
  */
887
- fun updateNowPlaying(update: NowPlayingUpdate?) {
888
- nowPlayingOverride = update
889
- applyNowPlayingMetadata()
890
- }
753
+ val nowPlaying: NowPlayingUpdater =
754
+ NowPlayingUpdater(
755
+ object : NowPlayingSurface {
756
+ override val currentIndex
757
+ get() = this@Player.currentIndex
758
+
759
+ override val currentTrack
760
+ get() = this@Player.currentTrack
761
+
762
+ override val playbackState
763
+ get() = this@Player.playbackState
764
+
765
+ override val playbackError
766
+ get() = this@Player.playbackError
767
+
768
+ override val playWhenReady
769
+ get() = exoPlayer.playWhenReady
770
+
771
+ override val isRebuffering
772
+ get() = loadControl.isRebuffering
773
+
774
+ override val isOnline
775
+ get() = networkMonitor.getOnline()
776
+
777
+ override val hasNowPlayingArtworkConfig
778
+ get() = browser?.browserManager?.config?.nowPlayingArtwork != null
779
+
780
+ override fun stampFields(
781
+ index: Int,
782
+ track: Track,
783
+ title: String?,
784
+ secondaryLine: String?,
785
+ album: String?,
786
+ ) =
787
+ restampMediaItem(index, track) {
788
+ setTitle(title)
789
+ .setDisplayTitle(title)
790
+ .setArtist(secondaryLine)
791
+ // Android Auto reads DISPLAY_SUBTITLE (not ARTIST) once DISPLAY_TITLE is set, so
792
+ // mirror the line into `subtitle`; `artist` still drives the lock screen /
793
+ // notification.
794
+ .setSubtitle(secondaryLine)
795
+ .setAlbumTitle(album)
796
+ }
797
+
798
+ override fun stampArtwork(index: Int, track: Track, uri: String) =
799
+ restampMediaItem(index, track) { setArtworkUri(uri.toUri()) }
800
+
801
+ override suspend fun resolveNowPlayingArtwork(track: Track, sizePx: Double): String? {
802
+ val browserManager = browser?.browserManager ?: return null
803
+ val config = browserManager.config.nowPlayingArtwork ?: return null
804
+ val resolved =
805
+ try {
806
+ browserManager.resolveArtworkUrl(track, config, ImageContext(sizePx, sizePx))
807
+ } catch (e: Exception) {
808
+ Timber.e(e, "Failed to resolve now-playing artwork for track id=${track.id}")
809
+ null
810
+ }
811
+ val uri = resolved?.uri?.takeIf { it.isNotEmpty() } ?: return null
812
+ // Remember how this URI was produced: Media3 hands it back to the bitmap loader, which
813
+ // re-resolves Track-first (with the nowPlayingArtwork kind, not the global artwork
814
+ // config).
815
+ browserManager.artworkResolutions.register(uri, track, config)
816
+ return uri
817
+ }
891
818
 
892
- /** Gets the current now playing metadata (override if set, else track metadata). */
893
- fun getNowPlaying(): NowPlayingMetadata? {
894
- val track = currentTrack ?: return null
895
- val override = nowPlayingOverride
896
-
897
- return NowPlayingMetadata(
898
- elapsedTime = null,
899
- title = override?.title ?: track.title,
900
- album = track.album,
901
- artist = override?.artist ?: track.artist,
902
- duration = track.duration,
903
- artwork = track.artwork,
904
- description = track.description,
905
- mediaId = track.src ?: track.url,
906
- genre = track.genre,
907
- rating = null, // TODO: map track.favorited to rating if needed
819
+ override fun emitNowPlayingChanged(metadata: NowPlayingMetadata) {
820
+ callbacks?.onNowPlayingChanged(metadata)
821
+ }
822
+ },
823
+ MainScope(),
908
824
  )
909
- }
910
825
 
911
- /**
912
- * Clears the now playing override when track changes. Called from
913
- * PlayerListener.onMediaItemTransition.
914
- */
915
- internal fun clearNowPlayingOverride() {
916
- nowPlayingOverride = null
826
+ /** Republishes the item at [index] with mutated metadata, preserving uri and Track tag. */
827
+ private fun restampMediaItem(
828
+ index: Int,
829
+ track: Track,
830
+ mutate: MediaMetadata.Builder.() -> MediaMetadata.Builder,
831
+ ) {
832
+ val currentMediaItem = exoPlayer.getMediaItemAt(index)
833
+ val updatedMediaItem =
834
+ currentMediaItem
835
+ .buildUpon()
836
+ .setUri(currentMediaItem.localConfiguration?.uri)
837
+ .setMediaMetadata(currentMediaItem.mediaMetadata.buildUpon().mutate().build())
838
+ .setTag(track)
839
+ .build()
840
+ exoPlayer.replaceMediaItem(index, updatedMediaItem)
917
841
  }
918
842
 
919
843
  /**
920
844
  * Resets the retry timer when track changes. Called from PlayerListener.onMediaItemTransition.
921
845
  */
846
+ /**
847
+ * A restart from a terminal error begins a new load of the same track: fresh retry budgets and
848
+ * unproven playback, so the user's tap yields a visible first-connect retry window instead of a
849
+ * single silent attempt — the same behavior as re-selecting the track (ADR 0004). Hooked off the
850
+ * player error clearing: every recovery path (retry(), play(), the transport controls' idle-error
851
+ * recovery) goes through exoPlayer.prepare(), which clears the error.
852
+ */
853
+ internal fun resetForNewLoad() {
854
+ resetRetryTimer()
855
+ hasPlayedCache = false
856
+ }
857
+
922
858
  internal fun resetRetryTimer() {
923
859
  mediaFactory.resetRetryTimer()
924
860
  }
925
861
 
926
862
  /**
927
- * Applies the current now playing metadata to the media notification. Uses the override if set,
928
- * otherwise uses track metadata.
863
+ * Finds the queue Track whose published artwork URI matches [uri], for display-time artwork
864
+ * resolution of app-supplied queue tracks that never went through browse (so they are not in the
865
+ * resolution registry). Must run on the main thread (ExoPlayer access).
929
866
  */
930
- private fun applyNowPlayingMetadata() {
931
- val index = currentIndex ?: return
932
- val track = currentTrack ?: return
933
- val override = nowPlayingOverride
934
-
935
- val currentMediaItem = exoPlayer.getMediaItemAt(index)
936
- val updatedMetadata =
937
- currentMediaItem.mediaMetadata
938
- .buildUpon()
939
- .setTitle(override?.title ?: track.title)
940
- .setDisplayTitle(override?.title ?: track.title)
941
- .setArtist(override?.artist ?: track.artist)
942
- .build()
943
-
944
- val updatedMediaItem =
945
- currentMediaItem
946
- .buildUpon()
947
- .setUri(currentMediaItem.localConfiguration?.uri)
948
- .setMediaMetadata(updatedMetadata)
949
- .setTag(track)
950
- .build()
951
-
952
- exoPlayer.replaceMediaItem(index, updatedMediaItem)
867
+ private fun findQueueTrackByArtworkUri(uri: String): Track? {
868
+ for (i in 0 until exoPlayer.mediaItemCount) {
869
+ val item = exoPlayer.getMediaItemAt(i)
870
+ if (item.mediaMetadata.artworkUri?.toString() == uri) {
871
+ return item.localConfiguration?.tag as? Track
872
+ }
873
+ }
874
+ return null
875
+ }
953
876
 
954
- // Notify JS of the now playing metadata change
955
- getNowPlaying()?.let { callbacks?.onNowPlayingChanged(it) }
877
+ /**
878
+ * Track-first display-time artwork resolution for [CoilBitmapLoader]: registry hit
879
+ * (browse/now-playing-resolved URIs) → queue-tag lookup (app-supplied tracks with raw artwork) →
880
+ * header-only fallback for unattributable URIs (registry eviction / process-death restore). Null
881
+ * means "fetch the URI as-is".
882
+ */
883
+ suspend fun resolveDisplayArtwork(uri: String, sizeHintPixels: Int?): ImageSource? {
884
+ val browserManager = browser?.browserManager ?: return null
885
+ val imageContext =
886
+ sizeHintPixels?.takeIf { it > 0 }?.let { ImageContext(it.toDouble(), it.toDouble()) }
887
+ browserManager.displayArtworkSource(uri, imageContext)?.let {
888
+ return it
889
+ }
890
+ withContext(Dispatchers.Main) { findQueueTrackByArtworkUri(uri) }
891
+ ?.let { track ->
892
+ return browserManager.resolveArtworkUrl(track, null, imageContext)
893
+ }
894
+ return browserManager.unattributedArtworkSource(uri)
956
895
  }
957
896
 
958
897
  /**
@@ -975,12 +914,25 @@ class Player(internal val context: Context) {
975
914
  }
976
915
 
977
916
  fun play() {
978
- exoPlayer.play()
917
+ // Through the setter so the STATE_ENDED replay handling applies
918
+ // (ExoPlayer.play() is only setPlayWhenReady(true)).
919
+ playWhenReady = true
979
920
  if (currentTrack != null) {
921
+ // No-op unless the player is STATE_IDLE (ExoPlayer.prepare early-returns
922
+ // otherwise), so this only reconnects after a stop() or error and never
923
+ // re-buffers a healthy stream. Reconnecting is also how live streams
924
+ // rejoin the live edge on resume.
980
925
  exoPlayer.prepare()
981
926
  }
982
927
  }
983
928
 
929
+ /** Jump to the live edge (default position) of a live item; no-op otherwise. */
930
+ fun seekToLiveEdge() {
931
+ if (exoPlayer.isCurrentMediaItemLive) {
932
+ exoPlayer.seekToDefaultPosition()
933
+ }
934
+ }
935
+
984
936
  /**
985
937
  * Executes a search and plays the results. Used for voice commands with structured search
986
938
  * parameters.
@@ -990,7 +942,20 @@ class Player(internal val context: Context) {
990
942
  */
991
943
  suspend fun playFromSearch(params: SearchParams): Boolean {
992
944
  return try {
993
- val browserManager = awaitBrowser().browserManager
945
+ val audioBrowser = awaitBrowser()
946
+ // A gate blocks external-surface "play" search too — otherwise voice search is a way around
947
+ // the gate (mirrors the iOS play-media intent guard). Resolved per request; a gated decision
948
+ // fires onGate(search) and refuses.
949
+ val gateOutcome =
950
+ audioBrowser.gateDecision(
951
+ NativeGateRequest(reason = GateReason.SEARCH, path = null, search = params)
952
+ )
953
+ if (gateOutcome.gated) {
954
+ Timber.i("playFromSearch refused — request is gated")
955
+ audioBrowser.onGate(GateEvent(GateReason.SEARCH))
956
+ return false
957
+ }
958
+ val browserManager = audioBrowser.browserManager
994
959
 
995
960
  Timber.d(
996
961
  "Executing voice search: mode=${params.mode}, query='${params.query}', artist='${params.artist}', album='${params.album}'"
@@ -1019,7 +984,10 @@ class Player(internal val context: Context) {
1019
984
  }
1020
985
 
1021
986
  fun pause() {
1022
- exoPlayer.pause()
987
+ // Through the setter so the eager playWhenReadyCache write holds for the
988
+ // drop direction too — the retry policy reads it off-main before
989
+ // ExoPlayer's listener round-trip syncs it.
990
+ playWhenReady = false
1023
991
  }
1024
992
 
1025
993
  fun togglePlayback() {
@@ -1036,8 +1004,11 @@ class Player(internal val context: Context) {
1036
1004
  * required for playback.
1037
1005
  */
1038
1006
  fun stop() {
1039
- playbackState = PlaybackState.STOPPED
1040
- exoPlayer.playWhenReady = false
1007
+ // State first so the machine's STOPPED guard suppresses the PAUSED transition
1008
+ // from the playWhenReady drop below; through setPlaybackState so the change
1009
+ // is emitted (direct field assignment bypasses events — see its docstring).
1010
+ setPlaybackState(PlaybackState.STOPPED)
1011
+ playWhenReady = false
1041
1012
  exoPlayer.stop()
1042
1013
  }
1043
1014
 
@@ -1051,7 +1022,15 @@ class Player(internal val context: Context) {
1051
1022
  * use [pause].
1052
1023
  */
1053
1024
  fun destroy() {
1025
+ // Cancel without a final save: the loop would tick against the released
1026
+ // player and persist position 0 over the real resumption position.
1027
+ playbackStateStore.cancelPeriodicSave()
1028
+ // An armed timer's Runnable would outlive the release and fire pause() into the
1029
+ // released player. Cleared here, while a mid-ramp fade can still restore volume.
1030
+ sleepTimer.clear()
1054
1031
  stop()
1032
+ nowPlaying.destroy()
1033
+ mediaSessionCallback.destroy()
1055
1034
  forwardingPlayer.removeListener(playerListener)
1056
1035
  automaticBufferManager?.detach()
1057
1036
  automaticBufferManager = null
@@ -1059,8 +1038,7 @@ class Player(internal val context: Context) {
1059
1038
  cache?.release()
1060
1039
  cache = null
1061
1040
  networkMonitor.destroy()
1062
- equalizerManager?.release()
1063
- equalizerManager = null
1041
+ equalizer.release()
1064
1042
  }
1065
1043
 
1066
1044
  fun seekTo(duration: Long, unit: TimeUnit) {
@@ -1073,6 +1051,57 @@ class Player(internal val context: Context) {
1073
1051
  exoPlayer.seekTo(positionMs)
1074
1052
  }
1075
1053
 
1054
+ /**
1055
+ * Invalidates in-flight [reportRetryingError] posts: bumped on track change so a report
1056
+ * classified for the outgoing track can't land on the incoming one's spinner. Written on main,
1057
+ * read on ExoPlayer's loader thread.
1058
+ */
1059
+ @Volatile private var retryingErrorGeneration = 0
1060
+
1061
+ /**
1062
+ * Surfaces a load failure the retry policy is still working on: attached to the current
1063
+ * (non-terminal) state via [Callbacks.onPlaybackChanged], so UIs can show the cause over the
1064
+ * spinner. Not an ERROR transition, and not a [Callbacks.onPlaybackError] — that event stays
1065
+ * reserved for terminal errors. Called from ExoPlayer's loader thread, hence the main hop.
1066
+ */
1067
+ private fun reportRetryingError(exception: IOException) {
1068
+ val error = PlaybackErrorClassifier.retryingLoadError(exception, networkMonitor.getOnline())
1069
+ val generation = retryingErrorGeneration
1070
+ mainHandler.post {
1071
+ // The track changed while this post was in flight — the error belongs to the old one.
1072
+ if (generation != retryingErrorGeneration) return@post
1073
+ // Only attach while the failure is what the listener is looking at. During PLAYING/READY
1074
+ // buffered audio is still audibly fine, and ExoPlayer's in-place retry can succeed without
1075
+ // any state change — an advisory attached there would never clear.
1076
+ if (
1077
+ playbackState != PlaybackState.LOADING &&
1078
+ playbackState != PlaybackState.BUFFERING &&
1079
+ playbackState != PlaybackState.PAUSED
1080
+ ) {
1081
+ return@post
1082
+ }
1083
+ // Each failed attempt re-reports; identical repeats add nothing (and would
1084
+ // re-render now-playing every backoff tick).
1085
+ if (playbackError == error) return@post
1086
+ playbackError = error
1087
+ callbacks?.onPlaybackChanged(Playback(playbackState, error))
1088
+ nowPlaying.render()
1089
+ }
1090
+ }
1091
+
1092
+ /**
1093
+ * Drops a retrying error whose track is gone. The new track may keep the state on
1094
+ * LOADING/BUFFERING — no state-change emit — so the clear is emitted here, or the old track's
1095
+ * error would linger over the new one's spinner.
1096
+ */
1097
+ internal fun clearRetryingError() {
1098
+ retryingErrorGeneration++
1099
+ if (playbackError?.retrying != true) return
1100
+ playbackError = null
1101
+ callbacks?.onPlaybackChanged(Playback(playbackState, null))
1102
+ nowPlaying.render()
1103
+ }
1104
+
1076
1105
  /**
1077
1106
  * Updates the player state and emits a state change event if the state has changed. Only emits an
1078
1107
  * event if the new state differs from the current state.
@@ -1088,39 +1117,78 @@ class Player(internal val context: Context) {
1088
1117
  val oldState = playbackState
1089
1118
  playbackState = state
1090
1119
 
1120
+ // Audio rendered: this load has proven itself — retries from here on get
1121
+ // the retry policy's full recovery budget, not the short first-connect one.
1122
+ if (state == PlaybackState.PLAYING) {
1123
+ hasPlayedCache = true
1124
+ }
1125
+
1126
+ // A natural end exhausts the play intent — nothing is left to play. Keeping
1127
+ // playWhenReady true inverted the play/pause toggle, held audio focus forever
1128
+ // (ExoPlayer abandons it only at IDLE or on the intent dropping), and kept the
1129
+ // periodic position save running. Cleared and emitted before the state /
1130
+ // queue-ended callbacks so JS observes the native order (intent → state →
1131
+ // queueEnded); the media3 listener's later echo is deduplicated by
1132
+ // emitPlayWhenReadyChanged. The state machine's ENDED guard keeps the drop
1133
+ // from re-reporting the state as PAUSED.
1134
+ if (state == PlaybackState.ENDED && playWhenReady) {
1135
+ playWhenReady = false
1136
+ emitPlayWhenReadyChanged(false)
1137
+ }
1138
+
1139
+ // The last track fires no AUTO media-item transition, so an armed
1140
+ // end-of-track sleep timer would stay set forever and pause the next
1141
+ // track played instead.
1142
+ if (state == PlaybackState.ENDED) {
1143
+ sleepTimer.onTrackEnd()
1144
+ }
1145
+
1091
1146
  // Clear error when transitioning away from error state
1092
1147
  if (oldState == PlaybackState.ERROR) {
1093
1148
  playbackError = null
1094
1149
  callbacks?.onPlaybackError(null)
1095
1150
  }
1096
1151
 
1152
+ // A retrying error rides along through LOADING/BUFFERING/PAUSED; a state that
1153
+ // proves recovery (READY/PLAYING — data flowed) or abandons the attempt clears
1154
+ // it, before the Playback below is built so the emitted event is already clean.
1155
+ if (
1156
+ playbackError?.retrying == true &&
1157
+ state != PlaybackState.LOADING &&
1158
+ state != PlaybackState.BUFFERING &&
1159
+ state != PlaybackState.PAUSED
1160
+ ) {
1161
+ playbackError = null
1162
+ }
1163
+
1097
1164
  val playback = Playback(state, playbackError)
1098
1165
  callbacks?.onPlaybackChanged(playback)
1099
1166
 
1100
- // Emit queue ended event when playback ends on the last track
1167
+ // Re-render the now-playing on every state change: the metadata formatter receives
1168
+ // `playbackState` (plus isRebuffering / isOnline / error), so any transition can change its
1169
+ // output — the live song while paused, a "Reconnecting…" line on a rebuffer, an offline/error
1170
+ // line. The publish-dedupe in applyNowPlayingFields drops redundant updates, so re-running
1171
+ // through the rapid startup sequence (none→loading→buffering→ready→playing) is cheap.
1172
+ nowPlaying.render()
1173
+
1174
+ // Emit queue ended event when playback ends on the last track. Repeat modes never
1175
+ // conceptually end the queue (matches web's endsQueue()): an ENDED that surfaces while
1176
+ // repeating (e.g. a seek to the end) must not read as "playlist over".
1101
1177
  // This coupling ensures queue ended events are always triggered consistently with state
1102
1178
  // changes
1103
- if (state == PlaybackState.ENDED && isLastTrack) {
1179
+ if (state == PlaybackState.ENDED && isLastInPlaybackOrder && repeatMode == RepeatMode.OFF) {
1104
1180
  currentIndex?.let { index ->
1105
1181
  val event =
1106
1182
  PlaybackQueueEndedEvent(track = index.toDouble(), position = position.toSeconds())
1107
1183
  callbacks?.onPlaybackQueueEnded(event)
1108
1184
  }
1109
- // Reset saved position to 0 so resumption starts from beginning (only when not repeating)
1110
- if (repeatMode == RepeatMode.OFF) {
1111
- playbackStateStore.savePositionZero()
1112
- }
1185
+ // Reset saved position to 0 so resumption starts from beginning
1186
+ playbackStateStore.savePositionZero()
1113
1187
  }
1114
1188
 
1115
- progressUpdateManager.onPlaybackStateChanged(state)
1116
- val newPlayingState = PlayingStateFactory.derive(playWhenReady, state)
1117
- if (newPlayingState != playingState) {
1118
- Timber.d(
1119
- "PlayingState changed: playing=${newPlayingState.playing}, buffering=${newPlayingState.buffering}"
1120
- )
1121
- playingState = newPlayingState
1122
- callbacks?.onPlaybackPlayingState(playingState)
1123
- }
1189
+ progressTimer.onPlaybackStateChanged(state)
1190
+ intervalTimer.onPlaybackStateChanged(state)
1191
+ refreshPlayingState()
1124
1192
  }
1125
1193
  }
1126
1194
 
@@ -1130,7 +1198,11 @@ class Player(internal val context: Context) {
1130
1198
  * @param interval The interval in seconds, or null to disable progress updates
1131
1199
  */
1132
1200
  fun setProgressUpdateInterval(interval: Double?) {
1133
- progressUpdateManager.setUpdateInterval(interval)
1201
+ progressTimer.setInterval(interval)
1202
+ }
1203
+
1204
+ fun setPlaybackIntervalEnabled(enabled: Boolean) {
1205
+ intervalTimer.setInterval(if (enabled) 1.0 else null)
1134
1206
  }
1135
1207
 
1136
1208
  /**
@@ -1149,8 +1221,6 @@ class Player(internal val context: Context) {
1149
1221
 
1150
1222
  // Check what changed
1151
1223
  val skipSilenceChanged = previousOptions.skipSilence != options.skipSilence
1152
- val ratingTypeChanged = previousOptions.ratingType != options.ratingType
1153
- val shuffleChanged = previousOptions.shuffle != options.shuffle
1154
1224
  val progressUpdateEventIntervalChanged =
1155
1225
  previousOptions.progressUpdateEventInterval != options.progressUpdateEventInterval
1156
1226
  val forwardJumpIntervalChanged =
@@ -1158,20 +1228,18 @@ class Player(internal val context: Context) {
1158
1228
  val backwardJumpIntervalChanged =
1159
1229
  previousOptions.backwardJumpInterval != options.backwardJumpInterval
1160
1230
  val capabilitiesChanged = previousOptions.capabilities != options.capabilities
1161
- val notificationButtonsChanged =
1162
- previousOptions.notificationButtons != options.notificationButtons
1231
+ val remoteButtonLayoutChanged =
1232
+ !previousOptions.remoteButtonLayout.sameAs(options.remoteButtonLayout)
1163
1233
  val appKilledPlaybackBehaviorChanged =
1164
1234
  previousOptions.appKilledPlaybackBehavior != options.appKilledPlaybackBehavior
1165
1235
 
1166
1236
  val hasChanged =
1167
1237
  skipSilenceChanged ||
1168
- ratingTypeChanged ||
1169
- shuffleChanged ||
1170
1238
  progressUpdateEventIntervalChanged ||
1171
1239
  forwardJumpIntervalChanged ||
1172
1240
  backwardJumpIntervalChanged ||
1173
1241
  capabilitiesChanged ||
1174
- notificationButtonsChanged ||
1242
+ remoteButtonLayoutChanged ||
1175
1243
  appKilledPlaybackBehaviorChanged
1176
1244
 
1177
1245
  // Apply only changed properties
@@ -1179,25 +1247,32 @@ class Player(internal val context: Context) {
1179
1247
  skipSilence = options.skipSilence
1180
1248
  }
1181
1249
 
1182
- if (ratingTypeChanged) {
1183
- options.ratingType?.let { ratingType = it }
1184
- }
1185
-
1186
- if (shuffleChanged) {
1187
- shuffleMode = options.shuffle
1250
+ if (capabilitiesChanged) {
1251
+ // The `favorite` capability is the single favoriting switch — propagate
1252
+ // its match mode to the browser so it can hydrate row hearts.
1253
+ browser?.browserManager?.setFavoriteMatch(options.capabilities.favoriteMatch)
1188
1254
  }
1189
1255
 
1190
1256
  if (progressUpdateEventIntervalChanged) {
1191
1257
  setProgressUpdateInterval(options.progressUpdateEventInterval)
1192
1258
  }
1193
1259
 
1194
- if (capabilitiesChanged || notificationButtonsChanged) {
1260
+ // The jump intervals pick the button icons (ICON_SKIP_BACK_15 and friends), so a changed
1261
+ // interval has to rebuild the layout even when capabilities and placement are untouched.
1262
+ if (
1263
+ capabilitiesChanged ||
1264
+ remoteButtonLayoutChanged ||
1265
+ forwardJumpIntervalChanged ||
1266
+ backwardJumpIntervalChanged
1267
+ ) {
1195
1268
  val searchAvailable = browser?.browserManager?.config?.hasSearch ?: false
1196
1269
  mediaSessionCallback.updateMediaSession(
1197
1270
  mediaSession,
1198
1271
  options.capabilities,
1199
- options.notificationButtons,
1272
+ options.remoteButtonLayout,
1200
1273
  searchAvailable,
1274
+ options.forwardJumpInterval,
1275
+ options.backwardJumpInterval,
1201
1276
  )
1202
1277
  }
1203
1278
 
@@ -1241,187 +1316,6 @@ class Player(internal val context: Context) {
1241
1316
  return networkMonitor.getOnline()
1242
1317
  }
1243
1318
 
1244
- /** Initializes the equalizer with the player's audio session ID. */
1245
- private fun initializeEqualizer() {
1246
- val audioSessionId = exoPlayer.audioSessionId
1247
- if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
1248
- Timber.d("Skipping equalizer init - no audio session yet, will init on first playback")
1249
- return
1250
- }
1251
-
1252
- try {
1253
- equalizerManager =
1254
- EqualizerManager(audioSessionId).apply {
1255
- setOnSettingsChanged { settings -> callbacks?.onEqualizerChanged(settings) }
1256
- }
1257
- Timber.d("Equalizer initialized with session ID: $audioSessionId")
1258
- } catch (e: Exception) {
1259
- Timber.e(e, "Failed to initialize equalizer")
1260
- equalizerManager = null
1261
- }
1262
- }
1263
-
1264
- /**
1265
- * Reinitializes the equalizer when the audio session ID changes. Preserves current settings
1266
- * (enabled state, preset, or custom levels). Also handles first-time initialization when the
1267
- * equalizer was skipped at startup due to unset audio session ID.
1268
- */
1269
- internal fun reinitializeEqualizer(newAudioSessionId: Int) {
1270
- val oldManager = equalizerManager
1271
-
1272
- // First-time initialization (skipped at startup because session was unset)
1273
- if (oldManager == null) {
1274
- Timber.d("First-time equalizer initialization with session ID: $newAudioSessionId")
1275
- try {
1276
- equalizerManager =
1277
- EqualizerManager(newAudioSessionId).apply {
1278
- setOnSettingsChanged { settings -> callbacks?.onEqualizerChanged(settings) }
1279
- }
1280
- Timber.d("Equalizer initialized with session ID: $newAudioSessionId")
1281
- } catch (e: Exception) {
1282
- Timber.e(e, "Failed to initialize equalizer")
1283
- equalizerManager = null
1284
- }
1285
- return
1286
- }
1287
-
1288
- // Same session ID, no need to reinitialize
1289
- if (oldManager.audioSessionId == newAudioSessionId) {
1290
- return
1291
- }
1292
-
1293
- try {
1294
- // Capture current settings before releasing old equalizer
1295
- val currentSettings = oldManager.getSettings()
1296
-
1297
- // Release old equalizer
1298
- oldManager.release()
1299
-
1300
- // Create new equalizer with new session ID
1301
- equalizerManager =
1302
- EqualizerManager(newAudioSessionId).apply {
1303
- setOnSettingsChanged { settings -> callbacks?.onEqualizerChanged(settings) }
1304
- }
1305
-
1306
- // Restore previous settings if available
1307
- currentSettings?.let { settings ->
1308
- if (settings.activePreset != null) {
1309
- // Restore preset
1310
- equalizerManager?.setPreset(settings.activePreset)
1311
- } else if (settings.enabled) {
1312
- // Restore custom levels
1313
- equalizerManager?.setLevels(settings.bandLevels)
1314
- }
1315
- // Restore enabled state
1316
- equalizerManager?.setEnabled(settings.enabled)
1317
- }
1318
-
1319
- Timber.d("Equalizer reinitialized for new session ID: $newAudioSessionId")
1320
- } catch (e: Exception) {
1321
- Timber.e(e, "Failed to reinitialize equalizer")
1322
- equalizerManager = null
1323
- }
1324
- }
1325
-
1326
- /**
1327
- * Gets the current equalizer settings.
1328
- *
1329
- * @return Current equalizer settings or null if not available
1330
- */
1331
- fun getEqualizerSettings(): com.margelo.nitro.audiobrowser.EqualizerSettings? {
1332
- return equalizerManager?.getSettings()
1333
- }
1334
-
1335
- /**
1336
- * Enables or disables the equalizer.
1337
- *
1338
- * @param enabled true to enable, false to disable
1339
- */
1340
- fun setEqualizerEnabled(enabled: Boolean) {
1341
- equalizerManager?.setEnabled(enabled)
1342
- }
1343
-
1344
- /**
1345
- * Applies a preset to the equalizer.
1346
- *
1347
- * @param preset Name of the preset to apply
1348
- */
1349
- fun setEqualizerPreset(preset: String) {
1350
- equalizerManager?.setPreset(preset)
1351
- }
1352
-
1353
- /**
1354
- * Sets custom band levels for the equalizer.
1355
- *
1356
- * @param levels Array of level values in millibels for each band
1357
- */
1358
- fun setEqualizerLevels(levels: DoubleArray) {
1359
- equalizerManager?.setLevels(levels)
1360
- }
1361
-
1362
- // MARK: - Sleep Timer
1363
-
1364
- /**
1365
- * Gets the current sleep timer state.
1366
- *
1367
- * @return Sleep timer state or null if no timer is active
1368
- */
1369
- fun getSleepTimer(): NitroSleepTimer {
1370
- return when {
1371
- sleepTimer.time != null -> {
1372
- NitroSleepTimer.create(SleepTimerTime(sleepTimer.time!!))
1373
- }
1374
- sleepTimer.sleepWhenPlayedToEnd -> {
1375
- NitroSleepTimer.create(SleepTimerEndOfTrack(true))
1376
- }
1377
- else -> {
1378
- NitroSleepTimer.create(NullType.NULL)
1379
- }
1380
- }
1381
- }
1382
-
1383
- /**
1384
- * Sets a sleep timer to stop playback after the specified duration.
1385
- *
1386
- * @param seconds Number of seconds until playback stops
1387
- */
1388
- fun setSleepTimer(seconds: Double) {
1389
- sleepTimer.sleepAfter(seconds)
1390
- callbacks?.onSleepTimerChanged(getSleepTimer())
1391
- }
1392
-
1393
- /** Sets a sleep timer to stop playback when the current track finishes playing. */
1394
- fun setSleepTimerToEndOfTrack() {
1395
- sleepTimer.sleepWhenPlayedToEnd()
1396
- callbacks?.onSleepTimerChanged(getSleepTimer())
1397
- }
1398
-
1399
- /**
1400
- * Clears the active sleep timer.
1401
- *
1402
- * @return true if a timer was cleared, false if no timer was active
1403
- */
1404
- fun clearSleepTimer(): Boolean {
1405
- val wasRunning = sleepTimer.clear()
1406
- if (wasRunning) {
1407
- callbacks?.onSleepTimerChanged(NitroSleepTimer.create(NullType.NULL))
1408
- }
1409
- return wasRunning
1410
- }
1411
-
1412
- /**
1413
- * Checks if the sleep timer is set to end on track completion and stops playback if so. Called
1414
- * when a track naturally finishes playing.
1415
- */
1416
- internal fun checkSleepTimerOnTrackEnd() {
1417
- if (sleepTimer.sleepWhenPlayedToEnd) {
1418
- Timber.d("Sleep timer triggered on track end, stopping playback")
1419
- sleepTimer.clear()
1420
- stop()
1421
- callbacks?.onSleepTimerChanged(NitroSleepTimer.create(NullType.NULL))
1422
- }
1423
- }
1424
-
1425
1319
  /**
1426
1320
  * Validates that an index is within bounds [0, trackCount).
1427
1321
  *
@@ -1469,6 +1363,10 @@ class Player(internal val context: Context) {
1469
1363
  mediaSessionCallback.notifyContentChanged(path)
1470
1364
  }
1471
1365
 
1366
+ fun invalidateAllContent() {
1367
+ mediaSessionCallback.invalidateAllContent()
1368
+ }
1369
+
1472
1370
  /**
1473
1371
  * Returns the recommended artwork size in pixels from the connected media browser (e.g., Android
1474
1372
  * Auto), or null if not provided.