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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (989) hide show
  1. package/README.md +36 -22
  2. package/android/build.gradle +35 -12
  3. package/android/gradle.properties +2 -2
  4. package/android/src/main/AndroidManifest.xml +3 -1
  5. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +541 -278
  6. package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
  7. package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
  8. package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +52 -0
  9. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
  10. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +27 -9
  11. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +288 -0
  12. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +97 -58
  13. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +70 -29
  14. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
  15. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +20 -7
  16. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  17. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  18. package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
  19. package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
  20. package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
  21. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +12 -12
  22. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
  23. package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
  24. package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
  25. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +509 -230
  26. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
  27. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -25
  28. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +113 -78
  29. package/android/src/main/java/com/audiobrowser/player/Player.kt +81 -38
  30. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  31. package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
  32. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
  33. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  34. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
  35. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
  36. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  37. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
  38. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
  39. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
  40. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +62 -40
  41. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  42. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +110 -0
  43. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +56 -52
  44. package/android/src/main/res/values/strings.xml +25 -5
  45. package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +100 -0
  46. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +119 -33
  47. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  48. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +15 -0
  49. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +346 -8
  50. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +40 -2
  51. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +301 -0
  52. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
  53. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +168 -0
  54. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
  55. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +160 -0
  56. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  57. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  58. package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
  59. package/android/src/test/java/com/audiobrowser/player/BrowseTilesTest.kt +385 -0
  60. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +101 -58
  61. package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
  62. package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
  63. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  64. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
  65. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  66. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
  67. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  68. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
  69. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
  70. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  71. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
  72. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +55 -16
  73. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +162 -0
  74. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  75. package/ios/AVPlayerEngine.swift +330 -0
  76. package/ios/AVPlayerEngineCallbacks.swift +15 -0
  77. package/ios/Browser/BrowseGate.swift +58 -0
  78. package/ios/Browser/BrowserConfig.swift +117 -78
  79. package/ios/Browser/BrowserManager.swift +209 -603
  80. package/ios/Browser/BrowserPathHelper.swift +50 -46
  81. package/ios/Browser/InertStyleDiagnostic.swift +296 -0
  82. package/ios/Browser/JsonModels.swift +264 -106
  83. package/ios/Browser/NavigationErrorState.swift +41 -0
  84. package/ios/Browser/PageResolver.swift +227 -0
  85. package/ios/Browser/RequestLayer.swift +90 -0
  86. package/ios/Browser/{BrowserManager+URLResolution.swift → RequestPipeline.swift} +193 -121
  87. package/ios/Browser/SearchDrillIn.swift +7 -7
  88. package/ios/Browser/SectionScope.swift +58 -24
  89. package/ios/Browser/StyleResolver.swift +58 -0
  90. package/ios/Browser/TabBarEntries.swift +9 -3
  91. package/ios/Browser/TrackIdTemplate.swift +37 -0
  92. package/ios/CarPlay/CPInterfaceController+Safe.swift +100 -43
  93. package/ios/CarPlay/CarPlayArtworkResolver.swift +36 -83
  94. package/ios/CarPlay/CarPlayController.swift +42 -1296
  95. package/ios/CarPlay/CarPlayImageLoader.swift +78 -97
  96. package/ios/CarPlay/CarPlayListItemFactory.swift +303 -92
  97. package/ios/CarPlay/CarPlayNowPlayingManager.swift +129 -136
  98. package/ios/CarPlay/CarPlaySession+Navigator.swift +114 -0
  99. package/ios/CarPlay/CarPlaySession+PageLoader.swift +92 -0
  100. package/ios/CarPlay/CarPlaySession+Refresher.swift +43 -0
  101. package/ios/CarPlay/CarPlaySession+TabBar.swift +193 -0
  102. package/ios/CarPlay/CarPlaySession.swift +209 -0
  103. package/ios/CarPlay/CarPlayTemplates.swift +63 -0
  104. package/ios/CarPlay/HybridAudioBrowser+IntentFavorite.swift +20 -0
  105. package/ios/{Player → CarPlay}/MediaIntentCriteria.swift +17 -9
  106. package/ios/CarPlay/MediaIntentPlayback.swift +74 -0
  107. package/ios/CarPlay/RNABMediaAddHandler.swift +8 -24
  108. package/ios/CarPlay/RNABMediaAffinityHandler.swift +7 -23
  109. package/ios/CarPlay/RNABMediaIntentHandler.swift +18 -19
  110. package/ios/CarPlay/SectionPresentation.swift +77 -0
  111. package/ios/CarPlay/TopTemplateReplacer.swift +96 -0
  112. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  113. package/ios/Extension/NavigationError+Conversion.swift +14 -4
  114. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  115. package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
  116. package/ios/Extension/Section+Copying.swift +29 -0
  117. package/ios/Extension/String+IcyCharset.swift +21 -0
  118. package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
  119. package/ios/Extension/Track+Copying.swift +8 -12
  120. package/ios/Extension/Track+Identity.swift +18 -0
  121. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  122. package/ios/Extension/UIImage+Adaptive.swift +12 -0
  123. package/ios/Extension/URLError+Connectivity.swift +15 -0
  124. package/ios/Http/HttpClient.swift +66 -142
  125. package/ios/HybridAudioBrowser+Browser.swift +153 -0
  126. package/ios/HybridAudioBrowser+Options.swift +144 -0
  127. package/ios/HybridAudioBrowser+Player.swift +397 -0
  128. package/ios/HybridAudioBrowser+PlayerCallbacks.swift +189 -0
  129. package/ios/HybridAudioBrowser+Stubs.swift +46 -0
  130. package/ios/HybridAudioBrowser.swift +153 -1610
  131. package/ios/Model/NitroTypeStubs.swift +146 -24
  132. package/ios/Model/PlayerUpdateOptions.swift +8 -1
  133. package/ios/Model/RemoteCommand.swift +18 -0
  134. package/ios/Model/TrackPlayerError.swift +2 -2
  135. package/ios/NowPlayingInfo/NowPlayingInfoController.swift +18 -53
  136. package/ios/Observer/ItemObserver.swift +90 -0
  137. package/ios/Observer/PlayerObserver.swift +59 -0
  138. package/ios/Option/SessionCategories.swift +42 -92
  139. package/ios/Player/AudioSessionController.swift +189 -0
  140. package/ios/Player/DeferredSeek.swift +70 -0
  141. package/ios/Player/ErrorStatus.swift +27 -0
  142. package/ios/Player/MediaLoader.swift +62 -35
  143. package/ios/Player/NowPlayingUpdater.swift +197 -50
  144. package/ios/Player/PlaybackFailure.swift +125 -0
  145. package/ios/Player/PlaybackPersistence.swift +73 -0
  146. package/ios/Player/PlaybackTimer.swift +8 -2
  147. package/ios/Player/Player.swift +1078 -0
  148. package/ios/Player/{PlaybackCoordinatorCallbacks.swift → PlayerCallbacks.swift} +3 -4
  149. package/ios/Player/PlayerEngine.swift +87 -0
  150. package/ios/Player/PlayerStatusTypes.swift +2 -6
  151. package/ios/Player/QueueManager.swift +93 -67
  152. package/ios/Player/RetryLoop.swift +261 -0
  153. package/ios/Player/ShuffleOrder.swift +33 -0
  154. package/ios/Player/SleepTimerManager.swift +1 -1
  155. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +46 -0
  156. package/ios/RemoteCommand/RemoteCommandController.swift +87 -331
  157. package/ios/Tests/BrowserPathHelperTests.swift +56 -15
  158. package/ios/Tests/CarPlayArtworkResolverTests.swift +60 -373
  159. package/ios/Tests/DeferredSeekTests.swift +70 -0
  160. package/ios/Tests/EmitterTests.swift +29 -0
  161. package/ios/Tests/GenerationMemoTests.swift +121 -0
  162. package/ios/Tests/InertStyleDiagnosticTests.swift +209 -0
  163. package/ios/Tests/JsonModelsTests.swift +164 -1
  164. package/ios/Tests/LRUCacheTests.swift +110 -0
  165. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  166. package/ios/Tests/MediaLoaderTests.swift +12 -48
  167. package/ios/Tests/ObserverTests.swift +88 -0
  168. package/ios/Tests/PlaybackErrorTests.swift +76 -0
  169. package/ios/Tests/PlaybackFailureTests.swift +189 -0
  170. package/ios/Tests/PlaybackPersistenceTests.swift +93 -0
  171. package/ios/Tests/PlayerPolicyTests.swift +420 -0
  172. package/ios/Tests/{PlaybackCoordinatorTests.swift → PlayerTests.swift} +300 -248
  173. package/ios/Tests/PlayingStateTests.swift +42 -0
  174. package/ios/Tests/QueueManagerTests.swift +196 -115
  175. package/ios/Tests/ReadyValueTests.swift +73 -0
  176. package/ios/Tests/RemoteCommandControllerTests.swift +152 -0
  177. package/ios/Tests/RemoteCommandTests.swift +65 -0
  178. package/ios/Tests/RequestLayerTests.swift +112 -0
  179. package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
  180. package/ios/Tests/RetryLoopTests.swift +270 -0
  181. package/ios/Tests/SearchDrillInTests.swift +16 -16
  182. package/ios/Tests/SectionPresentationTests.swift +98 -0
  183. package/ios/Tests/SectionScopeTests.swift +87 -55
  184. package/ios/Tests/StyleResolverTests.swift +132 -0
  185. package/ios/Tests/Support/TestTypes.swift +108 -104
  186. package/ios/Tests/TabBarEntriesTests.swift +19 -19
  187. package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
  188. package/ios/Tests/TopTemplateReplacerTests.swift +172 -0
  189. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  190. package/ios/Tests/TrackIdentityTests.swift +20 -0
  191. package/ios/Tests/TrackSelectorTests.swift +168 -33
  192. package/ios/Tests/URLErrorConnectivityTests.swift +20 -0
  193. package/ios/TrackSelector.swift +46 -31
  194. package/ios/Util/ArtworkImageFetcher.swift +58 -1
  195. package/ios/Util/Emitter.swift +42 -0
  196. package/ios/Util/GenerationMemo.swift +36 -0
  197. package/ios/Util/LRUCache.swift +23 -159
  198. package/ios/Util/NetworkMonitor.swift +43 -26
  199. package/ios/Util/ReadyValue.swift +56 -0
  200. package/ios/Util/SFSymbolRenderer.swift +59 -31
  201. package/ios/Util/SVGProcessor.swift +9 -12
  202. package/lib/commonjs/AudioBrowser.js +8 -8
  203. package/lib/commonjs/AudioBrowser.js.map +1 -1
  204. package/lib/commonjs/CLAUDE.md +52 -0
  205. package/lib/commonjs/features/browser-config.js +12 -8
  206. package/lib/commonjs/features/browser-config.js.map +1 -1
  207. package/lib/commonjs/features/browser.js +11 -3
  208. package/lib/commonjs/features/browser.js.map +1 -1
  209. package/lib/commonjs/features/carConnection.js +1 -1
  210. package/lib/commonjs/features/equalizer.js +1 -1
  211. package/lib/commonjs/features/errors.js +3 -3
  212. package/lib/commonjs/features/favorites.js +14 -11
  213. package/lib/commonjs/features/favorites.js.map +1 -1
  214. package/lib/commonjs/features/gate.js +16 -1
  215. package/lib/commonjs/features/gate.js.map +1 -1
  216. package/lib/commonjs/features/metadata.js +20 -3
  217. package/lib/commonjs/features/metadata.js.map +1 -1
  218. package/lib/commonjs/features/network.js +1 -1
  219. package/lib/commonjs/features/nowPlaying.js +1 -1
  220. package/lib/commonjs/features/output.js +1 -1
  221. package/lib/commonjs/features/playback/controls.js +4 -2
  222. package/lib/commonjs/features/playback/controls.js.map +1 -1
  223. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  224. package/lib/commonjs/features/playback/playing.js +1 -1
  225. package/lib/commonjs/features/playback/progress.js +3 -2
  226. package/lib/commonjs/features/playback/progress.js.map +1 -1
  227. package/lib/commonjs/features/playback/rate.js +8 -1
  228. package/lib/commonjs/features/playback/rate.js.map +1 -1
  229. package/lib/commonjs/features/playback/state.js +3 -4
  230. package/lib/commonjs/features/playback/state.js.map +1 -1
  231. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  232. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  233. package/lib/commonjs/features/playback/volume.js +1 -1
  234. package/lib/commonjs/features/player/options.js +48 -34
  235. package/lib/commonjs/features/player/options.js.map +1 -1
  236. package/lib/commonjs/features/player/setup.js +18 -9
  237. package/lib/commonjs/features/player/setup.js.map +1 -1
  238. package/lib/commonjs/features/player/validateOptions.js +23 -0
  239. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  240. package/lib/commonjs/features/queue/activeTrack.js +15 -4
  241. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  242. package/lib/commonjs/features/queue/queue.js +13 -6
  243. package/lib/commonjs/features/queue/queue.js.map +1 -1
  244. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  245. package/lib/commonjs/features/queue/shuffle.js +1 -1
  246. package/lib/commonjs/features/remoteControls.js +13 -92
  247. package/lib/commonjs/features/remoteControls.js.map +1 -1
  248. package/lib/commonjs/features/sleepTimer.js +13 -1
  249. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  250. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  251. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  252. package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
  253. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  254. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  255. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  256. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  257. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  258. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  259. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  260. package/lib/commonjs/web/NativeAudioBrowser.js +201 -76
  261. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  262. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
  263. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
  264. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  265. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  266. package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
  267. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  268. package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +1 -1
  269. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  270. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  271. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
  272. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
  273. package/lib/commonjs/web/browser/BrowserManager.js +175 -100
  274. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  275. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  276. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  277. package/lib/commonjs/web/browser/NavigationErrorManager.js +3 -1
  278. package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
  279. package/lib/commonjs/web/browser/SearchManager.js +18 -23
  280. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  281. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  282. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  283. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  284. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  285. package/lib/commonjs/web/browser/sections.js +89 -0
  286. package/lib/commonjs/web/browser/sections.js.map +1 -0
  287. package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
  288. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  289. package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
  290. package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
  291. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  292. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  293. package/lib/commonjs/web/util/BrowserPathHelper.js +62 -11
  294. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  295. package/lib/module/AudioBrowser.js +2 -1
  296. package/lib/module/AudioBrowser.js.map +1 -1
  297. package/lib/module/CLAUDE.md +52 -0
  298. package/lib/module/features/browser-config.js +12 -8
  299. package/lib/module/features/browser-config.js.map +1 -1
  300. package/lib/module/features/browser.js +11 -3
  301. package/lib/module/features/browser.js.map +1 -1
  302. package/lib/module/features/carConnection.js +1 -1
  303. package/lib/module/features/equalizer.js +1 -1
  304. package/lib/module/features/errors.js +3 -3
  305. package/lib/module/features/favorites.js +14 -11
  306. package/lib/module/features/favorites.js.map +1 -1
  307. package/lib/module/features/gate.js +16 -1
  308. package/lib/module/features/gate.js.map +1 -1
  309. package/lib/module/features/metadata.js +20 -3
  310. package/lib/module/features/metadata.js.map +1 -1
  311. package/lib/module/features/network.js +1 -1
  312. package/lib/module/features/nowPlaying.js +1 -1
  313. package/lib/module/features/output.js +1 -1
  314. package/lib/module/features/playback/controls.js +4 -2
  315. package/lib/module/features/playback/controls.js.map +1 -1
  316. package/lib/module/features/playback/playWhenReady.js +1 -1
  317. package/lib/module/features/playback/playing.js +1 -1
  318. package/lib/module/features/playback/progress.js +3 -2
  319. package/lib/module/features/playback/progress.js.map +1 -1
  320. package/lib/module/features/playback/rate.js +8 -1
  321. package/lib/module/features/playback/rate.js.map +1 -1
  322. package/lib/module/features/playback/state.js +3 -4
  323. package/lib/module/features/playback/state.js.map +1 -1
  324. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  325. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  326. package/lib/module/features/playback/volume.js +1 -1
  327. package/lib/module/features/player/options.js +47 -32
  328. package/lib/module/features/player/options.js.map +1 -1
  329. package/lib/module/features/player/setup.js +17 -8
  330. package/lib/module/features/player/setup.js.map +1 -1
  331. package/lib/module/features/player/validateOptions.js +19 -0
  332. package/lib/module/features/player/validateOptions.js.map +1 -0
  333. package/lib/module/features/queue/activeTrack.js +15 -4
  334. package/lib/module/features/queue/activeTrack.js.map +1 -1
  335. package/lib/module/features/queue/queue.js +13 -6
  336. package/lib/module/features/queue/queue.js.map +1 -1
  337. package/lib/module/features/queue/repeatMode.js +1 -1
  338. package/lib/module/features/queue/shuffle.js +1 -1
  339. package/lib/module/features/remoteControls.js +13 -92
  340. package/lib/module/features/remoteControls.js.map +1 -1
  341. package/lib/module/features/sleepTimer.js +13 -1
  342. package/lib/module/features/sleepTimer.js.map +1 -1
  343. package/lib/module/test-utils/reactNativeStub.js +58 -0
  344. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  345. package/lib/module/utils/LazyNativeEmitter.js +28 -14
  346. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  347. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  348. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  349. package/lib/module/utils/getTrackIdentity.js +20 -0
  350. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  351. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  352. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  353. package/lib/module/web/NativeAudioBrowser.js +201 -76
  354. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  355. package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
  356. package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
  357. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  358. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  359. package/lib/module/web/TrackPlayer/Player.js +254 -39
  360. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  361. package/lib/module/web/TrackPlayer/PlayingStateFactory.js +1 -1
  362. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  363. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  364. package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
  365. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
  366. package/lib/module/web/browser/BrowserManager.js +175 -100
  367. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  368. package/lib/module/web/browser/FavoriteManager.js +31 -21
  369. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  370. package/lib/module/web/browser/NavigationErrorManager.js +3 -1
  371. package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
  372. package/lib/module/web/browser/SearchManager.js +18 -23
  373. package/lib/module/web/browser/SearchManager.js.map +1 -1
  374. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  375. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  376. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  377. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  378. package/lib/module/web/browser/sections.js +83 -0
  379. package/lib/module/web/browser/sections.js.map +1 -0
  380. package/lib/module/web/http/RequestConfigBuilder.js +110 -27
  381. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  382. package/lib/module/web/http/followMediaRedirect.js +215 -0
  383. package/lib/module/web/http/followMediaRedirect.js.map +1 -0
  384. package/lib/module/web/player/NowPlayingManager.js +4 -3
  385. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  386. package/lib/module/web/util/BrowserPathHelper.js +62 -11
  387. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  388. package/lib/typescript/package.json +1 -0
  389. package/lib/typescript/src/AudioBrowser.d.ts +1 -1
  390. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  391. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  392. package/lib/typescript/src/features/browser.d.ts +10 -2
  393. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  394. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  395. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  396. package/lib/typescript/src/features/errors.d.ts +3 -3
  397. package/lib/typescript/src/features/favorites.d.ts +14 -11
  398. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  399. package/lib/typescript/src/features/gate.d.ts +18 -2
  400. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  401. package/lib/typescript/src/features/metadata.d.ts +20 -3
  402. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  403. package/lib/typescript/src/features/network.d.ts +1 -1
  404. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  405. package/lib/typescript/src/features/output.d.ts +1 -1
  406. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  407. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  408. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  409. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  410. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  411. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  412. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  413. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  414. package/lib/typescript/src/features/playback/state.d.ts +3 -4
  415. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  416. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  417. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  418. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  419. package/lib/typescript/src/features/player/options.d.ts +108 -56
  420. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  421. package/lib/typescript/src/features/player/setup.d.ts +14 -4
  422. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  423. package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
  424. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  425. package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
  426. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  427. package/lib/typescript/src/features/queue/queue.d.ts +7 -4
  428. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  429. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  430. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  431. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  432. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  433. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  434. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  435. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  436. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  437. package/lib/typescript/src/types/browser-native.d.ts +3 -2
  438. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  439. package/lib/typescript/src/types/browser-nodes.d.ts +387 -126
  440. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  441. package/lib/typescript/src/types/browser.d.ts +93 -61
  442. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  443. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
  444. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  445. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  446. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  447. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  448. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  449. package/lib/typescript/src/utils/resolveAssetSource.d.ts +1 -2
  450. package/lib/typescript/src/utils/resolveAssetSource.d.ts.map +1 -1
  451. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  452. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  453. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
  454. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +4 -2
  455. package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +2 -2
  456. package/nitrogen/generated/android/c++/JAndroidAudioOffloadSettings.hpp +2 -2
  457. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +12 -12
  458. package/nitrogen/generated/android/c++/JAndroidPlayerWakeMode.hpp +2 -2
  459. package/nitrogen/generated/android/c++/JAppKilledPlaybackBehavior.hpp +2 -2
  460. package/nitrogen/generated/android/c++/JArtworkRendering.hpp +58 -0
  461. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +10 -5
  462. package/nitrogen/generated/android/c++/JArtworkVariants.hpp +2 -2
  463. package/nitrogen/generated/android/c++/JBatteryOptimizationStatus.hpp +2 -2
  464. package/nitrogen/generated/android/c++/JBatteryOptimizationStatusChangedEvent.hpp +2 -2
  465. package/nitrogen/generated/android/c++/JBatteryWarningPendingChangedEvent.hpp +2 -2
  466. package/nitrogen/generated/android/c++/JBrowseError.hpp +2 -2
  467. package/nitrogen/generated/android/c++/JBrowseResult.hpp +19 -7
  468. package/nitrogen/generated/android/c++/JBrowserSourceCallbackParam.hpp +2 -2
  469. package/nitrogen/generated/android/c++/JCarPlayNowPlayingButton.hpp +2 -2
  470. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  471. package/nitrogen/generated/android/c++/JCarPlaySiriListButtonPosition.hpp +2 -2
  472. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  473. package/nitrogen/generated/android/c++/JChapterMetadata.hpp +2 -2
  474. package/nitrogen/generated/android/c++/JEqualizerSettings.hpp +10 -10
  475. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +10 -5
  476. package/nitrogen/generated/android/c++/JFormatNavigationErrorParams.hpp +11 -5
  477. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +10 -5
  478. package/nitrogen/generated/android/c++/JFormattedNavigationError.hpp +2 -2
  479. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.hpp +2 -2
  480. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
  481. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +10 -5
  482. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.hpp +4 -2
  483. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +10 -5
  484. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +10 -5
  485. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.hpp +2 -2
  486. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.hpp +2 -2
  487. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
  488. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +10 -5
  489. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.hpp +2 -2
  490. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +18 -12
  491. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +10 -4
  492. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
  493. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +18 -6
  494. package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -2
  495. package/nitrogen/generated/android/c++/JFunc_void_BatteryOptimizationStatusChangedEvent.hpp +2 -2
  496. package/nitrogen/generated/android/c++/JFunc_void_BatteryWarningPendingChangedEvent.hpp +2 -2
  497. package/nitrogen/generated/android/c++/JFunc_void_EqualizerSettings.hpp +2 -2
  498. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +10 -5
  499. package/nitrogen/generated/android/c++/JFunc_void_GateEvent.hpp +2 -2
  500. package/nitrogen/generated/android/c++/JFunc_void_NavigationErrorEvent.hpp +2 -2
  501. package/nitrogen/generated/android/c++/JFunc_void_NowPlayingMetadata.hpp +2 -2
  502. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +9 -12
  503. package/nitrogen/generated/android/c++/JFunc_void_Output.hpp +2 -2
  504. package/nitrogen/generated/android/c++/JFunc_void_Playback.hpp +2 -2
  505. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +10 -5
  506. package/nitrogen/generated/android/c++/JFunc_void_PlaybackErrorEvent.hpp +2 -2
  507. package/nitrogen/generated/android/c++/JFunc_void_PlaybackPlayWhenReadyChangedEvent.hpp +2 -2
  508. package/nitrogen/generated/android/c++/JFunc_void_PlaybackProgressUpdatedEvent.hpp +2 -2
  509. package/nitrogen/generated/android/c++/JFunc_void_PlaybackQueueEndedEvent.hpp +2 -2
  510. package/nitrogen/generated/android/c++/JFunc_void_PlayingState.hpp +2 -2
  511. package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpBackwardEvent.hpp +2 -2
  512. package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpForwardEvent.hpp +2 -2
  513. package/nitrogen/generated/android/c++/JFunc_void_RemotePlayIdEvent.hpp +2 -2
  514. package/nitrogen/generated/android/c++/JFunc_void_RemotePlaySearchEvent.hpp +2 -2
  515. package/nitrogen/generated/android/c++/JFunc_void_RemoteSeekEvent.hpp +2 -2
  516. package/nitrogen/generated/android/c++/JFunc_void_RemoteSkipEvent.hpp +2 -2
  517. package/nitrogen/generated/android/c++/JFunc_void_RepeatModeChangedEvent.hpp +2 -2
  518. package/nitrogen/generated/android/c++/JFunc_void_TimedMetadata.hpp +2 -2
  519. package/nitrogen/generated/android/c++/JFunc_void_TrackMetadata.hpp +2 -2
  520. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +2 -2
  521. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +2 -2
  522. package/nitrogen/generated/android/c++/JFunc_void_std__optional_FormattedNavigationError_.hpp +2 -2
  523. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +17 -5
  524. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.hpp +2 -2
  525. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +2 -2
  526. package/nitrogen/generated/android/c++/JFunc_void_std__vector_ChapterMetadata_.hpp +16 -16
  527. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +24 -18
  528. package/nitrogen/generated/android/c++/JGate.hpp +2 -2
  529. package/nitrogen/generated/android/c++/JGateDecision.hpp +2 -2
  530. package/nitrogen/generated/android/c++/JGateEvent.hpp +2 -2
  531. package/nitrogen/generated/android/c++/JGateReason.hpp +2 -2
  532. package/nitrogen/generated/android/c++/JGridTile.hpp +64 -0
  533. package/nitrogen/generated/android/c++/JHttpMethod.hpp +2 -2
  534. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +77 -53
  535. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +3 -3
  536. package/nitrogen/generated/android/c++/JIOSCategory.hpp +2 -2
  537. package/nitrogen/generated/android/c++/JIOSCategoryMode.hpp +2 -2
  538. package/nitrogen/generated/android/c++/JIOSCategoryOptions.hpp +2 -2
  539. package/nitrogen/generated/android/c++/JIOSCategoryPolicy.hpp +2 -2
  540. package/nitrogen/generated/android/c++/JIOSOptions.hpp +17 -11
  541. package/nitrogen/generated/android/c++/JImageContext.hpp +2 -2
  542. package/nitrogen/generated/android/c++/JImageQueryParams.hpp +2 -2
  543. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  544. package/nitrogen/generated/android/c++/JImageSource.hpp +2 -2
  545. package/nitrogen/generated/android/c++/JMediaReference.hpp +2 -2
  546. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +10 -5
  547. package/nitrogen/generated/android/c++/JMediaTransformParams.hpp +2 -2
  548. package/nitrogen/generated/android/c++/JNativeAndroidSetupOptions.hpp +2 -2
  549. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +48 -21
  550. package/nitrogen/generated/android/c++/JNativeGateRequest.hpp +2 -2
  551. package/nitrogen/generated/android/c++/JNativeIOSSetupOptions.hpp +10 -10
  552. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +16 -4
  553. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +17 -14
  554. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +9 -12
  555. package/nitrogen/generated/android/c++/JNavigationError.hpp +2 -2
  556. package/nitrogen/generated/android/c++/JNavigationErrorEvent.hpp +2 -2
  557. package/nitrogen/generated/android/c++/JNavigationErrorKind.hpp +58 -0
  558. package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +2 -2
  559. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +12 -12
  560. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +17 -11
  561. package/nitrogen/generated/android/c++/JNowPlayingMetadata.hpp +2 -2
  562. package/nitrogen/generated/android/c++/JNowPlayingUpdate.hpp +2 -2
  563. package/nitrogen/generated/android/c++/JOptions.hpp +9 -12
  564. package/nitrogen/generated/android/c++/JOutput.hpp +2 -2
  565. package/nitrogen/generated/android/c++/JOutputType.hpp +2 -2
  566. package/nitrogen/generated/android/c++/JPlayback.hpp +2 -2
  567. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +10 -5
  568. package/nitrogen/generated/android/c++/JPlaybackError.hpp +2 -2
  569. package/nitrogen/generated/android/c++/JPlaybackErrorEvent.hpp +2 -2
  570. package/nitrogen/generated/android/c++/JPlaybackErrorKind.hpp +2 -2
  571. package/nitrogen/generated/android/c++/JPlaybackPlayWhenReadyChangedEvent.hpp +2 -2
  572. package/nitrogen/generated/android/c++/JPlaybackProgressUpdatedEvent.hpp +2 -2
  573. package/nitrogen/generated/android/c++/JPlaybackQueueEndedEvent.hpp +2 -2
  574. package/nitrogen/generated/android/c++/JPlaybackState.hpp +2 -2
  575. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +7 -13
  576. package/nitrogen/generated/android/c++/JPlayingState.hpp +2 -2
  577. package/nitrogen/generated/android/c++/JProgress.hpp +2 -2
  578. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  579. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  580. package/nitrogen/generated/android/c++/JRemoteJumpBackwardEvent.hpp +2 -2
  581. package/nitrogen/generated/android/c++/JRemoteJumpForwardEvent.hpp +2 -2
  582. package/nitrogen/generated/android/c++/JRemotePlayIdEvent.hpp +2 -2
  583. package/nitrogen/generated/android/c++/JRemotePlaySearchEvent.hpp +2 -2
  584. package/nitrogen/generated/android/c++/JRemoteSeekEvent.hpp +2 -2
  585. package/nitrogen/generated/android/c++/JRemoteSkipEvent.hpp +2 -2
  586. package/nitrogen/generated/android/c++/JRepeatMode.hpp +2 -2
  587. package/nitrogen/generated/android/c++/JRepeatModeChangedEvent.hpp +2 -2
  588. package/nitrogen/generated/android/c++/JRequestConfig.hpp +2 -2
  589. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +67 -59
  590. package/nitrogen/generated/android/c++/JRetryConfig.hpp +2 -2
  591. package/nitrogen/generated/android/c++/JSearchMode.hpp +2 -2
  592. package/nitrogen/generated/android/c++/JSearchParams.hpp +2 -2
  593. package/nitrogen/generated/android/c++/JSection.hpp +116 -0
  594. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  595. package/nitrogen/generated/android/c++/JSleepTimer.hpp +4 -4
  596. package/nitrogen/generated/android/c++/JSleepTimerEndOfTrack.hpp +2 -2
  597. package/nitrogen/generated/android/c++/JSleepTimerTime.hpp +2 -2
  598. package/nitrogen/generated/android/c++/JStallReason.hpp +2 -2
  599. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  600. package/nitrogen/generated/android/c++/JTimedMetadata.hpp +2 -2
  601. package/nitrogen/generated/android/c++/JTrack.hpp +27 -48
  602. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +18 -12
  603. package/nitrogen/generated/android/c++/JTrackMetadata.hpp +2 -2
  604. package/nitrogen/generated/android/c++/JTrackRequest.hpp +2 -2
  605. package/nitrogen/generated/android/c++/JTrackStyle.hpp +47 -19
  606. package/nitrogen/generated/android/c++/JTransformableRequestConfig.hpp +2 -2
  607. package/nitrogen/generated/android/c++/JVariant_Boolean_AndroidAudioOffloadSettings.hpp +3 -3
  608. package/nitrogen/generated/android/c++/JVariant_Boolean_RetryConfig.hpp +3 -3
  609. package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +3 -3
  610. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  611. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  612. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +20 -8
  613. package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.hpp +3 -3
  614. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidAudioOffloadSettings.kt +15 -0
  615. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +20 -3
  616. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  617. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkRequestConfig.kt +37 -0
  618. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkVariants.kt +15 -0
  619. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryOptimizationStatusChangedEvent.kt +13 -0
  620. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryWarningPendingChangedEvent.kt +13 -0
  621. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseError.kt +13 -0
  622. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseResult.kt +16 -7
  623. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowserSourceCallbackParam.kt +15 -0
  624. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  625. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  626. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ChapterMetadata.kt +19 -0
  627. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/EqualizerSettings.kt +27 -0
  628. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteChangedEvent.kt +15 -0
  629. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNavigationErrorParams.kt +25 -3
  630. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNowPlayingParams.kt +21 -0
  631. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormattedNavigationError.kt +15 -0
  632. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.kt +0 -2
  633. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
  634. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___Track.kt +0 -2
  635. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.kt +0 -2
  636. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.kt +0 -2
  637. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_std__string____Track.kt +0 -2
  638. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.kt +0 -2
  639. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.kt +0 -2
  640. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
  641. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.kt +0 -2
  642. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.kt +0 -2
  643. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.kt +0 -2
  644. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.kt +0 -2
  645. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +78 -0
  646. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.kt +0 -2
  647. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void.kt +0 -2
  648. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryOptimizationStatusChangedEvent.kt +0 -2
  649. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryWarningPendingChangedEvent.kt +0 -2
  650. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_EqualizerSettings.kt +0 -2
  651. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_FavoriteChangedEvent.kt +0 -2
  652. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_GateEvent.kt +0 -2
  653. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NavigationErrorEvent.kt +0 -2
  654. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NowPlayingMetadata.kt +0 -2
  655. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Options.kt +0 -2
  656. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Output.kt +0 -2
  657. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Playback.kt +0 -2
  658. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackActiveTrackChangedEvent.kt +0 -2
  659. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackErrorEvent.kt +0 -2
  660. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackPlayWhenReadyChangedEvent.kt +0 -2
  661. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackProgressUpdatedEvent.kt +0 -2
  662. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackQueueEndedEvent.kt +0 -2
  663. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlayingState.kt +0 -2
  664. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpBackwardEvent.kt +0 -2
  665. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpForwardEvent.kt +0 -2
  666. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlayIdEvent.kt +0 -2
  667. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlaySearchEvent.kt +0 -2
  668. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSeekEvent.kt +0 -2
  669. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSkipEvent.kt +0 -2
  670. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RepeatModeChangedEvent.kt +0 -2
  671. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TimedMetadata.kt +0 -2
  672. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TrackMetadata.kt +0 -2
  673. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_bool.kt +0 -2
  674. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_double.kt +0 -2
  675. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_FormattedNavigationError_.kt +0 -2
  676. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_ResolvedTrack_.kt +0 -2
  677. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.kt +0 -2
  678. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__string.kt +0 -2
  679. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_ChapterMetadata_.kt +0 -2
  680. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_Track_.kt +0 -2
  681. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Gate.kt +15 -0
  682. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateDecision.kt +15 -0
  683. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateEvent.kt +13 -0
  684. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +25 -0
  685. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +3 -1
  686. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +25 -3
  687. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageContext.kt +15 -0
  688. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageQueryParams.kt +15 -0
  689. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  690. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageSource.kt +19 -0
  691. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaRequestConfig.kt +35 -0
  692. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaTransformParams.kt +15 -0
  693. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeAndroidSetupOptions.kt +31 -0
  694. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +51 -5
  695. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeGateRequest.kt +17 -0
  696. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeIOSSetupOptions.kt +21 -0
  697. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeRouteEntry.kt +27 -0
  698. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeSetupPlayerOptions.kt +29 -0
  699. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeUpdateOptions.kt +23 -0
  700. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationError.kt +19 -0
  701. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorEvent.kt +13 -0
  702. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorKind.kt +23 -0
  703. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +20 -3
  704. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +25 -3
  705. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingMetadata.kt +29 -0
  706. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingUpdate.kt +17 -0
  707. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Options.kt +23 -0
  708. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Output.kt +17 -0
  709. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Playback.kt +15 -0
  710. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackActiveTrackChangedEvent.kt +21 -0
  711. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackError.kt +21 -0
  712. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackErrorEvent.kt +13 -0
  713. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackPlayWhenReadyChangedEvent.kt +13 -0
  714. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackProgressUpdatedEvent.kt +19 -0
  715. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackQueueEndedEvent.kt +15 -0
  716. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +37 -2
  717. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayingState.kt +15 -0
  718. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Progress.kt +17 -0
  719. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  720. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +61 -0
  721. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpBackwardEvent.kt +13 -0
  722. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpForwardEvent.kt +13 -0
  723. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlayIdEvent.kt +15 -0
  724. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlaySearchEvent.kt +13 -0
  725. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSeekEvent.kt +13 -0
  726. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSkipEvent.kt +13 -0
  727. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RepeatModeChangedEvent.kt +13 -0
  728. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RequestConfig.kt +27 -0
  729. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +67 -15
  730. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RetryConfig.kt +17 -0
  731. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchParams.kt +27 -0
  732. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +66 -0
  733. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +86 -0
  734. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimer.kt +18 -8
  735. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerEndOfTrack.kt +13 -0
  736. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerTime.kt +13 -0
  737. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  738. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TimedMetadata.kt +21 -0
  739. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +55 -12
  740. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackLoadEvent.kt +17 -0
  741. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackMetadata.kt +43 -0
  742. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackRequest.kt +17 -0
  743. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +58 -5
  744. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TransformableRequestConfig.kt +31 -0
  745. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_AndroidAudioOffloadSettings.kt +16 -7
  746. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_RetryConfig.kt +16 -7
  747. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_NullType_Double.kt +16 -7
  748. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +24 -15
  749. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.kt +18 -8
  750. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_String_ArtworkVariants.kt +16 -7
  751. package/nitrogen/generated/ios/AudioBrowser+autolinking.rb +2 -0
  752. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +38 -29
  753. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +779 -665
  754. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +34 -16
  755. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +36 -18
  756. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  757. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  758. package/nitrogen/generated/ios/swift/BrowseResult.swift +12 -0
  759. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  760. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  761. package/nitrogen/generated/ios/swift/FormatNavigationErrorParams.swift +7 -2
  762. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
  763. package/nitrogen/generated/ios/swift/GridTile.swift +48 -0
  764. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
  765. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
  766. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  767. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  768. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
  769. package/nitrogen/generated/ios/swift/NavigationErrorKind.swift +40 -0
  770. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  771. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  772. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  773. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  774. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  775. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
  776. package/nitrogen/generated/ios/swift/Section.swift +82 -0
  777. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  778. package/nitrogen/generated/ios/swift/SleepTimer.swift +13 -0
  779. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  780. package/nitrogen/generated/ios/swift/Track.swift +28 -63
  781. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  782. package/nitrogen/generated/ios/swift/Variant_Bool_AndroidAudioOffloadSettings.swift +12 -0
  783. package/nitrogen/generated/ios/swift/Variant_Bool_RetryConfig.swift +12 -0
  784. package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +12 -0
  785. package/nitrogen/generated/ios/swift/Variant_NullType_RemoteButtonLayout.swift +30 -0
  786. package/nitrogen/generated/ios/swift/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.swift +13 -0
  787. package/nitrogen/generated/ios/swift/Variant_String_ArtworkVariants.swift +12 -0
  788. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  789. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  790. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  791. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  792. package/nitrogen/generated/shared/c++/FormatNavigationErrorParams.hpp +9 -2
  793. package/nitrogen/generated/shared/c++/GridTile.hpp +84 -0
  794. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
  795. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
  796. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  797. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  798. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
  799. package/nitrogen/generated/shared/c++/NavigationErrorKind.hpp +76 -0
  800. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  801. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  802. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  803. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  804. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  805. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
  806. package/nitrogen/generated/shared/c++/Section.hpp +102 -0
  807. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  808. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  809. package/nitrogen/generated/shared/c++/Track.hpp +18 -30
  810. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  811. package/package.json +24 -11
  812. package/src/AudioBrowser.ts +2 -1
  813. package/src/CLAUDE.md +52 -0
  814. package/src/features/battery.test.ts +79 -0
  815. package/src/features/browser-config.test.ts +21 -6
  816. package/src/features/browser-config.ts +12 -8
  817. package/src/features/browser.test.ts +1 -1
  818. package/src/features/browser.ts +11 -3
  819. package/src/features/carConnection.ts +1 -1
  820. package/src/features/equalizer.ts +1 -1
  821. package/src/features/errors.ts +3 -3
  822. package/src/features/favorites.ts +14 -11
  823. package/src/features/gate.ts +16 -1
  824. package/src/features/hook-wiring.test.ts +302 -0
  825. package/src/features/metadata.ts +20 -3
  826. package/src/features/network.ts +1 -1
  827. package/src/features/nowPlaying.ts +1 -1
  828. package/src/features/output.ts +1 -1
  829. package/src/features/playback/controls.ts +4 -2
  830. package/src/features/playback/playWhenReady.ts +1 -1
  831. package/src/features/playback/playing.ts +1 -1
  832. package/src/features/playback/progress.test.ts +169 -0
  833. package/src/features/playback/progress.ts +3 -2
  834. package/src/features/playback/rate.ts +8 -1
  835. package/src/features/playback/state.ts +3 -4
  836. package/src/features/playback/trackPlaybackTime.test.ts +16 -6
  837. package/src/features/playback/trackPlaybackTime.ts +1 -6
  838. package/src/features/playback/volume.ts +1 -1
  839. package/src/features/player/options.ts +126 -76
  840. package/src/features/player/setup.test.ts +24 -7
  841. package/src/features/player/setup.ts +22 -10
  842. package/src/features/player/validateOptions.ts +21 -0
  843. package/src/features/queue/activeTrack.ts +15 -4
  844. package/src/features/queue/queue.ts +13 -6
  845. package/src/features/queue/repeatMode.ts +1 -1
  846. package/src/features/queue/shuffle.ts +1 -1
  847. package/src/features/remoteControls.ts +13 -92
  848. package/src/features/sleepTimer.test.ts +172 -0
  849. package/src/features/sleepTimer.ts +9 -1
  850. package/src/nitro-stub-drift.test.ts +277 -0
  851. package/src/public-surface.test.ts +147 -0
  852. package/src/specs/audio-browser.nitro.ts +5 -4
  853. package/src/style-resolution.test.ts +210 -0
  854. package/src/test-utils/reactNativeStub.ts +60 -0
  855. package/src/types/browser-native.ts +7 -3
  856. package/src/types/browser-nodes.ts +403 -134
  857. package/src/types/browser.ts +97 -63
  858. package/src/utils/LazyNativeEmitter.ts +28 -15
  859. package/src/utils/NativeUpdatedValue.ts +29 -13
  860. package/src/utils/getTrackIdentity.ts +20 -0
  861. package/src/utils/useDebug.test.ts +182 -0
  862. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  863. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  864. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
  865. package/src/web/NativeAudioBrowser.ts +240 -83
  866. package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
  867. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
  868. package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
  869. package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
  870. package/src/web/TrackPlayer/Player.load.test.ts +137 -1
  871. package/src/web/TrackPlayer/Player.ts +284 -37
  872. package/src/web/TrackPlayer/PlayingStateFactory.test.ts +1 -1
  873. package/src/web/TrackPlayer/PlayingStateFactory.ts +1 -1
  874. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  875. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  876. package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
  877. package/src/web/__tests__/lifecycle.test.ts +345 -0
  878. package/src/web/__tests__/lifecycleHarness.ts +351 -0
  879. package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
  880. package/src/web/__tests__/loadSupersession.test.ts +175 -0
  881. package/src/web/__tests__/retry.test.ts +169 -0
  882. package/src/web/browser/BrowserManager.ts +209 -111
  883. package/src/web/browser/FavoriteManager.test.ts +84 -0
  884. package/src/web/browser/FavoriteManager.ts +28 -23
  885. package/src/web/browser/NavigationErrorManager.ts +3 -1
  886. package/src/web/browser/SearchManager.test.ts +75 -0
  887. package/src/web/browser/SearchManager.ts +25 -28
  888. package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
  889. package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
  890. package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
  891. package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
  892. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  893. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  894. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  895. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  896. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  897. package/src/web/browser/sections.test.ts +91 -0
  898. package/src/web/browser/sections.ts +72 -0
  899. package/src/web/http/RequestConfigBuilder.test.ts +241 -1
  900. package/src/web/http/RequestConfigBuilder.ts +140 -33
  901. package/src/web/http/followMediaRedirect.test.ts +289 -0
  902. package/src/web/http/followMediaRedirect.ts +253 -0
  903. package/src/web/player/NowPlayingManager.ts +4 -3
  904. package/src/web/util/BrowserPathHelper.test.ts +68 -0
  905. package/src/web/util/BrowserPathHelper.ts +70 -13
  906. package/website/guide/android-auto.md +114 -0
  907. package/website/guide/android-certificates.md +409 -0
  908. package/website/guide/artwork.md +255 -0
  909. package/website/guide/audio-output.md +135 -0
  910. package/website/guide/automotive.md +86 -0
  911. package/website/guide/basic-usage.md +151 -0
  912. package/website/guide/battery.md +115 -0
  913. package/website/guide/browser.md +587 -0
  914. package/website/guide/carplay.md +650 -0
  915. package/website/guide/configuration.md +173 -0
  916. package/website/guide/equalizer.md +231 -0
  917. package/website/guide/errors.md +310 -0
  918. package/website/guide/favorites.md +154 -0
  919. package/website/guide/gate.md +143 -0
  920. package/website/guide/getting-started.md +46 -0
  921. package/website/guide/hooks.md +238 -0
  922. package/website/guide/live-streams.md +125 -0
  923. package/website/guide/metadata.md +239 -0
  924. package/website/guide/migrating-from-track-player.md +244 -0
  925. package/website/guide/native-callback-fetch.md +127 -0
  926. package/website/guide/network.md +86 -0
  927. package/website/guide/now-playing.md +175 -0
  928. package/website/guide/playback.md +328 -0
  929. package/website/guide/queue.md +300 -0
  930. package/website/guide/remote-controls.md +256 -0
  931. package/website/guide/search.md +255 -0
  932. package/website/guide/sleep-timer.md +268 -0
  933. package/website/guide/track.md +247 -0
  934. package/ios/Bridge.h +0 -8
  935. package/ios/Browser/MediaResolveComposer.swift +0 -114
  936. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  937. package/ios/Model/MediaURL.swift +0 -67
  938. package/ios/NowPlayingInfo/MediaItemProperty.swift +0 -78
  939. package/ios/NowPlayingInfo/NowPlayingInfoKeyValue.swift +0 -6
  940. package/ios/NowPlayingInfo/NowPlayingInfoProperty.swift +0 -220
  941. package/ios/Observer/PlayerItemNotificationObserver.swift +0 -119
  942. package/ios/Observer/PlayerItemPropertyObserver.swift +0 -133
  943. package/ios/Observer/PlayerStateObserver.swift +0 -50
  944. package/ios/Observer/PlayerTimeObserver.swift +0 -55
  945. package/ios/Option/PitchAlgorithms.swift +0 -17
  946. package/ios/Option/TimeEventFrequency.swift +0 -18
  947. package/ios/Player/LoadSeekCoordinator.swift +0 -99
  948. package/ios/Player/MediaLoaderDelegate.swift +0 -11
  949. package/ios/Player/PlaybackCoordinator.swift +0 -754
  950. package/ios/Player/PlaybackEffectHandler.swift +0 -54
  951. package/ios/Player/PlaybackErrorHandler.swift +0 -175
  952. package/ios/Player/PlaybackStateStore.swift +0 -27
  953. package/ios/Player/PlayingStateManager.swift +0 -33
  954. package/ios/Player/RetryManager.swift +0 -322
  955. package/ios/Player/SeekCompletionHandler.swift +0 -3
  956. package/ios/Player/SleepTimerHandling.swift +0 -15
  957. package/ios/Support/RNTrackPlayer-Bridging-Header.h +0 -7
  958. package/ios/Tests/LoadSeekCoordinatorTests.swift +0 -325
  959. package/ios/Tests/MediaResolveTests.swift +0 -126
  960. package/ios/Tests/OnceValueTests.swift +0 -79
  961. package/ios/Tests/PlaybackErrorHandlerTests.swift +0 -547
  962. package/ios/Tests/PlaybackStateStoreTests.swift +0 -48
  963. package/ios/Tests/PlayingStateManagerTests.swift +0 -57
  964. package/ios/Tests/RetryManagerTests.swift +0 -89
  965. package/ios/TrackPlayer.swift +0 -1014
  966. package/ios/TrackPlayerCallbacks.swift +0 -86
  967. package/ios/Util/MetadataAdapter.swift +0 -131
  968. package/ios/Util/OnceValue.swift +0 -104
  969. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  970. package/nitrogen/generated/android/c++/JFavoritesMatchMode.hpp +0 -58
  971. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  972. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  973. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  974. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  975. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  976. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  977. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteConfig.kt +0 -38
  978. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  979. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  980. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  981. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  982. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  983. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  984. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  985. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  986. package/nitrogen/generated/ios/swift/Variant_NullType_NotificationButtonLayout.swift +0 -18
  987. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  988. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
  989. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
