react-native-queue-player 1.2.0 → 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/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 +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 +839 -271
- 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 +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 +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 +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 +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 +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 +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/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 +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/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 -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
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import Network
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// The loopback server the player reads every remote track through.
|
|
6
|
+
///
|
|
7
|
+
/// These drive a real listener over a real socket, because what matters here is
|
|
8
|
+
/// what an HTTP client actually receives — the byte-range maths is covered by
|
|
9
|
+
/// `MediaHTTPConnectionTests`.
|
|
10
|
+
final class ReadThroughServerTests: XCTestCase {
|
|
11
|
+
|
|
12
|
+
private var tempDir: URL!
|
|
13
|
+
|
|
14
|
+
override func setUpWithError() throws {
|
|
15
|
+
tempDir = FileManager.default.temporaryDirectory
|
|
16
|
+
.appendingPathComponent("readthrough-tests-\(UUID().uuidString)")
|
|
17
|
+
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override func tearDownWithError() throws {
|
|
21
|
+
try? FileManager.default.removeItem(at: tempDir)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private func writeFile(_ bytes: [UInt8], named name: String = "track.mp3") throws -> URL {
|
|
25
|
+
let url = tempDir.appendingPathComponent(name)
|
|
26
|
+
try Data(bytes).write(to: url)
|
|
27
|
+
return url
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// An origin that cannot be reached, for the paths where the cache must be
|
|
31
|
+
/// what answers — a request that reached the network would fail loudly.
|
|
32
|
+
private let unreachableOrigin = URL(string: "http://127.0.0.1:1/never.mp3")!
|
|
33
|
+
|
|
34
|
+
private func makeServer(
|
|
35
|
+
cachedFile: @escaping (String) -> URL? = { _ in nil },
|
|
36
|
+
onFirstDiskHit: @escaping (String) -> Void = { _ in }
|
|
37
|
+
) async -> ReadThroughServer {
|
|
38
|
+
let server = ReadThroughServer(
|
|
39
|
+
cachedFileForOrigin: cachedFile,
|
|
40
|
+
onFirstDiskHit: onFirstDiskHit
|
|
41
|
+
)
|
|
42
|
+
server.start()
|
|
43
|
+
XCTAssertTrue(server.waitUntilBound(), "the loopback listener binds within the wait")
|
|
44
|
+
return server
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// The cache is keyed on the consumer's `track.url` verbatim; the lookup
|
|
48
|
+
/// receives that string, not the origin URL's re-serialisation.
|
|
49
|
+
func testTheCacheLookupReceivesTheSourceStringVerbatim() async throws {
|
|
50
|
+
var asked: [String] = []
|
|
51
|
+
let server = await makeServer(cachedFile: { asked.append($0); return nil })
|
|
52
|
+
defer { server.stop() }
|
|
53
|
+
// Loopback port 1 refuses at once; the space is what the assertion needs.
|
|
54
|
+
let raw = "http://127.0.0.1:1/a b.mp3"
|
|
55
|
+
let origin = try XCTUnwrap(URL(string: raw))
|
|
56
|
+
let local = try XCTUnwrap(server.localURL(for: origin, sourceURL: raw))
|
|
57
|
+
|
|
58
|
+
_ = try? await get(local)
|
|
59
|
+
|
|
60
|
+
// The refused origin drops the connection and URLSession retries the
|
|
61
|
+
// request, so the lookup runs once per attempt; the string is what matters.
|
|
62
|
+
XCTAssertFalse(asked.isEmpty)
|
|
63
|
+
XCTAssertEqual(Set(asked), [raw])
|
|
64
|
+
XCTAssertNotEqual(origin.absoluteString, raw, "the origin re-serialises the space; the key must not")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// A raw HTTP client that reads `chunk` bytes at a time with a pause between
|
|
68
|
+
/// reads, so the relay is what fills up rather than the client. Returns the
|
|
69
|
+
/// body once `Content-Length` bytes have arrived.
|
|
70
|
+
private func slowGet(_ url: URL, chunk: Int, pauseNanoseconds: UInt64) async throws -> Data {
|
|
71
|
+
let host = try XCTUnwrap(url.host)
|
|
72
|
+
let port = try XCTUnwrap(NWEndpoint.Port(rawValue: UInt16(try XCTUnwrap(url.port))))
|
|
73
|
+
let connection = NWConnection(host: NWEndpoint.Host(host), port: port, using: .tcp)
|
|
74
|
+
defer { connection.cancel() }
|
|
75
|
+
connection.start(queue: DispatchQueue(label: "slow-get"))
|
|
76
|
+
// A relay that stops delivering would otherwise block the receive forever.
|
|
77
|
+
let deadline = DispatchWorkItem { connection.cancel() }
|
|
78
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + 30, execute: deadline)
|
|
79
|
+
defer { deadline.cancel() }
|
|
80
|
+
let request = "GET \(url.path) HTTP/1.1\r\nHost: \(host)\r\nConnection: close\r\n\r\n"
|
|
81
|
+
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
|
82
|
+
connection.send(content: Data(request.utf8), completion: .contentProcessed { error in
|
|
83
|
+
if let error { cont.resume(throwing: error) } else { cont.resume() }
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
var received = Data()
|
|
87
|
+
var bodyStart: Int?
|
|
88
|
+
var contentLength: Int?
|
|
89
|
+
while true {
|
|
90
|
+
let (piece, complete) = try await withCheckedThrowingContinuation {
|
|
91
|
+
(cont: CheckedContinuation<(Data?, Bool), Error>) in
|
|
92
|
+
connection.receive(minimumIncompleteLength: 1, maximumLength: chunk) {
|
|
93
|
+
data, _, isComplete, error in
|
|
94
|
+
if let error { cont.resume(throwing: error) } else { cont.resume(returning: (data, isComplete)) }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if let piece { received.append(piece) }
|
|
98
|
+
if bodyStart == nil, let headerEnd = received.range(of: Data("\r\n\r\n".utf8)) {
|
|
99
|
+
bodyStart = headerEnd.upperBound
|
|
100
|
+
let head = String(decoding: received[..<headerEnd.lowerBound], as: UTF8.self)
|
|
101
|
+
let lengthLine = head.split(separator: "\r\n").first { $0.lowercased().hasPrefix("content-length:") }
|
|
102
|
+
contentLength = lengthLine.flatMap { Int($0.split(separator: ":")[1].trimmingCharacters(in: .whitespaces)) }
|
|
103
|
+
}
|
|
104
|
+
if let bodyStart, let contentLength, received.count - bodyStart >= contentLength {
|
|
105
|
+
return received[bodyStart ..< bodyStart + contentLength]
|
|
106
|
+
}
|
|
107
|
+
if complete { throw URLError(.networkConnectionLost) }
|
|
108
|
+
try await Task.sleep(nanoseconds: pauseNanoseconds)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private func get(_ url: URL, range: String? = nil) async throws -> (HTTPURLResponse, Data) {
|
|
113
|
+
var request = URLRequest(url: url)
|
|
114
|
+
if let range { request.setValue(range, forHTTPHeaderField: "Range") }
|
|
115
|
+
let config = URLSessionConfiguration.ephemeral
|
|
116
|
+
config.timeoutIntervalForRequest = 10
|
|
117
|
+
let (data, response) = try await URLSession(configuration: config).data(for: request)
|
|
118
|
+
return (response as! HTTPURLResponse, data)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// MARK: - Routing
|
|
122
|
+
|
|
123
|
+
func testNoLocalURLBeforeTheListenerIsUp() {
|
|
124
|
+
let server = ReadThroughServer(cachedFileForOrigin: { _ in nil }, onFirstDiskHit: { _ in })
|
|
125
|
+
XCTAssertNil(
|
|
126
|
+
server.localURL(for: unreachableOrigin),
|
|
127
|
+
"an unbound server must report no URL so the caller keeps the origin")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func testTheURLIsLoopbackAndCarriesA32HexToken() async {
|
|
131
|
+
let server = await makeServer()
|
|
132
|
+
defer { server.stop() }
|
|
133
|
+
let url = server.localURL(for: unreachableOrigin)
|
|
134
|
+
XCTAssertEqual(url?.host, "127.0.0.1")
|
|
135
|
+
let token = url?.lastPathComponent ?? ""
|
|
136
|
+
XCTAssertEqual(token.count, 32, token)
|
|
137
|
+
XCTAssertTrue(token.allSatisfy { $0.isHexDigit }, token)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// One token per origin for the life of the session. A window rebuild hands
|
|
141
|
+
/// the player the URL it already holds, so the asset never re-points and
|
|
142
|
+
/// AVFoundation keeps reusing its own connections.
|
|
143
|
+
func testTheSameOriginAlwaysGetsTheSameURL() async {
|
|
144
|
+
let server = await makeServer()
|
|
145
|
+
defer { server.stop() }
|
|
146
|
+
let first = server.localURL(for: unreachableOrigin)
|
|
147
|
+
let second = server.localURL(for: unreachableOrigin)
|
|
148
|
+
XCTAssertEqual(first, second)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func testDifferentOriginsGetDifferentTokens() async {
|
|
152
|
+
let server = await makeServer()
|
|
153
|
+
defer { server.stop() }
|
|
154
|
+
let a = server.localURL(for: URL(string: "https://example.com/a.mp3")!)
|
|
155
|
+
let b = server.localURL(for: URL(string: "https://example.com/b.mp3")!)
|
|
156
|
+
XCTAssertNotEqual(a, b)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// MARK: - Serving
|
|
160
|
+
|
|
161
|
+
func testAFullyCachedFileIsServedFromDisk() async throws {
|
|
162
|
+
let bytes = (0 ..< 2048).map { UInt8($0 % 251) }
|
|
163
|
+
let file = try writeFile(bytes)
|
|
164
|
+
let server = await makeServer(cachedFile: { _ in file })
|
|
165
|
+
defer { server.stop() }
|
|
166
|
+
let url = try XCTUnwrap(server.localURL(for: unreachableOrigin))
|
|
167
|
+
|
|
168
|
+
let (response, data) = try await get(url)
|
|
169
|
+
|
|
170
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
171
|
+
XCTAssertEqual([UInt8](data), bytes, "the bytes served are not the cached file's")
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func testARangeRequestOnACachedFileGetsThatRange() async throws {
|
|
175
|
+
let bytes = (0 ..< 2048).map { UInt8($0 % 251) }
|
|
176
|
+
let file = try writeFile(bytes)
|
|
177
|
+
let server = await makeServer(cachedFile: { _ in file })
|
|
178
|
+
defer { server.stop() }
|
|
179
|
+
let url = try XCTUnwrap(server.localURL(for: unreachableOrigin))
|
|
180
|
+
|
|
181
|
+
let (response, data) = try await get(url, range: "bytes=100-199")
|
|
182
|
+
|
|
183
|
+
XCTAssertEqual(response.statusCode, 206)
|
|
184
|
+
XCTAssertEqual(response.value(forHTTPHeaderField: "Content-Range"), "bytes 100-199/2048")
|
|
185
|
+
XCTAssertEqual([UInt8](data), Array(bytes[100 ... 199]))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// AVFoundation opens every remote asset with this probe and reads the total
|
|
189
|
+
/// size out of the `Content-Range`.
|
|
190
|
+
func testTheContentInfoProbeIsAnsweredWithTheResourceSize() async throws {
|
|
191
|
+
let file = try writeFile((0 ..< 5000).map { UInt8($0 % 251) })
|
|
192
|
+
let server = await makeServer(cachedFile: { _ in file })
|
|
193
|
+
defer { server.stop() }
|
|
194
|
+
let url = try XCTUnwrap(server.localURL(for: unreachableOrigin))
|
|
195
|
+
|
|
196
|
+
let (response, data) = try await get(url, range: "bytes=0-1")
|
|
197
|
+
|
|
198
|
+
XCTAssertEqual(response.statusCode, 206)
|
|
199
|
+
XCTAssertEqual(response.value(forHTTPHeaderField: "Content-Range"), "bytes 0-1/5000")
|
|
200
|
+
XCTAssertEqual(data.count, 2)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func testAnUnknownTokenIs404() async throws {
|
|
204
|
+
let server = await makeServer()
|
|
205
|
+
defer { server.stop() }
|
|
206
|
+
let port = server.boundPortValue()
|
|
207
|
+
let url = URL(string: "http://127.0.0.1:\(port)/media/\(String(repeating: "a", count: 32))")!
|
|
208
|
+
|
|
209
|
+
let (response, _) = try await get(url)
|
|
210
|
+
|
|
211
|
+
XCTAssertEqual(response.statusCode, 404)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// The client names a token and nothing else. A path or query that carried a
|
|
215
|
+
/// URL would make this an open relay that fetches whatever it is told with
|
|
216
|
+
/// the consumer's credentials attached.
|
|
217
|
+
func testAPathThatNamesAURLIsRejected() async throws {
|
|
218
|
+
let server = await makeServer()
|
|
219
|
+
defer { server.stop() }
|
|
220
|
+
let port = server.boundPortValue()
|
|
221
|
+
let url = URL(string: "http://127.0.0.1:\(port)/media/https%3A%2F%2Fevil.example.com%2Fx.mp3")!
|
|
222
|
+
|
|
223
|
+
let (response, _) = try await get(url)
|
|
224
|
+
|
|
225
|
+
XCTAssertEqual(response.statusCode, 404)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// A cache entry whose file has gone reads through instead of failing, so a
|
|
229
|
+
/// `clear()` during playback costs a re-fetch rather than a dead item.
|
|
230
|
+
func testAMissingCacheFileFallsThroughToTheOrigin() async throws {
|
|
231
|
+
let vanished = tempDir.appendingPathComponent("not-written.mp3")
|
|
232
|
+
let server = await makeServer(cachedFile: { _ in vanished })
|
|
233
|
+
defer { server.stop() }
|
|
234
|
+
let url = try XCTUnwrap(server.localURL(for: unreachableOrigin))
|
|
235
|
+
|
|
236
|
+
// The origin is unreachable, so falling through surfaces as the server
|
|
237
|
+
// closing the connection without a response — the origin leg was tried
|
|
238
|
+
// and failed. A 404 would mean it never tried.
|
|
239
|
+
do {
|
|
240
|
+
let (response, _) = try await get(url)
|
|
241
|
+
XCTFail(
|
|
242
|
+
"a missing cache file must read through to the origin; got HTTP \(response.statusCode) instead of a dropped connection")
|
|
243
|
+
} catch let error as URLError {
|
|
244
|
+
XCTAssertEqual(
|
|
245
|
+
error.code, .networkConnectionLost,
|
|
246
|
+
"expected the origin leg to close the connection without a reply")
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// A cache miss relays the origin's body, in order and to the last byte. A
|
|
251
|
+
/// second loopback server serving the file from disk stands in as the origin,
|
|
252
|
+
/// so the relay leg runs over a real socket in both directions. The body is
|
|
253
|
+
/// larger than the relay's read buffer, so the bytes cross more than one
|
|
254
|
+
/// delivery.
|
|
255
|
+
func testACacheMissRelaysTheOriginBodyIntact() async throws {
|
|
256
|
+
let bytes = (0 ..< 3_000_000).map { UInt8($0 % 251) }
|
|
257
|
+
let file = try writeFile(bytes)
|
|
258
|
+
let origin = await makeServer(cachedFile: { _ in file })
|
|
259
|
+
defer { origin.stop() }
|
|
260
|
+
let originURL = try XCTUnwrap(origin.localURL(for: unreachableOrigin))
|
|
261
|
+
let relay = await makeServer(cachedFile: { _ in nil })
|
|
262
|
+
defer { relay.stop() }
|
|
263
|
+
let url = try XCTUnwrap(relay.localURL(for: originURL))
|
|
264
|
+
|
|
265
|
+
let (response, data) = try await get(url)
|
|
266
|
+
|
|
267
|
+
XCTAssertEqual(response.statusCode, 200)
|
|
268
|
+
XCTAssertEqual(data.count, bytes.count)
|
|
269
|
+
XCTAssertEqual([UInt8](data), bytes, "the relayed bytes are not the origin's")
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// The playback case: AVFoundation reads at playback rate while the origin
|
|
273
|
+
/// delivers as fast as the network allows. The relay holds a bounded amount
|
|
274
|
+
/// and stops reading the origin until the client has drained it — and then
|
|
275
|
+
/// reads again, so a slow client still receives the whole body.
|
|
276
|
+
func testASlowClientStillReceivesTheWholeRelayedBody() async throws {
|
|
277
|
+
let bytes = (0 ..< 6_000_000).map { UInt8($0 % 251) }
|
|
278
|
+
let file = try writeFile(bytes)
|
|
279
|
+
let origin = await makeServer(cachedFile: { _ in file })
|
|
280
|
+
defer { origin.stop() }
|
|
281
|
+
let originURL = try XCTUnwrap(origin.localURL(for: unreachableOrigin))
|
|
282
|
+
let relay = await makeServer(cachedFile: { _ in nil })
|
|
283
|
+
defer { relay.stop() }
|
|
284
|
+
let url = try XCTUnwrap(relay.localURL(for: originURL))
|
|
285
|
+
|
|
286
|
+
let body = try await slowGet(url, chunk: 64 * 1024, pauseNanoseconds: 3_000_000)
|
|
287
|
+
|
|
288
|
+
XCTAssertEqual(body.count, bytes.count)
|
|
289
|
+
XCTAssertEqual([UInt8](body), bytes, "the relayed bytes are not the origin's")
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/// The range a client asks for is what the origin is asked for.
|
|
293
|
+
func testACacheMissRelaysARangeRequestToTheOrigin() async throws {
|
|
294
|
+
let bytes = (0 ..< 2048).map { UInt8($0 % 251) }
|
|
295
|
+
let file = try writeFile(bytes)
|
|
296
|
+
let origin = await makeServer(cachedFile: { _ in file })
|
|
297
|
+
defer { origin.stop() }
|
|
298
|
+
let originURL = try XCTUnwrap(origin.localURL(for: unreachableOrigin))
|
|
299
|
+
let relay = await makeServer(cachedFile: { _ in nil })
|
|
300
|
+
defer { relay.stop() }
|
|
301
|
+
let url = try XCTUnwrap(relay.localURL(for: originURL))
|
|
302
|
+
|
|
303
|
+
let (response, data) = try await get(url, range: "bytes=100-199")
|
|
304
|
+
|
|
305
|
+
XCTAssertEqual(response.statusCode, 206)
|
|
306
|
+
XCTAssertEqual(response.value(forHTTPHeaderField: "Content-Range"), "bytes 100-199/2048")
|
|
307
|
+
XCTAssertEqual([UInt8](data), Array(bytes[100 ... 199]))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// LRU is bumped when a file is actually read, once per token — `touch`
|
|
311
|
+
/// writes the index on the same queue the lookup takes, and a player issues
|
|
312
|
+
/// several range requests per track.
|
|
313
|
+
func testTheCacheIsTouchedOncePerTokenNotPerRequest() async throws {
|
|
314
|
+
let file = try writeFile((0 ..< 512).map { UInt8($0 % 251) })
|
|
315
|
+
let touches = TouchCounter()
|
|
316
|
+
let server = await makeServer(cachedFile: { _ in file }, onFirstDiskHit: { _ in touches.bump() })
|
|
317
|
+
defer { server.stop() }
|
|
318
|
+
let url = try XCTUnwrap(server.localURL(for: unreachableOrigin))
|
|
319
|
+
|
|
320
|
+
_ = try await get(url, range: "bytes=0-1")
|
|
321
|
+
_ = try await get(url, range: "bytes=0-99")
|
|
322
|
+
_ = try await get(url)
|
|
323
|
+
|
|
324
|
+
XCTAssertEqual(touches.value, 1, "three requests for one token bumped LRU more than once")
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
func testStoppingDropsEveryToken() async throws {
|
|
328
|
+
let server = await makeServer()
|
|
329
|
+
XCTAssertNotNil(server.localURL(for: unreachableOrigin), "precondition: a bound server routes")
|
|
330
|
+
server.stop()
|
|
331
|
+
|
|
332
|
+
XCTAssertNil(
|
|
333
|
+
server.localURL(for: unreachableOrigin),
|
|
334
|
+
"a stopped server must not hand out URLs pointing at a dead port")
|
|
335
|
+
XCTAssertEqual(server.boundPortValue(), 0)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/// Counts callbacks from connection queues.
|
|
340
|
+
private final class TouchCounter: @unchecked Sendable {
|
|
341
|
+
private let lock = NSLock()
|
|
342
|
+
private var count = 0
|
|
343
|
+
func bump() { lock.lock(); count += 1; lock.unlock() }
|
|
344
|
+
var value: Int { lock.lock(); defer { lock.unlock() }; return count }
|
|
345
|
+
}
|
|
@@ -24,6 +24,7 @@ final class RemoteCommandsTests: XCTestCase {
|
|
|
24
24
|
|
|
25
25
|
override func tearDown() {
|
|
26
26
|
commands.uninstall()
|
|
27
|
+
drainMain()
|
|
27
28
|
commands = nil
|
|
28
29
|
resetSharedCenter()
|
|
29
30
|
super.tearDown()
|
|
@@ -34,6 +35,7 @@ final class RemoteCommandsTests: XCTestCase {
|
|
|
34
35
|
func testInstallEnablesAllOwnedCommands() {
|
|
35
36
|
let center = MPRemoteCommandCenter.shared()
|
|
36
37
|
commands.install()
|
|
38
|
+
drainMain()
|
|
37
39
|
XCTAssertTrue(center.playCommand.isEnabled)
|
|
38
40
|
XCTAssertTrue(center.pauseCommand.isEnabled)
|
|
39
41
|
XCTAssertTrue(center.togglePlayPauseCommand.isEnabled)
|
|
@@ -46,7 +48,9 @@ final class RemoteCommandsTests: XCTestCase {
|
|
|
46
48
|
func testUninstallDisablesAllOwnedCommands() {
|
|
47
49
|
let center = MPRemoteCommandCenter.shared()
|
|
48
50
|
commands.install()
|
|
51
|
+
drainMain()
|
|
49
52
|
commands.uninstall()
|
|
53
|
+
drainMain()
|
|
50
54
|
XCTAssertFalse(center.playCommand.isEnabled)
|
|
51
55
|
XCTAssertFalse(center.pauseCommand.isEnabled)
|
|
52
56
|
XCTAssertFalse(center.togglePlayPauseCommand.isEnabled)
|
|
@@ -59,10 +63,13 @@ final class RemoteCommandsTests: XCTestCase {
|
|
|
59
63
|
func testInstallTwiceLeavesEnabledStatesIntact() {
|
|
60
64
|
let center = MPRemoteCommandCenter.shared()
|
|
61
65
|
commands.install()
|
|
66
|
+
drainMain()
|
|
62
67
|
commands.install()
|
|
68
|
+
drainMain()
|
|
63
69
|
XCTAssertTrue(center.playCommand.isEnabled)
|
|
64
70
|
XCTAssertTrue(center.nextTrackCommand.isEnabled)
|
|
65
71
|
commands.uninstall()
|
|
72
|
+
drainMain()
|
|
66
73
|
XCTAssertFalse(center.playCommand.isEnabled)
|
|
67
74
|
}
|
|
68
75
|
|
|
@@ -70,7 +77,9 @@ final class RemoteCommandsTests: XCTestCase {
|
|
|
70
77
|
let center = MPRemoteCommandCenter.shared()
|
|
71
78
|
let priorPlay = center.playCommand.isEnabled
|
|
72
79
|
commands.uninstall()
|
|
80
|
+
drainMain()
|
|
73
81
|
commands.uninstall()
|
|
82
|
+
drainMain()
|
|
74
83
|
XCTAssertEqual(center.playCommand.isEnabled, priorPlay)
|
|
75
84
|
}
|
|
76
85
|
|
|
@@ -79,7 +88,9 @@ final class RemoteCommandsTests: XCTestCase {
|
|
|
79
88
|
center.seekForwardCommand.isEnabled = true
|
|
80
89
|
center.skipBackwardCommand.isEnabled = true
|
|
81
90
|
commands.install()
|
|
91
|
+
drainMain()
|
|
82
92
|
commands.uninstall()
|
|
93
|
+
drainMain()
|
|
83
94
|
XCTAssertTrue(center.seekForwardCommand.isEnabled,
|
|
84
95
|
"seekForward is not lib-owned; uninstall must leave it alone")
|
|
85
96
|
XCTAssertTrue(center.skipBackwardCommand.isEnabled,
|
|
@@ -93,6 +104,7 @@ final class RemoteCommandsTests: XCTestCase {
|
|
|
93
104
|
let priorNext = center.nextTrackCommand.isEnabled
|
|
94
105
|
let priorPrev = center.previousTrackCommand.isEnabled
|
|
95
106
|
commands.setSkipCapability(canSkipNext: !priorNext, canSkipPrevious: !priorPrev)
|
|
107
|
+
drainMain()
|
|
96
108
|
XCTAssertEqual(center.nextTrackCommand.isEnabled, priorNext)
|
|
97
109
|
XCTAssertEqual(center.previousTrackCommand.isEnabled, priorPrev)
|
|
98
110
|
}
|
|
@@ -100,10 +112,13 @@ final class RemoteCommandsTests: XCTestCase {
|
|
|
100
112
|
func testSetSkipCapabilityFlipsIsEnabledWhenInstalled() {
|
|
101
113
|
let center = MPRemoteCommandCenter.shared()
|
|
102
114
|
commands.install()
|
|
115
|
+
drainMain()
|
|
103
116
|
commands.setSkipCapability(canSkipNext: false, canSkipPrevious: false)
|
|
117
|
+
drainMain()
|
|
104
118
|
XCTAssertFalse(center.nextTrackCommand.isEnabled)
|
|
105
119
|
XCTAssertFalse(center.previousTrackCommand.isEnabled)
|
|
106
120
|
commands.setSkipCapability(canSkipNext: true, canSkipPrevious: true)
|
|
121
|
+
drainMain()
|
|
107
122
|
XCTAssertTrue(center.nextTrackCommand.isEnabled)
|
|
108
123
|
XCTAssertTrue(center.previousTrackCommand.isEnabled)
|
|
109
124
|
}
|
|
@@ -111,10 +126,13 @@ final class RemoteCommandsTests: XCTestCase {
|
|
|
111
126
|
func testSetSkipCapabilityNoOpAfterUninstall() {
|
|
112
127
|
let center = MPRemoteCommandCenter.shared()
|
|
113
128
|
commands.install()
|
|
129
|
+
drainMain()
|
|
114
130
|
commands.uninstall()
|
|
131
|
+
drainMain()
|
|
115
132
|
let priorNext = center.nextTrackCommand.isEnabled
|
|
116
133
|
let priorPrev = center.previousTrackCommand.isEnabled
|
|
117
134
|
commands.setSkipCapability(canSkipNext: true, canSkipPrevious: true)
|
|
135
|
+
drainMain()
|
|
118
136
|
XCTAssertEqual(center.nextTrackCommand.isEnabled, priorNext)
|
|
119
137
|
XCTAssertEqual(center.previousTrackCommand.isEnabled, priorPrev)
|
|
120
138
|
}
|
|
@@ -141,4 +159,14 @@ final class RemoteCommandsTests: XCTestCase {
|
|
|
141
159
|
cmd.isEnabled = false
|
|
142
160
|
}
|
|
143
161
|
}
|
|
162
|
+
|
|
163
|
+
/// Lets the command-centre writes handed to main land. `RemoteCommands`
|
|
164
|
+
/// enqueues them rather than applying them inline, so an assertion taken in
|
|
165
|
+
/// the same turn still sees the previous state.
|
|
166
|
+
private func drainMain() {
|
|
167
|
+
let drained = expectation(description: "main queue drained")
|
|
168
|
+
DispatchQueue.main.async { drained.fulfill() }
|
|
169
|
+
wait(for: [drained], timeout: 2.0)
|
|
170
|
+
}
|
|
171
|
+
|
|
144
172
|
}
|