react-native-queue-player 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (367) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/build.gradle +7 -0
  4. package/android/consumer-rules.pro +72 -3
  5. package/android/src/main/cpp/airplay2_control.c +4 -4
  6. package/android/src/main/cpp/airplay2_control.h +4 -4
  7. package/android/src/main/cpp/airplay2_jni.cpp +26 -22
  8. package/android/src/main/cpp/airplay2_pair.c +7 -7
  9. package/android/src/main/cpp/airplay2_pair.h +2 -2
  10. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  11. package/android/src/main/cpp/airplay2_session.c +2 -2
  12. package/android/src/main/cpp/airplay_jni.cpp +20 -72
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
  61. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  62. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  63. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  64. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  65. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
  114. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
  115. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  116. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  117. package/app.plugin.js +17 -1
  118. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  119. package/ios/AVQueueBuilder.swift +120 -85
  120. package/ios/ArtworkLoader.swift +0 -7
  121. package/ios/ArtworkResolver.swift +22 -4
  122. package/ios/AssetReplayGainReader.swift +118 -0
  123. package/ios/AudioSession.swift +67 -16
  124. package/ios/AudioTapProvider.swift +284 -149
  125. package/ios/CarPlayBridge.swift +0 -12
  126. package/ios/CarPlayCoordinator.swift +18 -2
  127. package/ios/CarPlaySceneDelegate.swift +2 -2
  128. package/ios/Cast/AirPlayRouteState.swift +9 -8
  129. package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
  130. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  131. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  132. package/ios/Cast/Core/CastSession.swift +11 -0
  133. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  134. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  135. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  136. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  137. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  138. package/ios/CastManager.swift +1 -1
  139. package/ios/CrossfadeEngine.swift +660 -226
  140. package/ios/EQTap.swift +20 -17
  141. package/ios/Equalizer.swift +1 -1
  142. package/ios/FLACStreamInfo.swift +65 -0
  143. package/ios/GaplessEngine.swift +57 -30
  144. package/ios/InputGuards.swift +35 -5
  145. package/ios/LookaheadCache.swift +202 -58
  146. package/ios/LookaheadCachePrefetcher.swift +48 -36
  147. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  148. package/ios/NetworkRecoveryPolicy.swift +32 -0
  149. package/ios/NowPlayingFormatExtractor.swift +25 -18
  150. package/ios/NowPlayingInfo.swift +84 -48
  151. package/ios/OutputRouteMonitor.swift +16 -3
  152. package/ios/PendingBrowseRequests.swift +1 -1
  153. package/ios/PlaceholderArtwork.swift +9 -5
  154. package/ios/PlaybackEngine.swift +165 -76
  155. package/ios/PlaybackErrorMapping.swift +3 -3
  156. package/ios/PlaybackModeStateMachine.swift +2 -2
  157. package/ios/PlaybackNetworkMonitor.swift +73 -0
  158. package/ios/PlayerStateDerivation.swift +6 -6
  159. package/ios/QueueMutationArithmetic.swift +25 -0
  160. package/ios/QueueSkipArithmetic.swift +5 -3
  161. package/ios/QueueWindowArithmetic.swift +152 -0
  162. package/ios/ReadThroughServer.swift +283 -0
  163. package/ios/RemoteCommands.swift +44 -4
  164. package/ios/ReplayGainData.swift +22 -3
  165. package/ios/ReplayGainExtractor.swift +70 -27
  166. package/ios/Siri/VoiceDonation.swift +14 -9
  167. package/ios/StreamingBitrateProbe.swift +22 -68
  168. package/ios/SuppliedReplayGain.swift +83 -0
  169. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  170. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  171. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  172. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  173. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  174. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  175. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  176. package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
  177. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  178. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  179. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  180. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  181. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  182. package/ios/Tests/EngineSwapTests.swift +91 -0
  183. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  184. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  185. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  186. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  187. package/ios/Tests/InputGuardsTests.swift +41 -0
  188. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  189. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  190. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  191. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  192. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  193. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  194. package/ios/Tests/MutationDeferralTests.swift +220 -0
  195. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  196. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  197. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  198. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  199. package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
  200. package/ios/Tests/PlayerFixtures.swift +48 -0
  201. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  202. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  203. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  204. package/ios/Tests/QueueStateTests.swift +3 -1
  205. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  206. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  207. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  208. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  209. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  210. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  211. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  212. package/ios/Tests/RetryRecoveryTests.swift +303 -0
  213. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  214. package/ios/Tests/SkipIndexTests.swift +32 -2
  215. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  216. package/ios/Tests/StallRecoveryTests.swift +97 -0
  217. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  218. package/ios/Tests/TopUpWindowGateTests.swift +394 -0
  219. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  220. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  221. package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
  222. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  223. package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
  224. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  225. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  226. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  227. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  228. package/ios/TrackPlayer+Automotive.swift +101 -0
  229. package/ios/TrackPlayer+Cache.swift +232 -0
  230. package/ios/TrackPlayer+Config.swift +291 -0
  231. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  232. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  233. package/ios/TrackPlayer+EventsDispatch.swift +968 -0
  234. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  235. package/ios/TrackPlayer+Lifecycle.swift +930 -0
  236. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  237. package/ios/TrackPlayer+Queue.swift +940 -0
  238. package/ios/TrackPlayer+Recovery.swift +196 -0
  239. package/ios/TrackPlayer+Skip.swift +455 -0
  240. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  241. package/ios/TrackPlayer+State.swift +108 -0
  242. package/ios/TrackPlayer+Threading.swift +135 -0
  243. package/ios/TrackPlayer+Transport.swift +267 -0
  244. package/ios/TrackPlayer+Window.swift +195 -0
  245. package/ios/TrackPlayer.swift +305 -4610
  246. package/ios/Visualizer.swift +6 -5
  247. package/ios/tests-harness/Podfile +1 -1
  248. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  249. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  250. package/lib/module/hooks/useActiveTrack.js +29 -22
  251. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  252. package/lib/module/hooks/useCast.js +8 -26
  253. package/lib/module/hooks/useCast.js.map +1 -1
  254. package/lib/module/hooks/useEqualizer.js +19 -12
  255. package/lib/module/hooks/useEqualizer.js.map +1 -1
  256. package/lib/module/hooks/useLookaheadCache.js +3 -7
  257. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  258. package/lib/module/hooks/useQueue.js +66 -19
  259. package/lib/module/hooks/useQueue.js.map +1 -1
  260. package/lib/module/index.js +6 -3
  261. package/lib/module/index.js.map +1 -1
  262. package/lib/module/queueDelta.js +41 -0
  263. package/lib/module/queueDelta.js.map +1 -0
  264. package/lib/module/types.js +28 -5
  265. package/lib/module/types.js.map +1 -1
  266. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  267. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  269. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useCast.d.ts +6 -1
  271. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  272. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  273. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  274. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  275. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  276. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  277. package/lib/typescript/index.d.ts +2 -1
  278. package/lib/typescript/index.d.ts.map +1 -1
  279. package/lib/typescript/queueDelta.d.ts +10 -0
  280. package/lib/typescript/queueDelta.d.ts.map +1 -0
  281. package/lib/typescript/types.d.ts +168 -12
  282. package/lib/typescript/types.d.ts.map +1 -1
  283. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  284. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  285. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  286. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  287. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  288. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  289. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  320. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  321. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  322. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  323. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  324. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  325. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  326. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  327. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  328. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  329. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  330. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  331. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  332. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  333. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  334. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  335. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  336. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  337. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  338. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  339. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  340. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  341. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  342. package/package.json +8 -8
  343. package/src/TrackPlayer.nitro.ts +53 -27
  344. package/src/hooks/useActiveTrack.ts +29 -22
  345. package/src/hooks/useCast.ts +14 -15
  346. package/src/hooks/useEqualizer.ts +19 -12
  347. package/src/hooks/useLookaheadCache.ts +3 -7
  348. package/src/hooks/useQueue.ts +66 -17
  349. package/src/index.ts +13 -3
  350. package/src/queueDelta.ts +41 -0
  351. package/src/types.ts +169 -12
  352. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  353. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
  354. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  355. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  356. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  357. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  358. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  359. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  360. package/ios/MetadataReader.swift +0 -555
  361. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  362. package/ios/Tests/MetadataReaderTests.swift +0 -603
  363. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  364. package/ios/Tests/VoiceDonationTests.swift +0 -26
  365. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  366. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  367. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -9,40 +9,29 @@ import androidx.media3.exoplayer.audio.TeeAudioProcessor
