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