react-native-queue-player 1.2.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/consumer-rules.pro +1 -1
  4. package/android/src/main/cpp/airplay2_control.c +4 -4
  5. package/android/src/main/cpp/airplay2_control.h +4 -4
  6. package/android/src/main/cpp/airplay2_jni.cpp +3 -19
  7. package/android/src/main/cpp/airplay2_pair.c +7 -7
  8. package/android/src/main/cpp/airplay2_pair.h +2 -2
  9. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  10. package/android/src/main/cpp/airplay2_session.c +2 -2
  11. package/android/src/main/cpp/airplay_jni.cpp +8 -69
  12. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  61. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  62. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  63. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  64. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  65. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  114. package/app.plugin.js +17 -1
  115. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  116. package/ios/AVQueueBuilder.swift +120 -85
  117. package/ios/ArtworkLoader.swift +0 -7
  118. package/ios/ArtworkResolver.swift +22 -4
  119. package/ios/AssetReplayGainReader.swift +118 -0
  120. package/ios/AudioSession.swift +67 -16
  121. package/ios/AudioTapProvider.swift +284 -149
  122. package/ios/CarPlayBridge.swift +0 -12
  123. package/ios/CarPlayCoordinator.swift +18 -2
  124. package/ios/CarPlaySceneDelegate.swift +2 -2
  125. package/ios/Cast/AirPlayRouteState.swift +9 -8
  126. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  127. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  128. package/ios/Cast/Core/CastSession.swift +2 -2
  129. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  130. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  131. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  132. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  133. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  134. package/ios/CastManager.swift +1 -1
  135. package/ios/CrossfadeEngine.swift +672 -226
  136. package/ios/EQTap.swift +20 -17
  137. package/ios/Equalizer.swift +1 -1
  138. package/ios/FLACStreamInfo.swift +65 -0
  139. package/ios/GaplessEngine.swift +57 -30
  140. package/ios/InputGuards.swift +35 -5
  141. package/ios/LookaheadCache.swift +229 -58
  142. package/ios/LookaheadCachePrefetcher.swift +48 -36
  143. package/ios/MediaResponseCheck.swift +122 -0
  144. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  145. package/ios/NetworkRecoveryPolicy.swift +32 -0
  146. package/ios/NowPlayingFormatExtractor.swift +25 -18
  147. package/ios/NowPlayingInfo.swift +84 -48
  148. package/ios/OutputRouteMonitor.swift +16 -3
  149. package/ios/PendingBrowseRequests.swift +1 -1
  150. package/ios/PlaceholderArtwork.swift +9 -5
  151. package/ios/PlaybackEngine.swift +165 -76
  152. package/ios/PlaybackErrorMapping.swift +3 -3
  153. package/ios/PlaybackModeStateMachine.swift +2 -2
  154. package/ios/PlaybackNetworkMonitor.swift +73 -0
  155. package/ios/PlayerStateDerivation.swift +6 -6
  156. package/ios/QueueMutationArithmetic.swift +25 -0
  157. package/ios/QueueSkipArithmetic.swift +5 -3
  158. package/ios/QueueWindowArithmetic.swift +152 -0
  159. package/ios/ReadThroughServer.swift +283 -0
  160. package/ios/RemoteCommands.swift +44 -4
  161. package/ios/ReplayGainData.swift +22 -3
  162. package/ios/ReplayGainExtractor.swift +70 -27
  163. package/ios/Siri/VoiceDonation.swift +14 -9
  164. package/ios/StreamingBitrateProbe.swift +22 -68
  165. package/ios/SuppliedReplayGain.swift +83 -0
  166. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  167. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  168. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  169. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  170. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  171. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  172. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  173. package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
  174. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  175. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  176. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  177. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  178. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  179. package/ios/Tests/EngineSwapTests.swift +91 -0
  180. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  181. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  182. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  183. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  184. package/ios/Tests/InputGuardsTests.swift +41 -0
  185. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  186. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  187. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  188. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  189. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  190. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  191. package/ios/Tests/MediaResponseCheckTests.swift +128 -0
  192. package/ios/Tests/MutationDeferralTests.swift +220 -0
  193. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  194. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  195. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  196. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  197. package/ios/Tests/PlayerFixtures.swift +48 -0
  198. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  199. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  200. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  201. package/ios/Tests/QueueStateTests.swift +3 -1
  202. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  203. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  204. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  205. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  206. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  207. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  208. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  209. package/ios/Tests/RetryRecoveryTests.swift +418 -0
  210. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  211. package/ios/Tests/SkipIndexTests.swift +32 -2
  212. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  213. package/ios/Tests/StallRecoveryTests.swift +97 -0
  214. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  215. package/ios/Tests/TopUpWindowGateTests.swift +441 -0
  216. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  217. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  218. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  219. package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
  220. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  221. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  222. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  223. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  224. package/ios/TrackPlayer+Automotive.swift +101 -0
  225. package/ios/TrackPlayer+Cache.swift +232 -0
  226. package/ios/TrackPlayer+Config.swift +291 -0
  227. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  228. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  229. package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
  230. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  231. package/ios/TrackPlayer+Lifecycle.swift +982 -0
  232. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  233. package/ios/TrackPlayer+Queue.swift +951 -0
  234. package/ios/TrackPlayer+Recovery.swift +213 -0
  235. package/ios/TrackPlayer+Skip.swift +465 -0
  236. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  237. package/ios/TrackPlayer+State.swift +108 -0
  238. package/ios/TrackPlayer+Threading.swift +135 -0
  239. package/ios/TrackPlayer+Transport.swift +272 -0
  240. package/ios/TrackPlayer+Window.swift +227 -0
  241. package/ios/TrackPlayer.swift +331 -4619
  242. package/ios/Visualizer.swift +6 -5
  243. package/ios/tests-harness/Podfile +1 -1
  244. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  245. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  246. package/lib/module/hooks/useActiveTrack.js +29 -22
  247. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  248. package/lib/module/hooks/useCast.js +8 -26
  249. package/lib/module/hooks/useCast.js.map +1 -1
  250. package/lib/module/hooks/useEqualizer.js +19 -12
  251. package/lib/module/hooks/useEqualizer.js.map +1 -1
  252. package/lib/module/hooks/useLookaheadCache.js +3 -7
  253. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  254. package/lib/module/hooks/useQueue.js +66 -19
  255. package/lib/module/hooks/useQueue.js.map +1 -1
  256. package/lib/module/index.js +6 -3
  257. package/lib/module/index.js.map +1 -1
  258. package/lib/module/queueDelta.js +41 -0
  259. package/lib/module/queueDelta.js.map +1 -0
  260. package/lib/module/types.js +28 -5
  261. package/lib/module/types.js.map +1 -1
  262. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  263. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  264. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  265. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  266. package/lib/typescript/hooks/useCast.d.ts +6 -1
  267. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  269. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  271. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  272. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  273. package/lib/typescript/index.d.ts +2 -1
  274. package/lib/typescript/index.d.ts.map +1 -1
  275. package/lib/typescript/queueDelta.d.ts +10 -0
  276. package/lib/typescript/queueDelta.d.ts.map +1 -0
  277. package/lib/typescript/types.d.ts +168 -12
  278. package/lib/typescript/types.d.ts.map +1 -1
  279. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  280. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  281. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  282. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  283. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  284. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  285. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  286. 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
  287. 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
  288. 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
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  300. 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
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  320. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  321. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  322. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  323. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  324. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  325. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  326. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  327. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  328. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  329. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  330. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  331. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  332. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  333. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  334. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  335. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  336. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  337. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  338. package/package.json +8 -8
  339. package/src/TrackPlayer.nitro.ts +53 -27
  340. package/src/hooks/useActiveTrack.ts +29 -22
  341. package/src/hooks/useCast.ts +14 -15
  342. package/src/hooks/useEqualizer.ts +19 -12
  343. package/src/hooks/useLookaheadCache.ts +3 -7
  344. package/src/hooks/useQueue.ts +66 -17
  345. package/src/index.ts +13 -3
  346. package/src/queueDelta.ts +41 -0
  347. package/src/types.ts +169 -12
  348. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  349. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  350. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  351. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  352. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  353. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  354. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  355. package/ios/MetadataReader.swift +0 -555
  356. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  357. package/ios/Tests/MetadataReaderTests.swift +0 -603
  358. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  359. package/ios/Tests/VoiceDonationTests.swift +0 -26
  360. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  361. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  362. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -1,14 +1,16 @@
