react-native-queue-player 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/build.gradle +7 -0
- package/android/consumer-rules.pro +72 -3
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +26 -22
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +20 -72
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +11 -0
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +660 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +202 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +303 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +394 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +968 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +930 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +940 -0
- package/ios/TrackPlayer+Recovery.swift +196 -0
- package/ios/TrackPlayer+Skip.swift +455 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +267 -0
- package/ios/TrackPlayer+Window.swift +195 -0
- package/ios/TrackPlayer.swift +305 -4610
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
@_spi(QueuePlayerTests) import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// A rebuild keeps the leading run the engine already holds and replaces only
|
|
6
|
+
/// what differs, so the item being rendered survives it.
|
|
7
|
+
final class QueueRebuildPrefixTests: XCTestCase {
|
|
8
|
+
|
|
9
|
+
// MARK: - The arithmetic
|
|
10
|
+
|
|
11
|
+
func testAnIdenticalSliceIsWhollyAPrefix() {
|
|
12
|
+
XCTAssertEqual(
|
|
13
|
+
QueueMutationArithmetic.commonPrefixLength(
|
|
14
|
+
current: ["a", "b", "c"], desired: ["a", "b", "c"]),
|
|
15
|
+
3)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func testThePrefixStopsAtTheFirstDifference() {
|
|
19
|
+
XCTAssertEqual(
|
|
20
|
+
QueueMutationArithmetic.commonPrefixLength(
|
|
21
|
+
current: ["a", "b", "x"], desired: ["a", "b", "c"]),
|
|
22
|
+
2)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func testAnAppendKeepsEverythingAlreadyHeld() {
|
|
26
|
+
XCTAssertEqual(
|
|
27
|
+
QueueMutationArithmetic.commonPrefixLength(
|
|
28
|
+
current: ["a", "b"], desired: ["a", "b", "c", "d"]),
|
|
29
|
+
2)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func testATruncationKeepsWhatSurvives() {
|
|
33
|
+
XCTAssertEqual(
|
|
34
|
+
QueueMutationArithmetic.commonPrefixLength(
|
|
35
|
+
current: ["a", "b", "c", "d"], desired: ["a", "b"]),
|
|
36
|
+
2)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func testADifferentHeadKeepsNothing() {
|
|
40
|
+
XCTAssertEqual(
|
|
41
|
+
QueueMutationArithmetic.commonPrefixLength(
|
|
42
|
+
current: ["z", "b", "c"], desired: ["a", "b", "c"]),
|
|
43
|
+
0)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// An item the library did not stamp has no identity to match on, so the
|
|
47
|
+
/// prefix cannot claim it.
|
|
48
|
+
func testAnUnstampedItemEndsThePrefix() {
|
|
49
|
+
XCTAssertEqual(
|
|
50
|
+
QueueMutationArithmetic.commonPrefixLength(
|
|
51
|
+
current: ["a", nil, "c"], desired: ["a", "b", "c"]),
|
|
52
|
+
1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func testEmptySidesHaveNoPrefix() {
|
|
56
|
+
XCTAssertEqual(
|
|
57
|
+
QueueMutationArithmetic.commonPrefixLength(current: [], desired: ["a"]), 0)
|
|
58
|
+
XCTAssertEqual(
|
|
59
|
+
QueueMutationArithmetic.commonPrefixLength(current: ["a"], desired: []), 0)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// MARK: - The rebuild
|
|
63
|
+
|
|
64
|
+
/// The engine already holds exactly this slice, so nothing is reinstalled
|
|
65
|
+
/// and its buffered items survive.
|
|
66
|
+
func testAnUnchangedSliceIsLeftAlone() async throws {
|
|
67
|
+
let player = try await seeded(count: 3)
|
|
68
|
+
let before = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
69
|
+
XCTAssertEqual(before.count, 3, "precondition: the engine holds the slice")
|
|
70
|
+
|
|
71
|
+
await onQueue(player) { p in p.performingMutation { p.fullRebuildPlayerQueue() } }
|
|
72
|
+
|
|
73
|
+
let after = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
74
|
+
XCTAssertEqual(after.count, 3)
|
|
75
|
+
for (old, new) in zip(before, after) {
|
|
76
|
+
XCTAssertTrue(old === new, "an unchanged slice must not be reinstalled")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// The model gained a track the engine does not hold: the run it does hold
|
|
81
|
+
/// stays, and only the new tail is installed.
|
|
82
|
+
func testAnAppendKeepsTheInstalledRunAndAddsTheTail() async throws {
|
|
83
|
+
let player = try await seeded(count: 3)
|
|
84
|
+
let before = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
85
|
+
XCTAssertEqual(before.count, 3)
|
|
86
|
+
|
|
87
|
+
// Extend the model behind the engine's back so the rebuild is what
|
|
88
|
+
// reconciles them — the public mutation APIs install the tail themselves.
|
|
89
|
+
let extra = try PlayerFixtures.localTracks(count: 2, for: self)
|
|
90
|
+
await onQueue(player) { p in
|
|
91
|
+
_ = p.queueState.insert(extra, at: p.tracks.count)
|
|
92
|
+
p.performingMutation { p.fullRebuildPlayerQueue() }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let after = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
96
|
+
XCTAssertEqual(after.count, 5, "the appended positions must be installed")
|
|
97
|
+
for (old, new) in zip(before, after.prefix(3)) {
|
|
98
|
+
XCTAssertTrue(old === new, "the run already held must be kept, not rebuilt")
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// A divergence part-way along replaces from that point, not from the head.
|
|
103
|
+
func testADivergenceRebuildsFromThePointItDiverges() async throws {
|
|
104
|
+
let player = try await seeded(count: 4)
|
|
105
|
+
let before = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
106
|
+
XCTAssertEqual(before.count, 4)
|
|
107
|
+
|
|
108
|
+
// Replace positions 2 and 3 in the model only.
|
|
109
|
+
let replacement = try PlayerFixtures.localTracks(count: 2, for: self)
|
|
110
|
+
await onQueue(player) { p in
|
|
111
|
+
_ = p.queueState.remove(at: 3)
|
|
112
|
+
_ = p.queueState.remove(at: 2)
|
|
113
|
+
_ = p.queueState.insert(replacement, at: 2)
|
|
114
|
+
p.performingMutation { p.fullRebuildPlayerQueue() }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let after = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
118
|
+
XCTAssertEqual(after.count, 4)
|
|
119
|
+
for (old, new) in zip(before.prefix(2), after.prefix(2)) {
|
|
120
|
+
XCTAssertTrue(old === new, "positions 0 and 1 still match, so they stay")
|
|
121
|
+
}
|
|
122
|
+
XCTAssertFalse(
|
|
123
|
+
before[2] === after[2], "the divergent tail must be replaced")
|
|
124
|
+
let ids = try await read(player) { $0.queueItemIds }
|
|
125
|
+
XCTAssertEqual(
|
|
126
|
+
after.map { $0.queueItemId }, ids,
|
|
127
|
+
"the engine must end up holding exactly the model's positions")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// A forced reinstall replaces the items even when identity says the slice
|
|
131
|
+
/// already matches — what the retry paths need, since a failed item keeps
|
|
132
|
+
/// its id.
|
|
133
|
+
func testAForcedReinstallReplacesAnIdenticalSlice() async throws {
|
|
134
|
+
let player = try await seeded(count: 3)
|
|
135
|
+
let before = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
136
|
+
XCTAssertEqual(before.count, 3)
|
|
137
|
+
|
|
138
|
+
await onQueue(player) { p in
|
|
139
|
+
p.performingMutation { p.fullRebuildPlayerQueue(forcingReinstall: true) }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let after = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
143
|
+
XCTAssertEqual(after.count, 3)
|
|
144
|
+
XCTAssertFalse(
|
|
145
|
+
before[0] === after[0],
|
|
146
|
+
"a forced reinstall must construct fresh items over the installed ones")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// MARK: - The windowed run
|
|
150
|
+
|
|
151
|
+
/// The gapless player consumes finished items, so it holds the floor ahead
|
|
152
|
+
/// of the playhead and nothing behind it — not the whole queue.
|
|
153
|
+
func testTheGaplessRunIsBoundedAndHoldsNothingBehindThePlayhead() async throws {
|
|
154
|
+
let player = try await seeded(count: 40)
|
|
155
|
+
|
|
156
|
+
let installed = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
157
|
+
let ids = try await read(player) { $0.queueItemIds }
|
|
158
|
+
|
|
159
|
+
XCTAssertEqual(
|
|
160
|
+
installed.count, QueueWindowArithmetic.floor,
|
|
161
|
+
"a long queue installs the floor, not all of it")
|
|
162
|
+
XCTAssertEqual(
|
|
163
|
+
installed.first?.queueItemId, ids.first,
|
|
164
|
+
"the run starts at the playhead")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// Advancing the playhead moves the run with it: what is now behind the
|
|
168
|
+
/// trail comes out, and the floor ahead is restored.
|
|
169
|
+
func testTheRunFollowsThePlayheadAndDropsWhatFallsBehindIt() async throws {
|
|
170
|
+
let player = try await seeded(count: 40)
|
|
171
|
+
let ids = try await read(player) { $0.queueItemIds }
|
|
172
|
+
|
|
173
|
+
await onQueue(player) { p in
|
|
174
|
+
p.currentTrackIndex = 12
|
|
175
|
+
p.performingMutation { p.fullRebuildPlayerQueue() }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let installed = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
179
|
+
let held = installed.compactMap { $0.queueItemId }
|
|
180
|
+
XCTAssertEqual(
|
|
181
|
+
held.first, ids[12], "gapless keeps no trail, so the run starts at the playhead")
|
|
182
|
+
XCTAssertEqual(held.count, QueueWindowArithmetic.floor)
|
|
183
|
+
XCTAssertFalse(
|
|
184
|
+
held.contains(ids[0]), "the position left behind is no longer installed")
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// The whole point of bounding the run: a long queue does not materialise
|
|
188
|
+
/// end to end.
|
|
189
|
+
func testALongQueueDoesNotMaterialiseEndToEnd() async throws {
|
|
190
|
+
let player = try await seeded(count: 200)
|
|
191
|
+
|
|
192
|
+
let installed = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
193
|
+
|
|
194
|
+
XCTAssertLessThanOrEqual(installed.count, QueueWindowArithmetic.floor + 2)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// MARK: - The crossfade run
|
|
198
|
+
|
|
199
|
+
/// The crossfade engine retains what it is given, so its run carries a trail
|
|
200
|
+
/// behind the playhead — the gapless player would drop it.
|
|
201
|
+
func testTheCrossfadeRunCarriesATrailBehindThePlayhead() async throws {
|
|
202
|
+
let player = try await seeded(count: 40, crossfade: true)
|
|
203
|
+
let ids = try await read(player) { $0.queueItemIds }
|
|
204
|
+
|
|
205
|
+
await onQueue(player) { p in
|
|
206
|
+
p.currentTrackIndex = 12
|
|
207
|
+
p.performingMutation { p.fullRebuildPlayerQueue() }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let held = try await read(player) {
|
|
211
|
+
($0.engine?.allMediaItems ?? []).compactMap { $0.queueItemId }
|
|
212
|
+
}
|
|
213
|
+
XCTAssertEqual(held.first, ids[10], "two played positions stay installed")
|
|
214
|
+
XCTAssertTrue(held.contains(ids[12]), "the playhead is installed")
|
|
215
|
+
XCTAssertLessThanOrEqual(
|
|
216
|
+
held.count, QueueWindowArithmetic.floor + 2, "the run stays bounded")
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Advancing drops what has fallen off the back of the trail without
|
|
220
|
+
/// disturbing the item being heard.
|
|
221
|
+
func testAdvancingTrimsTheTrailAndKeepsTheRenderingItem() async throws {
|
|
222
|
+
let player = try await seeded(count: 40, crossfade: true)
|
|
223
|
+
await onQueue(player) { p in
|
|
224
|
+
p.currentTrackIndex = 12
|
|
225
|
+
p.performingMutation { p.fullRebuildPlayerQueue() }
|
|
226
|
+
}
|
|
227
|
+
let ids = try await read(player) { $0.queueItemIds }
|
|
228
|
+
let before = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
229
|
+
let rendering = try XCTUnwrap(before.first { $0.queueItemId == ids[12] })
|
|
230
|
+
|
|
231
|
+
await onQueue(player) { p in
|
|
232
|
+
p.currentTrackIndex = 13
|
|
233
|
+
p.performingMutation { p.fullRebuildPlayerQueue() }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let after = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
237
|
+
let held = after.compactMap { $0.queueItemId }
|
|
238
|
+
XCTAssertEqual(held.first, ids[11], "the trail moved up with the playhead")
|
|
239
|
+
XCTAssertFalse(held.contains(ids[10]), "what fell off the trail is gone")
|
|
240
|
+
XCTAssertTrue(
|
|
241
|
+
after.contains { $0 === rendering },
|
|
242
|
+
"the item being heard survives the trim")
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// Removing the item being heard replaces the leading leg's current item,
|
|
246
|
+
/// which stops playback and restores no rate — so a divergence that would
|
|
247
|
+
/// take it installs the run whole instead.
|
|
248
|
+
func testADivergenceCoveringTheRenderingItemReinstallsRatherThanRemovingIt()
|
|
249
|
+
async throws
|
|
250
|
+
{
|
|
251
|
+
let player = try await seeded(count: 20, crossfade: true)
|
|
252
|
+
await onQueue(player) { p in
|
|
253
|
+
p.currentTrackIndex = 6
|
|
254
|
+
p.performingMutation { p.fullRebuildPlayerQueue() }
|
|
255
|
+
}
|
|
256
|
+
let before = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
257
|
+
let renderingId = try await read(player) { $0.engine?.leadingQueueItemId }
|
|
258
|
+
XCTAssertNotNil(renderingId, "the engine is rendering something to protect")
|
|
259
|
+
|
|
260
|
+
// Replace the model from a position at or before the playhead, so the run
|
|
261
|
+
// diverges across the item being rendered.
|
|
262
|
+
let replacement = try PlayerFixtures.localTracks(count: 6, for: self)
|
|
263
|
+
await onQueue(player) { p in
|
|
264
|
+
for index in stride(from: 10, through: 5, by: -1) {
|
|
265
|
+
_ = p.queueState.remove(at: index)
|
|
266
|
+
}
|
|
267
|
+
_ = p.queueState.insert(replacement, at: 5)
|
|
268
|
+
p.performingMutation { p.fullRebuildPlayerQueue() }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let after = try await read(player) { $0.engine?.allMediaItems ?? [] }
|
|
272
|
+
let renderingAfter = try await read(player) { $0.engine?.leadingQueueItemId }
|
|
273
|
+
XCTAssertFalse(after.isEmpty, "the run is reinstalled, not emptied")
|
|
274
|
+
XCTAssertTrue(
|
|
275
|
+
after.contains { $0.queueItemId == renderingAfter },
|
|
276
|
+
"the engine renders an item its own run holds")
|
|
277
|
+
XCTAssertFalse(
|
|
278
|
+
before.contains { $0 === after.first },
|
|
279
|
+
"a reinstall builds fresh items rather than surgically removing the old")
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// MARK: - Helpers
|
|
283
|
+
|
|
284
|
+
private var players: [TrackPlayer] = []
|
|
285
|
+
|
|
286
|
+
override func tearDown() async throws {
|
|
287
|
+
for player in players {
|
|
288
|
+
_ = try? await player.destroy().await()
|
|
289
|
+
}
|
|
290
|
+
players = []
|
|
291
|
+
try await super.tearDown()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private func seeded(count: Int, crossfade: Bool = false) async throws -> TrackPlayer {
|
|
295
|
+
let player = TrackPlayer()
|
|
296
|
+
players.append(player)
|
|
297
|
+
_ = try await player.configure(config: PlayerFixtures.config()).await()
|
|
298
|
+
if crossfade {
|
|
299
|
+
_ = try await player.setPlaybackMode(
|
|
300
|
+
mode: PlaybackMode(kind: .crossfade, crossfadeDurationMs: 3000)).await()
|
|
301
|
+
}
|
|
302
|
+
_ = try await player.setQueue(
|
|
303
|
+
tracks: try PlayerFixtures.localTracks(count: count, for: self),
|
|
304
|
+
startAtIndex: 0).await()
|
|
305
|
+
return player
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private func onQueue(_ player: TrackPlayer, _ body: @escaping (TrackPlayer) -> Void) async {
|
|
309
|
+
await player.onPlayerQueue { body(player) }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private func read<T>(
|
|
313
|
+
_ player: TrackPlayer, _ body: @escaping (TrackPlayer) -> T
|
|
314
|
+
) async throws -> T {
|
|
315
|
+
await player.onPlayerQueueValue { body(player) }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -44,7 +44,9 @@ final class QueueStateTests: XCTestCase {
|
|
|
44
44
|
private func track(_ url: String) -> TrackItem {
|
|
45
45
|
TrackItem(
|
|
46
46
|
id: nil, url: url, title: nil, artist: nil, album: nil,
|
|
47
|
-
duration: nil, artworkUrl: nil, extras: nil
|
|
47
|
+
duration: nil, artworkUrl: nil, extras: nil,
|
|
48
|
+
replayGainTrackGain: nil, replayGainTrackPeak: nil,
|
|
49
|
+
replayGainAlbumGain: nil, replayGainAlbumPeak: nil
|
|
48
50
|
)
|
|
49
51
|
}
|
|
50
52
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
@_spi(QueuePlayerTests) import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// Pure-Swift tests for the bounded gapless enqueue window.
|
|
6
|
+
final class QueueWindowArithmeticTests: XCTestCase {
|
|
7
|
+
|
|
8
|
+
private typealias W = QueueWindowArithmetic
|
|
9
|
+
|
|
10
|
+
// MARK: - isAtLogicalTail
|
|
11
|
+
|
|
12
|
+
func testIsAtLogicalTailMatchesTheLastPosition() {
|
|
13
|
+
XCTAssertTrue(W.isAtLogicalTail(anchorPosition: 9, trackCount: 10))
|
|
14
|
+
XCTAssertFalse(W.isAtLogicalTail(anchorPosition: 8, trackCount: 10))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func testIsAtLogicalTailIsFalseOnAnEmptyQueue() {
|
|
18
|
+
XCTAssertFalse(W.isAtLogicalTail(anchorPosition: -1, trackCount: 0))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func testIsAtLogicalTailIsFalseForAnAnchorPastTheEnd() {
|
|
22
|
+
XCTAssertFalse(W.isAtLogicalTail(anchorPosition: 12, trackCount: 10))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// MARK: - verdict
|
|
26
|
+
|
|
27
|
+
func testUndrainedEngineNeverEnds() {
|
|
28
|
+
for mode in [QueueSkipRepeatMode.off, .track, .queue] {
|
|
29
|
+
for tail in [true, false] {
|
|
30
|
+
XCTAssertEqual(
|
|
31
|
+
W.verdict(drained: false, atLogicalTail: tail, repeatMode: mode), .notEnded,
|
|
32
|
+
"mode=\(mode) tail=\(tail)")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func testRepeatOneAlwaysRefillsTheCurrentPosition() {
|
|
38
|
+
for tail in [true, false] {
|
|
39
|
+
XCTAssertEqual(
|
|
40
|
+
W.verdict(drained: true, atLogicalTail: tail, repeatMode: .track), .refillCurrent,
|
|
41
|
+
"tail=\(tail)")
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func testDrainedMidQueueRefillsTheWindow() {
|
|
46
|
+
XCTAssertEqual(W.verdict(drained: true, atLogicalTail: false, repeatMode: .off), .windowDrained)
|
|
47
|
+
XCTAssertEqual(W.verdict(drained: true, atLogicalTail: false, repeatMode: .queue),
|
|
48
|
+
.windowDrained)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func testDrainedAtTheTailEndsUnderRepeatOffAndWrapsUnderRepeatQueue() {
|
|
52
|
+
XCTAssertEqual(W.verdict(drained: true, atLogicalTail: true, repeatMode: .off), .queueEnd)
|
|
53
|
+
XCTAssertEqual(W.verdict(drained: true, atLogicalTail: true, repeatMode: .queue), .wrapToStart)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@_spi(QueuePlayerTests) import QueuePlayer
|
|
3
|
+
|
|
4
|
+
/// The set of positions the engine holds around the playhead: a trail behind
|
|
5
|
+
/// it for cheap backward skips, the floor ahead of it for uninterrupted
|
|
6
|
+
/// playback, each position held once.
|
|
7
|
+
final class QueueWindowSliceTests: XCTestCase {
|
|
8
|
+
|
|
9
|
+
private let floor = QueueWindowArithmetic.floor
|
|
10
|
+
|
|
11
|
+
// MARK: - Forward run
|
|
12
|
+
|
|
13
|
+
func testTheSliceStartsAtTheCurrentPositionWhenNothingPrecedesIt() {
|
|
14
|
+
let slice = QueueWindowArithmetic.slice(
|
|
15
|
+
currentIndex: 0, trackCount: 30, trail: 2, wraps: false)
|
|
16
|
+
|
|
17
|
+
XCTAssertEqual(slice, Array(0 ..< floor))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func testTheSliceReachesTheFloorAheadOfThePlayhead() {
|
|
21
|
+
let slice = QueueWindowArithmetic.slice(
|
|
22
|
+
currentIndex: 5, trackCount: 30, trail: 2, wraps: false)
|
|
23
|
+
|
|
24
|
+
XCTAssertEqual(slice, Array(3 ..< 5) + Array(5 ..< 5 + floor))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func testAQueueShorterThanTheFloorIsHeldWhole() {
|
|
28
|
+
let slice = QueueWindowArithmetic.slice(
|
|
29
|
+
currentIndex: 1, trackCount: 4, trail: 2, wraps: false)
|
|
30
|
+
|
|
31
|
+
XCTAssertEqual(slice, [0, 1, 2, 3])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func testTheForwardRunStopsAtTheEndWhenTheQueueDoesNotWrap() {
|
|
35
|
+
let slice = QueueWindowArithmetic.slice(
|
|
36
|
+
currentIndex: 28, trackCount: 30, trail: 2, wraps: false)
|
|
37
|
+
|
|
38
|
+
XCTAssertEqual(slice, [26, 27, 28, 29])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// MARK: - Wrapping
|
|
42
|
+
|
|
43
|
+
/// The incoming item at a repeat-queue boundary has to be materialised
|
|
44
|
+
/// before the outgoing one ends, or the boundary is a hard cut.
|
|
45
|
+
func testAWrappingQueueCarriesTheHeadIntoTheSliceAtTheTail() {
|
|
46
|
+
let slice = QueueWindowArithmetic.slice(
|
|
47
|
+
currentIndex: 29, trackCount: 30, trail: 2, wraps: true)
|
|
48
|
+
|
|
49
|
+
XCTAssertEqual(slice.prefix(3), [27, 28, 29])
|
|
50
|
+
XCTAssertEqual(Array(slice.dropFirst(3)), Array(0 ..< floor - 1))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func testANonWrappingQueueDoesNotCarryTheHead() {
|
|
54
|
+
let slice = QueueWindowArithmetic.slice(
|
|
55
|
+
currentIndex: 29, trackCount: 30, trail: 2, wraps: false)
|
|
56
|
+
|
|
57
|
+
XCTAssertEqual(slice, [27, 28, 29])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Where the wrap brings the forward run back around into the trail, the
|
|
61
|
+
/// slice already covers every position, so it holds each once and the trail
|
|
62
|
+
/// gives way to keep the run unbroken.
|
|
63
|
+
func testAWrappingSliceHoldsTheWholeQueueOnceWithTheTrailGivingWay() {
|
|
64
|
+
let slice = QueueWindowArithmetic.slice(
|
|
65
|
+
currentIndex: 10, trackCount: 11, trail: 2, wraps: true)
|
|
66
|
+
|
|
67
|
+
XCTAssertEqual(Set(slice).count, slice.count, "no position is held twice")
|
|
68
|
+
XCTAssertEqual(slice.count, 11, "the whole queue, once each")
|
|
69
|
+
XCTAssertEqual(slice.first, 9, "the trail gave way to the wrapped forward run")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func testASmallWrappingQueueHoldsOneUnbrokenRunWithEachPositionOnce() {
|
|
73
|
+
for count in 1 ... 14 {
|
|
74
|
+
for cur in 0 ..< count {
|
|
75
|
+
let slice = QueueWindowArithmetic.slice(
|
|
76
|
+
currentIndex: cur, trackCount: count, trail: 2, wraps: true)
|
|
77
|
+
XCTAssertEqual(
|
|
78
|
+
Set(slice).count, slice.count,
|
|
79
|
+
"count=\(count) cur=\(cur) held a position twice")
|
|
80
|
+
XCTAssertTrue(
|
|
81
|
+
slice.allSatisfy { $0 >= 0 && $0 < count },
|
|
82
|
+
"count=\(count) cur=\(cur) held a position outside the queue")
|
|
83
|
+
XCTAssertTrue(slice.contains(cur), "count=\(count) cur=\(cur) dropped the playhead")
|
|
84
|
+
for (position, next) in zip(slice, slice.dropFirst()) {
|
|
85
|
+
XCTAssertEqual(
|
|
86
|
+
(position + 1) % count, next,
|
|
87
|
+
"count=\(count) cur=\(cur) broke the run between \(position) and \(next)")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// The trail comes off the end of the queue when the head is playing and the
|
|
94
|
+
/// queue wraps — the only place the backward walk crosses zero.
|
|
95
|
+
func testTheTrailComesFromTheTailWhenTheHeadIsPlayingAndTheQueueWraps() {
|
|
96
|
+
XCTAssertEqual(
|
|
97
|
+
QueueWindowArithmetic.slice(
|
|
98
|
+
currentIndex: 0, trackCount: 30, trail: 2, wraps: true),
|
|
99
|
+
[28, 29] + Array(0 ..< floor))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// A queue of one cannot both hold a fresh incoming item and hold each
|
|
103
|
+
/// position once, so it holds the position and the boundary hard-cuts.
|
|
104
|
+
func testAQueueOfOneHoldsItsOnlyPosition() {
|
|
105
|
+
XCTAssertEqual(
|
|
106
|
+
QueueWindowArithmetic.slice(
|
|
107
|
+
currentIndex: 0, trackCount: 1, trail: 2, wraps: true),
|
|
108
|
+
[0])
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// MARK: - Cap
|
|
112
|
+
|
|
113
|
+
func testTheSliceNeverExceedsTheTrailPlusFloor() {
|
|
114
|
+
for count in [12, 20, 50] {
|
|
115
|
+
for cur in [0, 5, count - 1] {
|
|
116
|
+
for wraps in [true, false] {
|
|
117
|
+
let slice = QueueWindowArithmetic.slice(
|
|
118
|
+
currentIndex: cur, trackCount: count, trail: 2, wraps: wraps)
|
|
119
|
+
XCTAssertLessThanOrEqual(
|
|
120
|
+
slice.count, min(2 + floor, count),
|
|
121
|
+
"count=\(count) cur=\(cur) wraps=\(wraps) exceeded the cap")
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func testATrailDeeperThanTheFloorHoldsTrailPlusFloorPositions() {
|
|
128
|
+
let slice = QueueWindowArithmetic.slice(
|
|
129
|
+
currentIndex: 50, trackCount: 100, trail: 20, wraps: false)
|
|
130
|
+
|
|
131
|
+
XCTAssertEqual(slice, Array(30 ..< 60))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func testANegativeTrailHoldsNothingBehindThePlayhead() {
|
|
135
|
+
XCTAssertEqual(
|
|
136
|
+
QueueWindowArithmetic.slice(
|
|
137
|
+
currentIndex: 5, trackCount: 30, trail: -1, wraps: false),
|
|
138
|
+
Array(5 ..< 5 + floor))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func testATrailOfZeroHoldsNothingBehindThePlayhead() {
|
|
142
|
+
let slice = QueueWindowArithmetic.slice(
|
|
143
|
+
currentIndex: 5, trackCount: 30, trail: 0, wraps: false)
|
|
144
|
+
|
|
145
|
+
XCTAssertEqual(slice.first, 5)
|
|
146
|
+
XCTAssertEqual(slice.count, floor)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// MARK: - Nothing to play
|
|
150
|
+
|
|
151
|
+
func testAnEmptyOrOutOfRangeQueueHoldsNothing() {
|
|
152
|
+
XCTAssertTrue(
|
|
153
|
+
QueueWindowArithmetic.slice(
|
|
154
|
+
currentIndex: 0, trackCount: 0, trail: 2, wraps: false).isEmpty)
|
|
155
|
+
XCTAssertTrue(
|
|
156
|
+
QueueWindowArithmetic.slice(
|
|
157
|
+
currentIndex: 5, trackCount: 3, trail: 2, wraps: false).isEmpty)
|
|
158
|
+
XCTAssertTrue(
|
|
159
|
+
QueueWindowArithmetic.slice(
|
|
160
|
+
currentIndex: -1, trackCount: 3, trail: 2, wraps: false).isEmpty)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
|
|
4
|
+
/// The read-through server is bound before the first item can be built and
|
|
5
|
+
/// stays up, on the same port, across the cache being disabled and enabled:
|
|
6
|
+
/// seated items address it by port and are never re-pointed. What changes
|
|
7
|
+
/// with the flag is which server new items are routed through.
|
|
8
|
+
final class ReadThroughRoutingLifecycleTests: XCTestCase {
|
|
9
|
+
|
|
10
|
+
private var player: TrackPlayer!
|
|
11
|
+
|
|
12
|
+
override func setUp() async throws {
|
|
13
|
+
try await super.setUp()
|
|
14
|
+
player = TrackPlayer()
|
|
15
|
+
_ = try await player.configure(config: PlayerFixtures.config()).await()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override func tearDown() async throws {
|
|
19
|
+
if let player {
|
|
20
|
+
_ = try await player.destroy().await()
|
|
21
|
+
}
|
|
22
|
+
player = nil
|
|
23
|
+
try await super.tearDown()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func testTheServerIsBoundOnceConfigureResolves() async throws {
|
|
27
|
+
let port = try await read { $0.readThroughServer?.boundPortValue() ?? 0 }
|
|
28
|
+
XCTAssertNotEqual(port, 0, "an item built right after configure must be able to route")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func testDisablingTheCacheKeepsTheServerAndStopsRoutingNewItems() async throws {
|
|
32
|
+
let before = try await read {
|
|
33
|
+
(server: $0.readThroughServer, port: $0.readThroughServer?.boundPortValue() ?? 0)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_ = try await player.setLookaheadCache(
|
|
37
|
+
config: LookaheadCacheConfig(enabled: false, lookaheadCount: 3, allowsCellularAccess: nil)
|
|
38
|
+
).await()
|
|
39
|
+
|
|
40
|
+
let disabled = try await read {
|
|
41
|
+
(server: $0.readThroughServer, port: $0.readThroughServer?.boundPortValue() ?? 0,
|
|
42
|
+
routing: $0.routingServer)
|
|
43
|
+
}
|
|
44
|
+
XCTAssertTrue(disabled.server === before.server, "the seated items' server must survive a disable")
|
|
45
|
+
XCTAssertEqual(disabled.port, before.port)
|
|
46
|
+
XCTAssertNil(disabled.routing, "an item built while disabled binds its origin")
|
|
47
|
+
|
|
48
|
+
_ = try await player.setLookaheadCache(
|
|
49
|
+
config: LookaheadCacheConfig(enabled: true, lookaheadCount: 3, allowsCellularAccess: nil)
|
|
50
|
+
).await()
|
|
51
|
+
|
|
52
|
+
let enabled = try await read {
|
|
53
|
+
(server: $0.readThroughServer, routing: $0.routingServer)
|
|
54
|
+
}
|
|
55
|
+
XCTAssertTrue(enabled.server === before.server, "re-enabling must not change the port items hold")
|
|
56
|
+
XCTAssertTrue(enabled.routing === before.server)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private func read<T>(_ body: @escaping (TrackPlayer) -> T) async throws -> T {
|
|
60
|
+
let player = try XCTUnwrap(self.player)
|
|
61
|
+
return await player.onPlayerQueueValue { body(player) }
|
|
62
|
+
}
|
|
63
|
+
}
|