react-native-queue-player 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/build.gradle +7 -0
  4. package/android/consumer-rules.pro +72 -3
  5. package/android/src/main/cpp/airplay2_control.c +4 -4
  6. package/android/src/main/cpp/airplay2_control.h +4 -4
  7. package/android/src/main/cpp/airplay2_jni.cpp +26 -22
  8. package/android/src/main/cpp/airplay2_pair.c +7 -7
  9. package/android/src/main/cpp/airplay2_pair.h +2 -2
  10. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  11. package/android/src/main/cpp/airplay2_session.c +2 -2
  12. package/android/src/main/cpp/airplay_jni.cpp +20 -72
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
  61. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  62. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  63. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  64. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  65. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
  114. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
  115. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  116. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  117. package/app.plugin.js +17 -1
  118. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  119. package/ios/AVQueueBuilder.swift +120 -85
  120. package/ios/ArtworkLoader.swift +0 -7
  121. package/ios/ArtworkResolver.swift +22 -4
  122. package/ios/AssetReplayGainReader.swift +118 -0
  123. package/ios/AudioSession.swift +67 -16
  124. package/ios/AudioTapProvider.swift +284 -149
  125. package/ios/CarPlayBridge.swift +0 -12
  126. package/ios/CarPlayCoordinator.swift +18 -2
  127. package/ios/CarPlaySceneDelegate.swift +2 -2
  128. package/ios/Cast/AirPlayRouteState.swift +9 -8
  129. package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
  130. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  131. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  132. package/ios/Cast/Core/CastSession.swift +11 -0
  133. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  134. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  135. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  136. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  137. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  138. package/ios/CastManager.swift +1 -1
  139. package/ios/CrossfadeEngine.swift +660 -226
  140. package/ios/EQTap.swift +20 -17
  141. package/ios/Equalizer.swift +1 -1
  142. package/ios/FLACStreamInfo.swift +65 -0
  143. package/ios/GaplessEngine.swift +57 -30
  144. package/ios/InputGuards.swift +35 -5
  145. package/ios/LookaheadCache.swift +202 -58
  146. package/ios/LookaheadCachePrefetcher.swift +48 -36
  147. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  148. package/ios/NetworkRecoveryPolicy.swift +32 -0
  149. package/ios/NowPlayingFormatExtractor.swift +25 -18
  150. package/ios/NowPlayingInfo.swift +84 -48
  151. package/ios/OutputRouteMonitor.swift +16 -3
  152. package/ios/PendingBrowseRequests.swift +1 -1
  153. package/ios/PlaceholderArtwork.swift +9 -5
  154. package/ios/PlaybackEngine.swift +165 -76
  155. package/ios/PlaybackErrorMapping.swift +3 -3
  156. package/ios/PlaybackModeStateMachine.swift +2 -2
  157. package/ios/PlaybackNetworkMonitor.swift +73 -0
  158. package/ios/PlayerStateDerivation.swift +6 -6
  159. package/ios/QueueMutationArithmetic.swift +25 -0
  160. package/ios/QueueSkipArithmetic.swift +5 -3
  161. package/ios/QueueWindowArithmetic.swift +152 -0
  162. package/ios/ReadThroughServer.swift +283 -0
  163. package/ios/RemoteCommands.swift +44 -4
  164. package/ios/ReplayGainData.swift +22 -3
  165. package/ios/ReplayGainExtractor.swift +70 -27
  166. package/ios/Siri/VoiceDonation.swift +14 -9
  167. package/ios/StreamingBitrateProbe.swift +22 -68
  168. package/ios/SuppliedReplayGain.swift +83 -0
  169. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  170. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  171. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  172. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  173. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  174. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  175. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  176. package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
  177. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  178. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  179. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  180. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  181. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  182. package/ios/Tests/EngineSwapTests.swift +91 -0
  183. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  184. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  185. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  186. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  187. package/ios/Tests/InputGuardsTests.swift +41 -0
  188. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  189. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  190. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  191. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  192. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  193. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  194. package/ios/Tests/MutationDeferralTests.swift +220 -0
  195. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  196. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  197. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  198. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  199. package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
  200. package/ios/Tests/PlayerFixtures.swift +48 -0
  201. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  202. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  203. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  204. package/ios/Tests/QueueStateTests.swift +3 -1
  205. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  206. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  207. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  208. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  209. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  210. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  211. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  212. package/ios/Tests/RetryRecoveryTests.swift +303 -0
  213. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  214. package/ios/Tests/SkipIndexTests.swift +32 -2
  215. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  216. package/ios/Tests/StallRecoveryTests.swift +97 -0
  217. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  218. package/ios/Tests/TopUpWindowGateTests.swift +394 -0
  219. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  220. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  221. package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
  222. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  223. package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
  224. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  225. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  226. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  227. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  228. package/ios/TrackPlayer+Automotive.swift +101 -0
  229. package/ios/TrackPlayer+Cache.swift +232 -0
  230. package/ios/TrackPlayer+Config.swift +291 -0
  231. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  232. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  233. package/ios/TrackPlayer+EventsDispatch.swift +968 -0
  234. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  235. package/ios/TrackPlayer+Lifecycle.swift +930 -0
  236. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  237. package/ios/TrackPlayer+Queue.swift +940 -0
  238. package/ios/TrackPlayer+Recovery.swift +196 -0
  239. package/ios/TrackPlayer+Skip.swift +455 -0
  240. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  241. package/ios/TrackPlayer+State.swift +108 -0
  242. package/ios/TrackPlayer+Threading.swift +135 -0
  243. package/ios/TrackPlayer+Transport.swift +267 -0
  244. package/ios/TrackPlayer+Window.swift +195 -0
  245. package/ios/TrackPlayer.swift +305 -4610
  246. package/ios/Visualizer.swift +6 -5
  247. package/ios/tests-harness/Podfile +1 -1
  248. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  249. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  250. package/lib/module/hooks/useActiveTrack.js +29 -22
  251. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  252. package/lib/module/hooks/useCast.js +8 -26
  253. package/lib/module/hooks/useCast.js.map +1 -1
  254. package/lib/module/hooks/useEqualizer.js +19 -12
  255. package/lib/module/hooks/useEqualizer.js.map +1 -1
  256. package/lib/module/hooks/useLookaheadCache.js +3 -7
  257. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  258. package/lib/module/hooks/useQueue.js +66 -19
  259. package/lib/module/hooks/useQueue.js.map +1 -1
  260. package/lib/module/index.js +6 -3
  261. package/lib/module/index.js.map +1 -1
  262. package/lib/module/queueDelta.js +41 -0
  263. package/lib/module/queueDelta.js.map +1 -0
  264. package/lib/module/types.js +28 -5
  265. package/lib/module/types.js.map +1 -1
  266. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  267. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  269. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useCast.d.ts +6 -1
  271. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  272. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  273. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  274. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  275. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  276. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  277. package/lib/typescript/index.d.ts +2 -1
  278. package/lib/typescript/index.d.ts.map +1 -1
  279. package/lib/typescript/queueDelta.d.ts +10 -0
  280. package/lib/typescript/queueDelta.d.ts.map +1 -0
  281. package/lib/typescript/types.d.ts +168 -12
  282. package/lib/typescript/types.d.ts.map +1 -1
  283. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  284. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  285. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  286. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  287. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  288. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  289. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  320. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  321. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  322. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  323. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  324. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  325. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  326. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  327. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  328. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  329. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  330. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  331. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  332. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  333. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  334. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  335. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  336. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  337. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  338. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  339. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  340. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  341. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  342. package/package.json +8 -8
  343. package/src/TrackPlayer.nitro.ts +53 -27
  344. package/src/hooks/useActiveTrack.ts +29 -22
  345. package/src/hooks/useCast.ts +14 -15
  346. package/src/hooks/useEqualizer.ts +19 -12
  347. package/src/hooks/useLookaheadCache.ts +3 -7
  348. package/src/hooks/useQueue.ts +66 -17
  349. package/src/index.ts +13 -3
  350. package/src/queueDelta.ts +41 -0
  351. package/src/types.ts +169 -12
  352. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  353. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
  354. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  355. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  356. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  357. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  358. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  359. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  360. package/ios/MetadataReader.swift +0 -555
  361. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  362. package/ios/Tests/MetadataReaderTests.swift +0 -603
  363. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  364. package/ios/Tests/VoiceDonationTests.swift +0 -26
  365. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  366. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  367. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -0,0 +1,175 @@
