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
@@ -0,0 +1,85 @@
1
+ package com.margelo.nitro.queueplayer
2
+
3
+ import androidx.media3.common.util.UnstableApi
4
+ import androidx.test.core.app.ApplicationProvider
5
+ import org.junit.Assert.assertTrue
6
+ import org.junit.Test
7
+ import org.junit.runner.RunWith
8
+ import org.robolectric.RobolectricTestRunner
9
+ import org.robolectric.annotation.Config
10
+ import java.util.concurrent.CountDownLatch
11
+ import java.util.concurrent.TimeUnit
12
+
13
+ /**
14
+ * Cancelling a prefetch stops the write that is running, not only the
15
+ * coroutine around it.
16
+ */
17
+ @RunWith(RobolectricTestRunner::class)
18
+ @Config(sdk = [34])
19
+ @UnstableApi
20
+ class LookaheadCacheWriterCancelTest {
21
+
22
+ private fun httpTrack(id: String) = TrackItem(
23
+ id = id, url = "https://example.com/$id.m4a", title = null, artist = null, album = null,
24
+ duration = null, artworkUrl = null, extras = null,
25
+ replayGainTrackGain = null, replayGainTrackPeak = null,
26
+ replayGainAlbumGain = null, replayGainAlbumPeak = null)
27
+
28
+ /**
29
+ * `CacheWriter.cache()` blocks and polls only its own cancelled flag, so a
30
+ * cancelled coroutine alone would leave it writing. The download body
31
+ * relays the cancellation onto the writer; `CacheWriter` then closes its
32
+ * source, which is what this observes.
33
+ */
34
+ @Test
35
+ fun `cancel stops the running download, not just its coroutine`() {
36
+ val cache = LookaheadCache(
37
+ context = ApplicationProvider.getApplicationContext(),
38
+ maxSizeBytes = 4L * 1024L * 1024L,
39
+ cacheDirName = "writer-cancel-${System.nanoTime()}"
40
+ )
41
+ val source = TricklingDataSource()
42
+ val writer = LookaheadCacheWriter.create(
43
+ cache = cache,
44
+ httpFactory = androidx.media3.datasource.DataSource.Factory { source },
45
+ isOnCellular = { false }
46
+ )
47
+ try {
48
+ // The window is the tracks ahead of the current one.
49
+ writer.reschedule(listOf(httpTrack("current"), httpTrack("slow")), currentIndex = 0, lookaheadCount = 1)
50
+ assertTrue("the download must have opened its source", source.opened.await(5, TimeUnit.SECONDS))
51
+
52
+ writer.cancel()
53
+
54
+ assertTrue("a cancelled write closes its source", source.closed.await(5, TimeUnit.SECONDS))
55
+ } finally {
56
+ writer.release()
57
+ cache.release()
58
+ }
59
+ }
60
+
61
+ /** A source that hands out one byte at a time, forever, until closed. */
62
+ private class TricklingDataSource : androidx.media3.datasource.DataSource {
63
+ val opened = CountDownLatch(1)
64
+ val closed = CountDownLatch(1)
65
+ private var uri: android.net.Uri? = null
66
+
67
+ override fun addTransferListener(transferListener: androidx.media3.datasource.TransferListener) {}
68
+ override fun open(dataSpec: androidx.media3.datasource.DataSpec): Long {
69
+ uri = dataSpec.uri
70
+ opened.countDown()
71
+ return androidx.media3.common.C.LENGTH_UNSET.toLong()
72
+ }
73
+ override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
74
+ if (length == 0) return 0
75
+ Thread.sleep(2)
76
+ buffer[offset] = 0
77
+ return 1
78
+ }
79
+ override fun getUri(): android.net.Uri? = uri
80
+ override fun close() {
81
+ closed.countDown()
82
+ }
83
+ }
84
+
85
+ }
@@ -57,6 +57,18 @@ class LookaheadCacheWriterTest {
57
57
  assertTrue(w.computeTargets(emptyList(), currentIndex = 0, lookaheadCount = 3).isEmpty())
58
58
  }
59
59
 
60
+ @Test
61
+ fun `computeTargets clamps a lookaheadCount that would overflow the range`() {
62
+ // The count arrives as a JS Double, so Infinity reaches Int.MAX_VALUE here
63
+ // and `start + count` wraps negative — a range subList rejects outright.
64
+ val w = newWriter()
65
+ val tracks = (1..3).map { httpTrack("t$it") }
66
+ assertEquals(
67
+ 2,
68
+ w.computeTargets(tracks, currentIndex = 0, lookaheadCount = Int.MAX_VALUE).size
69
+ )
70
+ }
71
+
60
72
  @Test
