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
@@ -137,176 +137,129 @@ final class AVQueueBuilderTests: XCTestCase {
137
137
  XCTAssertEqual(item?.preferredForwardBufferDuration, 0.0)
138
138
  }
139
139
 
140
- // MARK: - Cache integration
140
+ // MARK: - Read-through routing
141
141
 
142
- func testMakeItemPrefersCachedURLWhenFullyCached() async throws {
143
- // Build a LookaheadCache, drive a download through a URLProtocol-
144
- // stubbed URLSession so the entry is fully cached, then call
145
- // makeItem(for:config:cache:) and assert the AVPlayerItem is
146
- // backed by the local file URL (not the remote one).
147
- let tempDir = FileManager.default.temporaryDirectory
148
- .appendingPathComponent("AVQueueBuilderTests-cache-\(UUID().uuidString)", isDirectory: true)
149
- defer { try? FileManager.default.removeItem(at: tempDir) }
150
-
151
- StubURLProtocol.reset()
152
- defer { StubURLProtocol.reset() }
153
- let sessionConfig = URLSessionConfiguration.ephemeral
154
- sessionConfig.protocolClasses = [StubURLProtocol.self] + (sessionConfig.protocolClasses ?? [])
155
- let session = URLSession(configuration: sessionConfig)
156
- defer { session.invalidateAndCancel() }
142
+ private func routableConfig() -> PlayerConfig {
143
+ PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, voiceVocabularyDonationEnabled: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil)
144
+ }
157
145
 
158
- let cache = LookaheadCache(
159
- directory: tempDir, maxSizeBytes: 1024 * 1024, urlSession: session
160
- )
161
- defer { cache.tearDown() }
146
+ private func startedServer() async -> ReadThroughServer {
147
+ let server = ReadThroughServer(cachedFileForOrigin: { _ in nil }, onFirstDiskHit: { _ in })
148
+ server.start()
149
+ XCTAssertTrue(server.waitUntilBound(), "the loopback listener binds within the wait")
150
+ return server
151
+ }
162
152
 
163
- let remoteURL = URL(string: "https://example.com/cached.m4a")!
164
- StubURLProtocol.stub(url: remoteURL, data: Data("payload".utf8), statusCode: 200)
153
+ /// The routing decision is stamped on the item at build time, which is what
154
+ /// source classification reads: a routed item carries the stamp and an
155
+ /// item built with no server does not.
156
+ func testTheItemCarriesWhetherItWasRouted() async throws {
157
+ let server = await startedServer()
158
+ defer { server.stop() }
159
+ let track = makeTrack(id: "remote-stamp", url: "https://example.com/stamp.mp3")
160
+
161
+ let routed = try XCTUnwrap(
162
+ AVQueueBuilder.makeItem(for: track, queueItemId: "q1", config: routableConfig(), server: server))
163
+ let direct = try XCTUnwrap(
164
+ AVQueueBuilder.makeItem(for: track, queueItemId: "q2", config: routableConfig(), server: nil))
165
+
166
+ XCTAssertTrue(routed.isRoutedThroughReadThroughServer)
167
+ XCTAssertFalse(direct.isRoutedThroughReadThroughServer)
168
+ }
165
169
 
166
- let track = makeTrack(id: "cached-1", url: remoteURL.absoluteString)
167
- try await cache.download(track: track, headers: nil)
170
+ /// A remote track binds the local server, not the origin — whether or not
171
+ /// the cache holds it yet. That is the point: the URL is chosen once and the
172
+ /// cache question is answered per request, later, when it can be answered.
173
+ func testARemoteTrackBindsTheReadThroughServer() async throws {
174
+ let server = await startedServer()
175
+ defer { server.stop() }
176
+ let track = makeTrack(id: "remote-1", url: "https://example.com/track.mp3")
168
177
 
169
178
  let item = AVQueueBuilder.makeItem(
170
- for: track,
171
- queueItemId: UUID().uuidString,
172
- config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
173
- cache: cache
174
- )
175
- XCTAssertNotNil(item)
176
- let urlAsset = try XCTUnwrap(item?.asset as? AVURLAsset)
177
- XCTAssertTrue(urlAsset.url.isFileURL,
178
- "fully-cached track must resolve to a local file URL, got \(urlAsset.url)")
179
- XCTAssertEqual(
180
- urlAsset.url.deletingLastPathComponent().path,
181
- cache.cacheDirectory.path,
182
- "local file URL must point inside the cache directory"
183
- )
184
- }
179
+ for: track, queueItemId: UUID().uuidString, config: routableConfig(), server: server)
185
180
 