1
+ import Foundation
2
+ import NitroModules
3
+
4
+ extension TrackPlayer {
5
+ func setSleepTimer(seconds: Double) throws -> Promise<Void> {
6
+ return enqueue { self.setSleepTimerInternal(seconds: seconds) }
7
+ }
8
+ func setSleepTimerToTrackEnd() throws -> Promise<Void> {
9
+ return enqueue { self.armSleepTimerToTrackEndInternal() }
10
+ }
11
+ func clearSleepTimer() throws -> Promise<Void> {
12
+ return enqueue { self.clearSleepTimerInternal() }
13
+ }
14
+ func getSleepTimer() throws -> SleepTimerState {
15
+ return onPlayerQueueSync { self.sleepTimerSnapshot() }
16
+ }
17
+ func onSleepTimerChange(
18
+ callback: @escaping (_ state: SleepTimerState) -> Void
19
+ ) throws -> () -> Void {
20
+ let id = sleepTimerListeners.add(callback)
21
+ playerQueue.async { [weak self] in
22
+ guard let self else { return }
23
+ guard self.sleepTimerListeners.contains(id: id) else { return }
24
+ callback(self.sleepTimerSnapshot())
25
+ }
26
+ return { [weak self] in _ = self?.sleepTimerListeners.remove(id: id) }
27
+ }
28
+
29
+ // MARK: - Sleep timer — internal
30
+
31
+ /// Arm a wall-clock duration timer; a non-positive value clears any timer.
32
+ private func setSleepTimerInternal(seconds: Double) {
33
+ guard seconds.isFinite, seconds > 0 else {
34
+ clearSleepTimerInternal()
35
+ return
36
+ }
37
+ restoreSleepTimerVolume()
38
+ sleepTimerCore.armDuration(seconds: seconds, nowMs: Self.nowEpochMs())
39
+ startSleepTimerTick()
40
+ emitSleepTimerChanged()
41
+ }
42
+
43
+ private func armSleepTimerToTrackEndInternal() {
44
+ restoreSleepTimerVolume()
45
+ sleepTimerCore.armEndOfTrack()
46
+ startSleepTimerTick()
47
+ emitSleepTimerChanged()
48
+ }
49
+
50
+ private func clearSleepTimerInternal() {
51
+ let wasActive = sleepTimerCore.isActive
52
+ restoreSleepTimerVolume()
53
+ sleepTimerCore.clear()
54
+ stopSleepTimerTick()
55
+ if wasActive { emitSleepTimerChanged() }
56
+ }
57
+
58
+ private func sleepTimerSnapshot() -> SleepTimerState {
59
+ let now = Self.nowEpochMs()
60
+ return SleepTimerState(
61
+ active: sleepTimerCore.isActive,
62
+ endsAtEpochMs: sleepTimerCore.deadlineEpochMs.map { Double($0) },
63
+ remainingSeconds: sleepTimerCore.remainingSeconds(nowMs: now).map { Double($0) },
64
+ endOfTrack: sleepTimerCore.isEndOfTrack)
65
+ }
66
+
67
+ private func emitSleepTimerChanged() {
68
+ let snapshot = sleepTimerSnapshot()
69
+ sleepTimerListeners.forEach { $0(snapshot) }
70
+ }
71
+
72
+ private func startSleepTimerTick() {
73
+ guard sleepTimerTimer == nil else { return }
74
+ let timer = DispatchSource.makeTimerSource(queue: playerQueue)
75
+ timer.schedule(
76
+ deadline: .now() + Self.sleepTimerTickInterval,
77
+ repeating: Self.sleepTimerTickInterval)
78
+ timer.setEventHandler { [weak self] in
79
+ self?.onSleepTimerTick()
80
+ }
81
+ sleepTimerTimer = timer
82
+ // The source is resumed exactly once and never suspended — a suspended
83
+ // source traps libdispatch on release.
84
+ timer.resume()
85
+ }
86
+
87
+ /// Caller must be on `playerQueue` — cancelling from the source's own
88
+ /// queue is what guarantees no further tick.
89
+ internal func stopSleepTimerTick() {
90
+ sleepTimerTimer?.cancel()
91
+ sleepTimerTimer = nil
92
+ }
93
+
94
+ private func onSleepTimerTick() {
95
+ // While casting, the local engine is intentionally frozen (its position
96
+ // never advances), so feed the state machine the receiver's position /
97
+ // duration instead — otherwise the end-of-track tail rule never trips and
98
+ // `.endOfTrack` stays idle forever.
99
+ let durationSec: Double
100
+ let positionSec: Double
101
+ if let remote = PlaybackStateRouter.shared.activeRemote() {
102
+ let d = Double(remote.lastDurationMs) / 1000.0
103
+ let p = Double(remote.lastPositionMs) / 1000.0
104
+ durationSec = (d.isFinite && d > 0) ? d : 0
105
+ positionSec = (p.isFinite && p > 0) ? p : 0
106
+ } else {
107
+ let duration = self.engine?.currentDurationSeconds ?? 0
108
+ let position = self.engine?.currentPositionSeconds ?? 0
109
+ durationSec = (duration.isFinite && duration > 0) ? duration : 0
110
+ positionSec = (position.isFinite && position > 0) ? position : 0
111
+ }
112
+ let result = sleepTimerCore.tick(
113
+ nowMs: Self.nowEpochMs(),
114
+ trackDurationSec: durationSec,
115
+ trackPositionSec: positionSec)
116
+ applySleepTimerResult(result)
117
+ }
118
+
119
+ /// Apply a `SleepTimerCore.Tick` decision to the engine — fade, state emit,
120
+ /// and the boundary pause. Shared by the polling tick and the real track-end
121
+ /// signal (`handlePlayerItemDidPlayToEndTime`).
122
+ internal func applySleepTimerResult(_ result: SleepTimerCore.Tick) {
123
+ applySleepTimerFade(result.fadeFraction)
124
+ if result.stateChanged { emitSleepTimerChanged() }
125
+ if result.pauseNow {
126
+ // Route the boundary pause to the receiver when casting (the receiver
127
+ // emits its own paused state); only the local-engine pause needs the
128
+ // sleep-timer reason tag.
129
+ if !CastTransportRouter.routePause() {
130
+ self.pendingStateChangeReason = .sleepTimer
131
+ // The timer's pause is a transport intent like the user's: without
132
+ // clearing these, a drain that follows (the timer firing at a track
133
+ // end on the last installed item) or a buffer refill would restart
134
+ // playback the timer just stopped.
135
+ self.wantsToPlay = false
136
+ self.isRecoveringFromStall = false
137
+ self.engine?.pause()
138
+ }
139
+ }
140
+ if !sleepTimerCore.isActive { stopSleepTimerTick() }
141
+ }
142
+
143
+ /// Fade multiplies off `volumeState` without mutating it, so `getVolume`
144
+ /// stays truthful and the pre-fade level is restored on clear/fire.
145
+ private func applySleepTimerFade(_ fraction: Double) {
146
+ // Fade is local-only: the receiver's volume is user/system-owned and
147
+ // `volumeState` tracks the local level, so ramping it on the receiver would
148
+ // clobber the cast volume. When casting the boundary pause still routes to
149
+ // the receiver; the pre-pause fade is skipped. Undo any in-progress local
150
+ // fade first so a cast session appearing mid-fade can't strand the local
151
+ // engine at a ramped-down volume for a later cast→local return.
152
+ if PlaybackStateRouter.shared.activeRemote() != nil {
153
+ restoreSleepTimerVolume()
154
+ return
155
+ }
156
+ if fraction < 1.0 {
157
+ self.engine?.setVolume(Float(Double(self.volumeState) * fraction))
158
+ sleepTimerFading = true
159
+ } else if sleepTimerFading {
160
+ self.engine?.setVolume(self.volumeState)
161
+ sleepTimerFading = false
162
+ }
163
+ }
164
+
165
+ private func restoreSleepTimerVolume() {
166
+ if sleepTimerFading {
167
+ self.engine?.setVolume(self.volumeState)
168
+ sleepTimerFading = false
169
+ }
170
+ }
171
+
172
+ private static func nowEpochMs() -> Int64 {
173
+ Int64(Date().timeIntervalSince1970 * 1000)
174
+ }
175
+ }
@@ -0,0 +1,108 @@
1
+ import Foundation
2
+
3
+ extension TrackPlayer {
4
+ // MARK: - State snapshots
5
+ //
6
+ // Nitro calls these getters on a JS worker thread. Player state lives
7
+ // on `playerQueue`, so every read hops there with `onPlayerQueueSync`,
8
+ // which runs the body inline when the caller is already on that queue
9
+ // (a `sync` from the queue onto itself would deadlock). The cached
10
+ // `canSkip*` Bools are written on `playerQueue` by
11
+ // `recomputeCapabilities()`, so the same hop reads them consistently.
12
+ func getState() throws -> PlayerState {
13
+ // The receiver owns playback during a cast session — the local engine is
14
+ // parked and paused for its duration, so its derived state describes
15
+ // nothing the user can hear. Until the receiver has reported (session
16
+ // still activating, holding its initial `.idle`) the local read is the
17
+ // better answer; after that the receiver is authoritative, including a
18
+ // later `.idle`, which means it genuinely has nothing loaded.
19
+ if let remote = PlaybackStateRouter.shared.activeRemote(), remote.hasReportedState {
20
+ return remote.lastState.asPlayerState
21
+ }
22
+ return onPlayerQueueSync {
23
+ // Both engines derive PlayerState from the same inputs via
24
+ // `engine.timeControlStatus` (gapless returns its AVQueuePlayer's, which
25
+ // is what `self.player` also points at; crossfade returns the active
26
+ // leg's), so gapless + crossfade report identical states. `.none` when no
27
+ // engine is configured.
28
+ guard let engine = self.engine else { return .none }
29
+ return PlayerStateDerivation.translate(
30
+ status: engine.timeControlStatus,
31
+ currentIndex: self.currentTrackIndex,
32
+ hasCurrentItem: engine.currentMediaItem != nil,
33
+ reachedQueueEnd: self.reachedQueueEnd
34
+ )
35
+ }
36
+ }
37
+ func getCurrentTrackIndex() throws -> Double {
38
+ // Nitrogen spec types this return as Double; bridge the internal Int.
39
+ return onPlayerQueueSync { Double(self.currentTrackIndex) }
40
+ }
41
+ func getCurrentTrackSource() throws -> TrackSource? {
42
+ return onPlayerQueueSync { self.currentTrackSource }
43
+ }
44
+ func getCanSkipNext() throws -> Bool {
45
+ return onPlayerQueueSync { self.cachedSkipCapability.canSkipNext }
46
+ }
47
+ func getSkipCapability() throws -> SkipCapability {
48
+ return onPlayerQueueSync { self.cachedSkipCapability }
49
+ }
50
+ func getCanSkipPrevious() throws -> Bool {
51
+ return onPlayerQueueSync { self.cachedSkipCapability.canSkipPrevious }
52
+ }
53
+ func getPlaybackSpeed() throws -> Double {
54
+ return onPlayerQueueSync { Double(self.playbackSpeedState) }
55
+ }
56
+ func getVolume() throws -> Double {
57
+ return onPlayerQueueSync { Double(self.volumeState) }
58
+ }
59
+ func getRemoteControls() throws -> RemoteControlOptions {
60
+ return onPlayerQueueSync {
61
+ let options: RemoteControlOptions = .init(
62
+ skipMode: self.remoteSkipMode,
63
+ forwardJumpInterval: self.remoteForwardInterval,
64
+ backwardJumpInterval: self.remoteBackwardInterval)
65
+ return options
66
+ }
67
+ }
68
+
69
+ func getBufferState() throws -> BufferState {
70
+ return onPlayerQueueSync { self.currentBufferState }
71
+ }
72
+
73
+ func getIsSeekable() throws -> Bool {
74
+ return onPlayerQueueSync {
75
+ guard let item = self.engine?.currentMediaItem else { return true }
76
+ return !item.seekableTimeRanges.isEmpty
77
+ }
78
+ }
79
+
80
+ func isFullyBuffered() throws -> Bool {
81
+ return onPlayerQueueSync { self.currentFullyBuffered }
82
+ }
83
+
84
+ func onBufferStateChange(
85
+ callback: @escaping (_ state: BufferState) -> Void
86
+ ) throws -> () -> Void {
87
+ let id = bufferStateListeners.add(callback)
88
+ // Fire once on subscribe with the current state so a UI renders immediately.
89
+ playerQueue.async { [weak self] in
90
+ guard let self else { return }
91
+ guard self.bufferStateListeners.contains(id: id) else { return }
92
+ callback(self.currentBufferState)
93
+ }
94
+ return { [weak self] in self?.bufferStateListeners.remove(id: id) }
95
+ }
96
+
97
+ func onFullyBufferedChange(
98
+ callback: @escaping (_ fullyBuffered: Bool) -> Void
99
+ ) throws -> () -> Void {
100
+ let id = fullyBufferedListeners.add(callback)
101
+ playerQueue.async { [weak self] in
102
+ guard let self else { return }
103
+ guard self.fullyBufferedListeners.contains(id: id) else { return }
104
+ callback(self.currentFullyBuffered)
105
+ }
106
+ return { [weak self] in self?.fullyBufferedListeners.remove(id: id) }
107
+ }
108
+ }
@@ -0,0 +1,135 @@
1
+ import Foundation
2
+ import NitroModules
3
+
4
+ extension TrackPlayer {
5
+ /// True when the caller is already executing on `playerQueue`.
6
+ ///
7
+ /// `DispatchQueue.sync` runs its body on the calling thread, so a thread
8
+ /// identity test answers the wrong question — it reports "main" for work
9
+ /// serialised on the queue and would let a nested `sync` deadlock. The
10
+ /// queue's own specific travels with the execution context instead.
11
+ internal var isOnPlayerQueue: Bool {
12
+ DispatchQueue.getSpecific(key: playerQueueKey) == ObjectIdentifier(self)
13
+ }
14
+
15
+ /// Claim the audio session, then start the engine.
16
+ ///
17
+ /// Every path that starts playback goes through here so activation cannot be
18
+ /// forgotten at a new one. `activateIfNeeded` is idempotent, so the cost
19
+ /// after the first play is a bool check.
20
+ ///
21
+ /// Activation is deliberately not done at `configure()`: that would stop
22
+ /// whatever else the user is listening to the moment a consumer app
23
+ /// launches, before they have pressed anything.
24
+ internal func beginPlayback() {
25
+ audioSession.activateIfNeeded()
26
+ engine?.play()
27
+ }
28
+
29
+ /// Enqueue `body` on `playerQueue` **at call time**, on the calling thread,
30
+ /// and resolve the returned promise when it completes.
31
+ ///
32
+ /// This is what makes the queue's FIFO order equal the JS call order.
33
+ /// `Promise.async` starts an unstructured `Task`, and two Tasks created back
34
+ /// to back have no ordering relationship with each other — so a body that
35
+ /// hops onto the queue *inside* the Task reaches the player at an arbitrary
36
+ /// later point. Two calls issued in order from JS could then arrive
37
+ /// reversed: `play()` then `pause()` landing as pause-then-play, leaving
38
+ /// audio running after the user paused it.
39
+ ///
40
+ /// A serial queue gives mutual exclusion, not ordering. Ordering comes from
41
+ /// when the work is enqueued, which is why this runs before returning.
42
+ internal func enqueue(_ body: @escaping () -> Void) -> Promise<Void> {
43
+ let promise = Promise<Void>()
44
+ playerQueue.async {
45
+ body()
46
+ promise.resolve()
47
+ }
48
+ return promise
49
+ }
50
+
51
+ /// Enqueue a mutation at call time and hand its result to a receiver mirror
52
+ /// that runs off the queue.
53
+ ///
54
+ /// The enqueue is what fixes ordering against other player work — a later
55
+ /// call cannot overtake this one. Doing it with `async` rather than `sync`
56
+ /// matters just as much: the mutation can rebuild the whole player queue,
57
+ /// and `sync` would run that on the JS thread.
58
+ ///
59
+ /// `mirror` awaits the receiver afterwards, holding neither the queue nor
60
+ /// the calling thread, and is chained behind the previous mutation's mirror
61
+ /// so the receiver sees mutations in the order they were made. The promise
62
+ /// resolves once the mirror returns, so a consumer awaiting the call knows
63
+ /// local and remote are both settled.
64
+ internal func enqueueThenMirror<T>(
65
+ _ body: @escaping () -> T,
66
+ mirror: @escaping (T, _ generation: UInt64) async -> Void
67
+ ) -> Promise<Void> {
68
+ let promise = Promise<Void>()
69
+ playerQueue.async {
70
+ self.queueMutationGeneration &+= 1
71
+ let generation = self.queueMutationGeneration
72
+ let value = body()
73
+ let previous = self.castMirrorChain
74
+ self.castMirrorChain = Task {
75
+ // Deltas are only correct against a receiver that has applied
76
+ // everything before them, so this waits rather than racing. A failed
77
+ // mirror still completes its Task, so one bad round-trip cannot wedge
78
+ // the chain.
79
+ await previous?.value
80
+ await mirror(value, generation)
81
+ promise.resolve()
82
+ }
83
+ }
84
+ return promise
85
+ }
86
+
87
+ /// Value-returning variant of `enqueue`.
88
+ internal func enqueueValue<T>(_ body: @escaping () -> T) -> Promise<T> {
89
+ let promise = Promise<T>()
90
+ playerQueue.async {
91
+ promise.resolve(withResult: body())
92
+ }
93
+ return promise
94
+ }
95
+
96
+ /// Throwing variant of `enqueue` — a thrown error rejects the JS promise
97
+ /// with its message, as `Promise.async` does for a throwing body.
98
+ internal func enqueueThrowing(_ body: @escaping () throws -> Void) -> Promise<Void> {
99
+ let promise = Promise<Void>()
100
+ playerQueue.async {
101
+ do {
102
+ try body()
103
+ promise.resolve()
104
+ } catch {
105
+ promise.reject(withError: error)
106
+ }
107
+ }
108
+ return promise
109
+ }
110
+
111
+ /// Serialise a mutation body on `playerQueue` and return a
112
+ /// Promise-compatible awaitable. Centralising here keeps every access to
113
+ /// player state on one queue, which is what makes the state safe to read
114
+ /// without locks inside these bodies.
115
+ internal func onPlayerQueue(_ body: @escaping () -> Void) async {
116
+ await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
117
+ playerQueue.async {
118
+ body()
119
+ cont.resume()
120
+ }
121
+ }
122
+ }
123
+
124
+ /// Run `body` on `playerQueue` synchronously and return its value. Used by
125
+ /// the Nitro getters and the listener register / dispose paths, which are
126
+ /// not Promise-wrapped and run on whatever thread the JS bridge uses.
127
+ ///
128
+ /// Running inline when already on the queue is what makes re-entrant calls
129
+ /// safe: `playerQueue.sync` from a block already on `playerQueue` is an
130
+ /// immediate deadlock.
131
+ internal func onPlayerQueueSync<T>(_ body: () -> T) -> T {
132
+ if isOnPlayerQueue { return body() }
133
+ return playerQueue.sync(execute: body)
134
+ }
135
+ }
@@ -0,0 +1,267 @@
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: a user re-tapping play after an exhausted-retries error
11
+ // is asking for this track again, so it gets the allowance the consumer
12
+ // configured back and a rebuild. Not a hard-coded 1: `autoRetries: 0`
13
+ // asked for none.
14
+ if self.lastReportedState == .error,
15
+ self.currentTrackIndex >= 0,
16
+ self.currentTrackIndex < self.queueItemIds.count {
17
+ let qid = self.queueItemIds[self.currentTrackIndex]
18
+ if (self.retryAttemptsRemaining[qid] ?? 0) == 0 {
19
+ // Asking for this track to play is incompatible with the queue being
20
+ // over, so `reachedQueueEnd` cannot survive it.
21
+ self.reachedQueueEnd = false
22
+ self.retryAttemptsRemaining[qid] = self.effectiveAutoRetries()
23
+ // A scheduled attempt for this item is superseded by the rebuild below.
24
+ if self.pendingRetryQueueItemId == qid { self.pendingRetryQueueItemId = nil }
25
+ self.lastErrorQueueItemId = nil
26
+ self.lastErrorCode = nil
27
+ // Rebuild whenever an engine is live — the rebuild is
28
+ // engine-agnostic, and the failed item has to be
29
+ // reconstructed on either engine for the retry to reach it.
30
+ // Forced: the failed item keeps its `queueItemId`, so a rebuild
31
+ // that compares identity would find nothing to do.
32
+ if self.engine != nil {
33
+ self.performingMutation {
34
+ self.fullRebuildPlayerQueue(forcingReinstall: true)
35
+ }
36
+ }
37
+ }
38
+ }
39
+ // At the end of the queue there is nothing to play: the engine is
40
+ // parked on the finished track (or drained), and writing a rate onto
41
+ // it would report `.playing` for a moment or, on some OS versions,
42
+ // re-post the item's end. A skip, a seek or a fresh queue leaves this
43
+ // state; play() alone restarts nothing.
44
+ if self.reachedQueueEnd, self.currentTrackIndex >= 0 { return }
45
+ self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
46
+ self.wantsToPlay = true
47
+ // An explicit play ends an interruption from the user's side: the
48
+ // system does not always deliver `.ended` (an app suspended mid-call
49
+ // never sees it), and the flag would otherwise keep retries, window
50
+ // refills and network recovery disabled for the rest of the session.
51
+ self.isInterrupted = false
52
+ self.beginPlayback()
53
+ self.recomputeBufferState()
54
+ }
55
+ }
56
+
57
+ func retry() throws -> Promise<Void> {
58
+ return enqueue {
59
+ // During cast the receiver owns playback — forward a play so a failed
60
+ // receiver item re-attempts.
61
+ if CastTransportRouter.routePlay() { return }
62
+ // A user asking for a retry has a reason, and the library is not the one
63
+ // to judge it. This is the manual attempt once the automatic one is
64
+ // spent, so gating it on the player's state would gate recovery on the
65
+ // state of a player that has already failed to recover — and a wedged
66
+ // decoder still reporting `playing` is exactly when someone reaches for
67
+ // the button.
68
+ //
69
+ // Only the bounds remain: no current track is genuinely nothing to
70
+ // retry, which is not second-guessing the request.
71
+ guard self.currentTrackIndex >= 0,
72
+ self.currentTrackIndex < self.queueItemIds.count else { return }
73
+ let qid = self.queueItemIds[self.currentTrackIndex]
74
+ // Capture the pre-failure position so a mid-track failure resumes where
75
+ // it stopped (the rebuild below reloads the item from 0). Matches
76
+ // Android's retryFailedItem position-restore.
77
+ let resumePos = self.engine?.currentPositionSeconds ?? 0
78
+ // Asking for this track to play is incompatible with the queue being over,
79
+ // and a queue whose only track failed to load has already drained: without
80
+ // this the rebuild seats an item into a queue that believes it is finished.
81
+ self.reachedQueueEnd = false
82
+ // The configured budget, not a hard-coded 1: this restores the track's
83
+ // automatic allowance for whatever fails *next*, and a consumer who set
84
+ // `autoRetries: 0` asked for none.
85
+ self.retryAttemptsRemaining[qid] = self.effectiveAutoRetries()
86
+ // A scheduled attempt for this item is superseded by the rebuild below;
87
+ // leaving it set would suppress the rebuilt item's own failure until the
88
+ // earlier attempt fired.
89
+ if self.pendingRetryQueueItemId == qid { self.pendingRetryQueueItemId = nil }
90
+ self.lastErrorQueueItemId = nil
91
+ self.lastErrorCode = nil
92
+ // Rebuild whenever an engine is live — the rebuild is
93
+ // engine-agnostic, and the failed item has to be reconstructed
94
+ // on either engine for the retry to reach it. Forced: the failed
95
+ // item keeps its `queueItemId`, so a rebuild that compares
96
+ // identity would find nothing to do.
97
+ if self.engine != nil {
98
+ self.performingMutation {
99
+ self.fullRebuildPlayerQueue(forcingReinstall: true)
100
+ }
101
+ }
102
+ self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
103
+ self.wantsToPlay = true
104
+ self.beginPlayback()
105
+ if resumePos.isFinite, resumePos > 0 {
106
+ _ = try? self.seekTo(position: resumePos)
107
+ }
108
+ self.recomputeBufferState()
109
+ }
110
+ }
111
+
112
+ func pause() throws -> Promise<Void> {
113
+ return enqueue {
114
+ if CastTransportRouter.routePause() { return }
115
+ self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
116
+ self.wantsToPlay = false
117
+ // An explicit pause outranks a pending stall recovery: without this the
118
+ // buffer refilling afterwards would restart audio the user just stopped.
119
+ self.isRecoveringFromStall = false
120
+ self.engine?.pause()
121
+ self.recomputeBufferState()
122
+ }
123
+ }
124
+
125
+ /// "Stop" = pause + seek to start of current item. Does NOT clear
126
+ /// the queue — consumers wanting a full reset call clearQueue().
127
+ func stop() throws -> Promise<Void> {
128
+ return enqueue {
129
+ if CastTransportRouter.routeStop() { return }
130
+ guard let engine = self.engine else { return }
131
+ self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
132
+ self.wantsToPlay = false
133
+ self.hasStartedPlaying = false
134
+ engine.stop()
135
+ self.recomputeBufferState()
136
+ }
137
+ }
138
+
139
+ /// Seek within the current track. Position in seconds; negative
140
+ /// values clamp to 0, values past the duration clamp to duration.
141
+ func seekTo(position: Double) throws -> Promise<Void> {
142
+ // Enqueued at call time, like every other transport method, so a seek
143
+ // reaches the player in the order JS issued it. The promise resolves once
144
+ // the seek's own completion has landed, which is why this builds the
145
+ // promise by hand rather than using `enqueue`.
146
+ let promise = Promise<Void>()
147
+ playerQueue.async {
148
+ // Match Android (`TrackPlayer.kt` seekToInternal): non-finite
149
+ // (NaN / ±Infinity) inputs are silent no-ops. See
150
+ // `InputGuards.sanitisedSeekPosition` for the rationale.
151
+ guard let position = InputGuards.sanitisedSeekPosition(position) else {
152
+ promise.resolve()
153
+ return
154
+ }
155
+ if CastTransportRouter.routeSeekTo(positionMs: Int64(position * 1000.0)) {
156
+ promise.resolve()
157
+ return
158
+ }
159
+
160
+ // Every player + queue read and the seek itself run on this one hop, so
161
+ // the player never crosses a concurrency boundary. The return value
162
+ // carries whether a seek was issued: the crossfade branch and the
163
+ // drained-gapless branch complete in place and yield false.
164
+ //
165
+ // Named rather than inlined so the two early-return branches and the
166
+ // seek branch share one exit.
167
+ let issueSeek: () -> Bool = {
168
+ // Engine-aware seek: under CrossfadeEngine `self.player` is
169
+ // nil. Without this branch every seek call would silently
170
+ // no-op once crossfade mode is active, breaking transport
171
+ // controls + auto-advance test coverage.
172
+ if self.player == nil {
173
+ guard let engine = self.engine else { return false }
174
+ // Seeking replays from a position — no longer at the end of the
175
+ // queue. (Gapless no-ops at end-of-queue with no current item below.)
176
+ self.reachedQueueEnd = false
177
+ let duration = engine.currentDurationSeconds
178
+ let clamped: Double
179
+ if position < 0 {
180
+ clamped = 0
181
+ } else {
182
+ clamped = (duration > 0 && position > duration) ? duration : position
183
+ }
184
+ let seekTarget = self.clampToBufferedIfEnabled(clamped)
185
+ self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
186
+ let engineIdx: Int
187
+ if let installed = self.engineIndex(forQueuePosition: self.currentTrackIndex) {
188
+ engineIdx = installed
189
+ } else {
190
+ // Active track absent from the engine — reinstall it, then seek
191
+ // within the reinstalled run so the position is applied rather
192
+ // than dropped with a resolved promise.
193
+ self.performingMutation { self.fullRebuildPlayerQueue() }
194
+ guard let reinstalled = self.engineIndex(forQueuePosition: self.currentTrackIndex)
195
+ else { return false }
196
+ engineIdx = reinstalled
197
+ }
198
+ engine.seek(toIndex: engineIdx, position: seekTarget)
199
+ self.milestoneTracker.seek(
200
+ durationSec: self.effectiveMilestoneDuration(playerDuration: duration),
201
+ positionSec: seekTarget)
202
+ self.nowPlayingInfo.refreshPositionAndRate(self.nowPlayingSnapshot())
203
+ return false
204
+ }
205
+
206
+ guard let player = self.player else { return false }
207
+ // A gapless player drained at the end of the queue has no item to
208
+ // seek in: reinstall the finished track first, as a same-index skip
209
+ // does, so a seek after the end replays it on every engine. Seeking
210
+ // is what leaves the ended state, so the flag clears here.
211
+ if player.currentItem == nil, self.reachedQueueEnd,
212
+ self.currentTrackIndex >= 0, self.currentTrackIndex < self.tracks.count {
213
+ self.reachedQueueEnd = false
214
+ self.performingMutation { self.fullRebuildPlayerQueue() }
215
+ }
216
+ guard let item = player.currentItem else { return false }
217
+ let itemDuration = CMTimeGetSeconds(item.duration)
218
+ let clamped: Double
219
+ if position < 0 {
220
+ clamped = 0
221
+ } else {
222
+ clamped = (itemDuration.isFinite && position > itemDuration) ? itemDuration : position
223
+ }
224
+ // Apply the opt-in buffered clamp on top of the 0/duration clamp — caps
225
+ // the target at the buffered edge when the seek is past what's downloaded.
226
+ let seekTarget = self.clampToBufferedIfEnabled(clamped)
227
+ // Consume jumped-over milestones BEFORE issuing the (async) seek so a
228
+ // time-jump progress tick that fires during the seek can't emit the
229
+ // thresholds the seek skips over.
230
+ self.milestoneTracker.seek(
231
+ durationSec: self.effectiveMilestoneDuration(
232
+ playerDuration: (itemDuration.isFinite && itemDuration > 0) ? itemDuration : 0),
233
+ positionSec: seekTarget)
234
+ // Stash `.user` reason BEFORE the seek so any state transition
235
+ // the seek triggers (e.g. playable → buffering on a long jump)
236
+ // gets the correct origin. Matches the play / pause / stop pattern.
237
+ self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
238
+ player.seek(
239
+ to: CMTime(seconds: seekTarget, preferredTimescale: CMTimeScale(NSEC_PER_SEC)),
240
+ toleranceBefore: .zero,
241
+ toleranceAfter: .zero
242
+ ) { _ in
243
+ // This closure captures `player`, so a teardown that drops every
244
+ // other reference cannot deallocate it with the seek still pending.
245
+ // AVFoundation invokes the handler when the seek completes or is
246
+ // interrupted, and the capture is released with it.
247
+ _ = player
248
+ self.playerQueue.async {
249
+ // Surface the new scrubber position to the lock-screen /
250
+ // control-center immediately. The KVO state-change emit covers
251
+ // transitions (e.g. paused → buffering on a long jump) but a
252
+ // same-state seek won't trigger an emit; without this the
253
+ // lock-screen scrubber sits at the pre-seek position until the
254
+ // next 10s periodic tick.
255
+ self.nowPlayingInfo.refreshPositionAndRate(self.nowPlayingSnapshot())
256
+ promise.resolve()
257
+ }
258
+ }
259
+ return true
260
+ }
261
+ // A branch that completed in place has nothing pending, so the promise
262
+ // resolves here; the seek branch resolves from its completion instead.
263
+ if !issueSeek() { promise.resolve() }
264
+ }
265
+ return promise
266
+ }
267
+ }