react-native-queue-player 1.2.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/consumer-rules.pro +1 -1
  4. package/android/src/main/cpp/airplay2_control.c +4 -4
  5. package/android/src/main/cpp/airplay2_control.h +4 -4
  6. package/android/src/main/cpp/airplay2_jni.cpp +3 -19
  7. package/android/src/main/cpp/airplay2_pair.c +7 -7
  8. package/android/src/main/cpp/airplay2_pair.h +2 -2
  9. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  10. package/android/src/main/cpp/airplay2_session.c +2 -2
  11. package/android/src/main/cpp/airplay_jni.cpp +8 -69
  12. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  61. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  62. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  63. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  64. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  65. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  114. package/app.plugin.js +17 -1
  115. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  116. package/ios/AVQueueBuilder.swift +120 -85
  117. package/ios/ArtworkLoader.swift +0 -7
  118. package/ios/ArtworkResolver.swift +22 -4
  119. package/ios/AssetReplayGainReader.swift +118 -0
  120. package/ios/AudioSession.swift +67 -16
  121. package/ios/AudioTapProvider.swift +284 -149
  122. package/ios/CarPlayBridge.swift +0 -12
  123. package/ios/CarPlayCoordinator.swift +18 -2
  124. package/ios/CarPlaySceneDelegate.swift +2 -2
  125. package/ios/Cast/AirPlayRouteState.swift +9 -8
  126. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  127. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  128. package/ios/Cast/Core/CastSession.swift +2 -2
  129. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  130. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  131. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  132. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  133. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  134. package/ios/CastManager.swift +1 -1
  135. package/ios/CrossfadeEngine.swift +672 -226
  136. package/ios/EQTap.swift +20 -17
  137. package/ios/Equalizer.swift +1 -1
  138. package/ios/FLACStreamInfo.swift +65 -0
  139. package/ios/GaplessEngine.swift +57 -30
  140. package/ios/InputGuards.swift +35 -5
  141. package/ios/LookaheadCache.swift +229 -58
  142. package/ios/LookaheadCachePrefetcher.swift +48 -36
  143. package/ios/MediaResponseCheck.swift +122 -0
  144. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  145. package/ios/NetworkRecoveryPolicy.swift +32 -0
  146. package/ios/NowPlayingFormatExtractor.swift +25 -18
  147. package/ios/NowPlayingInfo.swift +84 -48
  148. package/ios/OutputRouteMonitor.swift +16 -3
  149. package/ios/PendingBrowseRequests.swift +1 -1
  150. package/ios/PlaceholderArtwork.swift +9 -5
  151. package/ios/PlaybackEngine.swift +165 -76
  152. package/ios/PlaybackErrorMapping.swift +3 -3
  153. package/ios/PlaybackModeStateMachine.swift +2 -2
  154. package/ios/PlaybackNetworkMonitor.swift +73 -0
  155. package/ios/PlayerStateDerivation.swift +6 -6
  156. package/ios/QueueMutationArithmetic.swift +25 -0
  157. package/ios/QueueSkipArithmetic.swift +5 -3
  158. package/ios/QueueWindowArithmetic.swift +152 -0
  159. package/ios/ReadThroughServer.swift +283 -0
  160. package/ios/RemoteCommands.swift +44 -4
  161. package/ios/ReplayGainData.swift +22 -3
  162. package/ios/ReplayGainExtractor.swift +70 -27
  163. package/ios/Siri/VoiceDonation.swift +14 -9
  164. package/ios/StreamingBitrateProbe.swift +22 -68
  165. package/ios/SuppliedReplayGain.swift +83 -0
  166. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  167. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  168. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  169. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  170. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  171. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  172. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  173. package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
  174. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  175. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  176. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  177. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  178. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  179. package/ios/Tests/EngineSwapTests.swift +91 -0
  180. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  181. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  182. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  183. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  184. package/ios/Tests/InputGuardsTests.swift +41 -0
  185. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  186. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  187. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  188. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  189. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  190. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  191. package/ios/Tests/MediaResponseCheckTests.swift +128 -0
  192. package/ios/Tests/MutationDeferralTests.swift +220 -0
  193. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  194. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  195. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  196. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  197. package/ios/Tests/PlayerFixtures.swift +48 -0
  198. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  199. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  200. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  201. package/ios/Tests/QueueStateTests.swift +3 -1
  202. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  203. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  204. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  205. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  206. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  207. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  208. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  209. package/ios/Tests/RetryRecoveryTests.swift +418 -0
  210. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  211. package/ios/Tests/SkipIndexTests.swift +32 -2
  212. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  213. package/ios/Tests/StallRecoveryTests.swift +97 -0
  214. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  215. package/ios/Tests/TopUpWindowGateTests.swift +441 -0
  216. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  217. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  218. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  219. package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
  220. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  221. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  222. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  223. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  224. package/ios/TrackPlayer+Automotive.swift +101 -0
  225. package/ios/TrackPlayer+Cache.swift +232 -0
  226. package/ios/TrackPlayer+Config.swift +291 -0
  227. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  228. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  229. package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
  230. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  231. package/ios/TrackPlayer+Lifecycle.swift +982 -0
  232. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  233. package/ios/TrackPlayer+Queue.swift +951 -0
  234. package/ios/TrackPlayer+Recovery.swift +213 -0
  235. package/ios/TrackPlayer+Skip.swift +465 -0
  236. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  237. package/ios/TrackPlayer+State.swift +108 -0
  238. package/ios/TrackPlayer+Threading.swift +135 -0
  239. package/ios/TrackPlayer+Transport.swift +272 -0
  240. package/ios/TrackPlayer+Window.swift +227 -0
  241. package/ios/TrackPlayer.swift +331 -4619
  242. package/ios/Visualizer.swift +6 -5
  243. package/ios/tests-harness/Podfile +1 -1
  244. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  245. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  246. package/lib/module/hooks/useActiveTrack.js +29 -22
  247. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  248. package/lib/module/hooks/useCast.js +8 -26
  249. package/lib/module/hooks/useCast.js.map +1 -1
  250. package/lib/module/hooks/useEqualizer.js +19 -12
  251. package/lib/module/hooks/useEqualizer.js.map +1 -1
  252. package/lib/module/hooks/useLookaheadCache.js +3 -7
  253. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  254. package/lib/module/hooks/useQueue.js +66 -19
  255. package/lib/module/hooks/useQueue.js.map +1 -1
  256. package/lib/module/index.js +6 -3
  257. package/lib/module/index.js.map +1 -1
  258. package/lib/module/queueDelta.js +41 -0
  259. package/lib/module/queueDelta.js.map +1 -0
  260. package/lib/module/types.js +28 -5
  261. package/lib/module/types.js.map +1 -1
  262. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  263. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  264. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  265. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  266. package/lib/typescript/hooks/useCast.d.ts +6 -1
  267. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  269. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  271. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  272. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  273. package/lib/typescript/index.d.ts +2 -1
  274. package/lib/typescript/index.d.ts.map +1 -1
  275. package/lib/typescript/queueDelta.d.ts +10 -0
  276. package/lib/typescript/queueDelta.d.ts.map +1 -0
  277. package/lib/typescript/types.d.ts +168 -12
  278. package/lib/typescript/types.d.ts.map +1 -1
  279. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  280. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  281. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  282. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  283. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  284. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  285. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  320. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  321. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  322. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  323. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  324. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  325. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  326. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  327. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  328. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  329. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  330. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  331. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  332. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  333. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  334. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  335. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  336. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  337. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  338. package/package.json +8 -8
  339. package/src/TrackPlayer.nitro.ts +53 -27
  340. package/src/hooks/useActiveTrack.ts +29 -22
  341. package/src/hooks/useCast.ts +14 -15
  342. package/src/hooks/useEqualizer.ts +19 -12
  343. package/src/hooks/useLookaheadCache.ts +3 -7
  344. package/src/hooks/useQueue.ts +66 -17
  345. package/src/index.ts +13 -3
  346. package/src/queueDelta.ts +41 -0
  347. package/src/types.ts +169 -12
  348. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  349. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  350. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  351. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  352. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  353. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  354. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  355. package/ios/MetadataReader.swift +0 -555
  356. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  357. package/ios/Tests/MetadataReaderTests.swift +0 -603
  358. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  359. package/ios/Tests/VoiceDonationTests.swift +0 -26
  360. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  361. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  362. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -1,20 +1,20 @@
