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
|
@@ -25,9 +25,8 @@ private let ASYNC_DISPATCH_TIMEOUT: TimeInterval = 30.0
|
|
|
25
25
|
/// look-back tail, the in-flight keep/cancel rule, and the
|
|
26
26
|
/// drain-on-tearDown contract.
|
|
27
27
|
///
|
|
28
|
-
/// `@MainActor` because `reschedule` / `cancel` / `tearDown`
|
|
29
|
-
///
|
|
30
|
-
/// invoke them on main to match the production caller contract.
|
|
28
|
+
/// `@MainActor` because `reschedule` / `cancel` / `tearDown` assert the queue
|
|
29
|
+
/// the prefetcher was confined to, and these tests confine it to main.
|
|
31
30
|
@MainActor
|
|
32
31
|
final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
33
32
|
|
|
@@ -135,9 +134,33 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
135
134
|
|
|
136
135
|
// MARK: - Orchestration
|
|
137
136
|
|
|
137
|
+
func testRescheduleRunsOnTheInjectedQueueRatherThanMain() async {
|
|
138
|
+
let downloader = StubDownloader()
|
|
139
|
+
let confinement = DispatchQueue(label: "test.prefetcher.confinement")
|
|
140
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: confinement)
|
|
141
|
+
let tracks = (0 ..< 3).map { httpTrack("q\($0)") }
|
|
142
|
+
|
|
143
|
+
// The precondition inside `reschedule` fails the run if the caller is not
|
|
144
|
+
// on the confinement queue, so entering from it is the assertion.
|
|
145
|
+
let scheduled = expectation(description: "reschedule accepted from the injected queue")
|
|
146
|
+
// A `DispatchWorkItem` rather than a trailing closure: the prefetcher is
|
|
147
|
+
// confined to `confinement`, not shared across threads, so it does not
|
|
148
|
+
// belong in a `@Sendable` closure.
|
|
149
|
+
confinement.async(execute: DispatchWorkItem {
|
|
150
|
+
p.reschedule(tracks: tracks, currentIndex: 0, lookaheadCount: 2)
|
|
151
|
+
scheduled.fulfill()
|
|
152
|
+
})
|
|
153
|
+
await fulfillment(of: [scheduled], timeout: 2.0)
|
|
154
|
+
await p.awaitCurrent()
|
|
155
|
+
|
|
156
|
+
XCTAssertEqual(
|
|
157
|
+
downloader.invocations, ["q1", "q2"],
|
|
158
|
+
"the lookahead covers the tracks ahead of the current one, in order")
|
|
159
|
+
}
|
|
160
|
+
|
|
138
161
|
func testRescheduleWithEmptyTargetsLeavesOrchestratorIdle() async {
|
|
139
162
|
let downloader = StubDownloader()
|
|
140
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
163
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
141
164
|
p.reschedule(tracks: [], currentIndex: 0, lookaheadCount: 3)
|
|
142
165
|
await p.awaitCurrent()
|
|
143
166
|
XCTAssertFalse(p.hasInFlightTask)
|
|
@@ -146,7 +169,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
146
169
|
|
|
147
170
|
func testRescheduleDispatchesDownloadsSequentiallyInQueueOrder() async {
|
|
148
171
|
let downloader = StubDownloader()
|
|
149
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
172
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
150
173
|
let q = (0..<5).map { httpTrack("t\($0)") }
|
|
151
174
|
|
|
152
175
|
p.reschedule(tracks: q, currentIndex: 0, lookaheadCount: 3)
|
|
@@ -158,7 +181,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
158
181
|
|
|
159
182
|
func testRescheduleSwallowsPerTrackErrorAndContinues() async {
|
|
160
183
|
let downloader = StubDownloader(failingTrackIds: ["t2"])
|
|
161
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
184
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
162
185
|
let q = (0..<5).map { httpTrack("t\($0)") }
|
|
163
186
|
|
|
164
187
|
p.reschedule(tracks: q, currentIndex: 0, lookaheadCount: 3)
|
|
@@ -173,7 +196,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
173
196
|
// download attempt without marking the URL "done"; the next
|
|
174
197
|
// reschedule that still wants that URL must retry it.
|
|
175
198
|
let downloader = StubDownloader(failingTrackIds: ["t2"])
|
|
176
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
199
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
177
200
|
let q = (0..<5).map { httpTrack("t\($0)") }
|
|
178
201
|
|
|
179
202
|
p.reschedule(tracks: q, currentIndex: 0, lookaheadCount: 3)
|
|
@@ -204,7 +227,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
204
227
|
// leaving the prefetcher idle and ready for a fresh reschedule
|
|
205
228
|
// when connectivity returns.
|
|
206
229
|
let downloader = StubDownloader(failingTrackIds: ["t1", "t2", "t3"])
|
|
207
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
230
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
208
231
|
let q = (0..<5).map { httpTrack("t\($0)") }
|
|
209
232
|
|
|
210
233
|
p.reschedule(tracks: q, currentIndex: 0, lookaheadCount: 3)
|
|
@@ -237,7 +260,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
237
260
|
onStart: { firstStarted.fulfill() },
|
|
238
261
|
hold: firstHold
|
|
239
262
|
)
|
|
240
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
263
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
241
264
|
let queue = [
|
|
242
265
|
httpTrack("cur"), httpTrack("old-1"), httpTrack("old-2"),
|
|
243
266
|
httpTrack("old-3"), httpTrack("old-4"),
|
|
@@ -268,7 +291,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
268
291
|
onStart: { firstStarted.fulfill() },
|
|
269
292
|
hold: firstHold
|
|
270
293
|
)
|
|
271
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
294
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
272
295
|
|
|
273
296
|
p.reschedule(
|
|
274
297
|
tracks: [httpTrack("cur"), httpTrack("old-1"), httpTrack("old-2")],
|
|
@@ -296,7 +319,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
296
319
|
|
|
297
320
|
func testRescheduleAppendsLookbackAsLastTargetAfterForwardWindow() async {
|
|
298
321
|
let downloader = StubDownloader()
|
|
299
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
322
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
300
323
|
let q = (0..<10).map { httpTrack("t\($0)") }
|
|
301
324
|
|
|
302
325
|
// currentIndex = 5 → forward window [6,7,8] + look-back t0.
|
|
@@ -309,7 +332,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
309
332
|
|
|
310
333
|
func testRescheduleOmitsLookbackWhenCurrentTrackIsTrackZero() async {
|
|
311
334
|
let downloader = StubDownloader()
|
|
312
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
335
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
313
336
|
let q = (0..<5).map { httpTrack("t\($0)") }
|
|
314
337
|
|
|
315
338
|
p.reschedule(tracks: q, currentIndex: 0, lookaheadCount: 3)
|
|
@@ -321,7 +344,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
321
344
|
|
|
322
345
|
func testRescheduleOmitsLookbackWhenTrackZeroIsNonRemote() async {
|
|
323
346
|
let downloader = StubDownloader()
|
|
324
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
347
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
325
348
|
let q: [TrackItem] = [
|
|
326
349
|
makeTrack(id: "local-0", url: "file:///tmp/0.m4a"),
|
|
327
350
|
httpTrack("t1"), httpTrack("t2"), httpTrack("t3"), httpTrack("t4"),
|
|
@@ -338,7 +361,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
338
361
|
|
|
339
362
|
func testRescheduleDoesNotReEnqueueAlreadyCachedUrl() async {
|
|
340
363
|
let downloader = StubDownloader(cachedUrls: ["https://example.com/t2.m4a"])
|
|
341
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
364
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
342
365
|
let q = (0..<5).map { httpTrack("t\($0)") }
|
|
343
366
|
|
|
344
367
|
p.reschedule(tracks: q, currentIndex: 0, lookaheadCount: 3)
|
|
@@ -355,7 +378,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
355
378
|
// exercised on subsequent overlapping windows. Verify every
|
|
356
379
|
// remote track within reach downloads exactly once.
|
|
357
380
|
let downloader = TrackingDownloader()
|
|
358
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
381
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
359
382
|
let q = (0..<8).map { httpTrack("t\($0)") }
|
|
360
383
|
|
|
361
384
|
for cur in 0..<7 {
|
|
@@ -388,7 +411,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
388
411
|
onStart: { firstStarted.fulfill() },
|
|
389
412
|
hold: firstHold
|
|
390
413
|
)
|
|
391
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
414
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
392
415
|
|
|
393
416
|
p.reschedule(
|
|
394
417
|
tracks: [httpTrack("cur"), httpTrack("old-1"), httpTrack("old-2")],
|
|
@@ -435,7 +458,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
435
458
|
onStart: { started.fulfill() },
|
|
436
459
|
hold: hold
|
|
437
460
|
)
|
|
438
|
-
let p = LookaheadCachePrefetcher(downloader: downloader)
|
|
461
|
+
let p = LookaheadCachePrefetcher(downloader: downloader, confinedTo: .main)
|
|
439
462
|
p.reschedule(
|
|
440
463
|
tracks: [httpTrack("cur"), httpTrack("gated"), httpTrack("after")],
|
|
441
464
|
currentIndex: 0,
|
|
@@ -455,7 +478,7 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
455
478
|
// MARK: - Helpers
|
|
456
479
|
|
|
457
480
|
private func newPrefetcher() -> LookaheadCachePrefetcher {
|
|
458
|
-
return LookaheadCachePrefetcher(downloader: StubDownloader())
|
|
481
|
+
return LookaheadCachePrefetcher(downloader: StubDownloader(), confinedTo: .main)
|
|
459
482
|
}
|
|
460
483
|
|
|
461
484
|
private func httpTrack(_ id: String) -> TrackItem {
|
|
@@ -465,7 +488,9 @@ final class LookaheadCachePrefetcherTests: XCTestCase {
|
|
|
465
488
|
private func makeTrack(id: String, url: String) -> TrackItem {
|
|
466
489
|
return TrackItem(
|
|
467
490
|
id: id, url: url, title: nil, artist: nil, album: nil,
|
|
468
|
-
duration: nil, artworkUrl: nil, extras: nil
|
|
491
|
+
duration: nil, artworkUrl: nil, extras: nil,
|
|
492
|
+
replayGainTrackGain: nil, replayGainTrackPeak: nil,
|
|
493
|
+
replayGainAlbumGain: nil, replayGainAlbumPeak: nil
|
|
469
494
|
)
|
|
470
495
|
}
|
|
471
496
|
}
|
|
@@ -500,15 +525,15 @@ final class StubDownloader: LookaheadCacheDownloader, @unchecked Sendable {
|
|
|
500
525
|
}
|
|
501
526
|
|
|
502
527
|
func download(track: TrackItem, headers: [String: String]?) async throws {
|
|
503
|
-
lock.lock()
|
|
504
528
|
// Test fixtures supply non-nil track.id values; the `?? ""`
|
|
505
529
|
// satisfies the optional type while keeping the assertions
|
|
506
530
|
// simple. The lib itself never assumes track.id is non-empty —
|
|
507
531
|
// these tests verify orchestration order using fixture-supplied
|
|
508
532
|
// identifiers.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
533
|
+
let shouldFail = lock.withLock {
|
|
534
|
+
_invocations.append(track.id ?? "")
|
|
535
|
+
return _failingTrackIds.contains(track.id ?? "")
|
|
536
|
+
}
|
|
512
537
|
if shouldFail {
|
|
513
538
|
throw LookaheadCacheError.httpError(statusCode: 500)
|
|
514
539
|
}
|
|
@@ -534,10 +559,10 @@ final class TrackingDownloader: LookaheadCacheDownloader, @unchecked Sendable {
|
|
|
534
559
|
}
|
|
535
560
|
|
|
536
561
|
func download(track: TrackItem, headers: [String: String]?) async throws {
|
|
537
|
-
lock.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
562
|
+
lock.withLock {
|
|
563
|
+
_invocations.append(track.id ?? "")
|
|
564
|
+
_completed.insert(track.url)
|
|
565
|
+
}
|
|
541
566
|
}
|
|
542
567
|
|
|
543
568
|
func isFullyCached(url: String) -> Bool {
|
|
@@ -570,10 +595,8 @@ final class GatedDownloader: LookaheadCacheDownloader, @unchecked Sendable {
|
|
|
570
595
|
}
|
|
571
596
|
|
|
572
597
|
func download(track: TrackItem, headers: [String: String]?) async throws {
|
|
573
|
-
lock.lock()
|
|
574
598
|
// See `StubDownloader.download` — same `?? ""` rationale.
|
|
575
|
-
_invocations.append(track.id ?? "")
|
|
576
|
-
lock.unlock()
|
|
599
|
+
lock.withLock { _invocations.append(track.id ?? "") }
|
|
577
600
|
if track.id == gateTrackId {
|
|
578
601
|
onStart()
|
|
579
602
|
await hold.wait()
|
|
@@ -89,6 +89,27 @@ final class LookaheadCacheRuntimeConfigTests: XCTestCase {
|
|
|
89
89
|
XCTAssertTrue(cache.isFullyCached(url: track.url))
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/// A disabled cache serves nothing, whatever is on disk; the file is still
|
|
93
|
+
/// there for the next enable.
|
|
94
|
+
func testADisabledCacheAnswersNoCachedURL() async throws {
|
|
95
|
+
let cache = LookaheadCache(
|
|
96
|
+
directory: tempDir, maxSizeBytes: 1024 * 1024, urlSession: session
|
|
97
|
+
)
|
|
98
|
+
let url = URL(string: "https://example.com/held.m4a")!
|
|
99
|
+
StubURLProtocol.stub(url: url, data: Data("payload".utf8), statusCode: 200)
|
|
100
|
+
try await cache.download(
|
|
101
|
+
track: makeTrack(id: "held", url: url.absoluteString), headers: nil
|
|
102
|
+
)
|
|
103
|
+
let fileURL = try XCTUnwrap(cache.cachedURL(forUrl: url.absoluteString))
|
|
104
|
+
|
|
105
|
+
cache.disable()
|
|
106
|
+
XCTAssertNil(cache.cachedURL(forUrl: url.absoluteString), "disabled means every request goes to the origin")
|
|
107
|
+
XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path), "the file waits for the next enable")
|
|
108
|
+
|
|
109
|
+
cache.enable()
|
|
110
|
+
XCTAssertEqual(cache.cachedURL(forUrl: url.absoluteString), fileURL)
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
func testIsFullyCachedReturnsFalseWhenFileMissing() async throws {
|
|
93
114
|
let cache = LookaheadCache(
|
|
94
115
|
directory: tempDir, maxSizeBytes: 1024 * 1024, urlSession: session
|
|
@@ -164,6 +185,7 @@ final class LookaheadCacheRuntimeConfigTests: XCTestCase {
|
|
|
164
185
|
)
|
|
165
186
|
let prefetcher = LookaheadCachePrefetcher(
|
|
166
187
|
cache: cache,
|
|
188
|
+
confinedTo: .main,
|
|
167
189
|
configHeaders: ["X-Auth": "token-1"],
|
|
168
190
|
configUserAgent: "rnqp/1"
|
|
169
191
|
)
|
|
@@ -241,7 +263,144 @@ final class LookaheadCacheRuntimeConfigTests: XCTestCase {
|
|
|
241
263
|
private func makeTrack(id: String, url: String) -> TrackItem {
|
|
242
264
|
return TrackItem(
|
|
243
265
|
id: id, url: url, title: nil, artist: nil, album: nil,
|
|
244
|
-
duration: nil, artworkUrl: nil, extras: nil
|
|
266
|
+
duration: nil, artworkUrl: nil, extras: nil,
|
|
267
|
+
replayGainTrackGain: nil, replayGainTrackPeak: nil,
|
|
268
|
+
replayGainAlbumGain: nil, replayGainAlbumPeak: nil
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// MARK: - The live protected set guards every eviction, not just a shrink
|
|
273
|
+
|
|
274
|
+
func testCommitEvictionSparesTheProtectedPlayingTrack() async throws {
|
|
275
|
+
// Budget fits two entries. The third commit forces an eviction, and the
|
|
276
|
+
// oldest entry is the one playing — the LRU victim without protection.
|
|
277
|
+
let cache = LookaheadCache(
|
|
278
|
+
directory: tempDir, maxSizeBytes: 8_000, urlSession: session
|
|
279
|
+
)
|
|
280
|
+
let playingUrl = "https://example.com/playing.m4a"
|
|
281
|
+
cache.setProtectedUrls([playingUrl])
|
|
282
|
+
|
|
283
|
+
for id in ["playing", "b", "c"] {
|
|
284
|
+
let url = URL(string: "https://example.com/\(id).m4a")!
|
|
285
|
+
StubURLProtocol.stub(url: url, data: Data(repeating: 0x41, count: 4_000), statusCode: 200)
|
|
286
|
+
try await cache.download(
|
|
287
|
+
track: makeTrack(id: id, url: url.absoluteString), headers: nil
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
XCTAssertTrue(
|
|
292
|
+
cache.isFullyCached(url: playingUrl),
|
|
293
|
+
"a prefetch commit must not evict the track that is playing"
|
|
245
294
|
)
|
|
246
295
|
}
|
|
296
|
+
|
|
297
|
+
func testCommitEvictionStillTakesUnprotectedEntries() async throws {
|
|
298
|
+
let cache = LookaheadCache(
|
|
299
|
+
directory: tempDir, maxSizeBytes: 8_000, urlSession: session
|
|
300
|
+
)
|
|
301
|
+
cache.setProtectedUrls([])
|
|
302
|
+
|
|
303
|
+
for id in ["a", "b", "c"] {
|
|
304
|
+
let url = URL(string: "https://example.com/\(id).m4a")!
|
|
305
|
+
StubURLProtocol.stub(url: url, data: Data(repeating: 0x41, count: 4_000), statusCode: 200)
|
|
306
|
+
try await cache.download(
|
|
307
|
+
track: makeTrack(id: id, url: url.absoluteString), headers: nil
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
XCTAssertLessThanOrEqual(
|
|
312
|
+
cache.currentSizeBytes(), 8_000,
|
|
313
|
+
"with nothing protected the budget must still be enforced"
|
|
314
|
+
)
|
|
315
|
+
XCTAssertFalse(
|
|
316
|
+
cache.isFullyCached(url: "https://example.com/a.m4a"),
|
|
317
|
+
"the oldest unprotected entry is still the eviction victim"
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// MARK: - A download shorter than declared is never committed
|
|
322
|
+
|
|
323
|
+
private func downloadStubbed(
|
|
324
|
+
_ cache: LookaheadCache, id: String, body: Data, headers: [String: String]?
|
|
325
|
+
) async throws {
|
|
326
|
+
let url = URL(string: "https://example.com/\(id).m4a")!
|
|
327
|
+
StubURLProtocol.stub(url: url, data: body, statusCode: 200, headerFields: headers)
|
|
328
|
+
try await cache.download(track: makeTrack(id: id, url: url.absoluteString), headers: nil)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
func testTruncatedBodyIsRejected() async throws {
|
|
332
|
+
let cache = LookaheadCache(
|
|
333
|
+
directory: tempDir, maxSizeBytes: 1024 * 1024, urlSession: session
|
|
334
|
+
)
|
|
335
|
+
do {
|
|
336
|
+
try await downloadStubbed(
|
|
337
|
+
cache, id: "short", body: Data(repeating: 0x41, count: 400),
|
|
338
|
+
headers: ["Content-Length": "1000"])
|
|
339
|
+
XCTFail("a body shorter than the declared length must not be committed")
|
|
340
|
+
} catch LookaheadCacheError.truncatedBody(let expected, let received) {
|
|
341
|
+
XCTAssertEqual(expected, 1000)
|
|
342
|
+
XCTAssertEqual(received, 400)
|
|
343
|
+
}
|
|
344
|
+
XCTAssertFalse(
|
|
345
|
+
cache.isFullyCached(url: "https://example.com/short.m4a"),
|
|
346
|
+
"a rejected download must leave no cache entry to poison later plays")
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
func testExactLengthBodyIsAccepted() async throws {
|
|
350
|
+
let cache = LookaheadCache(
|
|
351
|
+
directory: tempDir, maxSizeBytes: 1024 * 1024, urlSession: session
|
|
352
|
+
)
|
|
353
|
+
try await downloadStubbed(
|
|
354
|
+
cache, id: "exact", body: Data(repeating: 0x41, count: 400),
|
|
355
|
+
headers: ["Content-Length": "400"])
|
|
356
|
+
XCTAssertTrue(cache.isFullyCached(url: "https://example.com/exact.m4a"))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
func testChunkedBodyWithNoDeclaredLengthIsAccepted() async throws {
|
|
360
|
+
let cache = LookaheadCache(
|
|
361
|
+
directory: tempDir, maxSizeBytes: 1024 * 1024, urlSession: session
|
|
362
|
+
)
|
|
363
|
+
try await downloadStubbed(
|
|
364
|
+
cache, id: "chunked", body: Data(repeating: 0x41, count: 400), headers: nil)
|
|
365
|
+
XCTAssertTrue(
|
|
366
|
+
cache.isFullyCached(url: "https://example.com/chunked.m4a"),
|
|
367
|
+
"no declared length means nothing to compare against, not a rejection")
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
func testBodyLongerThanDeclaredIsAccepted() async throws {
|
|
371
|
+
let cache = LookaheadCache(
|
|
372
|
+
directory: tempDir, maxSizeBytes: 1024 * 1024, urlSession: session
|
|
373
|
+
)
|
|
374
|
+
// A decoded body can exceed the declared compressed length; only short
|
|
375
|
+
// is a truncation.
|
|
376
|
+
try await downloadStubbed(
|
|
377
|
+
cache, id: "longer", body: Data(repeating: 0x41, count: 400),
|
|
378
|
+
headers: ["Content-Length": "200"])
|
|
379
|
+
XCTAssertTrue(cache.isFullyCached(url: "https://example.com/longer.m4a"))
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// MARK: - buildCacheStatusSnapshot
|
|
383
|
+
|
|
384
|
+
func testCacheStatusSnapshotReportsTheSuppliedInFlightUrl() {
|
|
385
|
+
let player = TrackPlayer()
|
|
386
|
+
let status = player.buildCacheStatusSnapshot(
|
|
387
|
+
inFlightUrl: "https://example.com/inflight.m4a")
|
|
388
|
+
XCTAssertEqual(status.currentlyCaching, ["https://example.com/inflight.m4a"])
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
func testCacheStatusSnapshotReportsNothingCachingWithoutAnInFlightUrl() {
|
|
392
|
+
let player = TrackPlayer()
|
|
393
|
+
XCTAssertEqual(player.buildCacheStatusSnapshot(inFlightUrl: nil).currentlyCaching, [])
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
func testCacheStatusSnapshotReportsTheDefaultCacheCeilingBeforeConfigure() {
|
|
397
|
+
let player = TrackPlayer()
|
|
398
|
+
let status = player.buildCacheStatusSnapshot(inFlightUrl: nil)
|
|
399
|
+
XCTAssertTrue(status.enabled)
|
|
400
|
+
XCTAssertEqual(status.currentSizeMb, 0)
|
|
401
|
+
XCTAssertEqual(status.tracksFullyCached, 0)
|
|
402
|
+
XCTAssertEqual(
|
|
403
|
+
status.maxSizeMb,
|
|
404
|
+
Double(LookaheadCache.DEFAULT_MAX_SIZE_BYTES) / Double(LookaheadCache.BYTES_PER_MB))
|
|
405
|
+
}
|
|
247
406
|
}
|
|
@@ -240,6 +240,31 @@ final class LookaheadCacheTests: XCTestCase {
|
|
|
240
240
|
XCTAssertNil(cache.cachedURL(forUrl: "wipe"))
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
// MARK: - Teardown quiescence
|
|
244
|
+
|
|
245
|
+
func testDownloadAfterTearDownIsANoOp() async throws {
|
|
246
|
+
// A prefetcher Task can reach `download` after the owning player has
|
|
247
|
+
// torn the cache down. Creating a URLSession task on the invalidated
|
|
248
|
+
// session raises an Obj-C exception that no Swift `do-catch` can
|
|
249
|
+
// intercept, so the teardown has to quiesce the cache first.
|
|
250
|
+
let cache = LookaheadCache(
|
|
251
|
+
directory: tempDir, maxSizeBytes: LookaheadCache.DEFAULT_MAX_SIZE_BYTES,
|
|
252
|
+
urlSession: session)
|
|
253
|
+
let url = URL(string: "https://example.com/after-teardown.m4a")!
|
|
254
|
+
StubURLProtocol.stub(url: url, data: Data("x".utf8), statusCode: 200)
|
|
255
|
+
let track = makeTrack(id: "after-teardown", url: url.absoluteString)
|
|
256
|
+
|
|
257
|
+
cache.tearDown()
|
|
258
|
+
try await cache.download(track: track, headers: nil)
|
|
259
|
+
|
|
260
|
+
XCTAssertNil(
|
|
261
|
+
cache.cachedURL(forUrl: track.url),
|
|
262
|
+
"a download issued after tearDown must not populate the cache")
|
|
263
|
+
XCTAssertEqual(
|
|
264
|
+
StubURLProtocol.requestCount(for: url), 0,
|
|
265
|
+
"a download issued after tearDown must not reach the session")
|
|
266
|
+
}
|
|
267
|
+
|
|
243
268
|
// MARK: - Eviction integration
|
|
244
269
|
|
|
245
270
|
func testDownloadEvictsOldestWhenBudgetExceeded() async throws {
|
|
@@ -498,17 +523,16 @@ final class LookaheadCacheTests: XCTestCase {
|
|
|
498
523
|
|
|
499
524
|
// MARK: - URL → bytes round-trip correctness
|
|
500
525
|
//
|
|
501
|
-
// These tests
|
|
526
|
+
// These tests cover the contract that downloading URL A's bytes
|
|
502
527
|
// surfaces those exact bytes via cachedURL(forUrl: A) — and that
|
|
503
528
|
// a sibling download of URL B doesn't disturb URL A's mapping.
|
|
504
|
-
//
|
|
505
|
-
//
|
|
529
|
+
// The cache serves each URL its own bytes after any sequence of
|
|
530
|
+
// forward and look-back prefetches.
|
|
506
531
|
|
|
507
532
|
func testCachedFileBytesMatchSourceUrlBytes() async throws {
|
|
508
533
|
// The fundamental contract: download URL A → cachedURL(A)'s file
|
|
509
534
|
// contains A's bytes. No way for the cache to serve B's bytes
|
|
510
|
-
// for A's URL. This is the core invariant
|
|
511
|
-
// wrong-file-mapping bug would violate.
|
|
535
|
+
// for A's URL. This is the core invariant.
|
|
512
536
|
let cache = LookaheadCache(directory: tempDir, maxSizeBytes: LookaheadCache.DEFAULT_MAX_SIZE_BYTES, urlSession: session)
|
|
513
537
|
let urlA = URL(string: "https://example.com/song-a.m4a")!
|
|
514
538
|
let bytesA = Data("AAAAAAAAAAAAAAAAAAA-payload-for-track-A".utf8)
|
|
@@ -553,7 +577,7 @@ final class LookaheadCacheTests: XCTestCase {
|
|
|
553
577
|
}
|
|
554
578
|
|
|
555
579
|
func testLookbackPathPreservesPerUrlBytes() async throws {
|
|
556
|
-
//
|
|
580
|
+
// Forward-prefetch t1/t2/t3,
|
|
557
581
|
// then look-back-prefetch t0. After the sequence, every URL
|
|
558
582
|
// resolves to its own bytes — the look-back doesn't clobber the
|
|
559
583
|
// forward window's mappings.
|
|
@@ -584,10 +608,9 @@ final class LookaheadCacheTests: XCTestCase {
|
|
|
584
608
|
}
|
|
585
609
|
|
|
586
610
|
func testFileNameUniquenessAcrossSimilarUrls() {
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
//
|
|
590
|
-
// ?id=N query parameter.
|
|
611
|
+
// fileName(forUrl:) must not collide for URLs with similar
|
|
612
|
+
// prefixes — e.g. stream URLs that differ only in an ?id=N query
|
|
613
|
+
// parameter.
|
|
591
614
|
let urls = [
|
|
592
615
|
"https://music.example.com/rest/stream?id=1&u=test",
|
|
593
616
|
"https://music.example.com/rest/stream?id=2&u=test",
|
|
@@ -601,10 +624,8 @@ final class LookaheadCacheTests: XCTestCase {
|
|
|
601
624
|
|
|
602
625
|
func testCollisionCandidateUrlsRoundTripDistinctBytes() async throws {
|
|
603
626
|
// End-to-end correctness: download four URLs whose only difference
|
|
604
|
-
// is an `?id=N` query parameter
|
|
605
|
-
//
|
|
606
|
-
// fileName uniqueness holds but `download` -> `cachedURL` keying
|
|
607
|
-
// ever drifted to use a non-URL field.
|
|
627
|
+
// is an `?id=N` query parameter and verify each resolves to its own
|
|
628
|
+
// bytes: `download` and `cachedURL` must key on the same URL.
|
|
608
629
|
let cache = LookaheadCache(directory: tempDir, maxSizeBytes: LookaheadCache.DEFAULT_MAX_SIZE_BYTES, urlSession: session)
|
|
609
630
|
let urlBytes: [(url: URL, bytes: Data)] = [
|
|
610
631
|
"https://music.example.com/rest/stream?id=1&u=test",
|
|
@@ -638,7 +659,9 @@ final class LookaheadCacheTests: XCTestCase {
|
|
|
638
659
|
private func makeTrack(id: String, url: String) -> TrackItem {
|
|
639
660
|
return TrackItem(
|
|
640
661
|
id: id, url: url, title: nil, artist: nil, album: nil,
|
|
641
|
-
duration: nil, artworkUrl: nil, extras: nil
|
|
662
|
+
duration: nil, artworkUrl: nil, extras: nil,
|
|
663
|
+
replayGainTrackGain: nil, replayGainTrackPeak: nil,
|
|
664
|
+
replayGainAlbumGain: nil, replayGainAlbumPeak: nil
|
|
642
665
|
)
|
|
643
666
|
}
|
|
644
667
|
}
|
|
@@ -654,15 +677,18 @@ final class StubURLProtocol: URLProtocol {
|
|
|
654
677
|
private struct Stub {
|
|
655
678
|
let data: Data
|
|
656
679
|
let statusCode: Int
|
|
680
|
+
let headerFields: [String: String]?
|
|
657
681
|
}
|
|
658
682
|
|
|
659
683
|
private static let lock = NSLock()
|
|
660
684
|
private static var stubs: [URL: Stub] = [:]
|
|
661
685
|
private static var requests: [URL: [URLRequest]] = [:]
|
|
662
686
|
|
|
663
|
-
static func stub(
|
|
687
|
+
static func stub(
|
|
688
|
+
url: URL, data: Data, statusCode: Int, headerFields: [String: String]? = nil
|
|
689
|
+
) {
|
|
664
690
|
lock.lock(); defer { lock.unlock() }
|
|
665
|
-
stubs[url] = Stub(data: data, statusCode: statusCode)
|
|
691
|
+
stubs[url] = Stub(data: data, statusCode: statusCode, headerFields: headerFields)
|
|
666
692
|
}
|
|
667
693
|
|
|
668
694
|
static func reset() {
|
|
@@ -706,7 +732,7 @@ final class StubURLProtocol: URLProtocol {
|
|
|
706
732
|
|
|
707
733
|
let response = HTTPURLResponse(
|
|
708
734
|
url: url, statusCode: stub.statusCode,
|
|
709
|
-
httpVersion: "HTTP/1.1", headerFields:
|
|
735
|
+
httpVersion: "HTTP/1.1", headerFields: stub.headerFields
|
|
710
736
|
)!
|
|
711
737
|
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
|
712
738
|
client?.urlProtocol(self, didLoad: stub.data)
|
|
@@ -161,3 +161,82 @@ final class MediaHTTPConnectionTests: XCTestCase {
|
|
|
161
161
|
XCTAssertTrue(head.contains("Content-Length: 0\r\n"))
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
+
|
|
165
|
+
/// The head the read-through leg relays from the origin to the player.
|
|
166
|
+
///
|
|
167
|
+
/// The player is AVFoundation reading media: it needs the status, the type, the
|
|
168
|
+
/// length and the range, and nothing else the origin chose to say.
|
|
169
|
+
final class MediaHTTPRelayHeadTests: XCTestCase {
|
|
170
|
+
|
|
171
|
+
private func response(
|
|
172
|
+
status: Int,
|
|
173
|
+
headers: [String: String]
|
|
174
|
+
) -> HTTPURLResponse {
|
|
175
|
+
HTTPURLResponse(
|
|
176
|
+
url: URL(string: "https://example.com/track.mp3")!,
|
|
177
|
+
statusCode: status,
|
|
178
|
+
httpVersion: "HTTP/1.1",
|
|
179
|
+
headerFields: headers
|
|
180
|
+
)!
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
func testRelaysTheOriginStatusLine() {
|
|
184
|
+
let head = MediaHTTPConnection.relayHead(response(status: 206, headers: [:]))
|
|
185
|
+
XCTAssertTrue(head.hasPrefix("HTTP/1.1 206 Partial Content\r\n"), head)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// The status line stays ASCII whatever the device locale; an unlisted
|
|
189
|
+
/// status carries an empty reason phrase, which HTTP/1.1 permits.
|
|
190
|
+
func testTheReasonPhraseIsFixedTextNotLocalisedText() {
|
|
191
|
+
XCTAssertEqual(MediaHTTPConnection.reasonPhrase(for: 404), "Not Found")
|
|
192
|
+
XCTAssertEqual(MediaHTTPConnection.reasonPhrase(for: 418), "")
|
|
193
|
+
let head = MediaHTTPConnection.relayHead(response(status: 418, headers: [:]))
|
|
194
|
+
XCTAssertTrue(head.hasPrefix("HTTP/1.1 418 \r\n"), head)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// An HTTP error reaches the player as an HTTP error, so it classifies as the
|
|
198
|
+
/// server's answer rather than as a transport failure.
|
|
199
|
+
func testRelaysAnErrorStatusRatherThanMaskingIt() {
|
|
200
|
+
let head = MediaHTTPConnection.relayHead(response(status: 404, headers: [:]))
|
|
201
|
+
XCTAssertTrue(head.hasPrefix("HTTP/1.1 404 "), head)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func testCarriesTheHeadersThePlayerReadsMediaWith() {
|
|
205
|
+
let head = MediaHTTPConnection.relayHead(response(status: 206, headers: [
|
|
206
|
+
"Content-Type": "audio/mpeg",
|
|
207
|
+
"Content-Length": "1024",
|
|
208
|
+
"Content-Range": "bytes 0-1023/8192",
|
|
209
|
+
"Accept-Ranges": "bytes",
|
|
210
|
+
]))
|
|
211
|
+
XCTAssertTrue(head.contains("Content-Type: audio/mpeg"), head)
|
|
212
|
+
XCTAssertTrue(head.contains("Content-Length: 1024"), head)
|
|
213
|
+
XCTAssertTrue(head.contains("Content-Range: bytes 0-1023/8192"), head)
|
|
214
|
+
XCTAssertTrue(head.contains("Accept-Ranges: bytes"), head)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func testDropsEverythingElseTheOriginSaid() {
|
|
218
|
+
let head = MediaHTTPConnection.relayHead(response(status: 200, headers: [
|
|
219
|
+
"Content-Type": "audio/mpeg",
|
|
220
|
+
"Set-Cookie": "session=secret",
|
|
221
|
+
"WWW-Authenticate": "Bearer realm=\"origin\"",
|
|
222
|
+
"Location": "https://elsewhere.example.com/track.mp3",
|
|
223
|
+
"Server": "origin/1.0",
|
|
224
|
+
]))
|
|
225
|
+
XCTAssertFalse(head.lowercased().contains("set-cookie"), head)
|
|
226
|
+
XCTAssertFalse(head.lowercased().contains("www-authenticate"), head)
|
|
227
|
+
XCTAssertFalse(head.lowercased().contains("location"), head)
|
|
228
|
+
XCTAssertFalse(head.lowercased().contains("server:"), head)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
func testHeadIsTerminatedSoABodyCanFollow() {
|
|
232
|
+
let head = MediaHTTPConnection.relayHead(response(status: 200, headers: ["Content-Length": "1"]))
|
|
233
|
+
XCTAssertTrue(head.hasSuffix("\r\n\r\n"), head.debugDescription)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// An absent header is omitted rather than sent empty — a `Content-Range: `
|
|
237
|
+
/// with no value is a malformed response to a range request.
|
|
238
|
+
func testOmitsHeadersTheOriginDidNotSend() {
|
|
239
|
+
let head = MediaHTTPConnection.relayHead(response(status: 200, headers: ["Content-Type": "audio/mpeg"]))
|
|
240
|
+
XCTAssertFalse(head.contains("Content-Range"), head)
|
|
241
|
+
}
|
|
242
|
+
}
|