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
@@ -0,0 +1,189 @@
1
+ package com.margelo.nitro.queueplayer
2
+
3
+ import android.net.Uri
4
+ import androidx.media3.common.PlaybackException
5
+ import androidx.media3.common.util.UnstableApi
6
+ import androidx.media3.datasource.DataSource
7
+ import androidx.media3.datasource.DataSpec
8
+ import androidx.media3.datasource.HttpDataSource
9
+ import androidx.media3.datasource.TransferListener
10
+ import org.junit.Assert.assertArrayEquals
11
+ import org.junit.Assert.assertEquals
12
+ import org.junit.Assert.assertTrue
13
+ import org.junit.Assert.fail
14
+ import org.junit.Test
15
+ import org.junit.runner.RunWith
16
+ import org.robolectric.RobolectricTestRunner
17
+ import org.robolectric.annotation.Config
18
+
19
+ /**
20
+ * A response that is a document rather than media fails before any of its bytes
21
+ * are returned, so neither cache write path — the prefetcher's `CacheWriter` nor
22
+ * the playback read path's own sink — ever sees them.
23
+ *
24
+ * `android.net.Uri` is a framework type, so this runs under Robolectric like
25
+ * every other test here that parses one.
26
+ */
27
+ @RunWith(RobolectricTestRunner::class)
28
+ @Config(sdk = [34])
29
+ @UnstableApi
30
+ class MediaValidatingDataSourceTest {
31
+
32
+ /** An HTTP source that answers with fixed headers and a fixed body. */
33
+ private class FakeHttpSource(
34
+ private val body: ByteArray,
35
+ private val contentType: String?,
36
+ /** Bytes to hand back per read, so a body arriving in dribs is covered. */
37
+ private val chunk: Int = Int.MAX_VALUE
38
+ ) : HttpDataSource {
39
+ private var position = 0
40
+ var readCount = 0
41
+ private set
42
+
43
+ override fun open(dataSpec: DataSpec): Long {
44
+ position = dataSpec.position.toInt()
45
+ return (body.size - position).toLong()
46
+ }
47
+
48
+ override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
49
+ readCount += 1
50
+ if (position >= body.size) return -1
51
+ val take = minOf(length, chunk, body.size - position)
52
+ body.copyInto(buffer, offset, position, position + take)
53
+ position += take
54
+ return take
55
+ }
56
+
57
+ override fun getUri(): Uri = Uri.parse("https://example.com/track")
58
+ override fun close() {}
59
+ override fun addTransferListener(transferListener: TransferListener) {}
60
+ override fun getResponseHeaders(): Map<String, List<String>> =
61
+ if (contentType == null) emptyMap() else mapOf("Content-Type" to listOf(contentType))
62
+
63
+ override fun setRequestProperty(name: String, value: String) {}
64
+ override fun clearRequestProperty(name: String) {}
65
+ override fun clearAllRequestProperties() {}
66
+ override fun getResponseCode(): Int = 200
67
+ }
68
+
69
+ /** A non-HTTP source, which carries no declared type and is passed through. */
70
+ private class FakeLocalSource(private val body: ByteArray) : DataSource {
71
+ private var position = 0
72
+ override fun open(dataSpec: DataSpec): Long = body.size.toLong()
73
+ override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
74
+ if (position >= body.size) return -1
75
+ val take = minOf(length, body.size - position)
76
+ body.copyInto(buffer, offset, position, position + take)
77
+ position += take
78
+ return take
79
+ }
80
+ override fun getUri(): Uri = Uri.parse("file:///track.mp3")
81
+ override fun close() {}
82
+ override fun addTransferListener(transferListener: TransferListener) {}
83
+ }
84
+
85
+ private val xmlEnvelope =
86
+ """<api-response status="failed"><error code="70"/></api-response>""".toByteArray()
87
+ private val mp3 = byteArrayOf(0x49, 0x44, 0x33, 0x04, 0, 0, 0, 0) + ByteArray(120)
88
+
89
+ private fun openAndRead(
90
+ source: DataSource,
91
+ position: Long = 0L,
92
+ uri: String = "https://example.com/track"
93
+ ): ByteArray {
94
+ source.open(DataSpec.Builder().setUri(uri).setPosition(position).build())
95
+ val out = ArrayList<Byte>()
96
+ val buffer = ByteArray(32)
97
+ while (true) {
98
+ val read = source.read(buffer, 0, buffer.size)
99
+ if (read == -1) break
100
+ for (i in 0 until read) out.add(buffer[i])
101
+ }
102
+ return out.toByteArray()
103
+ }
104
+
105
+ @Test
106
+ fun aDeclaredDocumentFailsBeforeAnythingIsRead() {
107
+ val upstream = FakeHttpSource(xmlEnvelope, "application/xml")
108
+ val source = MediaValidatingDataSource(upstream)
109
+ try {
110
+ source.open(DataSpec(Uri.parse("https://example.com/track")))
111
+ fail("a declared document must not open")
112
+ } catch (e: HttpDataSource.HttpDataSourceException) {
113
+ assertEquals(PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, e.reason)
114
+ }
115
+ assertEquals("nothing may be read from a refused response", 0, upstream.readCount)
116
+ }
117
+
118
+ @Test
119
+ fun anUndeclaredMarkupBodyFailsOnTheFirstRead() {
120
+ val source = MediaValidatingDataSource(FakeHttpSource(xmlEnvelope, null))
121
+ source.open(DataSpec(Uri.parse("https://example.com/track")))
122
+ try {
123
+ source.read(ByteArray(32), 0, 32)
124
+ fail("a markup body must not be returned")
125
+ } catch (e: HttpDataSource.HttpDataSourceException) {
126
+ assertEquals(PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, e.reason)
127
+ }
128
+ }
129
+
130
+ @Test
131
+ fun plainTextWithNoContainerSignatureFailsOnTheFirstRead() {
132
+ val source = MediaValidatingDataSource(FakeHttpSource("not found".toByteArray(), "text/plain"))
133
+ source.open(DataSpec(Uri.parse("https://example.com/track")))
134
+ try {
135
+ source.read(ByteArray(32), 0, 32)
136
+ fail("text with no container signature must not be returned")
137
+ } catch (e: HttpDataSource.HttpDataSourceException) {
138
+ assertEquals(PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, e.reason)
139
+ }
140
+ }
141
+
142
+ @Test
143
+ fun mediaIsPassedThroughWhole() {
144
+ val source = MediaValidatingDataSource(FakeHttpSource(mp3, "audio/mpeg"))
145
+ assertArrayEquals("every byte reaches the caller unchanged", mp3, openAndRead(source))
146
+ }
147
+
148
+ @Test
149
+ fun mediaArrivingInSmallChunksIsStillPassedThroughWhole() {
150
+ val source = MediaValidatingDataSource(FakeHttpSource(mp3, "audio/mpeg", chunk = 3))
151
+ assertArrayEquals(mp3, openAndRead(source))
152
+ }
153
+
154
+ @Test
155
+ fun aRangedRequestIsJudgedOnItsDeclaredTypeOnly() {
156
+ // The served window opens with `<`, so the leading-byte rule would refuse it
157
+ // if it were applied. A body read from an offset says nothing about the
158
+ // resource's first bytes, so it is not.
159
+ val markupAtOffset = "....<api-response/>".toByteArray()
160
+ val ranged = MediaValidatingDataSource(
161
+ FakeHttpSource(markupAtOffset, "application/octet-stream")
162
+ )
163
+ val read = openAndRead(ranged, position = 4L)
164
+ assertTrue("a partial body is not judged by its first byte", read.isNotEmpty())
165
+
166
+ // The same bytes as the whole resource are refused — which is what shows the
167
+ // acceptance above turns on the range and not on the bytes.
168
+ val whole = MediaValidatingDataSource(
169
+ FakeHttpSource("<api-response/>".toByteArray(), "application/octet-stream")
170
+ )
171
+ whole.open(DataSpec(Uri.parse("https://example.com/track")))
172
+ try {
173
+ whole.read(ByteArray(32), 0, 32)
174
+ fail("the same markup read from the start of the resource must be refused")
175
+ } catch (e: HttpDataSource.HttpDataSourceException) {
176
+ assertEquals(PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, e.reason)
177
+ }
178
+ }
179
+
180
+ @Test
181
+ fun aLocalSourceIsPassedThroughUnjudged() {
182
+ val source = MediaValidatingDataSource(FakeLocalSource(xmlEnvelope))
183
+ assertArrayEquals(
184
+ "a file:// source is not fetched from a server and is not the case this guards",
185
+ xmlEnvelope,
186
+ openAndRead(source, uri = "file:///track.mp3")
187
+ )
188
+ }
189
+ }
@@ -4,7 +4,10 @@ import androidx.media3.common.Format
4
4
  import androidx.media3.common.MimeTypes