1
1
  import Foundation
2
2
 
3
- /// Read the four ReplayGain tags from a media URL via direct byte
4
- /// parsing (`MetadataReader`) and parse the raw string values into
5
- /// `ReplayGainData`.
3
+ /// The ReplayGain value layer: the four tag names, the numeric
4
+ /// acceptance rules, the string parsers that feed them, and the
5
+ /// assembly of a `ReplayGainData` from a name/value dictionary.
6
6
  ///
7
- /// Tag-format coverage matches `MetadataReader`:
8
- /// - MP3 ID3v2.2 / 2.3 / 2.4 TXXX (user-defined text) frames
9
- /// - FLAC Vorbis comments
10
- /// - MP4 / M4A iTunes-style freeform `----` atoms
11
- /// - Opus containers return `nil` (decoder-applied gain would
12
- /// double-correct under tag-applied RG)
7
+ /// Deliberately source-agnostic. Names arrive from
8
+ /// `AssetReplayGainReader`, which derives them from whatever shape
9
+ /// the container gave AVFoundation (ID3 `TXXX` info fields, Vorbis
10
+ /// comment keys, M4A freeform reverse-DNS keys); values arrive as the
11
+ /// raw strings those tags carry. Every entry point lands on the same
12
+ /// `acceptGainDb` / `acceptPeak` pair so one rule governs all of them.
13
13
  ///
