react-native-queue-player 1.2.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/consumer-rules.pro +1 -1
  4. package/android/src/main/cpp/airplay2_control.c +4 -4
  5. package/android/src/main/cpp/airplay2_control.h +4 -4
  6. package/android/src/main/cpp/airplay2_jni.cpp +3 -19
  7. package/android/src/main/cpp/airplay2_pair.c +7 -7
  8. package/android/src/main/cpp/airplay2_pair.h +2 -2
  9. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  10. package/android/src/main/cpp/airplay2_session.c +2 -2
  11. package/android/src/main/cpp/airplay_jni.cpp +8 -69
  12. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  61. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  62. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  63. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  64. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  65. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  114. package/app.plugin.js +17 -1
  115. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  116. package/ios/AVQueueBuilder.swift +120 -85
  117. package/ios/ArtworkLoader.swift +0 -7
  118. package/ios/ArtworkResolver.swift +22 -4
  119. package/ios/AssetReplayGainReader.swift +118 -0
  120. package/ios/AudioSession.swift +67 -16
  121. package/ios/AudioTapProvider.swift +284 -149
  122. package/ios/CarPlayBridge.swift +0 -12
  123. package/ios/CarPlayCoordinator.swift +18 -2
  124. package/ios/CarPlaySceneDelegate.swift +2 -2
  125. package/ios/Cast/AirPlayRouteState.swift +9 -8
  126. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  127. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  128. package/ios/Cast/Core/CastSession.swift +2 -2
  129. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  130. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  131. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  132. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  133. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  134. package/ios/CastManager.swift +1 -1
  135. package/ios/CrossfadeEngine.swift +672 -226
  136. package/ios/EQTap.swift +20 -17
  137. package/ios/Equalizer.swift +1 -1
  138. package/ios/FLACStreamInfo.swift +65 -0
  139. package/ios/GaplessEngine.swift +57 -30
  140. package/ios/InputGuards.swift +35 -5
  141. package/ios/LookaheadCache.swift +229 -58
  142. package/ios/LookaheadCachePrefetcher.swift +48 -36
  143. package/ios/MediaResponseCheck.swift +122 -0
  144. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  145. package/ios/NetworkRecoveryPolicy.swift +32 -0
  146. package/ios/NowPlayingFormatExtractor.swift +25 -18
  147. package/ios/NowPlayingInfo.swift +84 -48
  148. package/ios/OutputRouteMonitor.swift +16 -3
  149. package/ios/PendingBrowseRequests.swift +1 -1
  150. package/ios/PlaceholderArtwork.swift +9 -5
  151. package/ios/PlaybackEngine.swift +165 -76
  152. package/ios/PlaybackErrorMapping.swift +3 -3
  153. package/ios/PlaybackModeStateMachine.swift +2 -2
  154. package/ios/PlaybackNetworkMonitor.swift +73 -0
  155. package/ios/PlayerStateDerivation.swift +6 -6
  156. package/ios/QueueMutationArithmetic.swift +25 -0
  157. package/ios/QueueSkipArithmetic.swift +5 -3
  158. package/ios/QueueWindowArithmetic.swift +152 -0
  159. package/ios/ReadThroughServer.swift +283 -0
  160. package/ios/RemoteCommands.swift +44 -4
  161. package/ios/ReplayGainData.swift +22 -3
  162. package/ios/ReplayGainExtractor.swift +70 -27
  163. package/ios/Siri/VoiceDonation.swift +14 -9
  164. package/ios/StreamingBitrateProbe.swift +22 -68
  165. package/ios/SuppliedReplayGain.swift +83 -0
  166. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  167. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  168. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  169. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  170. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  171. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  172. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  173. package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
  174. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  175. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  176. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  177. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  178. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  179. package/ios/Tests/EngineSwapTests.swift +91 -0
  180. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  181. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  182. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  183. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  184. package/ios/Tests/InputGuardsTests.swift +41 -0
  185. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  186. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  187. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  188. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  189. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  190. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  191. package/ios/Tests/MediaResponseCheckTests.swift +128 -0
  192. package/ios/Tests/MutationDeferralTests.swift +220 -0
  193. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  194. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  195. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  196. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  197. package/ios/Tests/PlayerFixtures.swift +48 -0
  198. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  199. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  200. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  201. package/ios/Tests/QueueStateTests.swift +3 -1
  202. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  203. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  204. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  205. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  206. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  207. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  208. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  209. package/ios/Tests/RetryRecoveryTests.swift +418 -0
  210. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  211. package/ios/Tests/SkipIndexTests.swift +32 -2
  212. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  213. package/ios/Tests/StallRecoveryTests.swift +97 -0
  214. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  215. package/ios/Tests/TopUpWindowGateTests.swift +441 -0
  216. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  217. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  218. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  219. package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
  220. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  221. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  222. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  223. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  224. package/ios/TrackPlayer+Automotive.swift +101 -0
  225. package/ios/TrackPlayer+Cache.swift +232 -0
  226. package/ios/TrackPlayer+Config.swift +291 -0
  227. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  228. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  229. package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
  230. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  231. package/ios/TrackPlayer+Lifecycle.swift +982 -0
  232. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  233. package/ios/TrackPlayer+Queue.swift +951 -0
  234. package/ios/TrackPlayer+Recovery.swift +213 -0
  235. package/ios/TrackPlayer+Skip.swift +465 -0
  236. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  237. package/ios/TrackPlayer+State.swift +108 -0
  238. package/ios/TrackPlayer+Threading.swift +135 -0
  239. package/ios/TrackPlayer+Transport.swift +272 -0
  240. package/ios/TrackPlayer+Window.swift +227 -0
  241. package/ios/TrackPlayer.swift +331 -4619
  242. package/ios/Visualizer.swift +6 -5
  243. package/ios/tests-harness/Podfile +1 -1
  244. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  245. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  246. package/lib/module/hooks/useActiveTrack.js +29 -22
  247. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  248. package/lib/module/hooks/useCast.js +8 -26
  249. package/lib/module/hooks/useCast.js.map +1 -1
  250. package/lib/module/hooks/useEqualizer.js +19 -12
  251. package/lib/module/hooks/useEqualizer.js.map +1 -1
  252. package/lib/module/hooks/useLookaheadCache.js +3 -7
  253. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  254. package/lib/module/hooks/useQueue.js +66 -19
  255. package/lib/module/hooks/useQueue.js.map +1 -1
  256. package/lib/module/index.js +6 -3
  257. package/lib/module/index.js.map +1 -1
  258. package/lib/module/queueDelta.js +41 -0
  259. package/lib/module/queueDelta.js.map +1 -0
  260. package/lib/module/types.js +28 -5
  261. package/lib/module/types.js.map +1 -1
  262. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  263. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  264. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  265. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  266. package/lib/typescript/hooks/useCast.d.ts +6 -1
  267. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  269. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  271. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  272. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  273. package/lib/typescript/index.d.ts +2 -1
  274. package/lib/typescript/index.d.ts.map +1 -1
  275. package/lib/typescript/queueDelta.d.ts +10 -0
  276. package/lib/typescript/queueDelta.d.ts.map +1 -0
  277. package/lib/typescript/types.d.ts +168 -12
  278. package/lib/typescript/types.d.ts.map +1 -1
  279. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  280. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  281. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  282. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  283. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  284. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  285. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  320. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  321. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  322. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  323. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  324. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  325. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  326. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  327. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  328. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  329. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  330. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  331. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  332. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  333. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  334. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  335. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  336. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  337. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  338. package/package.json +8 -8
  339. package/src/TrackPlayer.nitro.ts +53 -27
  340. package/src/hooks/useActiveTrack.ts +29 -22
  341. package/src/hooks/useCast.ts +14 -15
  342. package/src/hooks/useEqualizer.ts +19 -12
  343. package/src/hooks/useLookaheadCache.ts +3 -7
  344. package/src/hooks/useQueue.ts +66 -17
  345. package/src/index.ts +13 -3
  346. package/src/queueDelta.ts +41 -0
  347. package/src/types.ts +169 -12
  348. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  349. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  350. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  351. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  352. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  353. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  354. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  355. package/ios/MetadataReader.swift +0 -555
  356. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  357. package/ios/Tests/MetadataReaderTests.swift +0 -603
  358. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  359. package/ios/Tests/VoiceDonationTests.swift +0 -26
  360. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  361. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  362. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -1,11 +1,16 @@
