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,303 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// The automatic retry rebuilds the failed item and resumes where playback had
|
|
6
|
+
/// reached, on whichever engine is active.
|
|
7
|
+
final class RetryRecoveryTests: XCTestCase {
|
|
8
|
+
|
|
9
|
+
/// One automatic attempt by default. A transient failure that survives a
|
|
10
|
+
/// rebuild is rarely cured by a second, and further attempts only delay the
|
|
11
|
+
/// error the consumer needs to act on; `retry()` is the escape hatch after it.
|
|
12
|
+
func testTheAutomaticRetryBudgetIsOneAttemptByDefault() {
|
|
13
|
+
let player = TrackPlayer()
|
|
14
|
+
XCTAssertEqual(player.effectiveAutoRetries(), 1)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func testTheConsumerCanAskForNoneOrMore() {
|
|
18
|
+
let player = TrackPlayer()
|
|
19
|
+
player.config = PlayerFixtures.config(autoRetries: 0)
|
|
20
|
+
XCTAssertEqual(player.effectiveAutoRetries(), 0, "zero means zero")
|
|
21
|
+
player.config = PlayerFixtures.config(autoRetries: 4)
|
|
22
|
+
XCTAssertEqual(player.effectiveAutoRetries(), 4)
|
|
23
|
+
player.config = PlayerFixtures.config(autoRetries: 99)
|
|
24
|
+
XCTAssertEqual(player.effectiveAutoRetries(), 5, "clamped to the documented ceiling")
|
|
25
|
+
player.config = PlayerFixtures.config(autoRetries: .nan)
|
|
26
|
+
XCTAssertEqual(player.effectiveAutoRetries(), 1, "a value that is not a number falls back")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// A crossfade failure reaches the automatic retry.
|
|
30
|
+
///
|
|
31
|
+
/// The retry is keyed by `queueItemId`, so an engine reporting a failure
|
|
32
|
+
/// without the item leaves `dispatchErrorOrRetry` synthesising a key that
|
|
33
|
+
/// cannot appear in the queue — the retry branch is skipped and a transient
|
|
34
|
+
/// blip goes straight to the consumer instead of being recovered.
|
|
35
|
+
func testAnEngineFailureCarryingItsItemIsRetried() {
|
|
36
|
+
let player = TrackPlayer()
|
|
37
|
+
let engine = RetryEngineStub()
|
|
38
|
+
seed(player, engine: engine)
|
|
39
|
+
let qid = player.queueItemIds[0]
|
|
40
|
+
player.retryAttemptsRemaining[qid] = 1
|
|
41
|
+
// The engine names the item that failed, carrying the queue identity the
|
|
42
|
+
// retry is keyed by.
|
|
43
|
+
let item = AVPlayerItem(url: URL(string: "https://example.com/0.m4a")!)
|
|
44
|
+
item.queueItemId = qid
|
|
45
|
+
|
|
46
|
+
player.engineDidFailWithError(
|
|
47
|
+
engine, error: URLError(.timedOut), item: item)
|
|
48
|
+
|
|
49
|
+
XCTAssertEqual(
|
|
50
|
+
player.retryAttemptsRemaining[qid], 0,
|
|
51
|
+
"a transient failure spends the track's automatic attempt")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// The other half of the same contract: with no item there is nothing to key
|
|
55
|
+
/// a retry against, so the failure is surfaced rather than silently retried.
|
|
56
|
+
func testAFailureWithoutAnItemIsNotRetried() {
|
|
57
|
+
let player = TrackPlayer()
|
|
58
|
+
let engine = RetryEngineStub()
|
|
59
|
+
seed(player, engine: engine)
|
|
60
|
+
let qid = player.queueItemIds[0]
|
|
61
|
+
player.retryAttemptsRemaining[qid] = 1
|
|
62
|
+
|
|
63
|
+
player.engineDidFailWithError(
|
|
64
|
+
engine, error: URLError(.timedOut), item: nil)
|
|
65
|
+
|
|
66
|
+
XCTAssertEqual(
|
|
67
|
+
player.retryAttemptsRemaining[qid], 1,
|
|
68
|
+
"no item means no automatic attempt is spent")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// A repeat of the same failure while the attempt is pending stays inside
|
|
72
|
+
/// the library.
|
|
73
|
+
func testAnEchoOfThePendingFailureIsNotReported() {
|
|
74
|
+
let player = TrackPlayer()
|
|
75
|
+
let engine = RetryEngineStub()
|
|
76
|
+
seed(player, engine: engine)
|
|
77
|
+
let qid = player.queueItemIds[0]
|
|
78
|
+
player.retryAttemptsRemaining[qid] = 1
|
|
79
|
+
let item = AVPlayerItem(url: URL(string: "https://example.com/0.m4a")!)
|
|
80
|
+
item.queueItemId = qid
|
|
81
|
+
var reported: [PlaybackError] = []
|
|
82
|
+
_ = player.errorListeners.add { reported.append($0) }
|
|
83
|
+
|
|
84
|
+
player.engineDidFailWithError(engine, error: URLError(.timedOut), item: item)
|
|
85
|
+
player.engineDidFailWithError(engine, error: URLError(.timedOut), item: item)
|
|
86
|
+
|
|
87
|
+
XCTAssertEqual(reported.count, 0, "the pending attempt owns both fires")
|
|
88
|
+
XCTAssertEqual(
|
|
89
|
+
player.retryAttemptsRemaining[qid], 0,
|
|
90
|
+
"one failure spends one attempt, however many times it is reported")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// A failure after the attempt has run is the consumer's to see.
|
|
94
|
+
func testAFailureAfterTheAttemptIsReported() {
|
|
95
|
+
let player = TrackPlayer()
|
|
96
|
+
let engine = RetryEngineStub()
|
|
97
|
+
seed(player, engine: engine)
|
|
98
|
+
let qid = player.queueItemIds[0]
|
|
99
|
+
player.retryAttemptsRemaining[qid] = 1
|
|
100
|
+
let item = AVPlayerItem(url: URL(string: "https://example.com/0.m4a")!)
|
|
101
|
+
item.queueItemId = qid
|
|
102
|
+
var reported: [PlaybackError] = []
|
|
103
|
+
_ = player.errorListeners.add { reported.append($0) }
|
|
104
|
+
|
|
105
|
+
player.engineDidFailWithError(engine, error: URLError(.timedOut), item: item)
|
|
106
|
+
player.retryFailedItem(queueItemId: qid)
|
|
107
|
+
player.engineDidFailWithError(engine, error: URLError(.timedOut), item: item)
|
|
108
|
+
|
|
109
|
+
XCTAssertEqual(reported.count, 1, "the attempt was spent and the track is still broken")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// A user retry hands back the allowance the consumer configured, so one for
|
|
113
|
+
/// a default player and none for a consumer who turned the automatic attempt
|
|
114
|
+
/// off.
|
|
115
|
+
func testAUserRetryRestoresTheConfiguredBudget() async throws {
|
|
116
|
+
let player = TrackPlayer()
|
|
117
|
+
let engine = RetryEngineStub()
|
|
118
|
+
seed(player, engine: engine)
|
|
119
|
+
player.config = PlayerFixtures.config(autoRetries: 0)
|
|
120
|
+
let qid = player.queueItemIds[0]
|
|
121
|
+
|
|
122
|
+
_ = try await player.retry().await()
|
|
123
|
+
|
|
124
|
+
XCTAssertEqual(
|
|
125
|
+
player.retryAttemptsRemaining[qid], 0,
|
|
126
|
+
"a consumer who asked for no automatic attempt must not be given one")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// The user-facing retry clears `reachedQueueEnd` too — a queue whose
|
|
130
|
+
/// only track failed to load has already drained, and that is the case the
|
|
131
|
+
/// button exists for.
|
|
132
|
+
func testAUserRetryClearsTheEndOfQueueState() async throws {
|
|
133
|
+
let player = TrackPlayer()
|
|
134
|
+
let engine = RetryEngineStub()
|
|
135
|
+
seed(player, engine: engine)
|
|
136
|
+
player.reachedQueueEnd = true
|
|
137
|
+
|
|
138
|
+
_ = try await player.retry().await()
|
|
139
|
+
|
|
140
|
+
XCTAssertFalse(player.reachedQueueEnd)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// The rebuild a user asked for replaces a scheduled one, so the rebuilt
|
|
144
|
+
/// item's own failure is reported rather than taken for an echo.
|
|
145
|
+
func testAUserRetrySupersedesAScheduledAttempt() async throws {
|
|
146
|
+
let player = TrackPlayer()
|
|
147
|
+
let engine = RetryEngineStub()
|
|
148
|
+
seed(player, engine: engine)
|
|
149
|
+
player.pendingRetryQueueItemId = player.queueItemIds[0]
|
|
150
|
+
|
|
151
|
+
_ = try await player.retry().await()
|
|
152
|
+
|
|
153
|
+
XCTAssertNil(player.pendingRetryQueueItemId)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// A retry clears `reachedQueueEnd`.
|
|
157
|
+
///
|
|
158
|
+
/// A queue whose only track fails to load drains the engine, so the queue-end
|
|
159
|
+
/// verdict calls it finished and parks the player. Without clearing it
|
|
160
|
+
/// the retry seats an item into a queue that believes it is over, and
|
|
161
|
+
/// playback never resumes — the automatic attempt rebuilds and nothing plays.
|
|
162
|
+
func testRetryingClearsTheEndOfQueueState() {
|
|
163
|
+
let player = TrackPlayer()
|
|
164
|
+
let engine = RetryEngineStub()
|
|
165
|
+
seed(player, engine: engine)
|
|
166
|
+
player.reachedQueueEnd = true
|
|
167
|
+
|
|
168
|
+
player.retryFailedItem(queueItemId: player.queueItemIds[0])
|
|
169
|
+
|
|
170
|
+
XCTAssertFalse(
|
|
171
|
+
player.reachedQueueEnd,
|
|
172
|
+
"a retry is an intent to play this track, so the queue is not over")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// A drain whose current item failed with an automatic attempt already
|
|
176
|
+
/// scheduled is not the end of the queue. `AVQueuePlayer` consumes a failed
|
|
177
|
+
/// item and drains to `currentItem == nil`, which reaches `applyEndVerdict`;
|
|
178
|
+
/// deciding a queue-end there clears the play intent the scheduled retry
|
|
179
|
+
/// reads, so the recovered track never resumes. The verdict holds the queue
|
|
180
|
+
/// open while a retry is pending for the current track.
|
|
181
|
+
func testADrainWithAPendingRetryDoesNotEndTheQueue() {
|
|
182
|
+
let player = TrackPlayer()
|
|
183
|
+
let engine = RetryEngineStub()
|
|
184
|
+
seed(player, engine: engine)
|
|
185
|
+
player.currentTrackIndex = 1
|
|
186
|
+
player.wantsToPlay = true
|
|
187
|
+
player.pendingRetryQueueItemId = player.queueItemIds[1]
|
|
188
|
+
|
|
189
|
+
player.applyEndVerdict()
|
|
190
|
+
|
|
191
|
+
XCTAssertTrue(
|
|
192
|
+
player.wantsToPlay,
|
|
193
|
+
"a pending retry keeps the play intent so the automatic attempt can resume")
|
|
194
|
+
XCTAssertFalse(
|
|
195
|
+
player.reachedQueueEnd,
|
|
196
|
+
"a drain with a retry pending is a recovery in flight, not the end of the queue")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
func testRetryResumesFromWherePlaybackHadReached() {
|
|
200
|
+
let player = TrackPlayer()
|
|
201
|
+
let engine = RetryEngineStub()
|
|
202
|
+
engine.position = 42.5
|
|
203
|
+
seed(player, engine: engine)
|
|
204
|
+
// The crossfade shape: an engine is live, the gapless queue player is not.
|
|
205
|
+
XCTAssertNil(player.player)
|
|
206
|
+
|
|
207
|
+
player.retryFailedItem(queueItemId: player.queueItemIds[0])
|
|
208
|
+
|
|
209
|
+
XCTAssertEqual(
|
|
210
|
+
engine.seeks.first?.position, 42.5,
|
|
211
|
+
"a blip well into a long track must not restart it from zero")
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
func testRetryDoesNotSeekWhenPlaybackHadNotStarted() {
|
|
215
|
+
let player = TrackPlayer()
|
|
216
|
+
let engine = RetryEngineStub()
|
|
217
|
+
engine.position = 0
|
|
218
|
+
seed(player, engine: engine)
|
|
219
|
+
|
|
220
|
+
player.retryFailedItem(queueItemId: player.queueItemIds[0])
|
|
221
|
+
|
|
222
|
+
XCTAssertTrue(engine.seeks.isEmpty, "position 0 is the rebuild's own starting point")
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
func testRetryIsMootOnceTheUserHasMovedOn() {
|
|
226
|
+
let player = TrackPlayer()
|
|
227
|
+
let engine = RetryEngineStub()
|
|
228
|
+
engine.position = 30
|
|
229
|
+
seed(player, engine: engine)
|
|
230
|
+
let staleId = player.queueItemIds[1]
|
|
231
|
+
// The failure belongs to a track the player is no longer on.
|
|
232
|
+
player.retryFailedItem(queueItemId: staleId)
|
|
233
|
+
|
|
234
|
+
XCTAssertTrue(engine.seeks.isEmpty)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private func seed(_ player: TrackPlayer, engine: RetryEngineStub) {
|
|
238
|
+
_ = player.queueState.replaceAll((0 ..< 2).map {
|
|
239
|
+
TrackItem(
|
|
240
|
+
id: "t\($0)", url: "https://example.com/\($0).m4a", title: nil, artist: nil,
|
|
241
|
+
album: nil, duration: nil, artworkUrl: nil, extras: nil,
|
|
242
|
+
replayGainTrackGain: nil, replayGainTrackPeak: nil,
|
|
243
|
+
replayGainAlbumGain: nil, replayGainAlbumPeak: nil)
|
|
244
|
+
})
|
|
245
|
+
player.currentTrackIndex = 0
|
|
246
|
+
player.engine = engine
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// Records the seeks the retry issues; everything else is inert.
|
|
251
|
+
private final class RetryEngineStub: PlaybackEngine {
|
|
252
|
+
var position: TimeInterval = 0
|
|
253
|
+
private(set) var seeks: [(index: Int, position: TimeInterval)] = []
|
|
254
|
+
|
|
255
|
+
var currentPositionSeconds: TimeInterval { position }
|
|
256
|
+
func seek(toIndex index: Int, position: TimeInterval) {
|
|
257
|
+
seeks.append((index, position))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
func setAudioMixProvider(_ provider: AudioMixProvider?) {}
|
|
261
|
+
func refreshAllItemMixes() {}
|
|
262
|
+
func release() {}
|
|
263
|
+
/// Holds what it is given, so a caller resolving a queue position to an
|
|
264
|
+
/// engine index by `queueItemId` gets the same answer a real engine would.
|
|
265
|
+
func setItems(_ items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) {
|
|
266
|
+
installed = items
|
|
267
|
+
}
|
|
268
|
+
@discardableResult
|
|
269
|
+
func insertItems(_ items: [AVPlayerItem], after: AVPlayerItem?) -> Bool {
|
|
270
|
+
installed.append(contentsOf: items)
|
|
271
|
+
return true
|
|
272
|
+
}
|
|
273
|
+
func remove(_ item: AVPlayerItem) {
|
|
274
|
+
installed.removeAll { $0 === item }
|
|
275
|
+
}
|
|
276
|
+
func removeAllItems() {}
|
|
277
|
+
func canInsert(_ item: AVPlayerItem, after: AVPlayerItem?) -> Bool { true }
|
|
278
|
+
func play() {}
|
|
279
|
+
func pause() {}
|
|
280
|
+
func stop() {}
|
|
281
|
+
func skipToNext() {}
|
|
282
|
+
func skipToPrevious() {}
|
|
283
|
+
func setVolume(_ volume: Float) {}
|
|
284
|
+
func setPlaybackSpeed(_ speed: Float) {}
|
|
285
|
+
func seedPlaybackSpeed(_ speed: Float) {}
|
|
286
|
+
func setRepeatMode(_ mode: RepeatMode) {}
|
|
287
|
+
func setPitchCorrectionMode(_ mode: PitchCorrectionMode) {}
|
|
288
|
+
|
|
289
|
+
var currentDurationSeconds: TimeInterval { 0 }
|
|
290
|
+
var isPlaying: Bool { false }
|
|
291
|
+
var currentMediaItemIndex: Int { -1 }
|
|
292
|
+
var bufferedPositionSeconds: TimeInterval { 0 }
|
|
293
|
+
var timeControlStatus: AVPlayer.TimeControlStatus { .paused }
|
|
294
|
+
var currentMediaItem: AVPlayerItem? { nil }
|
|
295
|
+
private var installed: [AVPlayerItem] = []
|
|
296
|
+
var allMediaItems: [AVPlayerItem] { installed }
|
|
297
|
+
/// No fade to schedule — this stub exercises the rest of the surface.
|
|
298
|
+
var isFadePendingOrActive: Bool { false }
|
|
299
|
+
/// Nothing attached — this stub exercises the rest of the surface.
|
|
300
|
+
var attachedQueueItemIds: [String] { [] }
|
|
301
|
+
|
|
302
|
+
var delegate: PlaybackEngineDelegate?
|
|
303
|
+
}
|
|
@@ -34,20 +34,50 @@ final class SkipCapabilityTests: XCTestCase {
|
|
|
34
34
|
dispose()
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
/// A disposed subscription receives nothing once the auto-fire has settled.
|
|
38
|
+
///
|
|
39
|
+
/// An immediate dispose racing the auto-fire is a race, not a contract: the
|
|
40
|
+
/// fire is enqueued on the owner queue when `subscribe` returns, so the queue
|
|
41
|
+
/// block usually waits while the caller's synchronous dispose lands first,
|
|
42
|
+
/// and under load it does not. What the library guarantees is that the
|
|
43
|
+
/// registry is consulted before the callback runs, so a settled dispose is
|
|
44
|
+
/// final.
|
|
45
|
+
func testDisposeStopsCallbacksOnceSettled() throws {
|
|
40
46
|
let player = TrackPlayer()
|
|
41
47
|
var fireCount = 0
|
|
42
48
|
let dispose = try player.onSkipCapabilityChange { _ in
|
|
43
49
|
fireCount += 1
|
|
44
50
|
}
|
|
45
51
|
dispose()
|
|
46
|
-
|
|
47
|
-
let
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
drain(player)
|
|
53
|
+
let afterDispose = fireCount
|
|
54
|
+
|
|
55
|
+
// A queue with a track on either side, so the recompute below is a real
|
|
56
|
+
// change from the empty-queue capability rather than a deduplicated repeat
|
|
57
|
+
// — the disposed subscription has to be what keeps it from arriving.
|
|
58
|
+
player.playerQueue.async {
|
|
59
|
+
_ = player.queueState.replaceAll((0 ..< 3).map {
|
|
60
|
+
PlayerFixtures.track(id: "t\($0)", url: "https://example.com/\($0).m4a")
|
|
61
|
+
})
|
|
62
|
+
player.currentTrackIndex = 1
|
|
63
|
+
player.recomputeCapabilities()
|
|
64
|
+
}
|
|
65
|
+
drain(player)
|
|
66
|
+
|
|
67
|
+
XCTAssertEqual(
|
|
68
|
+
fireCount, afterDispose,
|
|
69
|
+
"a disposed subscription receives no further capability changes")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Two hops: the auto-fire runs on the owner queue and consumers are called
|
|
73
|
+
/// from there, so draining main alone can return before either has run.
|
|
74
|
+
private func drain(_ player: TrackPlayer) {
|
|
75
|
+
let queueDrained = expectation(description: "owner queue drained")
|
|
76
|
+
player.playerQueue.async { queueDrained.fulfill() }
|
|
77
|
+
wait(for: [queueDrained], timeout: 2.0)
|
|
78
|
+
let mainDrained = expectation(description: "main drained")
|
|
79
|
+
DispatchQueue.main.async { mainDrained.fulfill() }
|
|
80
|
+
wait(for: [mainDrained], timeout: 2.0)
|
|
51
81
|
}
|
|
52
82
|
|
|
53
83
|
func testOnSkipCapabilityChangeMultiSubscriberEachGetsAutoFire() throws {
|
|
@@ -81,6 +111,129 @@ final class SkipCapabilityTests: XCTestCase {
|
|
|
81
111
|
dispose()
|
|
82
112
|
}
|
|
83
113
|
|
|
114
|
+
// MARK: - computeCapabilities
|
|
115
|
+
|
|
116
|
+
func testComputeCapabilitiesEnablesBothDirectionsMidQueue() {
|
|
117
|
+
let player = TrackPlayer()
|
|
118
|
+
seedQueue(player, count: 3, currentIndex: 1)
|
|
119
|
+
let capability = player.computeCapabilities()
|
|
120
|
+
XCTAssertTrue(capability.canSkipPrevious)
|
|
121
|
+
XCTAssertTrue(capability.canSkipNext)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func testComputeCapabilitiesDisablesTheAbsentDirectionAtEachQueueEnd() {
|
|
125
|
+
let player = TrackPlayer()
|
|
126
|
+
seedQueue(player, count: 3, currentIndex: 0)
|
|
127
|
+
let first = player.computeCapabilities()
|
|
128
|
+
XCTAssertFalse(first.canSkipPrevious)
|
|
129
|
+
XCTAssertTrue(first.canSkipNext)
|
|
130
|
+
|
|
131
|
+
player.currentTrackIndex = 2
|
|
132
|
+
let last = player.computeCapabilities()
|
|
133
|
+
XCTAssertTrue(last.canSkipPrevious)
|
|
134
|
+
XCTAssertFalse(last.canSkipNext)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func testComputeCapabilitiesEnablesBothDirectionsUnderRepeatQueue() {
|
|
138
|
+
let player = TrackPlayer()
|
|
139
|
+
seedQueue(player, count: 3, currentIndex: 0)
|
|
140
|
+
player.repeatModeState = .queue
|
|
141
|
+
let capability = player.computeCapabilities()
|
|
142
|
+
XCTAssertTrue(capability.canSkipPrevious)
|
|
143
|
+
XCTAssertTrue(capability.canSkipNext)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func testComputeCapabilitiesLeavesTheCachedCapabilityUntouched() {
|
|
147
|
+
let player = TrackPlayer()
|
|
148
|
+
seedQueue(player, count: 3, currentIndex: 1)
|
|
149
|
+
player.cachedSkipCapability = SkipCapability(canSkipPrevious: true, canSkipNext: false)
|
|
150
|
+
|
|
151
|
+
_ = player.computeCapabilities()
|
|
152
|
+
|
|
153
|
+
XCTAssertTrue(player.cachedSkipCapability.canSkipPrevious)
|
|
154
|
+
XCTAssertFalse(player.cachedSkipCapability.canSkipNext)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// MARK: - buildCarPlayQueueSnapshot
|
|
158
|
+
|
|
159
|
+
func testBuildCarPlayQueueSnapshotMirrorsTheQueueAndCurrentIndex() {
|
|
160
|
+
let player = TrackPlayer()
|
|
161
|
+
seedQueue(player, count: 3, currentIndex: 2)
|
|
162
|
+
let snapshot = player.buildCarPlayQueueSnapshot()
|
|
163
|
+
XCTAssertEqual(snapshot?.entries.count, 3)
|
|
164
|
+
XCTAssertEqual(snapshot?.currentIndex, 2)
|
|
165
|
+
XCTAssertEqual(snapshot?.entries.map { $0.title }, ["Track 0", "Track 1", "Track 2"])
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
func testBuildCarPlayQueueSnapshotIsNilForAnEmptyQueue() {
|
|
169
|
+
let player = TrackPlayer()
|
|
170
|
+
XCTAssertNil(player.buildCarPlayQueueSnapshot())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func testBuildCarPlayQueueSnapshotFallsBackToAlbumWhenArtistIsAbsent() {
|
|
174
|
+
let player = TrackPlayer()
|
|
175
|
+
_ = player.queueState.replaceAll([
|
|
176
|
+
makeTrack(index: 0, title: nil, artist: nil, album: "Album"),
|
|
177
|
+
makeTrack(index: 1, title: "Titled", artist: "Artist", album: "Album"),
|
|
178
|
+
])
|
|
179
|
+
player.currentTrackIndex = 0
|
|
180
|
+
let entries = player.buildCarPlayQueueSnapshot()?.entries
|
|
181
|
+
XCTAssertEqual(entries?[0].title, "", "an absent title renders as an empty string")
|
|
182
|
+
XCTAssertEqual(entries?[0].subtitle, "Album")
|
|
183
|
+
XCTAssertEqual(entries?[1].subtitle, "Artist")
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// MARK: - publishCapabilities
|
|
187
|
+
|
|
188
|
+
func testPublishCapabilitiesRefreshesTheCarPlayMirrorWhenTheCapabilityIsUnchanged() {
|
|
189
|
+
let player = TrackPlayer()
|
|
190
|
+
seedQueue(player, count: 3, currentIndex: 1)
|
|
191
|
+
let capability = player.computeCapabilities()
|
|
192
|
+
player.cachedSkipCapability = capability
|
|
193
|
+
defer { CarPlayCoordinator.shared.queueProvider.setSnapshot(nil) }
|
|
194
|
+
|
|
195
|
+
player.publishCapabilities(
|
|
196
|
+
capability, queueSnapshot: player.buildCarPlayQueueSnapshot(), carAttached: true)
|
|
197
|
+
|
|
198
|
+
XCTAssertEqual(
|
|
199
|
+
CarPlayCoordinator.shared.queueProvider.getSnapshot()?.entries.count, 3,
|
|
200
|
+
"a reorder or metadata change refreshes the mirror even though it leaves "
|
|
201
|
+
+ "skip capability untouched")
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func testPublishCapabilitiesClearsTheCarPlayMirrorForAnEmptyQueue() {
|
|
205
|
+
let player = TrackPlayer()
|
|
206
|
+
defer { CarPlayCoordinator.shared.queueProvider.setSnapshot(nil) }
|
|
207
|
+
CarPlayCoordinator.shared.queueProvider.setSnapshot(
|
|
208
|
+
CarPlayQueueSnapshot(
|
|
209
|
+
entries: [CarPlayQueueEntry(title: "stale", subtitle: nil, artworkUrl: nil)],
|
|
210
|
+
currentIndex: 0))
|
|
211
|
+
|
|
212
|
+
player.publishCapabilities(
|
|
213
|
+
player.computeCapabilities(),
|
|
214
|
+
queueSnapshot: player.buildCarPlayQueueSnapshot(), carAttached: true)
|
|
215
|
+
|
|
216
|
+
XCTAssertNil(CarPlayCoordinator.shared.queueProvider.getSnapshot())
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func testPublishCapabilitiesLeavesTheMirrorAloneWithNoCarAttached() {
|
|
220
|
+
let player = TrackPlayer()
|
|
221
|
+
seedQueue(player, count: 2, currentIndex: 0)
|
|
222
|
+
defer { CarPlayCoordinator.shared.queueProvider.setSnapshot(nil) }
|
|
223
|
+
CarPlayCoordinator.shared.queueProvider.setSnapshot(
|
|
224
|
+
CarPlayQueueSnapshot(
|
|
225
|
+
entries: [CarPlayQueueEntry(title: "held", subtitle: nil, artworkUrl: nil)],
|
|
226
|
+
currentIndex: 0))
|
|
227
|
+
|
|
228
|
+
player.publishCapabilities(
|
|
229
|
+
player.computeCapabilities(), queueSnapshot: nil, carAttached: false)
|
|
230
|
+
|
|
231
|
+
XCTAssertEqual(
|
|
232
|
+
CarPlayCoordinator.shared.queueProvider.getSnapshot()?.entries.first?.title, "held",
|
|
233
|
+
"a snapshot that was never built because no car is attached must not be "
|
|
234
|
+
+ "mistaken for a request to clear")
|
|
235
|
+
}
|
|
236
|
+
|
|
84
237
|
func testListenerRegistryContainsAfterAddAndAbsentAfterRemove() {
|
|
85
238
|
// After `add()` returns an id, `contains(id:)` returns true.
|
|
86
239
|
// After `remove(id:)`, `contains(id:)` returns false.
|
|
@@ -92,4 +245,76 @@ final class SkipCapabilityTests: XCTestCase {
|
|
|
92
245
|
XCTAssertFalse(registry.contains(id: id))
|
|
93
246
|
XCTAssertFalse(registry.remove(id: id))
|
|
94
247
|
}
|
|
248
|
+
private func seedQueue(_ player: TrackPlayer, count: Int, currentIndex: Int) {
|
|
249
|
+
_ = player.queueState.replaceAll(
|
|
250
|
+
(0 ..< count).map { makeTrack(index: $0, title: "Track \($0)") })
|
|
251
|
+
player.currentTrackIndex = currentIndex
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private func makeTrack(
|
|
255
|
+
index: Int, title: String?, artist: String? = nil, album: String? = nil
|
|
256
|
+
) -> TrackItem {
|
|
257
|
+
return TrackItem(
|
|
258
|
+
id: "t\(index)", url: "https://example.com/\(index).m4a",
|
|
259
|
+
title: title, artist: artist, album: album, duration: nil, artworkUrl: nil,
|
|
260
|
+
extras: nil, replayGainTrackGain: nil, replayGainTrackPeak: nil,
|
|
261
|
+
replayGainAlbumGain: nil, replayGainAlbumPeak: nil
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
/// Restart-or-previous with no loaded track keeps Previous disabled.
|
|
267
|
+
func testRestartModeWithNoLoadedTrackKeepsPreviousDisabled() {
|
|
268
|
+
let player = TrackPlayer()
|
|
269
|
+
player.config = PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, voiceVocabularyDonationEnabled: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: .restartOrPrevious, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil)
|
|
270
|
+
seedQueue(player, count: 3, currentIndex: -1)
|
|
271
|
+
XCTAssertFalse(player.computeCapabilities().canSkipPrevious)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// Restart-or-previous with a loaded track at index 0 enables Previous.
|
|
275
|
+
func testRestartModeAtIndexZeroEnablesPrevious() {
|
|
276
|
+
let player = TrackPlayer()
|
|
277
|
+
player.config = PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, voiceVocabularyDonationEnabled: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: .restartOrPrevious, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil)
|
|
278
|
+
seedQueue(player, count: 3, currentIndex: 0)
|
|
279
|
+
XCTAssertTrue(player.computeCapabilities().canSkipPrevious)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// MARK: - cast index writes keep capability and source current
|
|
283
|
+
|
|
284
|
+
/// A receiver advance to the last track disables Next, as a local advance does.
|
|
285
|
+
func testRemoteTrackChangeToLastIndexDisablesNext() {
|
|
286
|
+
let player = TrackPlayer()
|
|
287
|
+
seedQueue(player, count: 3, currentIndex: 0)
|
|
288
|
+
player.playerQueue.async { player.recomputeCapabilities() }
|
|
289
|
+
drain(player)
|
|
290
|
+
XCTAssertTrue(player.cachedSkipCapability.canSkipNext)
|
|
291
|
+
|
|
292
|
+
player.emitRemoteTrackChange(index: 2)
|
|
293
|
+
drain(player)
|
|
294
|
+
XCTAssertEqual(player.currentTrackIndex, 2)
|
|
295
|
+
XCTAssertFalse(player.cachedSkipCapability.canSkipNext)
|
|
296
|
+
XCTAssertTrue(player.cachedSkipCapability.canSkipPrevious)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/// A receiver advance classifies the source of the track it moved to.
|
|
300
|
+
func testRemoteTrackChangeClassifiesTheNewTracksSource() {
|
|
301
|
+
let player = TrackPlayer()
|
|
302
|
+
_ = player.queueState.replaceAll([
|
|
303
|
+
makeTrack(index: 0, title: "a"),
|
|
304
|
+
makeTrack(index: 1, title: "b"),
|
|
305
|
+
TrackItem(
|
|
306
|
+
id: "local", url: "file:///tmp/local.m4a",
|
|
307
|
+
title: nil, artist: nil, album: nil, duration: nil, artworkUrl: nil,
|
|
308
|
+
extras: nil, replayGainTrackGain: nil, replayGainTrackPeak: nil,
|
|
309
|
+
replayGainAlbumGain: nil, replayGainAlbumPeak: nil),
|
|
310
|
+
])
|
|
311
|
+
player.currentTrackIndex = 0
|
|
312
|
+
player.playerQueue.async { player.reclassifyCurrentTrackSource() }
|
|
313
|
+
drain(player)
|
|
314
|
+
XCTAssertEqual(player.currentTrackSource, .streaming)
|
|
315
|
+
|
|
316
|
+
player.emitRemoteTrackChange(index: 2)
|
|
317
|
+
drain(player)
|
|
318
|
+
XCTAssertEqual(player.currentTrackSource, .local)
|
|
319
|
+
}
|
|
95
320
|
}
|
|
@@ -91,8 +91,8 @@ final class SkipIndexTests: XCTestCase {
|
|
|
91
91
|
|
|
92
92
|
func testComputePreviousRepeatOffAtStartReturnsNil() {
|
|
93
93
|
// No rewind-on-boundary under .off. The consumer-facing "tap Previous
|
|
94
|
-
// at index 0 restarts the track" behaviour lives in
|
|
95
|
-
//
|
|
94
|
+
// at index 0 restarts the track" behaviour lives in `skipToPrevious`,
|
|
95
|
+
// not here.
|
|
96
96
|
XCTAssertNil(QueueSkipArithmetic.computePrevious(
|
|
97
97
|
trackCount: 3, currentIndex: 0, repeatMode: .off))
|
|
98
98
|
}
|
|
@@ -353,4 +353,34 @@ final class SkipIndexTests: XCTestCase {
|
|
|
353
353
|
XCTAssertTrue(QueueSkipArithmetic.canSkipPrevious(
|
|
354
354
|
trackCount: 3, currentIndex: 1, repeatMode: .off, restartEnabled: false))
|
|
355
355
|
}
|
|
356
|
+
|
|
357
|
+
func testCanSkipPreviousRestartNothingPlayingReturnsFalseInAnyMode() {
|
|
358
|
+
// Nothing is loaded, so there is no track to restart and no previous
|
|
359
|
+
// track to reach: resolveSkipToPrevious returns no-op for this state.
|
|
360
|
+
for mode in [QueueSkipRepeatMode.off, .queue, .track] {
|
|
361
|
+
XCTAssertFalse(QueueSkipArithmetic.canSkipPrevious(
|
|
362
|
+
trackCount: 5, currentIndex: -1, repeatMode: mode, restartEnabled: true))
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func testCanSkipPreviousAgreesWithResolveSkipToPrevious() {
|
|
367
|
+
// canSkipPrevious is the enabled-state of the same decision
|
|
368
|
+
// resolveSkipToPrevious makes; the two must never disagree.
|
|
369
|
+
for count in [0, 1, 3] {
|
|
370
|
+
for index in [-1, 0, 1, 2] {
|
|
371
|
+
for mode in [QueueSkipRepeatMode.off, .queue, .track] {
|
|
372
|
+
for restart in [false, true] {
|
|
373
|
+
let can = QueueSkipArithmetic.canSkipPrevious(
|
|
374
|
+
trackCount: count, currentIndex: index, repeatMode: mode,
|
|
375
|
+
restartEnabled: restart)
|
|
376
|
+
let resolved = QueueSkipArithmetic.resolveSkipToPrevious(
|
|
377
|
+
trackCount: count, currentIndex: index, repeatMode: mode,
|
|
378
|
+
restartEnabled: restart, positionMs: 0)
|
|
379
|
+
XCTAssertEqual(can, resolved != .noOp,
|
|
380
|
+
"count=\(count) index=\(index) mode=\(mode) restart=\(restart)")
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
356
386
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
|
|
4
|
+
/// The sleep timer's pause is a transport intent: the paths that restart
|
|
5
|
+
/// playback on their own (a drain refill, a buffer coming back) read
|
|
6
|
+
/// `wantsToPlay`, so the timer clears it the way a user pause does.
|
|
7
|
+
final class SleepTimerPauseIntentTests: XCTestCase {
|
|
8
|
+
|
|
9
|
+
private var player: TrackPlayer!
|
|
10
|
+
|
|
11
|
+
override func setUp() async throws {
|
|
12
|
+
try await super.setUp()
|
|
13
|
+
player = TrackPlayer()
|
|
14
|
+
_ = try await player.configure(config: PlayerFixtures.config()).await()
|
|
15
|
+
_ = try await player.setQueue(
|
|
16
|
+
tracks: PlayerFixtures.localTracks(count: 2, for: self), startAtIndex: 0
|
|
17
|
+
).await()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override func tearDown() async throws {
|
|
21
|
+
if let player {
|
|
22
|
+
_ = try await player.destroy().await()
|
|
23
|
+
}
|
|
24
|
+
player = nil
|
|
25
|
+
try await super.tearDown()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func testTheTimersPauseClearsTheIntentToPlay() async throws {
|
|
29
|
+
let player = try XCTUnwrap(self.player)
|
|
30
|
+
await player.onPlayerQueue {
|
|
31
|
+
player.wantsToPlay = true
|
|
32
|
+
player.isRecoveringFromStall = true
|
|
33
|
+
player.applySleepTimerResult(
|
|
34
|
+
SleepTimerCore.Tick(fadeFraction: 1.0, pauseNow: true, stateChanged: true))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let state = await player.onPlayerQueueValue {
|
|
38
|
+
(wants: player.wantsToPlay, recovering: player.isRecoveringFromStall)
|
|
39
|
+
}
|
|
40
|
+
XCTAssertFalse(state.wants, "a drain or a buffer refill must not restart what the timer stopped")
|
|
41
|
+
XCTAssertFalse(state.recovering)
|
|
42
|
+
}
|
|
43
|
+
}
|