61
73
  fun `computeTargets returns empty for non-positive lookaheadCount`() {
62
74
  val w = newWriter()
@@ -632,6 +644,103 @@ class LookaheadCacheWriterTest {
632
644
 
633
645
  // --- Helpers
634
646
 
647
+ // --- allowsCellularAccess
648
+
649
+ @Test
650
+ fun `prefetch is skipped while cellular is not permitted and the network is cellular`() = runTest {
651
+ val downloads = mutableListOf<String>()
652
+ val writer = LookaheadCacheWriter(
653
+ downloader = { track -> downloads.add(track.id ?: "") },
654
+ scope = CoroutineScope(StandardTestDispatcher(testScheduler) + SupervisorJob()),
655
+ isOnCellular = { true }
656
+ )
657
+ writer.allowsCellularAccess = false
658
+
659
+ writer.reschedule((0..4).map { httpTrack("t$it") }, currentIndex = 0, lookaheadCount = 3)
660
+ advanceUntilIdle()
661
+
662
+ assertTrue(downloads.isEmpty())
663
+ assertNull(writer.orchestratorJob)
664
+ }
665
+
666
+ @Test
667
+ fun `prefetch runs on a non-cellular network even when cellular is not permitted`() = runTest {
668
+ val downloads = mutableListOf<String>()
669
+ val writer = LookaheadCacheWriter(
670
+ downloader = { track -> downloads.add(track.id ?: "") },
671
+ scope = CoroutineScope(StandardTestDispatcher(testScheduler) + SupervisorJob()),
672
+ isOnCellular = { false }
673
+ )
674
+ writer.allowsCellularAccess = false
675
+
676
+ writer.reschedule((0..4).map { httpTrack("t$it") }, currentIndex = 0, lookaheadCount = 3)
677
+ advanceUntilIdle()
678
+
679
+ assertEquals(listOf("t1", "t2", "t3"), downloads)
680
+ }
681
+
682
+ @Test
683
+ fun `prefetch runs on cellular while it is permitted`() = runTest {
684
+ val downloads = mutableListOf<String>()
685
+ val writer = LookaheadCacheWriter(
686
+ downloader = { track -> downloads.add(track.id ?: "") },
687
+ scope = CoroutineScope(StandardTestDispatcher(testScheduler) + SupervisorJob()),
688
+ isOnCellular = { true }
689
+ )
690
+
691
+ writer.reschedule((0..4).map { httpTrack("t$it") }, currentIndex = 0, lookaheadCount = 3)
692
+ advanceUntilIdle()
693
+
694
+ assertEquals(listOf("t1", "t2", "t3"), downloads)
695
+ }
696
+
697
+ @Test
698
+ fun `handing off to cellular cancels the download in flight`() = runBlocking<Unit> {
699
+ val started = CompletableDeferred<Unit>()
700
+ val release = CompletableDeferred<Unit>()
701
+ var onCellular = false
702
+ val writer = LookaheadCacheWriter(
703
+ downloader = { started.complete(Unit); release.await() },
704
+ scope = CoroutineScope(SupervisorJob()),
705
+ isOnCellular = { onCellular }
706
+ )
707
+ writer.allowsCellularAccess = false
708
+ val queue = (0..4).map { httpTrack("t$it") }
709
+ writer.reschedule(queue, currentIndex = 0, lookaheadCount = 3)
710
+ started.await()
711
+ // Captured before the handoff: the orchestrator clears `inFlightJob` on its
712
+ // own thread once the cancelled download resumes it, so reading the slot
713
+ // afterwards races that clear.
714
+ val inFlightBefore = writer.inFlightJob
715
+
716
+ onCellular = true
717
+ writer.reschedule(queue, currentIndex = 0, lookaheadCount = 3)
718
+
719
+ assertTrue("the download in flight must be cancelled", inFlightBefore?.isCancelled == true)
720
+ release.complete(Unit)
721
+ }
722
+
723
+ @Test
724
+ fun `turning cellular back on refills the window on the next reschedule`() = runTest {
725
+ val downloads = mutableListOf<String>()
726
+ val writer = LookaheadCacheWriter(
727
+ downloader = { track -> downloads.add(track.id ?: "") },
728
+ scope = CoroutineScope(StandardTestDispatcher(testScheduler) + SupervisorJob()),
729
+ isOnCellular = { true }
730
+ )
731
+ val queue = (0..4).map { httpTrack("t$it") }
732
+ writer.allowsCellularAccess = false
733
+ writer.reschedule(queue, currentIndex = 0, lookaheadCount = 3)
734
+ advanceUntilIdle()
735
+ assertTrue(downloads.isEmpty())
736
+
737
+ writer.allowsCellularAccess = true
738
+ writer.reschedule(queue, currentIndex = 0, lookaheadCount = 3)
739
+ advanceUntilIdle()
740
+
741
+ assertEquals(listOf("t1", "t2", "t3"), downloads)
742
+ }
743
+
635
744
  private fun httpTrack(id: String): TrackItem =
636
745
  makeTrack(id, "https://example.com/$id.m4a")
637
746
 
@@ -644,6 +753,8 @@ class LookaheadCacheWriterTest {
644
753
  album = null,
645
754
  duration = null,
646
755
  artworkUrl = null,
647
- extras = null
756
+ extras = null,
757
+ replayGainTrackGain = null, replayGainTrackPeak = null,
758
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
648
759
  )
649
760
  }
@@ -171,7 +171,7 @@ class MediaItemBuilderTest {
171
171
  // Parity with iOS `AVQueueBuilderTests.testMakeItemForFileURL
172
172
  // SkipsHeaderDictionary`: the test asserts the URI scheme is
173
173
  // preserved, but does NOT assert the file/remote routing
174
- // downstream. The factory plumbing is opaque post-build on
174
+ // downstream. What the factory built is unreadable post-build on
175
175
  // both platforms (AVURLAsset options can't be read back on
176
176
  // iOS; DefaultDataSource.Factory wiring is internal on
177
177
  // Android), so the documented contract is the test.
@@ -184,7 +184,7 @@ class MediaItemBuilderTest {
184
184
  assertEquals("file", uri?.scheme)
185
185
  }
186
186
 
187
- // --- buildMediaItems MIME plumbing
187
+ // --- buildMediaItems MIME handling
188
188
 
189
189
  @Test
190
190
  fun `buildMediaItems sets MIME from URL extension for http urls`() {
@@ -474,6 +474,8 @@ class MediaItemBuilderTest {
474
474
  album = album,
475
475
  duration = duration,
476
476
  artworkUrl = artworkUrl,
477
- extras = null
477
+ extras = null,
478
+ replayGainTrackGain = null, replayGainTrackPeak = null,
479
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
478
480
  )
479
481
  }
@@ -4,7 +4,10 @@ import androidx.media3.common.Format
4
4
  import androidx.media3.common.MimeTypes
5
5
  import androidx.media3.common.util.UnstableApi
6
6
  import org.junit.Assert.assertEquals
7
+ import org.junit.Assert.assertNull
8
+ import org.junit.Rule
7
9
  import org.junit.Test
10
+ import org.junit.rules.TemporaryFolder
8
11
 
9
12
  /**
10
13
  * Pure-mapping coverage for [NowPlayingFormatExtractor]. These
@@ -14,6 +17,48 @@ import org.junit.Test
14
17
  @UnstableApi
15
18
  class NowPlayingFormatExtractorTest {
16
19
 
20
+ @get:Rule
21
+ val tempFolder = TemporaryFolder()
22
+
23
+ /**
24
+ * FLAC declares no bitrate, so Media3 reports NO_VALUE for it on local files
25
+ * as well as remote. For a file on disk the average is arithmetic — encoded
26
+ * bytes over playing time — which is the same derivation iOS uses.
27
+ */
28
+ @Test
29
+ fun averageBitrateIsBytesOverPlayingTime() {
30
+ val file = tempFolder.newFile("track.flac")
31
+ file.writeBytes(ByteArray(1_000_000))
32
+
33
+ // 1,000,000 bytes over 40 s = 8,000,000 bits / 40 = 200 kbps.
34
+ assertEquals(
35
+ 200_000.0,
36
+ NowPlayingFormatExtractor.averageBitrateOf(file.absolutePath, 40.0)!!,
37
+ 0.001
38
+ )
39
+ }
40
+
41
+ /**
42
+ * Every input that would make the answer invented rather than measured
43
+ * yields null: the player has not resolved a duration yet, the duration is
44
+ * nonsense, the path is absent, or the file has no bytes.
45
+ */
46
+ @Test
47
+ fun averageBitrateIsNullWhenItCannotBeMeasured() {
48
+ val file = tempFolder.newFile("track.flac")
49
+ file.writeBytes(ByteArray(1_000))
50
+
51
+ assertNull(NowPlayingFormatExtractor.averageBitrateOf(file.absolutePath, null))
52
+ assertNull(NowPlayingFormatExtractor.averageBitrateOf(file.absolutePath, 0.0))
53
+ assertNull(NowPlayingFormatExtractor.averageBitrateOf(file.absolutePath, -5.0))
54
+ assertNull(NowPlayingFormatExtractor.averageBitrateOf(null, 40.0))
55
+ assertNull(
56
+ NowPlayingFormatExtractor.averageBitrateOf(
57
+ tempFolder.newFile("empty.flac").absolutePath, 40.0))
58
+ assertNull(
59
+ NowPlayingFormatExtractor.averageBitrateOf("/nonexistent/nope.flac", 40.0))
60
+ }
61
+
17
62
  @Test
18
63
  fun mapCodecCoversAACAliases() {
19
64
  assertEquals(AudioCodec.AAC, NowPlayingFormatExtractor.mapCodec(MimeTypes.AUDIO_AAC))
@@ -60,7 +60,7 @@ class PitchAwareAudioProcessorChainTest {
60
60
 
61
61
  @Test
62
62
  fun `signalsmith stays active on the float path even when not stretching`() {
63
- // The fix for Media3's static pipeline: Signalsmith must remain in the
63
+ // Media3's pipeline is static: Signalsmith must remain in the
64
64
  // active set regardless of mode/speed so it can stretch when music kicks in.
65
65
  val chain = newConfiguredChain()
66
66
  chain.setMode(PitchCorrectionMode.VOICE)
@@ -7,8 +7,16 @@ import androidx.media3.session.LibraryResult
7
7
  import androidx.media3.session.MediaLibraryService
8
8
  import androidx.media3.session.MediaSession
9
9
  import androidx.test.core.app.ApplicationProvider
10
+ import java.util.concurrent.CountDownLatch
11
+ import java.util.concurrent.TimeUnit
12
+ import java.util.concurrent.atomic.AtomicInteger
13
+ import kotlinx.coroutines.Dispatchers
14
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
15
+ import kotlinx.coroutines.test.resetMain
16
+ import kotlinx.coroutines.test.setMain
10
17
  import org.junit.After
11
18
  import org.junit.Assert.assertEquals
19
+ import org.junit.Assert.assertFalse
12
20
  import org.junit.Assert.assertNull
13
21
  import org.junit.Assert.assertTrue
14
22
  import org.junit.Before
@@ -167,4 +175,43 @@ class PlaybackServiceCallbackSearchTest {
167
175
  result.params
168
176
  )
169
177
  }
178
+
179
+ // --- dispatchSearchToJs
180
+
181
+ // `Dispatchers.setMain` is the only way to see which dispatcher the notify
182
+ // lands on; `notifySearchResultChanged` has no other observable effect with
183
+ // no controller connected.
184
+ @OptIn(ExperimentalCoroutinesApi::class)
185
+ @Test
186
+ fun `dispatchSearchToJs caches the answer then notifies on main`() {
187
+ // A Nitro Promise is JNI-constructed and cannot exist on the JVM, so
188
+ // the only JS answer reachable here is a throwing handler; that path
189
+ // still writes the cache and still notifies, which is the contract a
190
+ // connected controller depends on to stop waiting.
191
+ val mainDispatches = AtomicInteger(0)
192
+ val dispatched = CountDownLatch(1)
193
+ val recording = object : kotlinx.coroutines.CoroutineDispatcher() {
194
+ override fun dispatch(context: kotlin.coroutines.CoroutineContext, block: Runnable) {
195
+ mainDispatches.incrementAndGet()
196
+ dispatched.countDown()
197
+ android.os.Handler(android.os.Looper.getMainLooper()).post(block)
198
+ }
199
+ }
200
+ Dispatchers.setMain(recording)
201
+ try {
202
+ service.browseCallbacks.registerOnSearchRequest { _ ->
203
+ throw IllegalStateException("no Nitro runtime under Robolectric")
204
+ }
205
+ assertFalse(service.searchResultCache.hasResults("rock"))
206
+
207
+ service.dispatchSearchToJs("rock")
208
+
209
+ assertTrue("the notify reached a dispatcher", dispatched.await(2, TimeUnit.SECONDS))
210
+ org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle()
211
+ assertEquals("notify is dispatched to main", 1, mainDispatches.get())
212
+ assertEquals(emptyList<BrowseItem>(), service.searchResultCache.getResults("rock"))
213
+ } finally {
214
+ Dispatchers.resetMain()
215
+ }
216
+ }
170
217
  }
@@ -259,19 +259,6 @@ class PlaybackServiceCallbackTest {
259
259
  assertTrue(commands.contains(Player.COMMAND_SET_SPEED_AND_PITCH))
260
260
  }
261
261
 
262
- @Test
263
- fun `refreshAvailableCommands is a no-op when no handler is registered`() {
264
- // No handler, no controllers — exercising the early return.
265
- binder.refreshAvailableCommands()
266
- // Re-issue with a handler but no connected controllers — also a no-op.
267
- binder.setCommandHandler(RecordingHandler())
268
- binder.refreshAvailableCommands()
269
- // The per-controller iteration body (session.setAvailableCommands)
270
- // requires a live MediaController that has completed handshake,
271
- // which is not reachable from Robolectric without standing up a
272
- // full MediaController.Builder.buildAsync() flow.
273
- }
274
-
275
262
  // --- TrackPlayer-side handler implementation
276
263
 
277
264
  @Test
@@ -291,8 +278,8 @@ class PlaybackServiceCallbackTest {
291
278
  val player = newConfiguredPlayer()
292
279
  try {
293
280
  player.tracks = listOf(
294
- TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
295
- TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
281
+ TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
282
+ TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
296
283
  )
297
284
  player.currentTrackIndex = 0
298
285
 
@@ -326,8 +313,8 @@ class PlaybackServiceCallbackTest {
326
313
 
327
314
  // Two-track queue at index 0 — skip-next now allowed.
328
315
  player.tracks = listOf(
329
- TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
330
- TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
316
+ TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
317
+ TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
331
318
  )
332
319
  player.currentTrackIndex = 0
333
320
  player.recomputeCapabilities()
@@ -352,8 +339,8 @@ class PlaybackServiceCallbackTest {
352
339
  val player = newConfiguredPlayer()
353
340
  try {
354
341
  player.tracks = listOf(
355
- TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
356
- TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
342
+ TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
343
+ TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
357
344
  )
358
345
  player.currentTrackIndex = 1
359
346
 
@@ -377,9 +364,9 @@ class PlaybackServiceCallbackTest {
377
364
  val player = newConfiguredPlayer()
378
365
  try {
379
366
  player.tracks = listOf(
380
- TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
381
- TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
382
- TrackItem("c", "https://example.com/c.m4a", null, null, null, null, null, null),
367
+ TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
368
+ TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
369
+ TrackItem("c", "https://example.com/c.m4a", null, null, null, null, null, null, null, null, null, null),
383
370
  )
384
371
  player.currentTrackIndex = 0
385
372
 
@@ -404,8 +391,8 @@ class PlaybackServiceCallbackTest {
404
391
  val player = newConfiguredPlayer()
405
392
  try {
406
393
  player.tracks = listOf(
407
- TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
408
- TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
394
+ TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
395
+ TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
409
396
  )
410
397
  player.currentTrackIndex = 1
411
398
 
@@ -426,8 +413,8 @@ class PlaybackServiceCallbackTest {
426
413
  val player = newConfiguredPlayer()
427
414
  try {
428
415
  player.tracks = listOf(
429
- TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
430
- TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
416
+ TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
417
+ TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
431
418
  )
432
419
  player.currentTrackIndex = 1
433
420
 
@@ -444,6 +431,35 @@ class PlaybackServiceCallbackTest {
444
431
 
445
432
  // --- Voice / browse-leaf routing through onAddMediaItems / onSetMediaItems
446
433
 
434
+ @Test
435
+ fun `onAddMediaItems asks for append and onSetMediaItems does not`() {
436
+ val fakeEnv = RecordingEnvironment(service)
437
+ val routingCallback = PlaybackServiceCallback(fakeEnv)
438
+ fakeEnv.idResolution = listOf(
439
+ MediaItem.Builder().setMediaId("resolved")
440
+ .setUri("https://example.com/resolved.m4a".toUri()).build()
441
+ )
442
+ fun idOnlyItem() = MediaItem.Builder().setMediaId("album:1").build()
443
+
444
+ routingCallback.onAddMediaItems(
445
+ session, controllerInfo, mutableListOf(idOnlyItem())
446
+ ).get()
447
+ // "Queue up X" grows the queue; Media3 appends what the resolution
448
+ // returns, so the lib's own queue has to grow by the same amount rather
449
+ // than being replaced by the new tracks alone.
450
+ assertEquals(listOf(true), fakeEnv.idAppendFlags)
451
+
452
+ fakeEnv.idAppendFlags.clear()
453
+ routingCallback.onSetMediaItems(
454
+ session, controllerInfo, mutableListOf(idOnlyItem()), 0, 0L
455
+ ).get()
456
+ // A browse-leaf tap is a genuine replace: it takes the no-sync route,
457
+ // where the JS handler's own `setQueue` is the only writer, so no
458
+ // append-flagged request reaches the environment at all.
459
+ assertEquals(listOf("album:1"), fakeEnv.idRequestsNoSync)
460
+ assertEquals(emptyList<Boolean>(), fakeEnv.idAppendFlags)
461
+ }
462
+
447
463
  @Test
448
464
  fun `onAddMediaItems with searchQuery dispatches to onPlayFromSearchRequest`() {
449
465
  val fakeEnv = RecordingEnvironment(service)
@@ -783,7 +799,8 @@ class PlaybackServiceCallbackTest {
783
799
  networkTimeoutMs = null,
784
800
  audioContentType = null,
785
801
  audioCategoryOptions = null,
786
- visualizationEnabled = null,
802
+ voiceVocabularyDonationEnabled = null,
803
+ visualizationEnabled = null,
787
804
  clampSeekToBuffered = null,
788
805
  lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null,
789
806
  ),
@@ -878,19 +895,26 @@ class PlaybackServiceCallbackTest {
878
895
  service.playerCommandsFor(capability)
879
896
 
880
897
  override fun onPlayFromSearchRequest(
881
- request: MediaSearchRequest
898
+ request: MediaSearchRequest,
899
+ append: Boolean
882
900
  ): ListenableFuture<List<MediaItem>> {
883
901
  searchRequests += request
902
+ searchAppendFlags += append
884
903
  return Futures.immediateFuture(searchResolution)
885
904
  }
886
905
 
887
906
  override fun onPlayFromIdRequest(
888
- mediaId: String
907
+ mediaId: String,
908
+ append: Boolean
889
909
  ): ListenableFuture<List<MediaItem>> {
890
910
  idRequests += mediaId
911
+ idAppendFlags += append
891
912
  return Futures.immediateFuture(idResolution)
892
913
  }
893
914
 
915
+ val searchAppendFlags = mutableListOf<Boolean>()
916
+ val idAppendFlags = mutableListOf<Boolean>()
917
+
894
918
  override fun onPlayFromIdRequestNoSync(mediaId: String) {
895
919
  idRequestsNoSync += mediaId
896
920
  }
@@ -49,19 +49,19 @@ class PlaybackServiceLifecycleTest {
49
49
  val controller = Robolectric.buildService(PlaybackService::class.java).create()
50
50
  try {
51
51
  val binder = bindLocal(controller.get())
52
- assertNotNull("binder.player is initialised in onCreate", binder.player)
52
+ assertNotNull("the engine's player is initialised in onCreate", binder.engine.mediaSessionPlayer)
53
53
  assertNotNull("binder.session is initialised in onCreate", binder.session)
54
54
  // The session binds to the stable SessionCommandForwardingPlayer
55
55
  // wrapper for its lifetime; the wrapper forwards to the engine's
56
- // ExoPlayer the binder exposes.
56
+ // own ExoPlayer.
57
57
  val sessionPlayer = binder.session.player
58
58
  assertTrue(
59
59
  "binder.session.player is the stable forwarding wrapper",
60
60
  sessionPlayer is SessionCommandForwardingPlayer,
61
61
  )
62
62
  assertSame(
63
- "the wrapper forwards to the ExoPlayer instance the binder exposes",
64
- binder.player,
63
+ "the wrapper forwards to the engine's ExoPlayer instance",
64
+ binder.engine.mediaSessionPlayer,
65
65
  (sessionPlayer as SessionCommandForwardingPlayer).wrappedPlayer,
66
66
  )
67
67
  assertEquals(
@@ -173,8 +173,8 @@ class PlaybackServiceLifecycleTest {
173
173
  val binder = bindLocal(service)
174
174
  // A loaded + play-ready session: the previous behaviour kept the service
175
175
  // alive here. Swipe-away is now an unconditional kill.
176
- binder.player.addMediaItem(MediaItem.fromUri("http://example.com/a.mp3"))
177
- binder.player.playWhenReady = true
176
+ binder.engine.mediaSessionPlayer.addMediaItem(MediaItem.fromUri("http://example.com/a.mp3"))
177
+ binder.engine.mediaSessionPlayer.playWhenReady = true
178
178
  shadowOf(Looper.getMainLooper()).idle()
179
179
 
180
180
  service.onTaskRemoved(null)
@@ -183,7 +183,7 @@ class PlaybackServiceLifecycleTest {
183
183
  assertEquals(
184
184
  "the player is stopped (idle) on swipe-away",
185
185
  Player.STATE_IDLE,
186
- binder.player.playbackState,
186
+ binder.engine.mediaSessionPlayer.playbackState,
187
187
  )
188
188
  assertTrue(
189
189
  "the service stops even with a queued, play-ready session",
@@ -198,7 +198,7 @@ class PlaybackServiceLifecycleTest {
198
198
  fun `onDestroy releases the player and clears the session`() {
199
199
  val controller = Robolectric.buildService(PlaybackService::class.java).create()
200
200
  val binder = bindLocal(controller.get())
201
- val player = binder.player
201
+ val player = binder.engine.mediaSessionPlayer
202
202
  val session: MediaSession = binder.session
203
203
  assertFalse("player is live before destroy", player.isReleased)
204
204
 
@@ -134,7 +134,7 @@ class QueueSkipArithmeticTest {
134
134
  @Test
135
135
  fun `computePrevious OFF — at index 0 returns null (no wrap)`() {
136
136
  // The "tap Previous at track 0 restarts current" UX behaviour
137
- // is applied by the skipToPrevious glue, not here.
137
+ // is applied by `skipToPrevious`, not here.
138
138
  assertNull(
139
139
  QueueSkipArithmetic.computePrevious(5, 0, QueueSkipRepeatMode.OFF)
140
140
  )
@@ -443,4 +443,36 @@ class QueueSkipArithmeticTest {
443
443
  assertFalse(QueueSkipArithmetic.canSkipPrevious(3, 0, QueueSkipRepeatMode.OFF, restartEnabled = false))
444
444
  assertTrue(QueueSkipArithmetic.canSkipPrevious(3, 1, QueueSkipRepeatMode.OFF, restartEnabled = false))
445
445
  }
446
+
447
+ @Test
448
+ fun `canSkipPrevious restartEnabled — nothing playing is false in any mode`() {
449
+ // Nothing is loaded, so there is no track to restart and no previous
450
+ // track to reach: resolveSkipToPrevious returns NoOp for this state.
451
+ for (mode in QueueSkipRepeatMode.entries) {
452
+ assertFalse(QueueSkipArithmetic.canSkipPrevious(5, -1, mode, restartEnabled = true))
453
+ }
454
+ }
455
+
456
+ @Test
457
+ fun `canSkipPrevious agrees with resolveSkipToPrevious`() {
458
+ // canSkipPrevious is the enabled-state of the same decision
459
+ // resolveSkipToPrevious makes; the two must never disagree.
460
+ for (count in listOf(0, 1, 3)) {
461
+ for (index in listOf(-1, 0, 1, 2)) {
462
+ for (mode in QueueSkipRepeatMode.entries) {
463
+ for (restart in listOf(false, true)) {
464
+ val can = QueueSkipArithmetic.canSkipPrevious(count, index, mode, restart)
465
+ val resolved = QueueSkipArithmetic.resolveSkipToPrevious(
466
+ count, index, mode, restart, positionMs = 0
467
+ )
468
+ assertEquals(
469
+ "count=$count index=$index mode=$mode restart=$restart",
470
+ can,
471
+ resolved != QueueSkipArithmetic.SkipToPreviousAction.NoOp
472
+ )
473
+ }
474
+ }
475
+ }
476
+ }
477
+ }
446
478
  }