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,152 @@
1
+ import Foundation
2
+
3
+ /// Pure-Swift arithmetic for the bounded enqueue window.
4
+ ///
5
+ /// Everything here is expressed in absolute queue *positions* — never engine
6
+ /// indices, which move as `AVQueuePlayer` consumes finished items.
7
+ ///
8
+ /// `slice` describes the installed run — a trail behind the playhead plus the
9
+ /// forward window — and it **may wrap**: it can end at a lower absolute
10
+ /// position than it starts. A wrapped slice is a run in play order, not an
11
+ /// ordered set of integers, so it must not be compared with `<` / `>`, sorted,
12
+ /// or turned into a `Range<Int>`. The floor bounds its forward part.
13
+ ///
14
+ /// `isAtLogicalTail` and `verdict` answer what a drained engine means, in
15
+ /// absolute positions and without wrapping — see `isAtLogicalTail` for why
16
+ /// that asymmetry is deliberate.
17
+ ///
18
+ /// Helpers are gated behind `@_spi(QueuePlayerTests)` so they stay off the
19
+ /// library's consumer-facing surface, matching `QueueMutationArithmetic`.
20
+ public enum QueueWindowArithmetic {
21
+
22
+ /// Items materialised at or after the current track, inclusive of it.
23
+ ///
24
+ /// Three is the minimum that satisfies "top up whenever at most one item
25
+ /// remains after the current one, and never let that reach zero": a smaller
26
+ /// floor lands an append in the slot `AVQueuePlayer` is about to consume.
27
+ /// Ten sits well clear of that minimum. The cost windowing exists to avoid is
28
+ /// per-item — item construction, a cache lookup, an LRU touch — and it is a
29
+ /// queue-length problem, not a window-length one: ten items is a fixed,
30
+ /// negligible cost where the unbounded build was thousands.
31
+ @_spi(QueuePlayerTests)
32
+ public static let floor: Int = 10
33
+
34
+ /// What a drained engine means for the queue as a whole.
35
+ @_spi(QueuePlayerTests)
36
+ public enum EndVerdict: Equatable {
37
+ /// The engine still has an active item; nothing ended.
38
+ case notEnded
39
+ /// Re-enqueue the current position. Under `.track` the queue can neither
40
+ /// end nor advance, so a drain is always the same position again.
41
+ case refillCurrent
42
+ /// The engine ran out mid-queue. Re-seat the queue from the next position
43
+ /// and resume — not a queue end.
44
+ case windowDrained
45
+ /// The logical queue ended under `.queue`; restart from position 0.
46
+ case wrapToStart
47
+ /// The logical queue ended. Fire `onQueueEnd` and settle to `.ended`.
48
+ case queueEnd
49
+ }
50
+
51
+ /// Positions the engine should hold around `currentIndex`, in install order.
52
+ ///
53
+ /// `trail` positions behind the current one, the current one, and forward to
54
+ /// the floor. The trail exists so a backward skip reuses an `AVURLAsset`
55
+ /// already built; a run that only grows forward moves the memory peak later
56
+ /// rather than lowering it, because nothing is ever removed.
57
+ ///
58
+ /// `wraps` is whether the queue returns to position 0 at its end, which is
59
+ /// what makes a repeat-queue boundary a crossfade rather than a hard cut:
60
+ /// the incoming item has to be materialised before the outgoing one ends.
61
+ ///
62
+ /// Returns at most `min(trail + floor, trackCount)` positions, always as one
63
+ /// unbroken run in play order: each position is the previous one plus one,
64
+ /// modulo `trackCount`. Where the run covers the whole queue the position
65
+ /// after the last is the first; everywhere else it is not installed yet and
66
+ /// belongs to top-up.
67
+ ///
68
+ /// A position appears at most once. Two materialisations of one position
69
+ /// would give the engine two items carrying the same `queueItemId`, and
70
+ /// every translation from position to engine index is a first-match — so the
71
+ /// second copy would be unreachable and the trail could strand the item
72
+ /// being heard. The forward run and the trail can only meet when the slice
73
+ /// already covers every position, so nothing is lost either way; the forward
74
+ /// run keeps the contested position because that is what leaves the result
75
+ /// one unbroken run.
76
+ ///
77
+ /// A queue of one wraps onto itself, so it cannot both hold a fresh incoming
78
+ /// item and hold each position once. It holds the one position, and a
79
+ /// repeat-queue boundary there is a hard cut.
80
+ @_spi(QueuePlayerTests)
81
+ public static func slice(
82
+ currentIndex: Int, trackCount: Int, trail: Int, wraps: Bool
83
+ ) -> [Int] {
84
+ guard trackCount > 0, currentIndex >= 0, currentIndex < trackCount else { return [] }
85
+ let cap = min(max(0, trail) + floor, trackCount)
86
+
87
+ // Uniqueness rests on the cap: a run of at most `trackCount` positions
88
+ // stepping one at a time cannot revisit one. The `held` checks below are a
89
+ // second mechanism, and are unreachable while the cap holds — they are
90
+ // what keeps a future change to the cap from silently emitting duplicates.
91
+ var positions: [Int] = []
92
+ var held = Set<Int>()
93
+
94
+ var forward = currentIndex
95
+ while positions.count < min(floor, trackCount) {
96
+ guard !held.contains(forward) else { break }
97
+ positions.append(forward)
98
+ held.insert(forward)
99
+ let next = forward + 1
100
+ if next < trackCount {
101
+ forward = next
102
+ } else if wraps {
103
+ forward = 0
104
+ } else {
105
+ break
106
+ }
107
+ }
108
+
109
+ var behind: [Int] = []
110
+ var step = 1
111
+ while behind.count < max(0, trail), positions.count + behind.count < cap {
112
+ let raw = currentIndex - step
113
+ let position = raw >= 0 ? raw : (wraps ? raw + trackCount : -1)
114
+ guard position >= 0, !held.contains(position) else { break }
115
+ behind.append(position)
116
+ held.insert(position)
117
+ step += 1
118
+ }
119
+
120
+ return behind.reversed() + positions
121
+ }
122
+
123
+ /// Whether the last enqueued position is also the last position in the
124
+ /// queue. False on an empty queue, where `-1 == -1` would otherwise read
125
+ /// as a tail.
126
+ ///
127
+ /// Deliberately not wrap-aware, even though `slice` wraps. `verdict` pairs
128
+ /// this with `.windowDrained`, whose caller advances by a **non-modular**
129
+ /// clamp; today that clamp is unreachable precisely because this answers
130
+ /// true at the last position. An answer that went modular here would make it
131
+ /// reachable, and the last track would re-seat onto itself instead of
132
+ /// wrapping. A wrapping run's completeness is a question for the top-up
133
+ /// path, not for the drain verdict.
134
+ @_spi(QueuePlayerTests)
135
+ public static func isAtLogicalTail(anchorPosition: Int, trackCount: Int) -> Bool {
136
+ guard trackCount > 0 else { return false }
137
+ return anchorPosition == trackCount - 1
138
+ }
139
+
140
+ /// Resolve what a drained engine means. `drained` is "the engine has no
141
+ /// active item"; `atLogicalTail` is `isAtLogicalTail` against the anchor.
142
+ /// Caller must hold a non-empty queue — an empty one has nothing to end.
143
+ @_spi(QueuePlayerTests)
144
+ public static func verdict(
145
+ drained: Bool, atLogicalTail: Bool, repeatMode: QueueSkipRepeatMode
146
+ ) -> EndVerdict {
147
+ guard drained else { return .notEnded }
148
+ if repeatMode == .track { return .refillCurrent }
149
+ guard atLogicalTail else { return .windowDrained }
150
+ return repeatMode == .queue ? .wrapToStart : .queueEnd
151
+ }
152
+ }
@@ -0,0 +1,283 @@
1
+ import Foundation
2
+ import Network
3
+
4
+ /// Serves the player's remote tracks from the lookahead cache when it holds
5
+ /// them, and from the origin when it does not.
6
+ ///
7
+ /// The player binds one stable `http://127.0.0.1:<port>/media/<token>` per
8
+ /// source URL and never re-points it. Whether a request is answered from disk
9
+ /// or from the network is decided per request, which is the only moment the
10
+ /// answer is actually knowable — the cache fills while the queue is already
11
+ /// built, so a URL chosen at construction is always chosen too early.
12
+ ///
13
+ /// Loopback-bound: nothing outside the device needs to reach it. That is a
14
+ /// smaller default rather than a security boundary — another process on the
15
+ /// same device could reach it too, and the 128-bit token is what stands in its
16
+ /// way.
17
+ final class ReadThroughServer: @unchecked Sendable {
18
+
19
+ /// Token → the origin it stands for. Written on the player's queue, read on
20
+ /// connection queues.
21
+ private let lock = NSLock()
22
+ private var origins: [String: URL] = [:]
23
+ private var tokensBySource: [String: String] = [:]
24
+ /// The consumer's `track.url` string per token: the cache is keyed on it
25
+ /// verbatim, and `URL.absoluteString` is a re-serialisation that can differ
26
+ /// from it (percent-encoding on iOS 17+).
27
+ private var sourceURLs: [String: String] = [:]
28
+ private var touched: Set<String> = []
29
+ private var headers: [String: String] = [:]
30
+
31
+ private let listenerLock = NSLock()
32
+ private var listener: NWListener?
33
+ private var boundPort: UInt16 = 0
34
+ private var connections: [ObjectIdentifier: MediaHTTPConnection] = [:]
35
+ private var bindWaiters: [DispatchSemaphore] = []
36
+
37
+ private let serverQueue = DispatchQueue(label: "queueplayer.readthrough.listener")
38
+
39
+ /// One session for every origin leg, so connections are pooled across the
40
+ /// per-request client connections AVFoundation opens.
41
+ private let session: URLSession
42
+
43
+ /// The default session: pooled like `URLSession.shared`, but with no
44
+ /// `URLCache` — the lookahead cache already keeps the bytes on disk, and
45
+ /// `URLCache.shared` would hold a second copy of every small range.
46
+ private static func makeOriginSession() -> URLSession {
47
+ let configuration = URLSessionConfiguration.default
48
+ configuration.urlCache = nil
49
+ configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
50
+ return URLSession(configuration: configuration)
51
+ }
52
+
53
+ /// Consulted per request for the cache's complete copy of an origin. Injected
54
+ /// rather than referenced directly so the server owns no cache lifetime.
55
+ private let cachedFileForOrigin: (String) -> URL?
56
+
57
+ /// Called once per token the first time it is served from disk, so the cache
58
+ /// can bump LRU on a real read. Not per request: `touch` writes the index on
59
+ /// the same serial queue the lookup takes, and AVFoundation issues several
60
+ /// range requests per track.
61
+ private let onFirstDiskHit: (String) -> Void
62
+
63
+ init(
64
+ session: URLSession = ReadThroughServer.makeOriginSession(),
65
+ cachedFileForOrigin: @escaping (String) -> URL?,
66
+ onFirstDiskHit: @escaping (String) -> Void
67
+ ) {
68
+ self.session = session
69
+ self.cachedFileForOrigin = cachedFileForOrigin
70
+ self.onFirstDiskHit = onFirstDiskHit
71
+ }
72
+
73
+ // MARK: - Routing
74
+
75
+ /// The local URL standing for `origin`, or nil when the listener is not up.
76
+ ///
77
+ /// Stable for the life of the session: one token per source string, so a
78
+ /// window rebuild hands the player the URL it already has and AVFoundation
79
+ /// keeps reusing its own connections. Keyed on the string rather than the
80
+ /// parsed URL because two strings that parse alike are still two tracks with
81
+ /// two cache entries.
82
+ /// `sourceURL` is the `track.url` string the cache is keyed on; it defaults
83
+ /// to the origin's own serialisation for callers that have no other.
84
+ func localURL(for origin: URL, sourceURL: String? = nil) -> URL? {
85
+ let port = boundPortValue()
86
+ guard port != 0 else { return nil }
87
+ let source = sourceURL ?? origin.absoluteString
88
+ lock.lock()
89
+ let token: String
90
+ if let existing = tokensBySource[source] {
91
+ token = existing
92
+ } else {
93
+ token = Self.newToken()
94
+ tokensBySource[source] = token
95
+ origins[token] = origin
96
+ sourceURLs[token] = source
97
+ }
98
+ lock.unlock()
99
+ #if DEBUG
100
+ NSLog(
101
+ "[RNQP-CACHE] route token=%@ -> %@",
102
+ String(token.prefix(8)), PlaybackErrorMapping.stripQuery(origin.absoluteString))
103
+ #endif
104
+ return URL(string: "http://127.0.0.1:\(port)/media/\(token)")
105
+ }
106
+
107
+ /// The consumer's headers, sent on every origin request. Written by
108
+ /// `startReadThroughServer` — `configure()` and `setLookaheadCache` — so a
109
+ /// refreshed token reaches requests for items built before it changed.
110
+ func setHeaders(_ headers: [String: String]) {
111
+ lock.lock()
112
+ self.headers = headers
113
+ lock.unlock()
114
+ }
115
+
116
+ private func resolve(_ token: String) -> MediaSource? {
117
+ lock.lock()
118
+ guard let origin = origins[token] else {
119
+ lock.unlock()
120
+ return nil
121
+ }
122
+ let sourceURL = sourceURLs[token] ?? origin.absoluteString
123
+ let headers = self.headers
124
+ let alreadyTouched = touched.contains(token)
125
+ lock.unlock()
126
+
127
+ let cached = cachedFileForOrigin(sourceURL)
128
+ #if DEBUG
129
+ NSLog(
130
+ "[RNQP-CACHE] resolve token=%@ cached=%@ origin=%@",
131
+ String(token.prefix(8)), cached == nil ? "no" : "yes",
132
+ PlaybackErrorMapping.stripQuery(origin.absoluteString))
133
+ #endif
134
+ if cached != nil && !alreadyTouched {
135
+ lock.lock()
136
+ touched.insert(token)
137
+ lock.unlock()
138
+ onFirstDiskHit(sourceURL)
139
+ }
140
+ return .readThrough(
141
+ ReadThroughSource(origin: origin, headers: headers, cachedFile: cached, session: session)
142
+ )
143
+ }
144
+
145
+ /// 128 bits, so the token cannot be enumerated by something else on the
146
+ /// device. Same shape `MediaHTTPRequest.mediaToken` already validates.
147
+ private static func newToken() -> String {
148
+ var bytes = [UInt8](repeating: 0, count: 16)
149
+ if SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) != errSecSuccess {
150
+ for i in bytes.indices { bytes[i] = UInt8.random(in: 0 ... 255) }
151
+ }
152
+ return bytes.map { String(format: "%02x", $0) }.joined()
153
+ }
154
+
155
+ // MARK: - Lifecycle
156
+
157
+ /// Bring the listener up. Returns immediately; `waitUntilBound` is what waits.
158
+ func start() {
159
+ listenerLock.lock()
160
+ guard listener == nil else {
161
+ listenerLock.unlock()
162
+ return
163
+ }
164
+ let params = NWParameters.tcp
165
+ // Loopback only. The cast server binds every interface because a receiver
166
+ // fetches over the LAN; nothing outside this device ever reads from here.
167
+ params.requiredLocalEndpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: .any)
168
+ guard let newListener = try? NWListener(using: params) else {
169
+ listenerLock.unlock()
170
+ resumeReadinessWaiters()
171
+ return
172
+ }
173
+ listener = newListener
174
+ listenerLock.unlock()
175
+
176
+ newListener.stateUpdateHandler = { [weak self] state in
177
+ guard let self = self else { return }
178
+ switch state {
179
+ case .ready:
180
+ self.listenerLock.lock()
181
+ self.boundPort = newListener.port?.rawValue ?? 0
182
+ self.listenerLock.unlock()
183
+ self.resumeReadinessWaiters()
184
+ case .failed, .cancelled:
185
+ // Drop the listener too, so a later `start()` can try again; a
186
+ // failed listener left in the slot would make every restart a no-op.
187
+ self.listenerLock.lock()
188
+ if self.listener === newListener {
189
+ self.listener = nil
190
+ self.boundPort = 0
191
+ }
192
+ self.listenerLock.unlock()
193
+ self.resumeReadinessWaiters()
194
+ default:
195
+ break
196
+ }
197
+ }
198
+ newListener.newConnectionHandler = { [weak self] conn in
199
+ self?.accept(conn)
200
+ }
201
+ newListener.start(queue: serverQueue)
202
+ }
203
+
204
+ /// Block until the listener has bound, up to `timeoutMs`; `true` when it
205
+ /// has. Called right after `start()` on the queue that builds items, so
206
+ /// every item built afterwards sees a bound port: `localURL(for:)` is nil
207
+ /// before the bind, and an item built then is bound to its origin for good.
208
+ /// Binding loopback takes milliseconds; the timeout is for the listener
209
+ /// that never reaches `.ready`, which `start()` also reports by resuming.
210
+ @discardableResult
211
+ func waitUntilBound(timeoutMs: Int = 2000) -> Bool {
212
+ if boundPortValue() != 0 { return true }
213
+ let bound = DispatchSemaphore(value: 0)
214
+ listenerLock.lock()
215
+ if boundPort != 0 {
216
+ listenerLock.unlock()
217
+ return true
218
+ }
219
+ bindWaiters.append(bound)
220
+ listenerLock.unlock()
221
+ _ = bound.wait(timeout: .now() + .milliseconds(timeoutMs))
222
+ return boundPortValue() != 0
223
+ }
224
+
225
+ func boundPortValue() -> UInt16 {
226
+ listenerLock.lock(); defer { listenerLock.unlock() }
227
+ return boundPort
228
+ }
229
+
230
+ private func resumeReadinessWaiters() {
231
+ listenerLock.lock()
232
+ let waiters = bindWaiters
233
+ bindWaiters.removeAll()
234
+ listenerLock.unlock()
235
+ waiters.forEach { $0.signal() }
236
+ }
237
+
238
+ private func accept(_ conn: NWConnection) {
239
+ let responder = MediaHTTPConnection(
240
+ connection: conn,
241
+ resolve: { [weak self] token in self?.resolve(token) }
242
+ ) { [weak self] in
243
+ self?.untrack(conn)
244
+ }
245
+ listenerLock.lock()
246
+ connections[ObjectIdentifier(conn)] = responder
247
+ listenerLock.unlock()
248
+ responder.start()
249
+ }
250
+
251
+ private func untrack(_ conn: NWConnection) {
252
+ listenerLock.lock()
253
+ connections.removeValue(forKey: ObjectIdentifier(conn))
254
+ listenerLock.unlock()
255
+ }
256
+
257
+ /// Tear the listener down, invalidate the origin session and drop every
258
+ /// token. Tokens do not outlive a session, and the map is bounded by the
259
+ /// distinct origins seen in one — so nothing needs revoking as the queue
260
+ /// changes. A stopped server is not restarted; `startReadThroughServer`
261
+ /// builds a new one.
262
+ func stop() {
263
+ listenerLock.lock()
264
+ let live = listener
265
+ listener = nil
266
+ boundPort = 0
267
+ let open = connections
268
+ connections.removeAll()
269
+ listenerLock.unlock()
270
+ open.values.forEach { $0.cancel() }
271
+ live?.cancel()
272
+ session.invalidateAndCancel()
273
+ resumeReadinessWaiters()
274
+
275
+ lock.lock()
276
+ origins.removeAll()
277
+ tokensBySource.removeAll()
278
+ sourceURLs.removeAll()
279
+ touched.removeAll()
280
+ headers.removeAll()
281
+ lock.unlock()
282
+ }
283
+ }
@@ -8,7 +8,9 @@ import MediaPlayer
8
8
  /// `pendingStateChangeReason = .system` and routes through the
