react-native-queue-player 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/build.gradle +7 -0
  4. package/android/consumer-rules.pro +72 -3
  5. package/android/src/main/cpp/airplay2_control.c +4 -4
  6. package/android/src/main/cpp/airplay2_control.h +4 -4
  7. package/android/src/main/cpp/airplay2_jni.cpp +26 -22
  8. package/android/src/main/cpp/airplay2_pair.c +7 -7
  9. package/android/src/main/cpp/airplay2_pair.h +2 -2
  10. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  11. package/android/src/main/cpp/airplay2_session.c +2 -2
  12. package/android/src/main/cpp/airplay_jni.cpp +20 -72
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
  61. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  62. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  63. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  64. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  65. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
  114. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
  115. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  116. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  117. package/app.plugin.js +17 -1
  118. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  119. package/ios/AVQueueBuilder.swift +120 -85
  120. package/ios/ArtworkLoader.swift +0 -7
  121. package/ios/ArtworkResolver.swift +22 -4
  122. package/ios/AssetReplayGainReader.swift +118 -0
  123. package/ios/AudioSession.swift +67 -16
  124. package/ios/AudioTapProvider.swift +284 -149
  125. package/ios/CarPlayBridge.swift +0 -12
  126. package/ios/CarPlayCoordinator.swift +18 -2
  127. package/ios/CarPlaySceneDelegate.swift +2 -2
  128. package/ios/Cast/AirPlayRouteState.swift +9 -8
  129. package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
  130. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  131. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  132. package/ios/Cast/Core/CastSession.swift +11 -0
  133. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  134. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  135. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  136. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  137. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  138. package/ios/CastManager.swift +1 -1
  139. package/ios/CrossfadeEngine.swift +660 -226
  140. package/ios/EQTap.swift +20 -17
  141. package/ios/Equalizer.swift +1 -1
  142. package/ios/FLACStreamInfo.swift +65 -0
  143. package/ios/GaplessEngine.swift +57 -30
  144. package/ios/InputGuards.swift +35 -5
  145. package/ios/LookaheadCache.swift +202 -58
  146. package/ios/LookaheadCachePrefetcher.swift +48 -36
  147. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  148. package/ios/NetworkRecoveryPolicy.swift +32 -0
  149. package/ios/NowPlayingFormatExtractor.swift +25 -18
  150. package/ios/NowPlayingInfo.swift +84 -48
  151. package/ios/OutputRouteMonitor.swift +16 -3
  152. package/ios/PendingBrowseRequests.swift +1 -1
  153. package/ios/PlaceholderArtwork.swift +9 -5
  154. package/ios/PlaybackEngine.swift +165 -76
  155. package/ios/PlaybackErrorMapping.swift +3 -3
  156. package/ios/PlaybackModeStateMachine.swift +2 -2
  157. package/ios/PlaybackNetworkMonitor.swift +73 -0
  158. package/ios/PlayerStateDerivation.swift +6 -6
  159. package/ios/QueueMutationArithmetic.swift +25 -0
  160. package/ios/QueueSkipArithmetic.swift +5 -3
  161. package/ios/QueueWindowArithmetic.swift +152 -0
  162. package/ios/ReadThroughServer.swift +283 -0
  163. package/ios/RemoteCommands.swift +44 -4
  164. package/ios/ReplayGainData.swift +22 -3
  165. package/ios/ReplayGainExtractor.swift +70 -27
  166. package/ios/Siri/VoiceDonation.swift +14 -9
  167. package/ios/StreamingBitrateProbe.swift +22 -68
  168. package/ios/SuppliedReplayGain.swift +83 -0
  169. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  170. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  171. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  172. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  173. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  174. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  175. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  176. package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
  177. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  178. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  179. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  180. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  181. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  182. package/ios/Tests/EngineSwapTests.swift +91 -0
  183. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  184. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  185. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  186. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  187. package/ios/Tests/InputGuardsTests.swift +41 -0
  188. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  189. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  190. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  191. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  192. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  193. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  194. package/ios/Tests/MutationDeferralTests.swift +220 -0
  195. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  196. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  197. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  198. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  199. package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
  200. package/ios/Tests/PlayerFixtures.swift +48 -0
  201. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  202. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  203. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  204. package/ios/Tests/QueueStateTests.swift +3 -1
  205. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  206. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  207. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  208. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  209. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  210. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  211. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  212. package/ios/Tests/RetryRecoveryTests.swift +303 -0
  213. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  214. package/ios/Tests/SkipIndexTests.swift +32 -2
  215. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  216. package/ios/Tests/StallRecoveryTests.swift +97 -0
  217. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  218. package/ios/Tests/TopUpWindowGateTests.swift +394 -0
  219. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  220. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  221. package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
  222. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  223. package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
  224. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  225. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  226. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  227. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  228. package/ios/TrackPlayer+Automotive.swift +101 -0
  229. package/ios/TrackPlayer+Cache.swift +232 -0
  230. package/ios/TrackPlayer+Config.swift +291 -0
  231. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  232. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  233. package/ios/TrackPlayer+EventsDispatch.swift +968 -0
  234. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  235. package/ios/TrackPlayer+Lifecycle.swift +930 -0
  236. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  237. package/ios/TrackPlayer+Queue.swift +940 -0
  238. package/ios/TrackPlayer+Recovery.swift +196 -0
  239. package/ios/TrackPlayer+Skip.swift +455 -0
  240. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  241. package/ios/TrackPlayer+State.swift +108 -0
  242. package/ios/TrackPlayer+Threading.swift +135 -0
  243. package/ios/TrackPlayer+Transport.swift +267 -0
  244. package/ios/TrackPlayer+Window.swift +195 -0
  245. package/ios/TrackPlayer.swift +305 -4610
  246. package/ios/Visualizer.swift +6 -5
  247. package/ios/tests-harness/Podfile +1 -1
  248. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  249. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  250. package/lib/module/hooks/useActiveTrack.js +29 -22
  251. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  252. package/lib/module/hooks/useCast.js +8 -26
  253. package/lib/module/hooks/useCast.js.map +1 -1
  254. package/lib/module/hooks/useEqualizer.js +19 -12
  255. package/lib/module/hooks/useEqualizer.js.map +1 -1
  256. package/lib/module/hooks/useLookaheadCache.js +3 -7
  257. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  258. package/lib/module/hooks/useQueue.js +66 -19
  259. package/lib/module/hooks/useQueue.js.map +1 -1
  260. package/lib/module/index.js +6 -3
  261. package/lib/module/index.js.map +1 -1
  262. package/lib/module/queueDelta.js +41 -0
  263. package/lib/module/queueDelta.js.map +1 -0
  264. package/lib/module/types.js +28 -5
  265. package/lib/module/types.js.map +1 -1
  266. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  267. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  269. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useCast.d.ts +6 -1
  271. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  272. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  273. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  274. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  275. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  276. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  277. package/lib/typescript/index.d.ts +2 -1
  278. package/lib/typescript/index.d.ts.map +1 -1
  279. package/lib/typescript/queueDelta.d.ts +10 -0
  280. package/lib/typescript/queueDelta.d.ts.map +1 -0
  281. package/lib/typescript/types.d.ts +168 -12
  282. package/lib/typescript/types.d.ts.map +1 -1
  283. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  284. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  285. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  286. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  287. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  288. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  289. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  320. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  321. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  322. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  323. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  324. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  325. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  326. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  327. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  328. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  329. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  330. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  331. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  332. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  333. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  334. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  335. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  336. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  337. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  338. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  339. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  340. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  341. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  342. package/package.json +8 -8
  343. package/src/TrackPlayer.nitro.ts +53 -27
  344. package/src/hooks/useActiveTrack.ts +29 -22
  345. package/src/hooks/useCast.ts +14 -15
  346. package/src/hooks/useEqualizer.ts +19 -12
  347. package/src/hooks/useLookaheadCache.ts +3 -7
  348. package/src/hooks/useQueue.ts +66 -17
  349. package/src/index.ts +13 -3
  350. package/src/queueDelta.ts +41 -0
  351. package/src/types.ts +169 -12
  352. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  353. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
  354. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  355. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  356. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  357. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  358. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  359. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  360. package/ios/MetadataReader.swift +0 -555
  361. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  362. package/ios/Tests/MetadataReaderTests.swift +0 -603
  363. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  364. package/ios/Tests/VoiceDonationTests.swift +0 -26
  365. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  366. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  367. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -0,0 +1,70 @@
