react-native-audio-browser 0.1.0 → 0.2.0-next.1

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 (902) hide show
  1. package/AudioBrowser.podspec +1 -1
  2. package/README.md +180 -2
  3. package/android/build.gradle +10 -0
  4. package/android/gradle.properties +1 -1
  5. package/android/src/main/AndroidManifest.xml +15 -0
  6. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +401 -141
  7. package/android/src/main/java/com/audiobrowser/Callbacks.kt +4 -6
  8. package/android/src/main/java/com/audiobrowser/Service.kt +67 -18
  9. package/android/src/main/java/com/audiobrowser/browser/ArtworkResolutionRegistry.kt +37 -0
  10. package/android/src/main/java/com/audiobrowser/browser/BrowseArtworkRegistry.kt +34 -0
  11. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +443 -178
  12. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +300 -0
  13. package/android/src/main/java/com/audiobrowser/browser/JsonArtwork.kt +79 -0
  14. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +41 -4
  15. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +43 -0
  16. package/android/src/main/java/com/audiobrowser/browser/TrackLoadHandler.kt +4 -4
  17. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +35 -0
  18. package/android/src/main/java/com/audiobrowser/http/RequestConfigBuilder.kt +109 -97
  19. package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +6 -10
  20. package/android/src/main/java/com/audiobrowser/model/PlayerSetupOptions.kt +40 -8
  21. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +31 -37
  22. package/android/src/main/java/com/audiobrowser/player/AutomaticBufferManager.kt +4 -1
  23. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +119 -0
  24. package/android/src/main/java/com/audiobrowser/player/DynamicLoadControl.kt +18 -1
  25. package/android/src/main/java/com/audiobrowser/player/EqualizerController.kt +153 -0
  26. package/android/src/main/java/com/audiobrowser/player/InterceptingPlayer.kt +141 -0
  27. package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +15 -3
  28. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +415 -131
  29. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +154 -276
  30. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +348 -0
  31. package/android/src/main/java/com/audiobrowser/player/PlaybackErrorClassifier.kt +129 -0
  32. package/android/src/main/java/com/audiobrowser/player/PlaybackStateMachine.kt +85 -0
  33. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +27 -5
  34. package/android/src/main/java/com/audiobrowser/player/PlaybackTimer.kt +56 -0
  35. package/android/src/main/java/com/audiobrowser/player/Player.kt +452 -576
  36. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +20 -0
  37. package/android/src/main/java/com/audiobrowser/player/PlayerEngineFactory.kt +130 -0
  38. package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +190 -67
  39. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +117 -53
  40. package/android/src/main/java/com/audiobrowser/player/SleepTimer.kt +37 -3
  41. package/android/src/main/java/com/audiobrowser/player/SleepTimerManager.kt +80 -0
  42. package/android/src/main/java/com/audiobrowser/player/StuckRecoveryPolicy.kt +50 -0
  43. package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +84 -34
  44. package/android/src/main/java/com/audiobrowser/player/VolumeFader.kt +81 -0
  45. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +141 -0
  46. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +37 -0
  47. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +41 -0
  48. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +209 -0
  49. package/android/src/main/java/com/audiobrowser/util/ArtworkContentProvider.kt +161 -0
  50. package/android/src/main/java/com/audiobrowser/util/ArtworkUris.kt +28 -0
  51. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +26 -0
  52. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +24 -0
  53. package/android/src/main/java/com/audiobrowser/util/CoilArtworkLoader.kt +49 -0
  54. package/android/src/main/java/com/audiobrowser/util/CoilArtworkLoaderHolder.kt +36 -0
  55. package/android/src/main/java/com/audiobrowser/util/CoilBitmapLoader.kt +23 -300
  56. package/android/src/main/java/com/audiobrowser/util/EqualizerManager.kt +13 -23
  57. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +2 -2
  58. package/android/src/main/java/com/audiobrowser/util/MetadataAdapter.kt +4 -3
  59. package/android/src/main/java/com/audiobrowser/util/OutputMonitor.kt +211 -0
  60. package/android/src/main/java/com/audiobrowser/util/OutputSwitcher.kt +58 -0
  61. package/android/src/main/java/com/audiobrowser/util/PlayingStateFactory.kt +2 -1
  62. package/android/src/main/java/com/audiobrowser/util/RatingFavorites.kt +14 -0
  63. package/android/src/main/java/com/audiobrowser/util/SvgArtworkRenderer.kt +6 -111
  64. package/android/src/main/java/com/audiobrowser/util/SystemVolumeMonitor.kt +22 -8
  65. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +111 -80
  66. package/android/src/main/res/values/strings.xml +4 -0
  67. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +166 -0
  68. package/android/src/test/java/com/audiobrowser/browser/ArtworkResolutionRegistryTest.kt +46 -0
  69. package/android/src/test/java/com/audiobrowser/browser/BrowseArtworkRegistryTest.kt +39 -0
  70. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerBuildApiRequestTest.kt +114 -0
  71. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +141 -0
  72. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +4 -0
  73. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +177 -0
  74. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +54 -0
  75. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +96 -0
  76. package/android/src/test/java/com/audiobrowser/browser/SimpleRouterTest.kt +1 -2
  77. package/android/src/test/java/com/audiobrowser/http/RequestConfigBuilderTest.kt +72 -0
  78. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +159 -0
  79. package/android/src/test/java/com/audiobrowser/player/DynamicLoadControlTest.kt +56 -0
  80. package/android/src/test/java/com/audiobrowser/player/EqualizerControllerTest.kt +216 -0
  81. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +442 -0
  82. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +191 -0
  83. package/android/src/test/java/com/audiobrowser/player/PlaybackStateMachineTest.kt +198 -0
  84. package/android/src/test/java/com/audiobrowser/player/PlaybackTimerTest.kt +137 -0
  85. package/android/src/test/java/com/audiobrowser/player/PlayerEngineFactoryTest.kt +36 -0
  86. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +268 -0
  87. package/android/src/test/java/com/audiobrowser/player/SessionErrorMaskTest.kt +68 -0
  88. package/android/src/test/java/com/audiobrowser/player/StuckRecoveryPolicyTest.kt +40 -0
  89. package/android/src/test/java/com/audiobrowser/player/TransformingDataSourceTest.kt +193 -0
  90. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +107 -0
  91. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +73 -0
  92. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +75 -0
  93. package/android/src/test/java/com/audiobrowser/tls/CertFixtures.kt +18 -0
  94. package/android/src/test/java/com/audiobrowser/util/ArtworkContentProviderTest.kt +163 -0
  95. package/android/src/test/java/com/audiobrowser/util/ArtworkUrisTest.kt +34 -0
  96. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +28 -0
  97. package/android/src/test/java/com/audiobrowser/util/CoilArtworkLoaderHolderTest.kt +53 -0
  98. package/android/src/test/java/com/audiobrowser/util/CoilArtworkLoaderTest.kt +58 -0
  99. package/android/src/test/java/com/audiobrowser/util/FakeImageLoader.kt +57 -0
  100. package/android/src/test/java/com/audiobrowser/util/PlayingStateFactoryTest.kt +40 -0
  101. package/android/src/test/java/com/audiobrowser/util/RatingFavoritesTest.kt +34 -0
  102. package/android/src/test/java/com/audiobrowser/util/TrackFactoryBrowseTest.kt +50 -0
  103. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +63 -0
  104. package/android/src/test/java/com/audiobrowser/util/TrackFactoryRatingTest.kt +35 -0
  105. package/android/src/test/resources/certs/isrg-root-x1.pem +31 -0
  106. package/android/src/test/resources/certs/letsencrypt-r13.der +0 -0
  107. package/android/src/test/resources/certs/letsencrypt-r13.pem +29 -0
  108. package/android/src/test/resources/certs/stationplaylist-leaf.pem +34 -0
  109. package/ios/Browser/BrowserConfig.swift +41 -11
  110. package/ios/Browser/BrowserManager+URLResolution.swift +213 -93
  111. package/ios/Browser/BrowserManager.swift +303 -110
  112. package/ios/Browser/BrowserPathHelper.swift +32 -3
  113. package/ios/Browser/JsonModels.swift +287 -66
  114. package/ios/Browser/MediaResolveComposer.swift +114 -0
  115. package/ios/Browser/SearchDrillIn.swift +44 -0
  116. package/ios/Browser/SectionScope.swift +35 -0
  117. package/ios/Browser/TabBarEntries.swift +15 -0
  118. package/ios/CarPlay/CPInterfaceController+Safe.swift +126 -0
  119. package/ios/CarPlay/CarPlayArtworkResolver.swift +6 -6
  120. package/ios/CarPlay/CarPlayController.swift +739 -149
  121. package/ios/CarPlay/CarPlayImageLoader.swift +57 -8
  122. package/ios/CarPlay/CarPlayListItemFactory.swift +44 -35
  123. package/ios/CarPlay/CarPlayNowPlayingManager.swift +180 -36
  124. package/ios/CarPlay/ImageRowItem+CarPlay.swift +19 -0
  125. package/ios/CarPlay/RNABAudioBrowser.h +15 -10
  126. package/ios/CarPlay/RNABAudioBrowser.m +15 -14
  127. package/ios/CarPlay/RNABCarPlaySceneDelegate.m +2 -0
  128. package/ios/CarPlay/RNABMediaAddHandler.swift +75 -0
  129. package/ios/CarPlay/RNABMediaAffinityHandler.swift +69 -0
  130. package/ios/CarPlay/RNABMediaIntentHandler.swift +53 -43
  131. package/ios/Extension/Capability+RemoteCommand.swift +7 -9
  132. package/ios/Extension/ChapterMetadata+AVFoundation.swift +1 -1
  133. package/ios/Extension/NavigationError+Conversion.swift +8 -1
  134. package/ios/Extension/PlayerCapabilities+Favorite.swift +20 -0
  135. package/ios/Extension/RequestConfig+Copying.swift +31 -0
  136. package/ios/Extension/ResolvedTrack+Copying.swift +10 -4
  137. package/ios/Extension/TimedMetadata+AVFoundation.swift +1 -1
  138. package/ios/Extension/Track+AVPlayer.swift +4 -4
  139. package/ios/Extension/Track+Copying.swift +9 -3
  140. package/ios/Extension/TrackMetadata+AVFoundation.swift +1 -1
  141. package/ios/Extension/Variant+Artwork.swift +49 -0
  142. package/ios/HybridAudioBrowser.swift +750 -173
  143. package/ios/Model/NitroTypeStubs.swift +299 -224
  144. package/ios/Model/PlayerUpdateOptions.swift +23 -23
  145. package/ios/Model/RemoteCommand.swift +6 -6
  146. package/ios/Model/TrackPlayerError.swift +82 -18
  147. package/ios/NowPlayingInfo/NowPlayingInfoController.swift +99 -121
  148. package/ios/NowPlayingInfo/SilentAudioPrimer.swift +73 -0
  149. package/ios/Observer/PlayerItemNotificationObserver.swift +45 -5
  150. package/ios/Observer/PlayerItemPropertyObserver.swift +27 -4
  151. package/ios/Observer/PlayerTimeObserver.swift +3 -53
  152. package/ios/Option/SessionCategories.swift +33 -11
  153. package/ios/Player/AirPlayStallJudgement.swift +25 -0
  154. package/ios/Player/EndOfTrackJudgement.swift +32 -0
  155. package/ios/Player/LoadSeekCoordinator.swift +16 -1
  156. package/ios/Player/MediaIntentCriteria.swift +139 -0
  157. package/ios/Player/MediaLoader.swift +28 -10
  158. package/ios/Player/NowPlayingUpdater.swift +119 -26
  159. package/ios/Player/PersistedPlaybackState.swift +11 -0
  160. package/ios/Player/PlaybackCoordinator.swift +282 -51
  161. package/ios/Player/PlaybackCoordinatorCallbacks.swift +6 -1
  162. package/ios/Player/PlaybackEffectHandler.swift +19 -9
  163. package/ios/Player/PlaybackErrorHandler.swift +97 -13
  164. package/ios/Player/PlaybackStateMachine.swift +18 -13
  165. package/ios/Player/PlaybackStateStore.swift +27 -0
  166. package/ios/Player/PlaybackTimer.swift +61 -0
  167. package/ios/Player/PlayingStateManager.swift +3 -2
  168. package/ios/Player/QueueManager.swift +67 -61
  169. package/ios/Player/RetryManager.swift +97 -21
  170. package/ios/Player/SleepTimerHandling.swift +8 -0
  171. package/ios/Player/SleepTimerManager.swift +38 -5
  172. package/ios/Player/VolumeFader.swift +77 -0
  173. package/ios/RemoteCommand/RemoteCommandController.swift +31 -15
  174. package/ios/Tests/AirPlayStallJudgementTests.swift +52 -0
  175. package/ios/Tests/BrowserPathHelperTests.swift +28 -0
  176. package/ios/Tests/CarPlayArtworkResolverTests.swift +39 -39
  177. package/ios/Tests/ChapterMetadataAVFoundationTests.swift +2 -2
  178. package/ios/Tests/EndOfTrackJudgementTests.swift +35 -0
  179. package/ios/Tests/JsonModelsTests.swift +58 -0
  180. package/ios/Tests/LoadSeekCoordinatorTests.swift +38 -7
  181. package/ios/Tests/MediaIntentCriteriaTests.swift +170 -0
  182. package/ios/Tests/MediaLoaderAssetOptionsTests.swift +46 -0
  183. package/ios/Tests/MediaLoaderTests.swift +23 -8
  184. package/ios/Tests/MediaResolveTests.swift +126 -0
  185. package/ios/Tests/OnceValueTests.swift +79 -0
  186. package/ios/Tests/PlaybackCoordinatorTests.swift +784 -13
  187. package/ios/Tests/PlaybackErrorHandlerTests.swift +380 -46
  188. package/ios/Tests/PlaybackStateMachineTests.swift +42 -10
  189. package/ios/Tests/PlaybackStateStoreTests.swift +48 -0
  190. package/ios/Tests/PlaybackTimerTests.swift +55 -0
  191. package/ios/Tests/PlayingStateManagerTests.swift +57 -0
  192. package/ios/Tests/QueueManagerTests.swift +149 -18
  193. package/ios/Tests/ResolvedTrackCopyingTests.swift +15 -8
  194. package/ios/Tests/RetryManagerTests.swift +89 -0
  195. package/ios/Tests/SearchDrillInTests.swift +63 -0
  196. package/ios/Tests/SectionScopeTests.swift +82 -0
  197. package/ios/Tests/SleepTimerManagerTests.swift +104 -0
  198. package/ios/Tests/Support/TestTypes.swift +61 -20
  199. package/ios/Tests/TabBarEntriesTests.swift +71 -0
  200. package/ios/Tests/TrackSelectorTests.swift +15 -15
  201. package/ios/TrackPlayer.swift +380 -58
  202. package/ios/TrackPlayerCallbacks.swift +0 -6
  203. package/ios/TrackSelector.swift +5 -5
  204. package/ios/Util/ArtworkImageFetcher.swift +4 -2
  205. package/ios/Util/NetworkMonitor.swift +7 -3
  206. package/ios/Util/OnceValue.swift +34 -0
  207. package/lib/commonjs/features/browser-config.js +227 -0
  208. package/lib/commonjs/features/browser-config.js.map +1 -0
  209. package/lib/commonjs/features/browser.js +59 -158
  210. package/lib/commonjs/features/browser.js.map +1 -1
  211. package/lib/commonjs/features/carConnection.js +42 -0
  212. package/lib/commonjs/features/carConnection.js.map +1 -0
  213. package/lib/commonjs/features/errors.js +34 -2
  214. package/lib/commonjs/features/errors.js.map +1 -1
  215. package/lib/commonjs/features/gate.js +149 -0
  216. package/lib/commonjs/features/gate.js.map +1 -0
  217. package/lib/commonjs/features/index.js +22 -0
  218. package/lib/commonjs/features/index.js.map +1 -1
  219. package/lib/commonjs/features/metadata.js.map +1 -1
  220. package/lib/commonjs/features/nowPlaying.js +35 -0
  221. package/lib/commonjs/features/nowPlaying.js.map +1 -1
  222. package/lib/commonjs/features/output.js +37 -23
  223. package/lib/commonjs/features/output.js.map +1 -1
  224. package/lib/commonjs/features/playback/controls.js +9 -0
  225. package/lib/commonjs/features/playback/controls.js.map +1 -1
  226. package/lib/commonjs/features/playback/index.js +11 -0
  227. package/lib/commonjs/features/playback/index.js.map +1 -1
  228. package/lib/commonjs/features/playback/progress.js +28 -12
  229. package/lib/commonjs/features/playback/progress.js.map +1 -1
  230. package/lib/commonjs/features/playback/trackPlaybackTime.js +92 -0
  231. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -0
  232. package/lib/commonjs/features/playback/volume.js +1 -1
  233. package/lib/commonjs/features/player/options.js +49 -13
  234. package/lib/commonjs/features/player/options.js.map +1 -1
  235. package/lib/commonjs/features/player/setup.js +249 -3
  236. package/lib/commonjs/features/player/setup.js.map +1 -1
  237. package/lib/commonjs/features/queue/queue.js +10 -3
  238. package/lib/commonjs/features/queue/queue.js.map +1 -1
  239. package/lib/commonjs/features/remoteControls.js +1 -12
  240. package/lib/commonjs/features/remoteControls.js.map +1 -1
  241. package/lib/commonjs/features/sleepTimer.js +9 -4
  242. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  243. package/lib/commonjs/native.js +5 -0
  244. package/lib/commonjs/native.js.map +1 -1
  245. package/lib/commonjs/native.web.js +4 -1
  246. package/lib/commonjs/native.web.js.map +1 -1
  247. package/lib/commonjs/utils/LazyNativeEmitter.js +0 -3
  248. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  249. package/lib/commonjs/utils/NativeUpdatedValue.js +0 -3
  250. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  251. package/lib/commonjs/utils/artwork.js +31 -0
  252. package/lib/commonjs/utils/artwork.js.map +1 -0
  253. package/lib/commonjs/web/NativeAudioBrowser.js +301 -124
  254. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  255. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +71 -0
  256. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -0
  257. package/lib/commonjs/web/TrackPlayer/PlaybackTimer.js +34 -0
  258. package/lib/commonjs/web/TrackPlayer/PlaybackTimer.js.map +1 -0
  259. package/lib/commonjs/web/TrackPlayer/Player.js +90 -26
  260. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  261. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +21 -0
  262. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js.map +1 -0
  263. package/lib/commonjs/web/TrackPlayer/QueueManager.js +273 -0
  264. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -0
  265. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +158 -0
  266. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -0
  267. package/lib/commonjs/web/TrackPlayer/RepeatMode.js +1 -1
  268. package/lib/commonjs/web/TrackPlayer/RepeatMode.js.map +1 -1
  269. package/lib/commonjs/web/TrackPlayer/SleepTimer.js +34 -3
  270. package/lib/commonjs/web/TrackPlayer/SleepTimer.js.map +1 -1
  271. package/lib/commonjs/web/TrackPlayer/VolumeFader.js +75 -0
  272. package/lib/commonjs/web/TrackPlayer/VolumeFader.js.map +1 -0
  273. package/lib/commonjs/web/TrackPlayer/index.js +13 -5
  274. package/lib/commonjs/web/TrackPlayer/index.js.map +1 -1
  275. package/lib/commonjs/web/browser/BrowserManager.js +176 -50
  276. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  277. package/lib/commonjs/web/browser/NavigationErrorManager.js +3 -1
  278. package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
  279. package/lib/commonjs/web/browser/SearchManager.js +11 -9
  280. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  281. package/lib/commonjs/web/browser/assertBrowsePageShape.js +22 -0
  282. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -0
  283. package/lib/commonjs/web/browser/classifyTrackNavigation.js +41 -0
  284. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -0
  285. package/lib/commonjs/web/browser/parseSearchResponse.js +20 -0
  286. package/lib/commonjs/web/browser/parseSearchResponse.js.map +1 -0
  287. package/lib/commonjs/web/http/HttpClient.js +4 -23
  288. package/lib/commonjs/web/http/HttpClient.js.map +1 -1
  289. package/lib/commonjs/web/http/RequestConfigBuilder.js +107 -58
  290. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  291. package/lib/commonjs/web/playbackErrorKind.js +57 -0
  292. package/lib/commonjs/web/playbackErrorKind.js.map +1 -0
  293. package/lib/commonjs/web/player/MediaSessionManager.js +121 -0
  294. package/lib/commonjs/web/player/MediaSessionManager.js.map +1 -0
  295. package/lib/commonjs/web/player/NowPlayingManager.js +6 -5
  296. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  297. package/lib/commonjs/web/player/OptionsManager.js +9 -17
  298. package/lib/commonjs/web/player/OptionsManager.js.map +1 -1
  299. package/lib/commonjs/web/player/RemoteCommandController.js +96 -0
  300. package/lib/commonjs/web/player/RemoteCommandController.js.map +1 -0
  301. package/lib/module/features/browser-config.js +221 -0
  302. package/lib/module/features/browser-config.js.map +1 -0
  303. package/lib/module/features/browser.js +53 -156
  304. package/lib/module/features/browser.js.map +1 -1
  305. package/lib/module/features/carConnection.js +37 -0
  306. package/lib/module/features/carConnection.js.map +1 -0
  307. package/lib/module/features/errors.js +34 -2
  308. package/lib/module/features/errors.js.map +1 -1
  309. package/lib/module/features/gate.js +144 -0
  310. package/lib/module/features/gate.js.map +1 -0
  311. package/lib/module/features/index.js +2 -0
  312. package/lib/module/features/index.js.map +1 -1
  313. package/lib/module/features/metadata.js.map +1 -1
  314. package/lib/module/features/nowPlaying.js +33 -0
  315. package/lib/module/features/nowPlaying.js.map +1 -1
  316. package/lib/module/features/output.js +32 -19
  317. package/lib/module/features/output.js.map +1 -1
  318. package/lib/module/features/playback/controls.js +8 -0
  319. package/lib/module/features/playback/controls.js.map +1 -1
  320. package/lib/module/features/playback/index.js +1 -0
  321. package/lib/module/features/playback/index.js.map +1 -1
  322. package/lib/module/features/playback/progress.js +28 -12
  323. package/lib/module/features/playback/progress.js.map +1 -1
  324. package/lib/module/features/playback/trackPlaybackTime.js +88 -0
  325. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -0
  326. package/lib/module/features/playback/volume.js +1 -1
  327. package/lib/module/features/player/options.js +48 -13
  328. package/lib/module/features/player/options.js.map +1 -1
  329. package/lib/module/features/player/setup.js +249 -3
  330. package/lib/module/features/player/setup.js.map +1 -1
  331. package/lib/module/features/queue/queue.js +10 -3
  332. package/lib/module/features/queue/queue.js.map +1 -1
  333. package/lib/module/features/remoteControls.js +0 -11
  334. package/lib/module/features/remoteControls.js.map +1 -1
  335. package/lib/module/features/sleepTimer.js +9 -4
  336. package/lib/module/features/sleepTimer.js.map +1 -1
  337. package/lib/module/native.js +5 -0
  338. package/lib/module/native.js.map +1 -1
  339. package/lib/module/native.web.js +4 -1
  340. package/lib/module/native.web.js.map +1 -1
  341. package/lib/module/utils/LazyNativeEmitter.js +0 -3
  342. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  343. package/lib/module/utils/NativeUpdatedValue.js +0 -3
  344. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  345. package/lib/module/utils/artwork.js +26 -0
  346. package/lib/module/utils/artwork.js.map +1 -0
  347. package/lib/module/web/NativeAudioBrowser.js +302 -125
  348. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  349. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +67 -0
  350. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -0
  351. package/lib/module/web/TrackPlayer/PlaybackTimer.js +29 -0
  352. package/lib/module/web/TrackPlayer/PlaybackTimer.js.map +1 -0
  353. package/lib/module/web/TrackPlayer/Player.js +90 -26
  354. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  355. package/lib/module/web/TrackPlayer/PlayingStateFactory.js +17 -0
  356. package/lib/module/web/TrackPlayer/PlayingStateFactory.js.map +1 -0
  357. package/lib/module/web/TrackPlayer/QueueManager.js +269 -0
  358. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -0
  359. package/lib/module/web/TrackPlayer/QueuePlayer.js +154 -0
  360. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -0
  361. package/lib/module/web/TrackPlayer/RepeatMode.js +1 -1
  362. package/lib/module/web/TrackPlayer/RepeatMode.js.map +1 -1
  363. package/lib/module/web/TrackPlayer/SleepTimer.js +34 -3
  364. package/lib/module/web/TrackPlayer/SleepTimer.js.map +1 -1
  365. package/lib/module/web/TrackPlayer/VolumeFader.js +70 -0
  366. package/lib/module/web/TrackPlayer/VolumeFader.js.map +1 -0
  367. package/lib/module/web/TrackPlayer/index.js +2 -1
  368. package/lib/module/web/TrackPlayer/index.js.map +1 -1
  369. package/lib/module/web/browser/BrowserManager.js +176 -50
  370. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  371. package/lib/module/web/browser/NavigationErrorManager.js +3 -1
  372. package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
  373. package/lib/module/web/browser/SearchManager.js +11 -9
  374. package/lib/module/web/browser/SearchManager.js.map +1 -1
  375. package/lib/module/web/browser/assertBrowsePageShape.js +18 -0
  376. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -0
  377. package/lib/module/web/browser/classifyTrackNavigation.js +38 -0
  378. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -0
  379. package/lib/module/web/browser/parseSearchResponse.js +16 -0
  380. package/lib/module/web/browser/parseSearchResponse.js.map +1 -0
  381. package/lib/module/web/http/HttpClient.js +4 -23
  382. package/lib/module/web/http/HttpClient.js.map +1 -1
  383. package/lib/module/web/http/RequestConfigBuilder.js +107 -58
  384. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  385. package/lib/module/web/playbackErrorKind.js +52 -0
  386. package/lib/module/web/playbackErrorKind.js.map +1 -0
  387. package/lib/module/web/player/MediaSessionManager.js +116 -0
  388. package/lib/module/web/player/MediaSessionManager.js.map +1 -0
  389. package/lib/module/web/player/NowPlayingManager.js +7 -5
  390. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  391. package/lib/module/web/player/OptionsManager.js +9 -17
  392. package/lib/module/web/player/OptionsManager.js.map +1 -1
  393. package/lib/module/web/player/RemoteCommandController.js +92 -0
  394. package/lib/module/web/player/RemoteCommandController.js.map +1 -0
  395. package/lib/typescript/src/features/browser-config.d.ts +12 -0
  396. package/lib/typescript/src/features/browser-config.d.ts.map +1 -0
  397. package/lib/typescript/src/features/browser.d.ts +40 -1
  398. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  399. package/lib/typescript/src/features/carConnection.d.ts +20 -0
  400. package/lib/typescript/src/features/carConnection.d.ts.map +1 -0
  401. package/lib/typescript/src/features/errors.d.ts +58 -3
  402. package/lib/typescript/src/features/errors.d.ts.map +1 -1
  403. package/lib/typescript/src/features/gate.d.ts +77 -0
  404. package/lib/typescript/src/features/gate.d.ts.map +1 -0
  405. package/lib/typescript/src/features/index.d.ts +2 -0
  406. package/lib/typescript/src/features/index.d.ts.map +1 -1
  407. package/lib/typescript/src/features/metadata.d.ts +6 -4
  408. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  409. package/lib/typescript/src/features/nowPlaying.d.ts +27 -0
  410. package/lib/typescript/src/features/nowPlaying.d.ts.map +1 -1
  411. package/lib/typescript/src/features/output.d.ts +28 -18
  412. package/lib/typescript/src/features/output.d.ts.map +1 -1
  413. package/lib/typescript/src/features/playback/controls.d.ts +5 -0
  414. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  415. package/lib/typescript/src/features/playback/index.d.ts +1 -0
  416. package/lib/typescript/src/features/playback/index.d.ts.map +1 -1
  417. package/lib/typescript/src/features/playback/progress.d.ts +3 -0
  418. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  419. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +34 -0
  420. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -0
  421. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  422. package/lib/typescript/src/features/player/options.d.ts +92 -93
  423. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  424. package/lib/typescript/src/features/player/setup.d.ts +206 -167
  425. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  426. package/lib/typescript/src/features/queue/queue.d.ts +9 -2
  427. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  428. package/lib/typescript/src/features/remoteControls.d.ts +0 -13
  429. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  430. package/lib/typescript/src/features/sleepTimer.d.ts +18 -3
  431. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  432. package/lib/typescript/src/index.d.ts.map +1 -1
  433. package/lib/typescript/src/native.d.ts +1 -2
  434. package/lib/typescript/src/native.d.ts.map +1 -1
  435. package/lib/typescript/src/specs/audio-browser.nitro.d.ts +17 -225
  436. package/lib/typescript/src/specs/audio-browser.nitro.d.ts.map +1 -1
  437. package/lib/typescript/src/types/browser-native.d.ts +9 -3
  438. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  439. package/lib/typescript/src/types/browser-nodes.d.ts +145 -13
  440. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  441. package/lib/typescript/src/types/browser.d.ts +349 -85
  442. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  443. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +0 -2
  444. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  445. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +0 -2
  446. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  447. package/lib/typescript/src/utils/artwork.d.ts +19 -0
  448. package/lib/typescript/src/utils/artwork.d.ts.map +1 -0
  449. package/lib/typescript/src/utils/resolveAssetSource.d.ts.map +1 -1
  450. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +3 -1
  451. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +24 -10
  452. package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +6 -6
  453. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +1 -11
  454. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +33 -1
  455. package/nitrogen/generated/android/c++/JArtworkVariants.hpp +61 -0
  456. package/nitrogen/generated/android/c++/JBrowseResult.hpp +5 -0
  457. package/nitrogen/generated/android/c++/JCarPlayNowPlayingButton.hpp +3 -3
  458. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +6 -0
  459. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +58 -0
  460. package/nitrogen/generated/android/c++/JFavoritesMatchMode.hpp +58 -0
  461. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +100 -0
  462. package/nitrogen/generated/android/c++/JFormattedNavigationError.hpp +3 -2
  463. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.hpp +112 -0
  464. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +121 -0
  465. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +123 -0
  466. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +133 -0
  467. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +121 -0
  468. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.hpp +139 -0
  469. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +6 -0
  470. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.hpp +134 -0
  471. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -0
  472. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +6 -0
  473. 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
  474. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +6 -0
  475. package/nitrogen/generated/android/c++/JFunc_void_GateEvent.hpp +79 -0
  476. package/nitrogen/generated/android/c++/JFunc_void_NowPlayingMetadata.hpp +0 -10
  477. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +9 -4
  478. package/nitrogen/generated/android/c++/JFunc_void_Output.hpp +80 -0
  479. package/nitrogen/generated/android/c++/JFunc_void_Playback.hpp +2 -0
  480. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +6 -0
  481. package/nitrogen/generated/android/c++/JFunc_void_PlaybackErrorEvent.hpp +2 -0
  482. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +10 -10
  483. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +6 -0
  484. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +6 -0
  485. package/nitrogen/generated/android/c++/JGate.hpp +62 -0
  486. package/nitrogen/generated/android/c++/JGateDecision.hpp +64 -0
  487. package/nitrogen/generated/android/c++/{JThumbsRating.hpp → JGateEvent.hpp} +16 -15
  488. package/nitrogen/generated/android/c++/JGateReason.hpp +58 -0
  489. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +236 -212
  490. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +25 -22
  491. package/nitrogen/generated/android/c++/JIOSOptions.hpp +95 -0
  492. package/nitrogen/generated/android/c++/JImageRowItem.hpp +33 -3
  493. package/nitrogen/generated/android/c++/JMediaReference.hpp +58 -0
  494. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +33 -1
  495. package/nitrogen/generated/android/c++/{JPartialAndroidSetupPlayerOptions.hpp → JNativeAndroidSetupOptions.hpp} +10 -10
  496. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +61 -28
  497. package/nitrogen/generated/android/c++/JNativeGateRequest.hpp +74 -0
  498. package/nitrogen/generated/android/c++/{JPartialIOSSetupPlayerOptions.hpp → JNativeIOSSetupOptions.hpp} +10 -10
  499. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +20 -9
  500. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +182 -0
  501. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +16 -19
  502. package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +6 -0
  503. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +1 -11
  504. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +96 -0
  505. package/nitrogen/generated/android/c++/JNotificationButton.hpp +3 -3
  506. package/nitrogen/generated/android/c++/JNowPlayingMetadata.hpp +3 -17
  507. package/nitrogen/generated/android/c++/JNowPlayingUpdate.hpp +7 -3
  508. package/nitrogen/generated/android/c++/JOptions.hpp +14 -9
  509. package/nitrogen/generated/android/c++/{JIosOutput.hpp → JOutput.hpp} +15 -15
  510. package/nitrogen/generated/android/c++/JOutputType.hpp +82 -0
  511. package/nitrogen/generated/android/c++/JPlayback.hpp +2 -0
  512. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +6 -0
  513. package/nitrogen/generated/android/c++/JPlaybackError.hpp +18 -3
  514. package/nitrogen/generated/android/c++/JPlaybackErrorEvent.hpp +2 -0
  515. package/nitrogen/generated/android/c++/JPlaybackErrorKind.hpp +76 -0
  516. package/nitrogen/generated/android/c++/JPlaybackState.hpp +3 -3
  517. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +11 -5
  518. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +23 -5
  519. package/nitrogen/generated/android/c++/JRetryConfig.hpp +11 -7
  520. package/nitrogen/generated/android/c++/JSearchMode.hpp +27 -9
  521. package/nitrogen/generated/android/c++/JSearchParams.hpp +9 -3
  522. package/nitrogen/generated/android/c++/JStallReason.hpp +58 -0
  523. package/nitrogen/generated/android/c++/JTrack.hpp +23 -5
  524. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +6 -0
  525. package/nitrogen/generated/android/c++/JTrackRequest.hpp +93 -0
  526. package/nitrogen/generated/android/c++/JTransformableRequestConfig.hpp +14 -1
  527. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +26 -0
  528. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +71 -0
  529. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.cpp +41 -0
  530. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +121 -0
  531. package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.cpp +26 -0
  532. package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.hpp +70 -0
  533. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidAudioContentType.kt +4 -4
  534. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +2 -8
  535. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkRequestConfig.kt +10 -4
  536. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkVariants.kt +41 -0
  537. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayNowPlayingButton.kt +3 -3
  538. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{ThumbsRating.kt → FavoriteConfig.kt} +6 -6
  539. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{RatingType.kt → FavoritesMatchMode.kt} +5 -10
  540. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNowPlayingParams.kt +50 -0
  541. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormattedNavigationError.kt +2 -2
  542. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.kt +80 -0
  543. 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
  544. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___Track.kt +80 -0
  545. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.kt +80 -0
  546. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_std__string____Track.kt +80 -0
  547. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.kt +80 -0
  548. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.kt +80 -0
  549. 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
  550. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_void_RemoteSetRatingEvent.kt → Func_void_GateEvent.kt} +9 -9
  551. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_void_IosOutput.kt → Func_void_Output.kt} +9 -9
  552. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_bool.kt +9 -9
  553. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{HeartRating.kt → Gate.kt} +9 -6
  554. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PercentageRating.kt → GateDecision.kt} +9 -6
  555. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{StarRating.kt → GateEvent.kt} +6 -6
  556. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateReason.kt +23 -0
  557. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +103 -109
  558. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +44 -0
  559. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +24 -3
  560. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaReference.kt +23 -0
  561. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaRequestConfig.kt +10 -4
  562. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PartialAndroidSetupPlayerOptions.kt → NativeAndroidSetupOptions.kt} +5 -5
  563. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +18 -6
  564. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{RemoteSetRatingEvent.kt → NativeGateRequest.kt} +12 -6
  565. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PartialIOSSetupPlayerOptions.kt → NativeIOSSetupOptions.kt} +5 -5
  566. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeRouteEntry.kt +4 -4
  567. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeSetupPlayerOptions.kt +66 -0
  568. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeUpdateOptions.kt +6 -6
  569. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorType.kt +3 -1
  570. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +2 -8
  571. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +44 -0
  572. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButton.kt +5 -5
  573. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingMetadata.kt +3 -6
  574. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingUpdate.kt +6 -3
  575. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Options.kt +3 -3
  576. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{IosOutput.kt → Output.kt} +6 -6
  577. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/OutputType.kt +31 -0
  578. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackError.kt +12 -3
  579. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackErrorKind.kt +29 -0
  580. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackState.kt +6 -6
  581. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  582. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -3
  583. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RetryConfig.kt +7 -4
  584. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchMode.kt +12 -6
  585. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchParams.kt +6 -3
  586. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StallReason.kt +23 -0
  587. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +12 -3
  588. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackRequest.kt +44 -0
  589. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TransformableRequestConfig.kt +7 -4
  590. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +53 -0
  591. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.kt +65 -0
  592. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_String_ArtworkVariants.kt +53 -0
  593. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +139 -28
  594. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +1098 -447
  595. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +60 -39
  596. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +170 -112
  597. package/nitrogen/generated/ios/swift/AndroidAudioContentType.swift +8 -8
  598. package/nitrogen/generated/ios/swift/AndroidOptions.swift +2 -12
  599. package/nitrogen/generated/ios/swift/ArtworkRequestConfig.swift +95 -1
  600. package/nitrogen/generated/ios/swift/ArtworkVariants.swift +34 -0
  601. package/nitrogen/generated/ios/swift/CarPlayNowPlayingButton.swift +4 -4
  602. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +29 -0
  603. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +40 -0
  604. package/nitrogen/generated/ios/swift/FormatNowPlayingParams.swift +67 -0
  605. package/nitrogen/generated/ios/swift/FormattedNavigationError.swift +17 -4
  606. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.swift +54 -0
  607. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.swift +69 -0
  608. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___Track.swift +54 -0
  609. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.swift +60 -0
  610. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__optional_std__string____Track.swift +60 -0
  611. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.swift +61 -0
  612. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.swift +61 -0
  613. 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
  614. package/nitrogen/generated/ios/swift/Func_void_GateDecision.swift +46 -0
  615. package/nitrogen/generated/ios/swift/{Func_void_RemoteSetRatingEvent.swift → Func_void_GateEvent.swift} +10 -10
  616. package/nitrogen/generated/ios/swift/{Func_void_IosOutput.swift → Func_void_Output.swift} +10 -10
  617. package/nitrogen/generated/ios/swift/Func_void_TransformableRequestConfig.swift +46 -0
  618. package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
  619. package/nitrogen/generated/ios/swift/Func_void_std__optional_NowPlayingUpdate_.swift +46 -0
  620. package/nitrogen/generated/ios/swift/Func_void_std__optional_std__string_.swift +53 -0
  621. 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
  622. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_Promise_TransformableRequestConfig__.swift +66 -0
  623. package/nitrogen/generated/ios/swift/Func_void_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError____.swift +81 -0
  624. package/nitrogen/generated/ios/swift/Gate.swift +47 -0
  625. package/nitrogen/generated/ios/swift/GateDecision.swift +40 -0
  626. package/nitrogen/generated/ios/swift/GateEvent.swift +29 -0
  627. package/nitrogen/generated/ios/swift/GateReason.swift +40 -0
  628. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +20 -14
  629. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +230 -197
  630. package/nitrogen/generated/ios/swift/IOSOptions.swift +51 -0
  631. package/nitrogen/generated/ios/swift/ImageRowItem.swift +121 -2
  632. package/nitrogen/generated/ios/swift/MediaReference.swift +40 -0
  633. package/nitrogen/generated/ios/swift/MediaRequestConfig.swift +107 -1
  634. package/nitrogen/generated/ios/swift/{PartialAndroidSetupPlayerOptions.swift → NativeAndroidSetupOptions.swift} +5 -5
  635. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +161 -22
  636. package/nitrogen/generated/ios/swift/NativeGateRequest.swift +58 -0
  637. package/nitrogen/generated/ios/swift/{PartialIOSSetupPlayerOptions.swift → NativeIOSSetupOptions.swift} +5 -5
  638. package/nitrogen/generated/ios/swift/NativeRouteEntry.swift +19 -19
  639. package/nitrogen/generated/ios/swift/NativeSetupPlayerOptions.swift +206 -0
  640. package/nitrogen/generated/ios/swift/NativeUpdateOptions.swift +12 -25
  641. package/nitrogen/generated/ios/swift/NavigationErrorType.swift +8 -0
  642. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +1 -30
  643. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +90 -0
  644. package/nitrogen/generated/ios/swift/NotificationButton.swift +4 -4
  645. package/nitrogen/generated/ios/swift/NowPlayingMetadata.swift +1 -48
  646. package/nitrogen/generated/ios/swift/NowPlayingUpdate.swift +19 -1
  647. package/nitrogen/generated/ios/swift/Options.swift +10 -4
  648. package/nitrogen/generated/ios/swift/{IosOutput.swift → Output.swift} +7 -7
  649. package/nitrogen/generated/ios/swift/OutputType.swift +72 -0
  650. package/nitrogen/generated/ios/swift/PlaybackError.swift +43 -2
  651. package/nitrogen/generated/ios/swift/PlaybackErrorKind.swift +64 -0
  652. package/nitrogen/generated/ios/swift/PlaybackState.swift +4 -4
  653. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +27 -8
  654. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +74 -8
  655. package/nitrogen/generated/ios/swift/RetryConfig.swift +35 -4
  656. package/nitrogen/generated/ios/swift/SearchMode.swift +36 -12
  657. package/nitrogen/generated/ios/swift/SearchParams.swift +7 -2
  658. package/nitrogen/generated/ios/swift/StallReason.swift +40 -0
  659. package/nitrogen/generated/ios/swift/Track.swift +74 -8
  660. package/nitrogen/generated/ios/swift/TrackRequest.swift +106 -0
  661. package/nitrogen/generated/ios/swift/TransformableRequestConfig.swift +60 -1
  662. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +18 -0
  663. package/nitrogen/generated/ios/swift/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.swift +19 -0
  664. package/nitrogen/generated/ios/swift/Variant_String_ArtworkVariants.swift +18 -0
  665. package/nitrogen/generated/shared/c++/AndroidAudioContentType.hpp +10 -10
  666. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +1 -12
  667. package/nitrogen/generated/shared/c++/ArtworkRequestConfig.hpp +10 -2
  668. package/nitrogen/generated/shared/c++/ArtworkVariants.hpp +87 -0
  669. package/nitrogen/generated/shared/c++/CarPlayNowPlayingButton.hpp +6 -6
  670. package/nitrogen/generated/shared/c++/EqualizerSettings.hpp +0 -2
  671. package/nitrogen/generated/shared/c++/{StarRating.hpp → FavoriteConfig.hpp} +18 -17
  672. package/nitrogen/generated/shared/c++/FavoritesMatchMode.hpp +76 -0
  673. package/nitrogen/generated/shared/c++/FormatNowPlayingParams.hpp +110 -0
  674. package/nitrogen/generated/shared/c++/FormattedNavigationError.hpp +6 -5
  675. package/nitrogen/generated/shared/c++/{HeartRating.hpp → Gate.hpp} +21 -16
  676. package/nitrogen/generated/shared/c++/{ThumbsRating.hpp → GateDecision.hpp} +23 -17
  677. package/nitrogen/generated/shared/c++/{PercentageRating.hpp → GateEvent.hpp} +18 -17
  678. package/nitrogen/generated/shared/c++/GateReason.hpp +76 -0
  679. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +21 -18
  680. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +44 -35
  681. package/nitrogen/generated/shared/c++/IOSOptions.hpp +91 -0
  682. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +33 -2
  683. package/nitrogen/generated/shared/c++/MediaReference.hpp +76 -0
  684. package/nitrogen/generated/shared/c++/MediaRequestConfig.hpp +10 -2
  685. package/nitrogen/generated/shared/c++/{PartialAndroidSetupPlayerOptions.hpp → NativeAndroidSetupOptions.hpp} +11 -11
  686. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +31 -15
  687. package/nitrogen/generated/shared/c++/NativeGateRequest.hpp +97 -0
  688. package/nitrogen/generated/shared/c++/{PartialIOSSetupPlayerOptions.hpp → NativeIOSSetupOptions.hpp} +11 -11
  689. package/nitrogen/generated/shared/c++/NativeRouteEntry.hpp +5 -5
  690. package/nitrogen/generated/shared/c++/NativeSetupPlayerOptions.hpp +138 -0
  691. package/nitrogen/generated/shared/c++/NativeUpdateOptions.hpp +9 -7
  692. package/nitrogen/generated/shared/c++/NavigationErrorType.hpp +8 -0
  693. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +1 -12
  694. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +92 -0
  695. package/nitrogen/generated/shared/c++/NotificationButton.hpp +8 -8
  696. package/nitrogen/generated/shared/c++/NowPlayingMetadata.hpp +3 -19
  697. package/nitrogen/generated/shared/c++/NowPlayingUpdate.hpp +6 -2
  698. package/nitrogen/generated/shared/c++/Options.hpp +8 -8
  699. package/nitrogen/generated/shared/c++/{IosOutput.hpp → Output.hpp} +18 -18
  700. package/nitrogen/generated/shared/c++/OutputType.hpp +108 -0
  701. package/nitrogen/generated/shared/c++/PlaybackError.hpp +18 -3
  702. package/nitrogen/generated/shared/c++/PlaybackErrorKind.hpp +100 -0
  703. package/nitrogen/generated/shared/c++/PlaybackState.hpp +9 -9
  704. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +9 -6
  705. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +24 -5
  706. package/nitrogen/generated/shared/c++/RetryConfig.hpp +10 -6
  707. package/nitrogen/generated/shared/c++/SearchMode.hpp +39 -15
  708. package/nitrogen/generated/shared/c++/SearchParams.hpp +9 -2
  709. package/nitrogen/generated/shared/c++/StallReason.hpp +76 -0
  710. package/nitrogen/generated/shared/c++/Track.hpp +24 -5
  711. package/nitrogen/generated/shared/c++/TrackRequest.hpp +93 -0
  712. package/nitrogen/generated/shared/c++/TransformableRequestConfig.hpp +6 -2
  713. package/package.json +16 -16
  714. package/src/codegen-no-config-variant.test.ts +70 -0
  715. package/src/features/browser-config.test.ts +110 -0
  716. package/src/features/browser-config.ts +293 -0
  717. package/src/features/browser.test.ts +19 -0
  718. package/src/features/browser.ts +58 -201
  719. package/src/features/carConnection.ts +36 -0
  720. package/src/features/errors.ts +68 -2
  721. package/src/features/gate.test.ts +72 -0
  722. package/src/features/gate.ts +155 -0
  723. package/src/features/index.ts +2 -0
  724. package/src/features/metadata.ts +6 -12
  725. package/src/features/nowPlaying.ts +36 -0
  726. package/src/features/output.ts +35 -22
  727. package/src/features/playback/controls.ts +8 -0
  728. package/src/features/playback/index.ts +1 -0
  729. package/src/features/playback/progress.ts +31 -12
  730. package/src/features/playback/trackPlaybackTime.test.ts +66 -0
  731. package/src/features/playback/trackPlaybackTime.ts +106 -0
  732. package/src/features/playback/volume.ts +1 -1
  733. package/src/features/player/options.test.ts +38 -0
  734. package/src/features/player/options.ts +140 -75
  735. package/src/features/player/setup.test.ts +117 -0
  736. package/src/features/player/setup.ts +390 -175
  737. package/src/features/queue/queue.test.ts +36 -0
  738. package/src/features/queue/queue.ts +15 -4
  739. package/src/features/remoteControls.ts +0 -23
  740. package/src/features/sleepTimer.ts +23 -4
  741. package/src/native.ts +5 -0
  742. package/src/native.web.ts +4 -1
  743. package/src/specs/audio-browser.nitro.ts +131 -41
  744. package/src/types/browser-native.ts +19 -6
  745. package/src/types/browser-nodes.ts +160 -13
  746. package/src/types/browser.ts +377 -89
  747. package/src/utils/LazyNativeEmitter.ts +0 -2
  748. package/src/utils/NativeUpdatedValue.ts +0 -2
  749. package/src/utils/artwork.ts +30 -0
  750. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +301 -0
  751. package/src/web/NativeAudioBrowser.ts +368 -157
  752. package/src/web/SimpleRouter.test.ts +9 -9
  753. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +139 -0
  754. package/src/web/TrackPlayer/PlaybackStateMachine.ts +82 -0
  755. package/src/web/TrackPlayer/PlaybackTimer.test.ts +67 -0
  756. package/src/web/TrackPlayer/PlaybackTimer.ts +33 -0
  757. package/src/web/TrackPlayer/Player.dispatch.test.ts +229 -0
  758. package/src/web/TrackPlayer/Player.load.test.ts +86 -0
  759. package/src/web/TrackPlayer/Player.ts +113 -39
  760. package/src/web/TrackPlayer/PlayingStateFactory.test.ts +51 -0
  761. package/src/web/TrackPlayer/PlayingStateFactory.ts +24 -0
  762. package/src/web/TrackPlayer/QueueManager.test.ts +289 -0
  763. package/src/web/TrackPlayer/QueueManager.ts +302 -0
  764. package/src/web/TrackPlayer/QueuePlayer.ts +188 -0
  765. package/src/web/TrackPlayer/RepeatMode.ts +1 -1
  766. package/src/web/TrackPlayer/SleepTimer.ts +38 -3
  767. package/src/web/TrackPlayer/VolumeFader.ts +74 -0
  768. package/src/web/TrackPlayer/index.ts +2 -1
  769. package/src/web/browser/BrowserManager.ts +231 -62
  770. package/src/web/browser/NavigationErrorManager.ts +3 -1
  771. package/src/web/browser/SearchManager.ts +11 -15
  772. package/src/web/browser/__tests__/layer-resolver.test.ts +69 -0
  773. package/src/web/browser/assertBrowsePageShape.test.ts +27 -0
  774. package/src/web/browser/assertBrowsePageShape.ts +27 -0
  775. package/src/web/browser/classifyTrackNavigation.test.ts +41 -0
  776. package/src/web/browser/classifyTrackNavigation.ts +35 -0
  777. package/src/web/browser/parseSearchResponse.test.ts +25 -0
  778. package/src/web/browser/parseSearchResponse.ts +15 -0
  779. package/src/web/http/HttpClient.ts +5 -35
  780. package/src/web/http/RequestConfigBuilder.test.ts +59 -0
  781. package/src/web/http/RequestConfigBuilder.ts +120 -63
  782. package/src/web/playbackErrorKind.test.ts +55 -0
  783. package/src/web/playbackErrorKind.ts +56 -0
  784. package/src/web/player/MediaSessionManager.test.ts +220 -0
  785. package/src/web/player/MediaSessionManager.ts +151 -0
  786. package/src/web/player/NowPlayingManager.test.ts +46 -0
  787. package/src/web/player/NowPlayingManager.ts +6 -5
  788. package/src/web/player/OptionsManager.ts +12 -18
  789. package/src/web/player/RemoteCommandController.test.ts +171 -0
  790. package/src/web/player/RemoteCommandController.ts +166 -0
  791. package/android/src/main/java/com/audiobrowser/player/PlaybackProgressUpdateManager.kt +0 -59
  792. package/android/src/main/java/com/audiobrowser/util/RatingFactory.kt +0 -72
  793. package/android/src/main/java/com/audiobrowser/util/ResolvedTrackFactory.kt +0 -36
  794. package/android/src/test/java/com/audiobrowser/player/PlaybackProgressUpdateManagerTest.kt +0 -146
  795. package/ios/CarPlay/Track+CarPlay.swift +0 -63
  796. package/ios/NowPlayingInfo/NowPlayingInfoCenter.swift +0 -9
  797. package/ios/Player/PlaybackProgressUpdateManager.swift +0 -68
  798. package/ios/Tests/PlaybackProgressUpdateManagerTests.swift +0 -166
  799. package/lib/commonjs/features/rating.js +0 -2
  800. package/lib/commonjs/features/rating.js.map +0 -1
  801. package/lib/commonjs/types/player.js +0 -2
  802. package/lib/commonjs/types/player.js.map +0 -1
  803. package/lib/commonjs/web/SimpleRouter.test.js +0 -174
  804. package/lib/commonjs/web/SimpleRouter.test.js.map +0 -1
  805. package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js +0 -293
  806. package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js.map +0 -1
  807. package/lib/module/features/rating.js +0 -2
  808. package/lib/module/features/rating.js.map +0 -1
  809. package/lib/module/types/player.js +0 -2
  810. package/lib/module/types/player.js.map +0 -1
  811. package/lib/module/web/SimpleRouter.test.js +0 -174
  812. package/lib/module/web/SimpleRouter.test.js.map +0 -1
  813. package/lib/module/web/TrackPlayer/PlaylistPlayer.js +0 -288
  814. package/lib/module/web/TrackPlayer/PlaylistPlayer.js.map +0 -1
  815. package/lib/typescript/src/features/rating.d.ts +0 -14
  816. package/lib/typescript/src/features/rating.d.ts.map +0 -1
  817. package/lib/typescript/src/native.web.d.ts +0 -3
  818. package/lib/typescript/src/native.web.d.ts.map +0 -1
  819. package/lib/typescript/src/types/player.d.ts +0 -64
  820. package/lib/typescript/src/types/player.d.ts.map +0 -1
  821. package/lib/typescript/src/web/NativeAudioBrowser.d.ts +0 -188
  822. package/lib/typescript/src/web/NativeAudioBrowser.d.ts.map +0 -1
  823. package/lib/typescript/src/web/SimpleRouter.d.ts +0 -40
  824. package/lib/typescript/src/web/SimpleRouter.d.ts.map +0 -1
  825. package/lib/typescript/src/web/SimpleRouter.test.d.ts +0 -2
  826. package/lib/typescript/src/web/SimpleRouter.test.d.ts.map +0 -1
  827. package/lib/typescript/src/web/TrackPlayer/Event.d.ts +0 -11
  828. package/lib/typescript/src/web/TrackPlayer/Event.d.ts.map +0 -1
  829. package/lib/typescript/src/web/TrackPlayer/Player.d.ts +0 -59
  830. package/lib/typescript/src/web/TrackPlayer/Player.d.ts.map +0 -1
  831. package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts +0 -39
  832. package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts.map +0 -1
  833. package/lib/typescript/src/web/TrackPlayer/RepeatMode.d.ts +0 -6
  834. package/lib/typescript/src/web/TrackPlayer/RepeatMode.d.ts.map +0 -1
  835. package/lib/typescript/src/web/TrackPlayer/SetupNotCalledError.d.ts +0 -4
  836. package/lib/typescript/src/web/TrackPlayer/SetupNotCalledError.d.ts.map +0 -1
  837. package/lib/typescript/src/web/TrackPlayer/SleepTimer.d.ts +0 -34
  838. package/lib/typescript/src/web/TrackPlayer/SleepTimer.d.ts.map +0 -1
  839. package/lib/typescript/src/web/TrackPlayer/State.d.ts +0 -13
  840. package/lib/typescript/src/web/TrackPlayer/State.d.ts.map +0 -1
  841. package/lib/typescript/src/web/TrackPlayer/index.d.ts +0 -5
  842. package/lib/typescript/src/web/TrackPlayer/index.d.ts.map +0 -1
  843. package/lib/typescript/src/web/browser/BrowserManager.d.ts +0 -154
  844. package/lib/typescript/src/web/browser/BrowserManager.d.ts.map +0 -1
  845. package/lib/typescript/src/web/browser/FavoriteManager.d.ts +0 -42
  846. package/lib/typescript/src/web/browser/FavoriteManager.d.ts.map +0 -1
  847. package/lib/typescript/src/web/browser/NavigationErrorManager.d.ts +0 -40
  848. package/lib/typescript/src/web/browser/NavigationErrorManager.d.ts.map +0 -1
  849. package/lib/typescript/src/web/browser/SearchManager.d.ts +0 -22
  850. package/lib/typescript/src/web/browser/SearchManager.d.ts.map +0 -1
  851. package/lib/typescript/src/web/http/HttpClient.d.ts +0 -25
  852. package/lib/typescript/src/web/http/HttpClient.d.ts.map +0 -1
  853. package/lib/typescript/src/web/http/RequestConfigBuilder.d.ts +0 -98
  854. package/lib/typescript/src/web/http/RequestConfigBuilder.d.ts.map +0 -1
  855. package/lib/typescript/src/web/player/NowPlayingManager.d.ts +0 -34
  856. package/lib/typescript/src/web/player/NowPlayingManager.d.ts.map +0 -1
  857. package/lib/typescript/src/web/player/OptionsManager.d.ts +0 -32
  858. package/lib/typescript/src/web/player/OptionsManager.d.ts.map +0 -1
  859. package/lib/typescript/src/web/util/BrowserPathHelper.d.ts +0 -69
  860. package/lib/typescript/src/web/util/BrowserPathHelper.d.ts.map +0 -1
  861. package/lib/typescript/src/web/util/shuffle.d.ts +0 -8
  862. package/lib/typescript/src/web/util/shuffle.d.ts.map +0 -1
  863. package/nitrogen/generated/android/c++/JAndroidUpdateOptions.hpp +0 -86
  864. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.hpp +0 -147
  865. package/nitrogen/generated/android/c++/JFunc_void_IosOutput.hpp +0 -80
  866. package/nitrogen/generated/android/c++/JFunc_void_RemoteSetRatingEvent.hpp +0 -87
  867. package/nitrogen/generated/android/c++/JHeartRating.hpp +0 -57
  868. package/nitrogen/generated/android/c++/JIosOutputType.hpp +0 -85
  869. package/nitrogen/generated/android/c++/JPartialSetupPlayerOptions.hpp +0 -96
  870. package/nitrogen/generated/android/c++/JPercentageRating.hpp +0 -57
  871. package/nitrogen/generated/android/c++/JRatingType.hpp +0 -73
  872. package/nitrogen/generated/android/c++/JRemoteSetRatingEvent.hpp +0 -66
  873. package/nitrogen/generated/android/c++/JStarRating.hpp +0 -57
  874. package/nitrogen/generated/android/c++/JUpdateOptions.hpp +0 -105
  875. package/nitrogen/generated/android/c++/JVariant_HeartRating_ThumbsRating_StarRating_PercentageRating.cpp +0 -34
  876. package/nitrogen/generated/android/c++/JVariant_HeartRating_ThumbsRating_StarRating_PercentageRating.hpp +0 -105
  877. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidUpdateOptions.kt +0 -50
  878. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IosOutputType.kt +0 -32
  879. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PartialSetupPlayerOptions.kt +0 -47
  880. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/UpdateOptions.kt +0 -53
  881. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_HeartRating_ThumbsRating_StarRating_PercentageRating.kt +0 -77
  882. package/nitrogen/generated/ios/swift/AndroidUpdateOptions.swift +0 -119
  883. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.swift +0 -68
  884. package/nitrogen/generated/ios/swift/HeartRating.swift +0 -29
  885. package/nitrogen/generated/ios/swift/IosOutputType.swift +0 -76
  886. package/nitrogen/generated/ios/swift/PartialSetupPlayerOptions.swift +0 -101
  887. package/nitrogen/generated/ios/swift/PercentageRating.swift +0 -29
  888. package/nitrogen/generated/ios/swift/RatingType.swift +0 -60
  889. package/nitrogen/generated/ios/swift/RemoteSetRatingEvent.swift +0 -58
  890. package/nitrogen/generated/ios/swift/StarRating.swift +0 -29
  891. package/nitrogen/generated/ios/swift/ThumbsRating.swift +0 -29
  892. package/nitrogen/generated/ios/swift/UpdateOptions.swift +0 -143
  893. package/nitrogen/generated/ios/swift/Variant_HeartRating_ThumbsRating_StarRating_PercentageRating.swift +0 -20
  894. package/nitrogen/generated/shared/c++/AndroidUpdateOptions.hpp +0 -109
  895. package/nitrogen/generated/shared/c++/IosOutputType.hpp +0 -112
  896. package/nitrogen/generated/shared/c++/PartialSetupPlayerOptions.hpp +0 -104
  897. package/nitrogen/generated/shared/c++/RatingType.hpp +0 -96
  898. package/nitrogen/generated/shared/c++/RemoteSetRatingEvent.hpp +0 -94
  899. package/nitrogen/generated/shared/c++/UpdateOptions.hpp +0 -111
  900. package/src/features/rating.ts +0 -17
  901. package/src/types/player.ts +0 -69
  902. package/src/web/TrackPlayer/PlaylistPlayer.ts +0 -346
