react-native-queue-player 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/build.gradle +7 -0
  4. package/android/consumer-rules.pro +72 -3
  5. package/android/src/main/cpp/airplay2_control.c +4 -4
  6. package/android/src/main/cpp/airplay2_control.h +4 -4
  7. package/android/src/main/cpp/airplay2_jni.cpp +26 -22
  8. package/android/src/main/cpp/airplay2_pair.c +7 -7
  9. package/android/src/main/cpp/airplay2_pair.h +2 -2
  10. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  11. package/android/src/main/cpp/airplay2_session.c +2 -2
  12. package/android/src/main/cpp/airplay_jni.cpp +20 -72
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
  61. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  62. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  63. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  64. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  65. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
  114. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
  115. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  116. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  117. package/app.plugin.js +17 -1
  118. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  119. package/ios/AVQueueBuilder.swift +120 -85
  120. package/ios/ArtworkLoader.swift +0 -7
  121. package/ios/ArtworkResolver.swift +22 -4
  122. package/ios/AssetReplayGainReader.swift +118 -0
  123. package/ios/AudioSession.swift +67 -16
  124. package/ios/AudioTapProvider.swift +284 -149
  125. package/ios/CarPlayBridge.swift +0 -12
  126. package/ios/CarPlayCoordinator.swift +18 -2
  127. package/ios/CarPlaySceneDelegate.swift +2 -2
  128. package/ios/Cast/AirPlayRouteState.swift +9 -8
  129. package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
  130. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  131. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  132. package/ios/Cast/Core/CastSession.swift +11 -0
  133. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  134. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  135. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  136. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  137. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  138. package/ios/CastManager.swift +1 -1
  139. package/ios/CrossfadeEngine.swift +660 -226
  140. package/ios/EQTap.swift +20 -17
  141. package/ios/Equalizer.swift +1 -1
  142. package/ios/FLACStreamInfo.swift +65 -0
  143. package/ios/GaplessEngine.swift +57 -30
  144. package/ios/InputGuards.swift +35 -5
  145. package/ios/LookaheadCache.swift +202 -58
  146. package/ios/LookaheadCachePrefetcher.swift +48 -36
  147. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  148. package/ios/NetworkRecoveryPolicy.swift +32 -0
  149. package/ios/NowPlayingFormatExtractor.swift +25 -18
  150. package/ios/NowPlayingInfo.swift +84 -48
  151. package/ios/OutputRouteMonitor.swift +16 -3
  152. package/ios/PendingBrowseRequests.swift +1 -1
  153. package/ios/PlaceholderArtwork.swift +9 -5
  154. package/ios/PlaybackEngine.swift +165 -76
  155. package/ios/PlaybackErrorMapping.swift +3 -3
  156. package/ios/PlaybackModeStateMachine.swift +2 -2
  157. package/ios/PlaybackNetworkMonitor.swift +73 -0
  158. package/ios/PlayerStateDerivation.swift +6 -6
  159. package/ios/QueueMutationArithmetic.swift +25 -0
  160. package/ios/QueueSkipArithmetic.swift +5 -3
  161. package/ios/QueueWindowArithmetic.swift +152 -0
  162. package/ios/ReadThroughServer.swift +283 -0
  163. package/ios/RemoteCommands.swift +44 -4
  164. package/ios/ReplayGainData.swift +22 -3
  165. package/ios/ReplayGainExtractor.swift +70 -27
  166. package/ios/Siri/VoiceDonation.swift +14 -9
  167. package/ios/StreamingBitrateProbe.swift +22 -68
  168. package/ios/SuppliedReplayGain.swift +83 -0
  169. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  170. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  171. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  172. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  173. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  174. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  175. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  176. package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
  177. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  178. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  179. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  180. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  181. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  182. package/ios/Tests/EngineSwapTests.swift +91 -0
  183. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  184. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  185. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  186. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  187. package/ios/Tests/InputGuardsTests.swift +41 -0
  188. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  189. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  190. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  191. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  192. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  193. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  194. package/ios/Tests/MutationDeferralTests.swift +220 -0
  195. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  196. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  197. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  198. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  199. package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
  200. package/ios/Tests/PlayerFixtures.swift +48 -0
  201. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  202. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  203. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  204. package/ios/Tests/QueueStateTests.swift +3 -1
  205. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  206. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  207. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  208. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  209. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  210. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  211. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  212. package/ios/Tests/RetryRecoveryTests.swift +303 -0
  213. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  214. package/ios/Tests/SkipIndexTests.swift +32 -2
  215. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  216. package/ios/Tests/StallRecoveryTests.swift +97 -0
  217. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  218. package/ios/Tests/TopUpWindowGateTests.swift +394 -0
  219. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  220. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  221. package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
  222. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  223. package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
  224. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  225. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  226. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  227. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  228. package/ios/TrackPlayer+Automotive.swift +101 -0
  229. package/ios/TrackPlayer+Cache.swift +232 -0
  230. package/ios/TrackPlayer+Config.swift +291 -0
  231. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  232. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  233. package/ios/TrackPlayer+EventsDispatch.swift +968 -0
  234. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  235. package/ios/TrackPlayer+Lifecycle.swift +930 -0
  236. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  237. package/ios/TrackPlayer+Queue.swift +940 -0
  238. package/ios/TrackPlayer+Recovery.swift +196 -0
  239. package/ios/TrackPlayer+Skip.swift +455 -0
  240. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  241. package/ios/TrackPlayer+State.swift +108 -0
  242. package/ios/TrackPlayer+Threading.swift +135 -0
  243. package/ios/TrackPlayer+Transport.swift +267 -0
  244. package/ios/TrackPlayer+Window.swift +195 -0
  245. package/ios/TrackPlayer.swift +305 -4610
  246. package/ios/Visualizer.swift +6 -5
  247. package/ios/tests-harness/Podfile +1 -1
  248. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  249. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  250. package/lib/module/hooks/useActiveTrack.js +29 -22
  251. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  252. package/lib/module/hooks/useCast.js +8 -26
  253. package/lib/module/hooks/useCast.js.map +1 -1
  254. package/lib/module/hooks/useEqualizer.js +19 -12
  255. package/lib/module/hooks/useEqualizer.js.map +1 -1
  256. package/lib/module/hooks/useLookaheadCache.js +3 -7
  257. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  258. package/lib/module/hooks/useQueue.js +66 -19
  259. package/lib/module/hooks/useQueue.js.map +1 -1
  260. package/lib/module/index.js +6 -3
  261. package/lib/module/index.js.map +1 -1
  262. package/lib/module/queueDelta.js +41 -0
  263. package/lib/module/queueDelta.js.map +1 -0
  264. package/lib/module/types.js +28 -5
  265. package/lib/module/types.js.map +1 -1
  266. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  267. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  269. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useCast.d.ts +6 -1
  271. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  272. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  273. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  274. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  275. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  276. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  277. package/lib/typescript/index.d.ts +2 -1
  278. package/lib/typescript/index.d.ts.map +1 -1
  279. package/lib/typescript/queueDelta.d.ts +10 -0
  280. package/lib/typescript/queueDelta.d.ts.map +1 -0
  281. package/lib/typescript/types.d.ts +168 -12
  282. package/lib/typescript/types.d.ts.map +1 -1
  283. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  284. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  285. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  286. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  287. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  288. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  289. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  320. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  321. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  322. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  323. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  324. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  325. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  326. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  327. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  328. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  329. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  330. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  331. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  332. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  333. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  334. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  335. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  336. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  337. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  338. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  339. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  340. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  341. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  342. package/package.json +8 -8
  343. package/src/TrackPlayer.nitro.ts +53 -27
  344. package/src/hooks/useActiveTrack.ts +29 -22
  345. package/src/hooks/useCast.ts +14 -15
  346. package/src/hooks/useEqualizer.ts +19 -12
  347. package/src/hooks/useLookaheadCache.ts +3 -7
  348. package/src/hooks/useQueue.ts +66 -17
  349. package/src/index.ts +13 -3
  350. package/src/queueDelta.ts +41 -0
  351. package/src/types.ts +169 -12
  352. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  353. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
  354. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  355. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  356. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  357. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  358. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  359. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  360. package/ios/MetadataReader.swift +0 -555
  361. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  362. package/ios/Tests/MetadataReaderTests.swift +0 -603
  363. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  364. package/ios/Tests/VoiceDonationTests.swift +0 -26
  365. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  366. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  367. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -0,0 +1,20 @@
