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
@@ -52,6 +52,7 @@ final class CarPlayCoordinator {
52
52
 
53
53
  // MARK: - Connection state
54
54
 
55
+ private let connectionLock = NSLock()
55
56
  /// Whether the CarPlay scene is currently connected. Written by
56
57
  /// [CarPlaySceneDelegate] at scene attach / detach — the same points that
57
58
  /// fire the `onCarConnect` / `onCarDisconnect` callbacks — and read by the
@@ -63,7 +64,6 @@ final class CarPlayCoordinator {
63
64
  /// attached/detached flag rather than a connection count (the Android side
64
65
  /// counts because its headless controllers — Auto / Wear / Assistant — can
65
66
  /// overlap).
66
- private let connectionLock = NSLock()
67
67
  private var carConnectedFlag = false
68
68
 
69
69
  var isCarConnected: Bool {
@@ -72,9 +72,25 @@ final class CarPlayCoordinator {
72
72
  return carConnectedFlag
73
73
  }
74
74
 
75
+ /// Invoked on the main queue when a scene attaches. `TrackPlayer` uses it
76
+ /// to rebuild the queue mirror, which is only maintained while a car is
77
+ /// attached — without this a car connecting mid-session would show whatever
78
+ /// the mirror held when it was last connected.
79
+ var onCarAttached: (() -> Void)? {
80
+ get { connectionLock.withLock { carAttachedCallback } }
81
+ set { connectionLock.withLock { carAttachedCallback = newValue } }
82
+ }
83
+ /// Written by `TrackPlayer` on its player queue, read here on main.
84
+ private var carAttachedCallback: (() -> Void)?
85
+
75
86
  func setCarConnected(_ connected: Bool) {
76
87
  connectionLock.lock()
77
- defer { connectionLock.unlock() }
88
+ let wasConnected = carConnectedFlag
78
89
  carConnectedFlag = connected
90
+ let attached = carAttachedCallback
91
+ connectionLock.unlock()
92
+ if connected, !wasConnected {
93
+ attached?()
94
+ }
79
95
  }
80
96
  }
@@ -16,8 +16,8 @@ import Foundation
16
16
  /// the live `BrowseDataProvider`, `CarPlayBridge`,
17
17
  /// `PendingBrowseRequests`, and `ArtworkLoader` — all of which are
18
18
  /// also written from the JS-side `TrackPlayer` Hybrid path, so the
19
- /// scene delegate sees fresh data without needing a constructor
20
- /// injection seam.
19
+ /// scene delegate sees fresh data without anything being injected
20
+ /// through its initialiser.
21
21
  /// **Class is `internal` (not `public`) and uses `@objc(<bare-name>)`:**
22
22
  /// the auto-generated `<Module>-Swift.h` only emits public Swift
23
23
  /// classes; `internal` keeps this one out of the bridging header so
@@ -71,14 +71,15 @@ final class AirPlayRouteState {
71
71
  /// dispatching a tap to the picker's internal UIButton. Used by
72
72
  /// `CastManager.showSystemPicker()`.
73
73
  ///
74
- /// Caveat: the internal button is private layout, walked via
75
- /// recursive subview search rather than direct child lookup so the
76
- /// picker keeps working when Apple reshapes the view hierarchy
77
- /// (iOS 15 nested the button one level deeper; further reshuffles
78
- /// are likely). The mitigation here is "best-effort + fail-quiet":
79
- /// when no UIButton is found the call is a no-op and the consumer
80
- /// can fall back to rendering its own `AVRoutePickerView` in app UI
81
- /// (Apple's recommended pattern).
74
+ /// Workaround: `AVRoutePickerView` has no public API to present its
75
+ /// sheet, so this walks the view's subviews for its `UIButton` and
76
+ /// fires its `.touchUpInside` actions. The walk is recursive rather
77
+ /// than a direct child lookup because the button's depth has changed
78
+ /// between iOS releases. When no button is found the call is a no-op;
79
+ /// a consumer that needs a guaranteed sheet renders its own
80
+ /// `AVRoutePickerView`, which is the supported pattern.
81
+ /// TODO: monitor upstream for fixes — https://github.com/react-native-video/react-native-video/issues/3119
82
+ /// (the same subview walk, and the request for a public entry point).
82
83
  func showSystemPicker() {
83
84
  DispatchQueue.main.async {
84
85
  let picker = self.getOrCreatePicker()
@@ -40,10 +40,16 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
40
40
  private var _lastVolume: Float = .nan
41
41
  private var _lastVolumeEmitted: Float = .nan
42
42
 
43
+ private var _hasReportedState = false
44
+
43
45
  var lastState: RemotePlaybackStateKind {
44
46
  stateLock.lock(); defer { stateLock.unlock() }
45
47
  return _lastState
46
48
  }
49
+ var hasReportedState: Bool {
50
+ stateLock.lock(); defer { stateLock.unlock() }
51
+ return _hasReportedState
52
+ }
47
53
  var lastPositionMs: Int64 {
48
54
  stateLock.lock(); defer { stateLock.unlock() }
49
55
  return _lastPositionMs
@@ -143,8 +149,15 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
143
149
 
144
150
  private func updateState(_ s: RemotePlaybackStateKind) {
145
151
  stateLock.lock()
152
+ let previous = _lastState
146
153
  _lastState = s
154
+ if s != .idle { _hasReportedState = true }
147
155
  stateLock.unlock()
156
+ #if DEBUG
157
+ if previous != s {
158
+ NSLog("[ChromecastSession] receiver state \(previous) -> \(s)")
159
+ }
160
+ #endif
148
161
  fireOnEach(stateListeners) { $0(s) }
149
162
  }
150
163
 
@@ -302,11 +315,14 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
302
315
  }
303
316
  }
304
317
 
305
- func play() { fireRemote("play") { _ = $0.play() } }
306
- func pause() { fireRemote("pause") { _ = $0.pause() } }
307
- func stop() { fireRemote("stop") { _ = $0.stop() } }
308
- func queueNext() { fireRemote("queueNext") { _ = $0.queueNextItem() } }
309
- func queuePrev() { fireRemote("queuePrev") { _ = $0.queuePreviousItem() } }
318
+ // No bitmask flag gates `play` the mask models pause, not resume.
319
+ func play() { fireRemote("play") { $0.play() } }
320
+ func pause() { fireRemote("pause", requires: kGCKMediaCommandPause) { $0.pause() } }
321
+ func stop() { fireRemote("stop") { $0.stop() } }
322
+ func queueNext() { fireRemote("queueNext", requires: kGCKMediaCommandQueueNext) { $0.queueNextItem() } }
323
+ func queuePrev() {
324
+ fireRemote("queuePrev", requires: kGCKMediaCommandQueuePrevious) { $0.queuePreviousItem() }
325
+ }
310
326
  func skipToQueueIndex(index: Int) {
311
327
  guard index >= 0 else { return }
312
328
  // Resolve the absolute lib index to the receiver's itemId via the full
@@ -315,13 +331,15 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
315
331
  // model hasn't synced that slot yet; skip rather than jump blind.
316
332
  fireRemote("skipToQueueIndex") { client in
317
333
  let itemId = client.mediaQueue.itemID(at: UInt(index))
318
- guard itemId != kGCKMediaQueueInvalidItemID else { return }
319
- _ = client.queueJumpToItem(withID: itemId)
334
+ guard itemId != kGCKMediaQueueInvalidItemID else { return nil }
335
+ return client.queueJumpToItem(withID: itemId)
320
336
  }
321
337
  }
322
338
 
323
339
  func setPlaybackRate(_ rate: Float) {
324
- fireRemote("setPlaybackRate") { _ = $0.setPlaybackRate(rate) }
340
+ fireRemote("setPlaybackRate", requires: kGCKMediaCommandSetPlaybackRate) {
341
+ $0.setPlaybackRate(rate)
342
+ }
325
343
  }
326
344
 
327
345
  func setRepeatMode(_ mode: RemoteRepeatMode) {
@@ -331,7 +349,7 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
331
349
  case .single: sdkMode = .single
332
350
  case .all: sdkMode = .all
333
351
  }
334
- fireRemote("setRepeatMode") { _ = $0.queueSetRepeatMode(sdkMode) }
352
+ fireRemote("setRepeatMode") { $0.queueSetRepeatMode(sdkMode) }
335
353
  }
336
354
 
337
355
  func seekTo(positionMs: Int64) {
@@ -589,9 +607,21 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
589
607
  /// `GCKRemoteMediaClient`. Silently skips when the session is closed
590
608
  /// or the client is unavailable (log only — transport calls are
591
609
  /// fire-and-forget).
610
+ ///
611
+ /// `block` returns the `GCKRequest` the SDK scheduled, or `nil` when it
612
+ /// declined to issue one. A returned request gets a delegate so a
613
+ /// receiver-side rejection is reported instead of vanishing — without it
614
+ /// a refused command is indistinguishable from one the receiver honoured.
615
+ ///
616
+ /// `requires` names the `GCKMediaStatus` command bitmask flag the receiver
617
+ /// must advertise for this command (`nil` for commands not gated by the
618
+ /// mask). The receiver publishes support per media item, so an item can
619
+ /// accept queue commands while refusing `pause` — worth a warning, since
620
+ /// the command is otherwise dropped in silence.
592
621
  private func fireRemote(
593
622
  _ label: String,
594
- _ block: @escaping (GCKRemoteMediaClient) -> Void
623
+ requires command: NSInteger? = nil,
624
+ _ block: @escaping (GCKRemoteMediaClient) -> GCKRequest?
595
625
  ) {
596
626
  if closed { return }
597
627
  runOnMain { [weak self] in
@@ -600,7 +630,28 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
600
630
  NSLog("[ChromecastSession] \(label): no GCKRemoteMediaClient — session not fully live?")
601
631
  return
602
632
  }
603
- block(client)
633
+ if let command = command,
634
+ let status = client.mediaStatus,
635
+ !status.isMediaCommandSupported(command) {
636
+ NSLog(
637
+ "[ChromecastSession] \(label): receiver does not advertise support for this command; sending anyway"
638
+ )
639
+ }
640
+ #if DEBUG
641
+ NSLog("[ChromecastSession] \(label): issuing")
642
+ #endif
643
+ // `nil` means this call site declined to issue anything — the only such
644
+ // case is a queue index the receiver's model hasn't synced yet, which is
645
+ // a normal transient, not a fault.
646
+ guard let request = block(client) else {
647
+ #if DEBUG
648
+ NSLog("[ChromecastSession] \(label): no request issued")
649
+ #endif
650
+ return
651
+ }
652
+ let delegate = TransportRequestDelegate(label: label, owner: self)
653
+ self.retain(delegate)
654
+ request.delegate = delegate
604
655
  }
605
656
  }
606
657
 
@@ -707,6 +758,34 @@ fileprivate final class RequestContinuation: NSObject, GCKRequestDelegate {
707
758
  }
708
759
  }
709
760
 
761
+ /// Reports the outcome of a fire-and-forget transport request. The caller
762
+ /// doesn't await these, so without a delegate a receiver that refuses a
763
+ /// command leaves no trace at all.
764
+ fileprivate final class TransportRequestDelegate: NSObject, GCKRequestDelegate {
765
+ let label: String
766
+ weak var owner: ChromecastSession?
767
+
768
+ init(label: String, owner: ChromecastSession) {
769
+ self.label = label
770
+ self.owner = owner
771
+ super.init()
772
+ }
773
+
774
+ func requestDidComplete(_ request: GCKRequest) {
775
+ owner?.drop(self)
776
+ }
777
+
778
+ func request(_ request: GCKRequest, didFailWithError error: GCKError) {
779
+ NSLog("[ChromecastSession] \(label) failed: \(error.localizedDescription)")
780
+ owner?.drop(self)
781
+ }
782
+
783
+ func request(_ request: GCKRequest, didAbortWith abortReason: GCKRequestAbortReason) {
784
+ NSLog("[ChromecastSession] \(label) aborted: \(abortReason.rawValue)")
785
+ owner?.drop(self)
786
+ }
787
+ }
788
+
710
789
  /// Bridge `seek(with:)`'s `GCKRequest` into the session's
711
790
  /// `onSeekCompleted` listener fan-out. Fire-and-forget — failures are
712
791
  /// logged but do not surface as JS errors (the lib has no "seek-
@@ -15,6 +15,14 @@ import NitroModules
15
15
  /// `setActive`: once a session is removed from the router and its
16
16
  /// listeners are disposed, no further events from it can fire
17
17
  /// `owner.emitRemoteState`.
18
+ ///
19
+ /// The listener and session fields are main-thread state: the Cast SDK
20
+ /// delivers session and listener callbacks on main, and the owner's `start`
21
+ /// and `stop` hop there from its player queue rather than touching the fields
22
+ /// where they are called. The deactivation snapshot is the exception — it is
23
+ /// written from the player queue at handoff and has to be visible to a
24
+ /// deactivation that lands on main right after, so it sits behind a lock.
25
+ /// `seedStartMetadata` reads the owner's track list on the owner's queue.
18
26
  final class CastEventBridge {
19
27
 
20
28
  private weak var owner: TrackPlayer?
@@ -37,11 +45,21 @@ final class CastEventBridge {
37
45
  /// Receiver position at the moment the active session was cleared.
38
46
  /// The local engine can read this to resume from where the
39
47
  /// receiver was. Cleared when a new session activates.
40
- private(set) var lastPositionMsOnDeactivate: Int64 = 0
48
+ private(set) var lastPositionMsOnDeactivate: Int64 {
49
+ get { snapshotLock.withLock { positionMsOnDeactivate } }
50
+ set { snapshotLock.withLock { positionMsOnDeactivate = newValue } }
51
+ }
41
52
 
42
53
  /// Receiver-side current track index at deactivation. `-1` when
43
54
  /// unknown.
44
- private(set) var lastTrackIndexOnDeactivate: Int = -1
55
+ private(set) var lastTrackIndexOnDeactivate: Int {
56
+ get { snapshotLock.withLock { trackIndexOnDeactivate } }
57
+ set { snapshotLock.withLock { trackIndexOnDeactivate = newValue } }
58
+ }
59
+
60
+ private let snapshotLock = NSLock()
61
+ private var positionMsOnDeactivate: Int64 = 0
62
+ private var trackIndexOnDeactivate: Int = -1
45
63
 
46
64
  /// Last lib index emitted for the receiver's current item. Dedups
47
65
  /// repeat receiver statuses carrying the same resolved index so
@@ -54,19 +72,34 @@ final class CastEventBridge {
54
72
  }
55
73
 
56
74
  func start() {
57
- if activeChangeDisposer != nil { return }
58
- activeChangeDisposer = PlaybackStateRouter.shared.addActiveChangeListener {
59
- [weak self] session in
60
- self?.handleActiveSessionChange(to: session)
75
+ onMain { [self] in
76
+ if self.activeChangeDisposer != nil { return }
77
+ self.activeChangeDisposer = PlaybackStateRouter.shared.addActiveChangeListener {
78
+ [weak self] session in
79
+ self?.handleActiveSessionChange(to: session)
80
+ }
61
81
  }
62
82
  }
63
83
 
64
84
  func stop() {
65
- activeChangeDisposer?()
66
- activeChangeDisposer = nil
67
- sessionListenerDisposers.forEach { $0() }
68
- sessionListenerDisposers.removeAll()
69
- currentActiveSession = nil
85
+ onMain {
86
+ self.activeChangeDisposer?()
87
+ self.activeChangeDisposer = nil
88
+ self.sessionListenerDisposers.forEach { $0() }
89
+ self.sessionListenerDisposers.removeAll()
90
+ self.currentActiveSession = nil
91
+ }
92
+ }
93
+
94
+ /// Runs `body` on main, in order with the SDK callbacks that own this
95
+ /// object's state; inline when already there so a caller on main sees its
96
+ /// effect before returning.
97
+ private func onMain(_ body: @escaping () -> Void) {
98
+ if Thread.isMainThread {
99
+ body()
100
+ } else {
101
+ DispatchQueue.main.async(execute: body)
102
+ }
70
103
  }
71
104
 
72
105
  /// Seed the deactivation snapshot from the local pre-handoff position +
@@ -75,13 +108,18 @@ final class CastEventBridge {
75
108
  /// lands local on the right spot. A receiver item resolved at
76
109
  /// deactivation takes precedence.
77
110
  func seedDeactivationSnapshot(trackIndex: Int, positionMs: Int64) {
78
- lastTrackIndexOnDeactivate = trackIndex
79
- lastPositionMsOnDeactivate = positionMs
111
+ snapshotLock.withLock {
112
+ trackIndexOnDeactivate = trackIndex
113
+ positionMsOnDeactivate = positionMs
114
+ }
80
115
  }
81
116
 
82
117
  // MARK: - Active-flow observer
83
118
 
84
119
  private func handleActiveSessionChange(to newSession: CastSessionRemotePlayer?) {
120
+ // The router fires listeners on the caller's thread; every caller today
121
+ // is on main, and the fields below are read and written on that basis.
122
+ dispatchPrecondition(condition: .onQueue(.main))
85
123
  let prior = currentActiveSession
86
124
  if newSession == nil, let prior = prior {
87
125
  // Capture deactivation snapshot BEFORE tearing down listeners so
@@ -152,8 +190,7 @@ final class CastEventBridge {
152
190
  // MARK: - Lockscreen metadata mirror
153
191
 
154
192
  /// Seed the start track's lockscreen metadata after a `setQueue` so the
155
- /// surface doesn't wait for the first receiver status. Called on the
156
- /// main thread from `TrackPlayer.setQueue`.
193
+ /// surface doesn't wait for the first receiver status.
157
194
  func seedStartMetadata(startIndex: Int) {
158
195
  pushMetadata(forIndex: startIndex)
159
196
  }
@@ -175,19 +212,22 @@ final class CastEventBridge {
175
212
 
176
213
  /// Resolve a lib track index to its `TrackItem` and push its metadata
177
214
  /// to the cast lockscreen controller. Out-of-range indices are
178
- /// ignored (the controller keeps its prior snapshot).
215
+ /// ignored (the controller keeps its prior snapshot). The track list is
216
+ /// read on the queue that owns it, whichever thread this is called from.
179
217
  private func pushMetadata(forIndex index: Int) {
180
218
  guard let owner = owner else { return }
181
- let tracks = owner.tracks
182
- guard index >= 0, index < tracks.count else { return }
183
- let track = tracks[index]
184
- CastNowPlayingController.shared.updateTrackMetadata(
185
- title: track.title,
186
- artist: track.artist,
187
- album: track.album,
188
- durationSec: track.duration ?? 0,
189
- artworkUrl: track.artworkUrl
190
- )
219
+ owner.playerQueue.async {
220
+ guard index >= 0, index < owner.tracks.count else { return }
221
+ let track = owner.tracks[index]
222
+ // The controller hops to main itself.
223
+ CastNowPlayingController.shared.updateTrackMetadata(
224
+ title: track.title,
225
+ artist: track.artist,
226
+ album: track.album,
227
+ durationSec: track.duration ?? 0,
228
+ artworkUrl: track.artworkUrl
229
+ )
230
+ }
191
231
  }
192
232
 
193
233
  }
@@ -66,19 +66,34 @@ final class CastNowPlayingController {
66
66
  /// no-ops. Called from `TrackPlayer.configure` after the engine is
67
67
  /// set up.
68
68
  func start() {
69
- if activeChangeDisposer != nil { return }
70
- activeChangeDisposer = PlaybackStateRouter.shared.addActiveChangeListener {
71
- [weak self] session in
72
- self?.handleActiveChange(to: session)
69
+ onMain { [self] in
70
+ if self.activeChangeDisposer != nil { return }
71
+ self.activeChangeDisposer = PlaybackStateRouter.shared.addActiveChangeListener {
72
+ [weak self] session in
73
+ self?.handleActiveChange(to: session)
74
+ }
75
+ }
76
+ }
77
+
78
+ /// Every field here is main-thread state — the router and session
79
+ /// callbacks arrive on main — so the owner's entry points, called on its
80
+ /// player queue, hop rather than touch the fields where they are called.
81
+ private func onMain(_ body: @escaping () -> Void) {
82
+ if Thread.isMainThread {
83
+ body()
84
+ } else {
85
+ DispatchQueue.main.async(execute: body)
73
86
  }
74
87
  }
75
88
 
76
89
  /// Stop observing + tear down any active mirror. Called from
77
90
  /// `TrackPlayer.internalDestroy`.
78
91
  func stop() {
79
- deactivate()
80
- activeChangeDisposer?()
81
- activeChangeDisposer = nil
92
+ onMain {
93
+ self.deactivate()
94
+ self.activeChangeDisposer?()
95
+ self.activeChangeDisposer = nil
96
+ }
82
97
  }
83
98
 
84
99
  /// Push current-track metadata. Called by `TrackPlayer` when the
@@ -92,26 +107,33 @@ final class CastNowPlayingController {
92
107
  durationSec: Double,
93
108
  artworkUrl: String?
94
109
  ) {
95
- currentMetadata = TrackMetadata(
96
- title: title, artist: artist, album: album,
97
- durationSec: durationSec, artworkUrl: artworkUrl
98
- )
99
- if currentSession != nil {
100
- refreshNowPlayingInfo()
101
- resolveArtwork()
110
+ onMain {
111
+ self.currentMetadata = TrackMetadata(
112
+ title: title, artist: artist, album: album,
113
+ durationSec: durationSec, artworkUrl: artworkUrl
114
+ )
115
+ if self.currentSession != nil {
116
+ self.refreshNowPlayingInfo()
117
+ self.resolveArtwork()
118
+ }
102
119
  }
103
120
  }
104
121
 
105
122
  /// Forward the player's artwork HTTP config so authenticated artwork
106
123
  /// URLs resolve during cast. Called from `TrackPlayer.configure`.
107
124
  func configureArtwork(headers: [String: String]?, userAgent: String?) {
108
- artworkResolver.configHeaders = headers
109
- artworkResolver.configUserAgent = userAgent
125
+ onMain {
126
+ self.artworkResolver.configHeaders = headers
127
+ self.artworkResolver.configUserAgent = userAgent
128
+ }
110
129
  }
111
130
 
112
131
  // MARK: - Active-flow observer
113
132
 
114
133
  private func handleActiveChange(to session: CastSessionRemotePlayer?) {
134
+ // The router fires listeners on the caller's thread; every caller today
135
+ // is on main, and the fields here are read and written on that basis.
136
+ dispatchPrecondition(condition: .onQueue(.main))
115
137
  if let session, !session.isPushSink {
116
138
  activate(with: session)
117
139
  } else {
@@ -45,6 +45,17 @@ protocol CastSessionRemotePlayer: CastSession {
45
45
  /// Snapshot of most-recent receiver-driven state.
46
46
  var lastState: RemotePlaybackStateKind { get }
47
47
 
48
+ /// `true` once the receiver has reported any state other than `.idle`.
49
+ /// Set by the first such report and stays set for the session's lifetime;
50
+ /// nothing clears it.
51
+ ///
52
+ /// State reads use this to decide whether the receiver can answer yet: a
53
+ /// session that has just activated still holds its initial `.idle`, and the
54
+ /// local engine is the better answer until the receiver speaks. Once it
55
+ /// has, the receiver is authoritative — including a later `.idle`, which
56
+ /// means it genuinely has nothing loaded.
57
+ var hasReportedState: Bool { get }
58
+
48
59
  /// Most-recent receiver-reported stream position in milliseconds.
49
60
  /// `0` before the first status arrives.
50
61
  var lastPositionMs: Int64 { get }
@@ -15,27 +15,38 @@ import Foundation
15
15
  /// Mirrors Android's `internal object CastTransportRouter`.
16
16
  enum CastTransportRouter {
17
17
 
18
+ /// Hands a receiver command to main. The Cast SDK is main-only, while the
19
+ /// decision above it — whether a remote session is live — reads a
20
+ /// lock-guarded snapshot and is safe from any thread, so only the command
21
+ /// itself crosses.
22
+ /// Always asynchronous, never inline-when-already-on-main: mixing the two
23
+ /// lets a command issued later run before one issued earlier — a CarPlay tap
24
+ /// arriving on main would overtake a JS seek that is still enqueued.
25
+ private static func onMain(_ body: @escaping () -> Void) {
26
+ DispatchQueue.main.async(execute: body)
27
+ }
28
+
18
29
  static func routePlay() -> Bool {
19
30
  guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
20
- remote.play()
31
+ onMain { remote.play() }
21
32
  return true
22
33
  }
23
34
 
24
35
  static func routePause() -> Bool {
25
36
  guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
26
- remote.pause()
37
+ onMain { remote.pause() }
27
38
  return true
28
39
  }
29
40
 
30
41
  static func routeStop() -> Bool {
31
42
  guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
32
- remote.stop()
43
+ onMain { remote.stop() }
33
44
  return true
34
45
  }
35
46
 
36
47
  static func routeSeekTo(positionMs: Int64) -> Bool {
37
48
  guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
38
- remote.seekTo(positionMs: positionMs)
49
+ onMain { remote.seekTo(positionMs: positionMs) }
39
50
  return true
40
51
  }
41
52
 
@@ -46,31 +57,32 @@ enum CastTransportRouter {
46
57
  /// session for AirPlay/local, where the caller seeks the local engine).
47
58
  static func routeSeekBy(deltaMs: Int64) -> Bool {
48
59
  guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
49
- remote.seekBy(deltaMs: deltaMs)
60
+ onMain { remote.seekBy(deltaMs: deltaMs) }
50
61
  return true
51
62
  }
52
63
 
53
64
  static func routeSkipToNext() -> Bool {
54
65
  guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
55
- remote.queueNext()
66
+ onMain { remote.queueNext() }
56
67
  return true
57
68
  }
58
69
 
59
70
  static func routeSkipToPrevious() -> Bool {
60
71
  guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
61
- remote.queuePrev()
72
+ onMain { remote.queuePrev() }
62
73
  return true
63
74
  }
64
75
 
65
76
  static func routeSkipToIndex(index: Int) -> Bool {
66
77
  guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
67
- remote.skipToQueueIndex(index: index)
78
+ onMain { remote.skipToQueueIndex(index: index) }
68
79
  return true
69
80
  }
70
81
 
71
82
  static func routeSetVolume(value: Double) -> Bool {
72
83
  guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
73
- remote.setVolume(Float(max(0.0, min(1.0, value))))
84
+ let clamped = Float(max(0.0, min(1.0, value)))
85
+ onMain { remote.setVolume(clamped) }
74
86
  return true
75
87
  }
76
88