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
@@ -0,0 +1,173 @@
1
+ package com.margelo.nitro.queueplayer.cast.net
2
+
3
+ import android.content.Context
4
+ import android.net.ConnectivityManager
5
+ import android.net.LinkAddress
6
+ import android.net.LinkProperties
7
+ import android.net.Network
8
+ import android.net.NetworkCapabilities
9
+ import androidx.test.core.app.ApplicationProvider
10
+ import org.junit.Assert.assertEquals
11
+ import org.junit.Assert.assertFalse
12
+ import org.junit.Assert.assertNull
13
+ import org.junit.Assert.assertTrue
14
+ import org.junit.Test
15
+ import org.junit.runner.RunWith
16
+ import org.robolectric.RobolectricTestRunner
17
+ import org.robolectric.Shadows.shadowOf
18
+ import org.robolectric.annotation.Config
19
+ import org.robolectric.shadow.api.Shadow
20
+ import org.robolectric.shadows.ShadowNetwork
21
+ import org.robolectric.shadows.ShadowNetworkCapabilities
22
+ import org.robolectric.util.ReflectionHelpers
23
+ import org.robolectric.util.ReflectionHelpers.ClassParameter
24
+ import java.net.InetAddress
25
+
26
+ /**
27
+ * `currentAddress` is the Wi-Fi network's IPv4 address: the callback is
28
+ * registered for Wi-Fi networks only, the `start()` seed uses the active
29
+ * network only when it is Wi-Fi and not a VPN, and the address follows
30
+ * that network's link properties until it is lost.
31
+ */
32
+ @RunWith(RobolectricTestRunner::class)
33
+ @Config(sdk = [34], shadows = [ShadowConnectivityManagerRecordingRequests::class])
34
+ class LocalAddressMonitorTest {
35
+
36
+ private val context: Context = ApplicationProvider.getApplicationContext()
37
+ private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
38
+ private val shadowCm: ShadowConnectivityManagerRecordingRequests = Shadow.extract(cm)
39
+
40
+ // The shadow's default active network has netId 0; its transport is
41
+ // whatever capabilities the test puts on it. Any other netId is a
42
+ // network that exists but is not the active one.
43
+ private val activeNetwork: Network = ShadowNetwork.newInstance(0)
44
+ private val otherNetwork: Network = ShadowNetwork.newInstance(1)
45
+
46
+ private fun describe(network: Network, transports: List<Int>, ipv4: String, vpn: Boolean = false) {
47
+ val capabilities = ShadowNetworkCapabilities.newInstance()
48
+ transports.forEach { shadowOf(capabilities).addTransportType(it) }
49
+ if (vpn) shadowOf(capabilities).removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
50
+ shadowCm.setNetworkCapabilities(network, capabilities)
51
+ shadowCm.setLinkProperties(network, linkProperties(ipv4))
52
+ }
53
+
54
+ private fun linkProperties(vararg addresses: String): LinkProperties {
55
+ val linkAddresses = addresses.map { literal ->
56
+ val inet = InetAddress.getByName(literal)
57
+ ReflectionHelpers.callConstructor(
58
+ LinkAddress::class.java,
59
+ ClassParameter.from(InetAddress::class.java, inet),
60
+ ClassParameter.from(Int::class.javaPrimitiveType, if (inet.address.size == 4) 24 else 64),
61
+ )
62
+ }
63
+ return LinkProperties().apply { setLinkAddresses(linkAddresses) }
64
+ }
65
+
66
+ @Test
67
+ fun `the callback is registered for Wi-Fi networks only`() {
68
+ LocalAddressMonitor(context).start()
69
+
70
+ val request = shadowCm.requests.single()
71
+ assertTrue(request.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
72
+ assertFalse(request.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR))
73
+ assertTrue(request.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN))
74
+ }
75
+
76
+ @Test
77
+ fun `start seeds the address from a Wi-Fi active network`() {
78
+ describe(activeNetwork, listOf(NetworkCapabilities.TRANSPORT_WIFI), "192.168.1.10")
79
+
80
+ val monitor = LocalAddressMonitor(context).also { it.start() }
81
+
82
+ assertEquals("192.168.1.10", monitor.currentAddress)
83
+ }
84
+
85
+ @Test
86
+ fun `start does not seed the address from a cellular active network`() {
87
+ describe(otherNetwork, listOf(NetworkCapabilities.TRANSPORT_WIFI), "192.168.1.10")
88
+ describe(activeNetwork, listOf(NetworkCapabilities.TRANSPORT_CELLULAR), "10.0.0.5")
89
+
90
+ val monitor = LocalAddressMonitor(context).also { it.start() }
91
+
92
+ assertNull("a cellular address is unreachable from a LAN receiver", monitor.currentAddress)
93
+ }
94
+
95
+ @Test
96
+ fun `start does not seed the address from a VPN over Wi-Fi`() {
97
+ describe(
98
+ activeNetwork,
99
+ listOf(NetworkCapabilities.TRANSPORT_VPN, NetworkCapabilities.TRANSPORT_WIFI),
100
+ "10.8.0.2",
101
+ vpn = true,
102
+ )
103
+
104
+ val monitor = LocalAddressMonitor(context).also { it.start() }
105
+
106
+ assertNull("a tunnel address is unreachable from a LAN receiver", monitor.currentAddress)
107
+ }
108
+
109
+ @Test
110
+ fun `start registers the callback and seeds nothing when there is no active network`() {
111
+ shadowCm.setDefaultNetworkActive(false)
112
+
113
+ val monitor = LocalAddressMonitor(context).also { it.start() }
114
+
115
+ assertEquals(1, shadowCm.networkCallbacks.size)
116
+ assertNull(monitor.currentAddress)
117
+ }
118
+
119
+ @Test
120
+ fun `a matched network's link properties set the address and its loss clears it`() {
121
+ shadowCm.setDefaultNetworkActive(false)
122
+ val monitor = LocalAddressMonitor(context).also { it.start() }
123
+ val callback = shadowCm.networkCallbacks.single()
124
+
125
+ callback.onLinkPropertiesChanged(otherNetwork, linkProperties("192.168.1.20"))
126
+ assertEquals("192.168.1.20", monitor.currentAddress)
127
+
128
+ callback.onLost(otherNetwork)
129
+ assertNull(monitor.currentAddress)
130
+ }
131
+
132
+ /** A Wi-Fi handover reports the new network's link before the old network
133
+ * is lost; the loss of the old one must not clear the new address. */
134
+ @Test
135
+ fun `losing a network other than the one that supplied the address keeps it`() {
136
+ shadowCm.setDefaultNetworkActive(false)
137
+ val monitor = LocalAddressMonitor(context).also { it.start() }
138
+ val callback = shadowCm.networkCallbacks.single()
139
+ val oldWifi = otherNetwork
140
+ val newWifi = ShadowNetwork.newInstance(2)
141
+
142
+ callback.onLinkPropertiesChanged(oldWifi, linkProperties("192.168.1.20"))
143
+ callback.onLinkPropertiesChanged(newWifi, linkProperties("192.168.2.30"))
144
+ callback.onLost(oldWifi)
145
+ assertEquals("192.168.2.30", monitor.currentAddress)
146
+
147
+ callback.onLost(newWifi)
148
+ assertNull(monitor.currentAddress)
149
+ }
150
+
151
+ @Test
152
+ fun `the address is the first non-loopback IPv4 among the link addresses`() {
153
+ shadowCm.setDefaultNetworkActive(false)
154
+ val monitor = LocalAddressMonitor(context).also { it.start() }
155
+ val callback = shadowCm.networkCallbacks.single()
156
+
157
+ callback.onLinkPropertiesChanged(otherNetwork, linkProperties("fe80::1", "127.0.0.1", "192.168.1.20", "192.168.1.21"))
158
+ assertEquals("192.168.1.20", monitor.currentAddress)
159
+
160
+ callback.onLinkPropertiesChanged(otherNetwork, linkProperties("fe80::1"))
161
+ assertNull(monitor.currentAddress)
162
+ }
163
+
164
+ @Test
165
+ fun `stop unregisters the callback`() {
166
+ val monitor = LocalAddressMonitor(context).also { it.start() }
167
+ assertEquals(1, shadowCm.networkCallbacks.size)
168
+
169
+ monitor.stop()
170
+
171
+ assertTrue(shadowCm.networkCallbacks.isEmpty())
172
+ }
173
+ }
@@ -0,0 +1,27 @@
1
+ package com.margelo.nitro.queueplayer.cast.net
2
+
3
+ import android.net.ConnectivityManager
4
+ import android.net.NetworkRequest
5
+ import android.os.Handler
6
+ import org.robolectric.annotation.Implements
7
+ import org.robolectric.shadows.ShadowConnectivityManager
8
+
9
+ /**
10
+ * A [ShadowConnectivityManager] that keeps the [NetworkRequest] passed to
11
+ * `registerNetworkCallback`; the stock shadow discards it and stores only
12
+ * the callback.
13
+ */
14
+ @Implements(ConnectivityManager::class)
15
+ class ShadowConnectivityManagerRecordingRequests : ShadowConnectivityManager() {
16
+
17
+ val requests = mutableListOf<NetworkRequest>()
18
+
19
+ override fun registerNetworkCallback(
20
+ request: NetworkRequest,
21
+ networkCallback: ConnectivityManager.NetworkCallback,
22
+ handler: Handler?,
23
+ ) {
24
+ requests.add(request)
25
+ super.registerNetworkCallback(request, networkCallback, handler)
26
+ }
27
+ }
package/app.plugin.js CHANGED
@@ -331,6 +331,13 @@ const withChromecastSubspecPodfile = (config) =>
331
331
  },
