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,97 @@
1
+ import AVFoundation
2
+ import XCTest
3
+ @testable import QueuePlayer
4
+
5
+ /// Playback runs with `automaticallyWaitsToMinimizeStalling = false`, so
6
+ /// AVPlayer does not resume itself after a buffer underrun. A stall arms a
7
+ /// recovery that re-issues play when the buffer refills, and transport intent
8
+ /// decides whether it arms at all.
9
+ ///
10
+ /// The resume itself needs a real item whose buffer actually refills, so that
11
+ /// half is covered by the streaming demo suites; what is covered here is the
12
+ /// arming and clearing, which is where the intent bugs would be.
13
+ final class StallRecoveryTests: XCTestCase {
14
+
15
+ func testAStallArmsRecoveryWhilePlaybackIsIntended() {
16
+ let player = TrackPlayer()
17
+ let active = seedActiveItem(player)
18
+ player.wantsToPlay = true
19
+
20
+ player.handlePlaybackStalled(
21
+ Notification(name: AVPlayerItem.playbackStalledNotification, object: active))
22
+ drainPlayerQueue(player)
23
+
24
+ XCTAssertTrue(player.isRecoveringFromStall)
25
+ }
26
+
27
+ /// The observer sees every item in the process; a stall on an item that
28
+ /// is not the one being heard is not this player's stall.
29
+ func testAStallOnAnotherItemDoesNotArmRecovery() {
30
+ let player = TrackPlayer()
31
+ _ = seedActiveItem(player)
32
+ player.wantsToPlay = true
33
+ let other = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/other.mp3"))
34
+
35
+ player.handlePlaybackStalled(
36
+ Notification(name: AVPlayerItem.playbackStalledNotification, object: other))
37
+ drainPlayerQueue(player)
38
+
39
+ XCTAssertFalse(player.isRecoveringFromStall)
40
+ }
41
+
42
+ /// A notification with no item names nothing this player is playing — also
43
+ /// while the player itself has no active item, where "no item" and "no
44
+ /// active item" must not be read as a match.
45
+ func testAStallWithNoItemDoesNotArmRecovery() {
46
+ let player = TrackPlayer()
47
+ player.wantsToPlay = true
48
+
49
+ player.handlePlaybackStalled(Notification(name: AVPlayerItem.playbackStalledNotification))
50
+ drainPlayerQueue(player)
51
+
52
+ XCTAssertFalse(player.isRecoveringFromStall)
53
+ }
54
+
55
+ func testAStallWhilePausedDoesNotArmRecovery() {
56
+ let player = TrackPlayer()
57
+ let active = seedActiveItem(player)
58
+ player.wantsToPlay = false
59
+
60
+ player.handlePlaybackStalled(
61
+ Notification(name: AVPlayerItem.playbackStalledNotification, object: active))
62
+ drainPlayerQueue(player)
63
+
64
+ XCTAssertFalse(
65
+ player.isRecoveringFromStall,
66
+ "a stall arriving while paused must not arm a resume that fires later")
67
+ }
68
+
69
+ func testAnExplicitPauseDisarmsAPendingRecovery() async throws {
70
+ let player = TrackPlayer()
71
+ player.wantsToPlay = true
72
+ player.isRecoveringFromStall = true
73
+
74
+ _ = try await player.pause().await()
75
+
76
+ XCTAssertFalse(
77
+ player.isRecoveringFromStall,
78
+ "an explicit pause outranks a pending recovery, or the next refill "
79
+ + "restarts audio the user just stopped")
80
+ }
81
+
82
+ /// Installs an engine holding one item and returns that item.
83
+ private func seedActiveItem(_ player: TrackPlayer) -> AVPlayerItem {
84
+ let engine = ActiveItemEngineStub()
85
+ let item = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/active.mp3"))
86
+ engine.item = item
87
+ player.engine = engine
88
+ return item
89
+ }
90
+
91
+ /// The handler hops to the player queue, so the assertion has to follow it.
92
+ private func drainPlayerQueue(_ player: TrackPlayer) {
93
+ let drained = expectation(description: "player queue drained")
94
+ player.playerQueue.async { drained.fulfill() }
95
+ wait(for: [drained], timeout: 2.0)
96
+ }
97
+ }
@@ -1,29 +1,26 @@
1
1
  import XCTest
