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
@@ -2,8 +2,10 @@ package com.margelo.nitro.queueplayer
2
2
 
3
3
  import androidx.media3.common.util.UnstableApi
4
4
  import androidx.test.core.app.ApplicationProvider
5
+ import kotlinx.coroutines.runBlocking
5
6
  import org.junit.After
6
7
  import org.junit.Assert.assertEquals
8
+ import org.junit.Assert.assertFalse
7
9
  import org.junit.Assert.assertNotEquals
8
10
  import org.junit.Assert.assertNotNull
9
11
  import org.junit.Assert.assertNull
@@ -70,7 +72,7 @@ class TrackPlayerLookaheadConfigTest {
70
72
  // Size is configure-time (PlayerConfig.lookaheadCacheMaxSizeMb), not a
71
73
  // runtime dimension. Unset → 100 MB default; set → reflected read-only.
72
74
  player.configureInternal(emptyConfig(), context())
73
- assertEquals(100.0, player.getLookaheadCacheStatus().maxSizeMb, 0.001)
75
+ assertEquals(100.0, player.buildCacheStatusSnapshot().maxSizeMb, 0.001)
74
76
 
75
77
  player.destroyInternal()
76
78
  serviceHandle.destroy()
@@ -79,12 +81,14 @@ class TrackPlayerLookaheadConfigTest {
79
81
  PlayerConfig(
80
82
  httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null,
81
83
  networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null,
82
- visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = 250.0, lookaheadCacheEvictionPolicy = null,
84
+ voiceVocabularyDonationEnabled = null,
85
+ visualizationEnabled = null, clampSeekToBuffered = null,
86
+ lookaheadCacheMaxSizeMb = 250.0, lookaheadCacheEvictionPolicy = null,
83
87
  progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
84
88
  ),
85
89
  context()
86
90
  )
87
- assertEquals(250.0, player.getLookaheadCacheStatus().maxSizeMb, 0.001)
91
+ assertEquals(250.0, player.buildCacheStatusSnapshot().maxSizeMb, 0.001)
88
92
  }
89
93
 
90
94
  @Test
@@ -115,39 +119,263 @@ class TrackPlayerLookaheadConfigTest {
115
119
  assertNotNull(player.lookaheadCache)
116
120
  }
117
121
 