9
9
  /**
10
10
  * Strategy abstraction over the audio output pipeline.
11
11
  *
12
- * Two concrete implementations:
12
+ * Three concrete implementations:
13
13
  * - [GaplessEngine]: single ExoPlayer, native gapless via item chaining.
14
14
  * - [CrossfadeEngine]: dual ExoPlayer with equal-power gain ramping at
15
15
  * track boundaries.
16
+ * - [AirPlayEngine]: single ExoPlayer bound to an AirPlay PCM sink;
17
+ * selected by `applyEngineSwapForMode` while an AirPlay session is
18
+ * active.
16
19
  *
17
20
  * The engine owns the player instance(s), audio-thread effects, and
18
21
  * track-boundary scheduling. The lib's [TrackPlayer] holds the canonical
19
22
  * queue model + listener registries + system-side surfaces (NowPlaying,
20
23
  * RemoteCommands, AudioSession) and routes transport / queue calls
21
- * through whichever engine is active. Engine swap (gapless ↔ crossfade)
22
- * is a `handoff` at the next track boundary.
24
+ * through whichever engine is active. An engine swap (gapless ↔ crossfade)
25
+ * is `TrackPlayer.applyEngineSwapForMode`: [PlaybackService.swapEngine]
26
+ * installs the new engine and releases the old one, then `TrackPlayer`
27
+ * seeds the new engine through [setMediaItems].
23
28
  *
24
29
  * Threading: all methods are called on the service main looper unless
25
- * noted. `suspend` methods are launched on the same dispatcher; the
26
- * impl is responsible for any internal off-loop work (e.g. heavy
27
- * lookahead-cache prefetch).
30
+ * noted; the impl is responsible for any internal off-loop work (e.g.
31
+ * heavy lookahead-cache prefetch).
28
32
  */
