react-native-queue-player 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/build.gradle +7 -0
  4. package/android/consumer-rules.pro +72 -3
  5. package/android/src/main/cpp/airplay2_control.c +4 -4
  6. package/android/src/main/cpp/airplay2_control.h +4 -4
  7. package/android/src/main/cpp/airplay2_jni.cpp +26 -22
  8. package/android/src/main/cpp/airplay2_pair.c +7 -7
  9. package/android/src/main/cpp/airplay2_pair.h +2 -2
  10. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  11. package/android/src/main/cpp/airplay2_session.c +2 -2
  12. package/android/src/main/cpp/airplay_jni.cpp +20 -72
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
  61. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  62. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  63. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  64. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  65. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
  114. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
  115. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  116. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  117. package/app.plugin.js +17 -1
  118. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  119. package/ios/AVQueueBuilder.swift +120 -85
  120. package/ios/ArtworkLoader.swift +0 -7
  121. package/ios/ArtworkResolver.swift +22 -4
  122. package/ios/AssetReplayGainReader.swift +118 -0
  123. package/ios/AudioSession.swift +67 -16
  124. package/ios/AudioTapProvider.swift +284 -149
  125. package/ios/CarPlayBridge.swift +0 -12
  126. package/ios/CarPlayCoordinator.swift +18 -2
  127. package/ios/CarPlaySceneDelegate.swift +2 -2
  128. package/ios/Cast/AirPlayRouteState.swift +9 -8
  129. package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
  130. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  131. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  132. package/ios/Cast/Core/CastSession.swift +11 -0
  133. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  134. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  135. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  136. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  137. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  138. package/ios/CastManager.swift +1 -1
  139. package/ios/CrossfadeEngine.swift +660 -226
  140. package/ios/EQTap.swift +20 -17
  141. package/ios/Equalizer.swift +1 -1
  142. package/ios/FLACStreamInfo.swift +65 -0
  143. package/ios/GaplessEngine.swift +57 -30
  144. package/ios/InputGuards.swift +35 -5
  145. package/ios/LookaheadCache.swift +202 -58
  146. package/ios/LookaheadCachePrefetcher.swift +48 -36
  147. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  148. package/ios/NetworkRecoveryPolicy.swift +32 -0
  149. package/ios/NowPlayingFormatExtractor.swift +25 -18
  150. package/ios/NowPlayingInfo.swift +84 -48
  151. package/ios/OutputRouteMonitor.swift +16 -3
  152. package/ios/PendingBrowseRequests.swift +1 -1
  153. package/ios/PlaceholderArtwork.swift +9 -5
  154. package/ios/PlaybackEngine.swift +165 -76
  155. package/ios/PlaybackErrorMapping.swift +3 -3
  156. package/ios/PlaybackModeStateMachine.swift +2 -2
  157. package/ios/PlaybackNetworkMonitor.swift +73 -0
  158. package/ios/PlayerStateDerivation.swift +6 -6
  159. package/ios/QueueMutationArithmetic.swift +25 -0
  160. package/ios/QueueSkipArithmetic.swift +5 -3
  161. package/ios/QueueWindowArithmetic.swift +152 -0
  162. package/ios/ReadThroughServer.swift +283 -0
  163. package/ios/RemoteCommands.swift +44 -4
  164. package/ios/ReplayGainData.swift +22 -3
  165. package/ios/ReplayGainExtractor.swift +70 -27
  166. package/ios/Siri/VoiceDonation.swift +14 -9
  167. package/ios/StreamingBitrateProbe.swift +22 -68
  168. package/ios/SuppliedReplayGain.swift +83 -0
  169. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  170. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  171. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  172. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  173. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  174. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  175. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  176. package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
  177. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  178. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  179. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  180. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  181. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  182. package/ios/Tests/EngineSwapTests.swift +91 -0
  183. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  184. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  185. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  186. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  187. package/ios/Tests/InputGuardsTests.swift +41 -0
  188. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  189. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  190. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  191. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  192. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  193. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  194. package/ios/Tests/MutationDeferralTests.swift +220 -0
  195. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  196. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  197. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  198. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  199. package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
  200. package/ios/Tests/PlayerFixtures.swift +48 -0
  201. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  202. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  203. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  204. package/ios/Tests/QueueStateTests.swift +3 -1
  205. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  206. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  207. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  208. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  209. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  210. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  211. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  212. package/ios/Tests/RetryRecoveryTests.swift +303 -0
  213. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  214. package/ios/Tests/SkipIndexTests.swift +32 -2
  215. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  216. package/ios/Tests/StallRecoveryTests.swift +97 -0
  217. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  218. package/ios/Tests/TopUpWindowGateTests.swift +394 -0
  219. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  220. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  221. package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
  222. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  223. package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
  224. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  225. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  226. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  227. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  228. package/ios/TrackPlayer+Automotive.swift +101 -0
  229. package/ios/TrackPlayer+Cache.swift +232 -0
  230. package/ios/TrackPlayer+Config.swift +291 -0
  231. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  232. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  233. package/ios/TrackPlayer+EventsDispatch.swift +968 -0
  234. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  235. package/ios/TrackPlayer+Lifecycle.swift +930 -0
  236. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  237. package/ios/TrackPlayer+Queue.swift +940 -0
  238. package/ios/TrackPlayer+Recovery.swift +196 -0
  239. package/ios/TrackPlayer+Skip.swift +455 -0
  240. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  241. package/ios/TrackPlayer+State.swift +108 -0
  242. package/ios/TrackPlayer+Threading.swift +135 -0
  243. package/ios/TrackPlayer+Transport.swift +267 -0
  244. package/ios/TrackPlayer+Window.swift +195 -0
  245. package/ios/TrackPlayer.swift +305 -4610
  246. package/ios/Visualizer.swift +6 -5
  247. package/ios/tests-harness/Podfile +1 -1
  248. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  249. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  250. package/lib/module/hooks/useActiveTrack.js +29 -22
  251. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  252. package/lib/module/hooks/useCast.js +8 -26
  253. package/lib/module/hooks/useCast.js.map +1 -1
  254. package/lib/module/hooks/useEqualizer.js +19 -12
  255. package/lib/module/hooks/useEqualizer.js.map +1 -1
  256. package/lib/module/hooks/useLookaheadCache.js +3 -7
  257. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  258. package/lib/module/hooks/useQueue.js +66 -19
  259. package/lib/module/hooks/useQueue.js.map +1 -1
  260. package/lib/module/index.js +6 -3
  261. package/lib/module/index.js.map +1 -1
  262. package/lib/module/queueDelta.js +41 -0
  263. package/lib/module/queueDelta.js.map +1 -0
  264. package/lib/module/types.js +28 -5
  265. package/lib/module/types.js.map +1 -1
  266. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  267. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  269. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useCast.d.ts +6 -1
  271. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  272. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  273. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  274. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  275. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  276. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  277. package/lib/typescript/index.d.ts +2 -1
  278. package/lib/typescript/index.d.ts.map +1 -1
  279. package/lib/typescript/queueDelta.d.ts +10 -0
  280. package/lib/typescript/queueDelta.d.ts.map +1 -0
  281. package/lib/typescript/types.d.ts +168 -12
  282. package/lib/typescript/types.d.ts.map +1 -1
  283. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  284. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  285. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  286. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  287. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  288. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  289. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  320. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  321. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  322. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  323. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  324. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  325. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  326. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  327. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  328. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  329. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  330. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  331. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  332. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  333. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  334. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  335. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  336. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  337. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  338. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  339. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  340. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  341. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  342. package/package.json +8 -8
  343. package/src/TrackPlayer.nitro.ts +53 -27
  344. package/src/hooks/useActiveTrack.ts +29 -22
  345. package/src/hooks/useCast.ts +14 -15
  346. package/src/hooks/useEqualizer.ts +19 -12
  347. package/src/hooks/useLookaheadCache.ts +3 -7
  348. package/src/hooks/useQueue.ts +66 -17
  349. package/src/index.ts +13 -3
  350. package/src/queueDelta.ts +41 -0
  351. package/src/types.ts +169 -12
  352. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  353. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
  354. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  355. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  356. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  357. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  358. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  359. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  360. package/ios/MetadataReader.swift +0 -555
  361. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  362. package/ios/Tests/MetadataReaderTests.swift +0 -603
  363. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  364. package/ios/Tests/VoiceDonationTests.swift +0 -26
  365. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  366. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  367. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -0,0 +1,60 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// A re-issued origin transfer repeats bytes the client already has; the