186
- func testMakeItemFallsBackToRemoteWhenCacheMisses() {
187
- // No cache provided behaves identically to the no-cache path.
188
- let track = makeTrack(id: "t1", url: "https://example.com/track.m4a")
189
- let config = PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil)
190
- let item = AVQueueBuilder.makeItem(
191
- for: track, queueItemId: UUID().uuidString, config: config, cache: nil)
192
- XCTAssertNotNil(item)
193
- if let urlAsset = item?.asset as? AVURLAsset {
194
- XCTAssertEqual(urlAsset.url.absoluteString, "https://example.com/track.m4a",
195
- "no cache → remote URL")
196
- } else {
197
- XCTFail("expected AVURLAsset")
198
- }
181
+ let asset = try XCTUnwrap(item?.asset as? AVURLAsset)
182
+ XCTAssertEqual(asset.url.host, "127.0.0.1", "a remote track must read through the server")
199
183
  }
200
184
 
201
- func testMakeItemFallsBackToRemoteWhenTrackNotInCache() throws {
202
- let tempDir = FileManager.default.temporaryDirectory
203
- .appendingPathComponent("AVQueueBuilderTests-miss-\(UUID().uuidString)", isDirectory: true)
204
- defer { try? FileManager.default.removeItem(at: tempDir) }
185
+ /// The media's own URL survives on the item. Everything that reads a
186
+ /// container, a codec or a track identity out of a URL reads this one — the
187
+ /// asset URL now names the transport.
188
+ func testTheSourceURLRemainsTheOriginAfterRouting() async throws {
189
+ let server = await startedServer()
190
+ defer { server.stop() }
191
+ let track = makeTrack(id: "remote-2", url: "https://example.com/track.flac")
205
192
 
206
- let cache = LookaheadCache(
207
- directory: tempDir,
208
- maxSizeBytes: 1024 * 1024,
209
- urlSession: URLSession(configuration: .ephemeral)
210
- )
211
- defer { cache.tearDown() }
212
-
213
- let track = makeTrack(id: "uncached", url: "https://example.com/uncached.m4a")
214
193
  let item = AVQueueBuilder.makeItem(
215
- for: track,
216
- queueItemId: UUID().uuidString,
217
- config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
218
- cache: cache
219
- )
220
- XCTAssertNotNil(item)
221
- if let urlAsset = item?.asset as? AVURLAsset {
222
- XCTAssertEqual(urlAsset.url.absoluteString, "https://example.com/uncached.m4a",
223
- "cache miss → remote URL")
224
- } else {
225
- XCTFail("expected AVURLAsset")
226
- }
227
- }
194
+ for: track, queueItemId: UUID().uuidString, config: routableConfig(), server: server)
228
195
 
229
- // MARK: - Cache integration edge cases
196
+ XCTAssertEqual(item?.sourceURL, "https://example.com/track.flac")
197
+ }
230
198
 
