react-native-queue-player 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (355) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/consumer-rules.pro +1 -1
  4. package/android/src/main/cpp/airplay2_control.c +4 -4
  5. package/android/src/main/cpp/airplay2_control.h +4 -4
  6. package/android/src/main/cpp/airplay2_jni.cpp +3 -19
  7. package/android/src/main/cpp/airplay2_pair.c +7 -7
  8. package/android/src/main/cpp/airplay2_pair.h +2 -2
  9. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  10. package/android/src/main/cpp/airplay2_session.c +2 -2
  11. package/android/src/main/cpp/airplay_jni.cpp +8 -69
  12. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +839 -271
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  58. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  59. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  60. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  61. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  62. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  63. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
  64. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  65. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  109. package/app.plugin.js +17 -1
  110. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  111. package/ios/AVQueueBuilder.swift +120 -85
  112. package/ios/ArtworkLoader.swift +0 -7
  113. package/ios/ArtworkResolver.swift +22 -4
  114. package/ios/AssetReplayGainReader.swift +118 -0
  115. package/ios/AudioSession.swift +67 -16
  116. package/ios/AudioTapProvider.swift +284 -149
  117. package/ios/CarPlayBridge.swift +0 -12
  118. package/ios/CarPlayCoordinator.swift +18 -2
  119. package/ios/CarPlaySceneDelegate.swift +2 -2
  120. package/ios/Cast/AirPlayRouteState.swift +9 -8
  121. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  122. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  123. package/ios/Cast/Core/CastSession.swift +2 -2
  124. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  125. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  126. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  127. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  128. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  129. package/ios/CastManager.swift +1 -1
  130. package/ios/CrossfadeEngine.swift +660 -226
  131. package/ios/EQTap.swift +20 -17
  132. package/ios/Equalizer.swift +1 -1
  133. package/ios/FLACStreamInfo.swift +65 -0
  134. package/ios/GaplessEngine.swift +57 -30
  135. package/ios/InputGuards.swift +35 -5
  136. package/ios/LookaheadCache.swift +202 -58
  137. package/ios/LookaheadCachePrefetcher.swift +48 -36
  138. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  139. package/ios/NetworkRecoveryPolicy.swift +32 -0
  140. package/ios/NowPlayingFormatExtractor.swift +25 -18
  141. package/ios/NowPlayingInfo.swift +84 -48
  142. package/ios/OutputRouteMonitor.swift +16 -3
  143. package/ios/PendingBrowseRequests.swift +1 -1
  144. package/ios/PlaceholderArtwork.swift +9 -5
  145. package/ios/PlaybackEngine.swift +165 -76
  146. package/ios/PlaybackErrorMapping.swift +3 -3
  147. package/ios/PlaybackModeStateMachine.swift +2 -2
  148. package/ios/PlaybackNetworkMonitor.swift +73 -0
  149. package/ios/PlayerStateDerivation.swift +6 -6
  150. package/ios/QueueMutationArithmetic.swift +25 -0
  151. package/ios/QueueSkipArithmetic.swift +5 -3
  152. package/ios/QueueWindowArithmetic.swift +152 -0
  153. package/ios/ReadThroughServer.swift +283 -0
  154. package/ios/RemoteCommands.swift +44 -4
  155. package/ios/ReplayGainData.swift +22 -3
  156. package/ios/ReplayGainExtractor.swift +70 -27
  157. package/ios/Siri/VoiceDonation.swift +14 -9
  158. package/ios/StreamingBitrateProbe.swift +22 -68
  159. package/ios/SuppliedReplayGain.swift +83 -0
  160. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  161. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  162. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  163. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  164. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  165. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  166. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  167. package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
  168. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  169. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  170. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  171. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  172. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  173. package/ios/Tests/EngineSwapTests.swift +91 -0
  174. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  175. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  176. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  177. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  178. package/ios/Tests/InputGuardsTests.swift +41 -0
  179. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  180. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  181. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  182. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  183. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  184. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  185. package/ios/Tests/MutationDeferralTests.swift +220 -0
  186. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  187. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  188. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  189. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  190. package/ios/Tests/PlayerFixtures.swift +48 -0
  191. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  192. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  193. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  194. package/ios/Tests/QueueStateTests.swift +3 -1
  195. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  196. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  197. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  198. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  199. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  200. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  201. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  202. package/ios/Tests/RetryRecoveryTests.swift +303 -0
  203. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  204. package/ios/Tests/SkipIndexTests.swift +32 -2
  205. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  206. package/ios/Tests/StallRecoveryTests.swift +97 -0
  207. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  208. package/ios/Tests/TopUpWindowGateTests.swift +394 -0
  209. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  210. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  211. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  212. package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
  213. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  214. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  215. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  216. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  217. package/ios/TrackPlayer+Automotive.swift +101 -0
  218. package/ios/TrackPlayer+Cache.swift +232 -0
  219. package/ios/TrackPlayer+Config.swift +291 -0
  220. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  221. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  222. package/ios/TrackPlayer+EventsDispatch.swift +968 -0
  223. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  224. package/ios/TrackPlayer+Lifecycle.swift +930 -0
  225. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  226. package/ios/TrackPlayer+Queue.swift +940 -0
  227. package/ios/TrackPlayer+Recovery.swift +196 -0
  228. package/ios/TrackPlayer+Skip.swift +455 -0
  229. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  230. package/ios/TrackPlayer+State.swift +108 -0
  231. package/ios/TrackPlayer+Threading.swift +135 -0
  232. package/ios/TrackPlayer+Transport.swift +267 -0
  233. package/ios/TrackPlayer+Window.swift +195 -0
  234. package/ios/TrackPlayer.swift +305 -4619
  235. package/ios/Visualizer.swift +6 -5
  236. package/ios/tests-harness/Podfile +1 -1
  237. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  238. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  239. package/lib/module/hooks/useActiveTrack.js +29 -22
  240. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  241. package/lib/module/hooks/useCast.js +8 -26
  242. package/lib/module/hooks/useCast.js.map +1 -1
  243. package/lib/module/hooks/useEqualizer.js +19 -12
  244. package/lib/module/hooks/useEqualizer.js.map +1 -1
  245. package/lib/module/hooks/useLookaheadCache.js +3 -7
  246. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  247. package/lib/module/hooks/useQueue.js +66 -19
  248. package/lib/module/hooks/useQueue.js.map +1 -1
  249. package/lib/module/index.js +6 -3
  250. package/lib/module/index.js.map +1 -1
  251. package/lib/module/queueDelta.js +41 -0
  252. package/lib/module/queueDelta.js.map +1 -0
  253. package/lib/module/types.js +28 -5
  254. package/lib/module/types.js.map +1 -1
  255. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  256. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  257. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  258. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  259. package/lib/typescript/hooks/useCast.d.ts +6 -1
  260. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  261. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  262. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  263. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  264. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  265. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  266. package/lib/typescript/index.d.ts +2 -1
  267. package/lib/typescript/index.d.ts.map +1 -1
  268. package/lib/typescript/queueDelta.d.ts +10 -0
  269. package/lib/typescript/queueDelta.d.ts.map +1 -0
  270. package/lib/typescript/types.d.ts +168 -12
  271. package/lib/typescript/types.d.ts.map +1 -1
  272. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  273. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  274. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  275. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  276. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  277. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  278. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  279. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  280. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  281. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  282. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  283. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  284. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  285. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  313. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  314. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  315. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  316. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  317. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  318. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  319. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  320. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  321. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  322. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  323. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  324. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  325. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  326. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  327. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  328. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  329. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  330. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  331. package/package.json +8 -8
  332. package/src/TrackPlayer.nitro.ts +53 -27
  333. package/src/hooks/useActiveTrack.ts +29 -22
  334. package/src/hooks/useCast.ts +14 -15
  335. package/src/hooks/useEqualizer.ts +19 -12
  336. package/src/hooks/useLookaheadCache.ts +3 -7
  337. package/src/hooks/useQueue.ts +66 -17
  338. package/src/index.ts +13 -3
  339. package/src/queueDelta.ts +41 -0
  340. package/src/types.ts +169 -12
  341. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  342. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  343. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  344. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  345. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  346. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  347. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  348. package/ios/MetadataReader.swift +0 -555
  349. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  350. package/ios/Tests/MetadataReaderTests.swift +0 -603
  351. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  352. package/ios/Tests/VoiceDonationTests.swift +0 -26
  353. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  354. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  355. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