5
+ /// stitcher drops exactly those and refuses an answer that changed.
6
+ final class OriginRestartStitcherTests: XCTestCase {
7
+
8
+ private func head(
9
+ status: Int = 200, length: String? = "1500", etag: String? = nil
10
+ ) -> HTTPURLResponse {
11
+ var headers: [String: String] = [:]
12
+ if let length { headers["Content-Length"] = length }
13
+ if let etag { headers["ETag"] = etag }
14
+ return HTTPURLResponse(
15
+ url: URL(string: "http://127.0.0.1:1/a.mp3")!, statusCode: status,
16
+ httpVersion: "HTTP/1.1", headerFields: headers)!
17
+ }
18
+
19
+ func testAFirstTransferPassesThrough() {
20
+ var stitcher = OriginRestartStitcher()
21
+ XCTAssertEqual(stitcher.head(head()), .accept)
22
+ XCTAssertEqual(stitcher.body(Data(repeating: 1, count: 1000)).count, 1000)
23
+ XCTAssertEqual(stitcher.deliveredBytes, 1000)
24
+ }
25
+
26
+ func testARestartRepeatsOnlyWhatWasNotDelivered() {
27
+ var stitcher = OriginRestartStitcher()
28
+ _ = stitcher.head(head())
29
+ _ = stitcher.body(Data(repeating: 1, count: 1000))
30
+
31
+ XCTAssertEqual(stitcher.head(head()), .accept, "the same answer is stitched")
32
+ let restarted = Data((0 ..< 1500).map { UInt8($0 % 251) })
33
+ let passed = stitcher.body(restarted)
34
+
35
+ XCTAssertEqual(passed.count, 500)
36
+ XCTAssertTrue(passed.elementsEqual(restarted[1000...]), "the tail past the delivered bytes")
37
+ XCTAssertEqual(stitcher.deliveredBytes, 1500)
38
+ }
39
+
40
+ /// The repeated bytes can arrive across several chunks.
41
+ func testTheSkipSpansChunks() {
42
+ var stitcher = OriginRestartStitcher()
43
+ _ = stitcher.head(head())
44
+ _ = stitcher.body(Data(repeating: 1, count: 1000))
45
+ _ = stitcher.head(head())
46
+
47
+ XCTAssertTrue(stitcher.body(Data(repeating: 2, count: 400)).isEmpty)
48
+ XCTAssertTrue(stitcher.body(Data(repeating: 2, count: 600)).isEmpty)
49
+ XCTAssertEqual(stitcher.body(Data(repeating: 3, count: 100)).count, 100)
50
+ XCTAssertEqual(stitcher.deliveredBytes, 1100)
51
+ }
52
+
53
+ func testADifferentAnswerIsRefused() {
54
+ var stitcher = OriginRestartStitcher()
55
+ _ = stitcher.head(head(etag: "\"v1\""))
56
+ XCTAssertEqual(stitcher.head(head(etag: "\"v2\"")), .reject, "a changed ETag is a different resource")
57
+ XCTAssertEqual(stitcher.head(head(length: "2000")), .reject, "a changed length too")
58
+ XCTAssertEqual(stitcher.head(head(status: 206)), .reject, "and a changed status")
59
+ }
60
+ }
@@ -0,0 +1,95 @@
1
+ import Network
2
+ import XCTest
3
+ @testable import QueuePlayer
4
+ @_spi(QueuePlayerTests) import QueuePlayer
5
+
6
+ /// The monitor reports the network coming back after an outage, and only that.
7
+ /// A handoff between interfaces is not a recovery, and treating it as one would
8
+ /// retry against a connection that never dropped.
9
+ ///
10
+ /// `NWPathMonitor` delivers the current path once on `start`, and the monitor
11
+ /// seeds its last status as unsatisfied, so on a device with a satisfied path
12
+ /// every armed `start()` reports exactly one restore. That first delivery is
13
+ /// what makes the arming observable. A device with no satisfied path fails
14
+ /// these tests at the precondition rather than passing them empty.
15
+ final class PlaybackNetworkMonitorTests: XCTestCase {
16
+
17
+ private let firstDeliveryTimeout: TimeInterval = 5
18
+
19
+ /// Arms `monitor` and waits for the one restore its first path delivery reports.
20
+ private func armAndAwaitRestore(_ monitor: PlaybackNetworkMonitor, restores: Counter) {
21
+ let first = expectation(description: "the current path is delivered on start")
22
+ // The callback stays installed for the monitor's life, so a genuine
23
+ // down → up during a later window reaches the counter assertions rather
24
+ // than tripping this expectation's over-fulfil check.
25
+ first.assertForOverFulfill = false
26
+ monitor.onNetworkRestored = {
27
+ restores.bump()
28
+ first.fulfill()
29
+ }
30
+ monitor.start()
31
+ let outcome = XCTWaiter.wait(for: [first], timeout: firstDeliveryTimeout)
32
+ guard outcome == .completed else {
33
+ XCTFail(
34
+ "no restore within \(Int(firstDeliveryTimeout))s of start — this device has no satisfied "
35
+ + "network path, so the monitor cannot be observed here")
36
+ return
37
+ }
38
+ XCTAssertEqual(restores.value, 1, "exactly one restore per armed start")
39
+ }
40
+
41
+ /// A `stop()` before any `start()` is not terminal: the arming that follows
42
+ /// still delivers the current path.
43
+ func testStopBeforeStartChangesNothing() {
44
+ let monitor = PlaybackNetworkMonitor(queue: .main)
45
+ let restores = Counter()
46
+ monitor.stop()
47
+ armAndAwaitRestore(monitor, restores: restores)
48
+ monitor.stop()
49
+ }
50
+
51
+ }
52
+
53
+ /// Main-queue counter the monitor's `queue: .main` callbacks bump.
54
+ private final class Counter {
55
+ private(set) var value = 0
56
+ func bump() { value += 1 }
57
+ }
58
+
59
+ /// Recovery runs only when all four conditions hold together.
60
+ final class NetworkRecoveryPolicyTests: XCTestCase {
61
+
62
+ func testRetriesWhenPlaybackIsIntendedAndTheItemFailedTransiently() {
63
+ XCTAssertTrue(
64
+ NetworkRecoveryPolicy.shouldRetry(
65
+ wantsToPlay: true, isInterrupted: false, isInError: true, isTransient: true))
66
+ }
67
+
68
+ func testDoesNotRetryWhenPlaybackWasNeverIntended() {
69
+ XCTAssertFalse(
70
+ NetworkRecoveryPolicy.shouldRetry(
71
+ wantsToPlay: false, isInterrupted: false, isInError: true, isTransient: true),
72
+ "a network change must not start audio nobody asked for")
73
+ }
74
+
75
+ func testDoesNotRetryDuringAnInterruption() {
76
+ XCTAssertFalse(
77
+ NetworkRecoveryPolicy.shouldRetry(
78
+ wantsToPlay: true, isInterrupted: true, isInError: true, isTransient: true),
79
+ "the network returning mid-call must not start audio")
80
+ }
81
+
82
+ func testDoesNotRetryWhenNothingHasFailed() {
83
+ XCTAssertFalse(
84
+ NetworkRecoveryPolicy.shouldRetry(
85
+ wantsToPlay: true, isInterrupted: false, isInError: false, isTransient: true),
86
+ "an interface handoff must not rebuild a healthy item")
87
+ }
88
+
89
+ func testDoesNotRetryAFailureANetworkCannotCure() {
90
+ XCTAssertFalse(
91
+ NetworkRecoveryPolicy.shouldRetry(
92
+ wantsToPlay: true, isInterrupted: false, isInError: true, isTransient: false),
93
+ "a codec, format or auth failure re-surfaces on every rebuild")
94
+ }
95
+ }
@@ -124,6 +124,7 @@ final class FakeRemoteSession: CastSessionRemotePlayer {
124
124
  var isPushSink: Bool { false }
125
125
 
126
126
  var lastState: RemotePlaybackStateKind = .idle
127
+ var hasReportedState: Bool = false
127
128
  var lastPositionMs: Int64 = 0
128
129
  var lastDurationMs: Int64 = 0
129
130
  var lastBufferedMs: Int64 = 0
@@ -0,0 +1,48 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// Shared fixtures for tests that need a configured `TrackPlayer` with a real
5
+ /// playable queue.
6
+ enum PlayerFixtures {
7
+
8
+ static func config(autoRetries: Double? = nil) -> PlayerConfig {
9
+ return PlayerConfig(
10
+ httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil,
11
+ autoRetries: autoRetries, retryBackoffMs: nil, networkTimeoutMs: nil,
12
+ audioContentType: nil, audioCategoryOptions: nil,
13
+ voiceVocabularyDonationEnabled: nil,
14
+ visualizationEnabled: nil, clampSeekToBuffered: nil,
15
+ skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil,
16
+ lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil,
17
+ backgroundProgressUpdateIntervalMs: nil
18
+ )
19
+ }
20
+
21
+ static func track(id: String, url: String) -> TrackItem {
22
+ return TrackItem(
23
+ id: id, url: url, title: nil, artist: nil, album: nil,
24
+ duration: nil, artworkUrl: nil, extras: nil,
25
+ replayGainTrackGain: nil, replayGainTrackPeak: nil,
26
+ replayGainAlbumGain: nil, replayGainAlbumPeak: nil
27
+ )
28
+ }
29
+
30
+ /// Queue entries backed by a bundled local asset, so the queue seats real
31
+ /// playable items without any network fetch or prefetch scheduling.
32
+ static func localTracks(count: Int, for testCase: XCTestCase) throws -> [TrackItem] {
33
+ let url = try fixtureURL(name: "silence-aac-128", ext: "m4a", for: testCase)
34
+ return (0 ..< count).map { track(id: "t\($0)", url: url.absoluteString) }
35
+ }
36
+
37
+ /// A file under `Tests/Fixtures`, bundled through `test_spec.resources`.
38
+ /// A miss is a tests-harness `pod install` regression.
39
+ static func fixtureURL(name: String, ext: String, for testCase: XCTestCase) throws -> URL {
40
+ let bundle = Bundle(for: type(of: testCase))
41
+ return try XCTUnwrap(
42
+ bundle.url(forResource: name, withExtension: ext)
43
+ ?? bundle.urls(forResourcesWithExtension: ext, subdirectory: nil)?
44
+ .first(where: { $0.deletingPathExtension().lastPathComponent == name }),
45
+ "fixture \(name).\(ext) not bundled — pod install regression in tests-harness"
46
+ )
47
+ }
48
+ }
@@ -16,7 +16,7 @@ final class PlayerStateDerivationTests: XCTestCase {
16
16
  status: status, currentIndex: index, hasCurrentItem: hasItem, reachedQueueEnd: ended)
17
17
  }
