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
@@ -222,14 +222,10 @@ class Visualizer : HybridVisualizerSpec() {
222
222
  * fftSize=2048 should expect proportionally higher GC pressure on
223
223
  * the audio render thread.
224
224
  *
225
- * Write each float directly via `bb.putFloat()` advancing bb's
226
- * own position. The derived `bb.asFloatBuffer().put(values)` form
227
- * appears to write to bb's underlying storage on JVM but does not
228
- * propagate the bytes through `ArrayBuffer.wrap` on Android's
229
- * emulator backend — observed empirically: post-FFT magnitudes
230
- * surface as all-zero on the JS side, even though `bb.getFloat(0)`
231
- * reads back the right value on the Kotlin side. The explicit
232
- * putFloat loop sidesteps the whole class of issue.
225
+ * Writes with `putFloat` rather than `bb.asFloatBuffer().put(values)`:
226
+ * the view form delivered all-zero buffers to JS through
227
+ * `ArrayBuffer.wrap` while `bb.getFloat(0)` read back correctly in
228
+ * Kotlin. Cause not established.
233
229
  */
234
230
  private fun floatArrayToArrayBuffer(values: FloatArray): ArrayBuffer {
235
231
  val bb = ByteBuffer.allocateDirect(values.size * 4).order(ByteOrder.nativeOrder())
@@ -6,9 +6,10 @@ package com.margelo.nitro.queueplayer.cast
6
6
  * `ChromecastBackend` for Cast, future DLNA / Sonos similarly. The
7
7
  * core module never knows which backends are present.
8
8
  *
9
- * The interface is intentionally tiny. Anything richer (volume,
10
- * pairing PIN flow, capability discovery) lives on the [CastSession]
11
- * subtype that [connect] returns. Keeps the registry pluggable + the
9
+ * The interface is intentionally tiny. Anything richer lives on the
10
+ * concrete backend or on the [CastSession] subtype that [connect]
11
+ * returns volume on the session, the AirPlay 2 pairing-PIN flow on
12
+ * `AirPlayBackend.submitPairingPin`. Keeps the registry pluggable + the
12
13
  * routing layer protocol-agnostic.
13
14
  */
14
15
  interface CastBackend {
@@ -61,9 +61,10 @@ sealed class CastSession {
61
61
 
62
62
  /**
63
63
  * Optional `Player` implementation for `MediaSession`. When
64
- * non-null, `MusicService` swaps `mediaSession.player` to this on
65
- * cast connect and back on disconnect. Chromecast returns a
66
- * `RemoteCastPlayer` from `media3-cast`. Default null.
64
+ * non-null, `PlaybackService` retargets the session's forwarding
65
+ * player to this on cast connect and back to the local engine on
66
+ * disconnect. Chromecast returns a `RemoteCastPlayer` from
67
+ * `media3-cast`. Default null.
67
68
  */
68
69
  open val mediaSessionPlayer: Player? = null
69
70
 
@@ -126,7 +127,7 @@ sealed class CastSession {
126
127
 
127
128
  /**
128
129
  * `true` once the receiver has reported any state other than
129
- * [RemotePlaybackState.IDLE]. Latches on the first such report and stays
130
+ * [RemotePlaybackState.IDLE]. Set by the first such report and stays
130
131
  * set for the session's lifetime.
131
132
  *
132
133
  * State reads use this to decide whether the receiver can answer yet: a
@@ -91,10 +91,8 @@ object AirPlay2JNI {
91
91
 
92
92
  // -- Pairing --
93
93
 
94
- @JvmStatic external fun nativeAp2NeedsPairing(handle: Long): Boolean
95
94
  @JvmStatic external fun nativeAp2PairPinStart(handle: Long): Boolean
96
95
  @JvmStatic external fun nativeAp2PairSetup(handle: Long, credential: String): String?
97
- @JvmStatic external fun nativeAp2GetPairingState(handle: Long): Int
98
96
  @JvmStatic external fun nativeAp2GetAuthMode(handle: Long): Int
99
97
  @JvmStatic external fun nativeAp2GetDeviceId(handle: Long): String?
100
98
  @JvmStatic external fun nativeAp2SetPassword(handle: Long, password: String)
@@ -77,7 +77,7 @@ class AirPlay2Session(
77
77
  /**
78
78
  * Invoked from the native drain thread (via [AirPlay2JNI.onNativeSessionDied])
79
79
  * when the receiver has stopped responding. Notifies any [deathListener]
80
- * so CastModule can revert routing to local, then runs the normal close
80
+ * so `CastManager` can revert routing to local, then runs the normal close
81
81
  * path — native TEARDOWN is skipped because the socket is dead.
82
82
  */
83
83
  private fun onRemoteDied(reason: Int) {
@@ -100,7 +100,9 @@ class AirPlay2Session(
100
100
  AirPlay2JNI.unregisterSession(handle)
101
101
 
102
102
  /* Native TEARDOWN can block on a dead socket — never run it on the
103
- * caller's thread (typically the main/UI thread via CastModule). */
103
+ * caller's thread (`CastManager.disconnect()` on the JS thread, a
104
+ * replacing `connect()` or `submitPairingPin()` on `Dispatchers.IO`,
105
+ * `onRemoteDied` on the native drain thread). */
104
106
  val h = handle
105
107
  teardownScope.launch {
106
108
  try {
@@ -97,22 +97,28 @@ internal class AirPlayBackend(private val context: Context) : CastBackend {
97
97
 
98
98
  /**
99
99
  * Complete a deferred AP2 pairing by supplying the PIN / password the
100
- * receiver demanded. Returns the active session on success; throws on
101
- * pairing failure or when no pending pair exists.
100
+ * receiver demanded. Returns the session and the receiver it was
101
+ * paired with; throws on pairing failure or when no pending pair
102
+ * exists.
102
103
  *
103
104
  * Callers use this when `CastManager.submitPairingPin(pin)` is invoked
104
105
  * out-of-band of a follow-up `connect(receiverId, {password})` call.
105
106
  */
106
- suspend fun submitPairingPin(pin: String): CastSession = withContext(Dispatchers.IO) {
107
+ suspend fun submitPairingPin(pin: String): PairedSession = withContext(Dispatchers.IO) {
107
108
  val pending = synchronized(this@AirPlayBackend) { pendingPairState }
108
109
  ?: throw IllegalStateException("No pending AirPlay 2 pairing")
109
110
  val receiver = discovery.findReceiver(pending.receiverId)
110
111
  ?: throw IllegalStateException(
111
112
  "Receiver ${pending.receiverId} no longer visible — restart the connect flow"
112
113
  )
114
+ // resumeAp2Pairing reports the id of the pair it consumed and
115
+ // validated against `receiver`; the read above only locates it.
113
116
  resumeAp2Pairing(receiver, pin)
114
117
  }
115
118
 
119
+ /** A session established by [submitPairingPin] and the receiver it was paired with. */
120
+ data class PairedSession(val session: CastSession, val receiverId: String)
121
+
116
122
  /**
117
123
  * Persistent store for AirPlay 2 pair-setup auth keys + passwords,
118
124
  * keyed by the receiver's mDNS `deviceid` (MAC address).
@@ -306,7 +312,7 @@ internal class AirPlayBackend(private val context: Context) : CastBackend {
306
312
 
307
313
  // --- Phase 2: credential supplied → resume pending pairing ---
308
314
  if (credential != null) {
309
- return resumeAp2Pairing(receiver, credential)
315
+ return resumeAp2Pairing(receiver, credential).session
310
316
  }
311
317
 
312
318
  // --- Phase 1: initial connection attempt ---
@@ -423,11 +429,12 @@ internal class AirPlayBackend(private val context: Context) : CastBackend {
423
429
  }
424
430
 
425
431
  /**
426
- * Resume a pending AP2 pairing with user-provided credential.
427
- * Called when the UI re-invokes connect() with a password/PIN after
428
- * [AuthRequiredException] was thrown.
432
+ * Resume a pending AP2 pairing with user-provided credential, from
433
+ * either `connect()` re-invoked with a password or [submitPairingPin].
434
+ * Consumes the pending pair, validates it is for [receiver], and
435
+ * returns the session together with that receiver's id.
429
436
  */
430
- private fun resumeAp2Pairing(receiver: Receiver, credential: String): CastSession {
437
+ private fun resumeAp2Pairing(receiver: Receiver, credential: String): PairedSession {
431
438
  val pending = synchronized(this) {
432
439
  val p = pendingPairState
433
440
  pendingPairState = null
@@ -506,7 +513,7 @@ internal class AirPlayBackend(private val context: Context) : CastBackend {
506
513
  }
507
514
 
508
515
  Log.i(TAG, "Connected AP2 to ${receiver.name} via pair-verify")
509
- return AirPlay2Session(context, newHandle, host, port)
516
+ return PairedSession(AirPlay2Session(context, newHandle, host, port), pending.receiverId)
510
517
  }
511
518
 
512
519
  /** Destroy any stale pending pairing handle. */
@@ -59,7 +59,7 @@ internal class AirPlayMetadataSync(
59
59
  @Volatile
60
60
  private var started = false
61
61
 
62
- // Latches on stop so a restart cannot half-revive a spent instance: the
62
+ // Set on stop and never cleared, so a restart cannot half-revive a spent instance: the
63
63
  // scope is already cancelled, so the listener would still push metadata
64
64
  // while the progress loop and artwork fetch stay dead.
65
65
  @Volatile
@@ -10,12 +10,12 @@ import androidx.media3.exoplayer.audio.AudioSink
10
10
  * [AudioSink] IS the AirPlay session's sink ([AirPlay2Sink] / [AirPlaySink]).
11
11
  *
12
12
  * Deliberately a bare [DefaultRenderersFactory]: no `setEnableAudioFloatOutput`,
13
- * no audio processors. The AirPlay sinks only accept `AUDIO_RAW` +
14
- * `ENCODING_PCM_16BIT`, and the default factory decodes to Int16 PCM — so the
15
- * renderer negotiates a format the sink accepts and the PCM reaches the
16
- * receiver clean. (Forcing float output or inserting a processor chain is what
17
- * corrupted the AirPlay stream on the shared local engine; this engine is kept
18
- * dedicated and clean precisely to avoid that.)
13
+ * no audio processors, no extension-renderer preference. The AirPlay sinks
14
+ * only accept `AUDIO_RAW` + `ENCODING_PCM_16BIT`, and the default factory
15
+ * decodes to Int16 PCM — so the renderer negotiates a format the sink accepts
16
+ * and the PCM reaches the receiver clean. Float output or a processor chain
17
+ * in front of the sink corrupts the AirPlay stream, which is why this engine
18
+ * has its own factory instead of sharing the local pipeline's.
19
19
  *
20
20
  * One sink per factory instance — the AirPlay engine is built fresh for each
21
21
  * session and released when the session ends, so the sink is never swapped at
@@ -27,14 +27,6 @@ internal class AirPlayRenderersFactory(
27
27
  private val airplaySink: AudioSink,
28
28
  ) : DefaultRenderersFactory(context) {
29
29
 
30
- init {
31
- // Match the known-good fork AirPlay config: prefer extension decoders.
32
- // No-op when no extension renderers are bundled, but it keeps the
33
- // decoder-selection behaviour identical to the reference that streams
34
- // clean PCM to these sinks.
35
- setExtensionRendererMode(EXTENSION_RENDERER_MODE_PREFER)
36
- }
37
-
38
30
  override fun buildAudioSink(
39
31
  context: Context,
40
32
  enableFloatOutput: Boolean,
@@ -40,11 +40,6 @@ object AirplayJNI {
40
40
  false
41
41
  }
42
42
 
43
- // -- Library info --
44
-
45
- @JvmStatic external fun nativeVersion(): String
46
- @JvmStatic external fun nativeInitSsl(): Boolean
47
-
48
43
  // -- Session lifecycle --
49
44
 
50
45
  /**
@@ -73,10 +68,8 @@ object AirplayJNI {
73
68
  /** Bytes waiting in the ring buffer, not yet sent. Used for hasPendingData(). */
74
69
  @JvmStatic external fun nativeRingAvailable(handle: Long): Int
75
70
 
76
- // -- Audio streaming (legacy, kept for compatibility) --
71
+ // -- Transport control --
77
72
 
78
- @JvmStatic external fun nativeAcceptFrames(handle: Long): Boolean
79
- @JvmStatic external fun nativeSendChunk(handle: Long, data: ByteArray, offset: Int, length: Int): Boolean
80
73
  @JvmStatic external fun nativePause(handle: Long)
81
74
  @JvmStatic external fun nativeStop(handle: Long)
82
75
  @JvmStatic external fun nativeFlush(handle: Long): Boolean
@@ -91,16 +84,11 @@ object AirplayJNI {
91
84
 
92
85
  @JvmStatic external fun nativeSetVolume(handle: Long, volume: Float): Boolean
93
86
  @JvmStatic external fun nativeSetProgressMs(handle: Long, elapsed: Int, duration: Int): Boolean
94
- @JvmStatic external fun nativeGetProgressMs(handle: Long): Long
95
87
  @JvmStatic external fun nativeSetDaap(handle: Long, title: String?, artist: String?, album: String?): Boolean
96
88
  @JvmStatic external fun nativeSetArtwork(handle: Long, imageData: ByteArray, length: Int, contentType: String): Boolean
97
89
 
98
90
  // -- State queries --
99
91
 
100
92
  @JvmStatic external fun nativeIsConnected(handle: Long): Boolean
101
- @JvmStatic external fun nativeIsPlaying(handle: Long): Boolean
102
- @JvmStatic external fun nativeGetState(handle: Long): Int
103
- @JvmStatic external fun nativeGetLatency(handle: Long): Int
104
- @JvmStatic external fun nativeGetSampleRate(handle: Long): Int
105
93
  @JvmStatic external fun nativeKeepalive(handle: Long): Boolean
106
94
  }
@@ -70,6 +70,6 @@ internal class MulticastLockHolder(context: Context) {
70
70
  fun isHeld(): Boolean = lock?.isHeld == true
71
71
 
72
72
  private companion object {
73
- const val LOCK_TAG = "rntp-airplay-discovery"
73
+ const val LOCK_TAG = "QueuePlayer.AirPlayDiscovery"
74
74
  }
75
75
  }
@@ -14,9 +14,9 @@ import com.margelo.nitro.queueplayer.CastReceiver
14
14
 
15
15
  /**
16
16
  * Chromecast backend — owns discovery via MediaRouter and session
17
- * lifecycle via GMS SessionManager. Surfaces discovery, connect, and
18
- * disconnect today; queue load + transport-state translation arrive
19
- * with the cast engine-swap work.
17
+ * lifecycle via GMS SessionManager, plus the transport calls that
18
+ * address the SDK session directly. Queue load and transport-state
19
+ * translation live in [ChromecastSession] and [RemoteMediaCallbacks].
20
20
  */
21
21
  internal class ChromecastBackend(
22
22
  private val context: Context,
@@ -32,7 +32,7 @@ import kotlinx.coroutines.runBlocking
32
32
  *
33
33
  * Binds to `0.0.0.0` on an OS-assigned port. Range / HEAD /
34
34
  * Content-Length fall out of Ktor's `LocalFileContent` + `PartialContent`
35
- * plugin. Refcounted via `acquire(sessionId)` / `release()`.
35
+ * plugin. Refcounted via `acquire(sessionId, context)` / `release(sessionId)`.
36
36
  */
37
37
  class LocalMediaServer {
38
38
  private val lock = Any()
@@ -51,38 +51,26 @@ class LocalMediaServer {
51
51
  var serverPort: Int = -1
52
52
  private set
53
53
 
54
- @Volatile
55
- var lifecycleListener: LocalMediaServerLifecycle? = null
56
-
57
54
  @Volatile
58
55
  private var addressMonitor: LocalAddressMonitor? = null
59
56
 
60
57
  val currentLanAddress: String?
61
58
  get() = addressMonitor?.currentAddress
62
59
 
63
- fun attachAddressMonitor(monitor: LocalAddressMonitor) {
64
- addressMonitor = monitor
65
- }
66
-
67
- fun addAddressChangeListener(
68
- listener: LocalAddressMonitor.Listener,
69
- ): LocalAddressMonitor.Subscription? = addressMonitor?.addListener(listener)
70
-
71
60
  fun acquire(sessionId: String, context: Context): MediaServerHandle {
72
61
  val first = refCount.getAndIncrement() == 0
73
62
  if (first) {
74
63
  ensureStarted()
75
64
  ensureAddressMonitor(context)
76
- lifecycleListener?.onActiveStateChanged(true)
77
65
  }
78
66
  return MediaServerHandle(this, sessionId)
79
67
  }
80
68
 
81
69
  // Lazily create + start the LAN-address monitor the first time the
82
70
  // server is acquired. `start()` seeds `currentAddress` synchronously
83
- // from the active network, so the address is available to the very
84
- // first `currentLanAddress` read (the cast handoff loads immediately
85
- // on connect, before any async network callback would land).
71
+ // when the active network is Wi-Fi, so the address is available to
72
+ // the very first `currentLanAddress` read (the cast handoff loads
73
+ // immediately on connect, before any async network callback lands).
86
74
  private fun ensureAddressMonitor(context: Context) {
87
75
  synchronized(lock) {
88
76
  if (addressMonitor != null) return
@@ -93,29 +81,21 @@ class LocalMediaServer {
93
81
  internal fun release(sessionId: String) {
94
82
  registry.revokeAll(sessionId)
95
83
  val last = refCount.decrementAndGet() == 0
96
- if (last) {
97
- stop()
98
- lifecycleListener?.onActiveStateChanged(false)
99
- }
84
+ if (last) stop()
100
85
  }
101
86
 
102
87
  internal fun register(sessionId: String, file: File, contentType: String?): String =
103
88
  registry.register(sessionId, file, contentType)
104
89
 
105
- internal fun revoke(token: String) = registry.revoke(token)
106
-
107
- internal fun resolve(token: String): RegisteredMedia? = registry.resolve(token)
108
-
109
90
  private fun ensureStarted() {
110
91
  synchronized(lock) {
111
92
  if (engine != null) return
112
- // Bind on every interface (Wi-Fi LAN, cellular, VPN) Chromecast
113
- // receivers pull from the phone's LAN IP, and we cannot
114
- // pre-determine which interface the receiver will use. URLs
115
- // handed back to receivers go through `MediaServerHandle.url`,
116
- // which scopes hosts to the active Wi-Fi address via
117
- // `LocalAddressMonitor`. Tokens are 32 hex chars (high entropy);
118
- // unauthorized callers cannot enumerate served media.
93
+ // Listen on every interface; the URL host selects the Wi-Fi one.
94
+ // URLs handed back to receivers go through
95
+ // `MediaServerHandle.register(file, lanAddress, contentType)` with the
96
+ // host from `currentLanAddress` (`LocalAddressMonitor.currentAddress`,
97
+ // the Wi-Fi network's address). Tokens are 32 hex chars (high
98
+ // entropy); unauthorized callers cannot enumerate served media.
119
99
  val started = embeddedServer(CIO, port = 0, host = "0.0.0.0") {
120
100
  install(PartialContent)
121
101
  install(AutoHeadResponse)
@@ -153,8 +133,4 @@ class LocalMediaServer {
153
133
  addressMonitor = null
154
134
  }
155
135
  }
156
-
157
- internal fun stopForTest() {
158
- stop()
159
- }
160
136
  }
@@ -31,11 +31,6 @@ class MediaServerHandle internal constructor(
31
31
  return "http://$lanAddress:$port/media/$token"
32
32
  }
33
33
 
34
- fun revoke(token: String) {
35
- if (closed.get()) return
36
- server.revoke(token)
37
- }
38
-
39
34
  fun close() {
40
35
  if (closed.compareAndSet(false, true)) {
41
36
  server.release(sessionId)
@@ -22,16 +22,10 @@ internal class MediaTokenRegistry {
22
22
 
23
23
  fun resolve(token: String): RegisteredMedia? = tokens[token]
24
24
 
25
- fun revoke(token: String) {
26
- tokens.remove(token)
27
- }
28
-
29
25
  fun revokeAll(sessionId: String) {
30
26
  val iter = tokens.entries.iterator()
31
27
  while (iter.hasNext()) {
32
28
  if (iter.next().value.sessionId == sessionId) iter.remove()
33
29
  }
34
30
  }
35
-
36
- fun size(): Int = tokens.size
37
31
  }
@@ -7,37 +7,38 @@ import android.net.Network
7
7
  import android.net.NetworkCapabilities
8
8
  import android.net.NetworkRequest
9
9
  import java.net.Inet4Address
10
- import java.util.concurrent.CopyOnWriteArrayList
11
10
 
12
11
  /**
13
- * Surfaces the host's primary LAN IPv4 address and notifies listeners
14
- * on change. Owned by `LocalMediaServer`; backends consume via the
15
- * server's `addAddressChangeListener`.
12
+ * Surfaces the host's Wi-Fi IPv4 address. Owned by `LocalMediaServer`,
13
+ * which exposes it to backends as `currentLanAddress`.
16
14
  *
17
15
  * Detection via [ConnectivityManager.NetworkCallback] on a request for
18
- * `TRANSPORT_WIFI | TRANSPORT_CELLULAR`. Cellular included so mobile-
19
- * hotspot setups (phone hotspot fed to a Chromecast) work.
16
+ * `TRANSPORT_WIFI` (the request's default capabilities exclude VPNs), so
17
+ * only a Wi-Fi network's address is ever served, and a cellular network
18
+ * coming or going neither replaces nor clears it.
20
19
  */
21
20
  class LocalAddressMonitor(private val context: Context) {
22
21
 
23
- fun interface Listener {
24
- fun onAddressChanged(newAddress: String?)
25
- }
26
-
27
- private val listeners = CopyOnWriteArrayList<Listener>()
28
-
29
22
  @Volatile
30
23
  var currentAddress: String? = null
31
24
  private set
32
25
 
26
+ /**
27
+ * The network whose link properties supplied [currentAddress]. Two Wi-Fi
28
+ * networks overlap during a handover — the new one reports its link before
29
+ * the old one is lost — so a loss clears the address only when it is the
30
+ * network the address came from.
31
+ */
32
+ private var addressNetwork: Network? = null
33
+
33
34
  private var registered = false
34
35
  private val callback = object : ConnectivityManager.NetworkCallback() {
35
36
  override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
36
- updateAddress(linkProperties)
37
+ updateAddress(network, linkProperties)
37
38
  }
38
39
 
39
40
  override fun onLost(network: Network) {
40
- updateAddress(null)
41
+ if (network == addressNetwork) updateAddress(null, null)
41
42
  }
42
43
  }
43
44
 
@@ -47,14 +48,23 @@ class LocalAddressMonitor(private val context: Context) {
47
48
  ?: return
48
49
  val request = NetworkRequest.Builder()
49
50
  .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
50
- .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
51
51
  .build()
52
52
  cm.registerNetworkCallback(request, callback)
53
53
  registered = true
54
- // Seed synchronously from the active network so `currentAddress` is
55
- // populated immediately — the cast handoff reads it right after
56
- // connect, before the first async onLinkPropertiesChanged lands.
57
- updateAddress(cm.activeNetwork?.let { cm.getLinkProperties(it) })
54
+ // Seed synchronously so `currentAddress` is populated immediately —
55
+ // the cast handoff reads it right after connect, before the first
56
+ // async onLinkPropertiesChanged lands. The active network is used
57
+ // only when it passes the request's own filter (Wi-Fi, not a VPN:
58
+ // a VPN over Wi-Fi carries the Wi-Fi transport but a tunnel address).
59
+ // When Wi-Fi is up but not the default network, the address arrives
60
+ // with that network's first onLinkPropertiesChanged instead.
61
+ val active = cm.activeNetwork ?: return
62
+ val caps = cm.getNetworkCapabilities(active) ?: return
63
+ if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
64
+ caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
65
+ ) {
66
+ updateAddress(active, cm.getLinkProperties(active))
67
+ }
58
68
  }
59
69
 
60
70
  fun stop() {
@@ -65,22 +75,11 @@ class LocalAddressMonitor(private val context: Context) {
65
75
  registered = false
66
76
  }
67
77
 
68
- fun addListener(listener: Listener): Subscription {
69
- listeners.add(listener)
70
- return Subscription { listeners.remove(listener) }
71
- }
72
-
73
- private fun updateAddress(linkProperties: LinkProperties?) {
78
+ private fun updateAddress(network: Network?, linkProperties: LinkProperties?) {
74
79
  val ipv4 = linkProperties?.linkAddresses
75
80
  ?.firstOrNull { it.address is Inet4Address && !it.address.isLoopbackAddress }
76
81
  ?.address?.hostAddress
77
- if (ipv4 != currentAddress) {
78
- currentAddress = ipv4
79
- listeners.forEach { it.onAddressChanged(ipv4) }
80
- }
81
- }
82
-
83
- fun interface Subscription {
84
- fun remove()
82
+ addressNetwork = if (ipv4 != null) network else null
83
+ currentAddress = ipv4
85
84
  }
86
85
  }
@@ -3,7 +3,7 @@ package androidx.media3.session
3
3
  import androidx.media3.common.util.UnstableApi
4
4
 
5
5
  /**
6
- * Test-only seam into Media3's controller-request scope.
6
+ * Test-only access to Media3's controller-request scope.
7
7
  *
8
8
  * `MediaSession.getControllerForCurrentRequest()` is non-null only for
9
9
  * the duration of a controller-originated `Player` call, set by Media3
@@ -80,6 +80,8 @@ class AvrcpMetadataTest {
80
80
  duration = null,
81
81
  artworkUrl = null,
82
82
  extras = null,
83
+ replayGainTrackGain = null, replayGainTrackPeak = null,
84
+ replayGainAlbumGain = null, replayGainAlbumPeak = null,
83
85
  )
84
86
  setQueueOnSession(listOf(track))
85
87
 
@@ -100,6 +102,8 @@ class AvrcpMetadataTest {
100
102
  duration = null,
101
103
  artworkUrl = "https://example.com/cover.jpg",
102
104
  extras = null,
105
+ replayGainTrackGain = null, replayGainTrackPeak = null,
106
+ replayGainAlbumGain = null, replayGainAlbumPeak = null,
103
107
  )
104
108
  setQueueOnSession(listOf(track))
105
109
 
@@ -120,6 +124,8 @@ class AvrcpMetadataTest {
120
124
  duration = 42.0,
121
125
  artworkUrl = null,
122
126
  extras = null,
127
+ replayGainTrackGain = null, replayGainTrackPeak = null,
128
+ replayGainAlbumGain = null, replayGainAlbumPeak = null,
123
129
  )
124
130
  setQueueOnSession(listOf(track))
125
131
 
@@ -137,6 +143,8 @@ class AvrcpMetadataTest {
137
143
  duration = null,
138
144
  artworkUrl = null,
139
145
  extras = null,
146
+ replayGainTrackGain = null, replayGainTrackPeak = null,
147
+ replayGainAlbumGain = null, replayGainAlbumPeak = null,
140
148
  )
141
149
  val second = TrackItem(
142
150
  id = "second",
@@ -147,6 +155,8 @@ class AvrcpMetadataTest {
147
155
  duration = null,
148
156
  artworkUrl = null,
149
157
  extras = null,
158
+ replayGainTrackGain = null, replayGainTrackPeak = null,
159
+ replayGainAlbumGain = null, replayGainAlbumPeak = null,
150
160
  )
151
161
 
152
162
  setQueueOnSession(listOf(first))
@@ -170,6 +180,8 @@ class AvrcpMetadataTest {
170
180
  duration = null,
171
181
  artworkUrl = null,
172
182
  extras = null,
183
+ replayGainTrackGain = null, replayGainTrackPeak = null,
184
+ replayGainAlbumGain = null, replayGainAlbumPeak = null,
173
185
  )
174
186
  setQueueOnSession(listOf(track))
175
187
  assertEquals(1, session.player.mediaItemCount)