14
14
  /// Parser tolerance follows NOTES.md §25 — `" dB"` suffix optional,
15
15
  /// `+/-` prefix optional, U+2212 minus sign normalised, period
16
- /// decimal, whitespace tolerant, NaN/Inf rejected. Description
17
- /// match is case-insensitive.
16
+ /// decimal, whitespace tolerant, NaN/Inf rejected. Name matching is
17
+ /// case-insensitive.
18
18
  enum ReplayGainExtractor {
19
19
 
20
20
  // Tag descriptions matched (case-insensitive).
@@ -23,18 +23,66 @@ enum ReplayGainExtractor {
23
23
  static let albumGainKey = "REPLAYGAIN_ALBUM_GAIN"
24
24
  static let albumPeakKey = "REPLAYGAIN_ALBUM_PEAK"
25
25
 
26
- /// Read the four ReplayGain tags from `url`. Returns `nil` if no
27
- /// RG tag was found or the source can't be read. Sub-100 ms for
28
- /// local files; ~50-300 ms for `http(s)://` (network-dependent).
29
- static func extract(from url: URL) async -> ReplayGainData? {
30
- return await MetadataReader.readReplayGainTags(url: url)
26
+ // MARK: - Tag assembly
27
+
28
+ /// Build a `ReplayGainData` from a `[name: rawValue]` dictionary.
29
+ /// Names are matched case-insensitively against the four constants
30
+ /// above; each value goes through the parser for its kind, so an
31
+ /// unparseable or out-of-band value lands as `nil` rather than
32
+ /// rejecting its siblings.
33
+ ///
34
+ /// Returns `nil` when none of the four names produced a usable
35
+ /// value — "this source carries no ReplayGain".
36
+ static func rgDataFromTags(_ tags: [String: String]) -> ReplayGainData? {
37
+ let upper = tags.reduce(into: [String: String]()) { d, kv in
38
+ d[kv.key.uppercased()] = kv.value
39
+ }
40
+ let trackGain = upper[trackGainKey].flatMap { parseGainDb($0) }
41
+ let trackPeak = upper[trackPeakKey].flatMap { parsePeak($0) }
42
+ let albumGain = upper[albumGainKey].flatMap { parseGainDb($0) }
43
+ let albumPeak = upper[albumPeakKey].flatMap { parsePeak($0) }
44
+ if trackGain == nil && trackPeak == nil
45
+ && albumGain == nil && albumPeak == nil {
46
+ return nil
47
+ }
48
+ return ReplayGainData(
49
+ trackGainDb: trackGain, trackPeak: trackPeak,
50
+ albumGainDb: albumGain, albumPeak: albumPeak
51
+ )
52
+ }
53
+
54
+ // MARK: - Numeric acceptance
55
+
56
+ /// Shared numeric acceptance for a gain in dB, whatever entry point
57
+ /// it arrived through — a tag string via `parseGainDb` or a number
58
+ /// straight from the consumer. Rejects only non-finite values: a
59
+ /// gain carries no plausible range guard, unlike a peak, and clip
60
+ /// safety is the gain clamp's job rather than the acceptor's.
61
+ static func acceptGainDb(_ value: Double) -> Double? {
62
+ guard value.isFinite else { return nil }
63
+ return value
64
+ }
65
+
66
+ /// Shared numeric acceptance for a linear peak, whatever entry point
67
+ /// it arrived through — a tag string via `parsePeak` or a number
68
+ /// straight from the consumer. Accepts `0.001 ... 2.0` inclusive:
69
+ /// `1.0` is the spec's encoding of digital full scale and is a real
70
+ /// measurement, and values above it are ordinary true peaks. Rejects
71
+ /// non-finite values, anything below the encoder-zero floor and
72
+ /// anything above an implausible true peak.
73
+ static func acceptPeak(_ value: Double) -> Double? {
74
+ guard value.isFinite else { return nil }
75
+ guard value >= 0.001, value <= 2.0 else { return nil }
76
+ return value
31
77
  }
32
78
 
33
79
  // MARK: - Gain parsing
34
80
 
35
81
  /// Parse a `REPLAYGAIN_*_GAIN` string (e.g. `"-6.54 dB"`,
36
82
  /// `"+3.21"`, `"−1.0 dB"` with U+2212 minus) into a Double dB.
37
- /// Returns `nil` for unparseable / NaN / Inf inputs.
83
+ /// Returns `nil` for unparseable input; the parsed number is then
84
+ /// put through `acceptGainDb`, so text handling is all this adds
85
+ /// over the numeric entry point.
38
86
  static func parseGainDb(_ rawInput: String) -> Double? {
39
87
  var trimmed = rawInput.trimmingCharacters(in: .whitespacesAndNewlines)
40
88
  guard !trimmed.isEmpty else { return nil }
@@ -62,21 +110,16 @@ enum ReplayGainExtractor {
62
110
  if trimmed.contains(",") { return nil }
63
111
 
64
112
  guard let value = Double(trimmed) else { return nil }
65
- if !value.isFinite { return nil }
66
- return value
113
+ return acceptGainDb(value)
67
114
  }
68
115
 
69
116
  /// Parse a `REPLAYGAIN_*_PEAK` string (linear, period decimal).
70
- /// Returns `nil` for unparseable / NaN / Inf / out-of-range
71
- /// values, or for the encoder-default sentinels (peak `< 0.001`
72
- /// or peak exactly `1.0`, both signal the analyser didn't run).
117
+ /// Returns `nil` for unparseable input; the parsed number is then
118
+ /// put through `acceptPeak`, which holds the `0.001 ... 2.0`
119
+ /// acceptance band a full-scale `1.0` included.
73
120
  static func parsePeak(_ rawInput: String) -> Double? {
74
121
  let trimmed = rawInput.trimmingCharacters(in: .whitespacesAndNewlines)
75
122
  guard let value = Double(trimmed) else { return nil }
76
- if !value.isFinite { return nil }
77
- if value > 2.0 { return nil }
78
- if value < 0.001 { return nil }
79
- if value == 1.0 { return nil }
80
- return value
123
+ return acceptPeak(value)
81
124
  }
82
125
  }
@@ -15,19 +15,24 @@ import OSLog
15
15
  enum VoiceDonation {
16
16
  private static let logger = Logger(subsystem: "com.queueplayer.lib", category: "SiriDonation")
17
17
 
18
- /// Proxy for "Siri is wired up in this build" — the lib's config
19
- /// plugin (`react-native-queue-player/app.plugin.js`) sets
20
- /// `NSSiriUsageDescription` in Info.plist whenever the consumer
21
- /// opts in with `{ siri: true }`. Reading the key from `Bundle.main`
22
- /// avoids touching `INVocabulary.shared()`, which raises an
23
- /// `NSException` (Swift cannot catch this) when the
24
- /// `com.apple.developer.siri` entitlement is missing.
25
- private static var siriWiredUp: Bool {
18
+ /// Whether the host app has declared Siri support: the lib's config plugin
19
+ /// (`react-native-queue-player/app.plugin.js`) sets `NSSiriUsageDescription`
20
+ /// in Info.plist when the consumer opts in with `{ siri: true }`.
21
+ ///
22
+ /// The second of two gates, and the weaker one. `PlayerConfig`'s
23
+ /// `voiceVocabularyDonationEnabled` is the first: it is the consumer
24
+ /// asserting the `com.apple.developer.siri` entitlement is on the target,
25
+ /// which is the requirement `INVocabulary` actually checks — it throws
26
+ /// `NSInternalInconsistencyException` without it, and Swift cannot catch
27
+ /// that. iOS exposes no API for a process to read its own entitlements, so
28
+ /// no gate here can verify the assertion. This one catches the narrower
29
+ /// misconfiguration of opting in without the usage description.
30
+ private static var hostDeclaresSiri: Bool {
26
31
  return Bundle.main.object(forInfoDictionaryKey: "NSSiriUsageDescription") != nil
27
32
  }
28
33
 
29
34
  static func donateVoiceVocabulary(_ vocabulary: VoiceVocabulary) {
30
- guard siriWiredUp else {
35
+ guard hostDeclaresSiri else {
31
36
  logger.info("Skipping vocabulary donation — Siri not configured (Info.plist missing NSSiriUsageDescription)")
32
37
  return
33
38
  }
@@ -1,14 +1,13 @@
1
1
  import AudioToolbox
2
2
  import Foundation
3
3
 
4
- /// One-shot streaming-source bitrate probe via Core Audio's
4
+ /// One-shot local-file bitrate probe via Core Audio's
5
5
  /// `AudioFileStream`.
6
6
  ///
7
- /// **Why this exists.** AVPlayer's `AVAssetTrack.estimatedDataRate`
8
- /// is unreliable for progressive streaming MP3 (returns 0 even
9
- /// after `.readyToPlay`); the access-log fallback chain
10
- /// (`event.averageAudioBitrate` / `event.indicatedBitrate`) is
11
- /// HLS-oriented and routinely returns NaN/-1 for non-HLS sources.
7
+ /// **Why this exists.** `AVAssetTrack.estimatedDataRate` returns 0
8
+ /// for some local sources, and `AudioFile`'s
9
+ /// `kAudioFilePropertyBitRate` returns 0 for some transcoded MP3s
10
+ /// (some server-transcoded MP3s exhibit this).
12
11
  /// Core Audio's `AudioFileStreamOpen` parses the bitstream from
13
12
  /// raw bytes and surfaces format/packet info as soon as the first
14
13
  /// frame headers land — same approach SwiftAudioPlayer,
@@ -33,8 +32,8 @@ import Foundation
33
32
  /// mFramesPerPacket)`. For 192 kbps CBR this converges within
34
33
  /// ~1% after ~100 packets — well under our probe window.
35
34
  ///
36
- /// **Cost.** One `URLSession` Range:`bytes=0-262143` (~256 KB)
37
- /// GET per distinct streaming URL. The 256 KB window tolerates
35
+ /// **Cost.** One 256 KB `FileHandle` read per distinct local URL,
36
+ /// and no network request at all. The 256 KB window tolerates
38
37
  /// large ID3v2 tags (sometimes 32 KB+ for embedded album art)
39
38
  /// without starving the parser of audio frames. Parse runs
40
39
  /// synchronously on a detached utility-priority Task. In-memory
@@ -54,24 +53,23 @@ enum StreamingBitrateProbe {
54
53
  return c
55
54
  }()
56
55
 
57
- /// 256 KB Range: enough to skip past large ID3v2 tags + still
56
+ /// 256 KB prefix: enough to skip past large ID3v2 tags + still
58
57
  /// gather enough MP3 frames to compute a stable average.
59
58
  private static let probeByteCount = 262_144
60
59
 
61
- /// Resolve the encoded bitrate (bits/sec) of the audio at
62
- /// `url`. Handles both `http(s)://` (URLSession Range fetch)
63
- /// and `file://` (FileHandle prefix read) URLs uniformly via
64
- /// the same AudioFileStream-based parse — the local-file
65
- /// fallback is necessary because Core Audio's `AudioFile`
66
- /// API's `kAudioFilePropertyBitRate` returns 0 for some
67
- /// transcoded MP3s (e.g. Subsonic-served MP3s the lookahead
68
- /// cache has downloaded).
60
+ /// Resolve the encoded bitrate (bits/sec) of the audio at a
61
+ /// `file://` `url` by reading its prefix with `FileHandle` and
62
+ /// running an AudioFileStream parse over it. Necessary because
63
+ /// Core Audio's `AudioFile` API's `kAudioFilePropertyBitRate`
64
+ /// returns 0 for some transcoded MP3s (e.g. server-transcoded MP3s
65
+ /// the lookahead cache has downloaded). The format extractor uses it as
66
+ /// the third-tier fallback for `file://` sources whose codec has no
67
+ /// dedicated reader.
69
68
  ///
70
- /// Returns nil when the URL scheme is unsupported, the server
71
- /// doesn't honour Range, the response/file is empty, the
72
- /// parser can't identify the format, or no packets were
73
- /// parsed in the probe window.
74
- static func bitrate(forUrl url: URL, urlSession: URLSession = .shared) async -> Double? {
69
+ /// Returns nil for a non-`file://` URL, an empty or unreadable
70
+ /// file, a format the parser can't identify, or a probe window
71
+ /// that yielded no packets. Results are cached per URL string.
72
+ static func bitrate(forUrl url: URL) async -> Double? {
75
73
  let key = url.absoluteString
76
74
  if let cached = readCache(key) {
77
75
  #if DEBUG
@@ -79,15 +77,8 @@ enum StreamingBitrateProbe {
79
77
  #endif
80
78
  return cached
81
79
  }
82
- let bps: Double?
83
- if url.isFileURL {
84
- bps = await fetchAndParseLocal(url: url)
85
- } else if let scheme = url.scheme?.lowercased(),
86
- scheme == "http" || scheme == "https" {
87
- bps = await fetchAndParseRemote(url: url, urlSession: urlSession)
88
- } else {
89
- bps = nil
90
- }
80
+ guard url.isFileURL else { return nil }
81
+ let bps = await fetchAndParseLocal(url: url)
91
82
  if let bps = bps {
92
83
  writeCache(key, bps)
93
84
  }
@@ -120,43 +111,6 @@ enum StreamingBitrateProbe {
120
111
  }
121
112
  }
122
113
 
123
- /// Trade-off: this issues a parallel HTTP request to the same
124
- /// URL AVPlayer is streaming from. On servers with low per-IP
125
- /// connection caps (some self-hosted Subsonic / Navidrome) a
126
- /// concurrent Range fetch can briefly compete for a connection
127
- /// slot. In practice the probe completes in <100 ms and the
128
- /// cache returns immediately for repeat hits, so the impact is
129
- /// bounded. Worth knowing if the player ever stalls right after
130
- /// a track change against a server with strict connection
131
- /// limits — try a dedicated `URLSession` with
132
- /// `httpMaximumConnectionsPerHost = 1` if it bites.
133
- private static func fetchAndParseRemote(
134
- url: URL, urlSession: URLSession = .shared
135
- ) async -> Double? {
136
- var req = URLRequest(url: url, timeoutInterval: 10)
137
- req.setValue("bytes=0-\(probeByteCount - 1)", forHTTPHeaderField: "Range")
138
- let data: Data
139
- do {
140
- let (responseData, response) = try await urlSession.data(for: req)
141
- if let http = response as? HTTPURLResponse,
142
- !((200...299).contains(http.statusCode)) {
143
- #if DEBUG
144
- NSLog("[RNQP-BITRATE-PROBE] non-2xx status=%d url=%@",
145
- http.statusCode, url.absoluteString)
146
- #endif
147
- return nil
148
- }
149
- data = responseData
150
- } catch {
151
- #if DEBUG
152
- NSLog("[RNQP-BITRATE-PROBE] fetch-failed err=%@ url=%@",
153
- String(describing: error), url.absoluteString)
154
- #endif
155
- return nil
156
- }
157
- return await runParse(data: data, url: url, source: "remote")
158
- }
159
-
160
114
  /// Read the first `probeByteCount` bytes of a local file via
161
115
  /// FileHandle and run the same AudioFileStream parse.
162
116
  /// FileHandle is preferred over `Data(contentsOf:)` because
@@ -0,0 +1,83 @@
1
+ import AVFoundation
2
+
3
+ /// The consumer's ReplayGain claim over one queue entry, resolved once
4
+ /// at queue-build time and carried on the `AVPlayerItem` it was built
5
+ /// for.
6
+ ///
7
+ /// The claim and the values it survived with are two separate facts, and
8
+ /// keeping them separate is the whole point of this type. A `TrackItem`
9
+ /// that supplies only an out-of-band peak has claimed its track just as
10
+ /// firmly as one that supplies all four values, so collapsing the claim
11
+ /// into "did any value survive acceptance" would hand that track back to
12
+ /// the file's own tags. Presence claims the track; validity only decides
13
+ /// what there is to apply.
14
+ ///
15
+ /// The instance therefore exists whenever any of the four fields was
16
+ /// present, and `data` is `nil` when none of those values survived
17
+ /// `ReplayGainExtractor`'s acceptance rules — a track that owns its
18
+ /// ReplayGain and plays at unity.
19
+ final class SuppliedReplayGain {
20
+ /// The supplied values that passed acceptance. `nil` means the
21
+ /// consumer claimed this track and gave nothing usable.
22
+ let data: ReplayGainData?
23
+
24
+ private init(data: ReplayGainData?) {
25
+ self.data = data
26
+ }
27
+
28
+ /// Build the claim for [track], or `nil` when the consumer supplied
29
+ /// none of the four ReplayGain fields and the track is left on
30
+ /// whatever its file carries.
31
+ ///
32
+ /// Each present value goes through the same `acceptGainDb` /
33
+ /// `acceptPeak` pair the tag strings land on, so a supplied number and
34
+ /// a tagged number are held to one rule.
35
+ static func from(track: TrackItem) -> SuppliedReplayGain? {
36
+ guard track.replayGainTrackGain != nil
37
+ || track.replayGainTrackPeak != nil
38
+ || track.replayGainAlbumGain != nil
39
+ || track.replayGainAlbumPeak != nil
40
+ else {
41
+ return nil
42
+ }
43
+ let trackGain = track.replayGainTrackGain.flatMap { ReplayGainExtractor.acceptGainDb($0) }
44
+ let trackPeak = track.replayGainTrackPeak.flatMap { ReplayGainExtractor.acceptPeak($0) }
45
+ let albumGain = track.replayGainAlbumGain.flatMap { ReplayGainExtractor.acceptGainDb($0) }
46
+ let albumPeak = track.replayGainAlbumPeak.flatMap { ReplayGainExtractor.acceptPeak($0) }
47
+ if trackGain == nil && trackPeak == nil
48
+ && albumGain == nil && albumPeak == nil {
49
+ return SuppliedReplayGain(data: nil)
50
+ }
51
+ return SuppliedReplayGain(
52
+ data: ReplayGainData(
53
+ trackGainDb: trackGain, trackPeak: trackPeak,
54
+ albumGainDb: albumGain, albumPeak: albumPeak
55
+ )
56
+ )
57
+ }
58
+ }
59
+
60
+ private var suppliedReplayGainKey: UInt8 = 0
61
+
62
+ extension AVPlayerItem {
63
+ /// The consumer's ReplayGain claim over the `TrackItem` this item was
64
+ /// built from. Stamped by `AVQueueBuilder.makeItem` at construction
65
+ /// and carried across by the crossfade engine's item rebuilds, the
66
+ /// same way `queueItemId` and `sourceURL` are.
67
+ ///
68
+ /// Stamped rather than looked up: the queue-mutating surface is
69
+ /// `setQueue` / `addToQueue` / `removeFromQueue` / `moveInQueue` /
70
+ /// `clearQueue`, and every one of them rebuilds or moves items, so
71
+ /// there is no in-place metadata edit that could leave a stamped value
72
+ /// describing a different track than the one playing.
73
+ ///
74
+ /// `nil` means the consumer supplied nothing and the item's ReplayGain
75
+ /// comes from whatever `AssetReplayGainReader` finds in the file.
76
+ var suppliedReplayGain: SuppliedReplayGain? {
77
+ get { objc_getAssociatedObject(self, &suppliedReplayGainKey) as? SuppliedReplayGain }
78
+ set {
79
+ objc_setAssociatedObject(
80
+ self, &suppliedReplayGainKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
81
+ }
82
+ }
83
+ }