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
@@ -15,41 +15,40 @@ import Foundation
15
15
  /// track-boundary scheduling. The lib's `TrackPlayer` holds the canonical
16
16
  /// queue model + listener registries + system-side surfaces (NowPlaying,
17
17
  /// RemoteCommands, AudioSession, ArtworkResolver) and routes transport /
18
- /// queue calls through whichever engine is active. Engine swap (gapless ↔
19
- /// crossfade) is a `handoff` at the next track boundary.
18
+ /// queue calls through whichever engine is active. An engine swap (gapless ↔
19
+ /// crossfade) is `TrackPlayer.applyEngineSwapForMode`: it builds the new
20
+ /// engine, seeds it through `setItems`, then releases the old one.
20
21
  ///
21
- /// Threading: methods are called on the main actor by convention. State
22
- /// queries (positions, indices, isPlaying) are read off the underlying
23
- /// player and so reflect the same single-threaded view AVFoundation
24
- /// provides on the main thread. `async` methods are launched on the same
25
- /// actor; impl is responsible for any internal off-actor work (e.g. the
26
- /// crossfade engine's standby preroll).
22
+ /// Threading: `TrackPlayer` calls every method on its `playerQueue`, a
23
+ /// private serial queue, and every delegate fire lands on that queue.
24
+ /// The crossfade engine is constructed confined to it and hops each
25
+ /// AVFoundation observer, notification and timer callback onto it before
26
+ /// firing (a few fires are also synchronous inside a `setItems` / `seek`
27
+ /// / `remove` call). The gapless engine does not use the delegate at all:
28
+ /// `TrackPlayer` observes its player directly and does its own hop. State
29
+ /// queries (positions, indices, isPlaying) read the underlying player from
30
+ /// that queue. An engine is responsible for any internal off-queue work
31
+ /// (the crossfade engine's standby preroll) and for returning to the queue
32
+ /// before touching shared state.
27
33
  protocol PlaybackEngine: AnyObject {
28
34
 
29
- /// Build the underlying player(s), apply [items] starting at
30
- /// [startIndex] / [startPositionSeconds], and wait for the
31
- /// underlying player(s) to reach a playable state.
32
- ///
33
- /// Empty [items] is allowed; the engine prepares an empty queue
34
- /// and `currentMediaItemIndex` returns `-1` until items arrive
35
- /// via `setItems` / `insertItems`.
36
- func prepare(
37
- items: [AVPlayerItem],
38
- startIndex: Int,
39
- startPositionSeconds: TimeInterval
40
- ) async
41
-
42
35
  /// Release the underlying player(s) and stop all background work.
43
36
  /// After `release`, no further methods are valid.
44
37
  func release()
45
38
 
46
39
  // MARK: - Queue mutation
47
40
 
48
- /// Drain the queue and re-install the slice of [items] from
49
- /// [startIndex] onward. Items before [startIndex] are not held
50
- /// `AVQueuePlayer` is forward-only and the canonical queue model
51
- /// lives on `TrackPlayer`. After the call,
52
- /// `allMediaItems.count == max(0, items.count - startIndex)`.
41
+ /// Drain the queue and install [items], beginning playback at
42
+ /// [startIndex]. Callers pass a run that may start behind the playhead, so
43
+ /// [startIndex] names where in that run to begin rather than where to cut
44
+ /// it: an engine holds every item it is given unless it cannot.
45
+ ///
46
+ /// `GaplessEngine` cannot: `AVQueuePlayer` is forward-only and consumes
47
+ /// finished items, so it drops everything before [startIndex] and
48
+ /// `allMediaItems.count == max(0, items.count - startIndex)` there.
49
+ /// `CrossfadeEngine` holds the whole run, which is what lets it keep a
50
+ /// trail behind the playhead for an immediate backward skip.
51
+ ///
53
52
  /// Active index resolves to `items[startIndex]` at
54
53
  /// [startPositionSeconds].
55
54
  ///
@@ -67,8 +66,8 @@ protocol PlaybackEngine: AnyObject {
67
66
  ///
68
67
  /// Caller (`TrackPlayer`) constructs `[AVPlayerItem]` via
69
68
  /// `AVQueueBuilder` so consumer config (HTTP headers, user-agent,
70
- /// lookahead-cache file:// rewrites) is applied before items
71
- /// reach the engine.
69
+ /// read-through server routing) is applied before items reach
70
+ /// the engine.
72
71
  func setItems(
73
72
  _ items: [AVPlayerItem],
74
73
  startIndex: Int,
@@ -96,13 +95,16 @@ protocol PlaybackEngine: AnyObject {
96
95
  /// Remove [item] from the engine's queue. Removing the current
97
96
  /// item on `GaplessEngine` triggers `AVQueuePlayer` auto-advance
98
97
  /// to the next queued item; on `CrossfadeEngine` removing the
99
- /// leading leg's item triggers boundary handoff + standby
100
- /// promotion. No-op when [item] is not present.
98
+ /// current item cancels any pending fade and replaces the leading
99
+ /// leg's item with the one now at that index (the previous item when
100
+ /// the removed one was last), or drains when the queue is empty.
101
+ /// No-op when [item] is not present.
101
102
  func remove(_ item: AVPlayerItem)
102
103
 
103
104
  /// Remove every item from the queue. After this call,
104
105
  /// `currentMediaItem == nil` and `allMediaItems` is empty. Used
105
- /// by `TrackPlayer.fullRebuildPlayerQueue` before re-inserting
106
+ /// by `TrackPlayer.fullRebuildPlayerQueue` when it installs a whole slice,
107
+ /// before re-inserting
106
108
  /// the post-mutation queue snapshot.
107
109
  func removeAllItems()
108
110
 
@@ -118,6 +120,12 @@ protocol PlaybackEngine: AnyObject {
118
120
  func play()
119
121
  func pause()
120
122
  func stop()
123
+ /// Seek to [index] within the engine's own queue, at [position].
124
+ ///
125
+ /// [index] is an index into the slice this engine was handed, **not** a
126
+ /// queue position — the two differ whenever the caller installed anything
127
+ /// other than the whole track list. Callers translate by `queueItemId`
128
+ /// identity before calling.
121
129
  func seek(toIndex index: Int, position: TimeInterval)
122
130
  func skipToNext()
123
131
  func skipToPrevious()
@@ -141,10 +149,14 @@ protocol PlaybackEngine: AnyObject {
141
149
 
142
150
  // MARK: - State
143
151
 
144
- /// Current item index within the engine's queue. Returns `-1`
152
+ /// Current item index within the engine's own queue. Returns `-1`
145
153
  /// when the queue is empty (matches the canonical sentinel in
146
- /// `TrackPlayer`). When non-empty, mirrors the AVQueuePlayer's
147
- /// resolved index.
154
+ /// `TrackPlayer`).
155
+ ///
156
+ /// This is **not** a queue position — it counts within the slice the
157
+ /// engine was handed. Resolve the active track by item identity
158
+ /// (`currentMediaItem.queueItemId`) rather than reading this as an index
159
+ /// into the consumer's track list.
148
160
  var currentMediaItemIndex: Int { get }
149
161
  var currentPositionSeconds: TimeInterval { get }
150
162
  var currentDurationSeconds: TimeInterval { get }
@@ -163,6 +175,40 @@ protocol PlaybackEngine: AnyObject {
163
175
  /// duplicate it for hot-path reads.
164
176
  var allMediaItems: [AVPlayerItem] { get }
165
177
 
178
+ /// Identity of the item actually being heard.
179
+ ///
180
+ /// `currentMediaItem` reports the incoming leg the moment a crossfade is in
181
+ /// flight, which is right for "what is the queue moving to" and wrong for
182
+ /// "where is the run anchored". Callers that reason about the installed run
183
+ /// — where it starts, what may be trimmed behind it — read this instead, and
184
+ /// it stays on the outgoing leg until the fade completes.
185
+ var leadingQueueItemId: String? { get }
186
+
187
+ /// Identities of the items attached to the engine's players right now.
188
+ ///
189
+ /// Not the whole installed run — only what is loaded and about to be, or
190
+ /// already, heard. A queue mutation that touches one of these has to be
191
+ /// applied to the engine at once; one that does not can wait for a fade to
192
+ /// finish rather than cancelling it.
193
+ ///
194
+ /// Mid-fade a crossfading engine reports both legs, which is the only
195
+ /// truthful answer while two items are audible.
196
+ ///
197
+ /// Required rather than defaulted, for the same reason as
198
+ /// `isFadePendingOrActive` and more so: an engine that under-reports has a
199
+ /// mutation to an audible item deferred, and the fade then runs into a track
200
+ /// the queue no longer has.
201
+ var attachedQueueItemIds: [String] { get }
202
+
203
+ /// Whether a crossfade is scheduled or running.
204
+ ///
205
+ /// Every branch that changes the installed items cancels an in-flight fade,
206
+ /// so work that can wait — extending the run, trimming behind it — asks
207
+ /// first rather than cutting the fade short. Required rather than defaulted:
208
+ /// an engine that answers wrongly has its fades cut short silently, so each
209
+ /// one states its own answer.
210
+ var isFadePendingOrActive: Bool { get }
211
+
166
212
  // MARK: - Effect chain
167
213
 
168
214
  /// Set the single active `AudioMixProvider`. The engine consults
@@ -182,33 +228,15 @@ protocol PlaybackEngine: AnyObject {
182
228
  /// No-op when no provider is set.
183
229
  func refreshAllItemMixes()
184
230
 
185
- // MARK: - Handoff
186
-
187
- /// Hand playback off to [nextEngine] when the user changes
188
- /// `PlaybackMode`.
189
- ///
190
- /// Contract:
191
- /// - The receiver inherits the donor's queue, current index, and
192
- /// current position; the caller does NOT pre-call `prepare` on
193
- /// [nextEngine].
194
- /// - When [atTrackBoundary] is `true`, the donor plays the
195
- /// active track to its natural end and the receiver starts
196
- /// playing the NEXT queue position fresh.
197
- /// - When [atTrackBoundary] is `false` (idle player or empty
198
- /// queue), the swap is immediate; receiver picks up at the
199
- /// inherited position.
200
- /// - The donor's `release` is the caller's responsibility AFTER
201
- /// the boundary fires; the engine itself does not self-release.
202
- func handoff(to nextEngine: PlaybackEngine, atTrackBoundary: Bool) async
203
-
204
231
  // MARK: - Delegate
205
232
 
206
- /// Single delegate receiving state / track / progress / error /
207
- /// queue-end events. `TrackPlayer` is the canonical delegate;
208
- /// engines fire on the main actor.
233
+ /// Single delegate receiving state / track / error /
234
+ /// queue-end events. `TrackPlayer` is the canonical delegate; the
235
+ /// crossfade engine fires it on `playerQueue`, the gapless engine
236
+ /// never fires it (`TrackPlayer` leaves its delegate nil).
209
237
  ///
210
- /// Threading: setter MUST be invoked on the main actor to avoid
211
- /// a write-vs-fire race during `handoff`.
238
+ /// Threading: set on `playerQueue`, the queue the fires run on, so a
239
+ /// write cannot race a fire during an engine swap.
212
240
  var delegate: PlaybackEngineDelegate? { get set }
213
241
  }
214
242
 
@@ -240,11 +268,6 @@ protocol PlaybackEngineDelegate: AnyObject {
240
268
  reason: StateChangeReason?
241
269
  )
242
270
 
243
- /// Fires when the underlying player's `timeControlStatus`
244
- /// transitions to / from `.playing`. Drives diagnostics +
245
- /// progress-tick wake-up.
246
- func engineIsPlayingChanged(_ engine: PlaybackEngine, isPlaying: Bool)
247
-
248
271
  /// Fires on `AVPlayer.currentItem` KVO when the active item
249
272
  /// changes (auto-advance, user skip, queue replace). The
250
273
  /// delegate translates against its own state machine (e.g.
@@ -257,10 +280,16 @@ protocol PlaybackEngineDelegate: AnyObject {
257
280
  /// counterpart fires on `Player.Listener.onTracksChanged`.
258
281
  func engineActiveItemFormatChanged(_ engine: PlaybackEngine)
259
282
 
260
- /// Fires on a player item failure (`AVPlayerItem.failedToPlayToEndTime`
261
- /// or `AVPlayerItem.status == .failed`). The delegate maps the
262
- /// underlying `Error` via the lib's `PlaybackErrorMapping`.
263
- func engineDidFailWithError(_ engine: PlaybackEngine, error: Error)
283
+ /// A playback failure, with the item it happened on when the engine can name
284
+ /// it.
285
+ ///
286
+ /// The item is what makes the automatic retry reachable: the retry is keyed
287
+ /// by `queueItemId`, so a failure reported without one is surfaced to the
288
+ /// consumer and never retried. Engines that know which item failed must say
289
+ /// so — both of `CrossfadeEngine`'s failure paths do, since each is already
290
+ /// filtered to the leading leg's current item.
291
+ func engineDidFailWithError(
292
+ _ engine: PlaybackEngine, error: Error, item: AVPlayerItem?)
264
293
 
265
294
  /// Fires on every `AVPlayerItemDidPlayToEndTime` — i.e. each
266
295
  /// item's natural end, including non-terminal items in a queue.
@@ -286,10 +315,12 @@ protocol PlaybackEngineDelegate: AnyObject {
286
315
  /// subsequent `engineDidTransitionTrack` for the same incoming
287
316
  /// index is a structural KVO echo and must be deduplicated.
288
317
  ///
289
- /// Crossfade-only — `GaplessEngine` never invokes this.
290
- /// `incomingIndex` is the global queue index of the fading-in
291
- /// track.
292
- func engineCrossfadeDidBegin(_ engine: PlaybackEngine, incomingIndex: Int)
318
+ /// Crossfade-only — `GaplessEngine` never invokes this. The
319
+ /// incoming track is read from `engine.currentMediaItem`, which
320
+ /// already resolves to the standby leg once the fade is in flight;
321
+ /// the delegate maps that item to a queue position by identity
322
+ /// rather than trusting an engine-relative index.
323
+ func engineCrossfadeDidBegin(_ engine: PlaybackEngine)
293
324
 
294
325
  /// Fires when an in-flight crossfade is cancelled before
295
326
  /// `swapRoles` runs (pause / stop / audio-focus loss / queue
@@ -300,20 +331,78 @@ protocol PlaybackEngineDelegate: AnyObject {
300
331
  /// `engineCrossfadeDidBegin`: only fires when a `DidBegin` was
301
332
  /// fired earlier and the fade did not complete.
302
333
  ///
303
- /// Crossfade-only. `revertedIndex` is the leading-leg index that
304
- /// is now the active track.
305
- func engineCrossfadeDidCancel(_ engine: PlaybackEngine, revertedIndex: Int)
334
+ /// Crossfade-only. The reverted track is read from
335
+ /// `engine.currentMediaItem`, which resolves back to the leading leg
336
+ /// once `fadeInFlight` clears.
337
+ func engineCrossfadeDidCancel(_ engine: PlaybackEngine)
338
+
339
+ /// A fade is no longer scheduled or running, however it ended.
340
+ ///
341
+ /// Distinct from `engineCrossfadeDidCancel`, which reports that an
342
+ /// *in-flight* fade was reverted and carries a track change with it. This
343
+ /// fires for every end of every fade — completed, cancelled, abandoned
344
+ /// during the arm window, timed out — because work that waits on a fade has
345
+ /// to be released by all of them, not only the ones a listener cares about.
346
+ func engineFadeDidSettle(_ engine: PlaybackEngine)
347
+
348
+ /// The active leg's buffer flags changed (`isPlaybackBufferEmpty` /
349
+ /// `isPlaybackLikelyToKeepUp` / `isPlaybackBufferFull`). Prompts a
350
+ /// buffer-state recompute so the discrete `onBufferStateChange` surface
351
+ /// tracks the crossfade engine's leg the same way it tracks the gapless
352
+ /// `AVQueuePlayer`'s item KVO. Buffer state only — no direct player-state
353
+ /// emission.
354
+ func engineBufferStateMaybeChanged(_ engine: PlaybackEngine)
355
+ }
356
+
357
+ extension PlaybackEngineDelegate {
358
+ /// Nothing to release by default.
359
+ func engineFadeDidSettle(_ engine: PlaybackEngine) {}
360
+ /// The gapless engine drives buffer recompute through its own item KVO, not
361
+ /// this delegate; conformers that do not surface buffer state ignore it.
362
+ func engineBufferStateMaybeChanged(_ engine: PlaybackEngine) {}
363
+ }
364
+
365
+ extension PlaybackEngine {
366
+ /// An engine with one leg is always leading with the item it reports as
367
+ /// current; only a crossfading engine has two ends that can disagree.
368
+ var leadingQueueItemId: String? { currentMediaItem?.queueItemId }
369
+
306
370
  }
307
371
 
308
372
  /// Single-slot audio-mix provider. The equalizer + visualizer share
309
373
  /// a single provider that builds an `MTAudioProcessingTap`-backed
310
374
  /// `AVAudioMix` per `AVPlayerItem` the engine constructs.
311
375
  ///
312
- /// `audioMix(for:)` is called on the main thread; implementations
313
- /// may allocate freshly or cache + reuse. Returning `nil` skips
376
+ /// `audioMix(for:)` is called on the mix dispatch target — `TrackPlayer`
377
+ /// points it at `playerQueue`; implementations may allocate freshly or
378
+ /// cache + reuse. Returning `nil` skips
314
379
  /// attachment for that item — used to bypass mix on items that
315
380
  /// don't support a tap (HLS sources).
316
381
  protocol AudioMixProvider: AnyObject {
317
382
  /// Build the audio mix for [item], or return `nil` to skip.
318
383
  func audioMix(for item: AVPlayerItem) -> AVAudioMix?
384
+
385
+ /// Declare which asset is active, without building a mix. `audioMix(for:)`
386
+ /// announces whatever item it is handed, so a caller that refreshes several
387
+ /// items uses this to name the one actually playing.
388
+ func noteActiveAsset(_ asset: AVAsset?)
389
+
390
+ /// Whether any registered consumer wants the tap installed right now (the
391
+ /// equaliser is enabled, or a visualizer has subscribers). The crossfade
392
+ /// engine holds a freshly-seated leading leg at rate 0 until its
393
+ /// `.readyToPlay` mix install lands only when this is true — a mix set once
394
+ /// the leg is already rendering is dropped, silencing the tap — so the
395
+ /// common no-tap path is left to start immediately.
396
+ func anyConsumerWantsTap() -> Bool
397
+ }
398
+
399
+ extension AudioMixProvider {
400
+ /// A provider with no consumer model (a test double) never holds a leg for a
401
+ /// tap; only `AudioTapProvider` overrides this.
402
+ func anyConsumerWantsTap() -> Bool { false }
403
+ }
404
+
405
+ extension AudioMixProvider {
406
+ /// Providers that track no active asset need no behaviour here.
407
+ func noteActiveAsset(_ asset: AVAsset?) {}
319
408
  }
@@ -53,9 +53,9 @@ public enum PlaybackErrorMapping {
53
53
  }
54
54
 
55
55
  /// Strip query strings from a native error message so logged
56
- /// `nativeMessage` doesn't carry auth tokens (Subsonic-style
57
- /// `s=…&t=…` salt+token pairs in stream URLs end up inside
58
- /// `localizedDescription` strings).
56
+ /// `nativeMessage` doesn't carry auth tokens: stream URLs with
57
+ /// `?token=…`-style query strings end up inside
58
+ /// `localizedDescription` strings, and every `?…` run is removed.
59
59
  ///
60
60
  /// Walks the entire message, removing every `?…(whitespace|end)`
61
61
  /// run so a multi-URL error message ("redirect from
@@ -66,8 +66,8 @@ final class PlaybackModeStateMachine {
66
66
  /// the Nitro Promise<Void> bridge as a JS-thrown error.
67
67
  ///
68
68
  /// The caller is responsible for managing the brief audio
69
- /// interruption (resume position carryover, queue re-attach,
70
- /// etc.) that's the engine-handoff contract.
69
+ /// interruption `applyEngineSwapForMode` carries the position
70
+ /// and re-installs the queue.
71
71
  func setRequested(
72
72
  state: State,
73
73
  requested: PlaybackMode
@@ -0,0 +1,73 @@
1
+ import Foundation
2
+ import Network
3
+
4
+ /// Reports the moment the network comes back after an outage.
5
+ ///
6
+ /// A player that failed its retries while the device was offline has nothing
7
+ /// to react to when connectivity returns: the item stays failed and playback
8
+ /// stays stopped until the user notices and presses play. This is the signal
9
+ /// that lets recovery run at the point it can actually succeed.
10
+ ///
11
+ /// Only the **down → up** edge is reported. A satisfied → satisfied update —
12
+ /// a Wi-Fi to cellular handoff, an interface change — is not an outage
13
+ /// recovery, and treating it as one would retry against a connection that
14
+ /// never dropped.
15
+ ///
16
+ /// One-shot: `start()` arms the monitor once, and `stop()` after it is
17
+ /// terminal — a later `start()` does nothing. `NWPathMonitor` does not define
18
+ /// `start` after `cancel`, and the player never needs it: `configure()`
19
+ /// creates a fresh monitor and destroy stops it.
20
+ ///
21
+ /// `NWPathMonitor` delivers the current path once on `start` and then on
22
+ /// every change, so a monitor started while online reports that first
23
+ /// delivery as a restore (the seed below is unsatisfied). Downstream that is
24
+ /// harmless — recovery only runs for an item that has already failed.
25
+ final class PlaybackNetworkMonitor {
26
+
27
+ /// Fired on the down → up transition, on `queue`.
28
+ var onNetworkRestored: (() -> Void)?
29
+
30
+ private let monitor = NWPathMonitor()
31
+ private let queue: DispatchQueue
32
+ /// Seeded unsatisfied so a monitor that starts while offline reports the
33
+ /// first success as a recovery rather than swallowing it as "no change".
34
+ private var lastStatus: NWPath.Status = .unsatisfied
35
+ private var started = false
36
+ /// Set by `stop()` after a `start()`; nothing clears it.
37
+ private var stopped = false
38
+
39
+ /// - Parameter queue: the queue callbacks are delivered on. Callers pass the
40
+ /// queue that serialises player state, so the handler needs no hop.
41
+ init(queue: DispatchQueue) {
42
+ self.queue = queue
43
+ }
44
+
45
+ func start() {
46
+ guard !started, !stopped else { return }
47
+ started = true
48
+ monitor.pathUpdateHandler = { [weak self] path in
49
+ guard let self else { return }
50
+ let previous = self.lastStatus
51
+ self.lastStatus = path.status
52
+ guard previous != .satisfied, path.status == .satisfied else { return }
53
+ self.onNetworkRestored?()
54
+ }
55
+ monitor.start(queue: queue)
56
+ }
57
+
58
+ /// Terminal once the monitor has started; a `stop()` before any `start()`
59
+ /// changes nothing.
60
+ func stop() {
61
+ guard started else { return }
62
+ started = false
63
+ stopped = true
64
+ monitor.pathUpdateHandler = nil
65
+ monitor.cancel()
66
+ }
67
+
68
+ /// A monitor dropped without `stop()` — `configure()` replaced it, or the
69
+ /// player was released — must not keep its handler queue alive.
70
+ deinit {
71
+ monitor.cancel()
72
+ }
73
+ }
@@ -7,14 +7,14 @@ import AVFoundation
7
7
  /// Android's `TransportStateTranslator`; pure + deterministic.
8
8
  ///
9
9
  /// iOS has no native "ended" player state (unlike Android's `STATE_ENDED`), so
10
- /// `reachedQueueEnd` is a latch that `TrackPlayer` sets when the queue drains
11
- /// with repeat off and clears when playback resumes / a new track loads.
10
+ /// `TrackPlayer` sets `reachedQueueEnd` when the queue drains with repeat off
11
+ /// and clears it when playback resumes / a new track loads.
12
12
  enum PlayerStateDerivation {
13
13
  /// - Parameters:
14
14
  /// - status: the active player's `timeControlStatus`.
15
15
  /// - currentIndex: the lib's canonical current track index (`-1` = empty).
16
16
  /// - hasCurrentItem: whether the active player has a current item.
17
- /// - reachedQueueEnd: the end-of-queue latch (see type doc).
17
+ /// - reachedQueueEnd: whether the queue has drained with repeat off (see type doc).
18
18
  static func translate(
19
19
  status: AVPlayer.TimeControlStatus,
20
20
  currentIndex: Int,
@@ -22,8 +22,8 @@ enum PlayerStateDerivation {
22
22
  reachedQueueEnd: Bool
23
23
  ) -> PlayerState {
24
24
  // A drained queue (repeat off) that isn't actively playing is terminal.
25
- // `.playing` takes precedence so a resume reads correctly even before the
26
- // latch clears.
25
+ // `.playing` takes precedence so a resume reads correctly even before
26
+ // `reachedQueueEnd` is cleared.
27
27
  if reachedQueueEnd, currentIndex >= 0, status != .playing {
28
28
  return .ended
29
29
  }
@@ -33,7 +33,7 @@ enum PlayerStateDerivation {
33
33
  case .waitingToPlayAtSpecifiedRate:
34
34
  // Waiting WITH a current item = initial load / rebuffer → `.buffering`.
35
35
  // Waiting with NO item = the AVQueuePlayer advanced past the last item
36
- // (end of queue) before the latch flipped → terminal `.ended`.
36
+ // (end of queue) before `reachedQueueEnd` was set → terminal `.ended`.
37
37
  return hasCurrentItem ? .buffering : (currentIndex < 0 ? .none : .ended)
38
38
  case .playing:
39
39
  return .playing
@@ -19,6 +19,31 @@ import Foundation
19
19
  /// test bundle.
20
20
  public enum QueueMutationArithmetic {
21
21
 
22
+ /// How many leading positions the engine already holds correctly.
23
+ ///
24
+ /// A rebuild that installs the whole slice throws away the item the engine
25
+ /// has already buffered, which is an audible cut when it happens at a
26
+ /// boundary. Comparing what the engine holds against what it should hold
27
+ /// says how much of the front can stay, so only the divergent tail is
28
+ /// replaced.
29
+ ///
30
+ /// Both sides are `queueItemId` — the library's own per-position identity.
31
+ /// Consumer fields cannot be used here: two tracks may be the same URL, and
32
+ /// `track.id` is optional consumer data, so neither distinguishes positions.
33
+ /// A `nil` id belongs to an item the library did not stamp and never
34
+ /// matches, so the prefix stops there.
35
+ @_spi(QueuePlayerTests)
36
+ public static func commonPrefixLength(
37
+ current: [String?], desired: [String]
38
+ ) -> Int {
39
+ var length = 0
40
+ while length < current.count, length < desired.count {
41
+ guard let id = current[length], id == desired[length] else { break }
42
+ length += 1
43
+ }
44
+ return length
45
+ }
46
+
22
47
  /// Clamp an `insertBefore` argument to a valid insertion point in
23
48
  /// a track list of size `trackCount`. `nil` → append (return
24
49
  /// `trackCount`). Out-of-range values clamp into `0...trackCount`.
@@ -91,14 +91,16 @@ public enum QueueSkipArithmetic {
91
91
  /// When `restartEnabled` is true (`PlayerConfig.skipToPreviousBehavior
92
92
  /// == .restartOrPrevious`), Previous is enabled whenever a track is
93
93
  /// loaded — even at index 0, where past the threshold it restarts the
94
- /// current track — so the control never greys out.
94
+ /// current track — so the control never greys out while something is
95
+ /// playing. With no loaded track (`currentIndex < 0`) there is nothing
96
+ /// to restart, matching what resolveSkipToPrevious does for that state.
95
97
  public static func canSkipPrevious(
96
98
  trackCount: Int,
97
99
  currentIndex: Int,
98
100
  repeatMode: QueueSkipRepeatMode,
99
101
  restartEnabled: Bool = false
100
102
  ) -> Bool {
101
- if restartEnabled { return trackCount > 0 }
103
+ if restartEnabled { return trackCount > 0 && currentIndex >= 0 }
102
104
  return computePrevious(
103
105
  trackCount: trackCount,
104
106
  currentIndex: currentIndex,
@@ -117,7 +119,7 @@ public enum QueueSkipArithmetic {
117
119
  case previous(Int)
118
120
  /// Restart the current track (seek to 0) — not a track change.
119
121
  case restartCurrent
120
- /// Nothing to do (empty queue).
122
+ /// Nothing to do (empty queue, or no loaded track).
121
123
  case noOp
122
124
  }
123
125