@@ -5,7 +5,7 @@ import MediaPlayer
5
5
  import NitroModules
6
6
  import os.log
7
7
 
8
- @MainActor let playerAndConfiguredBrowser = OnceValue<(HybridAudioBrowser, TrackPlayer)> {
8
+ @MainActor let playerAndConfiguredBrowser = ReadyValue<(HybridAudioBrowser, Player)> {
9
9
  guard let browser = HybridAudioBrowser.shared,
10
10
  browser.browserManager.isConfigured,
11
11
  let player = browser.getPlayer() else { return nil }
@@ -16,8 +16,12 @@ import os.log
16
16
  /// Only safe when the caller blocks until the MainActor work completes (DispatchQueue.main.sync).
17
17
  private struct UncheckedSendableBox<T>: @unchecked Sendable { let value: T }
18
18
 
19
+ /// `@unchecked Sendable` because it must be: the spec's requirements are all
20
+ /// nonisolated and Nitro calls them synchronously on the JS thread, so the class
21
+ /// can't be `@MainActor`. Isolation is therefore per-property, and `@unchecked`
22
+ /// means the compiler won't tell you when a new one needs it.
19
23
  public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
20
- private let logger = Logger(subsystem: "com.audiobrowser", category: "AudioBrowser")
24
+ let logger = Logger(subsystem: "com.audiobrowser", category: "AudioBrowser")
21
25
 
22
26
  // MARK: - Shared Instance for CarPlay
23
27
 
@@ -30,60 +34,62 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
30
34
  /// silent.
31
35
  static let instanceChangedEmitter = Emitter<Void>()
32
36
 
37
+ /// Installed once, on first instance creation. React Native posts this
38
+ /// notification (from `RCTReloadCommand`) when a JS reload — dev or
39
+ /// OTA-update restart — is triggered, *before* the old runtime is destroyed:
40
+ /// the earliest signal that this instance's JS callbacks are about to go
41
+ /// stale. `init` of the replacement instance drops them too, but only after
42
+ /// the new runtime is up; this observer closes the window in between.
43
+ /// Observed by name so the library needs no React-Core dependency (and
44
+ /// stays inert under SPM tests, where nothing posts it).
45
+ @MainActor private static let reloadObserver: NSObjectProtocol = NotificationCenter.default.addObserver(
46
+ forName: Notification.Name("RCTTriggerReloadCommandNotification"),
47
+ object: nil,
48
+ queue: .main,
49
+ ) { _ in
50
+ MainActor.assumeIsolated {
51
+ HybridAudioBrowser.shared?.dropStaleJSCallbacks()
52
+ }
53
+ }
54
+
33
55
  // MARK: - Private Properties
34
56
 
35
- private var player: TrackPlayer?
36
- private let networkMonitor = NetworkMonitor()
37
- private let playbackStateStore = PlaybackStateStore()
57
+ /// Main-confined by convention, not annotation: `deinit` reads it as a
58
+ /// safety net and can't hop. Reach it from `onMainActor` / `@MainActor`.
59
+ var player: Player?
60
+ /// The media stack under `player`, for what only AVFoundation has: now playing, buffering,
61
+ /// volume and the remote commands.
62
+ var engine: AVPlayerEngine?
63
+ let networkMonitor = NetworkMonitor()
38
64
  let browserManager = BrowserManager()
39
- private let trackSelector: TrackSelector
40
- private var volumeObservation: NSKeyValueObservation?
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 = {}
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
65
+ let browseGate = BrowseGate()
66
+ let trackSelector: TrackSelector
67
+ @MainActor private(set) lazy var audioSession = AudioSessionController()
54
68
  /// Initial player state staged before the player exists — from setup options or the imperative
55
69
  /// 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?
60
- private let playerOptions = PlayerUpdateOptions()
70
+ @MainActor var pendingPlayWhenReady: Bool?
71
+ @MainActor var pendingRepeatMode: RepeatMode?
72
+ @MainActor let playerOptions = PlayerUpdateOptions()
61
73
 
62
74
  /// Configured playback rates for the playback-rate capability (for CarPlay rate cycling)
63
- var playbackRates: [Double] { playerOptions.playbackRates }
64
- var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
65
- var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] { playerOptions.carPlayNowPlayingButtons }
66
- private var lastNavigationError: NavigationError? {
67
- didSet {
68
- // Skip if both nil (no real change)
69
- guard oldValue != nil || lastNavigationError != nil else { return }
70
- navigationErrorEmitter.emit(NavigationErrorEvent(error: lastNavigationError))
71
- }
75
+ @MainActor var playbackRates: [Double] { playerOptions.playbackRates }
76
+ @MainActor var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
77
+ @MainActor var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] {
78
+ playerOptions.carPlayNowPlayingButtons
72
79
  }
73
80
 
74
- private var lastFormattedNavigationError: FormattedNavigationError? {
75
- didSet {
76
- // Skip if both nil (no real change)
77
- guard oldValue != nil || lastFormattedNavigationError != nil else { return }
78
- onFormattedNavigationError(lastFormattedNavigationError)
79
- }
81
+ @MainActor var carPlayPlayingIndicatorLocation: CarPlayPlayingIndicatorLocation {
82
+ playerOptions.carPlayPlayingIndicatorLocation
80
83
  }
81
84
 
85
+ @MainActor lazy var navigationError = NavigationErrorState(
86
+ onErrorChanged: { [weak self] in self?.navigationErrorEmitter.emit(NavigationErrorEvent(error: $0)) },
87
+ onFormattedChanged: { [weak self] in self?.onFormattedNavigationError($0) },
88
+ )
89
+
82
90
  // MARK: - Multi-Listener Emitters
83
91
 
84
92
  /// Emitters allow multiple listeners for each event type, avoiding callback hijacking
85
- public let tabsChangedEmitter = Emitter<[Track]>()
86
- public let contentChangedEmitter = Emitter<ResolvedTrack?>()
87
93
  public let favoriteChangedEmitter = Emitter<FavoriteChangedEvent>()
88
94
  public let activeTrackChangedEmitter = Emitter<PlaybackActiveTrackChangedEvent>()
89
95
  public let queueChangedEmitter = Emitter<[Track]>()
@@ -91,11 +97,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
91
97
  public let repeatModeChangedEmitter = Emitter<RepeatModeChangedEvent>()
92
98
  public let shuffleChangedEmitter = Emitter<Bool>()
93
99
  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
100
+ /// Fired when a voice media intent (`MediaIntentPlayback`) successfully
99
101
  /// starts playback. CarPlay surfaces the Now Playing template in response, so
100
102
  /// the user lands on the playing station (with the rest of the results in Up
101
103
  /// Next) rather than the screen they invoked Siri from. No-op where unobserved.
@@ -109,7 +111,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
109
111
 
110
112
  /// Runs a closure on MainActor synchronously. Safe for non-Sendable return types
111
113
  /// because DispatchQueue.main.sync blocks the caller — no concurrent access window.
112
- private func onMainActor<T>(_ work: @MainActor () -> T) -> T {
114
+ func onMainActor<T>(_ work: @MainActor () -> T) -> T {
113
115
  if Thread.isMainThread {
114
116
  MainActor.assumeIsolated { UncheckedSendableBox(value: work()) }.value
115
117
  } else {
@@ -119,7 +121,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
119
121
  }
120
122
  }
121
123
 
122
- private func onMainActor<T>(_ work: @MainActor () throws -> T) throws -> T {
124
+ func onMainActor<T>(_ work: @MainActor () throws -> T) throws -> T {
123
125
  if Thread.isMainThread {
124
126
  try MainActor.assumeIsolated { try UncheckedSendableBox(value: work()) }.value
125
127
  } else {
@@ -132,30 +134,29 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
132
134
  // MARK: - Browser Properties
133
135
 
134
136
  public var path: String? {
135
- get { onMainActor { browserManager.getPath() } }
137
+ get { onMainActor { browserManager.path } }
136
138
  set {
137
139
  guard let newPath = newValue else { return }
138
- Task {
139
- do {
140
- try await browserManager.navigate(newPath)
141
- } catch {
142
- handleNavigationError(error, path: newPath)
143
- }
144
- }
140
+ navigate(newPath, clearError: false)
145
141
  }
146
142
  }
147
143
 
148
144
  public var tabs: [Track]? {
149
- get { onMainActor { browserManager.getTabs() } }
145
+ get { onMainActor { browserManager.tabs } }
150
146
  set { /* tabs are managed internally by browserManager */ }
151
147
  }
152
148
 
153
149
  public var configuration: NativeBrowserConfiguration = .init(
154
150
  path: nil, request: nil, requestResolver: nil, browse: nil, browseResolver: nil, media: nil, artwork: nil, nowPlayingArtwork: nil, routes: nil,
155
151
  singleTrack: nil, handleTrackLoad: nil,
156
- androidControllerOfflineError: nil, carPlayLoadingTitle: nil, resolveAlbumUrl: nil, formatNavigationError: nil,
152
+ androidControllerOfflineError: nil, viewMoreTitle: nil, carPlayLoadingTitle: nil, resolveAlbumPath: nil,
153
+ formatNavigationError: nil,
157
154
  ) {
158
155
  didSet {
156
+ // Copied here, on the setter's thread: the Task below outlives this call,
157
+ // and Nitro can reassign `configuration` — a C++ struct holding
158
+ // std::string/std::function — from the JS thread while it runs.
159
+ let configuredPath = configuration.path
159
160
  onMainActor { browserManager.config = BrowserConfig(from: configuration) }
160
161
 
161
162
  // Query tabs and navigate to initial path after config is set (matches Kotlin behavior)
@@ -166,17 +167,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
166
167
  Task {
167
168
  let tabs = try? await browserManager.queryTabs()
168
169
  // Navigate to configured path, first tab, or "/"
169
- let initialPath = configuration.path ?? tabs?.first?.url ?? "/"
170
- // Clear error before navigation (matches Kotlin clearNavigationError())
171
- await MainActor.run {
172
- lastNavigationError = nil
173
- lastFormattedNavigationError = nil
174
- }
175
- do {
176
- try await browserManager.navigate(initialPath)
177
- } catch {
178
- handleNavigationError(error, path: initialPath)
179
- }
170
+ navigate(configuredPath ?? tabs?.first?.path ?? "/", clearError: true)
180
171
  }
181
172
  }
182
173
  }
@@ -196,13 +187,19 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
196
187
  /// JS-side Promise, the inner to the decision.
197
188
  ///
198
189
  /// 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
190
+ /// init window where the gate is already `.resolved` but JS hasn't
200
191
  /// (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
192
+ /// state, e.g. CarPlay deciding its tabs at scene connect). Serving content
202
193
  /// 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)))
194
+ /// resolver-error path in `BrowseGate.decide`. Matches Android's default.
195
+ public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = HybridAudioBrowser.defaultResolveGate
196
+
197
+ // Computed, not stored: a nonisolated static holding a non-Sendable closure
198
+ // is rejected under Swift 6 concurrency checking.
199
+ static var defaultResolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> {
200
+ { _ in
201
+ Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
202
+ }
206
203
  }
207
204
 
208
205
  /// Fired when a request is gated (served the gate). Set by JS.
@@ -271,32 +268,44 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
271
268
  public var onEqualizerChanged: (EqualizerSettings) -> Void = { _ in }
272
269
  public var onBatteryWarningPendingChanged: (BatteryWarningPendingChangedEvent) -> Void = { _ in }
273
270
  public var onBatteryOptimizationStatusChanged: (BatteryOptimizationStatusChangedEvent) -> Void = { _ in }
274
- public var onSystemVolumeChanged: (Double) -> Void = { _ in } {
275
- didSet {
276
- setupVolumeObserver()
277
- }
278
- }
271
+ public var onSystemVolumeChanged: (Double) -> Void = { _ in }
279
272
 
280
- public var onOutputChanged: (Output) -> Void = { _ in } {
273
+ public var onOutputChanged: (Output) -> Void = { _ in }
274
+
275
+ public var onCarConnectedChanged: (Bool) -> Void = { _ in } {
281
276
  didSet {
282
- setupRouteChangeObserver()
277
+ // Immediately notify current state (the scene may have connected before
278
+ // this JS runtime subscribed).
279
+ onCarConnectedChanged(HybridAudioBrowser.carPlayConnected)
283
280
  }
284
281
  }
285
282
 
283
+ /// Stored so a re-setup can re-apply it: unlike the other runtime options this
284
+ /// one has no home in `playerOptions`, and the player it was pushed to does not
285
+ /// survive `setupPlayer`.
286
+ @MainActor var playbackIntervalEnabled = false
287
+
286
288
  // MARK: - Initialization
287
289
 
288
290
  override public init() {
289
291
  trackSelector = TrackSelector(browserManager: browserManager)
290
292
  super.init()
291
293
 
292
- // Clean up the previous instance (e.g., on JS runtime reload).
293
- // The old HybridAudioBrowser may still be retained by the global
294
- // playerAndConfiguredBrowser OnceValue, so we must explicitly stop
295
- // its player to prevent two audio streams running simultaneously.
294
+ // Clean up the previous instance (e.g., on JS runtime reload): stop its
295
+ // player so two audio streams never run at once.
296
296
  onMainActor {
297
- HybridAudioBrowser.shared?.player?.destroy()
298
- playerAndConfiguredBrowser.reset()
297
+ // Statics are lazy — touch the observer so it's installed from the first
298
+ // instance onward.
299
+ _ = HybridAudioBrowser.reloadObserver
300
+ HybridAudioBrowser.shared?.destroyPlayer()
301
+ HybridAudioBrowser.shared?.dropStaleJSCallbacks()
299
302
  HybridAudioBrowser.shared = self
303
+ audioSession.outputChanged.addListener { [weak self] in self?.onOutputChanged($0) }
304
+ audioSession.volumeChanged.addListener { [weak self] in self?.onSystemVolumeChanged($0) }
305
+ browseGate.resolve = { [weak self] request in
306
+ guard let self else { throw CancellationError() }
307
+ return try await resolveGate(request).await().await()
308
+ }
300
309
  // Tell connected external controllers (CarPlay) to re-subscribe against
301
310
  // this instance. Unconditional: `shared` is weak, so on a JS reload the
302
311
  // old instance may already be gone — "shared was nil" does not mean no
@@ -304,33 +313,76 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
304
313
  HybridAudioBrowser.instanceChangedEmitter.emit(())
305
314
  }
306
315
  setupEmitterToNitroForwarding()
307
- setupBrowserCallbacks()
316
+ }
317
+
318
+ /// Drops every JS-provided callback that native code can invoke on its own
319
+ /// initiative (browse resolvers, `handleTrackLoad`, `resolveAlbumPath`,
320
+ /// `formatNavigationError`, `resolveGate`, the now-playing formatter).
321
+ ///
322
+ /// Called on the *previous* instance when a new one takes over: its JS runtime
323
+ /// is gone, and invoking a callback from a dead runtime aborts the process —
324
+ /// Nitro's `AsyncJSCallback` throws on a destroyed Dispatcher, and the throw
325
+ /// crosses the generated bridge wrapper's `noexcept` boundary. Native code
326
+ /// (e.g. a CarPlay controller still bound to this instance mid-reload) keeps
327
+ /// working off the data-only config with built-in fallbacks; `resolveGate`
328
+ /// reverts to the fail-closed default it documents for exactly this window.
329
+ /// One-shot: on a normal reload both triggers hit the same dying instance
330
+ /// (the reload notification, then the replacement's `init`), and the second
331
+ /// strip would re-fire the config-setter cascade — another `configChanged`
332
+ /// → CarPlay now-playing button rebuild — for nothing.
333
+ @MainActor private var didDropJSCallbacks = false
334
+
335
+ @MainActor
336
+ private func dropStaleJSCallbacks() {
337
+ guard !didDropJSCallbacks else { return }
338
+ didDropJSCallbacks = true
339
+ // Guarded: assigning `config` marks the browser configured (the setter's
340
+ // didSet), and a never-configured browser must keep cold-start waiters
341
+ // (CarPlay, Siri) blocked on the readiness gate rather than resolve them
342
+ // against an empty config.
343
+ if browserManager.isConfigured {
344
+ browserManager.config = browserManager.config.strippingJSCallbacks()
345
+ }
346
+ resolveGate = Self.defaultResolveGate
347
+ engine?.nowPlayingUpdater.formatter = nil
308
348
  }
309
349
 
310
350
  deinit {
311
351
  // Safety net: ensure the AVPlayer is stopped if this instance is deallocated.
312
- let player = self.player
352
+ let player = self.player, engine = self.engine
313
353
  if Thread.isMainThread {
314
- MainActor.assumeIsolated { player?.destroy() }
354
+ MainActor.assumeIsolated {
355
+ player?.clear()
356
+ engine?.destroy()
357
+ }
315
358
  } else {
316
- DispatchQueue.main.async { MainActor.assumeIsolated { player?.destroy() } }
359
+ DispatchQueue.main.async {
360
+ MainActor.assumeIsolated {
361
+ player?.clear()
362
+ engine?.destroy()
363
+ }
364
+ }
317
365
  }
318
366
  }
319
367
 
320
- /// Returns the TrackPlayer instance, if setup has been called.
321
- func getPlayer() -> TrackPlayer? {
368
+ /// Returns the player, if setup has been called.
369
+ func getPlayer() -> Player? {
322
370
  player
323
371
  }
324
372
 
373
+ /// Stops the player and releases its media stack: the AVPlayer and the now-playing session
374
+ /// outlive a dropped reference otherwise.
375
+ @MainActor
376
+ func destroyPlayer() {
377
+ player?.clear()
378
+ engine?.destroy()
379
+ }
380
+
325
381
  /// Sets up automatic forwarding from emitters to Nitro callbacks
326
382
  private func setupEmitterToNitroForwarding() {
327
- tabsChangedEmitter.addListener { [weak self] tabs in
328
- self?.onTabsChanged(tabs)
329
- }
330
-
331
- contentChangedEmitter.addListener { [weak self] content in
332
- self?.onContentChanged(content)
333
- }
383
+ browserManager.pathChanged.addListener { [weak self] in self?.onPathChanged($0) }
384
+ browserManager.tabsChanged.addListener { [weak self] in self?.onTabsChanged($0) }
385
+ browserManager.contentChanged.addListener { [weak self] in self?.onContentChanged($0) }
334
386
 
335
387
  favoriteChangedEmitter.addListener { [weak self] event in
336
388
  self?.onFavoriteChanged(event)
@@ -352,1515 +404,6 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
352
404
  self?.onPlaybackRepeatModeChanged(event)
353
405
  }
354
406
  }
355
-
356
- private func setupBrowserCallbacks() {
357
- onMainActor {
358
- browserManager.onPathChanged = { [weak self] path in
359
- self?.onPathChanged(path)
360
- }
361
- browserManager.onContentChanged = { [weak self] content in
362
- self?.contentChangedEmitter.emit(content)
363
- }
364
- browserManager.onTabsChanged = { [weak self] tabs in
365
- self?.tabsChangedEmitter.emit(tabs)
366
- }
367
- }
368
- }
369
-
370
- private func handleNavigationError(_ error: Error, path: String) {
371
- let navError = NavigationError.from(error)
372
-
373
- lastNavigationError = navError
374
-
375
- // Format the error (async if using JS callback, sync for defaults)
376
- let defaultFormatted = navError.defaultFormatted()
377
- if let formatter = onMainActor({ browserManager.config.formatNavigationError }) {
378
- let params = FormatNavigationErrorParams(error: navError, defaultFormatted: defaultFormatted, path: path)
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.
384
- DispatchQueue.main.async { [weak self] in
385
- formatter(params)
386
- .then { @Sendable [weak self] customDisplay in
387
- self?.lastFormattedNavigationError = customDisplay ?? defaultFormatted
388
- }
389
- .catch { @Sendable [weak self] _ in
390
- self?.lastFormattedNavigationError = defaultFormatted
391
- }
392
- }
393
- } else {
394
- lastFormattedNavigationError = defaultFormatted
395
- }
396
- }
397
-
398
- // MARK: - Browser Methods
399
-
400
- public func navigatePath(path: String) throws {
401
- // Clear error synchronously before starting navigation (didSet notifies JS)
402
- lastNavigationError = nil
403
- lastFormattedNavigationError = nil
404
- Task {
405
- do {
406
- try await browserManager.navigate(path)
407
- } catch {
408
- handleNavigationError(error, path: path)
409
- }
410
- }
411
- }
412
-
413
- public func navigateTrack(track: Track) throws {
414
- Task {
415
- guard let player else { return }
416
- let result = await trackSelector.select(track: track, player: player)
417
- switch result {
418
- case let .play(intent):
419
- executePlayback(intent)
420
- case .intercepted:
421
- break
422
- case let .browse(url):
423
- navigateToBrowsableUrl(url)
424
- case .none:
425
- break
426
- }
427
- }
428
- }
429
-
430
- private func executePlayback(_ intent: TrackSelector.PlaybackIntent) {
431
- onMainActor {
432
- switch intent {
433
- case let .skipTo(index):
434
- do {
435
- try player?.skipTo(index, playWhenReady: true)
436
- } catch {
437
- logger.error("Failed to skip to track at index \(index): \(error)")
438
- }
439
- case let .setQueue(tracks, startIndex, sourcePath):
440
- player?.setQueue(tracks, initialIndex: startIndex, playWhenReady: true, sourcePath: sourcePath)
441
- case let .loadTrack(track):
442
- player?.load(track, playWhenReady: true)
443
- }
444
- }
445
- }
446
-
447
- private func navigateToBrowsableUrl(_ url: String) {
448
- // Clear error synchronously before starting navigation (didSet notifies JS)
449
- lastNavigationError = nil
450
- lastFormattedNavigationError = nil
451
- Task {
452
- do {
453
- try await browserManager.navigate(url)
454
- } catch {
455
- handleNavigationError(error, path: url)
456
- }
457
- }
458
- }
459
-
460
- public func onSearch(query: String) throws -> Promise<[Track]> {
461
- Promise.async { [weak self] in
462
- guard let self else { return [] }
463
- let resolved = try await browserManager.search(query)
464
- return resolved.children ?? []
465
- }
466
- }
467
-
468
- public func getContent() throws -> ResolvedTrack? {
469
- onMainActor { browserManager.getContent() }
470
- }
471
-
472
- public func getNavigationError() throws -> NavigationError? {
473
- lastNavigationError
474
- }
475
-
476
- public func getFormattedNavigationError() throws -> FormattedNavigationError? {
477
- lastFormattedNavigationError
478
- }
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
-
486
- public func notifyContentChanged(path: String) throws {
487
- onMainActor { browserManager.invalidateContentCache(path) }
488
-
489
- // Notify external controllers (CarPlay) that content changed
490
- externalContentChangedEmitter.emit(path)
491
-
492
- // Re-resolve the path if it's the current browser path
493
- if onMainActor({ browserManager.getPath() }) == path {
494
- Task {
495
- do {
496
- try await browserManager.navigate(path)
497
- } catch {
498
- handleNavigationError(error, path: path)
499
- }
500
- }
501
- }
502
- }
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
-
530
- public func setFavorites(favorites: [String]) throws {
531
- onMainActor { browserManager.setFavorites(favorites) }
532
- }
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
-
635
- // MARK: - Player Setup
636
-
637
- public func setupPlayer(options: NativeSetupPlayerOptions) throws -> Promise<Void> {
638
- Promise.async {
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.
647
- let session = AVAudioSession.sharedInstance()
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()
661
-
662
- // Create player and configure on main actor
663
- await MainActor.run { [weak self] in
664
- guard let self else { return }
665
- self.applyAudioSessionCategory = applyCategory
666
-
667
- // Create player with self as callbacks delegate
668
- player = TrackPlayer(callbacks: self)
669
-
670
- // Apply buffer duration from options (in ms)
671
- if let buffer = options.ios?.buffer {
672
- player?.bufferDuration = buffer
673
- }
674
-
675
- // Apply retry configuration (top-level, cross-platform)
676
- if let retry = options.retry {
677
- player?.retryConfig = retry
678
- }
679
-
680
- // Wire up network monitor for accelerated retries when connectivity is restored
681
- player?.networkMonitor = networkMonitor
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
-
708
- // Configure media URL resolver
709
- player?.mediaLoader.mediaUrlResolver = { [weak self] src, track in
710
- guard let self else {
711
- return MediaResolvedUrl(url: src, headers: nil, userAgent: nil)
712
- }
713
- return await browserManager.resolveMediaUrl(src, track: track)
714
- }
715
-
716
- // Configure artwork URL resolver for Now Playing (with size context)
717
- player?.nowPlayingUpdater.artworkUrlResolver = { [weak self] track, imageContext in
718
- guard let self else { return nil }
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))
729
- }
730
-
731
- // Configure sleep timer callback
732
- player?.sleepTimerManager.onChanged = { [weak self] state in
733
- self?.onSleepTimerChanged(state)
734
- }
735
-
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()
760
-
761
- // Notify listeners that player is ready (e.g., CarPlay)
762
- playerAndConfiguredBrowser.check()
763
- }
764
- }
765
- }
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
-
783
- public func updateOptions(options: NativeUpdateOptions) {
784
- onMainActor {
785
- let previousInterval = playerOptions.progressUpdateEventInterval
786
-
787
- // Update stored options
788
- playerOptions.update(from: options)
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
-
794
- // Apply remote commands to player
795
- applyRemoteCommands()
796
-
797
- // Apply progress update interval if changed
798
- if playerOptions.progressUpdateEventInterval != previousInterval {
799
- player?.setProgressUpdateInterval(playerOptions.progressUpdateEventInterval)
800
- }
801
-
802
- onOptionsChanged(playerOptions.toOptions())
803
- playerOptionsChangedEmitter.emit(())
804
- }
805
- }
806
-
807
- /// Converts capabilities to remote commands and applies them to the player
808
- @MainActor
809
- private func applyRemoteCommands() {
810
- guard let player else { return }
811
-
812
- let remoteCommands = playerOptions.capabilities.buildRemoteCommands(
813
- forwardJumpInterval: NSNumber(value: playerOptions.forwardJumpInterval),
814
- backwardJumpInterval: NSNumber(value: playerOptions.backwardJumpInterval),
815
- playbackRates: playerOptions.playbackRates,
816
- )
817
-
818
- player.remoteCommands = remoteCommands
819
- }
820
-
821
- public func getOptions() throws -> Options {
822
- playerOptions.toOptions()
823
- }
824
-
825
- public func setPlaybackIntervalEnabled(enabled: Bool) {
826
- onMainActor { player?.setPlaybackIntervalEnabled(enabled) }
827
- }
828
-
829
- // MARK: - Playback Control
830
-
831
- public func load(track: Track) throws {
832
- try onMainActor {
833
- guard let player else {
834
- throw NSError(domain: "AudioBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "Player not initialized"])
835
- }
836
- player.load(track)
837
- }
838
- }
839
-
840
- public func reset() throws {
841
- onMainActor { player?.clear() }
842
- }
843
-
844
- public func play() throws {
845
- onMainActor { player?.play() }
846
- }
847
-
848
- public func pause() throws {
849
- onMainActor { player?.pause() }
850
- }
851
-
852
- public func togglePlayback() throws {
853
- onMainActor { player?.togglePlayback() }
854
- }
855
-
856
- public func stop() throws {
857
- onMainActor { player?.stop() }
858
- }
859
-
860
- public func setPlayWhenReady(playWhenReady: Bool) throws {
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
- }
866
- }
867
-
868
- public func getPlayWhenReady() throws -> Bool {
869
- onMainActor { player?.playWhenReady ?? pendingPlayWhenReady ?? false }
870
- }
871
-
872
- public func seekTo(position: Double) throws {
873
- onMainActor { player?.seekTo(position) }
874
- }
875
-
876
- public func seekBy(offset: Double) throws {
877
- onMainActor { player?.seekBy(offset) }
878
- }
879
-
880
- public func seekToLiveEdge() throws {
881
- onMainActor { player?.seekToLiveEdge() }
882
- }
883
-
884
- public func setVolume(level: Double) throws {
885
- onMainActor { player?.volume = Float(level) }
886
- }
887
-
888
- public func getVolume() throws -> Double {
889
- onMainActor { Double(player?.volume ?? 1.0) }
890
- }
891
-
892
- public func setRate(rate: Double) throws {
893
- onMainActor { player?.rate = Float(rate) }
894
- }
895
-
896
- public func getRate() throws -> Double {
897
- onMainActor { Double(player?.rate ?? 1.0) }
898
- }
899
-
900
- public func getProgress() throws -> Progress {
901
- onMainActor {
902
- Progress(
903
- position: player?.currentTime ?? 0,
904
- duration: player?.duration ?? 0,
905
- buffered: player?.bufferedPosition ?? 0,
906
- )
907
- }
908
- }
909
-
910
- public func getPlayback() throws -> Playback {
911
- onMainActor { player?.getPlayback() ?? Playback(state: .none, error: nil) }
912
- }
913
-
914
- public func getPlayingState() throws -> PlayingState {
915
- onMainActor {
916
- player?.playingStateManager.toPlayingState() ?? PlayingState(playing: false, buffering: false)
917
- }
918
- }
919
-
920
- public func getRepeatMode() throws -> RepeatMode {
921
- onMainActor { player?.repeatMode ?? pendingRepeatMode ?? .off }
922
- }
923
-
924
- public func setRepeatMode(mode: RepeatMode) throws {
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
- }
930
- }
931
-
932
- public func getShuffleEnabled() throws -> Bool {
933
- onMainActor { player?.shuffleEnabled ?? false }
934
- }
935
-
936
- public func setShuffleEnabled(enabled: Bool) throws {
937
- onMainActor { player?.shuffleEnabled = enabled }
938
- }
939
-
940
- public func getPlaybackError() throws -> PlaybackError? {
941
- onMainActor { player?.coordinator.nitroPlaybackError() }
942
- }
943
-
944
- public func retry() throws {
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) }
948
- }
949
-
950
- // MARK: - Sleep Timer
951
-
952
- public func getSleepTimer() throws -> SleepTimer {
953
- onMainActor {
954
- if let state = player?.sleepTimerManager.get() {
955
- return state
956
- }
957
- return .first(NullType.null)
958
- }
959
- }
960
-
961
- public func setSleepTimer(seconds: Double, fadeDuration: Double?) throws {
962
- onMainActor {
963
- player?.sleepTimerManager.set(seconds: seconds, fadeDuration: fadeDuration)
964
- }
965
- }
966
-
967
- public func setSleepTimerToEndOfTrack() throws {
968
- onMainActor {
969
- player?.sleepTimerManager.setToEndOfTrack()
970
- }
971
- }
972
-
973
- public func clearSleepTimer() throws -> Bool {
974
- onMainActor {
975
- player?.sleepTimerManager.clear() ?? false
976
- }
977
- }
978
-
979
- // MARK: - Queue Management
980
-
981
- public func add(tracks: [Track], insertBeforeIndex: Double?) throws {
982
- try onMainActor {
983
- guard let player else { return }
984
- if let index = insertBeforeIndex {
985
- try player.add(tracks, at: Int(index))
986
- } else {
987
- player.add(tracks)
988
- }
989
- }
990
- }
991
-
992
- public func move(fromIndex: Double, toIndex: Double) throws {
993
- try onMainActor {
994
- try player?.move(fromIndex: Int(fromIndex), toIndex: Int(toIndex))
995
- }
996
- }
997
-
998
- public func remove(indexes: [Double]) throws {
999
- try onMainActor {
1000
- guard let player else { return }
1001
- // Remove in reverse order to maintain index validity
1002
- for index in indexes.sorted().reversed() {
1003
- try player.remove(Int(index))
1004
- }
1005
- }
1006
- }
1007
-
1008
- public func removeUpcomingTracks() throws {
1009
- onMainActor { player?.removeUpcomingTracks() }
1010
- }
1011
-
1012
- public func skip(index: Double, initialPosition: Double?) throws {
1013
- try onMainActor {
1014
- try player?.skipTo(Int(index))
1015
- if let position = initialPosition {
1016
- player?.seekTo(position)
1017
- }
1018
- }
1019
- }
1020
-
1021
- public func skipToNext(initialPosition: Double?) throws {
1022
- onMainActor {
1023
- player?.next()
1024
- if let position = initialPosition {
1025
- player?.seekTo(position)
1026
- }
1027
- }
1028
- }
1029
-
1030
- public func skipToPrevious(initialPosition: Double?) throws {
1031
- onMainActor {
1032
- player?.previous()
1033
- if let position = initialPosition {
1034
- player?.seekTo(position)
1035
- }
1036
- }
1037
- }
1038
-
1039
- public func setActiveTrackFavorited(favorited: Bool) throws {
1040
- onMainActor {
1041
- guard let track = player?.currentTrack, let src = track.src else { return }
1042
- guard let index = player?.currentIndex, index >= 0 else { return }
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)
1047
- let updatedTrack = track.copying(favorited: favorited)
1048
- favoriteChangedEmitter.emit(FavoriteChangedEvent(track: updatedTrack, favorited: favorited))
1049
- // Fire active track changed so the now-playing heart + useActiveTrack() refresh.
1050
- let position = player?.currentTime ?? 0
1051
- activeTrackChangedEmitter.emit(PlaybackActiveTrackChangedEvent(
1052
- lastIndex: Double(index),
1053
- lastTrack: track,
1054
- lastPosition: position,
1055
- index: Double(index),
1056
- track: updatedTrack,
1057
- ))
1058
- }
1059
- }
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
-
1067
- public func toggleActiveTrackFavorited() throws {
1068
- try? setActiveTrackFavorited(favorited: !isActiveTrackFavorited())
1069
- }
1070
-
1071
- public func setQueue(tracks: [Track], startIndex: Double?, startPosition: Double?) throws {
1072
- onMainActor {
1073
- guard let player else { return }
1074
- player.setQueue(
1075
- tracks,
1076
- initialIndex: startIndex.map { Int($0) } ?? 0,
1077
- startPositionMs: startPosition.map { $0 * 1000 },
1078
- )
1079
- }
1080
- }
1081
-
1082
- public func getQueue() throws -> [Track] {
1083
- onMainActor { player?.tracks ?? [] }
1084
- }
1085
-
1086
- public func getTrack(index: Double) throws -> Track? {
1087
- onMainActor {
1088
- guard let tracks = player?.tracks else { return nil }
1089
- let i = Int(index)
1090
- guard i >= 0, i < tracks.count else { return nil }
1091
- return tracks[i]
1092
- }
1093
- }
1094
-
1095
- public func getActiveTrackIndex() throws -> Double? {
1096
- onMainActor {
1097
- guard let index = player?.currentIndex, index >= 0 else { return nil }
1098
- return Double(index)
1099
- }
1100
- }
1101
-
1102
- public func getActiveTrack() throws -> Track? {
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) }
1108
- }
1109
-
1110
- // MARK: - Now Playing
1111
-
1112
- public func updateNowPlaying(update: NowPlayingUpdate?) throws {
1113
- onMainActor {
1114
- nowPlayingOverride = update
1115
- applyNowPlayingMetadata()
1116
- }
1117
- }
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
-
1160
- public func getNowPlaying() throws -> NowPlayingMetadata? {
1161
- onMainActor {
1162
- guard let track = player?.currentTrack else { return nil }
1163
- let flash = nowPlayingFlash
1164
- let override = nowPlayingOverride
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,
1170
- )
1171
- }
1172
- }
1173
-
1174
- /// Returns the artwork URI suitable for JS consumption.
1175
- /// Prefers the resolved artworkSource URI (e.g. file:// for rendered SF Symbols).
1176
- /// Falls back to the raw artwork string only if it's not an SF Symbol (which JS can't load).
1177
- private func nowPlayingArtwork(for track: Track) -> String? {
1178
- if let uri = track.artworkSource?.uri { return uri }
1179
- guard let artwork = track.artwork?.url else { return nil }
1180
- return SFSymbolRenderer.isSFSymbol(artwork) ? nil : artwork
1181
- }
1182
-
1183
- /// Builds the JS-facing now-playing metadata for the given rendered text lines.
1184
- @MainActor
1185
- private func makeNowPlayingMetadata(
1186
- track: Track, title: String, artist: String?, album: String?,
1187
- ) -> NowPlayingMetadata {
1188
- NowPlayingMetadata(
1189
- elapsedTime: player?.currentTime,
1190
- title: title,
1191
- album: album,
1192
- artist: artist,
1193
- duration: track.duration,
1194
- artwork: nowPlayingArtwork(for: track),
1195
- description: track.description,
1196
- mediaId: track.src ?? track.url,
1197
- genre: track.genre,
1198
- )
1199
- }
1200
-
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
- )
1220
- }
1221
-
1222
- // MARK: - Network
1223
-
1224
- public func getOnline() throws -> Bool {
1225
- networkMonitor.getOnline()
1226
- }
1227
-
1228
- // MARK: - System Volume
1229
-
1230
- /// Sets up KVO observer for system volume changes
1231
- private func setupVolumeObserver() {
1232
- // Remove existing observation if any
1233
- volumeObservation?.invalidate()
1234
-
1235
- let audioSession = AVAudioSession.sharedInstance()
1236
- volumeObservation = audioSession.observe(\.outputVolume, options: [.new]) { [weak self] _, change in
1237
- guard let newVolume = change.newValue else { return }
1238
- self?.onSystemVolumeChanged(Double(newVolume))
1239
- }
1240
- }
1241
-
1242
- public func getSystemVolume() throws -> Double {
1243
- Double(AVAudioSession.sharedInstance().outputVolume)
1244
- }
1245
-
1246
- public func setSystemVolume(volume _: Double) throws {
1247
- // iOS doesn't provide a public API to set system volume programmatically.
1248
- // Users must adjust volume via hardware buttons or Control Center.
1249
- logger.debug("setSystemVolume is not supported on iOS - volume must be adjusted via hardware buttons or Control Center")
1250
- }
1251
-
1252
- // MARK: - External Audio Output
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
-
1311
- /// Sets up observer for audio route changes
1312
- private func setupRouteChangeObserver() {
1313
- if let observer = routeChangeObserver {
1314
- NotificationCenter.default.removeObserver(observer)
1315
- }
1316
-
1317
- routeChangeObserver = NotificationCenter.default.addObserver(
1318
- forName: AVAudioSession.routeChangeNotification,
1319
- object: nil,
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,
1353
- ) { [weak self] _ in
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() }
1359
- }
1360
- }
1361
-
1362
- /// Gets the current audio output info
1363
- private func getCurrentOutput() -> Output? {
1364
- let session = AVAudioSession.sharedInstance()
1365
- guard let output = session.currentRoute.outputs.first else { return nil }
1366
-
1367
- let (outputType, external): (OutputType, Bool) = switch output.portType {
1368
- case .builtInSpeaker:
1369
- (.speaker, false)
1370
- case .builtInReceiver:
1371
- (.receiver, false)
1372
- case .airPlay:
1373
- (.airplay, true)
1374
- case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
1375
- (.bluetooth, true)
1376
- case .headphones:
1377
- (.headphones, true)
1378
- case .carAudio:
1379
- (.car, true)
1380
- case .HDMI:
1381
- (.hdmi, true)
1382
- case .usbAudio:
1383
- (.usb, true)
1384
- default:
1385
- (.other, true)
1386
- }
1387
-
1388
- return Output(type: outputType, name: output.portName, external: external)
1389
- }
1390
-
1391
- public func getOutput() throws -> Output? {
1392
- getCurrentOutput()
1393
- }
1394
-
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 {
1401
- DispatchQueue.main.async { [weak self] in
1402
- guard let self else { return }
1403
- guard let windowScene = UIApplication.shared.connectedScenes
1404
- .compactMap({ $0 as? UIWindowScene })
1405
- .first(where: { $0.activationState == .foregroundActive }),
1406
- let window = windowScene.windows.first(where: { $0.isKeyWindow }),
1407
- var topController = window.rootViewController
1408
- else {
1409
- self.logger.error("openOutputPicker: no active window scene / root view controller")
1410
- return
1411
- }
1412
-
1413
- // Walk up to the topmost presented view controller
1414
- while let presented = topController.presentedViewController {
1415
- topController = presented
1416
- }
1417
-
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
1427
- topController.view.addSubview(routePicker)
1428
-
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")
1438
- }
1439
-
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) {
1443
- routePicker.removeFromSuperview()
1444
- }
1445
- }
1446
- }
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
-
1464
- // MARK: - Equalizer (unsupported on iOS)
1465
-
1466
- public func getEqualizerSettings() throws -> EqualizerSettings? {
1467
- // No-op: equalizer unsupported on iOS
1468
- nil
1469
- }
1470
-
1471
- public func setEqualizerEnabled(enabled _: Bool) throws {
1472
- // No-op: equalizer unsupported on iOS
1473
- }
1474
-
1475
- public func setEqualizerPreset(preset _: String) throws {
1476
- // No-op: equalizer unsupported on iOS
1477
- }
1478
-
1479
- public func setEqualizerLevels(levels _: [Double]) throws {
1480
- // No-op: equalizer unsupported on iOS
1481
- }
1482
-
1483
- // MARK: - Battery (Android-only, stub implementations)
1484
-
1485
- public func getBatteryWarningPending() throws -> Bool {
1486
- false
1487
- }
1488
-
1489
- public func getBatteryOptimizationStatus() throws -> BatteryOptimizationStatus {
1490
- // iOS doesn't have battery optimization restrictions like Android
1491
- .unrestricted
1492
- }
1493
-
1494
- public func dismissBatteryWarning() throws {
1495
- // No-op on iOS
1496
- }
1497
-
1498
- public func openBatterySettings() throws {
1499
- // No-op on iOS
1500
- }
1501
- }
1502
-
1503
- // MARK: - TrackPlayerCallbacks
1504
-
1505
- extension HybridAudioBrowser: TrackPlayerCallbacks {
1506
- public func playerDidChangePlayback(_ playback: Playback) {
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()
1511
- }
1512
-
1513
- public func playerDidChangeActiveTrack(_ event: PlaybackActiveTrackChangedEvent) {
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)
1516
- nowPlayingOverride = nil
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)
1530
- // Also notify now playing changed when track changes
1531
- applyNowPlayingMetadata()
1532
- }
1533
-
1534
- public func playerDidUpdateProgress(_ event: PlaybackProgressUpdatedEvent) {
1535
- onPlaybackProgressUpdated(event)
1536
- }
1537
-
1538
- public func playerDidFirePlaybackInterval() {
1539
- onPlaybackInterval()
1540
- }
1541
-
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() }
1545
- onPlaybackPlayWhenReadyChanged(PlaybackPlayWhenReadyChangedEvent(playWhenReady: playWhenReady))
1546
- }
1547
-
1548
- public func playerDidChangePlayingState(_ state: PlayingState) {
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
1592
- }
1593
-
1594
- public func playerDidEndQueue(_ event: PlaybackQueueEndedEvent) {
1595
- onPlaybackQueueEnded(event)
1596
- }
1597
-
1598
- public func playerDidChangeQueue(_ tracks: [Track]) {
1599
- queueChangedEmitter.emit(tracks)
1600
- }
1601
-
1602
- public func playerDidChangeRepeatMode(_ event: RepeatModeChangedEvent) {
1603
- repeatModeChangedEmitter.emit(event)
1604
- }
1605
-
1606
- public func playerDidChangeShuffleEnabled(_ enabled: Bool) {
1607
- shuffleChangedEmitter.emit(enabled)
1608
- onPlaybackShuffleModeChanged(enabled)
1609
- }
1610
-
1611
- public func playerDidError(_ event: PlaybackErrorEvent) {
1612
- onPlaybackError(event)
1613
- }
1614
-
1615
- public func playerDidReceiveCommonMetadata(_ items: [AVMetadataItem]) {
1616
- let metadata = TrackMetadata.from(items: items)
1617
- onTrackMetadata(metadata)
1618
- }
1619
-
1620
- public func playerDidReceiveTimedMetadata(_ groups: [AVTimedMetadataGroup]) {
1621
- for group in groups {
1622
- if let metadata = TimedMetadata.from(items: group.items) {
1623
- latestTimedMetadata = metadata
1624
- onTimedMetadata(metadata)
1625
- // Re-render the now-playing so the formatter can reflect the live song.
1626
- applyNowPlayingMetadata()
1627
- }
1628
- }
1629
- }
1630
-
1631
- public func playerDidReceiveChapterMetadata(_ groups: [AVTimedMetadataGroup]) {
1632
- let chapters = ChapterMetadata.from(groups: groups)
1633
- onChapterMetadata(chapters)
1634
- }
1635
-
1636
- public func playerDidCompleteSeek(position _: Double, didFinish _: Bool) {
1637
- // Not exposed to JS currently
1638
- }
1639
-
1640
- public func playerDidUpdateDuration(_: Double) {
1641
- // Not exposed to JS currently
1642
- }
1643
-
1644
- public func remotePlay() {
1645
- if let handler = handleRemotePlay {
1646
- handler()
1647
- } else {
1648
- // Default behavior: play the player
1649
- onMainActor { player?.play() }
1650
- }
1651
- onRemotePlay()
1652
- }
1653
-
1654
- public func remotePause() {
1655
- if let handler = handleRemotePause {
1656
- handler()
1657
- } else {
1658
- // Default behavior: pause the player
1659
- onMainActor { player?.pause() }
1660
- }
1661
- onRemotePause()
1662
- }
1663
-
1664
- public func remoteStop() {
1665
- if let handler = handleRemoteStop {
1666
- handler()
1667
- } else {
1668
- // Default behavior: stop the player
1669
- onMainActor { player?.stop() }
1670
- }
1671
- onRemoteStop()
1672
- }
1673
-
1674
- public func remotePlayPause() {
1675
- // Toggle based on current state
1676
- let isPlaying = onMainActor { player?.playWhenReady == true }
1677
- if isPlaying {
1678
- remotePause()
1679
- } else {
1680
- remotePlay()
1681
- }
1682
- }
1683
-
1684
- public func remoteNext() {
1685
- if let handler = handleRemoteNext {
1686
- handler()
1687
- } else {
1688
- // Default behavior: skip to next track
1689
- onMainActor { player?.next() }
1690
- }
1691
- onRemoteNext()
1692
- }
1693
-
1694
- public func remotePrevious() {
1695
- if let handler = handleRemotePrevious {
1696
- handler()
1697
- } else {
1698
- // Default behavior: skip to previous track
1699
- onMainActor { player?.previous() }
1700
- }
1701
- onRemotePrevious()
1702
- }
1703
-
1704
- public func remoteJumpForward(interval: Double) {
1705
- let event = RemoteJumpForwardEvent(interval: interval)
1706
- if let handler = handleRemoteJumpForward {
1707
- handler(event)
1708
- } else {
1709
- // Default behavior: seek forward by interval
1710
- onMainActor { player?.seekBy(interval) }
1711
- }
1712
- onRemoteJumpForward(event)
1713
- }
1714
-
1715
- public func remoteJumpBackward(interval: Double) {
1716
- let event = RemoteJumpBackwardEvent(interval: interval)
1717
- if let handler = handleRemoteJumpBackward {
1718
- handler(event)
1719
- } else {
1720
- // Default behavior: seek backward by interval
1721
- onMainActor { player?.seekBy(-interval) }
1722
- }
1723
- onRemoteJumpBackward(event)
1724
- }
1725
-
1726
- public func remoteSeek(position: Double) {
1727
- logger.info("remoteSeek called with position: \(position)")
1728
- let event = RemoteSeekEvent(position: position)
1729
- if let handler = handleRemoteSeek {
1730
- handler(event)
1731
- } else {
1732
- // Default behavior: seek to position
1733
- logger.info("remoteSeek: calling player.seekTo(\(position))")
1734
- onMainActor { player?.seekTo(position) }
1735
- }
1736
- onRemoteSeek(event)
1737
- }
1738
-
1739
- public func remoteChangePlaybackPosition(position: Double) {
1740
- remoteSeek(position: position)
1741
- }
1742
-
1743
- public func remotePlayId(id: String, index: Int?) {
1744
- let event = RemotePlayIdEvent(id: id, index: index.map { Double($0) })
1745
- if let handler = handleRemotePlayId {
1746
- handler(event)
1747
- } else {
1748
- onRemotePlayId(event)
1749
- }
1750
- }
1751
-
1752
- public func remotePlaySearch(query: String) {
1753
- let event = RemotePlaySearchEvent(query: query)
1754
- if let handler = handleRemotePlaySearch {
1755
- handler(event)
1756
- } else {
1757
- onRemotePlaySearch(event)
1758
- }
1759
- }
1760
-
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
-
1831
- do {
1832
- guard let tracks = try await browser.browserManager.searchPlayable(params) else {
1833
- completion(false)
1834
- return
1835
- }
1836
- player.setQueue(tracks, initialIndex: 0, playWhenReady: true)
1837
- browser.showNowPlayingRequestedEmitter.emit(())
1838
- completion(true)
1839
- } catch {
1840
- browser.logger.error("handlePlayMediaIntent failed: \(error.localizedDescription)")
1841
- completion(false)
1842
- }
1843
- }
1844
- }
1845
-
1846
- public func remoteChangeRepeatMode(mode: RepeatMode) {
1847
- // Apply the repeat mode change from CarPlay/lock screen
1848
- try? setRepeatMode(mode: mode)
1849
- }
1850
-
1851
- public func remoteChangeShuffleMode(enabled: Bool) {
1852
- // Apply the shuffle mode change from CarPlay/lock screen
1853
- try? setShuffleEnabled(enabled: enabled)
1854
- }
1855
-
1856
- public func remoteChangePlaybackRate(rate: Float) {
1857
- // Apply the playback rate change from CarPlay/lock screen
1858
- onMainActor { player?.rate = rate }
1859
- }
1860
-
1861
- func playerDidChangeOptions(_: PlayerUpdateOptions) {
1862
- // TODO: Convert to Options type
1863
- }
1864
407
  }
1865
408
 
1866
409
  // MARK: - Autolinking Alias