package/ios/EQTap.swift CHANGED
@@ -21,7 +21,7 @@ import MediaToolbox
21
21
  /// (e.g. ReplayGain pre-gain in front of the EQ cascade) may take
22
22
  /// short-held NSLock critical sections to read freshly-published
23
23
  /// coefficients; the project accepts that cost in exchange for
24
- /// cross-class consistency with the rest of the audio plumbing
24
+ /// cross-class consistency with the rest of the audio path
25
25
  /// (`AudioTapProvider.gainsSnapshot`, `Visualizer.subsLock`).
26
26
  ///
27
27
  /// AVFoundation delivers samples as deinterleaved float32: each
@@ -53,12 +53,17 @@ final class EQTap {
53
53
  ) -> Void
54
54
 
55
55
  /// Optional render-thread prepare hook. Fired once per tap from
56
- /// `MTAudioProcessingTapPrepareCallback` with the tap's
57
- /// `maxFrames` budget so DSP closures can size scratch buffers
58
- /// before the first process callback lands. Allocation here is
59
- /// safe because prepare runs synchronously on tap setup, ahead
60
- /// of the render-thread process loop.
61
- typealias PrepareFn = (_ maxFrames: Int) -> Void
56
+ /// `MTAudioProcessingTapPrepareCallback` with the tap's `maxFrames`
57
+ /// budget and the format the tap will actually deliver, so DSP closures
58
+ /// can size scratch buffers and configure themselves before the first
59
+ /// process callback lands. Allocation here is safe because prepare runs
60
+ /// synchronously on tap setup, ahead of the render-thread process loop.
61
+ ///
62
+ /// The format is the tap's own processing format, which is what the DSP
63
+ /// sees — not the encoded source format the asset's track advertises.
64
+ typealias PrepareFn = (
65
+ _ maxFrames: Int, _ format: AudioStreamBasicDescription
66
+ ) -> Void
62
67
 
