react-native-queue-player 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/build.gradle +7 -0
- package/android/consumer-rules.pro +72 -3
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +26 -22
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +20 -72
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +11 -0
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +660 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +202 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +303 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +394 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +968 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +930 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +940 -0
- package/ios/TrackPlayer+Recovery.swift +196 -0
- package/ios/TrackPlayer+Skip.swift +455 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +267 -0
- package/ios/TrackPlayer+Window.swift +195 -0
- package/ios/TrackPlayer.swift +305 -4610
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// Playback runs with `automaticallyWaitsToMinimizeStalling = false`, so
|
|
6
|
+
/// AVPlayer does not resume itself after a buffer underrun. A stall arms a
|
|
7
|
+
/// recovery that re-issues play when the buffer refills, and transport intent
|
|
8
|
+
/// decides whether it arms at all.
|
|
9
|
+
///
|
|
10
|
+
/// The resume itself needs a real item whose buffer actually refills, so that
|
|
11
|
+
/// half is covered by the streaming demo suites; what is covered here is the
|
|
12
|
+
/// arming and clearing, which is where the intent bugs would be.
|
|
13
|
+
final class StallRecoveryTests: XCTestCase {
|
|
14
|
+
|
|
15
|
+
func testAStallArmsRecoveryWhilePlaybackIsIntended() {
|
|
16
|
+
let player = TrackPlayer()
|
|
17
|
+
let active = seedActiveItem(player)
|
|
18
|
+
player.wantsToPlay = true
|
|
19
|
+
|
|
20
|
+
player.handlePlaybackStalled(
|
|
21
|
+
Notification(name: AVPlayerItem.playbackStalledNotification, object: active))
|
|
22
|
+
drainPlayerQueue(player)
|
|
23
|
+
|
|
24
|
+
XCTAssertTrue(player.isRecoveringFromStall)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// The observer sees every item in the process; a stall on an item that
|
|
28
|
+
/// is not the one being heard is not this player's stall.
|
|
29
|
+
func testAStallOnAnotherItemDoesNotArmRecovery() {
|
|
30
|
+
let player = TrackPlayer()
|
|
31
|
+
_ = seedActiveItem(player)
|
|
32
|
+
player.wantsToPlay = true
|
|
33
|
+
let other = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/other.mp3"))
|
|
34
|
+
|
|
35
|
+
player.handlePlaybackStalled(
|
|
36
|
+
Notification(name: AVPlayerItem.playbackStalledNotification, object: other))
|
|
37
|
+
drainPlayerQueue(player)
|
|
38
|
+
|
|
39
|
+
XCTAssertFalse(player.isRecoveringFromStall)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// A notification with no item names nothing this player is playing — also
|
|
43
|
+
/// while the player itself has no active item, where "no item" and "no
|
|
44
|
+
/// active item" must not be read as a match.
|
|
45
|
+
func testAStallWithNoItemDoesNotArmRecovery() {
|
|
46
|
+
let player = TrackPlayer()
|
|
47
|
+
player.wantsToPlay = true
|
|
48
|
+
|
|
49
|
+
player.handlePlaybackStalled(Notification(name: AVPlayerItem.playbackStalledNotification))
|
|
50
|
+
drainPlayerQueue(player)
|
|
51
|
+
|
|
52
|
+
XCTAssertFalse(player.isRecoveringFromStall)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func testAStallWhilePausedDoesNotArmRecovery() {
|
|
56
|
+
let player = TrackPlayer()
|
|
57
|
+
let active = seedActiveItem(player)
|
|
58
|
+
player.wantsToPlay = false
|
|
59
|
+
|
|
60
|
+
player.handlePlaybackStalled(
|
|
61
|
+
Notification(name: AVPlayerItem.playbackStalledNotification, object: active))
|
|
62
|
+
drainPlayerQueue(player)
|
|
63
|
+
|
|
64
|
+
XCTAssertFalse(
|
|
65
|
+
player.isRecoveringFromStall,
|
|
66
|
+
"a stall arriving while paused must not arm a resume that fires later")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func testAnExplicitPauseDisarmsAPendingRecovery() async throws {
|
|
70
|
+
let player = TrackPlayer()
|
|
71
|
+
player.wantsToPlay = true
|
|
72
|
+
player.isRecoveringFromStall = true
|
|
73
|
+
|
|
74
|
+
_ = try await player.pause().await()
|
|
75
|
+
|
|
76
|
+
XCTAssertFalse(
|
|
77
|
+
player.isRecoveringFromStall,
|
|
78
|
+
"an explicit pause outranks a pending recovery, or the next refill "
|
|
79
|
+
+ "restarts audio the user just stopped")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Installs an engine holding one item and returns that item.
|
|
83
|
+
private func seedActiveItem(_ player: TrackPlayer) -> AVPlayerItem {
|
|
84
|
+
let engine = ActiveItemEngineStub()
|
|
85
|
+
let item = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/active.mp3"))
|
|
86
|
+
engine.item = item
|
|
87
|
+
player.engine = engine
|
|
88
|
+
return item
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// The handler hops to the player queue, so the assertion has to follow it.
|
|
92
|
+
private func drainPlayerQueue(_ player: TrackPlayer) {
|
|
93
|
+
let drained = expectation(description: "player queue drained")
|
|
94
|
+
player.playerQueue.async { drained.fulfill() }
|
|
95
|
+
wait(for: [drained], timeout: 2.0)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
import XCTest
|
|
2
2
|
@testable import QueuePlayer
|
|
3
3
|
|
|
4
|
-
///
|
|
5
|
-
///
|
|
6
|
-
/// `URLSession.shared`. The bitrate-parsing math is exercised end-to-end
|
|
7
|
-
/// through the demo format/replaygain suites.
|
|
4
|
+
/// The probe derives a bitrate from a local file's first 256 KB and reads
|
|
5
|
+
/// nothing that is not a `file://` URL.
|
|
8
6
|
final class StreamingBitrateProbeTests: XCTestCase {
|
|
9
7
|
|
|
10
|
-
func
|
|
11
|
-
let
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
defer { StubURLProtocol.reset() }
|
|
16
|
-
// Unique URL per run so the probe's process-wide bitrate cache can't
|
|
17
|
-
// satisfy the call without a fetch. `.test` is a reserved
|
|
18
|
-
// non-resolvable TLD, so a real-network leak would fail with -1003
|
|
19
|
-
// instead of reaching the stub.
|
|
20
|
-
let url = URL(string: "https://example.test/probe-\(UUID().uuidString).m4a")!
|
|
21
|
-
StubURLProtocol.stub(url: url, data: Data(count: 4096), statusCode: 200)
|
|
8
|
+
func testALocalCbrMp3ReportsItsEncodedBitrate() async throws {
|
|
9
|
+
let url = try PlayerFixtures.fixtureURL(name: "silence-cbr-128", ext: "mp3", for: self)
|
|
10
|
+
let bitrate = await StreamingBitrateProbe.bitrate(forUrl: url)
|
|
11
|
+
XCTAssertEqual(try XCTUnwrap(bitrate), 128_000, accuracy: 128_000 * 0.05)
|
|
12
|
+
}
|
|
22
13
|
|
|
23
|
-
|
|
14
|
+
func testARemoteUrlIsNotProbed() async throws {
|
|
15
|
+
let url = try XCTUnwrap(URL(string: "http://127.0.0.1:1/\(UUID().uuidString).mp3"))
|
|
16
|
+
let bitrate = await StreamingBitrateProbe.bitrate(forUrl: url)
|
|
17
|
+
XCTAssertNil(bitrate)
|
|
18
|
+
}
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
20
|
+
func testAMissingFileReportsNoBitrate() async {
|
|
21
|
+
let url = FileManager.default.temporaryDirectory
|
|
22
|
+
.appendingPathComponent("absent-\(UUID().uuidString).mp3")
|
|
23
|
+
let bitrate = await StreamingBitrateProbe.bitrate(forUrl: url)
|
|
24
|
+
XCTAssertNil(bitrate)
|
|
28
25
|
}
|
|
29
26
|
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
@_spi(QueuePlayerTests) import QueuePlayer
|
|
5
|
+
|
|
6
|
+
/// Extending the installed run waits for a crossfade rather than cutting it
|
|
7
|
+
/// short, and reads the run's start from what is actually being heard.
|
|
8
|
+
final class TopUpWindowGateTests: XCTestCase {
|
|
9
|
+
|
|
10
|
+
private var player: TrackPlayer!
|
|
11
|
+
|
|
12
|
+
override func setUp() async throws {
|
|
13
|
+
try await super.setUp()
|
|
14
|
+
player = TrackPlayer()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override func tearDown() async throws {
|
|
18
|
+
player = nil
|
|
19
|
+
try await super.tearDown()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// The same run, the same owed top-up: only the pending fade differs.
|
|
23
|
+
func testAPendingFadeIsWhatHoldsTheTopUpBack() async throws {
|
|
24
|
+
let engine = WindowEngineStub(count: 3)
|
|
25
|
+
engine.fadePending = true
|
|
26
|
+
try await seed(engine: engine, trackCount: 20)
|
|
27
|
+
|
|
28
|
+
await onQueue { $0.topUpWindow() }
|
|
29
|
+
XCTAssertTrue(
|
|
30
|
+
engine.inserted.isEmpty,
|
|
31
|
+
"a pending fade must not be cut short to extend the run")
|
|
32
|
+
|
|
33
|
+
engine.fadePending = false
|
|
34
|
+
await onQueue { $0.topUpWindow() }
|
|
35
|
+
XCTAssertFalse(
|
|
36
|
+
engine.inserted.isEmpty,
|
|
37
|
+
"the same run extends once there is no fade to protect")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// The run starts at the item being heard. An engine that retains played
|
|
41
|
+
/// items reports those first, and measuring from them would call the run
|
|
42
|
+
/// satisfied while the playhead sat near its end.
|
|
43
|
+
func testTheRunIsMeasuredFromTheItemBeingHeardNotTheFirstInstalled() async throws {
|
|
44
|
+
// Ten installed positions, but the playhead is at the last of them — so
|
|
45
|
+
// the run ahead is exhausted and a top-up is owed.
|
|
46
|
+
let engine = WindowEngineStub(count: 10)
|
|
47
|
+
engine.leadingIndex = 9
|
|
48
|
+
try await seed(engine: engine, trackCount: 30)
|
|
49
|
+
|
|
50
|
+
await onQueue { $0.topUpWindow() }
|
|
51
|
+
|
|
52
|
+
XCTAssertFalse(
|
|
53
|
+
engine.inserted.isEmpty,
|
|
54
|
+
"measured from the playhead the run is exhausted, so it must extend")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// A skip is applied inside a mutation, and the transition it raises is not
|
|
58
|
+
/// allowed to top up while one is in flight — so the skip extends the run
|
|
59
|
+
/// itself when it lands on the last installed position.
|
|
60
|
+
func testASkipOntoTheLastInstalledPositionExtendsTheRun() async throws {
|
|
61
|
+
let engine = WindowEngineStub(count: 4)
|
|
62
|
+
try await seed(engine: engine, trackCount: 20)
|
|
63
|
+
|
|
64
|
+
await onQueue { $0.applyNewCurrentIndex(newIndex: 3, reason: .userSkipToIndex) }
|
|
65
|
+
|
|
66
|
+
XCTAssertFalse(
|
|
67
|
+
engine.inserted.isEmpty,
|
|
68
|
+
"the run ahead of the skip target is empty and must be extended")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// The crossfade steady state: the whole queue installed, the playhead part
|
|
72
|
+
/// way into it. This is where the claim that the machinery is inert has to
|
|
73
|
+
/// hold, and a shorter-than-floor queue does not test it.
|
|
74
|
+
func testAWhollyInstalledQueueNeedsNoTopUpFromMidRun() async throws {
|
|
75
|
+
let engine = WindowEngineStub(count: 30)
|
|
76
|
+
engine.leadingIndex = 5
|
|
77
|
+
try await seed(engine: engine, trackCount: 30)
|
|
78
|
+
|
|
79
|
+
await onQueue { $0.topUpWindow() }
|
|
80
|
+
|
|
81
|
+
XCTAssertTrue(
|
|
82
|
+
engine.inserted.isEmpty,
|
|
83
|
+
"nothing is owed when the queue is already installed to its end")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// The engine refusing the append falls back to installing the slice.
|
|
87
|
+
func testAnEngineThatRefusesTheAppendFallsBackToAFullInstall() async throws {
|
|
88
|
+
let engine = WindowEngineStub(count: 3)
|
|
89
|
+
engine.refuseInserts = true
|
|
90
|
+
try await seed(engine: engine, trackCount: 20)
|
|
91
|
+
|
|
92
|
+
await onQueue { $0.topUpWindow() }
|
|
93
|
+
|
|
94
|
+
XCTAssertTrue(engine.inserted.isEmpty)
|
|
95
|
+
XCTAssertTrue(
|
|
96
|
+
engine.didSetItems, "a refused append must be recovered by a full install")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func testAFullTailNeedsNoTopUp() async throws {
|
|
100
|
+
let engine = WindowEngineStub(count: 4)
|
|
101
|
+
try await seed(engine: engine, trackCount: 4)
|
|
102
|
+
|
|
103
|
+
await onQueue { $0.topUpWindow() }
|
|
104
|
+
|
|
105
|
+
XCTAssertTrue(
|
|
106
|
+
engine.inserted.isEmpty,
|
|
107
|
+
"the queue is wholly installed, so there is nothing to append")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// A queue that has ended is not extended by a top-up, even while the
|
|
111
|
+
/// engine still holds a short run and the model has tracks past it: the
|
|
112
|
+
/// finished track would otherwise get a run appended behind it, and the
|
|
113
|
+
/// next play() would fade into it.
|
|
114
|
+
func testTopUpDoesNotExtendAnEndedQueue() async throws {
|
|
115
|
+
let engine = WindowEngineStub(count: 3)
|
|
116
|
+
engine.leadingIndex = 2
|
|
117
|
+
try await seed(engine: engine, trackCount: 20)
|
|
118
|
+
await onQueue { $0.reachedQueueEnd = true }
|
|
119
|
+
|
|
120
|
+
await onQueue { $0.topUpWindow() }
|
|
121
|
+
|
|
122
|
+
XCTAssertTrue(engine.inserted.isEmpty, "nothing is appended behind the finished track")
|
|
123
|
+
XCTAssertFalse(engine.didSetItems, "an ended queue must not be seated again by a top-up")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// The same holds for a drained engine that has not ended: seating it is the
|
|
127
|
+
/// caller's job, not the top-up's.
|
|
128
|
+
func testTopUpDoesNotSeatADrainedEngine() async throws {
|
|
129
|
+
let engine = WindowEngineStub(count: 0)
|
|
130
|
+
try await seed(engine: engine, trackCount: 20)
|
|
131
|
+
|
|
132
|
+
await onQueue { $0.topUpWindow() }
|
|
133
|
+
|
|
134
|
+
XCTAssertFalse(engine.didSetItems)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// An add that fits inside the window is inserted surgically, so the items
|
|
138
|
+
/// already buffering ahead of it are kept.
|
|
139
|
+
func testAnAddInsideTheWindowIsInsertedWithoutReinstalling() async throws {
|
|
140
|
+
let engine = WindowEngineStub(count: QueueWindowArithmetic.floor)
|
|
141
|
+
try await seed(engine: engine, trackCount: QueueWindowArithmetic.floor)
|
|
142
|
+
|
|
143
|
+
_ = try await player.addToQueue(
|
|
144
|
+
tracks: [
|
|
145
|
+
PlayerFixtures.track(id: "n0", url: "https://example.com/n0.m4a"),
|
|
146
|
+
PlayerFixtures.track(id: "n1", url: "https://example.com/n1.m4a"),
|
|
147
|
+
],
|
|
148
|
+
insertBefore: 3
|
|
149
|
+
).await()
|
|
150
|
+
|
|
151
|
+
XCTAssertEqual(engine.inserted.count, 2, "both added items are inserted")
|
|
152
|
+
XCTAssertFalse(engine.didSetItems, "nothing is reinstalled")
|
|
153
|
+
let ids = try await onQueueValue { $0.queueItemIds }
|
|
154
|
+
XCTAssertEqual(
|
|
155
|
+
engine.allMediaItems.compactMap(\.queueItemId), Array(ids.prefix(QueueWindowArithmetic.floor + 2)),
|
|
156
|
+
"the run is the queue's positions in order with the two added items in place")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// An add that runs past the window is not built item by item: the engine
|
|
160
|
+
/// is reconciled to the slice, and `topUpWindow` materialises the rest as
|
|
161
|
+
/// the playhead advances.
|
|
162
|
+
func testAnAddPastTheWindowBuildsOnlyTheSlice() async throws {
|
|
163
|
+
let engine = WindowEngineStub(count: QueueWindowArithmetic.floor)
|
|
164
|
+
try await seed(engine: engine, trackCount: QueueWindowArithmetic.floor)
|
|
165
|
+
let album = (0 ..< 30).map {
|
|
166
|
+
PlayerFixtures.track(id: "n\($0)", url: "https://example.com/n\($0).m4a")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_ = try await player.addToQueue(tracks: album, insertBefore: 5).await()
|
|
170
|
+
|
|
171
|
+
XCTAssertEqual(
|
|
172
|
+
engine.inserted.count, QueueWindowArithmetic.floor - 5,
|
|
173
|
+
"only the positions inside the window are built")
|
|
174
|
+
XCTAssertFalse(engine.didSetItems, "the run is trimmed and extended, not reinstalled")
|
|
175
|
+
let ids = try await onQueueValue { $0.queueItemIds }
|
|
176
|
+
XCTAssertEqual(
|
|
177
|
+
engine.allMediaItems.compactMap(\.queueItemId), Array(ids.prefix(QueueWindowArithmetic.floor)),
|
|
178
|
+
"the engine holds exactly the window: the five kept positions and the first five added")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// The same after the end of the queue on an engine that keeps its items:
|
|
182
|
+
/// the finished item is a materialised anchor, and an item inserted behind
|
|
183
|
+
/// it would be what the next play() fades into.
|
|
184
|
+
func testAnAppendAfterTheEndOfTheQueueLeavesARetainingEngineAlone() async throws {
|
|
185
|
+
let engine = WindowEngineStub(count: 3)
|
|
186
|
+
engine.leadingIndex = 2
|
|
187
|
+
try await seed(engine: engine, trackCount: 3)
|
|
188
|
+
await onQueue { $0.reachedQueueEnd = true }
|
|
189
|
+
|
|
190
|
+
_ = try await player.addToQueue(
|
|
191
|
+
tracks: [PlayerFixtures.track(id: "t3", url: "https://example.com/t3.m4a")],
|
|
192
|
+
insertBefore: nil
|
|
193
|
+
).await()
|
|
194
|
+
|
|
195
|
+
XCTAssertTrue(engine.inserted.isEmpty, "nothing is inserted behind the finished item")
|
|
196
|
+
XCTAssertFalse(engine.didSetItems)
|
|
197
|
+
let state = try await onQueueValue { ($0.currentTrackIndex, $0.reachedQueueEnd) }
|
|
198
|
+
XCTAssertEqual(state.0, 2)
|
|
199
|
+
XCTAssertTrue(state.1)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// A drain reported while the queue is already over does not re-decide it:
|
|
203
|
+
/// with tracks added behind the finished one, the verdict would otherwise be
|
|
204
|
+
/// `.windowDrained` and seat the added track.
|
|
205
|
+
func testARepeatedEndReportWhileEndedSeatsNothing() async throws {
|
|
206
|
+
let engine = WindowEngineStub(count: 0)
|
|
207
|
+
engine.leadingIndex = 2
|
|
208
|
+
try await seed(engine: engine, trackCount: 3)
|
|
209
|
+
await onQueue {
|
|
210
|
+
$0.reachedQueueEnd = true
|
|
211
|
+
_ = $0.queueState.insert(
|
|
212
|
+
[PlayerFixtures.track(id: "t3", url: "https://example.com/t3.m4a")], at: 3)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await onQueue { $0.applyEndVerdict() }
|
|
216
|
+
|
|
217
|
+
XCTAssertFalse(engine.didSetItems, "the ended queue is not re-seated")
|
|
218
|
+
let state = try await onQueueValue { ($0.currentTrackIndex, $0.reachedQueueEnd) }
|
|
219
|
+
XCTAssertEqual(state.0, 2, "the index stays on the track that finished")
|
|
220
|
+
XCTAssertTrue(state.1)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// An append after the end of the queue extends the model, not the engine:
|
|
224
|
+
/// inserting into a drained engine would seat playback on the new track
|
|
225
|
+
/// while `currentTrackIndex` still names the one that finished, so the
|
|
226
|
+
/// next `play()` would start a track the consumer was never told about.
|
|
227
|
+
func testAnAppendAfterTheEndOfTheQueueLeavesTheDrainedEngineAlone() async throws {
|
|
228
|
+
let engine = WindowEngineStub(count: 0)
|
|
229
|
+
engine.leadingIndex = 2
|
|
230
|
+
try await seed(engine: engine, trackCount: 3)
|
|
231
|
+
await onQueue { $0.reachedQueueEnd = true }
|
|
232
|
+
|
|
233
|
+
_ = try await player.addToQueue(
|
|
234
|
+
tracks: [PlayerFixtures.track(id: "t3", url: "https://example.com/t3.m4a")],
|
|
235
|
+
insertBefore: nil
|
|
236
|
+
).await()
|
|
237
|
+
|
|
238
|
+
XCTAssertTrue(engine.inserted.isEmpty, "nothing is inserted into a drained engine")
|
|
239
|
+
XCTAssertFalse(engine.didSetItems)
|
|
240
|
+
let state = try await onQueueValue {
|
|
241
|
+
($0.tracks.count, $0.currentTrackIndex, $0.reachedQueueEnd)
|
|
242
|
+
}
|
|
243
|
+
XCTAssertEqual(state.0, 4, "the queue model carries the append")
|
|
244
|
+
XCTAssertEqual(state.1, 2, "the current index stays on the track that finished")
|
|
245
|
+
XCTAssertTrue(state.2, "the queue is still over until a skip or seek says otherwise")
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// Reinstalling the run the engine is already rendering from restarts the
|
|
249
|
+
/// track being heard unless its playhead is carried across.
|
|
250
|
+
func testAForcedReinstallKeepsThePlayheadOfTheTrackBeingHeard() async throws {
|
|
251
|
+
let engine = WindowEngineStub(count: 12)
|
|
252
|
+
engine.leadingIndex = 4
|
|
253
|
+
engine.position = 37.5
|
|
254
|
+
try await seed(engine: engine, trackCount: 20)
|
|
255
|
+
|
|
256
|
+
await onQueue { p in
|
|
257
|
+
p.performingMutation { p.fullRebuildPlayerQueue(forcingReinstall: true) }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
XCTAssertTrue(engine.didSetItems, "the run is installed whole")
|
|
261
|
+
XCTAssertEqual(
|
|
262
|
+
engine.seatedAtPosition, 37.5,
|
|
263
|
+
"the track being heard resumes where it was, not from its start")
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// A reinstall that seats on a different track is a track change, and a
|
|
267
|
+
/// track change starts at the beginning.
|
|
268
|
+
func testAReinstallOntoADifferentTrackStartsItFromZero() async throws {
|
|
269
|
+
let engine = WindowEngineStub(count: 12)
|
|
270
|
+
engine.leadingIndex = 4
|
|
271
|
+
engine.position = 37.5
|
|
272
|
+
try await seed(engine: engine, trackCount: 20)
|
|
273
|
+
await onQueue { $0.currentTrackIndex = 9 }
|
|
274
|
+
|
|
275
|
+
await onQueue { p in
|
|
276
|
+
p.performingMutation { p.fullRebuildPlayerQueue(forcingReinstall: true) }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
XCTAssertEqual(engine.seatedAtPosition, 0)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// MARK: - Helpers
|
|
283
|
+
|
|
284
|
+
private func seed(engine: WindowEngineStub, trackCount: Int) async throws {
|
|
285
|
+
let player = try XCTUnwrap(self.player)
|
|
286
|
+
await player.onPlayerQueue {
|
|
287
|
+
let ids = player.queueState.replaceAll(
|
|
288
|
+
(0 ..< trackCount).map {
|
|
289
|
+
PlayerFixtures.track(id: "t\($0)", url: "https://example.com/\($0).m4a")
|
|
290
|
+
})
|
|
291
|
+
engine.stampIds(Array(ids.prefix(engine.installedCount)))
|
|
292
|
+
player.currentTrackIndex = engine.leadingIndex
|
|
293
|
+
player.engine = engine
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private func onQueue(_ body: @escaping (TrackPlayer) -> Void) async {
|
|
298
|
+
guard let player else { return }
|
|
299
|
+
await player.onPlayerQueue { body(player) }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private func onQueueValue<T>(_ body: @escaping (TrackPlayer) -> T) async throws -> T {
|
|
303
|
+
let player = try XCTUnwrap(self.player)
|
|
304
|
+
return await player.onPlayerQueueValue { body(player) }
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// Reports an installed run of `count` items and records what is appended to
|
|
309
|
+
/// it. Every other member is inert — the window logic reads the run, the
|
|
310
|
+
/// leading identity and the fade flag, nothing else.
|
|
311
|
+
private final class WindowEngineStub: PlaybackEngine {
|
|
312
|
+
let installedCount: Int
|
|
313
|
+
var leadingIndex: Int = 0
|
|
314
|
+
var fadePending = false
|
|
315
|
+
/// `CrossfadeEngine.insertItems` refuses an anchor it does not hold; the
|
|
316
|
+
/// caller's recovery from that is a contract worth exercising.
|
|
317
|
+
var refuseInserts = false
|
|
318
|
+
/// The engine's own playhead, which a reinstall of the run it is already
|
|
319
|
+
/// rendering has to carry across.
|
|
320
|
+
var position: TimeInterval = 0
|
|
321
|
+
private(set) var inserted: [AVPlayerItem] = []
|
|
322
|
+
private(set) var didSetItems = false
|
|
323
|
+
private(set) var seatedAtPosition: TimeInterval?
|
|
324
|
+
private var items: [AVPlayerItem] = []
|
|
325
|
+
|
|
326
|
+
init(count: Int) {
|
|
327
|
+
self.installedCount = count
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
func stampIds(_ ids: [String]) {
|
|
331
|
+
items = ids.map { id in
|
|
332
|
+
let item = AVPlayerItem(url: URL(string: "https://example.com/\(id).m4a")!)
|
|
333
|
+
item.queueItemId = id
|
|
334
|
+
return item
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
var allMediaItems: [AVPlayerItem] { items }
|
|
339
|
+
var leadingQueueItemId: String? { items[safe: leadingIndex]?.queueItemId }
|
|
340
|
+
var isFadePendingOrActive: Bool { fadePending }
|
|
341
|
+
/// The whole installed run stands in for what is attached; this stub's
|
|
342
|
+
/// contract is the window, not the legs.
|
|
343
|
+
var attachedQueueItemIds: [String] { items.compactMap { $0.queueItemId } }
|
|
344
|
+
|
|
345
|
+
/// Inserts after the anchor, as the engines do, so the run's shape is
|
|
346
|
+
/// observable and not only the count of what was appended.
|
|
347
|
+
@discardableResult
|
|
348
|
+
func insertItems(_ newItems: [AVPlayerItem], after: AVPlayerItem?) -> Bool {
|
|
349
|
+
guard !refuseInserts else { return false }
|
|
350
|
+
let position: Int
|
|
351
|
+
if let after {
|
|
352
|
+
guard let index = items.firstIndex(where: { $0 === after }) else { return false }
|
|
353
|
+
position = index + 1
|
|
354
|
+
} else {
|
|
355
|
+
position = items.count
|
|
356
|
+
}
|
|
357
|
+
inserted.append(contentsOf: newItems)
|
|
358
|
+
items.insert(contentsOf: newItems, at: position)
|
|
359
|
+
return true
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
func setAudioMixProvider(_ provider: AudioMixProvider?) {}
|
|
363
|
+
func refreshAllItemMixes() {}
|
|
364
|
+
func release() {}
|
|
365
|
+
func setItems(_ items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) {
|
|
366
|
+
didSetItems = true
|
|
367
|
+
seatedAtPosition = startPositionSeconds
|
|
368
|
+
self.items = items
|
|
369
|
+
}
|
|
370
|
+
func remove(_ item: AVPlayerItem) { items.removeAll { $0 === item } }
|
|
371
|
+
func removeAllItems() { items.removeAll() }
|
|
372
|
+
func canInsert(_ item: AVPlayerItem, after: AVPlayerItem?) -> Bool { true }
|
|
373
|
+
func play() {}
|
|
374
|
+
func pause() {}
|
|
375
|
+
func stop() {}
|
|
376
|
+
/// A seek moves the playhead: the run is measured from the new leading item.
|
|
377
|
+
func seek(toIndex index: Int, position: TimeInterval) { leadingIndex = index }
|
|
378
|
+
func skipToNext() {}
|
|
379
|
+
func skipToPrevious() {}
|
|
380
|
+
func setVolume(_ volume: Float) {}
|
|
381
|
+
func setPlaybackSpeed(_ speed: Float) {}
|
|
382
|
+
func seedPlaybackSpeed(_ speed: Float) {}
|
|
383
|
+
func setRepeatMode(_ mode: RepeatMode) {}
|
|
384
|
+
func setPitchCorrectionMode(_ mode: PitchCorrectionMode) {}
|
|
385
|
+
|
|
386
|
+
var currentMediaItemIndex: Int { leadingIndex }
|
|
387
|
+
var currentPositionSeconds: TimeInterval { position }
|
|
388
|
+
var currentDurationSeconds: TimeInterval { 0 }
|
|
389
|
+
var bufferedPositionSeconds: TimeInterval { 0 }
|
|
390
|
+
var isPlaying: Bool { false }
|
|
391
|
+
var timeControlStatus: AVPlayer.TimeControlStatus { .paused }
|
|
392
|
+
var currentMediaItem: AVPlayerItem? { items[safe: leadingIndex] }
|
|
393
|
+
var delegate: PlaybackEngineDelegate?
|
|
394
|
+
}
|
|
@@ -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
|
+
}
|