react-native-queue-player 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/build.gradle +7 -0
  4. package/android/consumer-rules.pro +72 -3
  5. package/android/src/main/cpp/airplay2_control.c +4 -4
  6. package/android/src/main/cpp/airplay2_control.h +4 -4
  7. package/android/src/main/cpp/airplay2_jni.cpp +26 -22
  8. package/android/src/main/cpp/airplay2_pair.c +7 -7
  9. package/android/src/main/cpp/airplay2_pair.h +2 -2
  10. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  11. package/android/src/main/cpp/airplay2_session.c +2 -2
  12. package/android/src/main/cpp/airplay_jni.cpp +20 -72
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
  61. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  62. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  63. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  64. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  65. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
  114. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
  115. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  116. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  117. package/app.plugin.js +17 -1
  118. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  119. package/ios/AVQueueBuilder.swift +120 -85
  120. package/ios/ArtworkLoader.swift +0 -7
  121. package/ios/ArtworkResolver.swift +22 -4
  122. package/ios/AssetReplayGainReader.swift +118 -0
  123. package/ios/AudioSession.swift +67 -16
  124. package/ios/AudioTapProvider.swift +284 -149
  125. package/ios/CarPlayBridge.swift +0 -12
  126. package/ios/CarPlayCoordinator.swift +18 -2
  127. package/ios/CarPlaySceneDelegate.swift +2 -2
  128. package/ios/Cast/AirPlayRouteState.swift +9 -8
  129. package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
  130. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  131. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  132. package/ios/Cast/Core/CastSession.swift +11 -0
  133. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  134. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  135. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  136. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  137. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  138. package/ios/CastManager.swift +1 -1
  139. package/ios/CrossfadeEngine.swift +660 -226
  140. package/ios/EQTap.swift +20 -17
  141. package/ios/Equalizer.swift +1 -1
  142. package/ios/FLACStreamInfo.swift +65 -0
  143. package/ios/GaplessEngine.swift +57 -30
  144. package/ios/InputGuards.swift +35 -5
  145. package/ios/LookaheadCache.swift +202 -58
  146. package/ios/LookaheadCachePrefetcher.swift +48 -36
  147. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  148. package/ios/NetworkRecoveryPolicy.swift +32 -0
  149. package/ios/NowPlayingFormatExtractor.swift +25 -18
  150. package/ios/NowPlayingInfo.swift +84 -48
  151. package/ios/OutputRouteMonitor.swift +16 -3
  152. package/ios/PendingBrowseRequests.swift +1 -1
  153. package/ios/PlaceholderArtwork.swift +9 -5
  154. package/ios/PlaybackEngine.swift +165 -76
  155. package/ios/PlaybackErrorMapping.swift +3 -3
  156. package/ios/PlaybackModeStateMachine.swift +2 -2
  157. package/ios/PlaybackNetworkMonitor.swift +73 -0
  158. package/ios/PlayerStateDerivation.swift +6 -6
  159. package/ios/QueueMutationArithmetic.swift +25 -0
  160. package/ios/QueueSkipArithmetic.swift +5 -3
  161. package/ios/QueueWindowArithmetic.swift +152 -0
  162. package/ios/ReadThroughServer.swift +283 -0
  163. package/ios/RemoteCommands.swift +44 -4
  164. package/ios/ReplayGainData.swift +22 -3
  165. package/ios/ReplayGainExtractor.swift +70 -27
  166. package/ios/Siri/VoiceDonation.swift +14 -9
  167. package/ios/StreamingBitrateProbe.swift +22 -68
  168. package/ios/SuppliedReplayGain.swift +83 -0
  169. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  170. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  171. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  172. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  173. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  174. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  175. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  176. package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
  177. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  178. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  179. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  180. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  181. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  182. package/ios/Tests/EngineSwapTests.swift +91 -0
  183. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  184. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  185. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  186. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  187. package/ios/Tests/InputGuardsTests.swift +41 -0
  188. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  189. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  190. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  191. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  192. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  193. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  194. package/ios/Tests/MutationDeferralTests.swift +220 -0
  195. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  196. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  197. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  198. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  199. package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
  200. package/ios/Tests/PlayerFixtures.swift +48 -0
  201. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  202. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  203. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  204. package/ios/Tests/QueueStateTests.swift +3 -1
  205. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  206. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  207. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  208. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  209. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  210. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  211. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  212. package/ios/Tests/RetryRecoveryTests.swift +303 -0
  213. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  214. package/ios/Tests/SkipIndexTests.swift +32 -2
  215. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  216. package/ios/Tests/StallRecoveryTests.swift +97 -0
  217. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  218. package/ios/Tests/TopUpWindowGateTests.swift +394 -0
  219. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  220. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  221. package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
  222. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  223. package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
  224. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  225. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  226. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  227. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  228. package/ios/TrackPlayer+Automotive.swift +101 -0
  229. package/ios/TrackPlayer+Cache.swift +232 -0
  230. package/ios/TrackPlayer+Config.swift +291 -0
  231. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  232. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  233. package/ios/TrackPlayer+EventsDispatch.swift +968 -0
  234. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  235. package/ios/TrackPlayer+Lifecycle.swift +930 -0
  236. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  237. package/ios/TrackPlayer+Queue.swift +940 -0
  238. package/ios/TrackPlayer+Recovery.swift +196 -0
  239. package/ios/TrackPlayer+Skip.swift +455 -0
  240. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  241. package/ios/TrackPlayer+State.swift +108 -0
  242. package/ios/TrackPlayer+Threading.swift +135 -0
  243. package/ios/TrackPlayer+Transport.swift +267 -0
  244. package/ios/TrackPlayer+Window.swift +195 -0
  245. package/ios/TrackPlayer.swift +305 -4610
  246. package/ios/Visualizer.swift +6 -5
  247. package/ios/tests-harness/Podfile +1 -1
  248. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  249. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  250. package/lib/module/hooks/useActiveTrack.js +29 -22
  251. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  252. package/lib/module/hooks/useCast.js +8 -26
  253. package/lib/module/hooks/useCast.js.map +1 -1
  254. package/lib/module/hooks/useEqualizer.js +19 -12
  255. package/lib/module/hooks/useEqualizer.js.map +1 -1
  256. package/lib/module/hooks/useLookaheadCache.js +3 -7
  257. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  258. package/lib/module/hooks/useQueue.js +66 -19
  259. package/lib/module/hooks/useQueue.js.map +1 -1
  260. package/lib/module/index.js +6 -3
  261. package/lib/module/index.js.map +1 -1
  262. package/lib/module/queueDelta.js +41 -0
  263. package/lib/module/queueDelta.js.map +1 -0
  264. package/lib/module/types.js +28 -5
  265. package/lib/module/types.js.map +1 -1
  266. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  267. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  269. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useCast.d.ts +6 -1
  271. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  272. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  273. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  274. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  275. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  276. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  277. package/lib/typescript/index.d.ts +2 -1
  278. package/lib/typescript/index.d.ts.map +1 -1
  279. package/lib/typescript/queueDelta.d.ts +10 -0
  280. package/lib/typescript/queueDelta.d.ts.map +1 -0
  281. package/lib/typescript/types.d.ts +168 -12
  282. package/lib/typescript/types.d.ts.map +1 -1
  283. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  284. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  285. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  286. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  287. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  288. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  289. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  320. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  321. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  322. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  323. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  324. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  325. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  326. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  327. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  328. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  329. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  330. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  331. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  332. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  333. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  334. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  335. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  336. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  337. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  338. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  339. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  340. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  341. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  342. package/package.json +8 -8
  343. package/src/TrackPlayer.nitro.ts +53 -27
  344. package/src/hooks/useActiveTrack.ts +29 -22
  345. package/src/hooks/useCast.ts +14 -15
  346. package/src/hooks/useEqualizer.ts +19 -12
  347. package/src/hooks/useLookaheadCache.ts +3 -7
  348. package/src/hooks/useQueue.ts +66 -17
  349. package/src/index.ts +13 -3
  350. package/src/queueDelta.ts +41 -0
  351. package/src/types.ts +169 -12
  352. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  353. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
  354. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  355. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  356. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  357. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  358. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  359. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  360. package/ios/MetadataReader.swift +0 -555
  361. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  362. package/ios/Tests/MetadataReaderTests.swift +0 -603
  363. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  364. package/ios/Tests/VoiceDonationTests.swift +0 -26
  365. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  366. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  367. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -45,7 +45,9 @@ class TrackPlayerQueueTest {
45
45
  private fun track(id: String) = TrackItem(
46
46
  id = id, url = "https://example.com/$id.m4a",
47
47
  title = null, artist = null, album = null,
48
- duration = null, artworkUrl = null, extras = null
48
+ duration = null, artworkUrl = null, extras = null,
49
+ replayGainTrackGain = null, replayGainTrackPeak = null,
50
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
49
51
  )
50
52
 
51
53
  private lateinit var serviceHandle: PlaybackServiceTestHandle
@@ -55,7 +57,7 @@ class TrackPlayerQueueTest {
55
57
  player = TrackPlayer()
56
58
  serviceHandle = bindPlaybackServiceForTest(player, context())
57
59
  player.configureInternal(
58
- 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()
60
+ 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()
59
61
  )
60
62
  }
61
63
 
@@ -128,13 +130,15 @@ class TrackPlayerQueueTest {
128
130
  // --- getQueue / getCurrentTrackIndex
129
131
 
130
132
  @Test
131
- fun `getQueue returns the track list as an array`() {
133
+ fun `setQueueInternal stores the tracks in order`() {
134
+ // `getQueue()` itself returns a Nitro Promise, which cannot be resolved
135
+ // from Robolectric; it reads this list on main, and the demo cycle covers
136
+ // the bridged call.
132
137
  val tracks = (1..3).map { track("t$it") }
133
138
  player.setQueueInternal(tracks)
134
139
 
135
- val result = player.getQueue()
136
- assertEquals(3, result.size)
137
- assertEquals("t1", result[0].id)
140
+ assertEquals(3, player.tracks.size)
141
+ assertEquals("t1", player.tracks[0].id)
138
142
  }
139
143
 
140
144
  @Test
@@ -275,7 +279,7 @@ class TrackPlayerQueueTest {
275
279
  fun `skipToIndex after setQueue issues the ExoPlayer seek`() {
276
280
  // After setQueue the MediaItem timeline has 3 entries, so the
277
281
  // `target < mediaItemCount` guard in skipToIndexInternal no
278
- // longer short-circuits — confirms the skip → seek plumbing
282
+ // longer short-circuits — confirms the skip → seek path
279
283
  // survives once the timeline is populated.
280
284
  player.setQueueInternal((1..3).map { track("t$it") })
281
285
 
@@ -41,7 +41,8 @@ class TrackPlayerReadersTest {
41
41
  PlayerConfig(
42
42
  httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null,
43
43
  networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null,
44
- visualizationEnabled = null, clampSeekToBuffered = null,
44
+ voiceVocabularyDonationEnabled = null,
45
+ visualizationEnabled = null, clampSeekToBuffered = null,
45
46
  lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
46
47
  ),
47
48
  context()
@@ -40,7 +40,9 @@ class TrackPlayerSkipCapabilityTest {
40
40
  private fun track(id: String) = TrackItem(
41
41
  id = id, url = "https://example.com/$id.m4a",
42
42
  title = null, artist = null, album = null,
43
- duration = null, artworkUrl = null, extras = null
43
+ duration = null, artworkUrl = null, extras = null,
44
+ replayGainTrackGain = null, replayGainTrackPeak = null,
45
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
44
46
  )
45
47
 
46
48
  /** Drain main-looper posts so the auto-fire-on-subscribe lands. */
@@ -53,7 +55,7 @@ class TrackPlayerSkipCapabilityTest {
53
55
  player = TrackPlayer()
54
56
  serviceHandle = bindPlaybackServiceForTest(player, context())
55
57
  player.configureInternal(
56
- 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()
58
+ 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()
57
59
  )
58
60
  }
59
61
 
@@ -318,7 +320,7 @@ class TrackPlayerSkipCapabilityTest {
318
320
  player = TrackPlayer()
319
321
  serviceHandle = bindPlaybackServiceForTest(player, context())
320
322
  player.configureInternal(
321
- 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()
323
+ 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()
322
324
  )
323
325
  }
324
326
 
@@ -326,10 +328,25 @@ class TrackPlayerSkipCapabilityTest {
326
328
 
327
329
  private fun configureRestart() {
328
330
  player.configureInternal(
329
- 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 = SkipToPreviousBehavior.RESTART_OR_PREVIOUS), context()
331
+ 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 = SkipToPreviousBehavior.RESTART_OR_PREVIOUS), context()
330
332
  )
331
333
  }
332
334
 
335
+ @Test
336
+ fun `restart mode — addToQueueInternal into empty queue keeps Previous false (cur stays -1)`() {
337
+ configureRestart()
338
+ val emissions = mutableListOf<SkipCapability>()
339
+ player.onSkipCapabilityChange { cap -> emissions.add(cap) }
340
+ flushMain()
341
+ emissions.clear()
342
+
343
+ player.addToQueueInternal((1..3).map { track("t$it") }, insertBefore = null)
344
+ // No loaded track: nothing to restart and nothing before it, so the
345
+ // lock-screen Previous control stays disabled until skipToIndex.
346
+ assertEquals(false, player.getCanSkipPrevious())
347
+ assertEquals(emptyList<SkipCapability>(), emissions)
348
+ }
349
+
333
350
  @Test
334
351
  fun `restart mode forces canSkipPrevious true at index 0`() {
335
352
  configureRestart()
@@ -360,4 +377,73 @@ class TrackPlayerSkipCapabilityTest {
360
377
  emissions.last().canSkipPrevious
361
378
  )
362
379
  }
380
+
381
+ // --- cast index writes keep the capability current
382
+
383
+ @Test
384
+ fun `emitRemoteTrackChange to the last index disables Next`() {
385
+ player.setQueueInternal((1..3).map { track("t$it") })
386
+ assertEquals(true, player.getCanSkipNext())
387
+
388
+ // The receiver advanced to the last track; the lock-screen Next
389
+ // control must grey out exactly as it does for a local advance.
390
+ player.emitRemoteTrackChange(2)
391
+ assertEquals(2, player.currentTrackIndex)
392
+ assertEquals(false, player.getCanSkipNext())
393
+ assertEquals(true, player.getCanSkipPrevious())
394
+ }
395
+
396
+ @Test
397
+ fun `emitRemoteTrackChange emits the capability change`() {
398
+ player.setQueueInternal((1..3).map { track("t$it") })
399
+ val emissions = mutableListOf<SkipCapability>()
400
+ player.onSkipCapabilityChange { cap -> emissions.add(cap) }
401
+ flushMain()
402
+ emissions.clear()
403
+
404
+ player.emitRemoteTrackChange(2)
405
+ flushMain()
406
+ assertEquals(
407
+ listOf(SkipCapability(canSkipPrevious = true, canSkipNext = false)),
408
+ emissions
409
+ )
410
+ }
411
+
412
+ @Test
413
+ fun `emitRemoteTrackChange classifies the new track's source`() {
414
+ // A local file at index 2, streaming tracks elsewhere: the source the
415
+ // JS side reads must follow the index the receiver moved to.
416
+ val local = TrackItem(
417
+ id = "local", url = "file:///tmp/local.m4a",
418
+ title = null, artist = null, album = null,
419
+ duration = null, artworkUrl = null, extras = null,
420
+ replayGainTrackGain = null, replayGainTrackPeak = null,
421
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
422
+ )
423
+ player.setQueueInternal(listOf(track("t1"), track("t2"), local))
424
+ assertEquals(TrackSource.STREAMING, player.getCurrentTrackSource())
425
+
426
+ player.emitRemoteTrackChange(2)
427
+ assertEquals(TrackSource.LOCAL, player.getCurrentTrackSource())
428
+ }
429
+
430
+ @Test
431
+ fun `resumeLocalAfterCast lands on the handback index with its capability and source`() {
432
+ val local = TrackItem(
433
+ id = "local", url = "file:///tmp/local.m4a",
434
+ title = null, artist = null, album = null,
435
+ duration = null, artworkUrl = null, extras = null,
436
+ replayGainTrackGain = null, replayGainTrackPeak = null,
437
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
438
+ )
439
+ player.setQueueInternal(listOf(track("t1"), track("t2"), local))
440
+ assertEquals(true, player.getCanSkipNext())
441
+ assertEquals(TrackSource.STREAMING, player.getCurrentTrackSource())
442
+
443
+ player.resumeLocalAfterCast(trackIndex = 2, positionMs = 0L)
444
+ flushMain()
445
+ assertEquals(2, player.currentTrackIndex)
446
+ assertEquals(false, player.getCanSkipNext())
447
+ assertEquals(TrackSource.LOCAL, player.getCurrentTrackSource())
448
+ }
363
449
  }
@@ -15,9 +15,8 @@ import org.robolectric.annotation.Config
15
15
  * [TrackPlayer]. `QueueSkipArithmeticTest` covers the raw
16
16
  * arithmetic; these tests cover the adapter layer:
17
17
  *
18
- * - `computeNextIndex` / `computePreviousIndex` read
19
- * `repeatModeState` + `currentTrackIndex` + `tracks.size` and
20
- * delegate to [QueueSkipArithmetic].
18
+ * - `computeNextIndex` reads `repeatModeState` + `currentTrackIndex`
19
+ * + `tracks.size` and delegates to [QueueSkipArithmetic].
21
20
  * - `skipToNextInternal` / `skipToPreviousInternal` /
22
21
  * `skipToIndexInternal` — advance `currentTrackIndex` when the
23
22
  * arithmetic returns a target, no-op when it returns null. Under
@@ -41,7 +40,9 @@ class TrackPlayerSkipTest {
41
40
  private fun track(id: String) = TrackItem(
42
41
  id = id, url = "https://example.com/$id.m4a",
43
42
  title = null, artist = null, album = null,
44
- duration = null, artworkUrl = null, extras = null
43
+ duration = null, artworkUrl = null, extras = null,
44
+ replayGainTrackGain = null, replayGainTrackPeak = null,
45
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
45
46
  )
46
47
 
47
48
  private lateinit var serviceHandle: PlaybackServiceTestHandle
@@ -51,7 +52,7 @@ class TrackPlayerSkipTest {
51
52
  player = TrackPlayer()
52
53
  serviceHandle = bindPlaybackServiceForTest(player, context())
53
54
  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()
55
+ 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
56
  )
56
57
  player.tracks = (1..5).map { track("t$it") }
57
58
  player.currentTrackIndex = 2 // mid-queue default for most cases
@@ -63,7 +64,7 @@ class TrackPlayerSkipTest {
63
64
  serviceHandle.destroy()
64
65
  }
65
66
 
66
- // --- computeNextIndex / computePreviousIndex (adapter)
67
+ // --- computeNextIndex (adapter)
67
68
 
68
69
  @Test
69
70
  fun `computeNextIndex reads repeatModeState`() {
@@ -77,18 +78,6 @@ class TrackPlayerSkipTest {
77
78
  assertEquals(3, player.computeNextIndex())
78
79
  }
79
80
 
80
- @Test
81
- fun `computePreviousIndex reads repeatModeState`() {
82
- player.repeatModeState = RepeatMode.OFF
83
- assertEquals(1, player.computePreviousIndex())
84
-
85
- player.repeatModeState = RepeatMode.TRACK
86
- assertEquals(1, player.computePreviousIndex())
87
-
88
- player.repeatModeState = RepeatMode.QUEUE
89
- assertEquals(1, player.computePreviousIndex())
90
- }
91
-
92
81
  @Test
93
82
  fun `computeNextIndex returns null at end under REPEAT OFF`() {
94
83
  player.repeatModeState = RepeatMode.OFF
@@ -103,13 +92,6 @@ class TrackPlayerSkipTest {
103
92
  assertEquals(0, player.computeNextIndex())
104
93
  }
105
94
 
106
- @Test
107
- fun `computePreviousIndex wraps to last under REPEAT QUEUE at index 0`() {
108
- player.repeatModeState = RepeatMode.QUEUE
109
- player.currentTrackIndex = 0
110
- assertEquals(4, player.computePreviousIndex())
111
- }
112
-
113
95
  // --- skipToNextInternal / skipToPreviousInternal
114
96
 
115
97
  @Test
@@ -293,7 +275,7 @@ class TrackPlayerSkipTest {
293
275
 
294
276
  private fun configureSkipBehavior(behavior: SkipToPreviousBehavior?) {
295
277
  player.configureInternal(
296
- 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 = behavior), context()
278
+ 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 = behavior), context()
297
279
  )
298
280
  player.tracks = (1..5).map { track("t$it") }
299
281
  }
@@ -50,7 +50,7 @@ class TrackPlayerTransportTest {
50
50
  player = TrackPlayer()
51
51
  serviceHandle = bindPlaybackServiceForTest(player, context())
52
52
  player.configureInternal(
53
- 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()
53
+ 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()
54
54
  )
55
55
  }
56
56
 
@@ -80,7 +80,7 @@ class TrackPlayerTransportTest {
80
80
  // currentTrackIndex while the ExoPlayer is still IDLE.
81
81
  // iOS parity: paused-with-queue returns PAUSED, not NONE.
82
82
  player.tracks = listOf(
83
- TrackItem("t1", "https://example.com/1.m4a", null, null, null, null, null, null)
83
+ TrackItem("t1", "https://example.com/1.m4a", null, null, null, null, null, null, null, null, null, null)
84
84
  )
85
85
  player.currentTrackIndex = 0
86
86
 
@@ -104,7 +104,7 @@ class TrackPlayerTransportTest {
104
104
  // on eventually-seekable content still lands. The reset-to-start protection
105
105
  // applies once the item is loaded (STATE_READY) and confirmed unseekable.
106
106
  player.setQueueInternal(
107
- (1..3).map { TrackItem("t$it", "https://example.com/$it.m4a", null, null, null, null, null, null) }
107
+ (1..3).map { TrackItem("t$it", "https://example.com/$it.m4a", null, null, null, null, null, null, null, null, null, null) }
108
108
  )
109
109
  player.currentTrackIndex = 0
110
110
 
@@ -162,9 +162,6 @@ private class FakePlaybackEngineWithSinks : PlaybackEngine {
162
162
 
163
163
  override val mediaSessionPlayer: androidx.media3.exoplayer.ExoPlayer
164
164
  get() = error("not used in test")
165
- override suspend fun prepare(
166
- mediaItems: List<androidx.media3.common.MediaItem>, startIndex: Int, startPositionMs: Long,
167
- ) = error("not used in test")
168
165
  override fun release() = error("not used in test")
169
166
  override fun setMediaItems(items: List<androidx.media3.common.MediaItem>, startIndex: Int, startPositionMs: Long) = error("not used in test")
170
167
  override fun addMediaItems(items: List<androidx.media3.common.MediaItem>, insertBefore: Int) = error("not used in test")
@@ -197,6 +194,5 @@ private class FakePlaybackEngineWithSinks : PlaybackEngine {
197
194
  override fun detachAudioBufferSink(sink: TeeAudioProcessor.AudioBufferSink) {
198
195
  if (attachedSinks.remove(sink)) detachCount++
199
196
  }
200
- override suspend fun handoff(to: PlaybackEngine, atTrackBoundary: Boolean) = error("not used in test")
201
197
  override var delegate: PlaybackEngineDelegate? = null
202
198
  }
@@ -9,10 +9,11 @@ import kotlinx.coroutines.flow.asSharedFlow
9
9
  import kotlinx.coroutines.flow.asStateFlow
10
10
 
11
11
  /**
12
- * Test double for [CastSession.RemotePlayer]. Transport calls are no-ops;
13
- * the StateFlows are driveable via the public `_`-prefixed backing
14
- * fields; [mediaSessionPlayer] is injectable so bind tests can assert the
15
- * MediaSession swaps to it.
12
+ * Test double for [CastSession.RemotePlayer]. Transport calls record their
13
+ * name in [transportCalls]; the StateFlows are driveable via the public
14
+ * backing fields, or via [reportState] to mirror the real session's
15
+ * [hasReportedState] flag; [mediaSessionPlayer] is injectable so bind tests
16
+ * can assert the MediaSession swaps to it.
16
17
  */
17
18
  internal class FakeRemotePlayer(
18
19
  private val player: Player? = null,
@@ -36,15 +37,30 @@ internal class FakeRemotePlayer(
36
37
  override val mediaError: SharedFlow<RemoteMediaError> = backingError.asSharedFlow()
37
38
  override val seekCompleted: SharedFlow<Long> = backingSeek.asSharedFlow()
38
39
 
40
+ /** Transport calls in the order they arrived, for routing assertions. */
41
+ val transportCalls = mutableListOf<String>()
42
+
43
+ override var hasReportedState: Boolean = false
44
+
45
+ /**
46
+ * Drive the receiver state the way a live session does: a non-idle report
47
+ * sets [hasReportedState]. Set [backingState] directly to stage a state
48
+ * the receiver has not reported yet.
49
+ */
50
+ fun reportState(state: RemotePlaybackState) {
51
+ if (state != RemotePlaybackState.IDLE) hasReportedState = true
52
+ backingState.value = state
53
+ }
54
+
39
55
  override fun setVolume(value: Float) {}
40
56
  override fun close() {}
41
- override fun play() {}
42
- override fun pause() {}
43
- override fun stop() {}
44
- override fun seekTo(positionMs: Long) {}
45
- override fun queueNext() {}
46
- override fun queuePrev() {}
47
- override fun skipToQueueIndex(index: Int) {}
57
+ override fun play() { transportCalls += "play" }
58
+ override fun pause() { transportCalls += "pause" }
59
+ override fun stop() { transportCalls += "stop" }
60
+ override fun seekTo(positionMs: Long) { transportCalls += "seekTo:$positionMs" }
61
+ override fun queueNext() { transportCalls += "queueNext" }
62
+ override fun queuePrev() { transportCalls += "queuePrev" }
63
+ override fun skipToQueueIndex(index: Int) { transportCalls += "skipToQueueIndex:$index" }
48
64
  override fun setPlaybackRate(rate: Float) {}
49
65
  override fun setRepeatMode(mode: RemoteRepeatMode) {}
50
66
 
@@ -0,0 +1,303 @@
1
+ package com.margelo.nitro.queueplayer.cast.airplay
2
+
3
+ import android.net.Uri
4
+ import android.os.Looper
5
+ import androidx.media3.common.MediaItem
6
+ import androidx.media3.common.MediaMetadata
7
+ import androidx.media3.common.Player
8
+ import androidx.media3.common.SimpleBasePlayer
9
+ import androidx.media3.common.util.UnstableApi
10
+ import androidx.test.core.app.ApplicationProvider
11
+ import com.google.common.collect.ImmutableList
12
+ import kotlinx.coroutines.CoroutineScope
13
+ import kotlinx.coroutines.Dispatchers
14
+ import kotlinx.coroutines.cancel
15
+ import org.junit.After
16
+ import org.junit.Assert.assertEquals
17
+ import org.junit.Assert.assertFalse
18
+ import org.junit.Assert.assertTrue
19
+ import org.junit.Before
20
+ import org.junit.Test
21
+ import org.junit.runner.RunWith
22
+ import org.robolectric.RobolectricTestRunner
23
+ import org.robolectric.Shadows.shadowOf
24
+ import org.robolectric.annotation.Config
25
+ import java.io.File
26
+ import java.util.Base64
27
+ import java.time.Duration
28
+ import java.util.concurrent.CopyOnWriteArrayList
29
+ import java.util.concurrent.Executors
30
+ import java.util.concurrent.TimeUnit
31
+
32
+ /**
33
+ * Receiver-display sync lifecycle: what reaches the receiver while a sync is
34
+ * running, and that nothing reaches it once the session has been torn down.
35
+ *
36
+ * The teardown cases drive `stop()` from a non-looper thread, which is where
37
+ * it comes from in production — the JS thread on a user disconnect, the
38
+ * native drain thread on receiver death.
39
+ */
40
+ @RunWith(RobolectricTestRunner::class)
41
+ @Config(sdk = [34])
42
+ @UnstableApi
43
+ class AirPlayMetadataSyncTest {
44
+
45
+ private lateinit var player: FakePlayer
46
+ private lateinit var target: RecordingTarget
47
+ private lateinit var sync: AirPlayMetadataSync
48
+ private val scope = CoroutineScope(Dispatchers.Main.immediate)
49
+
50
+ private fun context() = ApplicationProvider.getApplicationContext<android.content.Context>()
51
+
52
+ private fun idle() = shadowOf(Looper.getMainLooper()).idle()
53
+
54
+ private fun idleFor(ms: Long) =
55
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(ms))
56
+
57
+ /** Runs [block] off the looper and waits for it, as a real teardown does. */
58
+ private fun offLooper(block: () -> Unit) {
59
+ val executor = Executors.newSingleThreadExecutor()
60
+ try {
61
+ executor.submit(block).get(5, TimeUnit.SECONDS)
62
+ } finally {
63
+ executor.shutdownNow()
64
+ }
65
+ }
66
+
67
+ /** Waits for work dispatched off the looper, e.g. the artwork fetch. */
68
+ private fun awaitTrue(timeoutMs: Long = 5_000, condition: () -> Boolean): Boolean {
69
+ val deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs)
70
+ while (System.nanoTime() < deadline) {
71
+ idle()
72
+ if (condition()) return true
73
+ Thread.sleep(20)
74
+ }
75
+ return condition()
76
+ }
77
+
78
+ @Before
79
+ fun setUp() {
80
+ player = FakePlayer(Looper.getMainLooper())
81
+ target = RecordingTarget()
82
+ sync = AirPlayMetadataSync(context(), target, player, scope)
83
+ }
84
+
85
+ @After
86
+ fun tearDown() {
87
+ sync.stop()
88
+ idle()
89
+ scope.cancel()
90
+ }
91
+
92
+ @Test
93
+ fun `start sends the current track metadata`() {
94
+ sync.start()
95
+ idle()
96
+
97
+ assertEquals(
98
+ listOf(Triple("Everlong", "Foo Fighters", "The Colour and the Shape")),
99
+ target.metadata,
100
+ )
101
+ }
102
+
103
+ @Test
104
+ fun `progress is pushed while playing`() {
105
+ sync.start()
106
+ idle()
107
+ val afterStart = target.progress.size
108
+
109
+ idleFor(11_000)
110
+
111
+ assertTrue(
112
+ "expected progress pushes while playing, got ${target.progress.size - afterStart}",
113
+ target.progress.size - afterStart >= 2,
114
+ )
115
+ }
116
+
117
+ @Test
118
+ fun `stop from another thread halts the progress loop`() {
119
+ sync.start()
120
+ idle()
121
+ idleFor(6_000)
122
+ assertTrue(target.progress.isNotEmpty())
123
+
124
+ offLooper { sync.stop() }
125
+ idle()
126
+ val atStop = target.progress.size
127
+
128
+ idleFor(30_000)
129
+
130
+ assertEquals(atStop, target.progress.size)
131
+ }
132
+
133
+ @Test
134
+ fun `a resume callback arriving after stop starts no progress loop`() {
135
+ sync.start()
136
+ idle()
137
+ player.emitPlayWhenReady(false)
138
+ idle()
139
+
140
+ // The resume lands while listener removal is still queued behind the
141
+ // off-looper stop, so it reaches startProgressTimer on a spent sync.
142
+ offLooper { sync.stop() }
143
+ player.emitPlayWhenReady(true)
144
+ idle()
145
+ val atStop = target.progress.size
146
+
147
+ idleFor(30_000)
148
+
149
+ assertEquals(atStop, target.progress.size)
150
+ }
151
+
152
+ @Test
153
+ fun `a track change arriving after stop pushes nothing`() {
154
+ sync.start()
155
+ idle()
156
+ val atStop = target.metadata.size
157
+
158
+ offLooper { sync.stop() }
159
+ player.emitTrackChange(trackMediaItem("Monkey Wrench", "Foo Fighters", "TCATS"))
160
+ idle()
161
+
162
+ assertEquals(atStop, target.metadata.size)
163
+ }
164
+
165
+ @Test
166
+ fun `artwork from a local file reaches the receiver while running`() {
167
+ player.emitTrackChange(
168
+ trackMediaItem("Everlong", "Foo Fighters", "TCATS", artworkUri = artworkFile())
169
+ )
170
+
171
+ sync.start()
172
+
173
+ assertTrue("artwork never arrived", awaitTrue { target.artwork.isNotEmpty() })
174
+ }
175
+
176
+ @Test
177
+ fun `artwork is not fetched after stop`() {
178
+ sync.start()
179
+ idle()
180
+
181
+ // Fired while listener removal is still queued behind the off-looper
182
+ // stop, so the fetch is reached on a spent sync.
183
+ offLooper { sync.stop() }
184
+ player.emitTrackChange(
185
+ trackMediaItem("Monkey Wrench", "Foo Fighters", "TCATS", artworkUri = artworkFile())
186
+ )
187
+
188
+ // Real elapsed time, not looper time: the fetch runs on Dispatchers.IO,
189
+ // so advancing the looper clock proves nothing about whether it ran.
190
+ assertFalse(
191
+ "artwork reached the receiver after stop",
192
+ awaitTrue(2_000) { target.artwork.isNotEmpty() },
193
+ )
194
+ }
195
+
196
+ @Test
197
+ fun `start after stop does nothing`() {
198
+ sync.start()
199
+ idle()
200
+ offLooper { sync.stop() }
201
+ idle()
202
+ val atStop = target.progress.size + target.metadata.size
203
+
204
+ sync.start()
205
+ idle()
206
+ idleFor(30_000)
207
+
208
+ assertEquals(atStop, target.progress.size + target.metadata.size)
209
+ }
210
+
211
+ private fun artworkFile(): Uri {
212
+ val file = File(context().cacheDir, "artwork.png")
213
+ if (!file.exists()) file.writeBytes(PNG_1X1)
214
+ return Uri.fromFile(file)
215
+ }
216
+
217
+ private class RecordingTarget : MetadataSyncTarget {
218
+ // The artwork fetch runs on Dispatchers.IO while the test thread reads
219
+ // these, so plain ArrayLists would be shared unsynchronised.
220
+ val metadata = CopyOnWriteArrayList<Triple<String?, String?, String?>>()
221
+ val progress = CopyOnWriteArrayList<Pair<Int, Int>>()
222
+ val artwork = CopyOnWriteArrayList<Int>()
223
+
224
+ override fun setMetadata(title: String?, artist: String?, album: String?) {
225
+ metadata += Triple(title, artist, album)
226
+ }
227
+
228
+ override fun setProgress(elapsedMs: Int, durationMs: Int) {
229
+ progress += elapsedMs to durationMs
230
+ }
231
+
232
+ override fun setArtwork(imageData: ByteArray, contentType: String) {
233
+ artwork += imageData.size
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Player double exposing exactly what the sync reads: the current item's
239
+ * metadata, position, duration and whether playback is running. Each item
240
+ * carries its own period uid, so replacing it is delivered as a media-item
241
+ * transition rather than a timeline change.
242
+ */
243
+ private class FakePlayer(looper: Looper) : SimpleBasePlayer(looper) {
244
+ private var item: MediaItem =
245
+ trackMediaItem("Everlong", "Foo Fighters", "The Colour and the Shape")
246
+ private var playing = true
247
+
248
+ fun emitTrackChange(next: MediaItem) {
249
+ item = next
250
+ invalidateState()
251
+ }
252
+
253
+ fun emitPlayWhenReady(value: Boolean) {
254
+ playing = value
255
+ invalidateState()
256
+ }
257
+
258
+ override fun getState(): State =
259
+ State.Builder()
260
+ .setAvailableCommands(Player.Commands.Builder().addAllCommands().build())
261
+ .setPlaybackState(Player.STATE_READY)
262
+ .setPlayWhenReady(playing, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
263
+ .setPlaylist(
264
+ ImmutableList.of(
265
+ MediaItemData.Builder(item)
266
+ .setMediaItem(item)
267
+ .setDurationUs(300_000_000L)
268
+ .build()
269
+ )
270
+ )
271
+ .setCurrentMediaItemIndex(0)
272
+ .setContentPositionMs(1_000L)
273
+ .build()
274
+ }
275
+
276
+ private companion object {
277
+ /**
278
+ * A real 1x1 PNG — Robolectric decodes artwork through ImageIO, so a
279
+ * stub header fails to decode and the fetch reports no artwork.
280
+ */
281
+ val PNG_1X1: ByteArray = Base64.getDecoder().decode(
282
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
283
+ )
284
+
285
+ fun trackMediaItem(
286
+ title: String,
287
+ artist: String,
288
+ album: String,
289
+ artworkUri: Uri? = null,
290
+ ): MediaItem =
291
+ MediaItem.Builder()
292
+ .setUri("https://example.invalid/${title.replace(' ', '-')}.mp3")
293
+ .setMediaMetadata(
294
+ MediaMetadata.Builder()
295
+ .setTitle(title)
296
+ .setArtist(artist)
297
+ .setAlbumTitle(album)
298
+ .apply { artworkUri?.let { setArtworkUri(it) } }
299
+ .build()
300
+ )
301
+ .build()
302
+ }
303
+ }