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,128 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// What the cache will and will not believe is a track, judged from a
5
+ /// response's declared type and the first bytes of its body.
6
+ final class MediaResponseCheckTests: XCTestCase {
7
+
8
+ private let xmlEnvelope = Data("""
9
+ <api-response status="failed"><error code="70"/></api-response>
10
+ """.utf8)
11
+ private let mp3 = Data([0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00]) // "ID3"
12
+
13
+ private func rejection(
14
+ _ contentType: String?, _ body: Data, whole: Bool = true
15
+ ) -> MediaResponseCheck.Rejection? {
16
+ MediaResponseCheck.rejection(
17
+ contentType: contentType, head: body, headIsStartOfResource: whole)
18
+ }
19
+
20
+ // MARK: - A declared document is refused whatever its body
21
+
22
+ func testMarkupAndJsonContentTypesAreRefused() {
23
+ for type in [
24
+ "application/xml", "text/xml", "text/html",
25
+ "application/xhtml+xml", "application/json", "text/json",
26
+ ] {
27
+ XCTAssertEqual(
28
+ rejection(type, xmlEnvelope), .markupContentType(type),
29
+ "\(type) is a document type")
30
+ }
31
+ }
32
+
33
+ func testTheDeclaredTypeIsReadWithoutItsParameters() {
34
+ XCTAssertEqual(
35
+ rejection("application/xml; charset=utf-8", xmlEnvelope),
36
+ .markupContentType("application/xml"))
37
+ XCTAssertEqual(rejection("APPLICATION/XML", xmlEnvelope), .markupContentType("application/xml"))
38
+ }
39
+
40
+ /// A ranged request answers from an arbitrary offset, so only the declared
41
+ /// type can be judged — but that much still holds.
42
+ func testADeclaredDocumentIsRefusedOnAPartialBodyToo() {
43
+ XCTAssertEqual(
44
+ rejection("application/xml", Data([0x3C, 0x00]), whole: false),
45
+ .markupContentType("application/xml"))
46
+ }
47
+
48
+ func testAPartialBodyIsNotJudgedByItsLeadingBytes() {
49
+ // The same markup, under a type that is not itself a document, so only the
50
+ // leading-byte rule can decide: refused as a whole resource, accepted as a
51
+ // range, which is what separates the two inputs.
52
+ let markup = Data("<api-response/>".utf8)
53
+ XCTAssertEqual(
54
+ rejection("application/octet-stream", markup),
55
+ .markupBody)
56
+ XCTAssertNil(
57
+ rejection("application/octet-stream", markup, whole: false),
58
+ "a body that begins part way into a file says nothing about its first bytes")
59
+ }
60
+
61
+ // MARK: - Plain text has to prove itself
62
+
63
+ func testTextWithoutAContainerSignatureIsRefused() {
64
+ XCTAssertEqual(
65
+ rejection("text/plain", Data("not found".utf8)),
66
+ .textWithoutMediaSignature("text/plain"))
67
+ }
68
+
69
+ func testTextCarryingAContainerSignatureIsAccepted() {
70
+ XCTAssertNil(
71
+ rejection("text/plain", mp3),
72
+ "a server mislabelling audio as text does not make it a document")
73
+ }
74
+
75
+ func testAPlaylistDeclaredAsTextIsAccepted() {
76
+ XCTAssertNil(rejection("text/plain", Data("#EXTM3U\n#EXT-X-VERSION:3".utf8)))
77
+ }
78
+
79
+ // MARK: - An unusable declared type falls to the body
80
+
81
+ func testAMarkupBodyIsRefusedWhenNothingUsableIsDeclared() {
82
+ XCTAssertEqual(rejection(nil, xmlEnvelope), .markupBody)
83
+ XCTAssertEqual(rejection("application/octet-stream", xmlEnvelope), .markupBody)
84
+ XCTAssertEqual(rejection(nil, Data("<!DOCTYPE html><html>".utf8)), .markupBody)
85
+ XCTAssertEqual(rejection(nil, Data("{\"error\":\"not found\"}".utf8)), .markupBody)
86
+ }
87
+
88
+ func testLeadingSpaceAndAByteOrderMarkDoNotHideAMarkupBody() {
89
+ XCTAssertEqual(rejection(nil, Data("\n <?xml version=\"1.0\"?>".utf8)), .markupBody)
90
+ XCTAssertEqual(
91
+ rejection(nil, Data([0xEF, 0xBB, 0xBF]) + Data("<error/>".utf8)), .markupBody)
92
+ // `FF FE` also satisfies the MPEG frame sync, so a UTF-16 document would
93
+ // read as audio if the mark were not dropped first.
94
+ XCTAssertEqual(
95
+ rejection(nil, Data([0xFF, 0xFE]) + Data("<e/>".utf8)), .markupBody)
96
+ }
97
+
98
+ // MARK: - Real media is accepted, however it is labelled
99
+
100
+ func testRecognisedContainersAreAccepted() {
101
+ let bodies: [String: [UInt8]] = [
102
+ "ID3": [0x49, 0x44, 0x33, 0x04, 0, 0, 0, 0],
103
+ "MPEG frame sync": [0xFF, 0xFB, 0x90, 0x00, 0, 0, 0, 0],
104
+ "ADTS AAC": [0xFF, 0xF1, 0x50, 0x80, 0, 0, 0, 0],
105
+ "FLAC": [0x66, 0x4C, 0x61, 0x43, 0, 0, 0, 0],
106
+ "Ogg": [0x4F, 0x67, 0x67, 0x53, 0, 0, 0, 0],
107
+ "WAV": [0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0],
108
+ "AIFF": [0x46, 0x4F, 0x52, 0x4D, 0, 0, 0, 0],
109
+ "CAF": [0x63, 0x61, 0x66, 0x66, 0, 0, 0, 0],
110
+ "MP4": [0, 0, 0, 0x20, 0x66, 0x74, 0x79, 0x70],
111
+ ]
112
+ for (name, bytes) in bodies {
113
+ XCTAssertNil(rejection("text/plain", Data(bytes)), "\(name) is media")
114
+ XCTAssertNil(rejection(nil, Data(bytes)), "\(name) is media")
115
+ }
116
+ }
117
+
118
+ func testAnUndeclaredOrOpaqueBinaryBodyIsAccepted() {
119
+ let binary = Data([0x00, 0x01, 0x02, 0x03, 0x04])
120
+ XCTAssertNil(rejection(nil, binary), "an unrecognised container is not a document")
121
+ XCTAssertNil(rejection("application/octet-stream", binary))
122
+ XCTAssertNil(rejection("audio/mpeg", binary))
123
+ }
124
+
125
+ func testAnEmptyBodyIsNotJudged() {
126
+ XCTAssertNil(rejection(nil, Data()))
127
+ }
128
+ }
@@ -0,0 +1,220 @@
1
+ import AVFoundation
2
+ import XCTest
3
+ @testable import QueuePlayer
4
+
5
+ /// A queue mutation that touches neither leg waits for a crossfade to settle
6
+ /// rather than cutting it short; one that touches a leg cannot wait.
7
+ final class MutationDeferralTests: 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
+ player = nil
18
+ try await super.tearDown()
19
+ }
20
+
21
+ func testAMutationClearOfBothLegsIsHeldBack() async throws {
22
+ try await seed(fading: true, attaching: [0, 1])
23
+
24
+ let deferred = try await onQueue { $0.deferEngineWork(touching: [7]) }
25
+
26
+ XCTAssertTrue(deferred)
27
+ let held = try await read { $0.engineReconcileDeferred }
28
+ XCTAssertTrue(held)
29
+ }
30
+
31
+ /// The prerolled standby leg counts: removing it out from under the arm
32
+ /// chain leaves the fade running into a track the queue no longer has.
33
+ func testAMutationTouchingTheStandbyLegIsNotHeldBack() async throws {
34
+ _ = try await seed(fading: true, attaching: [0, 1])
35
+
36
+ let deferred = try await onQueue { $0.deferEngineWork(touching: [1]) }
37
+
38
+ XCTAssertFalse(deferred)
39
+ let held = try await read { $0.engineReconcileDeferred }
40
+ XCTAssertFalse(held)
41
+ }
42
+
43
+ func testAMutationTouchingTheLeadingLegIsNotHeldBack() async throws {
44
+ _ = try await seed(fading: true, attaching: [0, 1])
45
+
46
+ let deferred = try await onQueue { $0.deferEngineWork(touching: [0]) }
47
+
48
+ XCTAssertFalse(deferred)
49
+ }
50
+
51
+ /// With no fade to protect there is nothing to wait for.
52
+ func testNothingIsHeldBackWhenNoFadeIsPending() async throws {
53
+ _ = try await seed(fading: false, attaching: [0])
54
+
55
+ let deferred = try await onQueue { $0.deferEngineWork(touching: [7]) }
56
+
57
+ XCTAssertFalse(deferred)
58
+ }
59
+
60
+ /// One flag, however many mutations: the reconcile re-derives what the
61
+ /// engine should hold from the model, so the order they arrived in and how
62
+ /// many there were do not matter.
63
+ func testSeveralHeldBackMutationsFlushAsOne() async throws {
64
+ let engine = try await seed(fading: true, attaching: [0, 1])
65
+ _ = try await onQueue { $0.deferEngineWork(touching: [6]) }
66
+ _ = try await onQueue { $0.deferEngineWork(touching: [7]) }
67
+ _ = try await onQueue { $0.deferEngineWork(touching: [8]) }
68
+
69
+ XCTAssertEqual(
70
+ engine.setItemsCount, 0, "nothing reaches the engine while the fade runs")
71
+
72
+ engine.fadePending = false
73
+ await onQueue { $0.flushDeferredEngineWork() }
74
+
75
+ let stillHeld = try await read { $0.engineReconcileDeferred }
76
+ XCTAssertFalse(stillHeld)
77
+ XCTAssertEqual(
78
+ engine.setItemsCount, 1, "the engine is reconciled once, not once per mutation")
79
+ }
80
+
81
+ func testFlushingWithNothingHeldBackLeavesTheEngineAlone() async throws {
82
+ let engine = try await seed(fading: false, attaching: [0])
83
+
84
+ await onQueue { $0.flushDeferredEngineWork() }
85
+
86
+ XCTAssertEqual(engine.setItemsCount, 0)
87
+ }
88
+
89
+ /// The new engine is installed from the queue model, which already carries
90
+ /// every mutation, so nothing is owed to it.
91
+ func testAnEngineSwapDropsWhatWasHeldBack() async throws {
92
+ _ = try await seed(fading: true, attaching: [0, 1])
93
+ _ = try await onQueue { $0.deferEngineWork(touching: [7]) }
94
+
95
+ await onQueue {
96
+ $0.applyEngineSwapForMode(PlaybackMode(kind: .gapless, crossfadeDurationMs: nil))
97
+ }
98
+
99
+ let held = try await read { $0.engineReconcileDeferred }
100
+ XCTAssertFalse(held)
101
+ }
102
+
103
+ // MARK: - The model stays authoritative
104
+
105
+ /// Holding the engine back does not hold the queue back: the model is
106
+ /// updated and the consumer is told at once either way.
107
+ func testADeferredRemoveStillChangesTheQueueAndReportsIt() async throws {
108
+ let engine = try await seed(fading: true, attaching: [0, 1])
109
+ var removeReports = 0
110
+ let unsubscribe = try player.onQueueChange { _, _, reason in
111
+ if reason == .remove { removeReports += 1 }
112
+ }
113
+ defer { unsubscribe() }
114
+
115
+ _ = try await player.removeFromQueue(indices: [7]).await()
116
+
117
+ let state = try await read { ($0.tracks.count, $0.queueItemIds.count) }
118
+ XCTAssertEqual(state.0, 11, "the queue model drops the track at once")
119
+ XCTAssertEqual(state.1, 11, "ids stay parallel to it")
120
+ XCTAssertEqual(removeReports, 1, "the consumer is told once")
121
+ XCTAssertTrue(engine.removed.isEmpty, "the engine is left alone until the fade settles")
122
+ let held = try await read { $0.engineReconcileDeferred }
123
+ XCTAssertTrue(held)
124
+ }
125
+
126
+ /// The same for a move, which reorders the model without touching a leg.
127
+ func testADeferredMoveStillReordersTheQueueAndReportsIt() async throws {
128
+ _ = try await seed(fading: true, attaching: [0, 1])
129
+ var moveReports = 0
130
+ let unsubscribe = try player.onQueueChange { _, _, reason in
131
+ if reason == .move { moveReports += 1 }
132
+ }
133
+ defer { unsubscribe() }
134
+ let idsBefore = try await read { $0.queueItemIds }
135
+
136
+ _ = try await player.moveInQueue(fromIndex: 8, toIndex: 6).await()
137
+
138
+ let idsAfter = try await read { $0.queueItemIds }
139
+ XCTAssertEqual(idsAfter[6], idsBefore[8], "the model is reordered at once")
140
+ XCTAssertEqual(moveReports, 1, "a deferred move is still reported")
141
+ }
142
+
143
+ // MARK: - Helpers
144
+
145
+ @discardableResult
146
+ private func seed(fading: Bool, attaching: [Int]) async throws -> DeferralEngineStub {
147
+ let player = try XCTUnwrap(self.player)
148
+ let engine = DeferralEngineStub()
149
+ await player.onPlayerQueue {
150
+ let ids = player.queueState.replaceAll(
151
+ (0 ..< 12).map {
152
+ PlayerFixtures.track(id: "t\($0)", url: "http://127.0.0.1:1/\($0).m4a")
153
+ })
154
+ engine.fadePending = fading
155
+ engine.attached = attaching.map { ids[$0] }
156
+ player.currentTrackIndex = 0
157
+ player.engine = engine
158
+ }
159
+ return engine
160
+ }
161
+
162
+ private func onQueue<T>(_ body: @escaping (TrackPlayer) -> T) async throws -> T {
163
+ let player = try XCTUnwrap(self.player)
164
+ return await player.onPlayerQueueValue { body(player) }
165
+ }
166
+
167
+ private func onQueue(_ body: @escaping (TrackPlayer) -> Void) async {
168
+ guard let player else { return }
169
+ await player.onPlayerQueue { body(player) }
170
+ }
171
+
172
+ private func read<T>(_ body: @escaping (TrackPlayer) -> T) async throws -> T {
173
+ try await onQueue(body)
174
+ }
175
+ }
176
+
177
+ /// Reports a fade and a chosen set of attached identities, and records what
178
+ /// the queue asks of it.
179
+ private final class DeferralEngineStub: PlaybackEngine {
180
+ var fadePending = false
181
+ var attached: [String] = []
182
+ private(set) var removed: [AVPlayerItem] = []
183
+ private(set) var setItemsCount = 0
184
+
185
+ var isFadePendingOrActive: Bool { fadePending }
186
+ var attachedQueueItemIds: [String] { attached }
187
+
188
+ func setItems(_ items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) {
189
+ setItemsCount += 1
190
+ }
191
+ func remove(_ item: AVPlayerItem) { removed.append(item) }
192
+ @discardableResult
193
+ func insertItems(_ items: [AVPlayerItem], after: AVPlayerItem?) -> Bool { true }
194
+ func setAudioMixProvider(_ provider: AudioMixProvider?) {}
195
+ func refreshAllItemMixes() {}
196
+ func release() {}
197
+ func removeAllItems() {}
198
+ func canInsert(_ item: AVPlayerItem, after: AVPlayerItem?) -> Bool { true }
199
+ func play() {}
200
+ func pause() {}
201
+ func stop() {}
202
+ func seek(toIndex index: Int, position: TimeInterval) {}
203
+ func skipToNext() {}
204
+ func skipToPrevious() {}
205
+ func setVolume(_ volume: Float) {}
206
+ func setPlaybackSpeed(_ speed: Float) {}
207
+ func seedPlaybackSpeed(_ speed: Float) {}
208
+ func setRepeatMode(_ mode: RepeatMode) {}
209
+ func setPitchCorrectionMode(_ mode: PitchCorrectionMode) {}
210
+
211
+ var currentMediaItemIndex: Int { 0 }
212
+ var currentPositionSeconds: TimeInterval { 0 }
213
+ var currentDurationSeconds: TimeInterval { 0 }
214
+ var bufferedPositionSeconds: TimeInterval { 0 }
215
+ var isPlaying: Bool { false }
216
+ var timeControlStatus: AVPlayer.TimeControlStatus { .paused }
217
+ var currentMediaItem: AVPlayerItem? { nil }
218
+ var allMediaItems: [AVPlayerItem] { [] }
219
+ var delegate: PlaybackEngineDelegate?
220
+ }
@@ -22,6 +22,9 @@ final class NowPlayingInfoTests: XCTestCase {
22
22
 
23
23
  override func tearDown() {
24
24
  nowPlaying.clear()
25
+ // Drain before dropping the subject: writes are enqueued, so one left in
26
+ // flight would land during the next test and clobber its seeded state.
27
+ _ = nowPlayingInfoAfterWritesLand()
25
28
  nowPlaying = nil
26
29
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
27
30
  super.tearDown()
@@ -29,31 +32,34 @@ final class NowPlayingInfoTests: XCTestCase {
29
32
 
30
33
  // MARK: - refreshAll
31
34
 
32
- func testRefreshAllClearsDictWhenSnapshotProviderReturnsNil() {
33
- MPNowPlayingInfoCenter.default().nowPlayingInfo = ["pre": "existing"]
34
- nowPlaying.snapshotProvider = nil
35
- nowPlaying.refreshAll()
36
- XCTAssertNil(MPNowPlayingInfoCenter.default().nowPlayingInfo)
35
+ func testRefreshAllPublishesTheSnapshotSuppliedToEachCall() {
36
+ let track = makeTrack(title: "T")
37
+ nowPlaying.refreshAll(
38
+ NowPlayingInfo.Snapshot(
39
+ track: track, elapsedSeconds: 5, durationSeconds: 60, rate: 1))
40
+ nowPlaying.refreshAll(
41
+ NowPlayingInfo.Snapshot(
42
+ track: track, elapsedSeconds: 40, durationSeconds: 90, rate: 0))
43
+ let info = nowPlayingInfoAfterWritesLand()
44
+ XCTAssertEqual(info?[MPNowPlayingInfoPropertyElapsedPlaybackTime] as? Double, 40)
45
+ XCTAssertEqual(info?[MPNowPlayingInfoPropertyPlaybackRate] as? Double, 0)
46
+ XCTAssertEqual(info?[MPMediaItemPropertyPlaybackDuration] as? Double, 90)
37
47
  }
38
48
 
39
49
  func testRefreshAllClearsDictWhenSnapshotHasNoTrack() {
40
50
  MPNowPlayingInfoCenter.default().nowPlayingInfo = ["pre": "existing"]
41
- nowPlaying.snapshotProvider = {
51
+ nowPlaying.refreshAll(
42
52
  NowPlayingInfo.Snapshot(
43
- track: nil, elapsedSeconds: 0, durationSeconds: 0, rate: 0)
44
- }
45
- nowPlaying.refreshAll()
46
- XCTAssertNil(MPNowPlayingInfoCenter.default().nowPlayingInfo)
53
+ track: nil, elapsedSeconds: 0, durationSeconds: 0, rate: 0))
54
+ XCTAssertNil(nowPlayingInfoAfterWritesLand())
47
55
  }
48
56
 
49
57
  func testRefreshAllPopulatesTitleArtistAlbum() {
50
58
  let track = makeTrack(title: "Song", artist: "Artist", album: "Album")
51
- nowPlaying.snapshotProvider = {
59
+ nowPlaying.refreshAll(
52
60
  NowPlayingInfo.Snapshot(
53
- track: track, elapsedSeconds: 12, durationSeconds: 180, rate: 1)
54
- }
55
- nowPlaying.refreshAll()
56
- let info = MPNowPlayingInfoCenter.default().nowPlayingInfo
61
+ track: track, elapsedSeconds: 12, durationSeconds: 180, rate: 1))
62
+ let info = nowPlayingInfoAfterWritesLand()
57
63
  XCTAssertEqual(info?[MPMediaItemPropertyTitle] as? String, "Song")
58
64
  XCTAssertEqual(info?[MPMediaItemPropertyArtist] as? String, "Artist")
59
65
  XCTAssertEqual(info?[MPMediaItemPropertyAlbumTitle] as? String, "Album")
@@ -61,12 +67,10 @@ final class NowPlayingInfoTests: XCTestCase {
61
67
 
62
68
  func testRefreshAllEmptyStringCoercesNilTitleArtistAlbum() {
63
69
  let track = makeTrack(title: nil, artist: nil, album: nil)
64
- nowPlaying.snapshotProvider = {
70
+ nowPlaying.refreshAll(
65
71
  NowPlayingInfo.Snapshot(
66
- track: track, elapsedSeconds: 0, durationSeconds: 60, rate: 0)
67
- }
68
- nowPlaying.refreshAll()
69
- let info = MPNowPlayingInfoCenter.default().nowPlayingInfo
72
+ track: track, elapsedSeconds: 0, durationSeconds: 60, rate: 0))
73
+ let info = nowPlayingInfoAfterWritesLand()
70
74
  XCTAssertEqual(info?[MPMediaItemPropertyTitle] as? String, "")
71
75
  XCTAssertEqual(info?[MPMediaItemPropertyArtist] as? String, "")
72
76
  XCTAssertEqual(info?[MPMediaItemPropertyAlbumTitle] as? String, "")
@@ -77,12 +81,10 @@ final class NowPlayingInfoTests: XCTestCase {
77
81
  let omitted: [Double] = [0, -1, .nan, .infinity, -.infinity]
78
82
  for d in omitted {
79
83
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
80
- nowPlaying.snapshotProvider = {
84
+ nowPlaying.refreshAll(
81
85
  NowPlayingInfo.Snapshot(
82
- track: track, elapsedSeconds: 0, durationSeconds: d, rate: 0)
83
- }
84
- nowPlaying.refreshAll()
85
- let info = MPNowPlayingInfoCenter.default().nowPlayingInfo
86
+ track: track, elapsedSeconds: 0, durationSeconds: d, rate: 0))
87
+ let info = nowPlayingInfoAfterWritesLand()
86
88
  XCTAssertNil(info?[MPMediaItemPropertyPlaybackDuration],
87
89
  "duration must be omitted for value \(d)")
88
90
  }
@@ -90,23 +92,19 @@ final class NowPlayingInfoTests: XCTestCase {
90
92
 
91
93
  func testRefreshAllSetsDurationWhenFinitePositive() {
92
94
  let track = makeTrack(title: "T")
93
- nowPlaying.snapshotProvider = {
95
+ nowPlaying.refreshAll(
94
96
  NowPlayingInfo.Snapshot(
95
- track: track, elapsedSeconds: 0, durationSeconds: 240, rate: 1)
96
- }
97
- nowPlaying.refreshAll()
98
- let info = MPNowPlayingInfoCenter.default().nowPlayingInfo
97
+ track: track, elapsedSeconds: 0, durationSeconds: 240, rate: 1))
98
+ let info = nowPlayingInfoAfterWritesLand()
99
99
  XCTAssertEqual(info?[MPMediaItemPropertyPlaybackDuration] as? Double, 240)
100
100
  }
101
101
 
102
102
  func testRefreshAllSetsAudioMediaType() {
103
103
  let track = makeTrack()
104
- nowPlaying.snapshotProvider = {
104
+ nowPlaying.refreshAll(
105
105
  NowPlayingInfo.Snapshot(
106
- track: track, elapsedSeconds: 0, durationSeconds: 0, rate: 0)
107
- }
108
- nowPlaying.refreshAll()
109
- let info = MPNowPlayingInfoCenter.default().nowPlayingInfo
106
+ track: track, elapsedSeconds: 0, durationSeconds: 0, rate: 0))
107
+ let info = nowPlayingInfoAfterWritesLand()
110
108
  XCTAssertEqual(
111
109
  info?[MPNowPlayingInfoPropertyMediaType] as? UInt,
112
110
  MPNowPlayingInfoMediaType.audio.rawValue
@@ -115,12 +113,10 @@ final class NowPlayingInfoTests: XCTestCase {
115
113
 
116
114
  func testRefreshAllPlacesArtworkPlaceholder() {
117
115
  let track = makeTrack(title: "T")
118
- nowPlaying.snapshotProvider = {
116
+ nowPlaying.refreshAll(
119
117
  NowPlayingInfo.Snapshot(
120
- track: track, elapsedSeconds: 0, durationSeconds: 60, rate: 0)
121
- }
122
- nowPlaying.refreshAll()
123
- let info = MPNowPlayingInfoCenter.default().nowPlayingInfo
118
+ track: track, elapsedSeconds: 0, durationSeconds: 60, rate: 0))
119
+ let info = nowPlayingInfoAfterWritesLand()
124
120
  XCTAssertNotNil(info?[MPMediaItemPropertyArtwork])
125
121
  XCTAssertTrue(info?[MPMediaItemPropertyArtwork] is MPMediaItemArtwork)
126
122
  }
@@ -129,12 +125,10 @@ final class NowPlayingInfoTests: XCTestCase {
129
125
  let track = makeTrack(title: "T")
130
126
  let bad: [Double] = [.nan, .infinity, -.infinity, -5]
131
127
  for e in bad {
132
- nowPlaying.snapshotProvider = {
128
+ nowPlaying.refreshAll(
133
129
  NowPlayingInfo.Snapshot(
134
- track: track, elapsedSeconds: e, durationSeconds: 60, rate: 1)
135
- }
136
- nowPlaying.refreshAll()
137
- let info = MPNowPlayingInfoCenter.default().nowPlayingInfo
130
+ track: track, elapsedSeconds: e, durationSeconds: 60, rate: 1))
131
+ let info = nowPlayingInfoAfterWritesLand()
138
132
  XCTAssertEqual(
139
133
  info?[MPNowPlayingInfoPropertyElapsedPlaybackTime] as? Double, 0,
140
134
  "elapsed must be 0 for value \(e)")
@@ -145,12 +139,10 @@ final class NowPlayingInfoTests: XCTestCase {
145
139
  let track = makeTrack(title: "T")
146
140
  let bad: [Float] = [.nan, .infinity, -.infinity]
147
141
  for r in bad {
148
- nowPlaying.snapshotProvider = {
142
+ nowPlaying.refreshAll(
149
143
  NowPlayingInfo.Snapshot(
150
- track: track, elapsedSeconds: 0, durationSeconds: 60, rate: r)
151
- }
152
- nowPlaying.refreshAll()
153
- let info = MPNowPlayingInfoCenter.default().nowPlayingInfo
144
+ track: track, elapsedSeconds: 0, durationSeconds: 60, rate: r))
145
+ let info = nowPlayingInfoAfterWritesLand()
154
146
  XCTAssertEqual(
155
147
  info?[MPNowPlayingInfoPropertyPlaybackRate] as? Double, 0,
156
148
  "rate must be 0 for value \(r)")
@@ -161,12 +153,10 @@ final class NowPlayingInfoTests: XCTestCase {
161
153
 
162
154
  func testRefreshPositionAndRateNoOpWhenSnapshotHasNoTrack() {
163
155
  MPNowPlayingInfoCenter.default().nowPlayingInfo = ["pre": "value"]
164
- nowPlaying.snapshotProvider = {
156
+ nowPlaying.refreshPositionAndRate(
165
157
  NowPlayingInfo.Snapshot(
166
- track: nil, elapsedSeconds: 5, durationSeconds: 60, rate: 1)
167
- }
168
- nowPlaying.refreshPositionAndRate()
169
- let info = MPNowPlayingInfoCenter.default().nowPlayingInfo
158
+ track: nil, elapsedSeconds: 5, durationSeconds: 60, rate: 1))
159
+ let info = nowPlayingInfoAfterWritesLand()
170
160
  XCTAssertEqual(info?["pre"] as? String, "value")
171
161
  XCTAssertNil(info?[MPNowPlayingInfoPropertyElapsedPlaybackTime])
172
162
  }
@@ -174,51 +164,39 @@ final class NowPlayingInfoTests: XCTestCase {
174
164
  func testRefreshPositionAndRateNoOpWhenDictNeverPopulated() {
175
165
  let track = makeTrack(title: "T")
176
166
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
177
- nowPlaying.snapshotProvider = {
167
+ nowPlaying.refreshPositionAndRate(
178
168
  NowPlayingInfo.Snapshot(
179
- track: track, elapsedSeconds: 5, durationSeconds: 60, rate: 1)
180
- }
181
- nowPlaying.refreshPositionAndRate()
182
- XCTAssertNil(MPNowPlayingInfoCenter.default().nowPlayingInfo)
169
+ track: track, elapsedSeconds: 5, durationSeconds: 60, rate: 1))
170
+ XCTAssertNil(nowPlayingInfoAfterWritesLand())
183
171
  }
184
172
 
185
173
  func testRefreshPositionAndRateNoOpAfterRefreshAllCleared() {
186
174
  let track = makeTrack(title: "T")
187
- nowPlaying.snapshotProvider = {
175
+ nowPlaying.refreshAll(
188
176
  NowPlayingInfo.Snapshot(
189
- track: track, elapsedSeconds: 5, durationSeconds: 60, rate: 1)
190
- }
191
- nowPlaying.refreshAll()
192
- XCTAssertNotNil(MPNowPlayingInfoCenter.default().nowPlayingInfo)
177
+ track: track, elapsedSeconds: 5, durationSeconds: 60, rate: 1))
178
+ XCTAssertNotNil(nowPlayingInfoAfterWritesLand())
193
179
 
194
- nowPlaying.snapshotProvider = {
180
+ nowPlaying.refreshAll(
195
181
  NowPlayingInfo.Snapshot(
196
- track: nil, elapsedSeconds: 0, durationSeconds: 0, rate: 0)
197
- }
198
- nowPlaying.refreshAll()
199
- XCTAssertNil(MPNowPlayingInfoCenter.default().nowPlayingInfo)
182
+ track: nil, elapsedSeconds: 0, durationSeconds: 0, rate: 0))
183
+ XCTAssertNil(nowPlayingInfoAfterWritesLand())
200
184
 
201
- nowPlaying.snapshotProvider = {
185
+ nowPlaying.refreshPositionAndRate(
202
186
  NowPlayingInfo.Snapshot(
203
- track: track, elapsedSeconds: 30, durationSeconds: 60, rate: 1)
204
- }
205
- nowPlaying.refreshPositionAndRate()
206
- XCTAssertNil(MPNowPlayingInfoCenter.default().nowPlayingInfo)
187
+ track: track, elapsedSeconds: 30, durationSeconds: 60, rate: 1))
188
+ XCTAssertNil(nowPlayingInfoAfterWritesLand())
207
189
  }
208
190
 
209
191
  func testRefreshPositionAndRateUpdatesElapsedAndRate() {
210
192
  let track = makeTrack(title: "T")
211
- nowPlaying.snapshotProvider = {
193
+ nowPlaying.refreshAll(
212
194
  NowPlayingInfo.Snapshot(
213
- track: track, elapsedSeconds: 5, durationSeconds: 60, rate: 1)
214
- }
215
- nowPlaying.refreshAll()
216
- nowPlaying.snapshotProvider = {
195
+ track: track, elapsedSeconds: 5, durationSeconds: 60, rate: 1))
196
+ nowPlaying.refreshPositionAndRate(
217
197
  NowPlayingInfo.Snapshot(
218
- track: track, elapsedSeconds: 30, durationSeconds: 60, rate: 0)
219
- }
220
- nowPlaying.refreshPositionAndRate()
221
- let info = MPNowPlayingInfoCenter.default().nowPlayingInfo
198
+ track: track, elapsedSeconds: 30, durationSeconds: 60, rate: 0))
199
+ let info = nowPlayingInfoAfterWritesLand()
222
200
  XCTAssertEqual(info?[MPNowPlayingInfoPropertyElapsedPlaybackTime] as? Double, 30)
223
201
  XCTAssertEqual(info?[MPNowPlayingInfoPropertyPlaybackRate] as? Double, 0)
224
202
  XCTAssertEqual(info?[MPMediaItemPropertyTitle] as? String, "T")
@@ -228,12 +206,10 @@ final class NowPlayingInfoTests: XCTestCase {
228
206
 
229
207
  func testApplyArtworkUpdatesDictWhenPopulated() {
230
208
  let track = makeTrack(title: "T")
231
- nowPlaying.snapshotProvider = {
209
+ nowPlaying.refreshAll(
232
210
  NowPlayingInfo.Snapshot(
233
- track: track, elapsedSeconds: 0, durationSeconds: 60, rate: 0)
234
- }
235
- nowPlaying.refreshAll()
236
- let placeholder = MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyArtwork]
211
+ track: track, elapsedSeconds: 0, durationSeconds: 60, rate: 0))
212
+ let placeholder = nowPlayingInfoAfterWritesLand()?[MPMediaItemPropertyArtwork]
237
213
  let size = CGSize(width: 8, height: 8)
238
214
  let renderer = UIGraphicsImageRenderer(size: size)
239
215
  let custom = renderer.image { ctx in
@@ -241,8 +217,8 @@ final class NowPlayingInfoTests: XCTestCase {
241
217
  ctx.fill(CGRect(x: 0, y: 0, width: 8, height: 8))
242
218
  }
243
219
  nowPlaying.applyArtwork(custom)
244
- let after = MPNowPlayingInfoCenter.default()
245
- .nowPlayingInfo?[MPMediaItemPropertyArtwork] as? MPMediaItemArtwork
220
+ let after = nowPlayingInfoAfterWritesLand()?[
221
+ MPMediaItemPropertyArtwork] as? MPMediaItemArtwork
246
222
  XCTAssertNotNil(after)
247
223
  XCTAssertFalse((after as AnyObject) === (placeholder as AnyObject))
248
224
  let resolvedImage = after?.image(at: size)
@@ -256,21 +232,19 @@ final class NowPlayingInfoTests: XCTestCase {
256
232
  let renderer = UIGraphicsImageRenderer(size: CGSize(width: 8, height: 8))
257
233
  let custom = renderer.image { ctx in UIColor.red.setFill(); ctx.fill(CGRect(x: 0, y: 0, width: 8, height: 8)) }
258
234
  nowPlaying.applyArtwork(custom)
259
- XCTAssertNil(MPNowPlayingInfoCenter.default().nowPlayingInfo)
235
+ XCTAssertNil(nowPlayingInfoAfterWritesLand())
260
236
  }
261
237
 
262
238
  // MARK: - clear
263
239
 
264
240
  func testClearNilsTheDict() {
265
241
  let track = makeTrack(title: "T")
266
- nowPlaying.snapshotProvider = {
242
+ nowPlaying.refreshAll(
267
243
  NowPlayingInfo.Snapshot(
268
- track: track, elapsedSeconds: 0, durationSeconds: 60, rate: 0)
269
- }
270
- nowPlaying.refreshAll()
271
- XCTAssertNotNil(MPNowPlayingInfoCenter.default().nowPlayingInfo)
244
+ track: track, elapsedSeconds: 0, durationSeconds: 60, rate: 0))
245
+ XCTAssertNotNil(nowPlayingInfoAfterWritesLand())
272
246
  nowPlaying.clear()
273
- XCTAssertNil(MPNowPlayingInfoCenter.default().nowPlayingInfo)
247
+ XCTAssertNil(nowPlayingInfoAfterWritesLand())
274
248
  }
275
249
 
276
250
  // MARK: - Helpers
@@ -286,7 +260,20 @@ final class NowPlayingInfoTests: XCTestCase {
286
260
  ) -> TrackItem {
287
261
  return TrackItem(
288
262
  id: id, url: url, title: title, artist: artist, album: album,
289
- duration: duration, artworkUrl: artworkUrl, extras: nil
263
+ duration: duration, artworkUrl: artworkUrl, extras: nil,
264
+ replayGainTrackGain: nil, replayGainTrackPeak: nil,
265
+ replayGainAlbumGain: nil, replayGainAlbumPeak: nil
290
266
  )
291
267
  }
268
+
269
+ /// The Now Playing dictionary once the writes handed to main have landed.
270
+ /// `NowPlayingInfo` enqueues its writes rather than performing them inline,
271
+ /// so a read taken in the same turn still sees the previous value.
272
+ private func nowPlayingInfoAfterWritesLand() -> [String: Any]? {
273
+ let drained = expectation(description: "main queue drained")
274
+ DispatchQueue.main.async { drained.fulfill() }
275
+ wait(for: [drained], timeout: 2.0)
276
+ return MPNowPlayingInfoCenter.default().nowPlayingInfo
277
+ }
278
+
292
279
  }