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
@@ -29,6 +29,33 @@ final class AudioTapProviderTests: XCTestCase {
29
29
  )
30
30
  }
31
31
 
32
+ func testConsumerWantingTapGetsAWorkingMixForALoadedLocalAsset() async throws {
33
+ let provider = AudioTapProvider()
34
+ provider.register(makeConsumer(wantsTap: { true }))
35
+
36
+ let url = try requireFixture(name: "silence-cbr-128", ext: "mp3")
37
+ let asset = AVURLAsset(url: url)
38
+ // The mix keys its input parameters to an `AVAssetTrack`, so the tracks
39
+ // key has to be loaded before there is anything to key them to.
40
+ let tracks = try await asset.load(.tracks)
41
+ let item = AVPlayerItem(asset: asset)
42
+
43
+ let mix = try XCTUnwrap(
44
+ provider.audioMix(for: item),
45
+ "a registered consumer wanting the tap must receive a mix")
46
+ let params = try XCTUnwrap(
47
+ mix.inputParameters.first as? AVMutableAudioMixInputParameters,
48
+ "the mix must carry input parameters for the asset's audio track")
49
+ XCTAssertNotNil(
50
+ params.audioTapProcessor,
51
+ "the input parameters must carry the processing tap — a mix without one "
52
+ + "silently drops ReplayGain, the equalizer and the visualizer")
53
+
54
+ let audioTrackID = try XCTUnwrap(
55
+ tracks.first { $0.mediaType == .audio }?.trackID)
56
+ XCTAssertEqual(params.trackID, audioTrackID)
57
+ }
58
+
32
59
  func testNoConsumersWantingTapReturnsNilEvenForLocalAssets() {
33
60
  let provider = AudioTapProvider()
34
61
  XCTAssertFalse(provider.isEnabledSnapshot())
@@ -106,4 +133,23 @@ final class AudioTapProviderTests: XCTestCase {
106
133
  _ = provider.audioMix(for: item)
107
134
  XCTAssertEqual(1, fires, "unregistered consumer must not see further events")
108
135
  }
136
+
137
+ private func requireFixture(name: String, ext: String) throws -> URL {
138
+ let bundle = Bundle(for: type(of: self))
139
+ if let url = bundle.url(forResource: name, withExtension: ext) {
140
+ return url
141
+ }
142
+ let candidates = bundle.urls(forResourcesWithExtension: ext, subdirectory: nil) ?? []
143
+ if let match = candidates.first(where: { $0.deletingPathExtension().lastPathComponent == name }) {
144
+ return match
145
+ }
146
+ // Fixtures are bundled via `test_spec.resources` in QueuePlayer.podspec.
147
+ // A miss means the test-harness pod install regressed — that's a real
148
+ // infrastructure failure, not a conditionally-inapplicable test.
149
+ return try XCTUnwrap(
150
+ nil as URL?,
151
+ "fixture \(name).\(ext) not bundled — pod install regression in tests-harness"
152
+ )
153
+ }
154
+
109
155
  }
@@ -257,7 +257,6 @@ private final class RecordingEngineStub: PlaybackEngine {
257
257
  var refreshCount: Int = 0
258
258
  var onRefresh: (() -> Void)?
259
259
 
260
- func prepare(items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) async {}
261
260
  func release() {}
262
261
  func setItems(_ items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) {}
263
262
  @discardableResult
@@ -295,7 +294,11 @@ private final class RecordingEngineStub: PlaybackEngine {
295
294
  onRefresh?()
296
295
  }
297
296
 
298
- func handoff(to nextEngine: PlaybackEngine, atTrackBoundary: Bool) async {}
297
+ /// No fade to schedule this stub exercises the rest of the surface.
298
+ var isFadePendingOrActive: Bool { false }
299
+
300
+ /// Nothing attached — this stub exercises the rest of the surface.
301
+ var attachedQueueItemIds: [String] { [] }
299
302
 
300
303
  var delegate: PlaybackEngineDelegate?
301
304
  }
