react-native-queue-player 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (355) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/consumer-rules.pro +1 -1
  4. package/android/src/main/cpp/airplay2_control.c +4 -4
  5. package/android/src/main/cpp/airplay2_control.h +4 -4
  6. package/android/src/main/cpp/airplay2_jni.cpp +3 -19
  7. package/android/src/main/cpp/airplay2_pair.c +7 -7
  8. package/android/src/main/cpp/airplay2_pair.h +2 -2
  9. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  10. package/android/src/main/cpp/airplay2_session.c +2 -2
  11. package/android/src/main/cpp/airplay_jni.cpp +8 -69
  12. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +839 -271
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  58. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  59. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  60. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  61. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  62. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  63. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
  64. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  65. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  109. package/app.plugin.js +17 -1
  110. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  111. package/ios/AVQueueBuilder.swift +120 -85
  112. package/ios/ArtworkLoader.swift +0 -7
  113. package/ios/ArtworkResolver.swift +22 -4
  114. package/ios/AssetReplayGainReader.swift +118 -0
  115. package/ios/AudioSession.swift +67 -16
  116. package/ios/AudioTapProvider.swift +284 -149
  117. package/ios/CarPlayBridge.swift +0 -12
  118. package/ios/CarPlayCoordinator.swift +18 -2
  119. package/ios/CarPlaySceneDelegate.swift +2 -2
  120. package/ios/Cast/AirPlayRouteState.swift +9 -8
  121. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  122. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  123. package/ios/Cast/Core/CastSession.swift +2 -2
  124. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  125. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  126. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  127. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  128. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  129. package/ios/CastManager.swift +1 -1
  130. package/ios/CrossfadeEngine.swift +660 -226
  131. package/ios/EQTap.swift +20 -17
  132. package/ios/Equalizer.swift +1 -1
  133. package/ios/FLACStreamInfo.swift +65 -0
  134. package/ios/GaplessEngine.swift +57 -30
  135. package/ios/InputGuards.swift +35 -5
  136. package/ios/LookaheadCache.swift +202 -58
  137. package/ios/LookaheadCachePrefetcher.swift +48 -36
  138. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  139. package/ios/NetworkRecoveryPolicy.swift +32 -0
  140. package/ios/NowPlayingFormatExtractor.swift +25 -18
  141. package/ios/NowPlayingInfo.swift +84 -48
  142. package/ios/OutputRouteMonitor.swift +16 -3
  143. package/ios/PendingBrowseRequests.swift +1 -1
  144. package/ios/PlaceholderArtwork.swift +9 -5
  145. package/ios/PlaybackEngine.swift +165 -76
  146. package/ios/PlaybackErrorMapping.swift +3 -3
  147. package/ios/PlaybackModeStateMachine.swift +2 -2
  148. package/ios/PlaybackNetworkMonitor.swift +73 -0
  149. package/ios/PlayerStateDerivation.swift +6 -6
  150. package/ios/QueueMutationArithmetic.swift +25 -0
  151. package/ios/QueueSkipArithmetic.swift +5 -3
  152. package/ios/QueueWindowArithmetic.swift +152 -0
  153. package/ios/ReadThroughServer.swift +283 -0
  154. package/ios/RemoteCommands.swift +44 -4
  155. package/ios/ReplayGainData.swift +22 -3
  156. package/ios/ReplayGainExtractor.swift +70 -27
  157. package/ios/Siri/VoiceDonation.swift +14 -9
  158. package/ios/StreamingBitrateProbe.swift +22 -68
  159. package/ios/SuppliedReplayGain.swift +83 -0
  160. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  161. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  162. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  163. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  164. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  165. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  166. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  167. package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
  168. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  169. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  170. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  171. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  172. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  173. package/ios/Tests/EngineSwapTests.swift +91 -0
  174. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  175. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  176. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  177. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  178. package/ios/Tests/InputGuardsTests.swift +41 -0
  179. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  180. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  181. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  182. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  183. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  184. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  185. package/ios/Tests/MutationDeferralTests.swift +220 -0
  186. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  187. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  188. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  189. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  190. package/ios/Tests/PlayerFixtures.swift +48 -0
  191. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  192. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  193. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  194. package/ios/Tests/QueueStateTests.swift +3 -1
  195. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  196. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  197. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  198. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  199. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  200. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  201. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  202. package/ios/Tests/RetryRecoveryTests.swift +303 -0
  203. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  204. package/ios/Tests/SkipIndexTests.swift +32 -2
  205. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  206. package/ios/Tests/StallRecoveryTests.swift +97 -0
  207. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  208. package/ios/Tests/TopUpWindowGateTests.swift +394 -0
  209. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  210. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  211. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  212. package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
  213. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  214. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  215. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  216. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  217. package/ios/TrackPlayer+Automotive.swift +101 -0
  218. package/ios/TrackPlayer+Cache.swift +232 -0
  219. package/ios/TrackPlayer+Config.swift +291 -0
  220. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  221. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  222. package/ios/TrackPlayer+EventsDispatch.swift +968 -0
  223. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  224. package/ios/TrackPlayer+Lifecycle.swift +930 -0
  225. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  226. package/ios/TrackPlayer+Queue.swift +940 -0
  227. package/ios/TrackPlayer+Recovery.swift +196 -0
  228. package/ios/TrackPlayer+Skip.swift +455 -0
  229. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  230. package/ios/TrackPlayer+State.swift +108 -0
  231. package/ios/TrackPlayer+Threading.swift +135 -0
  232. package/ios/TrackPlayer+Transport.swift +267 -0
  233. package/ios/TrackPlayer+Window.swift +195 -0
  234. package/ios/TrackPlayer.swift +305 -4619
  235. package/ios/Visualizer.swift +6 -5
  236. package/ios/tests-harness/Podfile +1 -1
  237. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  238. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  239. package/lib/module/hooks/useActiveTrack.js +29 -22
  240. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  241. package/lib/module/hooks/useCast.js +8 -26
  242. package/lib/module/hooks/useCast.js.map +1 -1
  243. package/lib/module/hooks/useEqualizer.js +19 -12
  244. package/lib/module/hooks/useEqualizer.js.map +1 -1
  245. package/lib/module/hooks/useLookaheadCache.js +3 -7
  246. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  247. package/lib/module/hooks/useQueue.js +66 -19
  248. package/lib/module/hooks/useQueue.js.map +1 -1
  249. package/lib/module/index.js +6 -3
  250. package/lib/module/index.js.map +1 -1
  251. package/lib/module/queueDelta.js +41 -0
  252. package/lib/module/queueDelta.js.map +1 -0
  253. package/lib/module/types.js +28 -5
  254. package/lib/module/types.js.map +1 -1
  255. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  256. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  257. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  258. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  259. package/lib/typescript/hooks/useCast.d.ts +6 -1
  260. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  261. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  262. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  263. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  264. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  265. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  266. package/lib/typescript/index.d.ts +2 -1
  267. package/lib/typescript/index.d.ts.map +1 -1
  268. package/lib/typescript/queueDelta.d.ts +10 -0
  269. package/lib/typescript/queueDelta.d.ts.map +1 -0
  270. package/lib/typescript/types.d.ts +168 -12
  271. package/lib/typescript/types.d.ts.map +1 -1
  272. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  273. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  274. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  275. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  276. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  277. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  278. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  279. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  280. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  281. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  282. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  283. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  284. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  285. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  313. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  314. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  315. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  316. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  317. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  318. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  319. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  320. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  321. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  322. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  323. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  324. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  325. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  326. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  327. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  328. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  329. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  330. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  331. package/package.json +8 -8
  332. package/src/TrackPlayer.nitro.ts +53 -27
  333. package/src/hooks/useActiveTrack.ts +29 -22
  334. package/src/hooks/useCast.ts +14 -15
  335. package/src/hooks/useEqualizer.ts +19 -12
  336. package/src/hooks/useLookaheadCache.ts +3 -7
  337. package/src/hooks/useQueue.ts +66 -17
  338. package/src/index.ts +13 -3
  339. package/src/queueDelta.ts +41 -0
  340. package/src/types.ts +169 -12
  341. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  342. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  343. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  344. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  345. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  346. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  347. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  348. package/ios/MetadataReader.swift +0 -555
  349. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  350. package/ios/Tests/MetadataReaderTests.swift +0 -603
  351. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  352. package/ios/Tests/VoiceDonationTests.swift +0 -26
  353. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  354. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  355. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -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
+ }