react-native-queue-player 1.2.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/QueuePlayer.podspec +14 -9
  2. package/README.md +3 -3
  3. package/android/consumer-rules.pro +1 -1
  4. package/android/src/main/cpp/airplay2_control.c +4 -4
  5. package/android/src/main/cpp/airplay2_control.h +4 -4
  6. package/android/src/main/cpp/airplay2_jni.cpp +3 -19
  7. package/android/src/main/cpp/airplay2_pair.c +7 -7
  8. package/android/src/main/cpp/airplay2_pair.h +2 -2
  9. package/android/src/main/cpp/airplay2_rtsp.c +8 -8
  10. package/android/src/main/cpp/airplay2_session.c +2 -2
  11. package/android/src/main/cpp/airplay_jni.cpp +8 -69
  12. package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
  13. package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
  14. package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
  15. package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
  16. package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
  17. package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
  18. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
  19. package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
  20. package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
  21. package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
  22. package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
  23. package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
  24. package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
  25. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
  26. package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
  27. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
  28. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
  29. package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
  30. package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
  31. package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
  32. package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
  33. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
  34. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
  35. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
  36. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
  37. package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
  38. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
  39. package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
  40. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
  41. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
  42. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
  43. package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
  44. package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
  45. package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
  46. package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
  47. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
  48. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
  49. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
  50. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
  51. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
  52. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
  53. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
  54. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
  55. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
  56. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
  57. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
  58. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
  59. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
  60. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
  61. package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
  62. package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
  63. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
  64. package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
  65. package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
  66. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
  67. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
  68. package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
  69. package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
  70. package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
  71. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
  72. package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
  73. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
  74. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
  75. package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
  76. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
  77. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
  78. package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
  79. package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
  80. package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
  81. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
  82. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
  83. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
  84. package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
  85. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
  86. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
  87. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
  88. package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
  89. package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
  90. package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
  91. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
  92. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
  93. package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
  94. package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
  95. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
  96. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
  97. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
  98. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
  99. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
  100. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
  101. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
  102. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
  103. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
  104. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
  105. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
  106. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
  107. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
  108. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
  109. package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
  110. package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
  111. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
  112. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
  113. package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
  114. package/app.plugin.js +17 -1
  115. package/ios/AVPlayerItemQueueItemId.swift +26 -12
  116. package/ios/AVQueueBuilder.swift +120 -85
  117. package/ios/ArtworkLoader.swift +0 -7
  118. package/ios/ArtworkResolver.swift +22 -4
  119. package/ios/AssetReplayGainReader.swift +118 -0
  120. package/ios/AudioSession.swift +67 -16
  121. package/ios/AudioTapProvider.swift +284 -149
  122. package/ios/CarPlayBridge.swift +0 -12
  123. package/ios/CarPlayCoordinator.swift +18 -2
  124. package/ios/CarPlaySceneDelegate.swift +2 -2
  125. package/ios/Cast/AirPlayRouteState.swift +9 -8
  126. package/ios/Cast/Core/CastEventBridge.swift +66 -26
  127. package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
  128. package/ios/Cast/Core/CastSession.swift +2 -2
  129. package/ios/Cast/Core/CastTransportRouter.swift +21 -9
  130. package/ios/Cast/Core/LocalMediaServer.swift +41 -52
  131. package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
  132. package/ios/Cast/Core/MediaServerHandle.swift +0 -4
  133. package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
  134. package/ios/CastManager.swift +1 -1
  135. package/ios/CrossfadeEngine.swift +672 -226
  136. package/ios/EQTap.swift +20 -17
  137. package/ios/Equalizer.swift +1 -1
  138. package/ios/FLACStreamInfo.swift +65 -0
  139. package/ios/GaplessEngine.swift +57 -30
  140. package/ios/InputGuards.swift +35 -5
  141. package/ios/LookaheadCache.swift +229 -58
  142. package/ios/LookaheadCachePrefetcher.swift +48 -36
  143. package/ios/MediaResponseCheck.swift +122 -0
  144. package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
  145. package/ios/NetworkRecoveryPolicy.swift +32 -0
  146. package/ios/NowPlayingFormatExtractor.swift +25 -18
  147. package/ios/NowPlayingInfo.swift +84 -48
  148. package/ios/OutputRouteMonitor.swift +16 -3
  149. package/ios/PendingBrowseRequests.swift +1 -1
  150. package/ios/PlaceholderArtwork.swift +9 -5
  151. package/ios/PlaybackEngine.swift +165 -76
  152. package/ios/PlaybackErrorMapping.swift +3 -3
  153. package/ios/PlaybackModeStateMachine.swift +2 -2
  154. package/ios/PlaybackNetworkMonitor.swift +73 -0
  155. package/ios/PlayerStateDerivation.swift +6 -6
  156. package/ios/QueueMutationArithmetic.swift +25 -0
  157. package/ios/QueueSkipArithmetic.swift +5 -3
  158. package/ios/QueueWindowArithmetic.swift +152 -0
  159. package/ios/ReadThroughServer.swift +283 -0
  160. package/ios/RemoteCommands.swift +44 -4
  161. package/ios/ReplayGainData.swift +22 -3
  162. package/ios/ReplayGainExtractor.swift +70 -27
  163. package/ios/Siri/VoiceDonation.swift +14 -9
  164. package/ios/StreamingBitrateProbe.swift +22 -68
  165. package/ios/SuppliedReplayGain.swift +83 -0
  166. package/ios/Tests/AVQueueBuilderTests.swift +158 -221
  167. package/ios/Tests/ActiveItemEngineStub.swift +44 -0
  168. package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
  169. package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
  170. package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
  171. package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
  172. package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
  173. package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
  174. package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
  175. package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
  176. package/ios/Tests/CrossfadeEngineTests.swift +164 -0
  177. package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
  178. package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
  179. package/ios/Tests/EngineSwapTests.swift +91 -0
  180. package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
  181. package/ios/Tests/EqualizerHybridTests.swift +5 -2
  182. package/ios/Tests/FLACStreamInfoTests.swift +88 -0
  183. package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
  184. package/ios/Tests/InputGuardsTests.swift +41 -0
  185. package/ios/Tests/InterruptionIntentTests.swift +77 -0
  186. package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
  187. package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
  188. package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
  189. package/ios/Tests/LookaheadCacheTests.swift +44 -18
  190. package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
  191. package/ios/Tests/MediaResponseCheckTests.swift +128 -0
  192. package/ios/Tests/MutationDeferralTests.swift +220 -0
  193. package/ios/Tests/NowPlayingInfoTests.swift +86 -99
  194. package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
  195. package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
  196. package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
  197. package/ios/Tests/PlayerFixtures.swift +48 -0
  198. package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
  199. package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
  200. package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
  201. package/ios/Tests/QueueStateTests.swift +3 -1
  202. package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
  203. package/ios/Tests/QueueWindowSliceTests.swift +162 -0
  204. package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
  205. package/ios/Tests/ReadThroughServerTests.swift +345 -0
  206. package/ios/Tests/RemoteCommandsTests.swift +28 -0
  207. package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
  208. package/ios/Tests/ReplayGainMergeTests.swift +230 -0
  209. package/ios/Tests/RetryRecoveryTests.swift +418 -0
  210. package/ios/Tests/SkipCapabilityTests.swift +233 -8
  211. package/ios/Tests/SkipIndexTests.swift +32 -2
  212. package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
  213. package/ios/Tests/StallRecoveryTests.swift +97 -0
  214. package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
  215. package/ios/Tests/TopUpWindowGateTests.swift +441 -0
  216. package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
  217. package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
  218. package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
  219. package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
  220. package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
  221. package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
  222. package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
  223. package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
  224. package/ios/TrackPlayer+Automotive.swift +101 -0
  225. package/ios/TrackPlayer+Cache.swift +232 -0
  226. package/ios/TrackPlayer+Config.swift +291 -0
  227. package/ios/TrackPlayer+EngineDelegate.swift +209 -0
  228. package/ios/TrackPlayer+EventsAPI.swift +111 -0
  229. package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
  230. package/ios/TrackPlayer+EventsWiring.swift +173 -0
  231. package/ios/TrackPlayer+Lifecycle.swift +982 -0
  232. package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
  233. package/ios/TrackPlayer+Queue.swift +951 -0
  234. package/ios/TrackPlayer+Recovery.swift +213 -0
  235. package/ios/TrackPlayer+Skip.swift +465 -0
  236. package/ios/TrackPlayer+SleepTimer.swift +175 -0
  237. package/ios/TrackPlayer+State.swift +108 -0
  238. package/ios/TrackPlayer+Threading.swift +135 -0
  239. package/ios/TrackPlayer+Transport.swift +272 -0
  240. package/ios/TrackPlayer+Window.swift +227 -0
  241. package/ios/TrackPlayer.swift +331 -4619
  242. package/ios/Visualizer.swift +6 -5
  243. package/ios/tests-harness/Podfile +1 -1
  244. package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
  245. package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
  246. package/lib/module/hooks/useActiveTrack.js +29 -22
  247. package/lib/module/hooks/useActiveTrack.js.map +1 -1
  248. package/lib/module/hooks/useCast.js +8 -26
  249. package/lib/module/hooks/useCast.js.map +1 -1
  250. package/lib/module/hooks/useEqualizer.js +19 -12
  251. package/lib/module/hooks/useEqualizer.js.map +1 -1
  252. package/lib/module/hooks/useLookaheadCache.js +3 -7
  253. package/lib/module/hooks/useLookaheadCache.js.map +1 -1
  254. package/lib/module/hooks/useQueue.js +66 -19
  255. package/lib/module/hooks/useQueue.js.map +1 -1
  256. package/lib/module/index.js +6 -3
  257. package/lib/module/index.js.map +1 -1
  258. package/lib/module/queueDelta.js +41 -0
  259. package/lib/module/queueDelta.js.map +1 -0
  260. package/lib/module/types.js +28 -5
  261. package/lib/module/types.js.map +1 -1
  262. package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
  263. package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
  264. package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
  265. package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
  266. package/lib/typescript/hooks/useCast.d.ts +6 -1
  267. package/lib/typescript/hooks/useCast.d.ts.map +1 -1
  268. package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
  269. package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
  270. package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
  271. package/lib/typescript/hooks/useQueue.d.ts +4 -4
  272. package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
  273. package/lib/typescript/index.d.ts +2 -1
  274. package/lib/typescript/index.d.ts.map +1 -1
  275. package/lib/typescript/queueDelta.d.ts +10 -0
  276. package/lib/typescript/queueDelta.d.ts.map +1 -0
  277. package/lib/typescript/types.d.ts +168 -12
  278. package/lib/typescript/types.d.ts.map +1 -1
  279. package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
  280. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
  281. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
  282. package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
  283. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
  284. package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
  285. package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
  286. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
  287. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
  288. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
  289. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
  290. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
  291. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
  292. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
  293. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
  294. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
  295. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
  296. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
  297. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
  298. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
  299. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
  300. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
  301. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
  302. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
  303. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
  304. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
  305. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
  306. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
  307. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
  308. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
  309. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
  310. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
  311. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
  312. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
  313. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
  314. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
  315. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
  316. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
  317. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
  318. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
  319. package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
  320. package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
  321. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
  322. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
  323. package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
  324. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
  325. package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
  326. package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
  327. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
  328. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
  329. package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
  330. package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
  331. package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
  332. package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
  333. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
  334. package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
  335. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
  336. package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
  337. package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
  338. package/package.json +8 -8
  339. package/src/TrackPlayer.nitro.ts +53 -27
  340. package/src/hooks/useActiveTrack.ts +29 -22
  341. package/src/hooks/useCast.ts +14 -15
  342. package/src/hooks/useEqualizer.ts +19 -12
  343. package/src/hooks/useLookaheadCache.ts +3 -7
  344. package/src/hooks/useQueue.ts +66 -17
  345. package/src/index.ts +13 -3
  346. package/src/queueDelta.ts +41 -0
  347. package/src/types.ts +169 -12
  348. package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
  349. package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
  350. package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
  351. package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
  352. package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
  353. package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
  354. package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
  355. package/ios/MetadataReader.swift +0 -555
  356. package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
  357. package/ios/Tests/MetadataReaderTests.swift +0 -603
  358. package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
  359. package/ios/Tests/VoiceDonationTests.swift +0 -26
  360. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
  361. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
  362. /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
