react-native-queue-player 1.2.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/consumer-rules.pro +1 -1
  4. package/android/src/main/cpp/airplay2_control.c +4 -4
  5. package/android/src/main/cpp/airplay2_control.h +4 -4
  6. package/android/src/main/cpp/airplay2_jni.cpp +3 -19
  7. package/android/src/main/cpp/airplay2_pair.c +7 -7
  8. package/android/src/main/cpp/airplay2_pair.h +2 -2
  9. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  10. package/android/src/main/cpp/airplay2_session.c +2 -2
  11. package/android/src/main/cpp/airplay_jni.cpp +8 -69
  12. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  61. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  62. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  63. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  64. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  65. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  114. package/app.plugin.js +17 -1
  115. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  116. package/ios/AVQueueBuilder.swift +120 -85
  117. package/ios/ArtworkLoader.swift +0 -7
  118. package/ios/ArtworkResolver.swift +22 -4
  119. package/ios/AssetReplayGainReader.swift +118 -0
  120. package/ios/AudioSession.swift +67 -16
  121. package/ios/AudioTapProvider.swift +284 -149
  122. package/ios/CarPlayBridge.swift +0 -12
  123. package/ios/CarPlayCoordinator.swift +18 -2
  124. package/ios/CarPlaySceneDelegate.swift +2 -2
  125. package/ios/Cast/AirPlayRouteState.swift +9 -8
  126. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  127. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  128. package/ios/Cast/Core/CastSession.swift +2 -2
  129. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  130. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  131. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  132. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  133. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  134. package/ios/CastManager.swift +1 -1
  135. package/ios/CrossfadeEngine.swift +672 -226
  136. package/ios/EQTap.swift +20 -17
  137. package/ios/Equalizer.swift +1 -1
  138. package/ios/FLACStreamInfo.swift +65 -0
  139. package/ios/GaplessEngine.swift +57 -30
  140. package/ios/InputGuards.swift +35 -5
  141. package/ios/LookaheadCache.swift +229 -58
  142. package/ios/LookaheadCachePrefetcher.swift +48 -36
  143. package/ios/MediaResponseCheck.swift +122 -0
  144. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  145. package/ios/NetworkRecoveryPolicy.swift +32 -0
  146. package/ios/NowPlayingFormatExtractor.swift +25 -18
  147. package/ios/NowPlayingInfo.swift +84 -48
  148. package/ios/OutputRouteMonitor.swift +16 -3
  149. package/ios/PendingBrowseRequests.swift +1 -1
  150. package/ios/PlaceholderArtwork.swift +9 -5
  151. package/ios/PlaybackEngine.swift +165 -76
  152. package/ios/PlaybackErrorMapping.swift +3 -3
  153. package/ios/PlaybackModeStateMachine.swift +2 -2
  154. package/ios/PlaybackNetworkMonitor.swift +73 -0
  155. package/ios/PlayerStateDerivation.swift +6 -6
  156. package/ios/QueueMutationArithmetic.swift +25 -0
  157. package/ios/QueueSkipArithmetic.swift +5 -3
  158. package/ios/QueueWindowArithmetic.swift +152 -0
  159. package/ios/ReadThroughServer.swift +283 -0
  160. package/ios/RemoteCommands.swift +44 -4
  161. package/ios/ReplayGainData.swift +22 -3
  162. package/ios/ReplayGainExtractor.swift +70 -27
  163. package/ios/Siri/VoiceDonation.swift +14 -9
  164. package/ios/StreamingBitrateProbe.swift +22 -68
  165. package/ios/SuppliedReplayGain.swift +83 -0
  166. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  167. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  168. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  169. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  170. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  171. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  172. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  173. package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
  174. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  175. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  176. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  177. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  178. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  179. package/ios/Tests/EngineSwapTests.swift +91 -0
  180. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  181. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  182. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  183. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  184. package/ios/Tests/InputGuardsTests.swift +41 -0
  185. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  186. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  187. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  188. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  189. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  190. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  191. package/ios/Tests/MediaResponseCheckTests.swift +128 -0
  192. package/ios/Tests/MutationDeferralTests.swift +220 -0
  193. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  194. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  195. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  196. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  197. package/ios/Tests/PlayerFixtures.swift +48 -0
  198. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  199. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  200. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  201. package/ios/Tests/QueueStateTests.swift +3 -1
  202. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  203. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  204. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  205. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  206. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  207. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  208. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  209. package/ios/Tests/RetryRecoveryTests.swift +418 -0
  210. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  211. package/ios/Tests/SkipIndexTests.swift +32 -2
  212. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  213. package/ios/Tests/StallRecoveryTests.swift +97 -0
  214. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  215. package/ios/Tests/TopUpWindowGateTests.swift +441 -0
  216. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  217. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  218. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  219. package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
  220. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  221. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  222. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  223. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  224. package/ios/TrackPlayer+Automotive.swift +101 -0
  225. package/ios/TrackPlayer+Cache.swift +232 -0
  226. package/ios/TrackPlayer+Config.swift +291 -0
  227. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  228. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  229. package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
  230. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  231. package/ios/TrackPlayer+Lifecycle.swift +982 -0
  232. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  233. package/ios/TrackPlayer+Queue.swift +951 -0
  234. package/ios/TrackPlayer+Recovery.swift +213 -0
  235. package/ios/TrackPlayer+Skip.swift +465 -0
  236. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  237. package/ios/TrackPlayer+State.swift +108 -0
  238. package/ios/TrackPlayer+Threading.swift +135 -0
  239. package/ios/TrackPlayer+Transport.swift +272 -0
  240. package/ios/TrackPlayer+Window.swift +227 -0
  241. package/ios/TrackPlayer.swift +331 -4619
  242. package/ios/Visualizer.swift +6 -5
  243. package/ios/tests-harness/Podfile +1 -1
  244. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  245. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  246. package/lib/module/hooks/useActiveTrack.js +29 -22
  247. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  248. package/lib/module/hooks/useCast.js +8 -26
  249. package/lib/module/hooks/useCast.js.map +1 -1
  250. package/lib/module/hooks/useEqualizer.js +19 -12
  251. package/lib/module/hooks/useEqualizer.js.map +1 -1
  252. package/lib/module/hooks/useLookaheadCache.js +3 -7
  253. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  254. package/lib/module/hooks/useQueue.js +66 -19
  255. package/lib/module/hooks/useQueue.js.map +1 -1
  256. package/lib/module/index.js +6 -3
  257. package/lib/module/index.js.map +1 -1
  258. package/lib/module/queueDelta.js +41 -0
  259. package/lib/module/queueDelta.js.map +1 -0
  260. package/lib/module/types.js +28 -5
  261. package/lib/module/types.js.map +1 -1
  262. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  263. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  264. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  265. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  266. package/lib/typescript/hooks/useCast.d.ts +6 -1
  267. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  269. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  271. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  272. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  273. package/lib/typescript/index.d.ts +2 -1
  274. package/lib/typescript/index.d.ts.map +1 -1
  275. package/lib/typescript/queueDelta.d.ts +10 -0
  276. package/lib/typescript/queueDelta.d.ts.map +1 -0
  277. package/lib/typescript/types.d.ts +168 -12
  278. package/lib/typescript/types.d.ts.map +1 -1
  279. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  280. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  281. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  282. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  283. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  284. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  285. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  320. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  321. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  322. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  323. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  324. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  325. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  326. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  327. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  328. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  329. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  330. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  331. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  332. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  333. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  334. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  335. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  336. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  337. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  338. package/package.json +8 -8
  339. package/src/TrackPlayer.nitro.ts +53 -27
  340. package/src/hooks/useActiveTrack.ts +29 -22
  341. package/src/hooks/useCast.ts +14 -15
  342. package/src/hooks/useEqualizer.ts +19 -12
  343. package/src/hooks/useLookaheadCache.ts +3 -7
  344. package/src/hooks/useQueue.ts +66 -17
  345. package/src/index.ts +13 -3
  346. package/src/queueDelta.ts +41 -0
  347. package/src/types.ts +169 -12
  348. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  349. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  350. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  351. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  352. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  353. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  354. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  355. package/ios/MetadataReader.swift +0 -555
  356. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  357. package/ios/Tests/MetadataReaderTests.swift +0 -603
  358. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  359. package/ios/Tests/VoiceDonationTests.swift +0 -26
  360. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  361. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  362. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -0,0 +1,115 @@