1
+ package com.margelo.nitro.queueplayer
2
+
3
+ import android.content.Context
4
+ import androidx.media3.common.PlaybackException
5
+ import androidx.media3.common.util.UnstableApi
6
+ import androidx.test.core.app.ApplicationProvider
7
+ import org.junit.Assert.assertEquals
8
+ import org.junit.Test
9
+ import org.junit.runner.RunWith
10
+ import org.robolectric.RobolectricTestRunner
11
+ import org.robolectric.Shadows.shadowOf
12
+ import org.robolectric.annotation.Config
13
+
14
+ /**
15
+ * When a player reaches STATE_ENDED the engines hand the delegate the end
16
+ * signal before the generic state callback, so the queue-end path owns the
17
+ * ENDED emit and its reason.
18
+ */
19
+ @RunWith(RobolectricTestRunner::class)
20
+ @Config(sdk = [34])
21
+ @UnstableApi
22
+ class EngineEndSignalOrderTest {
23
+
24
+ private val context: Context = ApplicationProvider.getApplicationContext()
25
+
26
+ private class OrderingDelegate : PlaybackEngineDelegate {
27
+ val calls = mutableListOf<String>()
28
+ override fun onStateMaybeChanged(engine: PlaybackEngine, playWhenReadyReason: Int?) { calls.add("state") }
29
+ override fun onPlaybackEnded(engine: PlaybackEngine) { calls.add("ended") }
30
+ override fun onIsPlayingChanged(engine: PlaybackEngine, isPlaying: Boolean) {}
31
+ override fun onTrackTransition(engine: PlaybackEngine, platformReason: Int) {}
32
+ override fun onActiveItemFormatChanged(engine: PlaybackEngine) {}
33
+ override fun onError(engine: PlaybackEngine, exception: PlaybackException, failedIndex: Int) {}
34
+ override fun onAutoTransitionDiscontinuity(engine: PlaybackEngine) {}
35
+ override fun onCrossfadeBegin(engine: PlaybackEngine, incomingIndex: Int) {}
36
+ override fun onCrossfadeCancel(engine: PlaybackEngine, revertedIndex: Int) {}
37
+ }
38
+
39
+ /** An empty playlist prepared is STATE_ENDED at once. */
40
+ private fun assertEndedBeforeState(engine: PlaybackEngine, delegate: OrderingDelegate) {
41
+ engine.setMediaItems(emptyList(), 0, 0L)
42
+ shadowOf(android.os.Looper.getMainLooper()).idle()
43
+ val ended = delegate.calls.indexOf("ended")
44
+ val stateAfter = delegate.calls.withIndex().any { it.value == "state" && it.index > ended }
45
+ assertEquals("the end signal must be delivered", true, ended >= 0)
46
+ assertEquals(
47
+ "the generic state callback follows the end signal: ${delegate.calls}",
48
+ true, stateAfter)
49
+ assertEquals(
50
+ "no state callback precedes the end signal for this transition: ${delegate.calls}",
51
+ -1, delegate.calls.subList(0, ended).lastIndexOf("state"))
52
+ engine.release()
53
+ }
54
+
55
+ @Test
56
+ fun `gapless reports the end before the state`() {
57
+ val delegate = OrderingDelegate()
58
+ val engine = GaplessEngine(context = context)
59
+ engine.delegate = delegate
60
+ assertEndedBeforeState(engine, delegate)
61
+ }
62
+
63
+ @Test
64
+ fun `crossfade reports the end before the state`() {
65
+ val delegate = OrderingDelegate()
66
+ val engine = CrossfadeEngine(context = context, crossfadeDurationMs = 3_000L)
67
+ engine.delegate = delegate
68
+ assertEndedBeforeState(engine, delegate)
69
+ }
70
+ }
@@ -4,7 +4,6 @@ import org.junit.Assert.assertArrayEquals
4
4
  import org.junit.Assert.assertEquals