@@ -0,0 +1,165 @@
1
+ package com.margelo.nitro.queueplayer
2
+
3
+ import android.net.Uri
4
+ import androidx.media3.common.C
5
+ import androidx.media3.common.PlaybackException
6
+ import androidx.media3.common.util.UnstableApi
7
+ import androidx.media3.datasource.DataSource
8
+ import androidx.media3.datasource.DataSpec
9
+ import androidx.media3.datasource.HttpDataSource
10
+ import androidx.media3.datasource.TransferListener
11
+
12
+ /**
13
+ * Wraps an HTTP-capable [DataSource.Factory] so a response that is a document
14
+ * rather than media fails before any of it is returned.
15
+ *
16
+ * Installed on the prefetcher's upstream only. Media3 writes to its cache sink
17
+ * only after this `read` returns, so throwing instead of returning keeps the
18
+ * body out of the cache — and the playback path is left alone, because there a
19
+ * refusal would be an unplayable track rather than a prefetch that did not
20
+ * happen. Playback cannot poison the cache either: its `CacheDataSource` is
21
+ * built read-only.
22
+ *
23
+ * Transparent for a source with no response headers (file:// / asset://):
24
+ * those carry no `Content-Type` and are not the case this exists for.
25
+ */
26
+ @UnstableApi
27
+ internal class MediaValidatingDataSourceFactory(
28
+ private val upstream: DataSource.Factory
29
+ ) : DataSource.Factory {
30
+ override fun createDataSource(): DataSource =
31
+ MediaValidatingDataSource(upstream.createDataSource())
32
+ }
33
+
34
+ @UnstableApi
35
+ internal class MediaValidatingDataSource(
36
+ private val delegate: DataSource
37
+ ) : DataSource {
38
+
39
+ private var dataSpec: DataSpec? = null
40
+ private var contentType: String? = null
41
+ private var isHttp = false
42
+ private var startsAtResourceStart = true
43
+
44
+ /**
45
+ * Bytes read from the delegate to judge the body, held back until the verdict
46
+ * is in and then served from here before the delegate is read again. Null once
47
+ * drained — every later read passes straight through.
48
+ */
49
+ private var head: ByteArray? = null
50
+ private var headOffset = 0
51
+ private var bodyJudged = false
52
+
53
+ override fun addTransferListener(transferListener: TransferListener) {
54
+ delegate.addTransferListener(transferListener)
55
+ }
56
+
57
+ override fun open(dataSpec: DataSpec): Long {
58
+ this.dataSpec = dataSpec
59
+ head = null
60
+ headOffset = 0
61
+ bodyJudged = false
62
+ val length = delegate.open(dataSpec)
63
+ // The scheme says whether this is a remote source. Response headers cannot:
64
+ // an HTTP answer that carries no `Content-Type` has none, and that is
65
+ // precisely a body this has to judge. Casting to `HttpDataSource` cannot
66
+ // either — a wrapper is a `DataSource`, so the cast misses whenever one
67
+ // sits between this and the HTTP source.
68
+ isHttp = dataSpec.uri.scheme?.lowercase() in setOf("http", "https")
69
+ if (!isHttp) {
70
+ bodyJudged = true
71
+ return length
72
+ }
73
+ // `responseHeaders` is declared on `DataSource`, so it reads through a
74
+ // wrapper. Header names are case-insensitive and not every server or proxy
75
+ // normalises them.
76
+ contentType = delegate.responseHeaders.entries
77
+ .firstOrNull { it.key.equals("Content-Type", ignoreCase = true) }
78
+ ?.value?.firstOrNull()
79
+ startsAtResourceStart = dataSpec.position == 0L
80
+ // A declared document type is refused here, before a byte is read.
81
+ MediaResponseCheck.rejection(
82
+ contentType = contentType,
83
+ head = EMPTY,
84
+ headIsStartOfResource = false
85
+ )?.let { throw rejected(it, dataSpec, HttpDataSource.HttpDataSourceException.TYPE_OPEN) }
86
+ return length
87
+ }
88
+
89
+ override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
90
+ if (!bodyJudged) judgeBody()
91
+ val pending = head
92
+ if (pending != null) {
93
+ val remaining = pending.size - headOffset
94
+ val take = minOf(remaining, length)
95
+ pending.copyInto(buffer, offset, headOffset, headOffset + take)
96
+ headOffset += take
97
+ if (headOffset == pending.size) head = null
98
+ return take
99
+ }
100
+ return delegate.read(buffer, offset, length)
101
+ }
102
+
103
+ /**
104
+ * Pull enough of the body from the delegate to read a container signature,
105
+ * judge it, and keep it for [read] to serve. Nothing reaches the caller — and
106
+ * so nothing reaches the cache — until this has run.
107
+ */
108
+ private fun judgeBody() {
109
+ bodyJudged = true
110
+ if (!isHttp) return
111
+ val scratch = ByteArray(MediaResponseCheck.HEAD_BYTES)
112
+ var filled = 0
113
+ while (filled < scratch.size) {
114
+ val read = delegate.read(scratch, filled, scratch.size - filled)
115
+ // A zero-length read would otherwise spin here; treat it as nothing more
116
+ // to judge, the same as the end of the input.
117
+ if (read == C.RESULT_END_OF_INPUT || read == 0) break
118
+ filled += read
119
+ }
120
+ if (filled == 0) return
121
+ val body = if (filled == scratch.size) scratch else scratch.copyOf(filled)
122
+ MediaResponseCheck.rejection(
123
+ contentType = contentType,
124
+ head = body,
125
+ headIsStartOfResource = startsAtResourceStart
126
+ )?.let { throw rejected(it, dataSpec, HttpDataSource.HttpDataSourceException.TYPE_READ) }
127
+ head = body
128
+ headOffset = 0
129
+ }
130
+
131
+ private fun rejected(
132
+ rejection: MediaResponseCheck.Rejection,
133
+ spec: DataSpec?,
134
+ type: Int
135
+ ): HttpDataSource.HttpDataSourceException {
136
+ val detail = when (rejection) {
137
+ is MediaResponseCheck.Rejection.MarkupContentType ->
138
+ "declared type ${rejection.contentType} is a document, not media"
139
+ is MediaResponseCheck.Rejection.TextWithoutMediaSignature ->
140
+ "declared type ${rejection.contentType} with no recognised container signature"
141
+ MediaResponseCheck.Rejection.MarkupBody ->
142
+ "body begins as a markup or JSON document"
143
+ }
144
+ return HttpDataSource.HttpDataSourceException(
145
+ "Response is not media: $detail",
146
+ spec ?: DataSpec(Uri.EMPTY),
147
+ PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE,
148
+ type
149
+ )
150
+ }
151
+
152
+ override fun getUri(): Uri? = delegate.uri
153
+
154
+ override fun close() {
155
+ head = null
156
+ headOffset = 0
157
+ delegate.close()
158
+ }
159
+
160
+ override fun getResponseHeaders(): Map<String, List<String>> = delegate.responseHeaders
161
+
162
+ private companion object {
163
+ val EMPTY = ByteArray(0)
164
+ }
165
+ }
@@ -4,7 +4,6 @@ import android.net.Uri
4
4
  import androidx.media3.common.util.UnstableApi