18
18
 
19
- // MARK: baseline (no end-of-queue latch)
19
+ // MARK: baseline (queue has not drained)
20
20
 
21
21
  func testPlayingIsPlaying() {
22
22
  XCTAssertEqual(derive(.playing), .playing)
@@ -40,14 +40,14 @@ final class PlayerStateDerivationTests: XCTestCase {
40
40
  }
41
41
 
42
42
  func testWaitingWithoutItemNonEmptyQueueIsEndedFallback() {
43
- // A drained AVQueuePlayer parks at .waitingToPlay with no item before the
44
- // latch flips — the end-of-queue fallback covers the timing race.
43
+ // A drained AVQueuePlayer parks at .waitingToPlay with no item before
44
+ // `reachedQueueEnd` is set — the end-of-queue fallback covers the timing race.
45
45
  XCTAssertEqual(derive(.waitingToPlayAtSpecifiedRate, index: 5, hasItem: false), .ended)
46
46
  }
47
47
 
48
- // MARK: reachedQueueEnd latch
48
+ // MARK: reachedQueueEnd
49
49
 
50
- func testLatchWithTrackNotPlayingIsEnded() {
50
+ func testReachedQueueEndWithTrackNotPlayingIsEnded() {
51
51
  // Any non-playing status at end-of-queue is terminal — the same result for
52
52
  // both engines (crossfade keeps its finished item, so hasItem is true).
53
53
  XCTAssertEqual(derive(.paused, index: 5, ended: true), .ended)
@@ -55,14 +55,14 @@ final class PlayerStateDerivationTests: XCTestCase {
55
55
  derive(.waitingToPlayAtSpecifiedRate, index: 5, hasItem: true, ended: true), .ended)
56
56
  }
57
57
 
58
- func testLatchYieldsToPlaying() {
59
- // Playing takes precedence over the latch, so a resume reads correctly even
60
- // before the latch clears.
58
+ func testReachedQueueEndYieldsToPlaying() {
59
+ // Playing takes precedence over `reachedQueueEnd`, so a resume reads
60
+ // correctly even before it is cleared.
61
61
  XCTAssertEqual(derive(.playing, index: 5, ended: true), .playing)
62
62
  }
63
63
 
64
- func testLatchIgnoredWhenQueueEmpty() {
65
- // A cleared queue (index < 0) skips the latch and derives normally.
64
+ func testReachedQueueEndIgnoredWhenQueueEmpty() {
65
+ // A cleared queue (index < 0) ignores `reachedQueueEnd` and derives normally.
66
66
  XCTAssertEqual(derive(.paused, index: -1, ended: true), .none)
67
67
  }
68
68
  }
@@ -0,0 +1,141 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// Receiver work runs in the order the mutations were made, and work that
5
+ /// outlives its mutation does not act on a queue that has since been replaced.
6
+ final class QueueMutationGenerationTests: XCTestCase {
7
+
8
+ private var player: TrackPlayer!
9
+
10
+ override func setUp() async throws {
11
+ try await super.setUp()
12
+ player = TrackPlayer()
13
+ _ = try await player.configure(config: PlayerFixtures.config()).await()
14
+ }
15
+
16
+ override func tearDown() async throws {
17
+ if let player {
18
+ _ = try await player.destroy().await()
19
+ }
20
+ player = nil
21
+ try await super.tearDown()
22
+ }
23
+
24
+ // MARK: - Mirror ordering
25
+
26
+ /// `addToQueue`, `removeFromQueue` and `moveInQueue` send index deltas
27
+ /// resolved against the queue as it stood before their own mutation, so the
28
+ /// receiver has to see them in that order or it diverges.
29
+ func testMirrorsRunInTheOrderTheMutationsWereMade() async throws {
30
+ let recorded = Recorder()
31
+
32
+ for step in 0 ..< 5 {
33
+ _ = player.enqueueThenMirror({ step }, mirror: { value, _ in
34
+ await recorded.append(value)
35
+ })
36
+ }
37
+ let last = player.enqueueThenMirror({ 5 }, mirror: { value, _ in
38
+ await recorded.append(value)
39
+ })
40
+ _ = try await last.await()
41
+
42
+ let order = await recorded.values
43
+ XCTAssertEqual(order, [0, 1, 2, 3, 4, 5])
44
+ }
45
+
46
+ /// A slow round-trip holds the ones behind it rather than letting them
47
+ /// overtake — the whole reason the chain exists.
48
+ func testASlowMirrorDoesNotLetTheNextOneOvertakeIt() async throws {
49
+ let recorded = Recorder()
50
+
51
+ _ = player.enqueueThenMirror({ 0 }, mirror: { value, _ in
52
+ try? await Task.sleep(nanoseconds: 120_000_000)
53
+ await recorded.append(value)
54
+ })
55
+ let second = player.enqueueThenMirror({ 1 }, mirror: { value, _ in
56
+ await recorded.append(value)
57
+ })
58
+ _ = try await second.await()
59
+
60
+ let order = await recorded.values
61
+ XCTAssertEqual(order, [0, 1], "the slow mirror must still land first")
62
+ }
63
+
64
+ /// The promise resolves only once that call's own receiver work has run, so
65
+ /// awaiting a mutation means local and remote are both settled.
66
+ func testTheCallResolvesOnlyAfterItsOwnMirrorHasRun() async throws {
67
+ let recorded = Recorder()
68
+
69
+ _ = try await player.enqueueThenMirror({ 0 }, mirror: { value, _ in
70
+ await recorded.append(value)
71
+ }).await()
72
+
73
+ let order = await recorded.values
74
+ XCTAssertEqual(order, [0])
75
+ }
76
+
77
+ // MARK: - Generation
78
+
79
+ func testEachQueueMutationTakesTheNextGeneration() async throws {
80
+ let start = try await read { $0.queueMutationGeneration }
81
+
82
+ _ = try await player.setQueue(tracks: try tracks(3), startAtIndex: 0).await()
83
+ let afterSet = try await read { $0.queueMutationGeneration }
84
+ _ = try await player.addToQueue(tracks: try tracks(1), insertBefore: nil).await()
85
+ let afterAdd = try await read { $0.queueMutationGeneration }
86
+
87
+ XCTAssertEqual(afterSet, start &+ 1)
88
+ XCTAssertEqual(afterAdd, start &+ 2)
89
+ }
90
+
91
+ /// The generation a mirror captured is what tells it whether the queue it
92
+ /// was computed against is still the live one.
93
+ func testAMirrorSeesItsOwnGenerationSupersededByALaterMutation() async throws {
94
+ let captured = Recorder()
95
+ _ = player.enqueueThenMirror({ 0 }, mirror: { _, generation in
96
+ await captured.append(Int(generation))
97
+ })
98
+ _ = try await player.setQueue(tracks: try tracks(2), startAtIndex: 0).await()
99
+
100
+ let capturedValues = await captured.values
101
+ let mine = try XCTUnwrap(capturedValues.first)
102
+ let live = try await read { Int($0.queueMutationGeneration) }
103
+ XCTAssertGreaterThan(
104
+ live, mine, "a later mutation must leave the earlier generation behind")
105
+ }
106
+
107
+ /// A burst leaves the queue the last call asked for, and the ids stay
108
+ /// parallel to it.
109
+ func testABurstLeavesTheQueueTheLastCallAskedFor() async throws {
110
+ let start = try await read { $0.queueMutationGeneration }
111
+
112
+ _ = try player.setQueue(tracks: try tracks(3), startAtIndex: 0)
113
+ _ = try player.setQueue(tracks: try tracks(5), startAtIndex: 0)
114
+ let last = try player.setQueue(tracks: try tracks(2), startAtIndex: 0)
115
+ _ = try await last.await()
116
+
117
+ let state = try await read {
118
+ (count: $0.tracks.count, ids: $0.queueItemIds.count,
119
+ generation: $0.queueMutationGeneration)
120
+ }
121
+ XCTAssertEqual(state.count, 2, "the last call in the burst is the one that stands")
122
+ XCTAssertEqual(state.ids, state.count, "ids stay parallel to tracks through a burst")
123
+ XCTAssertEqual(state.generation, start &+ 3)
124
+ }
125
+
126
+ // MARK: - Helpers
127
+
128
+ private actor Recorder {
129
+ private(set) var values: [Int] = []
130
+ func append(_ value: Int) { values.append(value) }
131
+ }
132
+
133
+ private func tracks(_ count: Int) throws -> [TrackItem] {
134
+ try PlayerFixtures.localTracks(count: count, for: self)
135
+ }
136
+
137
+ private func read<T>(_ body: @escaping (TrackPlayer) -> T) async throws -> T {
138
+ let player = try XCTUnwrap(self.player)
139
+ return await player.onPlayerQueueValue { body(player) }
140
+ }
141
+ }