react-native-queue-player 1.1.2 → 2.0.0

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 (367) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/build.gradle +7 -0
  4. package/android/consumer-rules.pro +72 -3
  5. package/android/src/main/cpp/airplay2_control.c +4 -4
  6. package/android/src/main/cpp/airplay2_control.h +4 -4
  7. package/android/src/main/cpp/airplay2_jni.cpp +26 -22
  8. package/android/src/main/cpp/airplay2_pair.c +7 -7
  9. package/android/src/main/cpp/airplay2_pair.h +2 -2
  10. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  11. package/android/src/main/cpp/airplay2_session.c +2 -2
  12. package/android/src/main/cpp/airplay_jni.cpp +20 -72
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
  61. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  62. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  63. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  64. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  65. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
  114. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
  115. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  116. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  117. package/app.plugin.js +17 -1
  118. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  119. package/ios/AVQueueBuilder.swift +120 -85
  120. package/ios/ArtworkLoader.swift +0 -7
  121. package/ios/ArtworkResolver.swift +22 -4
  122. package/ios/AssetReplayGainReader.swift +118 -0
  123. package/ios/AudioSession.swift +67 -16
  124. package/ios/AudioTapProvider.swift +284 -149
  125. package/ios/CarPlayBridge.swift +0 -12
  126. package/ios/CarPlayCoordinator.swift +18 -2
  127. package/ios/CarPlaySceneDelegate.swift +2 -2
  128. package/ios/Cast/AirPlayRouteState.swift +9 -8
  129. package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
  130. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  131. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  132. package/ios/Cast/Core/CastSession.swift +11 -0
  133. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  134. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  135. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  136. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  137. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  138. package/ios/CastManager.swift +1 -1
  139. package/ios/CrossfadeEngine.swift +660 -226
  140. package/ios/EQTap.swift +20 -17
  141. package/ios/Equalizer.swift +1 -1
  142. package/ios/FLACStreamInfo.swift +65 -0
  143. package/ios/GaplessEngine.swift +57 -30
  144. package/ios/InputGuards.swift +35 -5
  145. package/ios/LookaheadCache.swift +202 -58
  146. package/ios/LookaheadCachePrefetcher.swift +48 -36
  147. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  148. package/ios/NetworkRecoveryPolicy.swift +32 -0
  149. package/ios/NowPlayingFormatExtractor.swift +25 -18
  150. package/ios/NowPlayingInfo.swift +84 -48
  151. package/ios/OutputRouteMonitor.swift +16 -3
  152. package/ios/PendingBrowseRequests.swift +1 -1
  153. package/ios/PlaceholderArtwork.swift +9 -5
  154. package/ios/PlaybackEngine.swift +165 -76
  155. package/ios/PlaybackErrorMapping.swift +3 -3
  156. package/ios/PlaybackModeStateMachine.swift +2 -2
  157. package/ios/PlaybackNetworkMonitor.swift +73 -0
  158. package/ios/PlayerStateDerivation.swift +6 -6
  159. package/ios/QueueMutationArithmetic.swift +25 -0
  160. package/ios/QueueSkipArithmetic.swift +5 -3
  161. package/ios/QueueWindowArithmetic.swift +152 -0
  162. package/ios/ReadThroughServer.swift +283 -0
  163. package/ios/RemoteCommands.swift +44 -4
  164. package/ios/ReplayGainData.swift +22 -3
  165. package/ios/ReplayGainExtractor.swift +70 -27
  166. package/ios/Siri/VoiceDonation.swift +14 -9
  167. package/ios/StreamingBitrateProbe.swift +22 -68
  168. package/ios/SuppliedReplayGain.swift +83 -0
  169. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  170. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  171. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  172. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  173. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  174. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  175. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  176. package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
  177. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  178. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  179. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  180. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  181. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  182. package/ios/Tests/EngineSwapTests.swift +91 -0
  183. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  184. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  185. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  186. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  187. package/ios/Tests/InputGuardsTests.swift +41 -0
  188. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  189. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  190. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  191. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  192. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  193. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  194. package/ios/Tests/MutationDeferralTests.swift +220 -0
  195. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  196. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  197. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  198. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  199. package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
  200. package/ios/Tests/PlayerFixtures.swift +48 -0
  201. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  202. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  203. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  204. package/ios/Tests/QueueStateTests.swift +3 -1
  205. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  206. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  207. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  208. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  209. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  210. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  211. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  212. package/ios/Tests/RetryRecoveryTests.swift +303 -0
  213. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  214. package/ios/Tests/SkipIndexTests.swift +32 -2
  215. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  216. package/ios/Tests/StallRecoveryTests.swift +97 -0
  217. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  218. package/ios/Tests/TopUpWindowGateTests.swift +394 -0
  219. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  220. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  221. package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
  222. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  223. package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
  224. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  225. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  226. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  227. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  228. package/ios/TrackPlayer+Automotive.swift +101 -0
  229. package/ios/TrackPlayer+Cache.swift +232 -0
  230. package/ios/TrackPlayer+Config.swift +291 -0
  231. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  232. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  233. package/ios/TrackPlayer+EventsDispatch.swift +968 -0
  234. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  235. package/ios/TrackPlayer+Lifecycle.swift +930 -0
  236. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  237. package/ios/TrackPlayer+Queue.swift +940 -0
  238. package/ios/TrackPlayer+Recovery.swift +196 -0
  239. package/ios/TrackPlayer+Skip.swift +455 -0
  240. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  241. package/ios/TrackPlayer+State.swift +108 -0
  242. package/ios/TrackPlayer+Threading.swift +135 -0
  243. package/ios/TrackPlayer+Transport.swift +267 -0
  244. package/ios/TrackPlayer+Window.swift +195 -0
  245. package/ios/TrackPlayer.swift +305 -4610
  246. package/ios/Visualizer.swift +6 -5
  247. package/ios/tests-harness/Podfile +1 -1
  248. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  249. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  250. package/lib/module/hooks/useActiveTrack.js +29 -22
  251. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  252. package/lib/module/hooks/useCast.js +8 -26
  253. package/lib/module/hooks/useCast.js.map +1 -1
  254. package/lib/module/hooks/useEqualizer.js +19 -12
  255. package/lib/module/hooks/useEqualizer.js.map +1 -1
  256. package/lib/module/hooks/useLookaheadCache.js +3 -7
  257. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  258. package/lib/module/hooks/useQueue.js +66 -19
  259. package/lib/module/hooks/useQueue.js.map +1 -1
  260. package/lib/module/index.js +6 -3
  261. package/lib/module/index.js.map +1 -1
  262. package/lib/module/queueDelta.js +41 -0
  263. package/lib/module/queueDelta.js.map +1 -0
  264. package/lib/module/types.js +28 -5
  265. package/lib/module/types.js.map +1 -1
  266. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  267. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  269. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useCast.d.ts +6 -1
  271. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  272. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  273. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  274. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  275. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  276. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  277. package/lib/typescript/index.d.ts +2 -1
  278. package/lib/typescript/index.d.ts.map +1 -1
  279. package/lib/typescript/queueDelta.d.ts +10 -0
  280. package/lib/typescript/queueDelta.d.ts.map +1 -0
  281. package/lib/typescript/types.d.ts +168 -12
  282. package/lib/typescript/types.d.ts.map +1 -1
  283. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  284. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  285. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  286. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  287. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  288. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  289. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  320. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  321. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  322. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  323. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  324. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  325. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  326. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  327. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  328. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  329. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  330. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  331. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  332. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  333. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  334. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  335. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  336. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  337. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  338. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  339. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  340. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  341. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  342. package/package.json +8 -8
  343. package/src/TrackPlayer.nitro.ts +53 -27
  344. package/src/hooks/useActiveTrack.ts +29 -22
  345. package/src/hooks/useCast.ts +14 -15
  346. package/src/hooks/useEqualizer.ts +19 -12
  347. package/src/hooks/useLookaheadCache.ts +3 -7
  348. package/src/hooks/useQueue.ts +66 -17
  349. package/src/index.ts +13 -3
  350. package/src/queueDelta.ts +41 -0
  351. package/src/types.ts +169 -12
  352. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  353. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
  354. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  355. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  356. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  357. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  358. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  359. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  360. package/ios/MetadataReader.swift +0 -555
  361. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  362. package/ios/Tests/MetadataReaderTests.swift +0 -603
  363. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  364. package/ios/Tests/VoiceDonationTests.swift +0 -26
  365. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  366. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  367. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -0,0 +1,101 @@