5
5
  import androidx.media3.datasource.DataSource
6
6
  import androidx.media3.datasource.DataSpec
7
- import androidx.media3.datasource.HttpDataSource
8
7
  import androidx.media3.datasource.TransferListener
9
8
 
10
9
  /**
@@ -17,8 +16,8 @@ import androidx.media3.datasource.TransferListener
17
16
  * type observed during prefetch and let the playback path pass it
18
17
  * to Media3 via [androidx.media3.common.MediaItem.Builder.setMimeType].
19
18
  *
20
- * The wrapper is transparent for non-HTTP underlying sources (file://,
21
- * asset://) it forwards every call through and skips MIME capture.
19
+ * Only the prefetch path builds this, and its window holds http/https URLs
20
+ * alone, so every response it sees is a server's.
22
21
  */
23
22
  @UnstableApi
24
23
  internal class MimeCapturingDataSourceFactory(
@@ -41,12 +40,13 @@ internal class MimeCapturingDataSource(
41
40
 
42
41
  override fun open(dataSpec: DataSpec): Long {
43
42
  val length = delegate.open(dataSpec)
44
- val httpDelegate = delegate as? HttpDataSource ?: return length
45
- // HTTP headers are case-insensitive; Media3 normalises but
46
- // some servers / proxies emit lowercase. Accept either.
47
- val contentType = httpDelegate.responseHeaders["Content-Type"]?.firstOrNull()
48
- ?: httpDelegate.responseHeaders["content-type"]?.firstOrNull()
49
- onMime(dataSpec.uri.toString(), contentType)
43
+ // `responseHeaders` is declared on `DataSource`, so this reads through
44
+ // whatever wraps the HTTP source casting to `HttpDataSource` would miss,
45
+ // because a wrapper is a `DataSource`.
46
+ // Reported even when null: the sink resolves a declared type first and
47
+ // falls back to the URL's extension, so a response with no `Content-Type`
48
+ // still gets a usable type recorded.
49
+ onMime(dataSpec.uri.toString(), contentTypeOf(delegate.responseHeaders))
50
50
  return length
51
51
  }
52
52
 
@@ -61,4 +61,13 @@ internal class MimeCapturingDataSource(
61
61
 
62
62
  override fun getResponseHeaders(): Map<String, List<String>> =
63
63
  delegate.responseHeaders
64
+
65
+ private companion object {
66
+ /** HTTP header names are case-insensitive; some servers and proxies emit
67
+ * lowercase and Media3 does not always normalise. */
68
+ fun contentTypeOf(headers: Map<String, List<String>>): String? =
69
+ headers.entries
70
+ .firstOrNull { it.key.equals("Content-Type", ignoreCase = true) }
71
+ ?.value?.firstOrNull()
72
+ }
64
73
  }
@@ -28,6 +28,42 @@ import com.margelo.nitro.core.NullType
28
28
  @UnstableApi
29
29
  internal object NowPlayingFormatExtractor {
30
30
 
31
+ /**
32
+ * Average encoded bitrate of a `file://` source, from its size and duration.
33
+ *
34
+ * The figure is an average over the whole file, which is what "bitrate"
35
+ * means for a variable-rate codec anyway, and it includes the container's
36
+ * headers and any embedded artwork — a few kbps on a typical track.
37
+ *
38
+ * Returns null for any non-local source, for a file that cannot be measured,
39
+ * and for a duration the player has not resolved yet: a bitrate derived from
40
+ * an unknown duration would be arbitrary, and reporting nothing is the
41
+ * honest answer until the player knows.
42
+ */
43
+ private fun deriveLocalBitrate(player: Player, durationSec: Double?): Double? {
44
+ val uri = player.currentMediaItem?.localConfiguration?.uri ?: return null
45
+ if (!uri.scheme.equals("file", ignoreCase = true)) return null
46
+ return averageBitrateOf(uri.path, durationSec)
47
+ }
48
+
49
+ /**
50
+ * Average bits per second of the file at [path] over [durationSec].
51
+ *
52
+ * Split from its caller so the arithmetic is reachable without a player.
53
+ * Null for an unusable duration, an unreadable path, or an empty file —
54
+ * every case where the answer would be invented rather than measured.
55
+ */
56
+ internal fun averageBitrateOf(path: String?, durationSec: Double?): Double? {
57
+ if (durationSec == null || durationSec <= 0.0) return null
58
+ if (path == null) return null
59
+ // One `stat` of a local file, taken on the looper with the rest of the
60
+ // player snapshot; the answer is not worth an extra hop.
61
+ val bytes = runCatching { java.io.File(path).length() }.getOrNull() ?: return null
62
+ if (bytes <= 0L) return null
63
+ val bitsPerSecond = bytes * 8.0 / durationSec
64
+ return if (bitsPerSecond.isFinite() && bitsPerSecond > 0.0) bitsPerSecond else null
65
+ }
66
+
31
67
  /**
32
68
  * Extract the format snapshot from `player`'s currently selected
33
69
  * audio track. Returns `null` only when no audio track exists in the
@@ -53,7 +89,7 @@ internal object NowPlayingFormatExtractor {
53
89
  val mimeType = format.sampleMimeType
54
90
  val sampleRate = format.sampleRate.takeIf { it != Format.NO_VALUE }?.toDouble()
55
91
  val channelCount = format.channelCount.takeIf { it != Format.NO_VALUE }?.toDouble()
56
- val bitrate = format.bitrate.takeIf { it != Format.NO_VALUE }?.toDouble()
92
+ val reportedBitrate = format.bitrate.takeIf { it != Format.NO_VALUE }?.toDouble()
57
93
  val peakBitrate = format.peakBitrate.takeIf { it != Format.NO_VALUE }?.toDouble()
58
94
  val pcmEncoding = format.pcmEncoding.takeIf { it != Format.NO_VALUE }?.toDouble()
59
95
  val codecsString = format.codecs
@@ -61,6 +97,17 @@ internal object NowPlayingFormatExtractor {
61
97
  val durationSec = if (durationMs == C.TIME_UNSET || durationMs <= 0) null
62
98
  else durationMs / 1000.0
63
99
 
100
+ // Media3 leaves both bitrate fields unset for some codecs — FLAC reports
101
+ // NO_VALUE for average and peak alike, on local files as well as remote —
102
+ // so a consumer asking what it is playing gets nothing back. Where the
103
+ // source is on disk the average is arithmetic: encoded bytes over playing
104
+ // time. Not gated on codec; it fills in whatever the extractor did not
105
+ // report, and does nothing when Media3 already answered.
106
+ //
107
+ // A local stat only. A remote source would need its Content-Length, which
108
+ // costs a request the library deliberately does not make.
109
+ val bitrate = reportedBitrate ?: deriveLocalBitrate(player, durationSec)
110
+
64
111
  val rgData = engine?.currentReplayGainData()
65
112
  val rgActive = engine?.currentReplayGainActive() ?: false
66
113
 
@@ -9,8 +9,9 @@ import androidx.media3.common.PlaybackParameters
9
9
  * time-stretch resamples, so pitch shifts with speed.
10
10
  * - `voice` → pitch preserved (`pitch == 1`): the built-in (speech-tuned)
11
11
  * time-stretch holds pitch while changing speed.
12
- * - `music` → currently the same pitch-preserving path as `voice`; a
13
- * dedicated music-grade engine is a separate effort.
12
+ * - `music` → the same parameters as `voice` (`pitch == 1`); which
13
+ * stretcher applies them is `PitchAwareAudioProcessorChain`'s choice
14
+ * `SignalsmithStretchAudioProcessor` for `music`, Sonic otherwise.
14
15
  *
15
16
  * At `speed == 1.0` every mode yields `(1.0, 1.0)`, so the built-in
16
17
  * stretch is inactive — the auto-bypass at unity rate.
@@ -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
  *
@@ -43,9 +43,17 @@ internal object PlaybackErrorMapping {
43
43
  PlaybackErrorCode.AUTH_ERROR
44
44
  PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED ->
45
45
  PlaybackErrorCode.CLEARTEXT_NOT_PERMITTED
46
- PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND,
47
- PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE ->
46
+ PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND ->
48
47
  PlaybackErrorCode.SOURCE_UNREACHABLE
48
+ PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE ->
49
+ // The source answered, and what it sent is not media — a server
50
+ // reporting an API-level failure as a successful response, or a proxy
51
+ // serving an error page. Not SOURCE_UNREACHABLE: it was reached, and
52
+ // calling it unreachable invites a retry that cannot change the answer.
53
+ // Not HTTP_ERROR either: the status was a success. What is wrong is the
54
+ // payload, which is the same thing iOS reports when AVFoundation fails to
55
+ // parse the identical body.
56
+ PlaybackErrorCode.FILE_FORMAT_NOT_RECOGNIZED
49
57
  PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED,
50
58
  PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED,
51
59
  PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED,
@@ -85,9 +93,9 @@ internal object PlaybackErrorMapping {
85
93
 
86
94
  /**
87
95
  * 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`).
96
+ * `nativeMessage` doesn't leak auth tokens: stream URLs with
97
+ * `?token=…`-style query strings end up inside
98
+ * `PlaybackException.message`.
91
99
  *
92
100
  * Walks the full message removing EVERY `?…(whitespace|end)` run
93
101
  * 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,