react-native-queue-player 1.2.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/consumer-rules.pro +1 -1
  4. package/android/src/main/cpp/airplay2_control.c +4 -4
  5. package/android/src/main/cpp/airplay2_control.h +4 -4
  6. package/android/src/main/cpp/airplay2_jni.cpp +3 -19
  7. package/android/src/main/cpp/airplay2_pair.c +7 -7
  8. package/android/src/main/cpp/airplay2_pair.h +2 -2
  9. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  10. package/android/src/main/cpp/airplay2_session.c +2 -2
  11. package/android/src/main/cpp/airplay_jni.cpp +8 -69
  12. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  61. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  62. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  63. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  64. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  65. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  114. package/app.plugin.js +17 -1
  115. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  116. package/ios/AVQueueBuilder.swift +120 -85
  117. package/ios/ArtworkLoader.swift +0 -7
  118. package/ios/ArtworkResolver.swift +22 -4
  119. package/ios/AssetReplayGainReader.swift +118 -0
  120. package/ios/AudioSession.swift +67 -16
  121. package/ios/AudioTapProvider.swift +284 -149
  122. package/ios/CarPlayBridge.swift +0 -12
  123. package/ios/CarPlayCoordinator.swift +18 -2
  124. package/ios/CarPlaySceneDelegate.swift +2 -2
  125. package/ios/Cast/AirPlayRouteState.swift +9 -8
  126. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  127. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  128. package/ios/Cast/Core/CastSession.swift +2 -2
  129. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  130. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  131. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  132. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  133. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  134. package/ios/CastManager.swift +1 -1
  135. package/ios/CrossfadeEngine.swift +672 -226
  136. package/ios/EQTap.swift +20 -17
  137. package/ios/Equalizer.swift +1 -1
  138. package/ios/FLACStreamInfo.swift +65 -0
  139. package/ios/GaplessEngine.swift +57 -30
  140. package/ios/InputGuards.swift +35 -5
  141. package/ios/LookaheadCache.swift +229 -58
  142. package/ios/LookaheadCachePrefetcher.swift +48 -36
  143. package/ios/MediaResponseCheck.swift +122 -0
  144. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  145. package/ios/NetworkRecoveryPolicy.swift +32 -0
  146. package/ios/NowPlayingFormatExtractor.swift +25 -18
  147. package/ios/NowPlayingInfo.swift +84 -48
  148. package/ios/OutputRouteMonitor.swift +16 -3
  149. package/ios/PendingBrowseRequests.swift +1 -1
  150. package/ios/PlaceholderArtwork.swift +9 -5
  151. package/ios/PlaybackEngine.swift +165 -76
  152. package/ios/PlaybackErrorMapping.swift +3 -3
  153. package/ios/PlaybackModeStateMachine.swift +2 -2
  154. package/ios/PlaybackNetworkMonitor.swift +73 -0
  155. package/ios/PlayerStateDerivation.swift +6 -6
  156. package/ios/QueueMutationArithmetic.swift +25 -0
  157. package/ios/QueueSkipArithmetic.swift +5 -3
  158. package/ios/QueueWindowArithmetic.swift +152 -0
  159. package/ios/ReadThroughServer.swift +283 -0
  160. package/ios/RemoteCommands.swift +44 -4
  161. package/ios/ReplayGainData.swift +22 -3
  162. package/ios/ReplayGainExtractor.swift +70 -27
  163. package/ios/Siri/VoiceDonation.swift +14 -9
  164. package/ios/StreamingBitrateProbe.swift +22 -68
  165. package/ios/SuppliedReplayGain.swift +83 -0
  166. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  167. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  168. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  169. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  170. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  171. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  172. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  173. package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
  174. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  175. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  176. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  177. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  178. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  179. package/ios/Tests/EngineSwapTests.swift +91 -0
  180. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  181. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  182. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  183. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  184. package/ios/Tests/InputGuardsTests.swift +41 -0
  185. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  186. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  187. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  188. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  189. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  190. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  191. package/ios/Tests/MediaResponseCheckTests.swift +128 -0
  192. package/ios/Tests/MutationDeferralTests.swift +220 -0
  193. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  194. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  195. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  196. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  197. package/ios/Tests/PlayerFixtures.swift +48 -0
  198. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  199. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  200. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  201. package/ios/Tests/QueueStateTests.swift +3 -1
  202. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  203. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  204. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  205. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  206. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  207. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  208. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  209. package/ios/Tests/RetryRecoveryTests.swift +418 -0
  210. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  211. package/ios/Tests/SkipIndexTests.swift +32 -2
  212. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  213. package/ios/Tests/StallRecoveryTests.swift +97 -0
  214. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  215. package/ios/Tests/TopUpWindowGateTests.swift +441 -0
  216. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  217. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  218. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  219. package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
  220. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  221. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  222. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  223. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  224. package/ios/TrackPlayer+Automotive.swift +101 -0
  225. package/ios/TrackPlayer+Cache.swift +232 -0
  226. package/ios/TrackPlayer+Config.swift +291 -0
  227. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  228. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  229. package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
  230. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  231. package/ios/TrackPlayer+Lifecycle.swift +982 -0
  232. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  233. package/ios/TrackPlayer+Queue.swift +951 -0
  234. package/ios/TrackPlayer+Recovery.swift +213 -0
  235. package/ios/TrackPlayer+Skip.swift +465 -0
  236. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  237. package/ios/TrackPlayer+State.swift +108 -0
  238. package/ios/TrackPlayer+Threading.swift +135 -0
  239. package/ios/TrackPlayer+Transport.swift +272 -0
  240. package/ios/TrackPlayer+Window.swift +227 -0
  241. package/ios/TrackPlayer.swift +331 -4619
  242. package/ios/Visualizer.swift +6 -5
  243. package/ios/tests-harness/Podfile +1 -1
  244. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  245. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  246. package/lib/module/hooks/useActiveTrack.js +29 -22
  247. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  248. package/lib/module/hooks/useCast.js +8 -26
  249. package/lib/module/hooks/useCast.js.map +1 -1
  250. package/lib/module/hooks/useEqualizer.js +19 -12
  251. package/lib/module/hooks/useEqualizer.js.map +1 -1
  252. package/lib/module/hooks/useLookaheadCache.js +3 -7
  253. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  254. package/lib/module/hooks/useQueue.js +66 -19
  255. package/lib/module/hooks/useQueue.js.map +1 -1
  256. package/lib/module/index.js +6 -3
  257. package/lib/module/index.js.map +1 -1
  258. package/lib/module/queueDelta.js +41 -0
  259. package/lib/module/queueDelta.js.map +1 -0
  260. package/lib/module/types.js +28 -5
  261. package/lib/module/types.js.map +1 -1
  262. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  263. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  264. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  265. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  266. package/lib/typescript/hooks/useCast.d.ts +6 -1
  267. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  269. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  271. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  272. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  273. package/lib/typescript/index.d.ts +2 -1
  274. package/lib/typescript/index.d.ts.map +1 -1
  275. package/lib/typescript/queueDelta.d.ts +10 -0
  276. package/lib/typescript/queueDelta.d.ts.map +1 -0
  277. package/lib/typescript/types.d.ts +168 -12
  278. package/lib/typescript/types.d.ts.map +1 -1
  279. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  280. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  281. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  282. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  283. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  284. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  285. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  320. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  321. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  322. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  323. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  324. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  325. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  326. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  327. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  328. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  329. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  330. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  331. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  332. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  333. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  334. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  335. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  336. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  337. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  338. package/package.json +8 -8
  339. package/src/TrackPlayer.nitro.ts +53 -27
  340. package/src/hooks/useActiveTrack.ts +29 -22
  341. package/src/hooks/useCast.ts +14 -15
  342. package/src/hooks/useEqualizer.ts +19 -12
  343. package/src/hooks/useLookaheadCache.ts +3 -7
  344. package/src/hooks/useQueue.ts +66 -17
  345. package/src/index.ts +13 -3
  346. package/src/queueDelta.ts +41 -0
  347. package/src/types.ts +169 -12
  348. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  349. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  350. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  351. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  352. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  353. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  354. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  355. package/ios/MetadataReader.swift +0 -555
  356. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  357. package/ios/Tests/MetadataReaderTests.swift +0 -603
  358. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  359. package/ios/Tests/VoiceDonationTests.swift +0 -26
  360. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  361. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  362. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -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
