react-native-queue-player 1.2.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/consumer-rules.pro +1 -1
  4. package/android/src/main/cpp/airplay2_control.c +4 -4
  5. package/android/src/main/cpp/airplay2_control.h +4 -4
  6. package/android/src/main/cpp/airplay2_jni.cpp +3 -19
  7. package/android/src/main/cpp/airplay2_pair.c +7 -7
  8. package/android/src/main/cpp/airplay2_pair.h +2 -2
  9. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  10. package/android/src/main/cpp/airplay2_session.c +2 -2
  11. package/android/src/main/cpp/airplay_jni.cpp +8 -69
  12. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  61. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  62. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  63. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  64. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  65. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  114. package/app.plugin.js +17 -1
  115. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  116. package/ios/AVQueueBuilder.swift +120 -85
  117. package/ios/ArtworkLoader.swift +0 -7
  118. package/ios/ArtworkResolver.swift +22 -4
  119. package/ios/AssetReplayGainReader.swift +118 -0
  120. package/ios/AudioSession.swift +67 -16
  121. package/ios/AudioTapProvider.swift +284 -149
  122. package/ios/CarPlayBridge.swift +0 -12
  123. package/ios/CarPlayCoordinator.swift +18 -2
  124. package/ios/CarPlaySceneDelegate.swift +2 -2
  125. package/ios/Cast/AirPlayRouteState.swift +9 -8
  126. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  127. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  128. package/ios/Cast/Core/CastSession.swift +2 -2
  129. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  130. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  131. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  132. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  133. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  134. package/ios/CastManager.swift +1 -1
  135. package/ios/CrossfadeEngine.swift +672 -226
  136. package/ios/EQTap.swift +20 -17
  137. package/ios/Equalizer.swift +1 -1
  138. package/ios/FLACStreamInfo.swift +65 -0
  139. package/ios/GaplessEngine.swift +57 -30
  140. package/ios/InputGuards.swift +35 -5
  141. package/ios/LookaheadCache.swift +229 -58
  142. package/ios/LookaheadCachePrefetcher.swift +48 -36
  143. package/ios/MediaResponseCheck.swift +122 -0
  144. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  145. package/ios/NetworkRecoveryPolicy.swift +32 -0
  146. package/ios/NowPlayingFormatExtractor.swift +25 -18
  147. package/ios/NowPlayingInfo.swift +84 -48
  148. package/ios/OutputRouteMonitor.swift +16 -3
  149. package/ios/PendingBrowseRequests.swift +1 -1
  150. package/ios/PlaceholderArtwork.swift +9 -5
  151. package/ios/PlaybackEngine.swift +165 -76
  152. package/ios/PlaybackErrorMapping.swift +3 -3
  153. package/ios/PlaybackModeStateMachine.swift +2 -2
  154. package/ios/PlaybackNetworkMonitor.swift +73 -0
  155. package/ios/PlayerStateDerivation.swift +6 -6
  156. package/ios/QueueMutationArithmetic.swift +25 -0
  157. package/ios/QueueSkipArithmetic.swift +5 -3
  158. package/ios/QueueWindowArithmetic.swift +152 -0
  159. package/ios/ReadThroughServer.swift +283 -0
  160. package/ios/RemoteCommands.swift +44 -4
  161. package/ios/ReplayGainData.swift +22 -3
  162. package/ios/ReplayGainExtractor.swift +70 -27
  163. package/ios/Siri/VoiceDonation.swift +14 -9
  164. package/ios/StreamingBitrateProbe.swift +22 -68
  165. package/ios/SuppliedReplayGain.swift +83 -0
  166. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  167. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  168. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  169. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  170. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  171. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  172. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  173. package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
  174. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  175. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  176. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  177. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  178. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  179. package/ios/Tests/EngineSwapTests.swift +91 -0
  180. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  181. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  182. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  183. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  184. package/ios/Tests/InputGuardsTests.swift +41 -0
  185. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  186. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  187. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  188. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  189. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  190. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  191. package/ios/Tests/MediaResponseCheckTests.swift +128 -0
  192. package/ios/Tests/MutationDeferralTests.swift +220 -0
  193. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  194. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  195. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  196. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  197. package/ios/Tests/PlayerFixtures.swift +48 -0
  198. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  199. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  200. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  201. package/ios/Tests/QueueStateTests.swift +3 -1
  202. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  203. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  204. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  205. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  206. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  207. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  208. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  209. package/ios/Tests/RetryRecoveryTests.swift +418 -0
  210. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  211. package/ios/Tests/SkipIndexTests.swift +32 -2
  212. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  213. package/ios/Tests/StallRecoveryTests.swift +97 -0
  214. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  215. package/ios/Tests/TopUpWindowGateTests.swift +441 -0
  216. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  217. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  218. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  219. package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
  220. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  221. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  222. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  223. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  224. package/ios/TrackPlayer+Automotive.swift +101 -0
  225. package/ios/TrackPlayer+Cache.swift +232 -0
  226. package/ios/TrackPlayer+Config.swift +291 -0
  227. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  228. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  229. package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
  230. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  231. package/ios/TrackPlayer+Lifecycle.swift +982 -0
  232. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  233. package/ios/TrackPlayer+Queue.swift +951 -0
  234. package/ios/TrackPlayer+Recovery.swift +213 -0
  235. package/ios/TrackPlayer+Skip.swift +465 -0
  236. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  237. package/ios/TrackPlayer+State.swift +108 -0
  238. package/ios/TrackPlayer+Threading.swift +135 -0
  239. package/ios/TrackPlayer+Transport.swift +272 -0
  240. package/ios/TrackPlayer+Window.swift +227 -0
  241. package/ios/TrackPlayer.swift +331 -4619
  242. package/ios/Visualizer.swift +6 -5
  243. package/ios/tests-harness/Podfile +1 -1
  244. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  245. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  246. package/lib/module/hooks/useActiveTrack.js +29 -22
  247. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  248. package/lib/module/hooks/useCast.js +8 -26
  249. package/lib/module/hooks/useCast.js.map +1 -1
  250. package/lib/module/hooks/useEqualizer.js +19 -12
  251. package/lib/module/hooks/useEqualizer.js.map +1 -1
  252. package/lib/module/hooks/useLookaheadCache.js +3 -7
  253. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  254. package/lib/module/hooks/useQueue.js +66 -19
  255. package/lib/module/hooks/useQueue.js.map +1 -1
  256. package/lib/module/index.js +6 -3
  257. package/lib/module/index.js.map +1 -1
  258. package/lib/module/queueDelta.js +41 -0
  259. package/lib/module/queueDelta.js.map +1 -0
  260. package/lib/module/types.js +28 -5
  261. package/lib/module/types.js.map +1 -1
  262. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  263. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  264. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  265. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  266. package/lib/typescript/hooks/useCast.d.ts +6 -1
  267. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  269. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  271. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  272. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  273. package/lib/typescript/index.d.ts +2 -1
  274. package/lib/typescript/index.d.ts.map +1 -1
  275. package/lib/typescript/queueDelta.d.ts +10 -0
  276. package/lib/typescript/queueDelta.d.ts.map +1 -0
  277. package/lib/typescript/types.d.ts +168 -12
  278. package/lib/typescript/types.d.ts.map +1 -1
  279. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  280. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  281. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  282. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  283. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  284. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  285. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  320. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  321. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  322. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  323. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  324. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  325. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  326. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  327. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  328. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  329. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  330. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  331. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  332. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  333. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  334. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  335. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  336. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  337. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  338. package/package.json +8 -8
  339. package/src/TrackPlayer.nitro.ts +53 -27
  340. package/src/hooks/useActiveTrack.ts +29 -22
  341. package/src/hooks/useCast.ts +14 -15
  342. package/src/hooks/useEqualizer.ts +19 -12
  343. package/src/hooks/useLookaheadCache.ts +3 -7
  344. package/src/hooks/useQueue.ts +66 -17
  345. package/src/index.ts +13 -3
  346. package/src/queueDelta.ts +41 -0
  347. package/src/types.ts +169 -12
  348. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  349. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  350. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  351. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  352. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  353. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  354. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  355. package/ios/MetadataReader.swift +0 -555
  356. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  357. package/ios/Tests/MetadataReaderTests.swift +0 -603
  358. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  359. package/ios/Tests/VoiceDonationTests.swift +0 -26
  360. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  361. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  362. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -0,0 +1,272 @@
