react-native-queue-player 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/build.gradle +7 -0
  4. package/android/consumer-rules.pro +72 -3
  5. package/android/src/main/cpp/airplay2_control.c +4 -4
  6. package/android/src/main/cpp/airplay2_control.h +4 -4
  7. package/android/src/main/cpp/airplay2_jni.cpp +26 -22
  8. package/android/src/main/cpp/airplay2_pair.c +7 -7
  9. package/android/src/main/cpp/airplay2_pair.h +2 -2
  10. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  11. package/android/src/main/cpp/airplay2_session.c +2 -2
  12. package/android/src/main/cpp/airplay_jni.cpp +20 -72
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
  61. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  62. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  63. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  64. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  65. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
  114. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
  115. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  116. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  117. package/app.plugin.js +17 -1
  118. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  119. package/ios/AVQueueBuilder.swift +120 -85
  120. package/ios/ArtworkLoader.swift +0 -7
  121. package/ios/ArtworkResolver.swift +22 -4
  122. package/ios/AssetReplayGainReader.swift +118 -0
  123. package/ios/AudioSession.swift +67 -16
  124. package/ios/AudioTapProvider.swift +284 -149
  125. package/ios/CarPlayBridge.swift +0 -12
  126. package/ios/CarPlayCoordinator.swift +18 -2
  127. package/ios/CarPlaySceneDelegate.swift +2 -2
  128. package/ios/Cast/AirPlayRouteState.swift +9 -8
  129. package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
  130. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  131. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  132. package/ios/Cast/Core/CastSession.swift +11 -0
  133. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  134. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  135. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  136. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  137. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  138. package/ios/CastManager.swift +1 -1
  139. package/ios/CrossfadeEngine.swift +660 -226
  140. package/ios/EQTap.swift +20 -17
  141. package/ios/Equalizer.swift +1 -1
  142. package/ios/FLACStreamInfo.swift +65 -0
  143. package/ios/GaplessEngine.swift +57 -30
  144. package/ios/InputGuards.swift +35 -5
  145. package/ios/LookaheadCache.swift +202 -58
  146. package/ios/LookaheadCachePrefetcher.swift +48 -36
  147. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  148. package/ios/NetworkRecoveryPolicy.swift +32 -0
  149. package/ios/NowPlayingFormatExtractor.swift +25 -18
  150. package/ios/NowPlayingInfo.swift +84 -48
  151. package/ios/OutputRouteMonitor.swift +16 -3
  152. package/ios/PendingBrowseRequests.swift +1 -1
  153. package/ios/PlaceholderArtwork.swift +9 -5
  154. package/ios/PlaybackEngine.swift +165 -76
  155. package/ios/PlaybackErrorMapping.swift +3 -3
  156. package/ios/PlaybackModeStateMachine.swift +2 -2
  157. package/ios/PlaybackNetworkMonitor.swift +73 -0
  158. package/ios/PlayerStateDerivation.swift +6 -6
  159. package/ios/QueueMutationArithmetic.swift +25 -0
  160. package/ios/QueueSkipArithmetic.swift +5 -3
  161. package/ios/QueueWindowArithmetic.swift +152 -0
  162. package/ios/ReadThroughServer.swift +283 -0
  163. package/ios/RemoteCommands.swift +44 -4
  164. package/ios/ReplayGainData.swift +22 -3
  165. package/ios/ReplayGainExtractor.swift +70 -27
  166. package/ios/Siri/VoiceDonation.swift +14 -9
  167. package/ios/StreamingBitrateProbe.swift +22 -68
  168. package/ios/SuppliedReplayGain.swift +83 -0
  169. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  170. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  171. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  172. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  173. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  174. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  175. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  176. package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
  177. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  178. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  179. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  180. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  181. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  182. package/ios/Tests/EngineSwapTests.swift +91 -0
  183. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  184. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  185. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  186. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  187. package/ios/Tests/InputGuardsTests.swift +41 -0
  188. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  189. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  190. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  191. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  192. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  193. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  194. package/ios/Tests/MutationDeferralTests.swift +220 -0
  195. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  196. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  197. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  198. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  199. package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
  200. package/ios/Tests/PlayerFixtures.swift +48 -0
  201. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  202. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  203. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  204. package/ios/Tests/QueueStateTests.swift +3 -1
  205. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  206. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  207. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  208. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  209. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  210. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  211. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  212. package/ios/Tests/RetryRecoveryTests.swift +303 -0
  213. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  214. package/ios/Tests/SkipIndexTests.swift +32 -2
  215. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  216. package/ios/Tests/StallRecoveryTests.swift +97 -0
  217. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  218. package/ios/Tests/TopUpWindowGateTests.swift +394 -0
  219. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  220. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  221. package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
  222. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  223. package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
  224. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  225. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  226. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  227. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  228. package/ios/TrackPlayer+Automotive.swift +101 -0
  229. package/ios/TrackPlayer+Cache.swift +232 -0
  230. package/ios/TrackPlayer+Config.swift +291 -0
  231. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  232. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  233. package/ios/TrackPlayer+EventsDispatch.swift +968 -0
  234. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  235. package/ios/TrackPlayer+Lifecycle.swift +930 -0
  236. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  237. package/ios/TrackPlayer+Queue.swift +940 -0
  238. package/ios/TrackPlayer+Recovery.swift +196 -0
  239. package/ios/TrackPlayer+Skip.swift +455 -0
  240. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  241. package/ios/TrackPlayer+State.swift +108 -0
  242. package/ios/TrackPlayer+Threading.swift +135 -0
  243. package/ios/TrackPlayer+Transport.swift +267 -0
  244. package/ios/TrackPlayer+Window.swift +195 -0
  245. package/ios/TrackPlayer.swift +305 -4610
  246. package/ios/Visualizer.swift +6 -5
  247. package/ios/tests-harness/Podfile +1 -1
  248. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  249. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  250. package/lib/module/hooks/useActiveTrack.js +29 -22
  251. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  252. package/lib/module/hooks/useCast.js +8 -26
  253. package/lib/module/hooks/useCast.js.map +1 -1
  254. package/lib/module/hooks/useEqualizer.js +19 -12
  255. package/lib/module/hooks/useEqualizer.js.map +1 -1
  256. package/lib/module/hooks/useLookaheadCache.js +3 -7
  257. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  258. package/lib/module/hooks/useQueue.js +66 -19
  259. package/lib/module/hooks/useQueue.js.map +1 -1
  260. package/lib/module/index.js +6 -3
  261. package/lib/module/index.js.map +1 -1
  262. package/lib/module/queueDelta.js +41 -0
  263. package/lib/module/queueDelta.js.map +1 -0
  264. package/lib/module/types.js +28 -5
  265. package/lib/module/types.js.map +1 -1
  266. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  267. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  269. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useCast.d.ts +6 -1
  271. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  272. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  273. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  274. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  275. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  276. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  277. package/lib/typescript/index.d.ts +2 -1
  278. package/lib/typescript/index.d.ts.map +1 -1
  279. package/lib/typescript/queueDelta.d.ts +10 -0
  280. package/lib/typescript/queueDelta.d.ts.map +1 -0
  281. package/lib/typescript/types.d.ts +168 -12
  282. package/lib/typescript/types.d.ts.map +1 -1
  283. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  284. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  285. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  286. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  287. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  288. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  289. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  320. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  321. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  322. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  323. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  324. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  325. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  326. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  327. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  328. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  329. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  330. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  331. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  332. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  333. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  334. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  335. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  336. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  337. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  338. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  339. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  340. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  341. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  342. package/package.json +8 -8
  343. package/src/TrackPlayer.nitro.ts +53 -27
  344. package/src/hooks/useActiveTrack.ts +29 -22
  345. package/src/hooks/useCast.ts +14 -15
  346. package/src/hooks/useEqualizer.ts +19 -12
  347. package/src/hooks/useLookaheadCache.ts +3 -7
  348. package/src/hooks/useQueue.ts +66 -17
  349. package/src/index.ts +13 -3
  350. package/src/queueDelta.ts +41 -0
  351. package/src/types.ts +169 -12
  352. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  353. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
  354. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  355. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  356. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  357. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  358. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  359. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  360. package/ios/MetadataReader.swift +0 -555
  361. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  362. package/ios/Tests/MetadataReaderTests.swift +0 -603
  363. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  364. package/ios/Tests/VoiceDonationTests.swift +0 -26
  365. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  366. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  367. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -43,9 +43,13 @@ class LookaheadCache(
43
43
  evictionPolicy: EvictionPolicy = EvictionPolicy.LRU
44
44
  ) {
45
45
 
46
- /** Resolved on-disk cache directory: `context.cacheDir/<cacheDirName>`. */
46
+ /**
47
+ * Resolved on-disk cache directory: `context.cacheDir/<cacheDirName>`.
48
+ * Resolved through the application context, as the shared-instance key is,
49
+ * so a wrapper context with its own `cacheDir` cannot split the two.
50
+ */
47
51
  @VisibleForTesting
48
- val cacheDir: File = File(context.cacheDir, cacheDirName)
52
+ val cacheDir: File = File(context.applicationContext.cacheDir, cacheDirName)
49
53
 
50
54
  private val shared: SharedCacheInstance =
51
55
  sharedInstanceFor(context.applicationContext, cacheDir, maxSizeBytes, evictionPolicy)
@@ -58,7 +62,28 @@ class LookaheadCache(
58
62
  @VisibleForTesting
59
63
  internal val simpleCache: SimpleCache = shared.cache
60
64
 
61
- private val databaseProvider: StandaloneDatabaseProvider = shared.databaseProvider
65
+ private val protectingEvictor: FifoCacheEvictor? = shared.protectingEvictor
66
+
67
+ /**
68
+ * Name the urls eviction must not take — the playing track and its
69
+ * neighbours. Eviction runs on every span commit, so without this the entry
70
+ * backing the playing track can be deleted while it plays.
71
+ *
72
+ * A no-op under the LRU policy, whose evictor has no protected-key concept.
73
+ * LRU is not an equivalent safeguard — it bumps the recency of the span just
74
+ * read, not the whole resource — so on Android the playing track is only
75
+ * genuinely protected under FIFO. iOS protects under both policies.
76
+ */
77
+ fun setProtectedUrls(urls: Set<String>) {
78
+ protectingEvictor?.setProtectedKeys(urls)
79
+ }
80
+
81
+ /**
82
+ * Whether this cache's evictor honours [setProtectedUrls]. False under LRU,
83
+ * letting callers skip building a set that would be discarded.
84
+ */
85
+ val honoursProtection: Boolean
86
+ get() = protectingEvictor != null
62
87
 
63
88
  /**
64
89
  * Per-URL MIME store populated by the prefetch path's
@@ -89,8 +114,24 @@ class LookaheadCache(
89
114
  * transparently fall through to a re-download instead of
90
115
  * surfacing a load error.
91
116
  */
92
- fun wrapDataSourceFactory(upstream: DataSource.Factory): DataSource.Factory =
93
- cacheDataSourceFactory(upstream)
117
+ fun wrapDataSourceFactory(upstream: DataSource.Factory): DataSource.Factory {
118
+ val cached = cacheDataSourceFactory(upstream)
119
+ // Decided per data source, which Media3 creates per item load, so a
120
+ // change to `servesPlayback` reaches the next item without rebuilding
121
+ // the player; the item already loading keeps the source it opened.
122
+ return DataSource.Factory {
123
+ if (servesPlayback) cached.createDataSource() else upstream.createDataSource()
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Whether the player's reads go through this cache. False while the
129
+ * consumer has the cache disabled: every read then goes to the origin and
130
+ * nothing is written, whatever the disk already holds. Written on main by
131
+ * `TrackPlayer`, read on Media3's loading thread when an item opens.
132
+ */
133
+ @Volatile
134
+ var servesPlayback: Boolean = true
94
135
 
95
136
  /**
96
137
  * Build a [CacheDataSource.Factory] rooted at [simpleCache] +
@@ -121,8 +162,13 @@ class LookaheadCache(
121
162
  // helping" signal. Log the reason so a "why is this not
122
163
  // cached" investigation has a trail.
123
164
  .setEventListener(object : CacheDataSource.EventListener {
124
- // Required by EventListener; no per-byte progress surfaced today.
125
- override fun onCachedBytesRead(cacheSizeBytes: Long, cachedBytesRead: Long) { }
165
+ override fun onCachedBytesRead(cacheSizeBytes: Long, cachedBytesRead: Long) {
166
+ // Accumulated rather than logged per read: this fires per buffer, so
167
+ // logging here would flood. The running total is what answers
168
+ // "is playback actually being served from the cache", which cannot
169
+ // be told apart from a network read by listening.
170
+ cachedBytesReadTotal.addAndGet(cachedBytesRead)
171
+ }
126
172
  override fun onCacheIgnored(reason: Int) {
127
173
  val name = when (reason) {
128
174
  CacheDataSource.CACHE_IGNORED_REASON_ERROR -> "ERROR"
@@ -137,7 +183,13 @@ class LookaheadCache(
137
183
  })
138
184
 
139
185
  /**
140
- * Drop per-instance state. The [SimpleCache] + [databaseProvider]
186
+ * Total bytes served to the player from cache rather than upstream.
187
+ * Monotonic for the cache's lifetime; read as a delta across an interval.
188
+ */
189
+ val cachedBytesReadTotal = java.util.concurrent.atomic.AtomicLong(0)
190
+
191
+ /**
192
+ * Drop per-instance state. The [SimpleCache] + database provider
141
193
  * stay alive (process-singleton, see class doc) — releasing them
142
194
  * would race Media3's `lockedFolders` static registry on the next
143
195
  * [LookaheadCache] construction targeting the same directory.
@@ -173,18 +225,17 @@ class LookaheadCache(
173
225
  // --- Status surface ---
174
226
 
175
227
  /**
176
- * Sum of cached bytes across every key in the cache. Walks
177
- * `simpleCache.keys` + `getCachedBytes` per key. O(N) on the
178
- * key count + cheap span lookup. Used by
179
- * [TrackPlayer.getLookaheadCacheStatus] for the
180
- * `currentSizeMb` field.
228
+ * Bytes currently held on disk. Reads the total Media3 already maintains, so
229
+ * it is O(1) and needs no lock beyond the cache's own.
230
+ *
231
+ * Also more accurate than summing per-key cached bytes, which reports zero
232
+ * for any resource whose content length has not been recorded yet — a
233
+ * chunked download in flight occupies disk but has no length until it
234
+ * completes. Used by [TrackPlayer.getLookaheadCacheStatus] for `currentSizeMb`.
181
235
  */
182
- fun currentSizeBytes(): Long {
183
- return simpleCache.keys.sumOf { key ->
184
- val length = contentLength(key) ?: return@sumOf 0L
185
- simpleCache.getCachedBytes(key, 0, length)
186
- }
187
- }
236
+ // Every SimpleCache method rejects a released cache, and this is read off the
237
+ // looper that releases it. A cache that has gone away holds nothing.
238
+ fun currentSizeBytes(): Long = runCatching { simpleCache.cacheSpace }.getOrDefault(0L)
188
239
 
189
240
  /**
190
241
  * Whether the cache holds the entire content for `key`. True
@@ -210,12 +261,12 @@ class LookaheadCache(
210
261
  * field — the count reflects the cache itself, not the intersection
211
262
  * with `self.tracks`.
212
263
  */
213
- fun fullyCachedCount(): Int {
214
- return simpleCache.keys.count { key ->
264
+ fun fullyCachedCount(): Int = runCatching {
265
+ simpleCache.keys.count { key ->
215
266
  val length = contentLength(key) ?: return@count false
216
267
  simpleCache.getCachedBytes(key, 0, length) >= length
217
268
  }
218
- }
269
+ }.getOrDefault(0)
219
270
 
220
271
  /**
221
272
  * Wipe every cached entry + free the on-disk space. Used by
@@ -224,9 +275,14 @@ class LookaheadCache(
224
275
  * handles the file deletion + index update internally.
225
276
  */
226
277
  fun clear() {
227
- val keys = simpleCache.keys.toList()
278
+ // Every SimpleCache method rejects a released cache, the key walk included.
279
+ // A policy change releases the shared instance out from under an in-flight
280
+ // wipe, and this now runs off the caller's thread, so an escaping throw
281
+ // reaches the default handler rather than the JS call that asked for it.
282
+ // Nothing is left to clear in that case — the release took the files with it.
283
+ val keys = runCatching { simpleCache.keys.toList() }.getOrElse { return }
228
284
  for (key in keys) {
229
- simpleCache.removeResource(key)
285
+ runCatching { simpleCache.removeResource(key) }.getOrElse { return }
230
286
  }
231
287
  mimeStore.clear()
232
288
  }
@@ -312,11 +368,20 @@ class LookaheadCache(
312
368
  EvictionPolicy.FIFO -> FifoCacheEvictor(maxSizeBytes)
313
369
  }
314
370
  val cache = SimpleCache(cacheDir, evictor, db)
315
- val newInstance = SharedCacheInstance(cache, db)
371
+ val newInstance = SharedCacheInstance(cache, db, evictor as? FifoCacheEvictor, evictionPolicy)
316
372
  sharedInstances[key] = newInstance
317
373
  return newInstance
318
374
  }
319
375
 
376
+ /**
377
+ * The eviction policy the live cache for [cacheDir] was built with, or null
378
+ * when this process holds no cache there. Media3 fixes the evictor at
379
+ * construction, so the instance is the record of what was applied.
380
+ */
381
+ @JvmStatic
382
+ internal fun appliedEvictionPolicy(cacheDir: File): EvictionPolicy? =
383
+ sharedInstances[cacheDir.absolutePath]?.evictionPolicy
384
+
320
385
  /**
321
386
  * Release + forget the shared SimpleCache for [cacheDir] so the next
322
387
  * [LookaheadCache] construction builds a fresh one. Used when the eviction
@@ -326,18 +391,27 @@ class LookaheadCache(
326
391
  */
327
392
  @JvmStatic
328
393
  @Synchronized
329
- internal fun releaseSharedInstance(cacheDir: File) {
330
- sharedInstances.remove(cacheDir.absolutePath)?.cache?.release()
394
+ internal fun releaseSharedInstance(cacheDir: File): StandaloneDatabaseProvider? {
395
+ val instance = sharedInstances.remove(cacheDir.absolutePath) ?: return null
396
+ instance.cache.release()
397
+ // Handed back so the caller can drop the cache's index tables along with
398
+ // its files. Index storage is keyed by the cache uid, so deleting the
399
+ // directory alone strands one table per policy change.
400
+ return instance.databaseProvider
331
401
  }
332
402
  }
333
403
 
334
404
  /**
335
405
  * Pair of process-singleton SimpleCache + DatabaseProvider for a
336
406
  * single cache folder. Held by the companion object's
337
- * [sharedInstances] map; never released within the process lifetime.
407
+ * [sharedInstances] map until [releaseSharedInstance] drops it for a
408
+ * policy change; a destroy leaves it in place.
338
409
  */
339
410
  private data class SharedCacheInstance(
340
411
  val cache: SimpleCache,
341
- val databaseProvider: StandaloneDatabaseProvider
412
+ val databaseProvider: StandaloneDatabaseProvider,
413
+ /** Null under LRU, which has no protected-key concept. */
414
+ val protectingEvictor: FifoCacheEvictor?,
415
+ val evictionPolicy: EvictionPolicy,
342
416
  )
343
417
  }
@@ -9,6 +9,7 @@ import androidx.media3.datasource.DataSource
9
9
  import androidx.media3.datasource.DataSpec
10
10
  import androidx.media3.datasource.cache.CacheDataSource
11
11
  import androidx.media3.datasource.cache.CacheWriter
12
+ import java.io.IOException
12
13
  import kotlinx.coroutines.CancellationException
13
14
  import kotlinx.coroutines.CoroutineScope
14
15
  import kotlinx.coroutines.Deferred
@@ -16,7 +17,10 @@ import kotlinx.coroutines.Dispatchers
16
17
  import kotlinx.coroutines.Job
17
18
  import kotlinx.coroutines.SupervisorJob
18
19
  import kotlinx.coroutines.async
20
+ import kotlinx.coroutines.awaitCancellation
19
21
  import kotlinx.coroutines.cancel
22
+ import kotlinx.coroutines.ensureActive
23
+ import kotlinx.coroutines.isActive
20
24
  import kotlinx.coroutines.launch
21
25
  import kotlinx.coroutines.withContext
22
26
 
@@ -48,10 +52,10 @@ import kotlinx.coroutines.withContext
48
52
  * flaky-network resilience:
49
53
  *
50
54
  * - Per-track download failure (IOException, HTTP error, transient
51
- * network drop): swallowed via [logSwallowed]; the orchestrator
52
- * continues to the next pending target. The failed URL is NOT
53
- * marked as cached or in-flight, so it remains eligible for
54
- * re-attempt on a subsequent reschedule.
55
+ * network drop): caught in the orchestrator loop and logged at
56
+ * `Log.w`; the orchestrator continues to the next pending target.
57
+ * The failed URL is NOT marked as cached or in-flight, so it
58
+ * remains eligible for re-attempt on a subsequent reschedule.
55
59
  * - Complete network outage (every target throws): orchestrator
56
60
  * drains pendingTargets cleanly, leaves [inFlightUrl] null, and
57
61
  * exits. The next [reschedule] call re-enqueues the still-desired
@@ -75,9 +79,21 @@ import kotlinx.coroutines.withContext
75
79
  class LookaheadCacheWriter @VisibleForTesting internal constructor(
76
80
  @get:VisibleForTesting internal val downloader: suspend (TrackItem) -> Unit,
77
81
  @get:VisibleForTesting internal val scope: CoroutineScope,
78
- @get:VisibleForTesting internal val isFullyCached: (String) -> Boolean = { false }
82
+ @get:VisibleForTesting internal val isFullyCached: (String) -> Boolean = { false },
83
+ /** True when the active network is cellular. Consulted only while
84
+ * [allowsCellularAccess] is false. */
85
+ internal val isOnCellular: () -> Boolean = { false }
79
86
  ) {
80
87
 
88
+ /**
89
+ * Whether prefetch may run over cellular. False empties the desired
90
+ * window while the active network is cellular, so nothing is
91
+ * enqueued and an in-flight download is cancelled on the next
92
+ * [reschedule]. Set from `setLookaheadCache`.
93
+ */
94
+ @Volatile
95
+ internal var allowsCellularAccess: Boolean = true
96
+
81
97
  /**
82
98
  * Optional callback fired after each per-track download iteration
83
99
  * completes (success, failure, or cancellation handled). Used by
@@ -182,6 +198,14 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
182
198
  val desired = mutableListOf<TrackItem>()
183
199
  desired.addAll(computeTargets(tracks, currentIndex, lookaheadCount))
184
200
  lookbackTarget(tracks, currentIndex)?.let { desired.add(it) }
201
+ // Prefetch downloads whole upcoming tracks, so on a metered connection it
202
+ // can spend a lot of data on tracks the listener skips past. When it is not
203
+ // permitted nothing is desired, and the reconciliation below prunes what is
204
+ // pending and cancels what is in flight. The next reschedule refills it —
205
+ // a track advance, a skip, a queue change, or the flag going back on.
206
+ if (!allowsCellularAccess && isOnCellular()) {
207
+ desired.clear()
208
+ }
185
209
  val desiredUrls = desired.map { it.url }.toSet()
186
210
 
187
211
  val toCancel: Job?
@@ -225,6 +249,8 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
225
249
 
226
250
  // Cancel outside the lock — Job.cancel() is fast but can run
227
251
  // listeners on this thread.
252
+ // Cancelling the job is what stops the blocking write too: the download
253
+ // body relays its coroutine's cancellation onto its own `CacheWriter`.
228
254
  toCancel?.cancel()
229
255
 
230
256
  if (needsOrchestrator) {
@@ -266,10 +292,9 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
266
292
  }
267
293
 
268
294
  /**
269
- * Cancel + tear down the coroutine scope. Call from
270
- * [TrackPlayer.destroyInternal] before the [LookaheadCache] is
271
- * released once the cache is gone, an in-flight CacheWriter
272
- * reading from `cache.simpleCache` would NPE.
295
+ * Cancel + tear down the coroutine scope. Called before
296
+ * `LookaheadCache.release()` clears the MIME store, so a prefetch
297
+ * still in flight cannot record a MIME type after the clear.
273
298
  */
274
299
  fun release() {
275
300
  requireMainThread()
@@ -278,11 +303,12 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
278
303
  }
279
304
 
280
305
  /**
281
- * Reads/writes of `inFlightUrl`, `inFlightJob`, `orchestratorJob`,
282
- * and `pendingTargets` happen exclusively on main; this trap turns
283
- * a future off-main caller into a debug-build crash so the bug is
284
- * visible at the call site instead of as a silent race on the
285
- * cache state.
306
+ * The public entry points (reschedule / cancel / release) are main-only; this
307
+ * trap turns a future off-main caller into a crash at the call site
308
+ * so the mistake is visible there. The fields themselves
309
+ * `inFlightUrl`, `inFlightJob`, `orchestratorJob`, `pendingTargets`
310
+ * are also written from the IO orchestrator coroutine, so they are
311
+ * guarded by `stateLock`, not by thread affinity.
286
312
  *
287
313
  * `runCatching` swallows the "Method myLooper in android.os.Looper
288
314
  * not mocked" exception that fires on a JVM host without the Android
@@ -313,7 +339,11 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
313
339
  if (lookaheadCount <= 0 || tracks.isEmpty()) return emptyList()
314
340
  val start = (currentIndex + 1).coerceAtLeast(0)
315
341
  if (start >= tracks.size) return emptyList()
316
- val end = (start + lookaheadCount).coerceAtMost(tracks.size)
342
+ // `start + lookaheadCount` overflows to a negative for a very large count,
343
+ // which coerceAtMost then keeps and subList rejects. The count reaches here
344
+ // from a JS Double, so Int.MAX_VALUE is one `Infinity` away.
345
+ val remaining = tracks.size - start
346
+ val end = start + lookaheadCount.coerceAtMost(remaining)
317
347
  return tracks.subList(start, end).filter { isRemote(it.url) }
318
348
  }
319
349
 
@@ -475,7 +505,8 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
475
505
  */
476
506
  fun create(
477
507
  cache: LookaheadCache,
478
- httpFactory: DataSource.Factory
508
+ httpFactory: DataSource.Factory,
509
+ isOnCellular: () -> Boolean
479
510
  ): LookaheadCacheWriter {
480
511
  val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
481
512
  // Wrap the upstream HTTP factory so each prefetch download
@@ -492,27 +523,28 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
492
523
  return LookaheadCacheWriter(
493
524
  downloader = { track -> downloadViaCacheWriter(track, cacheDataSourceFactory) },
494
525
  scope = scope,
495
- isFullyCached = cache::isFullyCached
526
+ isFullyCached = cache::isFullyCached,
527
+ isOnCellular = isOnCellular
496
528
  )
497
529
  }
498
530
 
499
531
  /**
500
- * Per-track download body. Builds a fresh [CacheDataSource] from
501
- * the shared factory, runs the synchronous `CacheWriter.cache()`
502
- * on `Dispatchers.IO`. Coroutine cancellation propagates via the
503
- * explicit `CancellationException` catch `writer.cancel()`
504
- * sets the volatile cancel flag that the cache loop polls
505
- * between buffer iterations, then we rethrow so the parent
506
- * launch's `for (track in targets)` loop terminates.
532
+ * Per-track download body. Builds a fresh [CacheDataSource] from the
533
+ * shared factory and runs the synchronous `CacheWriter.cache()` on
534
+ * `Dispatchers.IO`. `cache()` blocks and polls only its own cancelled
535
+ * flag, so the coroutine's cancellation is relayed onto that flag by a
536
+ * child that exists to be cancelled with it each writer is tied to
537
+ * its own job, which is what keeps a cancel from reaching the download
538
+ * the orchestrator starts next.
507
539
  */
508
540
  private suspend fun downloadViaCacheWriter(
509
541
  track: TrackItem,
510
- cacheDataSourceFactory: CacheDataSource.Factory
542
+ cacheDataSourceFactory: CacheDataSource.Factory,
511
543
  ) = withContext(Dispatchers.IO) {
512
544
  val cacheDataSource = cacheDataSourceFactory.createDataSource()
513
545
  // Cache key = source URL. The consumer-supplied `track.id` is
514
- // a metadata field that varies per app (Subsonic-style hash,
515
- // Spotify URI, app-internal counter); keying by URL keeps the
546
+ // a metadata field that varies per app (a server-side hash, a
547
+ // catalogue URI, an app-internal counter); keying by URL keeps the
516
548
  // cache generic + correct across apps + matches iOS. Same
517
549
  // URL → same cache file (collision === "already cached").
518
550
  val dataSpec = DataSpec.Builder()
@@ -520,11 +552,27 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
520
552
  .setKey(track.url)
521
553
  .build()
522
554
  val writer = CacheWriter(cacheDataSource, dataSpec, /* temporaryBuffer = */ null, /* progressListener = */ null)
555
+ // Unconfined, so the relay resumes on the thread that cancels the job
556
+ // rather than waiting for an IO worker: the workers are what a
557
+ // blocking `cache()` occupies, and a cancel that queued behind them
558
+ // would leave the download running for as long as they stay busy.
559
+ val cancelRelay = launch(Dispatchers.Unconfined) {
560
+ try {
561
+ awaitCancellation()
562
+ } finally {
563
+ writer.cancel()
564
+ }
565
+ }
523
566
  try {
567
+ ensureActive()
524
568
  writer.cache()
525
- } catch (e: CancellationException) {
526
- writer.cancel()
569
+ } catch (e: IOException) {
570
+ // A cancelled writer throws InterruptedIOException; report it as the
571
+ // cancellation it is so the orchestrator does not log a failed prefetch.
572
+ if (!isActive) throw CancellationException("prefetch cancelled").also { it.initCause(e) }
527
573
  throw e
574
+ } finally {
575
+ cancelRelay.cancel()
528
576
  }
529
577
  }
530
578
  }
@@ -25,8 +25,8 @@ import androidx.media3.extractor.DefaultExtractorsFactory
25
25
  * header override) and lines up with which names
26
26
  * [DefaultHttpDataSource.Factory.setDefaultRequestProperties] as the
27
27
  * integration point. Per-track headers would require a custom
28
- * MediaSource.Factory reading per-item metadata extras tracked as
29
- * a backlog item if Subsonic-style per-request signing ever lands.
28
+ * MediaSource.Factory reading per-item metadata extras, which this
29
+ * builder does not do.
30
30
  *
31
31
  * `file://` URIs transparently route through [DefaultDataSource]
32
32
  * without touching the HTTP factory — matches iOS's file-URL
@@ -249,8 +249,8 @@ object MediaItemBuilder {
249
249
  * remain coherent.
250
250
  *
251
251
  * Read timeout: defaults to 30000 ms (vs Media3's 8s default).
252
- * The longer timeout tolerates slow-spin-up servers (e.g. Subsonic
253
- * on spun-down disks needing ~20s to respond). Connect timeout
252
+ * The longer timeout tolerates slow-spin-up servers (a media server
253
+ * on spun-down disks can take ~20s to respond). Connect timeout
254
254
  * stays at Media3's 8s default — TCP handshake is unaffected by
255
255
  * disk spin-up.
256
256
  */
@@ -264,8 +264,8 @@ object MediaItemBuilder {
264
264
  if (merged.isNotEmpty()) {
265
265
  setDefaultRequestProperties(merged)
266
266
  }
267
- // Subsonic-behind-reverse-proxy deployments redirect
268
- // http→https (or vice versa) for auth flows; disabling
267
+ // Some servers behind a reverse proxy redirect http→https (or
268
+ // vice versa) for auth flows; disabling
269
269
  // cross-protocol redirects silently truncates downloads
270
270
  // partway through a redirect.
271
271
  setAllowCrossProtocolRedirects(true)
@@ -28,6 +28,42 @@ import com.margelo.nitro.core.NullType
28
28
  @UnstableApi
29
29
  internal object NowPlayingFormatExtractor {
30
30
 
31
+ /**
32
+ * Average encoded bitrate of a `file://` source, from its size and duration.
33
+ *
34
+ * The figure is an average over the whole file, which is what "bitrate"
35
+ * means for a variable-rate codec anyway, and it includes the container's
36
+ * headers and any embedded artwork — a few kbps on a typical track.
37
+ *
38
+ * Returns null for any non-local source, for a file that cannot be measured,
39
+ * and for a duration the player has not resolved yet: a bitrate derived from
40
+ * an unknown duration would be arbitrary, and reporting nothing is the
41
+ * honest answer until the player knows.
42
+ */
43
+ private fun deriveLocalBitrate(player: Player, durationSec: Double?): Double? {
44
+ val uri = player.currentMediaItem?.localConfiguration?.uri ?: return null
45
+ if (!uri.scheme.equals("file", ignoreCase = true)) return null
46
+ return averageBitrateOf(uri.path, durationSec)
47
+ }
48
+
49
+ /**
50
+ * Average bits per second of the file at [path] over [durationSec].
51
+ *
52
+ * Split from its caller so the arithmetic is reachable without a player.
53
+ * Null for an unusable duration, an unreadable path, or an empty file —
54
+ * every case where the answer would be invented rather than measured.
55
+ */
56
+ internal fun averageBitrateOf(path: String?, durationSec: Double?): Double? {
57
+ if (durationSec == null || durationSec <= 0.0) return null
58
+ if (path == null) return null
59
+ // One `stat` of a local file, taken on the looper with the rest of the
60
+ // player snapshot; the answer is not worth an extra hop.
61
+ val bytes = runCatching { java.io.File(path).length() }.getOrNull() ?: return null
62
+ if (bytes <= 0L) return null
63
+ val bitsPerSecond = bytes * 8.0 / durationSec
64
+ return if (bitsPerSecond.isFinite() && bitsPerSecond > 0.0) bitsPerSecond else null
65
+ }
66
+
31
67
  /**
32
68
  * Extract the format snapshot from `player`'s currently selected
33
69
  * audio track. Returns `null` only when no audio track exists in the
@@ -53,7 +89,7 @@ internal object NowPlayingFormatExtractor {
53
89
  val mimeType = format.sampleMimeType
54
90
  val sampleRate = format.sampleRate.takeIf { it != Format.NO_VALUE }?.toDouble()
55
91
  val channelCount = format.channelCount.takeIf { it != Format.NO_VALUE }?.toDouble()
56
- val bitrate = format.bitrate.takeIf { it != Format.NO_VALUE }?.toDouble()
92
+ val reportedBitrate = format.bitrate.takeIf { it != Format.NO_VALUE }?.toDouble()
57
93
  val peakBitrate = format.peakBitrate.takeIf { it != Format.NO_VALUE }?.toDouble()
58
94
  val pcmEncoding = format.pcmEncoding.takeIf { it != Format.NO_VALUE }?.toDouble()
59
95
  val codecsString = format.codecs
@@ -61,6 +97,17 @@ internal object NowPlayingFormatExtractor {
61
97
  val durationSec = if (durationMs == C.TIME_UNSET || durationMs <= 0) null
62
98
  else durationMs / 1000.0
63
99
 
100
+ // Media3 leaves both bitrate fields unset for some codecs — FLAC reports
101
+ // NO_VALUE for average and peak alike, on local files as well as remote —
102
+ // so a consumer asking what it is playing gets nothing back. Where the
103
+ // source is on disk the average is arithmetic: encoded bytes over playing
104
+ // time. Not gated on codec; it fills in whatever the extractor did not
105
+ // report, and does nothing when Media3 already answered.
106
+ //
107
+ // A local stat only. A remote source would need its Content-Length, which
108
+ // costs a request the library deliberately does not make.
109
+ val bitrate = reportedBitrate ?: deriveLocalBitrate(player, durationSec)
110
+
64
111
  val rgData = engine?.currentReplayGainData()
65
112
  val rgActive = engine?.currentReplayGainActive() ?: false
66
113
 
@@ -9,8 +9,9 @@ import androidx.media3.common.PlaybackParameters
9
9
  * time-stretch resamples, so pitch shifts with speed.
10
10
  * - `voice` → pitch preserved (`pitch == 1`): the built-in (speech-tuned)
11
11
  * time-stretch holds pitch while changing speed.
12
- * - `music` → currently the same pitch-preserving path as `voice`; a
13
- * dedicated music-grade engine is a separate effort.
12
+ * - `music` → the same parameters as `voice` (`pitch == 1`); which
13
+ * stretcher applies them is `PitchAwareAudioProcessorChain`'s choice
14
+ * `SignalsmithStretchAudioProcessor` for `music`, Sonic otherwise.
14
15
  *
15
16
  * At `speed == 1.0` every mode yields `(1.0, 1.0)`, so the built-in
16
17
  * stretch is inactive — the auto-bypass at unity rate.