9
9
  /// existing public transport methods.
10
10
  ///
11
- /// Threading: every public method MUST be called on the main thread.
11
+ /// Threading: callable from any queue. Every public method hands its work to
12
+ /// main, which owns `MPRemoteCommandCenter`. Command handlers still arrive
13
+ /// on main — `dispatch` asserts that — and the consumer hops from there.
12
14
  /// Handlers registered with `addTarget` fire on main per Apple's docs.
13
15
  final class RemoteCommands {
14
16
 
@@ -24,11 +26,17 @@ final class RemoteCommands {
24
26
  case changePlaybackPosition(seconds: Double)
25
27
  }
26
28
 
27
- /// Set by the owner during `configure()`; cleared during `destroy()`.
28
- /// Invoked synchronously on main from the handler. Owner is
29
+ /// Set by the owner during `configure()` and cleared during `destroy()`,
30
+ /// both on the owner's player queue; invoked synchronously on main from
31
+ /// the handler, so the slot is read and written under a lock. Owner is
29
32
  /// responsible for stamping the state-change reason BEFORE routing
30
33
  /// into transport calls.
31
- var onCommand: ((Command) -> Void)?
34
+ var onCommand: ((Command) -> Void)? {
35
+ get { commandLock.withLock { commandCallback } }
36
+ set { commandLock.withLock { commandCallback = newValue } }
37
+ }
38
+ private let commandLock = NSLock()
39
+ private var commandCallback: ((Command) -> Void)?
32
40
 
33
41
  /// Drive `MPRemoteCommandCenter` `isEnabled` for the skip commands.
34
42
  /// Greys out the lock-screen / CarPlay buttons that would no-op
@@ -36,6 +44,11 @@ final class RemoteCommands {
36
44
  /// when not installed — the lib must not write to the shared
37
45
  /// command center after `uninstall()` has released ownership.
38
46
  func setSkipCapability(canSkipNext: Bool, canSkipPrevious: Bool) {
47
+ RemoteCommands.onMain { self.setSkipCapabilityOnMain(canSkipNext: canSkipNext, canSkipPrevious: canSkipPrevious) }
48
+ }
49
+
50
+ @MainActor
51
+ private func setSkipCapabilityOnMain(canSkipNext: Bool, canSkipPrevious: Bool) {
39
52
  // Only greys out the next/previous-TRACK buttons. In `.interval` mode the
40
53
  // lock-screen shows skip-forward/back, which always work (seek within the
41
54
  // current track), so queue-boundary capability doesn't apply.
@@ -52,6 +65,16 @@ final class RemoteCommands {
52
65
  /// consumer can toggle at runtime. The lib only owns the skip-interval
53
66
  /// commands while in `.interval` mode.
54
67
  func setRemoteControls(mode: RemoteSkipMode, forwardInterval: Double, backwardInterval: Double) {
68
+ RemoteCommands.onMain {
69
+ self.setRemoteControlsOnMain(
70
+ mode: mode, forwardInterval: forwardInterval, backwardInterval: backwardInterval)
71
+ }
72
+ }
73
+
74
+ @MainActor
75
+ private func setRemoteControlsOnMain(
76
+ mode: RemoteSkipMode, forwardInterval: Double, backwardInterval: Double
77
+ ) {
55
78
  self.skipMode = mode
56
79
  self.forwardInterval = forwardInterval
57
80
  self.backwardInterval = backwardInterval
@@ -114,6 +137,11 @@ final class RemoteCommands {
114
137
 
115
138
  /// Register handlers + enable the supported commands. Idempotent.
116
139
  func install() {
140
+ RemoteCommands.onMain { self.installOnMain() }
141
+ }
142
+
143
+ @MainActor
144
+ private func installOnMain() {
117
145
  guard !installed else { return }
118
146
  installed = true
119
147
  let center = MPRemoteCommandCenter.shared()
@@ -174,6 +202,11 @@ final class RemoteCommands {
174
202
  /// owns. Idempotent. Never touches commands the lib didn't enable
175
203
  /// in `install()`.
176
204
  func uninstall() {
205
+ RemoteCommands.onMain { self.uninstallOnMain() }
206
+ }
207
+
208
+ @MainActor
209
+ private func uninstallOnMain() {
177
210
  guard installed else { return }
178
211
  installed = false
179
212
  let center = MPRemoteCommandCenter.shared()
@@ -205,6 +238,13 @@ final class RemoteCommands {
205
238
  center.changePlaybackPositionCommand.isEnabled = false
206
239
  }
207
240
 
241
+ /// Hands `body` to main, which owns `MPRemoteCommandCenter` and therefore
242
+ /// the target registrations and `installed` flag below. Always asynchronous,
243
+ /// so an install and a capability update issued in order land in order.
244
+ private static func onMain(_ body: @escaping @MainActor () -> Void) {
245
+ DispatchQueue.main.async { MainActor.assumeIsolated(body) }
246
+ }
247
+
208
248
  private func dispatch(_ command: Command) {
209
249
  assert(Thread.isMainThread,
210
250
  "RemoteCommands.dispatch must run on the main thread — " +
@@ -14,7 +14,7 @@ enum ReplayGainSelectorMode: Equatable {
14
14
  /// Parsed ReplayGain tag values for a single track. All four fields
15
15
  /// are independent — a track may carry only track tags, only album
16
16
  /// tags, or both. The Opus container guard returns nil from
17
- /// `ReplayGainExtractor.extract(...)` rather than producing a
17
+ /// `AssetReplayGainReader.read(item:)` rather than producing a
18
18
  /// ReplayGainData with one nil pair.
19
19
  ///
20
20
  /// Strict mode (`.album` requires album tag, `.track` requires
@@ -26,14 +26,33 @@ struct ReplayGainData: Equatable {
26
26
  /// audio-stage boundary (vDSP / DSP processors operate on Float).
27
27
  let trackGainDb: Double?
28
28
  /// `REPLAYGAIN_TRACK_PEAK` parsed linear value. `nil` when absent
29
- /// or rejected (see encoder-default detection peak `1.000000`
30
- /// or `< 0.001` is treated as no-peak).
29
+ /// or rejected — `ReplayGainExtractor.acceptPeak` accepts the
30
+ /// `0.001 ... 2.0` band, so a full-scale `1.0` is kept and an
31
+ /// encoder zero (`< 0.001`) is treated as no-peak.
31
32
  let trackPeak: Double?
32
33
  /// `REPLAYGAIN_ALBUM_GAIN` parsed value in dB. `nil` when absent.
33
34
  let albumGainDb: Double?
34
35
  /// `REPLAYGAIN_ALBUM_PEAK` parsed linear value. `nil` when absent.
35
36
  let albumPeak: Double?
36
37
 
38
+ /// All-or-nothing merge of consumer-supplied ReplayGain against
39
+ /// values detected from the file, decided **per item** — never per
40
+ /// mode and never per field.
41
+ ///
42
+ /// [suppliedClaimed] is `true` when any of the four fields was
43
+ /// present on the consumer's `TrackItem`, captured before any value
44
+ /// was validated. When it is `true` the track's ReplayGain comes
45
+ /// solely from [supplied] and [detected] is discarded in full,
46
+ /// including for the mode the consumer said nothing about.
47
+ /// `suppliedClaimed == true` with `supplied == nil` is a real state
48
+ /// — the consumer owns this track and gave nothing usable — so it
49
+ /// yields no gain and still discards detection.
50
+ static func merge(
51
+ suppliedClaimed: Bool, supplied: ReplayGainData?, detected: ReplayGainData?
52
+ ) -> ReplayGainData? {
53
+ return suppliedClaimed ? supplied : detected
54
+ }
55
+
37
56
  /// Strict mode-aware selector. See NOTES.md §25.
38
57
  ///
39
58
  /// - `.off`: returns `(nil, nil)` (no-op).