1
1
  package com.margelo.nitro.queueplayer
2
2
 
3
+ import android.net.Uri
3
4
  import androidx.media3.common.util.UnstableApi
5
+ import androidx.media3.datasource.DataSource
6
+ import androidx.media3.datasource.DataSpec
4
7
  import androidx.media3.datasource.DefaultHttpDataSource
8
+ import androidx.media3.datasource.TransferListener
5
9
  import androidx.media3.datasource.cache.CacheDataSource
6
10
  import androidx.test.core.app.ApplicationProvider
7
11
  import org.junit.After
8
12
  import org.junit.Assert.assertEquals
13
+ import org.junit.Assert.assertFalse
9
14
  import org.junit.Assert.assertNotNull
10
15
  import org.junit.Assert.assertTrue
11
16
  import org.junit.Before
@@ -20,8 +25,8 @@ import java.io.File
20
25
  *
21
26
  * Exercises the cache directly without going through ExoPlayer:
22
27
  * - Constructor creates the cache directory under `context.cacheDir`.
23
- * - `wrapDataSourceFactory` returns a [CacheDataSource.Factory]-shaped
24
- * factory rooted at this cache.
28
+ * - `wrapDataSourceFactory` hands out a [CacheDataSource] while the cache
29
+ * serves playback and the bare upstream source while it does not.
25
30
  * - `release()` is idempotent, and a fresh cache can be constructed
