react-native-queue-player 1.2.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/consumer-rules.pro +1 -1
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +3 -19
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +8 -69
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +2 -2
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +672 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +229 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaResponseCheck.swift +122 -0
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MediaResponseCheckTests.swift +128 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +418 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +441 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +982 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +951 -0
- package/ios/TrackPlayer+Recovery.swift +213 -0
- package/ios/TrackPlayer+Skip.swift +465 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +272 -0
- package/ios/TrackPlayer+Window.swift +227 -0
- package/ios/TrackPlayer.swift +331 -4619
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// The lock-screen snapshot reports the engine's position, duration and
|
|
6
|
+
/// effective rate — the configured speed while playing, zero otherwise — for
|
|
7
|
+
/// whichever engine is active, including one with no gapless queue player.
|
|
8
|
+
final class NowPlayingSnapshotTests: XCTestCase {
|
|
9
|
+
|
|
10
|
+
func testSnapshotReportsTheEnginesPositionAndDurationWithNoQueuePlayer() {
|
|
11
|
+
let player = TrackPlayer()
|
|
12
|
+
seedOneTrack(player)
|
|
13
|
+
let engine = SnapshotEngineStub()
|
|
14
|
+
engine.position = 42.5
|
|
15
|
+
engine.duration = 180
|
|
16
|
+
engine.playing = true
|
|
17
|
+
player.engine = engine
|
|
18
|
+
XCTAssertNil(player.player, "precondition: no gapless queue player to read")
|
|
19
|
+
|
|
20
|
+
let snapshot = player.nowPlayingSnapshot()
|
|
21
|
+
|
|
22
|
+
XCTAssertEqual(snapshot.elapsedSeconds, 42.5)
|
|
23
|
+
XCTAssertEqual(snapshot.durationSeconds, 180)
|
|
24
|
+
XCTAssertEqual(
|
|
25
|
+
snapshot.rate, 1.0,
|
|
26
|
+
"a playing engine must report a non-zero rate, or the lock screen renders paused")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func testRateIsZeroBecauseTheEngineIsNotPlayingRatherThanAlways() {
|
|
30
|
+
let player = TrackPlayer()
|
|
31
|
+
seedOneTrack(player)
|
|
32
|
+
let engine = SnapshotEngineStub()
|
|
33
|
+
engine.position = 10
|
|
34
|
+
engine.duration = 60
|
|
35
|
+
engine.playing = false
|
|
36
|
+
player.engine = engine
|
|
37
|
+
player.playbackSpeedState = 1.5
|
|
38
|
+
|
|
39
|
+
XCTAssertEqual(player.nowPlayingSnapshot().rate, 0)
|
|
40
|
+
XCTAssertEqual(
|
|
41
|
+
player.nowPlayingSnapshot().elapsedSeconds, 10,
|
|
42
|
+
"position still reports while paused")
|
|
43
|
+
|
|
44
|
+
// Same fixture, same configured speed — only the play state changes.
|
|
45
|
+
engine.playing = true
|
|
46
|
+
XCTAssertEqual(player.nowPlayingSnapshot().rate, 1.5)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func testSnapshotCarriesTheConfiguredSpeedAsTheRate() {
|
|
50
|
+
let player = TrackPlayer()
|
|
51
|
+
seedOneTrack(player)
|
|
52
|
+
let engine = SnapshotEngineStub()
|
|
53
|
+
engine.playing = true
|
|
54
|
+
player.engine = engine
|
|
55
|
+
player.playbackSpeedState = 1.5
|
|
56
|
+
|
|
57
|
+
XCTAssertEqual(player.nowPlayingSnapshot().rate, 1.5)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func testSnapshotHasNoTrackForAnEmptyQueue() {
|
|
61
|
+
let player = TrackPlayer()
|
|
62
|
+
player.engine = SnapshotEngineStub()
|
|
63
|
+
|
|
64
|
+
XCTAssertNil(player.nowPlayingSnapshot().track)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private func seedOneTrack(_ player: TrackPlayer) {
|
|
68
|
+
_ = player.queueState.replaceAll([
|
|
69
|
+
TrackItem(
|
|
70
|
+
id: "t0", url: "https://example.com/0.m4a", title: "Song", artist: nil,
|
|
71
|
+
album: nil, duration: nil, artworkUrl: nil, extras: nil,
|
|
72
|
+
replayGainTrackGain: nil, replayGainTrackPeak: nil,
|
|
73
|
+
replayGainAlbumGain: nil, replayGainAlbumPeak: nil)
|
|
74
|
+
])
|
|
75
|
+
player.currentTrackIndex = 0
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Reports whatever the test sets. Every other `PlaybackEngine` member is an
|
|
80
|
+
/// inert stub — the snapshot only reads position, duration and
|
|
81
|
+
/// `timeControlStatus`.
|
|
82
|
+
private final class SnapshotEngineStub: PlaybackEngine {
|
|
83
|
+
var position: TimeInterval = 0
|
|
84
|
+
var duration: TimeInterval = 0
|
|
85
|
+
var playing = false
|
|
86
|
+
|
|
87
|
+
var currentPositionSeconds: TimeInterval { position }
|
|
88
|
+
var currentDurationSeconds: TimeInterval { duration }
|
|
89
|
+
/// The snapshot reads `timeControlStatus`; `isPlaying` is here only to
|
|
90
|
+
/// satisfy the protocol, and the two agree in this stub.
|
|
91
|
+
var isPlaying: Bool { playing }
|
|
92
|
+
|
|
93
|
+
func setAudioMixProvider(_ provider: AudioMixProvider?) {}
|
|
94
|
+
func refreshAllItemMixes() {}
|
|
95
|
+
func release() {}
|
|
96
|
+
func setItems(_ items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) {}
|
|
97
|
+
@discardableResult
|
|
98
|
+
func insertItems(_ items: [AVPlayerItem], after: AVPlayerItem?) -> Bool { true }
|
|
99
|
+
func remove(_ item: AVPlayerItem) {}
|
|
100
|
+
func removeAllItems() {}
|
|
101
|
+
func canInsert(_ item: AVPlayerItem, after: AVPlayerItem?) -> Bool { true }
|
|
102
|
+
func play() {}
|
|
103
|
+
func pause() {}
|
|
104
|
+
func stop() {}
|
|
105
|
+
func seek(toIndex index: Int, position: TimeInterval) {}
|
|
106
|
+
func skipToNext() {}
|
|
107
|
+
func skipToPrevious() {}
|
|
108
|
+
func setVolume(_ volume: Float) {}
|
|
109
|
+
func setPlaybackSpeed(_ speed: Float) {}
|
|
110
|
+
func seedPlaybackSpeed(_ speed: Float) {}
|
|
111
|
+
func setRepeatMode(_ mode: RepeatMode) {}
|
|
112
|
+
func setPitchCorrectionMode(_ mode: PitchCorrectionMode) {}
|
|
113
|
+
|
|
114
|
+
var currentMediaItemIndex: Int { -1 }
|
|
115
|
+
var bufferedPositionSeconds: TimeInterval { 0 }
|
|
116
|
+
var timeControlStatus: AVPlayer.TimeControlStatus { playing ? .playing : .paused }
|
|
117
|
+
var currentMediaItem: AVPlayerItem? { nil }
|
|
118
|
+
var allMediaItems: [AVPlayerItem] { [] }
|
|
119
|
+
|
|
120
|
+
/// No fade to schedule — this stub exercises the rest of the surface.
|
|
121
|
+
var isFadePendingOrActive: Bool { false }
|
|
122
|
+
|
|
123
|
+
/// Nothing attached — this stub exercises the rest of the surface.
|
|
124
|
+
var attachedQueueItemIds: [String] { [] }
|
|
125
|
+
|
|
126
|
+
var delegate: PlaybackEngineDelegate?
|
|
127
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
|
|
4
|
+
/// A re-issued origin transfer repeats bytes the client already has; the
|
|
5
|
+
/// stitcher drops exactly those and refuses an answer that changed.
|
|
6
|
+
final class OriginRestartStitcherTests: XCTestCase {
|
|
7
|
+
|
|
8
|
+
private func head(
|
|
9
|
+
status: Int = 200, length: String? = "1500", etag: String? = nil
|
|
10
|
+
) -> HTTPURLResponse {
|
|
11
|
+
var headers: [String: String] = [:]
|
|
12
|
+
if let length { headers["Content-Length"] = length }
|
|
13
|
+
if let etag { headers["ETag"] = etag }
|
|
14
|
+
return HTTPURLResponse(
|
|
15
|
+
url: URL(string: "http://127.0.0.1:1/a.mp3")!, statusCode: status,
|
|
16
|
+
httpVersion: "HTTP/1.1", headerFields: headers)!
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func testAFirstTransferPassesThrough() {
|
|
20
|
+
var stitcher = OriginRestartStitcher()
|
|
21
|
+
XCTAssertEqual(stitcher.head(head()), .accept)
|
|
22
|
+
XCTAssertEqual(stitcher.body(Data(repeating: 1, count: 1000)).count, 1000)
|
|
23
|
+
XCTAssertEqual(stitcher.deliveredBytes, 1000)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func testARestartRepeatsOnlyWhatWasNotDelivered() {
|
|
27
|
+
var stitcher = OriginRestartStitcher()
|
|
28
|
+
_ = stitcher.head(head())
|
|
29
|
+
_ = stitcher.body(Data(repeating: 1, count: 1000))
|
|
30
|
+
|
|
31
|
+
XCTAssertEqual(stitcher.head(head()), .accept, "the same answer is stitched")
|
|
32
|
+
let restarted = Data((0 ..< 1500).map { UInt8($0 % 251) })
|
|
33
|
+
let passed = stitcher.body(restarted)
|
|
34
|
+
|
|
35
|
+
XCTAssertEqual(passed.count, 500)
|
|
36
|
+
XCTAssertTrue(passed.elementsEqual(restarted[1000...]), "the tail past the delivered bytes")
|
|
37
|
+
XCTAssertEqual(stitcher.deliveredBytes, 1500)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// The repeated bytes can arrive across several chunks.
|
|
41
|
+
func testTheSkipSpansChunks() {
|
|
42
|
+
var stitcher = OriginRestartStitcher()
|
|
43
|
+
_ = stitcher.head(head())
|
|
44
|
+
_ = stitcher.body(Data(repeating: 1, count: 1000))
|
|
45
|
+
_ = stitcher.head(head())
|
|
46
|
+
|
|
47
|
+
XCTAssertTrue(stitcher.body(Data(repeating: 2, count: 400)).isEmpty)
|
|
48
|
+
XCTAssertTrue(stitcher.body(Data(repeating: 2, count: 600)).isEmpty)
|
|
49
|
+
XCTAssertEqual(stitcher.body(Data(repeating: 3, count: 100)).count, 100)
|
|
50
|
+
XCTAssertEqual(stitcher.deliveredBytes, 1100)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func testADifferentAnswerIsRefused() {
|
|
54
|
+
var stitcher = OriginRestartStitcher()
|
|
55
|
+
_ = stitcher.head(head(etag: "\"v1\""))
|
|
56
|
+
XCTAssertEqual(stitcher.head(head(etag: "\"v2\"")), .reject, "a changed ETag is a different resource")
|
|
57
|
+
XCTAssertEqual(stitcher.head(head(length: "2000")), .reject, "a changed length too")
|
|
58
|
+
XCTAssertEqual(stitcher.head(head(status: 206)), .reject, "and a changed status")
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import Network
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
@_spi(QueuePlayerTests) import QueuePlayer
|
|
5
|
+
|
|
6
|
+
/// The monitor reports the network coming back after an outage, and only that.
|
|
7
|
+
/// A handoff between interfaces is not a recovery, and treating it as one would
|
|
8
|
+
/// retry against a connection that never dropped.
|
|
9
|
+
///
|
|
10
|
+
/// `NWPathMonitor` delivers the current path once on `start`, and the monitor
|
|
11
|
+
/// seeds its last status as unsatisfied, so on a device with a satisfied path
|
|
12
|
+
/// every armed `start()` reports exactly one restore. That first delivery is
|
|
13
|
+
/// what makes the arming observable. A device with no satisfied path fails
|
|
14
|
+
/// these tests at the precondition rather than passing them empty.
|
|
15
|
+
final class PlaybackNetworkMonitorTests: XCTestCase {
|
|
16
|
+
|
|
17
|
+
private let firstDeliveryTimeout: TimeInterval = 5
|
|
18
|
+
|
|
19
|
+
/// Arms `monitor` and waits for the one restore its first path delivery reports.
|
|
20
|
+
private func armAndAwaitRestore(_ monitor: PlaybackNetworkMonitor, restores: Counter) {
|
|
21
|
+
let first = expectation(description: "the current path is delivered on start")
|
|
22
|
+
// The callback stays installed for the monitor's life, so a genuine
|
|
23
|
+
// down → up during a later window reaches the counter assertions rather
|
|
24
|
+
// than tripping this expectation's over-fulfil check.
|
|
25
|
+
first.assertForOverFulfill = false
|
|
26
|
+
monitor.onNetworkRestored = {
|
|
27
|
+
restores.bump()
|
|
28
|
+
first.fulfill()
|
|
29
|
+
}
|
|
30
|
+
monitor.start()
|
|
31
|
+
let outcome = XCTWaiter.wait(for: [first], timeout: firstDeliveryTimeout)
|
|
32
|
+
guard outcome == .completed else {
|
|
33
|
+
XCTFail(
|
|
34
|
+
"no restore within \(Int(firstDeliveryTimeout))s of start — this device has no satisfied "
|
|
35
|
+
+ "network path, so the monitor cannot be observed here")
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
XCTAssertEqual(restores.value, 1, "exactly one restore per armed start")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// A `stop()` before any `start()` is not terminal: the arming that follows
|
|
42
|
+
/// still delivers the current path.
|
|
43
|
+
func testStopBeforeStartChangesNothing() {
|
|
44
|
+
let monitor = PlaybackNetworkMonitor(queue: .main)
|
|
45
|
+
let restores = Counter()
|
|
46
|
+
monitor.stop()
|
|
47
|
+
armAndAwaitRestore(monitor, restores: restores)
|
|
48
|
+
monitor.stop()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Main-queue counter the monitor's `queue: .main` callbacks bump.
|
|
54
|
+
private final class Counter {
|
|
55
|
+
private(set) var value = 0
|
|
56
|
+
func bump() { value += 1 }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Recovery runs only when all four conditions hold together.
|
|
60
|
+
final class NetworkRecoveryPolicyTests: XCTestCase {
|
|
61
|
+
|
|
62
|
+
func testRetriesWhenPlaybackIsIntendedAndTheItemFailedTransiently() {
|
|
63
|
+
XCTAssertTrue(
|
|
64
|
+
NetworkRecoveryPolicy.shouldRetry(
|
|
65
|
+
wantsToPlay: true, isInterrupted: false, isInError: true, isTransient: true))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func testDoesNotRetryWhenPlaybackWasNeverIntended() {
|
|
69
|
+
XCTAssertFalse(
|
|
70
|
+
NetworkRecoveryPolicy.shouldRetry(
|
|
71
|
+
wantsToPlay: false, isInterrupted: false, isInError: true, isTransient: true),
|
|
72
|
+
"a network change must not start audio nobody asked for")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func testDoesNotRetryDuringAnInterruption() {
|
|
76
|
+
XCTAssertFalse(
|
|
77
|
+
NetworkRecoveryPolicy.shouldRetry(
|
|
78
|
+
wantsToPlay: true, isInterrupted: true, isInError: true, isTransient: true),
|
|
79
|
+
"the network returning mid-call must not start audio")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func testDoesNotRetryWhenNothingHasFailed() {
|
|
83
|
+
XCTAssertFalse(
|
|
84
|
+
NetworkRecoveryPolicy.shouldRetry(
|
|
85
|
+
wantsToPlay: true, isInterrupted: false, isInError: false, isTransient: true),
|
|
86
|
+
"an interface handoff must not rebuild a healthy item")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func testDoesNotRetryAFailureANetworkCannotCure() {
|
|
90
|
+
XCTAssertFalse(
|
|
91
|
+
NetworkRecoveryPolicy.shouldRetry(
|
|
92
|
+
wantsToPlay: true, isInterrupted: false, isInError: true, isTransient: false),
|
|
93
|
+
"a codec, format or auth failure re-surfaces on every rebuild")
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
|
|
4
|
+
/// Shared fixtures for tests that need a configured `TrackPlayer` with a real
|
|
5
|
+
/// playable queue.
|
|
6
|
+
enum PlayerFixtures {
|
|
7
|
+
|
|
8
|
+
static func config(autoRetries: Double? = nil) -> PlayerConfig {
|
|
9
|
+
return PlayerConfig(
|
|
10
|
+
httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil,
|
|
11
|
+
autoRetries: autoRetries, retryBackoffMs: nil, networkTimeoutMs: nil,
|
|
12
|
+
audioContentType: nil, audioCategoryOptions: nil,
|
|
13
|
+
voiceVocabularyDonationEnabled: nil,
|
|
14
|
+
visualizationEnabled: nil, clampSeekToBuffered: nil,
|
|
15
|
+
skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil,
|
|
16
|
+
lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil,
|
|
17
|
+
backgroundProgressUpdateIntervalMs: nil
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static func track(id: String, url: String) -> TrackItem {
|
|
22
|
+
return TrackItem(
|
|
23
|
+
id: id, url: url, title: nil, artist: nil, album: nil,
|
|
24
|
+
duration: nil, artworkUrl: nil, extras: nil,
|
|
25
|
+
replayGainTrackGain: nil, replayGainTrackPeak: nil,
|
|
26
|
+
replayGainAlbumGain: nil, replayGainAlbumPeak: nil
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Queue entries backed by a bundled local asset, so the queue seats real
|
|
31
|
+
/// playable items without any network fetch or prefetch scheduling.
|
|
32
|
+
static func localTracks(count: Int, for testCase: XCTestCase) throws -> [TrackItem] {
|
|
33
|
+
let url = try fixtureURL(name: "silence-aac-128", ext: "m4a", for: testCase)
|
|
34
|
+
return (0 ..< count).map { track(id: "t\($0)", url: url.absoluteString) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// A file under `Tests/Fixtures`, bundled through `test_spec.resources`.
|
|
38
|
+
/// A miss is a tests-harness `pod install` regression.
|
|
39
|
+
static func fixtureURL(name: String, ext: String, for testCase: XCTestCase) throws -> URL {
|
|
40
|
+
let bundle = Bundle(for: type(of: testCase))
|
|
41
|
+
return try XCTUnwrap(
|
|
42
|
+
bundle.url(forResource: name, withExtension: ext)
|
|
43
|
+
?? bundle.urls(forResourcesWithExtension: ext, subdirectory: nil)?
|
|
44
|
+
.first(where: { $0.deletingPathExtension().lastPathComponent == name }),
|
|
45
|
+
"fixture \(name).\(ext) not bundled — pod install regression in tests-harness"
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -16,7 +16,7 @@ final class PlayerStateDerivationTests: XCTestCase {
|
|
|
16
16
|
status: status, currentIndex: index, hasCurrentItem: hasItem, reachedQueueEnd: ended)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
// MARK: baseline (
|
|
19
|
+
// MARK: baseline (queue has not drained)
|
|
20
20
|
|
|
21
21
|
func testPlayingIsPlaying() {
|
|
22
22
|
XCTAssertEqual(derive(.playing), .playing)
|
|
@@ -40,14 +40,14 @@ final class PlayerStateDerivationTests: XCTestCase {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
func testWaitingWithoutItemNonEmptyQueueIsEndedFallback() {
|
|
43
|
-
// A drained AVQueuePlayer parks at .waitingToPlay with no item before
|
|
44
|
-
//
|
|
43
|
+
// A drained AVQueuePlayer parks at .waitingToPlay with no item before
|
|
44
|
+
// `reachedQueueEnd` is set — the end-of-queue fallback covers the timing race.
|
|
45
45
|
XCTAssertEqual(derive(.waitingToPlayAtSpecifiedRate, index: 5, hasItem: false), .ended)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// MARK: reachedQueueEnd
|
|
48
|
+
// MARK: reachedQueueEnd
|
|
49
49
|
|
|
50
|
-
func
|
|
50
|
+
func testReachedQueueEndWithTrackNotPlayingIsEnded() {
|
|
51
51
|
// Any non-playing status at end-of-queue is terminal — the same result for
|
|
52
52
|
// both engines (crossfade keeps its finished item, so hasItem is true).
|
|
53
53
|
XCTAssertEqual(derive(.paused, index: 5, ended: true), .ended)
|
|
@@ -55,14 +55,14 @@ final class PlayerStateDerivationTests: XCTestCase {
|
|
|
55
55
|
derive(.waitingToPlayAtSpecifiedRate, index: 5, hasItem: true, ended: true), .ended)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
func
|
|
59
|
-
// Playing takes precedence over
|
|
60
|
-
// before
|
|
58
|
+
func testReachedQueueEndYieldsToPlaying() {
|
|
59
|
+
// Playing takes precedence over `reachedQueueEnd`, so a resume reads
|
|
60
|
+
// correctly even before it is cleared.
|
|
61
61
|
XCTAssertEqual(derive(.playing, index: 5, ended: true), .playing)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
func
|
|
65
|
-
// A cleared queue (index < 0)
|
|
64
|
+
func testReachedQueueEndIgnoredWhenQueueEmpty() {
|
|
65
|
+
// A cleared queue (index < 0) ignores `reachedQueueEnd` and derives normally.
|
|
66
66
|
XCTAssertEqual(derive(.paused, index: -1, ended: true), .none)
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
|
|
4
|
+
/// Receiver work runs in the order the mutations were made, and work that
|
|
5
|
+
/// outlives its mutation does not act on a queue that has since been replaced.
|
|
6
|
+
final class QueueMutationGenerationTests: XCTestCase {
|
|
7
|
+
|
|
8
|
+
private var player: TrackPlayer!
|
|
9
|
+
|
|
10
|
+
override func setUp() async throws {
|
|
11
|
+
try await super.setUp()
|
|
12
|
+
player = TrackPlayer()
|
|
13
|
+
_ = try await player.configure(config: PlayerFixtures.config()).await()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override func tearDown() async throws {
|
|
17
|
+
if let player {
|
|
18
|
+
_ = try await player.destroy().await()
|
|
19
|
+
}
|
|
20
|
+
player = nil
|
|
21
|
+
try await super.tearDown()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// MARK: - Mirror ordering
|
|
25
|
+
|
|
26
|
+
/// `addToQueue`, `removeFromQueue` and `moveInQueue` send index deltas
|
|
27
|
+
/// resolved against the queue as it stood before their own mutation, so the
|
|
28
|
+
/// receiver has to see them in that order or it diverges.
|
|
29
|
+
func testMirrorsRunInTheOrderTheMutationsWereMade() async throws {
|
|
30
|
+
let recorded = Recorder()
|
|
31
|
+
|
|
32
|
+
for step in 0 ..< 5 {
|
|
33
|
+
_ = player.enqueueThenMirror({ step }, mirror: { value, _ in
|
|
34
|
+
await recorded.append(value)
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
let last = player.enqueueThenMirror({ 5 }, mirror: { value, _ in
|
|
38
|
+
await recorded.append(value)
|
|
39
|
+
})
|
|
40
|
+
_ = try await last.await()
|
|
41
|
+
|
|
42
|
+
let order = await recorded.values
|
|
43
|
+
XCTAssertEqual(order, [0, 1, 2, 3, 4, 5])
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// A slow round-trip holds the ones behind it rather than letting them
|
|
47
|
+
/// overtake — the whole reason the chain exists.
|
|
48
|
+
func testASlowMirrorDoesNotLetTheNextOneOvertakeIt() async throws {
|
|
49
|
+
let recorded = Recorder()
|
|
50
|
+
|
|
51
|
+
_ = player.enqueueThenMirror({ 0 }, mirror: { value, _ in
|
|
52
|
+
try? await Task.sleep(nanoseconds: 120_000_000)
|
|
53
|
+
await recorded.append(value)
|
|
54
|
+
})
|
|
55
|
+
let second = player.enqueueThenMirror({ 1 }, mirror: { value, _ in
|
|
56
|
+
await recorded.append(value)
|
|
57
|
+
})
|
|
58
|
+
_ = try await second.await()
|
|
59
|
+
|
|
60
|
+
let order = await recorded.values
|
|
61
|
+
XCTAssertEqual(order, [0, 1], "the slow mirror must still land first")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// The promise resolves only once that call's own receiver work has run, so
|
|
65
|
+
/// awaiting a mutation means local and remote are both settled.
|
|
66
|
+
func testTheCallResolvesOnlyAfterItsOwnMirrorHasRun() async throws {
|
|
67
|
+
let recorded = Recorder()
|
|
68
|
+
|
|
69
|
+
_ = try await player.enqueueThenMirror({ 0 }, mirror: { value, _ in
|
|
70
|
+
await recorded.append(value)
|
|
71
|
+
}).await()
|
|
72
|
+
|
|
73
|
+
let order = await recorded.values
|
|
74
|
+
XCTAssertEqual(order, [0])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// MARK: - Generation
|
|
78
|
+
|
|
79
|
+
func testEachQueueMutationTakesTheNextGeneration() async throws {
|
|
80
|
+
let start = try await read { $0.queueMutationGeneration }
|
|
81
|
+
|
|
82
|
+
_ = try await player.setQueue(tracks: try tracks(3), startAtIndex: 0).await()
|
|
83
|
+
let afterSet = try await read { $0.queueMutationGeneration }
|
|
84
|
+
_ = try await player.addToQueue(tracks: try tracks(1), insertBefore: nil).await()
|
|
85
|
+
let afterAdd = try await read { $0.queueMutationGeneration }
|
|
86
|
+
|
|
87
|
+
XCTAssertEqual(afterSet, start &+ 1)
|
|
88
|
+
XCTAssertEqual(afterAdd, start &+ 2)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// The generation a mirror captured is what tells it whether the queue it
|
|
92
|
+
/// was computed against is still the live one.
|
|
93
|
+
func testAMirrorSeesItsOwnGenerationSupersededByALaterMutation() async throws {
|
|
94
|
+
let captured = Recorder()
|
|
95
|
+
_ = player.enqueueThenMirror({ 0 }, mirror: { _, generation in
|
|
96
|
+
await captured.append(Int(generation))
|
|
97
|
+
})
|
|
98
|
+
_ = try await player.setQueue(tracks: try tracks(2), startAtIndex: 0).await()
|
|
99
|
+
|
|
100
|
+
let capturedValues = await captured.values
|
|
101
|
+
let mine = try XCTUnwrap(capturedValues.first)
|
|
102
|
+
let live = try await read { Int($0.queueMutationGeneration) }
|
|
103
|
+
XCTAssertGreaterThan(
|
|
104
|
+
live, mine, "a later mutation must leave the earlier generation behind")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// A burst leaves the queue the last call asked for, and the ids stay
|
|
108
|
+
/// parallel to it.
|
|
109
|
+
func testABurstLeavesTheQueueTheLastCallAskedFor() async throws {
|
|
110
|
+
let start = try await read { $0.queueMutationGeneration }
|
|
111
|
+
|
|
112
|
+
_ = try player.setQueue(tracks: try tracks(3), startAtIndex: 0)
|
|
113
|
+
_ = try player.setQueue(tracks: try tracks(5), startAtIndex: 0)
|
|
114
|
+
let last = try player.setQueue(tracks: try tracks(2), startAtIndex: 0)
|
|
115
|
+
_ = try await last.await()
|
|
116
|
+
|
|
117
|
+
let state = try await read {
|
|
118
|
+
(count: $0.tracks.count, ids: $0.queueItemIds.count,
|
|
119
|
+
generation: $0.queueMutationGeneration)
|
|
120
|
+
}
|
|
121
|
+
XCTAssertEqual(state.count, 2, "the last call in the burst is the one that stands")
|
|
122
|
+
XCTAssertEqual(state.ids, state.count, "ids stay parallel to tracks through a burst")
|
|
123
|
+
XCTAssertEqual(state.generation, start &+ 3)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// MARK: - Helpers
|
|
127
|
+
|
|
128
|
+
private actor Recorder {
|
|
129
|
+
private(set) var values: [Int] = []
|
|
130
|
+
func append(_ value: Int) { values.append(value) }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func tracks(_ count: Int) throws -> [TrackItem] {
|
|
134
|
+
try PlayerFixtures.localTracks(count: count, for: self)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private func read<T>(_ body: @escaping (TrackPlayer) -> T) async throws -> T {
|
|
138
|
+
let player = try XCTUnwrap(self.player)
|
|
139
|
+
return await player.onPlayerQueueValue { body(player) }
|
|
140
|
+
}
|
|
141
|
+
}
|