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,291 @@
1
+ import Foundation
2
+ import NitroModules
3
+
4
+ extension TrackPlayer {
5
+ // MARK: - Playback configuration — some stubs, some lifecycle-adjacent
6
+
7
+ func setPlaybackMode(mode: PlaybackMode) throws -> Promise<Void> {
8
+ return enqueueThrowing {
9
+ let (newState, effect) = try self.playbackModeStateMachine.setRequested(
10
+ state: self.playbackModeState,
11
+ requested: mode
12
+ )
13
+ self.playbackModeState = newState
14
+ switch effect {
15
+ case .noOp:
16
+ break
17
+ case .immediateSwap:
18
+ self.applyEngineSwapForMode(newState.active)
19
+ }
20
+ }
21
+ }
22
+
23
+ func getPlaybackMode() throws -> PlaybackMode {
24
+ // Lock-guarded, so this answers without hopping. A `playerQueue.sync` here
25
+ // parks the JS thread behind whatever the queue is running, which can be a
26
+ // full queue rebuild.
27
+ return self.playbackModeState.active
28
+ }
29
+
30
+ /// Build a fresh engine of the requested kind and swap it in
31
+ /// place of the active engine. The new engine starts empty; the
32
+ /// canonical queue model is re-installed via the engine surface
33
+ /// so playback continues from the current track / position.
34
+ ///
35
+ /// JS-facing event sources differ per engine. GaplessEngine ties
36
+ /// `self.player` to the underlying `AVQueuePlayer` and the KVO
37
+ /// chain installed by `installGaplessObservers` /
38
+ /// `installEventObservers` carries onTrackChange / onStateChange
39
+ /// / onError. CrossfadeEngine sets `self.player` to nil and
40
+ /// fires `PlaybackEngineDelegate` callbacks that this class
41
+ /// implements (extension at the end of the file); the delegate
42
+ /// is wired below. Both engines surface progress through
43
+ /// `emitProgressIfSubscribed`, polymorphic over self.player vs
44
+ /// engine state.
45
+ internal func applyEngineSwapForMode(_ active: PlaybackMode) {
46
+ let oldEngine = self.engine
47
+ // Capture pre-swap state — the engine swap releases the old
48
+ // engine; reading `newEngine.currentPositionSeconds` /
49
+ // `isPlaying` after would always return 0 / false. The
50
+ // wasPlaying capture is what carries playback continuity
51
+ // across the immediate engine swap.
52
+ let resumePosition = oldEngine?.currentPositionSeconds ?? 0
53
+ let resumeIndex = self.currentTrackIndex
54
+ // The transport's intent, not the old engine's audible state: a leg that
55
+ // is buffering, or mid-crossfade with its outgoing item finished, reports
56
+ // not-playing while the consumer is very much still playing. Swapping on
57
+ // that reading silently pauses them.
58
+ let wasPlaying = self.wantsToPlay
59
+ // Anything held back from the old engine goes with it: the new one is
60
+ // installed from the queue model, which already carries every mutation.
61
+ self.engineReconcileDeferred = false
62
+
63
+ let newEngine: PlaybackEngine
64
+ switch active.kind {
65
+ case .gapless:
66
+ newEngine = GaplessEngine()
67
+ case .crossfade:
68
+ // PlaybackModeStateMachine.requireValid guarantees this is
69
+ // non-nil + within [1000, 12000] when `kind == .crossfade`.
70
+ // Fail loud rather than silently degrading to 0 (which would
71
+ // route through CrossfadeEngine's hard-cut path + skip
72
+ // engineCrossfadeDidBegin entirely).
73
+ guard let durationMs = active.crossfadeDurationMs else {
74
+ preconditionFailure("PlaybackMode(crossfade) missing crossfadeDurationMs at engine construction")
75
+ }
76
+ let durationSeconds = durationMs / 1000.0
77
+ newEngine = CrossfadeEngine(
78
+ crossfadeDurationSeconds: durationSeconds, confinedTo: self.playerQueue)
79
+ }
80
+
81
+ self.engine = newEngine
82
+ // GaplessEngine path: keep the established self.player KVO
83
+ // chain (currentItem / timeControlStatus / item.status) as
84
+ // the canonical event source. CrossfadeEngine path:
85
+ // self.player stays nil; the engine fires PlaybackEngineDelegate
86
+ // callbacks for track-change / state-change / format-ready /
87
+ // failed / play-to-end / queue-end and progress is sourced
88
+ // from the engine via a timer-driven fallback.
89
+ if let gapless = newEngine as? GaplessEngine {
90
+ self.player = gapless.player
91
+ self.gaplessFlipArmed = true
92
+ self.installGaplessObservers(on: gapless.player)
93
+ self.installEventObservers(on: gapless.player)
94
+ self.stopProgressFallbackTimer()
95
+ // Gapless does not need the engine delegate — the underlying
96
+ // self.player KVO chain is the canonical event source.
97
+ newEngine.delegate = nil
98
+ } else {
99
+ self.tearDownGaplessObservers()
100
+ self.tearDownEventObservers()
101
+ self.player = nil
102
+ newEngine.delegate = self
103
+ self.startProgressFallbackTimer()
104
+ }
105
+
106
+ // Re-wire the shared audio-tap provider to the new engine's
107
+ // audio-mix provider slot.
108
+ AudioTapProvider.shared.wireToPlaybackEngine(newEngine)
109
+
110
+ // Re-apply canonical config to the freshly-built engine BEFORE the
111
+ // queue install / play — it starts at engine defaults (repeat off,
112
+ // volume 1, speed 1), so without this a gapless↔crossfade swap
113
+ // silently drops the user's settings.
114
+ newEngine.setRepeatMode(self.repeatModeState)
115
+ newEngine.setVolume(self.volumeState)
116
+ // Carry the user's speed into the new engine. When resuming playback,
117
+ // apply it live via `setPlaybackSpeed` (sets `AVPlayer.rate`) BEFORE
118
+ // `setItems` — GaplessEngine.setItems captures the player's rate to
119
+ // restore it across the queue rebuild, so a speed applied after
120
+ // setItems/play() would be clobbered by that async restore. When
121
+ // paused, seed it only (no rate write, so the engine stays paused);
122
+ // play() applies it on the next resume.
123
+ if wasPlaying {
124
+ newEngine.setPlaybackSpeed(self.playbackSpeedState)
125
+ } else {
126
+ newEngine.seedPlaybackSpeed(self.playbackSpeedState)
127
+ }
128
+ newEngine.setPitchCorrectionMode(self.pitchCorrectionModeState)
129
+
130
+ // Re-install the queue on the new engine — the same window every other
131
+ // install uses, not the whole queue: a crossfade engine holds every item
132
+ // it is given, and a long queue would put every track's asset load
133
+ // through the read-through server at once. `topUpWindow` extends it.
134
+ if resumeIndex >= 0, resumeIndex < self.tracks.count {
135
+ let positions = QueueWindowArithmetic.slice(
136
+ currentIndex: resumeIndex,
137
+ trackCount: self.tracks.count,
138
+ trail: self.engineTrail,
139
+ wraps: false
140
+ )
141
+ let sliceIds = positions.map { self.queueItemIds[$0] }
142
+ let items = AVQueueBuilder.buildPlayerItems(
143
+ tracks: positions.map { self.tracks[$0] },
144
+ queueItemIds: sliceIds,
145
+ config: self.config,
146
+ server: self.routingServer
147
+ )
148
+ // Seated the way every other install is; with nothing playable at or
149
+ // after the current position the new engine stays empty rather than
150
+ // replaying the trail.
151
+ let currentId = self.queueItemIds[resumeIndex]
152
+ if let startIndex = Self.seatIndex(in: items, sliceIds: sliceIds, currentId: currentId) {
153
+ // The playhead carries across only onto the track that was playing;
154
+ // any other seat is a track change and starts at zero.
155
+ let position = items[startIndex].queueItemId == currentId ? resumePosition : 0
156
+ newEngine.setItems(
157
+ items,
158
+ startIndex: startIndex,
159
+ startPositionSeconds: position
160
+ )
161
+ // Honour pre-swap playing state so a mid-play setPlaybackMode keeps
162
+ // playing on the new engine — through `beginPlayback`, which every
163
+ // path that starts playback goes through, and not while the session
164
+ // is interrupted.
165
+ if wasPlaying, !self.isInterrupted {
166
+ self.beginPlayback()
167
+ }
168
+ }
169
+ }
170
+
171
+ // Whatever was seated above, the engine being replaced goes: its items
172
+ // stop loading and its observers come down.
173
+ oldEngine?.release()
174
+ }
175
+
176
+ func setPlaybackSpeed(rate: Double) throws -> Promise<Void> {
177
+ return enqueue {
178
+ // Clamp to a 0.25 floor: rate=0 on AVPlayer is functionally equivalent
179
+ // to pause(), which would surprise consumers calling
180
+ // `setPlaybackSpeed(0)` for a slow-down effect. Cross-platform contract
181
+ // documented in TrackPlayer.nitro.ts. A value that is not a number
182
+ // leaves the current rate alone.
183
+ guard !rate.isNaN else { return }
184
+ let safeRate = Float(max(0.25, min(rate, 32)))
185
+ self.playbackSpeedState = safeRate
186
+ // Apply to the LIVE rate only when the user wants playback (already
187
+ // playing, OR buffering toward play). On a paused/idle engine writing
188
+ // `AVPlayer.rate` starts it (rate > 0 == "play"), so with no play intent
189
+ // seed the speed instead (no rate write); `play()` applies it on the next
190
+ // resume. This keeps a persisted speed applied at app start — before a
191
+ // restored `setQueue` — from auto-starting a paused queue. `wantsToPlay`
192
+ // (not just `isPlaying`) covers the buffering-toward-play window so a
193
+ // speed change there still applies live.
194
+ if self.wantsToPlay || self.engine?.isPlaying == true {
195
+ self.engine?.setPlaybackSpeed(safeRate)
196
+ } else {
197
+ self.engine?.seedPlaybackSpeed(safeRate)
198
+ }
199
+ }
200
+ }
201
+ func setPitchCorrectionMode(mode: PitchCorrectionMode) throws -> Promise<Void> {
202
+ return enqueue {
203
+ self.pitchCorrectionModeState = mode
204
+ self.engine?.setPitchCorrectionMode(mode)
205
+ }
206
+ }
207
+ func getPitchCorrectionMode() throws -> PitchCorrectionMode {
208
+ // Lock-guarded — see `getPlaybackMode`.
209
+ return self.pitchCorrectionModeState
210
+ }
211
+ func setVolume(volume: Double) throws -> Promise<Void> {
212
+ return enqueue {
213
+ // Clamped to the documented range before either branch, so the cast and
214
+ // local paths cannot diverge on the same input, and a value that is not
215
+ // a number cannot poison the stored level that `getVolume` and the
216
+ // sleep-timer fade read back.
217
+ guard !volume.isNaN else { return }
218
+ let level = Float(max(0, min(1, volume)))
219
+ if CastTransportRouter.routeSetVolume(value: Double(level)) { return }
220
+ self.volumeState = level
221
+ self.engine?.setVolume(level)
222
+ }
223
+ }
224
+
225
+ func setRepeatMode(mode: RepeatMode) throws -> Promise<Void> {
226
+ return enqueue {
227
+ self.repeatModeState = mode
228
+ // Forward to the active engine. The CrossfadeEngine needs the
229
+ // mode to suppress its look-ahead fade + loop the current item
230
+ // under `.track` (AVPlayer has no native repeat); GaplessEngine
231
+ // treats it as a no-op (repeat-track replay lives in
232
+ // handlePlayerItemDidPlayToEndTime).
233
+ self.engine?.setRepeatMode(mode)
234
+ // Both canSkipNext and canSkipPrevious depend on repeat mode
235
+ // (e.g. at idx 0 / last index, OFF↔QUEUE flips the boolean
236
+ // because QUEUE wraps). Recompute is cheap; dedup keeps the
237
+ // listener silent when the (prev, next) tuple is unchanged.
238
+ // The installed run is forward-only, so a repeat-mode change does not
239
+ // alter which positions it holds and there is nothing to reconcile. That
240
+ // stops being true the moment the run carries the wrap target, and the
241
+ // reconcile has to come back with it.
242
+ self.recomputeCapabilities()
243
+ }
244
+ }
245
+
246
+ func setRemoteControls(options: RemoteControlOptions) throws -> Promise<Void> {
247
+ return enqueue {
248
+ // Guard nonsensical negatives (a negative forward interval would seek
249
+ // backward) and values too large to convert to milliseconds. Out-of-range
250
+ // is clamped rather than rejected.
251
+ let fwd = InputGuards.sanitisedSeekPosition(options.forwardJumpInterval)
252
+ .map { max(0, $0) } ?? 0
253
+ let back = InputGuards.sanitisedSeekPosition(options.backwardJumpInterval)
254
+ .map { max(0, $0) } ?? 0
255
+ self.remoteSkipMode = options.skipMode
256
+ self.remoteForwardInterval = fwd
257
+ self.remoteBackwardInterval = back
258
+ self.remoteCommands.setRemoteControls(
259
+ mode: options.skipMode,
260
+ forwardInterval: fwd,
261
+ backwardInterval: back)
262
+ // Re-apply the cached grey-out: switching to track mode re-enables
263
+ // next/prev, which must reflect the queue-boundary capability
264
+ // (setSkipCapability is otherwise only called on queue/repeat change,
265
+ // and no-ops in interval mode).
266
+ self.remoteCommands.setSkipCapability(
267
+ canSkipNext: self.cachedSkipCapability.canSkipNext,
268
+ canSkipPrevious: self.cachedSkipCapability.canSkipPrevious)
269
+ }
270
+ }
271
+
272
+ // ReplayGain mode — thin wrapper around `AudioTapProvider.shared`.
273
+ // The provider owns the per-item state set + recompute walk; this
274
+ // Hybrid surface is the JS-facing setter / getter only.
275
+ func setReplayGainMode(mode: ReplayGainMode) throws -> Promise<Void> {
276
+ return enqueue {
277
+ AudioTapProvider.shared.setReplayGainMode(mode)
278
+ // Re-emit the active item's NowPlayingFormat so
279
+ // `onNowPlayingFormatChange` subscribers (e.g. the
280
+ // `useNowPlayingFormat` hook) observe the new `replayGainActive`
281
+ // flag without waiting for the next track change. Mirrors the
282
+ // Android path's `refreshNowPlayingFormatForActiveItem` call from
283
+ // its own `setReplayGainMode`.
284
+ guard let item = self.activeMediaItem else { return }
285
+ self.emitCurrentNowPlayingFormat(for: item)
286
+ }
287
+ }
288
+ func getReplayGainMode() throws -> ReplayGainMode {
289
+ return AudioTapProvider.shared.getReplayGainMode()
290
+ }
291
+ }
@@ -0,0 +1,209 @@
1
+ import AVFoundation
2
+ import Foundation
3
+
4
+ // MARK: - PlaybackEngineDelegate
5
+
6
+ /// `CrossfadeEngine` fires these callbacks; `GaplessEngine`'s
7
+ /// equivalent surface continues to flow through `self.player`'s
8
+ /// KVO chain installed by `installGaplessObservers` /
9
+ /// `installEventObservers`. Each delegate method therefore runs
10
+ /// only when the active engine is `CrossfadeEngine` (per the
11
+ /// `delegate = self` wiring in `applyEngineSwapForMode`).
12
+ extension TrackPlayer: PlaybackEngineDelegate {
13
+ func engineStateMaybeChanged(
14
+ _ engine: PlaybackEngine, reason: StateChangeReason?
15
+ ) {
16
+ if let reason {
17
+ // Engine-supplied classified reason takes precedence over
18
+ // any prior staked reason — match the gapless KVO path
19
+ // where pendingStateChangeReason is overwritten by the
20
+ // newer staked reason.
21
+ self.pendingStateChangeReason = reason
22
+ }
23
+ // The leading leg's `timeControlStatus` drives `.buffering` (load /
24
+ // rebuffer) and `reachedQueueEnd` drives `.ended`.
25
+ let computed = PlayerStateDerivation.translate(
26
+ status: engine.timeControlStatus,
27
+ currentIndex: self.currentTrackIndex,
28
+ hasCurrentItem: engine.currentMediaItem != nil,
29
+ reachedQueueEnd: self.reachedQueueEnd
30
+ )
31
+ emitState(computed, reason: reasonForComputedState(computed))
32
+ }
33
+
34
+ func engineDidTransitionTrack(_ engine: PlaybackEngine) {
35
+ // Bring TrackPlayer's currentTrackIndex into sync with the
36
+ // engine's index — CrossfadeEngine.completeCrossfade
37
+ // increments engine.currentMediaItemIndex; on setItems / seek
38
+ // the engine's index is already aligned with the caller's
39
+ // resumeIndex.
40
+ //
41
+ // Crossfade post-swap echo: fade-start already fired
42
+ // `engineCrossfadeDidBegin` which dispatched the JS-facing
43
+ // track-change for the same incoming index. The
44
+ // `lastEmittedQueueItemId` dedup inside `dispatchTrackChange`
45
+ // turns the second call into a no-op. Capability + lookahead
46
+ // refresh still run unconditionally — both are idempotent and
47
+ // the second pass picks up any state that landed during the
48
+ // fade window.
49
+ // Resolve the engine's active item to a queue position by identity.
50
+ // An engine's own index counts within whatever slice it was handed, so
51
+ // it is not a queue position; `queueItemId` is, and it survives the
52
+ // item-replacement paths (cross-attach, cached-URL upgrade).
53
+ //
54
+ // Still skipped during a queue mutation: the mutation already computed
55
+ // the authoritative `currentTrackIndex` via QueueMutationArithmetic, and
56
+ // the engine is mid-rebuild. (CrossfadeEngine.setItems fires this
57
+ // delegate synchronously; GaplessEngine does not.)
58
+ if !self.isMutatingPlayerQueue,
59
+ let resolved = self.matchTrackIndex(forCurrentItem: engine.currentMediaItem) {
60
+ self.currentTrackIndex = resolved
61
+ }
62
+ let reason = self.pendingTrackChangeReason ?? .autoAdvance
63
+ self.pendingTrackChangeReason = nil
64
+ dispatchTrackChange(reason: reason)
65
+ self.recomputeCapabilities()
66
+ self.rescheduleLookahead()
67
+ // Extend the installed run as the playhead advances into it. Driven from
68
+ // the transition rather than from arming, which would force the build into
69
+ // the moment immediately before the ramp is scheduled.
70
+ //
71
+ // Not while a mutation is in flight: this delegate is fired synchronously
72
+ // from `CrossfadeEngine.setItems`, so topping up there would re-enter
73
+ // `setItems` from inside itself — and the mutation that raised the flag
74
+ // installs the run it wants anyway.
75
+ if !self.isMutatingPlayerQueue {
76
+ self.topUpWindow()
77
+ }
78
+ // Refresh the now-playing format for the new active item. The crossfade
79
+ // engine's `\.tracks` KVO is `.new`-only and a forward skip rebuilds the
80
+ // incoming leg as a fresh AVPlayerItem whose tracks key isn't auto-loaded
81
+ // (only playable/duration are), so engineActiveItemFormatChanged may never
82
+ // fire for it — without this, getNowPlayingFormat() stays nil after a
83
+ // skip to a cached track. The extractor loads the format asynchronously
84
+ // and the lastEmittedFormatItemId dedup makes a redundant call a no-op.
85
+ self.refreshNowPlayingFormatForActiveItem()
86
+ }
87
+
88
+ func engineCrossfadeDidBegin(_ engine: PlaybackEngine) {
89
+ // Fade-start: the standby leg is now audibly playing (volume 0
90
+ // ramping up). Flip JS-facing active track + lock-screen / Now
91
+ // Playing surface to the incoming track immediately so the
92
+ // user sees the new track's title/artwork the moment they
93
+ // start hearing it. The leading leg continues fading out for
94
+ // the configured `crossfadeDurationMs`; engine accessors
95
+ // already prefer the standby leg while `fadeInFlight` is true.
96
+ //
97
+ // Reason is hardcoded `.autoAdvance` — fade-start is always an
98
+ // auto-advance event; any user-driven transition (skipToNext /
99
+ // skipToIndex / setMediaItems) would have called
100
+ // `cancelFadeInternal` first, so a stashed
101
+ // `pendingTrackChangeReason` here is stale and should not
102
+ // re-attribute the fade.
103
+ // A queue mutation already computed the authoritative index; the
104
+ // rebuild's own fade-cancel must not walk it back to the outgoing leg.
105
+ guard !self.isMutatingPlayerQueue,
106
+ let incoming = self.matchTrackIndex(forCurrentItem: engine.currentMediaItem)
107
+ else { return }
108
+ self.currentTrackIndex = incoming
109
+ self.pendingTrackChangeReason = nil
110
+ dispatchTrackChange(reason: .autoAdvance)
111
+ self.recomputeCapabilities()
112
+ self.rescheduleLookahead()
113
+ }
114
+
115
+ func engineCrossfadeDidCancel(_ engine: PlaybackEngine) {
116
+ // Fade-cancelled mid-flight (pause / stop / focus loss /
117
+ // queue mutation). JS already received `onTrackChange(incoming,
118
+ // reason: .autoAdvance)` from the prior fade-start fire; the
119
+ // incoming track is no longer the active one. Revert
120
+ // `currentTrackIndex` to the leading-leg index and emit
121
+ // `onTrackChange(leading, reason: .crossfadeCancelled)` so
122
+ // consumer UI can rewind to the leading track.
123
+ // See `engineCrossfadeDidBegin` — a rebuild issued by a queue mutation
124
+ // cancels the in-flight fade, and that cancel must not clobber the
125
+ // index the mutation just computed.
126
+ guard !self.isMutatingPlayerQueue,
127
+ let reverted = self.matchTrackIndex(forCurrentItem: engine.currentMediaItem)
128
+ else { return }
129
+ self.currentTrackIndex = reverted
130
+ self.pendingTrackChangeReason = nil
131
+ dispatchTrackChange(reason: .crossfadeCancelled)
132
+ self.recomputeCapabilities()
133
+ self.rescheduleLookahead()
134
+ }
135
+
136
+ /// Apply whatever was held back while the fade ran.
137
+ ///
138
+ /// Fired for every end of a fade, which is the point: a cancel taken during
139
+ /// the arm window reverts nothing and so reports nothing else, and work
140
+ /// waiting on that fade would otherwise wait for one that is already over —
141
+ /// with the engine still holding a run the queue has retired.
142
+ func engineFadeDidSettle(_ engine: PlaybackEngine) {
143
+ _ = engine
144
+ self.flushDeferredEngineWork()
145
+ }
146
+
147
+ func engineBufferStateMaybeChanged(_ engine: PlaybackEngine) {
148
+ _ = engine
149
+ // Buffer surface only — no direct player-state emission. The crossfade
150
+ // leading leg's buffer-flag KVO lands here; recompute the discrete
151
+ // buffer-state and fully-buffered surfaces, exactly as the gapless item KVO
152
+ // does through `installBufferObservers` (the stall-recovery restart inside
153
+ // `recomputeBufferState` still applies here, as on gapless). Already on
154
+ // `playerQueue` (the engine's ownerQueue), so recompute synchronously — a
155
+ // second async hop would reorder against the recompute's change dedup.
156
+ self.recomputeBufferState()
157
+ self.recomputeFullyBuffered()
158
+ }
159
+
160
+ func engineActiveItemFormatChanged(_ engine: PlaybackEngine) {
161
+ _ = engine
162
+ self.refreshNowPlayingFormatForActiveItem()
163
+ // Streaming-item audioMix catch-up for the crossfade engine
164
+ // (which fires this delegate hook on its own KVO chain). The
165
+ // gapless engine has a parallel catch-up path through
166
+ // `dispatchItemStatus(.readyToPlay)`; both call into
167
+ // `refreshActiveItemMixes`, which is idempotent on items that
168
+ // already carry a tap, so a double-fire is harmless.
169
+ AudioTapProvider.shared.refreshActiveItemMixes()
170
+ }
171
+
172
+ func engineDidFailWithError(
173
+ _ engine: PlaybackEngine, error: Error, item: AVPlayerItem?
174
+ ) {
175
+ _ = engine
176
+ let underlying = error as NSError
177
+ // The item is forwarded, not dropped: without it `dispatchErrorOrRetry`
178
+ // synthesises a key that cannot appear in `queueItemIds`, and the
179
+ // automatic-retry branch requires a real `queueItemId`.
180
+ dispatchErrorOrRetry(
181
+ item: item,
182
+ underlying: underlying,
183
+ nativeDomainOverride: nil
184
+ )
185
+ }
186
+
187
+ func engineDidPlayItemToEnd(_ engine: PlaybackEngine) {
188
+ // CrossfadeEngine fires this on natural end-of-leading; the
189
+ // boundary signal that drives JS-facing track-change is
190
+ // engineDidTransitionTrack which fires immediately after.
191
+ //
192
+ // Under repeat-one the same track replays with no track-change (the
193
+ // queueItemId is unchanged, so dispatchTrackChange's dedup skips its
194
+ // reset). Each loop is a fresh playthrough, so re-arm the milestone
195
+ // tracker here for that case only.
196
+ if self.repeatModeState == .track {
197
+ self.milestoneTracker.reset()
198
+ }
199
+ _ = engine
200
+ }
201
+
202
+ func enginePlaybackEnded(_ engine: PlaybackEngine) {
203
+ _ = engine
204
+ // The engine reports that it drained; what that means for the queue —
205
+ // wrap, refill, or over — is decided above it, the same way for both
206
+ // engines.
207
+ applyEndVerdict()
208
+ }
209
+ }
@@ -0,0 +1,111 @@
1
+ import Foundation
2
+
3
+ extension TrackPlayer {
4
+ // MARK: - Events — public registration surface
5
+ //
6
+ // Each event has a multi-listener `ListenerRegistry`. Each `onXxx`
7
+ // call appends a new entry; the returned closure removes that entry
8
+ // by stable id. ListenerRegistry's internal queue.sync makes
9
+ // add/remove safe against concurrent fire rounds — fire iterates a
10
+ // snapshot taken under the lock.
11
+
12
+ func onStateChange(
13
+ callback: @escaping (_ state: PlayerState, _ reason: StateChangeReason) -> Void
14
+ ) throws -> () -> Void {
15
+ let id = stateChangeListeners.add(callback)
16
+ return { [weak self] in self?.stateChangeListeners.remove(id: id) }
17
+ }
18
+
19
+ func onTrackChange(
20
+ callback: @escaping (_ track: TrackItem?, _ index: Double, _ reason: TrackChangeReason, _ transitionGapMs: Double?) -> Void
21
+ ) throws -> () -> Void {
22
+ let id = trackChangeListeners.add(callback)
23
+ return { [weak self] in self?.trackChangeListeners.remove(id: id) }
24
+ }
25
+
26
+ func onQueueChange(
27
+ callback: @escaping (_ delta: QueueChangeDelta, _ currentIndex: Double, _ reason: QueueChangeReason) -> Void
28
+ ) throws -> () -> Void {
29
+ let id = queueChangeListeners.add(callback)
30
+ return { [weak self] in self?.queueChangeListeners.remove(id: id) }
31
+ }
32
+
33
+ /// Emit a queue change as a description of what happened rather than a copy
34
+ /// of the queue. Callers pass only the fields their mutation defines; the
35
+ /// rest stay empty, and `revision` lets a subscriber notice a dropped event.
36
+ internal func emitQueueChange(
37
+ _ reason: QueueChangeReason,
38
+ inserted: [TrackItem] = [],
39
+ insertedAt: Int = -1,
40
+ removed: [Int] = [],
41
+ movedFrom: Int = -1,
42
+ movedTo: Int = -1,
43
+ order: [Int] = []
44
+ ) {
45
+ self.queueRevision += 1
46
+ let delta = QueueChangeDelta(
47
+ revision: Double(self.queueRevision),
48
+ length: Double(self.tracks.count),
49
+ inserted: inserted,
50
+ insertedAt: Double(insertedAt),
51
+ removed: removed.map(Double.init),
52
+ movedFrom: Double(movedFrom),
53
+ movedTo: Double(movedTo),
54
+ order: order.map(Double.init)
55
+ )
56
+ let index = Double(self.currentTrackIndex)
57
+ queueChangeListeners.forEach { $0(delta, index, reason) }
58
+ }
59
+
60
+ func onProgress(
61
+ callback: @escaping (_ progress: PlayerProgress) -> Void
62
+ ) throws -> () -> Void {
63
+ let id = progressListeners.add(callback)
64
+ return { [weak self] in self?.progressListeners.remove(id: id) }
65
+ }
66
+
67
+ func onPlaybackMilestone(
68
+ callback: @escaping (_ milestone: Double, _ trackIndex: Double) -> Void
69
+ ) throws -> () -> Void {
70
+ let id = milestoneListeners.add(callback)
71
+ return { [weak self] in self?.milestoneListeners.remove(id: id) }
72
+ }
73
+
74
+ func onError(callback: @escaping (_ error: PlaybackError) -> Void) throws -> () -> Void {
75
+ let id = errorListeners.add(callback)
76
+ return { [weak self] in self?.errorListeners.remove(id: id) }
77
+ }
78
+
79
+ func onQueueEnd(callback: @escaping () -> Void) throws -> () -> Void {
80
+ let id = queueEndListeners.add(callback)
81
+ return { [weak self] in self?.queueEndListeners.remove(id: id) }
82
+ }
83
+
84
+ /// Subscribe to skip-capability changes. Auto-fires the callback
85
+ /// once on registration with the current cached values (mirrors
86
+ /// Media3 `Player.Listener.onAvailableCommandsChanged`) so new
87
+ /// subscribers don't have to seed via the getters first. The
88
+ /// auto-fire hops to `playerQueue` so the cache read is on the
89
+ /// queue that writes it. Subsequent fires happen synchronously
90
+ /// from `recomputeCapabilities()`, also on `playerQueue`.
91
+ ///
92
+ /// **Dispose race guard:** the deferred fire checks the
93
+ /// subscriber is still registered (`contains(id:)`) before
94
+ /// invoking the callback. Strict-mode subscribe → dispose →
95
+ /// subscribe sequences fire the cleanup before the deferred fire
96
+ /// runs; without the guard, the auto-fire would call into an
97
+ /// already-unmounted React component's `setSnapshot`.
98
+ func onSkipCapabilityChange(
99
+ callback: @escaping (_ capability: SkipCapability) -> Void
100
+ ) throws -> () -> Void {
101
+ let id = skipCapabilityListeners.add(callback)
102
+ playerQueue.async { [weak self] in
103
+ guard let self else { return }
104
+ guard self.skipCapabilityListeners.contains(id: id) else { return }
105
+ callback(SkipCapability(
106
+ canSkipPrevious: self.cachedSkipCapability.canSkipPrevious,
107
+ canSkipNext: self.cachedSkipCapability.canSkipNext))
108
+ }
109
+ return { [weak self] in self?.skipCapabilityListeners.remove(id: id) }
110
+ }
111
+ }