react-native-queue-player 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/build.gradle +7 -0
  4. package/android/consumer-rules.pro +72 -3
  5. package/android/src/main/cpp/airplay2_control.c +4 -4
  6. package/android/src/main/cpp/airplay2_control.h +4 -4
  7. package/android/src/main/cpp/airplay2_jni.cpp +26 -22
  8. package/android/src/main/cpp/airplay2_pair.c +7 -7
  9. package/android/src/main/cpp/airplay2_pair.h +2 -2
  10. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  11. package/android/src/main/cpp/airplay2_session.c +2 -2
  12. package/android/src/main/cpp/airplay_jni.cpp +20 -72
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
  61. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  62. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  63. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  64. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  65. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
  114. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
  115. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  116. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  117. package/app.plugin.js +17 -1
  118. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  119. package/ios/AVQueueBuilder.swift +120 -85
  120. package/ios/ArtworkLoader.swift +0 -7
  121. package/ios/ArtworkResolver.swift +22 -4
  122. package/ios/AssetReplayGainReader.swift +118 -0
  123. package/ios/AudioSession.swift +67 -16
  124. package/ios/AudioTapProvider.swift +284 -149
  125. package/ios/CarPlayBridge.swift +0 -12
  126. package/ios/CarPlayCoordinator.swift +18 -2
  127. package/ios/CarPlaySceneDelegate.swift +2 -2
  128. package/ios/Cast/AirPlayRouteState.swift +9 -8
  129. package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
  130. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  131. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  132. package/ios/Cast/Core/CastSession.swift +11 -0
  133. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  134. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  135. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  136. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  137. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  138. package/ios/CastManager.swift +1 -1
  139. package/ios/CrossfadeEngine.swift +660 -226
  140. package/ios/EQTap.swift +20 -17
  141. package/ios/Equalizer.swift +1 -1
  142. package/ios/FLACStreamInfo.swift +65 -0
  143. package/ios/GaplessEngine.swift +57 -30
  144. package/ios/InputGuards.swift +35 -5
  145. package/ios/LookaheadCache.swift +202 -58
  146. package/ios/LookaheadCachePrefetcher.swift +48 -36
  147. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  148. package/ios/NetworkRecoveryPolicy.swift +32 -0
  149. package/ios/NowPlayingFormatExtractor.swift +25 -18
  150. package/ios/NowPlayingInfo.swift +84 -48
  151. package/ios/OutputRouteMonitor.swift +16 -3
  152. package/ios/PendingBrowseRequests.swift +1 -1
  153. package/ios/PlaceholderArtwork.swift +9 -5
  154. package/ios/PlaybackEngine.swift +165 -76
  155. package/ios/PlaybackErrorMapping.swift +3 -3
  156. package/ios/PlaybackModeStateMachine.swift +2 -2
  157. package/ios/PlaybackNetworkMonitor.swift +73 -0
  158. package/ios/PlayerStateDerivation.swift +6 -6
  159. package/ios/QueueMutationArithmetic.swift +25 -0
  160. package/ios/QueueSkipArithmetic.swift +5 -3
  161. package/ios/QueueWindowArithmetic.swift +152 -0
  162. package/ios/ReadThroughServer.swift +283 -0
  163. package/ios/RemoteCommands.swift +44 -4
  164. package/ios/ReplayGainData.swift +22 -3
  165. package/ios/ReplayGainExtractor.swift +70 -27
  166. package/ios/Siri/VoiceDonation.swift +14 -9
  167. package/ios/StreamingBitrateProbe.swift +22 -68
  168. package/ios/SuppliedReplayGain.swift +83 -0
  169. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  170. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  171. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  172. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  173. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  174. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  175. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  176. package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
  177. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  178. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  179. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  180. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  181. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  182. package/ios/Tests/EngineSwapTests.swift +91 -0
  183. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  184. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  185. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  186. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  187. package/ios/Tests/InputGuardsTests.swift +41 -0
  188. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  189. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  190. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  191. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  192. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  193. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  194. package/ios/Tests/MutationDeferralTests.swift +220 -0
  195. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  196. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  197. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  198. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  199. package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
  200. package/ios/Tests/PlayerFixtures.swift +48 -0
  201. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  202. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  203. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  204. package/ios/Tests/QueueStateTests.swift +3 -1
  205. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  206. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  207. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  208. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  209. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  210. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  211. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  212. package/ios/Tests/RetryRecoveryTests.swift +303 -0
  213. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  214. package/ios/Tests/SkipIndexTests.swift +32 -2
  215. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  216. package/ios/Tests/StallRecoveryTests.swift +97 -0
  217. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  218. package/ios/Tests/TopUpWindowGateTests.swift +394 -0
  219. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  220. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  221. package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
  222. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  223. package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
  224. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  225. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  226. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  227. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  228. package/ios/TrackPlayer+Automotive.swift +101 -0
  229. package/ios/TrackPlayer+Cache.swift +232 -0
  230. package/ios/TrackPlayer+Config.swift +291 -0
  231. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  232. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  233. package/ios/TrackPlayer+EventsDispatch.swift +968 -0
  234. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  235. package/ios/TrackPlayer+Lifecycle.swift +930 -0
  236. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  237. package/ios/TrackPlayer+Queue.swift +940 -0
  238. package/ios/TrackPlayer+Recovery.swift +196 -0
  239. package/ios/TrackPlayer+Skip.swift +455 -0
  240. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  241. package/ios/TrackPlayer+State.swift +108 -0
  242. package/ios/TrackPlayer+Threading.swift +135 -0
  243. package/ios/TrackPlayer+Transport.swift +267 -0
  244. package/ios/TrackPlayer+Window.swift +195 -0
  245. package/ios/TrackPlayer.swift +305 -4610
  246. package/ios/Visualizer.swift +6 -5
  247. package/ios/tests-harness/Podfile +1 -1
  248. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  249. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  250. package/lib/module/hooks/useActiveTrack.js +29 -22
  251. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  252. package/lib/module/hooks/useCast.js +8 -26
  253. package/lib/module/hooks/useCast.js.map +1 -1
  254. package/lib/module/hooks/useEqualizer.js +19 -12
  255. package/lib/module/hooks/useEqualizer.js.map +1 -1
  256. package/lib/module/hooks/useLookaheadCache.js +3 -7
  257. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  258. package/lib/module/hooks/useQueue.js +66 -19
  259. package/lib/module/hooks/useQueue.js.map +1 -1
  260. package/lib/module/index.js +6 -3
  261. package/lib/module/index.js.map +1 -1
  262. package/lib/module/queueDelta.js +41 -0
  263. package/lib/module/queueDelta.js.map +1 -0
  264. package/lib/module/types.js +28 -5
  265. package/lib/module/types.js.map +1 -1
  266. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  267. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  269. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useCast.d.ts +6 -1
  271. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  272. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  273. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  274. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  275. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  276. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  277. package/lib/typescript/index.d.ts +2 -1
  278. package/lib/typescript/index.d.ts.map +1 -1
  279. package/lib/typescript/queueDelta.d.ts +10 -0
  280. package/lib/typescript/queueDelta.d.ts.map +1 -0
  281. package/lib/typescript/types.d.ts +168 -12
  282. package/lib/typescript/types.d.ts.map +1 -1
  283. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  284. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  285. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  286. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  287. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  288. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  289. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  320. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  321. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  322. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  323. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  324. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  325. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  326. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  327. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  328. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  329. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  330. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  331. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  332. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  333. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  334. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  335. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  336. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  337. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  338. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  339. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  340. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  341. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  342. package/package.json +8 -8
  343. package/src/TrackPlayer.nitro.ts +53 -27
  344. package/src/hooks/useActiveTrack.ts +29 -22
  345. package/src/hooks/useCast.ts +14 -15
  346. package/src/hooks/useEqualizer.ts +19 -12
  347. package/src/hooks/useLookaheadCache.ts +3 -7
  348. package/src/hooks/useQueue.ts +66 -17
  349. package/src/index.ts +13 -3
  350. package/src/queueDelta.ts +41 -0
  351. package/src/types.ts +169 -12
  352. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  353. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
  354. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  355. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  356. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  357. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  358. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  359. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  360. package/ios/MetadataReader.swift +0 -555
  361. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  362. package/ios/Tests/MetadataReaderTests.swift +0 -603
  363. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  364. package/ios/Tests/VoiceDonationTests.swift +0 -26
  365. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  366. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  367. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -1,603 +0,0 @@