231
- func testMakeItemFallsBackToRemoteWhenCachedFileDeletedExternally() async throws {
232
- // An external `rm` of the cache file falls through to the remote
233
- // URL via `cachedURL`'s defensive stale-entry-prune branch.
234
- let tempDir = FileManager.default.temporaryDirectory
235
- .appendingPathComponent("AVQueueBuilderTests-stale-\(UUID().uuidString)", isDirectory: true)
236
- defer { try? FileManager.default.removeItem(at: tempDir) }
199
+ /// One URL per track for the session, so a window rebuild does not re-point
200
+ /// an asset the player is already reading.
201
+ func testRebuildingTheSameTrackBindsTheSameURL() async throws {
202
+ let server = await startedServer()
203
+ defer { server.stop() }
204
+ let track = makeTrack(id: "remote-3", url: "https://example.com/stable.mp3")
205
+ let config = routableConfig()
206
+
207
+ let first = AVQueueBuilder.makeItem(
208
+ for: track, queueItemId: UUID().uuidString, config: config, server: server)
209
+ let second = AVQueueBuilder.makeItem(
210
+ for: track, queueItemId: UUID().uuidString, config: config, server: server)
211
+
212
+ let a = try XCTUnwrap(first?.asset as? AVURLAsset).url
213
+ let b = try XCTUnwrap(second?.asset as? AVURLAsset).url
214
+ XCTAssertEqual(a, b)
215
+ }
237
216
 
238
- StubURLProtocol.reset()
239
- defer { StubURLProtocol.reset() }
240
- let sessionConfig = URLSessionConfiguration.ephemeral
241
- sessionConfig.protocolClasses = [StubURLProtocol.self] + (sessionConfig.protocolClasses ?? [])
242
- let session = URLSession(configuration: sessionConfig)
243
- defer { session.invalidateAndCancel() }
217
+ func testAFileTrackIsNeverRouted() async throws {
218
+ let server = await startedServer()
219
+ defer { server.stop() }
220
+ let track = makeTrack(id: "local-1", url: "file:///tmp/song.mp3")
244
221
 
245
- let cache = LookaheadCache(
246
- directory: tempDir, maxSizeBytes: 1024 * 1024, urlSession: session
247
- )
248
- defer { cache.tearDown() }
222
+ let item = AVQueueBuilder.makeItem(
223
+ for: track, queueItemId: UUID().uuidString, config: routableConfig(), server: server)
249
224
 
250
- let remoteURL = URL(string: "https://example.com/stale.m4a")!
251
- StubURLProtocol.stub(url: remoteURL, data: Data("payload".utf8), statusCode: 200)
252
- let track = makeTrack(id: "stale-1", url: remoteURL.absoluteString)
253
- try await cache.download(track: track, headers: nil)
225
+ let asset = try XCTUnwrap(item?.asset as? AVURLAsset)
226
+ XCTAssertTrue(asset.url.isFileURL, "a local file has nothing to read through")
227
+ }
254
228
 
255
- // External actor (consumer rm, OS reclaim, …) deletes the file
256
- // out from under the cache index.
257
- let fileURL = cache.cachedURL(forUrl: track.url)!
258
- try FileManager.default.removeItem(at: fileURL)
229
+ /// AVFoundation resolves a playlist's relative segment paths against the URL
230
+ /// it fetched the playlist from, so a proxied playlist would point every
231
+ /// segment at a token that names none of them.
232
+ func testAnHLSPlaylistIsNeverRouted() async throws {
233
+ let server = await startedServer()
234
+ defer { server.stop() }
235
+ let track = makeTrack(id: "hls-1", url: "https://example.com/stream.m3u8")
259
236
 
260
237
  let item = AVQueueBuilder.makeItem(
261
- for: track,
262
- queueItemId: UUID().uuidString,
263
- config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
264
- cache: cache
265
- )
266
- XCTAssertNotNil(item)
267
- let urlAsset = try XCTUnwrap(item?.asset as? AVURLAsset)
268
- XCTAssertEqual(urlAsset.url.absoluteString, remoteURL.absoluteString,
269
- "deleted-file path must fall back to remote URL via cachedURL's stale-entry prune")
238
+ for: track, queueItemId: UUID().uuidString, config: routableConfig(), server: server)
239
+
240
+ let asset = try XCTUnwrap(item?.asset as? AVURLAsset)
241
+ XCTAssertEqual(asset.url.absoluteString, "https://example.com/stream.m3u8")
270
242
  }
