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
@@ -1,555 +0,0 @@
1
- import Foundation
2
-
3
- /// Direct-bytes audio metadata reader for ReplayGain tags. Sniffs
4
- /// the container by leading magic bytes and dispatches to a format-
5
- /// specific parser. Reads the first 256 KB of the source — large
6
- /// enough to contain ID3v2 padding, FLAC metadata blocks, and the
7
- /// `moov` atom on most MP4 files. ReplayGain tags live near the
8
- /// start of every supported container so a bounded prefix read is
9
- /// always sufficient.
10
- ///
11
- /// Why not `AVAsset.load(.metadata)`: every AVFoundation metadata
12
- /// load serialises through the demux pipeline; with many concurrent
13
- /// asset preparations, total wait runs to tens of seconds even for
14
- /// trivially-tagged local files. This module reads the container
15
- /// header directly without involving AVFoundation at all — sub-
16
- /// 100 ms per local file regardless of queue size.
17
- ///
18
- /// Source-agnostic: `file://` reads via `FileHandle`, `http(s)://`
19
- /// reads via `URLSession` with a `Range: bytes=0-N` request. Same
20
- /// parsers handle both — only the byte-source shim differs.
21
- enum MetadataReader {
22
-
23
- /// Default prefix read — large enough for ID3v2 padding, FLAC
24
- /// metadata blocks, and the `moov` atom on standard MP4 files.
25
- static let defaultPrefixBytes = 256 * 1024
26
-
27
- /// Read the four `REPLAYGAIN_*` tags from a media URL. Returns
28
- /// `nil` when the container is Opus (decoder-applied gain),
29
- /// when none of the tags are present, or when the source can't
30
- /// be read.
31
- static func readReplayGainTags(
32
- url: URL, urlSession: URLSession = .shared
33
- ) async -> ReplayGainData? {
34
- if isOpusContainer(url: url) { return nil }
35
- guard let data = await fetchPrefixBytes(url: url, urlSession: urlSession) else { return nil }
36
- return rgDataFromTags(parseTags(from: data))
37
- }
38
-
39
- // MARK: - Format dispatch
40
-
41
- /// Sniff first bytes and route to the format-specific parser.
42
- /// Returns a normalised `[upperKey: rawValue]` dict containing
43
- /// every name/value pair the parser found (not just RG ones —
44
- /// keeping the full set keeps the parser side reusable for
45
- /// other tag categories later).
46
- static func parseTags(from data: Data) -> [String: String] {
47
- guard data.count >= 4 else { return [:] }
48
- let b0 = data[0], b1 = data[1], b2 = data[2], b3 = data[3]
49
- if b0 == 0x49 && b1 == 0x44 && b2 == 0x33 {
50
- return parseID3v2(data)
51
- }
52
- if b0 == 0x66 && b1 == 0x4c && b2 == 0x61 && b3 == 0x43 {
53
- return parseFLAC(data)
54
- }
55
- if data.count >= 8,
56
- data[4] == 0x66 && data[5] == 0x74 && data[6] == 0x79 && data[7] == 0x70 {
57
- return parseMP4(data)
58
- }
59
- return [:]
60
- }
61
-
62
- static func rgDataFromTags(_ tags: [String: String]) -> ReplayGainData? {
63
- let upper = tags.reduce(into: [String: String]()) { d, kv in
64
- d[kv.key.uppercased()] = kv.value
65
- }
66
- let trackGain = upper[ReplayGainExtractor.trackGainKey]
67
- .flatMap { ReplayGainExtractor.parseGainDb($0) }
68
- let trackPeak = upper[ReplayGainExtractor.trackPeakKey]
69
- .flatMap { ReplayGainExtractor.parsePeak($0) }
70
- let albumGain = upper[ReplayGainExtractor.albumGainKey]
71
- .flatMap { ReplayGainExtractor.parseGainDb($0) }
72
- let albumPeak = upper[ReplayGainExtractor.albumPeakKey]
73
- .flatMap { ReplayGainExtractor.parsePeak($0) }
74
- if trackGain == nil && trackPeak == nil
75
- && albumGain == nil && albumPeak == nil {
76
- return nil
77
- }
78
- return ReplayGainData(
79
- trackGainDb: trackGain, trackPeak: trackPeak,
80
- albumGainDb: albumGain, albumPeak: albumPeak
81
- )
82
- }
83
-
84
- static func isOpusContainer(url: URL) -> Bool {
85
- let ext = url.pathExtension.lowercased()
86
- return ext == "opus" || ext == "ogg"
87
- }
88
-
89
- // MARK: - Byte fetch shim
90
-
91
- /// File: `FileHandle` prefix read. HTTP/S: `URLSession` Range
92
- /// request. Returns `nil` on read error or unsupported scheme.
93
- static func fetchPrefixBytes(
94
- url: URL, byteCount: Int = defaultPrefixBytes, urlSession: URLSession = .shared
95
- ) async -> Data? {
96
- if url.isFileURL {
97
- do {
98
- let handle = try FileHandle(forReadingFrom: url)
99
- defer { try? handle.close() }
100
- return handle.readData(ofLength: byteCount)
101
- } catch {
102
- return nil
103
- }
104
- }
105
- guard let scheme = url.scheme?.lowercased(),
106
- scheme == "http" || scheme == "https" else {
107
- return nil
108
- }
109
- var req = URLRequest(url: url, timeoutInterval: 10)
110
- req.setValue("bytes=0-\(byteCount - 1)", forHTTPHeaderField: "Range")
111
- do {
112
- let (data, response) = try await urlSession.data(for: req)
113
- if let http = response as? HTTPURLResponse,
114
- !(200...299).contains(http.statusCode) {
115
- return nil
116
- }
117
- return data
118
- } catch {
119
- return nil
120
- }
121
- }
122
-
123
- // MARK: - ID3v2 (MP3)
124
-
125
- /// Parse ID3v2.2 / 2.3 / 2.4 TXXX (user-defined text) frames into
126
- /// a `[description: value]` dict. Only TXXX is walked because RG
127
- /// tags use that frame; standard text frames (TIT2 etc.) carry
128
- /// title/artist/album which the player resolves via Now Playing
129
- /// metadata, not this path.
130
- ///
131
- /// Tag-size, frame-size, and frame-id parsing follow the official
132
- /// ID3v2 spec; v2.4 frames use syncsafe sizes, v2.2/2.3 use plain
133
- /// big-endian. Unsynchronisation transformations (rare in modern
134
- /// taggers) are not undone — empirical: foobar2000 / mp3tag emit
135
- /// non-unsynchronised tags.
136
- static func parseID3v2(_ data: Data) -> [String: String] {
137
- guard data.count >= 10,
138
- data[0] == 0x49, data[1] == 0x44, data[2] == 0x33 else {
139
- return [:]
140
- }
141
- let majorVersion = data[3]
142
- let flags = data[5]
143
- let tagSize = Int(data[6] & 0x7f) << 21 |
144
- Int(data[7] & 0x7f) << 14 |
145
- Int(data[8] & 0x7f) << 7 |
146
- Int(data[9] & 0x7f)
147
- let tagEnd = min(10 + tagSize, data.count)
148
- var pos = 10
149
-
150
- // Optional extended header (flag bit 0x40). v2.3 size is 4-byte
151
- // BE excluding the 4 size bytes themselves; v2.4 is syncsafe and
152
- // includes itself. Consumers + RG parsers don't care — skip past.
153
- if flags & 0x40 != 0, pos + 4 <= tagEnd {
154
- let extSize: Int
155
- if majorVersion >= 4 {
156
- extSize = Int(data[pos] & 0x7f) << 21 |
157
- Int(data[pos+1] & 0x7f) << 14 |
158
- Int(data[pos+2] & 0x7f) << 7 |
159
- Int(data[pos+3] & 0x7f)
160
- } else {
161
- extSize = Int(data[pos]) << 24 | Int(data[pos+1]) << 16 |
162
- Int(data[pos+2]) << 8 | Int(data[pos+3])
163
- // v2.3 ext header size excludes itself.
164
- }
165
- pos += extSize + (majorVersion >= 4 ? 0 : 4)
166
- }
167
-
168
- // v2.2 used 3-byte frame IDs + 3-byte sizes. v2.3+ uses 4+4.
169
- let frameIdLen = majorVersion <= 2 ? 3 : 4
170
- let frameSizeLen = majorVersion <= 2 ? 3 : 4
171
- let frameHeaderLen = frameIdLen + frameSizeLen + (majorVersion <= 2 ? 0 : 2)
172
- let txxxId: String = majorVersion <= 2 ? "TXX" : "TXXX"
173
-
174
- var result: [String: String] = [:]
175
- while pos + frameHeaderLen <= tagEnd {
176
- // Padding starts when frame ID is all zero.
177
- if data[pos] == 0 { break }
178
- let frameID = String(
179
- data: data.subdata(in: pos..<(pos + frameIdLen)),
180
- encoding: .isoLatin1
181
- ) ?? ""
182
- let sizePos = pos + frameIdLen
183
- let frameSize: Int
184
- if majorVersion >= 4 {
185
- frameSize = Int(data[sizePos] & 0x7f) << 21 |
186
- Int(data[sizePos+1] & 0x7f) << 14 |
187
- Int(data[sizePos+2] & 0x7f) << 7 |
188
- Int(data[sizePos+3] & 0x7f)
189
- } else if majorVersion == 3 {
190
- frameSize = Int(data[sizePos]) << 24 | Int(data[sizePos+1]) << 16 |
191
- Int(data[sizePos+2]) << 8 | Int(data[sizePos+3])
192
- } else {
193
- frameSize = Int(data[sizePos]) << 16 | Int(data[sizePos+1]) << 8 |
194
- Int(data[sizePos+2])
195
- }
196
- let payloadStart = pos + frameHeaderLen
197
- let payloadEnd = payloadStart + frameSize
198
- if frameSize <= 0 || payloadEnd > tagEnd { break }
199
- if frameID == txxxId,
200
- let (desc, value) = parseTXXXFrame(
201
- data.subdata(in: payloadStart..<payloadEnd)) {
202
- result[desc] = value
203
- }
204
- pos = payloadEnd
205
- }
206
- return result
207
- }
208
-
209
- /// Decode a TXXX frame body into `(description, value)`. Encoding
210
- /// byte at offset 0; description is null-terminated, value follows.
211
- static func parseTXXXFrame(_ payload: Data) -> (String, String)? {
212
- guard payload.count >= 2 else { return nil }
213
- let encoding = payload[0]
214
- let body = payload.subdata(in: 1..<payload.count)
215
- switch encoding {
216
- case 0:
217
- return splitTXXXSingleNull(body, encoding: .isoLatin1)
218
- case 3:
219
- return splitTXXXSingleNull(body, encoding: .utf8)
220
- case 1:
221
- return splitTXXXDoubleNull(body)
222
- case 2:
223
- return splitTXXXDoubleNullFixed(body, encoding: .utf16BigEndian)
224
- default:
225
- return nil
226
- }
227
- }
228
-
229
- static func splitTXXXSingleNull(
230
- _ data: Data, encoding: String.Encoding
231
- ) -> (String, String)? {
232
- guard let nullIdx = data.firstIndex(of: 0) else { return nil }
233
- let descBytes = data.subdata(in: data.startIndex..<nullIdx)
234
- let valueStart = nullIdx + 1
235
- let valueBytes = valueStart < data.endIndex
236
- ? data.subdata(in: valueStart..<data.endIndex)
237
- : Data()
238
- guard let desc = String(data: descBytes, encoding: encoding),
239
- let value = String(data: valueBytes, encoding: encoding) else {
240
- return nil
241
- }
242
- return (stripBOM(desc), stripBOM(value))
243
- }
244
-
245
- /// Encoding-1 UTF-16 with per-string BOM. Description and value
246
- /// each have their own BOM. Separator is `0x00 0x00` aligned on
247
- /// a 16-bit boundary. Strips the leading BOM character (U+FEFF)
248
- /// from the decoded result — Swift's `String(data:encoding:.utf16…)`
249
- /// preserves the BOM byte-pair as a U+FEFF zero-width glyph when
250
- /// the explicit endianness encoding is used, which would prevent
251
- /// downstream `"REPLAYGAIN_*"` dictionary lookups from matching.
252
- static func splitTXXXDoubleNull(_ data: Data) -> (String, String)? {
253
- let bytes = [UInt8](data)
254
- var i = 0
255
- while i + 1 < bytes.count {
256
- if bytes[i] == 0 && bytes[i + 1] == 0 && i % 2 == 0 {
257
- let descBytes = Data(bytes[0..<i])
258
- let valueBytes = (i + 2) < bytes.count
259
- ? Data(bytes[(i + 2)..<bytes.count])
260
- : Data()
261
- let descEnc = detectUTF16Encoding(descBytes) ?? .utf16
262
- let valueEnc = detectUTF16Encoding(valueBytes) ?? descEnc
263
- guard let desc = String(data: descBytes, encoding: descEnc),
264
- let value = String(data: valueBytes, encoding: valueEnc) else {
265
- return nil
266
- }
267
- return (stripBOM(desc), stripBOM(value))
268
- }
269
- i += 1
270
- }
271
- return nil
272
- }
273
-
274
- /// Strip leading U+FEFF (UTF-16 BOM) and trim trailing null /
275
- /// control characters from a decoded string. ID3v2 taggers
276
- /// (foobar2000, mp3tag) commonly emit TXXX values with a trailing
277
- /// `\0` even though the spec says the final field has no
278
- /// terminator. Without this trim, downstream parsers
279
- /// (`Double.init` for gain, dictionary key lookup for description)
280
- /// fail on the embedded null character.
281
- static func stripBOM(_ s: String) -> String {
282
- var out = s
283
- if out.first == "\u{FEFF}" {
284
- out = String(out.dropFirst())
285
- }
286
- while let last = out.last, last == "\0" || last.isNewline
287
- || last.unicodeScalars.first.map({ $0.value < 0x20 }) == true {
288
- out = String(out.dropLast())
289
- }
290
- return out
291
- }
292
-
293
- static func splitTXXXDoubleNullFixed(
294
- _ data: Data, encoding: String.Encoding
295
- ) -> (String, String)? {
296
- let bytes = [UInt8](data)
297
- var i = 0
298
- while i + 1 < bytes.count {
299
- if bytes[i] == 0 && bytes[i + 1] == 0 && i % 2 == 0 {
300
- let descBytes = Data(bytes[0..<i])
301
- let valueBytes = (i + 2) < bytes.count
302
- ? Data(bytes[(i + 2)..<bytes.count])
303
- : Data()
304
- guard let desc = String(data: descBytes, encoding: encoding),
305
- let value = String(data: valueBytes, encoding: encoding) else {
306
- return nil
307
- }
308
- return (stripBOM(desc), stripBOM(value))
309
- }
310
- i += 1
311
- }
312
- return nil
313
- }
314
-
315
- static func detectUTF16Encoding(_ data: Data) -> String.Encoding? {
316
- guard data.count >= 2 else { return nil }
317
- let b0 = data[data.startIndex]
318
- let b1 = data[data.startIndex + 1]
319
- if b0 == 0xFE && b1 == 0xFF { return .utf16BigEndian }
320
- if b0 == 0xFF && b1 == 0xFE { return .utf16LittleEndian }
321
- return nil
322
- }
323
-
324
- // MARK: - FLAC
325
-
326
- /// FLAC STREAMINFO header values relevant to bitrate computation.
327
- /// Reading these + the file's byte size gives an accurate average
328
- /// encoded bitrate without involving AVFoundation.
329
- struct FLACStreamInfo {
330
- let sampleRate: Int // Hz, 1..655350
331
- let channelCount: Int // 1..8
332
- let bitsPerSample: Int // 4..32
333
- let totalSamples: Int64 // 0 = unknown
334
- }
335
-
336
- /// Parse a FLAC STREAMINFO block from a `fLaC`-prefixed buffer.
337
- /// STREAMINFO is type 0 and is required by the FLAC spec to be the
338
- /// FIRST metadata block. Returns nil when the buffer doesn't start
339
- /// with the FLAC magic or the block layout is malformed.
340
- ///
341
- /// STREAMINFO body layout (34 bytes after the 4-byte block header):
342
- /// - 16 bits: minBlockSize (samples)
343
- /// - 16 bits: maxBlockSize
344
- /// - 24 bits: minFrameSize (bytes)
345
- /// - 24 bits: maxFrameSize
346
- /// - 20 bits: sampleRate
347
- /// - 3 bits: channels - 1
348
- /// - 5 bits: bitsPerSample - 1
349
- /// - 36 bits: totalSamples
350
- /// - 128 bits: MD5
351
- static func parseFLACStreamInfo(_ data: Data) -> FLACStreamInfo? {
352
- guard data.count >= 4 + 4 + 34 else { return nil }
353
- guard data[0] == 0x66, data[1] == 0x4c,
354
- data[2] == 0x61, data[3] == 0x43 else { return nil }
355
- let blockType = data[4] & 0x7f
356
- guard blockType == 0 else { return nil }
357
- let blockSize = Int(data[5]) << 16 | Int(data[6]) << 8 | Int(data[7])
358
- guard blockSize >= 34, data.count >= 8 + blockSize else { return nil }
359
- // STREAMINFO body starts at offset 8.
360
- // Bits 64..83 (offset 10, 11, 12 in the body — i.e. data offsets
361
- // 18, 19, 20): sampleRate (20 bits, big-endian).
362
- let b18 = UInt64(data[18])
363
- let b19 = UInt64(data[19])
364
- let b20 = UInt64(data[20])
365
- let sampleRate = Int((b18 << 12) | (b19 << 4) | (b20 >> 4))
366
- let channels = Int((data[20] >> 1) & 0x07) + 1
367
- let bitsPerSample = Int(((data[20] & 0x01) << 4) | (data[21] >> 4)) + 1
368
- // totalSamples — 36 bits starting at byte 21 bit 4.
369
- let t21 = Int64(data[21] & 0x0f)
370
- let t22 = Int64(data[22])
371
- let t23 = Int64(data[23])
372
- let t24 = Int64(data[24])
373
- let t25 = Int64(data[25])
374
- let totalSamples =
375
- (t21 << 32) | (t22 << 24) | (t23 << 16) | (t24 << 8) | t25
376
- return FLACStreamInfo(
377
- sampleRate: sampleRate,
378
- channelCount: channels,
379
- bitsPerSample: bitsPerSample,
380
- totalSamples: totalSamples
381
- )
382
- }
383
-
384
- /// Walk FLAC metadata blocks from the `fLaC` magic and return the
385
- /// Vorbis-comment KEY=value dict from the first VORBIS_COMMENT
386
- /// (block type 4). Stops at the last-block flag.
387
- static func parseFLAC(_ data: Data) -> [String: String] {
388
- guard data.count >= 8 else { return [:] }
389
- guard data[0] == 0x66, data[1] == 0x4c,
390
- data[2] == 0x61, data[3] == 0x43 else { return [:] }
391
- var pos = 4
392
- while pos + 4 <= data.count {
393
- let header = data[pos]
394
- let isLast = (header & 0x80) != 0
395
- let blockType = header & 0x7f
396
- let blockSize = Int(data[pos+1]) << 16 |
397
- Int(data[pos+2]) << 8 |
398
- Int(data[pos+3])
399
- let blockStart = pos + 4
400
- let blockEnd = blockStart + blockSize
401
- if blockEnd > data.count { return [:] }
402
- if blockType == 4 {
403
- return parseVorbisComment(data.subdata(in: blockStart..<blockEnd))
404
- }
405
- if isLast { break }
406
- pos = blockEnd
407
- }
408
- return [:]
409
- }
410
-
411
- /// Vorbis comment block layout: vendor-len (LE u32) + vendor +
412
- /// count (LE u32) + count × (len LE u32, UTF-8 KEY=value).
413
- static func parseVorbisComment(_ data: Data) -> [String: String] {
414
- var pos = 0
415
- func readU32LE() -> Int? {
416
- guard pos + 4 <= data.count else { return nil }
417
- let v = Int(data[pos]) |
418
- (Int(data[pos+1]) << 8) |
419
- (Int(data[pos+2]) << 16) |
420
- (Int(data[pos+3]) << 24)
421
- pos += 4
422
- return v
423
- }
424
- guard let vendorLen = readU32LE(),
425
- pos + vendorLen <= data.count else { return [:] }
426
- pos += vendorLen
427
- guard let count = readU32LE() else { return [:] }
428
- var result: [String: String] = [:]
429
- for _ in 0..<count {
430
- guard let len = readU32LE(),
431
- len >= 0,
432
- pos + len <= data.count else { break }
433
- let bytes = data.subdata(in: pos..<(pos + len))
434
- pos += len
435
- guard let str = String(data: bytes, encoding: .utf8) else { continue }
436
- if let eqIdx = str.firstIndex(of: "=") {
437
- let key = String(str[..<eqIdx])
438
- let value = String(str[str.index(after: eqIdx)...])
439
- result[key] = value
440
- }
441
- }
442
- return result
443
- }
444
-
445
- // MARK: - MP4 / M4A iTunes-style atoms
446
-
447
- /// Walk the MP4 atom tree to `moov.udta.meta.ilst` and return the
448
- /// freeform `----` atoms keyed by their `name` value. iTunes
449
- /// stores ReplayGain in freeform atoms (mean=`com.apple.iTunes`,
450
- /// name=`REPLAYGAIN_*`, data=UTF-8 value).
451
- static func parseMP4(_ data: Data) -> [String: String] {
452
- guard let ilst = findAtomPath(
453
- data, path: ["moov", "udta", "meta", "ilst"]
454
- ) else {
455
- return [:]
456
- }
457
- var pos = 0
458
- var result: [String: String] = [:]
459
- while pos + 8 <= ilst.count {
460
- let size = Int(ilst[pos]) << 24 | Int(ilst[pos+1]) << 16 |
461
- Int(ilst[pos+2]) << 8 | Int(ilst[pos+3])
462
- let type = String(
463
- data: ilst.subdata(in: (pos+4)..<(pos+8)),
464
- encoding: .isoLatin1
465
- ) ?? ""
466
- if size < 8 || pos + size > ilst.count { break }
467
- let body = ilst.subdata(in: (pos+8)..<(pos+size))
468
- if type == "----" {
469
- if let (name, value) = parseFreeformAtom(body) {
470
- result[name] = value
471
- }
472
- }
473
- pos += size
474
- }
475
- return result
476
- }
477
-
478
- /// A `----` (freeform) atom contains three child atoms:
479
- /// `mean` (issuer, e.g. com.apple.iTunes), `name` (tag key), and
480
- /// `data` (tag value). The first two share an 8-byte header
481
- /// (size + type) plus a 4-byte version+flags before the UTF-8
482
- /// payload. The `data` atom's payload is preceded by 4 bytes of
483
- /// type indicator + 4 bytes of locale.
484
- static func parseFreeformAtom(_ data: Data) -> (String, String)? {
485
- var pos = 0
486
- var name: String?
487
- var value: String?
488
- while pos + 8 <= data.count {
489
- let size = Int(data[pos]) << 24 | Int(data[pos+1]) << 16 |
490
- Int(data[pos+2]) << 8 | Int(data[pos+3])
491
- let type = String(
492
- data: data.subdata(in: (pos+4)..<(pos+8)),
493
- encoding: .isoLatin1
494
- ) ?? ""
495
- if size < 8 || pos + size > data.count { break }
496
- let body = data.subdata(in: (pos+8)..<(pos+size))
497
- switch type {
498
- case "name":
499
- if body.count > 4 {
500
- name = String(
501
- data: body.subdata(in: 4..<body.count),
502
- encoding: .utf8
503
- )
504
- }
505
- case "data":
506
- if body.count > 8 {
507
- value = String(
508
- data: body.subdata(in: 8..<body.count),
509
- encoding: .utf8
510
- )
511
- }
512
- default:
513
- break
514
- }
515
- pos += size
516
- }
517
- if let name = name, let value = value {
518
- return (name, value)
519
- }
520
- return nil
521
- }
522
-
523
- /// Walk an atom tree by name path. The `meta` atom in iTunes-
524
- /// style MP4 carries a 4-byte version+flags prefix before its
525
- /// children — the only path-walk quirk worth handling.
526
- static func findAtomPath(_ data: Data, path: [String]) -> Data? {
527
- var current = data
528
- for name in path {
529
- guard var body = findAtom(in: current, type: name) else { return nil }
530
- if name == "meta" && body.count >= 4 {
531
- body = body.subdata(in: 4..<body.count)
532
- }
533
- current = body
534
- }
535
- return current
536
- }
537
-
538
- static func findAtom(in data: Data, type: String) -> Data? {
539
- var pos = 0
540
- while pos + 8 <= data.count {
541
- let size = Int(data[pos]) << 24 | Int(data[pos+1]) << 16 |
542
- Int(data[pos+2]) << 8 | Int(data[pos+3])
543
- let typeStr = String(
544
- data: data.subdata(in: (pos+4)..<(pos+8)),
545
- encoding: .isoLatin1
546
- ) ?? ""
547
- if size < 8 || pos + size > data.count { return nil }
548
- if typeStr == type {
549
- return data.subdata(in: (pos+8)..<(pos+size))
550
- }
551
- pos += size
552
- }
553
- return nil
554
- }
555
- }
@@ -1,43 +0,0 @@
1
- import AVFoundation
2
- import XCTest
3
- @testable import QueuePlayer
4
-
5
- /// Stub coverage for `CrossfadeEngine`. Constructor non-crash and
6
- /// delegate wirability are the only assertable surfaces — every
7
- /// runtime method crashes via `preconditionFailure`, which is
8
- /// non-recoverable from XCTest. The Kotlin counterpart asserts
9
- /// `NotImplementedError` on every method via `assertThrows`; on
10
- /// Swift the equivalent would require a crash-runner test
11
- /// harness, which adds infra for low value here.
12
- ///
13
- /// Negative-duration constructor guard at
14
- /// `CrossfadeEngine.init(crossfadeDurationSeconds:)` is also
15
- /// non-testable for the same reason. The load-bearing guard for
16
- /// runtime callers is the clamp inside `PlaybackModeStateMachine`;
17
- /// the constructor `precondition` is a defensive belt.
18
- final class CrossfadeEngineStubTests: XCTestCase {
19
-
20
- func testConstructorAcceptsNonNegativeCrossfadeDuration() {
21
- let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0)
22
- XCTAssertNotNil(engine)
23
- }
24
-
25
- func testDelegateIsWirableOnTheStub() {
26
- let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0)
27
- let delegate = NoopDelegate()
28
- engine.delegate = delegate
29
- XCTAssertTrue(engine.delegate === delegate)
30
- }
31
- }
32
-
33
- private final class NoopDelegate: PlaybackEngineDelegate {
34
- func engineStateMaybeChanged(_ engine: PlaybackEngine, reason: StateChangeReason?) {}
35
- func engineIsPlayingChanged(_ engine: PlaybackEngine, isPlaying: Bool) {}
36
- func engineDidTransitionTrack(_ engine: PlaybackEngine) {}
37
- func engineActiveItemFormatChanged(_ engine: PlaybackEngine) {}
38
- func engineDidFailWithError(_ engine: PlaybackEngine, error: Error) {}
39
- func engineDidPlayItemToEnd(_ engine: PlaybackEngine) {}
40
- func enginePlaybackEnded(_ engine: PlaybackEngine) {}
41
- func engineCrossfadeDidBegin(_ engine: PlaybackEngine, incomingIndex: Int) {}
42
- func engineCrossfadeDidCancel(_ engine: PlaybackEngine, revertedIndex: Int) {}
43
- }