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
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import XCTest
|
|
2
2
|
@testable import QueuePlayer
|
|
3
3
|
|
|
4
|
-
/// `TrackPlayer.classifyTrackSource
|
|
5
|
-
///
|
|
6
|
-
///
|
|
7
|
-
///
|
|
4
|
+
/// `TrackPlayer.classifyTrackSource` decides how the active track is being
|
|
5
|
+
/// played from lib-observable facts only: the URL scheme, whether the item is
|
|
6
|
+
/// routed through the read-through server, and a cache-hit probe. Pure +
|
|
7
|
+
/// static, so it's exercised directly without a live engine.
|
|
8
8
|
final class TrackSourceClassifierTests: XCTestCase {
|
|
9
9
|
|
|
10
|
-
private func classify(
|
|
11
|
-
|
|
10
|
+
private func classify(
|
|
11
|
+
_ url: String?, routed: Bool = true, cached: Bool = false
|
|
12
|
+
) -> TrackSource? {
|
|
13
|
+
TrackPlayer.classifyTrackSource(url: url, isRouted: routed) { _ in cached }
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
func testNilUrlIsUnclassified() {
|
|
@@ -40,11 +42,35 @@ final class TrackSourceClassifierTests: XCTestCase {
|
|
|
40
42
|
|
|
41
43
|
func testLocalFileNeverConsultsTheCacheProbe() {
|
|
42
44
|
var probed = false
|
|
43
|
-
let source = TrackPlayer.classifyTrackSource(url: "file:///a.m4a") { _ in
|
|
45
|
+
let source = TrackPlayer.classifyTrackSource(url: "file:///a.m4a", isRouted: true) { _ in
|
|
44
46
|
probed = true
|
|
45
47
|
return true
|
|
46
48
|
}
|
|
47
49
|
XCTAssertEqual(source, .local)
|
|
48
50
|
XCTAssertFalse(probed, "a file:// URL is local without a cache lookup")
|
|
49
51
|
}
|
|
52
|
+
|
|
53
|
+
/// Cached on disk is not the same as played from disk. Only an item bound to
|
|
54
|
+
/// the read-through server reads the cache, so a complete file the player is
|
|
55
|
+
/// not routed through is still being streamed — reporting otherwise is what
|
|
56
|
+
/// made this field claim `cached` while every byte came from the origin.
|
|
57
|
+
func testACachedTrackThePlayerIsNotRoutedThroughIsStreaming() {
|
|
58
|
+
XCTAssertEqual(
|
|
59
|
+
classify("https://example.com/a.mp3", routed: false, cached: true), .streaming)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func testARoutedTrackTheCacheHoldsInFullIsCached() {
|
|
63
|
+
XCTAssertEqual(
|
|
64
|
+
classify("https://example.com/a.mp3", routed: true, cached: true), .cached)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func testARoutedTrackTheCacheDoesNotHoldIsStreaming() {
|
|
68
|
+
XCTAssertEqual(
|
|
69
|
+
classify("https://example.com/a.mp3", routed: true, cached: false), .streaming)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// A local file is local whatever the server and cache report.
|
|
73
|
+
func testAFileTrackIsLocalRegardlessOfRouting() {
|
|
74
|
+
XCTAssertEqual(classify("file:///a.m4a", routed: false, cached: true), .local)
|
|
75
|
+
}
|
|
50
76
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
|
|
4
|
+
/// Siri vocabulary donation is off unless the consumer opts in, and the
|
|
5
|
+
/// opt-in is fixed once taken.
|
|
6
|
+
///
|
|
7
|
+
/// The flag stands for the `com.apple.developer.siri` entitlement, which
|
|
8
|
+
/// `INVocabulary` checks at runtime and which iOS gives a process no way to
|
|
9
|
+
/// read. An entitlement is decided when the app is signed, so a value that
|
|
10
|
+
/// changed later could not describe anything — and re-reading one would let a
|
|
11
|
+
/// consumer re-arm a path that terminates a host without the capability.
|
|
12
|
+
final class VoiceVocabularyOptInTests: XCTestCase {
|
|
13
|
+
|
|
14
|
+
func testDonationIsOffUntilTheConsumerOptsIn() {
|
|
15
|
+
let player = TrackPlayer()
|
|
16
|
+
XCTAssertFalse(
|
|
17
|
+
player.voiceVocabularyDonationEnabled,
|
|
18
|
+
"an untouched player has not been given the entitlement to assert")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func testTheFirstOptInIsTheOneThatCounts() {
|
|
22
|
+
let player = TrackPlayer()
|
|
23
|
+
player.pinVoiceVocabularyDonation(true)
|
|
24
|
+
player.pinVoiceVocabularyDonation(false)
|
|
25
|
+
XCTAssertTrue(
|
|
26
|
+
player.voiceVocabularyDonationEnabled,
|
|
27
|
+
"a later value does not unset an opt-in already taken")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func testDecliningIsAlsoFixed() {
|
|
31
|
+
let player = TrackPlayer()
|
|
32
|
+
player.pinVoiceVocabularyDonation(false)
|
|
33
|
+
player.pinVoiceVocabularyDonation(true)
|
|
34
|
+
XCTAssertFalse(
|
|
35
|
+
player.voiceVocabularyDonationEnabled,
|
|
36
|
+
"a later value does not turn donation on after it was declined")
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -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
|
+
}
|