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,228 @@
1
+ import XCTest
2
+ @testable import QueuePlayer
3
+
4
+ /// Unit tests for `AssetReplayGainReader.tagName(info:key:)` — the pure
5
+ /// reduction of AVFoundation's three container-dependent metadata shapes
6
+ /// to one comparable ReplayGain tag name.
7
+ ///
8
+ /// The shapes below are measured, not assumed:
9
+ /// - MP3 / ID3v2 `TXXX`: `info = REPLAYGAIN_TRACK_GAIN`, `key = TXXX`
10
+ /// - FLAC / Vorbis: `info = nil`, `key = REPLAYGAIN_TRACK_GAIN`
11
+ /// - M4A / iTunes freeform: `info = nil`,
12
+ /// `key = com.apple.iTunes.replaygain_track_gain`
13
+ ///
14
+ /// The rule: `info` when non-blank, else `key`; then the segment after
15
+ /// the last `.`; then uppercased.
16
+ final class AssetReplayGainReaderTests: XCTestCase {
17
+
18
+ private let replayGainNames = [
19
+ ReplayGainExtractor.trackGainKey,
20
+ ReplayGainExtractor.trackPeakKey,
21
+ ReplayGainExtractor.albumGainKey,
22
+ ReplayGainExtractor.albumPeakKey,
23
+ ]
24
+
25
+ private func assertNotAReplayGainName(
26
+ _ actual: String?,
27
+ _ message: String,
28
+ file: StaticString = #file, line: UInt = #line
29
+ ) {
30
+ guard let actual = actual else { return }
31
+ XCTAssertFalse(
32
+ replayGainNames.contains(actual), message, file: file, line: line
33
+ )
34
+ }
35
+
36
+ /// Every test below whose only other assertions are negative pairs
37
+ /// them with this. `assertNotAReplayGainName` is satisfied by `nil`,
38
+ /// so without a derivation that demonstrably works, "this key does
39
+ /// not resolve to a ReplayGain name" is equally satisfied by a
40
+ /// `tagName` that resolves nothing at all.
41
+ private func assertNameDerivationIsLive(
42
+ file: StaticString = #file, line: UInt = #line
43
+ ) {
44
+ XCTAssertEqual(
45
+ AssetReplayGainReader.tagName(info: nil, key: "REPLAYGAIN_TRACK_GAIN"),
46
+ ReplayGainExtractor.trackGainKey,
47
+ "positive control — a known-good key must resolve to its name",
48
+ file: file, line: line
49
+ )
50
+ }
51
+
52
+ // MARK: - The three measured container shapes
53
+
54
+ func testAnID3UserTextFrameTakesItsNameFromTheInfoField() {
55
+ XCTAssertEqual(
56
+ AssetReplayGainReader.tagName(
57
+ info: "REPLAYGAIN_TRACK_GAIN", key: "TXXX"
58
+ ),
59
+ ReplayGainExtractor.trackGainKey,
60
+ "MP3: the frame id is TXXX; the name is in the info field"
61
+ )
62
+ }
63
+
64
+ func testAVorbisCommentTakesItsNameFromTheKey() {
65
+ XCTAssertEqual(
66
+ AssetReplayGainReader.tagName(
67
+ info: nil, key: "REPLAYGAIN_TRACK_GAIN"
68
+ ),
69
+ ReplayGainExtractor.trackGainKey,
70
+ "FLAC: the key is already the name"
71
+ )
72
+ }
73
+
74
+ func testAnM4AFreeformKeyReducesToItsLastDotSegment() {
75
+ XCTAssertEqual(
76
+ AssetReplayGainReader.tagName(
77
+ info: nil, key: "com.apple.iTunes.replaygain_track_gain"
78
+ ),
79
+ ReplayGainExtractor.trackGainKey,
80
+ "M4A: reverse-DNS identifier, trimmed to its last segment and uppercased"
81
+ )
82
+ }
83
+
84
+ func testAllFourM4AFreeformKeysReduceToTheirReplayGainNames() {
85
+ let cases: [(String, String)] = [
86
+ ("com.apple.iTunes.replaygain_track_gain", ReplayGainExtractor.trackGainKey),
87
+ ("com.apple.iTunes.replaygain_track_peak", ReplayGainExtractor.trackPeakKey),
88
+ ("com.apple.iTunes.replaygain_album_gain", ReplayGainExtractor.albumGainKey),
89
+ ("com.apple.iTunes.replaygain_album_peak", ReplayGainExtractor.albumPeakKey),
90
+ ]
91
+ for (key, expected) in cases {
92
+ XCTAssertEqual(
93
+ AssetReplayGainReader.tagName(info: nil, key: key), expected,
94
+ "\(key) must reduce to \(expected)"
95
+ )
96
+ }
97
+ }
98
+
99
+ // MARK: - Info / key precedence
100
+
101
+ func testABlankInfoFieldFallsBackToTheKey() {
102
+ // Empty and whitespace-only are both blank — a nil check alone lets
103
+ // an empty info field shadow a usable key.
104
+ XCTAssertEqual(
105
+ AssetReplayGainReader.tagName(
106
+ info: "", key: "REPLAYGAIN_TRACK_GAIN"
107
+ ),
108
+ ReplayGainExtractor.trackGainKey,
109
+ "an empty info field falls back to the key"
110
+ )
111
+ XCTAssertEqual(
112
+ AssetReplayGainReader.tagName(
113
+ info: " ", key: "REPLAYGAIN_TRACK_GAIN"
114
+ ),
115
+ ReplayGainExtractor.trackGainKey,
116
+ "a whitespace-only info field falls back to the key"
117
+ )
118
+ }
119
+
120
+ func testALowercaseInfoFieldIsUppercased() {
121
+ XCTAssertEqual(
122
+ AssetReplayGainReader.tagName(
123
+ info: "replaygain_album_gain", key: "TXXX"
124
+ ),
125
+ ReplayGainExtractor.albumGainKey
126
+ )
127
+ }
128
+
129
+ func testWithNeitherInfoNorKeyThereIsNoName() {
130
+ assertNameDerivationIsLive()
131
+ XCTAssertNil(AssetReplayGainReader.tagName(info: nil, key: nil))
132
+ }
133
+
134
+ // MARK: - Names that must not reach the ReplayGain lookup
135
+
136
+ func testAnOrdinalKeyYieldsNoReplayGainName() {
137
+ // ffmpeg writes `mdta` metadata under ordinal keys ("1", "2", ...);
138
+ // the tag name is unrecoverable from them and none may match.
139
+ assertNameDerivationIsLive()
140
+ for key in ["1", "2", "10"] {
141
+ assertNotAReplayGainName(
142
+ AssetReplayGainReader.tagName(info: nil, key: key),
143
+ "ordinal key \(key) must not resolve to a ReplayGain name"
144
+ )
145
+ }
146
+ }
147
+
148
+ func testAnUnrelatedTagNameYieldsNoReplayGainName() {
149
+ assertNameDerivationIsLive()
150
+ for key in ["TITLE", "ARTIST", "com.apple.iTunes.iTunNORM"] {
151
+ assertNotAReplayGainName(
152
+ AssetReplayGainReader.tagName(info: nil, key: key),
153
+ "\(key) must not resolve to a ReplayGain name"
154
+ )
155
+ }
156
+ }
157
+
158
+ func testAKeyEndingInADotYieldsNoUsableName() {
159
+ // The last-segment split has nothing after the separator. It must
160
+ // return safely rather than trap on an out-of-range index, and must
161
+ // never hand back an empty name.
162
+ assertNameDerivationIsLive()
163
+ for key in ["com.apple.iTunes.", ".", "..", "REPLAYGAIN_TRACK_GAIN."] {
164
+ let name = AssetReplayGainReader.tagName(info: nil, key: key)
165
+ XCTAssertNotEqual(name, "", "\(key) must not yield an empty name")
166
+ assertNotAReplayGainName(
167
+ name, "\(key) must not resolve to a ReplayGain name"
168
+ )
169
+ }
170
+ }
171
+
172
+ // MARK: - Opus double-correction guard
173
+
174
+
175
+ func testIsOpusContainerRecognisesOpusExtension() {
176
+ XCTAssertTrue(
177
+ AssetReplayGainReader.isOpusContainer(
178
+ url: URL(string: "https://example.test/track.opus")!
179
+ )
180
+ )
181
+ XCTAssertTrue(
182
+ AssetReplayGainReader.isOpusContainer(
183
+ url: URL(string: "https://example.test/track.ogg")!
184
+ )
185
+ )
186
+ }
187
+
188
+ /// The guard reads the track's own URL, never the URL the asset is bound to.
189
+ ///
190
+ /// A routed track's asset URL is the read-through server's — a token with no
191
+ /// extension — so a guard that asked the asset would fail open on every Opus
192
+ /// track and apply a `REPLAYGAIN_*` tag on top of the decoder's own
193
+ /// `OpusHead.output_gain`. That is audible, and no playback suite covers it.
194
+ func testAServerTokenURLIsNotMistakenForANonOpusContainer() {
195
+ XCTAssertFalse(
196
+ AssetReplayGainReader.isOpusContainer(
197
+ url: URL(string: "http://127.0.0.1:52341/media/0123456789abcdef0123456789abcdef")!
198
+ ),
199
+ "the transport URL carries no container — the guard must read sourceURL"
200
+ )
201
+ XCTAssertTrue(
202
+ AssetReplayGainReader.isOpusContainer(
203
+ url: URL(string: "https://example.test/track.opus")!
204
+ ),
205
+ "and the media URL still identifies Opus"
206
+ )
207
+ }
208
+
209
+ func testIsOpusContainerReturnsFalseForOtherFormats() {
210
+ XCTAssertFalse(
211
+ AssetReplayGainReader.isOpusContainer(
212
+ url: URL(string: "https://example.test/track.mp3")!
213
+ )
214
+ )
215
+ XCTAssertFalse(
216
+ AssetReplayGainReader.isOpusContainer(
217
+ url: URL(string: "https://example.test/track.m4a")!
218
+ )
219
+ )
220
+ XCTAssertFalse(
221
+ AssetReplayGainReader.isOpusContainer(
222
+ url: URL(string: "https://example.test/track.flac")!
223
+ )
224
+ )
225
+ }
226
+
227
+ // MARK: - FLAC STREAMINFO
228
+ }
@@ -0,0 +1,158 @@
1
+ import AVFoundation
2
+ import XCTest
3
+ @testable import QueuePlayer
4
+
5
+ /// The tap provider's engine-facing mix work runs on the dispatch target it
6
+ /// was given: inline when the caller is already on that target, on the target
7
+ /// itself otherwise. `resetMixDispatchTarget` puts it back on main, and
8
+ /// `detachFromEngine` clears the provider off the engine it was attached to.
9
+ final class AudioTapProviderDispatchTargetTests: XCTestCase {
10
+
11
+ func testRefreshRunsInlineWhenTheCallerIsOnTheMixQueue() {
12
+ let provider = AudioTapProvider()
13
+ let engine = MixDispatchEngineStub()
14
+ provider.wireToPlaybackEngine(engine)
15
+ let queue = makeQueue(label: "inline")
16
+ provider.setMixDispatchTarget(
17
+ queue,
18
+ isCurrent: { DispatchQueue.getSpecific(key: MixDispatchEngineStub.queueKey) == "inline" })
19
+
20
+ queue.sync { provider.refreshActiveItemMixes() }
21
+
22
+ XCTAssertEqual(
23
+ engine.observedTargets, ["inline"],
24
+ "an in-queue caller reaches the engine before the call returns, without "
25
+ + "a second hop onto the queue it is already on")
26
+ }
27
+
28
+ func testRefreshLandsOnTheInjectedQueueWhenTheCallerIsNotOnIt() {
29
+ let provider = AudioTapProvider()
30
+ let engine = MixDispatchEngineStub()
31
+ provider.wireToPlaybackEngine(engine)
32
+ let landed = expectation(description: "refresh reaches the engine")
33
+ engine.onRefresh = { landed.fulfill() }
34
+ provider.setMixDispatchTarget(makeQueue(label: "deferred"), isCurrent: { false })
35
+
36
+ provider.refreshActiveItemMixes()
37
+
38
+ wait(for: [landed], timeout: 1.0)
39
+ XCTAssertEqual(engine.observedTargets, ["deferred"])
40
+ }
41
+
42
+ func testResetMixDispatchTargetReturnsTheWorkToMain() {
43
+ let provider = AudioTapProvider()
44
+ let engine = MixDispatchEngineStub()
45
+ provider.wireToPlaybackEngine(engine)
46
+ provider.setMixDispatchTarget(makeQueue(label: "player"), isCurrent: { false })
47
+
48
+ provider.resetMixDispatchTarget()
49
+ provider.refreshActiveItemMixes()
50
+
51
+ XCTAssertEqual(engine.observedTargets, [nil], "the player queue is no longer the target")
52
+ XCTAssertEqual(
53
+ engine.observedMainThreadFlags, [true],
54
+ "a main-thread caller runs inline once the target is back on main")
55
+ }
56
+
57
+ func testDetachFromEngineClearsTheEngineMixProvider() {
58
+ let provider = AudioTapProvider()
59
+ let engine = MixDispatchEngineStub()
60
+ provider.wireToPlaybackEngine(engine)
61
+ XCTAssertNotNil(engine.lastSetProvider)
62
+
63
+ provider.detachFromEngine()
64
+
65
+ XCTAssertNil(engine.lastSetProvider)
66
+ provider.refreshActiveItemMixes()
67
+ XCTAssertTrue(
68
+ engine.observedTargets.isEmpty,
69
+ "a detached provider has no engine left to refresh")
70
+ }
71
+
72
+ /// A queue tagged with its own label so the stub can report which target an
73
+ /// engine call arrived on.
74
+ private func makeQueue(label: String) -> DispatchQueue {
75
+ let queue = DispatchQueue(label: "test.mixDispatch.\(label)")
76
+ queue.setSpecific(key: MixDispatchEngineStub.queueKey, value: label)
77
+ return queue
78
+ }
79
+ }
80
+
81
+ /// Records the dispatch target of every `refreshAllItemMixes` call and the last
82
+ /// provider set on it. All other PlaybackEngine methods are no-op stubs.
83
+ private final class MixDispatchEngineStub: PlaybackEngine {
84
+
85
+ /// Tags a queue built by the test; `nil` in `observedTargets` means the call
86
+ /// arrived on a thread with no tagged queue.
87
+ static let queueKey = DispatchSpecificKey<String>()
88
+
89
+ private let lock = NSLock()
90
+ private var observed: [String?] = []
91
+ private var observedOnMain: [Bool] = []
92
+ var lastSetProvider: AudioMixProvider?
93
+ var onRefresh: (() -> Void)?
94
+
95
+ var observedTargets: [String?] {
96
+ lock.lock()
97
+ defer { lock.unlock() }
98
+ return observed
99
+ }
100
+
101
+ /// Whether each recorded call arrived on the main thread — an untagged
102
+ /// target is only evidence of "main" together with this.
103
+ var observedMainThreadFlags: [Bool] {
104
+ lock.lock()
105
+ defer { lock.unlock() }
106
+ return observedOnMain
107
+ }
108
+
109
+ func setAudioMixProvider(_ provider: AudioMixProvider?) {
110
+ lastSetProvider = provider
111
+ }
112
+
113
+ func refreshAllItemMixes() {
114
+ let target = DispatchQueue.getSpecific(key: Self.queueKey)
115
+ let onMain = Thread.isMainThread
116
+ lock.lock()
117
+ observed.append(target)
118
+ observedOnMain.append(onMain)
119
+ lock.unlock()
120
+ onRefresh?()
121
+ }
122
+
123
+ func release() {}
124
+ func setItems(_ items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) {}
125
+ @discardableResult
126
+ func insertItems(_ items: [AVPlayerItem], after: AVPlayerItem?) -> Bool { true }
127
+ func remove(_ item: AVPlayerItem) {}
128
+ func removeAllItems() {}
129
+ func canInsert(_ item: AVPlayerItem, after: AVPlayerItem?) -> Bool { true }
130
+ func play() {}
131
+ func pause() {}
132
+ func stop() {}
133
+ func seek(toIndex index: Int, position: TimeInterval) {}
134
+ func skipToNext() {}
135
+ func skipToPrevious() {}
136
+ func setVolume(_ volume: Float) {}
137
+ func setPlaybackSpeed(_ speed: Float) {}
138
+ func seedPlaybackSpeed(_ speed: Float) {}
139
+ func setRepeatMode(_ mode: RepeatMode) {}
140
+ func setPitchCorrectionMode(_ mode: PitchCorrectionMode) {}
141
+
142
+ var currentMediaItemIndex: Int { -1 }
143
+ var currentPositionSeconds: TimeInterval { 0 }
144
+ var currentDurationSeconds: TimeInterval { 0 }
145
+ var bufferedPositionSeconds: TimeInterval { 0 }
146
+ var isPlaying: Bool { false }
147
+ var timeControlStatus: AVPlayer.TimeControlStatus { .paused }
148
+ var currentMediaItem: AVPlayerItem? { nil }
149
+ var allMediaItems: [AVPlayerItem] { [] }
150
+
151
+ /// No fade to schedule — this stub exercises the rest of the surface.
152
+ var isFadePendingOrActive: Bool { false }
153
+
154
+ /// Nothing attached — this stub exercises the rest of the surface.
155
+ var attachedQueueItemIds: [String] { [] }
156
+
157
+ var delegate: PlaybackEngineDelegate?
158
+ }
@@ -96,29 +96,141 @@ final class AudioTapProviderReplayGainTests: XCTestCase {
96
96
  )