26
31
  * at the same directory afterwards (the SimpleCache one-instance-
27
32
  * per-directory invariant is restored on release).
@@ -69,25 +74,88 @@ class LookaheadCacheTest {
69
74
  }
70
75
 
71
76
  @Test
72
- fun `wrapDataSourceFactory returns a CacheDataSource Factory`() {
77
+ fun `the wrapped factory reads through the cache while it serves playback`() {
73
78
  val upstream = DefaultHttpDataSource.Factory()
74
79
  val wrapped = cache.wrapDataSourceFactory(upstream)
75
- assertNotNull(wrapped)
80
+ val source = wrapped.createDataSource()
76
81
  assertTrue(
77
- "wrapped factory must produce a CacheDataSource so reads check the LRU first",
78
- wrapped is CacheDataSource.Factory
82
+ "a data source opened while the cache is enabled checks the disk first",
83
+ source is CacheDataSource
79
84
  )
80
85
  }
81
86
 
87
+ /**
88
+ * Only the prefetcher fills the cache. The playback factory is built with a
89
+ * null cache-write sink, which is what Media3 reads as `cacheIsReadOnly`, so
90
+ * a body pulled all the way through it leaves nothing on disk.
91
+ */
92
+ @Test
93
+ fun `a body read through the playback factory is not written to the cache`() {
94
+ val body = ByteArray(4096) { it.toByte() }
95
+ val url = "https://example.com/playback-read.mp3"
96
+ val source = cache.wrapDataSourceFactory(FixedBodyDataSource.Factory(body))
97
+ .createDataSource()
98
+
99
+ source.open(DataSpec.Builder().setUri(url).setKey(url).build())
100
+ val buffer = ByteArray(1024)
101
+ var total = 0
102
+ while (true) {
103
+ val read = source.read(buffer, 0, buffer.size)
104
+ if (read == -1) break
105
+ total += read
106
+ }
107
+ source.close()
108
+
109
+ assertEquals("the whole body reaches the caller", body.size, total)
110
+ assertEquals("nothing is written to disk", 0L, cache.currentSizeBytes())
111
+ assertFalse("and the url is not held as cached", cache.isFullyCached(url))
112
+ }
113
+
114
+ /** Serves a fixed body for any [DataSpec], honouring its position. */
115
+ private class FixedBodyDataSource(private val body: ByteArray) : DataSource {
116
+ class Factory(private val body: ByteArray) : DataSource.Factory {
117
+ override fun createDataSource(): DataSource = FixedBodyDataSource(body)
118
+ }
119
+
120
+ private var uri: Uri? = null
121
+ private var position = 0
122
+
123
+ override fun open(dataSpec: DataSpec): Long {
124
+ uri = dataSpec.uri
125
+ position = dataSpec.position.toInt()
126
+ return (body.size - position).toLong()
127
+ }
128
+
129
+ override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
130
+ if (position >= body.size) return -1
131
+ val take = minOf(length, body.size - position)
132
+ body.copyInto(buffer, offset, position, position + take)
133
+ position += take
134
+ return take
135
+ }
136
+
137
+ override fun getUri(): Uri? = uri
138
+ override fun close() {}
139
+ override fun addTransferListener(transferListener: TransferListener) {}
140
+ }
141
+
142
+ /**
143
+ * Disabled means every read goes to the origin and nothing is written,
144
+ * whatever the disk holds — the wrapper hands out the bare upstream source.
145
+ * Decided per data source, so a change reaches the next item load.
146
+ */
82
147
  @Test