271
243
 
272
- func testBuildPlayerItemsMixesCachedAndUncachedTracks() async throws {
273
- let tempDir = FileManager.default.temporaryDirectory
274
- .appendingPathComponent("AVQueueBuilderTests-mixed-\(UUID().uuidString)", isDirectory: true)
275
- defer { try? FileManager.default.removeItem(at: tempDir) }
244
+ /// A consumer running without the lookahead cache has no server, and gets
245
+ /// exactly today's behaviour rather than a local hop that could never hit.
246
+ func testWithNoServerTheOriginIsBoundDirectly() throws {
247
+ let track = makeTrack(id: "remote-4", url: "https://example.com/direct.mp3")
276
248
 
277
- StubURLProtocol.reset()
278
- defer { StubURLProtocol.reset() }
279
- let sessionConfig = URLSessionConfiguration.ephemeral
280
- sessionConfig.protocolClasses = [StubURLProtocol.self] + (sessionConfig.protocolClasses ?? [])
281
- let session = URLSession(configuration: sessionConfig)
282
- defer { session.invalidateAndCancel() }
249
+ let item = AVQueueBuilder.makeItem(
250
+ for: track, queueItemId: UUID().uuidString, config: routableConfig(), server: nil)
283
251
 
284
- let cache = LookaheadCache(
285
- directory: tempDir, maxSizeBytes: 1024 * 1024, urlSession: session
286
- )
287
- defer { cache.tearDown() }
252
+ let asset = try XCTUnwrap(item?.asset as? AVURLAsset)
253
+ XCTAssertEqual(asset.url.absoluteString, "https://example.com/direct.mp3")
254
+ }
288
255
 
289
- // Download A only; B stays uncached.
290
- let urlA = URL(string: "https://example.com/a.m4a")!
291
- StubURLProtocol.stub(url: urlA, data: Data("a".utf8), statusCode: 200)
292
- let trackA = makeTrack(id: "a", url: urlA.absoluteString)
293
- let trackB = makeTrack(id: "b", url: "https://example.com/b.m4a")
294
- try await cache.download(track: trackA, headers: nil)
295
-
296
- let items = AVQueueBuilder.buildPlayerItems(
297
- tracks: [trackA, trackB],
298
- queueItemIds: [UUID().uuidString, UUID().uuidString],
299
- config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
300
- cache: cache
301
- )
302
- XCTAssertEqual(items.count, 2)
303
- let assetA = try XCTUnwrap(items[0].asset as? AVURLAsset)
304
- let assetB = try XCTUnwrap(items[1].asset as? AVURLAsset)
305
- XCTAssertTrue(assetA.url.isFileURL,
306
- "A is cached → file URL")
307
- XCTAssertFalse(assetB.url.isFileURL,
308
- "B is uncached → remote URL")
309
- XCTAssertEqual(assetB.url.absoluteString, "https://example.com/b.m4a")
256
+ func testRoutabilityIsSchemeAndManifestAware() {
257
+ XCTAssertTrue(AVQueueBuilder.isRoutable("https://example.com/a.mp3"))
258
+ XCTAssertTrue(AVQueueBuilder.isRoutable("http://example.com/a.mp3"))
259
+ XCTAssertTrue(AVQueueBuilder.isRoutable("https://example.com/no-extension"))
260
+ XCTAssertFalse(AVQueueBuilder.isRoutable("file:///tmp/a.mp3"))
261
+ XCTAssertFalse(AVQueueBuilder.isRoutable("https://example.com/stream.m3u8"))
262
+ XCTAssertFalse(AVQueueBuilder.isRoutable("HTTPS://example.com/stream.M3U8"))
310
263
  }
311
264
 
312
265
  func testSetMaxSizeBytesShrinkProtectsCurrentlyPlayingTrack() async throws {
@@ -371,13 +324,13 @@ final class AVQueueBuilderTests: XCTestCase {
371
324
  let queueItemIdB = UUID().uuidString
372
325
  let itemA = AVQueueBuilder.makeItem(
373
326
  for: track, queueItemId: queueItemIdA,
374
- config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
375
- cache: nil
327
+ config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, voiceVocabularyDonationEnabled: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
328
+ server: nil
376
329
  )
377
330
  let itemB = AVQueueBuilder.makeItem(
378
331
  for: track, queueItemId: queueItemIdB,
379
- config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
380
- cache: nil
332
+ config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, voiceVocabularyDonationEnabled: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
333
+ server: nil
381
334
  )
382
335
  XCTAssertNotNil(itemA)
383
336
  XCTAssertNotNil(itemB)
@@ -396,13 +349,15 @@ final class AVQueueBuilderTests: XCTestCase {
396
349
  let track = TrackItem(
397
350
  id: nil, url: "https://example.com/nil-id.m4a",
398
351
  title: nil, artist: nil, album: nil,
399
- duration: nil, artworkUrl: nil, extras: nil
352
+ duration: nil, artworkUrl: nil, extras: nil,
353
+ replayGainTrackGain: nil, replayGainTrackPeak: nil,
354
+ replayGainAlbumGain: nil, replayGainAlbumPeak: nil
400
355
  )
401
356
  let queueItemId = UUID().uuidString
402
357
  let item = AVQueueBuilder.makeItem(
403
358
  for: track, queueItemId: queueItemId,
404
- config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
405
- cache: nil
359
+ config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, voiceVocabularyDonationEnabled: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
360
+ server: nil
406
361
  )
407
362
  XCTAssertNotNil(item)
408
363
  XCTAssertEqual(item?.queueItemId, queueItemId,
@@ -417,88 +372,70 @@ final class AVQueueBuilderTests: XCTestCase {
417
372
  let queueItemId = UUID().uuidString
418
373
  let item = AVQueueBuilder.makeItem(
419
374
  for: track, queueItemId: queueItemId,
420
- config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
421
- cache: nil
375
+ config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, voiceVocabularyDonationEnabled: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
376
+ server: nil
422
377
  )
423
378
  XCTAssertNotNil(item)
424
379
  XCTAssertEqual(item?.queueItemId, queueItemId,
425
380
  "queueItemId must be set even when track.id is empty string")
426
381
  }
427
382
 
428
- // MARK: - preloadAssetForInsert
429
383
 
430
- func testPreloadAssetForInsertLoadsTracksForLocalFile() async throws {
431
- let url = try Self.writeTinyWav()
432
- defer { try? FileManager.default.removeItem(at: url) }
433
- let item = try XCTUnwrap(
434
- AVQueueBuilder.makeItemRaw(urlString: url.absoluteString, headers: nil, userAgent: nil))
384
+ private func makeTrack(id: String, url: String) -> TrackItem {
385
+ return TrackItem(
386
+ id: id, url: url, title: nil, artist: nil, album: nil,
387
+ duration: nil, artworkUrl: nil, extras: nil,
388
+ replayGainTrackGain: nil, replayGainTrackPeak: nil,
389
+ replayGainAlbumGain: nil, replayGainAlbumPeak: nil
390
+ )
391
+ }
435
392
 
436
- await AVQueueBuilder.preloadAssetForInsert(item)
437
393
 
438
- // The load resolved the tracks key on the same asset instance the
439
- // enqueue will insert, so the audio track is present without a second
440
- // fault the preroll after insert has nothing left to load.
441
- let tracks = try await item.asset.load(.tracks)
442
- XCTAssertFalse(tracks.isEmpty,
443
- "preload must resolve the local asset's tracks so the enqueue preroll is cheap")
394
+ // MARK: - Cancelling loads on discarded items
395
+
396
+ /// A cancelled asset can never become playable, so cancellation may only
397
+ /// touch assets that no surviving item reuses.
398
+ func testEveryOutgoingItemIsDroppedWhenNothingSurvives() {
399
+ let outgoing = [makeItem("a"), makeItem("b")]
400
+ let dropped = AVQueueBuilder.itemsBeingDropped(outgoing: outgoing, keeping: [])
401
+ XCTAssertEqual(dropped.count, 2)
444
402
  }
445
403
 
446
- func testPreloadAssetForInsertLeavesUnreadableAssetInsertable() async throws {
447
- // Best-effort contract: a missing file's load fails but the item stays
448
- // intact + insertable, so the enqueue proceeds unwarmed rather than
449
- // being blocked or corrupted.
450
- let missing = FileManager.default.temporaryDirectory
451
- .appendingPathComponent("does-not-exist-\(UUID().uuidString).m4a")
452
- let item = try XCTUnwrap(
453
- AVQueueBuilder.makeItemRaw(urlString: missing.absoluteString, headers: nil, userAgent: nil))
454
-
455
- let start = Date()
456
- await AVQueueBuilder.preloadAssetForInsert(item, timeout: 0.5)
457
- let elapsed = Date().timeIntervalSince(start)
458
-
459
- XCTAssertLessThan(elapsed, 2.0,
460
- "a best-effort preload of an unreadable asset must return promptly, not hang")
461
- let urlAsset = try XCTUnwrap(item.asset as? AVURLAsset)
462
- XCTAssertEqual(urlAsset.url, missing,
463
- "a failed preload must leave the item's asset unchanged and insertable")
404
+ func testAnItemWhoseAssetIsReusedIsNotDropped() {
405
+ let shared = AVURLAsset(url: URL(string: "https://example.com/shared.m4a")!)
406
+ let outgoing = [AVPlayerItem(asset: shared), makeItem("other")]
407
+ let rebuiltOnTheSameAsset = AVPlayerItem(asset: shared)
408
+
409
+ let dropped = AVQueueBuilder.itemsBeingDropped(
410
+ outgoing: outgoing, keeping: [rebuiltOnTheSameAsset])
411
+
412
+ XCTAssertEqual(
413
+ dropped.count, 1,
414
+ "an asset a surviving item rebuilt on must keep its loads — cancelling it "
415
+ + "leaves that item permanently unplayable")
416
+ XCTAssertFalse(
417
+ dropped.contains { $0.asset === shared },
418
+ "the reused asset must not be handed to cancellation")
464
419
  }
465
420
 
466
- private func makeTrack(id: String, url: String) -> TrackItem {
467
- return TrackItem(
468
- id: id, url: url, title: nil, artist: nil, album: nil,
469
- duration: nil, artworkUrl: nil, extras: nil
470
- )
421
+ func testKeepingIsComparedByAssetIdentityNotByItem() {
422
+ let shared = AVURLAsset(url: URL(string: "https://example.com/shared.m4a")!)
423
+ let outgoing = [AVPlayerItem(asset: shared)]
424
+ // A different item object wrapping the same asset still protects it.
425
+ let dropped = AVQueueBuilder.itemsBeingDropped(
426
+ outgoing: outgoing, keeping: [AVPlayerItem(asset: shared)])
427
+ XCTAssertTrue(dropped.isEmpty)
471
428
  }
472
429
 
473
- /// Writes a minimal valid 16-bit PCM mono WAV (silent samples) so
474
- /// AVFoundation resolves exactly one audio track from the file.
475
- private static func writeTinyWav() throws -> URL {
476
- let sampleRate: UInt32 = 44100
477
- let channels: UInt16 = 1
478
- let bitsPerSample: UInt16 = 16
479
- let frameCount = 1024
480
- let dataSize = UInt32(frameCount) * UInt32(channels) * UInt32(bitsPerSample / 8)
481
- var data = Data()
482
- func appendLE<T: FixedWidthInteger>(_ value: T) {
483
- withUnsafeBytes(of: value.littleEndian) { data.append(contentsOf: $0) }
484
- }
485
- data.append(contentsOf: Array("RIFF".utf8))
486
- appendLE(UInt32(36) + dataSize)
487
- data.append(contentsOf: Array("WAVE".utf8))
488
- data.append(contentsOf: Array("fmt ".utf8))
489
- appendLE(UInt32(16))
490
- appendLE(UInt16(1))
491
- appendLE(channels)
492
- appendLE(sampleRate)
493
- appendLE(sampleRate * UInt32(channels) * UInt32(bitsPerSample / 8))
494
- appendLE(UInt16(channels * bitsPerSample / 8))
495
- appendLE(bitsPerSample)
496
- data.append(contentsOf: Array("data".utf8))
497
- appendLE(dataSize)
498
- data.append(Data(count: Int(dataSize)))
499
- let url = FileManager.default.temporaryDirectory
500
- .appendingPathComponent("preload-\(UUID().uuidString).wav")
501
- try data.write(to: url)
502
- return url
430
+ func testAnUnrelatedSurvivorDoesNotProtectOtherAssets() {
431
+ let outgoing = [makeItem("a"), makeItem("b")]
432
+ let dropped = AVQueueBuilder.itemsBeingDropped(
433
+ outgoing: outgoing, keeping: [makeItem("c")])
434
+ XCTAssertEqual(dropped.count, 2)
503
435
  }
436
+
437
+ private func makeItem(_ name: String) -> AVPlayerItem {
438
+ AVPlayerItem(asset: AVURLAsset(url: URL(string: "https://example.com/\(name).m4a")!))
439
+ }
440
+
504
441
  }
@@ -0,0 +1,44 @@
1
+ import AVFoundation
2
+ @testable import QueuePlayer
3
+
4
+ /// A `PlaybackEngine` that reports one active item and nothing else; for
5
+ /// tests whose subject reads `activeMediaItem` and needs a real
6
+ /// `AVPlayerItem` identity behind it.
7
+ final class ActiveItemEngineStub: PlaybackEngine {
8
+ var item: AVPlayerItem?
9
+ private(set) var released = false
10
+ private(set) var playCalls = 0
11
+
12
+ var currentMediaItem: AVPlayerItem? { item }
13
+ var allMediaItems: [AVPlayerItem] { item.map { [$0] } ?? [] }
14
+ var attachedQueueItemIds: [String] { [] }
15
+ var isFadePendingOrActive: Bool { false }
16
+ var currentMediaItemIndex: Int { item == nil ? -1 : 0 }
17
+ var currentPositionSeconds: TimeInterval { 0 }
18
+ var currentDurationSeconds: TimeInterval { 0 }
19
+ var bufferedPositionSeconds: TimeInterval { 0 }
20
+ var isPlaying: Bool { false }
21
+ var timeControlStatus: AVPlayer.TimeControlStatus { .paused }
22
+ var delegate: PlaybackEngineDelegate?
23
+
24
+ func release() { released = true }
25
+ func setItems(_ items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) {}
26
+ @discardableResult
27
+ func insertItems(_ items: [AVPlayerItem], after: AVPlayerItem?) -> Bool { true }
28
+ func remove(_ item: AVPlayerItem) {}
29
+ func removeAllItems() {}
30
+ func canInsert(_ item: AVPlayerItem, after: AVPlayerItem?) -> Bool { true }
31
+ func play() { playCalls += 1 }
32
+ func pause() {}
33
+ func stop() {}
34
+ func seek(toIndex index: Int, position: TimeInterval) {}
35
+ func skipToNext() {}
36
+ func skipToPrevious() {}
37
+ func setVolume(_ volume: Float) {}
38
+ func setPlaybackSpeed(_ speed: Float) {}
39
+ func seedPlaybackSpeed(_ speed: Float) {}
40
+ func setRepeatMode(_ mode: RepeatMode) {}
41
+ func setPitchCorrectionMode(_ mode: PitchCorrectionMode) {}
42
+ func setAudioMixProvider(_ provider: AudioMixProvider?) {}
43
+ func refreshAllItemMixes() {}
44
+ }