29
33
  interface PlaybackEngine {
30
34
 
31
- /**
32
- * Build the underlying player(s), apply [mediaItems] starting at
33
- * [startIndex] / [startPositionMs], and wait for the underlying
34
- * player(s) to reach a playable state.
35
- *
36
- * Empty [mediaItems] is allowed; the engine prepares an empty
37
- * timeline and [currentMediaItemIndex] returns `-1` until items
38
- * arrive via [setMediaItems] / [addMediaItems].
39
- */
40
- suspend fun prepare(
41
- mediaItems: List<MediaItem>,
42
- startIndex: Int,
43
- startPositionMs: Long,
44
- )
45
-
46
35
  /**
47
36
  * Release the underlying player(s) and stop all background work.
48
37
  * After [release], no further methods are valid.
@@ -127,20 +116,16 @@ interface PlaybackEngine {
127
116
  val allMediaItems: List<MediaItem>
128
117
 
129
118
  /**
130
- * Underlying ExoPlayer instance the service-level wiring
131
- * (audio-focus listener, `MediaSession.setPlayer`) attaches
132
- * to. `GaplessEngine` returns its single player; `CrossfadeEngine`
119
+ * Underlying ExoPlayer for the audio-focus listener and the
120
+ * `MediaSession` binding. `GaplessEngine` returns its single
121
+ * player, which [PlaybackService] binds directly; `CrossfadeEngine`
133
122
  * returns the leading leg's player.
134
123
  *
135
- * Mid-fade role swap inside `CrossfadeEngine` flips which
136
- * underlying player this getter returns, but the
137
- * `MediaSession` currently bound by `PlaybackService.swapEngine`
138
- * does NOT auto-rebind `MediaSession.setPlayer` is invoked
139
- * once per engine swap, not per fade midpoint. The
140
- * `CrossfadeForwardingPlayer` wrapper exists to bridge that
141
- * gap (re-target the bound delegate via `setPlayer` on every
142
- * role swap); wiring lands when [PlaybackService] starts
143
- * binding through the wrapper for crossfade engines.
124
+ * A mid-fade role swap inside `CrossfadeEngine` flips which player this
125
+ * getter returns, and nothing re-reads it: [PlaybackService] binds a
126
+ * crossfade engine through a [CrossfadeForwardingPlayer] instead, which
127
+ * re-targets via `setPlayer` on every role swap, fade start and fade
128
+ * cancel, so the session and the focus listener follow the audible leg.
144
129
  */
145
130
  val mediaSessionPlayer: ExoPlayer
146
131
 
@@ -176,12 +161,36 @@ interface PlaybackEngine {
176
161
  fun detachAudioBufferSink(sink: TeeAudioProcessor.AudioBufferSink)
177
162
 
178
163
  /**
179
- * Snapshot the active item's cached [ReplayGainData], or `null`
180
- * when no usable RG tags have been parsed yet. Surfaced by
181
- * [NowPlayingFormatExtractor] to populate the four
182
- * `replayGain*Db` / `replayGain*Peak` fields. Default returns
183
- * `null` so engines without RG wiring (future stub variants)
184
- * compile without a no-op override.
164
+ * Settle any in-flight or armed crossfade so the queue can be mutated by
165
+ * something other than this engine.
166
+ *
167
+ * An armed standby leg holds a snapshot of the queue taken when it was
168
+ * armed. A caller that appends or replaces items through a path the engine
169
+ * does not see Media3 setting them on the session player after resolving
170
+ * an assistant request — leaves that snapshot stale, and promoting it drops
171
+ * whatever the caller added. Default is a no-op: an engine with one player
172
+ * has nothing to settle.
173
+ */
174
+ fun settlePendingFade() {}
175
+
176
+ /**
177
+ * Settle an in-flight fade by promoting the incoming leg, so the leg a
178
+ * caller reads is the track actually being heard.
179
+ *
180
+ * Mid-fade the leading leg holds the outgoing track while the canonical
181
+ * index names the incoming one, so a caller that reads the player and the
182
+ * index together sees two different tracks. Promoting resolves them to the
183
+ * incoming track, which is the one the listener is arriving at. Default is a
184
+ * no-op: an engine with one player is never in this state.
185
+ */
186
+ fun settlePendingFadeByPromoting() {}
187
+
188
+ /**
189
+ * Snapshot the active item's cached [ReplayGainData], or `null` when no
190
+ * usable RG tags have been parsed yet. Surfaced by
191
+ * [NowPlayingFormatExtractor] to populate the four `replayGain*Db` /
192
+ * `replayGain*Peak` fields. The default is `null`, for an engine that
193
+ * reads no tags.
185
194
  */
186
195
  fun currentReplayGainData(): ReplayGainData? = null
187
196
 
@@ -194,43 +203,22 @@ interface PlaybackEngine {
194
203
  */
195
204
  fun currentReplayGainActive(): Boolean = false
196
205
 
197
- /**
198
- * Hand playback off to [nextEngine] when the user changes
199
- * `PlaybackMode`.
200
- *
201
- * Contract:
202
- * - The receiver inherits the donor's queue, current index, and
203
- * current position; the caller does NOT pre-call [prepare] on
204
- * [nextEngine].
205
- * - When [atTrackBoundary] is `true`, the donor plays the active
206
- * track to its natural end and the receiver starts playing the
207
- * NEXT queue position fresh.
208
- * - When [atTrackBoundary] is `false` (idle player or empty
209
- * queue), the swap is immediate; receiver picks up at the
210
- * inherited position.
211
- * - The donor's [release] is the caller's responsibility AFTER
212
- * the boundary fires; the engine itself does not self-release.
213
- * - The donor's listeners on [delegate] continue firing until
214
- * [release] runs; the caller swaps the delegate before that.
215
- */
216
- suspend fun handoff(to: PlaybackEngine, atTrackBoundary: Boolean)
217
-
218
206
  /**
219
207
  * Single delegate receiving state / track / progress / error /
220
208
  * queue-end events. TrackPlayer is the canonical delegate; engines
221
209
  * fire on the service main looper.
222
210
  *
223
- * Strong reference by design; the iOS counterpart uses a weak
224
- * reference because the AVQueuePlayer-driven engine sits inside
225
- * the same actor as TrackPlayer and Swift's reference graph would
226
- * otherwise close (TrackPlayer engine delegate TrackPlayer).
227
- * On Android the engine lives inside [PlaybackService.serviceBinder]
228
- * which is owned by the service lifecycle, not by TrackPlayer; the
229
- * cycle is broken explicitly by [release] dropping `delegate` and
230
- * by [TrackPlayer.tearDownEventObservers] dropping the binder.
211
+ * Strong reference by design; the iOS counterpart is `weak` because
212
+ * `TrackPlayer` owns its engine strongly and Swift's reference graph
213
+ * would otherwise close (TrackPlayer engine delegate → TrackPlayer).
214
+ * On Android the engine lives inside [PlaybackService], reached through
215
+ * [PlaybackService.LocalBinder.engine] and owned by the service
216
+ * lifecycle, not by TrackPlayer; the cycle is broken explicitly by
217
+ * [release] dropping `delegate` and by `TrackPlayer.tearDownEventObservers`
218
+ * clearing itself from the engine's `delegate`.
231
219
  *
232
220
  * Threading: setter MUST be invoked on the service main looper to
233
- * avoid a write-vs-fire race during [handoff].
221
+ * avoid a write-vs-fire race during an engine swap.
234
222
  */
235
223
  var delegate: PlaybackEngineDelegate?
236
224
  }
@@ -290,10 +278,14 @@ interface PlaybackEngineDelegate {
290
278
  fun onActiveItemFormatChanged(engine: PlaybackEngine)
291
279
 
292
280
  /**
293
- * Fires on Media3 `Player.Listener.onPlayerError`. The delegate
294
- * maps [exception] via the lib's `PlaybackErrorMapping`.
281
+ * Fires on Media3 `Player.Listener.onPlayerError`. The delegate maps
282
+ * [exception] via the lib's `PlaybackErrorMapping`. [failedIndex] is the
283
+ * queue position of the item that failed, or -1 when the engine cannot
284
+ * attribute it. A crossfade engine can fail on its standby leg while the
285
+ * leading leg plays on, so the failure need not belong to the track the
286
+ * delegate considers current.
295
287
  */
296
- fun onError(engine: PlaybackEngine, exception: PlaybackException)
288
+ fun onError(engine: PlaybackEngine, exception: PlaybackException, failedIndex: Int = -1)
297
289
 
298
290
  /**
299
291
  * Fires on Media3 `Player.STATE_ENDED`. The delegate consults the
@@ -316,9 +308,10 @@ interface PlaybackEngineDelegate {
316
308
  * [ExoPlayer] has started playback at volume 0 and the leading
317
309
  * leg's fade-out has begun. The delegate treats this as the
318
310
  * canonical "active track changed" trigger and emits
319
- * `onTrackChange` + repoints `MediaSessionCompat` to the
320
- * incoming track. The subsequent `onTrackTransition` for the
321
- * same incoming index is a structural Media3 echo and must be
311
+ * `onTrackChange` for the incoming track; the `MediaSession` re-bind
312
+ * to the standby leg is [CrossfadeForwardingPlayer]'s, through the
313
+ * engine's `onCrossfadeBegin` lambda. The subsequent `onTrackTransition`
314
+ * for the same incoming index is a structural Media3 echo and must be
322
315
  * deduplicated.
323
316
  *
324
317
  * Crossfade-only — [GaplessEngine] never invokes this.
@@ -333,7 +326,9 @@ interface PlaybackEngineDelegate {
333
326
  * mutation that invalidates the standby leg). The delegate
334
327
  * reverts JS-facing state to the leading leg — emit
335
328
  * `onTrackChange(reason: CROSSFADE_CANCELLED)` for the leading
336
- * track + repoint `MediaSessionCompat` back. Pairs with
329
+ * track; the `MediaSession` re-bind back to the leading leg is
330
+ * [CrossfadeForwardingPlayer]'s, through the engine's
331
+ * `onCrossfadeCancel` lambda. Pairs with
337
332
  * [onCrossfadeBegin]: only fires when a `Begin` was fired
338
333
  * earlier and the fade did not complete.
339
334
  *
@@ -85,9 +85,9 @@ internal object PlaybackErrorMapping {
85
85
 
86
86
  /**
87
87
  * Strip query strings from a native error message so logged
88
- * `nativeMessage` doesn't leak auth tokens (Subsonic-style
89
- * `s=…&t=…` salt+token pairs in stream URLs end up inside
90
- * `PlaybackException.message`).
88
+ * `nativeMessage` doesn't leak auth tokens: stream URLs with
89
+ * `?token=…`-style query strings end up inside
90
+ * `PlaybackException.message`.
91
91
  *
92
92
  * Walks the full message removing EVERY `?…(whitespace|end)` run
93
93
  * so multi-URL error messages have all query strings stripped.
@@ -49,8 +49,8 @@ internal class PlaybackModeStateMachine {
49
49
  * the new duration.
50
50
  *
51
51
  * The caller is responsible for managing the brief audio
52
- * interruption (resume position carryover, queue re-attach,
53
- * etc.) that's the engine-handoff contract.
52
+ * interruption `applyEngineSwapForMode` carries the position
53
+ * and re-installs the queue.
54
54
  */
55
55
  fun setRequested(
56
56
  state: State,
@@ -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 /