1
+ package com.margelo.nitro.queueplayer.cast.airplay
2
+
3
+ /**
4
+ * What an AirPlay session accepts for the receiver's own display while it
5
+ * streams. [AirPlayMetadataSync] pushes into this; AirPlay 1 and AirPlay 2
6
+ * sessions implement it over their respective native senders.
7
+ */
8
+ interface MetadataSyncTarget {
9
+ /** Send track metadata (title/artist/album) to the receiver. */
10
+ fun setMetadata(title: String?, artist: String?, album: String?)
11
+
12
+ /** Update the progress bar on the receiver. */
13
+ fun setProgress(elapsedMs: Int, durationMs: Int)
14
+
15
+ /**
16
+ * Send artwork image bytes to the receiver. A receiver whose sender
17
+ * protocol carries no content type ignores [contentType].
18
+ */
19
+ fun setArtwork(imageData: ByteArray, contentType: String)
20
+ }
@@ -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,
@@ -14,6 +14,8 @@ import com.google.android.gms.cast.MediaQueueItem
14
14
  import com.google.android.gms.cast.MediaSeekOptions
15
15
  import com.google.android.gms.cast.MediaStatus
16
16
  import com.google.android.gms.cast.framework.media.RemoteMediaClient
17
+ import com.google.android.gms.common.api.PendingResult
18
+ import com.margelo.nitro.queueplayer.BuildConfig
17
19
  import com.margelo.nitro.queueplayer.cast.CastBackends
