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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import NitroModules
|
|
2
|
+
|
|
3
|
+
extension TrackPlayer {
|
|
4
|
+
// MARK: - Automotive / voice handler registration
|
|
5
|
+
|
|
6
|
+
/// Atomically replace the cached `BrowseSnapshot` on the
|
|
7
|
+
/// process-wide [CarPlayCoordinator]. The data provider posts a
|
|
8
|
+
/// `didUpdateNotification` on the main queue; an active CarPlay
|
|
9
|
+
/// scene observes the notification + rebuilds the root template
|
|
10
|
+
/// against the new snapshot. Push semantics — most-recent call
|
|
11
|
+
/// wins. Safe to call before a CarPlay scene attaches: the
|
|
12
|
+
/// snapshot is cached on the coordinator + the next scene reads
|
|
13
|
+
/// it directly at didConnect.
|
|
14
|
+
func setBrowseSnapshot(snapshot: BrowseSnapshot) throws {
|
|
15
|
+
CarPlayCoordinator.shared.dataProvider.setSnapshot(snapshot)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func onBrowseRequest(
|
|
19
|
+
callback: @escaping (_ parentId: String) -> Promise<Promise<[BrowseItem]>>
|
|
20
|
+
) throws -> () -> Void {
|
|
21
|
+
return CarPlayCoordinator.shared.bridge.registerOnBrowseRequest(callback)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// `onSearchRequest` is Android-only — CarPlay audio-entitled apps
|
|
25
|
+
// cannot legally present `CPSearchTemplate` (per the CarPlay App
|
|
26
|
+
// Programming Guide template-by-category rules); pushing or
|
|
27
|
+
// presenting it raises NSInvalidArgumentException at runtime. Apple
|
|
28
|
+
// Music + Spotify both dropped CarPlay search in iOS 26; the
|
|
29
|
+
// canonical CarPlay search affordance for audio apps is Siri via
|
|
30
|
+
// SiriKit's `INPlayMediaIntent`, not an in-app search template.
|
|
31
|
+
//
|
|
32
|
+
// `onPlayFromSearchRequest` IS wired on iOS — it serves the
|
|
33
|
+
// SiriKit `INPlayMediaIntent` path: Siri parses "Hey Siri play X
|
|
34
|
+
// on MyApp" into a structured `INMediaSearch`, the lib's
|
|
35
|
+
// `PlayMediaIntentHandler` maps it to `MediaSearchRequest`, and
|
|
36
|
+
// dispatches into this callback via `CarPlayBridge`.
|
|
37
|
+
//
|
|
38
|
+
// For `onSearchRequest` we accept the consumer's registration on
|
|
39
|
+
// iOS to keep the TS API shape cross-platform; the callback is
|
|
40
|
+
// discarded (`_ = callback` consumes the value), and the returned
|
|
41
|
+
// disposer is a no-op so the consumer's bookkeeping stays sane.
|
|
42
|
+
func onSearchRequest(
|
|
43
|
+
callback: @escaping (_ query: String) -> Promise<Promise<[BrowseItem]>>
|
|
44
|
+
) throws -> () -> Void {
|
|
45
|
+
_ = callback
|
|
46
|
+
return {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func onPlayFromSearchRequest(
|
|
50
|
+
callback: @escaping (_ request: MediaSearchRequest) -> Promise<Promise<[TrackItem]>>
|
|
51
|
+
) throws -> () -> Void {
|
|
52
|
+
return CarPlayCoordinator.shared.bridge.registerOnPlayFromSearchRequest(callback)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func onPlayFromIdRequest(
|
|
56
|
+
callback: @escaping (_ mediaId: String) -> Promise<Promise<[TrackItem]>>
|
|
57
|
+
) throws -> () -> Void {
|
|
58
|
+
return CarPlayCoordinator.shared.bridge.registerOnPlayFromIdRequest(callback)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Donation Hybrid method — fire-and-forget into Siri's vocabulary
|
|
62
|
+
// donation pipeline. Real impl lives in `ios/Siri/VoiceDonation.swift`.
|
|
63
|
+
// Uses `INVocabulary.setVocabularyStrings(_:of:)` to seed Siri's NLU
|
|
64
|
+
// with the consumer's artist + playlist names.
|
|
65
|
+
func donateVoiceVocabulary(vocabulary: VoiceVocabulary) throws {
|
|
66
|
+
// Opt-in only. `INVocabulary` raises an uncatchable Objective-C exception
|
|
67
|
+
// in a process without the `com.apple.developer.siri` entitlement, and
|
|
68
|
+
// iOS gives a process no way to read its own entitlements — so the
|
|
69
|
+
// consumer declaring the capability is the only signal the library can
|
|
70
|
+
// have that this is safe to touch.
|
|
71
|
+
guard self.voiceVocabularyDonationEnabled else { return }
|
|
72
|
+
VoiceDonation.donateVoiceVocabulary(vocabulary)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func onCarConnect(callback: @escaping () -> Void) throws -> () -> Void {
|
|
76
|
+
return CarPlayCoordinator.shared.bridge.registerOnCarConnect(callback)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// `onServiceReady` is an Android-only signal. iOS apps die fully
|
|
80
|
+
// under memory pressure (no separate service lifecycle), so there's
|
|
81
|
+
// no `service-reborn` scenario to surface here. The lib accepts the
|
|
82
|
+
// registration to keep the cross-platform API shape consistent and
|
|
83
|
+
// returns a no-op disposer; the callback is consumed (so it isn't
|
|
84
|
+
// retained) and never invoked. Consumers writing for both platforms
|
|
85
|
+
// register one handler; on iOS only `onCarConnect` carries the
|
|
86
|
+
// equivalent "automotive surface available" signal.
|
|
87
|
+
func onServiceReady(
|
|
88
|
+
callback: @escaping (_ reason: ServiceReadyReason) -> Void
|
|
89
|
+
) throws -> () -> Void {
|
|
90
|
+
_ = callback
|
|
91
|
+
return {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func onCarDisconnect(callback: @escaping () -> Void) throws -> () -> Void {
|
|
95
|
+
return CarPlayCoordinator.shared.bridge.registerOnCarDisconnect(callback)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func isCarConnected() throws -> Bool {
|
|
99
|
+
return CarPlayCoordinator.shared.isCarConnected
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
|
|
4
|
+
extension TrackPlayer {
|
|
5
|
+
// MARK: - Lookahead cache — runtime config + status surface
|
|
6
|
+
func setLookaheadCache(config: LookaheadCacheConfig) throws -> Promise<Void> {
|
|
7
|
+
return enqueueThrowing {
|
|
8
|
+
try self.applyLookaheadConfig(config)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func getLookaheadCacheStatus() throws -> Promise<CacheStatus> {
|
|
13
|
+
// The snapshot counts fully-cached entries, which is a filesystem check
|
|
14
|
+
// per entry — the Promise keeps that work off the calling JS thread.
|
|
15
|
+
return enqueueValue { self.buildCacheStatusSnapshot() }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func clearLookaheadCache() throws -> Promise<Void> {
|
|
19
|
+
return enqueue {
|
|
20
|
+
// Cancel any in-flight prefetch + drop everything on disk.
|
|
21
|
+
// We deliberately do NOT call rescheduleLookahead() here —
|
|
22
|
+
// an immediate refill makes `clearLookaheadCache` impossible
|
|
23
|
+
// to observe at zero from JS, and "clear" semantically means
|
|
24
|
+
// "wipe and stop" (the consumer can re-trigger prefetch by
|
|
25
|
+
// calling setLookaheadCache, skipToIndex, or setQueue again).
|
|
26
|
+
self.lookaheadCachePrefetcher?.cancel()
|
|
27
|
+
self.lookaheadCache?.clear()
|
|
28
|
+
self.emitCacheStatus()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func onCacheStatusChange(callback: @escaping (_ status: CacheStatus) -> Void) throws -> () -> Void {
|
|
33
|
+
// ListenerRegistry.add is internally serialized, so a JS consumer
|
|
34
|
+
// that does
|
|
35
|
+
// const unsub = onCacheStatusChange(cb)
|
|
36
|
+
// await setLookaheadCache(cfg)
|
|
37
|
+
// is guaranteed to see the post-config-change emit (the
|
|
38
|
+
// setLookaheadCache call synchronously crosses the bridge after
|
|
39
|
+
// the listener is in the registry).
|
|
40
|
+
let id = cacheStatusListeners.add(callback)
|
|
41
|
+
// Deliver the current status once. Otherwise a settings screen shows its
|
|
42
|
+
// placeholder until the next prefetch completes — which never happens for a
|
|
43
|
+
// fully-cached queue or a consumer running with prefetch off.
|
|
44
|
+
playerQueue.async { [weak self] in
|
|
45
|
+
guard let self else { return }
|
|
46
|
+
guard self.cacheStatusListeners.contains(id: id) else { return }
|
|
47
|
+
callback(self.buildCacheStatusSnapshot())
|
|
48
|
+
}
|
|
49
|
+
return { [weak self] in self?.cacheStatusListeners.remove(id: id) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Apply a runtime config update. All three runtime dimensions are
|
|
53
|
+
/// live (no configure() needed):
|
|
54
|
+
/// - `enabled` toggle: build/teardown the prefetcher against the cache
|
|
55
|
+
/// - `lookaheadCount` change: live update on the prefetcher
|
|
56
|
+
/// - `allowsCellularAccess` toggle: live on the cache, applied per request
|
|
57
|
+
///
|
|
58
|
+
/// Cache SIZE + eviction policy are NOT runtime dimensions — they are fixed
|
|
59
|
+
/// at `configure()` from `PlayerConfig` (kept consistent with Android, whose
|
|
60
|
+
/// SimpleCache is immutable after construction).
|
|
61
|
+
internal func applyLookaheadConfig(_ config: LookaheadCacheConfig) throws {
|
|
62
|
+
let previous = self.lookaheadConfig
|
|
63
|
+
self.lookaheadConfig = config
|
|
64
|
+
|
|
65
|
+
let enableChanged = config.enabled != previous.enabled
|
|
66
|
+
let countChanged = config.lookaheadCount != previous.lookaheadCount
|
|
67
|
+
|
|
68
|
+
if enableChanged {
|
|
69
|
+
rebuildCacheStack(config: config)
|
|
70
|
+
} else if config.enabled {
|
|
71
|
+
applyLiveConfigUpdates(config: config, countChanged: countChanged)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The cache applies this per request, so it lands whether or not the
|
|
75
|
+
// branches above ran. When there is no cache yet the stored config is what
|
|
76
|
+
// the next construction reads.
|
|
77
|
+
lookaheadCache?.allowsCellularAccess = config.allowsCellularAccess ?? true
|
|
78
|
+
|
|
79
|
+
emitCacheStatus()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Configure-time cache disk budget in MB — `PlayerConfig.lookaheadCacheMaxSizeMb`
|
|
83
|
+
/// or the 100 MB default. Fixed at cache construction; reported read-only via
|
|
84
|
+
/// `getLookaheadCacheStatus().maxSizeMb`.
|
|
85
|
+
internal func configuredCacheMaxSizeMb() -> Double {
|
|
86
|
+
self.config.lookaheadCacheMaxSizeMb
|
|
87
|
+
?? Double(LookaheadCache.DEFAULT_MAX_SIZE_BYTES) / Double(LookaheadCache.BYTES_PER_MB)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Configure-time cache eviction policy — `PlayerConfig.lookaheadCacheEvictionPolicy`
|
|
91
|
+
/// or `.lru`. Fixed at cache construction; a change at `configure()` clears the
|
|
92
|
+
/// cache and rebuilds under the new policy (see `configure`).
|
|
93
|
+
internal func configuredEvictionPolicy() -> EvictionPolicy {
|
|
94
|
+
self.config.lookaheadCacheEvictionPolicy ?? .lru
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Bring up the server that reads through `cache`. Called from both sites
|
|
98
|
+
/// that build a cache — `configure()` and the runtime `setLookaheadCache`
|
|
99
|
+
/// — because a cache without a server is one nothing ever reads from. A
|
|
100
|
+
/// server already reading through this same cache instance is kept, its
|
|
101
|
+
/// headers refreshed: replacing it would change the port seated items
|
|
102
|
+
/// address. Blocks for the bind (milliseconds) so every item built after
|
|
103
|
+
/// this call routes; an item built before the bind is bound to its origin.
|
|
104
|
+
internal func startReadThroughServer(for cache: LookaheadCache) {
|
|
105
|
+
if let existing = readThroughServer, readThroughServerCache === cache {
|
|
106
|
+
existing.setHeaders(
|
|
107
|
+
AVQueueBuilder.mergedHeaders(
|
|
108
|
+
headers: self.config.httpHeaders, userAgent: self.config.userAgent))
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
readThroughServer?.stop()
|
|
112
|
+
let server = ReadThroughServer(
|
|
113
|
+
cachedFileForOrigin: { [weak cache] sourceURL in
|
|
114
|
+
cache?.cachedURL(forUrl: sourceURL)
|
|
115
|
+
},
|
|
116
|
+
onFirstDiskHit: { [weak cache] sourceURL in
|
|
117
|
+
cache?.touch(url: sourceURL)
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
server.setHeaders(
|
|
121
|
+
AVQueueBuilder.mergedHeaders(
|
|
122
|
+
headers: self.config.httpHeaders, userAgent: self.config.userAgent))
|
|
123
|
+
server.start()
|
|
124
|
+
if !server.waitUntilBound() {
|
|
125
|
+
NSLog("[QueuePlayer] read-through server did not bind; remote tracks play from their origin")
|
|
126
|
+
}
|
|
127
|
+
self.readThroughServer = server
|
|
128
|
+
self.readThroughServerCache = cache
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Quiesce or re-arm the cache, prefetcher and read-through server
|
|
132
|
+
/// when the `enabled` flag flipped. Disable cancels in-flight prefetch
|
|
133
|
+
/// and quiesces the cache (URLSession STAYS alive — it is only invalidated
|
|
134
|
+
/// by full `destroy`); the server stays up too, serving seated items from
|
|
135
|
+
/// the origin while the cache answers nothing, and `routingServer` binds
|
|
136
|
+
/// new items to their origin. Enable rebuilds the prefetcher (or constructs
|
|
137
|
+
/// the pair from scratch on first enable), keeps the server and reschedules.
|
|
138
|
+
///
|
|
139
|
+
/// Keeping the cache instance + URLSession alive across disable
|
|
140
|
+
/// avoids `Task created in a session that has been invalidated`
|
|
141
|
+
/// (Obj-C exception, uncatchable in Swift's `do-catch`) when a
|
|
142
|
+
/// prefetcher Task survived disable + tried to download against
|
|
143
|
+
/// a freshly-invalidated session before the dispatch barrier saw
|
|
144
|
+
/// the cancellation.
|
|
145
|
+
private func rebuildCacheStack(config: LookaheadCacheConfig) {
|
|
146
|
+
if !config.enabled {
|
|
147
|
+
lookaheadCachePrefetcher?.tearDown()
|
|
148
|
+
lookaheadCachePrefetcher = nil
|
|
149
|
+
lookaheadCache?.disable()
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
if lookaheadCache == nil {
|
|
153
|
+
// Size is fixed at construction from the configure-time budget.
|
|
154
|
+
lookaheadCache = LookaheadCache(
|
|
155
|
+
maxSizeBytes: LookaheadCache.maxBytes(forMb: configuredCacheMaxSizeMb()),
|
|
156
|
+
evictionPolicy: configuredEvictionPolicy(),
|
|
157
|
+
allowsCellularAccess: config.allowsCellularAccess ?? true
|
|
158
|
+
)
|
|
159
|
+
} else {
|
|
160
|
+
lookaheadCache?.enable()
|
|
161
|
+
}
|
|
162
|
+
guard let cache = lookaheadCache else { return }
|
|
163
|
+
// The cache is built here as well as at `configure()`, and a cache with no
|
|
164
|
+
// server is a cache nothing reads: every item would bind its origin and
|
|
165
|
+
// route past it. Whatever builds the cache brings the server with it.
|
|
166
|
+
startReadThroughServer(for: cache)
|
|
167
|
+
let prefetcher = LookaheadCachePrefetcher(
|
|
168
|
+
cache: cache,
|
|
169
|
+
confinedTo: self.playerQueue,
|
|
170
|
+
configHeaders: self.config.httpHeaders,
|
|
171
|
+
configUserAgent: self.config.userAgent
|
|
172
|
+
)
|
|
173
|
+
prefetcher.defaultLookaheadCount = InputGuards.validLookaheadCount(config.lookaheadCount)
|
|
174
|
+
prefetcher.onTrackProcessed = { [weak self] in self?.emitCacheStatus() }
|
|
175
|
+
lookaheadCachePrefetcher = prefetcher
|
|
176
|
+
rescheduleLookahead()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Apply in-place config updates when the cache stack already exists
|
|
180
|
+
/// (no enable-flag change). Only `lookaheadCount` reaches here — it updates
|
|
181
|
+
/// the prefetcher's default + reschedules. `allowsCellularAccess` is set on
|
|
182
|
+
/// the cache by the caller, and cache size is configure-time.
|
|
183
|
+
private func applyLiveConfigUpdates(
|
|
184
|
+
config: LookaheadCacheConfig, countChanged: Bool
|
|
185
|
+
) {
|
|
186
|
+
if countChanged {
|
|
187
|
+
lookaheadCachePrefetcher?.defaultLookaheadCount =
|
|
188
|
+
InputGuards.validLookaheadCount(config.lookaheadCount)
|
|
189
|
+
rescheduleLookahead()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Build a [CacheStatus] snapshot from current cache + prefetcher
|
|
194
|
+
/// state. Synchronous — the caller is already on the queue that serialises
|
|
195
|
+
/// player state.
|
|
196
|
+
internal func buildCacheStatusSnapshot() -> CacheStatus {
|
|
197
|
+
// `currentlyDownloadingUrl` is prefetcher state confined to `playerQueue`,
|
|
198
|
+
// so it is read here and handed to the builder as a value.
|
|
199
|
+
return buildCacheStatusSnapshot(
|
|
200
|
+
inFlightUrl: lookaheadCachePrefetcher?.currentlyDownloadingUrl)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// [CacheStatus] from cache + config state plus the caller-supplied
|
|
204
|
+
/// in-flight prefetch URL. Reads no prefetcher state of its own.
|
|
205
|
+
internal func buildCacheStatusSnapshot(inFlightUrl: String?) -> CacheStatus {
|
|
206
|
+
let cache = self.lookaheadCache
|
|
207
|
+
let currentBytes = cache?.currentSizeBytes() ?? 0
|
|
208
|
+
// Queue-agnostic: count fully-cached entries in the cache itself,
|
|
209
|
+
// not the intersection with `self.tracks`. Mirrors the semantics
|
|
210
|
+
// of `currentSizeBytes` (total cache disk usage) and the
|
|
211
|
+
// prefetcher's `currentlyDownloadingUrl` (active downloads) — all
|
|
212
|
+
// three reflect cache state independent of any current queue.
|
|
213
|
+
// Per-track local/stream/cache attribution for the active queue
|
|
214
|
+
// is exposed via `getCurrentTrackSource()`.
|
|
215
|
+
let fullyCached = cache?.fullyCachedCount() ?? 0
|
|
216
|
+
return CacheStatus(
|
|
217
|
+
enabled: lookaheadConfig.enabled,
|
|
218
|
+
currentSizeMb: Double(currentBytes) / Double(LookaheadCache.BYTES_PER_MB),
|
|
219
|
+
maxSizeMb: configuredCacheMaxSizeMb(),
|
|
220
|
+
tracksFullyCached: Double(fullyCached),
|
|
221
|
+
currentlyCaching: inFlightUrl.map { [$0] } ?? []
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// Fire the cache-status callback with a fresh snapshot. Caller
|
|
226
|
+
/// must already be on the queue that serialises player state.
|
|
227
|
+
internal func emitCacheStatus() {
|
|
228
|
+
guard !cacheStatusListeners.isEmpty else { return }
|
|
229
|
+
let snapshot = buildCacheStatusSnapshot()
|
|
230
|
+
cacheStatusListeners.forEach { $0(snapshot) }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
|
|
4
|
+
extension TrackPlayer {
|
|
5
|
+
// MARK: - Playback configuration — some stubs, some lifecycle-adjacent
|
|
6
|
+
|
|
7
|
+
func setPlaybackMode(mode: PlaybackMode) throws -> Promise<Void> {
|
|
8
|
+
return enqueueThrowing {
|
|
9
|
+
let (newState, effect) = try self.playbackModeStateMachine.setRequested(
|
|
10
|
+
state: self.playbackModeState,
|
|
11
|
+
requested: mode
|
|
12
|
+
)
|
|
13
|
+
self.playbackModeState = newState
|
|
14
|
+
switch effect {
|
|
15
|
+
case .noOp:
|
|
16
|
+
break
|
|
17
|
+
case .immediateSwap:
|
|
18
|
+
self.applyEngineSwapForMode(newState.active)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func getPlaybackMode() throws -> PlaybackMode {
|
|
24
|
+
// Lock-guarded, so this answers without hopping. A `playerQueue.sync` here
|
|
25
|
+
// parks the JS thread behind whatever the queue is running, which can be a
|
|
26
|
+
// full queue rebuild.
|
|
27
|
+
return self.playbackModeState.active
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Build a fresh engine of the requested kind and swap it in
|
|
31
|
+
/// place of the active engine. The new engine starts empty; the
|
|
32
|
+
/// canonical queue model is re-installed via the engine surface
|
|
33
|
+
/// so playback continues from the current track / position.
|
|
34
|
+
///
|
|
35
|
+
/// JS-facing event sources differ per engine. GaplessEngine ties
|
|
36
|
+
/// `self.player` to the underlying `AVQueuePlayer` and the KVO
|
|
37
|
+
/// chain installed by `installGaplessObservers` /
|
|
38
|
+
/// `installEventObservers` carries onTrackChange / onStateChange
|
|
39
|
+
/// / onError. CrossfadeEngine sets `self.player` to nil and
|
|
40
|
+
/// fires `PlaybackEngineDelegate` callbacks that this class
|
|
41
|
+
/// implements (extension at the end of the file); the delegate
|
|
42
|
+
/// is wired below. Both engines surface progress through
|
|
43
|
+
/// `emitProgressIfSubscribed`, polymorphic over self.player vs
|
|
44
|
+
/// engine state.
|
|
45
|
+
internal func applyEngineSwapForMode(_ active: PlaybackMode) {
|
|
46
|
+
let oldEngine = self.engine
|
|
47
|
+
// Capture pre-swap state — the engine swap releases the old
|
|
48
|
+
// engine; reading `newEngine.currentPositionSeconds` /
|
|
49
|
+
// `isPlaying` after would always return 0 / false. The
|
|
50
|
+
// wasPlaying capture is what carries playback continuity
|
|
51
|
+
// across the immediate engine swap.
|
|
52
|
+
let resumePosition = oldEngine?.currentPositionSeconds ?? 0
|
|
53
|
+
let resumeIndex = self.currentTrackIndex
|
|
54
|
+
// The transport's intent, not the old engine's audible state: a leg that
|
|
55
|
+
// is buffering, or mid-crossfade with its outgoing item finished, reports
|
|
56
|
+
// not-playing while the consumer is very much still playing. Swapping on
|
|
57
|
+
// that reading silently pauses them.
|
|
58
|
+
let wasPlaying = self.wantsToPlay
|
|
59
|
+
// Anything held back from the old engine goes with it: the new one is
|
|
60
|
+
// installed from the queue model, which already carries every mutation.
|
|
61
|
+
self.engineReconcileDeferred = false
|
|
62
|
+
|
|
63
|
+
let newEngine: PlaybackEngine
|
|
64
|
+
switch active.kind {
|
|
65
|
+
case .gapless:
|
|
66
|
+
newEngine = GaplessEngine()
|
|
67
|
+
case .crossfade:
|
|
68
|
+
// PlaybackModeStateMachine.requireValid guarantees this is
|
|
69
|
+
// non-nil + within [1000, 12000] when `kind == .crossfade`.
|
|
70
|
+
// Fail loud rather than silently degrading to 0 (which would
|
|
71
|
+
// route through CrossfadeEngine's hard-cut path + skip
|
|
72
|
+
// engineCrossfadeDidBegin entirely).
|
|
73
|
+
guard let durationMs = active.crossfadeDurationMs else {
|
|
74
|
+
preconditionFailure("PlaybackMode(crossfade) missing crossfadeDurationMs at engine construction")
|
|
75
|
+
}
|
|
76
|
+
let durationSeconds = durationMs / 1000.0
|
|
77
|
+
newEngine = CrossfadeEngine(
|
|
78
|
+
crossfadeDurationSeconds: durationSeconds, confinedTo: self.playerQueue)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
self.engine = newEngine
|
|
82
|
+
// GaplessEngine path: keep the established self.player KVO
|
|
83
|
+
// chain (currentItem / timeControlStatus / item.status) as
|
|
84
|
+
// the canonical event source. CrossfadeEngine path:
|
|
85
|
+
// self.player stays nil; the engine fires PlaybackEngineDelegate
|
|
86
|
+
// callbacks for track-change / state-change / format-ready /
|
|
87
|
+
// failed / play-to-end / queue-end and progress is sourced
|
|
88
|
+
// from the engine via a timer-driven fallback.
|
|
89
|
+
if let gapless = newEngine as? GaplessEngine {
|
|
90
|
+
self.player = gapless.player
|
|
91
|
+
self.gaplessFlipArmed = true
|
|
92
|
+
self.installGaplessObservers(on: gapless.player)
|
|
93
|
+
self.installEventObservers(on: gapless.player)
|
|
94
|
+
self.stopProgressFallbackTimer()
|
|
95
|
+
// Gapless does not need the engine delegate — the underlying
|
|
96
|
+
// self.player KVO chain is the canonical event source.
|
|
97
|
+
newEngine.delegate = nil
|
|
98
|
+
} else {
|
|
99
|
+
self.tearDownGaplessObservers()
|
|
100
|
+
self.tearDownEventObservers()
|
|
101
|
+
self.player = nil
|
|
102
|
+
newEngine.delegate = self
|
|
103
|
+
self.startProgressFallbackTimer()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Re-wire the shared audio-tap provider to the new engine's
|
|
107
|
+
// audio-mix provider slot.
|
|
108
|
+
AudioTapProvider.shared.wireToPlaybackEngine(newEngine)
|
|
109
|
+
|
|
110
|
+
// Re-apply canonical config to the freshly-built engine BEFORE the
|
|
111
|
+
// queue install / play — it starts at engine defaults (repeat off,
|
|
112
|
+
// volume 1, speed 1), so without this a gapless↔crossfade swap
|
|
113
|
+
// silently drops the user's settings.
|
|
114
|
+
newEngine.setRepeatMode(self.repeatModeState)
|
|
115
|
+
newEngine.setVolume(self.volumeState)
|
|
116
|
+
// Carry the user's speed into the new engine. When resuming playback,
|
|
117
|
+
// apply it live via `setPlaybackSpeed` (sets `AVPlayer.rate`) BEFORE
|
|
118
|
+
// `setItems` — GaplessEngine.setItems captures the player's rate to
|
|
119
|
+
// restore it across the queue rebuild, so a speed applied after
|
|
120
|
+
// setItems/play() would be clobbered by that async restore. When
|
|
121
|
+
// paused, seed it only (no rate write, so the engine stays paused);
|
|
122
|
+
// play() applies it on the next resume.
|
|
123
|
+
if wasPlaying {
|
|
124
|
+
newEngine.setPlaybackSpeed(self.playbackSpeedState)
|
|
125
|
+
} else {
|
|
126
|
+
newEngine.seedPlaybackSpeed(self.playbackSpeedState)
|
|
127
|
+
}
|
|
128
|
+
newEngine.setPitchCorrectionMode(self.pitchCorrectionModeState)
|
|
129
|
+
|
|
130
|
+
// Re-install the queue on the new engine — the same window every other
|
|
131
|
+
// install uses, not the whole queue: a crossfade engine holds every item
|
|
132
|
+
// it is given, and a long queue would put every track's asset load
|
|
133
|
+
// through the read-through server at once. `topUpWindow` extends it.
|
|
134
|
+
if resumeIndex >= 0, resumeIndex < self.tracks.count {
|
|
135
|
+
let positions = QueueWindowArithmetic.slice(
|
|
136
|
+
currentIndex: resumeIndex,
|
|
137
|
+
trackCount: self.tracks.count,
|
|
138
|
+
trail: self.engineTrail,
|
|
139
|
+
wraps: false
|
|
140
|
+
)
|
|
141
|
+
let sliceIds = positions.map { self.queueItemIds[$0] }
|
|
142
|
+
let items = AVQueueBuilder.buildPlayerItems(
|
|
143
|
+
tracks: positions.map { self.tracks[$0] },
|
|
144
|
+
queueItemIds: sliceIds,
|
|
145
|
+
config: self.config,
|
|
146
|
+
server: self.routingServer
|
|
147
|
+
)
|
|
148
|
+
// Seated the way every other install is; with nothing playable at or
|
|
149
|
+
// after the current position the new engine stays empty rather than
|
|
150
|
+
// replaying the trail.
|
|
151
|
+
let currentId = self.queueItemIds[resumeIndex]
|
|
152
|
+
if let startIndex = Self.seatIndex(in: items, sliceIds: sliceIds, currentId: currentId) {
|
|
153
|
+
// The playhead carries across only onto the track that was playing;
|
|
154
|
+
// any other seat is a track change and starts at zero.
|
|
155
|
+
let position = items[startIndex].queueItemId == currentId ? resumePosition : 0
|
|
156
|
+
newEngine.setItems(
|
|
157
|
+
items,
|
|
158
|
+
startIndex: startIndex,
|
|
159
|
+
startPositionSeconds: position
|
|
160
|
+
)
|
|
161
|
+
// Honour pre-swap playing state so a mid-play setPlaybackMode keeps
|
|
162
|
+
// playing on the new engine — through `beginPlayback`, which every
|
|
163
|
+
// path that starts playback goes through, and not while the session
|
|
164
|
+
// is interrupted.
|
|
165
|
+
if wasPlaying, !self.isInterrupted {
|
|
166
|
+
self.beginPlayback()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Whatever was seated above, the engine being replaced goes: its items
|
|
172
|
+
// stop loading and its observers come down.
|
|
173
|
+
oldEngine?.release()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
func setPlaybackSpeed(rate: Double) throws -> Promise<Void> {
|
|
177
|
+
return enqueue {
|
|
178
|
+
// Clamp to a 0.25 floor: rate=0 on AVPlayer is functionally equivalent
|
|
179
|
+
// to pause(), which would surprise consumers calling
|
|
180
|
+
// `setPlaybackSpeed(0)` for a slow-down effect. Cross-platform contract
|
|
181
|
+
// documented in TrackPlayer.nitro.ts. A value that is not a number
|
|
182
|
+
// leaves the current rate alone.
|
|
183
|
+
guard !rate.isNaN else { return }
|
|
184
|
+
let safeRate = Float(max(0.25, min(rate, 32)))
|
|
185
|
+
self.playbackSpeedState = safeRate
|
|
186
|
+
// Apply to the LIVE rate only when the user wants playback (already
|
|
187
|
+
// playing, OR buffering toward play). On a paused/idle engine writing
|
|
188
|
+
// `AVPlayer.rate` starts it (rate > 0 == "play"), so with no play intent
|
|
189
|
+
// seed the speed instead (no rate write); `play()` applies it on the next
|
|
190
|
+
// resume. This keeps a persisted speed applied at app start — before a
|
|
191
|
+
// restored `setQueue` — from auto-starting a paused queue. `wantsToPlay`
|
|
192
|
+
// (not just `isPlaying`) covers the buffering-toward-play window so a
|
|
193
|
+
// speed change there still applies live.
|
|
194
|
+
if self.wantsToPlay || self.engine?.isPlaying == true {
|
|
195
|
+
self.engine?.setPlaybackSpeed(safeRate)
|
|
196
|
+
} else {
|
|
197
|
+
self.engine?.seedPlaybackSpeed(safeRate)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
func setPitchCorrectionMode(mode: PitchCorrectionMode) throws -> Promise<Void> {
|
|
202
|
+
return enqueue {
|
|
203
|
+
self.pitchCorrectionModeState = mode
|
|
204
|
+
self.engine?.setPitchCorrectionMode(mode)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
func getPitchCorrectionMode() throws -> PitchCorrectionMode {
|
|
208
|
+
// Lock-guarded — see `getPlaybackMode`.
|
|
209
|
+
return self.pitchCorrectionModeState
|
|
210
|
+
}
|
|
211
|
+
func setVolume(volume: Double) throws -> Promise<Void> {
|
|
212
|
+
return enqueue {
|
|
213
|
+
// Clamped to the documented range before either branch, so the cast and
|
|
214
|
+
// local paths cannot diverge on the same input, and a value that is not
|
|
215
|
+
// a number cannot poison the stored level that `getVolume` and the
|
|
216
|
+
// sleep-timer fade read back.
|
|
217
|
+
guard !volume.isNaN else { return }
|
|
218
|
+
let level = Float(max(0, min(1, volume)))
|
|
219
|
+
if CastTransportRouter.routeSetVolume(value: Double(level)) { return }
|
|
220
|
+
self.volumeState = level
|
|
221
|
+
self.engine?.setVolume(level)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
func setRepeatMode(mode: RepeatMode) throws -> Promise<Void> {
|
|
226
|
+
return enqueue {
|
|
227
|
+
self.repeatModeState = mode
|
|
228
|
+
// Forward to the active engine. The CrossfadeEngine needs the
|
|
229
|
+
// mode to suppress its look-ahead fade + loop the current item
|
|
230
|
+
// under `.track` (AVPlayer has no native repeat); GaplessEngine
|
|
231
|
+
// treats it as a no-op (repeat-track replay lives in
|
|
232
|
+
// handlePlayerItemDidPlayToEndTime).
|
|
233
|
+
self.engine?.setRepeatMode(mode)
|
|
234
|
+
// Both canSkipNext and canSkipPrevious depend on repeat mode
|
|
235
|
+
// (e.g. at idx 0 / last index, OFF↔QUEUE flips the boolean
|
|
236
|
+
// because QUEUE wraps). Recompute is cheap; dedup keeps the
|
|
237
|
+
// listener silent when the (prev, next) tuple is unchanged.
|
|
238
|
+
// The installed run is forward-only, so a repeat-mode change does not
|
|
239
|
+
// alter which positions it holds and there is nothing to reconcile. That
|
|
240
|
+
// stops being true the moment the run carries the wrap target, and the
|
|
241
|
+
// reconcile has to come back with it.
|
|
242
|
+
self.recomputeCapabilities()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func setRemoteControls(options: RemoteControlOptions) throws -> Promise<Void> {
|
|
247
|
+
return enqueue {
|
|
248
|
+
// Guard nonsensical negatives (a negative forward interval would seek
|
|
249
|
+
// backward) and values too large to convert to milliseconds. Out-of-range
|
|
250
|
+
// is clamped rather than rejected.
|
|
251
|
+
let fwd = InputGuards.sanitisedSeekPosition(options.forwardJumpInterval)
|
|
252
|
+
.map { max(0, $0) } ?? 0
|
|
253
|
+
let back = InputGuards.sanitisedSeekPosition(options.backwardJumpInterval)
|
|
254
|
+
.map { max(0, $0) } ?? 0
|
|
255
|
+
self.remoteSkipMode = options.skipMode
|
|
256
|
+
self.remoteForwardInterval = fwd
|
|
257
|
+
self.remoteBackwardInterval = back
|
|
258
|
+
self.remoteCommands.setRemoteControls(
|
|
259
|
+
mode: options.skipMode,
|
|
260
|
+
forwardInterval: fwd,
|
|
261
|
+
backwardInterval: back)
|
|
262
|
+
// Re-apply the cached grey-out: switching to track mode re-enables
|
|
263
|
+
// next/prev, which must reflect the queue-boundary capability
|
|
264
|
+
// (setSkipCapability is otherwise only called on queue/repeat change,
|
|
265
|
+
// and no-ops in interval mode).
|
|
266
|
+
self.remoteCommands.setSkipCapability(
|
|
267
|
+
canSkipNext: self.cachedSkipCapability.canSkipNext,
|
|
268
|
+
canSkipPrevious: self.cachedSkipCapability.canSkipPrevious)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ReplayGain mode — thin wrapper around `AudioTapProvider.shared`.
|
|
273
|
+
// The provider owns the per-item state set + recompute walk; this
|
|
274
|
+
// Hybrid surface is the JS-facing setter / getter only.
|
|
275
|
+
func setReplayGainMode(mode: ReplayGainMode) throws -> Promise<Void> {
|
|
276
|
+
return enqueue {
|
|
277
|
+
AudioTapProvider.shared.setReplayGainMode(mode)
|
|
278
|
+
// Re-emit the active item's NowPlayingFormat so
|
|
279
|
+
// `onNowPlayingFormatChange` subscribers (e.g. the
|
|
280
|
+
// `useNowPlayingFormat` hook) observe the new `replayGainActive`
|
|
281
|
+
// flag without waiting for the next track change. Mirrors the
|
|
282
|
+
// Android path's `refreshNowPlayingFormatForActiveItem` call from
|
|
283
|
+
// its own `setReplayGainMode`.
|
|
284
|
+
guard let item = self.activeMediaItem else { return }
|
|
285
|
+
self.emitCurrentNowPlayingFormat(for: item)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
func getReplayGainMode() throws -> ReplayGainMode {
|
|
289
|
+
return AudioTapProvider.shared.getReplayGainMode()
|
|
290
|
+
}
|
|
291
|
+
}
|