1
+ package com.margelo.nitro.queueplayer
2
+
3
+ import android.content.Context
4
+ import android.media.AudioManager
5
+ import androidx.media3.common.MediaItem
6
+ import androidx.media3.common.PlaybackException
7
+ import androidx.media3.common.Player
8
+ import androidx.media3.common.util.UnstableApi
9
+ import androidx.test.core.app.ApplicationProvider
10
+ import org.junit.Assert.assertEquals
11
+ import org.junit.Assert.assertNotNull
12
+ import org.junit.Test
13
+ import org.junit.runner.RunWith
14
+ import org.robolectric.RobolectricTestRunner
15
+ import org.robolectric.Shadows.shadowOf
16
+ import org.robolectric.annotation.Config
17
+
18
+ /**
19
+ * A crossfade engine owns audio focus itself, so the pause a focus loss
20
+ * makes reaches the delegate carrying the audio-focus reason, not the
21
+ * user-request reason `pause()` stamps by default.
22
+ */
23
+ @RunWith(RobolectricTestRunner::class)
24
+ @Config(sdk = [34])
25
+ @UnstableApi
26
+ class CrossfadeEngineFocusLossTest {
27
+
28
+ private val context: Context = ApplicationProvider.getApplicationContext()
29
+
30
+ /** Records every play-when-ready reason the engine reports. */
31
+ private class RecordingDelegate : PlaybackEngineDelegate {
32
+ val reasons = mutableListOf<Int?>()
33
+ override fun onStateMaybeChanged(engine: PlaybackEngine, playWhenReadyReason: Int?) {
34
+ reasons.add(playWhenReadyReason)
35
+ }
36
+ override fun onIsPlayingChanged(engine: PlaybackEngine, isPlaying: Boolean) {}
37
+ override fun onTrackTransition(engine: PlaybackEngine, platformReason: Int) {}
38
+ override fun onActiveItemFormatChanged(engine: PlaybackEngine) {}
39
+ override fun onError(engine: PlaybackEngine, exception: PlaybackException, failedIndex: Int) {}
40
+ override fun onPlaybackEnded(engine: PlaybackEngine) {}
41
+ override fun onAutoTransitionDiscontinuity(engine: PlaybackEngine) {}
42
+ override fun onCrossfadeBegin(engine: PlaybackEngine, incomingIndex: Int) {}
43
+ override fun onCrossfadeCancel(engine: PlaybackEngine, revertedIndex: Int) {}
44
+ }
45
+
46
+ private fun playingEngine(delegate: RecordingDelegate): CrossfadeEngine {
47
+ val engine = CrossfadeEngine(context = context, crossfadeDurationMs = 3_000L)
48
+ engine.delegate = delegate
49
+ engine.setMediaItems(listOf(MediaItem.fromUri("https://example.com/a.mp3")), 0, 0L)
50
+ engine.play()
51
+ shadowOf(android.os.Looper.getMainLooper()).idle()
52
+ return engine
53
+ }
54
+
55
+ private fun focusListener(): AudioManager.OnAudioFocusChangeListener {
56
+ val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
57
+ val request = shadowOf(audioManager).lastAudioFocusRequest
58
+ assertNotNull("play() must request focus", request)
59
+ return request.listener
60
+ }
61
+
62
+ @Test
63
+ fun `a focus loss pauses with the audio-focus reason`() {
64
+ val delegate = RecordingDelegate()
65
+ val engine = playingEngine(delegate)
66
+ delegate.reasons.clear()
67
+
68
+ focusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS)
69
+ shadowOf(android.os.Looper.getMainLooper()).idle()
70
+
71
+ assertEquals(
72
+ "the delegate stamps the last reason it is handed; a USER_REQUEST after the loss would label the pause a user pause",
73
+ Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS,
74
+ delegate.reasons.last { it != null },
75
+ )
76
+ engine.release()
77
+ }
78
+
79
+ @Test
80
+ fun `a transient focus loss pauses with the audio-focus reason too`() {
81
+ val delegate = RecordingDelegate()
82
+ val engine = playingEngine(delegate)
83
+ delegate.reasons.clear()
84
+
85
+ focusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)
86
+ shadowOf(android.os.Looper.getMainLooper()).idle()
87
+
88
+ assertEquals(
89
+ Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS,
90
+ delegate.reasons.last { it != null },
91
+ )
92
+ engine.release()
93
+ }
94
+
95
+ @Test
96
+ fun `a user pause after the loss keeps its own reason`() {
97
+ val delegate = RecordingDelegate()
98
+ val engine = playingEngine(delegate)
99
+ focusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)
100
+ focusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN)
101
+ engine.play()
102
+ shadowOf(android.os.Looper.getMainLooper()).idle()
103
+ delegate.reasons.clear()
104
+
105
+ engine.pause()
106
+ shadowOf(android.os.Looper.getMainLooper()).idle()
107
+
108
+ assertEquals(
109
+ "the substitution lasts only for the loss's own pauses",
110
+ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
111
+ delegate.reasons.last { it != null },
112
+ )
113
+ engine.release()
114
+ }
115
+ }
@@ -1,5 +1,6 @@
1
1
  package com.margelo.nitro.queueplayer