2
2
  @testable import QueuePlayer
3
3
 
4
- /// Pins `StreamingBitrateProbe`'s network-isolation seam: a remote probe
5
- /// must fetch via the injected `URLSession`, never the global
6
- /// `URLSession.shared`. The bitrate-parsing math is exercised end-to-end
7
- /// through the demo format/replaygain suites.
4
+ /// The probe derives a bitrate from a local file's first 256 KB and reads
5
+ /// nothing that is not a `file://` URL.
8
6
  final class StreamingBitrateProbeTests: XCTestCase {
9
7
 
10
- func testRemoteProbeUsesInjectedSession() async {
11
- let config = URLSessionConfiguration.ephemeral
12
- config.protocolClasses = [StubURLProtocol.self]
13
- let session = URLSession(configuration: config)
14
- StubURLProtocol.reset()
15
- defer { StubURLProtocol.reset() }
16
- // Unique URL per run so the probe's process-wide bitrate cache can't
17
- // satisfy the call without a fetch. `.test` is a reserved
18
- // non-resolvable TLD, so a real-network leak would fail with -1003
19
- // instead of reaching the stub.
20
- let url = URL(string: "https://example.test/probe-\(UUID().uuidString).m4a")!
21
- StubURLProtocol.stub(url: url, data: Data(count: 4096), statusCode: 200)
8
+ func testALocalCbrMp3ReportsItsEncodedBitrate() async throws {
9
+ let url = try PlayerFixtures.fixtureURL(name: "silence-cbr-128", ext: "mp3", for: self)
10
+ let bitrate = await StreamingBitrateProbe.bitrate(forUrl: url)
11
+ XCTAssertEqual(try XCTUnwrap(bitrate), 128_000, accuracy: 128_000 * 0.05)
12
+ }
22
13
 
23
- _ = await StreamingBitrateProbe.bitrate(forUrl: url, urlSession: session)
14
+ func testARemoteUrlIsNotProbed() async throws {
15
+ let url = try XCTUnwrap(URL(string: "http://127.0.0.1:1/\(UUID().uuidString).mp3"))
16
+ let bitrate = await StreamingBitrateProbe.bitrate(forUrl: url)
17
+ XCTAssertNil(bitrate)
18
+ }
24
19
 
25
- XCTAssertEqual(
26
- StubURLProtocol.requestCount(for: url), 1,
27
- "remote probe must fetch through the injected session, never URLSession.shared")
20
+ func testAMissingFileReportsNoBitrate() async {
21
+ let url = FileManager.default.temporaryDirectory
22
+ .appendingPathComponent("absent-\(UUID().uuidString).mp3")
23
+ let bitrate = await StreamingBitrateProbe.bitrate(forUrl: url)
24
+ XCTAssertNil(bitrate)
28
25
  }
29
26
  }
@@ -0,0 +1,394 @@
1
+ import AVFoundation
2
+ import XCTest
3
+ @testable import QueuePlayer
4
+ @_spi(QueuePlayerTests) import QueuePlayer
5
+
6
+ /// Extending the installed run waits for a crossfade rather than cutting it
7
+ /// short, and reads the run's start from what is actually being heard.
8
+ final class TopUpWindowGateTests: XCTestCase {
9
+
10
+ private var player: TrackPlayer!
11
+
12
+ override func setUp() async throws {
13
+ try await super.setUp()
14
+ player = TrackPlayer()
15
+ }
16
+
17
+ override func tearDown() async throws {
18
+ player = nil
19
+ try await super.tearDown()
20
+ }
21
+
22
+ /// The same run, the same owed top-up: only the pending fade differs.
23
+ func testAPendingFadeIsWhatHoldsTheTopUpBack() async throws {
24
+ let engine = WindowEngineStub(count: 3)
25
+ engine.fadePending = true
26
+ try await seed(engine: engine, trackCount: 20)
27
+
28
+ await onQueue { $0.topUpWindow() }
29
+ XCTAssertTrue(
30
+ engine.inserted.isEmpty,
31
+ "a pending fade must not be cut short to extend the run")
32
+
33
+ engine.fadePending = false
34
+ await onQueue { $0.topUpWindow() }
35
+ XCTAssertFalse(
36
+ engine.inserted.isEmpty,
37
+ "the same run extends once there is no fade to protect")
38
+ }
39
+
40
+ /// The run starts at the item being heard. An engine that retains played
41
+ /// items reports those first, and measuring from them would call the run
42
+ /// satisfied while the playhead sat near its end.
43
+ func testTheRunIsMeasuredFromTheItemBeingHeardNotTheFirstInstalled() async throws {
44
+ // Ten installed positions, but the playhead is at the last of them — so
45
+ // the run ahead is exhausted and a top-up is owed.
46
+ let engine = WindowEngineStub(count: 10)
47
+ engine.leadingIndex = 9
48
+ try await seed(engine: engine, trackCount: 30)
49
+
50
+ await onQueue { $0.topUpWindow() }
51
+
52
+ XCTAssertFalse(
53
+ engine.inserted.isEmpty,
54
+ "measured from the playhead the run is exhausted, so it must extend")
55
+ }
56
+
57
+ /// A skip is applied inside a mutation, and the transition it raises is not
58
+ /// allowed to top up while one is in flight — so the skip extends the run
59
+ /// itself when it lands on the last installed position.
60
+ func testASkipOntoTheLastInstalledPositionExtendsTheRun() async throws {
61
+ let engine = WindowEngineStub(count: 4)
62
+ try await seed(engine: engine, trackCount: 20)
63
+
64
+ await onQueue { $0.applyNewCurrentIndex(newIndex: 3, reason: .userSkipToIndex) }
65
+
66
+ XCTAssertFalse(
67
+ engine.inserted.isEmpty,
68
+ "the run ahead of the skip target is empty and must be extended")
69
+ }
70
+
71
+ /// The crossfade steady state: the whole queue installed, the playhead part
72
+ /// way into it. This is where the claim that the machinery is inert has to
73
+ /// hold, and a shorter-than-floor queue does not test it.
74
+ func testAWhollyInstalledQueueNeedsNoTopUpFromMidRun() async throws {
75
+ let engine = WindowEngineStub(count: 30)
76
+ engine.leadingIndex = 5
77
+ try await seed(engine: engine, trackCount: 30)
78
+
79
+ await onQueue { $0.topUpWindow() }
80
+
81
+ XCTAssertTrue(
82
+ engine.inserted.isEmpty,
83
+ "nothing is owed when the queue is already installed to its end")
84
+ }
85
+
86
+ /// The engine refusing the append falls back to installing the slice.
87
+ func testAnEngineThatRefusesTheAppendFallsBackToAFullInstall() async throws {
88
+ let engine = WindowEngineStub(count: 3)
89
+ engine.refuseInserts = true
90
+ try await seed(engine: engine, trackCount: 20)
91
+
92
+ await onQueue { $0.topUpWindow() }
93
+
94
+ XCTAssertTrue(engine.inserted.isEmpty)
95
+ XCTAssertTrue(
96
+ engine.didSetItems, "a refused append must be recovered by a full install")
97
+ }
98
+
99
+ func testAFullTailNeedsNoTopUp() async throws {
100
+ let engine = WindowEngineStub(count: 4)
101
+ try await seed(engine: engine, trackCount: 4)
102
+
103
+ await onQueue { $0.topUpWindow() }
104
+
105
+ XCTAssertTrue(
106
+ engine.inserted.isEmpty,
107
+ "the queue is wholly installed, so there is nothing to append")
108
+ }
109
+
110
+ /// A queue that has ended is not extended by a top-up, even while the
111
+ /// engine still holds a short run and the model has tracks past it: the
112
+ /// finished track would otherwise get a run appended behind it, and the
113
+ /// next play() would fade into it.
114
+ func testTopUpDoesNotExtendAnEndedQueue() async throws {
115
+ let engine = WindowEngineStub(count: 3)
116
+ engine.leadingIndex = 2
117
+ try await seed(engine: engine, trackCount: 20)
118
+ await onQueue { $0.reachedQueueEnd = true }
119
+
120
+ await onQueue { $0.topUpWindow() }
121
+
122
+ XCTAssertTrue(engine.inserted.isEmpty, "nothing is appended behind the finished track")
123
+ XCTAssertFalse(engine.didSetItems, "an ended queue must not be seated again by a top-up")
124
+ }
125
+
126
+ /// The same holds for a drained engine that has not ended: seating it is the
127
+ /// caller's job, not the top-up's.
128
+ func testTopUpDoesNotSeatADrainedEngine() async throws {
129
+ let engine = WindowEngineStub(count: 0)
130
+ try await seed(engine: engine, trackCount: 20)
131
+
132
+ await onQueue { $0.topUpWindow() }
133
+
134
+ XCTAssertFalse(engine.didSetItems)
135
+ }
136
+
137
+ /// An add that fits inside the window is inserted surgically, so the items
138
+ /// already buffering ahead of it are kept.
139
+ func testAnAddInsideTheWindowIsInsertedWithoutReinstalling() async throws {
140
+ let engine = WindowEngineStub(count: QueueWindowArithmetic.floor)
141
+ try await seed(engine: engine, trackCount: QueueWindowArithmetic.floor)
142
+
143
+ _ = try await player.addToQueue(
144
+ tracks: [
145
+ PlayerFixtures.track(id: "n0", url: "https://example.com/n0.m4a"),
146
+ PlayerFixtures.track(id: "n1", url: "https://example.com/n1.m4a"),
147
+ ],
148
+ insertBefore: 3
149
+ ).await()
150
+
151
+ XCTAssertEqual(engine.inserted.count, 2, "both added items are inserted")
152
+ XCTAssertFalse(engine.didSetItems, "nothing is reinstalled")
153
+ let ids = try await onQueueValue { $0.queueItemIds }
154
+ XCTAssertEqual(
155
+ engine.allMediaItems.compactMap(\.queueItemId), Array(ids.prefix(QueueWindowArithmetic.floor + 2)),
156
+ "the run is the queue's positions in order with the two added items in place")
157
+ }
158
+
159
+ /// An add that runs past the window is not built item by item: the engine
160
+ /// is reconciled to the slice, and `topUpWindow` materialises the rest as
161
+ /// the playhead advances.
162
+ func testAnAddPastTheWindowBuildsOnlyTheSlice() async throws {
163
+ let engine = WindowEngineStub(count: QueueWindowArithmetic.floor)
164
+ try await seed(engine: engine, trackCount: QueueWindowArithmetic.floor)
165
+ let album = (0 ..< 30).map {
166
+ PlayerFixtures.track(id: "n\($0)", url: "https://example.com/n\($0).m4a")
167
+ }
168
+
169
+ _ = try await player.addToQueue(tracks: album, insertBefore: 5).await()
170
+
171
+ XCTAssertEqual(
172
+ engine.inserted.count, QueueWindowArithmetic.floor - 5,
173
+ "only the positions inside the window are built")
174
+ XCTAssertFalse(engine.didSetItems, "the run is trimmed and extended, not reinstalled")
175
+ let ids = try await onQueueValue { $0.queueItemIds }
176
+ XCTAssertEqual(
177
+ engine.allMediaItems.compactMap(\.queueItemId), Array(ids.prefix(QueueWindowArithmetic.floor)),
178
+ "the engine holds exactly the window: the five kept positions and the first five added")
179
+ }
180
+
181
+ /// The same after the end of the queue on an engine that keeps its items:
182
+ /// the finished item is a materialised anchor, and an item inserted behind
183
+ /// it would be what the next play() fades into.
184
+ func testAnAppendAfterTheEndOfTheQueueLeavesARetainingEngineAlone() async throws {
185
+ let engine = WindowEngineStub(count: 3)
186
+ engine.leadingIndex = 2
187
+ try await seed(engine: engine, trackCount: 3)
188
+ await onQueue { $0.reachedQueueEnd = true }
189
+
190
+ _ = try await player.addToQueue(
191
+ tracks: [PlayerFixtures.track(id: "t3", url: "https://example.com/t3.m4a")],
192
+ insertBefore: nil
193
+ ).await()
194
+
195
+ XCTAssertTrue(engine.inserted.isEmpty, "nothing is inserted behind the finished item")
196
+ XCTAssertFalse(engine.didSetItems)
197
+ let state = try await onQueueValue { ($0.currentTrackIndex, $0.reachedQueueEnd) }
198
+ XCTAssertEqual(state.0, 2)
199
+ XCTAssertTrue(state.1)
200
+ }
201
+
202
+ /// A drain reported while the queue is already over does not re-decide it:
203
+ /// with tracks added behind the finished one, the verdict would otherwise be
204
+ /// `.windowDrained` and seat the added track.
205
+ func testARepeatedEndReportWhileEndedSeatsNothing() async throws {
206
+ let engine = WindowEngineStub(count: 0)
207
+ engine.leadingIndex = 2
208
+ try await seed(engine: engine, trackCount: 3)
209
+ await onQueue {
210
+ $0.reachedQueueEnd = true
211
+ _ = $0.queueState.insert(
212
+ [PlayerFixtures.track(id: "t3", url: "https://example.com/t3.m4a")], at: 3)
213
+ }
214
+
215
+ await onQueue { $0.applyEndVerdict() }
216
+
217
+ XCTAssertFalse(engine.didSetItems, "the ended queue is not re-seated")
218
+ let state = try await onQueueValue { ($0.currentTrackIndex, $0.reachedQueueEnd) }
219
+ XCTAssertEqual(state.0, 2, "the index stays on the track that finished")
220
+ XCTAssertTrue(state.1)
221
+ }
222
+
223
+ /// An append after the end of the queue extends the model, not the engine:
224
+ /// inserting into a drained engine would seat playback on the new track
225
+ /// while `currentTrackIndex` still names the one that finished, so the
226
+ /// next `play()` would start a track the consumer was never told about.
227
+ func testAnAppendAfterTheEndOfTheQueueLeavesTheDrainedEngineAlone() async throws {
228
+ let engine = WindowEngineStub(count: 0)
229
+ engine.leadingIndex = 2
230
+ try await seed(engine: engine, trackCount: 3)
231
+ await onQueue { $0.reachedQueueEnd = true }
232
+
233
+ _ = try await player.addToQueue(
234
+ tracks: [PlayerFixtures.track(id: "t3", url: "https://example.com/t3.m4a")],
235
+ insertBefore: nil
236
+ ).await()
237
+
238
+ XCTAssertTrue(engine.inserted.isEmpty, "nothing is inserted into a drained engine")
239
+ XCTAssertFalse(engine.didSetItems)
240
+ let state = try await onQueueValue {
241
+ ($0.tracks.count, $0.currentTrackIndex, $0.reachedQueueEnd)
242
+ }
243
+ XCTAssertEqual(state.0, 4, "the queue model carries the append")
244
+ XCTAssertEqual(state.1, 2, "the current index stays on the track that finished")
245
+ XCTAssertTrue(state.2, "the queue is still over until a skip or seek says otherwise")
246
+ }
247
+
248
+ /// Reinstalling the run the engine is already rendering from restarts the
249
+ /// track being heard unless its playhead is carried across.
250
+ func testAForcedReinstallKeepsThePlayheadOfTheTrackBeingHeard() async throws {
251
+ let engine = WindowEngineStub(count: 12)
252
+ engine.leadingIndex = 4
253
+ engine.position = 37.5
254
+ try await seed(engine: engine, trackCount: 20)
255
+
256
+ await onQueue { p in
257
+ p.performingMutation { p.fullRebuildPlayerQueue(forcingReinstall: true) }
258
+ }
259
+
260
+ XCTAssertTrue(engine.didSetItems, "the run is installed whole")
261
+ XCTAssertEqual(
262
+ engine.seatedAtPosition, 37.5,
263
+ "the track being heard resumes where it was, not from its start")
264
+ }
265
+
266
+ /// A reinstall that seats on a different track is a track change, and a
267
+ /// track change starts at the beginning.
268
+ func testAReinstallOntoADifferentTrackStartsItFromZero() async throws {
269
+ let engine = WindowEngineStub(count: 12)
270
+ engine.leadingIndex = 4
271
+ engine.position = 37.5
272
+ try await seed(engine: engine, trackCount: 20)
273
+ await onQueue { $0.currentTrackIndex = 9 }
274
+
275
+ await onQueue { p in
276
+ p.performingMutation { p.fullRebuildPlayerQueue(forcingReinstall: true) }
277
+ }
278
+
279
+ XCTAssertEqual(engine.seatedAtPosition, 0)
280
+ }
281
+
282
+ // MARK: - Helpers
283
+
284
+ private func seed(engine: WindowEngineStub, trackCount: Int) async throws {
285
+ let player = try XCTUnwrap(self.player)
286
+ await player.onPlayerQueue {
287
+ let ids = player.queueState.replaceAll(
288
+ (0 ..< trackCount).map {
289
+ PlayerFixtures.track(id: "t\($0)", url: "https://example.com/\($0).m4a")
290
+ })
291
+ engine.stampIds(Array(ids.prefix(engine.installedCount)))
292
+ player.currentTrackIndex = engine.leadingIndex
293
+ player.engine = engine
294
+ }
295
+ }
296
+
297
+ private func onQueue(_ body: @escaping (TrackPlayer) -> Void) async {
298
+ guard let player else { return }
299
+ await player.onPlayerQueue { body(player) }
300
+ }
301
+
302
+ private func onQueueValue<T>(_ body: @escaping (TrackPlayer) -> T) async throws -> T {
303
+ let player = try XCTUnwrap(self.player)
304
+ return await player.onPlayerQueueValue { body(player) }
305
+ }
306
+ }
307
+
308
+ /// Reports an installed run of `count` items and records what is appended to
309
+ /// it. Every other member is inert — the window logic reads the run, the
310
+ /// leading identity and the fade flag, nothing else.
311
+ private final class WindowEngineStub: PlaybackEngine {
312
+ let installedCount: Int
313
+ var leadingIndex: Int = 0
314
+ var fadePending = false
315
+ /// `CrossfadeEngine.insertItems` refuses an anchor it does not hold; the
316
+ /// caller's recovery from that is a contract worth exercising.
317
+ var refuseInserts = false
318
+ /// The engine's own playhead, which a reinstall of the run it is already
319
+ /// rendering has to carry across.
320
+ var position: TimeInterval = 0
321
+ private(set) var inserted: [AVPlayerItem] = []
322
+ private(set) var didSetItems = false
323
+ private(set) var seatedAtPosition: TimeInterval?
324
+ private var items: [AVPlayerItem] = []
325
+
326
+ init(count: Int) {
327
+ self.installedCount = count
328
+ }
329
+
330
+ func stampIds(_ ids: [String]) {
331
+ items = ids.map { id in
332
+ let item = AVPlayerItem(url: URL(string: "https://example.com/\(id).m4a")!)
333
+ item.queueItemId = id
334
+ return item
335
+ }
336
+ }
337
+
338
+ var allMediaItems: [AVPlayerItem] { items }
339
+ var leadingQueueItemId: String? { items[safe: leadingIndex]?.queueItemId }
340
+ var isFadePendingOrActive: Bool { fadePending }
341
+ /// The whole installed run stands in for what is attached; this stub's
342
+ /// contract is the window, not the legs.
343
+ var attachedQueueItemIds: [String] { items.compactMap { $0.queueItemId } }
344
+
345
+ /// Inserts after the anchor, as the engines do, so the run's shape is
346
+ /// observable and not only the count of what was appended.
347
+ @discardableResult
348
+ func insertItems(_ newItems: [AVPlayerItem], after: AVPlayerItem?) -> Bool {
349
+ guard !refuseInserts else { return false }
350
+ let position: Int
351
+ if let after {
352
+ guard let index = items.firstIndex(where: { $0 === after }) else { return false }
353
+ position = index + 1
354
+ } else {
355
+ position = items.count
356
+ }
357
+ inserted.append(contentsOf: newItems)
358
+ items.insert(contentsOf: newItems, at: position)
359
+ return true
360
+ }
361
+
362
+ func setAudioMixProvider(_ provider: AudioMixProvider?) {}
363
+ func refreshAllItemMixes() {}
364
+ func release() {}
365
+ func setItems(_ items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) {
366
+ didSetItems = true
367
+ seatedAtPosition = startPositionSeconds
368
+ self.items = items
369
+ }
370
+ func remove(_ item: AVPlayerItem) { items.removeAll { $0 === item } }
371
+ func removeAllItems() { items.removeAll() }
372
+ func canInsert(_ item: AVPlayerItem, after: AVPlayerItem?) -> Bool { true }
373
+ func play() {}
374
+ func pause() {}
375
+ func stop() {}
376
+ /// A seek moves the playhead: the run is measured from the new leading item.
377
+ func seek(toIndex index: Int, position: TimeInterval) { leadingIndex = index }
378
+ func skipToNext() {}
379
+ func skipToPrevious() {}
380
+ func setVolume(_ volume: Float) {}
381
+ func setPlaybackSpeed(_ speed: Float) {}
382
+ func seedPlaybackSpeed(_ speed: Float) {}
383
+ func setRepeatMode(_ mode: RepeatMode) {}
384
+ func setPitchCorrectionMode(_ mode: PitchCorrectionMode) {}
385
+
386
+ var currentMediaItemIndex: Int { leadingIndex }
387
+ var currentPositionSeconds: TimeInterval { position }
388
+ var currentDurationSeconds: TimeInterval { 0 }
389
+ var bufferedPositionSeconds: TimeInterval { 0 }
390
+ var isPlaying: Bool { false }
391
+ var timeControlStatus: AVPlayer.TimeControlStatus { .paused }
392
+ var currentMediaItem: AVPlayerItem? { items[safe: leadingIndex] }
393
+ var delegate: PlaybackEngineDelegate?
394
+ }
@@ -0,0 +1,14 @@
1
+ @testable import QueuePlayer
2
+
3
+ extension TrackPlayer {
4
+ /// Runs `body` on `playerQueue` and hands its value back — the read side of
5
+ /// the tests' player-state assertions. The library has no async getter that
6
+ /// needs this; its own hops are `onPlayerQueue` and `onPlayerQueueSync`.
7
+ func onPlayerQueueValue<T>(_ body: @escaping () -> T) async -> T {
8
+ await withCheckedContinuation { (cont: CheckedContinuation<T, Never>) in
9
+ playerQueue.async {
10
+ cont.resume(returning: body())
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,93 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// Queue mutations reach the player in the order JS called them.
5
+ ///
6
+ /// Each method is issued and a probe is enqueued immediately afterwards, the
7
+ /// way a transport call issued on the next line would be. The probe runs on
8
+ /// `playerQueue` behind whatever the method enqueued, so what it observes is
9
+ /// what that next call would act on.
10
+ final class TrackPlayerCallOrderTests: XCTestCase {
11
+
12
+ private var player: TrackPlayer!
13
+
14
+ override func setUp() async throws {
15
+ try await super.setUp()
16
+ player = TrackPlayer()
17
+ _ = try await player.configure(config: PlayerFixtures.config()).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
+ func testSetQueueIsVisibleToWorkEnqueuedAfterIt() async throws {
29
+ let tracks = try PlayerFixtures.localTracks(count: 3, for: self)
30
+
31
+ _ = try player.setQueue(tracks: tracks, startAtIndex: 1)
32
+ let seen = try await probe { (count: $0.tracks.count, index: $0.currentTrackIndex) }
33
+
34
+ XCTAssertEqual(seen.count, 3)
35
+ XCTAssertEqual(seen.index, 1)
36
+ }
37
+
38
+ func testAddToQueueIsVisibleToWorkEnqueuedAfterIt() async throws {
39
+ _ = try await player.setQueue(tracks: PlayerFixtures.localTracks(count: 3, for: self), startAtIndex: 0).await()
40
+
41
+ _ = try player.addToQueue(tracks: PlayerFixtures.localTracks(count: 2, for: self), insertBefore: nil)
42
+ let count = try await probe { $0.tracks.count }
43
+
44
+ XCTAssertEqual(count, 5)
45
+ }
46
+
47
+ /// The immediate-next slot is the insert `AVQueuePlayer` prerolls, so it is
48
+ /// the one whose commit is most at risk of being deferred.
49
+ func testAnImmediateNextInsertIsVisibleToWorkEnqueuedAfterIt() async throws {
50
+ _ = try await player.setQueue(tracks: PlayerFixtures.localTracks(count: 3, for: self), startAtIndex: 0).await()
51
+
52
+ _ = try player.addToQueue(tracks: PlayerFixtures.localTracks(count: 1, for: self), insertBefore: 1)
53
+ let count = try await probe { $0.tracks.count }
54
+
55
+ XCTAssertEqual(count, 4)
56
+ }
57
+
58
+ func testRemoveFromQueueIsVisibleToWorkEnqueuedAfterIt() async throws {
59
+ _ = try await player.setQueue(tracks: PlayerFixtures.localTracks(count: 4, for: self), startAtIndex: 0).await()
60
+
61
+ _ = try player.removeFromQueue(indices: [2, 3])
62
+ let count = try await probe { $0.tracks.count }
63
+
64
+ XCTAssertEqual(count, 2)
65
+ }
66
+
67
+ func testMoveInQueueIsVisibleToWorkEnqueuedAfterIt() async throws {
68
+ _ = try await player.setQueue(tracks: PlayerFixtures.localTracks(count: 3, for: self), startAtIndex: 0).await()
69
+
70
+ _ = try player.moveInQueue(fromIndex: 2, toIndex: 1)
71
+ let idAtOne = try await probe { $0.tracks[1].id }
72
+
73
+ XCTAssertEqual(idAtOne, "t2")
74
+ }
75
+
76
+ /// A start index of NaN reaches `Int(_:)` unless it is guarded in `Double`
77
+ /// space, where the conversion is undefined behaviour.
78
+ func testANonFiniteStartIndexResolvesToTheQueueHead() async throws {
79
+ _ = try player.setQueue(tracks: PlayerFixtures.localTracks(count: 3, for: self), startAtIndex: Double.nan)
80
+ let index = try await probe { $0.currentTrackIndex }
81
+
82
+ XCTAssertEqual(index, 0)
83
+ }
84
+
85
+ // MARK: - Helpers
86
+
87
+ /// Enqueue a read behind whatever the call under test just enqueued.
88
+ private func probe<T>(_ body: @escaping (TrackPlayer) -> T) async throws -> T {
89
+ let player = try XCTUnwrap(self.player)
90
+ return await player.onPlayerQueueValue { body(player) }
91
+ }
92
+
93
+ }