@@ -0,0 +1,88 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// The FLAC STREAMINFO parse, the only source of a local FLAC bitrate.
5
+ final class FLACStreamInfoTests: XCTestCase {
6
+
7
+
8
+ func testFLACStreamInfoExtractsSampleRateAndTotalSamples() throws {
9
+ // Build a STREAMINFO-only FLAC header.
10
+ // sampleRate=44100, channels=2, bitsPerSample=16, totalSamples=1234567
11
+ let info = makeFLACStreamInfo(
12
+ sampleRate: 44100,
13
+ channels: 2,
14
+ bitsPerSample: 16,
15
+ totalSamples: 1_234_567
16
+ )
17
+ let parsed = try XCTUnwrap(FLACStreamInfo.parse(info))
18
+ XCTAssertEqual(parsed.sampleRate, 44100)
19
+ XCTAssertEqual(parsed.channelCount, 2)
20
+ XCTAssertEqual(parsed.bitsPerSample, 16)
21
+ XCTAssertEqual(parsed.totalSamples, 1_234_567)
22
+ }
23
+
24
+ func testFLACStreamInfoHighSampleRate() throws {
25
+ // foobar2000 + some lossless masters use 96 kHz / 24-bit. Verify
26
+ // the 20-bit sampleRate field isn't truncated.
27
+ let info = makeFLACStreamInfo(
28
+ sampleRate: 96000,
29
+ channels: 2,
30
+ bitsPerSample: 24,
31
+ totalSamples: 9_876_543
32
+ )
33
+ let parsed = try XCTUnwrap(FLACStreamInfo.parse(info))
34
+ XCTAssertEqual(parsed.sampleRate, 96000)
35
+ XCTAssertEqual(parsed.bitsPerSample, 24)
36
+ XCTAssertEqual(parsed.totalSamples, 9_876_543)
37
+ }
38
+
39
+ func testFLACStreamInfoReturnsNilOnNonFLACBytes() {
40
+ XCTAssertNil(FLACStreamInfo.parse(
41
+ Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
42
+ ))
43
+ }
44
+
45
+ // MARK: - Test helpers — byte-level fixture builders
46
+
47
+ private func makeFLACStreamInfo(
48
+ sampleRate: UInt32,
49
+ channels: Int,
50
+ bitsPerSample: Int,
51
+ totalSamples: Int64
52
+ ) -> Data {
53
+ var data = Data()
54
+ data.append("fLaC".data(using: .ascii)!)
55
+ // Block header: last-block flag (0x80) + type 0 (STREAMINFO) +
56
+ // 24-bit size = 34 bytes.
57
+ data.append(0x80)
58
+ data.append(beUInt24(34))
59
+ // Body. 34 bytes total.
60
+ var body = Data(repeating: 0, count: 34)
61
+ // minBlockSize, maxBlockSize, minFrameSize, maxFrameSize: bytes
62
+ // 0..9, leave zero. Bytes 10..12 contain the 20-bit sampleRate
63
+ // followed by 3-bit channels-1 and 5-bit bitsPerSample-1 split
64
+ // across bytes 12..13. totalSamples is 36 bits at bytes 13..17.
65
+ let sr = UInt32(sampleRate) & 0x000FFFFF
66
+ let chMinusOne = UInt8((channels - 1) & 0x07)
67
+ let bpsMinusOne = UInt8((bitsPerSample - 1) & 0x1F)
68
+ body[10] = UInt8((sr >> 12) & 0xFF)
69
+ body[11] = UInt8((sr >> 4) & 0xFF)
70
+ body[12] = UInt8(((sr & 0x0F) << 4) | (UInt32(chMinusOne) << 1) | UInt32(bpsMinusOne >> 4))
71
+ body[13] = UInt8(((bpsMinusOne & 0x0F) << 4) | UInt8((totalSamples >> 32) & 0x0F))
72
+ body[14] = UInt8((totalSamples >> 24) & 0xFF)
73
+ body[15] = UInt8((totalSamples >> 16) & 0xFF)
74
+ body[16] = UInt8((totalSamples >> 8) & 0xFF)
75
+ body[17] = UInt8(totalSamples & 0xFF)
76
+ // Bytes 18..33: MD5, leave zero.
77
+ data.append(body)
78
+ return data
79
+ }
80
+
81
+ private func beUInt24(_ v: UInt32) -> Data {
82
+ return Data([
83
+ UInt8((v >> 16) & 0xff),
84
+ UInt8((v >> 8) & 0xff),
85
+ UInt8(v & 0xff),
86
+ ])
87
+ }
88
+ }
@@ -173,12 +173,12 @@ final class GaplessEngineLifecycleTests: XCTestCase {
173
173
 
174
174
  private final class NoopDelegate: PlaybackEngineDelegate {
175
175
  func engineStateMaybeChanged(_ engine: PlaybackEngine, reason: StateChangeReason?) {}
176
- func engineIsPlayingChanged(_ engine: PlaybackEngine, isPlaying: Bool) {}
177
176
  func engineDidTransitionTrack(_ engine: PlaybackEngine) {}
178
177
  func engineActiveItemFormatChanged(_ engine: PlaybackEngine) {}
179
- func engineDidFailWithError(_ engine: PlaybackEngine, error: Error) {}
178
+ func engineDidFailWithError(
179
+ _ engine: PlaybackEngine, error: Error, item: AVPlayerItem?) {}
180
180
  func engineDidPlayItemToEnd(_ engine: PlaybackEngine) {}
181
181
  func enginePlaybackEnded(_ engine: PlaybackEngine) {}
182
- func engineCrossfadeDidBegin(_ engine: PlaybackEngine, incomingIndex: Int) {}
183
- func engineCrossfadeDidCancel(_ engine: PlaybackEngine, revertedIndex: Int) {}
182
+ func engineCrossfadeDidBegin(_ engine: PlaybackEngine) {}
183
+ func engineCrossfadeDidCancel(_ engine: PlaybackEngine) {}
184
184
  }
@@ -8,8 +8,49 @@ import XCTest
8
8
  /// are covered without a live AVPlayer or AudioSession.
9
9
  final class InputGuardsTests: XCTestCase {
10
10
 
11
+ /// A finite value too large for `Int64` is as fatal as an infinite one once
12
+ /// it is scaled to milliseconds, so the guard bounds magnitude, not just
13
+ /// finiteness.
14
+ func testSeekPositionIsBoundedToAConvertibleMagnitude() throws {
15
+ let huge = try XCTUnwrap(InputGuards.sanitisedSeekPosition(1e300))
16
+ XCTAssertLessThan(huge * 1000.0, Double(Int64.max))
17
+ let negative = try XCTUnwrap(InputGuards.sanitisedSeekPosition(-1e300))
18
+ XCTAssertGreaterThan(negative * 1000.0, Double(Int64.min))
19
+ }
20
+
21
+ func testSeekPositionPassesOrdinaryValuesThrough() {
22
+ XCTAssertEqual(InputGuards.sanitisedSeekPosition(42.5), 42.5)
23
+ XCTAssertNil(InputGuards.sanitisedSeekPosition(.nan))
24
+ XCTAssertNil(InputGuards.sanitisedSeekPosition(.infinity))
25
+ }
26
+
27
+ /// The conversion has to be bounded in `Double` space; `Int(_:)` traps on a
28
+ /// finite value larger than `Int` before any clamp on the result can apply.
29
+ func testLookaheadCountIsBoundedBeforeConversion() {
30
+ XCTAssertEqual(InputGuards.validLookaheadCount(.infinity), 10_000)
31
+ XCTAssertEqual(InputGuards.validLookaheadCount(1e300), 10_000)
32
+ XCTAssertEqual(InputGuards.validLookaheadCount(.nan), 0)
33
+ XCTAssertEqual(InputGuards.validLookaheadCount(-5), 0)
34
+ XCTAssertEqual(InputGuards.validLookaheadCount(7), 7)
35
+ }
36
+
11
37
  // MARK: - validQueueIndex
12
38
 
39
+ /// A finite value past what `Int` holds means "append", the same as any
40
+ /// other position past the end; converting it first would trap.
41
+ func testInsertPositionClampsAMagnitudeIntCannotHold() {
42
+ XCTAssertEqual(InputGuards.insertPosition(1e300, count: 5), 5)
43
+ XCTAssertEqual(InputGuards.insertPosition(-1e300, count: 5), 0)
44
+ }
45
+
46
+ func testInsertPositionPassesOrdinaryValuesThroughAndDropsNonFinite() {
47
+ XCTAssertEqual(InputGuards.insertPosition(2, count: 5), 2)
48
+ XCTAssertEqual(InputGuards.insertPosition(7, count: 5), 5)
49
+ XCTAssertNil(InputGuards.insertPosition(nil, count: 5))
50
+ XCTAssertNil(InputGuards.insertPosition(.nan, count: 5))
51
+ XCTAssertNil(InputGuards.insertPosition(.infinity, count: 5))
52
+ }
53
+
13
54
  func testValidQueueIndexReturnsIntForInRangeValue() {
14
55
  XCTAssertEqual(InputGuards.validQueueIndex(0, count: 5), 0)
15
56
  XCTAssertEqual(InputGuards.validQueueIndex(2, count: 5), 2)
@@ -0,0 +1,77 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// `isInterrupted` disables retries, window refills and network recovery
5
+ /// while the system holds the session. The system does not always deliver
6
+ /// the interruption's end, so the flag has user-side exits too.
7
+ final class InterruptionIntentTests: XCTestCase {
8
+
9
+ private var player: TrackPlayer!
10
+
11
+ override func setUp() async throws {
12
+ try await super.setUp()
13
+ player = TrackPlayer()
14
+ _ = try await player.configure(config: PlayerFixtures.config()).await()
15
+ _ = try await player.setQueue(
16
+ tracks: PlayerFixtures.localTracks(count: 2, for: self), startAtIndex: 0
17
+ ).await()
18
+ }
19
+
20
+ override func tearDown() async throws {
21
+ if let player {
22
+ _ = try await player.destroy().await()
23
+ }
24
+ player = nil
25
+ try await super.tearDown()
26
+ }
27
+
28
+ /// An explicit play is the user ending the interruption from their side.
29
+ func testAnExplicitPlayEndsTheInterruption() async throws {
30
+ let player = try XCTUnwrap(self.player)
31
+ await player.onPlayerQueue { player.isInterrupted = true }
32
+
33
+ _ = try await player.play().await()
34
+
35
+ let interrupted = await player.onPlayerQueueValue { player.isInterrupted }
36
+ XCTAssertFalse(interrupted)
37
+ }
38
+
39
+ /// At the end of the queue play() does nothing: the engine is not asked to
40
+ /// play and the intent is not recorded, so a track a later skip seats waits
41
+ /// for its own play().
42
+ func testPlayAtTheEndOfTheQueueRestartsNothing() async throws {
43
+ let player = try XCTUnwrap(self.player)
44
+ let engine = ActiveItemEngineStub()
45
+ await player.onPlayerQueue {
46
+ player.engine = engine
47
+ player.reachedQueueEnd = true
48
+ player.wantsToPlay = false
49
+ }
50
+
51
+ _ = try await player.play().await()
52
+
53
+ XCTAssertEqual(engine.playCalls, 0, "the parked engine is left alone")
54
+ let state = await player.onPlayerQueueValue { (player.wantsToPlay, player.reachedQueueEnd) }
55
+ XCTAssertFalse(state.0, "no intent is recorded for a queue that is over")
56
+ XCTAssertTrue(state.1)
57
+ }
58
+
59
+ /// Nothing about the torn-down session survives into the next configure.
60
+ func testDestroyClearsTheInterruptionAndRecoveryFlags() async throws {
61
+ let player = try XCTUnwrap(self.player)
62
+ await player.onPlayerQueue {
63
+ player.isInterrupted = true
64
+ player.isRecoveringFromStall = true
65
+ player.engineReconcileDeferred = true
66
+ }
67
+
68
+ _ = try await player.destroy().await()
69
+
70
+ let flags = await player.onPlayerQueueValue {
71
+ (player.isInterrupted, player.isRecoveringFromStall, player.engineReconcileDeferred)
72
+ }
73
+ XCTAssertFalse(flags.0)
74
+ XCTAssertFalse(flags.1)
75
+ XCTAssertFalse(flags.2)
76
+ }
77
+ }
@@ -0,0 +1,185 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// Prefetch requests carry `allowsCellularAccess`, and `setLookaheadCache`
5
+ /// changes it live — the cache instance and everything it holds on disk stay
6
+ /// as they are.
7
+ final class LookaheadCacheCellularAccessTests: XCTestCase {
8
+
9
+ private var tempDir: URL!
10
+ private var session: URLSession!
11
+
12
+ override func setUp() {
13
+ super.setUp()
14
+ tempDir = FileManager.default.temporaryDirectory
15
+ .appendingPathComponent("CellularAccess-\(UUID().uuidString)", isDirectory: true)
16
+ let config = URLSessionConfiguration.ephemeral
17
+ config.protocolClasses = [StubURLProtocol.self] + (config.protocolClasses ?? [])
18
+ session = URLSession(configuration: config)
19
+ StubURLProtocol.reset()
20
+ }
21
+
22
+ override func tearDown() {
23
+ session?.invalidateAndCancel()
24
+ try? FileManager.default.removeItem(at: tempDir)
25
+ StubURLProtocol.reset()
26
+ super.tearDown()
27
+ }
28
+
29
+ // MARK: - The request carries the policy
30
+
31
+ func testAPrefetchRequestIsAllowedOverCellularByDefault() async throws {
32
+ let cache = makeCache()
33
+ let url = URL(string: "https://example.com/default.m4a")!
34
+ StubURLProtocol.stub(url: url, data: Data("payload".utf8), statusCode: 200)
35
+
36
+ try await cache.download(track: makeTrack(id: "t", url: url.absoluteString), headers: nil)
37
+
38
+ XCTAssertEqual(StubURLProtocol.lastRequest(for: url)?.allowsCellularAccess, true)
39
+ }
40
+
41
+ func testAPrefetchRequestRefusesCellularWhenTheCacheIsToldTo() async throws {
42
+ let cache = makeCache(allowsCellularAccess: false)
43
+ let url = URL(string: "https://example.com/denied.m4a")!
44
+ StubURLProtocol.stub(url: url, data: Data("payload".utf8), statusCode: 200)
45
+
46
+ try await cache.download(track: makeTrack(id: "t", url: url.absoluteString), headers: nil)
47
+
48
+ XCTAssertEqual(StubURLProtocol.lastRequest(for: url)?.allowsCellularAccess, false)
49
+ }
50
+
51
+ func testTheNextRequestPicksUpAChangedPolicy() async throws {
52
+ let cache = makeCache()
53
+ let first = URL(string: "https://example.com/before.m4a")!
54
+ let second = URL(string: "https://example.com/after.m4a")!
55
+ StubURLProtocol.stub(url: first, data: Data("a".utf8), statusCode: 200)
56
+ StubURLProtocol.stub(url: second, data: Data("b".utf8), statusCode: 200)
57
+ try await cache.download(track: makeTrack(id: "a", url: first.absoluteString), headers: nil)
58
+
59
+ cache.allowsCellularAccess = false
60
+ try await cache.download(track: makeTrack(id: "b", url: second.absoluteString), headers: nil)
61
+
62
+ XCTAssertEqual(StubURLProtocol.lastRequest(for: first)?.allowsCellularAccess, true)
63
+ XCTAssertEqual(StubURLProtocol.lastRequest(for: second)?.allowsCellularAccess, false)
64
+ }
65
+
66
+ // MARK: - The runtime switch
67
+
68
+ /// `applyLookaheadConfig` is player-queue confined — the prefetcher asserts
69
+ /// it — so the tests reach it the way `setLookaheadCache` does.
70
+ private func apply(
71
+ _ player: TrackPlayer, enabled: Bool, count: Double, cellular: Bool?
72
+ ) throws {
73
+ var caught: Error?
74
+ player.playerQueue.sync {
75
+ do {
76
+ try player.applyLookaheadConfig(
77
+ LookaheadCacheConfig(
78
+ enabled: enabled, lookaheadCount: count, allowsCellularAccess: cellular))
79
+ } catch {
80
+ caught = error
81
+ }
82
+ }
83
+ if let caught { throw caught }
84
+ }
85
+
86
+ /// `enabled` starts true, so the cache only exists once the flag has been
87
+ /// cycled — the construction path a consumer hits at `configure()`.
88
+ private func playerWithCache() throws -> TrackPlayer {
89
+ let player = TrackPlayer()
90
+ try apply(player, enabled: false, count: 1, cellular: true)
91
+ try apply(player, enabled: true, count: 1, cellular: true)
92
+ return player
93
+ }
94
+
95
+ func testTurningCellularOffKeepsTheCacheAndOnlyChangesThePolicy() throws {
96
+ let player = try playerWithCache()
97
+ defer { _ = try? player.destroy() }
98
+ let cache = try XCTUnwrap(player.lookaheadCache)
99
+ XCTAssertTrue(cache.allowsCellularAccess)
100
+
101
+ try apply(player, enabled: true, count: 1, cellular: false)
102
+
103
+ XCTAssertTrue(cache === player.lookaheadCache, "nothing is rebuilt — the policy is live")
104
+ XCTAssertFalse(cache.allowsCellularAccess)
105
+ }
106
+
107
+ func testTurningCellularBackOnRestoresIt() throws {
108
+ let player = try playerWithCache()
109
+ defer { _ = try? player.destroy() }
110
+ try apply(player, enabled: true, count: 1, cellular: false)
111
+
112
+ try apply(player, enabled: true, count: 1, cellular: true)
113
+
114
+ XCTAssertTrue(try XCTUnwrap(player.lookaheadCache).allowsCellularAccess)
115
+ }
116
+
117
+ /// The cache is constructed lazily, so a policy set while it does not exist
118
+ /// has to reach the instance the next construction produces.
119
+ func testACacheBuiltAfterThePolicyWasSetHonoursIt() throws {
120
+ let player = TrackPlayer()
121
+ defer { _ = try? player.destroy() }
122
+ try apply(player, enabled: false, count: 1, cellular: false)
123
+ XCTAssertNil(player.lookaheadCache, "precondition: nothing built yet")
124
+
125
+ try apply(player, enabled: true, count: 1, cellular: false)
126
+
127
+ XCTAssertFalse(try XCTUnwrap(player.lookaheadCache).allowsCellularAccess)
128
+ }
129
+
130
+ /// An omitted `allowsCellularAccess` reads as permitted, so a consumer that
131
+ /// never sets it is not silently cut off from prefetching.
132
+ func testOmittingTheFlagLeavesCellularPermitted() throws {
133
+ let player = TrackPlayer()
134
+ defer { _ = try? player.destroy() }
135
+ try apply(player, enabled: false, count: 1, cellular: nil)
136
+ try apply(player, enabled: true, count: 1, cellular: nil)
137
+
138
+ XCTAssertTrue(try XCTUnwrap(player.lookaheadCache).allowsCellularAccess)
139
+ }
140
+
141
+ /// `configure()` builds the cache itself rather than going through
142
+ /// `applyLookaheadConfig`, so it has to read the stored policy too.
143
+ func testConfigureBuildsTheCacheUnderTheStoredPolicy() async throws {
144
+ let player = TrackPlayer()
145
+ // Prefetch stays enabled — configure only builds the cache when it is —
146
+ // and with no cache yet the policy lives in the stored config alone.
147
+ try apply(player, enabled: true, count: 1, cellular: false)
148
+ XCTAssertNil(player.lookaheadCache, "precondition: configure does the building")
149
+
150
+ _ = try await player.configure(config: Self.playerConfig()).await()
151
+ defer { _ = try? player.destroy() }
152
+
153
+ let cache = try XCTUnwrap(player.lookaheadCache)
154
+ XCTAssertFalse(cache.allowsCellularAccess)
155
+ }
156
+
157
+ private static func playerConfig() -> PlayerConfig {
158
+ return PlayerConfig(
159
+ httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil,
160
+ autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil,
161
+ audioContentType: nil, audioCategoryOptions: nil,
162
+ voiceVocabularyDonationEnabled: nil,
163
+ visualizationEnabled: nil, clampSeekToBuffered: nil,
164
+ skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil,
165
+ lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil,
166
+ backgroundProgressUpdateIntervalMs: nil
167
+ )
168
+ }
169
+
170
+ private func makeTrack(id: String, url: String) -> TrackItem {
171
+ return TrackItem(
172
+ id: id, url: url, title: nil, artist: nil, album: nil,
173
+ duration: nil, artworkUrl: nil, extras: nil,
174
+ replayGainTrackGain: nil, replayGainTrackPeak: nil,
175
+ replayGainAlbumGain: nil, replayGainAlbumPeak: nil
176
+ )
177
+ }
178
+
179
+ private func makeCache(allowsCellularAccess: Bool = true) -> LookaheadCache {
180
+ LookaheadCache(
181
+ directory: tempDir, maxSizeBytes: 10 * 1024 * 1024, urlSession: session,
182
+ allowsCellularAccess: allowsCellularAccess
183
+ )
184
+ }
185
+ }