118
- private fun configWithPolicy(policy: EvictionPolicy) = PlayerConfig(
122
+ private fun configWithPolicy(policy: EvictionPolicy, maxSizeMb: Double? = null) = PlayerConfig(
119
123
  httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null,
120
124
  networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null,
121
- visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null,
125
+ voiceVocabularyDonationEnabled = null,
126
+ visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = maxSizeMb,
122
127
  lookaheadCacheEvictionPolicy = policy,
123
128
  progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
124
129
  )
125
130
 
131
+ @Test
132
+ fun `re-configuring with an unchanged policy keeps the cache`() {
133
+ // The policy comparison reads what the live cache was built with, not the
134
+ // in-memory config, which starts at the LRU default on every player.
135
+ player.configureInternal(configWithPolicy(EvictionPolicy.FIFO), context())
136
+ val first = player.lookaheadCache?.simpleCache
137
+ assertNotNull(first)
138
+
139
+ player.destroyInternal()
140
+ serviceHandle.destroy()
141
+ player = TrackPlayer()
142
+ serviceHandle = bindPlaybackServiceForTest(player, context())
143
+ player.configureInternal(configWithPolicy(EvictionPolicy.FIFO), context())
144
+
145
+ assertSame(
146
+ "an unchanged policy must not discard the shared cache",
147
+ first, player.lookaheadCache?.simpleCache
148
+ )
149
+ }
150
+
151
+ /**
152
+ * With no live cache in the process, the policy the directory was built
153
+ * with is read from prefs. A previous process is stood in for by a cache
154
+ * built and populated here, then released from the process-wide map; the
155
+ * span it holds is what a discard removes and a kept cache still serves.
156
+ */
157
+ @Test
158
+ fun `a policy recorded by a previous process is read from disk`() {
159
+ val ctx = context()
160
+ val url = "https://example.com/previous.m4a"
161
+ seedCacheFromAPreviousProcess(url, EvictionPolicy.FIFO)
162
+
163
+ player.configureInternal(configWithPolicy(EvictionPolicy.LRU), ctx)
164
+
165
+ assertTrue(
166
+ "the recorded policy differs from the requested one, so the old cache is discarded",
167
+ player.lookaheadCache!!.simpleCache.getCachedSpans(url).isEmpty())
168
+ }
169
+
170
+ @Test
171
+ fun `a policy recorded by a previous process that matches keeps the cache`() {
172
+ val ctx = context()
173
+ val url = "https://example.com/previous.m4a"
174
+ seedCacheFromAPreviousProcess(url, EvictionPolicy.FIFO)
175
+
176
+ player.configureInternal(configWithPolicy(EvictionPolicy.FIFO), ctx)
177
+
178
+ assertFalse(
179
+ "an unchanged policy keeps the cache and its spans",
180
+ player.lookaheadCache!!.simpleCache.getCachedSpans(url).isEmpty())
181
+ }
182
+
183
+ /**
184
+ * Build a cache under [policy], commit one span for [url], then leave the
185
+ * process with no live instance and prefs recording [policy] — the state
186
+ * the next launch finds.
187
+ */
188
+ private fun seedCacheFromAPreviousProcess(url: String, policy: EvictionPolicy) {
189
+ val ctx = context()
190
+ player.configureInternal(configWithPolicy(policy), ctx)
191
+ player.setLookaheadCacheInternal(
192
+ LookaheadCacheConfig(enabled = false, lookaheadCount = 0.0, allowsCellularAccess = null)
193
+ )
194
+ writeSpansAndListSurvivors(listOf(url))
195
+ val dir = player.lookaheadCache!!.cacheDir
196
+ player.destroyInternal()
197
+ serviceHandle.destroy()
198
+ LookaheadCache.releaseSharedInstance(dir)
199
+ ctx.getSharedPreferences("rnqp_lookahead_cache", android.content.Context.MODE_PRIVATE)
200
+ .edit().putString("eviction_policy", policy.name).commit()
201
+ player = TrackPlayer()
202
+ serviceHandle = bindPlaybackServiceForTest(player, ctx)
203
+ }
204
+
205
+ private fun track(id: String) = TrackItem(
206
+ id = id, url = "https://example.com/$id.m4a",
207
+ title = null, artist = null, album = null,
208
+ duration = null, artworkUrl = null, extras = null,
209
+ replayGainTrackGain = null, replayGainTrackPeak = null,
210
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
211
+ )
212
+
213
+ // --- cache protection window
214
+
215
+ /**
216
+ * The window is observed through the evictor: five 400 KB tracks against a
217
+ * 1 MB budget under FIFO leave exactly the protected ones — the one behind the
218
+ * playhead, the playing one and `lookaheadCount` ahead — on disk.
219
+ */
220
+ @Test
221
+ fun `the protection window keeps the playing track and its neighbours through eviction`() {
222
+ player.configureInternal(configWithPolicy(EvictionPolicy.FIFO, maxSizeMb = 1.0), context())
223
+ // The window is refreshed whether or not prefetch is enabled; disabled,
224
+ // the writer fetches nothing and the evictor is the only thing that
225
+ // decides which spans survive.
226
+ player.setLookaheadCacheInternal(
227
+ LookaheadCacheConfig(enabled = false, lookaheadCount = 1.0, allowsCellularAccess = null)
228
+ )
229
+ player.setQueueInternal((1..5).map { track("t$it") }, startAtIndex = 2)
230
+
231
+ val survivors = writeSpansAndListSurvivors((1..5).map { "https://example.com/t$it.m4a" })
232
+
233
+ assertEquals(
234
+ "t1 is the oldest unprotected entry and t5 lands over budget with nothing else to take",
235
+ listOf("https://example.com/t2.m4a", "https://example.com/t3.m4a", "https://example.com/t4.m4a"),
236
+ survivors)
237
+ }
238
+
239
+ @Test
240
+ fun `an out-of-range lookahead count clamps the protection window`() {
241
+ // lookaheadCount crosses from JS as a Double. The window drops non-finite
242
+ // values before any `toInt()`, and clamps a finite one to the queue and
243
+ // the protected-ahead ceiling before it is added to the index — so every
244
+ // one of these yields a well-formed set rather than a thrown subList.
245
+ player.configureInternal(configWithPolicy(EvictionPolicy.FIFO, maxSizeMb = 1.0), context())
246
+ player.setLookaheadCacheInternal(
247
+ LookaheadCacheConfig(enabled = false, lookaheadCount = 0.0, allowsCellularAccess = null)
248
+ )
249
+ player.setQueueInternal((1..5).map { track("t$it") }, startAtIndex = 2)
250
+
251
+ listOf(Double.POSITIVE_INFINITY, Double.NaN, Double.MAX_VALUE, -1.0).forEach { count ->
252
+ player.setLookaheadCacheInternal(
253
+ LookaheadCacheConfig(enabled = false, lookaheadCount = count, allowsCellularAccess = null)
254
+ )
255
+ }
256
+ // -1.0 was applied last: a count of zero ahead protects t2 and t3 only.
257
+ val survivors = writeSpansAndListSurvivors((1..5).map { "https://example.com/t$it.m4a" })
258
+
259
+ assertEquals(listOf("https://example.com/t2.m4a", "https://example.com/t3.m4a"), survivors)
260
+ }
261
+
262
+ /** Commit one 400 KB span per url in order and return the urls still cached. */
263
+ private fun writeSpansAndListSurvivors(urls: List<String>): List<String> {
264
+ val cache = player.lookaheadCache!!.simpleCache
265
+ val spanBytes = 400L * 1024
266
+ for (url in urls) {
267
+ // The write lock a CacheDataSink would hold: startFile requires it.
268
+ val hole = cache.startReadWrite(url, 0, spanBytes)
269
+ val file = cache.startFile(url, 0, spanBytes)
270
+ file.writeBytes(ByteArray(spanBytes.toInt()))
271
+ cache.commitFile(file, spanBytes)
272
+ cache.releaseHoleSpan(hole)
273
+ }
274
+ return urls.filter { cache.getCachedSpans(it).isNotEmpty() }
275
+ }
276
+
126
277
  // --- setLookaheadCacheInternal
127
278
 
128
279
  @Test
129
- fun `setLookaheadCacheInternal disable tears down writer but keeps cache`() {
130
- // Deferred-rebuild contract: disabling stops new prefetches but
131
- // leaves the cache instance alive so the player's MediaSource.Factory
132
- // can still read existing entries. Cache is fully released on the
133
- // next destroy() / configure() cycle.
280
+ fun `setLookaheadCacheInternal disable tears down writer and stops serving reads`() {
281
+ // Disabling stops new prefetches and routes the player's reads to the
282
+ // origin; the cache instance stays alive (one per directory) and is
283
+ // released on the next destroy() / configure() cycle.
134
284
  player.configureInternal(emptyConfig(), context())
135
285
  val cacheBeforeDisable = player.lookaheadCache
136
286
  assertNotNull(cacheBeforeDisable)
137
287
  assertNotNull(player.lookaheadCacheWriter)
288
+ assertTrue("enabled: playback reads go through the cache", cacheBeforeDisable!!.servesPlayback)
138
289
 
139
290
  player.setLookaheadCacheInternal(
140
291
  LookaheadCacheConfig(
141
- enabled = false, lookaheadCount = 3.0
292
+ enabled = false, lookaheadCount = 3.0,
293
+ allowsCellularAccess = null
142
294
  )
143
295
  )
144
296
 
145
- assertSame(
146
- "cache must stay alive — player's MediaSource.Factory still reads it",
147
- cacheBeforeDisable, player.lookaheadCache
148
- )
297
+ assertSame("the cache instance stays alive", cacheBeforeDisable, player.lookaheadCache)
149
298
  assertNull("writer torn down — no new prefetches", player.lookaheadCacheWriter)
299
+ assertFalse("disabled: playback reads go to the origin", cacheBeforeDisable.servesPlayback)
150
300
  assertEquals(false, player.lookaheadConfig.enabled)
301
+
302
+ player.setLookaheadCacheInternal(
303
+ LookaheadCacheConfig(enabled = true, lookaheadCount = 3.0, allowsCellularAccess = null)
304
+ )
305
+ assertTrue("re-enabled: reads go through the cache again", cacheBeforeDisable.servesPlayback)
306
+ }
307
+
308
+ // --- allowsCellularAccess
309
+
310
+ @Test
311
+ fun `configure builds the writer permitting cellular by default`() {
312
+ player.configureInternal(emptyConfig(), context())
313
+
314
+ assertEquals(true, player.lookaheadCacheWriter?.allowsCellularAccess)
315
+ }
316
+
317
+ @Test
318
+ fun `configure carries a non-default cellular policy into the writer`() {
319
+ player.lookaheadConfig = LookaheadCacheConfig(
320
+ enabled = true, lookaheadCount = 3.0, allowsCellularAccess = false
321
+ )
322
+
323
+ player.configureInternal(emptyConfig(), context())
324
+
325
+ assertEquals(false, player.lookaheadCacheWriter?.allowsCellularAccess)
326
+ }
327
+
328
+ @Test
329
+ fun `configure starts a transport monitor and destroy stops it`() {
330
+ player.configureInternal(emptyConfig(), context())
331
+ assertNotNull(player.cellularTransportMonitor)
332
+
333
+ player.destroyInternal()
334
+
335
+ assertNull(player.cellularTransportMonitor)
336
+ }
337
+
338
+ @Test
339
+ fun `setLookaheadCacheInternal applies allowsCellularAccess to the live writer`() {
340
+ player.configureInternal(emptyConfig(), context())
341
+ val writer = player.lookaheadCacheWriter
342
+ assertNotNull(writer)
343
+
344
+ player.setLookaheadCacheInternal(
345
+ LookaheadCacheConfig(
346
+ enabled = true, lookaheadCount = 3.0, allowsCellularAccess = false
347
+ )
348
+ )
349
+
350
+ assertSame("the writer is reused — only its policy changes", writer, player.lookaheadCacheWriter)
351
+ assertEquals(false, writer?.allowsCellularAccess)
352
+
353
+ player.setLookaheadCacheInternal(
354
+ LookaheadCacheConfig(
355
+ enabled = true, lookaheadCount = 3.0, allowsCellularAccess = true
356
+ )
357
+ )
358
+
359
+ assertEquals(true, writer?.allowsCellularAccess)
360
+ }
361
+
362
+ @Test
363
+ fun `a writer rebuilt by an enable flip carries the current cellular policy`() {
364
+ player.configureInternal(emptyConfig(), context())
365
+ player.setLookaheadCacheInternal(
366
+ LookaheadCacheConfig(
367
+ enabled = false, lookaheadCount = 3.0, allowsCellularAccess = false
368
+ )
369
+ )
370
+ assertNull(player.lookaheadCacheWriter)
371
+
372
+ player.setLookaheadCacheInternal(
373
+ LookaheadCacheConfig(
374
+ enabled = true, lookaheadCount = 3.0, allowsCellularAccess = false
375
+ )
376
+ )
377
+
378
+ assertEquals(false, player.lookaheadCacheWriter?.allowsCellularAccess)
151
379
  }
152
380
 
153
381
  @Test
@@ -164,7 +392,8 @@ class TrackPlayerLookaheadConfigTest {
164
392
  // Disable → writer released, cache kept for reads.
165
393
  player.setLookaheadCacheInternal(
166
394
  LookaheadCacheConfig(
167
- enabled = false, lookaheadCount = 3.0
395
+ enabled = false, lookaheadCount = 3.0,
396
+ allowsCellularAccess = null
168
397
  )
169
398
  )
170
399
  assertNull("disable releases the writer", player.lookaheadCacheWriter)
@@ -173,7 +402,8 @@ class TrackPlayerLookaheadConfigTest {
173
402
  // Re-enable → writer rebuilt at runtime, same cache, no configure().
174
403
  player.setLookaheadCacheInternal(
175
404
  LookaheadCacheConfig(
176
- enabled = true, lookaheadCount = 5.0
405
+ enabled = true, lookaheadCount = 5.0,
406
+ allowsCellularAccess = null
177
407
  )
178
408
  )
179
409
  assertNotNull(
@@ -190,7 +420,8 @@ class TrackPlayerLookaheadConfigTest {
190
420
  // Toggle off again → writer released once more (clean repeatable switch).
191
421
  player.setLookaheadCacheInternal(
192
422
  LookaheadCacheConfig(
193
- enabled = false, lookaheadCount = 5.0
423
+ enabled = false, lookaheadCount = 5.0,
424
+ allowsCellularAccess = null
194
425
  )
195
426
  )
196
427
  assertNull("second disable releases the writer again", player.lookaheadCacheWriter)
@@ -203,16 +434,21 @@ class TrackPlayerLookaheadConfigTest {
203
434
  // (built even when disabled) so the runtime enable can attach a writer.
204
435
  player.setLookaheadCacheInternal(
205
436
  LookaheadCacheConfig(
206
- enabled = false, lookaheadCount = 3.0
437
+ enabled = false, lookaheadCount = 3.0,
438
+ allowsCellularAccess = null
207
439
  )
208
440
  )
209
441
  player.configureInternal(emptyConfig(), context())
210
442
  assertNotNull("cache is built even when disabled at configure", player.lookaheadCache)
211
443
  assertNull("no writer while disabled", player.lookaheadCacheWriter)
444
+ assertFalse(
445
+ "built disabled: the player's reads go to the origin",
446
+ player.lookaheadCache!!.servesPlayback)
212
447
 
213
448
  player.setLookaheadCacheInternal(
214
449
  LookaheadCacheConfig(
215
- enabled = true, lookaheadCount = 4.0
450
+ enabled = true, lookaheadCount = 4.0,
451
+ allowsCellularAccess = null
216
452
  )
217
453
  )
218
454
  assertNotNull(
@@ -230,7 +466,8 @@ class TrackPlayerLookaheadConfigTest {
230
466
 
231
467
  player.setLookaheadCacheInternal(
232
468
  LookaheadCacheConfig(
233
- enabled = true, lookaheadCount = 7.0
469
+ enabled = true, lookaheadCount = 7.0,
470
+ allowsCellularAccess = null
234
471
  )
235
472
  )
236
473
 
@@ -253,7 +490,7 @@ class TrackPlayerLookaheadConfigTest {
253
490
  val writer = player.lookaheadCacheWriter
254
491
  assertNotNull(writer)
255
492
 
256
- player.clearLookaheadCacheInternal()
493
+ runBlocking { player.clearLookaheadCacheInternal()?.join() }
257
494
 
258
495
  assertNotNull("cache stays — clear is wipe, not destroy", player.lookaheadCache)
259
496
  assertNotNull("writer stays — clear is wipe, not destroy", player.lookaheadCacheWriter)
@@ -269,7 +506,7 @@ class TrackPlayerLookaheadConfigTest {
269
506
  @Test
270
507
  fun `getLookaheadCacheStatus on a fresh player reports defaults`() {
271
508
  player.configureInternal(emptyConfig(), context())
272
- val status = player.getLookaheadCacheStatus()
509
+ val status = player.buildCacheStatusSnapshot()
273
510
  assertEquals(true, status.enabled)
274
511
  assertEquals(0.0, status.currentSizeMb, 0.001)
275
512
  assertEquals(100.0, status.maxSizeMb, 0.001)
@@ -282,10 +519,11 @@ class TrackPlayerLookaheadConfigTest {
282
519
  player.configureInternal(emptyConfig(), context())
283
520
  player.setLookaheadCacheInternal(
284
521
  LookaheadCacheConfig(
285
- enabled = false, lookaheadCount = 0.0
522
+ enabled = false, lookaheadCount = 0.0,
523
+ allowsCellularAccess = null
286
524
  )
287
525
  )
288
- val status = player.getLookaheadCacheStatus()
526
+ val status = player.buildCacheStatusSnapshot()
289
527
  assertEquals(false, status.enabled)
290
528
  assertEquals(0.0, status.currentSizeMb, 0.001)
291
529
  // maxSizeMb is the configure-time budget (100 MB default), reported
@@ -298,7 +536,7 @@ class TrackPlayerLookaheadConfigTest {
298
536
  @Test
299
537
  fun `re-configure with changed httpHeaders rebuilds the player (writer + cache)`() {
300
538
  player.configureInternal(
301
- PlayerConfig(httpHeaders = mapOf("X-Auth" to "token-1"), userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null),
539
+ PlayerConfig(httpHeaders = mapOf("X-Auth" to "token-1"), userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null),
302
540
  context()
303
541
  )
304
542
  val firstWriter = player.lookaheadCacheWriter
@@ -307,15 +545,14 @@ class TrackPlayerLookaheadConfigTest {
307
545
  assertNotNull(firstCache)
308
546
 
309
547
  player.configureInternal(
310
- PlayerConfig(httpHeaders = mapOf("X-Auth" to "token-2"), userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null),
548
+ PlayerConfig(httpHeaders = mapOf("X-Auth" to "token-2"), userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null),
311
549
  context()
312
550
  )
313
551
 
314
- // previously only the writer was
315
- // rebuilt, leaving the player's MediaSource.Factory bound to
316
- // the OLD headers. Now we rebuild the entire player (and the
317
- // cache + writer with it) so subsequent playback requests
318
- // carry the new auth. The on-disk cache directory is
552
+ // A header change on re-configure rebuilds the entire player (and
553
+ // the cache + writer with it), not just the writer, so the player's
554
+ // MediaSource.Factory and every subsequent playback request carry
555
+ // the new auth. The on-disk cache directory is
319
556
  // preserved across the rebuild — `SimpleCache.release()` only
320
557
  // frees the in-memory instance + file lock; entries stay on
321
558
  // disk and the next `LookaheadCache(...)` constructor reads
@@ -339,7 +576,7 @@ class TrackPlayerLookaheadConfigTest {
339
576
  // simulated here by `destroyInternal` between
340
577
  // `configureInternal` calls) yields a fresh writer instance,
341
578
  // even when headers are unchanged.
342
- val cfg = PlayerConfig(httpHeaders = mapOf("X-Auth" to "token"), userAgent = "rnqp/test", autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null)
579
+ val cfg = PlayerConfig(httpHeaders = mapOf("X-Auth" to "token"), userAgent = "rnqp/test", autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null)
343
580
  player.configureInternal(cfg, context())
344
581
  val firstWriter = player.lookaheadCacheWriter
345
582
 
@@ -369,7 +606,8 @@ class TrackPlayerLookaheadConfigTest {
369
606
  // Step 1: disable. Writer torn down, cache stays.
370
607
  player.setLookaheadCacheInternal(
371
608
  LookaheadCacheConfig(
372
- enabled = false, lookaheadCount = 3.0
609
+ enabled = false, lookaheadCount = 3.0,
610
+ allowsCellularAccess = null
373
611
  )
374
612
  )
375
613
  assertSame(firstCache, player.lookaheadCache)
@@ -379,7 +617,8 @@ class TrackPlayerLookaheadConfigTest {
379
617
  // same cache instance.
380
618
  player.setLookaheadCacheInternal(
381
619
  LookaheadCacheConfig(
382
- enabled = true, lookaheadCount = 7.0
620
+ enabled = true, lookaheadCount = 7.0,
621
+ allowsCellularAccess = null
383
622
  )
384
623
  )
385
624
  assertSame("re-enable reuses the same cache instance", firstCache, player.lookaheadCache)
@@ -402,9 +641,9 @@ class TrackPlayerLookaheadConfigTest {
402
641
  @Test
403
642
  fun `clearLookaheadCacheInternal then status snapshot reports zero size + empty currentlyCaching`() {
404
643
  player.configureInternal(emptyConfig(), context())
405
- player.clearLookaheadCacheInternal()
644
+ runBlocking { player.clearLookaheadCacheInternal()?.join() }
406
645
 
407
- val status = player.getLookaheadCacheStatus()
646
+ val status = player.buildCacheStatusSnapshot()
408
647
  assertEquals(true, status.enabled)
409
648
  assertEquals(0.0, status.currentSizeMb, 0.001)
410
649
  assertEquals(0.0, status.tracksFullyCached, 0.001)
@@ -413,5 +652,5 @@ class TrackPlayerLookaheadConfigTest {
413
652
 
414
653
  // --- Helpers
415
654
 
416
- private fun emptyConfig() = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null)
655
+ private fun emptyConfig() = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null)
417
656
  }
@@ -35,7 +35,9 @@ class TrackPlayerMutationTest {
35
35
  private fun track(id: String) = TrackItem(
36
36
  id = id, url = "https://example.com/$id.m4a",
37
37
  title = null, artist = null, album = null,
38
- duration = null, artworkUrl = null, extras = null
38
+ duration = null, artworkUrl = null, extras = null,
39
+ replayGainTrackGain = null, replayGainTrackPeak = null,
40
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
39
41
  )
40
42
 
41
43
  private lateinit var serviceHandle: PlaybackServiceTestHandle
@@ -45,7 +47,7 @@ class TrackPlayerMutationTest {
45
47
  player = TrackPlayer()
46
48
  serviceHandle = bindPlaybackServiceForTest(player, context())
47
49
  player.configureInternal(
48
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
50
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
49
51
  )
50
52
  player.setQueueInternal((1..5).map { track("t$it") })
51
53
  // cur defaults to 0 after setQueue; tests needing mid-queue
@@ -127,7 +129,7 @@ class TrackPlayerMutationTest {
127
129
  val freshHandle = bindPlaybackServiceForTest(fresh, context())
128
130
  try {
129
131
  fresh.configureInternal(
130
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
132
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
131
133
  )
132
134
  assertEquals(-1, fresh.currentTrackIndex)
133
135
  assertEquals(Player.STATE_IDLE, fresh.player!!.playbackState)
@@ -34,7 +34,8 @@ class TrackPlayerProgressThrottleTest {
34
34
  private fun config(foregroundMs: Double?, backgroundMs: Double? = null) = PlayerConfig(
35
35
  httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null,
36
36
  networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null,
37
- visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null,
37
+ voiceVocabularyDonationEnabled = null,
38
+ visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null,
38
39
  progressUpdateIntervalMs = foregroundMs, backgroundProgressUpdateIntervalMs = backgroundMs, placeholderArtworkUri = null, skipToPreviousBehavior = null
39
40
  )
40
41
 
@@ -28,9 +28,11 @@ class TrackPlayerQueueChangeTest {
28
28
  private lateinit var serviceHandle: PlaybackServiceTestHandle
29
29
 
30
30
  private data class QueueEvent(
31
+ /** The mirror after applying this event's delta. */
31
32
  val queue: List<TrackItem>,
32
33
  val index: Int,
33
34
  val reason: QueueChangeReason,
35
+ val delta: QueueChangeDelta,
34
36
  )
35
37
 
36
38
  private fun context() =
@@ -40,6 +42,8 @@ class TrackPlayerQueueChangeTest {
40
42
  id = id, url = "https://example.com/$id.m4a",
41
43
  title = null, artist = null, album = null,
42
44
  duration = null, artworkUrl = null, extras = null,
45
+ replayGainTrackGain = null, replayGainTrackPeak = null,
46
+ replayGainAlbumGain = null, replayGainAlbumPeak = null,
43
47
  )
44
48
 
45
49
  @Before
@@ -47,7 +51,7 @@ class TrackPlayerQueueChangeTest {
47
51
  player = TrackPlayer()
48
52
  serviceHandle = bindPlaybackServiceForTest(player, context())
49
53
  player.configureInternal(
50
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
54
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
51
55
  )
52
56
  player.setQueueInternal((1..5).map { track("t$it") })
53
57
  }
@@ -62,12 +66,90 @@ class TrackPlayerQueueChangeTest {
62
66
  * therefore not captured — onQueueChange does not auto-fire). */
63
67
  private fun capture(): Pair<MutableList<QueueEvent>, () -> Unit> {
64
68
  val events = mutableListOf<QueueEvent>()
65
- val dispose = player.onQueueChange { queue, idx, reason ->
66
- events.add(QueueEvent(queue.toList(), idx.toInt(), reason))
69
+ // Mirror the queue by applying each delta, the way a consumer must. Every
70
+ // assertion below reads that mirror, so it doubles as proof the delta
71
+ // carries enough to reconstruct the queue.
72
+ var mirror = player.tracks.toList()
73
+ val dispose = player.onQueueChange { delta, idx, reason ->
74
+ mirror = applyDelta(mirror, delta, reason)
75
+ events.add(QueueEvent(mirror, idx.toInt(), reason, delta))
67
76
  }
68
77
  return events to dispose
69
78
  }
70
79
 
80
+ /** The reference delta application, mirroring what `useQueue()` does in JS. */
81
+ private fun applyDelta(
82
+ previous: List<TrackItem>,
83
+ delta: QueueChangeDelta,
84
+ reason: QueueChangeReason
85
+ ): List<TrackItem> = when (reason) {
86
+ QueueChangeReason.SET_QUEUE -> delta.inserted.toList()
87
+ QueueChangeReason.CLEAR -> emptyList()
88
+ QueueChangeReason.ADD -> previous.toMutableList().apply {
89
+ addAll(delta.insertedAt.toInt(), delta.inserted.toList())
90
+ }
91
+ QueueChangeReason.REMOVE -> {
92
+ val drop = delta.removed.map { it.toInt() }.toSet()
93
+ previous.filterIndexed { i, _ -> i !in drop }
94
+ }
95
+ QueueChangeReason.MOVE -> previous.toMutableList().apply {
96
+ add(delta.movedTo.toInt(), removeAt(delta.movedFrom.toInt()))
97
+ }
98
+ QueueChangeReason.SHUFFLE -> delta.order.map { previous[it.toInt()] }
99
+ }
100
+
101
+ @Test
102
+ fun `a mirror built only from deltas matches the live queue`() {
103
+ val (events, dispose) = capture()
104
+ player.setQueueInternal((1..6).map { track("s$it") }, startAtIndex = 0)
105
+ player.addToQueueInternal(listOf(track("added")), insertBefore = 2)
106
+ player.removeFromQueueInternal(listOf(4, 0))
107
+ player.moveInQueueInternal(3, 1)
108
+ dispose()
109
+
110
+ assertEquals(
111
+ "the delta stream must reconstruct the queue exactly",
112
+ player.tracks.map { it.id },
113
+ events.last().queue.map { it.id }
114
+ )
115
+ }
116
+
117
+ @Test
118
+ fun `each emit advances the revision by one`() {
119
+ val (events, dispose) = capture()
120
+ player.setQueueInternal((1..3).map { track("r$it") })
121
+ player.addToQueueInternal(listOf(track("r4")), insertBefore = null)
122
+ player.clearQueueInternal()
123
+ dispose()
124
+
125
+ val revisions = events.map { it.delta.revision.toInt() }
126
+ val first = revisions.first()
127
+ assertEquals(revisions.indices.map { first + it }, revisions)
128
+ }
129
+
130
+ /**
131
+ * `destroy` and `configure` clear the queue without an emit; the revision
132
+ * the next emit carries skips one, which is the signal a delta mirror
133
+ * re-reads on.
134
+ */
135
+ @Test
136
+ fun `a silent clear leaves a revision gap for the next emit`() {
137
+ val (events, dispose) = capture()
138
+ player.setQueueInternal((1..3).map { track("g$it") })
139
+ val before = events.last().delta.revision.toInt()
140
+
141
+ player.destroyInternal()
142
+ serviceHandle.destroy()
143
+ serviceHandle = bindPlaybackServiceForTest(player, context())
144
+ player.configureInternal(
145
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
146
+ )
147
+ player.setQueueInternal(listOf(track("after")))
148
+ dispose()
149
+
150
+ assertEquals(before + 2, events.last().delta.revision.toInt())
151
+ }
152
+
71
153
  // --- one emission per mutation, with the post-mutation snapshot
72
154
 
73
155
  @Test