1
- import XCTest
2
- @testable import QueuePlayer
3
-
4
- /// Byte-level unit tests for `MetadataReader`'s container parsers.
5
- /// Builds synthetic ID3v2 / FLAC / MP4 byte sequences in-memory and
6
- /// verifies tag extraction surfaces the expected `[key: value]` map.
7
- final class MetadataReaderTests: XCTestCase {
8
-
9
- // MARK: - ID3v2 TXXX
10
-
11
- func testID3v23ExtractsSingleTXXXReplayGainTrackGain() {
12
- // ID3v2.3 with one TXXX frame:
13
- // description="REPLAYGAIN_TRACK_GAIN", value="-7.85 dB"
14
- let data = makeID3v23(frames: [
15
- .txxx(description: "REPLAYGAIN_TRACK_GAIN", value: "-7.85 dB")
16
- ])
17
- let tags = MetadataReader.parseID3v2(data)
18
- XCTAssertEqual(tags["REPLAYGAIN_TRACK_GAIN"], "-7.85 dB")
19
- }
20
-
21
- func testID3v23ExtractsAllFourReplayGainTags() {
22
- let data = makeID3v23(frames: [
23
- .txxx(description: "REPLAYGAIN_TRACK_GAIN", value: "-7.85 dB"),
24
- .txxx(description: "REPLAYGAIN_TRACK_PEAK", value: "1.160082"),
25
- .txxx(description: "REPLAYGAIN_ALBUM_GAIN", value: "-6.05 dB"),
26
- .txxx(description: "REPLAYGAIN_ALBUM_PEAK", value: "1.160082"),
27
- ])
28
- let tags = MetadataReader.parseID3v2(data)
29
- XCTAssertEqual(tags["REPLAYGAIN_TRACK_GAIN"], "-7.85 dB")
30
- XCTAssertEqual(tags["REPLAYGAIN_TRACK_PEAK"], "1.160082")
31
- XCTAssertEqual(tags["REPLAYGAIN_ALBUM_GAIN"], "-6.05 dB")
32
- XCTAssertEqual(tags["REPLAYGAIN_ALBUM_PEAK"], "1.160082")
33
- }
34
-
35
- func testID3v24ExtractsTXXXReplayGainTags() {
36
- // v2.4 syncsafe frame size — verify the 28-bit decode path.
37
- let data = makeID3v24(frames: [
38
- .txxx(description: "REPLAYGAIN_TRACK_GAIN", value: "-7.85 dB"),
39
- ])
40
- let tags = MetadataReader.parseID3v2(data)
41
- XCTAssertEqual(tags["REPLAYGAIN_TRACK_GAIN"], "-7.85 dB")
42
- }
43
-
44
- func testID3v23ReturnsEmptyForNonReplayGainTags() {
45
- let data = makeID3v23(frames: [
46
- .txxx(description: "TITLE", value: "A Song"),
47
- .txxx(description: "ARTIST", value: "Some Band"),
48
- ])
49
- let tags = MetadataReader.parseID3v2(data)
50
- // Parser returns ALL TXXX descriptions; consumer filters for RG.
51
- // Confirm RG keys absent + non-RG present.
52
- XCTAssertNil(tags["REPLAYGAIN_TRACK_GAIN"])
53
- XCTAssertEqual(tags["TITLE"], "A Song")
54
- }
55
-
56
- func testID3v23ReturnsEmptyOnNonID3Bytes() {
57
- let data = Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
58
- let tags = MetadataReader.parseID3v2(data)
59
- XCTAssertTrue(tags.isEmpty)
60
- }
61
-
62
- /// foobar2000 emits TXXX values with a trailing `\0` even though
63
- /// the spec leaves the final field unterminated. Decoded values
64
- /// must be trimmed of the trailing null so downstream `Double.init`
65
- /// parses the gain string cleanly.
66
- func testID3v23TXXXWithEncodingZeroAndTrailingNullValue() {
67
- // encoding=0 ISO-8859-1, description "REPLAYGAIN_TRACK_GAIN" + 00,
68
- // value "-7.85 dB" + trailing 00 (foobar2000-style).
69
- var payload = Data([0x00])
70
- payload.append("REPLAYGAIN_TRACK_GAIN".data(using: .isoLatin1)!)
71
- payload.append(0x00)
72
- payload.append("-7.85 dB".data(using: .isoLatin1)!)
73
- payload.append(0x00)
74
- var tag = Data()
75
- tag.append("TXXX".data(using: .ascii)!)
76
- tag.append(beUInt32(UInt32(payload.count)))
77
- tag.append(Data([0x00, 0x00]))
78
- tag.append(payload)
79
- var full = Data()
80
- full.append("ID3".data(using: .ascii)!)
81
- full.append(0x03) // major version
82
- full.append(0x00) // minor
83
- full.append(0x00) // flags
84
- full.append(syncsafeUInt32(UInt32(tag.count)))
85
- full.append(tag)
86
- let tags = MetadataReader.parseID3v2(full)
87
- XCTAssertEqual(tags["REPLAYGAIN_TRACK_GAIN"], "-7.85 dB",
88
- "trailing-null TXXX value should round-trip clean — no \\0 in decoded value")
89
- let rg = MetadataReader.rgDataFromTags(tags)
90
- XCTAssertNotNil(rg)
91
- XCTAssertEqual(rg?.trackGainDb, -7.85)
92
- }
93
-
94
- /// foobar2000 + most ID3v2.3 taggers write TXXX with encoding=01
95
- /// (UTF-16 with BOM). Description and value each carry their own
96
- /// `0xFF 0xFE` BOM. Decoded strings must NOT retain the BOM as a
97
- /// leading U+FEFF — that would prevent `REPLAYGAIN_*` lookups.
98
- func testID3v23TXXXWithUTF16BOMStripsBOMFromDescriptionAndValue() {
99
- let payload = utf16BOMTXXXFramePayload(
100
- description: "REPLAYGAIN_TRACK_GAIN",
101
- value: "-7.85 dB"
102
- )
103
- var tag = Data()
104
- tag.append("TXXX".data(using: .ascii)!)
105
- tag.append(beUInt32(UInt32(payload.count)))
106
- tag.append(Data([0x00, 0x00]))
107
- tag.append(payload)
108
- var full = Data()
109
- full.append("ID3".data(using: .ascii)!)
110
- full.append(0x03) // major version
111
- full.append(0x00) // minor
112
- full.append(0x00) // flags
113
- full.append(syncsafeUInt32(UInt32(tag.count)))
114
- full.append(tag)
115
- let tags = MetadataReader.parseID3v2(full)
116
- XCTAssertEqual(tags["REPLAYGAIN_TRACK_GAIN"], "-7.85 dB",
117
- "BOM-prefixed UTF-16 description should map to bare REPLAYGAIN_TRACK_GAIN key")
118
- }
119
-
120
- // MARK: - FLAC Vorbis comments
121
-
122
- func testFLACExtractsVorbisReplayGainTags() {
123
- let data = makeFLAC(comments: [
124
- "REPLAYGAIN_TRACK_GAIN=4.43 dB",
125
- "REPLAYGAIN_TRACK_PEAK=0.621351",
126
- "REPLAYGAIN_ALBUM_GAIN=0.30 dB",
127
- "REPLAYGAIN_ALBUM_PEAK=0.923366",
128
- ])
129
- let tags = MetadataReader.parseFLAC(data)
130
- XCTAssertEqual(tags["REPLAYGAIN_TRACK_GAIN"], "4.43 dB")
131
- XCTAssertEqual(tags["REPLAYGAIN_TRACK_PEAK"], "0.621351")
132
- XCTAssertEqual(tags["REPLAYGAIN_ALBUM_GAIN"], "0.30 dB")
133
- XCTAssertEqual(tags["REPLAYGAIN_ALBUM_PEAK"], "0.923366")
134
- }
135
-
136
- func testFLACReturnsEmptyOnNonFLACBytes() {
137
- let data = Data([0x00, 0x01, 0x02, 0x03])
138
- let tags = MetadataReader.parseFLAC(data)
139
- XCTAssertTrue(tags.isEmpty)
140
- }
141
-
142
- func testFLACStreamInfoExtractsSampleRateAndTotalSamples() throws {
143
- // Build a STREAMINFO-only FLAC header.
144
- // sampleRate=44100, channels=2, bitsPerSample=16, totalSamples=1234567
145
- let info = makeFLACStreamInfo(
146
- sampleRate: 44100,
147
- channels: 2,
148
- bitsPerSample: 16,
149
- totalSamples: 1_234_567
150
- )
151
- let parsed = try XCTUnwrap(MetadataReader.parseFLACStreamInfo(info))
152
- XCTAssertEqual(parsed.sampleRate, 44100)
153
- XCTAssertEqual(parsed.channelCount, 2)
154
- XCTAssertEqual(parsed.bitsPerSample, 16)
155
- XCTAssertEqual(parsed.totalSamples, 1_234_567)
156
- }
157
-
158
- func testFLACStreamInfoHighSampleRate() throws {
159
- // foobar2000 + some lossless masters use 96 kHz / 24-bit. Verify
160
- // the 20-bit sampleRate field isn't truncated.
161
- let info = makeFLACStreamInfo(
162
- sampleRate: 96000,
163
- channels: 2,
164
- bitsPerSample: 24,
165
- totalSamples: 9_876_543
166
- )
167
- let parsed = try XCTUnwrap(MetadataReader.parseFLACStreamInfo(info))
168
- XCTAssertEqual(parsed.sampleRate, 96000)
169
- XCTAssertEqual(parsed.bitsPerSample, 24)
170
- XCTAssertEqual(parsed.totalSamples, 9_876_543)
171
- }
172
-
173
- func testFLACStreamInfoReturnsNilOnNonFLACBytes() {
174
- XCTAssertNil(MetadataReader.parseFLACStreamInfo(
175
- Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
176
- ))
177
- }
178
-
179
- func testFLACSkipsStreamInfoAndReachesVorbisComment() {
180
- // STREAMINFO (type 0, 34 bytes), then VORBIS_COMMENT (type 4).
181
- // Verify the block walker advances past the first block.
182
- let data = makeFLAC(
183
- includeStreamInfo: true,
184
- comments: ["REPLAYGAIN_TRACK_GAIN=-1.00 dB"]
185
- )
186
- let tags = MetadataReader.parseFLAC(data)
187
- XCTAssertEqual(tags["REPLAYGAIN_TRACK_GAIN"], "-1.00 dB")
188
- }
189
-
190
- // MARK: - MP4 freeform iTunes-style atoms
191
-
192
- func testMP4ExtractsFreeformReplayGainTags() {
193
- let data = makeMP4(freeformTags: [
194
- "REPLAYGAIN_TRACK_GAIN": "-3.21 dB",
195
- "REPLAYGAIN_ALBUM_GAIN": "-2.10 dB",
196
- ])
197
- let tags = MetadataReader.parseMP4(data)
198
- XCTAssertEqual(tags["REPLAYGAIN_TRACK_GAIN"], "-3.21 dB")
199
- XCTAssertEqual(tags["REPLAYGAIN_ALBUM_GAIN"], "-2.10 dB")
200
- }
201
-
202
- func testMP4ReturnsEmptyWhenNoIlstAtom() {
203
- // Just an ftyp atom with no moov.
204
- var data = Data()
205
- data.append(makeAtom(type: "ftyp", body: Data(repeating: 0, count: 16)))
206
- let tags = MetadataReader.parseMP4(data)
207
- XCTAssertTrue(tags.isEmpty)
208
- }
209
-
210
- // MARK: - Format dispatch (parseTags)
211
-
212
- func testParseTagsDispatchesByMagicBytes() {
213
- let mp3 = makeID3v23(frames: [
214
- .txxx(description: "REPLAYGAIN_TRACK_GAIN", value: "-1.0 dB")
215
- ])
216
- XCTAssertEqual(
217
- MetadataReader.parseTags(from: mp3)["REPLAYGAIN_TRACK_GAIN"], "-1.0 dB"
218
- )
219
-
220
- let flac = makeFLAC(comments: ["REPLAYGAIN_TRACK_GAIN=-2.0 dB"])
221
- XCTAssertEqual(
222
- MetadataReader.parseTags(from: flac)["REPLAYGAIN_TRACK_GAIN"], "-2.0 dB"
223
- )
224
-
225
- let mp4 = makeMP4(freeformTags: ["REPLAYGAIN_TRACK_GAIN": "-3.0 dB"])
226
- XCTAssertEqual(
227
- MetadataReader.parseTags(from: mp4)["REPLAYGAIN_TRACK_GAIN"], "-3.0 dB"
228
- )
229
- }
230
-
231
- // MARK: - rgDataFromTags integration
232
-
233
- func testRgDataFromTagsParsesAllFourFields() throws {
234
- let data = try XCTUnwrap(MetadataReader.rgDataFromTags([
235
- "REPLAYGAIN_TRACK_GAIN": "-12.0 dB",
236
- "REPLAYGAIN_TRACK_PEAK": "0.95",
237
- "REPLAYGAIN_ALBUM_GAIN": "-8.0 dB",
238
- "REPLAYGAIN_ALBUM_PEAK": "0.90",
239
- ]))
240
- XCTAssertEqual(try XCTUnwrap(data.trackGainDb), -12.0, accuracy: 0.001)
241
- XCTAssertEqual(try XCTUnwrap(data.trackPeak), 0.95, accuracy: 0.001)
242
- XCTAssertEqual(try XCTUnwrap(data.albumGainDb), -8.0, accuracy: 0.001)
243
- XCTAssertEqual(try XCTUnwrap(data.albumPeak), 0.90, accuracy: 0.001)
244
- }
245
-
246
- func testRgDataFromTagsAcceptsLowercaseKeys() throws {
247
- let data = try XCTUnwrap(MetadataReader.rgDataFromTags([
248
- "replaygain_track_gain": "-6.5 dB",
249
- ]))
250
- XCTAssertEqual(try XCTUnwrap(data.trackGainDb), -6.5, accuracy: 0.001)
251
- }
252
-
253
- func testRgDataFromTagsReturnsNilForEmptyDict() {
254
- XCTAssertNil(MetadataReader.rgDataFromTags([:]))
255
- }
256
-
257
- func testRgDataFromTagsReturnsNilWhenAllValuesUnparseable() {
258
- XCTAssertNil(MetadataReader.rgDataFromTags([
259
- "REPLAYGAIN_TRACK_GAIN": "garbage",
260
- "REPLAYGAIN_TRACK_PEAK": "1.0", // encoder default → rejected
261
- ]))
262
- }
263
-
264
- // MARK: - Opus container guard
265
-
266
- func testIsOpusContainerRecognisesOpusExtension() {
267
- XCTAssertTrue(
268
- MetadataReader.isOpusContainer(
269
- url: URL(string: "https://example.test/track.opus")!
270
- )
271
- )
272
- XCTAssertTrue(
273
- MetadataReader.isOpusContainer(
274
- url: URL(string: "https://example.test/track.ogg")!
275
- )
276
- )
277
- }
278
-
279
- func testIsOpusContainerReturnsFalseForOtherFormats() {
280
- XCTAssertFalse(
281
- MetadataReader.isOpusContainer(
282
- url: URL(string: "https://example.test/track.mp3")!
283
- )
284
- )
285
- XCTAssertFalse(
286
- MetadataReader.isOpusContainer(
287
- url: URL(string: "https://example.test/track.m4a")!
288
- )
289
- )
290
- XCTAssertFalse(
291
- MetadataReader.isOpusContainer(
292
- url: URL(string: "https://example.test/track.flac")!
293
- )
294
- )
295
- }
296
-
297
- // MARK: - End-to-end with file bytes
298
-
299
- func testReadReplayGainTagsFromTempFileMP3() async throws {
300
- let bytes = makeID3v23(frames: [
301
- .txxx(description: "REPLAYGAIN_TRACK_GAIN", value: "-7.85 dB"),
302
- .txxx(description: "REPLAYGAIN_TRACK_PEAK", value: "1.160082"),
303
- ])
304
- let url = try writeTempFile(bytes: bytes, suffix: ".mp3")
305
- defer { try? FileManager.default.removeItem(at: url) }
306
- let result = await MetadataReader.readReplayGainTags(url: url)
307
- let data = try XCTUnwrap(result)
308
- XCTAssertEqual(try XCTUnwrap(data.trackGainDb), -7.85, accuracy: 0.001)
309
- XCTAssertEqual(try XCTUnwrap(data.trackPeak), 1.160082, accuracy: 0.001)
310
- XCTAssertNil(data.albumGainDb)
311
- XCTAssertNil(data.albumPeak)
312
- }
313
-
314
- func testReadReplayGainTagsFromTempFileFLAC() async throws {
315
- let bytes = makeFLAC(comments: [
316
- "REPLAYGAIN_TRACK_GAIN=4.43 dB",
317
- "REPLAYGAIN_ALBUM_GAIN=0.30 dB",
318
- ])
319
- let url = try writeTempFile(bytes: bytes, suffix: ".flac")
320
- defer { try? FileManager.default.removeItem(at: url) }
321
- let result = await MetadataReader.readReplayGainTags(url: url)
322
- let data = try XCTUnwrap(result)
323
- XCTAssertEqual(try XCTUnwrap(data.trackGainDb), 4.43, accuracy: 0.001)
324
- XCTAssertEqual(try XCTUnwrap(data.albumGainDb), 0.30, accuracy: 0.001)
325
- }
326
-
327
- func testReadReplayGainTagsReturnsNilForOpusURL() async {
328
- let url = URL(string: "https://example.test/track.opus")!
329
- let result = await MetadataReader.readReplayGainTags(url: url)
330
- XCTAssertNil(result)
331
- }
332
-
333
- func testReadReplayGainTagsCompletesSubSecond() async throws {
334
- // Latency budget: a local-file extract has no excuse to be slow.
335
- // ID3v2 / FLAC / MP4 metadata sits at file start; the parser
336
- // walks ≤256 KB of bytes in pure Swift. 100 ms is generous on
337
- // any host; missing this budget is a regression.
338
- let bytes = makeID3v23(frames: [
339
- .txxx(description: "REPLAYGAIN_TRACK_GAIN", value: "-7.85 dB"),
340
- ])
341
- let url = try writeTempFile(bytes: bytes, suffix: ".mp3")
342
- defer { try? FileManager.default.removeItem(at: url) }
343
- let started = Date()
344
- _ = await MetadataReader.readReplayGainTags(url: url)
345
- let elapsed = Date().timeIntervalSince(started)
346
- XCTAssertLessThan(elapsed, 0.1,
347
- "extract took \(elapsed)s for a small local file — should be <100 ms")
348
- }
349
-
350
- // MARK: - Test helpers — byte-level fixture builders
351
-
352
- private func writeTempFile(bytes: Data, suffix: String) throws -> URL {
353
- let url = FileManager.default.temporaryDirectory
354
- .appendingPathComponent(UUID().uuidString + suffix)
355
- try bytes.write(to: url)
356
- return url
357
- }
358
-
359
- private enum Frame {
360
- case txxx(description: String, value: String)
361
- }
362
-
363
- /// Build a TXXX frame body with encoding=01 (UTF-16 with BOM).
364
- /// Description gets `0xFF 0xFE` BOM + UTF-16LE encoded chars + `00 00`
365
- /// terminator. Value follows with its own BOM + chars (no trailing
366
- /// terminator).
367
- private func utf16BOMTXXXFramePayload(
368
- description: String,
369
- value: String
370
- ) -> Data {
371
- var payload = Data([0x01]) // encoding byte: UTF-16 with BOM
372
- payload.append(contentsOf: [0xFF, 0xFE])
373
- payload.append(description.data(using: .utf16LittleEndian) ?? Data())
374
- payload.append(contentsOf: [0x00, 0x00]) // null terminator
375
- payload.append(contentsOf: [0xFF, 0xFE])
376
- payload.append(value.data(using: .utf16LittleEndian) ?? Data())
377
- return payload
378
- }
379
-
380
- /// Build an ID3v2.3 tag containing the given TXXX frames.
381
- /// Returns the tag bytes only — no MP3 audio frames follow.
382
- private func makeID3v23(frames: [Frame]) -> Data {
383
- return makeID3v2(majorVersion: 3, frames: frames)
384
- }
385
-
386
- /// Build an ID3v2.4 tag containing the given TXXX frames.
387
- private func makeID3v24(frames: [Frame]) -> Data {
388
- return makeID3v2(majorVersion: 4, frames: frames)
389
- }
390
-
391
- private func makeID3v2(majorVersion: UInt8, frames: [Frame]) -> Data {
392
- var frameBlock = Data()
393
- for frame in frames {
394
- switch frame {
395
- case .txxx(let desc, let value):
396
- // TXXX payload: encoding (0x03 UTF-8) + description + 0x00 + value
397
- var payload = Data([0x03])
398
- payload.append(desc.data(using: .utf8) ?? Data())
399
- payload.append(0x00)
400
- payload.append(value.data(using: .utf8) ?? Data())
401
- // Frame header: 4-byte ID, 4-byte size, 2-byte flags.
402
- frameBlock.append("TXXX".data(using: .ascii)!)
403
- if majorVersion >= 4 {
404
- frameBlock.append(syncsafeUInt32(UInt32(payload.count)))
405
- } else {
406
- frameBlock.append(beUInt32(UInt32(payload.count)))
407
- }
408
- frameBlock.append(Data([0x00, 0x00]))
409
- frameBlock.append(payload)
410
- }
411
- }
412
- var tag = Data()
413
- tag.append("ID3".data(using: .ascii)!)
414
- tag.append(majorVersion)
415
- tag.append(0x00) // minor version
416
- tag.append(0x00) // flags
417
- tag.append(syncsafeUInt32(UInt32(frameBlock.count)))
418
- tag.append(frameBlock)
419
- return tag
420
- }
421
-
422
- /// 28-bit syncsafe encode (used by ID3v2 tag size + v2.4 frame size).
423
- private func syncsafeUInt32(_ v: UInt32) -> Data {
424
- return Data([
425
- UInt8((v >> 21) & 0x7f),
426
- UInt8((v >> 14) & 0x7f),
427
- UInt8((v >> 7) & 0x7f),
428
- UInt8(v & 0x7f),
429
- ])
430
- }
431
-
432
- private func beUInt32(_ v: UInt32) -> Data {
433
- return Data([
434
- UInt8((v >> 24) & 0xff),
435
- UInt8((v >> 16) & 0xff),
436
- UInt8((v >> 8) & 0xff),
437
- UInt8(v & 0xff),
438
- ])
439
- }
440
-
441
- private func leUInt32(_ v: UInt32) -> Data {
442
- return Data([
443
- UInt8(v & 0xff),
444
- UInt8((v >> 8) & 0xff),
445
- UInt8((v >> 16) & 0xff),
446
- UInt8((v >> 24) & 0xff),
447
- ])
448
- }
449
-
450
- /// Build a minimal FLAC stream — `fLaC` magic + optional STREAMINFO
451
- /// + VORBIS_COMMENT block. The VORBIS_COMMENT is marked last when
452
- /// it's the only block (or the last block if STREAMINFO precedes).
453
- private func makeFLAC(
454
- includeStreamInfo: Bool = false,
455
- comments: [String]
456
- ) -> Data {
457
- var data = Data()
458
- data.append("fLaC".data(using: .ascii)!)
459
- if includeStreamInfo {
460
- // STREAMINFO is type 0, 34-byte body. Not last (VORBIS_COMMENT
461
- // follows). Body content doesn't matter for the parser test.
462
- data.append(0x00)
463
- data.append(beUInt24(34))
464
- data.append(Data(repeating: 0, count: 34))
465
- }
466
- // Build VORBIS_COMMENT block body.
467
- var body = Data()
468
- let vendor = "test-vendor".data(using: .utf8)!
469
- body.append(leUInt32(UInt32(vendor.count)))
470
- body.append(vendor)
471
- body.append(leUInt32(UInt32(comments.count)))
472
- for c in comments {
473
- let bytes = c.data(using: .utf8)!
474
- body.append(leUInt32(UInt32(bytes.count)))
475
- body.append(bytes)
476
- }
477
- // Header: last-block flag (0x80) + type (4)
478
- data.append(0x84)
479
- data.append(beUInt24(UInt32(body.count)))
480
- data.append(body)
481
- return data
482
- }
483
-
484
- /// Build a FLAC header with only a STREAMINFO block (last-block
485
- /// flag set), populated with the given values. Layout per FLAC
486
- /// spec — 16+16+24+24 bits of block sizes are zeroed; the bits
487
- /// the parser actually reads (sampleRate, channels, bitsPerSample,
488
- /// totalSamples) are packed in.
489
- private func makeFLACStreamInfo(
490
- sampleRate: UInt32,
491
- channels: Int,
492
- bitsPerSample: Int,
493
- totalSamples: Int64
494
- ) -> Data {
495
- var data = Data()
496
- data.append("fLaC".data(using: .ascii)!)
497
- // Block header: last-block flag (0x80) + type 0 (STREAMINFO) +
498
- // 24-bit size = 34 bytes.
499
- data.append(0x80)
500
- data.append(beUInt24(34))
501
- // Body. 34 bytes total.
502
- var body = Data(repeating: 0, count: 34)
503
- // minBlockSize, maxBlockSize, minFrameSize, maxFrameSize: bytes
504
- // 0..9, leave zero. Bytes 10..12 contain the 20-bit sampleRate
505
- // followed by 3-bit channels-1 and 5-bit bitsPerSample-1 split
506
- // across bytes 12..13. totalSamples is 36 bits at bytes 13..17.
507
- let sr = UInt32(sampleRate) & 0x000FFFFF
508
- let chMinusOne = UInt8((channels - 1) & 0x07)
509
- let bpsMinusOne = UInt8((bitsPerSample - 1) & 0x1F)
510
- body[10] = UInt8((sr >> 12) & 0xFF)
511
- body[11] = UInt8((sr >> 4) & 0xFF)
512
- body[12] = UInt8(((sr & 0x0F) << 4) | (UInt32(chMinusOne) << 1) | UInt32(bpsMinusOne >> 4))
513
- body[13] = UInt8(((bpsMinusOne & 0x0F) << 4) | UInt8((totalSamples >> 32) & 0x0F))
514
- body[14] = UInt8((totalSamples >> 24) & 0xFF)
515
- body[15] = UInt8((totalSamples >> 16) & 0xFF)
516
- body[16] = UInt8((totalSamples >> 8) & 0xFF)
517
- body[17] = UInt8(totalSamples & 0xFF)
518
- // Bytes 18..33: MD5, leave zero.
519
- data.append(body)
520
- return data
521
- }
522
-
523
- private func beUInt24(_ v: UInt32) -> Data {
524
- return Data([
525
- UInt8((v >> 16) & 0xff),
526
- UInt8((v >> 8) & 0xff),
527
- UInt8(v & 0xff),
528
- ])
529
- }
530
-
531
- /// Wrap a body in an MP4 atom (`size` BE u32 + 4-byte type + body).
532
- private func makeAtom(type: String, body: Data) -> Data {
533
- var data = Data()
534
- let total = UInt32(8 + body.count)
535
- data.append(beUInt32(total))
536
- data.append(type.data(using: .ascii)!)
537
- data.append(body)
538
- return data
539
- }
540
-
541
- /// Build a minimal MP4 with `moov.udta.meta.ilst` containing
542
- /// freeform `----` atoms for each (key, value) pair.
543
- private func makeMP4(freeformTags: [String: String]) -> Data {
544
- // Build ilst body — sequence of `----` atoms.
545
- var ilstBody = Data()
546
- for (key, value) in freeformTags {
547
- var freeformBody = Data()
548
- // mean atom: 4-byte version+flags + UTF-8 issuer.
549
- var meanInner = Data([0x00, 0x00, 0x00, 0x00])
550
- meanInner.append("com.apple.iTunes".data(using: .utf8)!)
551
- freeformBody.append(makeAtom(type: "mean", body: meanInner))
552
- // name atom: 4-byte version+flags + UTF-8 key.
553
- var nameInner = Data([0x00, 0x00, 0x00, 0x00])
554
- nameInner.append(key.data(using: .utf8)!)
555
- freeformBody.append(makeAtom(type: "name", body: nameInner))
556
- // data atom: 4-byte type indicator (1=UTF-8) + 4-byte locale + value.
557
- var dataInner = Data([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00])
558
- dataInner.append(value.data(using: .utf8)!)
559
- freeformBody.append(makeAtom(type: "data", body: dataInner))
560
- ilstBody.append(makeAtom(type: "----", body: freeformBody))
561
- }
562
- let ilst = makeAtom(type: "ilst", body: ilstBody)
563
- // meta atom: 4-byte version+flags prefix + ilst.
564
- var metaBody = Data([0x00, 0x00, 0x00, 0x00])
565
- metaBody.append(ilst)
566
- let meta = makeAtom(type: "meta", body: metaBody)
567
- let udta = makeAtom(type: "udta", body: meta)
568
- let moov = makeAtom(type: "moov", body: udta)
569
- // ftyp box at the start.
570
- let ftyp = makeAtom(type: "ftyp", body: Data([
571
- 0x4d, 0x34, 0x41, 0x20, // M4A_
572
- 0x00, 0x00, 0x00, 0x00, // minor version
573
- 0x4d, 0x34, 0x41, 0x20, // compatible brand
574
- ]))
575
- var data = Data()
576
- data.append(ftyp)
577
- data.append(moov)
578
- return data
579
- }
580
-
581
- // MARK: - Remote fetch uses the injected session (no real network)
582
-
583
- func testFetchPrefixBytesUsesInjectedSessionForRemoteUrl() async {
584
- let config = URLSessionConfiguration.ephemeral
585
- config.protocolClasses = [StubURLProtocol.self]
586
- let session = URLSession(configuration: config)
587
- StubURLProtocol.reset()
588
- defer { StubURLProtocol.reset() }
589
- // `.test` is a reserved non-resolvable TLD — if the read ever hit the
590
- // real network instead of the injected session it would fail with
591
- // -1003 and never reach the stub.
592
- let url = URL(string: "https://example.test/meta.mp3")!
593
- let blob = Data("ID3-mock-prefix-bytes".utf8)
594
- StubURLProtocol.stub(url: url, data: blob, statusCode: 200)
595
-
596
- let bytes = await MetadataReader.fetchPrefixBytes(url: url, urlSession: session)
597
-
598
- XCTAssertEqual(bytes, blob, "remote prefix read must resolve via the injected session")
599
- XCTAssertEqual(
600
- StubURLProtocol.requestCount(for: url), 1,
601
- "must fetch through the injected session, never URLSession.shared")
602
- }
603
- }
@@ -1,92 +0,0 @@
1
- import AVFoundation
2
- import XCTest
3
- @testable import QueuePlayer
4
-
5
- /// Type-check coverage for `PlaybackEngine` + `PlaybackEngineDelegate`
6
- /// + `AudioMixProvider`. The protocols have no runtime; this suite
7
- /// exercises the type surface so a regression that drops a method
8
- /// or rotates a parameter shape surfaces immediately. Goes away once
9
- /// the real `GaplessEngine` impl pins the type surface from a
10
- /// runtime perspective.
11
- final class PlaybackEngineProtocolTests: XCTestCase {
12
-
13
- func testProtocolSurfaceCompiles() {
14
- let stub = NoopPlaybackEngine()
15
- XCTAssertEqual(stub.currentMediaItemIndex, -1)
16
- XCTAssertEqual(stub.currentPositionSeconds, 0)
17
- XCTAssertEqual(stub.currentDurationSeconds, 0)
18
- XCTAssertEqual(stub.bufferedPositionSeconds, 0)
19
- XCTAssertFalse(stub.isPlaying)
20
- XCTAssertNil(stub.currentMediaItem)
21
- XCTAssertEqual(stub.allMediaItems.count, 0)
22
- }
23
-
24
- func testDelegateSurfaceCompiles() {
25
- let delegate = NoopDelegate()
26
- let stub = NoopPlaybackEngine()
27
- stub.delegate = delegate
28
- XCTAssertTrue(stub.delegate === delegate)
29
- }
30
-
31
- func testAudioMixProviderRegistration() {
32
- let stub = NoopPlaybackEngine()
33
- let provider = NoopMixProvider()
34
- stub.setAudioMixProvider(provider)
35
- stub.refreshAllItemMixes()
36
- stub.setAudioMixProvider(nil)
37
- }
38
- }
39
-
40
- private final class NoopPlaybackEngine: PlaybackEngine {
41
- func prepare(items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) async {}
42
- func release() {}
43
- func setItems(_ items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) {}
44
- @discardableResult
45
- func insertItems(_ items: [AVPlayerItem], after: AVPlayerItem?) -> Bool { true }
46
- func remove(_ item: AVPlayerItem) {}
47
- func removeAllItems() {}
48
- func canInsert(_ item: AVPlayerItem, after: AVPlayerItem?) -> Bool { true }
49
- func play() {}
50
- func pause() {}
51
- func stop() {}
52
- func seek(toIndex index: Int, position: TimeInterval) {}
53
- func skipToNext() {}
54
- func skipToPrevious() {}
55
- func setVolume(_ volume: Float) {}
56
- func setPlaybackSpeed(_ speed: Float) {}
57
- func seedPlaybackSpeed(_ speed: Float) {}
58
- func setRepeatMode(_ mode: RepeatMode) {}
59
- func setPitchCorrectionMode(_ mode: PitchCorrectionMode) {}
60
-
61
- var currentMediaItemIndex: Int { -1 }
62
- var currentPositionSeconds: TimeInterval { 0 }
63
- var currentDurationSeconds: TimeInterval { 0 }
64
- var bufferedPositionSeconds: TimeInterval { 0 }
65
- var isPlaying: Bool { false }
66
- var timeControlStatus: AVPlayer.TimeControlStatus { .paused }
67
- var currentMediaItem: AVPlayerItem? { nil }
68
- var allMediaItems: [AVPlayerItem] { [] }
69
-
70
- func setAudioMixProvider(_ provider: AudioMixProvider?) {}
71
- func refreshAllItemMixes() {}
72
-
73
- func handoff(to nextEngine: PlaybackEngine, atTrackBoundary: Bool) async {}
74
-
75
- var delegate: PlaybackEngineDelegate?
76
- }
77
-
78
- private final class NoopDelegate: PlaybackEngineDelegate {
79
- func engineStateMaybeChanged(_ engine: PlaybackEngine, reason: StateChangeReason?) {}
80
- func engineIsPlayingChanged(_ engine: PlaybackEngine, isPlaying: Bool) {}
81
- func engineDidTransitionTrack(_ engine: PlaybackEngine) {}
82
- func engineActiveItemFormatChanged(_ engine: PlaybackEngine) {}
83
- func engineDidFailWithError(_ engine: PlaybackEngine, error: Error) {}
84
- func engineDidPlayItemToEnd(_ engine: PlaybackEngine) {}
85
- func enginePlaybackEnded(_ engine: PlaybackEngine) {}
86
- func engineCrossfadeDidBegin(_ engine: PlaybackEngine, incomingIndex: Int) {}
87
- func engineCrossfadeDidCancel(_ engine: PlaybackEngine, revertedIndex: Int) {}
88
- }
89
-
90
- private final class NoopMixProvider: AudioMixProvider {
91
- func audioMix(for item: AVPlayerItem) -> AVAudioMix? { nil }
92
- }