@@ -24,19 +24,45 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
24
24
  /// Shared instance for CarPlay access. Set when HybridAudioBrowser is created.
25
25
  private(set) nonisolated(unsafe) weak static var shared: HybridAudioBrowser?
26
26
 
27
+ /// Fires when a new instance becomes `shared` (e.g. on a JS runtime reload),
28
+ /// so connected external controllers (CarPlay) can re-subscribe — their
29
+ /// listeners sit on the previous instance's emitters and would otherwise go
30
+ /// silent.
31
+ static let instanceChangedEmitter = Emitter<Void>()
32
+
27
33
  // MARK: - Private Properties
28
34
 
29
35
  private var player: TrackPlayer?
30
36
  private let networkMonitor = NetworkMonitor()
37
+ private let playbackStateStore = PlaybackStateStore()
31
38
  let browserManager = BrowserManager()
32
39
  private let trackSelector: TrackSelector
33
40
  private var volumeObservation: NSKeyValueObservation?
34
41
  private var routeChangeObserver: NSObjectProtocol?
42
+ private var interruptionObserver: NSObjectProtocol?
43
+ private var mediaServicesResetObserver: NSObjectProtocol?
44
+ /// Re-applies our resolved audio-session category. Captured at setup so a
45
+ /// media-services reset (which clears the category) can restore it.
46
+ private var applyAudioSessionCategory: () -> Void = {}
35
47
  private var nowPlayingOverride: NowPlayingUpdate?
