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
@@ -16,6 +16,10 @@ import os.log
16
16
  /// Only safe when the caller blocks until the MainActor work completes (DispatchQueue.main.sync).
17
17
  private struct UncheckedSendableBox<T>: @unchecked Sendable { let value: T }
18
18
 
19
+ /// `@unchecked Sendable` because it must be: the spec's requirements are all
20
+ /// nonisolated and Nitro calls them synchronously on the JS thread, so the class
21
+ /// can't be `@MainActor`. Isolation is therefore per-property, and `@unchecked`
22
+ /// means the compiler won't tell you when a new one needs it.
19
23
  public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
20
24
  private let logger = Logger(subsystem: "com.audiobrowser", category: "AudioBrowser")
21
25
 
@@ -24,20 +28,52 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
24
28
  /// Shared instance for CarPlay access. Set when HybridAudioBrowser is created.
25
29
  private(set) nonisolated(unsafe) weak static var shared: HybridAudioBrowser?
26
30
 
31
+ /// Fires when a new instance becomes `shared` (e.g. on a JS runtime reload),
32
+ /// so connected external controllers (CarPlay) can re-subscribe — their
33
+ /// listeners sit on the previous instance's emitters and would otherwise go
34
+ /// silent.
35
+ static let instanceChangedEmitter = Emitter<Void>()
36
+
27
37
  // MARK: - Private Properties
28
38
 
39
+ /// Main-confined by convention, not annotation: `deinit` reads it as a
40
+ /// safety net and can't hop. Reach it from `onMainActor` / `@MainActor`.
29
41
  private var player: TrackPlayer?
30
42
  private let networkMonitor = NetworkMonitor()
43
+ private let playbackStateStore = PlaybackStateStore()
31
44
  let browserManager = BrowserManager()
32
45
  private let trackSelector: TrackSelector
33
46
  private var volumeObservation: NSKeyValueObservation?
34
47
  private var routeChangeObserver: NSObjectProtocol?
35
- private var nowPlayingOverride: NowPlayingUpdate?
36
- private let playerOptions = PlayerUpdateOptions()
48
+ private var interruptionObserver: NSObjectProtocol?
49
+ private var mediaServicesResetObserver: NSObjectProtocol?
50
+ /// Re-applies our resolved audio-session category. Captured at setup so a
51
+ /// media-services reset (which clears the category) can restore it.
52
+ @MainActor private var applyAudioSessionCategory: () -> Void = {}
53
+ @MainActor private var nowPlayingOverride: NowPlayingUpdate?
54
+ /// When false, the now-playing surface uses the raw track fields (override + formatter ignored).
55
+ @MainActor private var nowPlayingMetadataEnabled = true
56
+ /// Customizes the now-playing title/subtitle from the track + live timed metadata + playback state.
57
+ @MainActor private var nowPlayingMetadataFormatter: ((_ params: FormatNowPlayingParams) -> Promise<NowPlayingUpdate?>)?
58
+ /// Initial player state staged before the player exists — from setup options or the imperative
59
+ /// setters called pre-setup. Strict last-write-wins; consumed when the player comes up.
60
+ @MainActor private var pendingPlayWhenReady: Bool?
61
+ @MainActor private var pendingRepeatMode: RepeatMode?
62
+ /// Latest live timed (ICY/ID3) metadata, passed to the formatter. Cleared on track change.
63
+ @MainActor private var latestTimedMetadata: TimedMetadata?
64
+ @MainActor private let playerOptions = PlayerUpdateOptions()
37
65
 
38
66
  /// Configured playback rates for the playback-rate capability (for CarPlay rate cycling)
39
- var playbackRates: [Double] { playerOptions.playbackRates }
40
- private var lastNavigationError: NavigationError? {
67
+ @MainActor var playbackRates: [Double] { playerOptions.playbackRates }
68
+ @MainActor var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
69
+ @MainActor var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] {
70
+ playerOptions.carPlayNowPlayingButtons
71
+ }
72
+
73
+ /// Written from the JS thread, the cooperative pool and MainActor, with a
74
+ /// `didSet` that calls back into Nitro. MainActor-isolated so the compiler
75
+ /// requires an `onMainActor` hop at every access.
76
+ @MainActor private var lastNavigationError: NavigationError? {
41
77
  didSet {
42
78
  // Skip if both nil (no real change)
43
79
  guard oldValue != nil || lastNavigationError != nil else { return }
@@ -45,7 +81,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
45
81
  }
46
82
  }
47
83
 