5
5
  import androidx.media3.common.util.UnstableApi
6
6
  import org.junit.Assert.assertEquals
7
+ import org.junit.Assert.assertNull
8
+ import org.junit.Rule
7
9
  import org.junit.Test
10
+ import org.junit.rules.TemporaryFolder
8
11
 
9
12
  /**
10
13
  * Pure-mapping coverage for [NowPlayingFormatExtractor]. These
@@ -14,6 +17,48 @@ import org.junit.Test
14
17
  @UnstableApi
15
18
  class NowPlayingFormatExtractorTest {
16
19
 
20
+ @get:Rule
21
+ val tempFolder = TemporaryFolder()
22
+
23
+ /**
24
+ * FLAC declares no bitrate, so Media3 reports NO_VALUE for it on local files
25
+ * as well as remote. For a file on disk the average is arithmetic — encoded
26
+ * bytes over playing time — which is the same derivation iOS uses.
27
+ */
28
+ @Test
29
+ fun averageBitrateIsBytesOverPlayingTime() {
30
+ val file = tempFolder.newFile("track.flac")
31
+ file.writeBytes(ByteArray(1_000_000))
32
+
33
+ // 1,000,000 bytes over 40 s = 8,000,000 bits / 40 = 200 kbps.
34
+ assertEquals(
35
+ 200_000.0,
36
+ NowPlayingFormatExtractor.averageBitrateOf(file.absolutePath, 40.0)!!,
37
+ 0.001
38
+ )
39
+ }
40
+
41
+ /**
42
+ * Every input that would make the answer invented rather than measured
43
+ * yields null: the player has not resolved a duration yet, the duration is
44
+ * nonsense, the path is absent, or the file has no bytes.
45
+ */
46
+ @Test
47
+ fun averageBitrateIsNullWhenItCannotBeMeasured() {
48
+ val file = tempFolder.newFile("track.flac")
49
+ file.writeBytes(ByteArray(1_000))
50
+
51
+ assertNull(NowPlayingFormatExtractor.averageBitrateOf(file.absolutePath, null))
52
+ assertNull(NowPlayingFormatExtractor.averageBitrateOf(file.absolutePath, 0.0))
53
+ assertNull(NowPlayingFormatExtractor.averageBitrateOf(file.absolutePath, -5.0))
54
+ assertNull(NowPlayingFormatExtractor.averageBitrateOf(null, 40.0))
55
+ assertNull(
56
+ NowPlayingFormatExtractor.averageBitrateOf(
57
+ tempFolder.newFile("empty.flac").absolutePath, 40.0))
58
+ assertNull(
59
+ NowPlayingFormatExtractor.averageBitrateOf("/nonexistent/nope.flac", 40.0))
60
+ }
61
+
17
62
  @Test