97
97
  }
98
98
 
99
- func testComputeLinearGainEncoderDefaultPeakIsIgnored() {
100
- // peak == 1.0 is the encoder-default sentinel; cap path skipped.
99
+ func testComputeLinearGainFullScalePeakCapsBoostAtUnity() {
100
+ // peak == 1.0 is digital full scale, so 1/peak == 1.0 and a +6 dB
101
+ // boost is capped back to unity. +6 dB, not -6 dB: at -6 dB the cap
102
+ // never binds and the row cannot discriminate. Mirrors Android's
103
+ // `peak equal to 1 caps the boost at unity`.
101
104
  let data = ReplayGainData(
102
- trackGainDb: -6.0, trackPeak: 1.0,
105
+ trackGainDb: 6.0, trackPeak: 1.0,
103
106
  albumGainDb: nil, albumPeak: nil
104
107
  )
105
- let raw = Float(pow(10.0, -6.0 / 20.0))
106
108
  XCTAssertEqual(
107
109
  AudioTapProvider.computeLinearGain(data: data, mode: .track),
108
- raw, accuracy: 0.001
110
+ 1.0, accuracy: 0.001
109
111
  )
110
112
  }
111
113
 
112
- func testComputeLinearGainNearZeroPeakIsIgnored() {
113
- // peak < 0.001 sentinelalso bypasses the cap.
114
+ func testComputeLinearGainUnusablePeakClampsBoostToUnity() {
115
+ // peak < 0.001 is an encoder zero no usable peak, so the boost is
116
+ // clamped to unity rather than running uncapped.
114
117
  let data = ReplayGainData(
115
- trackGainDb: -6.0, trackPeak: 0.0005,
118
+ trackGainDb: 6.0, trackPeak: 0.0005,
116
119
  albumGainDb: nil, albumPeak: nil
117
120
  )
118
- let raw = Float(pow(10.0, -6.0 / 20.0))
119
121
  XCTAssertEqual(
120
122
  AudioTapProvider.computeLinearGain(data: data, mode: .track),
121
- raw, accuracy: 0.001
123
+ 1.0, accuracy: 0.001
124
+ )
125
+ }
126
+
127
+ // MARK: - Clip safety — min(10^(gain/20), 1/peak), unity cap with no peak
128
+
129
+ func testComputeLinearGainAttenuationIsUnaffectedByTheMissingPeakClamp() {
130
+ // -20 dB, no peak. A negative gain can never clip, so the unity
131
+ // clamp only ever binds on a boost.
132
+ let data = ReplayGainData(
133
+ trackGainDb: -20.0, trackPeak: nil,
134
+ albumGainDb: nil, albumPeak: nil
135
+ )
136
+ XCTAssertEqual(
137
+ AudioTapProvider.computeLinearGain(data: data, mode: .track),
138
+ 0.100, accuracy: 0.001
139
+ )
140
+ }
141
+
142
+ func testComputeLinearGainWithNoPeakClampsBoostToUnity() {
143
+ // +6 dB, no peak. A peak cannot be derived from a gain, so boost is
144
+ // suppressed rather than run unprotected.
145
+ let data = ReplayGainData(
146
+ trackGainDb: 6.0, trackPeak: nil,
147
+ albumGainDb: nil, albumPeak: nil
148
+ )
149
+ XCTAssertEqual(
150
+ AudioTapProvider.computeLinearGain(data: data, mode: .track),
151
+ 1.000, accuracy: 0.001
152
+ )
153
+ }
154
+
155
+ func testComputeLinearGainWithNoPeakLeavesZeroGainAtUnity() {
156
+ let data = ReplayGainData(
157
+ trackGainDb: 0.0, trackPeak: nil,
158
+ albumGainDb: nil, albumPeak: nil
159
+ )
160
+ XCTAssertEqual(
161
+ AudioTapProvider.computeLinearGain(data: data, mode: .track),
162
+ 1.000, accuracy: 0.001
163
+ )
164
+ }
165
+
166
+ func testComputeLinearGainFullScalePeakDoesNotBindOnAttenuation() {
167
+ // -20 dB with peak 1.0: the cap is present at 1.0 but 0.1 is lower.
168
+ let data = ReplayGainData(
169
+ trackGainDb: -20.0, trackPeak: 1.0,
170
+ albumGainDb: nil, albumPeak: nil
171
+ )
172
+ XCTAssertEqual(
173
+ AudioTapProvider.computeLinearGain(data: data, mode: .track),
174
+ 0.100, accuracy: 0.001
175
+ )
176
+ }
177
+
178
+ func testComputeLinearGainBoostSurvivesWhenThePeakCapSitsAboveIt() {
179
+ // +6 dB ~= 1.995, peak 0.5 -> cap 2.0. The cap does not bind.
180
+ let data = ReplayGainData(
181
+ trackGainDb: 6.0, trackPeak: 0.5,
182
+ albumGainDb: nil, albumPeak: nil
183
+ )
184
+ XCTAssertEqual(
185
+ AudioTapProvider.computeLinearGain(data: data, mode: .track),
186
+ 1.995, accuracy: 0.001
187
+ )
188
+ }
189
+
190
+ func testComputeLinearGainBoostIsCappedByTheReciprocalOfThePeak() {
191
+ // +12 dB ~= 3.981, peak 0.5 -> cap 2.0. The cap wins.
192
+ let data = ReplayGainData(
193
+ trackGainDb: 12.0, trackPeak: 0.5,
194
+ albumGainDb: nil, albumPeak: nil
195
+ )
196
+ XCTAssertEqual(
197
+ AudioTapProvider.computeLinearGain(data: data, mode: .track),
198
+ 2.000, accuracy: 0.001
199
+ )
200
+ }
201
+
202
+ func testComputeLinearGainAPeakAboveUnityCapsBoostBelowUnity() {
203
+ // +6 dB with a true peak of 1.16 -> cap 1/1.16 ~= 0.862, below unity.
204
+ let data = ReplayGainData(
205
+ trackGainDb: 6.0, trackPeak: 1.16,
206
+ albumGainDb: nil, albumPeak: nil
207
+ )
208
+ XCTAssertEqual(
209
+ AudioTapProvider.computeLinearGain(data: data, mode: .track),
210
+ 0.862, accuracy: 0.001
211
+ )
212
+ }
213
+
214
+ func testComputeLinearGainClipSafetyIsMirroredInAlbumMode() {
215
+ // The whole cap applies to whichever pair the mode selects — applying
216
+ // it to the track pair only would pass every row above.
217
+ let noPeak = ReplayGainData(
218
+ trackGainDb: nil, trackPeak: nil,
219
+ albumGainDb: 6.0, albumPeak: nil
220
+ )
221
+ XCTAssertEqual(
222
+ AudioTapProvider.computeLinearGain(data: noPeak, mode: .album),
223
+ 1.000, accuracy: 0.001,
224
+ "album mode with no album peak clamps boost to unity"
225
+ )
226
+ let fullScale = ReplayGainData(
227
+ trackGainDb: nil, trackPeak: nil,
228
+ albumGainDb: 6.0, albumPeak: 1.0
229
+ )
230
+ XCTAssertEqual(
231
+ AudioTapProvider.computeLinearGain(data: fullScale, mode: .album),
232
+ 1.000, accuracy: 0.001,
233
+ "album mode with a full-scale album peak caps boost at unity"
122
234
  )
123
235
  }
124
236
 
@@ -0,0 +1,71 @@
1
+ import AVFoundation
2
+ import XCTest
3
+ @testable import QueuePlayer
4
+
5
+ /// Access-log entries arrive several times a second for a stream; the
6
+ /// bitrate re-resolve they trigger runs a bounded number of times per item,
7
+ /// since a server that never declares a bitrate would otherwise have the
8
+ /// extractor re-read the stream for the life of the track.
9
+ final class BitrateReresolveBudgetTests: XCTestCase {
10
+
11
+ private var player: TrackPlayer!
12
+
13
+ override func setUp() async throws {
14
+ try await super.setUp()
15
+ player = TrackPlayer()
16
+ }
17
+
18
+ override func tearDown() async throws {
19
+ player = nil
20
+ try await super.tearDown()
21
+ }
22
+
23
+ func testReresolvesStopAtTheBudgetForOneItem() async throws {
24
+ let player = try XCTUnwrap(self.player)
25
+ let engine = ActiveItemEngineStub()
26
+ engine.item = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/a.mp3"))
27
+ await player.onPlayerQueue { player.engine = engine }
28
+ let before = await player.onPlayerQueueValue { player.nowPlayingFormatRequestGen }
29
+
30
+ for _ in 0 ..< (TrackPlayer.maxBitrateReresolvesPerItem + 3) {
31
+ await player.onPlayerQueue { player.reresolveNowPlayingFormatForBitrate() }
32
+ try await waitUntilSettled(player)
33
+ }
34
+
35
+ let launched = await player.onPlayerQueueValue { player.nowPlayingFormatRequestGen }
36
+ XCTAssertEqual(
37
+ Int(launched - before), TrackPlayer.maxBitrateReresolvesPerItem,
38
+ "every call past the budget is dropped")
39
+ }
40
+
41
+ /// The budget is per item: the next track starts with its own.
42
+ func testANewActiveItemGetsAFreshBudget() async throws {
43
+ let player = try XCTUnwrap(self.player)
44
+ let engine = ActiveItemEngineStub()
45
+ engine.item = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/a.mp3"))
46
+ await player.onPlayerQueue { player.engine = engine }
47
+ for _ in 0 ..< TrackPlayer.maxBitrateReresolvesPerItem {
48
+ await player.onPlayerQueue { player.reresolveNowPlayingFormatForBitrate() }
49
+ try await waitUntilSettled(player)
50
+ }
51
+ let spent = await player.onPlayerQueueValue { player.nowPlayingFormatRequestGen }
52
+
53
+ engine.item = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/b.mp3"))
54
+ await player.onPlayerQueue { player.reresolveNowPlayingFormatForBitrate() }
55
+ try await waitUntilSettled(player)
56
+
57
+ let afterSwitch = await player.onPlayerQueueValue { player.nowPlayingFormatRequestGen }
58
+ XCTAssertEqual(afterSwitch, spent + 1, "the new item's first re-resolve runs")
59
+ }
60
+
61
+ /// A launched re-resolve finishes on the player queue; the next call is
62
+ /// only meaningful once it has.
63
+ private func waitUntilSettled(_ player: TrackPlayer) async throws {
64
+ for _ in 0 ..< 100 {
65
+ let inFlight = await player.onPlayerQueueValue { player.bitrateReresolveInFlight }
66
+ if !inFlight { return }
67
+ try await Task.sleep(nanoseconds: 20_000_000)
68
+ }
69
+ XCTFail("the re-resolve did not settle")
70
+ }
71
+ }
@@ -0,0 +1,72 @@
1
+ import AVFoundation
2
+ import MediaPlayer
3
+ import XCTest
4
+ @testable import QueuePlayer
5
+
6
+ /// A cast receiver is handed the consumer's own media URL, never the local
7
+ /// read-through server's.
8
+ ///
9
+ /// Cast never touches an `AVPlayerItem`: it builds from `self.tracks`, and the
10
+ /// receiver fetches the origin itself over the LAN. Read-through routing lives
11
+ /// inside `AVQueueBuilder.makeItem` and never rewrites `self.tracks`.
12
+ final class CastMediaItemTranslationTests: XCTestCase {
13
+
14
+ /// Configuring a real player writes the process-global
15
+ /// `MPNowPlayingInfoCenter` on main. Drain those writes and clear the centre
16
+ /// so they do not reach the next test class.
17
+ override func tearDown() {
18
+ let drained = expectation(description: "pending main-queue now-playing writes complete")
19
+ DispatchQueue.main.async { drained.fulfill() }
20
+ wait(for: [drained], timeout: 5.0)
21
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
22
+ super.tearDown()
23
+ }
24
+
25
+ private func track(url: String) -> TrackItem {
26
+ TrackItem(
27
+ id: "cast-1", url: url, title: "T", artist: nil, album: nil, duration: nil,
28
+ artworkUrl: nil, extras: nil,
29
+ replayGainTrackGain: nil, replayGainTrackPeak: nil,
30
+ replayGainAlbumGain: nil, replayGainAlbumPeak: nil)
31
+ }
32
+
33
+ func testAReceiverGetsTheConsumersRemoteURL() throws {
34
+ let item = try XCTUnwrap(
35
+ CastMediaItem.from(track: track(url: "https://example.com/song.mp3")))
36
+ XCTAssertEqual(item.url.absoluteString, "https://example.com/song.mp3")
37
+ }
38
+
39
+ /// Local content still goes to the cast server, which is a different server
40
+ /// on a different binding and is untouched by the read-through work.
41
+ func testALocalFileStillReachesCastAsAFileURL() throws {
42
+ let item = try XCTUnwrap(
43
+ CastMediaItem.from(track: track(url: "file:///var/mobile/song.mp3")))
44
+ XCTAssertTrue(item.url.isFileURL)
45
+ }
46
+
47
+ /// The routing decision on a configured player: the seated item addresses
48
+ /// the loopback server while the track the cast side builds from keeps the
49
+ /// consumer's URL.
50
+ func testRoutingRewritesTheSeatedItemAndNotTheTrack() async throws {
51
+ let player = TrackPlayer()
52
+ _ = try await player.configure(config: PlayerFixtures.config()).await()
53
+ let origin = "http://127.0.0.1:1/song.mp3"
54
+ _ = try await player.setQueue(tracks: [track(url: origin)], startAtIndex: 0).await()
55
+
56
+ let seatedItem = await player.onPlayerQueueValue { player.engine?.allMediaItems.first }
57
+ // `AVPlayerItem.asset` is main-actor isolated (`AVAsset` is not Sendable).
58
+ let seated = await MainActor.run { (seatedItem?.asset as? AVURLAsset)?.url }
59
+ let modelURL = await player.onPlayerQueueValue { player.tracks.first?.url }
60
+
61
+ XCTAssertEqual(seated?.host, "127.0.0.1", "the seated item reads through the loopback server")
62
+ XCTAssertNotEqual(seated?.port, 1, "on the server's own port, not the origin's")
63
+ XCTAssertEqual(modelURL, origin, "the track the cast side builds from is untouched")
64
+ XCTAssertEqual(
65
+ try XCTUnwrap(CastMediaItem.from(track: track(url: origin))).url.absoluteString, origin)
66
+
67
+ // Await destroy so its player-queue → main now-playing clear is enqueued
68
+ // before this test returns; `tearDown` then drains main and clears the
69
+ // centre so no stray write reaches the next test class.
70
+ _ = try await player.destroy().await()
71
+ }
72
+ }
@@ -188,7 +188,7 @@ final class ControllableRemoteSession: CastSessionRemotePlayer {
188
188
  private var positionCb: ((Int64, Int64, Int64) -> Void)?
189
189
 
190
190
  /// Drive the receiver state the way a live session does: a non-idle report
191
- /// latches `hasReportedState`. Set `lastState` directly to stage a state the
191
+ /// sets `hasReportedState`. Set `lastState` directly to stage a state the
192
192
  /// receiver has not reported yet.
193
193
  func fireState(_ state: RemotePlaybackStateKind) {
194
194
  lastState = state