48
- private var lastFormattedNavigationError: FormattedNavigationError? {
84
+ /// Isolated for the same reason as ``lastNavigationError``.
85
+ @MainActor private var lastFormattedNavigationError: FormattedNavigationError? {
49
86
  didSet {
50
87
  // Skip if both nil (no real change)
51
88
  guard oldValue != nil || lastFormattedNavigationError != nil else { return }
@@ -63,7 +100,21 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
63
100
  public let queueChangedEmitter = Emitter<[Track]>()
64
101
  public let navigationErrorEmitter = Emitter<NavigationErrorEvent>()
65
102
  public let repeatModeChangedEmitter = Emitter<RepeatModeChangedEvent>()
103
+ public let shuffleChangedEmitter = Emitter<Bool>()
66
104
  public let externalContentChangedEmitter = Emitter<String>()
105
+ /// Signals that the gate's active state changed (set or cleared). Carries
106
+ /// the active flag; per-request chrome is obtained at each serve site via
107
+ /// `gateDecision(for:)`, so the emitter only tells surfaces to re-render.
108
+ public let gateChangedEmitter = Emitter<Bool>()
109
+ /// Fired when a voice media intent (`handlePlayMediaIntent`) successfully
110
+ /// starts playback. CarPlay surfaces the Now Playing template in response, so
111
+ /// the user lands on the playing station (with the rest of the results in Up
112
+ /// Next) rather than the screen they invoked Siri from. No-op where unobserved.
113
+ public let showNowPlayingRequestedEmitter = Emitter<Void>()
114
+ /// Fires after `updateOptions` applies a change, so external-surface managers
115
+ /// (CarPlay) can refresh UI driven by now-runtime-updatable options
116
+ /// (now-playing buttons, Up Next).
117
+ public let playerOptionsChangedEmitter = Emitter<Void>()
67
118
 
68
119
  // MARK: - Thread Safety
69
120
 
@@ -71,9 +122,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
71
122
  /// because DispatchQueue.main.sync blocks the caller — no concurrent access window.
72
123
  private func onMainActor<T>(_ work: @MainActor () -> T) -> T {
73
124
  if Thread.isMainThread {
74
- return MainActor.assumeIsolated { UncheckedSendableBox(value: work()) }.value
125
+ MainActor.assumeIsolated { UncheckedSendableBox(value: work()) }.value
75
126
  } else {
76
- return DispatchQueue.main.sync {
127
+ DispatchQueue.main.sync {
77
128
  MainActor.assumeIsolated { UncheckedSendableBox(value: work()) }.value
78
129
  }
79
130
  }
@@ -81,10 +132,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
81
132
 
82
133
  private func onMainActor<T>(_ work: @MainActor () throws -> T) throws -> T {
83
134
  if Thread.isMainThread {
84
- return try MainActor.assumeIsolated { UncheckedSendableBox(value: try work()) }.value
135
+ try MainActor.assumeIsolated { try UncheckedSendableBox(value: work()) }.value
85
136
  } else {
86
- return try DispatchQueue.main.sync {
87
- try MainActor.assumeIsolated { UncheckedSendableBox(value: try work()) }.value
137
+ try DispatchQueue.main.sync {
138
+ try MainActor.assumeIsolated { try UncheckedSendableBox(value: work()) }.value
88
139
  }
89
140
  }
90
141
  }
@@ -111,11 +162,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
111
162
  }
112
163
 
113
164
  public var configuration: NativeBrowserConfiguration = .init(
114
- path: nil, request: nil, media: nil, artwork: nil, routes: nil,
115
- singleTrack: nil, handleTrackLoad: nil, androidControllerOfflineError: nil,
116
- carPlayUpNextButton: nil, carPlayNowPlayingButtons: nil, formatNavigationError: nil,
165
+ path: nil, request: nil, requestResolver: nil, browse: nil, browseResolver: nil, media: nil, artwork: nil, nowPlayingArtwork: nil, routes: nil,
166
+ singleTrack: nil, handleTrackLoad: nil,
167
+ androidControllerOfflineError: nil, carPlayLoadingTitle: nil, resolveAlbumUrl: nil, formatNavigationError: nil,
117
168
  ) {
118
169
  didSet {
170
+ // Copied here, on the setter's thread: the Task below outlives this call,
171
+ // and Nitro can reassign `configuration` — a C++ struct holding
172
+ // std::string/std::function — from the JS thread while it runs.
173
+ let configuredPath = configuration.path
119
174
  onMainActor { browserManager.config = BrowserConfig(from: configuration) }
120
175
 
121
176
  // Query tabs and navigate to initial path after config is set (matches Kotlin behavior)
@@ -126,12 +181,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
126
181
  Task {
127
182
  let tabs = try? await browserManager.queryTabs()
128
183
  // Navigate to configured path, first tab, or "/"
129
- let initialPath = configuration.path ?? tabs?.first?.url ?? "/"
184
+ let initialPath = configuredPath ?? tabs?.first?.url ?? "/"
130
185
  // Clear error before navigation (matches Kotlin clearNavigationError())
131
- await MainActor.run {
132
- lastNavigationError = nil
133
- lastFormattedNavigationError = nil
134
- }
186
+ clearNavigationError()
135
187
  do {
136
188
  try await browserManager.navigate(initialPath)
137
189
  } catch {
@@ -149,6 +201,25 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
149
201
  public var onNavigationError: (NavigationErrorEvent) -> Void = { _ in }
150
202
  public var onFormattedNavigationError: (FormattedNavigationError?) -> Void = { _ in }
151
203
 
204
+ /// Per-request gate decision, set by JS. Native calls it at a serve site to
205
+ /// learn whether a browse path / search interaction should be gated and with
206
+ /// what chrome. The double `Promise` mirrors every other native→JS
207
+ /// value-returning callback (e.g. `searchCallback`): the outer resolves to the
208
+ /// JS-side Promise, the inner to the decision.
209
+ ///
210
+ /// Default fails CLOSED (`gated: true`). This default is only consulted in the
211
+ /// init window where `hasResolver` was already recorded true but JS hasn't
212
+ /// (re)bound the real resolver yet (JS reload / instance churn re-seeding gate
213
+ /// state, e.g. CarPlay re-seeding `isGated` at scene connect). Serving content
214
+ /// in that window would be a fail-open leak — same direction as the
215
+ /// resolver-error path in `gateDecision`. Matches Android's default.
216
+ public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = { _ in
217
+ Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
218
+ }
219
+
220
+ /// Fired when a request is gated (served the gate). Set by JS.
221
+ public var onGate: (GateEvent) -> Void = { _ in }
222
+
152
223
  // MARK: - Player Callbacks
153
224
 
154
225
  public var onChapterMetadata: ([ChapterMetadata]) -> Void = { _ in }
@@ -159,6 +230,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
159
230
  public var onPlaybackPlayWhenReadyChanged: (PlaybackPlayWhenReadyChangedEvent) -> Void = { _ in }
160
231
  public var onPlaybackPlayingState: (PlayingState) -> Void = { _ in }
161
232
  public var onPlaybackProgressUpdated: (PlaybackProgressUpdatedEvent) -> Void = { _ in }
233
+ public var onPlaybackInterval: () -> Void = {}
162
234
  public var onPlaybackQueueEnded: (PlaybackQueueEndedEvent) -> Void = { _ in }
163
235
  public var onPlaybackQueueChanged: ([Track]) -> Void = { _ in }
164
236
  public var onPlaybackRepeatModeChanged: (RepeatModeChangedEvent) -> Void = { _ in }
@@ -168,11 +240,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
168
240
 
169
241
  // MARK: - Remote Callbacks
170
242
 
171
- public var onRemoteBookmark: () -> Void = {}
172
- public var onRemoteDislike: () -> Void = {}
173
243
  public var onRemoteJumpBackward: (RemoteJumpBackwardEvent) -> Void = { _ in }
174
244
  public var onRemoteJumpForward: (RemoteJumpForwardEvent) -> Void = { _ in }
175
- public var onRemoteLike: () -> Void = {}
176
245
  public var onRemoteNext: () -> Void = {}
177
246
  public var onRemotePause: () -> Void = {}
178
247
  public var onRemotePlay: () -> Void = {}
@@ -180,17 +249,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
180
249
  public var onRemotePlaySearch: (RemotePlaySearchEvent) -> Void = { _ in }
181
250
  public var onRemotePrevious: () -> Void = {}
182
251
  public var onRemoteSeek: (RemoteSeekEvent) -> Void = { _ in }
183
- public var onRemoteSetRating: (RemoteSetRatingEvent) -> Void = { _ in }
184
252
  public var onRemoteSkip: (RemoteSkipEvent) -> Void = { _ in }
185
253
  public var onRemoteStop: () -> Void = {}
186
254
 
187
255
  // MARK: - Remote Handlers (optional overrides from JS)
188
256
 
189
- public var handleRemoteBookmark: (() -> Void)?
190
- public var handleRemoteDislike: (() -> Void)?
191
257
  public var handleRemoteJumpBackward: ((RemoteJumpBackwardEvent) -> Void)?
192
258
  public var handleRemoteJumpForward: ((RemoteJumpForwardEvent) -> Void)?
193
- public var handleRemoteLike: (() -> Void)?
194
259
  public var handleRemoteNext: (() -> Void)?
195
260
  public var handleRemotePause: (() -> Void)?
196
261
  public var handleRemotePlay: (() -> Void)?
@@ -208,10 +273,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
208
273
  public var onNowPlayingChanged: (NowPlayingMetadata) -> Void = { _ in }
209
274
  public var onOnlineChanged: (Bool) -> Void = { _ in } {
210
275
  didSet {
211
- networkMonitor.onChanged = { [weak self] isOnline in
212
- self?.onOnlineChanged(isOnline)
213
- }
214
- // Immediately notify current state
276
+ // The monitor->JS bridge is wired centrally in setupPlayer (so it doesn't depend on a JS
277
+ // subscription and can also drive the now-playing re-render + player reconnect). Here we only
278
+ // push the current state to a newly-attached subscriber.
215
279
  onOnlineChanged(networkMonitor.isOnline)
216
280
  }
217
281
  }
@@ -225,7 +289,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
225
289
  }
226
290
  }
227
291
 
228
- public var onIosOutputChanged: (IosOutput) -> Void = { _ in } {
292
+ public var onOutputChanged: (Output) -> Void = { _ in } {
229
293
  didSet {
230
294
  setupRouteChangeObserver()
231
295
  }
@@ -245,6 +309,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
245
309
  HybridAudioBrowser.shared?.player?.destroy()
246
310
  playerAndConfiguredBrowser.reset()
247
311
  HybridAudioBrowser.shared = self
312
+ // Tell connected external controllers (CarPlay) to re-subscribe against
313
+ // this instance. Unconditional: `shared` is weak, so on a JS reload the
314
+ // old instance may already be gone — "shared was nil" does not mean no
315
+ // controller is subscribed to a dead instance's emitters.
316
+ HybridAudioBrowser.instanceChangedEmitter.emit(())
248
317
  }
249
318
  setupEmitterToNitroForwarding()
250
319
  setupBrowserCallbacks()
@@ -313,33 +382,50 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
313
382
  private func handleNavigationError(_ error: Error, path: String) {
314
383
  let navError = NavigationError.from(error)
315
384
 
316
- lastNavigationError = navError
385
+ onMainActor { lastNavigationError = navError }
317
386
 
318
387
  // Format the error (async if using JS callback, sync for defaults)
319
388
  let defaultFormatted = navError.defaultFormatted()
320
389
  if let formatter = onMainActor({ browserManager.config.formatNavigationError }) {
321
390
  let params = FormatNavigationErrorParams(error: navError, defaultFormatted: defaultFormatted, path: path)
322
- // Dispatch to main thread for the Nitro bridge call to avoid C++ noexcept crashes
391
+ // Dispatch to main thread for the Nitro bridge call to avoid C++ noexcept crashes.
392
+ // The result callbacks are @Sendable: a plain closure would inherit MainActor
393
+ // isolation from this block, and Nitro resolves its promises synchronously on the
394
+ // JS thread — Swift 6.2's dynamic isolation check then traps on closure entry.
395
+ // They therefore hop back explicitly, like every other writer.
323
396
  DispatchQueue.main.async { [weak self] in
324
397
  formatter(params)
325
- .then { customDisplay in
326
- self?.lastFormattedNavigationError = customDisplay ?? defaultFormatted
398
+ .then { @Sendable [weak self] customDisplay in
399
+ self?.setFormattedNavigationError(customDisplay ?? defaultFormatted)
327
400
  }
328
- .catch { _ in
329
- self?.lastFormattedNavigationError = defaultFormatted
401
+ .catch { @Sendable [weak self] _ in
402
+ self?.setFormattedNavigationError(defaultFormatted)
330
403
  }
331
404
  }
332
405
  } else {
333
- lastFormattedNavigationError = defaultFormatted
406
+ setFormattedNavigationError(defaultFormatted)
407
+ }
408
+ }
409
+
410
+ /// Callable from any thread — the Nitro promise callbacks below resolve on
411
+ /// the JS thread.
412
+ private func setFormattedNavigationError(_ formatted: FormattedNavigationError?) {
413
+ onMainActor { lastFormattedNavigationError = formatted }
414
+ }
415
+
416
+ /// Synchronous on purpose: the `didSet`s notify JS, which has to happen
417
+ /// before the navigation that may set them again.
418
+ private func clearNavigationError() {
419
+ onMainActor {
420
+ lastNavigationError = nil
421
+ lastFormattedNavigationError = nil
334
422
  }
335
423
  }
336
424
 
337
425
  // MARK: - Browser Methods
338
426
 
339
427
  public func navigatePath(path: String) throws {
340
- // Clear error synchronously before starting navigation (didSet notifies JS)
341
- lastNavigationError = nil
342
- lastFormattedNavigationError = nil
428
+ clearNavigationError()
343
429
  Task {
344
430
  do {
345
431
  try await browserManager.navigate(path)
@@ -350,15 +436,18 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
350
436
  }
351
437
 
352
438
  public func navigateTrack(track: Track) throws {
353
- Task {
439
+ // @MainActor: `player` is main-confined, and `trackSelector.select` is
440
+ // MainActor anyway — a bare Task read the reference off the cooperative
441
+ // pool, racing the assignment in `setupPlayer`.
442
+ Task { @MainActor in
354
443
  guard let player else { return }
355
444
  let result = await trackSelector.select(track: track, player: player)
356
445
  switch result {
357
- case .play(let intent):
446
+ case let .play(intent):
358
447
  executePlayback(intent)
359
448
  case .intercepted:
360
449
  break
361
- case .browse(let url):
450
+ case let .browse(url):
362
451
  navigateToBrowsableUrl(url)
363
452
  case .none:
364
453
  break
@@ -369,24 +458,22 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
369
458
  private func executePlayback(_ intent: TrackSelector.PlaybackIntent) {
370
459
  onMainActor {
371
460
  switch intent {
372
- case .skipTo(let index):
461
+ case let .skipTo(index):
373
462
  do {
374
463
  try player?.skipTo(index, playWhenReady: true)
375
464
  } catch {
376
465
  logger.error("Failed to skip to track at index \(index): \(error)")
377
466
  }
378
- case .setQueue(let tracks, let startIndex, let sourcePath):
467
+ case let .setQueue(tracks, startIndex, sourcePath):
379
468
  player?.setQueue(tracks, initialIndex: startIndex, playWhenReady: true, sourcePath: sourcePath)
380
- case .loadTrack(let track):
469
+ case let .loadTrack(track):
381
470
  player?.load(track, playWhenReady: true)
382
471
  }
383
472
  }
384
473
  }
385
474
 
386
475
  private func navigateToBrowsableUrl(_ url: String) {
387
- // Clear error synchronously before starting navigation (didSet notifies JS)
388
- lastNavigationError = nil
389
- lastFormattedNavigationError = nil
476
+ clearNavigationError()
390
477
  Task {
391
478
  do {
392
479
  try await browserManager.navigate(url)
@@ -396,7 +483,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
396
483
  }
397
484
  }
398
485
 
399
- public func onSearch(query: String) throws -> Promise<[Track]> {
486
+ public func search(query: String) throws -> Promise<[Track]> {
400
487
  Promise.async { [weak self] in
401
488
  guard let self else { return [] }
402
489
  let resolved = try await browserManager.search(query)
@@ -409,13 +496,19 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
409
496
  }
410
497
 
411
498
  public func getNavigationError() throws -> NavigationError? {
412
- lastNavigationError
499
+ onMainActor { lastNavigationError }
413
500
  }
414
501
 
415
502
  public func getFormattedNavigationError() throws -> FormattedNavigationError? {
416
- lastFormattedNavigationError
503
+ onMainActor { lastFormattedNavigationError }
417
504
  }
418
505
 
506
+ /// Internal signal sent on `externalContentChangedEmitter` to tell CarPlay to
507
+ /// refresh every displayed template. Not a real path (paths are always
508
+ /// `/…`), so it can't collide with one; kept internal — the public API is
509
+ /// `invalidateAllContent()`.
510
+ static let invalidateAllSentinel = "__rnab_invalidate_all__"
511
+
419
512
  public func notifyContentChanged(path: String) throws {
420
513
  onMainActor { browserManager.invalidateContentCache(path) }
421
514
 
@@ -434,22 +527,168 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
434
527
  }
435
528
  }
436
529
 
530
+ public func invalidateAllContent() throws {
531
+ onMainActor { browserManager.clearContentCache() }
532
+
533
+ // Tell external controllers (CarPlay) to refresh every displayed template.
534
+ externalContentChangedEmitter.emit(Self.invalidateAllSentinel)
535
+
536
+ // Re-query the tabs source too: a callback source can resolve to new
537
+ // titles after an invalidation (e.g. a locale switch). BrowserManager
538
+ // emits tabsChanged only when the result actually differs, and a failed
539
+ // re-query keeps the current tabs. (Android needs no equivalent — its
540
+ // root re-query already runs queryTabs(); the web stub re-queries tabs on
541
+ // the re-navigation.)
542
+ Task {
543
+ _ = try? await browserManager.queryTabs()
544
+ }
545
+
546
+ // Re-resolve the JS-facing current path with the cache cleared.
547
+ Task {
548
+ do {
549
+ try await browserManager.refresh()
550
+ } catch {
551
+ handleNavigationError(error, path: onMainActor { browserManager.getPath() })
552
+ }
553
+ }
554
+ }
555
+
437
556
  public func setFavorites(favorites: [String]) throws {
438
557
  onMainActor { browserManager.setFavorites(favorites) }
439
558
  }
440
559
 
560
+ // MARK: - Gate
561
+
562
+ /// The gate's default chrome, if one was set. Served when the resolver
563
+ /// returns a gated decision with no per-request override, and for the static
564
+ /// gate (no resolver). May be nil for a resolver-only gate.
565
+ private(set) var defaultChrome: Gate?
566
+
567
+ /// Whether JS installed a per-request resolver. When false the gate is static
568
+ /// — every request is gated with `defaultChrome` and the JS hop is skipped.
569
+ private(set) var hasResolver = false
570
+
571
+ /// True once `setGate` runs, until `clearGate`. While active, external
572
+ /// surfaces (CarPlay) keep their tabs visible but consult `gateDecision(for:)`
573
+ /// per request, rendering the gate chrome where gated and refusing
574
+ /// external-surface search. Playback, the queue and Now Playing are
575
+ /// unaffected — a gate blocks finding content, never hearing it.
576
+ private(set) var isGateActive = false
577
+
578
+ /// The minimal built-in gate served when a request is gated but neither a
579
+ /// per-request override nor a stored default chrome exists (resolver-only
580
+ /// gate returning `true`).
581
+ static let builtInGate = Gate(title: "Unavailable", message: nil)
582
+
583
+ /// The outcome of a single gate decision: whether to gate, and (if so) the
584
+ /// chrome to render.
585
+ struct GateOutcome {
586
+ let gated: Bool
587
+ let chrome: Gate?
588
+ }
589
+
590
+ public func setGate(gate: Gate?, hasResolver: Bool) throws {
591
+ onMainActor {
592
+ defaultChrome = gate
593
+ self.hasResolver = hasResolver
594
+ isGateActive = true
595
+ gateChangedEmitter.emit(true)
596
+ }
597
+ }
598
+
599
+ public func clearGate() throws {
600
+ onMainActor {
601
+ guard isGateActive else { return }
602
+ defaultChrome = nil
603
+ hasResolver = false
604
+ isGateActive = false
605
+ gateChangedEmitter.emit(false)
606
+ }
607
+ }
608
+
609
+ /// The single choke point for every gate enforcement site. Asks whether
610
+ /// `request` should be gated and, if so, with what chrome (override → stored
611
+ /// default → built-in). When no resolver is installed the gate is static and
612
+ /// resolves without a JS hop.
613
+ ///
614
+ /// Resolver errors **fail CLOSED by design**: a thrown/rejected resolver, or a
615
+ /// rejection at either `.await()` layer (bridge tear-down on JS reload, the JS
616
+ /// runtime mid-reload, a serialization error) cannot prove the request is
617
+ /// allowed — and a gate exists to withhold content, so serving on error would
618
+ /// be a content leak (a paywall bypass). On error we gate with the stored
619
+ /// default / built-in chrome so the user still sees a coherent gate page. A
620
+ /// *successful* resolver returning `gated: false` still allows the request.
621
+ @MainActor
622
+ func gateDecision(for request: NativeGateRequest) async -> GateOutcome {
623
+ guard isGateActive else { return GateOutcome(gated: false, chrome: nil) }
624
+ if !hasResolver { return GateOutcome(gated: true, chrome: defaultChrome ?? Self.builtInGate) }
625
+ guard let decision = try? await resolveGate(request).await().await() else {
626
+ // Resolver error: fail CLOSED (see doc comment) — gate with the default /
627
+ // built-in so the gate page still renders rather than leaking content.
628
+ return GateOutcome(gated: true, chrome: defaultChrome ?? Self.builtInGate)
629
+ }
630
+ guard decision.gated else { return GateOutcome(gated: false, chrome: nil) } // explicit allow
631
+ return GateOutcome(gated: true, chrome: decision.gate ?? defaultChrome ?? Self.builtInGate)
632
+ }
633
+
634
+ // MARK: - Car Connection (CarPlay)
635
+
636
+ /// Static because the CarPlay scene can connect before the JS runtime
637
+ /// creates the shared instance (cold start in the car).
638
+ private nonisolated(unsafe) static var carPlayConnected = false
639
+
640
+ /// Called by the CarPlay scene delegate (via `RNABCarPlayController`) on
641
+ /// scene connect/disconnect — not from controller start/stop, which also
642
+ /// cycle on a JS reload and would emit spurious connection changes.
643
+ static func setCarPlayConnected(_ connected: Bool) {
644
+ guard carPlayConnected != connected else { return }
645
+ carPlayConnected = connected
646
+ shared?.onCarConnectedChanged(connected)
647
+ }
648
+
649
+ public func isCarConnected() throws -> Bool {
650
+ HybridAudioBrowser.carPlayConnected
651
+ }
652
+
653
+ public var onCarConnectedChanged: (Bool) -> Void = { _ in } {
654
+ didSet {
655
+ // Immediately notify current state (the scene may have connected before
656
+ // this JS runtime subscribed).
657
+ onCarConnectedChanged(HybridAudioBrowser.carPlayConnected)
658
+ }
659
+ }
660
+
441
661
  // MARK: - Player Setup
442
662
 
443
- public func setupPlayer(options: PartialSetupPlayerOptions) throws -> Promise<Void> {
663
+ public func setupPlayer(options: NativeSetupPlayerOptions) throws -> Promise<Void> {
444
664
  Promise.async {
445
- // Configure audio session
665
+ // Configure the audio session from the iOS setup options (category, mode, route policy, and
666
+ // options like `allowBluetooth` / `allowAirPlay`), but do NOT activate it here. Activating a
667
+ // non-mixable `.playback` session interrupts other apps' audio — so activating at setup
668
+ // pauses whatever the user is listening to (Safari, Spotify, a podcast) the instant our app
669
+ // launches, before they play anything. Setting the category is silent; only activation
670
+ // interrupts. The session is activated lazily when playback actually starts producing output
671
+ // (see `playerDidChangePlayingState`) and re-activated on interruption-end. Best-effort: a
672
+ // category-config failure must never brick setup.
446
673
  let session = AVAudioSession.sharedInstance()
447
- try session.setCategory(.playback, mode: .default)
448
- try session.setActive(true)
674
+ let cfg: (
675
+ category: AVAudioSession.Category,
676
+ mode: AVAudioSession.Mode,
677
+ policy: AVAudioSession.RouteSharingPolicy,
678
+ options: AVAudioSession.CategoryOptions,
679
+ ) = options.ios?.resolveAudioSessionConfig()
680
+ ?? (category: .playback, mode: .default, policy: .default, options: [])
681
+ // Capture category application once so a media-services reset (which
682
+ // clears the category) can restore the exact same config later.
683
+ let applyCategory: () -> Void = {
684
+ try? session.setCategory(cfg.category, mode: cfg.mode, policy: cfg.policy, options: cfg.options)
685
+ }
686
+ applyCategory()
449
687
 
450
688
  // Create player and configure on main actor
451
689
  await MainActor.run { [weak self] in
452
690
  guard let self else { return }
691
+ self.applyAudioSessionCategory = applyCategory
453
692
 
454
693
  // Create player with self as callbacks delegate
455
694
  player = TrackPlayer(callbacks: self)
@@ -467,18 +706,52 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
467
706
  // Wire up network monitor for accelerated retries when connectivity is restored
468
707
  player?.networkMonitor = networkMonitor
469
708
 
709
+ // Central fan-out for connectivity changes (wired once here, idempotent on re-setup, and
710
+ // independent of any JS subscription):
711
+ // - bridge to the JS `onOnlineChanged` event (no-op default until JS subscribes);
712
+ // - re-render now-playing so the formatter's offline/online label updates immediately on a
713
+ // connectivity change, not only on the next playback transition (fixes the stall-entry
714
+ // race where the device goes offline after the buffering line was already rendered);
715
+ // - reconnect a stalled stream when connectivity returns (see TrackPlayer.handleNetworkRestored).
716
+ // The monitor only mutates state on the main thread, so this fires main-isolated.
717
+ networkMonitor.onChanged = { [weak self] isOnline in
718
+ MainActor.assumeIsolated {
719
+ guard let self else { return }
720
+ self.onOnlineChanged(isOnline)
721
+ self.applyNowPlayingMetadata()
722
+ if isOnline { self.player?.handleNetworkRestored() }
723
+ }
724
+ }
725
+
726
+ // Observe audio-session interruptions so another app taking over audio
727
+ // (or a phone call) reflects as a real pause rather than a stuck state.
728
+ setupInterruptionObserver()
729
+
730
+ // Observe media-server resets so a long-running stream recovers instead
731
+ // of going permanently silent.
732
+ setupMediaServicesResetObserver()
733
+
470
734
  // Configure media URL resolver
471
- player?.mediaLoader.mediaUrlResolver = { [weak self] src in
735
+ player?.mediaLoader.mediaUrlResolver = { [weak self] src, track in
472
736
  guard let self else {
473
737
  return MediaResolvedUrl(url: src, headers: nil, userAgent: nil)
474
738
  }
475
- return await browserManager.resolveMediaUrl(src)
739
+ return await browserManager.resolveMediaUrl(src, track: track)
476
740
  }
477
741
 
478
742
  // Configure artwork URL resolver for Now Playing (with size context)
479
743
  player?.nowPlayingUpdater.artworkUrlResolver = { [weak self] track, imageContext in
480
744
  guard let self else { return nil }
481
- return await browserManager.resolveArtworkUrl(track: track, perRouteConfig: nil, imageContext: imageContext)
745
+ let perRoute = (track.id?.isEmpty == false) ? browserManager.config.nowPlayingArtwork : nil
746
+ return await browserManager.resolveArtworkUrl(track: track, perRouteConfig: perRoute, imageContext: imageContext)
747
+ }
748
+
749
+ // Emit the JS `onNowPlayingChanged` event when the rendered text lines change. The updater
750
+ // owns the system now-playing write; this shapes the JS metadata (elapsed time / artwork).
751
+ player?.nowPlayingUpdater.onChanged = { [weak self] track, title, artist, album in
752
+ guard let self else { return }
753
+ self.onNowPlayingChanged(
754
+ self.makeNowPlayingMetadata(track: track, title: title, artist: artist, album: album))
482
755
  }
483
756
 
484
757
  // Configure sleep timer callback
@@ -486,8 +759,30 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
486
759
  self?.onSleepTimerChanged(state)
487
760
  }
488
761
 
489
- // Apply default remote commands (play, pause, next, previous, seekTo)
490
- applyRemoteCommands()
762
+ // Now-playing: whether to manage metadata, and the optional formatter that owns the
763
+ // rendered lines.
764
+ nowPlayingMetadataEnabled = options.autoUpdateNowPlayingMetadata ?? true
765
+ nowPlayingMetadataFormatter = options.nowPlayingMetadataFormatter
766
+ // `options.keepSessionAliveOnError` is deliberately ignored here: iOS needs no masking.
767
+ // A terminal error resolves the playback state to *paused*
768
+ // (PlaybackCoordinator.applySideEffects), the now-playing info is retained (never cleared
769
+ // on error), and next/previous stay enabled (gated on queue position, not state). So the
770
+ // session stays controllable through errors by default — the behavior Android achieves via
771
+ // InterceptingPlayer is already the iOS norm.
772
+
773
+ // The bundled runtime options and initial state are part of the atomic launch
774
+ // description. Stage the state first so setup options and earlier pre-setup setter
775
+ // calls resolve by strict last-write-wins, then apply everything to the live player.
776
+ if let bundled = options.options {
777
+ updateOptions(options: bundled)
778
+ } else {
779
+ // updateOptions applies remote commands itself; without bundled options, apply the
780
+ // defaults (play, pause, next, previous, seekTo) here.
781
+ applyRemoteCommands()
782
+ }
783
+ if let mode = options.repeatMode { pendingRepeatMode = mode }
784
+ if let intent = options.playWhenReady { pendingPlayWhenReady = intent }
785
+ applyPendingPlayerState()
491
786
 
492
787
  // Notify listeners that player is ready (e.g., CarPlay)
493
788
  playerAndConfiguredBrowser.check()
@@ -495,6 +790,22 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
495
790
  }
496
791
  }
497
792
 
793
+ /// Applies initial player state staged before the player existed — from setup options or the
794
+ /// imperative setters called pre-setup. Consumed on apply so a later re-setup doesn't replay
795
+ /// stale state.
796
+ @MainActor
797
+ private func applyPendingPlayerState() {
798
+ guard let player else { return }
799
+ if let mode = pendingRepeatMode {
800
+ player.repeatMode = mode
801
+ pendingRepeatMode = nil
802
+ }
803
+ if let intent = pendingPlayWhenReady {
804
+ player.playWhenReady = intent
805
+ pendingPlayWhenReady = nil
806
+ }
807
+ }
808
+
498
809
  public func updateOptions(options: NativeUpdateOptions) {
499
810
  onMainActor {
500
811
  let previousInterval = playerOptions.progressUpdateEventInterval
@@ -502,6 +813,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
502
813
  // Update stored options
503
814
  playerOptions.update(from: options)
504
815
 
816
+ // Propagate the favorite match mode to the browser so it can hydrate
817
+ // row hearts (the `favorite` capability is the single favoriting switch).
818
+ browserManager.setFavoriteMatch(playerOptions.capabilities.favoriteMatch)
819
+
505
820
  // Apply remote commands to player
506
821
  applyRemoteCommands()
507
822
 
@@ -509,6 +824,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
509
824
  if playerOptions.progressUpdateEventInterval != previousInterval {
510
825
  player?.setProgressUpdateInterval(playerOptions.progressUpdateEventInterval)
511
826
  }
827
+
828
+ onOptionsChanged(playerOptions.toOptions())
829
+ playerOptionsChangedEmitter.emit(())
512
830
  }
513
831
  }
514
832
 
@@ -526,8 +844,12 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
526
844
  player.remoteCommands = remoteCommands
527
845
  }
528
846
 
529
- public func getOptions() throws -> UpdateOptions {
530
- playerOptions.toUpdateOptions()
847
+ public func getOptions() throws -> Options {
848
+ onMainActor { playerOptions.toOptions() }
849
+ }
850
+
851
+ public func setPlaybackIntervalEnabled(enabled: Bool) {
852
+ onMainActor { player?.setPlaybackIntervalEnabled(enabled) }
531
853
  }
532
854
 
533
855
  // MARK: - Playback Control
@@ -562,11 +884,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
562
884
  }
563
885
 
564
886
  public func setPlayWhenReady(playWhenReady: Bool) throws {
565
- onMainActor { player?.playWhenReady = playWhenReady }
887
+ onMainActor {
888
+ // Pre-setup this stages the intent for the player to come up with — never a no-op.
889
+ if let player { player.playWhenReady = playWhenReady }
890
+ else { pendingPlayWhenReady = playWhenReady }
891
+ }
566
892
  }
567
893
 
568
894
  public func getPlayWhenReady() throws -> Bool {
569
- onMainActor { player?.playWhenReady ?? false }
895
+ onMainActor { player?.playWhenReady ?? pendingPlayWhenReady ?? false }
570
896
  }
571
897
 
572
898
  public func seekTo(position: Double) throws {
@@ -577,6 +903,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
577
903
  onMainActor { player?.seekBy(offset) }
578
904
  }
579
905
 
906
+ public func seekToLiveEdge() throws {
907
+ onMainActor { player?.seekToLiveEdge() }
908
+ }
909
+
580
910
  public func setVolume(level: Double) throws {
581
911
  onMainActor { player?.volume = Float(level) }
582
912
  }
@@ -614,11 +944,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
614
944
  }
615
945
 
616
946
  public func getRepeatMode() throws -> RepeatMode {
617
- onMainActor { player?.repeatMode ?? .off }
947
+ onMainActor { player?.repeatMode ?? pendingRepeatMode ?? .off }
618
948
  }
619
949
 
620
950
  public func setRepeatMode(mode: RepeatMode) throws {
621
- onMainActor { player?.repeatMode = mode }
951
+ onMainActor {
952
+ // Pre-setup this stages the mode for the player to come up with — never a no-op.
953
+ if let player { player.repeatMode = mode }
954
+ else { pendingRepeatMode = mode }
955
+ }
622
956
  }
623
957
 
624
958
  public func getShuffleEnabled() throws -> Bool {
@@ -630,11 +964,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
630
964
  }
631
965
 
632
966
  public func getPlaybackError() throws -> PlaybackError? {
633
- onMainActor { player?.playbackError?.toNitroError() }
967
+ onMainActor { player?.coordinator.nitroPlaybackError() }
634
968
  }
635
969
 
636
970
  public func retry() throws {
637
- onMainActor { player?.reload(startFromCurrentTime: true) }
971
+ // Re-resolve rather than replay the cached URL: short-lived URLs/tokens
972
+ // may have expired since the failure (matches the internal retry path).
973
+ onMainActor { player?.reloadResolving(startFromCurrentTime: true) }
638
974
  }
639
975
 
640
976
  // MARK: - Sleep Timer
@@ -648,9 +984,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
648
984
  }
649
985
  }
650
986
 
651
- public func setSleepTimer(seconds: Double) throws {
987
+ public func setSleepTimer(seconds: Double, fadeDuration: Double?) throws {
652
988
  onMainActor {
653
- player?.sleepTimerManager.set(seconds: seconds)
989
+ player?.sleepTimerManager.set(seconds: seconds, fadeDuration: fadeDuration)
654
990
  }
655
991
  }
656
992
 
@@ -730,13 +1066,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
730
1066
  onMainActor {
731
1067
  guard let track = player?.currentTrack, let src = track.src else { return }
732
1068
  guard let index = player?.currentIndex, index >= 0 else { return }
733
- browserManager.updateFavorite(id: src, favorited: favorited)
734
- // Create updated track with new favorited state
1069
+ // Optimistically reflect in the authoritative match set so the now-playing
1070
+ // heart (hydrated from it in getActiveTrack) flips immediately; native
1071
+ // reconciles to its canonical ids on the next setFavorites.
1072
+ browserManager.setFavorited(src: src, favorited: favorited)
735
1073
  let updatedTrack = track.copying(favorited: favorited)
736
- // Update the track in the player's queue so getActiveTrack() returns correct state
737
- player?.replace(index, updatedTrack)
738
1074
  favoriteChangedEmitter.emit(FavoriteChangedEvent(track: updatedTrack, favorited: favorited))
739
- // Fire active track changed so useActiveTrack() hook updates UI
1075
+ // Fire active track changed so the now-playing heart + useActiveTrack() refresh.
740
1076
  let position = player?.currentTime ?? 0
741
1077
  activeTrackChangedEmitter.emit(PlaybackActiveTrackChangedEvent(
742
1078
  lastIndex: Double(index),
@@ -748,27 +1084,24 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
748
1084
  }
749
1085
  }
750
1086
 
1087
+ /// Whether the active (now-playing) track is favorited, per the authoritative
1088
+ /// favorite set (via `getActiveTrack`'s hydration).
1089
+ func isActiveTrackFavorited() -> Bool {
1090
+ (try? getActiveTrack())?.favorited ?? false
1091
+ }
1092
+
751
1093
  public func toggleActiveTrackFavorited() throws {
752
- onMainActor {
753
- guard let track = player?.currentTrack, let src = track.src else { return }
754
- // Check current favorited state from cache
755
- let currentTrack = browserManager.getCachedTrack(src)
756
- let isFavorited = currentTrack?.favorited ?? track.favorited ?? false
757
- try? setActiveTrackFavorited(favorited: !isFavorited)
758
- }
1094
+ try? setActiveTrackFavorited(favorited: !isActiveTrackFavorited())
759
1095
  }
760
1096
 
761
- public func setQueue(tracks: [Track], startIndex: Double?, startPositionMs: Double?) throws {
762
- try onMainActor {
1097
+ public func setQueue(tracks: [Track], startIndex: Double?, startPosition: Double?) throws {
1098
+ onMainActor {
763
1099
  guard let player else { return }
764
- player.clear()
765
- player.add(tracks)
766
- if let index = startIndex, index >= 0 {
767
- try player.skipTo(Int(index))
768
- }
769
- if let position = startPositionMs {
770
- player.seekTo(position / 1000.0)
771
- }
1100
+ player.setQueue(
1101
+ tracks,
1102
+ initialIndex: startIndex.map { Int($0) } ?? 0,
1103
+ startPositionMs: startPosition.map { $0 * 1000 },
1104
+ )
772
1105
  }
773
1106
  }
774
1107
 
@@ -793,7 +1126,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
793
1126
  }
794
1127
 
795
1128
  public func getActiveTrack() throws -> Track? {
796
- onMainActor { player?.currentTrack }
1129
+ // Hydrate `favorited` from the authoritative favorite set — the now-playing
1130
+ // track is loaded outside the browse cache, so its own flag isn't otherwise
1131
+ // kept in sync as favorites change. This is the single source of truth for
1132
+ // "is the active track favorited" (now-playing heart, toggle direction, JS).
1133
+ onMainActor { player?.currentTrack.map(browserManager.hydrateFavorite) }
797
1134
  }
798
1135
 
799
1136
  // MARK: - Now Playing
@@ -805,21 +1142,58 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
805
1142
  }
806
1143
  }
807
1144
 
1145
+ /// Transient now-playing fields (e.g. feedback for a refused remote
1146
+ /// command). Outranks the formatter and the override while active, so live
1147
+ /// metadata can't stomp it mid-flash. Reverted by a NATIVE timer — JS
1148
+ /// timers pause with a backgrounded host on Android, and the lock screen
1149
+ /// is exactly the backgrounded case — and cleared early on track change.
1150
+ @MainActor private var nowPlayingFlash: NowPlayingUpdate?
1151
+ @MainActor private var nowPlayingFlashRevert: DispatchWorkItem?
1152
+
1153
+ /// Pending debounced audio-session release (see `playerShouldReleaseSession`).
1154
+ @MainActor private var sessionReleaseWork: DispatchWorkItem?
1155
+
1156
+ public func flashNowPlaying(update: NowPlayingUpdate, durationMs: Double) throws {
1157
+ onMainActor {
1158
+ nowPlayingFlashRevert?.cancel()
1159
+ nowPlayingFlash = update
1160
+ let revert = DispatchWorkItem { [weak self] in
1161
+ guard let self else { return }
1162
+ self.nowPlayingFlash = nil
1163
+ self.nowPlayingFlashRevert = nil
1164
+ self.applyNowPlayingMetadata()
1165
+ }
1166
+ nowPlayingFlashRevert = revert
1167
+ DispatchQueue.main.asyncAfter(deadline: .now() + durationMs / 1000, execute: revert)
1168
+ applyNowPlayingMetadata()
1169
+ }
1170
+ }
1171
+
1172
+ public func clearNowPlayingFlash() throws {
1173
+ onMainActor {
1174
+ guard nowPlayingFlash != nil else { return }
1175
+ cancelNowPlayingFlash()
1176
+ applyNowPlayingMetadata()
1177
+ }
1178
+ }
1179
+
1180
+ @MainActor
1181
+ private func cancelNowPlayingFlash() {
1182
+ nowPlayingFlashRevert?.cancel()
1183
+ nowPlayingFlashRevert = nil
1184
+ nowPlayingFlash = nil
1185
+ }
1186
+
808
1187
  public func getNowPlaying() throws -> NowPlayingMetadata? {
809
1188
  onMainActor {
810
1189
  guard let track = player?.currentTrack else { return nil }
1190
+ let flash = nowPlayingFlash
811
1191
  let override = nowPlayingOverride
812
- return NowPlayingMetadata(
813
- elapsedTime: player?.currentTime,
814
- title: override?.title ?? track.title,
815
- album: track.album,
816
- artist: override?.artist ?? track.artist,
817
- duration: track.duration,
818
- artwork: nowPlayingArtwork(for: track),
819
- description: track.description,
820
- mediaId: track.src ?? track.url,
821
- genre: track.genre,
822
- rating: nil,
1192
+ return makeNowPlayingMetadata(
1193
+ track: track,
1194
+ title: flash?.title ?? override?.title ?? track.title,
1195
+ artist: flash?.artist ?? override?.artist ?? track.artist,
1196
+ album: flash?.album ?? override?.album ?? track.album,
823
1197
  )
824
1198
  }
825
1199
  }
@@ -829,36 +1203,47 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
829
1203
  /// Falls back to the raw artwork string only if it's not an SF Symbol (which JS can't load).
830
1204
  private func nowPlayingArtwork(for track: Track) -> String? {
831
1205
  if let uri = track.artworkSource?.uri { return uri }
832
- guard let artwork = track.artwork else { return nil }
1206
+ guard let artwork = track.artwork?.url else { return nil }
833
1207
  return SFSymbolRenderer.isSFSymbol(artwork) ? nil : artwork
834
1208
  }
835
1209
 
836
- /// Applies the current now playing metadata (with override if set) to NowPlayingInfoCenter and notifies JS.
1210
+ /// Builds the JS-facing now-playing metadata for the given rendered text lines.
837
1211
  @MainActor
838
- private func applyNowPlayingMetadata() {
839
- guard let track = player?.currentTrack else { return }
840
- let override = nowPlayingOverride
841
-
842
- let nowPlaying = NowPlayingMetadata(
1212
+ private func makeNowPlayingMetadata(
1213
+ track: Track, title: String, artist: String?, album: String?,
1214
+ ) -> NowPlayingMetadata {
1215
+ NowPlayingMetadata(
843
1216
  elapsedTime: player?.currentTime,
844
- title: override?.title ?? track.title,
845
- album: track.album,
846
- artist: override?.artist ?? track.artist,
1217
+ title: title,
1218
+ album: album,
1219
+ artist: artist,
847
1220
  duration: track.duration,
848
1221
  artwork: nowPlayingArtwork(for: track),
849
1222
  description: track.description,
850
1223
  mediaId: track.src ?? track.url,
851
1224
  genre: track.genre,
852
- rating: nil,
853
1225
  )
1226
+ }
854
1227
 
855
- // Update NowPlayingInfoCenter with override values
856
- if let override {
857
- player?.nowPlayingInfoController.set(keyValue: MediaItemProperty.title(override.title ?? track.title))
858
- player?.nowPlayingInfoController.set(keyValue: MediaItemProperty.artist(override.artist ?? track.artist))
859
- }
860
-
861
- onNowPlayingChanged(nowPlaying)
1228
+ /// Re-renders the now-playing surface via `NowPlayingUpdater`, handing it the current playback
1229
+ /// signals, override, and formatter. Called on every track / metadata / playback-state change.
1230
+ @MainActor
1231
+ private func applyNowPlayingMetadata() {
1232
+ guard let player, let track = player.currentTrack else { return }
1233
+ // Classify a stall by connectivity so the formatter can show "No internet connection" vs
1234
+ // "Reconnecting…" without a separate read. nil while not stalled.
1235
+ let stalled: StallReason? =
1236
+ player.isStalled ? (networkMonitor.getOnline() ? .buffering : .offline) : nil
1237
+ player.nowPlayingUpdater.render(
1238
+ track: track,
1239
+ timedMetadata: latestTimedMetadata,
1240
+ playWhenReady: player.playWhenReady,
1241
+ stalled: stalled,
1242
+ error: player.coordinator.nitroPlaybackError(),
1243
+ flash: nowPlayingFlash,
1244
+ override: nowPlayingMetadataEnabled ? nowPlayingOverride : nil,
1245
+ formatter: nowPlayingMetadataEnabled ? nowPlayingMetadataFormatter : nil,
1246
+ )
862
1247
  }
863
1248
 
864
1249
  // MARK: - Network
@@ -893,6 +1278,63 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
893
1278
 
894
1279
  // MARK: - External Audio Output
895
1280
 
1281
+ /// Observes audio-session interruptions (phone calls, or another app such as
1282
+ /// Music/Spotify starting playback). iOS pauses our AVPlayer underneath us but
1283
+ /// doesn't update our playback state, so without this the UI stays stuck on
1284
+ /// "playing". On `.began` we reflect a real pause; on `.ended` we resume if we
1285
+ /// were playing and the system says we should.
1286
+ private func setupInterruptionObserver() {
1287
+ if let observer = interruptionObserver {
1288
+ NotificationCenter.default.removeObserver(observer)
1289
+ }
1290
+
1291
+ interruptionObserver = NotificationCenter.default.addObserver(
1292
+ forName: AVAudioSession.interruptionNotification,
1293
+ object: nil,
1294
+ queue: .main,
1295
+ ) { [weak self] notification in
1296
+ self?.handleAudioSessionInterruption(notification)
1297
+ }
1298
+ }
1299
+
1300
+ private func handleAudioSessionInterruption(_ notification: Notification) {
1301
+ guard
1302
+ let info = notification.userInfo,
1303
+ let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
1304
+ let type = AVAudioSession.InterruptionType(rawValue: typeValue)
1305
+ else { return }
1306
+
1307
+ switch type {
1308
+ case .began:
1309
+ // iOS 17+ fires a `.began` (with no matching `.ended`) when an output
1310
+ // route such as headphones disappears. Reflecting a pause here would
1311
+ // strand resume-intent state with nothing to clear it, and the
1312
+ // route-change observer already handles the output change — so ignore it.
1313
+ if #available(iOS 17.0, *),
1314
+ let reasonValue = info[AVAudioSessionInterruptionReasonKey] as? UInt,
1315
+ AVAudioSession.InterruptionReason(rawValue: reasonValue) == .routeDisconnected
1316
+ {
1317
+ return
1318
+ }
1319
+ onMainActor { player?.handleInterruptionBegan() }
1320
+ case .ended:
1321
+ let shouldResume = (info[AVAudioSessionInterruptionOptionKey] as? UInt)
1322
+ .map { AVAudioSession.InterruptionOptions(rawValue: $0).contains(.shouldResume) }
1323
+ ?? false
1324
+ onMainActor {
1325
+ // Reactivate the possibly-deactivated session before resuming — but
1326
+ // only when a resume will happen: activating while staying paused
1327
+ // grabs a non-mixable session with nothing to release it.
1328
+ if shouldResume, player?.willResumeAfterInterruption == true {
1329
+ try? AVAudioSession.sharedInstance().setActive(true)
1330
+ }
1331
+ player?.handleInterruptionEnded(shouldResume: shouldResume)
1332
+ }
1333
+ @unknown default:
1334
+ break
1335
+ }
1336
+ }
1337
+
896
1338
  /// Sets up observer for audio route changes
897
1339
  private func setupRouteChangeObserver() {
898
1340
  if let observer = routeChangeObserver {
@@ -903,58 +1345,97 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
903
1345
  forName: AVAudioSession.routeChangeNotification,
904
1346
  object: nil,
905
1347
  queue: .main,
1348
+ ) { [weak self] notification in
1349
+ guard let self else { return }
1350
+ // The previous output disappeared (headphones unplugged, a Bluetooth
1351
+ // speaker powered off). Pause rather than abruptly resume out of the
1352
+ // built-in speaker — the platform convention every media app follows.
1353
+ // A deliberate pause (clears play intent), not an interruption: the user
1354
+ // restarts playback themselves once they want it.
1355
+ if let reasonValue = notification.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt,
1356
+ AVAudioSession.RouteChangeReason(rawValue: reasonValue) == .oldDeviceUnavailable
1357
+ {
1358
+ // Also cancels a pending interruption auto-resume: unplugging during
1359
+ // a call must not blast the built-in speaker when the call ends.
1360
+ self.onMainActor { self.player?.handleRouteDisconnected() }
1361
+ }
1362
+ if let output = self.getCurrentOutput() {
1363
+ self.onOutputChanged(output)
1364
+ }
1365
+ }
1366
+ }
1367
+
1368
+ /// Observes audio-server (`mediaserverd`) resets. When it fires, the session
1369
+ /// category is cleared and every AVPlayer/session handle is invalid — a
1370
+ /// long-running stream would otherwise go permanently silent until the app
1371
+ /// restarts.
1372
+ private func setupMediaServicesResetObserver() {
1373
+ if let observer = mediaServicesResetObserver {
1374
+ NotificationCenter.default.removeObserver(observer)
1375
+ }
1376
+ mediaServicesResetObserver = NotificationCenter.default.addObserver(
1377
+ forName: AVAudioSession.mediaServicesWereResetNotification,
1378
+ object: nil,
1379
+ queue: .main,
906
1380
  ) { [weak self] _ in
907
- guard let self, let output = self.getCurrentOutput() else { return }
908
- self.onIosOutputChanged(output)
1381
+ guard let self else { return }
1382
+ // Re-apply our category (the reset cleared it), then have the player
1383
+ // recreate itself and reconnect the current stream.
1384
+ self.onMainActor {
1385
+ self.applyAudioSessionCategory()
1386
+ self.player?.handleMediaServicesReset()
1387
+ }
909
1388
  }
910
1389
  }
911
1390
 
912
1391
  /// Gets the current audio output info
913
- private func getCurrentOutput() -> IosOutput? {
1392
+ private func getCurrentOutput() -> Output? {
914
1393
  let session = AVAudioSession.sharedInstance()
915
1394
  guard let output = session.currentRoute.outputs.first else { return nil }
916
1395
 
917
- let (outputType, external): (IosOutputType, Bool) = switch output.portType {
1396
+ let (outputType, external): (OutputType, Bool) = switch output.portType {
918
1397
  case .builtInSpeaker:
919
- (.builtInSpeaker, false)
1398
+ (.speaker, false)
920
1399
  case .builtInReceiver:
921
- (.builtInReceiver, false)
1400
+ (.receiver, false)
922
1401
  case .airPlay:
923
1402
  (.airplay, true)
924
- case .bluetoothA2DP:
925
- (.bluetoothA2dp, true)
926
- case .bluetoothHFP:
927
- (.bluetoothHfp, true)
928
- case .bluetoothLE:
929
- (.bluetoothLe, true)
1403
+ case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
1404
+ (.bluetooth, true)
930
1405
  case .headphones:
931
1406
  (.headphones, true)
932
1407
  case .carAudio:
933
- (.carAudio, true)
1408
+ (.car, true)
934
1409
  case .HDMI:
935
1410
  (.hdmi, true)
936
1411
  case .usbAudio:
937
- (.usbAudio, true)
1412
+ (.usb, true)
938
1413
  default:
939
1414
  (.other, true)
940
1415
  }
941
1416
 
942
- return IosOutput(type: outputType, name: output.portName, external: external)
1417
+ return Output(type: outputType, name: output.portName, external: external)
943
1418
  }
944
1419
 
945
- public func getIosOutput() throws -> IosOutput? {
1420
+ public func getOutput() throws -> Output? {
946
1421
  getCurrentOutput()
947
1422
  }
948
1423
 
949
- public func openIosOutputPicker() throws {
1424
+ public func supportsOutputSwitcher() throws -> Bool {
1425
+ // iOS can always present the system route picker (AVRoutePickerView).
1426
+ true
1427
+ }
1428
+
1429
+ public func openOutputPicker() throws {
950
1430
  DispatchQueue.main.async { [weak self] in
1431
+ guard let self else { return }
951
1432
  guard let windowScene = UIApplication.shared.connectedScenes
952
1433
  .compactMap({ $0 as? UIWindowScene })
953
1434
  .first(where: { $0.activationState == .foregroundActive }),
954
1435
  let window = windowScene.windows.first(where: { $0.isKeyWindow }),
955
1436
  var topController = window.rootViewController
956
1437
  else {
957
- self?.logger.debug("openIosOutputPicker: Could not find active window scene")
1438
+ self.logger.error("openOutputPicker: no active window scene / root view controller")
958
1439
  return
959
1440
  }
960
1441
 
@@ -963,25 +1444,52 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
963
1444
  topController = presented
964
1445
  }
965
1446
 
966
- let routePicker = AVRoutePickerView(frame: .zero)
967
- routePicker.isHidden = true
1447
+ // AVRoutePickerView has no public API to present its picker
1448
+ // programmatically, so we add it to the hierarchy and tap its internal
1449
+ // button. Two requirements that are easy to get wrong:
1450
+ // 1. The view must be *visible* to the system (a `isHidden` source view
1451
+ // suppresses the presentation), so we keep it transparent instead.
1452
+ // 2. The internal button can be nested below the top-level subviews on
1453
+ // modern iOS, so we search recursively rather than only direct children.
1454
+ let routePicker = AVRoutePickerView(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
1455
+ routePicker.alpha = 0.01
968
1456
  topController.view.addSubview(routePicker)
969
1457
 
970
- // Find the button inside AVRoutePickerView and trigger it
971
- for subview in routePicker.subviews {
972
- if let button = subview as? UIButton {
973
- button.sendActions(for: .touchUpInside)
974
- break
975
- }
1458
+ let button = Self.firstButton(in: routePicker)
1459
+ self.logger.notice(
1460
+ "openOutputPicker: hierarchy=\(Self.describeHierarchy(routePicker), privacy: .public), buttonFound=\(button != nil, privacy: .public)",
1461
+ )
1462
+
1463
+ if let button {
1464
+ button.sendActions(for: .touchUpInside)
1465
+ } else {
1466
+ self.logger.error("openOutputPicker: no UIButton found inside AVRoutePickerView")
976
1467
  }
977
1468
 
978
- // Remove after a delay to ensure picker has opened
979
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
1469
+ // Keep the source view in the hierarchy long enough for the picker to
1470
+ // present (on iPad it anchors a popover to this view).
1471
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
980
1472
  routePicker.removeFromSuperview()
981
1473
  }
982
1474
  }
983
1475
  }
984
1476
 
1477
+ /// Depth-first search for the first `UIButton` inside a view tree.
1478
+ private static func firstButton(in view: UIView) -> UIButton? {
1479
+ if let button = view as? UIButton { return button }
1480
+ for subview in view.subviews {
1481
+ if let button = firstButton(in: subview) { return button }
1482
+ }
1483
+ return nil
1484
+ }
1485
+
1486
+ /// Compact description of a view tree, e.g. `AVRoutePickerView[UIButton[UIImageView]]`.
1487
+ private static func describeHierarchy(_ view: UIView) -> String {
1488
+ let name = String(describing: type(of: view))
1489
+ let children = view.subviews.map(describeHierarchy).joined(separator: ", ")
1490
+ return children.isEmpty ? name : "\(name)[\(children)]"
1491
+ }
1492
+
985
1493
  // MARK: - Equalizer (unsupported on iOS)
986
1494
 
987
1495
  public func getEqualizerSettings() throws -> EqualizerSettings? {
@@ -1026,12 +1534,28 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1026
1534
  extension HybridAudioBrowser: TrackPlayerCallbacks {
1027
1535
  public func playerDidChangePlayback(_ playback: Playback) {
1028
1536
  onPlaybackChanged(playback)
1537
+ // Re-render so the formatter reflects the new state — notably the error line on a failure (the
1538
+ // coordinator sets playbackError before this fires). Redundant updates are deduped on publish.
1539
+ applyNowPlayingMetadata()
1029
1540
  }
1030
1541
 
1031
1542
  public func playerDidChangeActiveTrack(_ event: PlaybackActiveTrackChangedEvent) {
1032
- // Clear now playing override when track changes (matches Kotlin behavior)
1543
+ // Clear now playing override + flash + live metadata when track changes
1544
+ // (matches Kotlin behavior; a flash must not carry over to a new track)
1033
1545
  nowPlayingOverride = nil
1034
- activeTrackChangedEmitter.emit(event)
1546
+ cancelNowPlayingFlash()
1547
+ latestTimedMetadata = nil
1548
+ // Hydrate the active track's `favorited` from the authoritative set so JS
1549
+ // consumers (useActiveTrack) match getActiveTrack()'s hydrated value — the
1550
+ // coordinator emits the raw queue track, whose flag isn't kept in sync.
1551
+ let hydrated = PlaybackActiveTrackChangedEvent(
1552
+ lastIndex: event.lastIndex,
1553
+ lastTrack: event.lastTrack,
1554
+ lastPosition: event.lastPosition,
1555
+ index: event.index,
1556
+ track: event.track.map(browserManager.hydrateFavorite),
1557
+ )
1558
+ activeTrackChangedEmitter.emit(hydrated)
1035
1559
  // Also notify now playing changed when track changes
1036
1560
  applyNowPlayingMetadata()
1037
1561
  }
@@ -1040,12 +1564,61 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1040
1564
  onPlaybackProgressUpdated(event)
1041
1565
  }
1042
1566
 
1567
+ public func playerDidFirePlaybackInterval() {
1568
+ onPlaybackInterval()
1569
+ }
1570
+
1043
1571
  public func playerDidChangePlayWhenReady(_ playWhenReady: Bool) {
1572
+ // Intent to play again is the earliest cancel signal for a pending session release.
1573
+ if playWhenReady { cancelSessionRelease() }
1044
1574
  onPlaybackPlayWhenReadyChanged(PlaybackPlayWhenReadyChangedEvent(playWhenReady: playWhenReady))
1045
1575
  }
1046
1576
 
1047
1577
  public func playerDidChangePlayingState(_ state: PlayingState) {
1048
1578
  onPlaybackPlayingState(state)
1579
+ // Lazily activate the audio session the moment playback actually produces — or is buffering to
1580
+ // produce — output. This (not setup) is where we grab the session, so launching the app never
1581
+ // interrupts other apps' audio; the interruption only happens when the user truly starts
1582
+ // playback, which is correct. Keyed off play *state*, not `playWhenReady`, because the intent
1583
+ // can be set at setup (before any track) while output only begins here. Idempotent:
1584
+ // `setActive(true)` is a no-op when the session is already active.
1585
+ if state.playing || state.buffering {
1586
+ try? AVAudioSession.sharedInstance().setActive(true)
1587
+ // The now-playing session's activation request at link time predates this
1588
+ // (the audio session was still inactive, and the system may have declined);
1589
+ // re-request now so the surface can appear even if playback never produces
1590
+ // audio (first-ever load failing on a dead stream).
1591
+ player?.nowPlayingInfoController.reactivateSessionIfNeeded()
1592
+ // Output resumed — cancel any pending release armed by an earlier stop.
1593
+ cancelSessionRelease()
1594
+ }
1595
+ // Re-render so the formatter reacts to a stall starting or recovering (`stalled`) — a buffering
1596
+ // flag change that may not transition the coordinator state.
1597
+ applyNowPlayingMetadata()
1598
+ }
1599
+
1600
+ public func playerShouldReleaseSession() {
1601
+ // Release the audio session so other apps' audio (Spotify, a podcast) can resume — debounced,
1602
+ // because iOS dislikes deactivating while our audio is still trailing off, and a quick
1603
+ // pause→resume shouldn't thrash the session. The coordinator only requests this on a genuine
1604
+ // stop (not an interruption, not mid-retry); a re-activation cancels it (see
1605
+ // `playerDidChangePlayWhenReady` and the activation above). Mirrors the now-playing flash timer.
1606
+ sessionReleaseWork?.cancel()
1607
+ let release = DispatchWorkItem { [weak self] in
1608
+ guard let self, self.player?.playWhenReady == false else { return }
1609
+ self.sessionReleaseWork = nil
1610
+ // `.notifyOthersOnDeactivation` signals other apps they may resume; without it the session
1611
+ // is released but nobody is told, so the user's audio would not come back.
1612
+ try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
1613
+ }
1614
+ sessionReleaseWork = release
1615
+ DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: release)
1616
+ }
1617
+
1618
+ @MainActor
1619
+ private func cancelSessionRelease() {
1620
+ sessionReleaseWork?.cancel()
1621
+ sessionReleaseWork = nil
1049
1622
  }
1050
1623
 
1051
1624
  public func playerDidEndQueue(_ event: PlaybackQueueEndedEvent) {
@@ -1061,6 +1634,7 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1061
1634
  }
1062
1635
 
1063
1636
  public func playerDidChangeShuffleEnabled(_ enabled: Bool) {
1637
+ shuffleChangedEmitter.emit(enabled)
1064
1638
  onPlaybackShuffleModeChanged(enabled)
1065
1639
  }
1066
1640
 
@@ -1076,7 +1650,10 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1076
1650
  public func playerDidReceiveTimedMetadata(_ groups: [AVTimedMetadataGroup]) {
1077
1651
  for group in groups {
1078
1652
  if let metadata = TimedMetadata.from(items: group.items) {
1653
+ latestTimedMetadata = metadata
1079
1654
  onTimedMetadata(metadata)
1655
+ // Re-render the now-playing so the formatter can reflect the live song.
1656
+ applyNowPlayingMetadata()
1080
1657
  }
1081
1658
  }
1082
1659
  }
@@ -1193,10 +1770,6 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1193
1770
  remoteSeek(position: position)
1194
1771
  }
1195
1772
 
1196
- public func remoteSetRating(rating _: Any) {
1197
- // TODO: Convert rating to RemoteSetRatingEvent
1198
- }
1199
-
1200
1773
  public func remotePlayId(id: String, index: Int?) {
1201
1774
  let event = RemotePlayIdEvent(id: id, index: index.map { Double($0) })
1202
1775
  if let handler = handleRemotePlayId {
@@ -1215,57 +1788,92 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1215
1788
  }
1216
1789
  }
1217
1790
 
1218
- /// Handles an INPlayMediaIntent by searching for the given term and starting playback.
1219
- ///
1220
- /// Host apps should call this from their `INPlayMediaIntentHandling` implementation.
1221
- /// Uses the browser's search route to find tracks, then queues and plays them.
1222
- ///
1223
- /// - Parameters:
1224
- /// - searchTerm: The search query from Siri.
1225
- /// - completion: Called with `true` if tracks were found and queued, `false` otherwise.
1226
- public func handlePlayMediaIntent(searchTerm: String, completion: @escaping @Sendable (Bool) -> Void) {
1227
- Task {
1791
+ /// Static so the Siri intent handler can call it before the shared instance
1792
+ /// exists: it waits on the `playerAndConfiguredBrowser` gate, which resolves
1793
+ /// once RN boots. Uses the resolved `browser`, never `self`.
1794
+ static func handlePlayMediaIntent(criteria: MediaIntentCriteria, completion: @escaping @Sendable (Bool) -> Void) {
1795
+ Task { @MainActor in
1796
+ guard let (browser, player) = await playerAndConfiguredBrowser.wait(timeout: .seconds(8)) else {
1797
+ Logger(subsystem: "com.audiobrowser", category: "AudioBrowser")
1798
+ .error("handlePlayMediaIntent: browser/player not ready within budget")
1799
+ completion(false)
1800
+ return
1801
+ }
1802
+ // No-criteria intent ("play «app»") → resume. The gate must NOT block
1803
+ // this: the gate blocks *finding* content, never *hearing* the active or
1804
+ // persisted track. The gate is checked below, for the search branch only.
1805
+ if criteria.isResume {
1806
+ if player.currentTrack != nil {
1807
+ browser.logger.info("resume: warm — playing current track")
1808
+ player.play()
1809
+ browser.showNowPlayingRequestedEmitter.emit(())
1810
+ completion(true)
1811
+ } else if let state = browser.playbackStateStore.load() {
1812
+ browser.logger.info("resume: cold — restoring persisted track")
1813
+ let track = state.track.toNitro()
1814
+ let startMs = (track.live == true) ? nil : state.positionMs
1815
+ // Match Android resume: re-expand the full queue from the track's contextual
1816
+ // URL (parent container → siblings + selected index). Fall back to the single
1817
+ // track when the url isn't contextual or expansion fails.
1818
+ if let url = state.track.url,
1819
+ let expanded = try? await browser.browserManager.expandQueueFromContextualUrl(url)
1820
+ {
1821
+ player.setQueue(expanded.tracks, initialIndex: expanded.selectedIndex, startPositionMs: startMs, playWhenReady: true)
1822
+ } else {
1823
+ player.setQueue([track], initialIndex: 0, startPositionMs: startMs, playWhenReady: true)
1824
+ }
1825
+ browser.showNowPlayingRequestedEmitter.emit(())
1826
+ completion(true)
1827
+ } else {
1828
+ browser.logger.error("resume: nothing playing and nothing persisted → no-op")
1829
+ completion(false) // nothing playing and nothing persisted
1830
+ }
1831
+ return
1832
+ }
1833
+
1834
+ // Assemble the Nitro SearchParams here (MainActor) from the criteria's
1835
+ // Sendable fields, so the structured mode/genre/… reach the request like
1836
+ // Android. Today the API still text-searches `q`; the rest is forward-compat.
1837
+ let params = SearchParams(
1838
+ mode: criteria.searchMode.flatMap { SearchMode(fromString: $0) },
1839
+ query: criteria.query,
1840
+ genre: criteria.genre,
1841
+ artist: criteria.artist,
1842
+ album: criteria.album,
1843
+ title: criteria.title,
1844
+ playlist: criteria.playlist,
1845
+ // .currentlyPlaying can't reach here — isResume routed it to resume.
1846
+ reference: criteria.reference == .my ? .my : .unknown,
1847
+ )
1848
+
1849
+ // Search is *finding* new content — refused when the gate decides this
1850
+ // request is gated (resume above is unaffected: the gate never blocks
1851
+ // hearing, and the gate check stays after the resume check).
1852
+ let outcome = await browser.gateDecision(
1853
+ for: NativeGateRequest(reason: .search, path: nil, search: params),
1854
+ )
1855
+ guard !outcome.gated else {
1856
+ browser.logger.info("handlePlayMediaIntent: search refused — gated")
1857
+ browser.onGate(GateEvent(reason: .search))
1858
+ completion(false)
1859
+ return
1860
+ }
1861
+
1228
1862
  do {
1229
- let resolved = try await self.browserManager.search(searchTerm)
1230
- guard let tracks = resolved.children, !tracks.isEmpty else {
1863
+ guard let tracks = try await browser.browserManager.searchPlayable(params) else {
1231
1864
  completion(false)
1232
1865
  return
1233
1866
  }
1234
- await MainActor.run {
1235
- self.getPlayer()?.setQueue(tracks, initialIndex: 0, playWhenReady: true)
1236
- }
1867
+ player.setQueue(tracks, initialIndex: 0, playWhenReady: true)
1868
+ browser.showNowPlayingRequestedEmitter.emit(())
1237
1869
  completion(true)
1238
1870
  } catch {
1239
- self.logger.error("handlePlayMediaIntent failed: \(error.localizedDescription)")
1871
+ browser.logger.error("handlePlayMediaIntent failed: \(error.localizedDescription)")
1240
1872
  completion(false)
1241
1873
  }
1242
1874
  }
1243
1875
  }
1244
1876
 
1245
- public func remoteLike() {
1246
- if let handler = handleRemoteLike {
1247
- handler()
1248
- } else {
1249
- onRemoteLike()
1250
- }
1251
- }
1252
-
1253
- public func remoteDislike() {
1254
- if let handler = handleRemoteDislike {
1255
- handler()
1256
- } else {
1257
- onRemoteDislike()
1258
- }
1259
- }
1260
-
1261
- public func remoteBookmark() {
1262
- if let handler = handleRemoteBookmark {
1263
- handler()
1264
- } else {
1265
- onRemoteBookmark()
1266
- }
1267
- }
1268
-
1269
1877
  public func remoteChangeRepeatMode(mode: RepeatMode) {
1270
1878
  // Apply the repeat mode change from CarPlay/lock screen
1271
1879
  try? setRepeatMode(mode: mode)