63
68
  /// Default pass-through process body. Copies input → output.
64
69
  static let passThrough: ProcessFn = { input, output, numberFrames, channels, _, _ in
@@ -87,11 +92,9 @@ final class EQTap {
87
92
  let asset = item.asset
88
93
  if isHLS(asset) { return nil }
89
94
 
90
- var keyError: NSError?
91
- let status = asset.statusOfValue(forKey: "tracks", error: &keyError)
92
- guard status == .loaded else { return nil }
95
+ guard case .loaded(let tracks) = asset.status(of: .tracks) else { return nil }
93
96
 
94
- guard let track = asset.tracks(withMediaType: .audio).first else {
97
+ guard let track = tracks.first(where: { $0.mediaType == .audio }) else {
95
98
  return nil
96
99
  }
97
100
 
@@ -186,14 +189,14 @@ private let tapFinalize: MTAudioProcessingTapFinalizeCallback = { tap in
186
189
 
187
190
  private let tapPrepare: MTAudioProcessingTapPrepareCallback = {
188
191
  tap, maxFrames, processingFormat in
189
- // Forward the tap's per-render-batch frame budget to the holder's
190
- // prepare hook so DSP closures size scratch buffers here, not on
191
- // the process callback. The default pass-through closure has no
192
- // prepare hook installed, so this is a no-op for non-DSP taps.
193
- _ = processingFormat
192
+ // Forward the tap's per-render-batch frame budget and processing format
193
+ // to the holder's prepare hook so DSP closures size scratch buffers and
194
+ // configure their cascade here, not on the process callback. The default
195
+ // pass-through closure has no prepare hook installed, so this is a no-op
196
+ // for non-DSP taps.
194
197
  let storage = MTAudioProcessingTapGetStorage(tap)
195
198
  let holder = Unmanaged<ProcessHolder>.fromOpaque(storage).takeUnretainedValue()
196
- holder.prepare?(maxFrames)
199
+ holder.prepare?(maxFrames, processingFormat.pointee)
197
200
  }
198
201
 
199
202
  private let tapUnprepare: MTAudioProcessingTapUnprepareCallback = { tap in
@@ -23,6 +23,7 @@ import NitroModules
23
23
  /// because the JS side already knows it just dropped the singleton.
24
24
  final class Equalizer: HybridEqualizerSpec {
25
25
 
26
+ private static let currentLock = NSLock()
26
27
  /// The most recently constructed Equalizer instance. The iOS
27
28
  /// playback engine reaches for this at attach time so it can
28
29
  /// wire the shared `AudioTapProvider` to the active player without
@@ -37,7 +38,6 @@ final class Equalizer: HybridEqualizerSpec {
37
38
  /// Hybrid when the JS-side `getEqualizer()` singleton drops),
38
39
  /// so the strong static doesn't cause a leak — JS reload zeroes
39
40
  /// it via the second-construction teardown path below.
40
- private static let currentLock = NSLock()
41
41
  private static var _current: Equalizer?
42
42
  static var current: Equalizer? {
43
43
  get {
@@ -0,0 +1,65 @@
1
+ import Foundation
2
+
3
+ /// FLAC STREAMINFO header values relevant to bitrate computation.
4
+ /// Reading these + the file's byte size gives an accurate average
5
+ /// encoded bitrate without involving AVFoundation.
6
+ ///
7
+ /// The only source of a **local** FLAC bitrate:
8
+ /// `estimatedDataRate` reports 0 for FLAC and Core Audio's
9
+ /// `AudioFileStream` probe does not model FLAC's variable-frame
10
+ /// container, so `NowPlayingFormatExtractor.readLocalFLACBitrate`
11
+ /// derives duration from STREAMINFO and divides the file size by it.
12
+ struct FLACStreamInfo {
13
+ let sampleRate: Int // Hz, 1..655350
14
+ let channelCount: Int // 1..8
15
+ let bitsPerSample: Int // 4..32
16
+ let totalSamples: Int64 // 0 = unknown
17
+
18
+ /// Parse a FLAC STREAMINFO block from a `fLaC`-prefixed buffer.
19
+ /// STREAMINFO is type 0 and is required by the FLAC spec to be the
20
+ /// FIRST metadata block. Returns nil when the buffer doesn't start
21
+ /// with the FLAC magic or the block layout is malformed.
22
+ ///
23
+ /// STREAMINFO body layout (34 bytes after the 4-byte block header):
24
+ /// - 16 bits: minBlockSize (samples)
25
+ /// - 16 bits: maxBlockSize
26
+ /// - 24 bits: minFrameSize (bytes)
27
+ /// - 24 bits: maxFrameSize
28
+ /// - 20 bits: sampleRate
29
+ /// - 3 bits: channels - 1
30
+ /// - 5 bits: bitsPerSample - 1
31
+ /// - 36 bits: totalSamples
32
+ /// - 128 bits: MD5
33
+ static func parse(_ data: Data) -> FLACStreamInfo? {
34
+ guard data.count >= 4 + 4 + 34 else { return nil }
35
+ guard data[0] == 0x66, data[1] == 0x4c,
36
+ data[2] == 0x61, data[3] == 0x43 else { return nil }
37
+ let blockType = data[4] & 0x7f
38
+ guard blockType == 0 else { return nil }
39
+ let blockSize = Int(data[5]) << 16 | Int(data[6]) << 8 | Int(data[7])
40
+ guard blockSize >= 34, data.count >= 8 + blockSize else { return nil }
41
+ // STREAMINFO body starts at offset 8.
42
+ // Bits 64..83 (offset 10, 11, 12 in the body — i.e. data offsets
43
+ // 18, 19, 20): sampleRate (20 bits, big-endian).
44
+ let b18 = UInt64(data[18])
45
+ let b19 = UInt64(data[19])
46
+ let b20 = UInt64(data[20])
47
+ let sampleRate = Int((b18 << 12) | (b19 << 4) | (b20 >> 4))
48
+ let channels = Int((data[20] >> 1) & 0x07) + 1
49
+ let bitsPerSample = Int(((data[20] & 0x01) << 4) | (data[21] >> 4)) + 1
50
+ // totalSamples — 36 bits starting at byte 21 bit 4.
51
+ let t21 = Int64(data[21] & 0x0f)
52
+ let t22 = Int64(data[22])
53
+ let t23 = Int64(data[23])
54
+ let t24 = Int64(data[24])
55
+ let t25 = Int64(data[25])
56
+ let totalSamples =
57
+ (t21 << 32) | (t22 << 24) | (t23 << 16) | (t24 << 8) | t25
58
+ return FLACStreamInfo(
59
+ sampleRate: sampleRate,
60
+ channelCount: channels,
61
+ bitsPerSample: bitsPerSample,
62
+ totalSamples: totalSamples
63
+ )
64
+ }
65
+ }
@@ -20,9 +20,14 @@ import Foundation
20
20
  /// callbacks; on the iOS side the equivalent fan-out continues to
21
21
  /// run through `TrackPlayer`'s direct observer code.
22
22
  ///
23
- /// Threading: methods run on the main thread by convention.
24
- /// `AVQueuePlayer` itself is documented as main-thread-only for
25
- /// every mutator + most state queries.
23
+ /// Threading: `TrackPlayer` calls every method on its `playerQueue`,
24
+ /// the one serial queue that owns the player. This engine never fires
25
+ /// its `delegate`; its events reach `TrackPlayer` through the KVO and
26
+ /// NotificationCenter observers `TrackPlayer` installs on `player`
27
+ /// (`installGaplessObservers` / `installEventObservers`), each of which
28
+ /// hops onto `playerQueue` before touching state. Main-only frameworks
29
+ /// (`MPNowPlayingInfoCenter`, CarPlay) are reached by `TrackPlayer`
30
+ /// hopping to main with the payload already computed.
26
31
  final class GaplessEngine: PlaybackEngine {
27
32
 
28
33
  /// Underlying AVQueuePlayer. Internal to the lib so service-level
@@ -36,6 +41,13 @@ final class GaplessEngine: PlaybackEngine {
36
41
 
37
42
  private var mixProvider: AudioMixProvider?
38
43
  private var released = false
44
+ /// Whether the transport wants playback, independent of what the player is
45
+ /// doing. `AVQueuePlayer` does not drop its rate when it runs out of items,
46
+ /// so a drained player is indistinguishable from a playing one by rate
47
+ /// alone — and a rebuild that reads the rate would start the next queue
48
+ /// installed on it. Set by `play()`, cleared by `pause()`, `stop()`,
49
+ /// `release()` and every path that drains the queue.
50
+ private var wantsPlayback = false
39
51
 
40
52
  init() {
41
53
  let p = AVQueuePlayer()
@@ -46,29 +58,15 @@ final class GaplessEngine: PlaybackEngine {
46
58
 
47
59
  // MARK: - Lifecycle
48
60
 
49
- func prepare(
50
- items: [AVPlayerItem],
51
- startIndex: Int,
52
- startPositionSeconds: TimeInterval
53
- ) async {
54
- // Queue construction lives on `TrackPlayer.fullRebuildPlayerQueue`
55
- // (see queue-mutation comment on `PlaybackEngine.swift`). The
56
- // engine's `prepare` is reserved for when a future engine impl
57
- // (e.g. a `CrossfadeEngine` standby preroll) needs an async
58
- // hook; for the gapless engine the queue arrives via direct
59
- // `engine.player.insert(...)` calls and `prepare` is a no-op.
60
- _ = items
61
- _ = startIndex
62
- _ = startPositionSeconds
63
- }
64
-
65
61
  func release() {
66
62
  if released { return }
67
63
  released = true
64
+ wantsPlayback = false
68
65
  // Releasing the queue empties the AVQueuePlayer's items; the
69
66
  // player itself is held by the strong `let player` until the
70
67
  // engine is deallocated, which TrackPlayer triggers when it
71
68
  // drops its engine reference.
69
+ AVQueueBuilder.cancelLoading(dropping: player.items())
72
70
  player.removeAllItems()
73
71
  mixProvider = nil
74
72
  // Detach the delegate so any in-flight event still on the run
@@ -95,11 +93,19 @@ final class GaplessEngine: PlaybackEngine {
95
93
  startIndex: Int,
96
94
  startPositionSeconds: TimeInterval
97
95
  ) {
98
- let savedRate = player.rate
96
+ let shouldResume = wantsPlayback
97
+ // The outgoing items are discarded here; anything the incoming set reuses
98
+ // keeps its loads, since a cancelled asset never becomes playable.
99
+ AVQueueBuilder.cancelLoading(dropping: player.items(), keeping: items)
99
100
  player.removeAllItems()
100
101
  guard !items.isEmpty,
101
102
  startIndex >= 0,
102
103
  startIndex < items.count else {
104
+ // Nothing seated, so nothing is being played. Removing the items does
105
+ // not stop the player, and a leg left running would start whatever is
106
+ // installed next on contact.
107
+ wantsPlayback = false
108
+ player.pause()
103
109
  // Empty queue, out-of-range startIndex, or startIndex past
104
110
  // end: leave drained. Matches `TrackPlayer.fullRebuildPlayerQueue`
105
111
  // where bad index also drains via the same removeAllItems-then-guard
@@ -129,8 +135,8 @@ final class GaplessEngine: PlaybackEngine {
129
135
  }
130
136
  }
131
137
  let restoreRate: () -> Void = { [weak self] in
132
- guard let player = self?.player, player.rate != savedRate else { return }
133
- player.rate = savedRate
138
+ guard let self, shouldResume, self.player.rate != self.userSpeed else { return }
139
+ self.player.rate = self.userSpeed
134
140
  }
135
141
  if startPositionSeconds > 0, let head = player.currentItem {
136
142
  head.seek(
@@ -177,6 +183,12 @@ final class GaplessEngine: PlaybackEngine {
177
183
 
178
184
  /// Drain the queue. After this call `currentMediaItem == nil`.
179
185
  func removeAllItems() {
186
+ // Emptying the queue is not the same as stopping: `AVQueuePlayer` keeps
187
+ // its rate across `removeAllItems`, so without this the next item seated
188
+ // on the player would start on contact.
189
+ wantsPlayback = false
190
+ player.pause()
191
+ AVQueueBuilder.cancelLoading(dropping: player.items())
180
192
  player.removeAllItems()
181
193
  }
182
194
 
@@ -200,10 +212,18 @@ final class GaplessEngine: PlaybackEngine {
200
212
  /// (the intended rate), not `player.rate` (0 while paused).
201
213
  private var pitchCorrectionMode: PitchCorrectionMode = .voice
202
214
 
203
- func play() { player.rate = userSpeed }
204
- func pause() { player.pause() }
215
+ func play() {
216
+ wantsPlayback = true
217
+ player.rate = userSpeed
218
+ }
219
+
220
+ func pause() {
221
+ wantsPlayback = false
222
+ player.pause()
223
+ }
205
224
 
206
225
  func stop() {
226
+ wantsPlayback = false
207
227
  player.pause()
208
228
  if let item = player.currentItem {
209
229
  item.seek(to: .zero, completionHandler: nil)
@@ -353,6 +373,14 @@ final class GaplessEngine: PlaybackEngine {
353
373
  player.currentItem
354
374
  }
355
375
 
376
+ /// This engine has one leg and no fade to schedule.
377
+ var isFadePendingOrActive: Bool { false }
378
+
379
+ /// One leg, so whatever is current is the whole of what is attached.
380
+ var attachedQueueItemIds: [String] {
381
+ player.currentItem?.queueItemId.map { [$0] } ?? []
382
+ }
383
+
356
384
  var allMediaItems: [AVPlayerItem] {
357
385
  player.items()
358
386
  }
@@ -381,11 +409,10 @@ final class GaplessEngine: PlaybackEngine {
381
409
  if item.audioMix != nil { continue }
382
410
  item.audioMix = provider?.audioMix(for: item)
383
411
  }
384
- }
385
-
386
- // MARK: - Handoff
387
-
388
- func handoff(to nextEngine: PlaybackEngine, atTrackBoundary: Bool) async {
389
- preconditionFailure("PlaybackEngine.handoff is not implemented for the single-engine configuration")
412
+ // `audioMix(for:)` announces each item it is given as the active asset,
413
+ // so a walk leaves the last item refreshed standing as active. Re-assert
414
+ // the one that is playing, or consumers read the wrong asset — the
415
+ // visualizer's HLS check is answered from it.
416
+ provider?.noteActiveAsset(player.currentItem?.asset)
390
417
  }
391
418
  }
@@ -27,10 +27,7 @@ import Foundation
27
27
  /// project-empirical working pattern; both shapes are valid SPI
28
28
  /// per the Swift language spec, but the per-method form is what
29
29
  /// the rest of the codebase uses (see `AVQueueBuilder`,
30
- /// `QueueMutationArithmetic`). The `TransportStateTranslator`
31
- /// deferral note in `plan/backlog.md` documents a separate
32
- /// SPI-related compiler oddity that does NOT manifest with this
33
- /// shape.
30
+ /// `QueueMutationArithmetic`).
34
31
  public enum InputGuards {
35
32
 
36
33
  /// Validate a `Double` that JS sends as a queue index. Returns
@@ -49,6 +46,18 @@ public enum InputGuards {
49
46
  return Int(value)
50
47
  }
51
48
 
49
+ /// Convert a `Double` insert position to the `Int` the queue arithmetic
50
+ /// clamps. `nil` for non-finite values (dropped: append). A finite value
51
+ /// past what `Int` holds is clamped to `count`, since every position past
52
+ /// the end means "append" anyway; `Int(value)` on such a value traps.
53
+ @_spi(QueuePlayerTests)
54
+ public static func insertPosition(_ value: Double?, count: Int) -> Int? {
55
+ guard let value, value.isFinite else { return nil }
56
+ if value < 0 { return 0 }
57
+ if value >= Double(count) { return count }
58
+ return Int(value)
59
+ }
60
+
52
61
  /// Validate a `Double` seek position (seconds). Returns `nil` for
53
62
  /// non-finite values; otherwise returns the value unchanged. The
54
63
  /// caller is responsible for clamping negative values to 0 and
@@ -58,6 +67,27 @@ public enum InputGuards {
58
67
  @_spi(QueuePlayerTests)
59
68
  public static func sanitisedSeekPosition(_ value: Double) -> Double? {
60
69
  guard value.isFinite else { return nil }
61
- return value
70
+ // Finiteness alone is not enough: a finite 1e300 traps the same way
71
+ // `.infinity` does once it is scaled to milliseconds and converted to
72
+ // `Int64`. The bound is far past any real media duration, so clamping to
73
+ // it never changes a seek a consumer could observe.
74
+ return max(-maxSeekSeconds, min(maxSeekSeconds, value))
75
+ }
76
+
77
+ private static let maxSeekSeconds: Double = 1e12
78
+
79
+ /// Validate a `Double` lookahead count. Returns a non-negative `Int`,
80
+ /// clamped in `Double` space so an enormous or infinite consumer value
81
+ /// cannot trap the conversion. The prefetcher bounds its own window by the
82
+ /// queue length, so the upper clamp loses nothing observable.
83
+ ///
84
+ /// `.infinity` clamps to the bound rather than falling back to zero — it
85
+ /// asks for as much lookahead as possible, and answering "none" would invert
86
+ /// the request. Only `NaN`, which asks for nothing intelligible, reads as
87
+ /// zero; comparison against it is always false, so it cannot be clamped.
88
+ @_spi(QueuePlayerTests)
89
+ public static func validLookaheadCount(_ value: Double) -> Int {
90
+ guard !value.isNaN else { return 0 }
91
+ return Int(max(0, min(value, 10_000)))
62
92
  }
63
93
  }