react-native-queue-player 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/build.gradle +7 -0
  4. package/android/consumer-rules.pro +72 -3
  5. package/android/src/main/cpp/airplay2_control.c +4 -4
  6. package/android/src/main/cpp/airplay2_control.h +4 -4
  7. package/android/src/main/cpp/airplay2_jni.cpp +26 -22
  8. package/android/src/main/cpp/airplay2_pair.c +7 -7
  9. package/android/src/main/cpp/airplay2_pair.h +2 -2
  10. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  11. package/android/src/main/cpp/airplay2_session.c +2 -2
  12. package/android/src/main/cpp/airplay_jni.cpp +20 -72
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
  61. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  62. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  63. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  64. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  65. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
  114. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
  115. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  116. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  117. package/app.plugin.js +17 -1
  118. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  119. package/ios/AVQueueBuilder.swift +120 -85
  120. package/ios/ArtworkLoader.swift +0 -7
  121. package/ios/ArtworkResolver.swift +22 -4
  122. package/ios/AssetReplayGainReader.swift +118 -0
  123. package/ios/AudioSession.swift +67 -16
  124. package/ios/AudioTapProvider.swift +284 -149
  125. package/ios/CarPlayBridge.swift +0 -12
  126. package/ios/CarPlayCoordinator.swift +18 -2
  127. package/ios/CarPlaySceneDelegate.swift +2 -2
  128. package/ios/Cast/AirPlayRouteState.swift +9 -8
  129. package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
  130. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  131. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  132. package/ios/Cast/Core/CastSession.swift +11 -0
  133. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  134. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  135. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  136. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  137. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  138. package/ios/CastManager.swift +1 -1
  139. package/ios/CrossfadeEngine.swift +660 -226
  140. package/ios/EQTap.swift +20 -17
  141. package/ios/Equalizer.swift +1 -1
  142. package/ios/FLACStreamInfo.swift +65 -0
  143. package/ios/GaplessEngine.swift +57 -30
  144. package/ios/InputGuards.swift +35 -5
  145. package/ios/LookaheadCache.swift +202 -58
  146. package/ios/LookaheadCachePrefetcher.swift +48 -36
  147. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  148. package/ios/NetworkRecoveryPolicy.swift +32 -0
  149. package/ios/NowPlayingFormatExtractor.swift +25 -18
  150. package/ios/NowPlayingInfo.swift +84 -48
  151. package/ios/OutputRouteMonitor.swift +16 -3
  152. package/ios/PendingBrowseRequests.swift +1 -1
  153. package/ios/PlaceholderArtwork.swift +9 -5
  154. package/ios/PlaybackEngine.swift +165 -76
  155. package/ios/PlaybackErrorMapping.swift +3 -3
  156. package/ios/PlaybackModeStateMachine.swift +2 -2
  157. package/ios/PlaybackNetworkMonitor.swift +73 -0
  158. package/ios/PlayerStateDerivation.swift +6 -6
  159. package/ios/QueueMutationArithmetic.swift +25 -0
  160. package/ios/QueueSkipArithmetic.swift +5 -3
  161. package/ios/QueueWindowArithmetic.swift +152 -0
  162. package/ios/ReadThroughServer.swift +283 -0
  163. package/ios/RemoteCommands.swift +44 -4
  164. package/ios/ReplayGainData.swift +22 -3
  165. package/ios/ReplayGainExtractor.swift +70 -27
  166. package/ios/Siri/VoiceDonation.swift +14 -9
  167. package/ios/StreamingBitrateProbe.swift +22 -68
  168. package/ios/SuppliedReplayGain.swift +83 -0
  169. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  170. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  171. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  172. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  173. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  174. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  175. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  176. package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
  177. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  178. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  179. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  180. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  181. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  182. package/ios/Tests/EngineSwapTests.swift +91 -0
  183. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  184. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  185. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  186. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  187. package/ios/Tests/InputGuardsTests.swift +41 -0
  188. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  189. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  190. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  191. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  192. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  193. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  194. package/ios/Tests/MutationDeferralTests.swift +220 -0
  195. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  196. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  197. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  198. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  199. package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
  200. package/ios/Tests/PlayerFixtures.swift +48 -0
  201. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  202. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  203. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  204. package/ios/Tests/QueueStateTests.swift +3 -1
  205. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  206. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  207. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  208. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  209. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  210. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  211. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  212. package/ios/Tests/RetryRecoveryTests.swift +303 -0
  213. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  214. package/ios/Tests/SkipIndexTests.swift +32 -2
  215. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  216. package/ios/Tests/StallRecoveryTests.swift +97 -0
  217. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  218. package/ios/Tests/TopUpWindowGateTests.swift +394 -0
  219. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  220. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  221. package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
  222. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  223. package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
  224. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  225. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  226. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  227. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  228. package/ios/TrackPlayer+Automotive.swift +101 -0
  229. package/ios/TrackPlayer+Cache.swift +232 -0
  230. package/ios/TrackPlayer+Config.swift +291 -0
  231. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  232. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  233. package/ios/TrackPlayer+EventsDispatch.swift +968 -0
  234. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  235. package/ios/TrackPlayer+Lifecycle.swift +930 -0
  236. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  237. package/ios/TrackPlayer+Queue.swift +940 -0
  238. package/ios/TrackPlayer+Recovery.swift +196 -0
  239. package/ios/TrackPlayer+Skip.swift +455 -0
  240. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  241. package/ios/TrackPlayer+State.swift +108 -0
  242. package/ios/TrackPlayer+Threading.swift +135 -0
  243. package/ios/TrackPlayer+Transport.swift +267 -0
  244. package/ios/TrackPlayer+Window.swift +195 -0
  245. package/ios/TrackPlayer.swift +305 -4610
  246. package/ios/Visualizer.swift +6 -5
  247. package/ios/tests-harness/Podfile +1 -1
  248. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  249. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  250. package/lib/module/hooks/useActiveTrack.js +29 -22
  251. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  252. package/lib/module/hooks/useCast.js +8 -26
  253. package/lib/module/hooks/useCast.js.map +1 -1
  254. package/lib/module/hooks/useEqualizer.js +19 -12
  255. package/lib/module/hooks/useEqualizer.js.map +1 -1
  256. package/lib/module/hooks/useLookaheadCache.js +3 -7
  257. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  258. package/lib/module/hooks/useQueue.js +66 -19
  259. package/lib/module/hooks/useQueue.js.map +1 -1
  260. package/lib/module/index.js +6 -3
  261. package/lib/module/index.js.map +1 -1
  262. package/lib/module/queueDelta.js +41 -0
  263. package/lib/module/queueDelta.js.map +1 -0
  264. package/lib/module/types.js +28 -5
  265. package/lib/module/types.js.map +1 -1
  266. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  267. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  269. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useCast.d.ts +6 -1
  271. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  272. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  273. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  274. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  275. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  276. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  277. package/lib/typescript/index.d.ts +2 -1
  278. package/lib/typescript/index.d.ts.map +1 -1
  279. package/lib/typescript/queueDelta.d.ts +10 -0
  280. package/lib/typescript/queueDelta.d.ts.map +1 -0
  281. package/lib/typescript/types.d.ts +168 -12
  282. package/lib/typescript/types.d.ts.map +1 -1
  283. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  284. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  285. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  286. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  287. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  288. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  289. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  320. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  321. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  322. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  323. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  324. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  325. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  326. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  327. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  328. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  329. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  330. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  331. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  332. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  333. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  334. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  335. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  336. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  337. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  338. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  339. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  340. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  341. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  342. package/package.json +8 -8
  343. package/src/TrackPlayer.nitro.ts +53 -27
  344. package/src/hooks/useActiveTrack.ts +29 -22
  345. package/src/hooks/useCast.ts +14 -15
  346. package/src/hooks/useEqualizer.ts +19 -12
  347. package/src/hooks/useLookaheadCache.ts +3 -7
  348. package/src/hooks/useQueue.ts +66 -17
  349. package/src/index.ts +13 -3
  350. package/src/queueDelta.ts +41 -0
  351. package/src/types.ts +169 -12
  352. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  353. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
  354. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  355. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  356. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  357. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  358. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  359. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  360. package/ios/MetadataReader.swift +0 -555
  361. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  362. package/ios/Tests/MetadataReaderTests.swift +0 -603
  363. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  364. package/ios/Tests/VoiceDonationTests.swift +0 -26
  365. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  366. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  367. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -0,0 +1,78 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// `getState()` reports the receiver while a cast session owns playback. The
