react-native-queue-player 1.2.0 → 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 (355) 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 +103 -29
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +839 -271
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  58. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  59. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  60. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  61. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  62. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  63. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
  64. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  65. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  109. package/app.plugin.js +17 -1
  110. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  111. package/ios/AVQueueBuilder.swift +120 -85
  112. package/ios/ArtworkLoader.swift +0 -7
  113. package/ios/ArtworkResolver.swift +22 -4
  114. package/ios/AssetReplayGainReader.swift +118 -0
  115. package/ios/AudioSession.swift +67 -16
  116. package/ios/AudioTapProvider.swift +284 -149
  117. package/ios/CarPlayBridge.swift +0 -12
  118. package/ios/CarPlayCoordinator.swift +18 -2
  119. package/ios/CarPlaySceneDelegate.swift +2 -2
  120. package/ios/Cast/AirPlayRouteState.swift +9 -8
  121. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  122. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  123. package/ios/Cast/Core/CastSession.swift +2 -2
  124. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  125. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  126. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  127. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  128. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  129. package/ios/CastManager.swift +1 -1
  130. package/ios/CrossfadeEngine.swift +660 -226
  131. package/ios/EQTap.swift +20 -17
  132. package/ios/Equalizer.swift +1 -1
  133. package/ios/FLACStreamInfo.swift +65 -0
  134. package/ios/GaplessEngine.swift +57 -30
  135. package/ios/InputGuards.swift +35 -5
  136. package/ios/LookaheadCache.swift +202 -58
  137. package/ios/LookaheadCachePrefetcher.swift +48 -36
  138. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  139. package/ios/NetworkRecoveryPolicy.swift +32 -0
  140. package/ios/NowPlayingFormatExtractor.swift +25 -18
  141. package/ios/NowPlayingInfo.swift +84 -48
  142. package/ios/OutputRouteMonitor.swift +16 -3
  143. package/ios/PendingBrowseRequests.swift +1 -1
  144. package/ios/PlaceholderArtwork.swift +9 -5
  145. package/ios/PlaybackEngine.swift +165 -76
  146. package/ios/PlaybackErrorMapping.swift +3 -3
  147. package/ios/PlaybackModeStateMachine.swift +2 -2
  148. package/ios/PlaybackNetworkMonitor.swift +73 -0
  149. package/ios/PlayerStateDerivation.swift +6 -6
  150. package/ios/QueueMutationArithmetic.swift +25 -0
  151. package/ios/QueueSkipArithmetic.swift +5 -3
  152. package/ios/QueueWindowArithmetic.swift +152 -0
  153. package/ios/ReadThroughServer.swift +283 -0
  154. package/ios/RemoteCommands.swift +44 -4
  155. package/ios/ReplayGainData.swift +22 -3
  156. package/ios/ReplayGainExtractor.swift +70 -27
  157. package/ios/Siri/VoiceDonation.swift +14 -9
  158. package/ios/StreamingBitrateProbe.swift +22 -68
  159. package/ios/SuppliedReplayGain.swift +83 -0
  160. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  161. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  162. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  163. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  164. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  165. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  166. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  167. package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
  168. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  169. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  170. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  171. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  172. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  173. package/ios/Tests/EngineSwapTests.swift +91 -0
  174. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  175. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  176. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  177. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  178. package/ios/Tests/InputGuardsTests.swift +41 -0
  179. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  180. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  181. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  182. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  183. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  184. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  185. package/ios/Tests/MutationDeferralTests.swift +220 -0
  186. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  187. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  188. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  189. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  190. package/ios/Tests/PlayerFixtures.swift +48 -0
  191. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  192. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  193. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  194. package/ios/Tests/QueueStateTests.swift +3 -1
  195. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  196. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  197. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  198. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  199. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  200. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  201. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  202. package/ios/Tests/RetryRecoveryTests.swift +303 -0
  203. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  204. package/ios/Tests/SkipIndexTests.swift +32 -2
  205. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  206. package/ios/Tests/StallRecoveryTests.swift +97 -0
  207. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  208. package/ios/Tests/TopUpWindowGateTests.swift +394 -0
  209. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  210. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  211. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  212. package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
  213. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  214. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  215. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  216. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  217. package/ios/TrackPlayer+Automotive.swift +101 -0
  218. package/ios/TrackPlayer+Cache.swift +232 -0
  219. package/ios/TrackPlayer+Config.swift +291 -0
  220. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  221. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  222. package/ios/TrackPlayer+EventsDispatch.swift +968 -0
  223. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  224. package/ios/TrackPlayer+Lifecycle.swift +930 -0
  225. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  226. package/ios/TrackPlayer+Queue.swift +940 -0
  227. package/ios/TrackPlayer+Recovery.swift +196 -0
  228. package/ios/TrackPlayer+Skip.swift +455 -0
  229. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  230. package/ios/TrackPlayer+State.swift +108 -0
  231. package/ios/TrackPlayer+Threading.swift +135 -0
  232. package/ios/TrackPlayer+Transport.swift +267 -0
  233. package/ios/TrackPlayer+Window.swift +195 -0
  234. package/ios/TrackPlayer.swift +305 -4619
  235. package/ios/Visualizer.swift +6 -5
  236. package/ios/tests-harness/Podfile +1 -1
  237. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  238. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  239. package/lib/module/hooks/useActiveTrack.js +29 -22
  240. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  241. package/lib/module/hooks/useCast.js +8 -26
  242. package/lib/module/hooks/useCast.js.map +1 -1
  243. package/lib/module/hooks/useEqualizer.js +19 -12
  244. package/lib/module/hooks/useEqualizer.js.map +1 -1
  245. package/lib/module/hooks/useLookaheadCache.js +3 -7
  246. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  247. package/lib/module/hooks/useQueue.js +66 -19
  248. package/lib/module/hooks/useQueue.js.map +1 -1
  249. package/lib/module/index.js +6 -3
  250. package/lib/module/index.js.map +1 -1
  251. package/lib/module/queueDelta.js +41 -0
  252. package/lib/module/queueDelta.js.map +1 -0
  253. package/lib/module/types.js +28 -5
  254. package/lib/module/types.js.map +1 -1
  255. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  256. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  257. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  258. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  259. package/lib/typescript/hooks/useCast.d.ts +6 -1
  260. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  261. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  262. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  263. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  264. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  265. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  266. package/lib/typescript/index.d.ts +2 -1
  267. package/lib/typescript/index.d.ts.map +1 -1
  268. package/lib/typescript/queueDelta.d.ts +10 -0
  269. package/lib/typescript/queueDelta.d.ts.map +1 -0
  270. package/lib/typescript/types.d.ts +168 -12
  271. package/lib/typescript/types.d.ts.map +1 -1
  272. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  273. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  274. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  275. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  276. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  277. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  278. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  279. 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
  280. 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
  281. 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
  282. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  283. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  284. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  285. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  293. 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
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  313. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  314. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  315. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  316. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  317. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  318. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  319. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  320. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  321. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  322. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  323. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  324. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  325. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  326. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  327. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  328. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  329. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  330. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  331. package/package.json +8 -8
  332. package/src/TrackPlayer.nitro.ts +53 -27
  333. package/src/hooks/useActiveTrack.ts +29 -22
  334. package/src/hooks/useCast.ts +14 -15
  335. package/src/hooks/useEqualizer.ts +19 -12
  336. package/src/hooks/useLookaheadCache.ts +3 -7
  337. package/src/hooks/useQueue.ts +66 -17
  338. package/src/index.ts +13 -3
  339. package/src/queueDelta.ts +41 -0
  340. package/src/types.ts +169 -12
  341. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  342. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  343. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  344. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  345. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  346. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  347. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  348. package/ios/MetadataReader.swift +0 -555
  349. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  350. package/ios/Tests/MetadataReaderTests.swift +0 -603
  351. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  352. package/ios/Tests/VoiceDonationTests.swift +0 -26
  353. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  354. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  355. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -41,7 +41,9 @@ class TrackPlayerEventsTest {
41
41
  private fun track(id: String) = TrackItem(
42
42
  id = id, url = "https://example.com/$id.m4a",
43
43
  title = null, artist = null, album = null,
44
- duration = null, artworkUrl = null, extras = null
44
+ duration = null, artworkUrl = null, extras = null,
45
+ replayGainTrackGain = null, replayGainTrackPeak = null,
46
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
45
47
  )
46
48
 
47
49
  private lateinit var serviceHandle: PlaybackServiceTestHandle
@@ -51,7 +53,7 @@ class TrackPlayerEventsTest {
51
53
  player = TrackPlayer()
52
54
  serviceHandle = bindPlaybackServiceForTest(player, context())
53
55
  player.configureInternal(
54
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
56
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
55
57
  )
56
58
  }
57
59
 
@@ -100,44 +102,84 @@ class TrackPlayerEventsTest {
100
102
  assertEquals(0.0, snap.buffered, 0.001)
101
103
  }
102
104
 
105
+ // --- onStateChange / onError / onQueueEnd registration
106
+
103
107
  @Test
104
- fun `emitProgressTick is a no-op when no subscriber exists`() {
105
- // Just verify it does not throw.
106
- player.emitProgressTick()
107
- }
108
+ fun `onStateChange delivers to a subscriber and stops after unsubscribe`() {
109
+ var fired = 0
110
+ val unsubscribe = player.onStateChange { _, _ -> fired += 1 }
108
111
 
109
- // --- onStateChange / onError / onQueueEnd registration
112
+ player.emitRemoteState(PlayerState.PAUSED, StateChangeReason.SYSTEM)
113
+ assertEquals(1, fired)
114
+
115
+ unsubscribe()
116
+ // A different state, so the only thing between it and the listener is
117
+ // the unsubscribe — a repeat of the same one would be deduped.
118
+ player.emitRemoteState(PlayerState.PLAYING, StateChangeReason.SYSTEM)
119
+ assertEquals("nothing reaches a removed subscriber", 1, fired)
120
+ }
110
121
 
111
122
  @Test
112
- fun `onStateChange registration stores and unsubscribe clears the callback`() {
113
- var fireCount = 0
114
- val unsubscribe = player.onStateChange { _, _ -> fireCount += 1 }
123
+ fun `onError delivers to a subscriber and stops after unsubscribe`() {
124
+ player.setQueueInternal((1..2).map { track("t$it") })
125
+ player.currentTrackIndex = 1
126
+ var fired = 0
127
+ val unsubscribe = player.onError { fired += 1 }
128
+
129
+ // A failure on a track that is not playing is reported straight away.
130
+ player.onError(serviceHandle.engine, networkFailure(), 0)
131
+ assertEquals(1, fired)
115
132
 
116
- // No way to force the Player.Listener onto READY without a
117
- // real media pipeline, but we can unregister + verify no
118
- // leftover reference causes a crash when destroyInternal runs.
119
133
  unsubscribe()
120
- assertEquals(0, fireCount)
134
+ // A different failure, so the only thing between it and the listener
135
+ // is the unsubscribe — a repeat of the same one would be deduped.
136
+ player.onError(serviceHandle.engine, decodingFailure(), 0)
137
+ assertEquals("nothing reaches a removed subscriber", 1, fired)
121
138
  }
122
139
 
123
140
  @Test
124
- fun `onError registration stores and unsubscribe clears the callback`() {
125
- val unsubscribe = player.onError { }
141
+ fun `onQueueEnd delivers to a subscriber and stops after unsubscribe`() {
142
+ player.setQueueInternal((1..2).map { track("t$it") })
143
+ var fired = 0
144
+ val unsubscribe = player.onQueueEnd { fired += 1 }
145
+
146
+ player.maybeEmitQueueEnd(androidx.media3.common.Player.STATE_ENDED)
147
+ assertEquals(1, fired)
148
+
126
149
  unsubscribe()
127
- // No-op sanity — callback slot is cleared; destroy must not
128
- // fire a spurious error call.
150
+ player.maybeEmitQueueEnd(androidx.media3.common.Player.STATE_ENDED)
151
+ assertEquals("nothing reaches a removed subscriber", 1, fired)
129
152
  }
130
153
 
154
+ /** Queue end is announced as ENDED / QUEUE_END, and the pause that parks the
155
+ * engine afterwards leaves no reason behind for the next emit. */
131
156
  @Test
132
- fun `onQueueEnd registration stores and unsubscribe clears the callback`() {
133
- val unsubscribe = player.onQueueEnd { }
157
+ fun `queue end announces ENDED with the queue-end reason and stamps nothing after`() {
158
+ player.setQueueInternal((1..2).map { track("t$it") })
159
+ val emits = mutableListOf<Pair<PlayerState, StateChangeReason>>()
160
+ val unsubscribe = player.onStateChange { state, reason -> emits.add(state to reason) }
161
+
162
+ player.maybeEmitQueueEnd(androidx.media3.common.Player.STATE_ENDED)
134
163
  unsubscribe()
164
+
165
+ assertEquals(PlayerState.ENDED to StateChangeReason.QUEUE_END, emits.last())
166
+ assertEquals("the parking pause's USER stamp must not survive", null, player.pendingStateChangeReason)
135
167
  }
136
168
 
137
169
  @Test
138
- fun `onTrackChange registration stores and unsubscribe clears the callback`() {
139
- val unsubscribe = player.onTrackChange { _, _, _, _ -> }
170
+ fun `onTrackChange delivers to a subscriber and stops after unsubscribe`() {
171
+ player.setQueueInternal((1..3).map { track("t$it") })
172
+ var fired = 0
173
+ val unsubscribe = player.onTrackChange { _, _, _, _ -> fired += 1 }
174
+
175
+ player.emitRemoteTrackChange(1)
176
+ assertEquals(1, fired)
177
+
140
178
  unsubscribe()
179
+ // A different index, so the only thing between it and the listener is
180
+ // the unsubscribe — a repeat of the current index returns early.
181
+ player.emitRemoteTrackChange(2)
182
+ assertEquals("nothing reaches a removed subscriber", 1, fired)
141
183
  }
142
184
 
143
185
  // --- PlaybackErrorMapping.isTransient classification
@@ -230,7 +272,7 @@ class TrackPlayerEventsTest {
230
272
  // the public effects — a new configure + getStateInternal
231
273
  // should return NONE.
232
274
  player.configureInternal(
233
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
275
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
234
276
  )
235
277
  assertEquals(PlayerState.NONE, player.getStateInternal())
236
278
  }
@@ -246,8 +288,8 @@ class TrackPlayerEventsTest {
246
288
  // - listener must NOT clobber the logical -1 from the Kotlin
247
289
  // side with the ExoPlayer-derived 0
248
290
  player.tracks = listOf(
249
- TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
250
- TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null)
291
+ TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
292
+ TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null)
251
293
  )
252
294
  player.currentTrackIndex = -1
253
295
 
@@ -267,7 +309,7 @@ class TrackPlayerEventsTest {
267
309
  // before issuing player.seekTo — the subsequent REASON_SEEK
268
310
  // listener fire must not reset it from the player.
269
311
  player.tracks = (1..5).map {
270
- TrackItem("t$it", "https://example.com/t$it.m4a", null, null, null, null, null, null)
312
+ TrackItem("t$it", "https://example.com/t$it.m4a", null, null, null, null, null, null, null, null, null, null)
271
313
  }
272
314
  player.currentTrackIndex = 3
273
315
 
@@ -281,7 +323,7 @@ class TrackPlayerEventsTest {
281
323
  @Test
282
324
  fun `handleMediaItemTransition REASON_SEEK consumes the pending track change reason`() {
283
325
  player.tracks = (1..5).map {
284
- TrackItem("t$it", "https://example.com/t$it.m4a", null, null, null, null, null, null)
326
+ TrackItem("t$it", "https://example.com/t$it.m4a", null, null, null, null, null, null, null, null, null, null)
285
327
  }
286
328
  player.currentTrackIndex = 2
287
329
 
@@ -305,7 +347,7 @@ class TrackPlayerEventsTest {
305
347
  @Test
306
348
  fun `handleMediaItemTransition REASON_SEEK without stashed reason falls back to SEEK`() {
307
349
  player.tracks = (1..5).map {
308
- TrackItem("t$it", "https://example.com/t$it.m4a", null, null, null, null, null, null)
350
+ TrackItem("t$it", "https://example.com/t$it.m4a", null, null, null, null, null, null, null, null, null, null)
309
351
  }
310
352
  player.currentTrackIndex = 2
311
353
 
@@ -321,11 +363,11 @@ class TrackPlayerEventsTest {
321
363
 
322
364
  @Test
323
365
  fun `handleMediaItemTransition fires callback with current track snapshot`() {
324
- val t2 = TrackItem("t2", "https://example.com/t2.m4a", null, null, null, null, null, null)
366
+ val t2 = TrackItem("t2", "https://example.com/t2.m4a", null, null, null, null, null, null, null, null, null, null)
325
367
  player.tracks = listOf(
326
- TrackItem("t1", "https://example.com/t1.m4a", null, null, null, null, null, null),
368
+ TrackItem("t1", "https://example.com/t1.m4a", null, null, null, null, null, null, null, null, null, null),
327
369
  t2,
328
- TrackItem("t3", "https://example.com/t3.m4a", null, null, null, null, null, null)
370
+ TrackItem("t3", "https://example.com/t3.m4a", null, null, null, null, null, null, null, null, null, null)
329
371
  )
330
372
  player.currentTrackIndex = 1
331
373
 
@@ -353,8 +395,8 @@ class TrackPlayerEventsTest {
353
395
  @Test
354
396
  fun `handleMediaItemTransition PLAYLIST_CHANGED reports null transitionGapMs`() {
355
397
  player.tracks = listOf(
356
- TrackItem("t1", "https://example.com/t1.m4a", null, null, null, null, null, null),
357
- TrackItem("t2", "https://example.com/t2.m4a", null, null, null, null, null, null)
398
+ TrackItem("t1", "https://example.com/t1.m4a", null, null, null, null, null, null, null, null, null, null),
399
+ TrackItem("t2", "https://example.com/t2.m4a", null, null, null, null, null, null, null, null, null, null)
358
400
  )
359
401
  player.currentTrackIndex = 0
360
402
 
@@ -376,7 +418,7 @@ class TrackPlayerEventsTest {
376
418
  @Test
377
419
  fun `handleMediaItemTransition SEEK reports null transitionGapMs`() {
378
420
  player.tracks = (1..3).map {
379
- TrackItem("t$it", "https://example.com/t$it.m4a", null, null, null, null, null, null)
421
+ TrackItem("t$it", "https://example.com/t$it.m4a", null, null, null, null, null, null, null, null, null, null)
380
422
  }
381
423
  player.currentTrackIndex = 1
382
424
 
@@ -395,7 +437,7 @@ class TrackPlayerEventsTest {
395
437
  @Test
396
438
  fun `maybeEmitQueueEnd does nothing on non-ENDED states`() {
397
439
  player.tracks = listOf(
398
- TrackItem("t1", "https://example.com/t1.m4a", null, null, null, null, null, null)
440
+ TrackItem("t1", "https://example.com/t1.m4a", null, null, null, null, null, null, null, null, null, null)
399
441
  )
400
442
  var queueEndFires = 0
401
443
  player.onQueueEnd { queueEndFires += 1 }
@@ -425,7 +467,7 @@ class TrackPlayerEventsTest {
425
467
  @Test
426
468
  fun `maybeEmitQueueEnd fires queueEnd and ENDED state on STATE_ENDED with tracks`() {
427
469
  player.tracks = (1..3).map {
428
- TrackItem("t$it", "https://example.com/t$it.m4a", null, null, null, null, null, null)
470
+ TrackItem("t$it", "https://example.com/t$it.m4a", null, null, null, null, null, null, null, null, null, null)
429
471
  }
430
472
 
431
473
  var queueEndFires = 0
@@ -450,7 +492,7 @@ class TrackPlayerEventsTest {
450
492
  fun `emitStateIfChanged defaults pending reason to SYSTEM on terminal state`() {
451
493
  player.setQueueInternal(
452
494
  (1..1).map {
453
- TrackItem("t$it", "https://example.com/t$it.m4a", null, null, null, null, null, null)
495
+ TrackItem("t$it", "https://example.com/t$it.m4a", null, null, null, null, null, null, null, null, null, null)
454
496
  }
455
497
  )
456
498
  // Manually reset dedup so the next call emits
@@ -479,7 +521,7 @@ class TrackPlayerEventsTest {
479
521
  serviceHandle.destroy()
480
522
  serviceHandle = bindPlaybackServiceForTest(player, context())
481
523
  player.configureInternal(
482
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
524
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
483
525
  )
484
526
  // No listener is bound to the new ExoPlayer's actual STATE_ENDED
485
527
  // path in a Robolectric test, but the callback slot itself is
@@ -493,4 +535,12 @@ class TrackPlayerEventsTest {
493
535
  unsubscribe()
494
536
  assertFalse(fired)
495
537
  }
538
+
539
+ private fun networkFailure() = androidx.media3.common.PlaybackException(
540
+ "boom", null,
541
+ androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED)
542
+
543
+ private fun decodingFailure() = androidx.media3.common.PlaybackException(
544
+ "bad frame", null,
545
+ androidx.media3.common.PlaybackException.ERROR_CODE_DECODING_FAILED)
496
546
  }
@@ -4,6 +4,7 @@ import androidx.media3.common.util.UnstableApi
4
4
  import androidx.test.core.app.ApplicationProvider
5
5
  import org.junit.After
6
6
  import org.junit.Assert.assertEquals
7
+ import org.junit.Assert.assertFalse
7
8
  import org.junit.Assert.assertNotEquals
8
9
  import org.junit.Assert.assertNotNull
9
10
  import org.junit.Assert.assertNull
@@ -58,18 +59,207 @@ class TrackPlayerLifecycleTest {
58
59
  serviceHandle.destroy()
59
60
  }
60
61
 
62
+ @Test
63
+ fun `a failure on a track that is not playing is reported, not swallowed`() {
64
+ seedThreeTrackQueue()
65
+
66
+ var reported = 0
67
+ val dispose = player.onError { reported += 1 }
68
+ val states = mutableListOf<PlayerState>()
69
+ val disposeState = player.onStateChange { state, _ -> states.add(state) }
70
+ // Index 2 is not the current track — the shape a standby preroll failure
71
+ // arrives in. The engine argument is not read by the handler.
72
+ player.onError(
73
+ serviceHandle.engine,
74
+ androidx.media3.common.PlaybackException(
75
+ "boom", null,
76
+ androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED),
77
+ 2)
78
+ dispose()
79
+ disposeState()
80
+
81
+ assertEquals("the failure is surfaced rather than swallowed", 1, reported)
82
+ assertFalse(
83
+ "the state is the current track's, which is still playing",
84
+ states.contains(PlayerState.ERROR))
85
+ }
86
+
87
+ /**
88
+ * A repeat of the same failure while the attempt is pending stays inside the
89
+ * library.
90
+ */
91
+ @Test
92
+ fun `an echo of the pending failure is not reported`() {
93
+ seedThreeTrackQueue()
94
+
95
+ var reported = 0
96
+ val dispose = player.onError { reported += 1 }
97
+ player.onError(serviceHandle.engine, networkFailure(), 0)
98
+ player.onError(serviceHandle.engine, networkFailure(), 0)
99
+ dispose()
100
+
101
+ assertEquals("the pending attempt owns both fires", 0, reported)
102
+ }
103
+
104
+ /** A failure after the attempt has run is the consumer's to see. */
105
+ @Test
106
+ fun `a failure after the attempt is reported`() {
107
+ seedThreeTrackQueue()
108
+
109
+ var reported = 0
110
+ val dispose = player.onError { reported += 1 }
111
+ player.onError(serviceHandle.engine, networkFailure(), 0)
112
+ player.retryFailedItem(0)
113
+ player.onError(serviceHandle.engine, networkFailure(), 0)
114
+ dispose()
115
+
116
+ assertEquals("the attempt was spent and the track is still broken", 1, reported)
117
+ }
118
+
119
+ /**
120
+ * Inserting ahead of a track moves it, and its retry budget moves with it.
121
+ * A budget of two with one attempt spent is the one value that tells the
122
+ * moved track apart from both a fresh entry (two left) and a dropped one
123
+ * (none left) at its new index.
124
+ */
125
+ @Test
126
+ fun `a track keeps its retry budget when a track is inserted before it`() {
127
+ seedThreeTrackQueue(autoRetries = 2.0)
128
+ player.onError(serviceHandle.engine, networkFailure(), 0)
129
+ player.retryFailedItem(0)
130
+
131
+ player.addToQueueInternal(listOf(track("https://example.com/new.mp3")), insertBefore = 0)
132
+ assertEquals("the playing track moved to index 1", 1, player.currentTrackIndex)
133
+
134
+ var reported = 0
135
+ val dispose = player.onError { reported += 1 }
136
+ player.onError(serviceHandle.engine, networkFailure(), 1)
137
+ assertEquals(
138
+ "one attempt is left at the new index, so the failure is retried silently; "
139
+ + "a dropped budget would have reported it",
140
+ 0, reported)
141
+ player.retryFailedItem(1)
142
+ player.onError(serviceHandle.engine, networkFailure(), 1)
143
+ dispose()
144
+
145
+ assertEquals(
146
+ "the second attempt was spent, so this failure is reported; "
147
+ + "a fresh budget of two would still be silent",
148
+ 1, reported)
149
+ }
150
+
151
+ /**
152
+ * A user's retry withdraws the automatic attempt scheduled for the same
153
+ * track: left posted, it would fire after the backoff and rebuild the track
154
+ * a second time.
155
+ */
156
+ @Test
157
+ fun `a retry withdraws the scheduled automatic attempt`() {
158
+ player.configureInternal(config(retryBackoffMs = 500.0), context())
159
+ player.setQueueInternal((0 until 3).map { track("https://example.com/$it.mp3") })
160
+ player.currentTrackIndex = 0
161
+ player.onError(serviceHandle.engine, networkFailure(), 0)
162
+
163
+ player.retryFailedItem(0)
164
+ val looper = org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper())
165
+ looper.idle()
166
+ // The retry rebuilt the item once (a remove and an add on the playlist);
167
+ // whatever the looper still holds for the backoff must not rebuild again.
168
+ // Only playlist changes count: the playback thread reports its own
169
+ // source refresh after the prepare, on its own schedule.
170
+ var timelineChanges = 0
171
+ val listener = object : androidx.media3.common.Player.Listener {
172
+ override fun onTimelineChanged(timeline: androidx.media3.common.Timeline, reason: Int) {
173
+ if (reason == androidx.media3.common.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) {
174
+ timelineChanges += 1
175
+ }
176
+ }
177
+ }
178
+ player.player!!.addListener(listener)
179
+ looper.idleFor(java.time.Duration.ofMillis(1_000))
180
+ player.player!!.removeListener(listener)
181
+
182
+ assertEquals(
183
+ "the automatic attempt fired after the user's retry and rebuilt the track again",
184
+ 0, timelineChanges)
185
+ }
186
+
187
+ /**
188
+ * The track that ends up at a position after a queue change reports its own
189
+ * failure instead of being taken for an echo of the old queue's attempt.
190
+ */
191
+ @Test
192
+ fun `a failure on the replacement queue is not taken for an echo of the old one`() {
193
+ seedThreeTrackQueue()
194
+ player.onError(serviceHandle.engine, networkFailure(), 0)
195
+
196
+ // The replacement queue gets no automatic attempt, so a failure on it can
197
+ // only be suppressed by a marker left over from the queue before it.
198
+ player.config = config(autoRetries = 0.0)
199
+ player.setQueueInternal((0 until 2).map {
200
+ track("https://example.com/next-$it.mp3")
201
+ })
202
+ player.currentTrackIndex = 0
203
+
204
+ var reported = 0
205
+ val dispose = player.onError { reported += 1 }
206
+ player.onError(serviceHandle.engine, networkFailure(), 0)
207
+ dispose()
208
+
209
+ assertEquals("the attempt belonged to a queue that is gone", 1, reported)
210
+ }
211
+
212
+ private fun config(
213
+ httpHeaders: Map<String, String>? = null,
214
+ userAgent: String? = null,
215
+ autoRetries: Double? = null,
216
+ retryBackoffMs: Double? = null,
217
+ ) = PlayerConfig(
218
+ httpHeaders = httpHeaders, userAgent = userAgent, autoRetries = autoRetries,
219
+ retryBackoffMs = retryBackoffMs, networkTimeoutMs = null, audioContentType = null,
220
+ audioCategoryOptions = null, voiceVocabularyDonationEnabled = null,
221
+ visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null,
222
+ lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null,
223
+ backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null,
224
+ skipToPreviousBehavior = null)
225
+
226
+ private fun track(url: String) = TrackItem(
227
+ id = null, url = url, title = null, artist = null, album = null, duration = null,
228
+ artworkUrl = null, extras = null, replayGainTrackGain = null, replayGainTrackPeak = null,
229
+ replayGainAlbumGain = null, replayGainAlbumPeak = null)
230
+
231
+ private fun networkFailure() = androidx.media3.common.PlaybackException(
232
+ "boom", null,
233
+ androidx.media3.common.PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED)
234
+
235
+ private fun seedThreeTrackQueue(autoRetries: Double? = null) {
236
+ player.configureInternal(config(autoRetries = autoRetries), context())
237
+ player.setQueueInternal((0 until 3).map { track("https://example.com/$it.mp3") })
238
+ player.currentTrackIndex = 0
239
+ }
240
+
241
+ @Test
242
+ fun `the automatic retry budget is one attempt by default`() {
243
+ assertEquals(1, player.effectiveAutoRetries())
244
+ }
245
+
246
+ @Test
247
+ fun `the consumer can ask for none or more`() {
248
+ player.config = config(autoRetries = 0.0)
249
+ assertEquals(0, player.effectiveAutoRetries())
250
+ player.config = config(autoRetries = 4.0)
251
+ assertEquals(4, player.effectiveAutoRetries())
252
+ player.config = config(autoRetries = 99.0)
253
+ assertEquals(5, player.effectiveAutoRetries())
254
+ player.config = config()
255
+ assertEquals(1, player.effectiveAutoRetries())
256
+ }
257
+
61
258
  @Test
62
259
  fun `configureInternal builds an ExoPlayer and stores config`() {
63
260
  assertNull("player not yet built before configure", player.player)
64
261
 
65
- val config = PlayerConfig(
66
- httpHeaders = mapOf("X-Foo" to "bar"),
67
- userAgent = "rnqp-test/0.0",
68
- autoRetries = null,
69
- retryBackoffMs = null,
70
- networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null,
71
- lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
72
- )
262
+ val config = config(httpHeaders = mapOf("X-Foo" to "bar"), userAgent = "rnqp-test/0.0")
73
263
  player.configureInternal(config, context())
74
264
 
75
265
  assertNotNull("configure must build ExoPlayer", player.player)
@@ -87,12 +277,12 @@ class TrackPlayerLifecycleTest {
87
277
  fun `configure destroys then rebuilds — second call yields a fresh ExoPlayer`() {
88
278
  // Universal `configure()` auto-destroy contract: every call to
89
279
  // the public configure tears down any prior state before
90
- // reinitialising. The internal `configureInternal` seam is
280
+ // reinitialising. The internal `configureInternal` entry point is
91
281
  // purely constructive (assumes `player == null` on entry); the
92
282
  // public path owns the destroy step. This test drives the
93
283
  // destroy + reconstruct shape directly to mirror what
94
284
  // `configure()` does end-to-end.
95
- val empty = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null)
285
+ val empty = config()
96
286
 
97
287
  player.configureInternal(empty, context())
98
288
  val firstInstance = player.player
@@ -122,16 +312,8 @@ class TrackPlayerLifecycleTest {
122
312
 
123
313
  @Test
124
314
  fun `configureInternal stores the latest config on repeat call`() {
125
- val first = PlayerConfig(
126
- httpHeaders = mapOf("A" to "1"), userAgent = "ua-1",
127
- autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null,
128
- lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
129
- )
130
- val second = PlayerConfig(
131
- httpHeaders = mapOf("B" to "2"), userAgent = "ua-2",
132
- autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null,
133
- lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
134
- )
315
+ val first = config(httpHeaders = mapOf("A" to "1"), userAgent = "ua-1")
316
+ val second = config(httpHeaders = mapOf("B" to "2"), userAgent = "ua-2")
135
317
 
136
318
  player.configureInternal(first, context())
137
319
  player.configureInternal(second, context())
@@ -141,11 +323,7 @@ class TrackPlayerLifecycleTest {
141
323
 
142
324
  @Test
143
325
  fun `destroyInternal releases ExoPlayer + cache, persists user config`() {
144
- val configured = PlayerConfig(
145
- httpHeaders = mapOf("h" to "v"), userAgent = "ua",
146
- autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null,
147
- lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
148
- )
326
+ val configured = config(httpHeaders = mapOf("h" to "v"), userAgent = "ua")
149
327
  player.configureInternal(configured, context())
150
328
  // Non-default playback state the consumer set post-configure.
151
329
  player.repeatModeState = RepeatMode.QUEUE
@@ -180,7 +358,7 @@ class TrackPlayerLifecycleTest {
180
358
  @Test
181
359
  fun `destroyInternal is safe to call twice`() {
182
360
  player.configureInternal(
183
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
361
+ config(), context()
184
362
  )
185
363
  player.destroyInternal()
186
364
  player.destroyInternal()
@@ -189,7 +367,7 @@ class TrackPlayerLifecycleTest {
189
367
 
190
368
  @Test
191
369
  fun `configureInternal after destroyInternal rebuilds the ExoPlayer`() {
192
- val cfg = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null)
370
+ val cfg = config()
193
371
 
194
372
  player.configureInternal(cfg, context())
195
373
  val first = player.player