18
63
  fun mapCodecCoversAACAliases() {
19
64
  assertEquals(AudioCodec.AAC, NowPlayingFormatExtractor.mapCodec(MimeTypes.AUDIO_AAC))
@@ -60,7 +60,7 @@ class PitchAwareAudioProcessorChainTest {
60
60
 
61
61
  @Test
62
62
  fun `signalsmith stays active on the float path even when not stretching`() {
63
- // The fix for Media3's static pipeline: Signalsmith must remain in the
63
+ // Media3's pipeline is static: Signalsmith must remain in the
64
64
  // active set regardless of mode/speed so it can stretch when music kicks in.
65
65
  val chain = newConfiguredChain()
66
66
  chain.setMode(PitchCorrectionMode.VOICE)
@@ -7,8 +7,16 @@ import androidx.media3.session.LibraryResult
7
7
  import androidx.media3.session.MediaLibraryService
8
8
  import androidx.media3.session.MediaSession
9
9
  import androidx.test.core.app.ApplicationProvider
10
+ import java.util.concurrent.CountDownLatch
11
+ import java.util.concurrent.TimeUnit
12
+ import java.util.concurrent.atomic.AtomicInteger
13
+ import kotlinx.coroutines.Dispatchers
14
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
15
+ import kotlinx.coroutines.test.resetMain
16
+ import kotlinx.coroutines.test.setMain
10
17
  import org.junit.After
11
18
  import org.junit.Assert.assertEquals
19
+ import org.junit.Assert.assertFalse
12
20
  import org.junit.Assert.assertNull
13
21
  import org.junit.Assert.assertTrue
14
22
  import org.junit.Before
@@ -167,4 +175,43 @@ class PlaybackServiceCallbackSearchTest {
167
175
  result.params
168
176
  )
169
177
  }
178
+
179
+ // --- dispatchSearchToJs
180
+
181
+ // `Dispatchers.setMain` is the only way to see which dispatcher the notify
182
+ // lands on; `notifySearchResultChanged` has no other observable effect with
183
+ // no controller connected.
184
+ @OptIn(ExperimentalCoroutinesApi::class)
185
+ @Test
186
+ fun `dispatchSearchToJs caches the answer then notifies on main`() {
187
+ // A Nitro Promise is JNI-constructed and cannot exist on the JVM, so
188
+ // the only JS answer reachable here is a throwing handler; that path
189
+ // still writes the cache and still notifies, which is the contract a
190
+ // connected controller depends on to stop waiting.
191
+ val mainDispatches = AtomicInteger(0)
192
+ val dispatched = CountDownLatch(1)
193
+ val recording = object : kotlinx.coroutines.CoroutineDispatcher() {
194
+ override fun dispatch(context: kotlin.coroutines.CoroutineContext, block: Runnable) {
195
+ mainDispatches.incrementAndGet()
196
+ dispatched.countDown()
197
+ android.os.Handler(android.os.Looper.getMainLooper()).post(block)
198
+ }
199
+ }
200
+ Dispatchers.setMain(recording)
201
+ try {
202
+ service.browseCallbacks.registerOnSearchRequest { _ ->
203
+ throw IllegalStateException("no Nitro runtime under Robolectric")
204
+ }
205
+ assertFalse(service.searchResultCache.hasResults("rock"))
206
+
207
+ service.dispatchSearchToJs("rock")
208
+
209
+ assertTrue("the notify reached a dispatcher", dispatched.await(2, TimeUnit.SECONDS))
210
+ org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle()
211
+ assertEquals("notify is dispatched to main", 1, mainDispatches.get())
212
+ assertEquals(emptyList<BrowseItem>(), service.searchResultCache.getResults("rock"))
213
+ } finally {
214
+ Dispatchers.resetMain()
215
+ }
216
+ }
170
217
  }
@@ -259,19 +259,6 @@ class PlaybackServiceCallbackTest {
259
259
  assertTrue(commands.contains(Player.COMMAND_SET_SPEED_AND_PITCH))
260
260
  }
261
261
 
262
- @Test
263
- fun `refreshAvailableCommands is a no-op when no handler is registered`() {
264
- // No handler, no controllers — exercising the early return.
265
- binder.refreshAvailableCommands()
266
- // Re-issue with a handler but no connected controllers — also a no-op.
267
- binder.setCommandHandler(RecordingHandler())
268
- binder.refreshAvailableCommands()
269
- // The per-controller iteration body (session.setAvailableCommands)
270
- // requires a live MediaController that has completed handshake,
271
- // which is not reachable from Robolectric without standing up a
272
- // full MediaController.Builder.buildAsync() flow.
273
- }
274
-
275
262
  // --- TrackPlayer-side handler implementation
276
263
 
277
264
  @Test
@@ -291,8 +278,8 @@ class PlaybackServiceCallbackTest {
291
278
  val player = newConfiguredPlayer()
292
279
  try {
293
280
  player.tracks = listOf(
294
- TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
295
- TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
281
+ TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
282
+ TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
296
283
  )
297
284
  player.currentTrackIndex = 0
298
285
 
@@ -326,8 +313,8 @@ class PlaybackServiceCallbackTest {
326
313
 
327
314
  // Two-track queue at index 0 — skip-next now allowed.
328
315
  player.tracks = listOf(
329
- TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
330
- TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
316
+ TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
317
+ TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
331
318
  )
332
319
  player.currentTrackIndex = 0
333
320
  player.recomputeCapabilities()
@@ -352,8 +339,8 @@ class PlaybackServiceCallbackTest {
352
339
  val player = newConfiguredPlayer()
353
340
  try {
354
341
  player.tracks = listOf(
355
- TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
356
- TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
342
+ TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
343
+ TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
357
344
  )
358
345
  player.currentTrackIndex = 1
359
346
 
@@ -377,9 +364,9 @@ class PlaybackServiceCallbackTest {
377
364
  val player = newConfiguredPlayer()
378
365
  try {
379
366
  player.tracks = listOf(
380
- TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
381
- TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
382
- TrackItem("c", "https://example.com/c.m4a", null, null, null, null, null, null),
367
+ TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
368
+ TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
369
+ TrackItem("c", "https://example.com/c.m4a", null, null, null, null, null, null, null, null, null, null),
383
370
  )
384
371
  player.currentTrackIndex = 0
385
372
 
@@ -404,8 +391,8 @@ class PlaybackServiceCallbackTest {
404
391
  val player = newConfiguredPlayer()
405
392
  try {
406
393
  player.tracks = listOf(
407
- TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
408
- TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
394
+ TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
395
+ TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
409
396
  )
410
397
  player.currentTrackIndex = 1
411
398
 
@@ -426,8 +413,8 @@ class PlaybackServiceCallbackTest {
426
413
  val player = newConfiguredPlayer()
427
414
  try {
428
415
  player.tracks = listOf(
429
- TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
430
- TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
416
+ TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
417
+ TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
431
418
  )
432
419
  player.currentTrackIndex = 1
433
420
 
@@ -444,6 +431,35 @@ class PlaybackServiceCallbackTest {
444
431
 
445
432
  // --- Voice / browse-leaf routing through onAddMediaItems / onSetMediaItems
446
433
 
434
+ @Test
435
+ fun `onAddMediaItems asks for append and onSetMediaItems does not`() {
436
+ val fakeEnv = RecordingEnvironment(service)
437
+ val routingCallback = PlaybackServiceCallback(fakeEnv)
438
+ fakeEnv.idResolution = listOf(
439
+ MediaItem.Builder().setMediaId("resolved")
440
+ .setUri("https://example.com/resolved.m4a".toUri()).build()
441
+ )
442
+ fun idOnlyItem() = MediaItem.Builder().setMediaId("album:1").build()
443
+
444
+ routingCallback.onAddMediaItems(
445
+ session, controllerInfo, mutableListOf(idOnlyItem())
446
+ ).get()
447
+ // "Queue up X" grows the queue; Media3 appends what the resolution
448
+ // returns, so the lib's own queue has to grow by the same amount rather
449
+ // than being replaced by the new tracks alone.
450
+ assertEquals(listOf(true), fakeEnv.idAppendFlags)
451
+
452
+ fakeEnv.idAppendFlags.clear()
453
+ routingCallback.onSetMediaItems(
454
+ session, controllerInfo, mutableListOf(idOnlyItem()), 0, 0L
455
+ ).get()
456
+ // A browse-leaf tap is a genuine replace: it takes the no-sync route,
457
+ // where the JS handler's own `setQueue` is the only writer, so no
458
+ // append-flagged request reaches the environment at all.
459
+ assertEquals(listOf("album:1"), fakeEnv.idRequestsNoSync)
460
+ assertEquals(emptyList<Boolean>(), fakeEnv.idAppendFlags)
461
+ }
462
+
447
463
  @Test
448
464
  fun `onAddMediaItems with searchQuery dispatches to onPlayFromSearchRequest`() {
449
465
  val fakeEnv = RecordingEnvironment(service)
@@ -783,7 +799,8 @@ class PlaybackServiceCallbackTest {
783
799
  networkTimeoutMs = null,
784
800
  audioContentType = null,
785
801
  audioCategoryOptions = null,
786
- visualizationEnabled = null,
802
+ voiceVocabularyDonationEnabled = null,
803
+ visualizationEnabled = null,
787
804
  clampSeekToBuffered = null,
788
805
  lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null,
789
806
  ),
@@ -878,19 +895,26 @@ class PlaybackServiceCallbackTest {
878
895
  service.playerCommandsFor(capability)
879
896
 
880
897
  override fun onPlayFromSearchRequest(
881
- request: MediaSearchRequest
898
+ request: MediaSearchRequest,
899
+ append: Boolean
882
900
  ): ListenableFuture<List<MediaItem>> {
883
901
  searchRequests += request
902
+ searchAppendFlags += append
884
903
  return Futures.immediateFuture(searchResolution)
885
904
  }
886
905
 
887
906
  override fun onPlayFromIdRequest(
888
- mediaId: String
907
+ mediaId: String,
908
+ append: Boolean
889
909
  ): ListenableFuture<List<MediaItem>> {
890
910
  idRequests += mediaId
911
+ idAppendFlags += append
891
912
  return Futures.immediateFuture(idResolution)
892
913
  }
893
914
 
915
+ val searchAppendFlags = mutableListOf<Boolean>()
916
+ val idAppendFlags = mutableListOf<Boolean>()
917
+
894
918
  override fun onPlayFromIdRequestNoSync(mediaId: String) {
895
919
  idRequestsNoSync += mediaId
896
920
  }
@@ -49,19 +49,19 @@ class PlaybackServiceLifecycleTest {
49
49
  val controller = Robolectric.buildService(PlaybackService::class.java).create()
50
50
  try {
51
51
  val binder = bindLocal(controller.get())
52
- assertNotNull("binder.player is initialised in onCreate", binder.player)
52
+ assertNotNull("the engine's player is initialised in onCreate", binder.engine.mediaSessionPlayer)
53
53
  assertNotNull("binder.session is initialised in onCreate", binder.session)
54
54
  // The session binds to the stable SessionCommandForwardingPlayer
55
55
  // wrapper for its lifetime; the wrapper forwards to the engine's
56
- // ExoPlayer the binder exposes.
56
+ // own ExoPlayer.
57
57
  val sessionPlayer = binder.session.player
58
58
  assertTrue(
59
59
  "binder.session.player is the stable forwarding wrapper",
60
60
  sessionPlayer is SessionCommandForwardingPlayer,
61
61
  )
62
62
  assertSame(
63
- "the wrapper forwards to the ExoPlayer instance the binder exposes",
64
- binder.player,
63
+ "the wrapper forwards to the engine's ExoPlayer instance",
64
+ binder.engine.mediaSessionPlayer,
65
65
  (sessionPlayer as SessionCommandForwardingPlayer).wrappedPlayer,
66
66
  )
67
67
  assertEquals(
@@ -173,8 +173,8 @@ class PlaybackServiceLifecycleTest {
173
173
  val binder = bindLocal(service)
174
174
  // A loaded + play-ready session: the previous behaviour kept the service
175
175
  // alive here. Swipe-away is now an unconditional kill.
176
- binder.player.addMediaItem(MediaItem.fromUri("http://example.com/a.mp3"))
177
- binder.player.playWhenReady = true
176
+ binder.engine.mediaSessionPlayer.addMediaItem(MediaItem.fromUri("http://example.com/a.mp3"))
177
+ binder.engine.mediaSessionPlayer.playWhenReady = true
178
178
  shadowOf(Looper.getMainLooper()).idle()
179
179
 
180
180
  service.onTaskRemoved(null)
@@ -183,7 +183,7 @@ class PlaybackServiceLifecycleTest {
183
183
  assertEquals(
184
184
  "the player is stopped (idle) on swipe-away",
185
185
  Player.STATE_IDLE,
186
- binder.player.playbackState,
186
+ binder.engine.mediaSessionPlayer.playbackState,
187
187
  )
188
188
  assertTrue(
189
189
  "the service stops even with a queued, play-ready session",
@@ -198,7 +198,7 @@ class PlaybackServiceLifecycleTest {
198
198
  fun `onDestroy releases the player and clears the session`() {
199
199
  val controller = Robolectric.buildService(PlaybackService::class.java).create()
200
200
  val binder = bindLocal(controller.get())
201
- val player = binder.player
201
+ val player = binder.engine.mediaSessionPlayer
202
202
  val session: MediaSession = binder.session
203
203
  assertFalse("player is live before destroy", player.isReleased)
204
204
 
@@ -134,7 +134,7 @@ class QueueSkipArithmeticTest {
134
134
  @Test
135
135
  fun `computePrevious OFF — at index 0 returns null (no wrap)`() {
136
136
  // The "tap Previous at track 0 restarts current" UX behaviour
137
- // is applied by the skipToPrevious glue, not here.
137
+ // is applied by `skipToPrevious`, not here.
138
138
  assertNull(
139
139
  QueueSkipArithmetic.computePrevious(5, 0, QueueSkipRepeatMode.OFF)
140
140
  )
@@ -443,4 +443,36 @@ class QueueSkipArithmeticTest {
443
443
  assertFalse(QueueSkipArithmetic.canSkipPrevious(3, 0, QueueSkipRepeatMode.OFF, restartEnabled = false))
444
444
  assertTrue(QueueSkipArithmetic.canSkipPrevious(3, 1, QueueSkipRepeatMode.OFF, restartEnabled = false))
445
445
  }
446
+
447
+ @Test
448
+ fun `canSkipPrevious restartEnabled — nothing playing is false in any mode`() {
449
+ // Nothing is loaded, so there is no track to restart and no previous
450
+ // track to reach: resolveSkipToPrevious returns NoOp for this state.
451
+ for (mode in QueueSkipRepeatMode.entries) {
452
+ assertFalse(QueueSkipArithmetic.canSkipPrevious(5, -1, mode, restartEnabled = true))
453
+ }
454
+ }
455
+
456
+ @Test
457
+ fun `canSkipPrevious agrees with resolveSkipToPrevious`() {
458
+ // canSkipPrevious is the enabled-state of the same decision
459
+ // resolveSkipToPrevious makes; the two must never disagree.
460
+ for (count in listOf(0, 1, 3)) {
461
+ for (index in listOf(-1, 0, 1, 2)) {
462
+ for (mode in QueueSkipRepeatMode.entries) {
463
+ for (restart in listOf(false, true)) {
464
+ val can = QueueSkipArithmetic.canSkipPrevious(count, index, mode, restart)
465
+ val resolved = QueueSkipArithmetic.resolveSkipToPrevious(
466
+ count, index, mode, restart, positionMs = 0
467
+ )
468
+ assertEquals(
469
+ "count=$count index=$index mode=$mode restart=$restart",
470
+ can,
471
+ resolved != QueueSkipArithmetic.SkipToPreviousAction.NoOp
472
+ )
473
+ }
474
+ }
475
+ }
476
+ }
477
+ }
446
478
  }