18
20
  import com.margelo.nitro.queueplayer.cast.CastMediaItem
19
21
  import com.margelo.nitro.queueplayer.cast.CastSession
@@ -88,6 +90,10 @@ internal class ChromecastSession(
88
90
  private val _seekCompleted = MutableSharedFlow<Long>(extraBufferCapacity = 4)
89
91
 
90
92
  override val state: StateFlow<RemotePlaybackState> = _state.asStateFlow()
93
+
94
+ @Volatile
95
+ override var hasReportedState: Boolean = false
96
+ private set
91
97
  override val positionMs: StateFlow<Long> = _positionMs.asStateFlow()
92
98
  override val durationMs: StateFlow<Long> = _durationMs.asStateFlow()
93
99
  override val currentTrackIndex: StateFlow<Int> = _currentTrackIndex.asStateFlow()
@@ -110,7 +116,13 @@ internal class ChromecastSession(
110
116
  private var serverHandle: MediaServerHandle? = null
111
117
 
112
118
  private val remoteMediaCallbacks = RemoteMediaCallbacks(
113
- onStateChanged = { _state.value = it },
119
+ onStateChanged = { next ->
120
+ if (BuildConfig.DEBUG && _state.value != next) {
121
+ Log.d(TAG, "receiver state ${_state.value} -> $next")
122
+ }
123
+ if (next != RemotePlaybackState.IDLE) hasReportedState = true
124
+ _state.value = next
125
+ },
114
126
  onPositionChanged = { _positionMs.value = it },
115
127
  onDurationChanged = { _durationMs.value = it },
116
128
  onEndedReason = { _endedReason.tryEmit(it) },
@@ -264,12 +276,13 @@ internal class ChromecastSession(
264
276
  _currentTrackIndex.value = clampedStart
265
277
  }
266
278
 
279
+ // No bitmask flag gates `play` — the mask models pause, not resume.
267
280
  override fun play() {
268
281
  fireRemote("play") { it.play() }
269
282
  }
270
283
 
271
284
  override fun pause() {
272
- fireRemote("pause") { it.pause() }
285
+ fireRemote("pause", requires = MediaStatus.COMMAND_PAUSE) { it.pause() }
273
286
  }
274
287
 
275
288
  override fun stop() {
@@ -277,11 +290,11 @@ internal class ChromecastSession(
277
290
  }
278
291
 
279
292
  override fun queueNext() {
280
- fireRemote("queueNext") { it.queueNext(null) }
293
+ fireRemote("queueNext", requires = MediaStatus.COMMAND_QUEUE_NEXT) { it.queueNext(null) }
281
294
  }
282
295
 
283
296
  override fun queuePrev() {
284
- fireRemote("queuePrev") { it.queuePrev(null) }
297
+ fireRemote("queuePrev", requires = MediaStatus.COMMAND_QUEUE_PREVIOUS) { it.queuePrev(null) }
285
298
  }
286
299
 
287
300
  override fun skipToQueueIndex(index: Int) {
@@ -292,8 +305,8 @@ internal class ChromecastSession(
292
305
  // synced that slot yet; skip rather than jump blind.
293
306
  fireRemote("skipToQueueIndex") { client ->
294
307
  val itemId = receiverItemIdAtIndex(index)
295
- if (itemId == MediaQueueItem.INVALID_ITEM_ID) return@fireRemote
296
- client.queueJumpToItem(itemId, null)
308
+ if (itemId == MediaQueueItem.INVALID_ITEM_ID) null
309
+ else client.queueJumpToItem(itemId, null)
297
310
  }
298
311
  }
299
312
 
@@ -367,7 +380,9 @@ internal class ChromecastSession(
367
380
  }
368
381
 
369
382
  override fun setPlaybackRate(rate: Float) {
370
- fireRemote("setPlaybackRate") { it.setPlaybackRate(rate.toDouble()) }
383
+ fireRemote("setPlaybackRate", requires = MediaStatus.COMMAND_PLAYBACK_RATE) {
384
+ it.setPlaybackRate(rate.toDouble())
385
+ }
371
386
  }
372
387
 
373
388
  override fun setRepeatMode(mode: RemoteRepeatMode) {
@@ -435,11 +450,26 @@ internal class ChromecastSession(
435
450
  }
436
451
 
437
452
  /**
438
- * Post [block] to main and invoke it with the current remote media
439
- * client. Silently skips when the session is closed or the client
440
- * is unavailable (log only — transport calls are fire-and-forget).
453
+ * Post [block] to main and invoke it with the current [RemoteMediaClient].
454
+ * Silently skips when the session is closed or the client is unavailable
455
+ * (log only — transport calls are fire-and-forget).
456
+ *
457
+ * [block] returns the [PendingResult] the SDK scheduled, or `null` when it
458
+ * declined to issue one. A returned result gets a callback so a
459
+ * receiver-side rejection is reported instead of vanishing — without it a
460
+ * refused command is indistinguishable from one the receiver honoured.
461
+ *
462
+ * [requires] names the [MediaStatus] command bitmask flag the receiver must
463
+ * advertise for this command (`null` for commands not gated by the mask).
464
+ * The receiver publishes support per media item, so an item can accept queue
465
+ * commands while refusing `pause` — worth a warning, since the command is
466
+ * otherwise dropped in silence.
441
467
  */
442
- private fun fireRemote(label: String, block: (RemoteMediaClient) -> Unit) {
468
+ private fun fireRemote(
469
+ label: String,
470
+ requires: Long? = null,
471
+ block: (RemoteMediaClient) -> PendingResult<RemoteMediaClient.MediaChannelResult>?,
472
+ ) {
443
473
  if (closed) return
444
474
  runOnMain {
445
475
  if (closed) return@runOnMain
@@ -448,8 +478,38 @@ internal class ChromecastSession(
448
478
  Log.w(TAG, "$label: no RemoteMediaClient — session not fully live?")
449
479
  return@runOnMain
450
480
  }
451
- runCatching { block(client) }
452
- .onFailure { Log.w(TAG, "$label failed", it) }
481
+ val status = client.mediaStatus
482
+ if (requires != null && status != null && !status.isMediaCommandSupported(requires)) {
483
+ Log.w(
484
+ TAG,
485
+ "$label: receiver does not advertise support for this command " +
486
+ "(supportedMediaCommands=${status.supportedMediaCommands}); sending anyway",
487
+ )
488
+ }
489
+ if (BuildConfig.DEBUG) Log.d(TAG, "$label: issuing")
490
+ val outcome = runCatching { block(client) }
491
+ val thrown = outcome.exceptionOrNull()
492
+ if (thrown != null) {
493
+ Log.w(TAG, "$label failed", thrown)
494
+ return@runOnMain
495
+ }
496
+ // A null result means this call site declined to issue anything — the
497
+ // only such case is a queue index the receiver's model hasn't synced
498
+ // yet, which is a normal transient, not a fault.
499
+ val pending = outcome.getOrNull()
500
+ if (pending == null) {
501
+ if (BuildConfig.DEBUG) Log.d(TAG, "$label: no request issued")
502
+ return@runOnMain
503
+ }
504
+ pending.setResultCallback { result ->
505
+ if (closed) return@setResultCallback
506
+ if (!result.status.isSuccess) {
507
+ Log.w(
508
+ TAG,
509
+ "$label failed: ${result.status.statusCode} ${result.status.statusMessage}",
510
+ )
511
+ }
512
+ }
453
513
  }
454
514
  }
455
515
 
@@ -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)
@@ -0,0 +1,115 @@
1
+ package com.margelo.nitro.queueplayer
2
+
3
+ import android.content.Context
4
+ import android.media.AudioManager
5
+ import androidx.media3.common.MediaItem
6
+ import androidx.media3.common.PlaybackException
7
+ import androidx.media3.common.Player
8
+ import androidx.media3.common.util.UnstableApi
9
+ import androidx.test.core.app.ApplicationProvider
10
+ import org.junit.Assert.assertEquals
11
+ import org.junit.Assert.assertNotNull
12
+ import org.junit.Test
13
+ import org.junit.runner.RunWith
14
+ import org.robolectric.RobolectricTestRunner
15
+ import org.robolectric.Shadows.shadowOf
16
+ import org.robolectric.annotation.Config
17
+
18
+ /**
19
+ * A crossfade engine owns audio focus itself, so the pause a focus loss
20
+ * makes reaches the delegate carrying the audio-focus reason, not the
21
+ * user-request reason `pause()` stamps by default.
22
+ */
23
+ @RunWith(RobolectricTestRunner::class)
24
+ @Config(sdk = [34])
25
+ @UnstableApi
26
+ class CrossfadeEngineFocusLossTest {
27
+
28
+ private val context: Context = ApplicationProvider.getApplicationContext()
29
+
30
+ /** Records every play-when-ready reason the engine reports. */
31
+ private class RecordingDelegate : PlaybackEngineDelegate {
32
+ val reasons = mutableListOf<Int?>()
33
+ override fun onStateMaybeChanged(engine: PlaybackEngine, playWhenReadyReason: Int?) {
34
+ reasons.add(playWhenReadyReason)
35
+ }
36
+ override fun onIsPlayingChanged(engine: PlaybackEngine, isPlaying: Boolean) {}
37
+ override fun onTrackTransition(engine: PlaybackEngine, platformReason: Int) {}
38
+ override fun onActiveItemFormatChanged(engine: PlaybackEngine) {}
39
+ override fun onError(engine: PlaybackEngine, exception: PlaybackException, failedIndex: Int) {}
40
+ override fun onPlaybackEnded(engine: PlaybackEngine) {}
41
+ override fun onAutoTransitionDiscontinuity(engine: PlaybackEngine) {}
42
+ override fun onCrossfadeBegin(engine: PlaybackEngine, incomingIndex: Int) {}
43
+ override fun onCrossfadeCancel(engine: PlaybackEngine, revertedIndex: Int) {}
44
+ }
45
+
46
+ private fun playingEngine(delegate: RecordingDelegate): CrossfadeEngine {
47
+ val engine = CrossfadeEngine(context = context, crossfadeDurationMs = 3_000L)
48
+ engine.delegate = delegate
49
+ engine.setMediaItems(listOf(MediaItem.fromUri("https://example.com/a.mp3")), 0, 0L)
50
+ engine.play()
51
+ shadowOf(android.os.Looper.getMainLooper()).idle()
52
+ return engine
53
+ }
54
+
55
+ private fun focusListener(): AudioManager.OnAudioFocusChangeListener {
56
+ val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
57
+ val request = shadowOf(audioManager).lastAudioFocusRequest
58
+ assertNotNull("play() must request focus", request)
59
+ return request.listener
60
+ }
61
+
62
+ @Test
63
+ fun `a focus loss pauses with the audio-focus reason`() {
64
+ val delegate = RecordingDelegate()
65
+ val engine = playingEngine(delegate)
66
+ delegate.reasons.clear()
67
+
68
+ focusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS)
69
+ shadowOf(android.os.Looper.getMainLooper()).idle()
70
+
71
+ assertEquals(
72
+ "the delegate stamps the last reason it is handed; a USER_REQUEST after the loss would label the pause a user pause",
73
+ Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS,
74
+ delegate.reasons.last { it != null },
75
+ )
76
+ engine.release()
77
+ }
78
+
79
+ @Test
80
+ fun `a transient focus loss pauses with the audio-focus reason too`() {
81
+ val delegate = RecordingDelegate()
82
+ val engine = playingEngine(delegate)
83
+ delegate.reasons.clear()
84
+
85
+ focusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)
86
+ shadowOf(android.os.Looper.getMainLooper()).idle()
87
+
88
+ assertEquals(
89
+ Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS,
90
+ delegate.reasons.last { it != null },
91
+ )
92
+ engine.release()
93
+ }
94
+
95
+ @Test
96
+ fun `a user pause after the loss keeps its own reason`() {
97
+ val delegate = RecordingDelegate()
98
+ val engine = playingEngine(delegate)
99
+ focusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)
100
+ focusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN)
101
+ engine.play()
102
+ shadowOf(android.os.Looper.getMainLooper()).idle()
103
+ delegate.reasons.clear()
104
+
105
+ engine.pause()
106
+ shadowOf(android.os.Looper.getMainLooper()).idle()
107
+
108
+ assertEquals(
109
+ "the substitution lasts only for the loss's own pauses",
110
+ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
111
+ delegate.reasons.last { it != null },
112
+ )
113
+ engine.release()
114
+ }
115
+ }
@@ -1,5 +1,6 @@
1
1
  package com.margelo.nitro.queueplayer
2
2
 
3
+ import androidx.media3.common.MediaItem
3
4
  import androidx.media3.common.util.UnstableApi
4
5
  import androidx.test.core.app.ApplicationProvider
5
6
  import org.junit.Assert.assertEquals
@@ -31,6 +32,49 @@ class CrossfadeEngineLifecycleTest {
31
32
  crossfadeDurationMs = crossfadeDurationMs,
32
33
  )
33
34
 
35
+ /**
36
+ * Settling leaves the standby leg holding nothing.
37
+ *
38
+ * An armed standby carries the queue as it stood when it was armed. A caller
39
+ * that mutates the queue through a path the engine does not see — Media3
40
+ * setting resolved assistant items on the session player — needs that
41
+ * snapshot gone, or promoting it drops whatever the caller added.
42
+ */
43
+ @Test
44
+ fun `settling a pending fade leaves nothing staged on the standby leg`() {
45
+ val engine = makeEngine()
46
+ engine.standbyPlayer.setMediaItems(
47
+ listOf(MediaItem.fromUri("http://example.com/next.mp3")))
48
+ engine.standbyPlayer.prepare()
49
+ assertEquals(1, engine.standbyPlayer.mediaItemCount)
50
+
51
+ engine.settlePendingFade()
52
+
53
+ assertEquals(0, engine.standbyPlayer.mediaItemCount)
54
+ engine.release()
55
+ }
56
+
57
+ /**
58
+ * A settle with nothing to settle leaves the legs as they are: a caller
59
+ * settling defensively must not swap them. Promotion itself needs a live
60
+ * fade, which needs real playback with duration metadata, so that half is
61
+ * covered by the crossfade demo suites.
62
+ */
63
+ @Test
64
+ fun `settling by promotion leaves the roles alone when no fade is in flight`() {
65
+ val engine = makeEngine()
66
+ val leading = engine.leadingPlayer
67
+ val standby = engine.standbyPlayer
68
+
69
+ engine.settlePendingFadeByPromoting()
70
+ engine.settlePendingFadeByPromoting()
71
+
72
+ assertSame(leading, engine.leadingPlayer)
73
+ assertSame(standby, engine.standbyPlayer)
74
+ assertTrue(engine.currentRoleIsA)
75
+ engine.release()
76
+ }
77
+
34
78
  @Test
35
79
  fun `constructor accepts a non-negative crossfade duration`() {
36
80
  val engine = makeEngine()
@@ -204,6 +248,7 @@ class CrossfadeEngineLifecycleTest {
204
248
  override fun onError(
205
249
  engine: PlaybackEngine,
206
250
  exception: androidx.media3.common.PlaybackException,
251
+ failedIndex: Int,
207
252
  ) = Unit
208
253
  override fun onPlaybackEnded(engine: PlaybackEngine) = Unit
209
254
  override fun onAutoTransitionDiscontinuity(engine: PlaybackEngine) = Unit