react-native-queue-player 1.2.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/consumer-rules.pro +1 -1
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +3 -19
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +8 -69
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +2 -2
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +672 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +229 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaResponseCheck.swift +122 -0
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MediaResponseCheckTests.swift +128 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +418 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +441 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +982 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +951 -0
- package/ios/TrackPlayer+Recovery.swift +213 -0
- package/ios/TrackPlayer+Skip.swift +465 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +272 -0
- package/ios/TrackPlayer+Window.swift +227 -0
- package/ios/TrackPlayer.swift +331 -4619
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
|
|
4
|
+
extension TrackPlayer {
|
|
5
|
+
// MARK: - Transport — pass-through to AVQueuePlayer.
|
|
6
|
+
|
|
7
|
+
func play() throws -> Promise<Void> {
|
|
8
|
+
return enqueue {
|
|
9
|
+
if CastTransportRouter.routePlay() { return }
|
|
10
|
+
// Stuck-recovery: the queue holds on a track whose failure was reported,
|
|
11
|
+
// and pressing play is asking for that track again — so it gets the
|
|
12
|
+
// allowance the consumer configured back and a rebuild. Not a hard-coded
|
|
13
|
+
// 1: `autoRetries: 0` asked for none.
|
|
14
|
+
if let qid = self.reportedFailureOnCurrentTrack {
|
|
15
|
+
// Asking for this track to play is incompatible with the queue being
|
|
16
|
+
// over, so `reachedQueueEnd` cannot survive it.
|
|
17
|
+
self.reachedQueueEnd = false
|
|
18
|
+
self.retryAttemptsRemaining[qid] = self.effectiveAutoRetries()
|
|
19
|
+
// This track only. A standby leg's report is a different track's, and
|
|
20
|
+
// discarding it here would hand the listener that failure a second
|
|
21
|
+
// time when the engine seats it.
|
|
22
|
+
self.reportedFailureQueueItemIds.remove(qid)
|
|
23
|
+
// A scheduled attempt for this item is superseded by the rebuild below.
|
|
24
|
+
// Reachable because the two are set by different errors on the same
|
|
25
|
+
// item: a reported failure leaves the report standing, and a later
|
|
26
|
+
// failure carrying a different code is transient enough to schedule an
|
|
27
|
+
// attempt. Leaving it would fire a second rebuild behind this one.
|
|
28
|
+
if self.pendingRetryQueueItemId == qid { self.pendingRetryQueueItemId = nil }
|
|
29
|
+
self.lastErrorQueueItemId = nil
|
|
30
|
+
self.lastErrorCode = nil
|
|
31
|
+
// Rebuild whenever an engine is live — the rebuild is
|
|
32
|
+
// engine-agnostic, and the failed item has to be
|
|
33
|
+
// reconstructed on either engine for the retry to reach it.
|
|
34
|
+
// Forced: the failed item keeps its `queueItemId`, so a rebuild
|
|
35
|
+
// that compares identity would find nothing to do.
|
|
36
|
+
if self.engine != nil {
|
|
37
|
+
self.performingMutation {
|
|
38
|
+
self.fullRebuildPlayerQueue(forcingReinstall: true)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// At the end of the queue there is nothing to play: the engine is
|
|
43
|
+
// parked on the finished track (or drained), and writing a rate onto
|
|
44
|
+
// it would report `.playing` for a moment or, on some OS versions,
|
|
45
|
+
// re-post the item's end. A skip, a seek or a fresh queue leaves this
|
|
46
|
+
// state; play() alone restarts nothing.
|
|
47
|
+
if self.reachedQueueEnd, self.currentTrackIndex >= 0 { return }
|
|
48
|
+
self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
|
|
49
|
+
self.wantsToPlay = true
|
|
50
|
+
// An explicit play ends an interruption from the user's side: the
|
|
51
|
+
// system does not always deliver `.ended` (an app suspended mid-call
|
|
52
|
+
// never sees it), and the flag would otherwise keep retries, window
|
|
53
|
+
// refills and network recovery disabled for the rest of the session.
|
|
54
|
+
self.isInterrupted = false
|
|
55
|
+
self.beginPlayback()
|
|
56
|
+
self.recomputeBufferState()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func retry() throws -> Promise<Void> {
|
|
61
|
+
return enqueue {
|
|
62
|
+
// During cast the receiver owns playback — forward a play so a failed
|
|
63
|
+
// receiver item re-attempts.
|
|
64
|
+
if CastTransportRouter.routePlay() { return }
|
|
65
|
+
// A user asking for a retry has a reason, and the library is not the one
|
|
66
|
+
// to judge it. This is the manual attempt once the automatic one is
|
|
67
|
+
// spent, so gating it on the player's state would gate recovery on the
|
|
68
|
+
// state of a player that has already failed to recover — and a wedged
|
|
69
|
+
// decoder still reporting `playing` is exactly when someone reaches for
|
|
70
|
+
// the button.
|
|
71
|
+
//
|
|
72
|
+
// Only the bounds remain: no current track is genuinely nothing to
|
|
73
|
+
// retry, which is not second-guessing the request.
|
|
74
|
+
guard self.currentTrackIndex >= 0,
|
|
75
|
+
self.currentTrackIndex < self.queueItemIds.count else { return }
|
|
76
|
+
let qid = self.queueItemIds[self.currentTrackIndex]
|
|
77
|
+
// Capture the pre-failure position so a mid-track failure resumes where
|
|
78
|
+
// it stopped (the rebuild below reloads the item from 0). Matches
|
|
79
|
+
// Android's retryFailedItem position-restore.
|
|
80
|
+
let resumePos = self.engine?.currentPositionSeconds ?? 0
|
|
81
|
+
// Asking for this track to play is incompatible with the queue being over,
|
|
82
|
+
// and a queue whose only track failed to load has already drained: without
|
|
83
|
+
// this the rebuild seats an item into a queue that believes it is finished.
|
|
84
|
+
self.reachedQueueEnd = false
|
|
85
|
+
// The configured budget, not a hard-coded 1: this restores the track's
|
|
86
|
+
// automatic allowance for whatever fails *next*, and a consumer who set
|
|
87
|
+
// `autoRetries: 0` asked for none.
|
|
88
|
+
self.retryAttemptsRemaining[qid] = self.effectiveAutoRetries()
|
|
89
|
+
// A scheduled attempt for this item is superseded by the rebuild below;
|
|
90
|
+
// leaving it set would suppress the rebuilt item's own failure until the
|
|
91
|
+
// earlier attempt fired.
|
|
92
|
+
if self.pendingRetryQueueItemId == qid { self.pendingRetryQueueItemId = nil }
|
|
93
|
+
self.lastErrorQueueItemId = nil
|
|
94
|
+
self.lastErrorCode = nil
|
|
95
|
+
// This track only, for the reason `play()` gives above.
|
|
96
|
+
self.reportedFailureQueueItemIds.remove(qid)
|
|
97
|
+
// Rebuild whenever an engine is live — the rebuild is
|
|
98
|
+
// engine-agnostic, and the failed item has to be reconstructed
|
|
99
|
+
// on either engine for the retry to reach it. Forced: the failed
|
|
100
|
+
// item keeps its `queueItemId`, so a rebuild that compares
|
|
101
|
+
// identity would find nothing to do.
|
|
102
|
+
if self.engine != nil {
|
|
103
|
+
self.performingMutation {
|
|
104
|
+
self.fullRebuildPlayerQueue(forcingReinstall: true)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
|
|
108
|
+
self.wantsToPlay = true
|
|
109
|
+
self.beginPlayback()
|
|
110
|
+
if resumePos.isFinite, resumePos > 0 {
|
|
111
|
+
_ = try? self.seekTo(position: resumePos)
|
|
112
|
+
}
|
|
113
|
+
self.recomputeBufferState()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func pause() throws -> Promise<Void> {
|
|
118
|
+
return enqueue {
|
|
119
|
+
if CastTransportRouter.routePause() { return }
|
|
120
|
+
self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
|
|
121
|
+
self.wantsToPlay = false
|
|
122
|
+
// An explicit pause outranks a pending stall recovery: without this the
|
|
123
|
+
// buffer refilling afterwards would restart audio the user just stopped.
|
|
124
|
+
self.isRecoveringFromStall = false
|
|
125
|
+
self.engine?.pause()
|
|
126
|
+
self.recomputeBufferState()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// "Stop" = pause + seek to start of current item. Does NOT clear
|
|
131
|
+
/// the queue — consumers wanting a full reset call clearQueue().
|
|
132
|
+
func stop() throws -> Promise<Void> {
|
|
133
|
+
return enqueue {
|
|
134
|
+
if CastTransportRouter.routeStop() { return }
|
|
135
|
+
guard let engine = self.engine else { return }
|
|
136
|
+
self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
|
|
137
|
+
self.wantsToPlay = false
|
|
138
|
+
self.hasStartedPlaying = false
|
|
139
|
+
engine.stop()
|
|
140
|
+
self.recomputeBufferState()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Seek within the current track. Position in seconds; negative
|
|
145
|
+
/// values clamp to 0, values past the duration clamp to duration.
|
|
146
|
+
func seekTo(position: Double) throws -> Promise<Void> {
|
|
147
|
+
// Enqueued at call time, like every other transport method, so a seek
|
|
148
|
+
// reaches the player in the order JS issued it. The promise resolves once
|
|
149
|
+
// the seek's own completion has landed, which is why this builds the
|
|
150
|
+
// promise by hand rather than using `enqueue`.
|
|
151
|
+
let promise = Promise<Void>()
|
|
152
|
+
playerQueue.async {
|
|
153
|
+
// Match Android (`TrackPlayer.kt` seekToInternal): non-finite
|
|
154
|
+
// (NaN / ±Infinity) inputs are silent no-ops. See
|
|
155
|
+
// `InputGuards.sanitisedSeekPosition` for the rationale.
|
|
156
|
+
guard let position = InputGuards.sanitisedSeekPosition(position) else {
|
|
157
|
+
promise.resolve()
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if CastTransportRouter.routeSeekTo(positionMs: Int64(position * 1000.0)) {
|
|
161
|
+
promise.resolve()
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Every player + queue read and the seek itself run on this one hop, so
|
|
166
|
+
// the player never crosses a concurrency boundary. The return value
|
|
167
|
+
// carries whether a seek was issued: the crossfade branch and the
|
|
168
|
+
// drained-gapless branch complete in place and yield false.
|
|
169
|
+
//
|
|
170
|
+
// Named rather than inlined so the two early-return branches and the
|
|
171
|
+
// seek branch share one exit.
|
|
172
|
+
let issueSeek: () -> Bool = {
|
|
173
|
+
// Engine-aware seek: under CrossfadeEngine `self.player` is
|
|
174
|
+
// nil. Without this branch every seek call would silently
|
|
175
|
+
// no-op once crossfade mode is active, breaking transport
|
|
176
|
+
// controls + auto-advance test coverage.
|
|
177
|
+
if self.player == nil {
|
|
178
|
+
guard let engine = self.engine else { return false }
|
|
179
|
+
// Seeking replays from a position — no longer at the end of the
|
|
180
|
+
// queue. (Gapless no-ops at end-of-queue with no current item below.)
|
|
181
|
+
self.reachedQueueEnd = false
|
|
182
|
+
let duration = engine.currentDurationSeconds
|
|
183
|
+
let clamped: Double
|
|
184
|
+
if position < 0 {
|
|
185
|
+
clamped = 0
|
|
186
|
+
} else {
|
|
187
|
+
clamped = (duration > 0 && position > duration) ? duration : position
|
|
188
|
+
}
|
|
189
|
+
let seekTarget = self.clampToBufferedIfEnabled(clamped)
|
|
190
|
+
self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
|
|
191
|
+
let engineIdx: Int
|
|
192
|
+
if let installed = self.engineIndex(forQueuePosition: self.currentTrackIndex) {
|
|
193
|
+
engineIdx = installed
|
|
194
|
+
} else {
|
|
195
|
+
// Active track absent from the engine — reinstall it, then seek
|
|
196
|
+
// within the reinstalled run so the position is applied rather
|
|
197
|
+
// than dropped with a resolved promise.
|
|
198
|
+
self.performingMutation { self.fullRebuildPlayerQueue() }
|
|
199
|
+
guard let reinstalled = self.engineIndex(forQueuePosition: self.currentTrackIndex)
|
|
200
|
+
else { return false }
|
|
201
|
+
engineIdx = reinstalled
|
|
202
|
+
}
|
|
203
|
+
engine.seek(toIndex: engineIdx, position: seekTarget)
|
|
204
|
+
self.milestoneTracker.seek(
|
|
205
|
+
durationSec: self.effectiveMilestoneDuration(playerDuration: duration),
|
|
206
|
+
positionSec: seekTarget)
|
|
207
|
+
self.nowPlayingInfo.refreshPositionAndRate(self.nowPlayingSnapshot())
|
|
208
|
+
return false
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
guard let player = self.player else { return false }
|
|
212
|
+
// A gapless player drained at the end of the queue has no item to
|
|
213
|
+
// seek in: reinstall the finished track first, as a same-index skip
|
|
214
|
+
// does, so a seek after the end replays it on every engine. Seeking
|
|
215
|
+
// is what leaves the ended state, so the flag clears here.
|
|
216
|
+
if player.currentItem == nil, self.reachedQueueEnd,
|
|
217
|
+
self.currentTrackIndex >= 0, self.currentTrackIndex < self.tracks.count {
|
|
218
|
+
self.reachedQueueEnd = false
|
|
219
|
+
self.performingMutation { self.fullRebuildPlayerQueue() }
|
|
220
|
+
}
|
|
221
|
+
guard let item = player.currentItem else { return false }
|
|
222
|
+
let itemDuration = CMTimeGetSeconds(item.duration)
|
|
223
|
+
let clamped: Double
|
|
224
|
+
if position < 0 {
|
|
225
|
+
clamped = 0
|
|
226
|
+
} else {
|
|
227
|
+
clamped = (itemDuration.isFinite && position > itemDuration) ? itemDuration : position
|
|
228
|
+
}
|
|
229
|
+
// Apply the opt-in buffered clamp on top of the 0/duration clamp — caps
|
|
230
|
+
// the target at the buffered edge when the seek is past what's downloaded.
|
|
231
|
+
let seekTarget = self.clampToBufferedIfEnabled(clamped)
|
|
232
|
+
// Consume jumped-over milestones BEFORE issuing the (async) seek so a
|
|
233
|
+
// time-jump progress tick that fires during the seek can't emit the
|
|
234
|
+
// thresholds the seek skips over.
|
|
235
|
+
self.milestoneTracker.seek(
|
|
236
|
+
durationSec: self.effectiveMilestoneDuration(
|
|
237
|
+
playerDuration: (itemDuration.isFinite && itemDuration > 0) ? itemDuration : 0),
|
|
238
|
+
positionSec: seekTarget)
|
|
239
|
+
// Stash `.user` reason BEFORE the seek so any state transition
|
|
240
|
+
// the seek triggers (e.g. playable → buffering on a long jump)
|
|
241
|
+
// gets the correct origin. Matches the play / pause / stop pattern.
|
|
242
|
+
self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
|
|
243
|
+
player.seek(
|
|
244
|
+
to: CMTime(seconds: seekTarget, preferredTimescale: CMTimeScale(NSEC_PER_SEC)),
|
|
245
|
+
toleranceBefore: .zero,
|
|
246
|
+
toleranceAfter: .zero
|
|
247
|
+
) { _ in
|
|
248
|
+
// This closure captures `player`, so a teardown that drops every
|
|
249
|
+
// other reference cannot deallocate it with the seek still pending.
|
|
250
|
+
// AVFoundation invokes the handler when the seek completes or is
|
|
251
|
+
// interrupted, and the capture is released with it.
|
|
252
|
+
_ = player
|
|
253
|
+
self.playerQueue.async {
|
|
254
|
+
// Surface the new scrubber position to the lock-screen /
|
|
255
|
+
// control-center immediately. The KVO state-change emit covers
|
|
256
|
+
// transitions (e.g. paused → buffering on a long jump) but a
|
|
257
|
+
// same-state seek won't trigger an emit; without this the
|
|
258
|
+
// lock-screen scrubber sits at the pre-seek position until the
|
|
259
|
+
// next 10s periodic tick.
|
|
260
|
+
self.nowPlayingInfo.refreshPositionAndRate(self.nowPlayingSnapshot())
|
|
261
|
+
promise.resolve()
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return true
|
|
265
|
+
}
|
|
266
|
+
// A branch that completed in place has nothing pending, so the promise
|
|
267
|
+
// resolves here; the seek branch resolves from its completion instead.
|
|
268
|
+
if !issueSeek() { promise.resolve() }
|
|
269
|
+
}
|
|
270
|
+
return promise
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
extension TrackPlayer {
|
|
2
|
+
/// Bring the engine's installed run back in line as the playhead moves.
|
|
3
|
+
///
|
|
4
|
+
/// Reconciling against `QueueWindowArithmetic.slice` rather than measuring
|
|
5
|
+
/// from an anchor: a run that wraps ends at a lower absolute position than
|
|
6
|
+
/// it starts, so any comparison of the two as integers reads it as
|
|
7
|
+
/// backwards. The reconcile keeps what is still wanted, drops what has
|
|
8
|
+
/// fallen off the trail, and builds the rest.
|
|
9
|
+
internal func topUpWindow() {
|
|
10
|
+
guard let engine = self.engine else { return }
|
|
11
|
+
// Every branch of the reconcile changes the installed items, and each of
|
|
12
|
+
// those cancels an in-flight fade. Extending the run can wait for the fade
|
|
13
|
+
// to finish; cutting a crossfade short to do it cannot be undone.
|
|
14
|
+
guard !engine.isFadePendingOrActive else { return }
|
|
15
|
+
// This extends a run; it never creates one. A drained engine is either a
|
|
16
|
+
// queue that has ended or one the caller is about to seat itself, and
|
|
17
|
+
// installing into it here would put the finished track back and let the
|
|
18
|
+
// rate the engine restores carry it on playing.
|
|
19
|
+
guard !engine.allMediaItems.isEmpty, !self.reachedQueueEnd else { return }
|
|
20
|
+
// A current position that has failed is not put back, whether its failure
|
|
21
|
+
// has been reported or an attempt for it is still scheduled. `AVQueuePlayer`
|
|
22
|
+
// consumes a failed item, and when the run holds something behind it the
|
|
23
|
+
// player advances there instead of draining — so the end verdict never runs
|
|
24
|
+
// and this reconcile is what rebuilds. It rebuilds from `currentTrackIndex`,
|
|
25
|
+
// still the failed position because the resync needs a play-to-end and a
|
|
26
|
+
// failure fires `AVPlayerItemFailedToPlayToEndTime` instead, and a reinstall
|
|
27
|
+
// is exactly what a missing current position asks for. The rebuilt item
|
|
28
|
+
// fails the same way and its error is swallowed — as a repeat, or as an echo
|
|
29
|
+
// of the pending attempt — so the rebuild would repeat unannounced.
|
|
30
|
+
guard self.reportedFailureOnCurrentTrack == nil else { return }
|
|
31
|
+
if let currentId = self.queueItemIds[safe: self.currentTrackIndex],
|
|
32
|
+
self.pendingRetryQueueItemId == currentId {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
self.performingMutation { self.fullRebuildPlayerQueue() }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Notices that the gapless player has auto-advanced past its last item,
|
|
39
|
+
/// and hands the decision to `applyEndVerdict`.
|
|
40
|
+
internal func detectQueueEnd() {
|
|
41
|
+
// Engine-aware "is the active item nil" check. With
|
|
42
|
+
// CrossfadeEngine `self.player` is nil by design — reading it
|
|
43
|
+
// alone would mis-detect every mid-play engine swap as
|
|
44
|
+
// queue-end and fire a spurious onQueueEnd + emitState(.ended).
|
|
45
|
+
let activeItemNil: Bool
|
|
46
|
+
if self.player != nil {
|
|
47
|
+
activeItemNil = self.player?.currentItem == nil
|
|
48
|
+
} else if let engine = self.engine {
|
|
49
|
+
activeItemNil = engine.currentMediaItem == nil
|
|
50
|
+
} else {
|
|
51
|
+
activeItemNil = true
|
|
52
|
+
}
|
|
53
|
+
guard activeItemNil else { return }
|
|
54
|
+
applyEndVerdict()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Play `currentTrackIndex` from its start and announce it.
|
|
58
|
+
///
|
|
59
|
+
/// A rebuild replaces only the positions that differ, so it cannot be relied
|
|
60
|
+
/// on for either half of this: when the position being re-seated is already
|
|
61
|
+
/// the one installed, nothing is replaced, the seek is what restarts it, and
|
|
62
|
+
/// no `currentItem` change fires to carry the track change out. Dispatching
|
|
63
|
+
/// here is deduped against `lastEmittedQueueItemId`, so a rebuild that *did*
|
|
64
|
+
/// replace the item still emits exactly once.
|
|
65
|
+
private func restartCurrentPosition() {
|
|
66
|
+
if let engine = self.engine,
|
|
67
|
+
let engineIdx = engineIndex(forQueuePosition: self.currentTrackIndex) {
|
|
68
|
+
engine.seek(toIndex: engineIdx, position: 0)
|
|
69
|
+
}
|
|
70
|
+
dispatchTrackChange(reason: .autoAdvance)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Decide and apply what a drained engine means for the queue.
|
|
74
|
+
///
|
|
75
|
+
/// Both ends funnel here — the gapless player's `currentItem` going nil, and
|
|
76
|
+
/// the crossfade engine reporting its last item played out — so the queue
|
|
77
|
+
/// reaches the same verdict whichever engine is installed. The engines
|
|
78
|
+
/// report that they drained; they do not decide whether the queue is over.
|
|
79
|
+
///
|
|
80
|
+
/// A drained engine exposes no items, so the anchor cannot be derived from
|
|
81
|
+
/// them. `matchTrackIndex(forCurrentItem: nil)` is nil at a drain, which
|
|
82
|
+
/// leaves `currentTrackIndex` on the track that just finished — the furthest
|
|
83
|
+
/// position the queue actually reached.
|
|
84
|
+
internal func applyEndVerdict() {
|
|
85
|
+
// `verdict` is specified against a queue that has something to end.
|
|
86
|
+
guard !self.tracks.isEmpty else { return }
|
|
87
|
+
// A queue that has ended stays ended: a further drain report — a rate
|
|
88
|
+
// written onto a leg parked at its end re-posts the item's end — must
|
|
89
|
+
// not re-decide against a queue that has since grown and seat the added
|
|
90
|
+
// track. A skip, a seek or a fresh queue clears the flag first.
|
|
91
|
+
guard !self.reachedQueueEnd else { return }
|
|
92
|
+
// A drain whose current item failed to load with an auto-retry already
|
|
93
|
+
// scheduled is not the end of the queue — the pending retry rebuilds and
|
|
94
|
+
// resumes that item. `AVQueuePlayer` consumes a failed item (its
|
|
95
|
+
// `actionAtItemEnd` is `.advance`) and drains to `currentItem == nil`,
|
|
96
|
+
// which lands here; deciding a queue-end would clear the play intent the
|
|
97
|
+
// retry reads, leaving the recovered track paused. The scheduled retry
|
|
98
|
+
// clears `pendingRetryQueueItemId` and re-seats the item, so this only
|
|
99
|
+
// holds for the brief window between the failure and the attempt.
|
|
100
|
+
//
|
|
101
|
+
// Relies on the item's `.failed` status KVO — which stakes
|
|
102
|
+
// `pendingRetryQueueItemId` in `dispatchErrorOrRetry` — being delivered
|
|
103
|
+
// before the `currentItem`→nil drain KVO that reaches here. The status
|
|
104
|
+
// change is what causes the advance-to-nil, so it is observed first. The
|
|
105
|
+
// `retry-recovery` demo suite is the integration coverage for that order.
|
|
106
|
+
if let currentId = self.queueItemIds[safe: self.currentTrackIndex],
|
|
107
|
+
self.pendingRetryQueueItemId == currentId {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
// A drain that follows the current item's reported failure is that failure,
|
|
111
|
+
// not the end of the queue. The same `.advance` walks a failed item out of
|
|
112
|
+
// the player, and with nothing installed behind it the queue drains to
|
|
113
|
+
// `currentItem == nil` and lands here. Deciding a verdict would move past a
|
|
114
|
+
// track the consumer has just been told failed — and when the tracks behind
|
|
115
|
+
// it fail the same way, the advance, rebuild and resume repeat with nothing
|
|
116
|
+
// counting them, wrapping forever under repeat-all. The queue stays where it
|
|
117
|
+
// is, in `.error`, until `play()` or `retry()` asks for the track again.
|
|
118
|
+
if self.reportedFailureOnCurrentTrack != nil {
|
|
119
|
+
// Park the engine, as the queue-end branch below does and for the same
|
|
120
|
+
// reason: neither engine drops its rate when it runs out of items, so a
|
|
121
|
+
// leg left running would start whatever lands on it next. The gapless
|
|
122
|
+
// failure path pauses the `AVQueuePlayer` itself, but the crossfade one
|
|
123
|
+
// has no `AVQueuePlayer` to pause.
|
|
124
|
+
self.engine?.pause()
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
var verdict = QueueWindowArithmetic.verdict(
|
|
128
|
+
drained: true,
|
|
129
|
+
atLogicalTail: QueueWindowArithmetic.isAtLogicalTail(
|
|
130
|
+
anchorPosition: self.currentTrackIndex, trackCount: self.tracks.count),
|
|
131
|
+
repeatMode: repeatModeAsSkipMode())
|
|
132
|
+
|
|
133
|
+
// Re-decided in place when the position a verdict seats cannot be built:
|
|
134
|
+
// `.windowDrained` advances the anchor each time round, so it reaches the
|
|
135
|
+
// tail; `.refillCurrent` cannot advance (repeat-track re-seats the same
|
|
136
|
+
// position), so an unplayable current track ends the queue instead.
|
|
137
|
+
while true {
|
|
138
|
+
switch verdict {
|
|
139
|
+
case .notEnded:
|
|
140
|
+
return
|
|
141
|
+
case .wrapToStart:
|
|
142
|
+
self.currentTrackIndex = 0
|
|
143
|
+
self.pendingTrackChangeReason = .autoAdvance
|
|
144
|
+
// The queueItemId at index 0 is unchanged across a single-track wrap,
|
|
145
|
+
// so without this clear the next dispatch would dedup-swallow the
|
|
146
|
+
// re-emission and the consumer would see no track change.
|
|
147
|
+
self.lastEmittedQueueItemId = nil
|
|
148
|
+
self.performingMutation { self.fullRebuildPlayerQueue() }
|
|
149
|
+
// Restart position 0 explicitly: on a single-track queue the wrap target
|
|
150
|
+
// is the item already installed, so the rebuild replaces nothing.
|
|
151
|
+
restartCurrentPosition()
|
|
152
|
+
// Each engine restores its own rate across a rebuild only while its
|
|
153
|
+
// play intent stands, and a reconcile that removes the item the
|
|
154
|
+
// crossfade engine's leading leg held drops that intent with it — so
|
|
155
|
+
// the resume is asked for here. Gated on the transport intent because
|
|
156
|
+
// a rebuild cannot tell "was playing" from "was paused" by rate alone,
|
|
157
|
+
// and on the interruption flag because the system may not permit
|
|
158
|
+
// playback right now.
|
|
159
|
+
if self.wantsToPlay, !self.isInterrupted { self.beginPlayback() }
|
|
160
|
+
return
|
|
161
|
+
case .windowDrained, .refillCurrent:
|
|
162
|
+
// The engine ran out with queue left. `currentTrackIndex` still names
|
|
163
|
+
// the track that just finished, so `.windowDrained` moves past it while
|
|
164
|
+
// `.refillCurrent` re-seats it. Clearing `lastEmittedQueueItemId` keeps
|
|
165
|
+
// the refilled track from being dedup-swallowed, and the drained player
|
|
166
|
+
// has parked at rate 0, so it has to be told to resume.
|
|
167
|
+
if verdict == .windowDrained {
|
|
168
|
+
self.currentTrackIndex = min(self.currentTrackIndex + 1, self.tracks.count - 1)
|
|
169
|
+
}
|
|
170
|
+
self.pendingTrackChangeReason = .autoAdvance
|
|
171
|
+
self.lastEmittedQueueItemId = nil
|
|
172
|
+
self.performingMutation { self.fullRebuildPlayerQueue() }
|
|
173
|
+
self.rescheduleLookahead()
|
|
174
|
+
// A position whose item cannot be built is absent from the installed
|
|
175
|
+
// run, and no engine signal follows to re-enter on. Asking whether the
|
|
176
|
+
// run holds *this* position rather than whether the engine holds
|
|
177
|
+
// anything: the run reaches behind the playhead, so an unplayable
|
|
178
|
+
// current position still leaves the trail installed.
|
|
179
|
+
//
|
|
180
|
+
// Re-decide rather than parking mid-queue.
|
|
181
|
+
if engineIndex(forQueuePosition: self.currentTrackIndex) == nil {
|
|
182
|
+
if verdict == .refillCurrent {
|
|
183
|
+
verdict = .queueEnd
|
|
184
|
+
} else {
|
|
185
|
+
verdict = QueueWindowArithmetic.verdict(
|
|
186
|
+
drained: true,
|
|
187
|
+
atLogicalTail: QueueWindowArithmetic.isAtLogicalTail(
|
|
188
|
+
anchorPosition: self.currentTrackIndex, trackCount: self.tracks.count),
|
|
189
|
+
repeatMode: repeatModeAsSkipMode())
|
|
190
|
+
}
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
// Both verdicts need the explicit restart the wrap does — after the
|
|
194
|
+
// guard above, so a position that could not be built is never announced
|
|
195
|
+
// as a track change. An engine that retains its run rebuilds by
|
|
196
|
+
// extending it, which never re-seats the leg: it would still hold the
|
|
197
|
+
// item that just ended, and the resume below would write a rate onto a
|
|
198
|
+
// track already at its end. On an engine that consumes its items the
|
|
199
|
+
// rebuild has reinstalled from the anchor already, and this seeks to a
|
|
200
|
+
// position it is on.
|
|
201
|
+
restartCurrentPosition()
|
|
202
|
+
// Playback is asked to resume for the same reason as the wrap above,
|
|
203
|
+
// and only if it was running: neither engine drops its rate when it
|
|
204
|
+
// runs out of items, so rate cannot tell "was playing" from "was
|
|
205
|
+
// paused" and the transport intent is what does. Not while the system
|
|
206
|
+
// has playback interrupted — the refill must not start audio during a
|
|
207
|
+
// call, nor after one the user declined to resume.
|
|
208
|
+
if self.wantsToPlay, !self.isInterrupted { self.beginPlayback() }
|
|
209
|
+
return
|
|
210
|
+
case .queueEnd:
|
|
211
|
+
// Park the engine. Draining is not the same as being stopped — neither
|
|
212
|
+
// engine drops its rate when it runs out of items, so a leg left running
|
|
213
|
+
// would start the next queue installed on it the moment an item lands.
|
|
214
|
+
// The other verdicts deliberately resume instead, just below.
|
|
215
|
+
self.engine?.pause()
|
|
216
|
+
// The intent to play ends with the queue: nothing that seats a track
|
|
217
|
+
// from here — a skip, a seek, a fresh queue, an engine swap — starts
|
|
218
|
+
// it until play() is called again.
|
|
219
|
+
self.wantsToPlay = false
|
|
220
|
+
self.reachedQueueEnd = true
|
|
221
|
+
queueEndListeners.forEach { $0() }
|
|
222
|
+
emitState(.ended, reason: .queueEnd)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|