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,32 @@
1
+ import Foundation
2
+
3
+ /// Whether the network returning should trigger a retry.
4
+ ///
5
+ /// Pure and primitive-typed so it can be asserted directly: the player state
6
+ /// it derives from is a Nitrogen-backed type, whose members are not reachable
7
+ /// from the test module under cxx-interop.
8
+ enum NetworkRecoveryPolicy {
9
+
10
+ /// - Parameters:
11
+ /// - wantsToPlay: the user's transport intent.
12
+ /// - isInterrupted: whether an audio-session interruption is in force.
13
+ /// - isInError: whether the current track has a failure that has not
14
+ /// recovered. Read from the error key rather than the reported state:
15
+ /// the state has moved on by the time a network returns.
16
+ /// - isTransient: whether that failure is one a network can cure
17
+ /// (`PlaybackErrorMapping.isTransient`). A codec, format or auth
18
+ /// failure is not, and rebuilding it on every network edge would
19
+ /// re-surface the same error each time.
20
+ ///
21
+ /// All four must hold. Intent alone would start audio that was never
22
+ /// playing when the network happens to change; ignoring the interruption
23
+ /// would start it mid-call; without the error check every interface
24
+ /// handoff would rebuild a perfectly healthy item; and without the
25
+ /// transient check a fatal failure would be retried without end.
26
+ @_spi(QueuePlayerTests)
27
+ public static func shouldRetry(
28
+ wantsToPlay: Bool, isInterrupted: Bool, isInError: Bool, isTransient: Bool
29
+ ) -> Bool {
30
+ wantsToPlay && !isInterrupted && isInError && isTransient
31
+ }
32
+ }
@@ -29,6 +29,7 @@ enum NowPlayingFormatExtractor {
29
29
  // strict concurrency. Hop to main for the read; the remainder of
30
30
  // this function works against the captured asset reference.
31
31
  let asset: AVAsset = await MainActor.run { item.asset }
32
+ let sourceURL: String? = await MainActor.run { item.sourceURL }
32
33
  let tracks: [AVAssetTrack]
33
34
  do {
34
35
  tracks = try await asset.load(.tracks)
@@ -72,7 +73,10 @@ enum NowPlayingFormatExtractor {
72
73
  if asbd.mBitsPerChannel > 0 { bitsPerChannel = Double(asbd.mBitsPerChannel) }
73
74
  }
74
75
 
75
- let url = (asset as? AVURLAsset)?.url
76
+ // The media's URL, not the transport's: a routed track's asset URL is the
77
+ // read-through server's, which has no extension to map a container from
78
+ // and is never a file URL even when the bytes come off local disk.
79
+ let url = sourceURL.flatMap { URL(string: $0) } ?? (asset as? AVURLAsset)?.url
76
80
  let container = mapContainer(url: url)
77
81
  let mimeType = mapMimeType(codec: codec)
78
82
  var bitrate: Double? = estimatedDataRate > 0 ? Double(estimatedDataRate) : nil
@@ -82,15 +86,16 @@ enum NowPlayingFormatExtractor {
82
86
  // (`kAudioFilePropertyBitRate`) — reliable for CBR, average
83
87
  // for VBR-with-Xing. MP3-specific because the local-file
84
88
  // AudioFile API needs the format hint up-front.
85
- // 2. Streaming http(s):// of any codec: `StreamingBitrateProbe`
86
- // issues a Range:0-65535 fetch and parses via Core Audio's
87
- // `AudioFileStream` (codec-agnostic). Apple does not expose
88
- // a reliable bitrate field for progressive streams via
89
- // AVFoundation; the probe fills the gap.
90
- // 3. HLS or any other streaming source the probe couldn't
91
- // parse: `event.averageAudioBitrate` / `event.indicatedBitrate`
92
- // from the AVPlayer access log surfaces a real value for
93
- // HLS variants whose playlist declares BANDWIDTH.
89
+ // 2. Local file:// of any other codec: `StreamingBitrateProbe`
90
+ // reads a 256 KB prefix off disk and derives a rate via Core
91
+ // Audio's `AudioFileStream` (codec-agnostic).
92
+ // 3. Remote http(s)://, any codec: `event.averageAudioBitrate` /
93
+ // `event.indicatedBitrate` from the AVPlayer access log
94
+ // surfaces a real value for HLS variants whose playlist
95
+ // declares BANDWIDTH, and for progressive servers that declare
96
+ // one. A remote source whose server declares nothing has no
97
+ // bitrate: `estimatedDataRate` reports 0 and nothing reads the
98
+ // container header off the wire.
94
99
  if bitrate == nil, let url = url {
95
100
  if url.isFileURL {
96
101
  // Local file: try Core Audio's `AudioFile`-based reader
@@ -98,8 +103,7 @@ enum NowPlayingFormatExtractor {
98
103
  // single property read). Falls back to the
99
104
  // `AudioFileStream` packet-derived probe for codecs
100
105
  // without a dedicated reader OR when AudioFile returned
101
- // 0 (some transcoded MP3s — the lookahead cache's
102
- // Subsonic-served files exhibit this).
106
+ // 0 (some server-transcoded MP3s).
103
107
  if codec == .mp3 {
104
108
  bitrate = await readLocalMP3Bitrate(url: url)
105
109
  }
@@ -111,10 +115,13 @@ enum NowPlayingFormatExtractor {
111
115
  }
112
116
  } else if let scheme = url.scheme?.lowercased(),
113
117
  scheme == "http" || scheme == "https" {
114
- bitrate = await StreamingBitrateProbe.bitrate(forUrl: url)
115
- if bitrate == nil, codec == .mp3 {
116
- bitrate = readStreamingMP3Bitrate(item: item)
117
- }
118
+ // Remote: nothing here reads bytes off the wire. The library issues
119
+ // no side-band range request for a remote track, so the only source
120
+ // is what the player itself reports — the access log, which declares
121
+ // a bitrate for HLS variants and for progressive servers that send
122
+ // one. Not gated on codec: the access log reports whatever the player
123
+ // negotiated, and MP3 is neither the only nor the usual codec.
124
+ bitrate = readStreamingBitrate(item: item)
118
125
  }
119
126
  }
120
127
 
@@ -310,7 +317,7 @@ enum NowPlayingFormatExtractor {
310
317
  } catch {
311
318
  return nil
312
319
  }
313
- guard let info = MetadataReader.parseFLACStreamInfo(header) else {
320
+ guard let info = FLACStreamInfo.parse(header) else {
314
321
  return nil
315
322
  }
316
323
  guard info.sampleRate > 0, info.totalSamples > 0 else { return nil }
@@ -351,7 +358,7 @@ enum NowPlayingFormatExtractor {
351
358
  /// main); `AVPlayerItem.accessLog()` returns a snapshot and is
352
359
  /// safe to call from any thread, matching the off-main usage in
353
360
  /// `TrackPlayer.swift`'s DEBUG access-log timeline logger.
354
- static func readStreamingMP3Bitrate(item: AVPlayerItem) -> Double? {
361
+ static func readStreamingBitrate(item: AVPlayerItem) -> Double? {
355
362
  guard let event = item.accessLog()?.events.last else { return nil }
356
363
  let avgAudio: Double = event.averageAudioBitrate
357
364
  if avgAudio.isFinite, avgAudio > 0 { return avgAudio }
@@ -18,15 +18,18 @@ import MediaPlayer
18
18
  /// interpolation honest across long-running playback, rate changes, and
19
19
  /// background-thread suspensions.
20
20
  ///
21
- /// Threading: every public method MUST be called from the main thread.
22
- /// Callers in `TrackPlayer` either run inside `onMain {}` or are invoked
23
- /// from KVO bodies that have already async-hopped to main.
21
+ /// Threading: callable from any queue. Every public method hands its work
22
+ /// to main, which owns both the Now Playing surface and this object's state;
23
+ /// `TrackPlayer` calls in from `playerQueue`. Player state
24
+ /// is pushed in as a `Snapshot` argument — this writer never reads
25
+ /// player state itself, so it holds no synchronous read edge back into
26
+ /// whichever context owns that state.
24
27
  final class NowPlayingInfo {
25
28
 
26
- /// Snapshot of player state at emit time. The provider closure is
27
- /// responsible for reading every field within the same main-thread
28
- /// tick — divergent values (elapsed taken at t0, rate at t0+Δ) show
29
- /// up on the lock-screen scrubber as jitter.
29
+ /// Snapshot of player state at emit time. The caller is responsible
30
+ /// for reading every field within the same tick — divergent values
31
+ /// (elapsed taken at t0, rate at t0+Δ) show up on the lock-screen
32
+ /// scrubber as jitter.
30
33
  struct Snapshot {
31
34
  let track: TrackItem?
32
35
  /// Current playback position in seconds. NaN / ±Inf coerce to 0.
@@ -34,15 +37,13 @@ final class NowPlayingInfo {
34
37
  /// Total duration in seconds. NaN / ±Inf / ≤0 → omit (signals
35
38
  /// live-stream / unknown to the system).
36
39
  let durationSeconds: Double
37
- /// `AVPlayer.rate`. 0 paused, 1 playing at speed, etc.
40
+ /// Effective playback rate: the configured speed while playing, 0
41
+ /// otherwise. Not the player's requested rate, which is non-zero while
42
+ /// waiting to play and would extrapolate the scrubber through a stall.
38
43
  let rate: Float
39
44
  }
40
45
 
41
- /// Provider read on every emit. Owner sets it during `configure()`
42
- /// and clears it during `destroy()`.
43
- var snapshotProvider: (() -> Snapshot)?
44
-
45
- private var periodicTimer: Timer?
46
+ private var periodicTimer: DispatchSourceTimer?
46
47
  /// Subscription to the global cast active-session router. When a
47
48
  /// remote session is active, `CastNowPlayingController` owns the
48
49
  /// lockscreen surface and this writer becomes a no-op — every
@@ -54,21 +55,38 @@ final class NowPlayingInfo {
54
55
  init() {
55
56
  castRouterDisposer = PlaybackStateRouter.shared.addActiveChangeListener {
56
57
  [weak self] session in
57
- self?.castActive = (session != nil)
58
+ let active = (session != nil)
59
+ NowPlayingInfo.onMain { self?.castActive = active }
58
60
  }
59
61
  }
60
62
 
63
+ /// Hands `body` to main, where this object's state and the Now Playing
64
+ /// surface both live. Always asynchronous, never inline-when-already-on-main:
65
+ /// mixing the two lets a later write land before an earlier one, and the
66
+ /// surface is last-write-wins.
67
+ private static func onMain(_ body: @escaping @MainActor () -> Void) {
68
+ DispatchQueue.main.async { MainActor.assumeIsolated(body) }
69
+ }
70
+
61
71
  deinit {
62
72
  castRouterDisposer?()
73
+ // libdispatch retains an armed source, so one that is never cancelled
74
+ // keeps waking the main queue for the life of the process.
75
+ periodicTimer?.cancel()
63
76
  }
64
77
 
65
78
  /// Full refresh — metadata + position + rate. Call on track-change.
66
- /// A nil snapshot or a snapshot with `track == nil` (queue empty /
67
- /// pre-first-track) clears the dictionary so the lock-screen /
68
- /// control-center surface goes away.
69
- func refreshAll() {
79
+ /// A snapshot with `track == nil` (queue empty / pre-first-track)
80
+ /// clears the dictionary so the lock-screen / control-center surface
81
+ /// goes away.
82
+ func refreshAll(_ snapshot: Snapshot) {
83
+ NowPlayingInfo.onMain { self.refreshAllOnMain(snapshot) }
84
+ }
85
+
86
+ @MainActor
87
+ private func refreshAllOnMain(_ snapshot: Snapshot) {
70
88
  if castActive { return }
71
- guard let snapshot = snapshotProvider?(), snapshot.track != nil else {
89
+ guard snapshot.track != nil else {
72
90
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
73
91
  return
74
92
  }
@@ -90,13 +108,16 @@ final class NowPlayingInfo {
90
108
  /// an artwork-only payload.
91
109
  ///
92
110
  /// `boundsSize: image.size` and `requestHandler` returning the
93
- /// image unchanged matches the canonical SwiftAudioEx /
94
- /// react-native-track-player pattern (and Substreamer's real-car
95
- /// behaviour). The system handles size negotiation; over-engineering
96
- /// the handler (forced 1024² canvas + aspect-fit render) was tested
97
- /// and found to make no difference on the standalone CarPlay
98
- /// Simulator's main-art region.
111
+ /// image unchanged is the shape `MPMediaItemArtwork` documents; the
112
+ /// system handles size negotiation. A forced 1024² canvas with an
113
+ /// aspect-fit render makes no difference to the CarPlay main-art
114
+ /// region.
99
115
  func applyArtwork(_ image: UIImage) {
116
+ NowPlayingInfo.onMain { self.applyArtworkOnMain(image) }
117
+ }
118
+
119
+ @MainActor
120
+ private func applyArtworkOnMain(_ image: UIImage) {
100
121
  if castActive { return }
101
122
  guard var info = MPNowPlayingInfoCenter.default().nowPlayingInfo else { return }
102
123
  info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
@@ -116,9 +137,14 @@ final class NowPlayingInfo {
116
137
  /// surface stays absent until this method's late re-publish lands
117
138
  /// (typically ~100-500ms after play starts, via the state-change
118
139
  /// dispatch in [TrackPlayer]).
119
- func refreshPositionAndRate() {
140
+ func refreshPositionAndRate(_ snapshot: Snapshot) {
141
+ NowPlayingInfo.onMain { self.refreshPositionAndRateOnMain(snapshot) }
142
+ }
143
+
144
+ @MainActor
145
+ private func refreshPositionAndRateOnMain(_ snapshot: Snapshot) {
120
146
  if castActive { return }
121
- guard let snapshot = snapshotProvider?(), snapshot.track != nil else { return }
147
+ guard snapshot.track != nil else { return }
122
148
  guard var info = MPNowPlayingInfoCenter.default().nowPlayingInfo else { return }
123
149
  applyPositionAndRate(snapshot, into: &info)
124
150
  if snapshot.durationSeconds.isFinite && snapshot.durationSeconds > 0 {
@@ -130,31 +156,41 @@ final class NowPlayingInfo {
130
156
  /// Stops the periodic timer and clears the dictionary. Call on
131
157
  /// `destroy()` before the audio session deactivates.
132
158
  func clear() {
133
- stopPeriodicTimer()
134
- MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
135
- }
136
-
137
- /// Schedule the 10s periodic refresh on the main run loop. Idempotent.
138
- /// `tolerance = 1.0` lets the OS coalesce with other timers without
139
- /// affecting scrubber accuracy (the system extrapolates from elapsed
140
- /// + rate between emits). The closure short-circuits when the player
141
- /// is paused — elapsed is unchanged and rate is already 0, so the
142
- /// write would be pure noise.
143
- func startPeriodicTimer() {
144
- stopPeriodicTimer()
145
- let timer = Timer(timeInterval: 10, repeats: true) { [weak self] _ in
146
- guard let self,
147
- let snapshot = self.snapshotProvider?(),
148
- snapshot.rate != 0 else { return }
149
- self.refreshPositionAndRate()
159
+ NowPlayingInfo.onMain {
160
+ self.stopPeriodicTimerOnMain()
161
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
150
162
  }
151
- timer.tolerance = 1.0
152
- RunLoop.main.add(timer, forMode: .common)
163
+ }
164
+
165
+ /// Schedule the 10s periodic tick on the main queue. Restarts a running
166
+ /// tick rather than stacking a second one. A 1s leeway lets the OS
167
+ /// coalesce with other timers without affecting scrubber accuracy (the
168
+ /// system extrapolates from elapsed + rate between emits). `onTick`
169
+ /// builds the snapshot on the side that owns player state and pushes the
170
+ /// refresh; it carries the paused short-circuit, since only that side can
171
+ /// tell whether the write would be pure noise.
172
+ func startPeriodicTimer(onTick: @escaping () -> Void) {
173
+ NowPlayingInfo.onMain { self.startPeriodicTimerOnMain(onTick: onTick) }
174
+ }
175
+
176
+ @MainActor
177
+ private func startPeriodicTimerOnMain(onTick: @escaping () -> Void) {
178
+ stopPeriodicTimerOnMain()
179
+ let timer = DispatchSource.makeTimerSource(queue: .main)
180
+ timer.schedule(deadline: .now() + 10, repeating: 10, leeway: .seconds(1))
181
+ timer.setEventHandler { onTick() }
153
182
  periodicTimer = timer
183
+ // The source is resumed exactly once and never suspended — a suspended
184
+ // source traps libdispatch on release.
185
+ timer.resume()
154
186
  }
155
187
 
156
- func stopPeriodicTimer() {
157
- periodicTimer?.invalidate()
188
+ /// Cancel the tick source. `cancel()` is safe from any thread; calling it
189
+ /// on the source's own queue additionally guarantees no further tick runs
190
+ /// once it returns.
191
+ @MainActor
192
+ private func stopPeriodicTimerOnMain() {
193
+ periodicTimer?.cancel()
158
194
  periodicTimer = nil
159
195
  }
160
196
 
@@ -44,7 +44,17 @@ final class OutputRouteMonitor {
44
44
  /// Latest classification. Read at engine track-boundary fire
45
45
  /// so per-transition fade-vs-cut decisions reflect the current
46
46
  /// route without depending on a callback delivery deadline.
47
- private(set) var currentClass: OutputClass = .localMixable
47
+ ///
48
+ /// Written here on main as route changes arrive, read from the queue the
49
+ /// engine serialises on, so it carries its own lock.
50
+ var currentClass: OutputClass {
51
+ classLock.lock()
52
+ defer { classLock.unlock() }
53
+ return storedClass
54
+ }
55
+
56
+ private let classLock = NSLock()
57
+ private var storedClass: OutputClass = .localMixable
48
58
 
49
59
  /// Convenience accessor — `true` outside AirPlay 2 / CarPlay.
50
60
  var isCurrentRouteMixable: Bool {
@@ -107,8 +117,11 @@ final class OutputRouteMonitor {
107
117
  let outputs = AVAudioSession.sharedInstance().currentRoute.outputs
108
118
  let isNonMixable = outputs.contains { Self.isNonMixableOutput($0) }
109
119
  let newClass: OutputClass = isNonMixable ? .nonMixableRemote : .localMixable
110
- let flipped = newClass == .nonMixableRemote && currentClass == .localMixable
111
- currentClass = newClass
120
+ // One critical section: the flip test reads the value this write replaces.
121
+ classLock.lock()
122
+ let flipped = newClass == .nonMixableRemote && storedClass == .localMixable
123
+ storedClass = newClass
124
+ classLock.unlock()
112
125
  if flipped {
113
126
  onRouteBecameNonMixable?()
114
127
  }
@@ -47,7 +47,7 @@ final class PendingBrowseRequests {
47
47
  }
48
48
 
49
49
  /// Snapshot the pending parents without consuming. Production
50
- /// code drains via [drainAll]; this is a test-only seam. Stays
50
+ /// code drains via [drainAll]; this is for tests only. Stays
51
51
  /// `internal` (test target imports via `@testable import`) —
52
52
  /// mirrors the Kotlin `internal fun snapshotForTest` shape on
53
53
  /// the Android side.
@@ -17,7 +17,7 @@ enum PlaceholderArtwork {
17
17
 
18
18
  /// The active placeholder: the consumer-supplied image when one is
19
19
  /// configured + loadable, otherwise the bundled RNQP placeholder.
20
- static var image: UIImage { customImage ?? bundledImage }
20
+ static var image: UIImage { lock.withLock { customImage } ?? bundledImage }
21
21
 
22
22
  /// Set (or clear) the consumer-supplied placeholder from
23
23
  /// `PlayerConfig.placeholderArtworkUri`. Loaded from a local `file://`
@@ -26,17 +26,21 @@ enum PlaceholderArtwork {
26
26
  /// built-in is a local file, and a remote URL would break offline +
27
27
  /// change behaviour; anything else (or an unloadable file) leaves the
28
28
  /// bundled placeholder in effect. Decode is synchronous, so a very large
29
- /// consumer image briefly blocks the `configure` call. Main-thread only
30
- /// (called from `configure`).
29
+ /// consumer image briefly blocks the `configure` call. Called from
30
+ /// `configure` on the player queue; the readers are on main.
31
31
  static func setCustom(uri: String?) {
32
32
  guard let uri = uri, !uri.isEmpty,
33
33
  let url = URL(string: uri), url.scheme?.lowercased() == "file" else {
34
- customImage = nil
34
+ lock.withLock { customImage = nil }
35
35
  return
36
36
  }
37
- customImage = UIImage(contentsOfFile: url.path)
37
+ let image = UIImage(contentsOfFile: url.path)
38
+ lock.withLock { customImage = image }
38
39
  }
39
40
 
41
+ /// Written from `configure()` on the player queue, read wherever the
42
+ /// now-playing surfaces build their snapshot.
43
+ private static let lock = NSLock()
40
44
  private static var customImage: UIImage?
41
45
  private static let bundledImage: UIImage = loadImage()
42
46