48
+ /// When false, the now-playing surface uses the raw track fields (override + formatter ignored).
49
+ private var nowPlayingMetadataEnabled = true
50
+ /// Customizes the now-playing title/subtitle from the track + live timed metadata + playback state.
51
+ private var nowPlayingMetadataFormatter: ((_ params: FormatNowPlayingParams) -> Promise<NowPlayingUpdate?>)?
52
+ /// Keep the media session controllable through a terminal error (see `keepSessionAliveOnError`).
53
+ private var keepSessionAliveOnError = true
54
+ /// Initial player state staged before the player exists — from setup options or the imperative
55
+ /// setters called pre-setup. Strict last-write-wins; consumed when the player comes up.
56
+ private var pendingPlayWhenReady: Bool?
57
+ private var pendingRepeatMode: RepeatMode?
58
+ /// Latest live timed (ICY/ID3) metadata, passed to the formatter. Cleared on track change.
59
+ private var latestTimedMetadata: TimedMetadata?
36
60
  private let playerOptions = PlayerUpdateOptions()
37
61
 
38
62
  /// Configured playback rates for the playback-rate capability (for CarPlay rate cycling)
39
63
  var playbackRates: [Double] { playerOptions.playbackRates }
64
+ var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
65
+ var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] { playerOptions.carPlayNowPlayingButtons }
40
66
  private var lastNavigationError: NavigationError? {
41
67
  didSet {
42
68
  // Skip if both nil (no real change)
@@ -63,7 +89,21 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
63
89
  public let queueChangedEmitter = Emitter<[Track]>()
64
90
  public let navigationErrorEmitter = Emitter<NavigationErrorEvent>()
65
91
  public let repeatModeChangedEmitter = Emitter<RepeatModeChangedEvent>()
92
+ public let shuffleChangedEmitter = Emitter<Bool>()
66
93
  public let externalContentChangedEmitter = Emitter<String>()
94
+ /// Signals that the gate's active state changed (set or cleared). Carries
95
+ /// the active flag; per-request chrome is obtained at each serve site via
96
+ /// `gateDecision(for:)`, so the emitter only tells surfaces to re-render.
97
+ public let gateChangedEmitter = Emitter<Bool>()
98
+ /// Fired when a voice media intent (`handlePlayMediaIntent`) successfully
99
+ /// starts playback. CarPlay surfaces the Now Playing template in response, so
100
+ /// the user lands on the playing station (with the rest of the results in Up
101
+ /// Next) rather than the screen they invoked Siri from. No-op where unobserved.
102
+ public let showNowPlayingRequestedEmitter = Emitter<Void>()
103
+ /// Fires after `updateOptions` applies a change, so external-surface managers
104
+ /// (CarPlay) can refresh UI driven by now-runtime-updatable options
105
+ /// (now-playing buttons, Up Next).
106
+ public let playerOptionsChangedEmitter = Emitter<Void>()
67
107
 
68
108
  // MARK: - Thread Safety
69
109
 
@@ -71,9 +111,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
71
111
  /// because DispatchQueue.main.sync blocks the caller — no concurrent access window.
72
112
  private func onMainActor<T>(_ work: @MainActor () -> T) -> T {
73
113
  if Thread.isMainThread {
74
- return MainActor.assumeIsolated { UncheckedSendableBox(value: work()) }.value
114
+ MainActor.assumeIsolated { UncheckedSendableBox(value: work()) }.value
75
115
  } else {
76
- return DispatchQueue.main.sync {
116
+ DispatchQueue.main.sync {
77
117
  MainActor.assumeIsolated { UncheckedSendableBox(value: work()) }.value
78
118
  }
79
119
  }
@@ -81,10 +121,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
81
121
 
82
122
  private func onMainActor<T>(_ work: @MainActor () throws -> T) throws -> T {
83
123
  if Thread.isMainThread {
84
- return try MainActor.assumeIsolated { UncheckedSendableBox(value: try work()) }.value
124
+ try MainActor.assumeIsolated { try UncheckedSendableBox(value: work()) }.value
85
125
  } else {
86
- return try DispatchQueue.main.sync {
87
- try MainActor.assumeIsolated { UncheckedSendableBox(value: try work()) }.value
126
+ try DispatchQueue.main.sync {
127
+ try MainActor.assumeIsolated { try UncheckedSendableBox(value: work()) }.value
88
128
  }
89
129
  }
90
130
  }
@@ -111,9 +151,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
111
151
  }
112
152
 
113
153
  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,
154
+ path: nil, request: nil, requestResolver: nil, browse: nil, browseResolver: nil, media: nil, artwork: nil, nowPlayingArtwork: nil, routes: nil,
155
+ singleTrack: nil, handleTrackLoad: nil,
156
+ androidControllerOfflineError: nil, carPlayLoadingTitle: nil, resolveAlbumUrl: nil, formatNavigationError: nil,
117
157
  ) {
118
158
  didSet {
119
159
  onMainActor { browserManager.config = BrowserConfig(from: configuration) }
@@ -149,6 +189,25 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
149
189
  public var onNavigationError: (NavigationErrorEvent) -> Void = { _ in }
150
190
  public var onFormattedNavigationError: (FormattedNavigationError?) -> Void = { _ in }
151
191
 
192
+ /// Per-request gate decision, set by JS. Native calls it at a serve site to
193
+ /// learn whether a browse path / search interaction should be gated and with
194
+ /// what chrome. The double `Promise` mirrors every other native→JS
195
+ /// value-returning callback (e.g. `searchCallback`): the outer resolves to the
196
+ /// JS-side Promise, the inner to the decision.
197
+ ///
198
+ /// Default fails CLOSED (`gated: true`). This default is only consulted in the
199
+ /// init window where `hasResolver` was already recorded true but JS hasn't
200
+ /// (re)bound the real resolver yet (JS reload / instance churn re-seeding gate
201
+ /// state, e.g. CarPlay re-seeding `isGated` at scene connect). Serving content
202
+ /// in that window would be a fail-open leak — same direction as the
203
+ /// resolver-error path in `gateDecision`. Matches Android's default.
204
+ public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = { _ in
205
+ Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
206
+ }
207
+
208
+ /// Fired when a request is gated (served the gate). Set by JS.
209
+ public var onGate: (GateEvent) -> Void = { _ in }
210
+
152
211
  // MARK: - Player Callbacks
153
212
 
154
213
  public var onChapterMetadata: ([ChapterMetadata]) -> Void = { _ in }
@@ -159,6 +218,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
159
218
  public var onPlaybackPlayWhenReadyChanged: (PlaybackPlayWhenReadyChangedEvent) -> Void = { _ in }
160
219
  public var onPlaybackPlayingState: (PlayingState) -> Void = { _ in }
161
220
  public var onPlaybackProgressUpdated: (PlaybackProgressUpdatedEvent) -> Void = { _ in }
221
+ public var onPlaybackInterval: () -> Void = {}
162
222
  public var onPlaybackQueueEnded: (PlaybackQueueEndedEvent) -> Void = { _ in }
163
223
  public var onPlaybackQueueChanged: ([Track]) -> Void = { _ in }
164
224
  public var onPlaybackRepeatModeChanged: (RepeatModeChangedEvent) -> Void = { _ in }
@@ -168,11 +228,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
168
228
 
169
229
  // MARK: - Remote Callbacks
170
230
 
171
- public var onRemoteBookmark: () -> Void = {}
172
- public var onRemoteDislike: () -> Void = {}
173
231
  public var onRemoteJumpBackward: (RemoteJumpBackwardEvent) -> Void = { _ in }
174
232
  public var onRemoteJumpForward: (RemoteJumpForwardEvent) -> Void = { _ in }
175
- public var onRemoteLike: () -> Void = {}
176
233
  public var onRemoteNext: () -> Void = {}
177
234
  public var onRemotePause: () -> Void = {}
178
235
  public var onRemotePlay: () -> Void = {}
@@ -180,17 +237,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
180
237
  public var onRemotePlaySearch: (RemotePlaySearchEvent) -> Void = { _ in }
181
238
  public var onRemotePrevious: () -> Void = {}
182
239
  public var onRemoteSeek: (RemoteSeekEvent) -> Void = { _ in }
183
- public var onRemoteSetRating: (RemoteSetRatingEvent) -> Void = { _ in }
184
240
  public var onRemoteSkip: (RemoteSkipEvent) -> Void = { _ in }
185
241
  public var onRemoteStop: () -> Void = {}
186
242
 
187
243
  // MARK: - Remote Handlers (optional overrides from JS)
188
244
 
189
- public var handleRemoteBookmark: (() -> Void)?
190
- public var handleRemoteDislike: (() -> Void)?
191
245
  public var handleRemoteJumpBackward: ((RemoteJumpBackwardEvent) -> Void)?
192
246
  public var handleRemoteJumpForward: ((RemoteJumpForwardEvent) -> Void)?
193
- public var handleRemoteLike: (() -> Void)?
194
247
  public var handleRemoteNext: (() -> Void)?
195
248
  public var handleRemotePause: (() -> Void)?
196
249
  public var handleRemotePlay: (() -> Void)?
@@ -208,10 +261,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
208
261
  public var onNowPlayingChanged: (NowPlayingMetadata) -> Void = { _ in }
209
262
  public var onOnlineChanged: (Bool) -> Void = { _ in } {
210
263
  didSet {
211
- networkMonitor.onChanged = { [weak self] isOnline in
212
- self?.onOnlineChanged(isOnline)
213
- }
214
- // Immediately notify current state
264
+ // The monitor->JS bridge is wired centrally in setupPlayer (so it doesn't depend on a JS
265
+ // subscription and can also drive the now-playing re-render + player reconnect). Here we only
266
+ // push the current state to a newly-attached subscriber.
215
267
  onOnlineChanged(networkMonitor.isOnline)
216
268
  }
217
269
  }
@@ -225,7 +277,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
225
277
  }
226
278
  }
227
279
 
228
- public var onIosOutputChanged: (IosOutput) -> Void = { _ in } {
280
+ public var onOutputChanged: (Output) -> Void = { _ in } {
229
281
  didSet {
230
282
  setupRouteChangeObserver()
231
283
  }
@@ -245,6 +297,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
245
297
  HybridAudioBrowser.shared?.player?.destroy()
246
298
  playerAndConfiguredBrowser.reset()
247
299
  HybridAudioBrowser.shared = self
300
+ // Tell connected external controllers (CarPlay) to re-subscribe against
301
+ // this instance. Unconditional: `shared` is weak, so on a JS reload the
302
+ // old instance may already be gone — "shared was nil" does not mean no
303
+ // controller is subscribed to a dead instance's emitters.
304
+ HybridAudioBrowser.instanceChangedEmitter.emit(())
248
305
  }
249
306
  setupEmitterToNitroForwarding()
250
307
  setupBrowserCallbacks()
@@ -319,13 +376,17 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
319
376
  let defaultFormatted = navError.defaultFormatted()
320
377
  if let formatter = onMainActor({ browserManager.config.formatNavigationError }) {
321
378
  let params = FormatNavigationErrorParams(error: navError, defaultFormatted: defaultFormatted, path: path)
322
- // Dispatch to main thread for the Nitro bridge call to avoid C++ noexcept crashes
379
+ // Dispatch to main thread for the Nitro bridge call to avoid C++ noexcept crashes.
380
+ // The result callbacks are @Sendable: a plain closure would inherit MainActor
381
+ // isolation from this block, and Nitro resolves its promises synchronously on the
382
+ // JS thread — Swift 6.2's dynamic isolation check then traps on closure entry.
383
+ // The property is JS-thread-touched elsewhere too, so no hop back is needed.
323
384
  DispatchQueue.main.async { [weak self] in
324
385
  formatter(params)
325
- .then { customDisplay in
386
+ .then { @Sendable [weak self] customDisplay in
326
387
  self?.lastFormattedNavigationError = customDisplay ?? defaultFormatted
327
388
  }
328
- .catch { _ in
389
+ .catch { @Sendable [weak self] _ in
329
390
  self?.lastFormattedNavigationError = defaultFormatted
330
391
  }
331
392
  }
@@ -354,11 +415,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
354
415
  guard let player else { return }
355
416
  let result = await trackSelector.select(track: track, player: player)
356
417
  switch result {
357
- case .play(let intent):
418
+ case let .play(intent):
358
419
  executePlayback(intent)
359
420
  case .intercepted:
360
421
  break
361
- case .browse(let url):
422
+ case let .browse(url):
362
423
  navigateToBrowsableUrl(url)
363
424
  case .none:
364
425
  break
@@ -369,15 +430,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
369
430
  private func executePlayback(_ intent: TrackSelector.PlaybackIntent) {
370
431
  onMainActor {
371
432
  switch intent {
372
- case .skipTo(let index):
433
+ case let .skipTo(index):
373
434
  do {
374
435
  try player?.skipTo(index, playWhenReady: true)
375
436
  } catch {
376
437
  logger.error("Failed to skip to track at index \(index): \(error)")
377
438
  }
378
- case .setQueue(let tracks, let startIndex, let sourcePath):
439
+ case let .setQueue(tracks, startIndex, sourcePath):
379
440
  player?.setQueue(tracks, initialIndex: startIndex, playWhenReady: true, sourcePath: sourcePath)
380
- case .loadTrack(let track):
441
+ case let .loadTrack(track):
381
442
  player?.load(track, playWhenReady: true)
382
443
  }
383
444
  }
@@ -416,6 +477,12 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
416
477
  lastFormattedNavigationError
417
478
  }
418
479
 
480
+ /// Internal signal sent on `externalContentChangedEmitter` to tell CarPlay to
481
+ /// refresh every displayed template. Not a real path (paths are always
482
+ /// `/…`), so it can't collide with one; kept internal — the public API is
483
+ /// `invalidateAllContent()`.
484
+ static let invalidateAllSentinel = "__rnab_invalidate_all__"
485
+
419
486
  public func notifyContentChanged(path: String) throws {
420
487
  onMainActor { browserManager.invalidateContentCache(path) }
421
488
 
@@ -434,22 +501,168 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
434
501
  }
435
502
  }
436
503
 
504
+ public func invalidateAllContent() throws {
505
+ onMainActor { browserManager.clearContentCache() }
506
+
507
+ // Tell external controllers (CarPlay) to refresh every displayed template.
508
+ externalContentChangedEmitter.emit(Self.invalidateAllSentinel)
509
+
510
+ // Re-query the tabs source too: a callback source can resolve to new
511
+ // titles after an invalidation (e.g. a locale switch). BrowserManager
512
+ // emits tabsChanged only when the result actually differs, and a failed
513
+ // re-query keeps the current tabs. (Android needs no equivalent — its
514
+ // root re-query already runs queryTabs(); the web stub re-queries tabs on
515
+ // the re-navigation.)
516
+ Task {
517
+ _ = try? await browserManager.queryTabs()
518
+ }
519
+
520
+ // Re-resolve the JS-facing current path with the cache cleared.
521
+ Task {
522
+ do {
523
+ try await browserManager.refresh()
524
+ } catch {
525
+ handleNavigationError(error, path: onMainActor { browserManager.getPath() })
526
+ }
527
+ }
528
+ }
529
+
437
530
  public func setFavorites(favorites: [String]) throws {
438
531
  onMainActor { browserManager.setFavorites(favorites) }
439
532
  }
440
533
 
534
+ // MARK: - Gate
535
+
536
+ /// The gate's default chrome, if one was set. Served when the resolver
537
+ /// returns a gated decision with no per-request override, and for the static
538
+ /// gate (no resolver). May be nil for a resolver-only gate.
539
+ private(set) var defaultChrome: Gate?
540
+
541
+ /// Whether JS installed a per-request resolver. When false the gate is static
542
+ /// — every request is gated with `defaultChrome` and the JS hop is skipped.
543
+ private(set) var hasResolver = false
544
+
545
+ /// True once `setGate` runs, until `clearGate`. While active, external
546
+ /// surfaces (CarPlay) keep their tabs visible but consult `gateDecision(for:)`
547
+ /// per request, rendering the gate chrome where gated and refusing
548
+ /// external-surface search. Playback, the queue and Now Playing are
549
+ /// unaffected — a gate blocks finding content, never hearing it.
550
+ private(set) var isGateActive = false
551
+
552
+ /// The minimal built-in gate served when a request is gated but neither a
553
+ /// per-request override nor a stored default chrome exists (resolver-only
554
+ /// gate returning `true`).
555
+ static let builtInGate = Gate(title: "Unavailable", message: nil)
556
+
557
+ /// The outcome of a single gate decision: whether to gate, and (if so) the
558
+ /// chrome to render.
559
+ struct GateOutcome {
560
+ let gated: Bool
561
+ let chrome: Gate?
562
+ }
563
+
564
+ public func setGate(gate: Gate?, hasResolver: Bool) throws {
565
+ onMainActor {
566
+ defaultChrome = gate
567
+ self.hasResolver = hasResolver
568
+ isGateActive = true
569
+ gateChangedEmitter.emit(true)
570
+ }
571
+ }
572
+
573
+ public func clearGate() throws {
574
+ onMainActor {
575
+ guard isGateActive else { return }
576
+ defaultChrome = nil
577
+ hasResolver = false
578
+ isGateActive = false
579
+ gateChangedEmitter.emit(false)
580
+ }
581
+ }
582
+
583
+ /// The single choke point for every gate enforcement site. Asks whether
584
+ /// `request` should be gated and, if so, with what chrome (override → stored
585
+ /// default → built-in). When no resolver is installed the gate is static and
586
+ /// resolves without a JS hop.
587
+ ///
588
+ /// Resolver errors **fail CLOSED by design**: a thrown/rejected resolver, or a
589
+ /// rejection at either `.await()` layer (bridge tear-down on JS reload, the JS
590
+ /// runtime mid-reload, a serialization error) cannot prove the request is
591
+ /// allowed — and a gate exists to withhold content, so serving on error would
592
+ /// be a content leak (a paywall bypass). On error we gate with the stored
593
+ /// default / built-in chrome so the user still sees a coherent gate page. A
594
+ /// *successful* resolver returning `gated: false` still allows the request.
595
+ @MainActor
596
+ func gateDecision(for request: NativeGateRequest) async -> GateOutcome {
597
+ guard isGateActive else { return GateOutcome(gated: false, chrome: nil) }
598
+ if !hasResolver { return GateOutcome(gated: true, chrome: defaultChrome ?? Self.builtInGate) }
599
+ guard let decision = try? await resolveGate(request).await().await() else {
600
+ // Resolver error: fail CLOSED (see doc comment) — gate with the default /
601
+ // built-in so the gate page still renders rather than leaking content.
602
+ return GateOutcome(gated: true, chrome: defaultChrome ?? Self.builtInGate)
603
+ }
604
+ guard decision.gated else { return GateOutcome(gated: false, chrome: nil) } // explicit allow
605
+ return GateOutcome(gated: true, chrome: decision.gate ?? defaultChrome ?? Self.builtInGate)
606
+ }
607
+
608
+ // MARK: - Car Connection (CarPlay)
609
+
610
+ /// Static because the CarPlay scene can connect before the JS runtime
611
+ /// creates the shared instance (cold start in the car).
612
+ private nonisolated(unsafe) static var carPlayConnected = false
613
+
614
+ /// Called by the CarPlay scene delegate (via `RNABCarPlayController`) on
615
+ /// scene connect/disconnect — not from controller start/stop, which also
616
+ /// cycle on a JS reload and would emit spurious connection changes.
617
+ static func setCarPlayConnected(_ connected: Bool) {
618
+ guard carPlayConnected != connected else { return }
619
+ carPlayConnected = connected
620
+ shared?.onCarConnectedChanged(connected)
621
+ }
622
+
623
+ public func isCarConnected() throws -> Bool {
624
+ HybridAudioBrowser.carPlayConnected
625
+ }
626
+
627
+ public var onCarConnectedChanged: (Bool) -> Void = { _ in } {
628
+ didSet {
629
+ // Immediately notify current state (the scene may have connected before
630
+ // this JS runtime subscribed).
631
+ onCarConnectedChanged(HybridAudioBrowser.carPlayConnected)
632
+ }
633
+ }
634
+
441
635
  // MARK: - Player Setup
442
636
 
443
- public func setupPlayer(options: PartialSetupPlayerOptions) throws -> Promise<Void> {
637
+ public func setupPlayer(options: NativeSetupPlayerOptions) throws -> Promise<Void> {
444
638
  Promise.async {
445
- // Configure audio session
639
+ // Configure the audio session from the iOS setup options (category, mode, route policy, and
640
+ // options like `allowBluetooth` / `allowAirPlay`), but do NOT activate it here. Activating a
641
+ // non-mixable `.playback` session interrupts other apps' audio — so activating at setup
642
+ // pauses whatever the user is listening to (Safari, Spotify, a podcast) the instant our app
643
+ // launches, before they play anything. Setting the category is silent; only activation
644
+ // interrupts. The session is activated lazily when playback actually starts producing output
645
+ // (see `playerDidChangePlayingState`) and re-activated on interruption-end. Best-effort: a
646
+ // category-config failure must never brick setup.
446
647
  let session = AVAudioSession.sharedInstance()
447
- try session.setCategory(.playback, mode: .default)
448
- try session.setActive(true)
648
+ let cfg: (
649
+ category: AVAudioSession.Category,
650
+ mode: AVAudioSession.Mode,
651
+ policy: AVAudioSession.RouteSharingPolicy,
652
+ options: AVAudioSession.CategoryOptions
653
+ ) = options.ios?.resolveAudioSessionConfig()
654
+ ?? (category: .playback, mode: .default, policy: .default, options: [])
655
+ // Capture category application once so a media-services reset (which
656
+ // clears the category) can restore the exact same config later.
657
+ let applyCategory: () -> Void = {
658
+ try? session.setCategory(cfg.category, mode: cfg.mode, policy: cfg.policy, options: cfg.options)
659
+ }
660
+ applyCategory()
449
661
 
450
662
  // Create player and configure on main actor
451
663
  await MainActor.run { [weak self] in
452
664
  guard let self else { return }
665
+ self.applyAudioSessionCategory = applyCategory
453
666
 
454
667
  // Create player with self as callbacks delegate
455
668
  player = TrackPlayer(callbacks: self)
@@ -467,18 +680,52 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
467
680
  // Wire up network monitor for accelerated retries when connectivity is restored
468
681
  player?.networkMonitor = networkMonitor
469
682
 
683
+ // Central fan-out for connectivity changes (wired once here, idempotent on re-setup, and
684
+ // independent of any JS subscription):
685
+ // - bridge to the JS `onOnlineChanged` event (no-op default until JS subscribes);
686
+ // - re-render now-playing so the formatter's offline/online label updates immediately on a
687
+ // connectivity change, not only on the next playback transition (fixes the stall-entry
688
+ // race where the device goes offline after the buffering line was already rendered);
689
+ // - reconnect a stalled stream when connectivity returns (see TrackPlayer.handleNetworkRestored).
690
+ // The monitor only mutates state on the main thread, so this fires main-isolated.
691
+ networkMonitor.onChanged = { [weak self] isOnline in
692
+ MainActor.assumeIsolated {
693
+ guard let self else { return }
694
+ self.onOnlineChanged(isOnline)
695
+ self.applyNowPlayingMetadata()
696
+ if isOnline { self.player?.handleNetworkRestored() }
697
+ }
698
+ }
699
+
700
+ // Observe audio-session interruptions so another app taking over audio
701
+ // (or a phone call) reflects as a real pause rather than a stuck state.
702
+ setupInterruptionObserver()
703
+
704
+ // Observe media-server resets so a long-running stream recovers instead
705
+ // of going permanently silent.
706
+ setupMediaServicesResetObserver()
707
+
470
708
  // Configure media URL resolver
471
- player?.mediaLoader.mediaUrlResolver = { [weak self] src in
709
+ player?.mediaLoader.mediaUrlResolver = { [weak self] src, track in
472
710
  guard let self else {
473
711
  return MediaResolvedUrl(url: src, headers: nil, userAgent: nil)
474
712
  }
475
- return await browserManager.resolveMediaUrl(src)
713
+ return await browserManager.resolveMediaUrl(src, track: track)
476
714
  }
477
715
 
478
716
  // Configure artwork URL resolver for Now Playing (with size context)
479
717
  player?.nowPlayingUpdater.artworkUrlResolver = { [weak self] track, imageContext in
480
718
  guard let self else { return nil }
481
- return await browserManager.resolveArtworkUrl(track: track, perRouteConfig: nil, imageContext: imageContext)
719
+ let perRoute = (track.id?.isEmpty == false) ? browserManager.config.nowPlayingArtwork : nil
720
+ return await browserManager.resolveArtworkUrl(track: track, perRouteConfig: perRoute, imageContext: imageContext)
721
+ }
722
+
723
+ // Emit the JS `onNowPlayingChanged` event when the rendered text lines change. The updater
724
+ // owns the system now-playing write; this shapes the JS metadata (elapsed time / artwork).
725
+ player?.nowPlayingUpdater.onChanged = { [weak self] track, title, artist, album in
726
+ guard let self else { return }
727
+ self.onNowPlayingChanged(
728
+ self.makeNowPlayingMetadata(track: track, title: title, artist: artist, album: album))
482
729
  }
483
730
 
484
731
  // Configure sleep timer callback
@@ -486,8 +733,30 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
486
733
  self?.onSleepTimerChanged(state)
487
734
  }
488
735
 
489
- // Apply default remote commands (play, pause, next, previous, seekTo)
490
- applyRemoteCommands()
736
+ // Now-playing: whether to manage metadata, the optional formatter that owns the rendered
737
+ // lines, and whether to keep the session alive through a terminal error.
738
+ nowPlayingMetadataEnabled = options.autoUpdateNowPlayingMetadata ?? true
739
+ nowPlayingMetadataFormatter = options.nowPlayingMetadataFormatter
740
+ // Stored for API symmetry, but iOS needs no masking: a terminal error resolves the playback
741
+ // state to *paused* (PlaybackCoordinator.applySideEffects), the now-playing info is retained
742
+ // (never cleared on error), and next/previous stay enabled (gated on queue position, not
743
+ // state). So the session stays controllable through errors by default — the behavior Android
744
+ // achieves via InterceptingPlayer is already the iOS norm.
745
+ keepSessionAliveOnError = options.keepSessionAliveOnError ?? true
746
+
747
+ // The bundled runtime options and initial state are part of the atomic launch
748
+ // description. Stage the state first so setup options and earlier pre-setup setter
749
+ // calls resolve by strict last-write-wins, then apply everything to the live player.
750
+ if let bundled = options.options {
751
+ updateOptions(options: bundled)
752
+ } else {
753
+ // updateOptions applies remote commands itself; without bundled options, apply the
754
+ // defaults (play, pause, next, previous, seekTo) here.
755
+ applyRemoteCommands()
756
+ }
757
+ if let mode = options.repeatMode { pendingRepeatMode = mode }
758
+ if let intent = options.playWhenReady { pendingPlayWhenReady = intent }
759
+ applyPendingPlayerState()
491
760
 
492
761
  // Notify listeners that player is ready (e.g., CarPlay)
493
762
  playerAndConfiguredBrowser.check()
@@ -495,6 +764,22 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
495
764
  }
496
765
  }
497
766
 
767
+ /// Applies initial player state staged before the player existed — from setup options or the
768
+ /// imperative setters called pre-setup. Consumed on apply so a later re-setup doesn't replay
769
+ /// stale state.
770
+ @MainActor
771
+ private func applyPendingPlayerState() {
772
+ guard let player else { return }
773
+ if let mode = pendingRepeatMode {
774
+ player.repeatMode = mode
775
+ pendingRepeatMode = nil
776
+ }
777
+ if let intent = pendingPlayWhenReady {
778
+ player.playWhenReady = intent
779
+ pendingPlayWhenReady = nil
780
+ }
781
+ }
782
+
498
783
  public func updateOptions(options: NativeUpdateOptions) {
499
784
  onMainActor {
500
785
  let previousInterval = playerOptions.progressUpdateEventInterval
@@ -502,6 +787,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
502
787
  // Update stored options
503
788
  playerOptions.update(from: options)
504
789
 
790
+ // Propagate the favorite match mode to the browser so it can hydrate
791
+ // row hearts (the `favorite` capability is the single favoriting switch).
792
+ browserManager.setFavoriteMatch(playerOptions.capabilities.favoriteMatch)
793
+
505
794
  // Apply remote commands to player
506
795
  applyRemoteCommands()
507
796
 
@@ -509,6 +798,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
509
798
  if playerOptions.progressUpdateEventInterval != previousInterval {
510
799
  player?.setProgressUpdateInterval(playerOptions.progressUpdateEventInterval)
511
800
  }
801
+
802
+ onOptionsChanged(playerOptions.toOptions())
803
+ playerOptionsChangedEmitter.emit(())
512
804
  }
513
805
  }
514
806
 
@@ -526,8 +818,12 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
526
818
  player.remoteCommands = remoteCommands
527
819
  }
528
820
 
529
- public func getOptions() throws -> UpdateOptions {
530
- playerOptions.toUpdateOptions()
821
+ public func getOptions() throws -> Options {
822
+ playerOptions.toOptions()
823
+ }
824
+
825
+ public func setPlaybackIntervalEnabled(enabled: Bool) {
826
+ onMainActor { player?.setPlaybackIntervalEnabled(enabled) }
531
827
  }
532
828
 
533
829
  // MARK: - Playback Control
@@ -562,11 +858,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
562
858
  }
563
859
 
564
860
  public func setPlayWhenReady(playWhenReady: Bool) throws {
565
- onMainActor { player?.playWhenReady = playWhenReady }
861
+ onMainActor {
862
+ // Pre-setup this stages the intent for the player to come up with — never a no-op.
863
+ if let player { player.playWhenReady = playWhenReady }
864
+ else { pendingPlayWhenReady = playWhenReady }
865
+ }
566
866
  }
567
867
 
568
868
  public func getPlayWhenReady() throws -> Bool {
569
- onMainActor { player?.playWhenReady ?? false }
869
+ onMainActor { player?.playWhenReady ?? pendingPlayWhenReady ?? false }
570
870
  }
571
871
 
572
872
  public func seekTo(position: Double) throws {
@@ -577,6 +877,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
577
877
  onMainActor { player?.seekBy(offset) }
578
878
  }
579
879
 
880
+ public func seekToLiveEdge() throws {
881
+ onMainActor { player?.seekToLiveEdge() }
882
+ }
883
+
580
884
  public func setVolume(level: Double) throws {
581
885
  onMainActor { player?.volume = Float(level) }
582
886
  }
@@ -614,11 +918,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
614
918
  }
615
919
 
616
920
  public func getRepeatMode() throws -> RepeatMode {
617
- onMainActor { player?.repeatMode ?? .off }
921
+ onMainActor { player?.repeatMode ?? pendingRepeatMode ?? .off }
618
922
  }
619
923
 
620
924
  public func setRepeatMode(mode: RepeatMode) throws {
621
- onMainActor { player?.repeatMode = mode }
925
+ onMainActor {
926
+ // Pre-setup this stages the mode for the player to come up with — never a no-op.
927
+ if let player { player.repeatMode = mode }
928
+ else { pendingRepeatMode = mode }
929
+ }
622
930
  }
623
931
 
624
932
  public func getShuffleEnabled() throws -> Bool {
@@ -630,11 +938,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
630
938
  }
631
939
 
632
940
  public func getPlaybackError() throws -> PlaybackError? {
633
- onMainActor { player?.playbackError?.toNitroError() }
941
+ onMainActor { player?.coordinator.nitroPlaybackError() }
634
942
  }
635
943
 
636
944
  public func retry() throws {
637
- onMainActor { player?.reload(startFromCurrentTime: true) }
945
+ // Re-resolve rather than replay the cached URL: short-lived URLs/tokens
946
+ // may have expired since the failure (matches the internal retry path).
947
+ onMainActor { player?.reloadResolving(startFromCurrentTime: true) }
638
948
  }
639
949
 
640
950
  // MARK: - Sleep Timer
@@ -648,9 +958,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
648
958
  }
649
959
  }
650
960
 
651
- public func setSleepTimer(seconds: Double) throws {
961
+ public func setSleepTimer(seconds: Double, fadeDuration: Double?) throws {
652
962
  onMainActor {
653
- player?.sleepTimerManager.set(seconds: seconds)
963
+ player?.sleepTimerManager.set(seconds: seconds, fadeDuration: fadeDuration)
654
964
  }
655
965
  }
656
966
 
@@ -730,13 +1040,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
730
1040
  onMainActor {
731
1041
  guard let track = player?.currentTrack, let src = track.src else { return }
732
1042
  guard let index = player?.currentIndex, index >= 0 else { return }
733
- browserManager.updateFavorite(id: src, favorited: favorited)
734
- // Create updated track with new favorited state
1043
+ // Optimistically reflect in the authoritative match set so the now-playing
1044
+ // heart (hydrated from it in getActiveTrack) flips immediately; native
1045
+ // reconciles to its canonical ids on the next setFavorites.
1046
+ browserManager.setFavorited(src: src, favorited: favorited)
735
1047
  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
1048
  favoriteChangedEmitter.emit(FavoriteChangedEvent(track: updatedTrack, favorited: favorited))
739
- // Fire active track changed so useActiveTrack() hook updates UI
1049
+ // Fire active track changed so the now-playing heart + useActiveTrack() refresh.
740
1050
  let position = player?.currentTime ?? 0
741
1051
  activeTrackChangedEmitter.emit(PlaybackActiveTrackChangedEvent(
742
1052
  lastIndex: Double(index),
@@ -748,27 +1058,24 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
748
1058
  }
749
1059
  }
750
1060
 
1061
+ /// Whether the active (now-playing) track is favorited, per the authoritative
1062
+ /// favorite set (via `getActiveTrack`'s hydration).
1063
+ func isActiveTrackFavorited() -> Bool {
1064
+ (try? getActiveTrack())?.favorited ?? false
1065
+ }
1066
+
751
1067
  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
- }
1068
+ try? setActiveTrackFavorited(favorited: !isActiveTrackFavorited())
759
1069
  }
760
1070
 
761
- public func setQueue(tracks: [Track], startIndex: Double?, startPositionMs: Double?) throws {
762
- try onMainActor {
1071
+ public func setQueue(tracks: [Track], startIndex: Double?, startPosition: Double?) throws {
1072
+ onMainActor {
763
1073
  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
- }
1074
+ player.setQueue(
1075
+ tracks,
1076
+ initialIndex: startIndex.map { Int($0) } ?? 0,
1077
+ startPositionMs: startPosition.map { $0 * 1000 },
1078
+ )
772
1079
  }
773
1080
  }
774
1081
 
@@ -793,7 +1100,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
793
1100
  }
794
1101
 
795
1102
  public func getActiveTrack() throws -> Track? {
796
- onMainActor { player?.currentTrack }
1103
+ // Hydrate `favorited` from the authoritative favorite set — the now-playing
1104
+ // track is loaded outside the browse cache, so its own flag isn't otherwise
1105
+ // kept in sync as favorites change. This is the single source of truth for
1106
+ // "is the active track favorited" (now-playing heart, toggle direction, JS).
1107
+ onMainActor { player?.currentTrack.map(browserManager.hydrateFavorite) }
797
1108
  }
798
1109
 
799
1110
  // MARK: - Now Playing
@@ -805,21 +1116,57 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
805
1116
  }
806
1117
  }
807
1118
 
1119
+ /// Transient now-playing fields (e.g. feedback for a refused remote
1120
+ /// command). Outranks the formatter and the override while active, so live
1121
+ /// metadata can't stomp it mid-flash. Reverted by a NATIVE timer — JS
1122
+ /// timers pause with a backgrounded host on Android, and the lock screen
1123
+ /// is exactly the backgrounded case — and cleared early on track change.
1124
+ private var nowPlayingFlash: NowPlayingUpdate?
1125
+ private var nowPlayingFlashRevert: DispatchWorkItem?
1126
+
1127
+ /// Pending debounced audio-session release (see `playerShouldReleaseSession`).
1128
+ private var sessionReleaseWork: DispatchWorkItem?
1129
+
1130
+ public func flashNowPlaying(update: NowPlayingUpdate, durationMs: Double) throws {
1131
+ onMainActor {
1132
+ nowPlayingFlashRevert?.cancel()
1133
+ nowPlayingFlash = update
1134
+ let revert = DispatchWorkItem { [weak self] in
1135
+ guard let self else { return }
1136
+ self.nowPlayingFlash = nil
1137
+ self.nowPlayingFlashRevert = nil
1138
+ self.applyNowPlayingMetadata()
1139
+ }
1140
+ nowPlayingFlashRevert = revert
1141
+ DispatchQueue.main.asyncAfter(deadline: .now() + durationMs / 1000, execute: revert)
1142
+ applyNowPlayingMetadata()
1143
+ }
1144
+ }
1145
+
1146
+ public func clearNowPlayingFlash() throws {
1147
+ onMainActor {
1148
+ guard nowPlayingFlash != nil else { return }
1149
+ cancelNowPlayingFlash()
1150
+ applyNowPlayingMetadata()
1151
+ }
1152
+ }
1153
+
1154
+ private func cancelNowPlayingFlash() {
1155
+ nowPlayingFlashRevert?.cancel()
1156
+ nowPlayingFlashRevert = nil
1157
+ nowPlayingFlash = nil
1158
+ }
1159
+
808
1160
  public func getNowPlaying() throws -> NowPlayingMetadata? {
809
1161
  onMainActor {
810
1162
  guard let track = player?.currentTrack else { return nil }
1163
+ let flash = nowPlayingFlash
811
1164
  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,
1165
+ return makeNowPlayingMetadata(
1166
+ track: track,
1167
+ title: flash?.title ?? override?.title ?? track.title,
1168
+ artist: flash?.artist ?? override?.artist ?? track.artist,
1169
+ album: flash?.album ?? override?.album ?? track.album,
823
1170
  )
824
1171
  }
825
1172
  }
@@ -829,36 +1176,47 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
829
1176
  /// Falls back to the raw artwork string only if it's not an SF Symbol (which JS can't load).
830
1177
  private func nowPlayingArtwork(for track: Track) -> String? {
831
1178
  if let uri = track.artworkSource?.uri { return uri }
832
- guard let artwork = track.artwork else { return nil }
1179
+ guard let artwork = track.artwork?.url else { return nil }
833
1180
  return SFSymbolRenderer.isSFSymbol(artwork) ? nil : artwork
834
1181
  }
835
1182
 
836
- /// Applies the current now playing metadata (with override if set) to NowPlayingInfoCenter and notifies JS.
1183
+ /// Builds the JS-facing now-playing metadata for the given rendered text lines.
837
1184
  @MainActor
838
- private func applyNowPlayingMetadata() {
839
- guard let track = player?.currentTrack else { return }
840
- let override = nowPlayingOverride
841
-
842
- let nowPlaying = NowPlayingMetadata(
1185
+ private func makeNowPlayingMetadata(
1186
+ track: Track, title: String, artist: String?, album: String?,
1187
+ ) -> NowPlayingMetadata {
1188
+ NowPlayingMetadata(
843
1189
  elapsedTime: player?.currentTime,
844
- title: override?.title ?? track.title,
845
- album: track.album,
846
- artist: override?.artist ?? track.artist,
1190
+ title: title,
1191
+ album: album,
1192
+ artist: artist,
847
1193
  duration: track.duration,
848
1194
  artwork: nowPlayingArtwork(for: track),
849
1195
  description: track.description,
850
1196
  mediaId: track.src ?? track.url,
851
1197
  genre: track.genre,
852
- rating: nil,
853
1198
  )
1199
+ }
854
1200
 
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)
1201
+ /// Re-renders the now-playing surface via `NowPlayingUpdater`, handing it the current playback
1202
+ /// signals, override, and formatter. Called on every track / metadata / playback-state change.
1203
+ @MainActor
1204
+ private func applyNowPlayingMetadata() {
1205
+ guard let player, let track = player.currentTrack else { return }
1206
+ // Classify a stall by connectivity so the formatter can show "No internet connection" vs
1207
+ // "Reconnecting…" without a separate read. nil while not stalled.
1208
+ let stalled: StallReason? =
1209
+ player.isStalled ? (networkMonitor.getOnline() ? .buffering : .offline) : nil
1210
+ player.nowPlayingUpdater.render(
1211
+ track: track,
1212
+ timedMetadata: latestTimedMetadata,
1213
+ playWhenReady: player.playWhenReady,
1214
+ stalled: stalled,
1215
+ error: player.coordinator.nitroPlaybackError(),
1216
+ flash: nowPlayingFlash,
1217
+ override: nowPlayingMetadataEnabled ? nowPlayingOverride : nil,
1218
+ formatter: nowPlayingMetadataEnabled ? nowPlayingMetadataFormatter : nil,
1219
+ )
862
1220
  }
863
1221
 
864
1222
  // MARK: - Network
@@ -893,6 +1251,63 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
893
1251
 
894
1252
  // MARK: - External Audio Output
895
1253
 
1254
+ /// Observes audio-session interruptions (phone calls, or another app such as
1255
+ /// Music/Spotify starting playback). iOS pauses our AVPlayer underneath us but
1256
+ /// doesn't update our playback state, so without this the UI stays stuck on
1257
+ /// "playing". On `.began` we reflect a real pause; on `.ended` we resume if we
1258
+ /// were playing and the system says we should.
1259
+ private func setupInterruptionObserver() {
1260
+ if let observer = interruptionObserver {
1261
+ NotificationCenter.default.removeObserver(observer)
1262
+ }
1263
+
1264
+ interruptionObserver = NotificationCenter.default.addObserver(
1265
+ forName: AVAudioSession.interruptionNotification,
1266
+ object: nil,
1267
+ queue: .main,
1268
+ ) { [weak self] notification in
1269
+ self?.handleAudioSessionInterruption(notification)
1270
+ }
1271
+ }
1272
+
1273
+ private func handleAudioSessionInterruption(_ notification: Notification) {
1274
+ guard
1275
+ let info = notification.userInfo,
1276
+ let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
1277
+ let type = AVAudioSession.InterruptionType(rawValue: typeValue)
1278
+ else { return }
1279
+
1280
+ switch type {
1281
+ case .began:
1282
+ // iOS 17+ fires a `.began` (with no matching `.ended`) when an output
1283
+ // route such as headphones disappears. Reflecting a pause here would
1284
+ // strand resume-intent state with nothing to clear it, and the
1285
+ // route-change observer already handles the output change — so ignore it.
1286
+ if #available(iOS 17.0, *),
1287
+ let reasonValue = info[AVAudioSessionInterruptionReasonKey] as? UInt,
1288
+ AVAudioSession.InterruptionReason(rawValue: reasonValue) == .routeDisconnected
1289
+ {
1290
+ return
1291
+ }
1292
+ onMainActor { player?.handleInterruptionBegan() }
1293
+ case .ended:
1294
+ let shouldResume = (info[AVAudioSessionInterruptionOptionKey] as? UInt)
1295
+ .map { AVAudioSession.InterruptionOptions(rawValue: $0).contains(.shouldResume) }
1296
+ ?? false
1297
+ onMainActor {
1298
+ // Reactivate the possibly-deactivated session before resuming — but
1299
+ // only when a resume will happen: activating while staying paused
1300
+ // grabs a non-mixable session with nothing to release it.
1301
+ if shouldResume, player?.willResumeAfterInterruption == true {
1302
+ try? AVAudioSession.sharedInstance().setActive(true)
1303
+ }
1304
+ player?.handleInterruptionEnded(shouldResume: shouldResume)
1305
+ }
1306
+ @unknown default:
1307
+ break
1308
+ }
1309
+ }
1310
+
896
1311
  /// Sets up observer for audio route changes
897
1312
  private func setupRouteChangeObserver() {
898
1313
  if let observer = routeChangeObserver {
@@ -903,58 +1318,95 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
903
1318
  forName: AVAudioSession.routeChangeNotification,
904
1319
  object: nil,
905
1320
  queue: .main,
1321
+ ) { [weak self] notification in
1322
+ guard let self else { return }
1323
+ // The previous output disappeared (headphones unplugged, a Bluetooth
1324
+ // speaker powered off). Pause rather than abruptly resume out of the
1325
+ // built-in speaker — the platform convention every media app follows.
1326
+ // A deliberate pause (clears play intent), not an interruption: the user
1327
+ // restarts playback themselves once they want it.
1328
+ if let reasonValue = notification.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt,
1329
+ AVAudioSession.RouteChangeReason(rawValue: reasonValue) == .oldDeviceUnavailable
1330
+ {
1331
+ // Also cancels a pending interruption auto-resume: unplugging during
1332
+ // a call must not blast the built-in speaker when the call ends.
1333
+ self.onMainActor { self.player?.handleRouteDisconnected() }
1334
+ }
1335
+ if let output = self.getCurrentOutput() {
1336
+ self.onOutputChanged(output)
1337
+ }
1338
+ }
1339
+ }
1340
+
1341
+ /// Observes audio-server (`mediaserverd`) resets. When it fires, the session
1342
+ /// category is cleared and every AVPlayer/session handle is invalid — a
1343
+ /// long-running stream would otherwise go permanently silent until the app
1344
+ /// restarts.
1345
+ private func setupMediaServicesResetObserver() {
1346
+ if let observer = mediaServicesResetObserver {
1347
+ NotificationCenter.default.removeObserver(observer)
1348
+ }
1349
+ mediaServicesResetObserver = NotificationCenter.default.addObserver(
1350
+ forName: AVAudioSession.mediaServicesWereResetNotification,
1351
+ object: nil,
1352
+ queue: .main,
906
1353
  ) { [weak self] _ in
907
- guard let self, let output = self.getCurrentOutput() else { return }
908
- self.onIosOutputChanged(output)
1354
+ guard let self else { return }
1355
+ // Re-apply our category (the reset cleared it), then have the player
1356
+ // recreate itself and reconnect the current stream.
1357
+ self.applyAudioSessionCategory()
1358
+ self.onMainActor { self.player?.handleMediaServicesReset() }
909
1359
  }
910
1360
  }
911
1361
 
912
1362
  /// Gets the current audio output info
913
- private func getCurrentOutput() -> IosOutput? {
1363
+ private func getCurrentOutput() -> Output? {
914
1364
  let session = AVAudioSession.sharedInstance()
915
1365
  guard let output = session.currentRoute.outputs.first else { return nil }
916
1366
 
917
- let (outputType, external): (IosOutputType, Bool) = switch output.portType {
1367
+ let (outputType, external): (OutputType, Bool) = switch output.portType {
918
1368
  case .builtInSpeaker:
919
- (.builtInSpeaker, false)
1369
+ (.speaker, false)
920
1370
  case .builtInReceiver:
921
- (.builtInReceiver, false)
1371
+ (.receiver, false)
922
1372
  case .airPlay:
923
1373
  (.airplay, true)
924
- case .bluetoothA2DP:
925
- (.bluetoothA2dp, true)
926
- case .bluetoothHFP:
927
- (.bluetoothHfp, true)
928
- case .bluetoothLE:
929
- (.bluetoothLe, true)
1374
+ case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
1375
+ (.bluetooth, true)
930
1376
  case .headphones:
931
1377
  (.headphones, true)
932
1378
  case .carAudio:
933
- (.carAudio, true)
1379
+ (.car, true)
934
1380
  case .HDMI:
935
1381
  (.hdmi, true)
936
1382
  case .usbAudio:
937
- (.usbAudio, true)
1383
+ (.usb, true)
938
1384
  default:
939
1385
  (.other, true)
940
1386
  }
941
1387
 
942
- return IosOutput(type: outputType, name: output.portName, external: external)
1388
+ return Output(type: outputType, name: output.portName, external: external)
943
1389
  }
944
1390
 
945
- public func getIosOutput() throws -> IosOutput? {
1391
+ public func getOutput() throws -> Output? {
946
1392
  getCurrentOutput()
947
1393
  }
948
1394
 
949
- public func openIosOutputPicker() throws {
1395
+ public func supportsOutputSwitcher() throws -> Bool {
1396
+ // iOS can always present the system route picker (AVRoutePickerView).
1397
+ return true
1398
+ }
1399
+
1400
+ public func openOutputPicker() throws {
950
1401
  DispatchQueue.main.async { [weak self] in
1402
+ guard let self else { return }
951
1403
  guard let windowScene = UIApplication.shared.connectedScenes
952
1404
  .compactMap({ $0 as? UIWindowScene })
953
1405
  .first(where: { $0.activationState == .foregroundActive }),
954
1406
  let window = windowScene.windows.first(where: { $0.isKeyWindow }),
955
1407
  var topController = window.rootViewController
956
1408
  else {
957
- self?.logger.debug("openIosOutputPicker: Could not find active window scene")
1409
+ self.logger.error("openOutputPicker: no active window scene / root view controller")
958
1410
  return
959
1411
  }
960
1412
 
@@ -963,25 +1415,52 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
963
1415
  topController = presented
964
1416
  }
965
1417
 
966
- let routePicker = AVRoutePickerView(frame: .zero)
967
- routePicker.isHidden = true
1418
+ // AVRoutePickerView has no public API to present its picker
1419
+ // programmatically, so we add it to the hierarchy and tap its internal
1420
+ // button. Two requirements that are easy to get wrong:
1421
+ // 1. The view must be *visible* to the system (a `isHidden` source view
1422
+ // suppresses the presentation), so we keep it transparent instead.
1423
+ // 2. The internal button can be nested below the top-level subviews on
1424
+ // modern iOS, so we search recursively rather than only direct children.
1425
+ let routePicker = AVRoutePickerView(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
1426
+ routePicker.alpha = 0.01
968
1427
  topController.view.addSubview(routePicker)
969
1428
 
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
- }
1429
+ let button = Self.firstButton(in: routePicker)
1430
+ self.logger.notice(
1431
+ "openOutputPicker: hierarchy=\(Self.describeHierarchy(routePicker), privacy: .public), buttonFound=\(button != nil, privacy: .public)",
1432
+ )
1433
+
1434
+ if let button {
1435
+ button.sendActions(for: .touchUpInside)
1436
+ } else {
1437
+ self.logger.error("openOutputPicker: no UIButton found inside AVRoutePickerView")
976
1438
  }
977
1439
 
978
- // Remove after a delay to ensure picker has opened
979
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
1440
+ // Keep the source view in the hierarchy long enough for the picker to
1441
+ // present (on iPad it anchors a popover to this view).
1442
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
980
1443
  routePicker.removeFromSuperview()
981
1444
  }
982
1445
  }
983
1446
  }
984
1447
 
1448
+ /// Depth-first search for the first `UIButton` inside a view tree.
1449
+ private static func firstButton(in view: UIView) -> UIButton? {
1450
+ if let button = view as? UIButton { return button }
1451
+ for subview in view.subviews {
1452
+ if let button = firstButton(in: subview) { return button }
1453
+ }
1454
+ return nil
1455
+ }
1456
+
1457
+ /// Compact description of a view tree, e.g. `AVRoutePickerView[UIButton[UIImageView]]`.
1458
+ private static func describeHierarchy(_ view: UIView) -> String {
1459
+ let name = String(describing: type(of: view))
1460
+ let children = view.subviews.map(describeHierarchy).joined(separator: ", ")
1461
+ return children.isEmpty ? name : "\(name)[\(children)]"
1462
+ }
1463
+
985
1464
  // MARK: - Equalizer (unsupported on iOS)
986
1465
 
987
1466
  public func getEqualizerSettings() throws -> EqualizerSettings? {
@@ -1026,12 +1505,28 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
1026
1505
  extension HybridAudioBrowser: TrackPlayerCallbacks {
1027
1506
  public func playerDidChangePlayback(_ playback: Playback) {
1028
1507
  onPlaybackChanged(playback)
1508
+ // Re-render so the formatter reflects the new state — notably the error line on a failure (the
1509
+ // coordinator sets playbackError before this fires). Redundant updates are deduped on publish.
1510
+ applyNowPlayingMetadata()
1029
1511
  }
1030
1512
 
1031
1513
  public func playerDidChangeActiveTrack(_ event: PlaybackActiveTrackChangedEvent) {
1032
- // Clear now playing override when track changes (matches Kotlin behavior)
1514
+ // Clear now playing override + flash + live metadata when track changes
1515
+ // (matches Kotlin behavior; a flash must not carry over to a new track)
1033
1516
  nowPlayingOverride = nil
1034
- activeTrackChangedEmitter.emit(event)
1517
+ cancelNowPlayingFlash()
1518
+ latestTimedMetadata = nil
1519
+ // Hydrate the active track's `favorited` from the authoritative set so JS
1520
+ // consumers (useActiveTrack) match getActiveTrack()'s hydrated value — the
1521
+ // coordinator emits the raw queue track, whose flag isn't kept in sync.
1522
+ let hydrated = PlaybackActiveTrackChangedEvent(
1523
+ lastIndex: event.lastIndex,
1524
+ lastTrack: event.lastTrack,
1525
+ lastPosition: event.lastPosition,
1526
+ index: event.index,
1527
+ track: event.track.map(browserManager.hydrateFavorite),
1528
+ )
1529
+ activeTrackChangedEmitter.emit(hydrated)
1035
1530
  // Also notify now playing changed when track changes
1036
1531
  applyNowPlayingMetadata()
1037
1532
  }
@@ -1040,12 +1535,60 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1040
1535
  onPlaybackProgressUpdated(event)
1041
1536
  }
1042
1537
 
1538
+ public func playerDidFirePlaybackInterval() {
1539
+ onPlaybackInterval()
1540
+ }
1541
+
1043
1542
  public func playerDidChangePlayWhenReady(_ playWhenReady: Bool) {
1543
+ // Intent to play again is the earliest cancel signal for a pending session release.
1544
+ if playWhenReady { cancelSessionRelease() }
1044
1545
  onPlaybackPlayWhenReadyChanged(PlaybackPlayWhenReadyChangedEvent(playWhenReady: playWhenReady))
1045
1546
  }
1046
1547
 
1047
1548
  public func playerDidChangePlayingState(_ state: PlayingState) {
1048
1549
  onPlaybackPlayingState(state)
1550
+ // Lazily activate the audio session the moment playback actually produces — or is buffering to
1551
+ // produce — output. This (not setup) is where we grab the session, so launching the app never
1552
+ // interrupts other apps' audio; the interruption only happens when the user truly starts
1553
+ // playback, which is correct. Keyed off play *state*, not `playWhenReady`, because the intent
1554
+ // can be set at setup (before any track) while output only begins here. Idempotent:
1555
+ // `setActive(true)` is a no-op when the session is already active.
1556
+ if state.playing || state.buffering {
1557
+ try? AVAudioSession.sharedInstance().setActive(true)
1558
+ // The now-playing session's activation request at link time predates this
1559
+ // (the audio session was still inactive, and the system may have declined);
1560
+ // re-request now so the surface can appear even if playback never produces
1561
+ // audio (first-ever load failing on a dead stream).
1562
+ player?.nowPlayingInfoController.reactivateSessionIfNeeded()
1563
+ // Output resumed — cancel any pending release armed by an earlier stop.
1564
+ cancelSessionRelease()
1565
+ }
1566
+ // Re-render so the formatter reacts to a stall starting or recovering (`stalled`) — a buffering
1567
+ // flag change that may not transition the coordinator state.
1568
+ applyNowPlayingMetadata()
1569
+ }
1570
+
1571
+ public func playerShouldReleaseSession() {
1572
+ // Release the audio session so other apps' audio (Spotify, a podcast) can resume — debounced,
1573
+ // because iOS dislikes deactivating while our audio is still trailing off, and a quick
1574
+ // pause→resume shouldn't thrash the session. The coordinator only requests this on a genuine
1575
+ // stop (not an interruption, not mid-retry); a re-activation cancels it (see
1576
+ // `playerDidChangePlayWhenReady` and the activation above). Mirrors the now-playing flash timer.
1577
+ sessionReleaseWork?.cancel()
1578
+ let release = DispatchWorkItem { [weak self] in
1579
+ guard let self, self.player?.playWhenReady == false else { return }
1580
+ self.sessionReleaseWork = nil
1581
+ // `.notifyOthersOnDeactivation` signals other apps they may resume; without it the session
1582
+ // is released but nobody is told, so the user's audio would not come back.
1583
+ try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
1584
+ }
1585
+ sessionReleaseWork = release
1586
+ DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: release)
1587
+ }
1588
+
1589
+ private func cancelSessionRelease() {
1590
+ sessionReleaseWork?.cancel()
1591
+ sessionReleaseWork = nil
1049
1592
  }
1050
1593
 
1051
1594
  public func playerDidEndQueue(_ event: PlaybackQueueEndedEvent) {
@@ -1061,6 +1604,7 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1061
1604
  }
1062
1605
 
1063
1606
  public func playerDidChangeShuffleEnabled(_ enabled: Bool) {
1607
+ shuffleChangedEmitter.emit(enabled)
1064
1608
  onPlaybackShuffleModeChanged(enabled)
1065
1609
  }
1066
1610
 
@@ -1076,7 +1620,10 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1076
1620
  public func playerDidReceiveTimedMetadata(_ groups: [AVTimedMetadataGroup]) {
1077
1621
  for group in groups {
1078
1622
  if let metadata = TimedMetadata.from(items: group.items) {
1623
+ latestTimedMetadata = metadata
1079
1624
  onTimedMetadata(metadata)
1625
+ // Re-render the now-playing so the formatter can reflect the live song.
1626
+ applyNowPlayingMetadata()
1080
1627
  }
1081
1628
  }
1082
1629
  }
@@ -1193,10 +1740,6 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1193
1740
  remoteSeek(position: position)
1194
1741
  }
1195
1742
 
1196
- public func remoteSetRating(rating _: Any) {
1197
- // TODO: Convert rating to RemoteSetRatingEvent
1198
- }
1199
-
1200
1743
  public func remotePlayId(id: String, index: Int?) {
1201
1744
  let event = RemotePlayIdEvent(id: id, index: index.map { Double($0) })
1202
1745
  if let handler = handleRemotePlayId {
@@ -1215,57 +1758,91 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
1215
1758
  }
1216
1759
  }
1217
1760
 
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 {
1761
+ /// Static so the Siri intent handler can call it before the shared instance
1762
+ /// exists: it waits on the `playerAndConfiguredBrowser` gate, which resolves
1763
+ /// once RN boots. Uses the resolved `browser`, never `self`.
1764
+ static func handlePlayMediaIntent(criteria: MediaIntentCriteria, completion: @escaping @Sendable (Bool) -> Void) {
1765
+ Task { @MainActor in
1766
+ guard let (browser, player) = await playerAndConfiguredBrowser.wait(timeout: .seconds(8)) else {
1767
+ Logger(subsystem: "com.audiobrowser", category: "AudioBrowser")
1768
+ .error("handlePlayMediaIntent: browser/player not ready within budget")
1769
+ completion(false)
1770
+ return
1771
+ }
1772
+ // No-criteria intent ("play «app»") → resume. The gate must NOT block
1773
+ // this: the gate blocks *finding* content, never *hearing* the active or
1774
+ // persisted track. The gate is checked below, for the search branch only.
1775
+ if criteria.isResume {
1776
+ if player.currentTrack != nil {
1777
+ browser.logger.info("resume: warm — playing current track")
1778
+ player.play()
1779
+ browser.showNowPlayingRequestedEmitter.emit(())
1780
+ completion(true)
1781
+ } else if let state = browser.playbackStateStore.load() {
1782
+ browser.logger.info("resume: cold — restoring persisted track")
1783
+ let track = state.track.toNitro()
1784
+ let startMs = (track.live == true) ? nil : state.positionMs
1785
+ // Match Android resume: re-expand the full queue from the track's contextual
1786
+ // URL (parent container → siblings + selected index). Fall back to the single
1787
+ // track when the url isn't contextual or expansion fails.
1788
+ if let url = state.track.url,
1789
+ let expanded = try? await browser.browserManager.expandQueueFromContextualUrl(url) {
1790
+ player.setQueue(expanded.tracks, initialIndex: expanded.selectedIndex, startPositionMs: startMs, playWhenReady: true)
1791
+ } else {
1792
+ player.setQueue([track], initialIndex: 0, startPositionMs: startMs, playWhenReady: true)
1793
+ }
1794
+ browser.showNowPlayingRequestedEmitter.emit(())
1795
+ completion(true)
1796
+ } else {
1797
+ browser.logger.error("resume: nothing playing and nothing persisted → no-op")
1798
+ completion(false) // nothing playing and nothing persisted
1799
+ }
1800
+ return
1801
+ }
1802
+
1803
+ // Assemble the Nitro SearchParams here (MainActor) from the criteria's
1804
+ // Sendable fields, so the structured mode/genre/… reach the request like
1805
+ // Android. Today the API still text-searches `q`; the rest is forward-compat.
1806
+ let params = SearchParams(
1807
+ mode: criteria.searchMode.flatMap { SearchMode(fromString: $0) },
1808
+ query: criteria.query,
1809
+ genre: criteria.genre,
1810
+ artist: criteria.artist,
1811
+ album: criteria.album,
1812
+ title: criteria.title,
1813
+ playlist: criteria.playlist,
1814
+ // .currentlyPlaying can't reach here — isResume routed it to resume.
1815
+ reference: criteria.reference == .my ? .my : .unknown
1816
+ )
1817
+
1818
+ // Search is *finding* new content — refused when the gate decides this
1819
+ // request is gated (resume above is unaffected: the gate never blocks
1820
+ // hearing, and the gate check stays after the resume check).
1821
+ let outcome = await browser.gateDecision(
1822
+ for: NativeGateRequest(reason: .search, path: nil, search: params)
1823
+ )
1824
+ guard !outcome.gated else {
1825
+ browser.logger.info("handlePlayMediaIntent: search refused — gated")
1826
+ browser.onGate(GateEvent(reason: .search))
1827
+ completion(false)
1828
+ return
1829
+ }
1830
+
1228
1831
  do {
1229
- let resolved = try await self.browserManager.search(searchTerm)
1230
- guard let tracks = resolved.children, !tracks.isEmpty else {
1832
+ guard let tracks = try await browser.browserManager.searchPlayable(params) else {
1231
1833
  completion(false)
1232
1834
  return
1233
1835
  }
1234
- await MainActor.run {
1235
- self.getPlayer()?.setQueue(tracks, initialIndex: 0, playWhenReady: true)
1236
- }
1836
+ player.setQueue(tracks, initialIndex: 0, playWhenReady: true)
1837
+ browser.showNowPlayingRequestedEmitter.emit(())
1237
1838
  completion(true)
1238
1839
  } catch {
1239
- self.logger.error("handlePlayMediaIntent failed: \(error.localizedDescription)")
1840
+ browser.logger.error("handlePlayMediaIntent failed: \(error.localizedDescription)")
1240
1841
  completion(false)
1241
1842
  }
1242
1843
  }
1243
1844
  }
1244
1845
 
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
1846
  public func remoteChangeRepeatMode(mode: RepeatMode) {
1270
1847
  // Apply the repeat mode change from CarPlay/lock screen
1271
1848
  try? setRepeatMode(mode: mode)