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,14 @@
|
|
|
1
|
+
@testable import QueuePlayer
|
|
2
|
+
|
|
3
|
+
extension TrackPlayer {
|
|
4
|
+
/// Runs `body` on `playerQueue` and hands its value back — the read side of
|
|
5
|
+
/// the tests' player-state assertions. The library has no async getter that
|
|
6
|
+
/// needs this; its own hops are `onPlayerQueue` and `onPlayerQueueSync`.
|
|
7
|
+
func onPlayerQueueValue<T>(_ body: @escaping () -> T) async -> T {
|
|
8
|
+
await withCheckedContinuation { (cont: CheckedContinuation<T, Never>) in
|
|
9
|
+
playerQueue.async {
|
|
10
|
+
cont.resume(returning: body())
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
|
|
4
|
+
/// Queue mutations reach the player in the order JS called them.
|
|
5
|
+
///
|
|
6
|
+
/// Each method is issued and a probe is enqueued immediately afterwards, the
|
|
7
|
+
/// way a transport call issued on the next line would be. The probe runs on
|
|
8
|
+
/// `playerQueue` behind whatever the method enqueued, so what it observes is
|
|
9
|
+
/// what that next call would act on.
|
|
10
|
+
final class TrackPlayerCallOrderTests: XCTestCase {
|
|
11
|
+
|
|
12
|
+
private var player: TrackPlayer!
|
|
13
|
+
|
|
14
|
+
override func setUp() async throws {
|
|
15
|
+
try await super.setUp()
|
|
16
|
+
player = TrackPlayer()
|
|
17
|
+
_ = try await player.configure(config: PlayerFixtures.config()).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 testSetQueueIsVisibleToWorkEnqueuedAfterIt() async throws {
|
|
29
|
+
let tracks = try PlayerFixtures.localTracks(count: 3, for: self)
|
|
30
|
+
|
|
31
|
+
_ = try player.setQueue(tracks: tracks, startAtIndex: 1)
|
|
32
|
+
let seen = try await probe { (count: $0.tracks.count, index: $0.currentTrackIndex) }
|
|
33
|
+
|
|
34
|
+
XCTAssertEqual(seen.count, 3)
|
|
35
|
+
XCTAssertEqual(seen.index, 1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func testAddToQueueIsVisibleToWorkEnqueuedAfterIt() async throws {
|
|
39
|
+
_ = try await player.setQueue(tracks: PlayerFixtures.localTracks(count: 3, for: self), startAtIndex: 0).await()
|
|
40
|
+
|
|
41
|
+
_ = try player.addToQueue(tracks: PlayerFixtures.localTracks(count: 2, for: self), insertBefore: nil)
|
|
42
|
+
let count = try await probe { $0.tracks.count }
|
|
43
|
+
|
|
44
|
+
XCTAssertEqual(count, 5)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// The immediate-next slot is the insert `AVQueuePlayer` prerolls, so it is
|
|
48
|
+
/// the one whose commit is most at risk of being deferred.
|
|
49
|
+
func testAnImmediateNextInsertIsVisibleToWorkEnqueuedAfterIt() async throws {
|
|
50
|
+
_ = try await player.setQueue(tracks: PlayerFixtures.localTracks(count: 3, for: self), startAtIndex: 0).await()
|
|
51
|
+
|
|
52
|
+
_ = try player.addToQueue(tracks: PlayerFixtures.localTracks(count: 1, for: self), insertBefore: 1)
|
|
53
|
+
let count = try await probe { $0.tracks.count }
|
|
54
|
+
|
|
55
|
+
XCTAssertEqual(count, 4)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func testRemoveFromQueueIsVisibleToWorkEnqueuedAfterIt() async throws {
|
|
59
|
+
_ = try await player.setQueue(tracks: PlayerFixtures.localTracks(count: 4, for: self), startAtIndex: 0).await()
|
|
60
|
+
|
|
61
|
+
_ = try player.removeFromQueue(indices: [2, 3])
|
|
62
|
+
let count = try await probe { $0.tracks.count }
|
|
63
|
+
|
|
64
|
+
XCTAssertEqual(count, 2)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func testMoveInQueueIsVisibleToWorkEnqueuedAfterIt() async throws {
|
|
68
|
+
_ = try await player.setQueue(tracks: PlayerFixtures.localTracks(count: 3, for: self), startAtIndex: 0).await()
|
|
69
|
+
|
|
70
|
+
_ = try player.moveInQueue(fromIndex: 2, toIndex: 1)
|
|
71
|
+
let idAtOne = try await probe { $0.tracks[1].id }
|
|
72
|
+
|
|
73
|
+
XCTAssertEqual(idAtOne, "t2")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// A start index of NaN reaches `Int(_:)` unless it is guarded in `Double`
|
|
77
|
+
/// space, where the conversion is undefined behaviour.
|
|
78
|
+
func testANonFiniteStartIndexResolvesToTheQueueHead() async throws {
|
|
79
|
+
_ = try player.setQueue(tracks: PlayerFixtures.localTracks(count: 3, for: self), startAtIndex: Double.nan)
|
|
80
|
+
let index = try await probe { $0.currentTrackIndex }
|
|
81
|
+
|
|
82
|
+
XCTAssertEqual(index, 0)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// MARK: - Helpers
|
|
86
|
+
|
|
87
|
+
/// Enqueue a read behind whatever the call under test just enqueued.
|
|
88
|
+
private func probe<T>(_ body: @escaping (TrackPlayer) -> T) async throws -> T {
|
|
89
|
+
let player = try XCTUnwrap(self.player)
|
|
90
|
+
return await player.onPlayerQueueValue { body(player) }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
|
|
4
|
+
/// `configure()` is a clean-slate operation: it releases whichever engine is
|
|
5
|
+
/// live and clears the queue model before building the fresh one. The
|
|
6
|
+
/// "is anything configured?" gate keys on the engine, which both engines
|
|
7
|
+
/// populate — `player` holds the gapless `AVQueuePlayer` and is nil for the
|
|
8
|
+
/// whole time a `CrossfadeEngine` is active, so it cannot answer that
|
|
9
|
+
/// question.
|
|
10
|
+
final class TrackPlayerConfigureTeardownTests: XCTestCase {
|
|
11
|
+
|
|
12
|
+
private var player: TrackPlayer!
|
|
13
|
+
|
|
14
|
+
override func setUp() async throws {
|
|
15
|
+
try await super.setUp()
|
|
16
|
+
player = TrackPlayer()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override func tearDown() async throws {
|
|
20
|
+
if let player {
|
|
21
|
+
_ = try await player.destroy().await()
|
|
22
|
+
}
|
|
23
|
+
player = nil
|
|
24
|
+
try await super.tearDown()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func testReconfigureClearsQueueUnderGapless() async throws {
|
|
28
|
+
try await assertReconfigureIsCleanSlate(
|
|
29
|
+
mode: PlaybackMode(kind: .gapless, crossfadeDurationMs: nil),
|
|
30
|
+
label: "gapless"
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func testReconfigureClearsQueueUnderCrossfade() async throws {
|
|
35
|
+
try await assertReconfigureIsCleanSlate(
|
|
36
|
+
mode: PlaybackMode(kind: .crossfade, crossfadeDurationMs: 3000),
|
|
37
|
+
label: "crossfade"
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Configure, install [mode], load a queue, then configure again without an
|
|
42
|
+
/// intervening `destroy()`. The second configure must have torn the prior
|
|
43
|
+
/// engine down: queue empty, current index reset.
|
|
44
|
+
private func assertReconfigureIsCleanSlate(
|
|
45
|
+
mode: PlaybackMode, label: String
|
|
46
|
+
) async throws {
|
|
47
|
+
_ = try await player.configure(config: PlayerFixtures.config()).await()
|
|
48
|
+
_ = try await player.setPlaybackMode(mode: mode).await()
|
|
49
|
+
let tracks = try PlayerFixtures.localTracks(count: 3, for: self)
|
|
50
|
+
_ = try await player.setQueue(tracks: tracks, startAtIndex: 0).await()
|
|
51
|
+
|
|
52
|
+
let seeded = try await player.getQueue().await()
|
|
53
|
+
XCTAssertEqual(
|
|
54
|
+
seeded.count, tracks.count,
|
|
55
|
+
"\(label): expected the seeded queue before the re-configure")
|
|
56
|
+
|
|
57
|
+
_ = try await player.configure(config: PlayerFixtures.config()).await()
|
|
58
|
+
|
|
59
|
+
let cleared = try await player.getQueue().await()
|
|
60
|
+
XCTAssertEqual(
|
|
61
|
+
cleared.count, 0,
|
|
62
|
+
"\(label): configure() must clear the queue, got \(cleared.count) entries")
|
|
63
|
+
XCTAssertEqual(
|
|
64
|
+
try player.getCurrentTrackIndex(), -1,
|
|
65
|
+
"\(label): configure() must reset the current index")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// The re-configure clears the queue without an emit; the revision the next
|
|
69
|
+
/// emit carries skips one, which is the signal a delta mirror re-reads on.
|
|
70
|
+
func testReconfigureLeavesARevisionGapForTheNextEmit() async throws {
|
|
71
|
+
_ = try await player.configure(config: PlayerFixtures.config()).await()
|
|
72
|
+
var revisions: [Int] = []
|
|
73
|
+
let unsubscribe = try player.onQueueChange { delta, _, _ in
|
|
74
|
+
revisions.append(Int(delta.revision))
|
|
75
|
+
}
|
|
76
|
+
defer { unsubscribe() }
|
|
77
|
+
_ = try await player.setQueue(tracks: PlayerFixtures.localTracks(count: 2, for: self), startAtIndex: 0).await()
|
|
78
|
+
let before = try XCTUnwrap(revisions.last)
|
|
79
|
+
|
|
80
|
+
_ = try await player.configure(config: PlayerFixtures.config()).await()
|
|
81
|
+
_ = try await player.setQueue(tracks: PlayerFixtures.localTracks(count: 1, for: self), startAtIndex: 0).await()
|
|
82
|
+
|
|
83
|
+
XCTAssertEqual(revisions.last, before + 2)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
|
|
4
|
+
/// A drained engine reaches the same end-of-queue verdict whichever engine is
|
|
5
|
+
/// installed. The engines report that they drained; the queue above them
|
|
6
|
+
/// decides what that means.
|
|
7
|
+
final class TrackPlayerEndVerdictTests: 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
|
+
}
|
|
16
|
+
|
|
17
|
+
override func tearDown() async throws {
|
|
18
|
+
if let player {
|
|
19
|
+
_ = try await player.destroy().await()
|
|
20
|
+
}
|
|
21
|
+
player = nil
|
|
22
|
+
try await super.tearDown()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// MARK: - The wrap case
|
|
26
|
+
|
|
27
|
+
func testRepeatQueueWrapsToTheHeadFromTheTail() async throws {
|
|
28
|
+
try await seedQueue(count: 3, at: 2, repeatMode: .queue)
|
|
29
|
+
|
|
30
|
+
await onQueue { $0.applyEndVerdict() }
|
|
31
|
+
|
|
32
|
+
let state = try await read { (index: $0.currentTrackIndex, ended: $0.reachedQueueEnd) }
|
|
33
|
+
XCTAssertEqual(state.index, 0)
|
|
34
|
+
XCTAssertFalse(state.ended, "a wrap is not the end of the queue")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// A single-track queue is already at position 0, so only the re-seat marks
|
|
38
|
+
/// the wrap: the dedup key is cleared so the repeated track still emits.
|
|
39
|
+
func testRepeatQueueReSeatsASingleTrackQueue() async throws {
|
|
40
|
+
try await seedQueue(count: 1, at: 0, repeatMode: .queue)
|
|
41
|
+
await onQueue { $0.lastEmittedQueueItemId = "stale" }
|
|
42
|
+
|
|
43
|
+
await onQueue { $0.applyEndVerdict() }
|
|
44
|
+
|
|
45
|
+
let state = try await read {
|
|
46
|
+
(ended: $0.reachedQueueEnd, emitted: $0.lastEmittedQueueItemId,
|
|
47
|
+
head: $0.queueItemIds.first)
|
|
48
|
+
}
|
|
49
|
+
XCTAssertFalse(state.ended)
|
|
50
|
+
XCTAssertEqual(
|
|
51
|
+
state.emitted, state.head,
|
|
52
|
+
"clearing the dedup key lets the repeated track emit again")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Under crossfade there is no `AVQueuePlayer`, and the verdict must not
|
|
56
|
+
/// depend on one being there.
|
|
57
|
+
func testRepeatQueueWrapsUnderCrossfadeToo() async throws {
|
|
58
|
+
try await installCrossfade()
|
|
59
|
+
try await seedQueue(count: 3, at: 2, repeatMode: .queue)
|
|
60
|
+
let queuePlayer = try await read { $0.player }
|
|
61
|
+
XCTAssertNil(queuePlayer, "precondition: no gapless queue player")
|
|
62
|
+
|
|
63
|
+
await onQueue { $0.applyEndVerdict() }
|
|
64
|
+
|
|
65
|
+
let state = try await read { (index: $0.currentTrackIndex, ended: $0.reachedQueueEnd) }
|
|
66
|
+
XCTAssertEqual(state.index, 0)
|
|
67
|
+
XCTAssertFalse(state.ended, "a wrap is not the end of the queue on either engine")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func testRepeatOffEndsTheQueueAtTheTail() async throws {
|
|
71
|
+
try await seedQueue(count: 3, at: 2, repeatMode: .off)
|
|
72
|
+
|
|
73
|
+
await onQueue { $0.applyEndVerdict() }
|
|
74
|
+
|
|
75
|
+
let ended = try await read { $0.reachedQueueEnd }
|
|
76
|
+
XCTAssertTrue(ended)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MARK: - The engine's own signal reaches the verdict
|
|
80
|
+
|
|
81
|
+
/// The crossfade engine reports a drain through the delegate, which is the
|
|
82
|
+
/// route that has to reach the same decision.
|
|
83
|
+
func testTheEngineDelegateEndSignalWrapsTheQueue() async throws {
|
|
84
|
+
try await installCrossfade()
|
|
85
|
+
try await seedQueue(count: 3, at: 2, repeatMode: .queue)
|
|
86
|
+
let engine = try await read { $0.engine }
|
|
87
|
+
let reporting = try XCTUnwrap(engine)
|
|
88
|
+
|
|
89
|
+
await onQueue { $0.enginePlaybackEnded(reporting) }
|
|
90
|
+
|
|
91
|
+
let state = try await read { (index: $0.currentTrackIndex, ended: $0.reachedQueueEnd) }
|
|
92
|
+
XCTAssertEqual(state.index, 0)
|
|
93
|
+
XCTAssertFalse(state.ended)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// MARK: - A tail that cannot be built
|
|
97
|
+
|
|
98
|
+
/// A track whose URL cannot be built is dropped from the engine's items, so
|
|
99
|
+
/// the engine drains before the logical tail. Re-seating that position
|
|
100
|
+
/// installs nothing and fires no further engine signal, so the decision has
|
|
101
|
+
/// to be retaken rather than left parked mid-queue.
|
|
102
|
+
func testAnUnbuildableTailStillEndsTheQueue() async throws {
|
|
103
|
+
try await installCrossfade()
|
|
104
|
+
try await seedQueue(count: 3, at: 1, repeatMode: .off, lastTrackURL: "")
|
|
105
|
+
|
|
106
|
+
await onQueue { $0.applyEndVerdict() }
|
|
107
|
+
|
|
108
|
+
let ended = try await read { $0.reachedQueueEnd }
|
|
109
|
+
XCTAssertTrue(ended, "an unplayable last track must still end the queue")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Repeat-track re-seats the same position, so when that position cannot
|
|
113
|
+
/// be built there is nothing to refill: the queue ends rather than being
|
|
114
|
+
/// re-decided on the same anchor again.
|
|
115
|
+
func testAnUnbuildableCurrentTrackUnderRepeatTrackEndsTheQueue() async throws {
|
|
116
|
+
try await installCrossfade()
|
|
117
|
+
try await seedQueue(count: 3, at: 2, repeatMode: .track, lastTrackURL: "")
|
|
118
|
+
|
|
119
|
+
await onQueue { $0.applyEndVerdict() }
|
|
120
|
+
|
|
121
|
+
let state = try await read { (index: $0.currentTrackIndex, ended: $0.reachedQueueEnd) }
|
|
122
|
+
XCTAssertEqual(state.index, 2)
|
|
123
|
+
XCTAssertTrue(state.ended, "an unplayable repeated track has nothing to refill")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func testAnUnbuildableTailStillWrapsUnderRepeatQueue() async throws {
|
|
127
|
+
try await installCrossfade()
|
|
128
|
+
try await seedQueue(count: 3, at: 1, repeatMode: .queue, lastTrackURL: "")
|
|
129
|
+
|
|
130
|
+
await onQueue { $0.applyEndVerdict() }
|
|
131
|
+
|
|
132
|
+
let state = try await read { (index: $0.currentTrackIndex, ended: $0.reachedQueueEnd) }
|
|
133
|
+
XCTAssertEqual(state.index, 0)
|
|
134
|
+
XCTAssertFalse(state.ended)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// MARK: - A drain that is really a failure
|
|
138
|
+
|
|
139
|
+
/// A drain that follows the current track's reported failure stays where it
|
|
140
|
+
/// is rather than wrapping.
|
|
141
|
+
func testADrainAfterTheCurrentTrackFailedDoesNotAdvanceTheQueue() async throws {
|
|
142
|
+
try await seedQueue(count: 3, at: 2, repeatMode: .queue)
|
|
143
|
+
await onQueue { $0.reportedFailureQueueItemIds = [$0.queueItemIds[2]] }
|
|
144
|
+
|
|
145
|
+
await onQueue { $0.applyEndVerdict() }
|
|
146
|
+
|
|
147
|
+
let state = try await read { (index: $0.currentTrackIndex, ended: $0.reachedQueueEnd) }
|
|
148
|
+
XCTAssertEqual(state.index, 2, "the queue stays on the track whose failure was reported")
|
|
149
|
+
XCTAssertFalse(state.ended, "a reported failure is not the end of the queue either")
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// The crossfade engine's own end signal reaches the same decision.
|
|
153
|
+
func testAnEngineEndReportAfterTheCurrentTrackFailedDoesNotAdvanceTheQueue() async throws {
|
|
154
|
+
try await installCrossfade()
|
|
155
|
+
try await seedQueue(count: 3, at: 2, repeatMode: .queue)
|
|
156
|
+
await onQueue { $0.reportedFailureQueueItemIds = [$0.queueItemIds[2]] }
|
|
157
|
+
let engine = try await read { $0.engine }
|
|
158
|
+
let reporting = try XCTUnwrap(engine)
|
|
159
|
+
|
|
160
|
+
await onQueue { $0.enginePlaybackEnded(reporting) }
|
|
161
|
+
|
|
162
|
+
let index = try await read { $0.currentTrackIndex }
|
|
163
|
+
XCTAssertEqual(index, 2)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// A report against a track the playhead has left does not hold a genuine
|
|
167
|
+
/// end of queue open.
|
|
168
|
+
func testADrainAfterADifferentTrackFailedStillEndsTheQueue() async throws {
|
|
169
|
+
try await seedQueue(count: 3, at: 2, repeatMode: .off)
|
|
170
|
+
await onQueue { $0.reportedFailureQueueItemIds = [$0.queueItemIds[0]] }
|
|
171
|
+
|
|
172
|
+
await onQueue { $0.applyEndVerdict() }
|
|
173
|
+
|
|
174
|
+
let ended = try await read { $0.reachedQueueEnd }
|
|
175
|
+
XCTAssertTrue(ended)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// MARK: - Helpers
|
|
179
|
+
|
|
180
|
+
private func installCrossfade() async throws {
|
|
181
|
+
_ = try await player.setPlaybackMode(
|
|
182
|
+
mode: PlaybackMode(kind: .crossfade, crossfadeDurationMs: 3000)).await()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private func seedQueue(
|
|
186
|
+
count: Int, at index: Int, repeatMode: RepeatMode, lastTrackURL: String? = nil
|
|
187
|
+
) async throws {
|
|
188
|
+
var tracks = try PlayerFixtures.localTracks(count: count, for: self)
|
|
189
|
+
if let lastTrackURL {
|
|
190
|
+
tracks[count - 1] = PlayerFixtures.track(id: "unbuildable", url: lastTrackURL)
|
|
191
|
+
}
|
|
192
|
+
_ = try await player.setQueue(tracks: tracks, startAtIndex: 0).await()
|
|
193
|
+
_ = try await player.setRepeatMode(mode: repeatMode).await()
|
|
194
|
+
await onQueue { $0.currentTrackIndex = index }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private func onQueue(_ body: @escaping (TrackPlayer) -> Void) async {
|
|
198
|
+
guard let player else { return }
|
|
199
|
+
await player.onPlayerQueue { body(player) }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private func read<T>(_ body: @escaping (TrackPlayer) -> T) async throws -> T {
|
|
203
|
+
let player = try XCTUnwrap(self.player)
|
|
204
|
+
return await player.onPlayerQueueValue { body(player) }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import MediaPlayer
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import XCTest
|
|
4
|
+
@testable import QueuePlayer
|
|
5
|
+
|
|
6
|
+
/// `seekTo` resolves on every branch, including the ones that complete without
|
|
7
|
+
/// issuing a seek — a branch that returned without resuming would strand the
|
|
8
|
+
/// calling JS Promise for the life of the process.
|
|
9
|
+
final class TrackPlayerSeekTests: XCTestCase {
|
|
10
|
+
|
|
11
|
+
/// A seek against a live engine writes the process-global
|
|
12
|
+
/// `MPNowPlayingInfoCenter` on main. Drain that write and clear the centre
|
|
13
|
+
/// so it does not reach the next test class.
|
|
14
|
+
override func tearDown() {
|
|
15
|
+
let drained = expectation(description: "pending main-queue now-playing writes complete")
|
|
16
|
+
DispatchQueue.main.async { drained.fulfill() }
|
|
17
|
+
wait(for: [drained], timeout: 5.0)
|
|
18
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
|
19
|
+
super.tearDown()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func testSeekResolvesWithNoEngineAttached() {
|
|
23
|
+
let player = TrackPlayer()
|
|
24
|
+
expectSeekToResolve(player, position: 10, "no engine and no queue player")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func testSeekResolvesOnADrainedQueuePlayer() {
|
|
28
|
+
let player = TrackPlayer()
|
|
29
|
+
player.player = AVQueuePlayer()
|
|
30
|
+
expectSeekToResolve(player, position: 10, "a queue player with no current item")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// With no queue there is nothing to reinstall, so a seek on a drained
|
|
34
|
+
/// player resolves without changing the end-of-queue state.
|
|
35
|
+
func testSeekOnADrainedQueuePlayerWithNoQueueLeavesTheEndOfQueueStateSet() {
|
|
36
|
+
let player = TrackPlayer()
|
|
37
|
+
player.player = AVQueuePlayer()
|
|
38
|
+
player.reachedQueueEnd = true
|
|
39
|
+
|
|
40
|
+
expectSeekToResolve(player, position: 10, "a drained player at the end of the queue")
|
|
41
|
+
|
|
42
|
+
XCTAssertTrue(
|
|
43
|
+
player.reachedQueueEnd,
|
|
44
|
+
"a seek that never reached the player must not report the queue as replaying")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func testSeekResolvesForAPositionBelowZero() {
|
|
48
|
+
let player = TrackPlayer()
|
|
49
|
+
expectSeekToResolve(player, position: -5, "a negative position")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// A crossfade engine that does not hold the current track has it
|
|
53
|
+
/// reinstalled, and the seek is then issued against the reinstalled run.
|
|
54
|
+
func testSeekReinstallsAnAbsentCurrentTrackAndThenSeeksIt() {
|
|
55
|
+
let player = TrackPlayer()
|
|
56
|
+
let engine = RecordingEngineStub()
|
|
57
|
+
_ = player.queueState.replaceAll((0 ..< 2).map {
|
|
58
|
+
PlayerFixtures.track(id: "t\($0)", url: "http://127.0.0.1:1/\($0).m4a")
|
|
59
|
+
})
|
|
60
|
+
player.currentTrackIndex = 1
|
|
61
|
+
player.engine = engine
|
|
62
|
+
|
|
63
|
+
expectSeekToResolve(player, position: 30, "a current track the engine does not hold")
|
|
64
|
+
|
|
65
|
+
XCTAssertTrue(
|
|
66
|
+
engine.installedIds.contains(player.queueItemIds[1]),
|
|
67
|
+
"the reinstalled run holds the current track")
|
|
68
|
+
let seek = try? XCTUnwrap(engine.seeks.first)
|
|
69
|
+
XCTAssertEqual(
|
|
70
|
+
engine.allMediaItems[safe: seek?.index ?? -1]?.queueItemId, player.queueItemIds[1],
|
|
71
|
+
"the seek targets the current track's position in the reinstalled run")
|
|
72
|
+
XCTAssertEqual(seek?.position, 30)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Drives `seekTo` to completion and fails if it has not resolved within the
|
|
76
|
+
/// timeout, rather than hanging the suite on a stranded continuation.
|
|
77
|
+
private func expectSeekToResolve(
|
|
78
|
+
_ player: TrackPlayer, position: Double, _ branch: String,
|
|
79
|
+
file: StaticString = #filePath, line: UInt = #line
|
|
80
|
+
) {
|
|
81
|
+
let resolved = expectation(description: "seekTo resolves on \(branch)")
|
|
82
|
+
Task {
|
|
83
|
+
do {
|
|
84
|
+
_ = try await player.seekTo(position: position).await()
|
|
85
|
+
resolved.fulfill()
|
|
86
|
+
} catch {
|
|
87
|
+
XCTFail("seekTo rejected on \(branch): \(error)", file: file, line: line)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
wait(for: [resolved], timeout: 5.0)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Holds what it is given and records the seeks it is asked for.
|
|
95
|
+
private final class RecordingEngineStub: PlaybackEngine {
|
|
96
|
+
private(set) var seeks: [(index: Int, position: TimeInterval)] = []
|
|
97
|
+
private var installed: [AVPlayerItem] = []
|
|
98
|
+
var installedIds: [String] { installed.compactMap { $0.queueItemId } }
|
|
99
|
+
|
|
100
|
+
var currentMediaItem: AVPlayerItem? { nil }
|
|
101
|
+
var allMediaItems: [AVPlayerItem] { installed }
|
|
102
|
+
var attachedQueueItemIds: [String] { [] }
|
|
103
|
+
var isFadePendingOrActive: Bool { false }
|
|
104
|
+
var currentMediaItemIndex: Int { -1 }
|
|
105
|
+
var currentPositionSeconds: TimeInterval { 0 }
|
|
106
|
+
var currentDurationSeconds: TimeInterval { 0 }
|
|
107
|
+
var bufferedPositionSeconds: TimeInterval { 0 }
|
|
108
|
+
var isPlaying: Bool { false }
|
|
109
|
+
var timeControlStatus: AVPlayer.TimeControlStatus { .paused }
|
|
110
|
+
var delegate: PlaybackEngineDelegate?
|
|
111
|
+
|
|
112
|
+
func release() {}
|
|
113
|
+
func setItems(_ items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) {
|
|
114
|
+
installed = items
|
|
115
|
+
}
|
|
116
|
+
@discardableResult
|
|
117
|
+
func insertItems(_ items: [AVPlayerItem], after: AVPlayerItem?) -> Bool {
|
|
118
|
+
installed.append(contentsOf: items)
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
func remove(_ item: AVPlayerItem) { installed.removeAll { $0 === item } }
|
|
122
|
+
func removeAllItems() { installed.removeAll() }
|
|
123
|
+
func canInsert(_ item: AVPlayerItem, after: AVPlayerItem?) -> Bool { true }
|
|
124
|
+
func play() {}
|
|
125
|
+
func pause() {}
|
|
126
|
+
func stop() {}
|
|
127
|
+
func seek(toIndex index: Int, position: TimeInterval) { seeks.append((index, position)) }
|
|
128
|
+
func skipToNext() {}
|
|
129
|
+
func skipToPrevious() {}
|
|
130
|
+
func setVolume(_ volume: Float) {}
|
|
131
|
+
func setPlaybackSpeed(_ speed: Float) {}
|
|
132
|
+
func seedPlaybackSpeed(_ speed: Float) {}
|
|
133
|
+
func setRepeatMode(_ mode: RepeatMode) {}
|
|
134
|
+
func setPitchCorrectionMode(_ mode: PitchCorrectionMode) {}
|
|
135
|
+
func setAudioMixProvider(_ provider: AudioMixProvider?) {}
|
|
136
|
+
func refreshAllItemMixes() {}
|
|
137
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
|
|
4
|
+
/// `onPlayerQueueSync` serialises on `playerQueue`, and recognises when it is already
|
|
5
|
+
/// there. The recognition is what makes re-entrancy safe: a `sync` onto the
|
|
6
|
+
/// queue from a block already running on it deadlocks immediately.
|
|
7
|
+
final class TrackPlayerThreadingTests: XCTestCase {
|
|
8
|
+
|
|
9
|
+
func testSyncAccessorRunsInlineWhenAlreadyOnThePlayerQueue() {
|
|
10
|
+
let player = TrackPlayer()
|
|
11
|
+
let returned = expectation(description: "onPlayerQueueSync returns from the player queue")
|
|
12
|
+
var value = 0
|
|
13
|
+
|
|
14
|
+
player.playerQueue.async {
|
|
15
|
+
value = player.onPlayerQueueSync { 42 }
|
|
16
|
+
returned.fulfill()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
wait(for: [returned], timeout: 2.0)
|
|
20
|
+
XCTAssertEqual(value, 42)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func testSyncAccessorReturnsFromOffTheQueue() {
|
|
24
|
+
let player = TrackPlayer()
|
|
25
|
+
let returned = expectation(description: "onPlayerQueueSync returns from an off-queue caller")
|
|
26
|
+
var value = 0
|
|
27
|
+
|
|
28
|
+
DispatchQueue.global().async {
|
|
29
|
+
value = player.onPlayerQueueSync { 7 }
|
|
30
|
+
returned.fulfill()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
wait(for: [returned], timeout: 2.0)
|
|
34
|
+
XCTAssertEqual(value, 7)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func testNestedSyncAccessorsDoNotDeadlock() {
|
|
38
|
+
let player = TrackPlayer()
|
|
39
|
+
let returned = expectation(description: "a nested onPlayerQueueSync returns")
|
|
40
|
+
var value = 0
|
|
41
|
+
|
|
42
|
+
DispatchQueue.global().async {
|
|
43
|
+
value = player.onPlayerQueueSync { player.onPlayerQueueSync { 9 } }
|
|
44
|
+
returned.fulfill()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
wait(for: [returned], timeout: 2.0)
|
|
48
|
+
XCTAssertEqual(value, 9)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func testOnQueueRecognitionIsPerInstance() {
|
|
52
|
+
let first = TrackPlayer()
|
|
53
|
+
let second = TrackPlayer()
|
|
54
|
+
let checked = expectation(description: "recognition evaluated on first's queue")
|
|
55
|
+
var firstSeesItsOwnQueue = false
|
|
56
|
+
var secondSeesAnotherPlayersQueue = true
|
|
57
|
+
|
|
58
|
+
first.playerQueue.async {
|
|
59
|
+
firstSeesItsOwnQueue = first.isOnPlayerQueue
|
|
60
|
+
secondSeesAnotherPlayersQueue = second.isOnPlayerQueue
|
|
61
|
+
checked.fulfill()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
wait(for: [checked], timeout: 2.0)
|
|
65
|
+
XCTAssertTrue(firstSeesItsOwnQueue)
|
|
66
|
+
XCTAssertFalse(
|
|
67
|
+
secondSeesAnotherPlayersQueue,
|
|
68
|
+
"one player must not report being serialised by another player's queue")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// MARK: - Call-order preservation
|
|
72
|
+
|
|
73
|
+
/// Work enqueued from one thread runs in the order it was enqueued. This is
|
|
74
|
+
/// what makes a JS `play()` then `pause()` reach the player in that order:
|
|
75
|
+
/// a serial queue gives mutual exclusion, and enqueueing at call time is
|
|
76
|
+
/// what gives ordering on top of it.
|
|
77
|
+
func testEnqueuedWorkRunsInTheOrderItWasCalled() {
|
|
78
|
+
let player = TrackPlayer()
|
|
79
|
+
let lock = NSLock()
|
|
80
|
+
var order: [Int] = []
|
|
81
|
+
let ran = expectation(description: "every enqueued body ran")
|
|
82
|
+
ran.expectedFulfillmentCount = 50
|
|
83
|
+
|
|
84
|
+
for i in 0 ..< 50 {
|
|
85
|
+
_ = player.enqueue {
|
|
86
|
+
lock.lock()
|
|
87
|
+
order.append(i)
|
|
88
|
+
lock.unlock()
|
|
89
|
+
ran.fulfill()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
wait(for: [ran], timeout: 5.0)
|
|
94
|
+
XCTAssertEqual(order, Array(0 ..< 50))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func testEnqueueResolvesItsPromiseAfterTheBodyRuns() async throws {
|
|
98
|
+
let player = TrackPlayer()
|
|
99
|
+
var didRun = false
|
|
100
|
+
let promise = player.enqueue { didRun = true }
|
|
101
|
+
|
|
102
|
+
_ = try await promise.await()
|
|
103
|
+
|
|
104
|
+
XCTAssertTrue(didRun, "the promise must not resolve before the body has run")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func testEnqueueThrowingRejectsWithTheThrownError() async {
|
|
108
|
+
let player = TrackPlayer()
|
|
109
|
+
struct Boom: Error {}
|
|
110
|
+
do {
|
|
111
|
+
_ = try await player.enqueueThrowing { throw Boom() }.await()
|
|
112
|
+
XCTFail("expected the promise to reject")
|
|
113
|
+
} catch {
|
|
114
|
+
// The bridge surfaces the message; reaching here is the contract.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func testEnqueueValueCarriesItsResultBack() async throws {
|
|
119
|
+
let player = TrackPlayer()
|
|
120
|
+
player.currentTrackIndex = 7
|
|
121
|
+
|
|
122
|
+
let value = try await player.enqueueValue { player.currentTrackIndex }.await()
|
|
123
|
+
|
|
124
|
+
XCTAssertEqual(value, 7)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
}
|