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
@@ -17,7 +17,6 @@ import android.os.IBinder
17
17
  import android.os.Looper
18
18
  import androidx.core.content.ContextCompat
19
19
  import androidx.annotation.VisibleForTesting
20
- import androidx.media3.common.C
21
20
  import androidx.media3.common.MediaItem
22
21
  import androidx.media3.common.Player
23
22
  import androidx.media3.common.util.UnstableApi
@@ -26,25 +25,17 @@ import androidx.media3.exoplayer.ExoPlayer
26
25
  import androidx.media3.exoplayer.source.MediaSource
27
26
  import androidx.media3.session.CacheBitmapLoader
28
27
  import androidx.media3.session.CommandButton
29
- import androidx.media3.session.LibraryResult
30
28
  import androidx.media3.session.MediaLibraryService
31
- import androidx.media3.session.MediaLibraryService.LibraryParams
32
29
  import androidx.media3.session.MediaLibraryService.MediaLibrarySession
33
30
  import androidx.media3.session.MediaSession
34
- import androidx.media3.session.SessionResult
35
- import com.google.common.collect.ImmutableList
36
- import com.google.common.util.concurrent.Futures
37
31
  import com.google.common.util.concurrent.ListenableFuture
38
32
  import com.google.common.util.concurrent.MoreExecutors
39
33
  import com.google.common.util.concurrent.SettableFuture
40
34
  import com.margelo.nitro.core.Promise
41
35
  import com.margelo.nitro.queueplayer.cast.PlaybackStateRouter
42
36
  import java.util.UUID
37
+ import kotlinx.coroutines.Dispatchers
43
38
  import kotlinx.coroutines.cancel
44
- import kotlinx.coroutines.currentCoroutineContext
45
- import kotlinx.coroutines.delay
46
- import kotlinx.coroutines.flow.collect
47
- import kotlinx.coroutines.isActive
48
39
  import kotlinx.coroutines.launch
49
40
  import kotlinx.coroutines.withContext
50
41
 
