react-native-queue-player 1.2.0 → 2.0.1

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 (362) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/consumer-rules.pro +1 -1
  4. package/android/src/main/cpp/airplay2_control.c +4 -4
  5. package/android/src/main/cpp/airplay2_control.h +4 -4
  6. package/android/src/main/cpp/airplay2_jni.cpp +3 -19
  7. package/android/src/main/cpp/airplay2_pair.c +7 -7
  8. package/android/src/main/cpp/airplay2_pair.h +2 -2
  9. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  10. package/android/src/main/cpp/airplay2_session.c +2 -2
  11. package/android/src/main/cpp/airplay_jni.cpp +8 -69
  12. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  61. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  62. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  63. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  64. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  65. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  114. package/app.plugin.js +17 -1
  115. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  116. package/ios/AVQueueBuilder.swift +120 -85
  117. package/ios/ArtworkLoader.swift +0 -7
  118. package/ios/ArtworkResolver.swift +22 -4
  119. package/ios/AssetReplayGainReader.swift +118 -0
  120. package/ios/AudioSession.swift +67 -16
  121. package/ios/AudioTapProvider.swift +284 -149
  122. package/ios/CarPlayBridge.swift +0 -12
  123. package/ios/CarPlayCoordinator.swift +18 -2
  124. package/ios/CarPlaySceneDelegate.swift +2 -2
  125. package/ios/Cast/AirPlayRouteState.swift +9 -8
  126. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  127. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  128. package/ios/Cast/Core/CastSession.swift +2 -2
  129. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  130. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  131. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  132. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  133. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  134. package/ios/CastManager.swift +1 -1
  135. package/ios/CrossfadeEngine.swift +672 -226
  136. package/ios/EQTap.swift +20 -17
  137. package/ios/Equalizer.swift +1 -1
  138. package/ios/FLACStreamInfo.swift +65 -0
  139. package/ios/GaplessEngine.swift +57 -30
  140. package/ios/InputGuards.swift +35 -5
  141. package/ios/LookaheadCache.swift +229 -58
  142. package/ios/LookaheadCachePrefetcher.swift +48 -36
  143. package/ios/MediaResponseCheck.swift +122 -0
  144. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  145. package/ios/NetworkRecoveryPolicy.swift +32 -0
  146. package/ios/NowPlayingFormatExtractor.swift +25 -18
  147. package/ios/NowPlayingInfo.swift +84 -48
  148. package/ios/OutputRouteMonitor.swift +16 -3
  149. package/ios/PendingBrowseRequests.swift +1 -1
  150. package/ios/PlaceholderArtwork.swift +9 -5
  151. package/ios/PlaybackEngine.swift +165 -76
  152. package/ios/PlaybackErrorMapping.swift +3 -3
  153. package/ios/PlaybackModeStateMachine.swift +2 -2
  154. package/ios/PlaybackNetworkMonitor.swift +73 -0
  155. package/ios/PlayerStateDerivation.swift +6 -6
  156. package/ios/QueueMutationArithmetic.swift +25 -0
  157. package/ios/QueueSkipArithmetic.swift +5 -3
  158. package/ios/QueueWindowArithmetic.swift +152 -0
  159. package/ios/ReadThroughServer.swift +283 -0
  160. package/ios/RemoteCommands.swift +44 -4
  161. package/ios/ReplayGainData.swift +22 -3
  162. package/ios/ReplayGainExtractor.swift +70 -27
  163. package/ios/Siri/VoiceDonation.swift +14 -9
  164. package/ios/StreamingBitrateProbe.swift +22 -68
  165. package/ios/SuppliedReplayGain.swift +83 -0
  166. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  167. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  168. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  169. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  170. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  171. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  172. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  173. package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
  174. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  175. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  176. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  177. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  178. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  179. package/ios/Tests/EngineSwapTests.swift +91 -0
  180. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  181. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  182. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  183. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  184. package/ios/Tests/InputGuardsTests.swift +41 -0
  185. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  186. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  187. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  188. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  189. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  190. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  191. package/ios/Tests/MediaResponseCheckTests.swift +128 -0
  192. package/ios/Tests/MutationDeferralTests.swift +220 -0
  193. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  194. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  195. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  196. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  197. package/ios/Tests/PlayerFixtures.swift +48 -0
  198. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  199. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  200. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  201. package/ios/Tests/QueueStateTests.swift +3 -1
  202. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  203. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  204. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  205. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  206. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  207. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  208. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  209. package/ios/Tests/RetryRecoveryTests.swift +418 -0
  210. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  211. package/ios/Tests/SkipIndexTests.swift +32 -2
  212. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  213. package/ios/Tests/StallRecoveryTests.swift +97 -0
  214. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  215. package/ios/Tests/TopUpWindowGateTests.swift +441 -0
  216. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  217. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  218. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  219. package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
  220. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  221. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  222. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  223. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  224. package/ios/TrackPlayer+Automotive.swift +101 -0
  225. package/ios/TrackPlayer+Cache.swift +232 -0
  226. package/ios/TrackPlayer+Config.swift +291 -0
  227. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  228. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  229. package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
  230. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  231. package/ios/TrackPlayer+Lifecycle.swift +982 -0
  232. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  233. package/ios/TrackPlayer+Queue.swift +951 -0
  234. package/ios/TrackPlayer+Recovery.swift +213 -0
  235. package/ios/TrackPlayer+Skip.swift +465 -0
  236. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  237. package/ios/TrackPlayer+State.swift +108 -0
  238. package/ios/TrackPlayer+Threading.swift +135 -0
  239. package/ios/TrackPlayer+Transport.swift +272 -0
  240. package/ios/TrackPlayer+Window.swift +227 -0
  241. package/ios/TrackPlayer.swift +331 -4619
  242. package/ios/Visualizer.swift +6 -5
  243. package/ios/tests-harness/Podfile +1 -1
  244. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  245. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  246. package/lib/module/hooks/useActiveTrack.js +29 -22
  247. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  248. package/lib/module/hooks/useCast.js +8 -26
  249. package/lib/module/hooks/useCast.js.map +1 -1
  250. package/lib/module/hooks/useEqualizer.js +19 -12
  251. package/lib/module/hooks/useEqualizer.js.map +1 -1
  252. package/lib/module/hooks/useLookaheadCache.js +3 -7
  253. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  254. package/lib/module/hooks/useQueue.js +66 -19
  255. package/lib/module/hooks/useQueue.js.map +1 -1
  256. package/lib/module/index.js +6 -3
  257. package/lib/module/index.js.map +1 -1
  258. package/lib/module/queueDelta.js +41 -0
  259. package/lib/module/queueDelta.js.map +1 -0
  260. package/lib/module/types.js +28 -5
  261. package/lib/module/types.js.map +1 -1
  262. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  263. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  264. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  265. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  266. package/lib/typescript/hooks/useCast.d.ts +6 -1
  267. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  269. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  271. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  272. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  273. package/lib/typescript/index.d.ts +2 -1
  274. package/lib/typescript/index.d.ts.map +1 -1
  275. package/lib/typescript/queueDelta.d.ts +10 -0
  276. package/lib/typescript/queueDelta.d.ts.map +1 -0
  277. package/lib/typescript/types.d.ts +168 -12
  278. package/lib/typescript/types.d.ts.map +1 -1
  279. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  280. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  281. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  282. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  283. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  284. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  285. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  286. 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
  287. 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
  288. 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
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  300. 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
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  320. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  321. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  322. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  323. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  324. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  325. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  326. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  327. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  328. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  329. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  330. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  331. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  332. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  333. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  334. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  335. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  336. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  337. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  338. package/package.json +8 -8
  339. package/src/TrackPlayer.nitro.ts +53 -27
  340. package/src/hooks/useActiveTrack.ts +29 -22
  341. package/src/hooks/useCast.ts +14 -15
  342. package/src/hooks/useEqualizer.ts +19 -12
  343. package/src/hooks/useLookaheadCache.ts +3 -7
  344. package/src/hooks/useQueue.ts +66 -17
  345. package/src/index.ts +13 -3
  346. package/src/queueDelta.ts +41 -0
  347. package/src/types.ts +169 -12
  348. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  349. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  350. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  351. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  352. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  353. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  354. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  355. package/ios/MetadataReader.swift +0 -555
  356. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  357. package/ios/Tests/MetadataReaderTests.swift +0 -603
  358. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  359. package/ios/Tests/VoiceDonationTests.swift +0 -26
  360. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  361. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  362. /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()
