react-native-queue-player 1.2.0 → 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/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 +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 +839 -271
- 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 +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 +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 +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 +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 +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 +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/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 +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/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 -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,120 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// An item that reaches its end mid-queue with no fade in flight advances the
|
|
6
|
+
/// engine anyway. A fade is how this engine usually crosses a boundary, not
|
|
7
|
+
/// the only way it can — the arm can fail to load, time out, or be cancelled
|
|
8
|
+
/// by a route change or a queue mutation, and the queue must not stop on a
|
|
9
|
+
/// track that has finished.
|
|
10
|
+
final class CrossfadeAdvanceWithoutFadeTests: XCTestCase {
|
|
11
|
+
|
|
12
|
+
func testAnItemEndingMidQueueAdvancesToTheNextItem() {
|
|
13
|
+
let engine = makeEngine(count: 3)
|
|
14
|
+
engine.play()
|
|
15
|
+
|
|
16
|
+
endLeadingItem(on: engine)
|
|
17
|
+
|
|
18
|
+
XCTAssertEqual(
|
|
19
|
+
engine.currentMediaItem?.queueItemId, "id-1",
|
|
20
|
+
"the engine moves to the next item in the run")
|
|
21
|
+
XCTAssertEqual(engine.currentMediaItemIndex, 1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// The queue slot may still hold the instance the arm attached to the
|
|
25
|
+
/// standby leg, and attaching an item still associated with another player
|
|
26
|
+
/// raises an exception Swift cannot catch.
|
|
27
|
+
func testTheAdvanceAttachesAFreshItemRatherThanTheStoredOne() {
|
|
28
|
+
let engine = makeEngine(count: 3)
|
|
29
|
+
engine.play()
|
|
30
|
+
let stored = engine.allMediaItems
|
|
31
|
+
|
|
32
|
+
endLeadingItem(on: engine)
|
|
33
|
+
|
|
34
|
+
XCTAssertFalse(
|
|
35
|
+
engine.currentMediaItem === stored[1],
|
|
36
|
+
"the incoming item is rebuilt, never the stored instance")
|
|
37
|
+
XCTAssertEqual(
|
|
38
|
+
engine.currentMediaItem?.queueItemId, stored[1].queueItemId,
|
|
39
|
+
"the rebuild carries the queue identity across")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// The end-of-item notification hops onto the owner's queue, so a pause
|
|
43
|
+
/// taken in between must survive the advance.
|
|
44
|
+
func testAnItemEndingWhilePausedDoesNotResumePlayback() {
|
|
45
|
+
let engine = makeEngine(count: 3)
|
|
46
|
+
engine.play()
|
|
47
|
+
engine.pause()
|
|
48
|
+
|
|
49
|
+
endLeadingItem(on: engine)
|
|
50
|
+
|
|
51
|
+
XCTAssertEqual(
|
|
52
|
+
engine.currentMediaItem?.queueItemId, "id-1",
|
|
53
|
+
"the advance still happens")
|
|
54
|
+
XCTAssertEqual(
|
|
55
|
+
engine.timeControlStatus, .paused,
|
|
56
|
+
"a paused engine stays paused across the advance")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func testTheLastItemEndingEndsTheQueueRatherThanAdvancing() {
|
|
60
|
+
let engine = makeEngine(count: 2)
|
|
61
|
+
let delegate = RecordingDelegate()
|
|
62
|
+
engine.delegate = delegate
|
|
63
|
+
engine.play()
|
|
64
|
+
engine.seek(toIndex: 1, position: 0)
|
|
65
|
+
|
|
66
|
+
endLeadingItem(on: engine)
|
|
67
|
+
|
|
68
|
+
XCTAssertTrue(delegate.playbackEnded, "the queue reports that it ran out")
|
|
69
|
+
XCTAssertEqual(engine.currentMediaItemIndex, 1, "and stays on the last item")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// MARK: - Helpers
|
|
73
|
+
|
|
74
|
+
private func makeEngine(count: Int) -> CrossfadeEngine {
|
|
75
|
+
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
|
|
76
|
+
let items = (0 ..< count).map { index -> AVPlayerItem in
|
|
77
|
+
let item = AVPlayerItem(url: URL(string: "http://127.0.0.1:1/\(index).m4a")!)
|
|
78
|
+
item.queueItemId = "id-\(index)"
|
|
79
|
+
return item
|
|
80
|
+
}
|
|
81
|
+
engine.setItems(items, startIndex: 0, startPositionSeconds: 0)
|
|
82
|
+
engines.append(engine)
|
|
83
|
+
return engine
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Post the end-of-item notification the way AVFoundation does, then let the
|
|
87
|
+
/// engine's hop onto its owner queue run before asserting.
|
|
88
|
+
private func endLeadingItem(on engine: CrossfadeEngine) {
|
|
89
|
+
guard let item = engine.currentMediaItem else {
|
|
90
|
+
return XCTFail("no item is attached to end")
|
|
91
|
+
}
|
|
92
|
+
NotificationCenter.default.post(
|
|
93
|
+
name: .AVPlayerItemDidPlayToEndTime, object: item)
|
|
94
|
+
let drained = expectation(description: "owner queue drained")
|
|
95
|
+
DispatchQueue.main.async { drained.fulfill() }
|
|
96
|
+
wait(for: [drained], timeout: 2.0)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private var engines: [CrossfadeEngine] = []
|
|
100
|
+
|
|
101
|
+
override func tearDown() {
|
|
102
|
+
engines.forEach { $0.release() }
|
|
103
|
+
engines.removeAll()
|
|
104
|
+
super.tearDown()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private final class RecordingDelegate: PlaybackEngineDelegate {
|
|
109
|
+
var playbackEnded = false
|
|
110
|
+
|
|
111
|
+
func engineStateMaybeChanged(_ engine: PlaybackEngine, reason: StateChangeReason?) {}
|
|
112
|
+
func engineDidTransitionTrack(_ engine: PlaybackEngine) {}
|
|
113
|
+
func engineActiveItemFormatChanged(_ engine: PlaybackEngine) {}
|
|
114
|
+
func engineDidFailWithError(
|
|
115
|
+
_ engine: PlaybackEngine, error: Error, item: AVPlayerItem?) {}
|
|
116
|
+
func engineDidPlayItemToEnd(_ engine: PlaybackEngine) {}
|
|
117
|
+
func enginePlaybackEnded(_ engine: PlaybackEngine) { playbackEnded = true }
|
|
118
|
+
func engineCrossfadeDidBegin(_ engine: PlaybackEngine) {}
|
|
119
|
+
func engineCrossfadeDidCancel(_ engine: PlaybackEngine) {}
|
|
120
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// The crossfade leading leg installs its EQ / visualizer / ReplayGain tap-mix
|
|
6
|
+
/// before it renders. AVPlayer honours `audioMix` only before an item renders,
|
|
7
|
+
/// and `AVPlayerItem.status == .readyToPlay` does not imply the asset's `tracks`
|
|
8
|
+
/// key has loaded — the provider yields nil until it is — so a leg that wants
|
|
9
|
+
/// the tap is held at rate 0 until the tracks load and the mix is installed,
|
|
10
|
+
/// then resumed. Miss that window and the tap is absent for the whole track.
|
|
11
|
+
///
|
|
12
|
+
/// The incoming-leg attach, which is reached only through a real crossfade's
|
|
13
|
+
/// arm chain, is covered end-to-end by the demo `boundary-audio-chain` suite
|
|
14
|
+
/// (it drives an actual boundary on device); a unit test that drives the real
|
|
15
|
+
/// fade is too sensitive to a loaded CI simulator's arm-chain timing.
|
|
16
|
+
@MainActor
|
|
17
|
+
final class CrossfadeEngineIncomingMixTests: XCTestCase {
|
|
18
|
+
|
|
19
|
+
/// A tap-wanting consumer holds a freshly-seated leading leg at rate 0 until
|
|
20
|
+
/// its `tracks` key loads and the tap-bearing mix is installed, then resumes
|
|
21
|
+
/// it — the leg never renders untapped, and it does not strand at rate 0.
|
|
22
|
+
func testLeadingLegHeldForTapUntilMixInstalledThenResumes() async throws {
|
|
23
|
+
let url = try requireFixture(name: "silence-cbr-128", ext: "mp3")
|
|
24
|
+
let provider = TapWantingTracksGatedMixProvider()
|
|
25
|
+
let engine = CrossfadeEngine(
|
|
26
|
+
crossfadeDurationSeconds: 0.5, confinedTo: .main)
|
|
27
|
+
defer { engine.release() }
|
|
28
|
+
engine.setAudioMixProvider(provider)
|
|
29
|
+
engine.setItems(
|
|
30
|
+
[makeItem(url: url, id: "leading"), makeItem(url: url, id: "incoming")],
|
|
31
|
+
startIndex: 0,
|
|
32
|
+
startPositionSeconds: 0
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
// A fresh `AVURLAsset` starts `.unknown`, so the leading item is not yet
|
|
36
|
+
// `.readyToPlay` here. A tap is wanted, so play() must hold the leg at
|
|
37
|
+
// rate 0 rather than start it rendering before the mix is installed.
|
|
38
|
+
engine.play()
|
|
39
|
+
XCTAssertEqual(
|
|
40
|
+
engine.timeControlStatus, .paused,
|
|
41
|
+
"a tap-wanting leading leg must be held at rate 0 until its mix is "
|
|
42
|
+
+ "installed, not started rendering by play()"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// The `.readyToPlay` observer loads tracks, installs the tap-bearing mix,
|
|
46
|
+
// then resumes the held leg — it must reach `.playing`, not strand.
|
|
47
|
+
try await waitUntil("the held leading leg resumes playing") {
|
|
48
|
+
engine.timeControlStatus == .playing
|
|
49
|
+
}
|
|
50
|
+
let leading = try XCTUnwrap(engine.currentMediaItem)
|
|
51
|
+
let installed = leading.audioMix?.inputParameters.first
|
|
52
|
+
as? AVMutableAudioMixInputParameters
|
|
53
|
+
XCTAssertTrue(
|
|
54
|
+
installed === provider.issued["leading"],
|
|
55
|
+
"the leading leg must carry the provider's tap-bearing mix once playing; "
|
|
56
|
+
+ "a nil or foreign mix means it resumed untapped"
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// MARK: - helpers
|
|
61
|
+
|
|
62
|
+
/// Built without `tracks` in `automaticallyLoadedAssetKeys`, so the asset's
|
|
63
|
+
/// tracks key is unloaded until something explicitly loads it — the state a
|
|
64
|
+
/// remote item is in when the arm chain picks it up.
|
|
65
|
+
private func makeItem(url: URL, id: String) -> AVPlayerItem {
|
|
66
|
+
let item = AVPlayerItem(asset: AVURLAsset(url: url))
|
|
67
|
+
item.queueItemId = id
|
|
68
|
+
return item
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private func waitUntil(
|
|
72
|
+
_ what: String,
|
|
73
|
+
timeout: TimeInterval = 20,
|
|
74
|
+
_ condition: () -> Bool
|
|
75
|
+
) async throws {
|
|
76
|
+
let deadline = Date().addingTimeInterval(timeout)
|
|
77
|
+
while Date() < deadline {
|
|
78
|
+
if condition() { return }
|
|
79
|
+
try await Task.sleep(nanoseconds: 20_000_000)
|
|
80
|
+
}
|
|
81
|
+
XCTFail("timed out waiting for \(what)")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private func requireFixture(name: String, ext: String) throws -> URL {
|
|
85
|
+
let bundle = Bundle(for: type(of: self))
|
|
86
|
+
if let url = bundle.url(forResource: name, withExtension: ext) {
|
|
87
|
+
return url
|
|
88
|
+
}
|
|
89
|
+
let candidates = bundle.urls(forResourcesWithExtension: ext, subdirectory: nil) ?? []
|
|
90
|
+
if let match = candidates.first(where: { $0.deletingPathExtension().lastPathComponent == name }) {
|
|
91
|
+
return match
|
|
92
|
+
}
|
|
93
|
+
// Fixtures are bundled via `test_spec.resources` in QueuePlayer.podspec.
|
|
94
|
+
// A miss means the test-harness pod install regressed — that's a real
|
|
95
|
+
// infrastructure failure, not a conditionally-inapplicable test.
|
|
96
|
+
return try XCTUnwrap(
|
|
97
|
+
nil as URL?,
|
|
98
|
+
"fixture \(name).\(ext) not bundled — pod install regression in tests-harness"
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Like `AudioTapProvider`, reports a live tap consumer so the crossfade engine
|
|
104
|
+
/// holds a not-yet-ready leading leg for the tap rather than resuming it
|
|
105
|
+
/// immediately. Issues a real tap-bearing mix once tracks load.
|
|
106
|
+
private final class TapWantingTracksGatedMixProvider: AudioMixProvider {
|
|
107
|
+
private(set) var issued: [String: AVMutableAudioMixInputParameters] = [:]
|
|
108
|
+
|
|
109
|
+
func anyConsumerWantsTap() -> Bool { true }
|
|
110
|
+
|
|
111
|
+
func audioMix(for item: AVPlayerItem) -> AVAudioMix? {
|
|
112
|
+
guard case .loaded(let tracks) = item.asset.status(of: .tracks),
|
|
113
|
+
let track = tracks.first(where: { $0.mediaType == .audio }) else {
|
|
114
|
+
return nil
|
|
115
|
+
}
|
|
116
|
+
let params = AVMutableAudioMixInputParameters(track: track)
|
|
117
|
+
let mix = AVMutableAudioMix()
|
|
118
|
+
mix.inputParameters = [params]
|
|
119
|
+
issued[item.queueItemId ?? ""] = params
|
|
120
|
+
return mix
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// Coverage for `CrossfadeEngine`'s construction, delegate assignment and
|
|
6
|
+
/// index contract.
|
|
7
|
+
///
|
|
8
|
+
/// The index contract is the load-bearing part: the engine indexes within
|
|
9
|
+
/// whatever slice it was handed, so its indices are NOT queue positions.
|
|
10
|
+
/// Callers translate by `queueItemId` identity. These tests cover the engine
|
|
11
|
+
/// side of that contract — slice-relative indexing, ordered identity-bearing
|
|
12
|
+
/// `allMediaItems`, and identity surviving the seek cross-attach. The
|
|
13
|
+
/// translation itself lives in `TrackPlayer.engineIndex(forQueuePosition:)`,
|
|
14
|
+
/// which needs a configured engine and is covered by the
|
|
15
|
+
/// `non-zero-start-index-seeks-within-the-started-track` demo step.
|
|
16
|
+
///
|
|
17
|
+
/// The negative-duration constructor guard at
|
|
18
|
+
/// `CrossfadeEngine.init(crossfadeDurationSeconds:)` is not testable — a
|
|
19
|
+
/// `precondition` trap is non-recoverable from XCTest. The load-bearing
|
|
20
|
+
/// guard for runtime callers is the clamp inside
|
|
21
|
+
/// `PlaybackModeStateMachine`; the constructor check is a defensive belt.
|
|
22
|
+
final class CrossfadeEngineTests: XCTestCase {
|
|
23
|
+
|
|
24
|
+
func testConstructorAcceptsNonNegativeCrossfadeDuration() {
|
|
25
|
+
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
|
|
26
|
+
XCTAssertEqual(engine.currentMediaItemIndex, -1, "a fresh engine holds no queue")
|
|
27
|
+
XCTAssertTrue(engine.allMediaItems.isEmpty)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func testDelegateIsWirable() {
|
|
31
|
+
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
|
|
32
|
+
let delegate = NoopDelegate()
|
|
33
|
+
engine.delegate = delegate
|
|
34
|
+
XCTAssertTrue(engine.delegate === delegate)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// MARK: - Index contract
|
|
38
|
+
|
|
39
|
+
/// Items carrying the lib-generated identity the translation relies on.
|
|
40
|
+
private func makeItems(ids: [String]) -> [AVPlayerItem] {
|
|
41
|
+
ids.map { id in
|
|
42
|
+
let item = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/\(id).mp3"))
|
|
43
|
+
item.queueItemId = id
|
|
44
|
+
return item
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func testEngineIndexesWithinItsOwnSliceNotTheQueuePosition() {
|
|
49
|
+
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
|
|
50
|
+
// The slice a caller hands over when playback starts at queue position 5.
|
|
51
|
+
engine.setItems(makeItems(ids: ["q5", "q6", "q7"]), startIndex: 0, startPositionSeconds: 0)
|
|
52
|
+
|
|
53
|
+
// Zero, not 5 — the engine has no knowledge of the queue position the
|
|
54
|
+
// slice came from. Reading this as a queue position seats the wrong track.
|
|
55
|
+
XCTAssertEqual(engine.currentMediaItemIndex, 0)
|
|
56
|
+
XCTAssertEqual(engine.allMediaItems.count, 3)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// `allMediaItems` exposes the installed slice in order, with identities
|
|
60
|
+
/// intact. Callers translate a queue position into an engine index through
|
|
61
|
+
/// this, so an engine that reordered or dropped identities would break the
|
|
62
|
+
/// translation silently.
|
|
63
|
+
func testAllMediaItemsPreservesSliceOrderAndIdentity() {
|
|
64
|
+
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
|
|
65
|
+
engine.setItems(makeItems(ids: ["q5", "q6", "q7"]), startIndex: 0, startPositionSeconds: 0)
|
|
66
|
+
|
|
67
|
+
XCTAssertEqual(engine.allMediaItems.map(\.queueItemId), ["q5", "q6", "q7"])
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// The cross-attach inside `seek` rebuilds the target item; identity has to
|
|
71
|
+
/// survive it or the translation stops resolving after the first seek.
|
|
72
|
+
func testIdentitySurvivesTheSeekCrossAttach() {
|
|
73
|
+
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
|
|
74
|
+
engine.setItems(makeItems(ids: ["q5", "q6", "q7"]), startIndex: 0, startPositionSeconds: 0)
|
|
75
|
+
|
|
76
|
+
engine.seek(toIndex: 1, position: 0)
|
|
77
|
+
|
|
78
|
+
XCTAssertEqual(engine.allMediaItems.map(\.queueItemId), ["q5", "q6", "q7"])
|
|
79
|
+
XCTAssertEqual(engine.currentMediaItem?.queueItemId, "q6")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// The seek cross-attach rebuilds the target item; the read-through routing
|
|
83
|
+
/// stamp has to survive it too, or `getCurrentTrackSource()` reports a
|
|
84
|
+
/// cache-served track as `.streaming` (the gapless engine never rebuilds, so
|
|
85
|
+
/// it never loses the stamp).
|
|
86
|
+
func testRoutingStampSurvivesTheSeekCrossAttach() {
|
|
87
|
+
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
|
|
88
|
+
let items = makeItems(ids: ["q5", "q6", "q7"])
|
|
89
|
+
items.forEach { $0.isRoutedThroughReadThroughServer = true }
|
|
90
|
+
engine.setItems(items, startIndex: 0, startPositionSeconds: 0)
|
|
91
|
+
|
|
92
|
+
engine.seek(toIndex: 1, position: 0)
|
|
93
|
+
|
|
94
|
+
XCTAssertEqual(
|
|
95
|
+
engine.allMediaItems.map(\.isRoutedThroughReadThroughServer),
|
|
96
|
+
[true, true, true],
|
|
97
|
+
"the rebuilt item must keep its read-through routing stamp")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func testSeekToIndexMovesWithinTheEngineSlice() {
|
|
101
|
+
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
|
|
102
|
+
engine.setItems(makeItems(ids: ["q5", "q6", "q7"]), startIndex: 0, startPositionSeconds: 0)
|
|
103
|
+
|
|
104
|
+
engine.seek(toIndex: 2, position: 0)
|
|
105
|
+
XCTAssertEqual(engine.currentMediaItemIndex, 2)
|
|
106
|
+
XCTAssertEqual(engine.currentMediaItem?.queueItemId, "q7")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func testSeekOutsideTheSliceIsRejected() {
|
|
110
|
+
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
|
|
111
|
+
engine.setItems(makeItems(ids: ["q5", "q6", "q7"]), startIndex: 0, startPositionSeconds: 0)
|
|
112
|
+
|
|
113
|
+
engine.seek(toIndex: 7, position: 0)
|
|
114
|
+
XCTAssertEqual(engine.currentMediaItemIndex, 0, "an out-of-slice index must not move the engine")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// A missing local file's `AVURLAsset` load can stall in `.unknown` on
|
|
118
|
+
/// `AVPlayer`, so `.failed` never fires and the failure never reaches the
|
|
119
|
+
/// delegate. Seating one must surface `engineDidFailWithError` from the file's
|
|
120
|
+
/// non-existence directly, rather than wait on the load.
|
|
121
|
+
func testAMissingLocalFileSurfacesAFailure() {
|
|
122
|
+
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
|
|
123
|
+
let delegate = FailCapturingDelegate()
|
|
124
|
+
engine.delegate = delegate
|
|
125
|
+
let missing = URL(fileURLWithPath: "/tmp/rnqp-missing-\(UUID().uuidString).mp3")
|
|
126
|
+
let item = AVPlayerItem(url: missing)
|
|
127
|
+
item.queueItemId = "missing"
|
|
128
|
+
item.sourceURL = missing.absoluteString
|
|
129
|
+
|
|
130
|
+
let fired = expectation(description: "a missing local file surfaces a failure")
|
|
131
|
+
delegate.onFail = { error in
|
|
132
|
+
let ns = error as NSError
|
|
133
|
+
if ns.domain == NSURLErrorDomain, ns.code == NSURLErrorFileDoesNotExist { fired.fulfill() }
|
|
134
|
+
}
|
|
135
|
+
engine.setItems([item], startIndex: 0, startPositionSeconds: 0)
|
|
136
|
+
|
|
137
|
+
wait(for: [fired], timeout: 3.0)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private final class NoopDelegate: PlaybackEngineDelegate {
|
|
142
|
+
func engineStateMaybeChanged(_ engine: PlaybackEngine, reason: StateChangeReason?) {}
|
|
143
|
+
func engineDidTransitionTrack(_ engine: PlaybackEngine) {}
|
|
144
|
+
func engineActiveItemFormatChanged(_ engine: PlaybackEngine) {}
|
|
145
|
+
func engineDidFailWithError(
|
|
146
|
+
_ engine: PlaybackEngine, error: Error, item: AVPlayerItem?) {}
|
|
147
|
+
func engineDidPlayItemToEnd(_ engine: PlaybackEngine) {}
|
|
148
|
+
func enginePlaybackEnded(_ engine: PlaybackEngine) {}
|
|
149
|
+
func engineCrossfadeDidBegin(_ engine: PlaybackEngine) {}
|
|
150
|
+
func engineCrossfadeDidCancel(_ engine: PlaybackEngine) {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private final class FailCapturingDelegate: PlaybackEngineDelegate {
|
|
154
|
+
var onFail: ((Error) -> Void)?
|
|
155
|
+
func engineStateMaybeChanged(_ engine: PlaybackEngine, reason: StateChangeReason?) {}
|
|
156
|
+
func engineDidTransitionTrack(_ engine: PlaybackEngine) {}
|
|
157
|
+
func engineActiveItemFormatChanged(_ engine: PlaybackEngine) {}
|
|
158
|
+
func engineDidFailWithError(
|
|
159
|
+
_ engine: PlaybackEngine, error: Error, item: AVPlayerItem?) { onFail?(error) }
|
|
160
|
+
func engineDidPlayItemToEnd(_ engine: PlaybackEngine) {}
|
|
161
|
+
func enginePlaybackEnded(_ engine: PlaybackEngine) {}
|
|
162
|
+
func engineCrossfadeDidBegin(_ engine: PlaybackEngine) {}
|
|
163
|
+
func engineCrossfadeDidCancel(_ engine: PlaybackEngine) {}
|
|
164
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// The rendering item's audio mix is rebuilt only when a ramp was scheduled.
|
|
6
|
+
///
|
|
7
|
+
/// Rebuilding it means a fresh processing tap on an item already in flight,
|
|
8
|
+
/// which AVPlayer silently drops — so a cancel that had no ramp to unwind must
|
|
9
|
+
/// leave the mix it is already rendering alone.
|
|
10
|
+
final class CrossfadeMixReapplyTests: XCTestCase {
|
|
11
|
+
|
|
12
|
+
func testRemovingItemsDoesNotRebuildTheRenderingItemsMix() {
|
|
13
|
+
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
|
|
14
|
+
let provider = CountingMixProvider()
|
|
15
|
+
engine.setAudioMixProvider(provider)
|
|
16
|
+
engine.setItems(items(count: 4), startIndex: 0, startPositionSeconds: 0)
|
|
17
|
+
let afterInstall = provider.buildCount
|
|
18
|
+
|
|
19
|
+
// The removal loop a rebuild runs when it replaces a divergent tail.
|
|
20
|
+
for item in engine.allMediaItems.suffix(3) {
|
|
21
|
+
engine.remove(item)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
XCTAssertEqual(
|
|
25
|
+
provider.buildCount, afterInstall,
|
|
26
|
+
"removing queued items must not rebuild the mix of the item playing")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Arming happens from the boundary observer during playback, so the armed
|
|
30
|
+
/// half of this contract is covered by the crossfade demo suites rather than
|
|
31
|
+
/// here; what is reachable is that a cancel with no ramp to unwind leaves the
|
|
32
|
+
/// rendering item's mix as it is.
|
|
33
|
+
func testCancellingWithNothingArmedLeavesTheMixAlone() {
|
|
34
|
+
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
|
|
35
|
+
let provider = CountingMixProvider()
|
|
36
|
+
engine.setAudioMixProvider(provider)
|
|
37
|
+
engine.setItems(items(count: 2), startIndex: 0, startPositionSeconds: 0)
|
|
38
|
+
let afterInstall = provider.buildCount
|
|
39
|
+
|
|
40
|
+
engine.cancelFade()
|
|
41
|
+
|
|
42
|
+
XCTAssertEqual(provider.buildCount, afterInstall)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private func items(count: Int) -> [AVPlayerItem] {
|
|
46
|
+
(0 ..< count).map { index in
|
|
47
|
+
let item = AVPlayerItem(
|
|
48
|
+
url: URL(string: "http://127.0.0.1:1/\(index).m4a")!)
|
|
49
|
+
item.queueItemId = "id-\(index)"
|
|
50
|
+
return item
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Counts how many times a mix is built, which is what the dropped-tap hazard
|
|
56
|
+
/// is really about.
|
|
57
|
+
private final class CountingMixProvider: AudioMixProvider {
|
|
58
|
+
private(set) var buildCount = 0
|
|
59
|
+
|
|
60
|
+
func audioMix(for item: AVPlayerItem) -> AVAudioMix? {
|
|
61
|
+
buildCount += 1
|
|
62
|
+
return AVMutableAudioMix()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func noteActiveAsset(_ asset: AVAsset?) {}
|
|
66
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// `seek(toIndex:)` and `setItems` resume from the transport's intent, never
|
|
6
|
+
/// from a leg's rate or status.
|
|
7
|
+
///
|
|
8
|
+
/// `replaceCurrentItem` drops the player it is called on to rate 0, so each of
|
|
9
|
+
/// these paths has to put the rate back. Reading a leg to decide whether to is
|
|
10
|
+
/// wrong in two states the engine is routinely in: mid-fade, where the
|
|
11
|
+
/// outgoing leading leg has already stopped while the incoming one plays, and
|
|
12
|
+
/// at the end of an item, where the leg sits at rate 0 with the transport
|
|
13
|
+
/// still wanting playback. Either reading leaves the queue silently paused.
|
|
14
|
+
///
|
|
15
|
+
/// What is reachable here is the half that must NOT resume. The resuming half
|
|
16
|
+
/// needs a leg genuinely at rate 0 with playback still wanted, and only real
|
|
17
|
+
/// playback produces that: measured on the simulator, an item pointed at an
|
|
18
|
+
/// unreachable URL holds `timeControlStatus == .waitingToPlayAtSpecifiedRate`
|
|
19
|
+
/// indefinitely and never drops its rate, so every assertion built on one is
|
|
20
|
+
/// vacuous. The `consecutive-auto-advance` and `queue-end-then-set-queue` demo
|
|
21
|
+
/// suites cover both halves on both engines.
|
|
22
|
+
final class CrossfadePlaybackIntentTests: XCTestCase {
|
|
23
|
+
|
|
24
|
+
func testASeekAfterPauseStaysPaused() {
|
|
25
|
+
let engine = makeEngine(count: 3)
|
|
26
|
+
engine.play()
|
|
27
|
+
engine.pause()
|
|
28
|
+
|
|
29
|
+
engine.seek(toIndex: 1, position: 0)
|
|
30
|
+
|
|
31
|
+
XCTAssertEqual(
|
|
32
|
+
engine.timeControlStatus, .paused,
|
|
33
|
+
"a paused engine is not resumed by a seek")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func testAReinstallBeforeAnyPlayStaysPaused() {
|
|
37
|
+
let engine = makeEngine(count: 3)
|
|
38
|
+
|
|
39
|
+
engine.setItems(items(count: 3, from: 10), startIndex: 0, startPositionSeconds: 0)
|
|
40
|
+
|
|
41
|
+
XCTAssertEqual(
|
|
42
|
+
engine.timeControlStatus, .paused,
|
|
43
|
+
"an engine that was never told to play stays stopped")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func testASeekAfterStopStaysPaused() {
|
|
47
|
+
let engine = makeEngine(count: 3)
|
|
48
|
+
engine.play()
|
|
49
|
+
engine.stop()
|
|
50
|
+
|
|
51
|
+
engine.seek(toIndex: 2, position: 0)
|
|
52
|
+
|
|
53
|
+
XCTAssertEqual(engine.timeControlStatus, .paused)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// MARK: - Helpers
|
|
57
|
+
|
|
58
|
+
private func items(count: Int, from base: Int = 0) -> [AVPlayerItem] {
|
|
59
|
+
(0 ..< count).map { offset in
|
|
60
|
+
let index = base + offset
|
|
61
|
+
let item = AVPlayerItem(url: URL(string: "http://127.0.0.1:1/\(index).m4a")!)
|
|
62
|
+
item.queueItemId = "id-\(index)"
|
|
63
|
+
return item
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private func makeEngine(count: Int) -> CrossfadeEngine {
|
|
68
|
+
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0, confinedTo: .main)
|
|
69
|
+
engine.setItems(items(count: count), startIndex: 0, startPositionSeconds: 0)
|
|
70
|
+
engines.append(engine)
|
|
71
|
+
return engine
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private var engines: [CrossfadeEngine] = []
|
|
75
|
+
|
|
76
|
+
override func tearDown() {
|
|
77
|
+
engines.forEach { $0.release() }
|
|
78
|
+
engines.removeAll()
|
|
79
|
+
super.tearDown()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// A playback-mode swap installs the window on the new engine and always
|
|
6
|
+
/// releases the one it replaces.
|
|
7
|
+
final class EngineSwapTests: XCTestCase {
|
|
8
|
+
|
|
9
|
+
private var player: TrackPlayer!
|
|
10
|
+
|
|
11
|
+
override func setUp() async throws {
|
|
12
|
+
try await super.setUp()
|
|
13
|
+
player = TrackPlayer()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override func tearDown() async throws {
|
|
17
|
+
if let player {
|
|
18
|
+
_ = try await player.destroy().await()
|
|
19
|
+
}
|
|
20
|
+
player = nil
|
|
21
|
+
try await super.tearDown()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Nothing at or after the current position can be built, so the new engine
|
|
25
|
+
/// is left empty — and the old one still goes.
|
|
26
|
+
func testTheOldEngineIsReleasedWhenNothingCanBeSeated() async throws {
|
|
27
|
+
let player = try XCTUnwrap(self.player)
|
|
28
|
+
let old = ActiveItemEngineStub()
|
|
29
|
+
await player.onPlayerQueue {
|
|
30
|
+
_ = player.queueState.replaceAll([PlayerFixtures.track(id: "t0", url: "")])
|
|
31
|
+
player.currentTrackIndex = 0
|
|
32
|
+
player.engine = old
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await player.onPlayerQueue {
|
|
36
|
+
player.applyEngineSwapForMode(PlaybackMode(kind: .gapless, crossfadeDurationMs: nil))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
XCTAssertTrue(old.released, "the replaced engine is released whatever was seated")
|
|
40
|
+
let installed = await player.onPlayerQueueValue { player.engine?.allMediaItems.count ?? -1 }
|
|
41
|
+
XCTAssertEqual(installed, 0, "an unbuildable current track seats nothing")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func testTheOldEngineIsReleasedOnAnOrdinarySwap() async throws {
|
|
45
|
+
let player = try XCTUnwrap(self.player)
|
|
46
|
+
let old = ActiveItemEngineStub()
|
|
47
|
+
await player.onPlayerQueue {
|
|
48
|
+
_ = player.queueState.replaceAll([
|
|
49
|
+
PlayerFixtures.track(id: "t0", url: "https://example.com/0.m4a"),
|
|
50
|
+
PlayerFixtures.track(id: "t1", url: "https://example.com/1.m4a"),
|
|
51
|
+
])
|
|
52
|
+
player.currentTrackIndex = 0
|
|
53
|
+
player.engine = old
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await player.onPlayerQueue {
|
|
57
|
+
player.applyEngineSwapForMode(PlaybackMode(kind: .gapless, crossfadeDurationMs: nil))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
XCTAssertTrue(old.released)
|
|
61
|
+
let installed = await player.onPlayerQueueValue { player.engine?.allMediaItems.count ?? -1 }
|
|
62
|
+
XCTAssertEqual(installed, 2, "the window is installed on the new engine")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// MARK: - seatIndex
|
|
66
|
+
|
|
67
|
+
private func item(_ id: String) -> AVPlayerItem {
|
|
68
|
+
let item = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/\(id).mp3"))
|
|
69
|
+
item.queueItemId = id
|
|
70
|
+
return item
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func testTheSeatIsTheCurrentItemWhenItBuilt() {
|
|
74
|
+
let items = [item("a"), item("b"), item("c")]
|
|
75
|
+
XCTAssertEqual(
|
|
76
|
+
TrackPlayer.seatIndex(in: items, sliceIds: ["a", "b", "c"], currentId: "b"), 1)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// A current position whose item did not build seats on the next built
|
|
80
|
+
/// position forward, never on the trail behind it.
|
|
81
|
+
func testTheSeatMovesForwardPastAnUnbuiltCurrentItem() {
|
|
82
|
+
let items = [item("a"), item("c")]
|
|
83
|
+
XCTAssertEqual(
|
|
84
|
+
TrackPlayer.seatIndex(in: items, sliceIds: ["a", "b", "c"], currentId: "b"), 1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func testThereIsNoSeatWhenNothingAheadBuilt() {
|
|
88
|
+
let items = [item("a")]
|
|
89
|
+
XCTAssertNil(TrackPlayer.seatIndex(in: items, sliceIds: ["a", "b", "c"], currentId: "b"))
|
|
90
|
+
}
|
|
91
|
+
}
|