5
+ /// local engine is parked and paused for the session's duration, so reading it
6
+ /// would describe nothing the user can hear.
7
+ final class TrackPlayerCastStateTests: XCTestCase {
8
+
9
+ private let router = PlaybackStateRouter.shared
10
+
11
+ override func setUp() {
12
+ super.setUp()
13
+ router._resetForTesting()
14
+ }
15
+
16
+ override func tearDown() {
17
+ router._resetForTesting()
18
+ super.tearDown()
19
+ }
20
+
21
+ /// Register a session that has already reported `state` to the receiver.
22
+ private func activateReporting(_ state: RemotePlaybackStateKind) throws {
23
+ let session = ControllableRemoteSession()
24
+ session.fireState(state)
25
+ try router.setActive(session)
26
+ }
27
+
28
+ func testReportsReceiverPlayingDuringCastSession() throws {
29
+ try activateReporting(.playing)
30
+ XCTAssertEqual(try TrackPlayer().getState(), .playing)
31
+ }
32
+
33
+ func testReportsReceiverPausedDuringCastSession() throws {
34
+ try activateReporting(.paused)
35
+ XCTAssertEqual(try TrackPlayer().getState(), .paused)
36
+ }
37
+
38
+ func testMapsBufferingReceiverToBuffering() throws {
39
+ try activateReporting(.buffering)
40
+ XCTAssertEqual(try TrackPlayer().getState(), .buffering)
41
+ }
42
+
43
+ func testMapsEndedReceiverToEnded() throws {
44
+ try activateReporting(.ended)
45
+ XCTAssertEqual(try TrackPlayer().getState(), .ended)
46
+ }
47
+
48
+ func testReportsIdleReceiverOnceItHasReported() throws {
49
+ // A receiver that has spoken stays authoritative: a later idle means it
50
+ // genuinely has nothing loaded, not that it is still activating.
51
+ let session = ControllableRemoteSession()
52
+ session.fireState(.playing)
53
+ session.fireState(.idle)
54
+ try router.setActive(session)
55
+
56
+ XCTAssertEqual(try TrackPlayer().getState(), PlayerState.none)
57
+ }
58
+
59
+ func testReadsLocalEngineUntilReceiverReports() throws {
60
+ // Session activating: the receiver holds a state it has not reported, so
61
+ // the local engine answers. Distinguishable from the receiver's `.playing`
62
+ // precisely because no engine is configured here.
63
+ let session = ControllableRemoteSession()
64
+ session.lastState = .playing
65
+ try router.setActive(session)
66
+
67
+ XCTAssertFalse(session.hasReportedState)
68
+ XCTAssertEqual(try TrackPlayer().getState(), PlayerState.none)
69
+ }
70
+
71
+ func testReadsLocalEngineWithNoCastSession() throws {
72
+ // An unregistered session must not be consulted, however it reports.
73
+ let session = ControllableRemoteSession()
74
+ session.fireState(.playing)
75
+
76
+ XCTAssertEqual(try TrackPlayer().getState(), PlayerState.none)
77
+ }
78
+ }
@@ -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,165 @@
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: - Helpers
138
+
139
+ private func installCrossfade() async throws {
140
+ _ = try await player.setPlaybackMode(
141
+ mode: PlaybackMode(kind: .crossfade, crossfadeDurationMs: 3000)).await()
142
+ }
143
+
144
+ private func seedQueue(
145
+ count: Int, at index: Int, repeatMode: RepeatMode, lastTrackURL: String? = nil
146
+ ) async throws {
147
+ var tracks = try PlayerFixtures.localTracks(count: count, for: self)
148
+ if let lastTrackURL {
149
+ tracks[count - 1] = PlayerFixtures.track(id: "unbuildable", url: lastTrackURL)
150
+ }
151
+ _ = try await player.setQueue(tracks: tracks, startAtIndex: 0).await()
152
+ _ = try await player.setRepeatMode(mode: repeatMode).await()
153
+ await onQueue { $0.currentTrackIndex = index }
154
+ }
155
+
156
+ private func onQueue(_ body: @escaping (TrackPlayer) -> Void) async {
157
+ guard let player else { return }
158
+ await player.onPlayerQueue { body(player) }
159
+ }
160
+
161
+ private func read<T>(_ body: @escaping (TrackPlayer) -> T) async throws -> T {
162
+ let player = try XCTUnwrap(self.player)
163
+ return await player.onPlayerQueueValue { body(player) }
164
+ }
165
+ }
@@ -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
+ }
@@ -1,14 +1,16 @@
1
1
  import XCTest
