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
@@ -45,7 +45,9 @@ class TrackPlayerQueueTest {
45
45
  private fun track(id: String) = TrackItem(
46
46
  id = id, url = "https://example.com/$id.m4a",
47
47
  title = null, artist = null, album = null,
48
- duration = null, artworkUrl = null, extras = null
48
+ duration = null, artworkUrl = null, extras = null,
49
+ replayGainTrackGain = null, replayGainTrackPeak = null,
50
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
49
51
  )
50
52
 
51
53
  private lateinit var serviceHandle: PlaybackServiceTestHandle
@@ -55,7 +57,7 @@ class TrackPlayerQueueTest {
55
57
  player = TrackPlayer()
56
58
  serviceHandle = bindPlaybackServiceForTest(player, context())
57
59
  player.configureInternal(
58
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
60
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
59
61
  )
60
62
  }
61
63
 
@@ -128,13 +130,15 @@ class TrackPlayerQueueTest {
128
130
  // --- getQueue / getCurrentTrackIndex
129
131
 
130
132
  @Test
131
- fun `getQueue returns the track list as an array`() {
133
+ fun `setQueueInternal stores the tracks in order`() {
134
+ // `getQueue()` itself returns a Nitro Promise, which cannot be resolved
135
+ // from Robolectric; it reads this list on main, and the demo cycle covers
136
+ // the bridged call.
132
137
  val tracks = (1..3).map { track("t$it") }
133
138
  player.setQueueInternal(tracks)
134
139
 
135
- val result = player.getQueue()
136
- assertEquals(3, result.size)
137
- assertEquals("t1", result[0].id)
140
+ assertEquals(3, player.tracks.size)
141
+ assertEquals("t1", player.tracks[0].id)
138
142
  }
139
143
 
140
144
  @Test
@@ -275,7 +279,7 @@ class TrackPlayerQueueTest {
275
279
  fun `skipToIndex after setQueue issues the ExoPlayer seek`() {
276
280
  // After setQueue the MediaItem timeline has 3 entries, so the
277
281
  // `target < mediaItemCount` guard in skipToIndexInternal no
278
- // longer short-circuits — confirms the skip → seek plumbing
282
+ // longer short-circuits — confirms the skip → seek path
279
283
  // survives once the timeline is populated.
280
284
  player.setQueueInternal((1..3).map { track("t$it") })
281
285
 
@@ -41,7 +41,8 @@ class TrackPlayerReadersTest {
41
41
  PlayerConfig(
42
42
  httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null,
43
43
  networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null,
44
- visualizationEnabled = null, clampSeekToBuffered = null,
44
+ voiceVocabularyDonationEnabled = null,
45
+ visualizationEnabled = null, clampSeekToBuffered = null,
45
46
  lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
46
47
  ),
47
48
  context()
@@ -40,7 +40,9 @@ class TrackPlayerSkipCapabilityTest {
40
40
  private fun track(id: String) = TrackItem(
41
41
  id = id, url = "https://example.com/$id.m4a",
42
42
  title = null, artist = null, album = null,
43
- duration = null, artworkUrl = null, extras = null
43
+ duration = null, artworkUrl = null, extras = null,
44
+ replayGainTrackGain = null, replayGainTrackPeak = null,
45
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
44
46
  )
45
47
 
46
48
  /** Drain main-looper posts so the auto-fire-on-subscribe lands. */
@@ -53,7 +55,7 @@ class TrackPlayerSkipCapabilityTest {
53
55
  player = TrackPlayer()
54
56
  serviceHandle = bindPlaybackServiceForTest(player, context())
55
57
  player.configureInternal(
56
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
58
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
57
59
  )
58
60
  }
59
61
 
@@ -318,7 +320,7 @@ class TrackPlayerSkipCapabilityTest {
318
320
  player = TrackPlayer()
319
321
  serviceHandle = bindPlaybackServiceForTest(player, context())
320
322
  player.configureInternal(
321
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
323
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
322
324
  )
323
325
  }
324
326
 
@@ -326,10 +328,25 @@ class TrackPlayerSkipCapabilityTest {
326
328
 
327
329
  private fun configureRestart() {
328
330
  player.configureInternal(
329
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = SkipToPreviousBehavior.RESTART_OR_PREVIOUS), context()
331
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = SkipToPreviousBehavior.RESTART_OR_PREVIOUS), context()
330
332
  )
331
333
  }
332
334
 
335
+ @Test
336
+ fun `restart mode — addToQueueInternal into empty queue keeps Previous false (cur stays -1)`() {
337
+ configureRestart()
338
+ val emissions = mutableListOf<SkipCapability>()
339
+ player.onSkipCapabilityChange { cap -> emissions.add(cap) }
340
+ flushMain()
341
+ emissions.clear()
342
+
343
+ player.addToQueueInternal((1..3).map { track("t$it") }, insertBefore = null)
344
+ // No loaded track: nothing to restart and nothing before it, so the
345
+ // lock-screen Previous control stays disabled until skipToIndex.
346
+ assertEquals(false, player.getCanSkipPrevious())
347
+ assertEquals(emptyList<SkipCapability>(), emissions)
348
+ }
349
+
333
350
  @Test
334
351
  fun `restart mode forces canSkipPrevious true at index 0`() {
335
352
  configureRestart()
@@ -360,4 +377,73 @@ class TrackPlayerSkipCapabilityTest {
360
377
  emissions.last().canSkipPrevious
361
378
  )
362
379
  }
380
+
381
+ // --- cast index writes keep the capability current
382
+
383
+ @Test
384
+ fun `emitRemoteTrackChange to the last index disables Next`() {
385
+ player.setQueueInternal((1..3).map { track("t$it") })
386
+ assertEquals(true, player.getCanSkipNext())
387
+
388
+ // The receiver advanced to the last track; the lock-screen Next
389
+ // control must grey out exactly as it does for a local advance.
390
+ player.emitRemoteTrackChange(2)
391
+ assertEquals(2, player.currentTrackIndex)
392
+ assertEquals(false, player.getCanSkipNext())
393
+ assertEquals(true, player.getCanSkipPrevious())
394
+ }
395
+
396
+ @Test
397
+ fun `emitRemoteTrackChange emits the capability change`() {
398
+ player.setQueueInternal((1..3).map { track("t$it") })
399
+ val emissions = mutableListOf<SkipCapability>()
400
+ player.onSkipCapabilityChange { cap -> emissions.add(cap) }
401
+ flushMain()
402
+ emissions.clear()
403
+
404
+ player.emitRemoteTrackChange(2)
405
+ flushMain()
406
+ assertEquals(
407
+ listOf(SkipCapability(canSkipPrevious = true, canSkipNext = false)),
408
+ emissions
409
+ )
410
+ }
411
+
412
+ @Test
413
+ fun `emitRemoteTrackChange classifies the new track's source`() {
414
+ // A local file at index 2, streaming tracks elsewhere: the source the
415
+ // JS side reads must follow the index the receiver moved to.
416
+ val local = TrackItem(
417
+ id = "local", url = "file:///tmp/local.m4a",
418
+ title = null, artist = null, album = null,
419
+ duration = null, artworkUrl = null, extras = null,
420
+ replayGainTrackGain = null, replayGainTrackPeak = null,
421
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
422
+ )
423
+ player.setQueueInternal(listOf(track("t1"), track("t2"), local))
424
+ assertEquals(TrackSource.STREAMING, player.getCurrentTrackSource())
425
+
426
+ player.emitRemoteTrackChange(2)
427
+ assertEquals(TrackSource.LOCAL, player.getCurrentTrackSource())
428
+ }
429
+
430
+ @Test
431
+ fun `resumeLocalAfterCast lands on the handback index with its capability and source`() {
432
+ val local = TrackItem(
433
+ id = "local", url = "file:///tmp/local.m4a",
434
+ title = null, artist = null, album = null,
435
+ duration = null, artworkUrl = null, extras = null,
436
+ replayGainTrackGain = null, replayGainTrackPeak = null,
437
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
438
+ )
439
+ player.setQueueInternal(listOf(track("t1"), track("t2"), local))
440
+ assertEquals(true, player.getCanSkipNext())
441
+ assertEquals(TrackSource.STREAMING, player.getCurrentTrackSource())
442
+
443
+ player.resumeLocalAfterCast(trackIndex = 2, positionMs = 0L)
444
+ flushMain()
445
+ assertEquals(2, player.currentTrackIndex)
446
+ assertEquals(false, player.getCanSkipNext())
447
+ assertEquals(TrackSource.LOCAL, player.getCurrentTrackSource())
448
+ }
363
449
  }
@@ -15,9 +15,8 @@ import org.robolectric.annotation.Config
15
15
  * [TrackPlayer]. `QueueSkipArithmeticTest` covers the raw
16
16
  * arithmetic; these tests cover the adapter layer:
17
17
  *
18
- * - `computeNextIndex` / `computePreviousIndex` read
19
- * `repeatModeState` + `currentTrackIndex` + `tracks.size` and
20
- * delegate to [QueueSkipArithmetic].
18
+ * - `computeNextIndex` reads `repeatModeState` + `currentTrackIndex`
19
+ * + `tracks.size` and delegates to [QueueSkipArithmetic].
21
20
  * - `skipToNextInternal` / `skipToPreviousInternal` /
22
21
  * `skipToIndexInternal` — advance `currentTrackIndex` when the
23
22
  * arithmetic returns a target, no-op when it returns null. Under
@@ -41,7 +40,9 @@ class TrackPlayerSkipTest {
41
40
  private fun track(id: String) = TrackItem(
42
41
  id = id, url = "https://example.com/$id.m4a",
43
42
  title = null, artist = null, album = null,
44
- duration = null, artworkUrl = null, extras = null
43
+ duration = null, artworkUrl = null, extras = null,
44
+ replayGainTrackGain = null, replayGainTrackPeak = null,
45
+ replayGainAlbumGain = null, replayGainAlbumPeak = null
45
46
  )
46
47
 
47
48
  private lateinit var serviceHandle: PlaybackServiceTestHandle
@@ -51,7 +52,7 @@ class TrackPlayerSkipTest {
51
52
  player = TrackPlayer()
52
53
  serviceHandle = bindPlaybackServiceForTest(player, context())
53
54
  player.configureInternal(
54
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
55
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
55
56
  )
56
57
  player.tracks = (1..5).map { track("t$it") }
57
58
  player.currentTrackIndex = 2 // mid-queue default for most cases
@@ -63,7 +64,7 @@ class TrackPlayerSkipTest {
63
64
  serviceHandle.destroy()
64
65
  }
65
66
 
66
- // --- computeNextIndex / computePreviousIndex (adapter)
67
+ // --- computeNextIndex (adapter)
67
68
 
68
69
  @Test
69
70
  fun `computeNextIndex reads repeatModeState`() {
@@ -77,18 +78,6 @@ class TrackPlayerSkipTest {
77
78
  assertEquals(3, player.computeNextIndex())
78
79
  }
79
80
 
80
- @Test
81
- fun `computePreviousIndex reads repeatModeState`() {
82
- player.repeatModeState = RepeatMode.OFF
83
- assertEquals(1, player.computePreviousIndex())
84
-
85
- player.repeatModeState = RepeatMode.TRACK
86
- assertEquals(1, player.computePreviousIndex())
87
-
88
- player.repeatModeState = RepeatMode.QUEUE
89
- assertEquals(1, player.computePreviousIndex())
90
- }
91
-
92
81
  @Test
93
82
  fun `computeNextIndex returns null at end under REPEAT OFF`() {
94
83
  player.repeatModeState = RepeatMode.OFF
@@ -103,13 +92,6 @@ class TrackPlayerSkipTest {
103
92
  assertEquals(0, player.computeNextIndex())
104
93
  }
105
94
 
106
- @Test
107
- fun `computePreviousIndex wraps to last under REPEAT QUEUE at index 0`() {
108
- player.repeatModeState = RepeatMode.QUEUE
109
- player.currentTrackIndex = 0
110
- assertEquals(4, player.computePreviousIndex())
111
- }
112
-
113
95
  // --- skipToNextInternal / skipToPreviousInternal
114
96
 
115
97
  @Test
@@ -293,7 +275,7 @@ class TrackPlayerSkipTest {
293
275
 
294
276
  private fun configureSkipBehavior(behavior: SkipToPreviousBehavior?) {
295
277
  player.configureInternal(
296
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = behavior), context()
278
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = behavior), context()
297
279
  )
298
280
  player.tracks = (1..5).map { track("t$it") }
299
281
  }
@@ -50,7 +50,7 @@ class TrackPlayerTransportTest {
50
50
  player = TrackPlayer()
51
51
  serviceHandle = bindPlaybackServiceForTest(player, context())
52
52
  player.configureInternal(
53
- PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
53
+ PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
54
54
  )
55
55
  }
56
56
 
@@ -80,7 +80,7 @@ class TrackPlayerTransportTest {
80
80
  // currentTrackIndex while the ExoPlayer is still IDLE.
81
81
  // iOS parity: paused-with-queue returns PAUSED, not NONE.
82
82
  player.tracks = listOf(
83
- TrackItem("t1", "https://example.com/1.m4a", null, null, null, null, null, null)
83
+ TrackItem("t1", "https://example.com/1.m4a", null, null, null, null, null, null, null, null, null, null)
84
84
  )
85
85
  player.currentTrackIndex = 0
86
86
 
@@ -104,7 +104,7 @@ class TrackPlayerTransportTest {
104
104
  // on eventually-seekable content still lands. The reset-to-start protection
105
105
  // applies once the item is loaded (STATE_READY) and confirmed unseekable.
106
106
  player.setQueueInternal(
107
- (1..3).map { TrackItem("t$it", "https://example.com/$it.m4a", null, null, null, null, null, null) }
107
+ (1..3).map { TrackItem("t$it", "https://example.com/$it.m4a", null, null, null, null, null, null, null, null, null, null) }
108
108
  )
109
109
  player.currentTrackIndex = 0
110
110
 
@@ -162,9 +162,6 @@ private class FakePlaybackEngineWithSinks : PlaybackEngine {
162
162
 
163
163
  override val mediaSessionPlayer: androidx.media3.exoplayer.ExoPlayer
164
164
  get() = error("not used in test")
165
- override suspend fun prepare(
166
- mediaItems: List<androidx.media3.common.MediaItem>, startIndex: Int, startPositionMs: Long,
167
- ) = error("not used in test")
168
165
  override fun release() = error("not used in test")
169
166
  override fun setMediaItems(items: List<androidx.media3.common.MediaItem>, startIndex: Int, startPositionMs: Long) = error("not used in test")
170
167
  override fun addMediaItems(items: List<androidx.media3.common.MediaItem>, insertBefore: Int) = error("not used in test")
@@ -197,6 +194,5 @@ private class FakePlaybackEngineWithSinks : PlaybackEngine {
197
194
  override fun detachAudioBufferSink(sink: TeeAudioProcessor.AudioBufferSink) {
198
195
  if (attachedSinks.remove(sink)) detachCount++
199
196
  }
200
- override suspend fun handoff(to: PlaybackEngine, atTrackBoundary: Boolean) = error("not used in test")
201
197
  override var delegate: PlaybackEngineDelegate? = null
202
198
  }
@@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.asStateFlow
12
12
  * Test double for [CastSession.RemotePlayer]. Transport calls record their
13
13
  * name in [transportCalls]; the StateFlows are driveable via the public
14
14
  * backing fields, or via [reportState] to mirror the real session's
15
- * [hasReportedState] latch; [mediaSessionPlayer] is injectable so bind tests
15
+ * [hasReportedState] flag; [mediaSessionPlayer] is injectable so bind tests
16
16
  * can assert the MediaSession swaps to it.
17
17
  */
18
18
  internal class FakeRemotePlayer(
@@ -44,7 +44,7 @@ internal class FakeRemotePlayer(
44
44
 
45
45
  /**
46
46
  * Drive the receiver state the way a live session does: a non-idle report
47
- * latches [hasReportedState]. Set [backingState] directly to stage a state
47
+ * sets [hasReportedState]. Set [backingState] directly to stage a state
48
48
  * the receiver has not reported yet.
49
49
  */
50
50
  fun reportState(state: RemotePlaybackState) {
@@ -0,0 +1,173 @@
1
+ package com.margelo.nitro.queueplayer.cast.net
2
+
3
+ import android.content.Context
4
+ import android.net.ConnectivityManager
5
+ import android.net.LinkAddress
6
+ import android.net.LinkProperties
7
+ import android.net.Network
8
+ import android.net.NetworkCapabilities
9
+ import androidx.test.core.app.ApplicationProvider
10
+ import org.junit.Assert.assertEquals
11
+ import org.junit.Assert.assertFalse
12
+ import org.junit.Assert.assertNull
13
+ import org.junit.Assert.assertTrue
14
+ import org.junit.Test
15
+ import org.junit.runner.RunWith
16
+ import org.robolectric.RobolectricTestRunner
17
+ import org.robolectric.Shadows.shadowOf
18
+ import org.robolectric.annotation.Config
19
+ import org.robolectric.shadow.api.Shadow
20
+ import org.robolectric.shadows.ShadowNetwork
21
+ import org.robolectric.shadows.ShadowNetworkCapabilities
22
+ import org.robolectric.util.ReflectionHelpers
23
+ import org.robolectric.util.ReflectionHelpers.ClassParameter
24
+ import java.net.InetAddress
25
+
26
+ /**
27
+ * `currentAddress` is the Wi-Fi network's IPv4 address: the callback is
28
+ * registered for Wi-Fi networks only, the `start()` seed uses the active
29
+ * network only when it is Wi-Fi and not a VPN, and the address follows
30
+ * that network's link properties until it is lost.
31
+ */
32
+ @RunWith(RobolectricTestRunner::class)
33
+ @Config(sdk = [34], shadows = [ShadowConnectivityManagerRecordingRequests::class])
34
+ class LocalAddressMonitorTest {
35
+
36
+ private val context: Context = ApplicationProvider.getApplicationContext()
37
+ private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
38
+ private val shadowCm: ShadowConnectivityManagerRecordingRequests = Shadow.extract(cm)
39
+
40
+ // The shadow's default active network has netId 0; its transport is
41
+ // whatever capabilities the test puts on it. Any other netId is a
42
+ // network that exists but is not the active one.
43
+ private val activeNetwork: Network = ShadowNetwork.newInstance(0)
44
+ private val otherNetwork: Network = ShadowNetwork.newInstance(1)
45
+
46
+ private fun describe(network: Network, transports: List<Int>, ipv4: String, vpn: Boolean = false) {
47
+ val capabilities = ShadowNetworkCapabilities.newInstance()
48
+ transports.forEach { shadowOf(capabilities).addTransportType(it) }
49
+ if (vpn) shadowOf(capabilities).removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
50
+ shadowCm.setNetworkCapabilities(network, capabilities)
51
+ shadowCm.setLinkProperties(network, linkProperties(ipv4))
52
+ }
53
+
54
+ private fun linkProperties(vararg addresses: String): LinkProperties {
55
+ val linkAddresses = addresses.map { literal ->
56
+ val inet = InetAddress.getByName(literal)
57
+ ReflectionHelpers.callConstructor(
58
+ LinkAddress::class.java,
59
+ ClassParameter.from(InetAddress::class.java, inet),
60
+ ClassParameter.from(Int::class.javaPrimitiveType, if (inet.address.size == 4) 24 else 64),
61
+ )
62
+ }
63
+ return LinkProperties().apply { setLinkAddresses(linkAddresses) }
64
+ }
65
+
66
+ @Test
67
+ fun `the callback is registered for Wi-Fi networks only`() {
68
+ LocalAddressMonitor(context).start()
69
+
70
+ val request = shadowCm.requests.single()
71
+ assertTrue(request.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
72
+ assertFalse(request.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR))
73
+ assertTrue(request.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN))
74
+ }
75
+
76
+ @Test
77
+ fun `start seeds the address from a Wi-Fi active network`() {
78
+ describe(activeNetwork, listOf(NetworkCapabilities.TRANSPORT_WIFI), "192.168.1.10")
79
+
80
+ val monitor = LocalAddressMonitor(context).also { it.start() }
81
+
82
+ assertEquals("192.168.1.10", monitor.currentAddress)
83
+ }
84
+
85
+ @Test
86
+ fun `start does not seed the address from a cellular active network`() {
87
+ describe(otherNetwork, listOf(NetworkCapabilities.TRANSPORT_WIFI), "192.168.1.10")
88
+ describe(activeNetwork, listOf(NetworkCapabilities.TRANSPORT_CELLULAR), "10.0.0.5")
89
+
90
+ val monitor = LocalAddressMonitor(context).also { it.start() }
91
+
92
+ assertNull("a cellular address is unreachable from a LAN receiver", monitor.currentAddress)
93
+ }
94
+
95
+ @Test
96
+ fun `start does not seed the address from a VPN over Wi-Fi`() {
97
+ describe(
98
+ activeNetwork,
99
+ listOf(NetworkCapabilities.TRANSPORT_VPN, NetworkCapabilities.TRANSPORT_WIFI),
100
+ "10.8.0.2",
101
+ vpn = true,
102
+ )
103
+
104
+ val monitor = LocalAddressMonitor(context).also { it.start() }
105
+
106
+ assertNull("a tunnel address is unreachable from a LAN receiver", monitor.currentAddress)
107
+ }
108
+
109
+ @Test
110
+ fun `start registers the callback and seeds nothing when there is no active network`() {
111
+ shadowCm.setDefaultNetworkActive(false)
112
+
113
+ val monitor = LocalAddressMonitor(context).also { it.start() }
114
+
115
+ assertEquals(1, shadowCm.networkCallbacks.size)
116
+ assertNull(monitor.currentAddress)
117
+ }
118
+
119
+ @Test
120
+ fun `a matched network's link properties set the address and its loss clears it`() {
121
+ shadowCm.setDefaultNetworkActive(false)
122
+ val monitor = LocalAddressMonitor(context).also { it.start() }
123
+ val callback = shadowCm.networkCallbacks.single()
124
+
125
+ callback.onLinkPropertiesChanged(otherNetwork, linkProperties("192.168.1.20"))
126
+ assertEquals("192.168.1.20", monitor.currentAddress)
127
+
128
+ callback.onLost(otherNetwork)
129
+ assertNull(monitor.currentAddress)
130
+ }
131
+
132
+ /** A Wi-Fi handover reports the new network's link before the old network
133
+ * is lost; the loss of the old one must not clear the new address. */
134
+ @Test
135
+ fun `losing a network other than the one that supplied the address keeps it`() {
136
+ shadowCm.setDefaultNetworkActive(false)
137
+ val monitor = LocalAddressMonitor(context).also { it.start() }
138
+ val callback = shadowCm.networkCallbacks.single()
139
+ val oldWifi = otherNetwork
140
+ val newWifi = ShadowNetwork.newInstance(2)
141
+
142
+ callback.onLinkPropertiesChanged(oldWifi, linkProperties("192.168.1.20"))
143
+ callback.onLinkPropertiesChanged(newWifi, linkProperties("192.168.2.30"))
144
+ callback.onLost(oldWifi)
145
+ assertEquals("192.168.2.30", monitor.currentAddress)
146
+
147
+ callback.onLost(newWifi)
148
+ assertNull(monitor.currentAddress)
149
+ }
150
+
151
+ @Test
152
+ fun `the address is the first non-loopback IPv4 among the link addresses`() {
153
+ shadowCm.setDefaultNetworkActive(false)
154
+ val monitor = LocalAddressMonitor(context).also { it.start() }
155
+ val callback = shadowCm.networkCallbacks.single()
156
+
157
+ callback.onLinkPropertiesChanged(otherNetwork, linkProperties("fe80::1", "127.0.0.1", "192.168.1.20", "192.168.1.21"))
158
+ assertEquals("192.168.1.20", monitor.currentAddress)
159
+
160
+ callback.onLinkPropertiesChanged(otherNetwork, linkProperties("fe80::1"))
161
+ assertNull(monitor.currentAddress)
162
+ }
163
+
164
+ @Test
165
+ fun `stop unregisters the callback`() {
166
+ val monitor = LocalAddressMonitor(context).also { it.start() }
167
+ assertEquals(1, shadowCm.networkCallbacks.size)
168
+
169
+ monitor.stop()
170
+
171
+ assertTrue(shadowCm.networkCallbacks.isEmpty())
172
+ }
173
+ }
@@ -0,0 +1,27 @@
1
+ package com.margelo.nitro.queueplayer.cast.net
2
+
3
+ import android.net.ConnectivityManager
4
+ import android.net.NetworkRequest
5
+ import android.os.Handler
6
+ import org.robolectric.annotation.Implements
7
+ import org.robolectric.shadows.ShadowConnectivityManager
8
+
9
+ /**
10
+ * A [ShadowConnectivityManager] that keeps the [NetworkRequest] passed to
11
+ * `registerNetworkCallback`; the stock shadow discards it and stores only
12
+ * the callback.
13
+ */
14
+ @Implements(ConnectivityManager::class)
15
+ class ShadowConnectivityManagerRecordingRequests : ShadowConnectivityManager() {
16
+
17
+ val requests = mutableListOf<NetworkRequest>()
18
+
19
+ override fun registerNetworkCallback(
20
+ request: NetworkRequest,
21
+ networkCallback: ConnectivityManager.NetworkCallback,
22
+ handler: Handler?,
23
+ ) {
24
+ requests.add(request)
25
+ super.registerNetworkCallback(request, networkCallback, handler)
26
+ }
27
+ }
package/app.plugin.js CHANGED
@@ -331,6 +331,13 @@ const withChromecastSubspecPodfile = (config) =>
331
331
  },
332
332
  ]);
333
333
 
334
+ /**
335
+ * Bonjour type the local-network permission probe advertises and browses.
336
+ * Mirrors `LocalNetworkPermissionProbe.serviceType`; the two must agree or the
337
+ * probe is blocked with nothing to indicate why.
338
+ */
339
+ const LOCAL_NETWORK_PROBE_SERVICE_TYPE = '_rnqp-lnp._tcp';
340
+
334
341
  const withChromecastInfoPlist = (config, receiverAppId) =>
335
342
  withInfoPlist(config, (cfg) => {
336
343
  const appId = (receiverAppId || 'CC1AD845').toUpperCase();
@@ -344,7 +351,16 @@ const withChromecastInfoPlist = (config, receiverAppId) =>
344
351
  const existingBonjour = Array.isArray(cfg.modResults.NSBonjourServices)
345
352
  ? cfg.modResults.NSBonjourServices
346
353
  : [];
347
- const additions = ['_googlecast._tcp', `_${appId}._googlecast._tcp`];
354
+ // The permission probe browses its own service type. iOS refuses to browse
355
+ // a type that is not declared here, so without this entry the probe can
356
+ // never succeed and every consumer reads `undetermined` forever.
357
+ // Must match `LocalNetworkPermissionProbe.serviceType` in
358
+ // ios/Cast/Core/LocalNetworkPermissionProbe.swift.
359
+ const additions = [
360
+ '_googlecast._tcp',
361
+ `_${appId}._googlecast._tcp`,
362
+ LOCAL_NETWORK_PROBE_SERVICE_TYPE,
363
+ ];
348
364
  for (const s of additions) {
349
365
  if (!existingBonjour.includes(s)) existingBonjour.push(s);
350
366
  }
@@ -26,14 +26,18 @@ import AVFoundation
26
26
  /// standard idiom — no `.m` / `.h` source files introduced.
27
27
  private var queueItemIdKey: UInt8 = 0
28
28
  private var sourceURLKey: UInt8 = 0
29
+ private var routedThroughReadThroughServerKey: UInt8 = 0
29
30
 
31
+ /// `nonisolated` throughout: the SDK marks `AVPlayerItem` main-actor isolated
32
+ /// because `asset` is not Sendable, but these stamps are written once at
33
+ /// construction and read on the player queue.
30
34
  extension AVPlayerItem {
31
35
  /// Lib-internal identity for this AVPlayerItem. Set once at
32
36
  /// construction by `AVQueueBuilder.makeItem`; never mutated
33
37
  /// after. nil only for AVPlayerItems that did not flow through
34
38
  /// the lib's queue construction path (e.g. built directly via
35
39
  /// `makeItemRaw`).
36
- var queueItemId: String? {
40
+ nonisolated var queueItemId: String? {
37
41
  get { objc_getAssociatedObject(self, &queueItemIdKey) as? String }
38
42
  set {
39
43
  objc_setAssociatedObject(
@@ -41,21 +45,31 @@ extension AVPlayerItem {
41
45
  }
42
46
  }
43
47
 
44
- /// The exact `track.url` string this item was built from. Stored
45
- /// verbatim `LookaheadCache` keys entries by raw URL-string
46
- /// equality, so a re-resolution against the cache must use the same
47
- /// string the cache was keyed with, NOT the item's `AVURLAsset.url`
48
- /// (which is already a `file://` path once an item is built from a
49
- /// cached local file, and is a parse/re-serialize round-trip of the
50
- /// original for a remote item). Lets a later attach (crossfade
51
- /// skip / fade-arm) look the track up in the cache and swap a
52
- /// remote-built item for its local cache file once it finishes
53
- /// downloading.
54
- var sourceURL: String? {
48
+ /// The exact `track.url` string this item was built from, stored
49
+ /// verbatim. A routed item's `AVURLAsset.url` is the read-through
50
+ /// server's `127.0.0.1` address and an unrouted one is a
51
+ /// parse/re-serialize round-trip of the original, so anything that
52
+ /// reads a container, codec or track identity out of a URL reads this:
53
+ /// `NowPlayingFormatExtractor`, `AssetReplayGainReader`, the error
54
+ /// `url` field, and the source diagnostic log.
55
+ nonisolated var sourceURL: String? {
55
56
  get { objc_getAssociatedObject(self, &sourceURLKey) as? String }
56
57
  set {
57
58
  objc_setAssociatedObject(
58
59
  self, &sourceURLKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
59
60
  }
60
61
  }
62
+
63
+ /// Whether this item's asset URL is the read-through server's loopback
64
+ /// address rather than `sourceURL` itself. Set once by
65
+ /// `AVQueueBuilder.makeItem`; the routing decision is made at build time
66
+ /// and an item is never re-pointed, so the stamp is what source
67
+ /// classification reads instead of guessing from the server's presence.
68
+ nonisolated var isRoutedThroughReadThroughServer: Bool {
69
+ get { (objc_getAssociatedObject(self, &routedThroughReadThroughServerKey) as? Bool) ?? false }
70
+ set {
71
+ objc_setAssociatedObject(
72
+ self, &routedThroughReadThroughServerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
73
+ }
74
+ }
61
75
  }