1
1
  import XCTest
2
2
  @testable import QueuePlayer
3
3
 
4
- /// `TrackPlayer.classifyTrackSource(url:isUrlCached:)` decides how the active
5
- /// track is being played from lib-observable facts only: the URL scheme plus a
6
- /// cache-hit probe. Pure + static, so it's exercised directly without a live
7
- /// engine. Mirrors the Android source-classifier coverage.
4
+ /// `TrackPlayer.classifyTrackSource` decides how the active track is being
5
+ /// played from lib-observable facts only: the URL scheme, whether the item is
6
+ /// routed through the read-through server, and a cache-hit probe. Pure +
7
+ /// static, so it's exercised directly without a live engine.
8
8
  final class TrackSourceClassifierTests: XCTestCase {
9
9
 
10
- private func classify(_ url: String?, cached: Bool = false) -> TrackSource? {
11
- TrackPlayer.classifyTrackSource(url: url) { _ in cached }
10
+ private func classify(
11
+ _ url: String?, routed: Bool = true, cached: Bool = false
12
+ ) -> TrackSource? {
13
+ TrackPlayer.classifyTrackSource(url: url, isRouted: routed) { _ in cached }
12
14
  }
13
15
 
14
16
  func testNilUrlIsUnclassified() {
@@ -40,11 +42,35 @@ final class TrackSourceClassifierTests: XCTestCase {
40
42
 
41
43
  func testLocalFileNeverConsultsTheCacheProbe() {
42
44
  var probed = false
43
- let source = TrackPlayer.classifyTrackSource(url: "file:///a.m4a") { _ in
45
+ let source = TrackPlayer.classifyTrackSource(url: "file:///a.m4a", isRouted: true) { _ in
44
46
  probed = true
45
47
  return true
46
48
  }
47
49
  XCTAssertEqual(source, .local)
48
50
  XCTAssertFalse(probed, "a file:// URL is local without a cache lookup")
49
51
  }
52
+
53
+ /// Cached on disk is not the same as played from disk. Only an item bound to
54
+ /// the read-through server reads the cache, so a complete file the player is
55
+ /// not routed through is still being streamed — reporting otherwise is what
56
+ /// made this field claim `cached` while every byte came from the origin.
57
+ func testACachedTrackThePlayerIsNotRoutedThroughIsStreaming() {
58
+ XCTAssertEqual(
59
+ classify("https://example.com/a.mp3", routed: false, cached: true), .streaming)
60
+ }
61
+
62
+ func testARoutedTrackTheCacheHoldsInFullIsCached() {
63
+ XCTAssertEqual(
64
+ classify("https://example.com/a.mp3", routed: true, cached: true), .cached)
65
+ }
66
+
67
+ func testARoutedTrackTheCacheDoesNotHoldIsStreaming() {
68
+ XCTAssertEqual(
69
+ classify("https://example.com/a.mp3", routed: true, cached: false), .streaming)
70
+ }
71
+
72
+ /// A local file is local whatever the server and cache report.
73
+ func testAFileTrackIsLocalRegardlessOfRouting() {
74
+ XCTAssertEqual(classify("file:///a.m4a", routed: false, cached: true), .local)
75
+ }
50
76
  }
@@ -0,0 +1,38 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// Siri vocabulary donation is off unless the consumer opts in, and the
5
+ /// opt-in is fixed once taken.
6
+ ///
7
+ /// The flag stands for the `com.apple.developer.siri` entitlement, which
8
+ /// `INVocabulary` checks at runtime and which iOS gives a process no way to
9
+ /// read. An entitlement is decided when the app is signed, so a value that
10
+ /// changed later could not describe anything — and re-reading one would let a
11
+ /// consumer re-arm a path that terminates a host without the capability.
12
+ final class VoiceVocabularyOptInTests: XCTestCase {
13
+
14
+ func testDonationIsOffUntilTheConsumerOptsIn() {
15
+ let player = TrackPlayer()
16
+ XCTAssertFalse(
17
+ player.voiceVocabularyDonationEnabled,
18
+ "an untouched player has not been given the entitlement to assert")
19
+ }
20
+
21
+ func testTheFirstOptInIsTheOneThatCounts() {
22
+ let player = TrackPlayer()
23
+ player.pinVoiceVocabularyDonation(true)
24
+ player.pinVoiceVocabularyDonation(false)
25
+ XCTAssertTrue(
26
+ player.voiceVocabularyDonationEnabled,
27
+ "a later value does not unset an opt-in already taken")
28
+ }
29
+
30
+ func testDecliningIsAlsoFixed() {
31
+ let player = TrackPlayer()
32
+ player.pinVoiceVocabularyDonation(false)
33
+ player.pinVoiceVocabularyDonation(true)
34
+ XCTAssertFalse(
35
+ player.voiceVocabularyDonationEnabled,
36
+ "a later value does not turn donation on after it was declined")
37
+ }
38
+ }
@@ -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
+ }