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,120 @@
1
+ import AVFoundation
2
+ import XCTest
3
+ @testable import QueuePlayer
4
+
5
+ /// An item that reaches its end mid-queue with no fade in flight advances the
6
+ /// engine anyway. A fade is how this engine usually crosses a boundary, not
7
+ /// the only way it can — the arm can fail to load, time out, or be cancelled
8
+ /// by a route change or a queue mutation, and the queue must not stop on a
9
+ /// track that has finished.
10
+ final class CrossfadeAdvanceWithoutFadeTests: XCTestCase {
11
+
12
+ func testAnItemEndingMidQueueAdvancesToTheNextItem() {
13
+ let engine = makeEngine(count: 3)
14
+ engine.play()
15
+
16
+ endLeadingItem(on: engine)
17
+
18
+ XCTAssertEqual(
19
+ engine.currentMediaItem?.queueItemId, "id-1",
20
+ "the engine moves to the next item in the run")
21
+ XCTAssertEqual(engine.currentMediaItemIndex, 1)
22
+ }
23
+
24
+ /// The queue slot may still hold the instance the arm attached to the
25
+ /// standby leg, and attaching an item still associated with another player
26
+ /// raises an exception Swift cannot catch.
27
+ func testTheAdvanceAttachesAFreshItemRatherThanTheStoredOne() {
28
+ let engine = makeEngine(count: 3)
29
+ engine.play()
30
+ let stored = engine.allMediaItems
31
+
32
+ endLeadingItem(on: engine)
33
+
34
+ XCTAssertFalse(
35
+ engine.currentMediaItem === stored[1],
36
+ "the incoming item is rebuilt, never the stored instance")
37
+ XCTAssertEqual(
38
+ engine.currentMediaItem?.queueItemId, stored[1].queueItemId,
39
+ "the rebuild carries the queue identity across")
40
+ }
41
+
42
+ /// The end-of-item notification hops onto the owner's queue, so a pause
43
+ /// taken in between must survive the advance.
44
+ func testAnItemEndingWhilePausedDoesNotResumePlayback() {
45
+ let engine = makeEngine(count: 3)
46
+ engine.play()
47
+ engine.pause()
48
+
49
+ endLeadingItem(on: engine)
50
+
51
+ XCTAssertEqual(
52
+ engine.currentMediaItem?.queueItemId, "id-1",
53
+ "the advance still happens")
54
+ XCTAssertEqual(
55
+ engine.timeControlStatus, .paused,
56
+ "a paused engine stays paused across the advance")
57
+ }
58
+
59
+ func testTheLastItemEndingEndsTheQueueRatherThanAdvancing() {
60
+ let engine = makeEngine(count: 2)
61
+ let delegate = RecordingDelegate()
62
+ engine.delegate = delegate
63
+ engine.play()
64
+ engine.seek(toIndex: 1, position: 0)
65
+
66
+ endLeadingItem(on: engine)
67
+
68
+ XCTAssertTrue(delegate.playbackEnded, "the queue reports that it ran out")
69
+ XCTAssertEqual(engine.currentMediaItemIndex, 1, "and stays on the last item")
70
+ }
71
+
72
+ // MARK: - Helpers
73
+
74
+ private func makeEngine(count: Int) -> CrossfadeEngine {
75
+ let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
76
+ let items = (0 ..< count).map { index -> AVPlayerItem in
77
+ let item = AVPlayerItem(url: URL(string: "http://127.0.0.1:1/\(index).m4a")!)
78
+ item.queueItemId = "id-\(index)"
79
+ return item
80
+ }
81
+ engine.setItems(items, startIndex: 0, startPositionSeconds: 0)
82
+ engines.append(engine)
83
+ return engine
84
+ }
85
+
86
+ /// Post the end-of-item notification the way AVFoundation does, then let the
87
+ /// engine's hop onto its owner queue run before asserting.
88
+ private func endLeadingItem(on engine: CrossfadeEngine) {
89
+ guard let item = engine.currentMediaItem else {
90
+ return XCTFail("no item is attached to end")
91
+ }
92
+ NotificationCenter.default.post(
93
+ name: .AVPlayerItemDidPlayToEndTime, object: item)
94
+ let drained = expectation(description: "owner queue drained")
95
+ DispatchQueue.main.async { drained.fulfill() }
96
+ wait(for: [drained], timeout: 2.0)
97
+ }
98
+
99
+ private var engines: [CrossfadeEngine] = []
100
+
101
+ override func tearDown() {
102
+ engines.forEach { $0.release() }
103
+ engines.removeAll()
104
+ super.tearDown()
105
+ }
106
+ }
107
+
108
+ private final class RecordingDelegate: PlaybackEngineDelegate {
109
+ var playbackEnded = false
110
+
111
+ func engineStateMaybeChanged(_ engine: PlaybackEngine, reason: StateChangeReason?) {}
112
+ func engineDidTransitionTrack(_ engine: PlaybackEngine) {}
113
+ func engineActiveItemFormatChanged(_ engine: PlaybackEngine) {}
114
+ func engineDidFailWithError(
115
+ _ engine: PlaybackEngine, error: Error, item: AVPlayerItem?) {}
116
+ func engineDidPlayItemToEnd(_ engine: PlaybackEngine) {}
117
+ func enginePlaybackEnded(_ engine: PlaybackEngine) { playbackEnded = true }
118
+ func engineCrossfadeDidBegin(_ engine: PlaybackEngine) {}
119
+ func engineCrossfadeDidCancel(_ engine: PlaybackEngine) {}
120
+ }
@@ -0,0 +1,122 @@
1
+ import AVFoundation
2
+ import XCTest
3
+ @testable import QueuePlayer
4
+
5
+ /// The crossfade leading leg installs its EQ / visualizer / ReplayGain tap-mix
6
+ /// before it renders. AVPlayer honours `audioMix` only before an item renders,
7
+ /// and `AVPlayerItem.status == .readyToPlay` does not imply the asset's `tracks`
8
+ /// key has loaded — the provider yields nil until it is — so a leg that wants
9
+ /// the tap is held at rate 0 until the tracks load and the mix is installed,
10
+ /// then resumed. Miss that window and the tap is absent for the whole track.
11
+ ///
12
+ /// The incoming-leg attach, which is reached only through a real crossfade's
13
+ /// arm chain, is covered end-to-end by the demo `boundary-audio-chain` suite
14
+ /// (it drives an actual boundary on device); a unit test that drives the real
15
+ /// fade is too sensitive to a loaded CI simulator's arm-chain timing.
16
+ @MainActor
17
+ final class CrossfadeEngineIncomingMixTests: XCTestCase {
18
+
19
+ /// A tap-wanting consumer holds a freshly-seated leading leg at rate 0 until
20
+ /// its `tracks` key loads and the tap-bearing mix is installed, then resumes
21
+ /// it — the leg never renders untapped, and it does not strand at rate 0.
22
+ func testLeadingLegHeldForTapUntilMixInstalledThenResumes() async throws {
23
+ let url = try requireFixture(name: "silence-cbr-128", ext: "mp3")
24
+ let provider = TapWantingTracksGatedMixProvider()
25
+ let engine = CrossfadeEngine(
26
+ crossfadeDurationSeconds: 0.5, confinedTo: .main)
27
+ defer { engine.release() }
28
+ engine.setAudioMixProvider(provider)
29
+ engine.setItems(
30
+ [makeItem(url: url, id: "leading"), makeItem(url: url, id: "incoming")],
31
+ startIndex: 0,
32
+ startPositionSeconds: 0
33
+ )
34
+
35
+ // A fresh `AVURLAsset` starts `.unknown`, so the leading item is not yet
36
+ // `.readyToPlay` here. A tap is wanted, so play() must hold the leg at
37
+ // rate 0 rather than start it rendering before the mix is installed.
38
+ engine.play()
39
+ XCTAssertEqual(
40
+ engine.timeControlStatus, .paused,
41
+ "a tap-wanting leading leg must be held at rate 0 until its mix is "
42
+ + "installed, not started rendering by play()"
43
+ )
44
+
45
+ // The `.readyToPlay` observer loads tracks, installs the tap-bearing mix,
46
+ // then resumes the held leg — it must reach `.playing`, not strand.
47
+ try await waitUntil("the held leading leg resumes playing") {
48
+ engine.timeControlStatus == .playing
49
+ }
50
+ let leading = try XCTUnwrap(engine.currentMediaItem)
51
+ let installed = leading.audioMix?.inputParameters.first
52
+ as? AVMutableAudioMixInputParameters
53
+ XCTAssertTrue(
54
+ installed === provider.issued["leading"],
55
+ "the leading leg must carry the provider's tap-bearing mix once playing; "
56
+ + "a nil or foreign mix means it resumed untapped"
57
+ )
58
+ }
59
+
60
+ // MARK: - helpers
61
+
62
+ /// Built without `tracks` in `automaticallyLoadedAssetKeys`, so the asset's
63
+ /// tracks key is unloaded until something explicitly loads it — the state a
64
+ /// remote item is in when the arm chain picks it up.
65
+ private func makeItem(url: URL, id: String) -> AVPlayerItem {
66
+ let item = AVPlayerItem(asset: AVURLAsset(url: url))
67
+ item.queueItemId = id
68
+ return item
69
+ }
70
+
71
+ private func waitUntil(
72
+ _ what: String,
73
+ timeout: TimeInterval = 20,
74
+ _ condition: () -> Bool
75
+ ) async throws {
76
+ let deadline = Date().addingTimeInterval(timeout)
77
+ while Date() < deadline {
78
+ if condition() { return }
79
+ try await Task.sleep(nanoseconds: 20_000_000)
80
+ }
81
+ XCTFail("timed out waiting for \(what)")
82
+ }
83
+
84
+ private func requireFixture(name: String, ext: String) throws -> URL {
85
+ let bundle = Bundle(for: type(of: self))
86
+ if let url = bundle.url(forResource: name, withExtension: ext) {
87
+ return url
88
+ }
89
+ let candidates = bundle.urls(forResourcesWithExtension: ext, subdirectory: nil) ?? []
90
+ if let match = candidates.first(where: { $0.deletingPathExtension().lastPathComponent == name }) {
91
+ return match
92
+ }
93
+ // Fixtures are bundled via `test_spec.resources` in QueuePlayer.podspec.
94
+ // A miss means the test-harness pod install regressed — that's a real
95
+ // infrastructure failure, not a conditionally-inapplicable test.
96
+ return try XCTUnwrap(
97
+ nil as URL?,
98
+ "fixture \(name).\(ext) not bundled — pod install regression in tests-harness"
99
+ )
100
+ }
101
+ }
102
+
103
+ /// Like `AudioTapProvider`, reports a live tap consumer so the crossfade engine
104
+ /// holds a not-yet-ready leading leg for the tap rather than resuming it
105
+ /// immediately. Issues a real tap-bearing mix once tracks load.
106
+ private final class TapWantingTracksGatedMixProvider: AudioMixProvider {
107
+ private(set) var issued: [String: AVMutableAudioMixInputParameters] = [:]
108
+
109
+ func anyConsumerWantsTap() -> Bool { true }
110
+
111
+ func audioMix(for item: AVPlayerItem) -> AVAudioMix? {
112
+ guard case .loaded(let tracks) = item.asset.status(of: .tracks),
113
+ let track = tracks.first(where: { $0.mediaType == .audio }) else {
114
+ return nil
115
+ }
116
+ let params = AVMutableAudioMixInputParameters(track: track)
117
+ let mix = AVMutableAudioMix()
118
+ mix.inputParameters = [params]
119
+ issued[item.queueItemId ?? ""] = params
120
+ return mix
121
+ }
122
+ }
@@ -0,0 +1,164 @@
1
+ import AVFoundation
2
+ import XCTest
3
+ @testable import QueuePlayer
4
+
5
+ /// Coverage for `CrossfadeEngine`'s construction, delegate assignment and
6
+ /// index contract.
7
+ ///
8
+ /// The index contract is the load-bearing part: the engine indexes within
9
+ /// whatever slice it was handed, so its indices are NOT queue positions.
10
+ /// Callers translate by `queueItemId` identity. These tests cover the engine
11
+ /// side of that contract — slice-relative indexing, ordered identity-bearing
12
+ /// `allMediaItems`, and identity surviving the seek cross-attach. The
13
+ /// translation itself lives in `TrackPlayer.engineIndex(forQueuePosition:)`,
14
+ /// which needs a configured engine and is covered by the
15
+ /// `non-zero-start-index-seeks-within-the-started-track` demo step.
16
+ ///
17
+ /// The negative-duration constructor guard at
18
+ /// `CrossfadeEngine.init(crossfadeDurationSeconds:)` is not testable — a
19
+ /// `precondition` trap is non-recoverable from XCTest. The load-bearing
20
+ /// guard for runtime callers is the clamp inside
21
+ /// `PlaybackModeStateMachine`; the constructor check is a defensive belt.
22
+ final class CrossfadeEngineTests: XCTestCase {
23
+
24
+ func testConstructorAcceptsNonNegativeCrossfadeDuration() {
25
+ let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
26
+ XCTAssertEqual(engine.currentMediaItemIndex, -1, "a fresh engine holds no queue")
27
+ XCTAssertTrue(engine.allMediaItems.isEmpty)
28
+ }
29
+
30
+ func testDelegateIsWirable() {
31
+ let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
32
+ let delegate = NoopDelegate()
33
+ engine.delegate = delegate
34
+ XCTAssertTrue(engine.delegate === delegate)
35
+ }
36
+
37
+ // MARK: - Index contract
38
+
39
+ /// Items carrying the lib-generated identity the translation relies on.
40
+ private func makeItems(ids: [String]) -> [AVPlayerItem] {
41
+ ids.map { id in
42
+ let item = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/\(id).mp3"))
43
+ item.queueItemId = id
44
+ return item
45
+ }
46
+ }
47
+
48
+ func testEngineIndexesWithinItsOwnSliceNotTheQueuePosition() {
49
+ let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
50
+ // The slice a caller hands over when playback starts at queue position 5.
51
+ engine.setItems(makeItems(ids: ["q5", "q6", "q7"]), startIndex: 0, startPositionSeconds: 0)
52
+
53
+ // Zero, not 5 — the engine has no knowledge of the queue position the
54
+ // slice came from. Reading this as a queue position seats the wrong track.
55
+ XCTAssertEqual(engine.currentMediaItemIndex, 0)
56
+ XCTAssertEqual(engine.allMediaItems.count, 3)
57
+ }
58
+
59
+ /// `allMediaItems` exposes the installed slice in order, with identities
60
+ /// intact. Callers translate a queue position into an engine index through
61
+ /// this, so an engine that reordered or dropped identities would break the
62
+ /// translation silently.
63
+ func testAllMediaItemsPreservesSliceOrderAndIdentity() {
64
+ let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
65
+ engine.setItems(makeItems(ids: ["q5", "q6", "q7"]), startIndex: 0, startPositionSeconds: 0)
66
+
67
+ XCTAssertEqual(engine.allMediaItems.map(\.queueItemId), ["q5", "q6", "q7"])
68
+ }
69
+
70
+ /// The cross-attach inside `seek` rebuilds the target item; identity has to
71
+ /// survive it or the translation stops resolving after the first seek.
72
+ func testIdentitySurvivesTheSeekCrossAttach() {
73
+ let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
74
+ engine.setItems(makeItems(ids: ["q5", "q6", "q7"]), startIndex: 0, startPositionSeconds: 0)
75
+
76
+ engine.seek(toIndex: 1, position: 0)
77
+
78
+ XCTAssertEqual(engine.allMediaItems.map(\.queueItemId), ["q5", "q6", "q7"])
79
+ XCTAssertEqual(engine.currentMediaItem?.queueItemId, "q6")
80
+ }
81
+
82
+ /// The seek cross-attach rebuilds the target item; the read-through routing
83
+ /// stamp has to survive it too, or `getCurrentTrackSource()` reports a
84
+ /// cache-served track as `.streaming` (the gapless engine never rebuilds, so
85
+ /// it never loses the stamp).
86
+ func testRoutingStampSurvivesTheSeekCrossAttach() {
87
+ let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
88
+ let items = makeItems(ids: ["q5", "q6", "q7"])
89
+ items.forEach { $0.isRoutedThroughReadThroughServer = true }
90
+ engine.setItems(items, startIndex: 0, startPositionSeconds: 0)
91
+
92
+ engine.seek(toIndex: 1, position: 0)
93
+
94
+ XCTAssertEqual(
95
+ engine.allMediaItems.map(\.isRoutedThroughReadThroughServer),
96
+ [true, true, true],
97
+ "the rebuilt item must keep its read-through routing stamp")
98
+ }
99
+
100
+ func testSeekToIndexMovesWithinTheEngineSlice() {
101
+ let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
102
+ engine.setItems(makeItems(ids: ["q5", "q6", "q7"]), startIndex: 0, startPositionSeconds: 0)
103
+
104
+ engine.seek(toIndex: 2, position: 0)
105
+ XCTAssertEqual(engine.currentMediaItemIndex, 2)
106
+ XCTAssertEqual(engine.currentMediaItem?.queueItemId, "q7")
107
+ }
108
+
109
+ func testSeekOutsideTheSliceIsRejected() {
110
+ let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
111
+ engine.setItems(makeItems(ids: ["q5", "q6", "q7"]), startIndex: 0, startPositionSeconds: 0)
112
+
113
+ engine.seek(toIndex: 7, position: 0)
114
+ XCTAssertEqual(engine.currentMediaItemIndex, 0, "an out-of-slice index must not move the engine")
115
+ }
116
+
117
+ /// A missing local file's `AVURLAsset` load can stall in `.unknown` on
118
+ /// `AVPlayer`, so `.failed` never fires and the failure never reaches the
119
+ /// delegate. Seating one must surface `engineDidFailWithError` from the file's
120
+ /// non-existence directly, rather than wait on the load.
121
+ func testAMissingLocalFileSurfacesAFailure() {
122
+ let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
123
+ let delegate = FailCapturingDelegate()
124
+ engine.delegate = delegate
125
+ let missing = URL(fileURLWithPath: "/tmp/rnqp-missing-\(UUID().uuidString).mp3")
126
+ let item = AVPlayerItem(url: missing)
127
+ item.queueItemId = "missing"
128
+ item.sourceURL = missing.absoluteString
129
+
130
+ let fired = expectation(description: "a missing local file surfaces a failure")
131
+ delegate.onFail = { error in
132
+ let ns = error as NSError
133
+ if ns.domain == NSURLErrorDomain, ns.code == NSURLErrorFileDoesNotExist { fired.fulfill() }
134
+ }
135
+ engine.setItems([item], startIndex: 0, startPositionSeconds: 0)
136
+
137
+ wait(for: [fired], timeout: 3.0)
138
+ }
139
+ }
140
+
141
+ private final class NoopDelegate: PlaybackEngineDelegate {
142
+ func engineStateMaybeChanged(_ engine: PlaybackEngine, reason: StateChangeReason?) {}
143
+ func engineDidTransitionTrack(_ engine: PlaybackEngine) {}
144
+ func engineActiveItemFormatChanged(_ engine: PlaybackEngine) {}
145
+ func engineDidFailWithError(
146
+ _ engine: PlaybackEngine, error: Error, item: AVPlayerItem?) {}
147
+ func engineDidPlayItemToEnd(_ engine: PlaybackEngine) {}
148
+ func enginePlaybackEnded(_ engine: PlaybackEngine) {}
149
+ func engineCrossfadeDidBegin(_ engine: PlaybackEngine) {}
150
+ func engineCrossfadeDidCancel(_ engine: PlaybackEngine) {}
151
+ }
152
+
153
+ private final class FailCapturingDelegate: PlaybackEngineDelegate {
154
+ var onFail: ((Error) -> Void)?
155
+ func engineStateMaybeChanged(_ engine: PlaybackEngine, reason: StateChangeReason?) {}
156
+ func engineDidTransitionTrack(_ engine: PlaybackEngine) {}
157
+ func engineActiveItemFormatChanged(_ engine: PlaybackEngine) {}
158
+ func engineDidFailWithError(
159
+ _ engine: PlaybackEngine, error: Error, item: AVPlayerItem?) { onFail?(error) }
160
+ func engineDidPlayItemToEnd(_ engine: PlaybackEngine) {}
161
+ func enginePlaybackEnded(_ engine: PlaybackEngine) {}
162
+ func engineCrossfadeDidBegin(_ engine: PlaybackEngine) {}
163
+ func engineCrossfadeDidCancel(_ engine: PlaybackEngine) {}
164
+ }
@@ -0,0 +1,66 @@
1
+ import AVFoundation
2
+ import XCTest
3
+ @testable import QueuePlayer
4
+
5
+ /// The rendering item's audio mix is rebuilt only when a ramp was scheduled.
6
+ ///
7
+ /// Rebuilding it means a fresh processing tap on an item already in flight,
8
+ /// which AVPlayer silently drops — so a cancel that had no ramp to unwind must
9
+ /// leave the mix it is already rendering alone.
10
+ final class CrossfadeMixReapplyTests: XCTestCase {
11
+
12
+ func testRemovingItemsDoesNotRebuildTheRenderingItemsMix() {
13
+ let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
14
+ let provider = CountingMixProvider()
15
+ engine.setAudioMixProvider(provider)
16
+ engine.setItems(items(count: 4), startIndex: 0, startPositionSeconds: 0)
17
+ let afterInstall = provider.buildCount
18
+
19
+ // The removal loop a rebuild runs when it replaces a divergent tail.
20
+ for item in engine.allMediaItems.suffix(3) {
21
+ engine.remove(item)
22
+ }
23
+
24
+ XCTAssertEqual(
25
+ provider.buildCount, afterInstall,
26
+ "removing queued items must not rebuild the mix of the item playing")
27
+ }
28
+
29
+ /// Arming happens from the boundary observer during playback, so the armed
30
+ /// half of this contract is covered by the crossfade demo suites rather than
31
+ /// here; what is reachable is that a cancel with no ramp to unwind leaves the
32
+ /// rendering item's mix as it is.
33
+ func testCancellingWithNothingArmedLeavesTheMixAlone() {
34
+ let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
35
+ let provider = CountingMixProvider()
36
+ engine.setAudioMixProvider(provider)
37
+ engine.setItems(items(count: 2), startIndex: 0, startPositionSeconds: 0)
38
+ let afterInstall = provider.buildCount
39
+
40
+ engine.cancelFade()
41
+
42
+ XCTAssertEqual(provider.buildCount, afterInstall)
43
+ }
44
+
45
+ private func items(count: Int) -> [AVPlayerItem] {
46
+ (0 ..< count).map { index in
47
+ let item = AVPlayerItem(
48
+ url: URL(string: "http://127.0.0.1:1/\(index).m4a")!)
49
+ item.queueItemId = "id-\(index)"
50
+ return item
51
+ }
52
+ }
53
+ }
54
+
55
+ /// Counts how many times a mix is built, which is what the dropped-tap hazard
56
+ /// is really about.
57
+ private final class CountingMixProvider: AudioMixProvider {
58
+ private(set) var buildCount = 0
59
+
60
+ func audioMix(for item: AVPlayerItem) -> AVAudioMix? {
61
+ buildCount += 1
62
+ return AVMutableAudioMix()
63
+ }
64
+
65
+ func noteActiveAsset(_ asset: AVAsset?) {}
66
+ }
@@ -0,0 +1,81 @@
1
+ import AVFoundation
2
+ import XCTest
3
+ @testable import QueuePlayer
4
+
5
+ /// `seek(toIndex:)` and `setItems` resume from the transport's intent, never
6
+ /// from a leg's rate or status.
7
+ ///
8
+ /// `replaceCurrentItem` drops the player it is called on to rate 0, so each of
9
+ /// these paths has to put the rate back. Reading a leg to decide whether to is
10
+ /// wrong in two states the engine is routinely in: mid-fade, where the
11
+ /// outgoing leading leg has already stopped while the incoming one plays, and
12
+ /// at the end of an item, where the leg sits at rate 0 with the transport
13
+ /// still wanting playback. Either reading leaves the queue silently paused.
14
+ ///
15
+ /// What is reachable here is the half that must NOT resume. The resuming half
16
+ /// needs a leg genuinely at rate 0 with playback still wanted, and only real
17
+ /// playback produces that: measured on the simulator, an item pointed at an
18
+ /// unreachable URL holds `timeControlStatus == .waitingToPlayAtSpecifiedRate`
19
+ /// indefinitely and never drops its rate, so every assertion built on one is
20
+ /// vacuous. The `consecutive-auto-advance` and `queue-end-then-set-queue` demo
21
+ /// suites cover both halves on both engines.
22
+ final class CrossfadePlaybackIntentTests: XCTestCase {
23
+
24
+ func testASeekAfterPauseStaysPaused() {
25
+ let engine = makeEngine(count: 3)
26
+ engine.play()
27
+ engine.pause()
28
+
29
+ engine.seek(toIndex: 1, position: 0)
30
+
31
+ XCTAssertEqual(
32
+ engine.timeControlStatus, .paused,
33
+ "a paused engine is not resumed by a seek")
34
+ }
35
+
36
+ func testAReinstallBeforeAnyPlayStaysPaused() {
37
+ let engine = makeEngine(count: 3)
38
+
39
+ engine.setItems(items(count: 3, from: 10), startIndex: 0, startPositionSeconds: 0)
40
+
41
+ XCTAssertEqual(
42
+ engine.timeControlStatus, .paused,
43
+ "an engine that was never told to play stays stopped")
44
+ }
45
+
46
+ func testASeekAfterStopStaysPaused() {
47
+ let engine = makeEngine(count: 3)
48
+ engine.play()
49
+ engine.stop()
50
+
51
+ engine.seek(toIndex: 2, position: 0)
52
+
53
+ XCTAssertEqual(engine.timeControlStatus, .paused)
54
+ }
55
+
56
+ // MARK: - Helpers
57
+
58
+ private func items(count: Int, from base: Int = 0) -> [AVPlayerItem] {
59
+ (0 ..< count).map { offset in
60
+ let index = base + offset
61
+ let item = AVPlayerItem(url: URL(string: "http://127.0.0.1:1/\(index).m4a")!)
62
+ item.queueItemId = "id-\(index)"
63
+ return item
64
+ }
65
+ }
66
+
67
+ private func makeEngine(count: Int) -> CrossfadeEngine {
68
+ let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
69
+ engine.setItems(items(count: count), startIndex: 0, startPositionSeconds: 0)
70
+ engines.append(engine)
71
+ return engine
72
+ }
73
+
74
+ private var engines: [CrossfadeEngine] = []
75
+
76
+ override func tearDown() {
77
+ engines.forEach { $0.release() }
78
+ engines.removeAll()
79
+ super.tearDown()
80
+ }
81
+ }
@@ -0,0 +1,91 @@
1
+ import AVFoundation
2
+ import XCTest
3
+ @testable import QueuePlayer
4
+
5
+ /// A playback-mode swap installs the window on the new engine and always
6
+ /// releases the one it replaces.
7
+ final class EngineSwapTests: XCTestCase {
8
+
9
+ private var player: TrackPlayer!
10
+
11
+ override func setUp() async throws {
12
+ try await super.setUp()
13
+ player = TrackPlayer()
14
+ }
15
+
16
+ override func tearDown() async throws {
17
+ if let player {
18
+ _ = try await player.destroy().await()
19
+ }
20
+ player = nil
21
+ try await super.tearDown()
22
+ }
23
+
24
+ /// Nothing at or after the current position can be built, so the new engine
25
+ /// is left empty — and the old one still goes.
26
+ func testTheOldEngineIsReleasedWhenNothingCanBeSeated() async throws {
27
+ let player = try XCTUnwrap(self.player)
28
+ let old = ActiveItemEngineStub()
29
+ await player.onPlayerQueue {
30
+ _ = player.queueState.replaceAll([PlayerFixtures.track(id: "t0", url: "")])
31
+ player.currentTrackIndex = 0
32
+ player.engine = old
33
+ }
34
+
35
+ await player.onPlayerQueue {
36
+ player.applyEngineSwapForMode(PlaybackMode(kind: .gapless, crossfadeDurationMs: nil))
37
+ }
38
+
39
+ XCTAssertTrue(old.released, "the replaced engine is released whatever was seated")
40
+ let installed = await player.onPlayerQueueValue { player.engine?.allMediaItems.count ?? -1 }
41
+ XCTAssertEqual(installed, 0, "an unbuildable current track seats nothing")
42
+ }
43
+
44
+ func testTheOldEngineIsReleasedOnAnOrdinarySwap() async throws {
45
+ let player = try XCTUnwrap(self.player)
46
+ let old = ActiveItemEngineStub()
47
+ await player.onPlayerQueue {
48
+ _ = player.queueState.replaceAll([
49
+ PlayerFixtures.track(id: "t0", url: "https://example.com/0.m4a"),
50
+ PlayerFixtures.track(id: "t1", url: "https://example.com/1.m4a"),
51
+ ])
52
+ player.currentTrackIndex = 0
53
+ player.engine = old
54
+ }
55
+
56
+ await player.onPlayerQueue {
57
+ player.applyEngineSwapForMode(PlaybackMode(kind: .gapless, crossfadeDurationMs: nil))
58
+ }
59
+
60
+ XCTAssertTrue(old.released)
61
+ let installed = await player.onPlayerQueueValue { player.engine?.allMediaItems.count ?? -1 }
62
+ XCTAssertEqual(installed, 2, "the window is installed on the new engine")
63
+ }
64
+
65
+ // MARK: - seatIndex
66
+
67
+ private func item(_ id: String) -> AVPlayerItem {
68
+ let item = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/\(id).mp3"))
69
+ item.queueItemId = id
70
+ return item
71
+ }
72
+
73
+ func testTheSeatIsTheCurrentItemWhenItBuilt() {
74
+ let items = [item("a"), item("b"), item("c")]
75
+ XCTAssertEqual(
76
+ TrackPlayer.seatIndex(in: items, sliceIds: ["a", "b", "c"], currentId: "b"), 1)
77
+ }
78
+
79
+ /// A current position whose item did not build seats on the next built
80
+ /// position forward, never on the trail behind it.
81
+ func testTheSeatMovesForwardPastAnUnbuiltCurrentItem() {
82
+ let items = [item("a"), item("c")]
83
+ XCTAssertEqual(
84
+ TrackPlayer.seatIndex(in: items, sliceIds: ["a", "b", "c"], currentId: "b"), 1)
85
+ }
86
+
87
+ func testThereIsNoSeatWhenNothingAheadBuilt() {
88
+ let items = [item("a")]
89
+ XCTAssertNil(TrackPlayer.seatIndex(in: items, sliceIds: ["a", "b", "c"], currentId: "b"))
90
+ }
91
+ }