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.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/build.gradle +7 -0
- package/android/consumer-rules.pro +72 -3
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +26 -22
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +20 -72
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +11 -0
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +660 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +202 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +303 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +394 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +968 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +930 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +940 -0
- package/ios/TrackPlayer+Recovery.swift +196 -0
- package/ios/TrackPlayer+Skip.swift +455 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +267 -0
- package/ios/TrackPlayer+Window.swift +195 -0
- package/ios/TrackPlayer.swift +305 -4610
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- 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
- 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
- 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
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- 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
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
|
@@ -4,6 +4,7 @@ import android.content.Context
|
|
|
4
4
|
import android.media.AudioFocusRequest
|
|
5
5
|
import android.media.AudioManager
|
|
6
6
|
import android.os.Build
|
|
7
|
+
import android.util.Log
|
|
7
8
|
import androidx.annotation.MainThread
|
|
8
9
|
import androidx.annotation.RequiresApi
|
|
9
10
|
import androidx.media3.common.AudioAttributes
|
|
@@ -281,24 +282,54 @@ internal class CrossfadeEngine(
|
|
|
281
282
|
if (duckActive) userVolume * DUCK_MULTIPLIER else userVolume
|
|
282
283
|
|
|
283
284
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
285
|
+
* Set for the duration of the two `pause()` calls a focus loss makes.
|
|
286
|
+
*
|
|
287
|
+
* This engine sets `handleAudioFocus = false` and owns focus itself, so
|
|
288
|
+
* Media3 never stamps a focus pause with its audio-focus reason: `pause()`
|
|
289
|
+
* dispatches `onPlayWhenReadyChanged(false, USER_REQUEST)` inline, and the
|
|
290
|
+
* delegate stamps whatever reason the last callback carried. While this is
|
|
291
|
+
* set the leading listener reports `AUDIO_FOCUS_LOSS` instead — the reason
|
|
292
|
+
* Media3 would have used, which the delegate maps to an interruption — so a
|
|
293
|
+
* phone call does not arrive as "the user pressed pause". Reporting the
|
|
294
|
+
* loss before the pauses does not work: the state is still PLAYING then, so
|
|
295
|
+
* that report is deduplicated and the pause's own reason overwrites it.
|
|
296
|
+
*/
|
|
297
|
+
private var pausingForFocusLoss = false
|
|
298
|
+
|
|
299
|
+
private fun pauseForFocusLoss() {
|
|
300
|
+
pausingForFocusLoss = true
|
|
301
|
+
try {
|
|
302
|
+
playerA.pause()
|
|
303
|
+
playerB.pause()
|
|
304
|
+
} finally {
|
|
305
|
+
pausingForFocusLoss = false
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Threading: AudioManager dispatches the focus callback on the Looper
|
|
311
|
+
* attached to the main thread by default (no Handler passed to the
|
|
312
|
+
* builder), so this lands on the same dispatcher the engine expects for
|
|
313
|
+
* every other call.
|
|
288
314
|
*/
|
|
289
315
|
private val focusListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
|
290
316
|
when (focusChange) {
|
|
291
317
|
AudioManager.AUDIOFOCUS_LOSS -> {
|
|
292
318
|
pausedByFocusLoss = false
|
|
293
319
|
duckActive = false
|
|
294
|
-
|
|
295
|
-
|
|
320
|
+
// Cancel before pausing. The fade ramp runs on wall-clock time and is
|
|
321
|
+
// not gated on isPlaying, so pausing the legs alone leaves it running
|
|
322
|
+
// through the interruption: it finishes during the call and swaps
|
|
323
|
+
// roles, and playback resumes on the wrong leg with the session
|
|
324
|
+
// pointing at the other one.
|
|
325
|
+
cancelFade()
|
|
326
|
+
pauseForFocusLoss()
|
|
296
327
|
}
|
|
297
328
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
298
329
|
pausedByFocusLoss = leadingPlayer.isPlaying
|
|
299
330
|
duckActive = false
|
|
300
|
-
|
|
301
|
-
|
|
331
|
+
cancelFade()
|
|
332
|
+
pauseForFocusLoss()
|
|
302
333
|
}
|
|
303
334
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
|
304
335
|
// Notification ping over media etc. — Media3's
|
|
@@ -348,7 +379,6 @@ internal class CrossfadeEngine(
|
|
|
348
379
|
private val leadingPlayerListener = object : Player.Listener {
|
|
349
380
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
|
350
381
|
if (released) return
|
|
351
|
-
delegate?.onStateMaybeChanged(this@CrossfadeEngine, playWhenReadyReason = null)
|
|
352
382
|
if (playbackState == Player.STATE_ENDED) {
|
|
353
383
|
// Leading player ended naturally — if a fade was in flight
|
|
354
384
|
// it didn't complete in time. Cancel the ramp, snap
|
|
@@ -357,13 +387,21 @@ internal class CrossfadeEngine(
|
|
|
357
387
|
if (fadeJob?.isActive == true) {
|
|
358
388
|
cancelFadeAndPromoteStandby()
|
|
359
389
|
}
|
|
390
|
+
// End signal before the generic state callback, as in GaplessEngine.
|
|
360
391
|
delegate?.onPlaybackEnded(this@CrossfadeEngine)
|
|
361
392
|
}
|
|
393
|
+
delegate?.onStateMaybeChanged(this@CrossfadeEngine, playWhenReadyReason = null)
|
|
362
394
|
}
|
|
363
395
|
|
|
364
396
|
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
|
365
397
|
if (released) return
|
|
366
|
-
|
|
398
|
+
val effectiveReason =
|
|
399
|
+
if (pausingForFocusLoss && !playWhenReady) {
|
|
400
|
+
Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS
|
|
401
|
+
} else {
|
|
402
|
+
reason
|
|
403
|
+
}
|
|
404
|
+
delegate?.onStateMaybeChanged(this@CrossfadeEngine, playWhenReadyReason = effectiveReason)
|
|
367
405
|
}
|
|
368
406
|
|
|
369
407
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
@@ -374,6 +412,9 @@ internal class CrossfadeEngine(
|
|
|
374
412
|
|
|
375
413
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
376
414
|
if (released) return
|
|
415
|
+
// The leg moved on: a later visit to the index whose preroll failed is
|
|
416
|
+
// a fresh visit (a repeat-all wrap, a skip back) and arms again.
|
|
417
|
+
armFailedForIndex = -1
|
|
377
418
|
delegate?.onTrackTransition(this@CrossfadeEngine, reason)
|
|
378
419
|
}
|
|
379
420
|
|
|
@@ -395,7 +436,22 @@ internal class CrossfadeEngine(
|
|
|
395
436
|
|
|
396
437
|
override fun onPlayerError(error: PlaybackException) {
|
|
397
438
|
if (released) return
|
|
398
|
-
|
|
439
|
+
// A failure mid-fade is the outgoing track failing on its way out, and
|
|
440
|
+
// the incoming one is already prerolled and audible. Promote it, exactly
|
|
441
|
+
// as a leading leg that *ends* mid-fade does above — there is nothing
|
|
442
|
+
// worth retrying about a track that was seconds from being discarded,
|
|
443
|
+
// and rebuilding it would restart it under a ramp that is still running,
|
|
444
|
+
// leaving two legs audible.
|
|
445
|
+
if (fadeJob?.isActive == true) {
|
|
446
|
+
// Logged on the way out: a permanent failure here — a codec or a
|
|
447
|
+
// malformed container — leaves no other trace, and "the audio glitched
|
|
448
|
+
// at a track boundary" is undiagnosable without it.
|
|
449
|
+
Log.w("RNQP.CrossfadeEngine", "leading leg failed mid-fade; promoting standby", error)
|
|
450
|
+
cancelFadeAndPromoteStandby()
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
delegate?.onError(
|
|
454
|
+
this@CrossfadeEngine, error, failedIndex = leadingPlayer.currentMediaItemIndex)
|
|
399
455
|
}
|
|
400
456
|
}
|
|
401
457
|
|
|
@@ -419,8 +475,16 @@ internal class CrossfadeEngine(
|
|
|
419
475
|
// decide whether to skip / retry; the leading leg keeps
|
|
420
476
|
// playing.
|
|
421
477
|
if (released) return
|
|
478
|
+
// Before the cancel: it clears the standby leg's items, and an empty
|
|
479
|
+
// timeline reports the masking index 0 rather than the track that
|
|
480
|
+
// failed — which would spend track 0's retry budget and name its url.
|
|
481
|
+
val failedIndex = standbyPlayer.currentMediaItemIndex
|
|
482
|
+
// The boundary keeps its own disarm marker. `cancelFade` clears
|
|
483
|
+
// `standbyArmed`, so without this the next poll tick re-arms the same
|
|
484
|
+
// failing source and keeps re-prerolling it for the rest of the track.
|
|
485
|
+
armFailedForIndex = leadingPlayer.currentMediaItemIndex
|
|
422
486
|
cancelFade()
|
|
423
|
-
delegate?.onError(this@CrossfadeEngine, error)
|
|
487
|
+
delegate?.onError(this@CrossfadeEngine, error, failedIndex = failedIndex)
|
|
424
488
|
}
|
|
425
489
|
}
|
|
426
490
|
|
|
@@ -507,15 +571,6 @@ internal class CrossfadeEngine(
|
|
|
507
571
|
|
|
508
572
|
// --- PlaybackEngine: lifecycle ---
|
|
509
573
|
|
|
510
|
-
override suspend fun prepare(
|
|
511
|
-
mediaItems: List<MediaItem>,
|
|
512
|
-
startIndex: Int,
|
|
513
|
-
startPositionMs: Long,
|
|
514
|
-
) {
|
|
515
|
-
leadingPlayer.setMediaItems(mediaItems, startIndex, startPositionMs)
|
|
516
|
-
leadingPlayer.prepare()
|
|
517
|
-
}
|
|
518
|
-
|
|
519
574
|
override fun release() {
|
|
520
575
|
if (released) return
|
|
521
576
|
released = true
|
|
@@ -524,7 +579,7 @@ internal class CrossfadeEngine(
|
|
|
524
579
|
// state, so a mid-fade revert emit here would surface a
|
|
525
580
|
// spurious onTrackChange to the OUTGOING leg of a fade that
|
|
526
581
|
// the consumer no longer cares about.
|
|
527
|
-
cancelFade(emitRevert = false)
|
|
582
|
+
cancelFade(emitRevert = false, rebindWrapper = false)
|
|
528
583
|
stopArmingPoll()
|
|
529
584
|
audioManager.abandonAudioFocusRequest(focusRequest)
|
|
530
585
|
// [leadingPlayerListener] / [standbyPlayerListener] are
|
|
@@ -573,18 +628,33 @@ internal class CrossfadeEngine(
|
|
|
573
628
|
// [standbyArmed] could remain true against an empty / unrelated
|
|
574
629
|
// queue and [maybeArmCrossfade] would early-return forever.
|
|
575
630
|
standbyArmed = false
|
|
631
|
+
armFailedForIndex = -1
|
|
576
632
|
leadingPlayer.setMediaItems(items, startIndex, startPositionMs)
|
|
577
633
|
leadingPlayer.prepare()
|
|
578
634
|
standbyPlayer.stop()
|
|
579
635
|
standbyPlayer.clearMediaItems()
|
|
580
636
|
standbyPlayer.volume = 0f
|
|
581
637
|
// Empty queue — no boundaries to arm. Stop the poll until
|
|
582
|
-
// play() restarts it
|
|
638
|
+
// play() restarts it, and park the leg: draining is not pausing, so
|
|
639
|
+
// `playWhenReady` would otherwise carry into the next queue seated here.
|
|
583
640
|
if (items.isEmpty()) {
|
|
584
641
|
stopArmingPoll()
|
|
642
|
+
leadingPlayer.pause()
|
|
585
643
|
}
|
|
586
644
|
}
|
|
587
645
|
|
|
646
|
+
override fun settlePendingFade() {
|
|
647
|
+
// `emitRevert = true`: the caller keeps playing what the leading leg holds
|
|
648
|
+
// rather than seating a new active track, so without the revert the audio
|
|
649
|
+
// returns to the outgoing track while the canonical index still names the
|
|
650
|
+
// incoming one, and no track change follows to correct it.
|
|
651
|
+
cancelFade(emitRevert = true)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
override fun settlePendingFadeByPromoting() {
|
|
655
|
+
if (fadeJob?.isActive == true) cancelFadeAndPromoteStandby()
|
|
656
|
+
}
|
|
657
|
+
|
|
588
658
|
override fun addMediaItems(items: List<MediaItem>, insertBefore: Int) {
|
|
589
659
|
// Queue mutation invalidates the standby leg's pre-loaded
|
|
590
660
|
// future. Cancel any in-flight fade before mutating the
|
|
@@ -593,17 +663,22 @@ internal class CrossfadeEngine(
|
|
|
593
663
|
// emitRevert=false: the TrackPlayer mutation body recomputes the
|
|
594
664
|
// canonical index, so a mid-fade revert here would clobber it.
|
|
595
665
|
cancelFade(emitRevert = false)
|
|
666
|
+
// The failed-preroll marker is an index; after a mutation it may name a
|
|
667
|
+
// different track.
|
|
668
|
+
armFailedForIndex = -1
|
|
596
669
|
leadingPlayer.addMediaItems(insertBefore, items)
|
|
597
670
|
}
|
|
598
671
|
|
|
599
672
|
override fun removeMediaItems(indices: IntArray) {
|
|
600
673
|
cancelFade(emitRevert = false)
|
|
674
|
+
armFailedForIndex = -1
|
|
601
675
|
val sorted = indices.toMutableList().apply { sortDescending() }
|
|
602
676
|
for (i in sorted) leadingPlayer.removeMediaItem(i)
|
|
603
677
|
}
|
|
604
678
|
|
|
605
679
|
override fun moveMediaItem(fromIndex: Int, toIndex: Int) {
|
|
606
680
|
cancelFade(emitRevert = false)
|
|
681
|
+
armFailedForIndex = -1
|
|
607
682
|
leadingPlayer.moveMediaItem(fromIndex, toIndex)
|
|
608
683
|
}
|
|
609
684
|
|
|
@@ -826,13 +901,58 @@ internal class CrossfadeEngine(
|
|
|
826
901
|
recomputeLegFromConfig(isA = false)
|
|
827
902
|
}
|
|
828
903
|
|
|
904
|
+
/**
|
|
905
|
+
* Resolves the consumer's ReplayGain claim over the queue entry at a
|
|
906
|
+
* given index. Installed by [TrackPlayer] at engine construction and
|
|
907
|
+
* invoked on every read rather than cached: a `setQueue` carrying
|
|
908
|
+
* corrected values then takes effect at the next recompute, with no
|
|
909
|
+
* cache to invalidate at the wrong moment.
|
|
910
|
+
*
|
|
911
|
+
* A `null` return means the consumer supplied nothing for that entry
|
|
912
|
+
* and the file's own tags stand. A non-null result with a `null`
|
|
913
|
+
* [SuppliedReplayGain.data] means the consumer claimed the track and
|
|
914
|
+
* gave nothing usable — a different state, and one the merge has to
|
|
915
|
+
* be able to tell apart.
|
|
916
|
+
*/
|
|
917
|
+
internal var suppliedReplayGainResolver: ((Int) -> SuppliedReplayGain?)? = null
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* The ReplayGain a leg's current item actually plays with: the
|
|
921
|
+
* consumer's claim where there is one, that leg's cached tags
|
|
922
|
+
* otherwise. Every read of ReplayGain on this engine goes through
|
|
923
|
+
* here, so the merge lands before the first value reaches the audio
|
|
924
|
+
* or a listener.
|
|
925
|
+
*
|
|
926
|
+
* Per leg, because each leg holds its own item and its own processor.
|
|
927
|
+
* The index is that leg's own `currentMediaItemIndex`, which is
|
|
928
|
+
* meaningful for both legs: [armCrossfade] loads the FULL queue onto
|
|
929
|
+
* standby rather than the tail, so both legs count in the same global
|
|
930
|
+
* index space as the consumer-supplied queue.
|
|
931
|
+
*/
|
|
932
|
+
private fun effectiveReplayGainData(isA: Boolean): ReplayGainData? {
|
|
933
|
+
val player = if (isA) playerA else playerB
|
|
934
|
+
val cached = if (isA) cachedReplayGainDataA else cachedReplayGainDataB
|
|
935
|
+
// An empty timeline reports index 0, which would name the first
|
|
936
|
+
// queue entry for a leg holding no items at all — the standby leg
|
|
937
|
+
// before it is armed. Nothing is claimed until there is an item for
|
|
938
|
+
// the index to mean something.
|
|
939
|
+
val supplied = if (player.mediaItemCount == 0) null
|
|
940
|
+
else suppliedReplayGainResolver?.invoke(player.currentMediaItemIndex)
|
|
941
|
+
return ReplayGainData.merge(
|
|
942
|
+
suppliedClaimed = supplied != null,
|
|
943
|
+
supplied = supplied?.data,
|
|
944
|
+
detected = cached,
|
|
945
|
+
)
|
|
946
|
+
}
|
|
947
|
+
|
|
829
948
|
@MainThread
|
|
830
949
|
private fun recomputeLegFromConfig(isA: Boolean) {
|
|
831
950
|
val mode = ReplayGainConfig.getMode()
|
|
951
|
+
val gain = computeLinearGain(effectiveReplayGainData(isA), mode)
|
|
832
952
|
if (isA) {
|
|
833
|
-
replayGainProcessorA.setLinearGain(
|
|
953
|
+
replayGainProcessorA.setLinearGain(gain)
|
|
834
954
|
} else {
|
|
835
|
-
replayGainProcessorB.setLinearGain(
|
|
955
|
+
replayGainProcessorB.setLinearGain(gain)
|
|
836
956
|
}
|
|
837
957
|
}
|
|
838
958
|
|
|
@@ -862,18 +982,19 @@ internal class CrossfadeEngine(
|
|
|
862
982
|
}
|
|
863
983
|
|
|
864
984
|
/**
|
|
865
|
-
* Snapshot the LEADING leg's
|
|
985
|
+
* Snapshot the ReplayGain the LEADING leg's item plays with, for the
|
|
866
986
|
* [NowPlayingFormatExtractor] surface. The active item from JS's
|
|
867
987
|
* point of view is always the leading leg's current item except
|
|
868
988
|
* during the fade window — at which point [activePlayer] flips to
|
|
869
|
-
* standby and this read should follow. Returns `null` when
|
|
870
|
-
*
|
|
989
|
+
* standby and this read should follow. Returns `null` when nothing is
|
|
990
|
+
* supplied for the active leg's track and no usable RG tags have been
|
|
991
|
+
* parsed for it either.
|
|
871
992
|
*/
|
|
872
993
|
override fun currentReplayGainData(): ReplayGainData? =
|
|
873
|
-
|
|
994
|
+
effectiveReplayGainData(isA = activePlayer === playerA)
|
|
874
995
|
|
|
875
996
|
/**
|
|
876
|
-
* `true` when the active leg's
|
|
997
|
+
* `true` when the active leg's ReplayGain carries a tag for the
|
|
877
998
|
* current [ReplayGainConfig.getMode]. Used by the NowPlayingFormat
|
|
878
999
|
* path to populate `replayGainActive`.
|
|
879
1000
|
*/
|
|
@@ -932,37 +1053,43 @@ internal class CrossfadeEngine(
|
|
|
932
1053
|
return null
|
|
933
1054
|
}
|
|
934
1055
|
|
|
935
|
-
// --- PlaybackEngine: handoff ---
|
|
936
|
-
|
|
937
|
-
override suspend fun handoff(to: PlaybackEngine, atTrackBoundary: Boolean) {
|
|
938
|
-
// Engine handoff (gapless ↔ crossfade swap at TrackPlayer
|
|
939
|
-
// level) is wired in the swapEngine path on PlaybackService
|
|
940
|
-
// alongside this engine's own boundary detection. The
|
|
941
|
-
// crossfade-to-other-engine handoff lives at PlaybackService
|
|
942
|
-
// because TrackPlayer holds the canonical queue model that
|
|
943
|
-
// the receiving engine needs.
|
|
944
|
-
throw NotImplementedError(
|
|
945
|
-
"PlaybackEngine.handoff is driven by PlaybackService.swapEngine; do not call directly"
|
|
946
|
-
)
|
|
947
|
-
}
|
|
948
|
-
|
|
949
1056
|
// --- Fade orchestration ---
|
|
950
1057
|
|
|
951
1058
|
/**
|
|
952
|
-
* Cancel any in-flight fade and snap the leading leg back to
|
|
953
|
-
*
|
|
954
|
-
*
|
|
1059
|
+
* Cancel any in-flight fade and snap the leading leg back to full volume +
|
|
1060
|
+
* drop the standby leg's item. Called from manual skip / pause / seek paths
|
|
1061
|
+
* and the route-degradation callback. [emitRevert] controls the JS-facing
|
|
1062
|
+
* revert only. Re-binding the MediaSession wrapper is not optional and is
|
|
1063
|
+
* governed by [rebindWrapper], which only [release] clears — see the note on
|
|
1064
|
+
* the revert block below.
|
|
955
1065
|
*/
|
|
956
|
-
internal fun cancelFade(emitRevert: Boolean = true) {
|
|
1066
|
+
internal fun cancelFade(emitRevert: Boolean = true, rebindWrapper: Boolean = true) {
|
|
957
1067
|
val job = fadeJob
|
|
958
1068
|
val wasMidFade = job?.isActive == true
|
|
959
1069
|
val leadingIdx = leadingPlayer.currentMediaItemIndex
|
|
960
1070
|
fadeJob = null
|
|
961
1071
|
job?.cancel()
|
|
1072
|
+
// Disarm as well as cancel. [standbyArmed] is what stops
|
|
1073
|
+
// [maybeArmCrossfade] arming twice for one track, so leaving it set
|
|
1074
|
+
// after a cancel means it stays set for the rest of the session: every later
|
|
1075
|
+
// arm attempt returns early and each boundary becomes a hard cut, with
|
|
1076
|
+
// nothing surfaced and no way back except a seek, skip or new queue.
|
|
1077
|
+
// Cancelling before the preroll settles leaves it set with no fade job
|
|
1078
|
+
// to clear it, which is the window a pause lands in.
|
|
1079
|
+
standbyArmed = false
|
|
962
1080
|
leadingPlayer.volume = userVolume
|
|
963
1081
|
standbyPlayer.volume = 0f
|
|
964
1082
|
standbyPlayer.stop()
|
|
965
1083
|
standbyPlayer.clearMediaItems()
|
|
1084
|
+
if (wasMidFade && rebindWrapper) {
|
|
1085
|
+
// Re-bind first and unconditionally. A mid-fade cancel stops and clears
|
|
1086
|
+
// the standby leg, but the fade's start already pointed the session at
|
|
1087
|
+
// it, so leaving the binding alone hands the lock screen, notification,
|
|
1088
|
+
// AVRCP and Android Auto a player with no items — a blank surface and a
|
|
1089
|
+
// dead scrubber while audio continues on the leading leg. This is
|
|
1090
|
+
// independent of whether JS wants a revert event.
|
|
1091
|
+
onCrossfadeCancel?.invoke()
|
|
1092
|
+
}
|
|
966
1093
|
if (wasMidFade && emitRevert) {
|
|
967
1094
|
// Mid-fade cancel: fade-start fired `onCrossfadeBegin` →
|
|
968
1095
|
// delegate moved JS-facing state to the incoming leg. Revert
|
|
@@ -976,7 +1103,6 @@ internal class CrossfadeEngine(
|
|
|
976
1103
|
// track state, so an intermediate revert here would surface
|
|
977
1104
|
// a spurious onTrackChange to the OUTGOING leg of a fade
|
|
978
1105
|
// the consumer no longer cares about).
|
|
979
|
-
onCrossfadeCancel?.invoke()
|
|
980
1106
|
delegate?.onCrossfadeCancel(this, leadingIdx)
|
|
981
1107
|
}
|
|
982
1108
|
}
|
|
@@ -1057,7 +1183,7 @@ internal class CrossfadeEngine(
|
|
|
1057
1183
|
// listener (which forwards onTrackTransition / onPlaybackStateChanged
|
|
1058
1184
|
// / onIsPlayingChanged / onPlayerError to the engine delegate)
|
|
1059
1185
|
// fires from the player that's now leading. Without this the
|
|
1060
|
-
// listeners stay
|
|
1186
|
+
// listeners stay attached to playerA / playerB and post-swap events
|
|
1061
1187
|
// route to the wrong listener, swallowing track transitions on
|
|
1062
1188
|
// the new leading leg.
|
|
1063
1189
|
//
|
|
@@ -1117,6 +1243,14 @@ internal class CrossfadeEngine(
|
|
|
1117
1243
|
armingJob = null
|
|
1118
1244
|
}
|
|
1119
1245
|
|
|
1246
|
+
/**
|
|
1247
|
+
* The leading index whose standby preroll failed. Arming is skipped for
|
|
1248
|
+
* that boundary so one failing source is not re-prerolled on every poll
|
|
1249
|
+
* tick; cleared when the leading leg transitions and on every queue
|
|
1250
|
+
* mutation, since the index is only meaningful for this visit to it.
|
|
1251
|
+
*/
|
|
1252
|
+
private var armFailedForIndex: Int = -1
|
|
1253
|
+
|
|
1120
1254
|
/**
|
|
1121
1255
|
* Decide whether to arm the standby preroll + run the fade based
|
|
1122
1256
|
* on the leading leg's current position relative to its
|
|
@@ -1125,6 +1259,7 @@ internal class CrossfadeEngine(
|
|
|
1125
1259
|
*/
|
|
1126
1260
|
private fun maybeArmCrossfade() {
|
|
1127
1261
|
if (standbyArmed) return
|
|
1262
|
+
if (leadingPlayer.currentMediaItemIndex == armFailedForIndex) return
|
|
1128
1263
|
if (fadeJob?.isActive == true) return
|
|
1129
1264
|
if (!leadingPlayer.isPlaying) return
|
|
1130
1265
|
if (crossfadeDurationMs <= 0L) return
|
|
@@ -1252,8 +1387,8 @@ internal class CrossfadeEngine(
|
|
|
1252
1387
|
// forwarding wrapper to the standby `ExoPlayer` so MediaSession's
|
|
1253
1388
|
// auto-published metadata + scrubber flip to the incoming
|
|
1254
1389
|
// track from 00:00 the moment it begins audibly playing. Then
|
|
1255
|
-
// notify the delegate to fire JS-facing `onTrackChange
|
|
1256
|
-
//
|
|
1390
|
+
// notify the delegate to fire JS-facing `onTrackChange`, refresh the
|
|
1391
|
+
// now-playing format and recompute skip capability.
|
|
1257
1392
|
onCrossfadeBegin?.invoke()
|
|
1258
1393
|
delegate?.onCrossfadeBegin(this, incomingIdx)
|
|
1259
1394
|
}
|
|
@@ -7,6 +7,8 @@ import androidx.media3.common.util.UnstableApi
|
|
|
7
7
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
8
8
|
import com.margelo.nitro.NitroModules
|
|
9
9
|
import com.margelo.nitro.core.Promise
|
|
10
|
+
import kotlinx.coroutines.Dispatchers
|
|
11
|
+
import kotlinx.coroutines.withContext
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* 10-band parametric equalizer HybridObject. Picks
|
|
@@ -182,7 +184,11 @@ class Equalizer @JvmOverloads constructor(
|
|
|
182
184
|
internal fun setEnabledInternal(enabled: Boolean) {
|
|
183
185
|
if (engine.isEnabled() == enabled) return
|
|
184
186
|
engine.setEnabled(enabled)
|
|
185
|
-
|
|
187
|
+
// Emit what the engine committed, not what was asked for: a released
|
|
188
|
+
// engine drops the call, and `onEnabledChange` registration replays
|
|
189
|
+
// `engine.isEnabled()`, so emitting the request here would hand
|
|
190
|
+
// subscribers a value `isEnabled()` contradicts.
|
|
191
|
+
onEnabledChangeListeners.forEach { it(engine.isEnabled()) }
|
|
186
192
|
}
|
|
187
193
|
|
|
188
194
|
internal fun setBandGainInternal(index: Int, gainDb: Float) {
|
|
@@ -233,26 +239,36 @@ class Equalizer @JvmOverloads constructor(
|
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
// --- JS-facing async surface ---
|
|
242
|
+
//
|
|
243
|
+
// Every mutation of the engine runs on the main looper: `attachAll` is
|
|
244
|
+
// driven there by the playback engine's session-id listener, and running
|
|
245
|
+
// the setters on the `Promise.async` pool alongside it let a setter iterate
|
|
246
|
+
// an instance the looper had just released, two setters interleave their
|
|
247
|
+
// band writes, or a setter miss an instance an attach was inserting. One
|
|
248
|
+
// executor for all of it closes those.
|
|
236
249
|
|
|
237
250
|
override fun setEnabled(enabled: Boolean): Promise<Unit> =
|
|
238
|
-
Promise.async { setEnabledInternal(enabled) }
|
|
251
|
+
Promise.async { withContext(Dispatchers.Main.immediate) { setEnabledInternal(enabled) } }
|
|
239
252
|
|
|
240
253
|
override fun isEnabled(): Boolean = engine.isEnabled()
|
|
241
254
|
|
|
242
255
|
override fun getBands(): Array<EqualizerBand> = currentBandsSnapshot()
|
|
243
256
|
|
|
244
257
|
override fun setBandGain(index: Double, gainDb: Double): Promise<Unit> =
|
|
245
|
-
Promise.async {
|
|
258
|
+
Promise.async {
|
|
259
|
+
withContext(Dispatchers.Main.immediate) { setBandGainInternal(index.toInt(), gainDb.toFloat()) }
|
|
260
|
+
}
|
|
246
261
|
|
|
247
262
|
override fun setAllBandGains(gains: DoubleArray): Promise<Unit> =
|
|
248
263
|
Promise.async {
|
|
249
264
|
val asFloats = FloatArray(gains.size) { i -> gains[i].toFloat() }
|
|
250
|
-
setAllBandGainsInternal(asFloats)
|
|
265
|
+
withContext(Dispatchers.Main.immediate) { setAllBandGainsInternal(asFloats) }
|
|
251
266
|
}
|
|
252
267
|
|
|
253
268
|
override fun applyPreset(name: String): Promise<Unit> =
|
|
254
269
|
Promise.async {
|
|
255
|
-
|
|
270
|
+
val applied = withContext(Dispatchers.Main.immediate) { applyPresetInternal(name) }
|
|
271
|
+
if (!applied) {
|
|
256
272
|
throw IllegalArgumentException("Equalizer preset not found: $name")
|
|
257
273
|
}
|
|
258
274
|
}
|
|
@@ -288,13 +304,17 @@ class Equalizer @JvmOverloads constructor(
|
|
|
288
304
|
if (EqualizerPresets.getByName(name) != null) {
|
|
289
305
|
throw IllegalArgumentException("Cannot overwrite built-in preset: $name")
|
|
290
306
|
}
|
|
291
|
-
|
|
307
|
+
// The gains are read on the looper that writes them; the `commit` to
|
|
308
|
+
// preferences stays off it.
|
|
309
|
+
val gains = withContext(Dispatchers.Main.immediate) { engine.getBandGains() }
|
|
310
|
+
customPresetStore?.save(name, gains)
|
|
292
311
|
}
|
|
293
312
|
|
|
294
313
|
override fun deleteCustomPreset(name: String): Promise<Unit> =
|
|
295
314
|
Promise.async { deleteCustomPresetInternal(name) }
|
|
296
315
|
|
|
297
|
-
override fun reset(): Promise<Unit> =
|
|
316
|
+
override fun reset(): Promise<Unit> =
|
|
317
|
+
Promise.async { withContext(Dispatchers.Main.immediate) { resetInternal() } }
|
|
298
318
|
|
|
299
319
|
override fun onBandChange(
|
|
300
320
|
callback: (bands: Array<EqualizerBand>) -> Unit
|
|
@@ -5,6 +5,7 @@ import android.os.Build
|
|
|
5
5
|
import android.util.Log
|
|
6
6
|
import androidx.annotation.RequiresApi
|
|
7
7
|
import androidx.media3.common.C
|
|
8
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* 10-band pre-EQ for API 28+ backed by [DynamicsProcessing].
|
|
@@ -29,11 +30,16 @@ import androidx.media3.common.C
|
|
|
29
30
|
* [attach] `channelCount` parameter is therefore ignored on this
|
|
30
31
|
* backend — retained on the interface for future per-track engines.
|
|
31
32
|
*
|
|
32
|
-
* Threading:
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
* Threading: not serialised. [attachAll] / [detachAll] run on the
|
|
34
|
+
* service main looper from the audio-session-id listener, and on the
|
|
35
|
+
* thread constructing an [Equalizer] (its init wires the engine, and
|
|
36
|
+
* the listener registration replays the current session ids
|
|
37
|
+
* synchronously). [setBandGain] / [setAllBandGains] / [setEnabled] run
|
|
38
|
+
* on the `Promise.async` pool, so two JS calls may run concurrently;
|
|
39
|
+
* [isEnabled] / [getBandGains] run synchronously on the JS thread.
|
|
40
|
+
* `instances` is a `ConcurrentHashMap` and the flags are `@Volatile`,
|
|
41
|
+
* which rules out structural corruption and stale reads. What remains
|
|
42
|
+
* is documented on the fields.
|
|
37
43
|
* [release] briefly blocks while the native effect tears down (a
|
|
38
44
|
* few ms typical, longer on slow devices) — callers may dispatch
|
|
39
45
|
* off-thread if a teardown burst risks a UI hitch.
|
|
@@ -54,16 +60,26 @@ class EqualizerEngine : IEqualizerEngine {
|
|
|
54
60
|
private const val LOG_TAG = "EqualizerEngine"
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
// Written by the setters and by attach, all on the main looper:
|
|
64
|
+
// `Equalizer` hops every JS-facing mutation there, and the playback
|
|
65
|
+
// engine's session-id listener drives attach there. Nothing touches
|
|
66
|
+
// this off the looper.
|
|
57
67
|
private val gainsDb: FloatArray = FloatArray(BAND_COUNT)
|
|
58
|
-
private var enabled: Boolean = false
|
|
68
|
+
@Volatile private var enabled: Boolean = false
|
|
59
69
|
/**
|
|
60
70
|
* One [DynamicsProcessing] instance per attached audio session.
|
|
61
71
|
* Crossfade-window callers can hold two simultaneously
|
|
62
72
|
* (outgoing + incoming legs) so EQ continuity is preserved
|
|
63
73
|
* across the fade. Single-session callers see exactly one entry.
|
|
74
|
+
* Iteration order is never relied on: the attach diff is set
|
|
75
|
+
* arithmetic and every other loop applies one operation per entry.
|
|
76
|
+
* Inserts use `putIfAbsent` and removals go through `remove`, so two
|
|
77
|
+
* attaches racing on one session release the loser instead of
|
|
78
|
+
* leaking it, and a detach cannot drop an instance inserted between
|
|
79
|
+
* its iteration and its clear.
|
|
64
80
|
*/
|
|
65
|
-
private val instances: MutableMap<Int, DynamicsProcessing> =
|
|
66
|
-
private var released: Boolean = false
|
|
81
|
+
private val instances: MutableMap<Int, DynamicsProcessing> = ConcurrentHashMap()
|
|
82
|
+
@Volatile private var released: Boolean = false
|
|
67
83
|
|
|
68
84
|
/**
|
|
69
85
|
* Diff [sessionIds] against the current per-session map:
|
|
@@ -104,6 +120,7 @@ class EqualizerEngine : IEqualizerEngine {
|
|
|
104
120
|
results[id] = true
|
|
105
121
|
continue
|
|
106
122
|
}
|
|
123
|
+
var instance: DynamicsProcessing? = null
|
|
107
124
|
results[id] = try {
|
|
108
125
|
val config = DynamicsProcessing.Config.Builder(
|
|
109
126
|
DynamicsProcessing.VARIANT_FAVOR_FREQUENCY_RESOLUTION,
|
|
@@ -113,13 +130,18 @@ class EqualizerEngine : IEqualizerEngine {
|
|
|
113
130
|
/* postEqInUse = */ false, /* postEqBandCount = */ 0,
|
|
114
131
|
/* limiterInUse = */ false,
|
|
115
132
|
).build()
|
|
116
|
-
|
|
117
|
-
instances[id] = instance
|
|
133
|
+
instance = DynamicsProcessing(PRIORITY, id, config)
|
|
118
134
|
applyAllBandsToInstance(instance)
|
|
119
135
|
instance.setEnabled(enabled)
|
|
136
|
+
// Insert only once the instance is fully configured, so a throw
|
|
137
|
+
// above leaves no entry for isAttached() to report. A concurrent
|
|
138
|
+
// attach that got here first keeps its instance; ours is released.
|
|
139
|
+
val existing = instances.putIfAbsent(id, instance)
|
|
140
|
+
if (existing != null) instance.release()
|
|
120
141
|
true
|
|
121
142
|
} catch (e: RuntimeException) {
|
|
122
143
|
Log.w(LOG_TAG, "DynamicsProcessing attach failed for session $id", e)
|
|
144
|
+
instance?.release()
|
|
123
145
|
false
|
|
124
146
|
}
|
|
125
147
|
}
|
|
@@ -131,8 +153,7 @@ class EqualizerEngine : IEqualizerEngine {
|
|
|
131
153
|
* gains + enabled state survive for the next [attachAll].
|
|
132
154
|
*/
|
|
133
155
|
override fun detachAll() {
|
|
134
|
-
for (
|
|
135
|
-
instances.clear()
|
|
156
|
+
for (id in instances.keys) instances.remove(id)?.release()
|
|
136
157
|
}
|
|
137
158
|
|
|
138
159
|
/** True when the engine is attached to at least one audio session. */
|