1
+ import AVFoundation
2
+ import NitroModules
3
+
4
+ extension TrackPlayer {
5
+ // MARK: - Transport — pass-through to AVQueuePlayer.
6
+
7
+ func play() throws -> Promise<Void> {
8
+ return enqueue {
9
+ if CastTransportRouter.routePlay() { return }
10
+ // Stuck-recovery: the queue holds on a track whose failure was reported,
11
+ // and pressing play is asking for that track again — so it gets the
12
+ // allowance the consumer configured back and a rebuild. Not a hard-coded
13
+ // 1: `autoRetries: 0` asked for none.
14
+ if let qid = self.reportedFailureOnCurrentTrack {
15
+ // Asking for this track to play is incompatible with the queue being
16
+ // over, so `reachedQueueEnd` cannot survive it.
17
+ self.reachedQueueEnd = false
18
+ self.retryAttemptsRemaining[qid] = self.effectiveAutoRetries()
19
+ // This track only. A standby leg's report is a different track's, and
20
+ // discarding it here would hand the listener that failure a second
21
+ // time when the engine seats it.
22
+ self.reportedFailureQueueItemIds.remove(qid)
23
+ // A scheduled attempt for this item is superseded by the rebuild below.
24
+ // Reachable because the two are set by different errors on the same
25
+ // item: a reported failure leaves the report standing, and a later
26
+ // failure carrying a different code is transient enough to schedule an
27
+ // attempt. Leaving it would fire a second rebuild behind this one.
28
+ if self.pendingRetryQueueItemId == qid { self.pendingRetryQueueItemId = nil }
29
+ self.lastErrorQueueItemId = nil
30
+ self.lastErrorCode = nil
31
+ // Rebuild whenever an engine is live — the rebuild is
32
+ // engine-agnostic, and the failed item has to be
33
+ // reconstructed on either engine for the retry to reach it.
34
+ // Forced: the failed item keeps its `queueItemId`, so a rebuild
35
+ // that compares identity would find nothing to do.
36
+ if self.engine != nil {
37
+ self.performingMutation {
38
+ self.fullRebuildPlayerQueue(forcingReinstall: true)
39
+ }
40
+ }
41
+ }
42
+ // At the end of the queue there is nothing to play: the engine is
43
+ // parked on the finished track (or drained), and writing a rate onto
44
+ // it would report `.playing` for a moment or, on some OS versions,
45
+ // re-post the item's end. A skip, a seek or a fresh queue leaves this
46
+ // state; play() alone restarts nothing.
47
+ if self.reachedQueueEnd, self.currentTrackIndex >= 0 { return }
48
+ self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
49
+ self.wantsToPlay = true
50
+ // An explicit play ends an interruption from the user's side: the
51
+ // system does not always deliver `.ended` (an app suspended mid-call
52
+ // never sees it), and the flag would otherwise keep retries, window
53
+ // refills and network recovery disabled for the rest of the session.
54
+ self.isInterrupted = false
55
+ self.beginPlayback()
56
+ self.recomputeBufferState()
57
+ }
58
+ }
59
+
60
+ func retry() throws -> Promise<Void> {
61
+ return enqueue {
62
+ // During cast the receiver owns playback — forward a play so a failed
63
+ // receiver item re-attempts.
64
+ if CastTransportRouter.routePlay() { return }
65
+ // A user asking for a retry has a reason, and the library is not the one
66
+ // to judge it. This is the manual attempt once the automatic one is
67
+ // spent, so gating it on the player's state would gate recovery on the
68
+ // state of a player that has already failed to recover — and a wedged
69
+ // decoder still reporting `playing` is exactly when someone reaches for
70
+ // the button.
71
+ //
72
+ // Only the bounds remain: no current track is genuinely nothing to
73
+ // retry, which is not second-guessing the request.
74
+ guard self.currentTrackIndex >= 0,
75
+ self.currentTrackIndex < self.queueItemIds.count else { return }
76
+ let qid = self.queueItemIds[self.currentTrackIndex]
77
+ // Capture the pre-failure position so a mid-track failure resumes where
78
+ // it stopped (the rebuild below reloads the item from 0). Matches
79
+ // Android's retryFailedItem position-restore.
80
+ let resumePos = self.engine?.currentPositionSeconds ?? 0
81
+ // Asking for this track to play is incompatible with the queue being over,
82
+ // and a queue whose only track failed to load has already drained: without
83
+ // this the rebuild seats an item into a queue that believes it is finished.
84
+ self.reachedQueueEnd = false
85
+ // The configured budget, not a hard-coded 1: this restores the track's
86
+ // automatic allowance for whatever fails *next*, and a consumer who set
87
+ // `autoRetries: 0` asked for none.
88
+ self.retryAttemptsRemaining[qid] = self.effectiveAutoRetries()
89
+ // A scheduled attempt for this item is superseded by the rebuild below;
90
+ // leaving it set would suppress the rebuilt item's own failure until the
91
+ // earlier attempt fired.
92
+ if self.pendingRetryQueueItemId == qid { self.pendingRetryQueueItemId = nil }
93
+ self.lastErrorQueueItemId = nil
94
+ self.lastErrorCode = nil
95
+ // This track only, for the reason `play()` gives above.
96
+ self.reportedFailureQueueItemIds.remove(qid)
97
+ // Rebuild whenever an engine is live — the rebuild is
98
+ // engine-agnostic, and the failed item has to be reconstructed
99
+ // on either engine for the retry to reach it. Forced: the failed
100
+ // item keeps its `queueItemId`, so a rebuild that compares
101
+ // identity would find nothing to do.
102
+ if self.engine != nil {
103
+ self.performingMutation {
104
+ self.fullRebuildPlayerQueue(forcingReinstall: true)
105
+ }
106
+ }
107
+ self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
108
+ self.wantsToPlay = true
109
+ self.beginPlayback()
110
+ if resumePos.isFinite, resumePos > 0 {
111
+ _ = try? self.seekTo(position: resumePos)
112
+ }
113
+ self.recomputeBufferState()
114
+ }
115
+ }
116
+
117
+ func pause() throws -> Promise<Void> {
118
+ return enqueue {
119
+ if CastTransportRouter.routePause() { return }
120
+ self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
121
+ self.wantsToPlay = false
122
+ // An explicit pause outranks a pending stall recovery: without this the
123
+ // buffer refilling afterwards would restart audio the user just stopped.
124
+ self.isRecoveringFromStall = false
125
+ self.engine?.pause()
126
+ self.recomputeBufferState()
127
+ }
128
+ }
129
+
130
+ /// "Stop" = pause + seek to start of current item. Does NOT clear
131
+ /// the queue — consumers wanting a full reset call clearQueue().
132
+ func stop() throws -> Promise<Void> {
133
+ return enqueue {
134
+ if CastTransportRouter.routeStop() { return }
135
+ guard let engine = self.engine else { return }
136
+ self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
137
+ self.wantsToPlay = false
138
+ self.hasStartedPlaying = false
139
+ engine.stop()
140
+ self.recomputeBufferState()
141
+ }
142
+ }
143
+
144
+ /// Seek within the current track. Position in seconds; negative
145
+ /// values clamp to 0, values past the duration clamp to duration.
146
+ func seekTo(position: Double) throws -> Promise<Void> {
147
+ // Enqueued at call time, like every other transport method, so a seek
148
+ // reaches the player in the order JS issued it. The promise resolves once
149
+ // the seek's own completion has landed, which is why this builds the
150
+ // promise by hand rather than using `enqueue`.
151
+ let promise = Promise<Void>()
152
+ playerQueue.async {
153
+ // Match Android (`TrackPlayer.kt` seekToInternal): non-finite
154
+ // (NaN / ±Infinity) inputs are silent no-ops. See
155
+ // `InputGuards.sanitisedSeekPosition` for the rationale.
156
+ guard let position = InputGuards.sanitisedSeekPosition(position) else {
157
+ promise.resolve()
158
+ return
159
+ }
160
+ if CastTransportRouter.routeSeekTo(positionMs: Int64(position * 1000.0)) {
161
+ promise.resolve()
162
+ return
163
+ }
164
+
165
+ // Every player + queue read and the seek itself run on this one hop, so
166
+ // the player never crosses a concurrency boundary. The return value
167
+ // carries whether a seek was issued: the crossfade branch and the
168
+ // drained-gapless branch complete in place and yield false.
169
+ //
170
+ // Named rather than inlined so the two early-return branches and the
171
+ // seek branch share one exit.
172
+ let issueSeek: () -> Bool = {
173
+ // Engine-aware seek: under CrossfadeEngine `self.player` is
174
+ // nil. Without this branch every seek call would silently
175
+ // no-op once crossfade mode is active, breaking transport
176
+ // controls + auto-advance test coverage.
177
+ if self.player == nil {
178
+ guard let engine = self.engine else { return false }
179
+ // Seeking replays from a position — no longer at the end of the
180
+ // queue. (Gapless no-ops at end-of-queue with no current item below.)
181
+ self.reachedQueueEnd = false
182
+ let duration = engine.currentDurationSeconds
183
+ let clamped: Double
184
+ if position < 0 {
185
+ clamped = 0
186
+ } else {
187
+ clamped = (duration > 0 && position > duration) ? duration : position
188
+ }
189
+ let seekTarget = self.clampToBufferedIfEnabled(clamped)
190
+ self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
191
+ let engineIdx: Int
192
+ if let installed = self.engineIndex(forQueuePosition: self.currentTrackIndex) {
193
+ engineIdx = installed
194
+ } else {
195
+ // Active track absent from the engine — reinstall it, then seek
196
+ // within the reinstalled run so the position is applied rather
197
+ // than dropped with a resolved promise.
198
+ self.performingMutation { self.fullRebuildPlayerQueue() }
199
+ guard let reinstalled = self.engineIndex(forQueuePosition: self.currentTrackIndex)
200
+ else { return false }
201
+ engineIdx = reinstalled
202
+ }
203
+ engine.seek(toIndex: engineIdx, position: seekTarget)
204
+ self.milestoneTracker.seek(
205
+ durationSec: self.effectiveMilestoneDuration(playerDuration: duration),
206
+ positionSec: seekTarget)
207
+ self.nowPlayingInfo.refreshPositionAndRate(self.nowPlayingSnapshot())
208
+ return false
209
+ }
210
+
211
+ guard let player = self.player else { return false }
212
+ // A gapless player drained at the end of the queue has no item to
213
+ // seek in: reinstall the finished track first, as a same-index skip
214
+ // does, so a seek after the end replays it on every engine. Seeking
215
+ // is what leaves the ended state, so the flag clears here.
216
+ if player.currentItem == nil, self.reachedQueueEnd,
217
+ self.currentTrackIndex >= 0, self.currentTrackIndex < self.tracks.count {
218
+ self.reachedQueueEnd = false
219
+ self.performingMutation { self.fullRebuildPlayerQueue() }
220
+ }
221
+ guard let item = player.currentItem else { return false }
222
+ let itemDuration = CMTimeGetSeconds(item.duration)
223
+ let clamped: Double
224
+ if position < 0 {
225
+ clamped = 0
226
+ } else {
227
+ clamped = (itemDuration.isFinite && position > itemDuration) ? itemDuration : position
228
+ }
229
+ // Apply the opt-in buffered clamp on top of the 0/duration clamp — caps
230
+ // the target at the buffered edge when the seek is past what's downloaded.
231
+ let seekTarget = self.clampToBufferedIfEnabled(clamped)
232
+ // Consume jumped-over milestones BEFORE issuing the (async) seek so a
233
+ // time-jump progress tick that fires during the seek can't emit the
234
+ // thresholds the seek skips over.
235
+ self.milestoneTracker.seek(
236
+ durationSec: self.effectiveMilestoneDuration(
237
+ playerDuration: (itemDuration.isFinite && itemDuration > 0) ? itemDuration : 0),
238
+ positionSec: seekTarget)
239
+ // Stash `.user` reason BEFORE the seek so any state transition
240
+ // the seek triggers (e.g. playable → buffering on a long jump)
241
+ // gets the correct origin. Matches the play / pause / stop pattern.
242
+ self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
243
+ player.seek(
244
+ to: CMTime(seconds: seekTarget, preferredTimescale: CMTimeScale(NSEC_PER_SEC)),
245
+ toleranceBefore: .zero,
246
+ toleranceAfter: .zero
247
+ ) { _ in
248
+ // This closure captures `player`, so a teardown that drops every
249
+ // other reference cannot deallocate it with the seek still pending.
250
+ // AVFoundation invokes the handler when the seek completes or is
251
+ // interrupted, and the capture is released with it.
252
+ _ = player
253
+ self.playerQueue.async {
254
+ // Surface the new scrubber position to the lock-screen /
255
+ // control-center immediately. The KVO state-change emit covers
256
+ // transitions (e.g. paused → buffering on a long jump) but a
257
+ // same-state seek won't trigger an emit; without this the
258
+ // lock-screen scrubber sits at the pre-seek position until the
259
+ // next 10s periodic tick.
260
+ self.nowPlayingInfo.refreshPositionAndRate(self.nowPlayingSnapshot())
261
+ promise.resolve()
262
+ }
263
+ }
264
+ return true
265
+ }
266
+ // A branch that completed in place has nothing pending, so the promise
267
+ // resolves here; the seek branch resolves from its completion instead.
268
+ if !issueSeek() { promise.resolve() }
269
+ }
270
+ return promise
271
+ }
272
+ }
@@ -0,0 +1,227 @@
1
+ extension TrackPlayer {
2
+ /// Bring the engine's installed run back in line as the playhead moves.
3
+ ///
4
+ /// Reconciling against `QueueWindowArithmetic.slice` rather than measuring
5
+ /// from an anchor: a run that wraps ends at a lower absolute position than
6
+ /// it starts, so any comparison of the two as integers reads it as
7
+ /// backwards. The reconcile keeps what is still wanted, drops what has
8
+ /// fallen off the trail, and builds the rest.
9
+ internal func topUpWindow() {
10
+ guard let engine = self.engine else { return }
11
+ // Every branch of the reconcile changes the installed items, and each of
12
+ // those cancels an in-flight fade. Extending the run can wait for the fade
13
+ // to finish; cutting a crossfade short to do it cannot be undone.
14
+ guard !engine.isFadePendingOrActive else { return }
15
+ // This extends a run; it never creates one. A drained engine is either a
16
+ // queue that has ended or one the caller is about to seat itself, and
17
+ // installing into it here would put the finished track back and let the
18
+ // rate the engine restores carry it on playing.
19
+ guard !engine.allMediaItems.isEmpty, !self.reachedQueueEnd else { return }
20
+ // A current position that has failed is not put back, whether its failure
21
+ // has been reported or an attempt for it is still scheduled. `AVQueuePlayer`
22
+ // consumes a failed item, and when the run holds something behind it the
23
+ // player advances there instead of draining — so the end verdict never runs
24
+ // and this reconcile is what rebuilds. It rebuilds from `currentTrackIndex`,
25
+ // still the failed position because the resync needs a play-to-end and a
26
+ // failure fires `AVPlayerItemFailedToPlayToEndTime` instead, and a reinstall
27
+ // is exactly what a missing current position asks for. The rebuilt item
28
+ // fails the same way and its error is swallowed — as a repeat, or as an echo
29
+ // of the pending attempt — so the rebuild would repeat unannounced.
30
+ guard self.reportedFailureOnCurrentTrack == nil else { return }
31
+ if let currentId = self.queueItemIds[safe: self.currentTrackIndex],
32
+ self.pendingRetryQueueItemId == currentId {
33
+ return
34
+ }
35
+ self.performingMutation { self.fullRebuildPlayerQueue() }
36
+ }
37
+
38
+ /// Notices that the gapless player has auto-advanced past its last item,
39
+ /// and hands the decision to `applyEndVerdict`.
40
+ internal func detectQueueEnd() {
41
+ // Engine-aware "is the active item nil" check. With
42
+ // CrossfadeEngine `self.player` is nil by design — reading it
43
+ // alone would mis-detect every mid-play engine swap as
44
+ // queue-end and fire a spurious onQueueEnd + emitState(.ended).
45
+ let activeItemNil: Bool
46
+ if self.player != nil {
47
+ activeItemNil = self.player?.currentItem == nil
48
+ } else if let engine = self.engine {
49
+ activeItemNil = engine.currentMediaItem == nil
50
+ } else {
51
+ activeItemNil = true
52
+ }
53
+ guard activeItemNil else { return }
54
+ applyEndVerdict()
55
+ }
56
+
57
+ /// Play `currentTrackIndex` from its start and announce it.
58
+ ///
59
+ /// A rebuild replaces only the positions that differ, so it cannot be relied
60
+ /// on for either half of this: when the position being re-seated is already
61
+ /// the one installed, nothing is replaced, the seek is what restarts it, and
62
+ /// no `currentItem` change fires to carry the track change out. Dispatching
63
+ /// here is deduped against `lastEmittedQueueItemId`, so a rebuild that *did*
64
+ /// replace the item still emits exactly once.
65
+ private func restartCurrentPosition() {
66
+ if let engine = self.engine,
67
+ let engineIdx = engineIndex(forQueuePosition: self.currentTrackIndex) {
68
+ engine.seek(toIndex: engineIdx, position: 0)
69
+ }
70
+ dispatchTrackChange(reason: .autoAdvance)
71
+ }
72
+
73
+ /// Decide and apply what a drained engine means for the queue.
74
+ ///
75
+ /// Both ends funnel here — the gapless player's `currentItem` going nil, and
76
+ /// the crossfade engine reporting its last item played out — so the queue
77
+ /// reaches the same verdict whichever engine is installed. The engines
78
+ /// report that they drained; they do not decide whether the queue is over.
79
+ ///
80
+ /// A drained engine exposes no items, so the anchor cannot be derived from
81
+ /// them. `matchTrackIndex(forCurrentItem: nil)` is nil at a drain, which
82
+ /// leaves `currentTrackIndex` on the track that just finished — the furthest
83
+ /// position the queue actually reached.
84
+ internal func applyEndVerdict() {
85
+ // `verdict` is specified against a queue that has something to end.
86
+ guard !self.tracks.isEmpty else { return }
87
+ // A queue that has ended stays ended: a further drain report — a rate
88
+ // written onto a leg parked at its end re-posts the item's end — must
89
+ // not re-decide against a queue that has since grown and seat the added
90
+ // track. A skip, a seek or a fresh queue clears the flag first.
91
+ guard !self.reachedQueueEnd else { return }
92
+ // A drain whose current item failed to load with an auto-retry already
93
+ // scheduled is not the end of the queue — the pending retry rebuilds and
94
+ // resumes that item. `AVQueuePlayer` consumes a failed item (its
95
+ // `actionAtItemEnd` is `.advance`) and drains to `currentItem == nil`,
96
+ // which lands here; deciding a queue-end would clear the play intent the
97
+ // retry reads, leaving the recovered track paused. The scheduled retry
98
+ // clears `pendingRetryQueueItemId` and re-seats the item, so this only
99
+ // holds for the brief window between the failure and the attempt.
100
+ //
101
+ // Relies on the item's `.failed` status KVO — which stakes
102
+ // `pendingRetryQueueItemId` in `dispatchErrorOrRetry` — being delivered
103
+ // before the `currentItem`→nil drain KVO that reaches here. The status
104
+ // change is what causes the advance-to-nil, so it is observed first. The
105
+ // `retry-recovery` demo suite is the integration coverage for that order.
106
+ if let currentId = self.queueItemIds[safe: self.currentTrackIndex],
107
+ self.pendingRetryQueueItemId == currentId {
108
+ return
109
+ }
110
+ // A drain that follows the current item's reported failure is that failure,
111
+ // not the end of the queue. The same `.advance` walks a failed item out of
112
+ // the player, and with nothing installed behind it the queue drains to
113
+ // `currentItem == nil` and lands here. Deciding a verdict would move past a
114
+ // track the consumer has just been told failed — and when the tracks behind
115
+ // it fail the same way, the advance, rebuild and resume repeat with nothing
116
+ // counting them, wrapping forever under repeat-all. The queue stays where it
117
+ // is, in `.error`, until `play()` or `retry()` asks for the track again.
118
+ if self.reportedFailureOnCurrentTrack != nil {
119
+ // Park the engine, as the queue-end branch below does and for the same
120
+ // reason: neither engine drops its rate when it runs out of items, so a
121
+ // leg left running would start whatever lands on it next. The gapless
122
+ // failure path pauses the `AVQueuePlayer` itself, but the crossfade one
123
+ // has no `AVQueuePlayer` to pause.
124
+ self.engine?.pause()
125
+ return
126
+ }
127
+ var verdict = QueueWindowArithmetic.verdict(
128
+ drained: true,
129
+ atLogicalTail: QueueWindowArithmetic.isAtLogicalTail(
130
+ anchorPosition: self.currentTrackIndex, trackCount: self.tracks.count),
131
+ repeatMode: repeatModeAsSkipMode())
132
+
133
+ // Re-decided in place when the position a verdict seats cannot be built:
134
+ // `.windowDrained` advances the anchor each time round, so it reaches the
135
+ // tail; `.refillCurrent` cannot advance (repeat-track re-seats the same
136
+ // position), so an unplayable current track ends the queue instead.
137
+ while true {
138
+ switch verdict {
139
+ case .notEnded:
140
+ return
141
+ case .wrapToStart:
142
+ self.currentTrackIndex = 0
143
+ self.pendingTrackChangeReason = .autoAdvance
144
+ // The queueItemId at index 0 is unchanged across a single-track wrap,
145
+ // so without this clear the next dispatch would dedup-swallow the
146
+ // re-emission and the consumer would see no track change.
147
+ self.lastEmittedQueueItemId = nil
148
+ self.performingMutation { self.fullRebuildPlayerQueue() }
149
+ // Restart position 0 explicitly: on a single-track queue the wrap target
150
+ // is the item already installed, so the rebuild replaces nothing.
151
+ restartCurrentPosition()
152
+ // Each engine restores its own rate across a rebuild only while its
153
+ // play intent stands, and a reconcile that removes the item the
154
+ // crossfade engine's leading leg held drops that intent with it — so
155
+ // the resume is asked for here. Gated on the transport intent because
156
+ // a rebuild cannot tell "was playing" from "was paused" by rate alone,
157
+ // and on the interruption flag because the system may not permit
158
+ // playback right now.
159
+ if self.wantsToPlay, !self.isInterrupted { self.beginPlayback() }
160
+ return
161
+ case .windowDrained, .refillCurrent:
162
+ // The engine ran out with queue left. `currentTrackIndex` still names
163
+ // the track that just finished, so `.windowDrained` moves past it while
164
+ // `.refillCurrent` re-seats it. Clearing `lastEmittedQueueItemId` keeps
165
+ // the refilled track from being dedup-swallowed, and the drained player
166
+ // has parked at rate 0, so it has to be told to resume.
167
+ if verdict == .windowDrained {
168
+ self.currentTrackIndex = min(self.currentTrackIndex + 1, self.tracks.count - 1)
169
+ }
170
+ self.pendingTrackChangeReason = .autoAdvance
171
+ self.lastEmittedQueueItemId = nil
172
+ self.performingMutation { self.fullRebuildPlayerQueue() }
173
+ self.rescheduleLookahead()
174
+ // A position whose item cannot be built is absent from the installed
175
+ // run, and no engine signal follows to re-enter on. Asking whether the
176
+ // run holds *this* position rather than whether the engine holds
177
+ // anything: the run reaches behind the playhead, so an unplayable
178
+ // current position still leaves the trail installed.
179
+ //
180
+ // Re-decide rather than parking mid-queue.
181
+ if engineIndex(forQueuePosition: self.currentTrackIndex) == nil {
182
+ if verdict == .refillCurrent {
183
+ verdict = .queueEnd
184
+ } else {
185
+ verdict = QueueWindowArithmetic.verdict(
186
+ drained: true,
187
+ atLogicalTail: QueueWindowArithmetic.isAtLogicalTail(
188
+ anchorPosition: self.currentTrackIndex, trackCount: self.tracks.count),
189
+ repeatMode: repeatModeAsSkipMode())
190
+ }
191
+ continue
192
+ }
193
+ // Both verdicts need the explicit restart the wrap does — after the
194
+ // guard above, so a position that could not be built is never announced
195
+ // as a track change. An engine that retains its run rebuilds by
196
+ // extending it, which never re-seats the leg: it would still hold the
197
+ // item that just ended, and the resume below would write a rate onto a
198
+ // track already at its end. On an engine that consumes its items the
199
+ // rebuild has reinstalled from the anchor already, and this seeks to a
200
+ // position it is on.
201
+ restartCurrentPosition()
202
+ // Playback is asked to resume for the same reason as the wrap above,
203
+ // and only if it was running: neither engine drops its rate when it
204
+ // runs out of items, so rate cannot tell "was playing" from "was
205
+ // paused" and the transport intent is what does. Not while the system
206
+ // has playback interrupted — the refill must not start audio during a
207
+ // call, nor after one the user declined to resume.
208
+ if self.wantsToPlay, !self.isInterrupted { self.beginPlayback() }
209
+ return
210
+ case .queueEnd:
211
+ // Park the engine. Draining is not the same as being stopped — neither
212
+ // engine drops its rate when it runs out of items, so a leg left running
213
+ // would start the next queue installed on it the moment an item lands.
214
+ // The other verdicts deliberately resume instead, just below.
215
+ self.engine?.pause()
216
+ // The intent to play ends with the queue: nothing that seats a track
217
+ // from here — a skip, a seek, a fresh queue, an engine swap — starts
218
+ // it until play() is called again.
219
+ self.wantsToPlay = false
220
+ self.reachedQueueEnd = true
221
+ queueEndListeners.forEach { $0() }
222
+ emitState(.ended, reason: .queueEnd)
223
+ return
224
+ }
225
+ }
226
+ }
227
+ }