332
332
  ]);
333
333
 
334
+ /**
335
+ * Bonjour type the local-network permission probe advertises and browses.
336
+ * Mirrors `LocalNetworkPermissionProbe.serviceType`; the two must agree or the
337
+ * probe is blocked with nothing to indicate why.
338
+ */
339
+ const LOCAL_NETWORK_PROBE_SERVICE_TYPE = '_rnqp-lnp._tcp';
340
+
334
341
  const withChromecastInfoPlist = (config, receiverAppId) =>
335
342
  withInfoPlist(config, (cfg) => {
336
343
  const appId = (receiverAppId || 'CC1AD845').toUpperCase();
@@ -344,7 +351,16 @@ const withChromecastInfoPlist = (config, receiverAppId) =>
344
351
  const existingBonjour = Array.isArray(cfg.modResults.NSBonjourServices)
345
352
  ? cfg.modResults.NSBonjourServices
346
353
  : [];
347
- const additions = ['_googlecast._tcp', `_${appId}._googlecast._tcp`];
354
+ // The permission probe browses its own service type. iOS refuses to browse
355
+ // a type that is not declared here, so without this entry the probe can
356
+ // never succeed and every consumer reads `undetermined` forever.
357
+ // Must match `LocalNetworkPermissionProbe.serviceType` in
358
+ // ios/Cast/Core/LocalNetworkPermissionProbe.swift.
359
+ const additions = [
360
+ '_googlecast._tcp',
361
+ `_${appId}._googlecast._tcp`,
362
+ LOCAL_NETWORK_PROBE_SERVICE_TYPE,
363
+ ];
348
364
  for (const s of additions) {
349
365
  if (!existingBonjour.includes(s)) existingBonjour.push(s);
350
366
  }
@@ -26,14 +26,18 @@ import AVFoundation
26
26
  /// standard idiom — no `.m` / `.h` source files introduced.
27
27
  private var queueItemIdKey: UInt8 = 0
28
28
  private var sourceURLKey: UInt8 = 0
29
+ private var routedThroughReadThroughServerKey: UInt8 = 0
29
30
 
31
+ /// `nonisolated` throughout: the SDK marks `AVPlayerItem` main-actor isolated
32
+ /// because `asset` is not Sendable, but these stamps are written once at
33
+ /// construction and read on the player queue.
30
34
  extension AVPlayerItem {
31
35
  /// Lib-internal identity for this AVPlayerItem. Set once at
32
36
  /// construction by `AVQueueBuilder.makeItem`; never mutated
33
37
  /// after. nil only for AVPlayerItems that did not flow through
34
38
  /// the lib's queue construction path (e.g. built directly via
35
39
  /// `makeItemRaw`).
36
- var queueItemId: String? {
40
+ nonisolated var queueItemId: String? {
37
41
  get { objc_getAssociatedObject(self, &queueItemIdKey) as? String }
38
42
  set {
39
43
  objc_setAssociatedObject(
@@ -41,21 +45,31 @@ extension AVPlayerItem {
41
45
  }
42
46
  }
43
47
 
44
- /// The exact `track.url` string this item was built from. Stored
45
- /// verbatim `LookaheadCache` keys entries by raw URL-string
46
- /// equality, so a re-resolution against the cache must use the same
47
- /// string the cache was keyed with, NOT the item's `AVURLAsset.url`
48
- /// (which is already a `file://` path once an item is built from a
49
- /// cached local file, and is a parse/re-serialize round-trip of the
50
- /// original for a remote item). Lets a later attach (crossfade
51
- /// skip / fade-arm) look the track up in the cache and swap a
52
- /// remote-built item for its local cache file once it finishes
53
- /// downloading.
54
- var sourceURL: String? {
48
+ /// The exact `track.url` string this item was built from, stored
49
+ /// verbatim. A routed item's `AVURLAsset.url` is the read-through
50
+ /// server's `127.0.0.1` address and an unrouted one is a
51
+ /// parse/re-serialize round-trip of the original, so anything that
52
+ /// reads a container, codec or track identity out of a URL reads this:
53
+ /// `NowPlayingFormatExtractor`, `AssetReplayGainReader`, the error
54
+ /// `url` field, and the source diagnostic log.
55
+ nonisolated var sourceURL: String? {
55
56
  get { objc_getAssociatedObject(self, &sourceURLKey) as? String }
56
57
  set {
57
58
  objc_setAssociatedObject(
58
59
  self, &sourceURLKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
59
60
  }
60
61
  }
62
+
63
+ /// Whether this item's asset URL is the read-through server's loopback
64
+ /// address rather than `sourceURL` itself. Set once by
65
+ /// `AVQueueBuilder.makeItem`; the routing decision is made at build time
66
+ /// and an item is never re-pointed, so the stamp is what source
67
+ /// classification reads instead of guessing from the server's presence.
68
+ nonisolated var isRoutedThroughReadThroughServer: Bool {
69
+ get { (objc_getAssociatedObject(self, &routedThroughReadThroughServerKey) as? Bool) ?? false }
70
+ set {
71
+ objc_setAssociatedObject(
72
+ self, &routedThroughReadThroughServerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
73
+ }
74
+ }
61
75
  }
@@ -12,11 +12,11 @@ import AVFoundation
12
12
  /// Constructs AVPlayerItem instances from TrackItem descriptors,
13
13
  /// applying the PlayerConfig's custom HTTP headers + user agent.
14
14
  ///
15
- /// Headers are the load-bearing feature for Subsonic-behind-reverse-
16
- /// proxy deployments every remote fetch (asset read, lookahead-
17
- /// cache download, artwork fetch) has to carry the consumer app's
18
- /// auth. `AVURLAssetHTTPHeaderFieldsKey` on the AVURLAsset options
19
- /// dictionary does this on iOS.
15
+ /// Headers carry the consumer app's auth on every remote fetch —
16
+ /// asset read, lookahead-cache download, artwork fetch. An unrouted
17
+ /// asset gets them through `AVURLAssetHTTPHeaderFieldsKey` on its
18
+ /// options dictionary; a routed one is built with no asset options,
19
+ /// and the read-through server sends them on its origin leg instead.
20
20
  ///
21
21
  /// User-Agent handling: AVURLAsset has no dedicated UA option. The
22
22
  /// standard workaround is to include `User-Agent: <ua>` inside the
@@ -28,19 +28,20 @@ public enum AVQueueBuilder {
28
28
 
29
29
  /// Build AVPlayerItems for every track in `tracks`, applying config
30
30
  /// headers + UA on remote URLs. Returns an empty array for an empty
31
- /// input. Never throws — invalid URLs silently produce a nil asset
32
- /// which AVQueuePlayer surfaces as a playback error at runtime.
31
+ /// input. Never throws — a track whose URL does not parse is dropped
32
+ /// from the result (`makeItem` returns nil), so the engine never sees
33
+ /// it.
33
34
  ///
34
- /// When `cache` is non-nil + a track is fully cached, the
35
- /// AVPlayerItem is built from the local file URL instead of the
36
- /// remote one. Bytes never hit the network on playback. Partial
37
- /// caches fall through to the remote URL `cache.cachedURL`
38
- /// returns nil for entries that haven't fully downloaded.
35
+ /// With a non-nil `server`, every routable remote track (`isRoutable`)
36
+ /// is built on the read-through server's local URL, and the server
37
+ /// decides per request whether bytes come from the lookahead cache or
38
+ /// the origin. `file://` tracks, `.m3u8` manifests, and every track when
39
+ /// `server` is nil bind `track.url` directly see `makeItem`.
39
40
  static func buildPlayerItems(
40
41
  tracks: [TrackItem],
41
42
  queueItemIds: [String],
42
43
  config: PlayerConfig,
43
- cache: LookaheadCache? = nil
44
+ server: ReadThroughServer? = nil
44
45
  ) -> [AVPlayerItem] {
45
46
  // Parallel-array invariant: a violation is a programming error
46
47
  // we own (QueueState uphold-able at construction). `precondition`
@@ -51,41 +52,43 @@ public enum AVQueueBuilder {
51
52
  "AVQueueBuilder.buildPlayerItems requires queueItemIds parallel to tracks (1:1)"
52
53
  )
53
54
  return zip(tracks, queueItemIds).compactMap { track, id in
54
- makeItem(for: track, queueItemId: id, config: config, cache: cache)
55
+ makeItem(for: track, queueItemId: id, config: config, server: server)
55
56
  }
56
57
  }
57
58
 
58
- /// Best-effort warm-up of a freshly-built item's asset BEFORE it is
59
- /// inserted into a live `AVQueuePlayer`. When the insert lands in the
60
- /// immediate-next slot of a playing queue, `AVQueuePlayer` prerolls the
61
- /// new item on the shared decode/render pipeline while the current item
62
- /// is still rendering; faulting the asset's `tracks` / `duration` /
63
- /// `playable` keys in synchronously at that moment briefly disturbs the
64
- /// currently-playing item. Loading them here first makes that preroll
65
- /// cheap.
59
+ /// Cancel outstanding key loads on the assets of items being discarded.
66
60
  ///
67
- /// Best-effort: the load races a [timeout] deadline and the caller
68
- /// proceeds as soon as either wins. `AVAsset`'s async loader honours
69
- /// task cancellation, so `cancelAll` caps a slow / unreachable remote
70
- /// asset at [timeout] and the item is inserted unwarmed rather than
71
- /// deferring the enqueue. Load errors are ignored for the same reason.
72
- @MainActor
73
- static func preloadAssetForInsert(_ item: AVPlayerItem, timeout: TimeInterval = 2.0) async {
74
- let asset = item.asset
75
- await withTaskGroup(of: Void.self) { group in
76
- group.addTask { @MainActor in
77
- _ = try? await asset.load(.tracks, .duration, .isPlayable)
78
- }
79
- group.addTask {
80
- try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
81
- }
82
- _ = await group.next()
83
- group.cancelAll()
61
+ /// `AVURLAsset.dealloc` cancels them implicitly and **blocks the deallocating
62
+ /// thread while it does** a dispatch-group wait on the asset inspector,
63
+ /// measured at tens of seconds for a queue of 200 with loads in flight.
64
+ /// Cancelling first makes that wait a no-op. That thread is
65
+ /// `playerQueue`, so the block stalls every queue operation behind it.
66
+ ///
67
+ /// Only assets that are genuinely being dropped may be cancelled: a
68
+ /// cancelled asset can never become playable, so an item rebuilt on a reused
69
+ /// asset would stall silently. `keeping` is the set that survives, compared
70
+ /// by identity.
71
+ static func cancelLoading(dropping outgoing: [AVPlayerItem], keeping: [AVPlayerItem] = []) {
72
+ for item in itemsBeingDropped(outgoing: outgoing, keeping: keeping) {
73
+ item.asset.cancelLoading()
84
74
  }
85
75
  }
86
76
 
87
- /// Single-item builder. Used by addToQueue. Honours the cache
88
- /// lookup when a cache instance is supplied.
77
+ /// The items in `outgoing` that no entry of `keeping` reuses. Identity, not
78
+ /// equality: two items can wrap the same asset, and it is the asset's
79
+ /// survival that decides whether cancelling it is safe.
80
+ @_spi(QueuePlayerTests)
81
+ public static func itemsBeingDropped(
82
+ outgoing: [AVPlayerItem], keeping: [AVPlayerItem]
83
+ ) -> [AVPlayerItem] {
84
+ guard !keeping.isEmpty else { return outgoing }
85
+ let survivingAssets = keeping.map { ObjectIdentifier($0.asset) }
86
+ let surviving = Set(survivingAssets)
87
+ return outgoing.filter { !surviving.contains(ObjectIdentifier($0.asset)) }
88
+ }
89
+
90
+ /// Single-item builder behind `buildPlayerItems`. Routes a routable
91
+ /// remote track through `server` when one is supplied.
89
92
  ///
90
93
  /// `queueItemId` is the lib-generated identity for this AVPlayerItem
91
94
  /// instance — set on the constructed item via the
@@ -95,53 +98,68 @@ public enum AVQueueBuilder {
95
98
  /// position; the value passed here is the corresponding entry.
96
99
  /// Never derived from any consumer-supplied TrackItem field —
97
100
  /// only `track.url` is required + reliable on consumer data.
101
+ ///
102
+ /// The consumer's ReplayGain claim over [track] is resolved and
103
+ /// validated here, once, and stamped on the item alongside those two.
104
+ /// The item is the only thing the audio path holds, and this is the
105
+ /// one place that has both it and the `TrackItem` it came from.
98
106
  static func makeItem(
99
107
  for track: TrackItem,
100
108
  queueItemId: String,
101
109
  config: PlayerConfig,
102
- cache: LookaheadCache? = nil
110
+ server: ReadThroughServer? = nil
103
111
  ) -> AVPlayerItem? {
104
- if let cachedURL = cache?.cachedURL(forUrl: track.url) {
105
- // Local file URL no headers needed. `cachedURL` is a pure
106
- // read; LRU bookkeeping is opt-in via `touch` so a read-heavy
107
- // playback path doesn't trigger a plist write per resolution.
108
- cache?.touch(url: track.url)
109
- // `automaticallyLoadedAssetKeys` pre-loads the listed keys
110
- // asynchronously while the AVPlayerItem itself becomes
111
- // addressable Apple's documented iOS 16+ preferred
112
- // construction pattern, shaving first-frame latency vs a
113
- // bare `AVPlayerItem(asset:)` that loads keys lazily on first
114
- // access.
115
- //
116
- // `"tracks"` is included here (file:// branch only — the
117
- // HTTP path in `makeItemRaw` keeps the original two-key set
118
- // so HLS / streaming items don't pay the variant-playlist +
119
- // segment-header round-trip at insert time). The local
120
- // tracks-key load is free; once it's loaded, the engine's
121
- // per-item `audioMix(for:)` install (called BEFORE
122
- // `player.insert`) gets a tap-bearing mix synchronously,
123
- // which is the only window AVPlayer honours per Apple's
124
- // contract (`audioMix` changes after the item starts
125
- // rendering are silently dropped).
126
- let asset = AVURLAsset(url: cachedURL)
127
- let item = AVPlayerItem(
112
+ // Remote tracks bind the read-through server's URL, which is stable for
113
+ // the session and never re-pointed. Whether the bytes come from the cache
114
+ // or the origin is the server's decision, made per request — the only
115
+ // moment it can be answered, because the cache fills long after the queue
116
+ // is built. Headers move to the origin leg with it.
117
+ //
118
+ // HLS is excluded: proxying a playlist would re-base its relative segment
119
+ // paths against the local URL. No server means no routing, which is what a
120
+ // consumer running without the lookahead cache gets.
121
+ let routed: URL? = Self.isRoutable(track.url) ? server.flatMap { srv in
122
+ URL(string: track.url).flatMap { srv.localURL(for: $0, sourceURL: track.url) }
123
+ } : nil
124
+
125
+ let item: AVPlayerItem?
126
+ if let routed {
127
+ let asset = AVURLAsset(url: routed)
128
+ let built = AVPlayerItem(
128
129
  asset: asset,
129
- automaticallyLoadedAssetKeys: ["playable", "duration", "tracks"]
130
+ automaticallyLoadedAssetKeys: ["playable", "duration"]
130
131
  )
131
- pinForwardBufferDuration(on: item)
132
- item.queueItemId = queueItemId
133
- item.sourceURL = track.url
134
- return item
132
+ pinForwardBufferDuration(on: built)
133
+ built.isRoutedThroughReadThroughServer = true
134
+ item = built
135
+ } else {
136
+ item = makeItemRaw(
137
+ urlString: track.url,
138
+ headers: config.httpHeaders,
139
+ userAgent: config.userAgent)
135
140
  }
136
- let item = makeItemRaw(
137
- urlString: track.url,
138
- headers: config.httpHeaders,
139
- userAgent: config.userAgent)
140
141
  item?.queueItemId = queueItemId
142
+ // The media's own URL, always — never the transport's. Everything that
143
+ // reads a container, a codec or a track identity out of a URL reads this.
141
144
  item?.sourceURL = track.url
145
+ item?.suppliedReplayGain = SuppliedReplayGain.from(track: track)
142
146
  return item
143
147
  }
144
148
 
149
+ /// Whether a URL is one the read-through server can carry.
150
+ ///
151
+ /// `http(s)` only, and not a manifest: AVFoundation resolves an HLS
152
+ /// playlist's relative segment paths against the URL it fetched the playlist
153
+ /// from, so a proxied playlist would point every segment at the local server
154
+ /// under a token that names none of them.
155
+ static func isRoutable(_ urlString: String) -> Bool {
156
+ guard let url = URL(string: urlString), let scheme = url.scheme?.lowercased(),
157
+ scheme == "http" || scheme == "https"
158
+ else { return false }
159
+ return url.pathExtension.lowercased() != "m3u8"
160
+ }
161
+
162
+
145
163
  /// Apple-acknowledged AVPlayer mitigation: explicitly setting
146
164
  /// `preferredForwardBufferDuration = 0.0` on every item closes a
147
165
  /// long-standing AVQueuePlayer bug where items can be dropped
@@ -198,9 +216,24 @@ public enum AVQueueBuilder {
198
216
  return item
199
217
  }
200
218
 
201
- /// Plain-Swift merging core takes primitive arguments so it stays
202
- /// clear of the Swift↔C++ interop boundary.
203
- /// `makeItemRaw(urlString:headers:userAgent:)` below
219
+ /// The consumer's headers with `userAgent` folded in, which is the request
220
+ /// shape every outbound leg uses: asset loads, cache prefetches, and the
221
+ /// read-through server's origin requests.
222
+ @_spi(QueuePlayerTests)
223
+ public static func mergedHeaders(
224
+ headers: [String: String]?,
225
+ userAgent: String?
226
+ ) -> [String: String] {
227
+ var merged: [String: String] = headers ?? [:]
228
+ if let ua = userAgent, !ua.isEmpty {
229
+ merged["User-Agent"] = ua
230
+ }
231
+ return merged
232
+ }
233
+
234
+ /// Plain-Swift options-dictionary builder — takes primitive arguments so
235
+ /// it stays clear of the Swift↔C++ interop boundary.
236
+ /// `makeItemRaw(urlString:headers:userAgent:)`
204
237
  /// calls this, and `makeItem(for:, config:)` unpacks its `PlayerConfig`
205
238
  /// argument into the same primitive pair and calls `makeItemRaw`.
206
239
  ///
@@ -209,18 +242,20 @@ public enum AVQueueBuilder {
209
242
  ///
210
243
  /// Key spelling: `AVURLAssetHTTPHeaderFieldsKey` is not a public
211
244
  /// Swift API. The raw Obj-C string still works at runtime. See
212
- /// NOTES.md §7. We do not implement an
213
- /// `AVAssetResourceLoaderDelegate` replacement — see
214
- /// `plan/blocked.md`.
245
+ /// NOTES.md §7.
246
+ ///
247
+ /// No `AVAssetResourceLoaderDelegate` is installed, and none will be: an
248
+ /// asset routed through a loader takes a custom URL scheme, and Apple states
249
+ /// AirPlay is unsupported for one. Remote tracks reach the lookahead cache
250
+ /// through a local HTTP server instead, which keeps AVFoundation's own HTTP
251
+ /// stack — including the redirect following a loader would have to
252
+ /// reimplement.
215
253
  @_spi(QueuePlayerTests)
216
254
  public static func httpOptionsRaw(
217
255
  headers: [String: String]?,
218
256
  userAgent: String?
219
257
  ) -> [String: Any] {
220
- var merged: [String: String] = headers ?? [:]
221
- if let ua = userAgent, !ua.isEmpty {
222
- merged["User-Agent"] = ua
223
- }
258
+ let merged = mergedHeaders(headers: headers, userAgent: userAgent)
224
259
  guard !merged.isEmpty else { return [:] }
225
260
  return ["AVURLAssetHTTPHeaderFieldsKey": merged]
226
261
  }
@@ -127,10 +127,3 @@ final class ArtworkLoader {
127
127
  private var _configHeaders: [String: String]?
128
128
  private var _configUserAgent: String?
129
129
  }
130
-
131
- private extension NSLock {
132
- func withLock<T>(_ body: () throws -> T) rethrows -> T {
133
- lock(); defer { unlock() }
134
- return try body()
135
- }
136
- }
@@ -53,11 +53,10 @@ final class ArtworkResolver {
53
53
  /// only one the consumer cares about. Threading: completion fires
54
54
  /// on main.
55
55
  func resolve(url: String?, completion: @escaping (UIImage) -> Void) {
56
- self.currentSeq &+= 1
57
- let seq = self.currentSeq
56
+ let seq = bumpSeq()
58
57
 
59
58
  loader.load(uri: url) { [weak self] image in
60
- guard let self = self, self.currentSeq == seq else { return }
59
+ guard let self = self, self.isCurrent(seq) else { return }
61
60
  completion(image ?? PlaceholderArtwork.image)
62
61
  }
63
62
  }
@@ -68,7 +67,7 @@ final class ArtworkResolver {
68
67
  /// already in transit will populate the loader's cache for a
69
68
  /// future `resolve(...)` that hits the same URL.
70
69
  func cancelInFlight() {
71
- currentSeq &+= 1
70
+ _ = bumpSeq()
72
71
  }
73
72
 
74
73
  /// Clear the underlying loader's cache + drop the in-flight
@@ -83,5 +82,24 @@ final class ArtworkResolver {
83
82
  /// Monotonic resolve-call counter. Captured into completion closures
84
83
  /// to drop responses for superseded requests, including same-URL
85
84
  /// re-resolves that the URL-string compare cannot distinguish.
85
+ /// Callers bump this on the queue that owns playback while the loader
86
+ /// reports completions on main, so the counter is the one piece of state
87
+ /// crossing between them and it carries its own lock.
88
+ private let seqLock = NSLock()
86
89
  private var currentSeq: UInt64 = 0
90
+
91
+ /// Advance the sequence and return the new value.
92
+ private func bumpSeq() -> UInt64 {
93
+ seqLock.lock()
94
+ defer { seqLock.unlock() }
95
+ currentSeq &+= 1
96
+ return currentSeq
97
+ }
98
+
99
+ /// Whether `seq` is still the newest request.
100
+ private func isCurrent(_ seq: UInt64) -> Bool {
101
+ seqLock.lock()
102
+ defer { seqLock.unlock() }
103
+ return currentSeq == seq
104
+ }
87
105
  }