1
+ import NitroModules
2
+
3
+ extension TrackPlayer {
4
+ // MARK: - Automotive / voice handler registration
5
+
6
+ /// Atomically replace the cached `BrowseSnapshot` on the
7
+ /// process-wide [CarPlayCoordinator]. The data provider posts a
8
+ /// `didUpdateNotification` on the main queue; an active CarPlay
9
+ /// scene observes the notification + rebuilds the root template
10
+ /// against the new snapshot. Push semantics — most-recent call
11
+ /// wins. Safe to call before a CarPlay scene attaches: the
12
+ /// snapshot is cached on the coordinator + the next scene reads
13
+ /// it directly at didConnect.
14
+ func setBrowseSnapshot(snapshot: BrowseSnapshot) throws {
15
+ CarPlayCoordinator.shared.dataProvider.setSnapshot(snapshot)
16
+ }
17
+
18
+ func onBrowseRequest(
19
+ callback: @escaping (_ parentId: String) -> Promise<Promise<[BrowseItem]>>
20
+ ) throws -> () -> Void {
21
+ return CarPlayCoordinator.shared.bridge.registerOnBrowseRequest(callback)
22
+ }
23
+
24
+ // `onSearchRequest` is Android-only — CarPlay audio-entitled apps
25
+ // cannot legally present `CPSearchTemplate` (per the CarPlay App
26
+ // Programming Guide template-by-category rules); pushing or
27
+ // presenting it raises NSInvalidArgumentException at runtime. Apple
28
+ // Music + Spotify both dropped CarPlay search in iOS 26; the
29
+ // canonical CarPlay search affordance for audio apps is Siri via
30
+ // SiriKit's `INPlayMediaIntent`, not an in-app search template.
31
+ //
32
+ // `onPlayFromSearchRequest` IS wired on iOS — it serves the
33
+ // SiriKit `INPlayMediaIntent` path: Siri parses "Hey Siri play X
34
+ // on MyApp" into a structured `INMediaSearch`, the lib's
35
+ // `PlayMediaIntentHandler` maps it to `MediaSearchRequest`, and
36
+ // dispatches into this callback via `CarPlayBridge`.
37
+ //
38
+ // For `onSearchRequest` we accept the consumer's registration on
39
+ // iOS to keep the TS API shape cross-platform; the callback is
40
+ // discarded (`_ = callback` consumes the value), and the returned
41
+ // disposer is a no-op so the consumer's bookkeeping stays sane.
42
+ func onSearchRequest(
43
+ callback: @escaping (_ query: String) -> Promise<Promise<[BrowseItem]>>
44
+ ) throws -> () -> Void {
45
+ _ = callback
46
+ return {}
47
+ }
48
+
49
+ func onPlayFromSearchRequest(
50
+ callback: @escaping (_ request: MediaSearchRequest) -> Promise<Promise<[TrackItem]>>
51
+ ) throws -> () -> Void {
52
+ return CarPlayCoordinator.shared.bridge.registerOnPlayFromSearchRequest(callback)
53
+ }
54
+
55
+ func onPlayFromIdRequest(
56
+ callback: @escaping (_ mediaId: String) -> Promise<Promise<[TrackItem]>>
57
+ ) throws -> () -> Void {
58
+ return CarPlayCoordinator.shared.bridge.registerOnPlayFromIdRequest(callback)
59
+ }
60
+
61
+ // Donation Hybrid method — fire-and-forget into Siri's vocabulary
62
+ // donation pipeline. Real impl lives in `ios/Siri/VoiceDonation.swift`.
63
+ // Uses `INVocabulary.setVocabularyStrings(_:of:)` to seed Siri's NLU
64
+ // with the consumer's artist + playlist names.
65
+ func donateVoiceVocabulary(vocabulary: VoiceVocabulary) throws {
66
+ // Opt-in only. `INVocabulary` raises an uncatchable Objective-C exception
67
+ // in a process without the `com.apple.developer.siri` entitlement, and
68
+ // iOS gives a process no way to read its own entitlements — so the
69
+ // consumer declaring the capability is the only signal the library can
70
+ // have that this is safe to touch.
71
+ guard self.voiceVocabularyDonationEnabled else { return }
72
+ VoiceDonation.donateVoiceVocabulary(vocabulary)
73
+ }
74
+
75
+ func onCarConnect(callback: @escaping () -> Void) throws -> () -> Void {
76
+ return CarPlayCoordinator.shared.bridge.registerOnCarConnect(callback)
77
+ }
78
+
79
+ // `onServiceReady` is an Android-only signal. iOS apps die fully
80
+ // under memory pressure (no separate service lifecycle), so there's
81
+ // no `service-reborn` scenario to surface here. The lib accepts the
82
+ // registration to keep the cross-platform API shape consistent and
83
+ // returns a no-op disposer; the callback is consumed (so it isn't
84
+ // retained) and never invoked. Consumers writing for both platforms
85
+ // register one handler; on iOS only `onCarConnect` carries the
86
+ // equivalent "automotive surface available" signal.
87
+ func onServiceReady(
88
+ callback: @escaping (_ reason: ServiceReadyReason) -> Void
89
+ ) throws -> () -> Void {
90
+ _ = callback
91
+ return {}
92
+ }
93
+
94
+ func onCarDisconnect(callback: @escaping () -> Void) throws -> () -> Void {
95
+ return CarPlayCoordinator.shared.bridge.registerOnCarDisconnect(callback)
96
+ }
97
+
98
+ func isCarConnected() throws -> Bool {
99
+ return CarPlayCoordinator.shared.isCarConnected
100
+ }
101
+ }
@@ -0,0 +1,232 @@
1
+ import Foundation
2
+ import NitroModules
3
+
4
+ extension TrackPlayer {
5
+ // MARK: - Lookahead cache — runtime config + status surface
6
+ func setLookaheadCache(config: LookaheadCacheConfig) throws -> Promise<Void> {
7
+ return enqueueThrowing {
8
+ try self.applyLookaheadConfig(config)
9
+ }
10
+ }
11
+
12
+ func getLookaheadCacheStatus() throws -> Promise<CacheStatus> {
13
+ // The snapshot counts fully-cached entries, which is a filesystem check
14
+ // per entry — the Promise keeps that work off the calling JS thread.
15
+ return enqueueValue { self.buildCacheStatusSnapshot() }
16
+ }
17
+
18
+ func clearLookaheadCache() throws -> Promise<Void> {
19
+ return enqueue {
20
+ // Cancel any in-flight prefetch + drop everything on disk.
21
+ // We deliberately do NOT call rescheduleLookahead() here —
22
+ // an immediate refill makes `clearLookaheadCache` impossible
23
+ // to observe at zero from JS, and "clear" semantically means
24
+ // "wipe and stop" (the consumer can re-trigger prefetch by
25
+ // calling setLookaheadCache, skipToIndex, or setQueue again).
26
+ self.lookaheadCachePrefetcher?.cancel()
27
+ self.lookaheadCache?.clear()
28
+ self.emitCacheStatus()
29
+ }
30
+ }
31
+
32
+ func onCacheStatusChange(callback: @escaping (_ status: CacheStatus) -> Void) throws -> () -> Void {
33
+ // ListenerRegistry.add is internally serialized, so a JS consumer
34
+ // that does
35
+ // const unsub = onCacheStatusChange(cb)
36
+ // await setLookaheadCache(cfg)
37
+ // is guaranteed to see the post-config-change emit (the
38
+ // setLookaheadCache call synchronously crosses the bridge after
39
+ // the listener is in the registry).
40
+ let id = cacheStatusListeners.add(callback)
41
+ // Deliver the current status once. Otherwise a settings screen shows its
42
+ // placeholder until the next prefetch completes — which never happens for a
43
+ // fully-cached queue or a consumer running with prefetch off.
44
+ playerQueue.async { [weak self] in
45
+ guard let self else { return }
46
+ guard self.cacheStatusListeners.contains(id: id) else { return }
47
+ callback(self.buildCacheStatusSnapshot())
48
+ }
49
+ return { [weak self] in self?.cacheStatusListeners.remove(id: id) }
50
+ }
51
+
52
+ /// Apply a runtime config update. All three runtime dimensions are
53
+ /// live (no configure() needed):
54
+ /// - `enabled` toggle: build/teardown the prefetcher against the cache
55
+ /// - `lookaheadCount` change: live update on the prefetcher
56
+ /// - `allowsCellularAccess` toggle: live on the cache, applied per request
57
+ ///
58
+ /// Cache SIZE + eviction policy are NOT runtime dimensions — they are fixed
59
+ /// at `configure()` from `PlayerConfig` (kept consistent with Android, whose
60
+ /// SimpleCache is immutable after construction).
61
+ internal func applyLookaheadConfig(_ config: LookaheadCacheConfig) throws {
62
+ let previous = self.lookaheadConfig
63
+ self.lookaheadConfig = config
64
+
65
+ let enableChanged = config.enabled != previous.enabled
66
+ let countChanged = config.lookaheadCount != previous.lookaheadCount
67
+
68
+ if enableChanged {
69
+ rebuildCacheStack(config: config)
70
+ } else if config.enabled {
71
+ applyLiveConfigUpdates(config: config, countChanged: countChanged)
72
+ }
73
+
74
+ // The cache applies this per request, so it lands whether or not the
75
+ // branches above ran. When there is no cache yet the stored config is what
76
+ // the next construction reads.
77
+ lookaheadCache?.allowsCellularAccess = config.allowsCellularAccess ?? true
78
+
79
+ emitCacheStatus()
80
+ }
81
+
82
+ /// Configure-time cache disk budget in MB — `PlayerConfig.lookaheadCacheMaxSizeMb`
83
+ /// or the 100 MB default. Fixed at cache construction; reported read-only via
84
+ /// `getLookaheadCacheStatus().maxSizeMb`.
85
+ internal func configuredCacheMaxSizeMb() -> Double {
86
+ self.config.lookaheadCacheMaxSizeMb
87
+ ?? Double(LookaheadCache.DEFAULT_MAX_SIZE_BYTES) / Double(LookaheadCache.BYTES_PER_MB)
88
+ }
89
+
90
+ /// Configure-time cache eviction policy — `PlayerConfig.lookaheadCacheEvictionPolicy`
91
+ /// or `.lru`. Fixed at cache construction; a change at `configure()` clears the
92
+ /// cache and rebuilds under the new policy (see `configure`).
93
+ internal func configuredEvictionPolicy() -> EvictionPolicy {
94
+ self.config.lookaheadCacheEvictionPolicy ?? .lru
95
+ }
96
+
97
+ /// Bring up the server that reads through `cache`. Called from both sites
98
+ /// that build a cache — `configure()` and the runtime `setLookaheadCache`
99
+ /// — because a cache without a server is one nothing ever reads from. A
100
+ /// server already reading through this same cache instance is kept, its
101
+ /// headers refreshed: replacing it would change the port seated items
102
+ /// address. Blocks for the bind (milliseconds) so every item built after
103
+ /// this call routes; an item built before the bind is bound to its origin.
104
+ internal func startReadThroughServer(for cache: LookaheadCache) {
105
+ if let existing = readThroughServer, readThroughServerCache === cache {
106
+ existing.setHeaders(
107
+ AVQueueBuilder.mergedHeaders(
108
+ headers: self.config.httpHeaders, userAgent: self.config.userAgent))
109
+ return
110
+ }
111
+ readThroughServer?.stop()
112
+ let server = ReadThroughServer(
113
+ cachedFileForOrigin: { [weak cache] sourceURL in
114
+ cache?.cachedURL(forUrl: sourceURL)
115
+ },
116
+ onFirstDiskHit: { [weak cache] sourceURL in
117
+ cache?.touch(url: sourceURL)
118
+ }
119
+ )
120
+ server.setHeaders(
121
+ AVQueueBuilder.mergedHeaders(
122
+ headers: self.config.httpHeaders, userAgent: self.config.userAgent))
123
+ server.start()
124
+ if !server.waitUntilBound() {
125
+ NSLog("[QueuePlayer] read-through server did not bind; remote tracks play from their origin")
126
+ }
127
+ self.readThroughServer = server
128
+ self.readThroughServerCache = cache
129
+ }
130
+
131
+ /// Quiesce or re-arm the cache, prefetcher and read-through server
132
+ /// when the `enabled` flag flipped. Disable cancels in-flight prefetch
133
+ /// and quiesces the cache (URLSession STAYS alive — it is only invalidated
134
+ /// by full `destroy`); the server stays up too, serving seated items from
135
+ /// the origin while the cache answers nothing, and `routingServer` binds
136
+ /// new items to their origin. Enable rebuilds the prefetcher (or constructs
137
+ /// the pair from scratch on first enable), keeps the server and reschedules.
138
+ ///
139
+ /// Keeping the cache instance + URLSession alive across disable
140
+ /// avoids `Task created in a session that has been invalidated`
141
+ /// (Obj-C exception, uncatchable in Swift's `do-catch`) when a
142
+ /// prefetcher Task survived disable + tried to download against
143
+ /// a freshly-invalidated session before the dispatch barrier saw
144
+ /// the cancellation.
145
+ private func rebuildCacheStack(config: LookaheadCacheConfig) {
146
+ if !config.enabled {
147
+ lookaheadCachePrefetcher?.tearDown()
148
+ lookaheadCachePrefetcher = nil
149
+ lookaheadCache?.disable()
150
+ return
151
+ }
152
+ if lookaheadCache == nil {
153
+ // Size is fixed at construction from the configure-time budget.
154
+ lookaheadCache = LookaheadCache(
155
+ maxSizeBytes: LookaheadCache.maxBytes(forMb: configuredCacheMaxSizeMb()),
156
+ evictionPolicy: configuredEvictionPolicy(),
157
+ allowsCellularAccess: config.allowsCellularAccess ?? true
158
+ )
159
+ } else {
160
+ lookaheadCache?.enable()
161
+ }
162
+ guard let cache = lookaheadCache else { return }
163
+ // The cache is built here as well as at `configure()`, and a cache with no
164
+ // server is a cache nothing reads: every item would bind its origin and
165
+ // route past it. Whatever builds the cache brings the server with it.
166
+ startReadThroughServer(for: cache)
167
+ let prefetcher = LookaheadCachePrefetcher(
168
+ cache: cache,
169
+ confinedTo: self.playerQueue,
170
+ configHeaders: self.config.httpHeaders,
171
+ configUserAgent: self.config.userAgent
172
+ )
173
+ prefetcher.defaultLookaheadCount = InputGuards.validLookaheadCount(config.lookaheadCount)
174
+ prefetcher.onTrackProcessed = { [weak self] in self?.emitCacheStatus() }
175
+ lookaheadCachePrefetcher = prefetcher
176
+ rescheduleLookahead()
177
+ }
178
+
179
+ /// Apply in-place config updates when the cache stack already exists
180
+ /// (no enable-flag change). Only `lookaheadCount` reaches here — it updates
181
+ /// the prefetcher's default + reschedules. `allowsCellularAccess` is set on
182
+ /// the cache by the caller, and cache size is configure-time.
183
+ private func applyLiveConfigUpdates(
184
+ config: LookaheadCacheConfig, countChanged: Bool
185
+ ) {
186
+ if countChanged {
187
+ lookaheadCachePrefetcher?.defaultLookaheadCount =
188
+ InputGuards.validLookaheadCount(config.lookaheadCount)
189
+ rescheduleLookahead()
190
+ }
191
+ }
192
+
193
+ /// Build a [CacheStatus] snapshot from current cache + prefetcher
194
+ /// state. Synchronous — the caller is already on the queue that serialises
195
+ /// player state.
196
+ internal func buildCacheStatusSnapshot() -> CacheStatus {
197
+ // `currentlyDownloadingUrl` is prefetcher state confined to `playerQueue`,
198
+ // so it is read here and handed to the builder as a value.
199
+ return buildCacheStatusSnapshot(
200
+ inFlightUrl: lookaheadCachePrefetcher?.currentlyDownloadingUrl)
201
+ }
202
+
203
+ /// [CacheStatus] from cache + config state plus the caller-supplied
204
+ /// in-flight prefetch URL. Reads no prefetcher state of its own.
205
+ internal func buildCacheStatusSnapshot(inFlightUrl: String?) -> CacheStatus {
206
+ let cache = self.lookaheadCache
207
+ let currentBytes = cache?.currentSizeBytes() ?? 0
208
+ // Queue-agnostic: count fully-cached entries in the cache itself,
209
+ // not the intersection with `self.tracks`. Mirrors the semantics
210
+ // of `currentSizeBytes` (total cache disk usage) and the
211
+ // prefetcher's `currentlyDownloadingUrl` (active downloads) — all
212
+ // three reflect cache state independent of any current queue.
213
+ // Per-track local/stream/cache attribution for the active queue
214
+ // is exposed via `getCurrentTrackSource()`.
215
+ let fullyCached = cache?.fullyCachedCount() ?? 0
216
+ return CacheStatus(
217
+ enabled: lookaheadConfig.enabled,
218
+ currentSizeMb: Double(currentBytes) / Double(LookaheadCache.BYTES_PER_MB),
219
+ maxSizeMb: configuredCacheMaxSizeMb(),
220
+ tracksFullyCached: Double(fullyCached),
221
+ currentlyCaching: inFlightUrl.map { [$0] } ?? []
222
+ )
223
+ }
224
+
225
+ /// Fire the cache-status callback with a fresh snapshot. Caller
226
+ /// must already be on the queue that serialises player state.
227
+ internal func emitCacheStatus() {
228
+ guard !cacheStatusListeners.isEmpty else { return }
229
+ let snapshot = buildCacheStatusSnapshot()
230
+ cacheStatusListeners.forEach { $0(snapshot) }
231
+ }
232
+ }
@@ -0,0 +1,291 @@
1
+ import Foundation
2
+ import NitroModules
3
+
4
+ extension TrackPlayer {
5
+ // MARK: - Playback configuration — some stubs, some lifecycle-adjacent
6
+
7
+ func setPlaybackMode(mode: PlaybackMode) throws -> Promise<Void> {
8
+ return enqueueThrowing {
9
+ let (newState, effect) = try self.playbackModeStateMachine.setRequested(
10
+ state: self.playbackModeState,
11
+ requested: mode
12
+ )
13
+ self.playbackModeState = newState
14
+ switch effect {
15
+ case .noOp:
16
+ break
17
+ case .immediateSwap:
18
+ self.applyEngineSwapForMode(newState.active)
19
+ }
20
+ }
21
+ }
22
+
23
+ func getPlaybackMode() throws -> PlaybackMode {
24
+ // Lock-guarded, so this answers without hopping. A `playerQueue.sync` here
25
+ // parks the JS thread behind whatever the queue is running, which can be a
26
+ // full queue rebuild.
27
+ return self.playbackModeState.active
28
+ }
29
+
30
+ /// Build a fresh engine of the requested kind and swap it in
31
+ /// place of the active engine. The new engine starts empty; the
32
+ /// canonical queue model is re-installed via the engine surface
33
+ /// so playback continues from the current track / position.
34
+ ///
35
+ /// JS-facing event sources differ per engine. GaplessEngine ties
36
+ /// `self.player` to the underlying `AVQueuePlayer` and the KVO
37
+ /// chain installed by `installGaplessObservers` /
38
+ /// `installEventObservers` carries onTrackChange / onStateChange
39
+ /// / onError. CrossfadeEngine sets `self.player` to nil and
40
+ /// fires `PlaybackEngineDelegate` callbacks that this class
41
+ /// implements (extension at the end of the file); the delegate
42
+ /// is wired below. Both engines surface progress through
43
+ /// `emitProgressIfSubscribed`, polymorphic over self.player vs
44
+ /// engine state.
45
+ internal func applyEngineSwapForMode(_ active: PlaybackMode) {
46
+ let oldEngine = self.engine
47
+ // Capture pre-swap state — the engine swap releases the old
48
+ // engine; reading `newEngine.currentPositionSeconds` /
49
+ // `isPlaying` after would always return 0 / false. The
50
+ // wasPlaying capture is what carries playback continuity
51
+ // across the immediate engine swap.
52
+ let resumePosition = oldEngine?.currentPositionSeconds ?? 0
53
+ let resumeIndex = self.currentTrackIndex
54
+ // The transport's intent, not the old engine's audible state: a leg that
55
+ // is buffering, or mid-crossfade with its outgoing item finished, reports
56
+ // not-playing while the consumer is very much still playing. Swapping on
57
+ // that reading silently pauses them.
58
+ let wasPlaying = self.wantsToPlay
59
+ // Anything held back from the old engine goes with it: the new one is
60
+ // installed from the queue model, which already carries every mutation.
61
+ self.engineReconcileDeferred = false
62
+
63
+ let newEngine: PlaybackEngine
64
+ switch active.kind {
65
+ case .gapless:
66
+ newEngine = GaplessEngine()
67
+ case .crossfade:
68
+ // PlaybackModeStateMachine.requireValid guarantees this is
69
+ // non-nil + within [1000, 12000] when `kind == .crossfade`.
70
+ // Fail loud rather than silently degrading to 0 (which would
71
+ // route through CrossfadeEngine's hard-cut path + skip
72
+ // engineCrossfadeDidBegin entirely).
73
+ guard let durationMs = active.crossfadeDurationMs else {
74
+ preconditionFailure("PlaybackMode(crossfade) missing crossfadeDurationMs at engine construction")
75
+ }
76
+ let durationSeconds = durationMs / 1000.0
77
+ newEngine = CrossfadeEngine(
78
+ crossfadeDurationSeconds: durationSeconds, confinedTo: self.playerQueue)
79
+ }
80
+
81
+ self.engine = newEngine
82
+ // GaplessEngine path: keep the established self.player KVO
83
+ // chain (currentItem / timeControlStatus / item.status) as
84
+ // the canonical event source. CrossfadeEngine path:
85
+ // self.player stays nil; the engine fires PlaybackEngineDelegate
86
+ // callbacks for track-change / state-change / format-ready /
87
+ // failed / play-to-end / queue-end and progress is sourced
88
+ // from the engine via a timer-driven fallback.
89
+ if let gapless = newEngine as? GaplessEngine {
90
+ self.player = gapless.player
91
+ self.gaplessFlipArmed = true
92
+ self.installGaplessObservers(on: gapless.player)
93
+ self.installEventObservers(on: gapless.player)
94
+ self.stopProgressFallbackTimer()
95
+ // Gapless does not need the engine delegate — the underlying
96
+ // self.player KVO chain is the canonical event source.
97
+ newEngine.delegate = nil
98
+ } else {
99
+ self.tearDownGaplessObservers()
100
+ self.tearDownEventObservers()
101
+ self.player = nil
102
+ newEngine.delegate = self
103
+ self.startProgressFallbackTimer()
104
+ }
105
+
106
+ // Re-wire the shared audio-tap provider to the new engine's
107
+ // audio-mix provider slot.
108
+ AudioTapProvider.shared.wireToPlaybackEngine(newEngine)
109
+
110
+ // Re-apply canonical config to the freshly-built engine BEFORE the
111
+ // queue install / play — it starts at engine defaults (repeat off,
112
+ // volume 1, speed 1), so without this a gapless↔crossfade swap
113
+ // silently drops the user's settings.
114
+ newEngine.setRepeatMode(self.repeatModeState)
115
+ newEngine.setVolume(self.volumeState)
116
+ // Carry the user's speed into the new engine. When resuming playback,
117
+ // apply it live via `setPlaybackSpeed` (sets `AVPlayer.rate`) BEFORE
118
+ // `setItems` — GaplessEngine.setItems captures the player's rate to
119
+ // restore it across the queue rebuild, so a speed applied after
120
+ // setItems/play() would be clobbered by that async restore. When
121
+ // paused, seed it only (no rate write, so the engine stays paused);
122
+ // play() applies it on the next resume.
123
+ if wasPlaying {
124
+ newEngine.setPlaybackSpeed(self.playbackSpeedState)
125
+ } else {
126
+ newEngine.seedPlaybackSpeed(self.playbackSpeedState)
127
+ }
128
+ newEngine.setPitchCorrectionMode(self.pitchCorrectionModeState)
129
+
130
+ // Re-install the queue on the new engine — the same window every other
131
+ // install uses, not the whole queue: a crossfade engine holds every item
132
+ // it is given, and a long queue would put every track's asset load
133
+ // through the read-through server at once. `topUpWindow` extends it.
134
+ if resumeIndex >= 0, resumeIndex < self.tracks.count {
135
+ let positions = QueueWindowArithmetic.slice(
136
+ currentIndex: resumeIndex,
137
+ trackCount: self.tracks.count,
138
+ trail: self.engineTrail,
139
+ wraps: false
140
+ )
141
+ let sliceIds = positions.map { self.queueItemIds[$0] }
142
+ let items = AVQueueBuilder.buildPlayerItems(
143
+ tracks: positions.map { self.tracks[$0] },
144
+ queueItemIds: sliceIds,
145
+ config: self.config,
146
+ server: self.routingServer
147
+ )
148
+ // Seated the way every other install is; with nothing playable at or
149
+ // after the current position the new engine stays empty rather than
150
+ // replaying the trail.
151
+ let currentId = self.queueItemIds[resumeIndex]
152
+ if let startIndex = Self.seatIndex(in: items, sliceIds: sliceIds, currentId: currentId) {
153
+ // The playhead carries across only onto the track that was playing;
154
+ // any other seat is a track change and starts at zero.
155
+ let position = items[startIndex].queueItemId == currentId ? resumePosition : 0
156
+ newEngine.setItems(
157
+ items,
158
+ startIndex: startIndex,
159
+ startPositionSeconds: position
160
+ )
161
+ // Honour pre-swap playing state so a mid-play setPlaybackMode keeps
162
+ // playing on the new engine — through `beginPlayback`, which every
163
+ // path that starts playback goes through, and not while the session
164
+ // is interrupted.
165
+ if wasPlaying, !self.isInterrupted {
166
+ self.beginPlayback()
167
+ }
168
+ }
169
+ }
170
+
171
+ // Whatever was seated above, the engine being replaced goes: its items
172
+ // stop loading and its observers come down.
173
+ oldEngine?.release()
174
+ }
175
+
176
+ func setPlaybackSpeed(rate: Double) throws -> Promise<Void> {
177
+ return enqueue {
178
+ // Clamp to a 0.25 floor: rate=0 on AVPlayer is functionally equivalent
179
+ // to pause(), which would surprise consumers calling
180
+ // `setPlaybackSpeed(0)` for a slow-down effect. Cross-platform contract
181
+ // documented in TrackPlayer.nitro.ts. A value that is not a number
182
+ // leaves the current rate alone.
183
+ guard !rate.isNaN else { return }
184
+ let safeRate = Float(max(0.25, min(rate, 32)))
185
+ self.playbackSpeedState = safeRate
186
+ // Apply to the LIVE rate only when the user wants playback (already
187
+ // playing, OR buffering toward play). On a paused/idle engine writing
188
+ // `AVPlayer.rate` starts it (rate > 0 == "play"), so with no play intent
189
+ // seed the speed instead (no rate write); `play()` applies it on the next
190
+ // resume. This keeps a persisted speed applied at app start — before a
191
+ // restored `setQueue` — from auto-starting a paused queue. `wantsToPlay`
192
+ // (not just `isPlaying`) covers the buffering-toward-play window so a
193
+ // speed change there still applies live.
194
+ if self.wantsToPlay || self.engine?.isPlaying == true {
195
+ self.engine?.setPlaybackSpeed(safeRate)
196
+ } else {
197
+ self.engine?.seedPlaybackSpeed(safeRate)
198
+ }
199
+ }
200
+ }
201
+ func setPitchCorrectionMode(mode: PitchCorrectionMode) throws -> Promise<Void> {
202
+ return enqueue {
203
+ self.pitchCorrectionModeState = mode
204
+ self.engine?.setPitchCorrectionMode(mode)
205
+ }
206
+ }
207
+ func getPitchCorrectionMode() throws -> PitchCorrectionMode {
208
+ // Lock-guarded — see `getPlaybackMode`.
209
+ return self.pitchCorrectionModeState
210
+ }
211
+ func setVolume(volume: Double) throws -> Promise<Void> {
212
+ return enqueue {
213
+ // Clamped to the documented range before either branch, so the cast and
214
+ // local paths cannot diverge on the same input, and a value that is not
215
+ // a number cannot poison the stored level that `getVolume` and the
216
+ // sleep-timer fade read back.
217
+ guard !volume.isNaN else { return }
218
+ let level = Float(max(0, min(1, volume)))
219
+ if CastTransportRouter.routeSetVolume(value: Double(level)) { return }
220
+ self.volumeState = level
221
+ self.engine?.setVolume(level)
222
+ }
223
+ }
224
+
225
+ func setRepeatMode(mode: RepeatMode) throws -> Promise<Void> {
226
+ return enqueue {
227
+ self.repeatModeState = mode
228
+ // Forward to the active engine. The CrossfadeEngine needs the
229
+ // mode to suppress its look-ahead fade + loop the current item
230
+ // under `.track` (AVPlayer has no native repeat); GaplessEngine
231
+ // treats it as a no-op (repeat-track replay lives in
232
+ // handlePlayerItemDidPlayToEndTime).
233
+ self.engine?.setRepeatMode(mode)
234
+ // Both canSkipNext and canSkipPrevious depend on repeat mode
235
+ // (e.g. at idx 0 / last index, OFF↔QUEUE flips the boolean
236
+ // because QUEUE wraps). Recompute is cheap; dedup keeps the
237
+ // listener silent when the (prev, next) tuple is unchanged.
238
+ // The installed run is forward-only, so a repeat-mode change does not
239
+ // alter which positions it holds and there is nothing to reconcile. That
240
+ // stops being true the moment the run carries the wrap target, and the
241
+ // reconcile has to come back with it.
242
+ self.recomputeCapabilities()
243
+ }
244
+ }
245
+
246
+ func setRemoteControls(options: RemoteControlOptions) throws -> Promise<Void> {
247
+ return enqueue {
248
+ // Guard nonsensical negatives (a negative forward interval would seek
249
+ // backward) and values too large to convert to milliseconds. Out-of-range
250
+ // is clamped rather than rejected.
251
+ let fwd = InputGuards.sanitisedSeekPosition(options.forwardJumpInterval)
252
+ .map { max(0, $0) } ?? 0
253
+ let back = InputGuards.sanitisedSeekPosition(options.backwardJumpInterval)
254
+ .map { max(0, $0) } ?? 0
255
+ self.remoteSkipMode = options.skipMode
256
+ self.remoteForwardInterval = fwd
257
+ self.remoteBackwardInterval = back
258
+ self.remoteCommands.setRemoteControls(
259
+ mode: options.skipMode,
260
+ forwardInterval: fwd,
261
+ backwardInterval: back)
262
+ // Re-apply the cached grey-out: switching to track mode re-enables
263
+ // next/prev, which must reflect the queue-boundary capability
264
+ // (setSkipCapability is otherwise only called on queue/repeat change,
265
+ // and no-ops in interval mode).
266
+ self.remoteCommands.setSkipCapability(
267
+ canSkipNext: self.cachedSkipCapability.canSkipNext,
268
+ canSkipPrevious: self.cachedSkipCapability.canSkipPrevious)
269
+ }
270
+ }
271
+
272
+ // ReplayGain mode — thin wrapper around `AudioTapProvider.shared`.
273
+ // The provider owns the per-item state set + recompute walk; this
274
+ // Hybrid surface is the JS-facing setter / getter only.
275
+ func setReplayGainMode(mode: ReplayGainMode) throws -> Promise<Void> {
276
+ return enqueue {
277
+ AudioTapProvider.shared.setReplayGainMode(mode)
278
+ // Re-emit the active item's NowPlayingFormat so
279
+ // `onNowPlayingFormatChange` subscribers (e.g. the
280
+ // `useNowPlayingFormat` hook) observe the new `replayGainActive`
281
+ // flag without waiting for the next track change. Mirrors the
282
+ // Android path's `refreshNowPlayingFormatForActiveItem` call from
283
+ // its own `setReplayGainMode`.
284
+ guard let item = self.activeMediaItem else { return }
285
+ self.emitCurrentNowPlayingFormat(for: item)
286
+ }
287
+ }
288
+ func getReplayGainMode() throws -> ReplayGainMode {
289
+ return AudioTapProvider.shared.getReplayGainMode()
290
+ }
291
+ }