5
5
  import org.junit.Assert.assertFalse
6
6
  import org.junit.Assert.assertTrue
7
- import org.junit.Assume.assumeTrue
8
7
  import org.junit.Test
9
8
  import org.junit.runner.RunWith
10
9
  import org.robolectric.RobolectricTestRunner
@@ -168,7 +167,7 @@ class EqualizerEngineTest {
168
167
  fun `reattach on same session is idempotent`() {
169
168
  val engine = EqualizerEngine()
170
169
  val first = engine.attach(audioSessionId = 123, channelCount = 2)
171
- assumeTrue("Robolectric audio-effect shadow refused first attach", first)
170
+ assertTrue("first attach succeeds", first)
172
171
  val second = engine.attach(audioSessionId = 123, channelCount = 6)
173
172
  assertTrue("repeat attach on the same session succeeds", second)
174
173
  assertTrue(
@@ -181,7 +180,7 @@ class EqualizerEngineTest {
181
180
  fun `attachAll holds multiple sessions simultaneously for crossfade`() {
182
181
  val engine = EqualizerEngine()
183
182
  val results = engine.attachAll(intArrayOf(101, 102))
184
- assumeTrue("Robolectric audio-effect shadow refused attach", results[101] == true)
183
+ assertTrue("session attaches", results[101] == true)
185
184
  assertTrue("incoming session also attached", results[102] == true)
186
185
  assertTrue("at least one session held", engine.isAttached())
187
186
  }
@@ -190,7 +189,7 @@ class EqualizerEngineTest {
190
189
  fun `attachAll diff releases sessions no longer in the input array`() {
191
190
  val engine = EqualizerEngine()
192
191
  val first = engine.attachAll(intArrayOf(201, 202))
193
- assumeTrue("Robolectric audio-effect shadow refused attach", first[201] == true)
192
+ assertTrue("session attaches", first[201] == true)
194
193
  val second = engine.attachAll(intArrayOf(202))
195
194
  // 201 dropped, 202 retained.
196
195
  assertTrue("retained session marked succeeded", second[202] == true)
@@ -201,22 +200,34 @@ class EqualizerEngineTest {
201
200
  fun `detachAll releases every per-session instance`() {
202
201
  val engine = EqualizerEngine()
203
202
  val results = engine.attachAll(intArrayOf(301, 302))
204
- assumeTrue("Robolectric audio-effect shadow refused attach", results[301] == true)
203
+ assertTrue("session attaches", results[301] == true)
205
204
  engine.detachAll()
206
205
  assertTrue("no sessions held after detachAll", !engine.isAttached())
207
206
  }
208
207
 
209
208
  @Test
210
- fun `setBandGain fans gain across all attached sessions`() {
209
+ fun `setBandGain buffers the gain while sessions are attached`() {
211
210
  val engine = EqualizerEngine()
212
211
  val results = engine.attachAll(intArrayOf(401, 402))
213
- assumeTrue("Robolectric audio-effect shadow refused attach", results[401] == true)
212
+ assertTrue("session attaches", results[401] == true)
214
213
  engine.setBandGain(3, 4f)
215
214
  assertEquals(4f, engine.getBandGains()[3])
216
- // The buffered gain mirrors what each per-session instance
217
- // received via setPreEqBandAllChannelsTo. Direct hardware
218
- // verification requires a real device Robolectric's audio-
219
- // effect shadow doesn't expose the per-band gain readback —
220
- // so this asserts the lib-side state mirrors the call.
215
+ }
216
+
217
+ /** A failed attach leaves no trace: nothing attached, and a retry is possible. */
218
+ @Test
219
+ @Config(sdk = [34], shadows = [ShadowDynamicsProcessingRejectingEnable::class])
220
+ fun `attach that fails while configuring leaves the engine detached`() {
221
+ val engine = EqualizerEngine()
222
+
223
+ val attached = engine.attach(audioSessionId = 801, channelCount = 2)
224
+
225
+ assertFalse("the attach reports its failure", attached)
226
+ assertFalse("nothing is attached after a failed configure", engine.isAttached())
227
+ assertEquals(
228
+ "the failed session is not carried as attached on the next diff",
229
+ mapOf(801 to false),
230
+ engine.attachAll(intArrayOf(801))
231
+ )
221
232
  }
222
233
  }
@@ -232,11 +232,6 @@ private class FakePlaybackEngine : PlaybackEngine {
232
232
  override val mediaSessionPlayer: androidx.media3.exoplayer.ExoPlayer
233
233
  get() = error("not used in test")
234
234
 
235
- override suspend fun prepare(
236
- mediaItems: List<androidx.media3.common.MediaItem>,
237
- startIndex: Int,
238
- startPositionMs: Long,
239
- ) = error("not used in test")
240
235
  override fun release() = error("not used in test")
241
236
  override fun setMediaItems(items: List<androidx.media3.common.MediaItem>, startIndex: Int, startPositionMs: Long) = error("not used in test")
242
237
  override fun addMediaItems(items: List<androidx.media3.common.MediaItem>, insertBefore: Int) = error("not used in test")
@@ -268,6 +263,5 @@ private class FakePlaybackEngine : PlaybackEngine {
268
263
  }
269
264
  override fun attachAudioBufferSink(sink: androidx.media3.exoplayer.audio.TeeAudioProcessor.AudioBufferSink) = error("not used in test")
270
265
  override fun detachAudioBufferSink(sink: androidx.media3.exoplayer.audio.TeeAudioProcessor.AudioBufferSink) = error("not used in test")
271
- override suspend fun handoff(to: PlaybackEngine, atTrackBoundary: Boolean) = error("not used in test")
272
266
  override var delegate: PlaybackEngineDelegate? = null
273
267
  }
@@ -4,20 +4,22 @@ import org.junit.Assert.assertArrayEquals
4
4
  import org.junit.Assert.assertEquals
5
5
  import org.junit.Assert.assertFalse
6
6
  import org.junit.Assert.assertTrue
7
+ import android.media.audiofx.Equalizer
7
8
  import org.junit.Test
8
9
  import org.junit.runner.RunWith
9
10
  import org.robolectric.RobolectricTestRunner
11
+ import org.robolectric.shadow.api.Shadow
10
12
  import org.robolectric.annotation.Config
13
+ import org.robolectric.shadows.ShadowAudioEffect
11
14
 
12
15
  /**
13
- * State-mirror coverage for [EqualizerLegacyEngine] on the API <28 path.
14
- * Robolectric's audio-effect shadow doesn't model the hardware-side
15
- * band centre frequencies + gain range; the band-mapping + dB↔mB
16
- * conversion are the invariants this covers. Legacy AudioEffect
17
- * behaviour varies per OEM.
16
+ * State-mirror coverage for [EqualizerLegacyEngine] on the API <28 path:
17
+ * the band mapping, the dB↔mB conversion, and what reaches each attached
18
+ * session. [ShadowEqualizer] supplies the hardware-side band count, centre
19
+ * frequencies and gain range that legacy AudioEffect varies per OEM.
18
20
  */
19
21
  @RunWith(RobolectricTestRunner::class)
20
- @Config(sdk = [27])
22
+ @Config(sdk = [27], shadows = [ShadowEqualizer::class])
21
23
  class EqualizerLegacyEngineTest {
22
24
 
23
25
  @Test
@@ -181,9 +183,7 @@ class EqualizerLegacyEngineTest {
181
183
  fun `attachAll on empty array detaches every active instance`() {
182
184
  val engine = EqualizerLegacyEngine()
183
185
  val first = engine.attachAll(intArrayOf(501, 502))
184
- org.junit.Assume.assumeTrue(
185
- "Robolectric audio-effect shadow refused first attach", first[501] == true,
186
- )
186
+ assertEquals("both sessions attach", mapOf(501 to true, 502 to true), first)
187
187
  val second = engine.attachAll(intArrayOf())
188
188
  assertEquals("empty array clears the map", 0, second.size)
189
189
  assertFalse("no sessions held after empty attachAll", engine.isAttached())
@@ -193,9 +193,7 @@ class EqualizerLegacyEngineTest {
193
193
  fun `detachAll releases every per-session instance`() {
194
194
  val engine = EqualizerLegacyEngine()
195
195
  val results = engine.attachAll(intArrayOf(601, 602))
196
- org.junit.Assume.assumeTrue(
197
- "Robolectric audio-effect shadow refused attach", results[601] == true,
198
- )
196
+ assertEquals("both sessions attach", mapOf(601 to true, 602 to true), results)
199
197
  engine.detachAll()
200
198
  assertFalse("no sessions held after detachAll", engine.isAttached())
201
199
  }
@@ -204,10 +202,40 @@ class EqualizerLegacyEngineTest {
204
202
  fun `setBandGain fans gain across every attached session`() {
205
203
  val engine = EqualizerLegacyEngine()
206
204
  val results = engine.attachAll(intArrayOf(701, 702))
207
- org.junit.Assume.assumeTrue(
208
- "Robolectric audio-effect shadow refused attach", results[701] == true,
209
- )
205
+ assertEquals("both sessions attach", mapOf(701 to true, 702 to true), results)
206
+
210
207
  engine.setBandGain(4, 5f)
208
+
211
209
  assertEquals("buffered band gain mirrors the call", 5f, engine.getBandGains()[4])
210
+ // Our 500 Hz band maps onto the shadow's 230 Hz legacy band, and +5 dB
211
+ // is 500 mB against a ±1500 mB device range.
212
+ val legacyBand: Short = 1
213
+ assertEquals("session 701 receives the gain", 500, levelFor(701, legacyBand))
214
+ assertEquals("session 702 receives the gain", 500, levelFor(702, legacyBand))
215
+ }
216
+
217
+ /** Band level the session's own equalizer reports, in millibels. */
218
+ private fun levelFor(sessionId: Int, band: Short): Int {
219
+ val effect = ShadowAudioEffect.getAudioEffects().firstOrNull {
220
+ Shadow.extract<ShadowEqualizer>(it).audioSession == sessionId
221
+ } ?: error("no equalizer attached for session $sessionId")
222
+ return (effect as Equalizer).getBandLevel(band).toInt()
223
+ }
224
+
225
+ /** A failed attach leaves no trace: nothing attached, and a retry is possible. */
226
+ @Test
227
+ @Config(sdk = [27], shadows = [ShadowEqualizerRejectingBandWrites::class])
228
+ fun `attach that fails while configuring leaves the engine detached`() {
229
+ val engine = EqualizerLegacyEngine()
230
+
231
+ val attached = engine.attach(audioSessionId = 801, channelCount = 2)
232
+
233
+ assertFalse("the attach reports its failure", attached)
234
+ assertFalse("nothing is attached after a failed configure", engine.isAttached())
235
+ assertEquals(
236
+ "the failed session is not carried as attached on the next diff",
237
+ mapOf(801 to false),
238
+ engine.attachAll(intArrayOf(801))
239
+ )
212
240
  }
213
241
  }
@@ -181,4 +181,19 @@ class FFTProcessorTeeTest {
181
181
  assertEquals(expectedPosition, original.position())
182
182
  assertEquals(expectedOrder, original.order())
183
183
  }
184
+
185
+ /** A flush resets the emit throttle, so the first frame of a new stream is not delayed. */
186
+ @Test
187
+ fun `flush resets the emit throttle`() {
188
+ val reported = mutableListOf<Int>()
189
+ val tee = makeTee(fftSize = 256, intervalMs = 1000) { _, _, sampleRate -> reported += sampleRate }
190
+ tee.flush(48000, 1, C.ENCODING_PCM_16BIT)
191
+ tee.handleBuffer(pcm16Buffer(ShortArray(256) { 1000 }))
192
+ assertEquals(listOf(48000), reported)
193
+
194
+ // Well inside the 1000 ms window: without the reset this emit is throttled.
195
+ tee.flush(96000, 1, C.ENCODING_PCM_16BIT)
196
+ tee.handleBuffer(pcm16Buffer(ShortArray(256) { 1000 }))
197
+ assertEquals(listOf(48000, 96000), reported)
198
+ }
184
199
  }
@@ -13,8 +13,10 @@ import java.io.File
13
13
  /**
14
14
  * Unit tests for [FifoCacheEvictor]'s eviction ordering. Drives the evictor's
15
15
  * [CacheEvictor] callbacks directly against a minimal fake [Cache] whose only
16
- * live method is `removeSpan` (delegates back to `onSpanRemoved`), so the tests
17
- * assert insertion-order eviction without a real SimpleCache or filesystem.
16
+ * live method is `removeSpan`, so the tests assert insertion-order eviction
17
+ * without a real SimpleCache or filesystem. The fake normally delegates back to
18
+ * `onSpanRemoved`; `callsBack = false` models a span the cache index has already
19
+ * dropped, which notifies nothing.
18
20
  */
19
21
  @UnstableApi
20
22
  class FifoCacheEvictorTest {
@@ -72,11 +74,140 @@ class FifoCacheEvictorTest {
72
74
  assertEquals(listOf("b"), removed.map { it.key })
73
75
  }
74
76
 
77
+ @Test
78
+ fun `a url retires its insertion order once its last span is gone`() {
79
+ // The cache notifies an add even when the evictor's own set rejects the
80
+ // span, so a count taken from the callbacks over-counts. Here both adds are
81
+ // the same span, so one entry exists and one removal empties the url.
82
+ val evictor = FifoCacheEvictor(maxBytes = 1000)
83
+ val cache = FakeCache(evictor)
84
+ val only = CacheSpan("a", 0, 100, 1L, File("/tmp/a"))
85
+ evictor.onSpanAdded(cache, only)
86
+ evictor.onSpanAdded(cache, only)
87
+ evictor.onSpanAdded(cache, span("b", length = 100))
88
+ evictor.onSpanRemoved(cache, only)
89
+ removed.clear()
90
+
91
+ // `a` is empty, so re-adding it stamps a fresh newest order and `b` — now
92
+ // the oldest — is the victim. An over-count leaves `a` on its old order and
93
+ // evicts `a` instead.
94
+ evictor.onSpanAdded(cache, span("a", length = 100))
95
+ evictor.onSpanAdded(cache, span("c", length = 900)) // 1100 > maxBytes
96
+ assertEquals(listOf("b"), removed.map { it.key })
97
+ }
98
+
99
+ @Test
100
+ fun `a url keeps its insertion order while any of its spans remain`() {
101
+ val evictor = FifoCacheEvictor(maxBytes = 1000)
102
+ val cache = FakeCache(evictor)
103
+ val first = CacheSpan("a", 0, 100, 1L, File("/tmp/a-1"))
104
+ val second = CacheSpan("a", 100, 100, 1L, File("/tmp/a-2"))
105
+ evictor.onSpanAdded(cache, first)
106
+ evictor.onSpanAdded(cache, second)
107
+ evictor.onSpanAdded(cache, second) // rejected by the set, still notified
108
+ evictor.onSpanAdded(cache, span("b", length = 100))
109
+ evictor.onSpanRemoved(cache, first)
110
+ removed.clear()
111
+
112
+ // `a` still holds `second`, so it keeps its original (oldest) order and is
113
+ // still the first victim. A retired order would sort it newest and evict b.
114
+ evictor.onSpanAdded(cache, span("c", length = 900))
115
+ assertEquals("a", removed.first().key)
116
+ removed.clear()
117
+
118
+ // With `a` now empty, a re-add stamps a fresh newest order. An over-count
119
+ // from the rejected add leaves `a` on its old order and evicts it again.
120
+ evictor.onSpanRemoved(cache, second)
121
+ evictor.onSpanAdded(cache, span("a", length = 100))
122
+ evictor.onSpanAdded(cache, span("d", length = 900))
123
+ assertEquals("b", removed.first().key)
124
+ }
125
+
126
+ @Test(timeout = 5_000)
127
+ fun `eviction advances past a span the cache no longer holds`() {
128
+ // removeSpan is a no-op here, modelling a span the cache index has already
129
+ // dropped: no onSpanRemoved callback, so the head does not move on its own.
130
+ // The evictor must retire its own record and carry on — spinning wedges the
131
+ // cache's lock, and giving up makes that span the victim of every later
132
+ // eviction, which stops eviction permanently.
133
+ val evictor = FifoCacheEvictor(maxBytes = 150)
134
+ val deaf = FakeCache(evictor, callsBack = false)
135
+ evictor.onSpanAdded(deaf, span("a", length = 100))
136
+ evictor.onSpanAdded(deaf, span("b", length = 100))
137
+ evictor.onSpanAdded(deaf, span("c", length = 100))
138
+ assertEquals(listOf("a", "b"), removed.map { it.key })
139
+ }
140
+
141
+ @Test
142
+ fun `a protected url survives eviction and the next-oldest goes instead`() {
143
+ val evictor = FifoCacheEvictor(maxBytes = 250)
144
+ val cache = FakeCache(evictor)
145
+ evictor.setProtectedKeys(setOf("a"))
146
+ evictor.onSpanAdded(cache, span("a", length = 100))
147
+ evictor.onSpanAdded(cache, span("b", length = 100))
148
+ evictor.onSpanAdded(cache, span("c", length = 100)) // 300 > 250
149
+ assertEquals(listOf("b"), removed.map { it.key })
150
+ }
151
+
152
+ @Test(timeout = 5_000)
153
+ fun `eviction gives up when every remaining url is protected`() {
154
+ // Over budget with nothing evictable is a real state: the protected window
155
+ // can outgrow the budget. Staying over budget is the outcome; spinning
156
+ // under the cache's lock is not.
157
+ val evictor = FifoCacheEvictor(maxBytes = 150)
158
+ val cache = FakeCache(evictor)
159
+ evictor.setProtectedKeys(setOf("a", "b"))
160
+ evictor.onSpanAdded(cache, span("a", length = 100))
161
+ evictor.onSpanAdded(cache, span("b", length = 100)) // 200 > 150, both shielded
162
+ assertEquals(emptyList<String>(), removed.map { it.key })
163
+ }
164
+
165
+ @Test(timeout = 5_000)
166
+ fun `a protected span ahead of the victim does not stall the self-heal`() {
167
+ // The victim is not the head here: `a` is protected and sorts first, so a
168
+ // progress check that compared the head would never see the removal fail
169
+ // and would spin under the cache's lock.
170
+ val evictor = FifoCacheEvictor(maxBytes = 150)
171
+ val deaf = FakeCache(evictor, callsBack = false)
172
+ evictor.setProtectedKeys(setOf("a"))
173
+ evictor.onSpanAdded(deaf, span("a", length = 100))
174
+ evictor.onSpanAdded(deaf, span("b", length = 100)) // 200 > 150; a shielded
175
+ assertEquals(listOf("b"), removed.map { it.key })
176
+ }
177
+
178
+ @Test
179
+ fun `dropping protection makes the url evictable again`() {
180
+ val evictor = FifoCacheEvictor(maxBytes = 250)
181
+ val cache = FakeCache(evictor)
182
+ evictor.setProtectedKeys(setOf("a"))
183
+ evictor.onSpanAdded(cache, span("a", length = 100))
184
+ evictor.onSpanAdded(cache, span("b", length = 100))
185
+ evictor.setProtectedKeys(emptySet())
186
+ evictor.onSpanAdded(cache, span("c", length = 100)) // 300 > 250
187
+ assertEquals(listOf("a"), removed.map { it.key })
188
+ }
189
+
190
+ @Test
191
+ fun `the protected set is copied so a caller's later mutation cannot leak in`() {
192
+ val evictor = FifoCacheEvictor(maxBytes = 250)
193
+ val cache = FakeCache(evictor)
194
+ val live = mutableSetOf("a")
195
+ evictor.setProtectedKeys(live)
196
+ live.add("b") // must not reach the evictor
197
+ evictor.onSpanAdded(cache, span("a", length = 100))
198
+ evictor.onSpanAdded(cache, span("b", length = 100))
199
+ evictor.onSpanAdded(cache, span("c", length = 100)) // 300 > 250
200
+ assertEquals(listOf("b"), removed.map { it.key })
201
+ }
202
+
75
203
  /** Minimal [Cache]: only `removeSpan` is live (records + calls back the evictor). */
76
- private inner class FakeCache(private val evictor: FifoCacheEvictor) : Cache {
204
+ private inner class FakeCache(
205
+ private val evictor: FifoCacheEvictor,
206
+ private val callsBack: Boolean = true
207
+ ) : Cache {
77
208
  override fun removeSpan(span: CacheSpan) {
78
209
  removed.add(span)
79
- evictor.onSpanRemoved(this, span)
210
+ if (callsBack) evictor.onSpanRemoved(this, span)
80
211
  }
81
212
 
82
213
  override fun getUid(): Long = throw UnsupportedOperationException()
@@ -45,6 +45,7 @@ class GaplessEngineLifecycleTest {
45
45
  override fun onError(
46
46
  engine: PlaybackEngine,
47
47
  exception: androidx.media3.common.PlaybackException,
48
+ failedIndex: Int,
48
49
  ) = Unit
49
50
  override fun onPlaybackEnded(engine: PlaybackEngine) = Unit
50
51
  override fun onAutoTransitionDiscontinuity(engine: PlaybackEngine) = Unit
@@ -21,8 +21,9 @@ import org.robolectric.annotation.Config
21
21
  * the engine-internal `handleReplayGainParse` helper which these
22
22
  * tests drive directly with synthetic [ReplayGainData].
23
23
  * This file covers the no-data baseline,
24
- * mode-change-without-data behaviour, and the
25
- * `handleReplayGainParse` cache + recompute contract.
24
+ * mode-change-without-data behaviour, the
25
+ * `handleReplayGainParse` cache + recompute contract, and the
26
+ * consumer's supplied claim outranking the parsed tags.
26
27
  */
27
28
  @RunWith(RobolectricTestRunner::class)
28
29
  @Config(sdk = [34])
@@ -81,6 +82,39 @@ class GaplessEngineReplayGainTest {
81
82
  engine.release()
82
83
  }
83
84
 
85
+ /**
86
+ * The consumer's claim over the active item outranks the file's tags: a
87
+ * supplied value replaces detection in full, and a claim whose values were
88
+ * all rejected still discards detection and plays at unity.
89
+ */
90
+ @Test
91
+ fun `a supplied claim on the active item replaces the detected tags`() {
92
+ val engine = makeEngine()
93
+ ReplayGainConfig.setMode(ReplayGainMode.TRACK)
94
+ engine.setMediaItems(listOf(androidx.media3.common.MediaItem.fromUri("https://example.com/a.mp3")), 0, 0L)
95
+ val detected = ReplayGainData(trackGainDb = -6.0f, trackPeak = 0.5f, albumGainDb = null, albumPeak = null)
96
+ engine.handleReplayGainParse(detected)
97
+ assertEquals(detected, engine.currentReplayGainData())
98
+
99
+ var claim: TrackItem = suppliedTrack(trackGain = -3.0)
100
+ engine.suppliedReplayGainResolver = { SuppliedReplayGain.from(claim) }
101
+ assertEquals(
102
+ ReplayGainData(trackGainDb = -3.0f, trackPeak = null, albumGainDb = null, albumPeak = null),
103
+ engine.currentReplayGainData())
104
+ assertTrue(engine.currentReplayGainActive())
105
+
106
+ claim = suppliedTrack(trackGain = Double.NaN)
107
+ assertNull("a claim with no usable value discards the detection too", engine.currentReplayGainData())
108
+ assertFalse(engine.currentReplayGainActive())
109
+ engine.release()
110
+ }
111
+
112
+ private fun suppliedTrack(trackGain: Double) = TrackItem(
113
+ id = null, url = "https://example.com/a.mp3", title = null, artist = null, album = null,
114
+ duration = null, artworkUrl = null, extras = null,
115
+ replayGainTrackGain = trackGain, replayGainTrackPeak = null,
116
+ replayGainAlbumGain = null, replayGainAlbumPeak = null)
117
+
84
118
  @Test
85
119
  fun `handleReplayGainParse with album-only data is inactive in track mode`() {
86
120
  val engine = makeEngine()
@@ -20,8 +20,8 @@ import java.io.File
20
20
  *
21
21
  * Exercises the cache directly without going through ExoPlayer:
22
22
  * - Constructor creates the cache directory under `context.cacheDir`.
23
- * - `wrapDataSourceFactory` returns a [CacheDataSource.Factory]-shaped
24
- * factory rooted at this cache.
23
+ * - `wrapDataSourceFactory` hands out a [CacheDataSource] while the cache
24
+ * serves playback and the bare upstream source while it does not.
25
25
  * - `release()` is idempotent, and a fresh cache can be constructed
26
26
  * at the same directory afterwards (the SimpleCache one-instance-
27
27
  * per-directory invariant is restored on release).
@@ -69,25 +69,33 @@ class LookaheadCacheTest {
69
69
  }
70
70
 
71
71
  @Test
72
- fun `wrapDataSourceFactory returns a CacheDataSource Factory`() {
72
+ fun `the wrapped factory reads through the cache while it serves playback`() {
73
73
  val upstream = DefaultHttpDataSource.Factory()
74
74
  val wrapped = cache.wrapDataSourceFactory(upstream)
75
- assertNotNull(wrapped)
75
+ val source = wrapped.createDataSource()
76
76
  assertTrue(
77
- "wrapped factory must produce a CacheDataSource so reads check the LRU first",
78
- wrapped is CacheDataSource.Factory
77
+ "a data source opened while the cache is enabled checks the disk first",
78
+ source is CacheDataSource
79
79
  )
80
80
  }
81
81
 
82
+ /**
83
+ * Disabled means every read goes to the origin and nothing is written,
84
+ * whatever the disk holds — the wrapper hands out the bare upstream source.
85
+ * Decided per data source, so a change reaches the next item load.
86
+ */
82
87
  @Test
83
- fun `wrapped factory creates DataSources rooted at the same SimpleCache`() {
88
+ fun `the wrapped factory bypasses the cache while it does not serve playback`() {
84
89
  val upstream = DefaultHttpDataSource.Factory()
85
90
  val wrapped = cache.wrapDataSourceFactory(upstream)
86
- val source = wrapped.createDataSource()
87
- assertTrue(
88
- "createDataSource must produce a CacheDataSource — the cache check happens at read time",
89
- source is androidx.media3.datasource.cache.CacheDataSource
90
- )
91
+
92
+ cache.servesPlayback = false
93
+ val bypassed = wrapped.createDataSource()
94
+ cache.servesPlayback = true
95
+ val served = wrapped.createDataSource()
96
+
97
+ assertTrue("disabled: the upstream source, no cache in front", bypassed !is CacheDataSource)
98
+ assertTrue("re-enabled: the next source goes through the cache again", served is CacheDataSource)
91
99
  }
92
100
 
93
101
  @Test
@@ -125,13 +133,11 @@ class LookaheadCacheTest {
125
133
  }
126
134
 
127
135
  @Test
128
- fun `default max size constant matches the documented bootstrap budget`() {
129
- // Guards against an accidental tweak the bootstrap budget is
130
- // load-bearing for the 1.24-1.27 interim (consumers see the
131
- // library's automatic disk usage before the runtime config API
132
- // ships).
136
+ fun `default max size is 100 MB`() {
137
+ // The default is what a consumer that omits `lookaheadCacheMaxSizeMb`
138
+ // from `configure()` gets, so it is part of the public contract.
133
139
  assertEquals(
134
- "bootstrap budget should be 100 MB the runtime config",
140
+ "default disk budget",
135
141
  100L * 1024L * 1024L,
136
142
  LookaheadCache.DEFAULT_MAX_SIZE_BYTES
137
143
  )
@@ -0,0 +1,85 @@
1
+ package com.margelo.nitro.queueplayer
2
+
3
+ import androidx.media3.common.util.UnstableApi
4
+ import androidx.test.core.app.ApplicationProvider
5
+ import org.junit.Assert.assertTrue
6
+ import org.junit.Test
7
+ import org.junit.runner.RunWith
8
+ import org.robolectric.RobolectricTestRunner
9
+ import org.robolectric.annotation.Config
10
+ import java.util.concurrent.CountDownLatch
11
+ import java.util.concurrent.TimeUnit
12
+
13
+ /**
14
+ * Cancelling a prefetch stops the write that is running, not only the
15
+ * coroutine around it.
16
+ */
17
+ @RunWith(RobolectricTestRunner::class)
18
+ @Config(sdk = [34])
19
+ @UnstableApi
20
+ class LookaheadCacheWriterCancelTest {
21
+
22
+ private fun httpTrack(id: String) = TrackItem(
23
+ id = id, url = "https://example.com/$id.m4a", title = null, artist = null, album = null,
24
+ duration = null, artworkUrl = null, extras = null,
25
+ replayGainTrackGain = null, replayGainTrackPeak = null,
26
+ replayGainAlbumGain = null, replayGainAlbumPeak = null)
27
+
28
+ /**
29
+ * `CacheWriter.cache()` blocks and polls only its own cancelled flag, so a
30
+ * cancelled coroutine alone would leave it writing. The download body
31
+ * relays the cancellation onto the writer; `CacheWriter` then closes its
32
+ * source, which is what this observes.
33
+ */
34
+ @Test
35
+ fun `cancel stops the running download, not just its coroutine`() {
36
+ val cache = LookaheadCache(
37
+ context = ApplicationProvider.getApplicationContext(),
38
+ maxSizeBytes = 4L * 1024L * 1024L,
39
+ cacheDirName = "writer-cancel-${System.nanoTime()}"
40
+ )
41
+ val source = TricklingDataSource()
42
+ val writer = LookaheadCacheWriter.create(
43
+ cache = cache,
44
+ httpFactory = androidx.media3.datasource.DataSource.Factory { source },
45
+ isOnCellular = { false }
46
+ )
47
+ try {
48
+ // The window is the tracks ahead of the current one.
49
+ writer.reschedule(listOf(httpTrack("current"), httpTrack("slow")), currentIndex = 0, lookaheadCount = 1)
50
+ assertTrue("the download must have opened its source", source.opened.await(5, TimeUnit.SECONDS))
51
+
52
+ writer.cancel()
53
+
54
+ assertTrue("a cancelled write closes its source", source.closed.await(5, TimeUnit.SECONDS))
55
+ } finally {
56
+ writer.release()
57
+ cache.release()
58
+ }
59
+ }
60
+
61
+ /** A source that hands out one byte at a time, forever, until closed. */
62
+ private class TricklingDataSource : androidx.media3.datasource.DataSource {
63
+ val opened = CountDownLatch(1)
64
+ val closed = CountDownLatch(1)
65
+ private var uri: android.net.Uri? = null
66
+
67
+ override fun addTransferListener(transferListener: androidx.media3.datasource.TransferListener) {}
68
+ override fun open(dataSpec: androidx.media3.datasource.DataSpec): Long {
69
+ uri = dataSpec.uri
70
+ opened.countDown()
71
+ return androidx.media3.common.C.LENGTH_UNSET.toLong()
72
+ }
73
+ override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
74
+ if (length == 0) return 0
75
+ Thread.sleep(2)
76
+ buffer[offset] = 0
77
+ return 1
78
+ }
79
+ override fun getUri(): android.net.Uri? = uri
80
+ override fun close() {
81
+ closed.countDown()
82
+ }
83
+ }
84
+
85
+ }