+ }
@@ -0,0 +1,86 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// `configure()` is a clean-slate operation: it releases whichever engine is
5
+ /// live and clears the queue model before building the fresh one. The
6
+ /// "is anything configured?" gate keys on the engine, which both engines
7
+ /// populate — `player` holds the gapless `AVQueuePlayer` and is nil for the
8
+ /// whole time a `CrossfadeEngine` is active, so it cannot answer that
9
+ /// question.
10
+ final class TrackPlayerConfigureTeardownTests: XCTestCase {
11
+
12
+ private var player: TrackPlayer!
13
+
14
+ override func setUp() async throws {
15
+ try await super.setUp()
16
+ player = TrackPlayer()
17
+ }
18
+
19
+ override func tearDown() async throws {
20
+ if let player {
21
+ _ = try await player.destroy().await()
22
+ }
23
+ player = nil
24
+ try await super.tearDown()
25
+ }
26
+
27
+ func testReconfigureClearsQueueUnderGapless() async throws {
28
+ try await assertReconfigureIsCleanSlate(
29
+ mode: PlaybackMode(kind: .gapless, crossfadeDurationMs: nil),
30
+ label: "gapless"
31
+ )
32
+ }
33
+
34
+ func testReconfigureClearsQueueUnderCrossfade() async throws {
35
+ try await assertReconfigureIsCleanSlate(
36
+ mode: PlaybackMode(kind: .crossfade, crossfadeDurationMs: 3000),
37
+ label: "crossfade"
38
+ )
39
+ }
40
+
41
+ /// Configure, install [mode], load a queue, then configure again without an
42
+ /// intervening `destroy()`. The second configure must have torn the prior
43
+ /// engine down: queue empty, current index reset.
44
+ private func assertReconfigureIsCleanSlate(
45
+ mode: PlaybackMode, label: String
46
+ ) async throws {
47
+ _ = try await player.configure(config: PlayerFixtures.config()).await()
48
+ _ = try await player.setPlaybackMode(mode: mode).await()
49
+ let tracks = try PlayerFixtures.localTracks(count: 3, for: self)
50
+ _ = try await player.setQueue(tracks: tracks, startAtIndex: 0).await()
51
+
52
+ let seeded = try await player.getQueue().await()
53
+ XCTAssertEqual(
54
+ seeded.count, tracks.count,
55
+ "\(label): expected the seeded queue before the re-configure")
56
+
57
+ _ = try await player.configure(config: PlayerFixtures.config()).await()
58
+
59
+ let cleared = try await player.getQueue().await()
60
+ XCTAssertEqual(
61
+ cleared.count, 0,
62
+ "\(label): configure() must clear the queue, got \(cleared.count) entries")
63
+ XCTAssertEqual(
64
+ try player.getCurrentTrackIndex(), -1,
65
+ "\(label): configure() must reset the current index")
66
+ }
67
+
68
+ /// The re-configure clears the queue without an emit; the revision the next
69
+ /// emit carries skips one, which is the signal a delta mirror re-reads on.
70
+ func testReconfigureLeavesARevisionGapForTheNextEmit() async throws {
71
+ _ = try await player.configure(config: PlayerFixtures.config()).await()
72
+ var revisions: [Int] = []
73
+ let unsubscribe = try player.onQueueChange { delta, _, _ in
74
+ revisions.append(Int(delta.revision))
75
+ }
76
+ defer { unsubscribe() }
77
+ _ = try await player.setQueue(tracks: PlayerFixtures.localTracks(count: 2, for: self), startAtIndex: 0).await()
78
+ let before = try XCTUnwrap(revisions.last)
79
+
80
+ _ = try await player.configure(config: PlayerFixtures.config()).await()
81
+ _ = try await player.setQueue(tracks: PlayerFixtures.localTracks(count: 1, for: self), startAtIndex: 0).await()
82
+
83
+ XCTAssertEqual(revisions.last, before + 2)
84
+ }
85
+
86
+ }
@@ -0,0 +1,206 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// A drained engine reaches the same end-of-queue verdict whichever engine is
5
+ /// installed. The engines report that they drained; the queue above them
6
+ /// decides what that means.
7
+ final class TrackPlayerEndVerdictTests: XCTestCase {
8
+
9
+ private var player: TrackPlayer!
10
+
11
+ override func setUp() async throws {
12
+ try await super.setUp()
13
+ player = TrackPlayer()
14
+ _ = try await player.configure(config: PlayerFixtures.config()).await()
15
+ }
16
+
17
+ override func tearDown() async throws {
18
+ if let player {
19
+ _ = try await player.destroy().await()
20
+ }
21
+ player = nil
22
+ try await super.tearDown()
23
+ }
24
+
25
+ // MARK: - The wrap case
26
+
27
+ func testRepeatQueueWrapsToTheHeadFromTheTail() async throws {
28
+ try await seedQueue(count: 3, at: 2, repeatMode: .queue)
29
+
30
+ await onQueue { $0.applyEndVerdict() }
31
+
32
+ let state = try await read { (index: $0.currentTrackIndex, ended: $0.reachedQueueEnd) }
33
+ XCTAssertEqual(state.index, 0)
34
+ XCTAssertFalse(state.ended, "a wrap is not the end of the queue")
35
+ }
36
+
37
+ /// A single-track queue is already at position 0, so only the re-seat marks
38
+ /// the wrap: the dedup key is cleared so the repeated track still emits.
39
+ func testRepeatQueueReSeatsASingleTrackQueue() async throws {
40
+ try await seedQueue(count: 1, at: 0, repeatMode: .queue)
41
+ await onQueue { $0.lastEmittedQueueItemId = "stale" }
42
+
43
+ await onQueue { $0.applyEndVerdict() }
44
+
45
+ let state = try await read {
46
+ (ended: $0.reachedQueueEnd, emitted: $0.lastEmittedQueueItemId,
47
+ head: $0.queueItemIds.first)
48
+ }
49
+ XCTAssertFalse(state.ended)
50
+ XCTAssertEqual(
51
+ state.emitted, state.head,
52
+ "clearing the dedup key lets the repeated track emit again")
53
+ }
54
+
55
+ /// Under crossfade there is no `AVQueuePlayer`, and the verdict must not
56
+ /// depend on one being there.
57
+ func testRepeatQueueWrapsUnderCrossfadeToo() async throws {
58
+ try await installCrossfade()
59
+ try await seedQueue(count: 3, at: 2, repeatMode: .queue)
60
+ let queuePlayer = try await read { $0.player }
61
+ XCTAssertNil(queuePlayer, "precondition: no gapless queue player")
62
+
63
+ await onQueue { $0.applyEndVerdict() }
64
+
65
+ let state = try await read { (index: $0.currentTrackIndex, ended: $0.reachedQueueEnd) }
66
+ XCTAssertEqual(state.index, 0)
67
+ XCTAssertFalse(state.ended, "a wrap is not the end of the queue on either engine")
68
+ }
69
+
70
+ func testRepeatOffEndsTheQueueAtTheTail() async throws {
71
+ try await seedQueue(count: 3, at: 2, repeatMode: .off)
72
+
73
+ await onQueue { $0.applyEndVerdict() }
74
+
75
+ let ended = try await read { $0.reachedQueueEnd }
76
+ XCTAssertTrue(ended)
77
+ }
78
+
79
+ // MARK: - The engine's own signal reaches the verdict
80
+
81
+ /// The crossfade engine reports a drain through the delegate, which is the
82
+ /// route that has to reach the same decision.
83
+ func testTheEngineDelegateEndSignalWrapsTheQueue() async throws {
84
+ try await installCrossfade()
85
+ try await seedQueue(count: 3, at: 2, repeatMode: .queue)
86
+ let engine = try await read { $0.engine }
87
+ let reporting = try XCTUnwrap(engine)
88
+
89
+ await onQueue { $0.enginePlaybackEnded(reporting) }
90
+
91
+ let state = try await read { (index: $0.currentTrackIndex, ended: $0.reachedQueueEnd) }
92
+ XCTAssertEqual(state.index, 0)
93
+ XCTAssertFalse(state.ended)
94
+ }
95
+
96
+ // MARK: - A tail that cannot be built
97
+
98
+ /// A track whose URL cannot be built is dropped from the engine's items, so
99
+ /// the engine drains before the logical tail. Re-seating that position
100
+ /// installs nothing and fires no further engine signal, so the decision has
101
+ /// to be retaken rather than left parked mid-queue.
102
+ func testAnUnbuildableTailStillEndsTheQueue() async throws {
103
+ try await installCrossfade()
104
+ try await seedQueue(count: 3, at: 1, repeatMode: .off, lastTrackURL: "")
105
+
106
+ await onQueue { $0.applyEndVerdict() }
107
+
108
+ let ended = try await read { $0.reachedQueueEnd }
109
+ XCTAssertTrue(ended, "an unplayable last track must still end the queue")
110
+ }
111
+
112
+ /// Repeat-track re-seats the same position, so when that position cannot
113
+ /// be built there is nothing to refill: the queue ends rather than being
114
+ /// re-decided on the same anchor again.
115
+ func testAnUnbuildableCurrentTrackUnderRepeatTrackEndsTheQueue() async throws {
116
+ try await installCrossfade()
117
+ try await seedQueue(count: 3, at: 2, repeatMode: .track, lastTrackURL: "")
118
+
119
+ await onQueue { $0.applyEndVerdict() }
120
+
121
+ let state = try await read { (index: $0.currentTrackIndex, ended: $0.reachedQueueEnd) }
122
+ XCTAssertEqual(state.index, 2)
123
+ XCTAssertTrue(state.ended, "an unplayable repeated track has nothing to refill")
124
+ }
125
+
126
+ func testAnUnbuildableTailStillWrapsUnderRepeatQueue() async throws {
127
+ try await installCrossfade()
128
+ try await seedQueue(count: 3, at: 1, repeatMode: .queue, lastTrackURL: "")
129
+
130
+ await onQueue { $0.applyEndVerdict() }
131
+
132
+ let state = try await read { (index: $0.currentTrackIndex, ended: $0.reachedQueueEnd) }
133
+ XCTAssertEqual(state.index, 0)
134
+ XCTAssertFalse(state.ended)
135
+ }
136
+
137
+ // MARK: - A drain that is really a failure
138
+
139
+ /// A drain that follows the current track's reported failure stays where it
140
+ /// is rather than wrapping.
141
+ func testADrainAfterTheCurrentTrackFailedDoesNotAdvanceTheQueue() async throws {
142
+ try await seedQueue(count: 3, at: 2, repeatMode: .queue)
143
+ await onQueue { $0.reportedFailureQueueItemIds = [$0.queueItemIds[2]] }
144
+
145
+ await onQueue { $0.applyEndVerdict() }
146
+
147
+ let state = try await read { (index: $0.currentTrackIndex, ended: $0.reachedQueueEnd) }
148
+ XCTAssertEqual(state.index, 2, "the queue stays on the track whose failure was reported")
149
+ XCTAssertFalse(state.ended, "a reported failure is not the end of the queue either")
150
+ }
151
+
152
+ /// The crossfade engine's own end signal reaches the same decision.
153
+ func testAnEngineEndReportAfterTheCurrentTrackFailedDoesNotAdvanceTheQueue() async throws {
154
+ try await installCrossfade()
155
+ try await seedQueue(count: 3, at: 2, repeatMode: .queue)
156
+ await onQueue { $0.reportedFailureQueueItemIds = [$0.queueItemIds[2]] }
157
+ let engine = try await read { $0.engine }
158
+ let reporting = try XCTUnwrap(engine)
159
+
160
+ await onQueue { $0.enginePlaybackEnded(reporting) }
161
+
162
+ let index = try await read { $0.currentTrackIndex }
163
+ XCTAssertEqual(index, 2)
164
+ }
165
+
166
+ /// A report against a track the playhead has left does not hold a genuine
167
+ /// end of queue open.
168
+ func testADrainAfterADifferentTrackFailedStillEndsTheQueue() async throws {
169
+ try await seedQueue(count: 3, at: 2, repeatMode: .off)
170
+ await onQueue { $0.reportedFailureQueueItemIds = [$0.queueItemIds[0]] }
171
+
172
+ await onQueue { $0.applyEndVerdict() }
173
+
174
+ let ended = try await read { $0.reachedQueueEnd }
175
+ XCTAssertTrue(ended)
176
+ }
177
+
178
+ // MARK: - Helpers
179
+
180
+ private func installCrossfade() async throws {
181
+ _ = try await player.setPlaybackMode(
182
+ mode: PlaybackMode(kind: .crossfade, crossfadeDurationMs: 3000)).await()
183
+ }
184
+
185
+ private func seedQueue(
186
+ count: Int, at index: Int, repeatMode: RepeatMode, lastTrackURL: String? = nil
187
+ ) async throws {
188
+ var tracks = try PlayerFixtures.localTracks(count: count, for: self)
189
+ if let lastTrackURL {
190
+ tracks[count - 1] = PlayerFixtures.track(id: "unbuildable", url: lastTrackURL)
191
+ }
192
+ _ = try await player.setQueue(tracks: tracks, startAtIndex: 0).await()
193
+ _ = try await player.setRepeatMode(mode: repeatMode).await()
194
+ await onQueue { $0.currentTrackIndex = index }
195
+ }
196
+
197
+ private func onQueue(_ body: @escaping (TrackPlayer) -> Void) async {
198
+ guard let player else { return }
199
+ await player.onPlayerQueue { body(player) }
200
+ }
201
+
202
+ private func read<T>(_ body: @escaping (TrackPlayer) -> T) async throws -> T {
203
+ let player = try XCTUnwrap(self.player)
204
+ return await player.onPlayerQueueValue { body(player) }
205
+ }
206
+ }
@@ -0,0 +1,137 @@
1
+ import MediaPlayer
2
+ import AVFoundation
3
+ import XCTest
4
+ @testable import QueuePlayer
5
+
6
+ /// `seekTo` resolves on every branch, including the ones that complete without
7
+ /// issuing a seek — a branch that returned without resuming would strand the
8
+ /// calling JS Promise for the life of the process.
9
+ final class TrackPlayerSeekTests: XCTestCase {
10
+
11
+ /// A seek against a live engine writes the process-global
12
+ /// `MPNowPlayingInfoCenter` on main. Drain that write and clear the centre
13
+ /// so it does not reach the next test class.
14
+ override func tearDown() {
15
+ let drained = expectation(description: "pending main-queue now-playing writes complete")
16
+ DispatchQueue.main.async { drained.fulfill() }
17
+ wait(for: [drained], timeout: 5.0)
18
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
19
+ super.tearDown()
20
+ }
21
+
22
+ func testSeekResolvesWithNoEngineAttached() {
23
+ let player = TrackPlayer()
24
+ expectSeekToResolve(player, position: 10, "no engine and no queue player")
25
+ }
26
+
27
+ func testSeekResolvesOnADrainedQueuePlayer() {
28
+ let player = TrackPlayer()
29
+ player.player = AVQueuePlayer()
30
+ expectSeekToResolve(player, position: 10, "a queue player with no current item")
31
+ }
32
+
33
+ /// With no queue there is nothing to reinstall, so a seek on a drained
34
+ /// player resolves without changing the end-of-queue state.
35
+ func testSeekOnADrainedQueuePlayerWithNoQueueLeavesTheEndOfQueueStateSet() {
36
+ let player = TrackPlayer()
37
+ player.player = AVQueuePlayer()
38
+ player.reachedQueueEnd = true
39
+
40
+ expectSeekToResolve(player, position: 10, "a drained player at the end of the queue")
41
+
42
+ XCTAssertTrue(
43
+ player.reachedQueueEnd,
44
+ "a seek that never reached the player must not report the queue as replaying")
45
+ }
46
+
47
+ func testSeekResolvesForAPositionBelowZero() {
48
+ let player = TrackPlayer()
49
+ expectSeekToResolve(player, position: -5, "a negative position")
50
+ }
51
+
52
+ /// A crossfade engine that does not hold the current track has it
53
+ /// reinstalled, and the seek is then issued against the reinstalled run.
54
+ func testSeekReinstallsAnAbsentCurrentTrackAndThenSeeksIt() {
55
+ let player = TrackPlayer()
56
+ let engine = RecordingEngineStub()
57
+ _ = player.queueState.replaceAll((0 ..< 2).map {
58
+ PlayerFixtures.track(id: "t\($0)", url: "http://127.0.0.1:1/\($0).m4a")
59
+ })
60
+ player.currentTrackIndex = 1
61
+ player.engine = engine
62
+
63
+ expectSeekToResolve(player, position: 30, "a current track the engine does not hold")
64
+
65
+ XCTAssertTrue(
66
+ engine.installedIds.contains(player.queueItemIds[1]),
67
+ "the reinstalled run holds the current track")
68
+ let seek = try? XCTUnwrap(engine.seeks.first)
69
+ XCTAssertEqual(
70
+ engine.allMediaItems[safe: seek?.index ?? -1]?.queueItemId, player.queueItemIds[1],
71
+ "the seek targets the current track's position in the reinstalled run")
72
+ XCTAssertEqual(seek?.position, 30)
73
+ }
74
+
75
+ /// Drives `seekTo` to completion and fails if it has not resolved within the
76
+ /// timeout, rather than hanging the suite on a stranded continuation.
77
+ private func expectSeekToResolve(
78
+ _ player: TrackPlayer, position: Double, _ branch: String,
79
+ file: StaticString = #filePath, line: UInt = #line
80
+ ) {
81
+ let resolved = expectation(description: "seekTo resolves on \(branch)")
82
+ Task {
83
+ do {
84
+ _ = try await player.seekTo(position: position).await()
85
+ resolved.fulfill()
86
+ } catch {
87
+ XCTFail("seekTo rejected on \(branch): \(error)", file: file, line: line)
88
+ }
89
+ }
90
+ wait(for: [resolved], timeout: 5.0)
91
+ }
92
+ }
93
+
94
+ /// Holds what it is given and records the seeks it is asked for.
95
+ private final class RecordingEngineStub: PlaybackEngine {
96
+ private(set) var seeks: [(index: Int, position: TimeInterval)] = []
97
+ private var installed: [AVPlayerItem] = []
98
+ var installedIds: [String] { installed.compactMap { $0.queueItemId } }
99
+
100
+ var currentMediaItem: AVPlayerItem? { nil }
101
+ var allMediaItems: [AVPlayerItem] { installed }
102
+ var attachedQueueItemIds: [String] { [] }
103
+ var isFadePendingOrActive: Bool { false }
104
+ var currentMediaItemIndex: Int { -1 }
105
+ var currentPositionSeconds: TimeInterval { 0 }
106
+ var currentDurationSeconds: TimeInterval { 0 }
107
+ var bufferedPositionSeconds: TimeInterval { 0 }
108
+ var isPlaying: Bool { false }
109
+ var timeControlStatus: AVPlayer.TimeControlStatus { .paused }
110
+ var delegate: PlaybackEngineDelegate?
111
+
112
+ func release() {}
113
+ func setItems(_ items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) {
114
+ installed = items
115
+ }
116
+ @discardableResult
117
+ func insertItems(_ items: [AVPlayerItem], after: AVPlayerItem?) -> Bool {
118
+ installed.append(contentsOf: items)
119
+ return true
120
+ }
121
+ func remove(_ item: AVPlayerItem) { installed.removeAll { $0 === item } }
122
+ func removeAllItems() { installed.removeAll() }
123
+ func canInsert(_ item: AVPlayerItem, after: AVPlayerItem?) -> Bool { true }
124
+ func play() {}
125
+ func pause() {}
126
+ func stop() {}
127
+ func seek(toIndex index: Int, position: TimeInterval) { seeks.append((index, position)) }
128
+ func skipToNext() {}
129
+ func skipToPrevious() {}
130
+ func setVolume(_ volume: Float) {}
131
+ func setPlaybackSpeed(_ speed: Float) {}
132
+ func seedPlaybackSpeed(_ speed: Float) {}
133
+ func setRepeatMode(_ mode: RepeatMode) {}
134
+ func setPitchCorrectionMode(_ mode: PitchCorrectionMode) {}
135
+ func setAudioMixProvider(_ provider: AudioMixProvider?) {}
136
+ func refreshAllItemMixes() {}
137
+ }
@@ -0,0 +1,127 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// `onPlayerQueueSync` serialises on `playerQueue`, and recognises when it is already
5
+ /// there. The recognition is what makes re-entrancy safe: a `sync` onto the
6
+ /// queue from a block already running on it deadlocks immediately.
7
+ final class TrackPlayerThreadingTests: XCTestCase {
8
+
9
+ func testSyncAccessorRunsInlineWhenAlreadyOnThePlayerQueue() {
10
+ let player = TrackPlayer()
11
+ let returned = expectation(description: "onPlayerQueueSync returns from the player queue")
12
+ var value = 0
13
+
14
+ player.playerQueue.async {
15
+ value = player.onPlayerQueueSync { 42 }
16
+ returned.fulfill()
17
+ }
18
+
19
+ wait(for: [returned], timeout: 2.0)
20
+ XCTAssertEqual(value, 42)
21
+ }
22
+
23
+ func testSyncAccessorReturnsFromOffTheQueue() {
24
+ let player = TrackPlayer()
25
+ let returned = expectation(description: "onPlayerQueueSync returns from an off-queue caller")
26
+ var value = 0
27
+
28
+ DispatchQueue.global().async {
29
+ value = player.onPlayerQueueSync { 7 }
30
+ returned.fulfill()
31
+ }
32
+
33
+ wait(for: [returned], timeout: 2.0)
34
+ XCTAssertEqual(value, 7)
35
+ }
36
+
37
+ func testNestedSyncAccessorsDoNotDeadlock() {
38
+ let player = TrackPlayer()
39
+ let returned = expectation(description: "a nested onPlayerQueueSync returns")
40
+ var value = 0
41
+
42
+ DispatchQueue.global().async {
43
+ value = player.onPlayerQueueSync { player.onPlayerQueueSync { 9 } }
44
+ returned.fulfill()
45
+ }
46
+
47
+ wait(for: [returned], timeout: 2.0)
48
+ XCTAssertEqual(value, 9)
49
+ }
50
+
51
+ func testOnQueueRecognitionIsPerInstance() {
52
+ let first = TrackPlayer()
53
+ let second = TrackPlayer()
54
+ let checked = expectation(description: "recognition evaluated on first's queue")
55
+ var firstSeesItsOwnQueue = false
56
+ var secondSeesAnotherPlayersQueue = true
57
+
58
+ first.playerQueue.async {
59
+ firstSeesItsOwnQueue = first.isOnPlayerQueue
60
+ secondSeesAnotherPlayersQueue = second.isOnPlayerQueue
61
+ checked.fulfill()
62
+ }
63
+
64
+ wait(for: [checked], timeout: 2.0)
65
+ XCTAssertTrue(firstSeesItsOwnQueue)
66
+ XCTAssertFalse(
67
+ secondSeesAnotherPlayersQueue,
68
+ "one player must not report being serialised by another player's queue")
69
+ }
70
+
71
+ // MARK: - Call-order preservation
72
+
73
+ /// Work enqueued from one thread runs in the order it was enqueued. This is
74
+ /// what makes a JS `play()` then `pause()` reach the player in that order:
75
+ /// a serial queue gives mutual exclusion, and enqueueing at call time is
76
+ /// what gives ordering on top of it.
77
+ func testEnqueuedWorkRunsInTheOrderItWasCalled() {
78
+ let player = TrackPlayer()
79
+ let lock = NSLock()
80
+ var order: [Int] = []
81
+ let ran = expectation(description: "every enqueued body ran")
82
+ ran.expectedFulfillmentCount = 50
83
+
84
+ for i in 0 ..< 50 {
85
+ _ = player.enqueue {
86
+ lock.lock()
87
+ order.append(i)
88
+ lock.unlock()
89
+ ran.fulfill()
90
+ }
91
+ }
92
+
93
+ wait(for: [ran], timeout: 5.0)
94
+ XCTAssertEqual(order, Array(0 ..< 50))
95
+ }
96
+
97
+ func testEnqueueResolvesItsPromiseAfterTheBodyRuns() async throws {
98
+ let player = TrackPlayer()
99
+ var didRun = false
100
+ let promise = player.enqueue { didRun = true }
101
+
102
+ _ = try await promise.await()
103
+
104
+ XCTAssertTrue(didRun, "the promise must not resolve before the body has run")
105
+ }
106
+
107
+ func testEnqueueThrowingRejectsWithTheThrownError() async {
108
+ let player = TrackPlayer()
109
+ struct Boom: Error {}
110
+ do {
111
+ _ = try await player.enqueueThrowing { throw Boom() }.await()
112
+ XCTFail("expected the promise to reject")
113
+ } catch {
114
+ // The bridge surfaces the message; reaching here is the contract.
115
+ }
116
+ }
117
+
118
+ func testEnqueueValueCarriesItsResultBack() async throws {
119
+ let player = TrackPlayer()
120
+ player.currentTrackIndex = 7
121
+
122
+ let value = try await player.enqueueValue { player.currentTrackIndex }.await()
123
+
124
+ XCTAssertEqual(value, 7)
125
+ }
126
+
127
+ }