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,175 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
|
|
4
|
+
extension TrackPlayer {
|
|
5
|
+
func setSleepTimer(seconds: Double) throws -> Promise<Void> {
|
|
6
|
+
return enqueue { self.setSleepTimerInternal(seconds: seconds) }
|
|
7
|
+
}
|
|
8
|
+
func setSleepTimerToTrackEnd() throws -> Promise<Void> {
|
|
9
|
+
return enqueue { self.armSleepTimerToTrackEndInternal() }
|
|
10
|
+
}
|
|
11
|
+
func clearSleepTimer() throws -> Promise<Void> {
|
|
12
|
+
return enqueue { self.clearSleepTimerInternal() }
|
|
13
|
+
}
|
|
14
|
+
func getSleepTimer() throws -> SleepTimerState {
|
|
15
|
+
return onPlayerQueueSync { self.sleepTimerSnapshot() }
|
|
16
|
+
}
|
|
17
|
+
func onSleepTimerChange(
|
|
18
|
+
callback: @escaping (_ state: SleepTimerState) -> Void
|
|
19
|
+
) throws -> () -> Void {
|
|
20
|
+
let id = sleepTimerListeners.add(callback)
|
|
21
|
+
playerQueue.async { [weak self] in
|
|
22
|
+
guard let self else { return }
|
|
23
|
+
guard self.sleepTimerListeners.contains(id: id) else { return }
|
|
24
|
+
callback(self.sleepTimerSnapshot())
|
|
25
|
+
}
|
|
26
|
+
return { [weak self] in _ = self?.sleepTimerListeners.remove(id: id) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// MARK: - Sleep timer — internal
|
|
30
|
+
|
|
31
|
+
/// Arm a wall-clock duration timer; a non-positive value clears any timer.
|
|
32
|
+
private func setSleepTimerInternal(seconds: Double) {
|
|
33
|
+
guard seconds.isFinite, seconds > 0 else {
|
|
34
|
+
clearSleepTimerInternal()
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
restoreSleepTimerVolume()
|
|
38
|
+
sleepTimerCore.armDuration(seconds: seconds, nowMs: Self.nowEpochMs())
|
|
39
|
+
startSleepTimerTick()
|
|
40
|
+
emitSleepTimerChanged()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private func armSleepTimerToTrackEndInternal() {
|
|
44
|
+
restoreSleepTimerVolume()
|
|
45
|
+
sleepTimerCore.armEndOfTrack()
|
|
46
|
+
startSleepTimerTick()
|
|
47
|
+
emitSleepTimerChanged()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private func clearSleepTimerInternal() {
|
|
51
|
+
let wasActive = sleepTimerCore.isActive
|
|
52
|
+
restoreSleepTimerVolume()
|
|
53
|
+
sleepTimerCore.clear()
|
|
54
|
+
stopSleepTimerTick()
|
|
55
|
+
if wasActive { emitSleepTimerChanged() }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private func sleepTimerSnapshot() -> SleepTimerState {
|
|
59
|
+
let now = Self.nowEpochMs()
|
|
60
|
+
return SleepTimerState(
|
|
61
|
+
active: sleepTimerCore.isActive,
|
|
62
|
+
endsAtEpochMs: sleepTimerCore.deadlineEpochMs.map { Double($0) },
|
|
63
|
+
remainingSeconds: sleepTimerCore.remainingSeconds(nowMs: now).map { Double($0) },
|
|
64
|
+
endOfTrack: sleepTimerCore.isEndOfTrack)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private func emitSleepTimerChanged() {
|
|
68
|
+
let snapshot = sleepTimerSnapshot()
|
|
69
|
+
sleepTimerListeners.forEach { $0(snapshot) }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private func startSleepTimerTick() {
|
|
73
|
+
guard sleepTimerTimer == nil else { return }
|
|
74
|
+
let timer = DispatchSource.makeTimerSource(queue: playerQueue)
|
|
75
|
+
timer.schedule(
|
|
76
|
+
deadline: .now() + Self.sleepTimerTickInterval,
|
|
77
|
+
repeating: Self.sleepTimerTickInterval)
|
|
78
|
+
timer.setEventHandler { [weak self] in
|
|
79
|
+
self?.onSleepTimerTick()
|
|
80
|
+
}
|
|
81
|
+
sleepTimerTimer = timer
|
|
82
|
+
// The source is resumed exactly once and never suspended — a suspended
|
|
83
|
+
// source traps libdispatch on release.
|
|
84
|
+
timer.resume()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Caller must be on `playerQueue` — cancelling from the source's own
|
|
88
|
+
/// queue is what guarantees no further tick.
|
|
89
|
+
internal func stopSleepTimerTick() {
|
|
90
|
+
sleepTimerTimer?.cancel()
|
|
91
|
+
sleepTimerTimer = nil
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private func onSleepTimerTick() {
|
|
95
|
+
// While casting, the local engine is intentionally frozen (its position
|
|
96
|
+
// never advances), so feed the state machine the receiver's position /
|
|
97
|
+
// duration instead — otherwise the end-of-track tail rule never trips and
|
|
98
|
+
// `.endOfTrack` stays idle forever.
|
|
99
|
+
let durationSec: Double
|
|
100
|
+
let positionSec: Double
|
|
101
|
+
if let remote = PlaybackStateRouter.shared.activeRemote() {
|
|
102
|
+
let d = Double(remote.lastDurationMs) / 1000.0
|
|
103
|
+
let p = Double(remote.lastPositionMs) / 1000.0
|
|
104
|
+
durationSec = (d.isFinite && d > 0) ? d : 0
|
|
105
|
+
positionSec = (p.isFinite && p > 0) ? p : 0
|
|
106
|
+
} else {
|
|
107
|
+
let duration = self.engine?.currentDurationSeconds ?? 0
|
|
108
|
+
let position = self.engine?.currentPositionSeconds ?? 0
|
|
109
|
+
durationSec = (duration.isFinite && duration > 0) ? duration : 0
|
|
110
|
+
positionSec = (position.isFinite && position > 0) ? position : 0
|
|
111
|
+
}
|
|
112
|
+
let result = sleepTimerCore.tick(
|
|
113
|
+
nowMs: Self.nowEpochMs(),
|
|
114
|
+
trackDurationSec: durationSec,
|
|
115
|
+
trackPositionSec: positionSec)
|
|
116
|
+
applySleepTimerResult(result)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Apply a `SleepTimerCore.Tick` decision to the engine — fade, state emit,
|
|
120
|
+
/// and the boundary pause. Shared by the polling tick and the real track-end
|
|
121
|
+
/// signal (`handlePlayerItemDidPlayToEndTime`).
|
|
122
|
+
internal func applySleepTimerResult(_ result: SleepTimerCore.Tick) {
|
|
123
|
+
applySleepTimerFade(result.fadeFraction)
|
|
124
|
+
if result.stateChanged { emitSleepTimerChanged() }
|
|
125
|
+
if result.pauseNow {
|
|
126
|
+
// Route the boundary pause to the receiver when casting (the receiver
|
|
127
|
+
// emits its own paused state); only the local-engine pause needs the
|
|
128
|
+
// sleep-timer reason tag.
|
|
129
|
+
if !CastTransportRouter.routePause() {
|
|
130
|
+
self.pendingStateChangeReason = .sleepTimer
|
|
131
|
+
// The timer's pause is a transport intent like the user's: without
|
|
132
|
+
// clearing these, a drain that follows (the timer firing at a track
|
|
133
|
+
// end on the last installed item) or a buffer refill would restart
|
|
134
|
+
// playback the timer just stopped.
|
|
135
|
+
self.wantsToPlay = false
|
|
136
|
+
self.isRecoveringFromStall = false
|
|
137
|
+
self.engine?.pause()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if !sleepTimerCore.isActive { stopSleepTimerTick() }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Fade multiplies off `volumeState` without mutating it, so `getVolume`
|
|
144
|
+
/// stays truthful and the pre-fade level is restored on clear/fire.
|
|
145
|
+
private func applySleepTimerFade(_ fraction: Double) {
|
|
146
|
+
// Fade is local-only: the receiver's volume is user/system-owned and
|
|
147
|
+
// `volumeState` tracks the local level, so ramping it on the receiver would
|
|
148
|
+
// clobber the cast volume. When casting the boundary pause still routes to
|
|
149
|
+
// the receiver; the pre-pause fade is skipped. Undo any in-progress local
|
|
150
|
+
// fade first so a cast session appearing mid-fade can't strand the local
|
|
151
|
+
// engine at a ramped-down volume for a later cast→local return.
|
|
152
|
+
if PlaybackStateRouter.shared.activeRemote() != nil {
|
|
153
|
+
restoreSleepTimerVolume()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
if fraction < 1.0 {
|
|
157
|
+
self.engine?.setVolume(Float(Double(self.volumeState) * fraction))
|
|
158
|
+
sleepTimerFading = true
|
|
159
|
+
} else if sleepTimerFading {
|
|
160
|
+
self.engine?.setVolume(self.volumeState)
|
|
161
|
+
sleepTimerFading = false
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private func restoreSleepTimerVolume() {
|
|
166
|
+
if sleepTimerFading {
|
|
167
|
+
self.engine?.setVolume(self.volumeState)
|
|
168
|
+
sleepTimerFading = false
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private static func nowEpochMs() -> Int64 {
|
|
173
|
+
Int64(Date().timeIntervalSince1970 * 1000)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
extension TrackPlayer {
|
|
4
|
+
// MARK: - State snapshots
|
|
5
|
+
//
|
|
6
|
+
// Nitro calls these getters on a JS worker thread. Player state lives
|
|
7
|
+
// on `playerQueue`, so every read hops there with `onPlayerQueueSync`,
|
|
8
|
+
// which runs the body inline when the caller is already on that queue
|
|
9
|
+
// (a `sync` from the queue onto itself would deadlock). The cached
|
|
10
|
+
// `canSkip*` Bools are written on `playerQueue` by
|
|
11
|
+
// `recomputeCapabilities()`, so the same hop reads them consistently.
|
|
12
|
+
func getState() throws -> PlayerState {
|
|
13
|
+
// The receiver owns playback during a cast session — the local engine is
|
|
14
|
+
// parked and paused for its duration, so its derived state describes
|
|
15
|
+
// nothing the user can hear. Until the receiver has reported (session
|
|
16
|
+
// still activating, holding its initial `.idle`) the local read is the
|
|
17
|
+
// better answer; after that the receiver is authoritative, including a
|
|
18
|
+
// later `.idle`, which means it genuinely has nothing loaded.
|
|
19
|
+
if let remote = PlaybackStateRouter.shared.activeRemote(), remote.hasReportedState {
|
|
20
|
+
return remote.lastState.asPlayerState
|
|
21
|
+
}
|
|
22
|
+
return onPlayerQueueSync {
|
|
23
|
+
// Both engines derive PlayerState from the same inputs via
|
|
24
|
+
// `engine.timeControlStatus` (gapless returns its AVQueuePlayer's, which
|
|
25
|
+
// is what `self.player` also points at; crossfade returns the active
|
|
26
|
+
// leg's), so gapless + crossfade report identical states. `.none` when no
|
|
27
|
+
// engine is configured.
|
|
28
|
+
guard let engine = self.engine else { return .none }
|
|
29
|
+
return PlayerStateDerivation.translate(
|
|
30
|
+
status: engine.timeControlStatus,
|
|
31
|
+
currentIndex: self.currentTrackIndex,
|
|
32
|
+
hasCurrentItem: engine.currentMediaItem != nil,
|
|
33
|
+
reachedQueueEnd: self.reachedQueueEnd
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
func getCurrentTrackIndex() throws -> Double {
|
|
38
|
+
// Nitrogen spec types this return as Double; bridge the internal Int.
|
|
39
|
+
return onPlayerQueueSync { Double(self.currentTrackIndex) }
|
|
40
|
+
}
|
|
41
|
+
func getCurrentTrackSource() throws -> TrackSource? {
|
|
42
|
+
return onPlayerQueueSync { self.currentTrackSource }
|
|
43
|
+
}
|
|
44
|
+
func getCanSkipNext() throws -> Bool {
|
|
45
|
+
return onPlayerQueueSync { self.cachedSkipCapability.canSkipNext }
|
|
46
|
+
}
|
|
47
|
+
func getSkipCapability() throws -> SkipCapability {
|
|
48
|
+
return onPlayerQueueSync { self.cachedSkipCapability }
|
|
49
|
+
}
|
|
50
|
+
func getCanSkipPrevious() throws -> Bool {
|
|
51
|
+
return onPlayerQueueSync { self.cachedSkipCapability.canSkipPrevious }
|
|
52
|
+
}
|
|
53
|
+
func getPlaybackSpeed() throws -> Double {
|
|
54
|
+
return onPlayerQueueSync { Double(self.playbackSpeedState) }
|
|
55
|
+
}
|
|
56
|
+
func getVolume() throws -> Double {
|
|
57
|
+
return onPlayerQueueSync { Double(self.volumeState) }
|
|
58
|
+
}
|
|
59
|
+
func getRemoteControls() throws -> RemoteControlOptions {
|
|
60
|
+
return onPlayerQueueSync {
|
|
61
|
+
let options: RemoteControlOptions = .init(
|
|
62
|
+
skipMode: self.remoteSkipMode,
|
|
63
|
+
forwardJumpInterval: self.remoteForwardInterval,
|
|
64
|
+
backwardJumpInterval: self.remoteBackwardInterval)
|
|
65
|
+
return options
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func getBufferState() throws -> BufferState {
|
|
70
|
+
return onPlayerQueueSync { self.currentBufferState }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func getIsSeekable() throws -> Bool {
|
|
74
|
+
return onPlayerQueueSync {
|
|
75
|
+
guard let item = self.engine?.currentMediaItem else { return true }
|
|
76
|
+
return !item.seekableTimeRanges.isEmpty
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func isFullyBuffered() throws -> Bool {
|
|
81
|
+
return onPlayerQueueSync { self.currentFullyBuffered }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func onBufferStateChange(
|
|
85
|
+
callback: @escaping (_ state: BufferState) -> Void
|
|
86
|
+
) throws -> () -> Void {
|
|
87
|
+
let id = bufferStateListeners.add(callback)
|
|
88
|
+
// Fire once on subscribe with the current state so a UI renders immediately.
|
|
89
|
+
playerQueue.async { [weak self] in
|
|
90
|
+
guard let self else { return }
|
|
91
|
+
guard self.bufferStateListeners.contains(id: id) else { return }
|
|
92
|
+
callback(self.currentBufferState)
|
|
93
|
+
}
|
|
94
|
+
return { [weak self] in self?.bufferStateListeners.remove(id: id) }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func onFullyBufferedChange(
|
|
98
|
+
callback: @escaping (_ fullyBuffered: Bool) -> Void
|
|
99
|
+
) throws -> () -> Void {
|
|
100
|
+
let id = fullyBufferedListeners.add(callback)
|
|
101
|
+
playerQueue.async { [weak self] in
|
|
102
|
+
guard let self else { return }
|
|
103
|
+
guard self.fullyBufferedListeners.contains(id: id) else { return }
|
|
104
|
+
callback(self.currentFullyBuffered)
|
|
105
|
+
}
|
|
106
|
+
return { [weak self] in self?.fullyBufferedListeners.remove(id: id) }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
|
|
4
|
+
extension TrackPlayer {
|
|
5
|
+
/// True when the caller is already executing on `playerQueue`.
|
|
6
|
+
///
|
|
7
|
+
/// `DispatchQueue.sync` runs its body on the calling thread, so a thread
|
|
8
|
+
/// identity test answers the wrong question — it reports "main" for work
|
|
9
|
+
/// serialised on the queue and would let a nested `sync` deadlock. The
|
|
10
|
+
/// queue's own specific travels with the execution context instead.
|
|
11
|
+
internal var isOnPlayerQueue: Bool {
|
|
12
|
+
DispatchQueue.getSpecific(key: playerQueueKey) == ObjectIdentifier(self)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// Claim the audio session, then start the engine.
|
|
16
|
+
///
|
|
17
|
+
/// Every path that starts playback goes through here so activation cannot be
|
|
18
|
+
/// forgotten at a new one. `activateIfNeeded` is idempotent, so the cost
|
|
19
|
+
/// after the first play is a bool check.
|
|
20
|
+
///
|
|
21
|
+
/// Activation is deliberately not done at `configure()`: that would stop
|
|
22
|
+
/// whatever else the user is listening to the moment a consumer app
|
|
23
|
+
/// launches, before they have pressed anything.
|
|
24
|
+
internal func beginPlayback() {
|
|
25
|
+
audioSession.activateIfNeeded()
|
|
26
|
+
engine?.play()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Enqueue `body` on `playerQueue` **at call time**, on the calling thread,
|
|
30
|
+
/// and resolve the returned promise when it completes.
|
|
31
|
+
///
|
|
32
|
+
/// This is what makes the queue's FIFO order equal the JS call order.
|
|
33
|
+
/// `Promise.async` starts an unstructured `Task`, and two Tasks created back
|
|
34
|
+
/// to back have no ordering relationship with each other — so a body that
|
|
35
|
+
/// hops onto the queue *inside* the Task reaches the player at an arbitrary
|
|
36
|
+
/// later point. Two calls issued in order from JS could then arrive
|
|
37
|
+
/// reversed: `play()` then `pause()` landing as pause-then-play, leaving
|
|
38
|
+
/// audio running after the user paused it.
|
|
39
|
+
///
|
|
40
|
+
/// A serial queue gives mutual exclusion, not ordering. Ordering comes from
|
|
41
|
+
/// when the work is enqueued, which is why this runs before returning.
|
|
42
|
+
internal func enqueue(_ body: @escaping () -> Void) -> Promise<Void> {
|
|
43
|
+
let promise = Promise<Void>()
|
|
44
|
+
playerQueue.async {
|
|
45
|
+
body()
|
|
46
|
+
promise.resolve()
|
|
47
|
+
}
|
|
48
|
+
return promise
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Enqueue a mutation at call time and hand its result to a receiver mirror
|
|
52
|
+
/// that runs off the queue.
|
|
53
|
+
///
|
|
54
|
+
/// The enqueue is what fixes ordering against other player work — a later
|
|
55
|
+
/// call cannot overtake this one. Doing it with `async` rather than `sync`
|
|
56
|
+
/// matters just as much: the mutation can rebuild the whole player queue,
|
|
57
|
+
/// and `sync` would run that on the JS thread.
|
|
58
|
+
///
|
|
59
|
+
/// `mirror` awaits the receiver afterwards, holding neither the queue nor
|
|
60
|
+
/// the calling thread, and is chained behind the previous mutation's mirror
|
|
61
|
+
/// so the receiver sees mutations in the order they were made. The promise
|
|
62
|
+
/// resolves once the mirror returns, so a consumer awaiting the call knows
|
|
63
|
+
/// local and remote are both settled.
|
|
64
|
+
internal func enqueueThenMirror<T>(
|
|
65
|
+
_ body: @escaping () -> T,
|
|
66
|
+
mirror: @escaping (T, _ generation: UInt64) async -> Void
|
|
67
|
+
) -> Promise<Void> {
|
|
68
|
+
let promise = Promise<Void>()
|
|
69
|
+
playerQueue.async {
|
|
70
|
+
self.queueMutationGeneration &+= 1
|
|
71
|
+
let generation = self.queueMutationGeneration
|
|
72
|
+
let value = body()
|
|
73
|
+
let previous = self.castMirrorChain
|
|
74
|
+
self.castMirrorChain = Task {
|
|
75
|
+
// Deltas are only correct against a receiver that has applied
|
|
76
|
+
// everything before them, so this waits rather than racing. A failed
|
|
77
|
+
// mirror still completes its Task, so one bad round-trip cannot wedge
|
|
78
|
+
// the chain.
|
|
79
|
+
await previous?.value
|
|
80
|
+
await mirror(value, generation)
|
|
81
|
+
promise.resolve()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return promise
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Value-returning variant of `enqueue`.
|
|
88
|
+
internal func enqueueValue<T>(_ body: @escaping () -> T) -> Promise<T> {
|
|
89
|
+
let promise = Promise<T>()
|
|
90
|
+
playerQueue.async {
|
|
91
|
+
promise.resolve(withResult: body())
|
|
92
|
+
}
|
|
93
|
+
return promise
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Throwing variant of `enqueue` — a thrown error rejects the JS promise
|
|
97
|
+
/// with its message, as `Promise.async` does for a throwing body.
|
|
98
|
+
internal func enqueueThrowing(_ body: @escaping () throws -> Void) -> Promise<Void> {
|
|
99
|
+
let promise = Promise<Void>()
|
|
100
|
+
playerQueue.async {
|
|
101
|
+
do {
|
|
102
|
+
try body()
|
|
103
|
+
promise.resolve()
|
|
104
|
+
} catch {
|
|
105
|
+
promise.reject(withError: error)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return promise
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Serialise a mutation body on `playerQueue` and return a
|
|
112
|
+
/// Promise-compatible awaitable. Centralising here keeps every access to
|
|
113
|
+
/// player state on one queue, which is what makes the state safe to read
|
|
114
|
+
/// without locks inside these bodies.
|
|
115
|
+
internal func onPlayerQueue(_ body: @escaping () -> Void) async {
|
|
116
|
+
await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
|
|
117
|
+
playerQueue.async {
|
|
118
|
+
body()
|
|
119
|
+
cont.resume()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Run `body` on `playerQueue` synchronously and return its value. Used by
|
|
125
|
+
/// the Nitro getters and the listener register / dispose paths, which are
|
|
126
|
+
/// not Promise-wrapped and run on whatever thread the JS bridge uses.
|
|
127
|
+
///
|
|
128
|
+
/// Running inline when already on the queue is what makes re-entrant calls
|
|
129
|
+
/// safe: `playerQueue.sync` from a block already on `playerQueue` is an
|
|
130
|
+
/// immediate deadlock.
|
|
131
|
+
internal func onPlayerQueueSync<T>(_ body: () -> T) -> T {
|
|
132
|
+
if isOnPlayerQueue { return body() }
|
|
133
|
+
return playerQueue.sync(execute: body)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
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: a user re-tapping play after an exhausted-retries error
|
|
11
|
+
// is asking for this track again, so it gets the allowance the consumer
|
|
12
|
+
// configured back and a rebuild. Not a hard-coded 1: `autoRetries: 0`
|
|
13
|
+
// asked for none.
|
|
14
|
+
if self.lastReportedState == .error,
|
|
15
|
+
self.currentTrackIndex >= 0,
|
|
16
|
+
self.currentTrackIndex < self.queueItemIds.count {
|
|
17
|
+
let qid = self.queueItemIds[self.currentTrackIndex]
|
|
18
|
+
if (self.retryAttemptsRemaining[qid] ?? 0) == 0 {
|
|
19
|
+
// Asking for this track to play is incompatible with the queue being
|
|
20
|
+
// over, so `reachedQueueEnd` cannot survive it.
|
|
21
|
+
self.reachedQueueEnd = false
|
|
22
|
+
self.retryAttemptsRemaining[qid] = self.effectiveAutoRetries()
|
|
23
|
+
// A scheduled attempt for this item is superseded by the rebuild below.
|
|
24
|
+
if self.pendingRetryQueueItemId == qid { self.pendingRetryQueueItemId = nil }
|
|
25
|
+
self.lastErrorQueueItemId = nil
|
|
26
|
+
self.lastErrorCode = nil
|
|
27
|
+
// Rebuild whenever an engine is live — the rebuild is
|
|
28
|
+
// engine-agnostic, and the failed item has to be
|
|
29
|
+
// reconstructed on either engine for the retry to reach it.
|
|
30
|
+
// Forced: the failed item keeps its `queueItemId`, so a rebuild
|
|
31
|
+
// that compares identity would find nothing to do.
|
|
32
|
+
if self.engine != nil {
|
|
33
|
+
self.performingMutation {
|
|
34
|
+
self.fullRebuildPlayerQueue(forcingReinstall: true)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// At the end of the queue there is nothing to play: the engine is
|
|
40
|
+
// parked on the finished track (or drained), and writing a rate onto
|
|
41
|
+
// it would report `.playing` for a moment or, on some OS versions,
|
|
42
|
+
// re-post the item's end. A skip, a seek or a fresh queue leaves this
|
|
43
|
+
// state; play() alone restarts nothing.
|
|
44
|
+
if self.reachedQueueEnd, self.currentTrackIndex >= 0 { return }
|
|
45
|
+
self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
|
|
46
|
+
self.wantsToPlay = true
|
|
47
|
+
// An explicit play ends an interruption from the user's side: the
|
|
48
|
+
// system does not always deliver `.ended` (an app suspended mid-call
|
|
49
|
+
// never sees it), and the flag would otherwise keep retries, window
|
|
50
|
+
// refills and network recovery disabled for the rest of the session.
|
|
51
|
+
self.isInterrupted = false
|
|
52
|
+
self.beginPlayback()
|
|
53
|
+
self.recomputeBufferState()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func retry() throws -> Promise<Void> {
|
|
58
|
+
return enqueue {
|
|
59
|
+
// During cast the receiver owns playback — forward a play so a failed
|
|
60
|
+
// receiver item re-attempts.
|
|
61
|
+
if CastTransportRouter.routePlay() { return }
|
|
62
|
+
// A user asking for a retry has a reason, and the library is not the one
|
|
63
|
+
// to judge it. This is the manual attempt once the automatic one is
|
|
64
|
+
// spent, so gating it on the player's state would gate recovery on the
|
|
65
|
+
// state of a player that has already failed to recover — and a wedged
|
|
66
|
+
// decoder still reporting `playing` is exactly when someone reaches for
|
|
67
|
+
// the button.
|
|
68
|
+
//
|
|
69
|
+
// Only the bounds remain: no current track is genuinely nothing to
|
|
70
|
+
// retry, which is not second-guessing the request.
|
|
71
|
+
guard self.currentTrackIndex >= 0,
|
|
72
|
+
self.currentTrackIndex < self.queueItemIds.count else { return }
|
|
73
|
+
let qid = self.queueItemIds[self.currentTrackIndex]
|
|
74
|
+
// Capture the pre-failure position so a mid-track failure resumes where
|
|
75
|
+
// it stopped (the rebuild below reloads the item from 0). Matches
|
|
76
|
+
// Android's retryFailedItem position-restore.
|
|
77
|
+
let resumePos = self.engine?.currentPositionSeconds ?? 0
|
|
78
|
+
// Asking for this track to play is incompatible with the queue being over,
|
|
79
|
+
// and a queue whose only track failed to load has already drained: without
|
|
80
|
+
// this the rebuild seats an item into a queue that believes it is finished.
|
|
81
|
+
self.reachedQueueEnd = false
|
|
82
|
+
// The configured budget, not a hard-coded 1: this restores the track's
|
|
83
|
+
// automatic allowance for whatever fails *next*, and a consumer who set
|
|
84
|
+
// `autoRetries: 0` asked for none.
|
|
85
|
+
self.retryAttemptsRemaining[qid] = self.effectiveAutoRetries()
|
|
86
|
+
// A scheduled attempt for this item is superseded by the rebuild below;
|
|
87
|
+
// leaving it set would suppress the rebuilt item's own failure until the
|
|
88
|
+
// earlier attempt fired.
|
|
89
|
+
if self.pendingRetryQueueItemId == qid { self.pendingRetryQueueItemId = nil }
|
|
90
|
+
self.lastErrorQueueItemId = nil
|
|
91
|
+
self.lastErrorCode = nil
|
|
92
|
+
// Rebuild whenever an engine is live — the rebuild is
|
|
93
|
+
// engine-agnostic, and the failed item has to be reconstructed
|
|
94
|
+
// on either engine for the retry to reach it. Forced: the failed
|
|
95
|
+
// item keeps its `queueItemId`, so a rebuild that compares
|
|
96
|
+
// identity would find nothing to do.
|
|
97
|
+
if self.engine != nil {
|
|
98
|
+
self.performingMutation {
|
|
99
|
+
self.fullRebuildPlayerQueue(forcingReinstall: true)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
|
|
103
|
+
self.wantsToPlay = true
|
|
104
|
+
self.beginPlayback()
|
|
105
|
+
if resumePos.isFinite, resumePos > 0 {
|
|
106
|
+
_ = try? self.seekTo(position: resumePos)
|
|
107
|
+
}
|
|
108
|
+
self.recomputeBufferState()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func pause() throws -> Promise<Void> {
|
|
113
|
+
return enqueue {
|
|
114
|
+
if CastTransportRouter.routePause() { return }
|
|
115
|
+
self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
|
|
116
|
+
self.wantsToPlay = false
|
|
117
|
+
// An explicit pause outranks a pending stall recovery: without this the
|
|
118
|
+
// buffer refilling afterwards would restart audio the user just stopped.
|
|
119
|
+
self.isRecoveringFromStall = false
|
|
120
|
+
self.engine?.pause()
|
|
121
|
+
self.recomputeBufferState()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// "Stop" = pause + seek to start of current item. Does NOT clear
|
|
126
|
+
/// the queue — consumers wanting a full reset call clearQueue().
|
|
127
|
+
func stop() throws -> Promise<Void> {
|
|
128
|
+
return enqueue {
|
|
129
|
+
if CastTransportRouter.routeStop() { return }
|
|
130
|
+
guard let engine = self.engine else { return }
|
|
131
|
+
self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
|
|
132
|
+
self.wantsToPlay = false
|
|
133
|
+
self.hasStartedPlaying = false
|
|
134
|
+
engine.stop()
|
|
135
|
+
self.recomputeBufferState()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Seek within the current track. Position in seconds; negative
|
|
140
|
+
/// values clamp to 0, values past the duration clamp to duration.
|
|
141
|
+
func seekTo(position: Double) throws -> Promise<Void> {
|
|
142
|
+
// Enqueued at call time, like every other transport method, so a seek
|
|
143
|
+
// reaches the player in the order JS issued it. The promise resolves once
|
|
144
|
+
// the seek's own completion has landed, which is why this builds the
|
|
145
|
+
// promise by hand rather than using `enqueue`.
|
|
146
|
+
let promise = Promise<Void>()
|
|
147
|
+
playerQueue.async {
|
|
148
|
+
// Match Android (`TrackPlayer.kt` seekToInternal): non-finite
|
|
149
|
+
// (NaN / ±Infinity) inputs are silent no-ops. See
|
|
150
|
+
// `InputGuards.sanitisedSeekPosition` for the rationale.
|
|
151
|
+
guard let position = InputGuards.sanitisedSeekPosition(position) else {
|
|
152
|
+
promise.resolve()
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
if CastTransportRouter.routeSeekTo(positionMs: Int64(position * 1000.0)) {
|
|
156
|
+
promise.resolve()
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Every player + queue read and the seek itself run on this one hop, so
|
|
161
|
+
// the player never crosses a concurrency boundary. The return value
|
|
162
|
+
// carries whether a seek was issued: the crossfade branch and the
|
|
163
|
+
// drained-gapless branch complete in place and yield false.
|
|
164
|
+
//
|
|
165
|
+
// Named rather than inlined so the two early-return branches and the
|
|
166
|
+
// seek branch share one exit.
|
|
167
|
+
let issueSeek: () -> Bool = {
|
|
168
|
+
// Engine-aware seek: under CrossfadeEngine `self.player` is
|
|
169
|
+
// nil. Without this branch every seek call would silently
|
|
170
|
+
// no-op once crossfade mode is active, breaking transport
|
|
171
|
+
// controls + auto-advance test coverage.
|
|
172
|
+
if self.player == nil {
|
|
173
|
+
guard let engine = self.engine else { return false }
|
|
174
|
+
// Seeking replays from a position — no longer at the end of the
|
|
175
|
+
// queue. (Gapless no-ops at end-of-queue with no current item below.)
|
|
176
|
+
self.reachedQueueEnd = false
|
|
177
|
+
let duration = engine.currentDurationSeconds
|
|
178
|
+
let clamped: Double
|
|
179
|
+
if position < 0 {
|
|
180
|
+
clamped = 0
|
|
181
|
+
} else {
|
|
182
|
+
clamped = (duration > 0 && position > duration) ? duration : position
|
|
183
|
+
}
|
|
184
|
+
let seekTarget = self.clampToBufferedIfEnabled(clamped)
|
|
185
|
+
self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
|
|
186
|
+
let engineIdx: Int
|
|
187
|
+
if let installed = self.engineIndex(forQueuePosition: self.currentTrackIndex) {
|
|
188
|
+
engineIdx = installed
|
|
189
|
+
} else {
|
|
190
|
+
// Active track absent from the engine — reinstall it, then seek
|
|
191
|
+
// within the reinstalled run so the position is applied rather
|
|
192
|
+
// than dropped with a resolved promise.
|
|
193
|
+
self.performingMutation { self.fullRebuildPlayerQueue() }
|
|
194
|
+
guard let reinstalled = self.engineIndex(forQueuePosition: self.currentTrackIndex)
|
|
195
|
+
else { return false }
|
|
196
|
+
engineIdx = reinstalled
|
|
197
|
+
}
|
|
198
|
+
engine.seek(toIndex: engineIdx, position: seekTarget)
|
|
199
|
+
self.milestoneTracker.seek(
|
|
200
|
+
durationSec: self.effectiveMilestoneDuration(playerDuration: duration),
|
|
201
|
+
positionSec: seekTarget)
|
|
202
|
+
self.nowPlayingInfo.refreshPositionAndRate(self.nowPlayingSnapshot())
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
guard let player = self.player else { return false }
|
|
207
|
+
// A gapless player drained at the end of the queue has no item to
|
|
208
|
+
// seek in: reinstall the finished track first, as a same-index skip
|
|
209
|
+
// does, so a seek after the end replays it on every engine. Seeking
|
|
210
|
+
// is what leaves the ended state, so the flag clears here.
|
|
211
|
+
if player.currentItem == nil, self.reachedQueueEnd,
|
|
212
|
+
self.currentTrackIndex >= 0, self.currentTrackIndex < self.tracks.count {
|
|
213
|
+
self.reachedQueueEnd = false
|
|
214
|
+
self.performingMutation { self.fullRebuildPlayerQueue() }
|
|
215
|
+
}
|
|
216
|
+
guard let item = player.currentItem else { return false }
|
|
217
|
+
let itemDuration = CMTimeGetSeconds(item.duration)
|
|
218
|
+
let clamped: Double
|
|
219
|
+
if position < 0 {
|
|
220
|
+
clamped = 0
|
|
221
|
+
} else {
|
|
222
|
+
clamped = (itemDuration.isFinite && position > itemDuration) ? itemDuration : position
|
|
223
|
+
}
|
|
224
|
+
// Apply the opt-in buffered clamp on top of the 0/duration clamp — caps
|
|
225
|
+
// the target at the buffered edge when the seek is past what's downloaded.
|
|
226
|
+
let seekTarget = self.clampToBufferedIfEnabled(clamped)
|
|
227
|
+
// Consume jumped-over milestones BEFORE issuing the (async) seek so a
|
|
228
|
+
// time-jump progress tick that fires during the seek can't emit the
|
|
229
|
+
// thresholds the seek skips over.
|
|
230
|
+
self.milestoneTracker.seek(
|
|
231
|
+
durationSec: self.effectiveMilestoneDuration(
|
|
232
|
+
playerDuration: (itemDuration.isFinite && itemDuration > 0) ? itemDuration : 0),
|
|
233
|
+
positionSec: seekTarget)
|
|
234
|
+
// Stash `.user` reason BEFORE the seek so any state transition
|
|
235
|
+
// the seek triggers (e.g. playable → buffering on a long jump)
|
|
236
|
+
// gets the correct origin. Matches the play / pause / stop pattern.
|
|
237
|
+
self.pendingStateChangeReason = self.pendingStateChangeReason ?? .user
|
|
238
|
+
player.seek(
|
|
239
|
+
to: CMTime(seconds: seekTarget, preferredTimescale: CMTimeScale(NSEC_PER_SEC)),
|
|
240
|
+
toleranceBefore: .zero,
|
|
241
|
+
toleranceAfter: .zero
|
|
242
|
+
) { _ in
|
|
243
|
+
// This closure captures `player`, so a teardown that drops every
|
|
244
|
+
// other reference cannot deallocate it with the seek still pending.
|
|
245
|
+
// AVFoundation invokes the handler when the seek completes or is
|
|
246
|
+
// interrupted, and the capture is released with it.
|
|
247
|
+
_ = player
|
|
248
|
+
self.playerQueue.async {
|
|
249
|
+
// Surface the new scrubber position to the lock-screen /
|
|
250
|
+
// control-center immediately. The KVO state-change emit covers
|
|
251
|
+
// transitions (e.g. paused → buffering on a long jump) but a
|
|
252
|
+
// same-state seek won't trigger an emit; without this the
|
|
253
|
+
// lock-screen scrubber sits at the pre-seek position until the
|
|
254
|
+
// next 10s periodic tick.
|
|
255
|
+
self.nowPlayingInfo.refreshPositionAndRate(self.nowPlayingSnapshot())
|
|
256
|
+
promise.resolve()
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return true
|
|
260
|
+
}
|
|
261
|
+
// A branch that completed in place has nothing pending, so the promise
|
|
262
|
+
// resolves here; the seek branch resolves from its completion instead.
|
|
263
|
+
if !issueSeek() { promise.resolve() }
|
|
264
|
+
}
|
|
265
|
+
return promise
|
|
266
|
+
}
|
|
267
|
+
}
|