2
2
 
3
+ import androidx.media3.common.MediaItem
3
4
  import androidx.media3.common.util.UnstableApi
4
5
  import androidx.test.core.app.ApplicationProvider
5
6
  import org.junit.Assert.assertEquals
@@ -31,6 +32,49 @@ class CrossfadeEngineLifecycleTest {
31
32
  crossfadeDurationMs = crossfadeDurationMs,
32
33
  )
33
34
 
35
+ /**
36
+ * Settling leaves the standby leg holding nothing.
37
+ *
38
+ * An armed standby carries the queue as it stood when it was armed. A caller
39
+ * that mutates the queue through a path the engine does not see — Media3
40
+ * setting resolved assistant items on the session player — needs that
41
+ * snapshot gone, or promoting it drops whatever the caller added.
42
+ */
43
+ @Test
44
+ fun `settling a pending fade leaves nothing staged on the standby leg`() {
45
+ val engine = makeEngine()
46
+ engine.standbyPlayer.setMediaItems(
47
+ listOf(MediaItem.fromUri("http://example.com/next.mp3")))
48
+ engine.standbyPlayer.prepare()
49
+ assertEquals(1, engine.standbyPlayer.mediaItemCount)
50
+
51
+ engine.settlePendingFade()
52
+
53
+ assertEquals(0, engine.standbyPlayer.mediaItemCount)
54
+ engine.release()
55
+ }
56
+
57
+ /**
58
+ * A settle with nothing to settle leaves the legs as they are: a caller
59
+ * settling defensively must not swap them. Promotion itself needs a live
60
+ * fade, which needs real playback with duration metadata, so that half is
61
+ * covered by the crossfade demo suites.
62
+ */
63
+ @Test
64
+ fun `settling by promotion leaves the roles alone when no fade is in flight`() {
65
+ val engine = makeEngine()
66
+ val leading = engine.leadingPlayer
67
+ val standby = engine.standbyPlayer
68
+
69
+ engine.settlePendingFadeByPromoting()
70
+ engine.settlePendingFadeByPromoting()
71
+
72
+ assertSame(leading, engine.leadingPlayer)
73
+ assertSame(standby, engine.standbyPlayer)
74
+ assertTrue(engine.currentRoleIsA)
75
+ engine.release()
76
+ }
77
+
34
78
  @Test
35
79
  fun `constructor accepts a non-negative crossfade duration`() {
36
80
  val engine = makeEngine()
@@ -204,6 +248,7 @@ class CrossfadeEngineLifecycleTest {
204
248
  override fun onError(
205
249
  engine: PlaybackEngine,
206
250
  exception: androidx.media3.common.PlaybackException,
251
+ failedIndex: Int,
207
252
  ) = Unit
208
253
  override fun onPlaybackEnded(engine: PlaybackEngine) = Unit
209
254
  override fun onAutoTransitionDiscontinuity(engine: PlaybackEngine) = Unit
@@ -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
+ }
@@ -213,4 +213,21 @@ class EqualizerEngineTest {
213
213
  engine.setBandGain(3, 4f)
214
214
  assertEquals(4f, engine.getBandGains()[3])
215
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
+ )
232
+ }
216
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
  }
@@ -221,4 +221,21 @@ class EqualizerLegacyEngineTest {
221
221
  } ?: error("no equalizer attached for session $sessionId")
222
222
  return (effect as Equalizer).getBandLevel(band).toInt()
223
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
+ )
240
+ }
224
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()