83
- fun `wrapped factory creates DataSources rooted at the same SimpleCache`() {
148
+ fun `the wrapped factory bypasses the cache while it does not serve playback`() {
84
149
  val upstream = DefaultHttpDataSource.Factory()
85
150
  val wrapped = cache.wrapDataSourceFactory(upstream)
86
- val source = wrapped.createDataSource()
87
- assertTrue(
88
- "createDataSource must produce a CacheDataSource — the cache check happens at read time",
89
- source is androidx.media3.datasource.cache.CacheDataSource
90
- )
151
+
152
+ cache.servesPlayback = false
153
+ val bypassed = wrapped.createDataSource()
154
+ cache.servesPlayback = true
155
+ val served = wrapped.createDataSource()
156
+
157
+ assertTrue("disabled: the upstream source, no cache in front", bypassed !is CacheDataSource)
158
+ assertTrue("re-enabled: the next source goes through the cache again", served is CacheDataSource)
91
159
  }
92
160
 
93
161
  @Test
@@ -125,13 +193,11 @@ class LookaheadCacheTest {
125
193
  }
126
194
 
127
195
  @Test
128
- fun `default max size constant matches the documented bootstrap budget`() {
129
- // Guards against an accidental tweak the bootstrap budget is
130
- // load-bearing for the 1.24-1.27 interim (consumers see the
131
- // library's automatic disk usage before the runtime config API
132
- // ships).
196
+ fun `default max size is 100 MB`() {
197
+ // The default is what a consumer that omits `lookaheadCacheMaxSizeMb`
198
+ // from `configure()` gets, so it is part of the public contract.
133
199
  assertEquals(
134
- "bootstrap budget should be 100 MB the runtime config",
200
+ "default disk budget",
135
201
  100L * 1024L * 1024L,
136
202
  LookaheadCache.DEFAULT_MAX_SIZE_BYTES
137
203
  )
@@ -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
  }
@@ -0,0 +1,163 @@
1
+ package com.margelo.nitro.queueplayer
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertNull
5
+ import org.junit.Test
6
+
7
+ /**
8
+ * What the cache will and will not believe is a track, judged from a response's
9
+ * declared type and the first bytes of its body. Mirrors iOS's
10
+ * `MediaResponseCheckTests` case for case so the two platforms refuse and
11
+ * accept the same responses.
12
+ */
13
+ class MediaResponseCheckTest {
14
+
15
+ private val xmlEnvelope =
16
+ """<api-response status="failed"><error code="70"/></api-response>""".toByteArray()
17
+ private val mp3 = byteArrayOf(0x49, 0x44, 0x33, 0x04, 0, 0, 0, 0)
18
+
19
+ private fun rejection(
20
+ contentType: String?,
21
+ body: ByteArray,
22
+ whole: Boolean = true
23
+ ): MediaResponseCheck.Rejection? =
24
+ MediaResponseCheck.rejection(contentType, body, headIsStartOfResource = whole)
25
+
26
+ @Test
27
+ fun markupAndJsonContentTypesAreRefused() {
28
+ for (type in listOf(
29
+ "application/xml", "text/xml", "text/html",
30
+ "application/xhtml+xml", "application/json", "text/json",
31
+ )) {
32
+ assertEquals(
33
+ "$type is a document type",
34
+ MediaResponseCheck.Rejection.MarkupContentType(type),
35
+ rejection(type, xmlEnvelope)
36
+ )
37
+ }
38
+ }
39
+
40
+ @Test
41
+ fun theDeclaredTypeIsReadWithoutItsParameters() {
42
+ assertEquals(
43
+ MediaResponseCheck.Rejection.MarkupContentType("application/xml"),
44
+ rejection("application/xml; charset=utf-8", xmlEnvelope)
45
+ )
46
+ assertEquals(
47
+ MediaResponseCheck.Rejection.MarkupContentType("application/xml"),
48
+ rejection("APPLICATION/XML", xmlEnvelope)
49
+ )
50
+ }
51
+
52
+ @Test
53
+ fun aDeclaredDocumentIsRefusedOnAPartialBodyToo() {
54
+ assertEquals(
55
+ MediaResponseCheck.Rejection.MarkupContentType("application/xml"),
56
+ rejection("application/xml", byteArrayOf(0x3C, 0x00), whole = false)
57
+ )
58
+ }
59
+
60
+ @Test
61
+ fun aPartialBodyIsNotJudgedByItsLeadingBytes() {
62
+ // The same markup, under a type that is not itself a document, so only the
63
+ // leading-byte rule can decide: refused as a whole resource, accepted as a
64
+ // range, which is what separates the two inputs.
65
+ val markup = "<api-response/>".toByteArray()
66
+ assertEquals(
67
+ MediaResponseCheck.Rejection.MarkupBody,
68
+ rejection("application/octet-stream", markup)
69
+ )
70
+ assertNull(
71
+ "a body that begins part way into a file says nothing about its first bytes",
72
+ rejection("application/octet-stream", markup, whole = false)
73
+ )
74
+ }
75
+
76
+ @Test
77
+ fun textWithoutAContainerSignatureIsRefused() {
78
+ assertEquals(
79
+ MediaResponseCheck.Rejection.TextWithoutMediaSignature("text/plain"),
80
+ rejection("text/plain", "not found".toByteArray())
81
+ )
82
+ }
83
+
84
+ @Test
85
+ fun textCarryingAContainerSignatureIsAccepted() {
86
+ assertNull(
87
+ "a server mislabelling audio as text does not make it a document",
88
+ rejection("text/plain", mp3)
89
+ )
90
+ }
91
+
92
+ @Test
93
+ fun aPlaylistDeclaredAsTextIsAccepted() {
94
+ assertNull(rejection("text/plain", "#EXTM3U\n#EXT-X-VERSION:3".toByteArray()))
95
+ }
96
+
97
+ @Test
98
+ fun aMarkupBodyIsRefusedWhenNothingUsableIsDeclared() {
99
+ assertEquals(MediaResponseCheck.Rejection.MarkupBody, rejection(null, xmlEnvelope))
100
+ assertEquals(
101
+ MediaResponseCheck.Rejection.MarkupBody,
102
+ rejection("application/octet-stream", xmlEnvelope)
103
+ )
104
+ assertEquals(
105
+ MediaResponseCheck.Rejection.MarkupBody,
106
+ rejection(null, "<!DOCTYPE html><html>".toByteArray())
107
+ )
108
+ assertEquals(
109
+ MediaResponseCheck.Rejection.MarkupBody,
110
+ rejection(null, """{"error":"not found"}""".toByteArray())
111
+ )
112
+ }
113
+
114
+ @Test
115
+ fun leadingSpaceAndAByteOrderMarkDoNotHideAMarkupBody() {
116
+ assertEquals(
117
+ MediaResponseCheck.Rejection.MarkupBody,
118
+ rejection(null, "\n <?xml version=\"1.0\"?>".toByteArray())
119
+ )
120
+ assertEquals(
121
+ MediaResponseCheck.Rejection.MarkupBody,
122
+ rejection(null, byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()) + "<error/>".toByteArray())
123
+ )
124
+ // `FF FE` also satisfies the MPEG frame sync, so a UTF-16 document would
125
+ // read as audio if the mark were not dropped first.
126
+ assertEquals(
127
+ MediaResponseCheck.Rejection.MarkupBody,
128
+ rejection(null, byteArrayOf(0xFF.toByte(), 0xFE.toByte()) + "<e/>".toByteArray())
129
+ )
130
+ }
131
+
132
+ @Test
133
+ fun recognisedContainersAreAccepted() {
134
+ val bodies = mapOf(
135
+ "ID3" to byteArrayOf(0x49, 0x44, 0x33, 0x04, 0, 0, 0, 0),
136
+ "MPEG frame sync" to byteArrayOf(0xFF.toByte(), 0xFB.toByte(), 0x90.toByte(), 0, 0, 0, 0, 0),
137
+ "ADTS AAC" to byteArrayOf(0xFF.toByte(), 0xF1.toByte(), 0x50, 0x80.toByte(), 0, 0, 0, 0),
138
+ "FLAC" to byteArrayOf(0x66, 0x4C, 0x61, 0x43, 0, 0, 0, 0),
139
+ "Ogg" to byteArrayOf(0x4F, 0x67, 0x67, 0x53, 0, 0, 0, 0),
140
+ "WAV" to byteArrayOf(0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0),
141
+ "AIFF" to byteArrayOf(0x46, 0x4F, 0x52, 0x4D, 0, 0, 0, 0),
142
+ "CAF" to byteArrayOf(0x63, 0x61, 0x66, 0x66, 0, 0, 0, 0),
143
+ "MP4" to byteArrayOf(0, 0, 0, 0x20, 0x66, 0x74, 0x79, 0x70),
144
+ )
145
+ for ((name, bytes) in bodies) {
146
+ assertNull("$name is media", rejection("text/plain", bytes))
147
+ assertNull("$name is media", rejection(null, bytes))
148
+ }
149
+ }
150
+
151
+ @Test
152
+ fun anUndeclaredOrOpaqueBinaryBodyIsAccepted() {
153
+ val binary = byteArrayOf(0x00, 0x01, 0x02, 0x03, 0x04)
154
+ assertNull("an unrecognised container is not a document", rejection(null, binary))
155
+ assertNull(rejection("application/octet-stream", binary))
156
+ assertNull(rejection("audio/mpeg", binary))
157
+ }
158
+
159
+ @Test
160
+ fun anEmptyBodyIsNotJudged() {
161
+ assertNull(rejection(null, ByteArray(0)))
162
+ }
163
+ }