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
|
@@ -3,6 +3,7 @@ package com.margelo.nitro.queueplayer
|
|
|
3
3
|
import android.media.audiofx.Equalizer
|
|
4
4
|
import android.util.Log
|
|
5
5
|
import androidx.media3.common.C
|
|
6
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
6
7
|
import kotlin.math.abs
|
|
7
8
|
import kotlin.math.roundToInt
|
|
8
9
|
|
|
@@ -27,7 +28,16 @@ import kotlin.math.roundToInt
|
|
|
27
28
|
* also clamped to the device's reported `getBandLevelRange()` to
|
|
28
29
|
* avoid IllegalArgumentException on aggressive devices.
|
|
29
30
|
*
|
|
30
|
-
* Threading:
|
|
31
|
+
* Threading: not serialised. [attachAll] / [detachAll] run on the
|
|
32
|
+
* service main looper from the audio-session-id listener, and on the
|
|
33
|
+
* thread constructing an [Equalizer] (its init wires the engine, and
|
|
34
|
+
* the listener registration replays the current session ids
|
|
35
|
+
* synchronously). [setBandGain] / [setAllBandGains] / [setEnabled] run
|
|
36
|
+
* on the `Promise.async` pool, so two JS calls may run concurrently;
|
|
37
|
+
* [isEnabled] / [getBandGains] run synchronously on the JS thread.
|
|
38
|
+
* `instances` is a `ConcurrentHashMap` and the flags are `@Volatile`,
|
|
39
|
+
* which rules out structural corruption and stale reads. What remains
|
|
40
|
+
* is documented on the fields.
|
|
31
41
|
* [release] briefly blocks while the native effect tears down.
|
|
32
42
|
*/
|
|
33
43
|
class EqualizerLegacyEngine : IEqualizerEngine {
|
|
@@ -70,7 +80,7 @@ class EqualizerLegacyEngine : IEqualizerEngine {
|
|
|
70
80
|
|
|
71
81
|
/**
|
|
72
82
|
* Per-session equalizer instance + the per-device-derived
|
|
73
|
-
* mapping & gain bounds
|
|
83
|
+
* mapping & gain bounds read once at attach time. Each session
|
|
74
84
|
* derives independently so AOSP-fork variance across sessions
|
|
75
85
|
* is handled correctly.
|
|
76
86
|
*/
|
|
@@ -81,10 +91,19 @@ class EqualizerLegacyEngine : IEqualizerEngine {
|
|
|
81
91
|
val deviceMaxMb: Short,
|
|
82
92
|
)
|
|
83
93
|
|
|
94
|
+
// Written by the setters and by attach, all on the main looper:
|
|
95
|
+
// `Equalizer` hops every JS-facing mutation there, and the playback
|
|
96
|
+
// engine's session-id listener drives attach there. Nothing touches
|
|
97
|
+
// this off the looper.
|
|
84
98
|
private val gainsDb: FloatArray = FloatArray(BAND_COUNT)
|
|
85
|
-
private var enabled: Boolean = false
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
@Volatile private var enabled: Boolean = false
|
|
100
|
+
// Iteration order is never relied on: the attach diff is set arithmetic
|
|
101
|
+
// and every other loop applies one operation per entry. Inserts use
|
|
102
|
+
// `putIfAbsent` and removals go through `remove`, so two attaches
|
|
103
|
+
// racing on one session release the loser instead of leaking it, and a
|
|
104
|
+
// detach cannot drop a slot inserted between its iteration and clear.
|
|
105
|
+
private val instances: MutableMap<Int, Slot> = ConcurrentHashMap()
|
|
106
|
+
@Volatile private var released: Boolean = false
|
|
88
107
|
|
|
89
108
|
/**
|
|
90
109
|
* Diff [sessionIds] against the current per-session map.
|
|
@@ -138,9 +157,14 @@ class EqualizerLegacyEngine : IEqualizerEngine {
|
|
|
138
157
|
deviceMinMb = range[0],
|
|
139
158
|
deviceMaxMb = range[1],
|
|
140
159
|
)
|
|
141
|
-
instances[audioSessionId] = slot
|
|
142
160
|
applyAllBandsToSlot(slot)
|
|
143
161
|
instance.enabled = enabled
|
|
162
|
+
// Insert only once the slot is fully configured, so a throw above
|
|
163
|
+
// leaves no entry for isAttached() to report against a released
|
|
164
|
+
// native handle. A concurrent attach that got here first keeps its
|
|
165
|
+
// slot; ours is released.
|
|
166
|
+
val existing = instances.putIfAbsent(audioSessionId, slot)
|
|
167
|
+
if (existing != null) instance.release()
|
|
144
168
|
true
|
|
145
169
|
} catch (e: RuntimeException) {
|
|
146
170
|
Log.w(LOG_TAG, "Equalizer attach failed for session $audioSessionId", e)
|
|
@@ -150,8 +174,7 @@ class EqualizerLegacyEngine : IEqualizerEngine {
|
|
|
150
174
|
}
|
|
151
175
|
|
|
152
176
|
override fun detachAll() {
|
|
153
|
-
for (
|
|
154
|
-
instances.clear()
|
|
177
|
+
for (id in instances.keys) instances.remove(id)?.equalizer?.release()
|
|
155
178
|
}
|
|
156
179
|
|
|
157
180
|
override fun isAttached(): Boolean = instances.isNotEmpty()
|
|
@@ -38,10 +38,9 @@ internal class FFTProcessorTee(
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// 2× ring so we can copy the latest fftSize-sample window without
|
|
41
|
-
// worrying about mid-window wraparound.
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// two so wrap is a free bitmask.
|
|
41
|
+
// worrying about mid-window wraparound. handleBuffer is the only
|
|
42
|
+
// writer and reader of the ring, and it runs on the audio render
|
|
43
|
+
// thread. Length is a power of two so wrap is a free bitmask.
|
|
45
44
|
private val ring = FloatArray(fftSize * 2)
|
|
46
45
|
private val ringMask: Int = ring.size - 1
|
|
47
46
|
private var writeIdx = 0L
|
|
@@ -57,21 +56,33 @@ internal class FFTProcessorTee(
|
|
|
57
56
|
private val magsOut = FloatArray(fftSize / 2)
|
|
58
57
|
private val samplesOut: FloatArray? = if (includeSamples) FloatArray(fftSize) else null
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
/**
|
|
60
|
+
* The stream format a buffer is decoded under. Immutable and published
|
|
61
|
+
* as a whole through the single [format] reference, so a decode never
|
|
62
|
+
* mixes one flush's channel count with another's encoding.
|
|
63
|
+
*/
|
|
64
|
+
private class Format(val sampleRate: Int, val channelCount: Int, val encoding: Int) {
|
|
65
|
+
val bytesPerSample: Int = bytesPerSample(encoding)
|
|
66
|
+
val bytesPerFrame: Int = bytesPerSample * channelCount
|
|
67
|
+
}
|
|
65
68
|
|
|
69
|
+
// flush() runs on whichever thread attaches the sink (see
|
|
70
|
+
// MultiplexingAudioBufferSink.primeNewSink) and on the render thread;
|
|
71
|
+
// handleBuffer runs on the render thread and reads this once per call.
|
|
72
|
+
@Volatile private var format = Format(44100, 2, C.ENCODING_PCM_16BIT)
|
|
73
|
+
|
|
74
|
+
// `writeIdx` (above) and `lastEmitNs` are written by handleBuffer and
|
|
75
|
+
// reset by flush, with no ordering between the two. A flush racing a
|
|
76
|
+
// handleBuffer can lose the `writeIdx` reset, so exactly one emitted
|
|
77
|
+
// frame carries up to fftSize pre-flush samples — comparable to the
|
|
78
|
+
// delayed first frame MultiplexingAudioBufferSink documents for a
|
|
79
|
+
// double flush. A lost `lastEmitNs` reset delays the first post-flush
|
|
80
|
+
// frame by at most intervalMs. On a 32-bit ABI the Long resets can
|
|
81
|
+
// tear; the outcomes are the same one frame.
|
|
66
82
|
private var lastEmitNs: Long = 0L
|
|
67
83
|
|
|
68
84
|
override fun flush(sampleRateHz: Int, channelCount: Int, encoding: Int) {
|
|
69
|
-
|
|
70
|
-
this.channelCount = channelCount.coerceAtLeast(1)
|
|
71
|
-
this.encoding = encoding
|
|
72
|
-
val sampleSize = bytesPerSample(encoding)
|
|
73
|
-
this.bytesPerSampleCached = sampleSize
|
|
74
|
-
this.bytesPerFrame = sampleSize * this.channelCount
|
|
85
|
+
format = Format(sampleRateHz, channelCount.coerceAtLeast(1), encoding)
|
|
75
86
|
// Reset write index on stream change so the ring doesn't carry
|
|
76
87
|
// samples from the previous track / sample-rate config across.
|
|
77
88
|
writeIdx = 0L
|
|
@@ -79,17 +90,19 @@ internal class FFTProcessorTee(
|
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
override fun handleBuffer(buffer: ByteBuffer) {
|
|
82
|
-
|
|
93
|
+
// One read: every field this call uses comes from the same flush.
|
|
94
|
+
val fmt = format
|
|
95
|
+
if (fmt.bytesPerFrame == 0 || fmt.bytesPerSample == 0) return // unsupported encoding
|
|
83
96
|
// Read via a duplicated view so the source buffer's position +
|
|
84
97
|
// byte order are not mutated. Media3 documents the input as a
|
|
85
98
|
// duplicated read-only buffer, but local-mutating it would
|
|
86
99
|
// surprise any future consumer that relied on the contract.
|
|
87
100
|
val view = buffer.duplicate().order(ByteOrder.nativeOrder())
|
|
88
|
-
val frameCount = view.remaining() / bytesPerFrame
|
|
101
|
+
val frameCount = view.remaining() / fmt.bytesPerFrame
|
|
89
102
|
var idxLo = (writeIdx and ringMask.toLong()).toInt()
|
|
90
103
|
var i = 0
|
|
91
104
|
while (i < frameCount) {
|
|
92
|
-
ring[idxLo] = readMonoSample(view)
|
|
105
|
+
ring[idxLo] = readMonoSample(view, fmt)
|
|
93
106
|
idxLo = (idxLo + 1) and ringMask
|
|
94
107
|
i++
|
|
95
108
|
}
|
|
@@ -100,10 +113,10 @@ internal class FFTProcessorTee(
|
|
|
100
113
|
if (nowNs - lastEmitNs < intervalMs * 1_000_000L) return
|
|
101
114
|
lastEmitNs = nowNs
|
|
102
115
|
|
|
103
|
-
emitFrame()
|
|
116
|
+
emitFrame(fmt.sampleRate)
|
|
104
117
|
}
|
|
105
118
|
|
|
106
|
-
private fun emitFrame() {
|
|
119
|
+
private fun emitFrame(sampleRate: Int) {
|
|
107
120
|
// Latest fftSize samples ending at writeIdx - 1.
|
|
108
121
|
val startLo = ((writeIdx - fftSize) and ringMask.toLong()).toInt()
|
|
109
122
|
var i = 0
|
|
@@ -134,24 +147,24 @@ internal class FFTProcessorTee(
|
|
|
134
147
|
* a sample (returns 0); the audio chain doesn't typically deliver
|
|
135
148
|
* those to AudioSink.
|
|
136
149
|
*/
|
|
137
|
-
private fun readMonoSample(buffer: ByteBuffer): Float {
|
|
150
|
+
private fun readMonoSample(buffer: ByteBuffer, fmt: Format): Float {
|
|
138
151
|
var sum = 0f
|
|
139
152
|
var ch = 0
|
|
140
|
-
while (ch < channelCount) {
|
|
141
|
-
sum += when (encoding) {
|
|
153
|
+
while (ch < fmt.channelCount) {
|
|
154
|
+
sum += when (fmt.encoding) {
|
|
142
155
|
C.ENCODING_PCM_16BIT -> buffer.short.toFloat() / Short.MAX_VALUE.toFloat()
|
|
143
156
|
C.ENCODING_PCM_FLOAT -> buffer.float
|
|
144
157
|
C.ENCODING_PCM_32BIT -> buffer.int.toFloat() / Int.MAX_VALUE.toFloat()
|
|
145
158
|
C.ENCODING_PCM_24BIT -> read24BitPacked(buffer)
|
|
146
159
|
else -> {
|
|
147
160
|
// Skip unknown encoding: advance position by sample size guess.
|
|
148
|
-
buffer.position(buffer.position() + bytesPerSample
|
|
161
|
+
buffer.position(buffer.position() + fmt.bytesPerSample)
|
|
149
162
|
0f
|
|
150
163
|
}
|
|
151
164
|
}
|
|
152
165
|
ch++
|
|
153
166
|
}
|
|
154
|
-
return sum / channelCount
|
|
167
|
+
return sum / fmt.channelCount
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
private fun read24BitPacked(buffer: ByteBuffer): Float {
|
|
@@ -164,10 +177,12 @@ internal class FFTProcessorTee(
|
|
|
164
177
|
return v.toFloat() / 8_388_608f // 2^23
|
|
165
178
|
}
|
|
166
179
|
|
|
167
|
-
private
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
180
|
+
private companion object {
|
|
181
|
+
fun bytesPerSample(encoding: Int): Int = when (encoding) {
|
|
182
|
+
C.ENCODING_PCM_16BIT -> 2
|
|
183
|
+
C.ENCODING_PCM_24BIT -> 3
|
|
184
|
+
C.ENCODING_PCM_32BIT, C.ENCODING_PCM_FLOAT -> 4
|
|
185
|
+
else -> 0
|
|
186
|
+
}
|
|
172
187
|
}
|
|
173
188
|
}
|
|
@@ -26,17 +26,48 @@ import java.util.TreeSet
|
|
|
26
26
|
* only to force the recency-bump rewrite.)
|
|
27
27
|
*
|
|
28
28
|
* Callbacks are invoked under the [Cache]'s lock, so the plain fields below need
|
|
29
|
-
* no additional synchronization — mirrors Media3's own evictor.
|
|
29
|
+
* no additional synchronization — mirrors Media3's own evictor. [protectedKeys]
|
|
30
|
+
* is the exception: it is written from outside those callbacks.
|
|
31
|
+
*
|
|
32
|
+
* [setProtectedKeys] names urls eviction must leave alone. Only this evictor
|
|
33
|
+
* honours them — Media3's LRU evictor has no such concept, and LRU is the
|
|
34
|
+
* default policy, so protection ships off unless a consumer selects FIFO. LRU
|
|
35
|
+
* is not equivalent: it bumps the recency of the span just read, not the whole
|
|
36
|
+
* resource, so a long track's already-played spans stay evictable while it is
|
|
37
|
+
* still playing.
|
|
30
38
|
*/
|
|
31
39
|
@UnstableApi
|
|
32
40
|
class FifoCacheEvictor(private val maxBytes: Long) : CacheEvictor {
|
|
33
41
|
|
|
34
42
|
private var insertionCounter: Long = 0
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Bytes held by the spans this evictor tracks — not the cache's on-disk total.
|
|
46
|
+
*
|
|
47
|
+
* The two differ when the cache notifies an add whose span [fifo] rejects: the
|
|
48
|
+
* bytes land on disk but no removal is ever notified for them, so counting
|
|
49
|
+
* them here would push the evictor permanently over budget and make it evict
|
|
50
|
+
* everything on every add. Untracked bytes are reclaimed when their resource
|
|
51
|
+
* is removed as a whole.
|
|
52
|
+
*/
|
|
35
53
|
private var currentSize: Long = 0
|
|
36
54
|
|
|
37
55
|
/** Insertion order per resource key (the track URL); stamped once, never bumped. */
|
|
38
56
|
private val insertionOrderByKey = HashMap<String, Long>()
|
|
39
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Live span count per resource key, maintained from the [fifo] mutations
|
|
60
|
+
* themselves rather than from the callback count.
|
|
61
|
+
*
|
|
62
|
+
* [CacheSpan] defines no `equals`/`hashCode`, and [Cache] notifies an add even
|
|
63
|
+
* when the set rejects it as a duplicate, so a count derived from callbacks
|
|
64
|
+
* drifts from the set. A drifted count drops an [insertionOrderByKey] entry
|
|
65
|
+
* while spans for that key are still live, which changes the comparator's
|
|
66
|
+
* verdict for elements already in [fifo]; removals then silently fail and
|
|
67
|
+
* [evictToFit] cannot make progress.
|
|
68
|
+
*/
|
|
69
|
+
private val spanCountByKey = HashMap<String, Int>()
|
|
70
|
+
|
|
40
71
|
/** Spans ordered oldest-first by (resource insertion order, byte position). */
|
|
41
72
|
private val fifo = TreeSet<CacheSpan> { a, b ->
|
|
42
73
|
val oa = insertionOrderByKey[a.key] ?: Long.MAX_VALUE
|
|
@@ -48,6 +79,26 @@ class FifoCacheEvictor(private val maxBytes: Long) : CacheEvictor {
|
|
|
48
79
|
}
|
|
49
80
|
}
|
|
50
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Urls eviction must not take. Written from the main thread while callbacks
|
|
84
|
+
* run under the [Cache] lock on whichever thread touched the cache, hence
|
|
85
|
+
* volatile. The setter copies because a Kotlin `Set` is read-only rather than
|
|
86
|
+
* immutable: without the copy a caller could keep mutating the set it handed
|
|
87
|
+
* over and change what is protected behind this evictor's back.
|
|
88
|
+
*/
|
|
89
|
+
@Volatile
|
|
90
|
+
private var protectedKeys: Set<String> = emptySet()
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Name the urls eviction must leave alone — the playing track and the ones
|
|
94
|
+
* around it. Without this, FIFO evicts the track being played the moment the
|
|
95
|
+
* cache is over budget: [requiresCacheSpanTouches] is false, so a read never
|
|
96
|
+
* bumps a resource's order and the playing track cannot protect itself.
|
|
97
|
+
*/
|
|
98
|
+
fun setProtectedKeys(keys: Set<String>) {
|
|
99
|
+
protectedKeys = keys.toSet()
|
|
100
|
+
}
|
|
101
|
+
|
|
51
102
|
override fun requiresCacheSpanTouches(): Boolean = false
|
|
52
103
|
|
|
53
104
|
override fun onCacheInitialized() {
|
|
@@ -61,36 +112,81 @@ class FifoCacheEvictor(private val maxBytes: Long) : CacheEvictor {
|
|
|
61
112
|
}
|
|
62
113
|
|
|
63
114
|
override fun onSpanAdded(cache: Cache, span: CacheSpan) {
|
|
115
|
+
// Stamp the order before inserting: the comparator reads it for every
|
|
116
|
+
// element it touches during the insert.
|
|
64
117
|
insertionOrderByKey.getOrPut(span.key) { insertionCounter++ }
|
|
65
|
-
fifo.add(span)
|
|
66
|
-
|
|
118
|
+
if (fifo.add(span)) {
|
|
119
|
+
spanCountByKey.merge(span.key, 1, Int::plus)
|
|
120
|
+
currentSize += span.length
|
|
121
|
+
}
|
|
67
122
|
evictToFit(cache, 0)
|
|
68
123
|
}
|
|
69
124
|
|
|
70
125
|
override fun onSpanRemoved(cache: Cache, span: CacheSpan) {
|
|
71
|
-
fifo.remove(span)
|
|
126
|
+
if (!fifo.remove(span)) return
|
|
72
127
|
currentSize -= span.length
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
128
|
+
spanCountByKey.merge(span.key, -1, Int::plus)
|
|
129
|
+
retireIfEmpty(span.key)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Forget a resource's insertion order once it holds no spans, so a later
|
|
134
|
+
* re-fetch of the same URL is treated as newly inserted.
|
|
135
|
+
*
|
|
136
|
+
* Retires on `<= 0` rather than `== 0`: `HashMap.merge` stores the value
|
|
137
|
+
* directly for an absent key, so a decrement that outruns an increment lands
|
|
138
|
+
* at -1, and an equality test would pin the entry in place permanently.
|
|
139
|
+
*/
|
|
140
|
+
private fun retireIfEmpty(key: String) {
|
|
141
|
+
if ((spanCountByKey[key] ?: 0) <= 0) {
|
|
142
|
+
spanCountByKey.remove(key)
|
|
143
|
+
insertionOrderByKey.remove(key)
|
|
77
144
|
}
|
|
78
145
|
}
|
|
79
146
|
|
|
80
147
|
override fun onSpanTouched(cache: Cache, oldSpan: CacheSpan, newSpan: CacheSpan) {
|
|
81
148
|
// Not called while requiresCacheSpanTouches() is false. Handled defensively:
|
|
82
|
-
// swap the span in place, preserving the resource's insertion order
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
149
|
+
// swap the span in place, preserving the resource's insertion order —
|
|
150
|
+
// re-stamping it here would move the resource to the back of the queue on a
|
|
151
|
+
// mere access. The count tracks each mutation separately so a swap that
|
|
152
|
+
// only half-applies cannot retire an order whose spans are still live.
|
|
153
|
+
if (fifo.remove(oldSpan)) {
|
|
154
|
+
currentSize -= oldSpan.length
|
|
155
|
+
spanCountByKey.merge(oldSpan.key, -1, Int::plus)
|
|
156
|
+
}
|
|
157
|
+
insertionOrderByKey.getOrPut(newSpan.key) { insertionCounter++ }
|
|
158
|
+
if (fifo.add(newSpan)) {
|
|
159
|
+
currentSize += newSpan.length
|
|
160
|
+
spanCountByKey.merge(newSpan.key, 1, Int::plus)
|
|
161
|
+
}
|
|
162
|
+
// Settle both keys only after both mutations: a same-key swap dips to zero
|
|
163
|
+
// in between, and retiring there would re-stamp the resource as newest.
|
|
164
|
+
retireIfEmpty(oldSpan.key)
|
|
165
|
+
retireIfEmpty(newSpan.key)
|
|
87
166
|
}
|
|
88
167
|
|
|
89
168
|
private fun evictToFit(cache: Cache, requiredSpace: Long) {
|
|
90
|
-
|
|
169
|
+
// One volatile read, so the whole scan sees a single consistent set.
|
|
170
|
+
val shielded = protectedKeys
|
|
171
|
+
while (currentSize + requiredSpace > maxBytes) {
|
|
172
|
+
// Oldest first, skipping what must survive. Nothing evictable left means
|
|
173
|
+
// over budget is the outcome — the alternative is spinning here.
|
|
174
|
+
val victim = fifo.firstOrNull { it.key !in shielded } ?: return
|
|
91
175
|
// removeSpan synchronously calls back onSpanRemoved, which updates
|
|
92
176
|
// currentSize + fifo, so the loop re-checks against the new total.
|
|
93
|
-
cache.removeSpan(
|
|
177
|
+
cache.removeSpan(victim)
|
|
178
|
+
if (fifo.contains(victim)) {
|
|
179
|
+
// The cache index no longer holds this span, so removeSpan notified
|
|
180
|
+
// nothing. Drop our own record: its bytes are unreclaimable, but
|
|
181
|
+
// leaving it in place would make it the victim of every future
|
|
182
|
+
// eviction and stop the cache evicting at all. Membership, not the
|
|
183
|
+
// head, is the test — a protected span can sit ahead of the victim.
|
|
184
|
+
// Membership stays exact because a span still in the tree always has a
|
|
185
|
+
// live order entry: the entry is retired only after its last successful
|
|
186
|
+
// removal. Once retired, the span sorts at the Long.MAX_VALUE sentinel,
|
|
187
|
+
// past every live element, so `contains` reports false.
|
|
188
|
+
onSpanRemoved(cache, victim)
|
|
189
|
+
}
|
|
94
190
|
}
|
|
95
191
|
}
|
|
96
192
|
}
|
|
@@ -70,9 +70,10 @@ internal class GaplessEngine(
|
|
|
70
70
|
/**
|
|
71
71
|
* Per-engine ReplayGain audio processor. Always present in the
|
|
72
72
|
* pipeline; the processor's own [ReplayGainAudioProcessor.isActive]
|
|
73
|
-
* gates
|
|
74
|
-
*
|
|
75
|
-
* reads container-level
|
|
73
|
+
* gates only on encoding, so a gain pushed mid-stream reaches the
|
|
74
|
+
* audio without a sink reconfigure. The
|
|
75
|
+
* [Player.Listener.onTracksChanged] listener reads container-level
|
|
76
|
+
* tags out of the active audio track's
|
|
76
77
|
* `Format.metadata` and pushes a fresh gain;
|
|
77
78
|
* [Player.Listener.onMediaItemTransition] resets to unity until
|
|
78
79
|
* the next track's tracks become available. The
|
|
@@ -135,10 +136,13 @@ internal class GaplessEngine(
|
|
|
135
136
|
|
|
136
137
|
private val playerListener = object : Player.Listener {
|
|
137
138
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
|
138
|
-
|
|
139
|
+
// The end signal goes first: the queue-end path emits ENDED with its
|
|
140
|
+
// own reason, and a plain state emit before it would take that slot
|
|
141
|
+
// with SYSTEM and leave the queue-end emit deduplicated.
|
|
139
142
|
if (playbackState == Player.STATE_ENDED) {
|
|
140
143
|
delegate?.onPlaybackEnded(this@GaplessEngine)
|
|
141
144
|
}
|
|
145
|
+
delegate?.onStateMaybeChanged(this@GaplessEngine, playWhenReadyReason = null)
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
|
@@ -199,7 +203,7 @@ internal class GaplessEngine(
|
|
|
199
203
|
}
|
|
200
204
|
|
|
201
205
|
override fun onPlayerError(error: PlaybackException) {
|
|
202
|
-
delegate?.onError(this@GaplessEngine, error)
|
|
206
|
+
delegate?.onError(this@GaplessEngine, error, failedIndex = exoPlayer.currentMediaItemIndex)
|
|
203
207
|
}
|
|
204
208
|
}
|
|
205
209
|
|
|
@@ -225,15 +229,6 @@ internal class GaplessEngine(
|
|
|
225
229
|
|
|
226
230
|
// --- PlaybackEngine — lifecycle ---
|
|
227
231
|
|
|
228
|
-
override suspend fun prepare(
|
|
229
|
-
mediaItems: List<MediaItem>,
|
|
230
|
-
startIndex: Int,
|
|
231
|
-
startPositionMs: Long,
|
|
232
|
-
) {
|
|
233
|
-
exoPlayer.setMediaItems(mediaItems, startIndex, startPositionMs)
|
|
234
|
-
exoPlayer.prepare()
|
|
235
|
-
}
|
|
236
|
-
|
|
237
232
|
override fun release() {
|
|
238
233
|
if (released) return
|
|
239
234
|
released = true
|
|
@@ -257,6 +252,11 @@ internal class GaplessEngine(
|
|
|
257
252
|
startIndex: Int,
|
|
258
253
|
startPositionMs: Long,
|
|
259
254
|
) {
|
|
255
|
+
if (items.isEmpty()) {
|
|
256
|
+
// Draining is not pausing: `playWhenReady` survives an empty playlist,
|
|
257
|
+
// so the next queue seated on this player would start on contact.
|
|
258
|
+
exoPlayer.pause()
|
|
259
|
+
}
|
|
260
260
|
exoPlayer.setMediaItems(items, startIndex, startPositionMs)
|
|
261
261
|
// ExoPlayer is in STATE_IDLE after construction + after stop().
|
|
262
262
|
// setMediaItems alone doesn't transition out of IDLE — prepare()
|
|
@@ -425,12 +425,47 @@ internal class GaplessEngine(
|
|
|
425
425
|
// --- ReplayGain — engine-internal accessors ---
|
|
426
426
|
|
|
427
427
|
/**
|
|
428
|
-
*
|
|
428
|
+
* Resolves the consumer's ReplayGain claim over the queue entry at a
|
|
429
|
+
* given index. Installed by [TrackPlayer] at engine construction and
|
|
430
|
+
* invoked on every read rather than cached: a `setQueue` carrying
|
|
431
|
+
* corrected values then takes effect at the next recompute, with no
|
|
432
|
+
* cache to invalidate at the wrong moment.
|
|
433
|
+
*
|
|
434
|
+
* A `null` return means the consumer supplied nothing for that entry
|
|
435
|
+
* and the file's own tags stand. A non-null result with a `null`
|
|
436
|
+
* [SuppliedReplayGain.data] means the consumer claimed the track and
|
|
437
|
+
* gave nothing usable — a different state, and one the merge has to
|
|
438
|
+
* be able to tell apart.
|
|
439
|
+
*/
|
|
440
|
+
internal var suppliedReplayGainResolver: ((Int) -> SuppliedReplayGain?)? = null
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* The ReplayGain the active item actually plays with: the consumer's
|
|
444
|
+
* claim where there is one, [cachedReplayGainData] otherwise. Every
|
|
445
|
+
* read of ReplayGain on this engine goes through here, so the merge
|
|
446
|
+
* lands before the first value reaches the audio or a listener.
|
|
447
|
+
*/
|
|
448
|
+
private fun effectiveReplayGainData(): ReplayGainData? {
|
|
449
|
+
// An empty timeline reports index 0, which would name the first
|
|
450
|
+
// queue entry for a player holding no items at all — so nothing is
|
|
451
|
+
// claimed until there is an item for the index to mean something.
|
|
452
|
+
val supplied = if (exoPlayer.mediaItemCount == 0) null
|
|
453
|
+
else suppliedReplayGainResolver?.invoke(exoPlayer.currentMediaItemIndex)
|
|
454
|
+
return ReplayGainData.merge(
|
|
455
|
+
suppliedClaimed = supplied != null,
|
|
456
|
+
supplied = supplied?.data,
|
|
457
|
+
detected = cachedReplayGainData,
|
|
458
|
+
)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Recompute the processor's linear gain from [effectiveReplayGainData]
|
|
429
463
|
* + the current [ReplayGainConfig.getMode]. Called from
|
|
430
464
|
* [Player.Listener.onTracksChanged] / [Player.Listener.onMetadata]
|
|
431
465
|
* via [handleReplayGainParse] when fresh tags arrive and from
|
|
432
466
|
* [TrackPlayer.setReplayGainMode] when the user flips mode
|
|
433
|
-
* mid-track. With
|
|
467
|
+
* mid-track. With nothing supplied and nothing cached the processor
|
|
468
|
+
* stays at unity.
|
|
434
469
|
*
|
|
435
470
|
* Main thread only — the listener bodies dispatch on the
|
|
436
471
|
* application looper and [TrackPlayer.setReplayGainMode] hops to
|
|
@@ -438,7 +473,7 @@ internal class GaplessEngine(
|
|
|
438
473
|
*/
|
|
439
474
|
@MainThread
|
|
440
475
|
internal fun recomputeReplayGainFromConfig() {
|
|
441
|
-
val gain = computeLinearGain(
|
|
476
|
+
val gain = computeLinearGain(effectiveReplayGainData(), ReplayGainConfig.getMode())
|
|
442
477
|
replayGainProcessor.setLinearGain(gain)
|
|
443
478
|
}
|
|
444
479
|
|
|
@@ -465,20 +500,21 @@ internal class GaplessEngine(
|
|
|
465
500
|
}
|
|
466
501
|
|
|
467
502
|
/**
|
|
468
|
-
* Snapshot the active item
|
|
469
|
-
* [NowPlayingFormatExtractor] surface. Returns `null` when
|
|
470
|
-
*
|
|
503
|
+
* Snapshot the ReplayGain the active item plays with, for the
|
|
504
|
+
* [NowPlayingFormatExtractor] surface. Returns `null` when nothing is
|
|
505
|
+
* supplied for the current track and no usable RG tags have been
|
|
506
|
+
* parsed for it either.
|
|
471
507
|
*/
|
|
472
|
-
override fun currentReplayGainData(): ReplayGainData? =
|
|
508
|
+
override fun currentReplayGainData(): ReplayGainData? = effectiveReplayGainData()
|
|
473
509
|
|
|
474
510
|
/**
|
|
475
|
-
* `true` when
|
|
476
|
-
* mode (i.e. the gain push lands at non-unity). Used by the
|
|
511
|
+
* `true` when the active item's ReplayGain carries a tag for the
|
|
512
|
+
* active mode (i.e. the gain push lands at non-unity). Used by the
|
|
477
513
|
* NowPlayingFormat path to populate `replayGainActive` without
|
|
478
514
|
* recomputing the linear gain itself.
|
|
479
515
|
*/
|
|
480
516
|
override fun currentReplayGainActive(): Boolean {
|
|
481
|
-
val data =
|
|
517
|
+
val data = effectiveReplayGainData() ?: return false
|
|
482
518
|
val (gainDb, _) = data.selectGain(ReplayGainConfig.getMode())
|
|
483
519
|
return gainDb != null
|
|
484
520
|
}
|
|
@@ -532,15 +568,6 @@ internal class GaplessEngine(
|
|
|
532
568
|
}
|
|
533
569
|
return null
|
|
534
570
|
}
|
|
535
|
-
|
|
536
|
-
// --- PlaybackEngine — handoff ---
|
|
537
|
-
|
|
538
|
-
override suspend fun handoff(to: PlaybackEngine, atTrackBoundary: Boolean) {
|
|
539
|
-
// Wired alongside CrossfadeEngine; with only the gapless engine
|
|
540
|
-
// available there is no swap target.
|
|
541
|
-
throw NotImplementedError("PlaybackEngine.handoff is not yet implemented")
|
|
542
|
-
}
|
|
543
|
-
|
|
544
571
|
}
|
|
545
572
|
|
|
546
573
|
/**
|
|
@@ -57,7 +57,7 @@ public abstract class HeadlessJsMediaService :
|
|
|
57
57
|
internal val activeTasks: MutableSet<Int> = CopyOnWriteArraySet()
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
60
|
+
* Set on the first task spawn so a re-entrant
|
|
61
61
|
* [onStartCommand] for the same wake doesn't double-start the JS
|
|
62
62
|
* handler. Reset is implicit — service destruction reclaims this
|
|
63
63
|
* field along with the rest of the instance.
|
|
@@ -8,7 +8,9 @@ package com.margelo.nitro.queueplayer
|
|
|
8
8
|
* call through the interface; consumers never see which backend
|
|
9
9
|
* is active.
|
|
10
10
|
*
|
|
11
|
-
* Threading:
|
|
11
|
+
* Threading: see the implementations — attach / detach arrive on the
|
|
12
|
+
* service main looper and on the Equalizer-constructing thread, the
|
|
13
|
+
* mutators on the `Promise.async` pool, the getters on the JS thread.
|
|
12
14
|
*/
|
|
13
15
|
interface IEqualizerEngine {
|
|
14
16
|
|