2
2
  @testable import QueuePlayer
3
3
 
4
- /// `TrackPlayer.classifyTrackSource(url:isUrlCached:)` decides how the active
5
- /// track is being played from lib-observable facts only: the URL scheme plus a
6
- /// cache-hit probe. Pure + static, so it's exercised directly without a live
7
- /// engine. Mirrors the Android source-classifier coverage.
4
+ /// `TrackPlayer.classifyTrackSource` decides how the active track is being
5
+ /// played from lib-observable facts only: the URL scheme, whether the item is
6
+ /// routed through the read-through server, and a cache-hit probe. Pure +
7
+ /// static, so it's exercised directly without a live engine.
8
8
  final class TrackSourceClassifierTests: XCTestCase {
9
9
 
10
- private func classify(_ url: String?, cached: Bool = false) -> TrackSource? {
11
- TrackPlayer.classifyTrackSource(url: url) { _ in cached }
10
+ private func classify(
11
+ _ url: String?, routed: Bool = true, cached: Bool = false
12
+ ) -> TrackSource? {
13
+ TrackPlayer.classifyTrackSource(url: url, isRouted: routed) { _ in cached }
12
14
  }
13
15
 
14
16
  func testNilUrlIsUnclassified() {
@@ -40,11 +42,35 @@ final class TrackSourceClassifierTests: XCTestCase {
40
42
 
41
43
  func testLocalFileNeverConsultsTheCacheProbe() {
42
44
  var probed = false
43
- let source = TrackPlayer.classifyTrackSource(url: "file:///a.m4a") { _ in
45
+ let source = TrackPlayer.classifyTrackSource(url: "file:///a.m4a", isRouted: true) { _ in
44
46
  probed = true
45
47
  return true
46
48
  }
47
49
  XCTAssertEqual(source, .local)
48
50
  XCTAssertFalse(probed, "a file:// URL is local without a cache lookup")
49
51
  }
52
+
53
+ /// Cached on disk is not the same as played from disk. Only an item bound to
54
+ /// the read-through server reads the cache, so a complete file the player is
55
+ /// not routed through is still being streamed — reporting otherwise is what
56
+ /// made this field claim `cached` while every byte came from the origin.
57
+ func testACachedTrackThePlayerIsNotRoutedThroughIsStreaming() {
58
+ XCTAssertEqual(
59
+ classify("https://example.com/a.mp3", routed: false, cached: true), .streaming)
60
+ }
61
+
62
+ func testARoutedTrackTheCacheHoldsInFullIsCached() {
63
+ XCTAssertEqual(
64
+ classify("https://example.com/a.mp3", routed: true, cached: true), .cached)
65
+ }
66
+
67
+ func testARoutedTrackTheCacheDoesNotHoldIsStreaming() {
68
+ XCTAssertEqual(
69
+ classify("https://example.com/a.mp3", routed: true, cached: false), .streaming)
70
+ }
71
+
72
+ /// A local file is local whatever the server and cache report.
73
+ func testAFileTrackIsLocalRegardlessOfRouting() {
74
+ XCTAssertEqual(classify("file:///a.m4a", routed: false, cached: true), .local)
75
+ }
50
76
  }
@@ -0,0 +1,38 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// Siri vocabulary donation is off unless the consumer opts in, and the
5
+ /// opt-in is fixed once taken.
6
+ ///
7
+ /// The flag stands for the `com.apple.developer.siri` entitlement, which
8
+ /// `INVocabulary` checks at runtime and which iOS gives a process no way to
9
+ /// read. An entitlement is decided when the app is signed, so a value that
10
+ /// changed later could not describe anything — and re-reading one would let a
11
+ /// consumer re-arm a path that terminates a host without the capability.
12
+ final class VoiceVocabularyOptInTests: XCTestCase {
13
+
14
+ func testDonationIsOffUntilTheConsumerOptsIn() {
15
+ let player = TrackPlayer()
16
+ XCTAssertFalse(
17
+ player.voiceVocabularyDonationEnabled,
18
+ "an untouched player has not been given the entitlement to assert")
19
+ }
20
+
21
+ func testTheFirstOptInIsTheOneThatCounts() {
22
+ let player = TrackPlayer()
23
+ player.pinVoiceVocabularyDonation(true)
24
+ player.pinVoiceVocabularyDonation(false)
25
+ XCTAssertTrue(
26
+ player.voiceVocabularyDonationEnabled,
27
+ "a later value does not unset an opt-in already taken")
28
+ }
29
+
30
+ func testDecliningIsAlsoFixed() {
31
+ let player = TrackPlayer()
32
+ player.pinVoiceVocabularyDonation(false)
33
+ player.pinVoiceVocabularyDonation(true)
34
+ XCTAssertFalse(
35
+ player.voiceVocabularyDonationEnabled,
36
+ "a later value does not turn donation on after it was declined")
37
+ }
38
+ }