@@ -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 {
@@ -46,8 +46,8 @@ protocol CastSessionRemotePlayer: CastSession {
46
46
  var lastState: RemotePlaybackStateKind { get }
47
47
 
48
48
  /// `true` once the receiver has reported any state other than `.idle`.
49
- /// Latches on the first such report and stays set for the session's
50
- /// lifetime.
49
+ /// Set by the first such report and stays set for the session's lifetime;
50
+ /// nothing clears it.
51
51
  ///
52
52
  /// State reads use this to decide whether the receiver can answer yet: a
53
53
  /// session that has just activated still holds its initial `.idle`, and the
@@ -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
 
@@ -17,8 +17,8 @@ import Network
17
17
  /// Backends call `acquire(sessionId:)` for a `MediaServerHandle`; the
18
18
  /// listener starts on first acquire and stops when the last handle
19
19
  /// closes. Range / HEAD / Content-Length are handled by the responder.
20
- // Thread-safe singleton: all mutable state is guarded by `lock` / `listenerLock`,
21
- // so it is safe to capture in the listener's `@Sendable` dispatch closures.
20
+ /// Thread-safe singleton: all mutable state is guarded by `lock`, so it is
21
+ /// safe to capture in the listener's `@Sendable` dispatch closures.
22
22
  final class LocalMediaServer: @unchecked Sendable {
23
23
  /// Process-wide singleton — only the Chromecast / DLNA / Sonos
24
24
  /// backends acquire handles; AirPlay-only apps never touch this and
@@ -32,7 +32,6 @@ final class LocalMediaServer: @unchecked Sendable {
32
32
  private var boundPort: UInt16 = 0
33
33
  private var refCount = 0
34
34
  private let registry = MediaTokenRegistry()
35
- private var addressMonitor: LocalAddressMonitor?
36
35
  /// Live accepted connections, tracked so `stop()` can cancel any
37
36
  /// in-flight transfer. Keyed by identity; each self-removes on finish.
38
37
  private var connections: [ObjectIdentifier: MediaHTTPConnection] = [:]
@@ -43,62 +42,51 @@ final class LocalMediaServer: @unchecked Sendable {
43
42
  // acquiring async task suspends instead of busy-waiting a cooperative thread.
44
43
  private var readinessWaiters: [CheckedContinuation<Void, Never>] = []
45
44
 
46
- // `lifecycleListener` is read from acquire/release (which may run on
47
- // any thread the consumer routes calls through) and set from caller
48
- // code on app init. Reads + writes go through this dedicated lock so
49
- // a concurrent set during a refcount flip doesn't race the read.
50
- private let listenerLock = NSLock()
51
- private var _lifecycleListener: ((Bool) -> Void)?
52
-
53
- var lifecycleListener: ((Bool) -> Void)? {
54
- get {
55
- listenerLock.lock(); defer { listenerLock.unlock() }
56
- return _lifecycleListener
57
- }
58
- set {
59
- listenerLock.lock()
60
- _lifecycleListener = newValue
61
- listenerLock.unlock()
62
- }
63
- }
64
-
65
- private func fireLifecycle(_ active: Bool) {
66
- listenerLock.lock()
67
- let cb = _lifecycleListener
68
- listenerLock.unlock()
69
- cb?(active)
70
- }
71
-
72
45
  /// OS-assigned port. `nil` when the listener is not bound.
73
46
  var serverPortOrNil: Int? {
74
47
  lock.lock(); defer { lock.unlock() }
75
48
  return boundPort == 0 ? nil : Int(boundPort)
76
49
  }
77
50
 
78
- /// LAN IPv4 address for receivers to reach. Prefers the attached
79
- /// `LocalAddressMonitor`'s cached value; falls back to a synchronous
80
- /// `getifaddrs` lookup so the first cast attempt after app launch
81
- /// doesn't fail just because the monitor's first path-update
82
- /// callback hasn't fired yet.
51
+ /// LAN IPv4 address for receivers to reach: the first IPv4 on an `en*`
52
+ /// interface, looked up with `getifaddrs` on every read; `nil` when
53
+ /// there is none.
83
54
  var currentLanAddress: String? {
84
- if let cached = addressMonitor?.currentAddress { return cached }
85
- return LocalAddressMonitor.firstWifiIPv4()
55
+ Self.firstWifiIPv4()
86
56
  }
87
57
 
88
- func attachAddressMonitor(_ monitor: LocalAddressMonitor) {
89
- addressMonitor = monitor
90
- }
91
-
92
- func addAddressChangeListener(
93
- _ listener: @escaping (String?) -> Void
94
- ) -> LocalAddressMonitor.Subscription? {
95
- addressMonitor?.addListener(listener)
58
+ /// The first IPv4 address on an `en*` interface (Wi-Fi), from a
59
+ /// synchronous `getifaddrs` scan; `nil` when no such interface has one.
60
+ /// Cellular addresses can't reach a LAN receiver, so other interfaces
61
+ /// are ignored.
62
+ private static func firstWifiIPv4() -> String? {
63
+ var ifaddrPtr: UnsafeMutablePointer<ifaddrs>?
64
+ guard getifaddrs(&ifaddrPtr) == 0, let first = ifaddrPtr else { return nil }
65
+ defer { freeifaddrs(ifaddrPtr) }
66
+ var ptr: UnsafeMutablePointer<ifaddrs>? = first
67
+ while let current = ptr {
68
+ defer { ptr = current.pointee.ifa_next }
69
+ let name = String(cString: current.pointee.ifa_name)
70
+ guard name.hasPrefix("en") else { continue }
71
+ guard let sa = current.pointee.ifa_addr else { continue }
72
+ guard sa.pointee.sa_family == UInt8(AF_INET) else { continue }
73
+ var hostBuf = [CChar](repeating: 0, count: Int(NI_MAXHOST))
74
+ let result = getnameinfo(
75
+ sa, socklen_t(sa.pointee.sa_len),
76
+ &hostBuf, socklen_t(hostBuf.count),
77
+ nil, 0, NI_NUMERICHOST
78
+ )
79
+ if result == 0 {
80
+ let host = String(cString: hostBuf)
81
+ if !host.isEmpty && host != "0.0.0.0" { return host }
82
+ }
83
+ }
84
+ return nil
96
85
  }
97
86
 
98
87
  func acquire(sessionId: String) async -> MediaServerHandle {
99
88
  if takeRefCountWasFirst() {
100
89
  await startAndWait()
101
- fireLifecycle(true)
102
90
  }
103
91
  return MediaServerHandle(server: self, sessionId: sessionId)
104
92
  }
@@ -122,7 +110,6 @@ final class LocalMediaServer: @unchecked Sendable {
122
110
  lock.unlock()
123
111
  if last {
124
112
  stop()
125
- fireLifecycle(false)
126
113
  }
127
114
  }
128
115
 
@@ -130,10 +117,6 @@ final class LocalMediaServer: @unchecked Sendable {
130
117
  registry.register(sessionId: sessionId, fileURL: fileURL, contentType: contentType)
131
118
  }
132
119
 
133
- func revoke(_ token: String) {
134
- registry.revoke(token)
135
- }
136
-
137
120
  // MARK: - Listener lifecycle
138
121
 
139
122
  /// Start the listener (if not already bound) and suspend, bounded, until it
@@ -186,7 +169,8 @@ final class LocalMediaServer: @unchecked Sendable {
186
169
  // Bind every interface (Wi-Fi LAN, etc.) on an OS-assigned port — the
187
170
  // Cast receiver fetches URLs over the LAN, so unlike a loopback proxy
188
171
  // this listener must accept en0 connections. URLs handed to receivers
189
- // scope the host to the active Wi-Fi address via `LocalAddressMonitor`;
172
+ // go through `MediaServerHandle.register(fileURL:lanAddress:contentType:)`
173
+ // with the host from `currentLanAddress` (the first IPv4 on an `en*` interface);
190
174
  // tokens are 32 hex chars (high entropy), so the LAN-reachable surface
191
175
  // can't be enumerated. `allowLocalEndpointReuse` avoids bind churn when
192
176
  // a prior listener's socket is still in TIME_WAIT.
@@ -238,7 +222,12 @@ final class LocalMediaServer: @unchecked Sendable {
238
222
  }
239
223
 
240
224
  private func accept(_ conn: NWConnection) {
241
- let responder = MediaHTTPConnection(connection: conn, registry: registry) { [weak self] in
225
+ let responder = MediaHTTPConnection(
226
+ connection: conn,
227
+ resolve: { [registry] token in
228
+ registry.resolve(token).map { .file($0.fileURL, contentType: $0.contentType) }
229
+ }
230
+ ) { [weak self] in
242
231
  self?.untrack(conn)
243
232
  }
244
233
  // Track BEFORE start(): the responder's finish callback (-> untrack)
@@ -51,15 +51,17 @@ final class LocalNetworkPermissionProbe {
51
51
 
52
52
  init() {}
53
53
 
54
- /// Snapshot of the most recently resolved permission state. Returns
55
- /// `.undetermined` if no probe has resolved yet, or if the cached
56
- /// result has expired past the TTL.
54
+ /// The most recently resolved permission state, or `.undetermined` if no
55
+ /// probe has resolved yet.
56
+ ///
57
+ /// Deliberately not TTL-gated. The TTL governs whether
58
+ /// `requestPermission()` re-probes; expiring the *answer* as well would
59
+ /// report `.undetermined` for a user who granted access a minute ago, and any
60
+ /// consumer rule of the form "granted suppresses the prompt" would then
61
+ /// invert — re-prompting exactly the people who already said yes.
57
62
  var cachedState: CastLocalNetworkPermission {
58
63
  stateLock.lock(); defer { stateLock.unlock() }
59
- if let at = _cachedAt, Date().timeIntervalSince(at) < Self.cacheTTLSeconds {
60
- return _cachedState
61
- }
62
- return .undetermined
64
+ return _cachedState
63
65
  }
64
66
 
65
67
  /// Trigger the probe. Returns immediately with a `Promise` that
@@ -215,6 +217,15 @@ fileprivate final class ProbeRun {
215
217
 
216
218
  private func handleBrowserState(_ state: NWBrowser.State) {
217
219
  switch state {
220
+ case .waiting(let error):
221
+ // Denial surfaces here, not in `.failed`: refusing local-network access
222
+ // leaves the browser waiting with kDNSServiceErr_PolicyDenied (-65570).
223
+ // `.waiting` is also the ordinary "no connectivity yet" state, so only
224
+ // the error code decides — anything else stays unresolved and lets the
225
+ // timeout or a later state settle it.
226
+ if Self.isLocalNetworkDeniedError(error) {
227
+ resolve(.denied)
228
+ }
218
229
  case .failed(let error):
219
230
  if Self.isLocalNetworkDeniedError(error) {
220
231
  resolve(.denied)
@@ -252,14 +263,24 @@ fileprivate final class ProbeRun {
252
263
  for p in resolvedPromises { p.resolve(withResult: state) }
253
264
  }
254
265
 
255
- /// Detect the LNA-denied error code. Network.framework surfaces it
256
- /// as a POSIX `EACCES` wrapped in `NWError.posix(.EACCES)`. Other
257
- /// failure modes (no Wi-Fi, system error) fall through to
258
- /// `.undetermined`.
266
+ /// Detect the local-network-denied error.
267
+ ///
268
+ /// A denied browse reports `kDNSServiceErr_PolicyDenied` (-65570) as
269
+ /// `NWError.dns`, which is the form that actually arrives on the `.waiting`
270
+ /// path. `NWError.posix(.EACCES)` is kept for the listener side, which
271
+ /// reports the same refusal as a POSIX error. Everything else (no Wi-Fi,
272
+ /// system error) is not a denial and falls through.
259
273
  static func isLocalNetworkDeniedError(_ error: NWError) -> Bool {
260
274
  if case .posix(let code) = error, code == .EACCES {
261
275
  return true
262
276
  }
277
+ if case .dns(let code) = error, code == Self.dnsPolicyDenied {
278
+ return true
279
+ }
263
280
  return false
264
281
  }
282
+
283
+ /// `kDNSServiceErr_PolicyDenied`. Spelled out rather than imported so the
284
+ /// probe does not pull in dnssd for one constant.
285
+ private static let dnsPolicyDenied: DNSServiceErrorType = -65570
265
286
  }
@@ -26,10 +26,6 @@ final class MediaServerHandle {
26
26
  return URL(string: "http://\(lanAddress):\(port)/media/\(token)")
27
27
  }
28
28
 
29
- func revoke(_ token: String) {
30
- server?.revoke(token)
31
- }
32
-
33
29
  func close() {
34
30
  lock.lock()
35
31
  if closed {
@@ -28,12 +28,6 @@ final class MediaTokenRegistry {
28
28
  return entries[token]
29
29
  }
30
30
 
31
- func revoke(_ token: String) {
32
- lock.lock()
33
- entries.removeValue(forKey: token)
34
- lock.unlock()
35
- }
36
-
37
31
  func revokeAll(sessionId: String) {
38
32
  lock.lock()
39
33
  entries = entries.filter { $0.value.sessionId != sessionId }