@@ -147,7 +138,7 @@ class PlaybackService : HeadlessJsMediaService() {
147
138
  * resolve — covers (a) the JS bridge spinning up + the consumer's
148
139
  * `registerPlaybackService` running, AND (b) the consumer's
149
140
  * resolution Promise running. 20 s mirrors the iOS
150
- * [PendingIntentBuffer] TTL: ~3 s for bridge boot + ~17 s of
141
+ * `PendingIntentBuffer` TTL: ~3 s for bridge boot + ~17 s of
151
142
  * headroom for the consumer's network calls before the controller
152
143
  * surfaces "no results" and the user retries. The two platforms'
153
144
  * cold-start windows are deliberately kept the same so the
@@ -260,8 +251,8 @@ class PlaybackService : HeadlessJsMediaService() {
260
251
 
261
252
  /**
262
253
  * Per-query cache of JS-supplied search results. Populated by
263
- * `TrackPlayer.setSearchResults` once the JS handler
264
- * responds to a search request; read by
254
+ * [dispatchSearchToJs] once the JS `onSearchRequest` handler
255
+ * responds; read by
265
256
  * [PlaybackServiceCallback.onGetSearchResult]. Empty by default —
266
257
  * a controller's `getSearchResult` call before the JS responder
267
258
  * has answered returns an empty page.
@@ -315,7 +306,7 @@ class PlaybackService : HeadlessJsMediaService() {
315
306
  // Cold-wake the JS bridge so the consumer's
316
307
  // `registerPlaybackService` can fire its callbacks before
317
308
  // Media3's framework dispatches `onAddMediaItems`. Idempotent
318
- // — `HeadlessJsMediaService.initialized` latches the JS task
309
+ // — `HeadlessJsMediaService.initialized` is set by the JS task
319
310
  // spawn so re-entrant connects do not double-spawn.
320
311
  this@PlaybackService.selfWakeForHeadlessClient(packageName)
321
312
  }
@@ -341,14 +332,16 @@ class PlaybackService : HeadlessJsMediaService() {
341
332
  }
342
333
 
343
334
  override fun onPlayFromSearchRequest(
344
- request: MediaSearchRequest
335
+ request: MediaSearchRequest,
336
+ append: Boolean
345
337
  ): ListenableFuture<List<MediaItem>> =
346
- this@PlaybackService.dispatchPlayFromSearchToJs(request)
338
+ this@PlaybackService.dispatchPlayFromSearchToJs(request, append)
347
339
 
348
340
  override fun onPlayFromIdRequest(
349
- mediaId: String
341
+ mediaId: String,
342
+ append: Boolean
350
343
  ): ListenableFuture<List<MediaItem>> =
351
- this@PlaybackService.dispatchPlayFromIdToJs(mediaId)
344
+ this@PlaybackService.dispatchPlayFromIdToJs(mediaId, append)
352
345
 
353
346
  override fun onPlayFromIdRequestNoSync(mediaId: String) {
354
347
  this@PlaybackService.dispatchPlayFromIdToJsFireOnly(mediaId)
@@ -425,14 +418,6 @@ class PlaybackService : HeadlessJsMediaService() {
425
418
  */
426
419
  val instanceId: java.util.UUID = java.util.UUID.randomUUID()
427
420
 
428
- /**
429
- * Direct access to the underlying ExoPlayer. Retained as a
430
- * back-compat shim for [TrackPlayer] until its transport /
431
- * queue calls migrate onto the [engine] surface; engine-aware
432
- * code paths use [engine] instead.
433
- */
434
- val player: ExoPlayer get() = exoPlayer
435
-
436
421
  /**
437
422
  * Strategy-pattern engine wrapping the active player(s).
438
423
  * Routes through the abstract [PlaybackEngine] surface so
@@ -464,7 +449,7 @@ class PlaybackService : HeadlessJsMediaService() {
464
449
  * AVRCP, Android Auto, and Wear surfaces grey out skip buttons
465
450
  * that the queue boundary makes invalid. No-op when no handler
466
451
  * is registered (no authoritative capability source) or when no
467
- * controllers have connected yet (next [PlaybackCallback.onConnect]
452
+ * controllers have connected yet (next [PlaybackServiceCallback.onConnect]
468
453
  * picks up the fresh capability).
469
454
  */
470
455
  internal fun refreshAvailableCommands() {
@@ -504,9 +489,12 @@ class PlaybackService : HeadlessJsMediaService() {
504
489
  private val localBinder by lazy { LocalBinder() }
505
490
 
506
491
  /**
507
- * Currently-registered handler consulted by [PlaybackCallback]
508
- * before the default routing runs. `null` until
509
- * [TrackPlayer.configureInternal] wires itself in; under that
492
+ * Currently-registered handler consulted by [SessionCommandForwardingPlayer]
493
+ * before the default routing runs, by [MediaButtonDispatch] for raw
494
+ * media-button intents, by the audio-focus listener, and by
495
+ * [PlaybackServiceCallback.onConnect] for the initial player commands.
496
+ * `null` until
497
+ * [TrackPlayer.configureInternal] registers one; under that
510
498
  * state every callback falls through to the Media3 default and
511
499
  * raw media-button intents fall through to the framework.
512
500
  */
@@ -640,7 +628,7 @@ class PlaybackService : HeadlessJsMediaService() {
640
628
  * PcmStream (AirPlay) session is not a RemotePlayer.
641
629
  */
642
630
  private val castBindScope = kotlinx.coroutines.CoroutineScope(
643
- kotlinx.coroutines.SupervisorJob() + kotlinx.coroutines.Dispatchers.Main.immediate
631
+ kotlinx.coroutines.SupervisorJob() + Dispatchers.Main.immediate
644
632
  )
645
633
 
646
634
  override fun onCreate() {
@@ -925,8 +913,8 @@ class PlaybackService : HeadlessJsMediaService() {
925
913
  * the new leading player, swap `MediaSession` to the new
926
914
  * player, carry the delegate over, re-wire the Equalizer, and
927
915
  * release the old engine. Queue migration is the caller's
928
- * responsibility — `TrackPlayer` re-installs the canonical
929
- * queue model onto the new engine before the swap if needed.
916
+ * responsibility — `TrackPlayer` seeds the new engine through
917
+ * `setMediaItems` after this returns.
930
918
  */
931
919
  fun swapEngine(newEngine: PlaybackEngine): PlaybackEngine {
932
920
  return swapEngineInternal(newEngine)
@@ -1080,6 +1068,16 @@ class PlaybackService : HeadlessJsMediaService() {
1080
1068
  * complete kill. A future multi-session change would need to stop each.
1081
1069
  */
1082
1070
  override fun onTaskRemoved(rootIntent: Intent?) {
1071
+ // Stop the engine, not the session's player. Under crossfade the session
1072
+ // is bound to a forwarding wrapper that addresses one leg, so stopping it
1073
+ // leaves the other leg running: the task is swiped away, the notification
1074
+ // and session go, and audio keeps playing with nothing left to control
1075
+ // it. The engine stops both legs and abandons audio focus.
1076
+ engine.stop()
1077
+ // And the session's player. While cast-bound that is the receiver, which
1078
+ // the engine does not own — stopping only the legs leaves the Chromecast
1079
+ // playing after the task, notification and session are all gone. Off cast
1080
+ // it is the forwarding wrapper around a leg the engine just stopped.
1083
1081
  mediaSession?.player?.stop()
1084
1082
  stopForeground(Service.STOP_FOREGROUND_REMOVE)
1085
1083
  stopSelf()
@@ -1129,7 +1127,7 @@ class PlaybackService : HeadlessJsMediaService() {
1129
1127
  */
1130
1128
  private val callbackDispatchScope =
1131
1129
  kotlinx.coroutines.CoroutineScope(
1132
- kotlinx.coroutines.SupervisorJob() + kotlinx.coroutines.Dispatchers.IO
1130
+ kotlinx.coroutines.SupervisorJob() + Dispatchers.IO
1133
1131
  )
1134
1132
 
1135
1133
  /**
@@ -1152,6 +1150,9 @@ class PlaybackService : HeadlessJsMediaService() {
1152
1150
  kotlinx.coroutines.withTimeoutOrNull(SEARCH_DISPATCH_TIMEOUT_MS) {
1153
1151
  callback(query).await().await().toList()
1154
1152
  } ?: emptyList()
1153
+ } catch (t: kotlinx.coroutines.CancellationException) {
1154
+ // The service is being destroyed; nothing below may run.
1155
+ throw t
1155
1156
  } catch (t: Throwable) {
1156
1157
  // JS handler threw — surface as an empty answered result so
1157
1158
  // the controller stops waiting. The thrown error is the
@@ -1165,7 +1166,11 @@ class PlaybackService : HeadlessJsMediaService() {
1165
1166
  emptyList()
1166
1167
  }
1167
1168
  searchResultCache.setResults(query, items)
1168
- notifySearchResultChanged(query, items.size)
1169
+ // `mediaSession` is owned by main and nulled by onDestroy there; the
1170
+ // notify has to read it on the same thread or it races the release.
1171
+ withContext(Dispatchers.Main) {
1172
+ notifySearchResultChanged(query, items.size)
1173
+ }
1169
1174
  }
1170
1175
  }
1171
1176
 
@@ -1185,9 +1190,11 @@ class PlaybackService : HeadlessJsMediaService() {
1185
1190
  * retries).
1186
1191
  */
1187
1192
  internal fun dispatchPlayFromSearchToJs(
1188
- request: MediaSearchRequest
1193
+ request: MediaSearchRequest,
1194
+ append: Boolean = false
1189
1195
  ): ListenableFuture<List<MediaItem>> = dispatchAssistantPlay(
1190
1196
  methodLabel = "onPlayFromSearchRequest",
1197
+ append = append,
1191
1198
  awaitSlot = { browseCallbacks.awaitPlayFromSearchRequestCallback() }
1192
1199
  ) { browseCallbacks.playFromSearchRequestCallback()?.invoke(request) }
1193
1200
 
@@ -1198,9 +1205,11 @@ class PlaybackService : HeadlessJsMediaService() {
1198
1205
  * [dispatchPlayFromSearchToJs].
1199
1206
  */
1200
1207
  internal fun dispatchPlayFromIdToJs(
1201
- mediaId: String
1208
+ mediaId: String,
1209
+ append: Boolean = false
1202
1210
  ): ListenableFuture<List<MediaItem>> = dispatchAssistantPlay(
1203
1211
  methodLabel = "onPlayFromIdRequest",
1212
+ append = append,
1204
1213
  awaitSlot = { browseCallbacks.awaitPlayFromIdRequestCallback() }
1205
1214
  ) { browseCallbacks.playFromIdRequestCallback()?.invoke(mediaId) }
1206
1215
 
@@ -1233,6 +1242,9 @@ class PlaybackService : HeadlessJsMediaService() {
1233
1242
  }
1234
1243
  promise?.await()?.await()
1235
1244
  }
1245
+ } catch (t: kotlinx.coroutines.CancellationException) {
1246
+ // The service is being destroyed; nothing below may run.
1247
+ throw t
1236
1248
  } catch (t: Throwable) {
1237
1249
  android.util.Log.w(
1238
1250
  LOG_TAG,
@@ -1263,6 +1275,9 @@ class PlaybackService : HeadlessJsMediaService() {
1263
1275
  }
1264
1276
  promise?.await()?.await()?.toList() ?: emptyList()
1265
1277
  } ?: emptyList()
1278
+ } catch (t: kotlinx.coroutines.CancellationException) {
1279
+ // The service is being destroyed; nothing below may run.
1280
+ throw t
1266
1281
  } catch (t: Throwable) {
1267
1282
  android.util.Log.w(
1268
1283
  LOG_TAG,
@@ -1286,6 +1301,7 @@ class PlaybackService : HeadlessJsMediaService() {
1286
1301
 
1287
1302
  private inline fun dispatchAssistantPlay(
1288
1303
  methodLabel: String,
1304
+ append: Boolean,
1289
1305
  crossinline awaitSlot: suspend () -> Unit,
1290
1306
  crossinline invoke: () -> Promise<Promise<Array<TrackItem>>>?
1291
1307
  ): ListenableFuture<List<MediaItem>> {
@@ -1303,6 +1319,9 @@ class PlaybackService : HeadlessJsMediaService() {
1303
1319
  }
1304
1320
  promise?.await()?.await()?.toList() ?: emptyList()
1305
1321
  } ?: emptyList()
1322
+ } catch (t: kotlinx.coroutines.CancellationException) {
1323
+ // The service is being destroyed; nothing below may run.
1324
+ throw t
1306
1325
  } catch (t: Throwable) {
1307
1326
  android.util.Log.w(
1308
1327
  LOG_TAG,
@@ -1316,7 +1335,7 @@ class PlaybackService : HeadlessJsMediaService() {
1316
1335
  // with whatever this future resolves to, so we MUST return the
1317
1336
  // real items — returning emptyList here would wipe the queue
1318
1337
  // immediately after Media3 installs it.
1319
- val items = applyAssistantQueueOnMain(tracks)
1338
+ val items = applyAssistantQueueOnMain(tracks, append)
1320
1339
  future.set(items)
1321
1340
  }
1322
1341
  // Cancel the launched dispatch when the controller drops the
@@ -1355,26 +1374,27 @@ class PlaybackService : HeadlessJsMediaService() {
1355
1374
  )
1356
1375
  return@launch
1357
1376
  }
1358
- kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) {
1377
+ withContext(Dispatchers.Main) {
1359
1378
  callback()
1360
1379
  }
1361
1380
  }
1362
1381
  }
1363
1382
 
1364
1383
  private suspend fun applyAssistantQueueOnMain(
1365
- tracks: List<TrackItem>
1384
+ tracks: List<TrackItem>,
1385
+ append: Boolean
1366
1386
  ): List<MediaItem> {
1367
1387
  if (tracks.isEmpty()) return emptyList()
1368
1388
  val player = TrackPlayer.current ?: return emptyList()
1369
- return kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) {
1370
- player.applyResolvedAssistantQueue(tracks)
1389
+ return withContext(Dispatchers.Main) {
1390
+ player.applyResolvedAssistantQueue(tracks, append)
1371
1391
  }
1372
1392
  }
1373
1393
 
1374
1394
  /**
1375
1395
  * Boot the JS task IF a headless-eligible controller has connected
1376
1396
  * AND no JS task is already running. Idempotent — the
1377
- * `HeadlessJsMediaService.initialized` latch + the wake-intent
1397
+ * `HeadlessJsMediaService.initialized` flag + the wake-intent
1378
1398
  * action gate prevent re-entrancy.
1379
1399
  */
1380
1400
  internal fun selfWakeForHeadlessClient(packageName: String) {
@@ -1593,6 +1613,8 @@ class PlaybackService : HeadlessJsMediaService() {
1593
1613
  /**
1594
1614
  * Fire `notifySearchResultChanged` for [query] across every
1595
1615
  * connected controller after [searchResultCache] is populated.
1616
+ * Caller is on main; the session's own thread-safety contract
1617
+ * handles the controller iteration.
1596
1618
  */
1597
1619
  private fun notifySearchResultChanged(query: String, count: Int) {
1598
1620
  val session = mediaSession ?: return
@@ -1626,11 +1648,11 @@ class PlaybackService : HeadlessJsMediaService() {
1626
1648
  }
1627
1649
 
1628
1650
  /**
1629
- * Hook the [PlaybackCallback] consults before letting Media3 route
1651
+ * Interface [SessionCommandForwardingPlayer] consults before letting Media3 route
1630
1652
  * a controller-initiated command to the bound player. Calls run on
1631
1653
  * the service main looper. Skip-next / skip-previous / seek-to-queue-item
1632
1654
  * return true when the implementation has handled the command itself,
1633
- * so the default routing is suppressed; the transport hook fires for
1655
+ * so the default routing is suppressed; `onTransportCommand` fires for
1634
1656
  * play / pause / in-item seek / setPlaybackSpeed and is purely a
1635
1657
  * side-channel notification (no return value).
1636
1658
  */
@@ -1654,7 +1676,7 @@ class PlaybackService : HeadlessJsMediaService() {
1654
1676
  /**
1655
1677
  * Returns the lib's current authoritative skip capability —
1656
1678
  * driven by repeat-mode + queue position + queue size on the
1657
- * `TrackPlayer` side. Read by [PlaybackCallback.onConnect] and
1679
+ * `TrackPlayer` side. Read by [PlaybackServiceCallback.onConnect] and
1658
1680
  * [LocalBinder.refreshAvailableCommands] so the available
1659
1681
  * `Player.Commands` reflect what skip arithmetic actually allows.
1660
1682
  */
@@ -91,7 +91,8 @@ internal class PlaybackServiceCallback(
91
91
  * registered, the controller's "play X" voice command surfaces
92
92
  * as "no results". */
93
93
  fun onPlayFromSearchRequest(
94
- request: MediaSearchRequest
94
+ request: MediaSearchRequest,
95
+ append: Boolean = false
95
96
  ): ListenableFuture<List<MediaItem>> =
96
97
  Futures.immediateFuture(emptyList())
97
98
 
@@ -101,7 +102,8 @@ internal class PlaybackServiceCallback(
101
102
  *
102
103
  * Default returns an empty list. */
103
104
  fun onPlayFromIdRequest(
104
- mediaId: String
105
+ mediaId: String,
106
+ append: Boolean = false
105
107
  ): ListenableFuture<List<MediaItem>> =
106
108
  Futures.immediateFuture(emptyList())
107
109
 
@@ -194,7 +196,7 @@ internal class PlaybackServiceCallback(
194
196
  requestMetadata.extras
195
197
  )
196
198
  return Futures.transform(
197
- environment.onPlayFromSearchRequest(request),
199
+ environment.onPlayFromSearchRequest(request, append = true),
198
200
  { items -> items.toMutableList() },
199
201
  MoreExecutors.directExecutor()
200
202
  )
@@ -202,7 +204,7 @@ internal class PlaybackServiceCallback(
202
204
  val mediaId = unresolved.mediaId.takeIf { it.isNotEmpty() }
203
205
  if (mediaId != null) {
204
206
  return Futures.transform(
205
- environment.onPlayFromIdRequest(mediaId),
207
+ environment.onPlayFromIdRequest(mediaId, append = true),
206
208
  { items -> items.toMutableList() },
207
209
  MoreExecutors.directExecutor()
208
210
  )
@@ -749,7 +751,6 @@ internal class PlaybackServiceCallback(
749
751
  internal const val CONTENT_STYLE_PLAYABLE_HINT =
750
752
  "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT"
751
753
  internal const val CONTENT_STYLE_LIST_ITEM = 1
752
- internal const val CONTENT_STYLE_GRID_ITEM = 2
753
754
 
754
755
  /** Root extras flag: when `true`, gearhead surfaces a search
755
756
  * affordance ("Search artists, albums, songs" mic / magnifying-
@@ -1,12 +1,11 @@
1
1
  package com.margelo.nitro.queueplayer
2
2
 
3
3
  /**
4
- * Pure-Kotlin index-shift arithmetic for queue mutations. Extracted
5
- * from [TrackPlayer] so the shift semantics stand alone without
6
- * an ExoPlayer. Mirrors (and lightly supersedes) the inline math
7
- * inside iOS `TrackPlayer.swift`'s `addToQueueBody` /
8
- * `removeFromQueueBody` / `moveInQueueBody` — the iOS backlog carries
9
- * an item to extract the same shapes on that side for parity.
4
+ * Pure-Kotlin index-shift arithmetic for queue mutations, kept out of
5
+ * [TrackPlayer] so the shift semantics stand alone without an
6
+ * ExoPlayer. Its iOS counterpart is `QueueMutationArithmetic.swift`,
7
+ * with the same function shapes and input/output semantics for these
8
+ * five helpers.
10
9
  *
11
10
  * The helper only computes the new `currentTrackIndex` and (for
12
11
  * `removeFromQueue`) which original indices apply after dedup /
@@ -4,11 +4,12 @@ package com.margelo.nitro.queueplayer
4
4
  * Pure-Kotlin skip-index arithmetic. Mirrors the iOS sibling
5
5
  * `ios/QueueSkipArithmetic.swift` 1-for-1.
6
6
  *
7
- * Extracted from [TrackPlayer] so the logic stands alone without
8
- * instantiating an ExoPlayer. [TrackPlayer.computeNextIndex] /
9
- * [TrackPlayer.computePreviousIndex] are thin adapters that translate
10
- * the Nitrogen-generated [RepeatMode] enum into [QueueSkipRepeatMode]
11
- * and delegate.
7
+ * Kept separate from [TrackPlayer] so the logic runs without an
8
+ * ExoPlayer. [TrackPlayer.computeNextIndex] is the thin adapter that
9
+ * translates the Nitrogen-generated [RepeatMode] enum into
10
+ * [QueueSkipRepeatMode] and delegates; the skip-previous path goes
11
+ * through [QueueSkipArithmetic.resolveSkipToPrevious], which wraps
12
+ * [QueueSkipArithmetic.computePrevious].
12
13
  *
13
14
  * Value order (0/1/2) is held fixed across the iOS enum and this one
14
15
  * so a test that asserts the raw integer catches any accidental
@@ -102,7 +103,9 @@ internal object QueueSkipArithmetic {
102
103
  * When [restartEnabled] is true (`PlayerConfig.skipToPreviousBehavior
103
104
  * == RESTART_OR_PREVIOUS`), Previous is enabled whenever a track is
104
105
  * loaded — even at index 0, where past the threshold it restarts the
105
- * current track — so the control never greys out.
106
+ * current track — so the control never greys out while something is
107
+ * playing. With no loaded track (`currentIndex < 0`) there is nothing
108
+ * to restart, matching what resolveSkipToPrevious does for that state.
106
109
  */
107
110
  fun canSkipPrevious(
108
111
  trackCount: Int,
@@ -110,7 +113,7 @@ internal object QueueSkipArithmetic {
110
113
  repeatMode: QueueSkipRepeatMode,
111
114
  restartEnabled: Boolean = false
112
115
  ): Boolean =
113
- if (restartEnabled) trackCount > 0
116
+ if (restartEnabled) trackCount > 0 && currentIndex >= 0
114
117
  else computePrevious(trackCount, currentIndex, repeatMode) != null
115
118
 
116
119
  /**
@@ -126,7 +129,7 @@ internal object QueueSkipArithmetic {
126
129
  data class Previous(val index: Int) : SkipToPreviousAction
127
130
  /** Restart the current track (seek to 0) — not a track change. */
128
131
  object RestartCurrent : SkipToPreviousAction
129
- /** Nothing to do (empty queue). */
132
+ /** Nothing to do (empty queue, or no loaded track). */
130
133
  object NoOp : SkipToPreviousAction
131
134
  }
132
135
 
@@ -12,8 +12,20 @@ import kotlin.math.roundToInt
12
12
  * gain to a PCM stream. No knowledge of the engine, the EQ stage, or the
13
13
  * visualizer pipeline — purely "given a PCM buffer, multiply by
14
14
  * [linearGain]". The owning engine pushes a new gain via [setLinearGain]
15
- * from its `Player.Listener` callbacks; `onMediaItemTransition` resets to
16
- * unity until the next track's tags arrive.
15
+ * from its `Player.Listener` callbacks and from the ReplayGain-mode
16
+ * setter; `onMediaItemTransition` resets to unity until the next track's
17
+ * tags arrive.
18
+ *
19
+ * It is **always active** on a supported PCM encoding once configured, NOT
20
+ * gated on the current gain. Media3's
21
+ * [androidx.media3.common.audio.AudioProcessingPipeline] fixes the
22
+ * active-processor set in `configure()` / `flush()` and never re-evaluates
23
+ * it while a stream plays, so gating `isActive` on the (later-pushed) gain
24
+ * would exclude this processor from the pipeline for any queue loaded at
25
+ * unity and no gain could ever reach the audio afterwards. Staying active
26
+ * and passing the block straight through at unity keeps it present and
27
+ * ready, at the cost of one bulk buffer copy per block when no gain is
28
+ * applied.
17
29
  *
18
30
  * Handles both 16-bit and float PCM. The sink runs at 16-bit (see
19
31
  * [AudioPipelineRenderersFactory] — float output would drop the whole
@@ -30,7 +42,7 @@ internal class ReplayGainAudioProcessor : BaseAudioProcessor() {
30
42
  * it on every [queueInput] while the engine's main thread writes it
31
43
  * from the metadata listener. The value is `pow(10, gainDb / 20)`
32
44
  * already clip-capped against the track peak (caller's job); a
33
- * value of `1.0f` means RG is inactive for this track.
45
+ * value of `1.0f` passes the stream through untouched.
34
46
  */
35
47
  @Volatile
36
48
  private var linearGain: Float = 1.0f
@@ -43,8 +55,8 @@ internal class ReplayGainAudioProcessor : BaseAudioProcessor() {
43
55
  private var encoding: Int = C.ENCODING_INVALID
44
56
 
45
57
  /**
46
- * Push a new linear gain. `1.0f` disables the processor's per-sample
47
- * multiply via [isActive].
58
+ * Push a new linear gain, observed from the next [queueInput] onwards.
59
+ * `1.0f` skips the per-sample multiply and passes the block through.
48
60
  */
49
61
  fun setLinearGain(value: Float) {
50
62
  linearGain = value
@@ -60,7 +72,7 @@ internal class ReplayGainAudioProcessor : BaseAudioProcessor() {
60
72
  }
61
73
 
62
74
  override fun isActive(): Boolean =
63
- super.isActive() && linearGain != 1.0f &&
75
+ super.isActive() &&
64
76
  (encoding == C.ENCODING_PCM_FLOAT || encoding == C.ENCODING_PCM_16BIT)
65
77
 
66
78
  override fun queueInput(input: ByteBuffer) {
@@ -68,6 +80,14 @@ internal class ReplayGainAudioProcessor : BaseAudioProcessor() {
68
80
  val byteCount = input.remaining()
69
81
  if (byteCount == 0) return
70
82
  val out = replaceOutputBuffer(byteCount)
83
+ if (gain == 1.0f) {
84
+ // Unity — present in the pipeline but applying nothing. A bulk copy
85
+ // is ~25x cheaper per block than walking the samples for a multiply
86
+ // by one, and both consume the input and produce identical bytes.
87
+ out.put(input)
88
+ out.flip()
89
+ return
90
+ }
71
91
  if (encoding == C.ENCODING_PCM_FLOAT) {
72
92
  val inF = input.asFloatBuffer()
73
93
  val outF = out.asFloatBuffer()
@@ -31,9 +31,10 @@ data class ReplayGainData(
31
31
  * both may be null when the album tag is absent. No fallback
32
32
  * to track gain.
33
33
  *
34
- * Caller computes `linearGain = 10^(gainDb / 20)` and clip-caps via
35
- * peak when both are non-null. When `gainDb` is null the caller
36
- * treats RG as inactive for this track (`linearGain = 1.0f`,
34
+ * Caller computes `linearGain = 10^(gainDb / 20)` and clip-caps it
35
+ * at `1 / peak` when the peak is usable, at unity when it is not
36
+ * see [computeLinearGain]. When `gainDb` is null the caller treats
37
+ * RG as inactive for this track (`linearGain = 1.0f`,
37
38
  * `replayGainActive = false`).
38
39
  */
39
40
  fun selectGain(mode: ReplayGainMode): Pair<Float?, Float?> = when (mode) {
@@ -41,4 +42,22 @@ data class ReplayGainData(
41
42
  ReplayGainMode.TRACK -> trackGainDb to trackPeak
42
43
  ReplayGainMode.ALBUM -> albumGainDb to albumPeak
43
44
  }
45
+
46
+ companion object {
47
+ /**
48
+ * Resolve the ReplayGain a track plays with, all-or-nothing per
49
+ * item. [suppliedClaimed] is `true` when the consumer put any of
50
+ * the four fields on the `TrackItem`, computed before any value
51
+ * is validated: a claim discards detection outright, including
52
+ * for the mode the consumer never mentioned.
53
+ * `suppliedClaimed && supplied == null` is a real state — the
54
+ * consumer owns the track and gave nothing usable, so no gain is
55
+ * applied and the file's own tags stay unused.
56
+ */
57
+ internal fun merge(
58
+ suppliedClaimed: Boolean,
59
+ supplied: ReplayGainData?,
60
+ detected: ReplayGainData?,
61
+ ): ReplayGainData? = if (suppliedClaimed) supplied else detected
62
+ }
44
63
  }
@@ -159,7 +159,9 @@ internal object ReplayGainExtractor {
159
159
  /**
160
160
  * Parse a `REPLAYGAIN_*_GAIN` string (e.g. `"-6.54 dB"`,
161
161
  * `"+3.21"`, `"−1.0 dB"` with U+2212 minus) into a Float dB.
162
- * Returns `null` for unparseable / NaN / Inf inputs.
162
+ * Returns `null` for unparseable input; the parsed number is then
163
+ * put through [acceptGainDb], so text handling is all this adds
164
+ * over the numeric entry point.
163
165
  */
164
166
  internal fun parseGainDb(rawInput: String): Float? {
165
167
  var trimmed = rawInput.trim()
@@ -184,25 +186,49 @@ internal object ReplayGainExtractor {
184
186
  // Reject comma decimals (RG2 spec — period only).
185
187
  if (trimmed.contains(',')) return null
186
188
 
187
- val value = trimmed.toFloatOrNull() ?: return null
189
+ val value = trimmed.toDoubleOrNull() ?: return null
190
+ return acceptGainDb(value)
191
+ }
192
+
193
+ /**
194
+ * Shared numeric acceptance for a peak value, whatever entry point
195
+ * it arrived through — a tag string or a consumer-supplied number.
196
+ * Accepts `0.001 ... 2.0` inclusive (`1.0` is digital full scale and
197
+ * a legitimate measurement); rejects anything outside that band and
198
+ * anything non-finite. The band is checked on the `Double`, before the
199
+ * narrowing to `Float`, so a value just past it cannot round in.
200
+ */
201
+ internal fun acceptPeak(value: Double): Float? {
202
+ if (value.isNaN() || value.isInfinite()) return null
203
+ // Band-check the Double: 2.0000001 narrows to 2.0f, so a check
204
+ // applied after the narrowing would let it through.
205
+ if (value < 0.001 || value > 2.0) return null
206
+ return value.toFloat()
207
+ }
208
+
209
+ /**
210
+ * Shared numeric acceptance for a gain in dB, whatever entry point
211
+ * it arrived through. There is no plausibility band on gain — only
212
+ * non-finite values are rejected, including a magnitude that
213
+ * narrows to an infinite [Float].
214
+ */
215
+ internal fun acceptGainDb(value: Double): Float? {
188
216
  if (value.isNaN() || value.isInfinite()) return null
189
- return value
217
+ val narrowed = value.toFloat()
218
+ if (narrowed.isInfinite()) return null
219
+ return narrowed
190
220
  }
191
221
 
192
222
  /**
193
223
  * Parse a `REPLAYGAIN_*_PEAK` string (linear, period decimal).
194
- * Returns `null` for unparseable / NaN / Inf / out-of-range
195
- * values, or for the encoder-default sentinels (peak `< 0.001`
196
- * or peak exactly `1.0`, both signal the analyser didn't run).
224
+ * Returns `null` for unparseable input; the parsed number is then
225
+ * put through [acceptPeak], which holds the `0.001 ... 2.0`
226
+ * acceptance band a full-scale `1.0` included.
197
227
  */
198
228
  internal fun parsePeak(rawInput: String): Float? {
199
229
  val trimmed = rawInput.trim()
200
- val value = trimmed.toFloatOrNull() ?: return null
201
- if (value.isNaN() || value.isInfinite()) return null
202
- if (value < 0f || value > 2.0f) return null
203
- if (value < 0.001f) return null
204
- if (value == 1.0f) return null
205
- return value
230
+ val value = trimmed.toDoubleOrNull() ?: return null
231
+ return acceptPeak(value)
206
232
  }
207
233
 
208
234
  // endregion
@@ -5,30 +5,34 @@ import kotlin.math.pow
5
5
 
6
6
  /**
7
7
  * Pure gain math per NOTES.md §25. Selects the `(gainDb, peak)` pair
8
- * from [data] under [mode] (strict, no fallback), then:
8
+ * from [data] under [mode] (strict, no fallback), then caps the result
9
+ * for clip safety:
9
10
  *
10
11
  * ```
11
- * linearGain = pow(10, gainDb / 20)
12
- * if peak != null and peak > 0.001 and peak != 1.0:
13
- * linearGain = min(linearGain, 1.0 / peak)
12
+ * cap = usable peak ? 1.0 / peak : 1.0
13
+ * linearGain = min(pow(10, gainDb / 20), cap)
14
14
  * ```
15
15
  *
16
+ * A peak is usable inside the `0.001 .. 2.0` band that
17
+ * [ReplayGainExtractor.acceptPeak] holds — full scale (`1.0`) and true
18
+ * peaks above it included. With no usable peak the cap sits at unity,
19
+ * so a boost is suppressed rather than applied unprotected: a peak
20
+ * cannot be derived from a gain. Attenuation is untouched either way,
21
+ * since a negative gain can never clip. The band is re-checked here so
22
+ * the math stays correct for any caller that bypasses the extractor.
23
+ *
16
24
  * Returns `1.0f` (unity) whenever the selected mode's gain tag is
17
25
  * absent — RG is inactive for that track. Mirrors the iOS
18
26
  * `AudioTapProvider.computeLinearGain(data:mode:)` contract so a tag
19
27
  * fixture parses to the same linear gain on both platforms.
20
- *
21
- * Encoder-default sentinels (`peak == 1.0` and `peak < 0.001`) bypass
22
- * the peak-based clip cap. The extractor returns `null` for both
23
- * already, but defending here keeps the math correct against any
24
- * future caller that bypasses the extractor.
25
28
  */
26
29
  internal fun computeLinearGain(data: ReplayGainData?, mode: ReplayGainMode): Float {
27
30
  val (gainDb, peak) = data?.selectGain(mode) ?: (null to null)
28
31
  if (gainDb == null) return 1.0f
29
- var raw = 10.0.pow(gainDb.toDouble() / 20.0).toFloat()
30
- if (peak != null && peak > 0.001f && peak != 1.0f) {
31
- raw = min(raw, 1.0f / peak)
32
+ val raw = 10.0.pow(gainDb.toDouble() / 20.0).toFloat()
33
+ var cap = 1.0f
34
+ if (peak != null && peak >= 0.001f && peak <= 2.0f) {
35
+ cap = 1.0f / peak
32
36
  }
33
- return raw
37
+ return min(raw, cap)
34
38
  }