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,291 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
|
|
4
|
+
extension TrackPlayer {
|
|
5
|
+
// MARK: - Playback configuration — some stubs, some lifecycle-adjacent
|
|
6
|
+
|
|
7
|
+
func setPlaybackMode(mode: PlaybackMode) throws -> Promise<Void> {
|
|
8
|
+
return enqueueThrowing {
|
|
9
|
+
let (newState, effect) = try self.playbackModeStateMachine.setRequested(
|
|
10
|
+
state: self.playbackModeState,
|
|
11
|
+
requested: mode
|
|
12
|
+
)
|
|
13
|
+
self.playbackModeState = newState
|
|
14
|
+
switch effect {
|
|
15
|
+
case .noOp:
|
|
16
|
+
break
|
|
17
|
+
case .immediateSwap:
|
|
18
|
+
self.applyEngineSwapForMode(newState.active)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func getPlaybackMode() throws -> PlaybackMode {
|
|
24
|
+
// Lock-guarded, so this answers without hopping. A `playerQueue.sync` here
|
|
25
|
+
// parks the JS thread behind whatever the queue is running, which can be a
|
|
26
|
+
// full queue rebuild.
|
|
27
|
+
return self.playbackModeState.active
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Build a fresh engine of the requested kind and swap it in
|
|
31
|
+
/// place of the active engine. The new engine starts empty; the
|
|
32
|
+
/// canonical queue model is re-installed via the engine surface
|
|
33
|
+
/// so playback continues from the current track / position.
|
|
34
|
+
///
|
|
35
|
+
/// JS-facing event sources differ per engine. GaplessEngine ties
|
|
36
|
+
/// `self.player` to the underlying `AVQueuePlayer` and the KVO
|
|
37
|
+
/// chain installed by `installGaplessObservers` /
|
|
38
|
+
/// `installEventObservers` carries onTrackChange / onStateChange
|
|
39
|
+
/// / onError. CrossfadeEngine sets `self.player` to nil and
|
|
40
|
+
/// fires `PlaybackEngineDelegate` callbacks that this class
|
|
41
|
+
/// implements (extension at the end of the file); the delegate
|
|
42
|
+
/// is wired below. Both engines surface progress through
|
|
43
|
+
/// `emitProgressIfSubscribed`, polymorphic over self.player vs
|
|
44
|
+
/// engine state.
|
|
45
|
+
internal func applyEngineSwapForMode(_ active: PlaybackMode) {
|
|
46
|
+
let oldEngine = self.engine
|
|
47
|
+
// Capture pre-swap state — the engine swap releases the old
|
|
48
|
+
// engine; reading `newEngine.currentPositionSeconds` /
|
|
49
|
+
// `isPlaying` after would always return 0 / false. The
|
|
50
|
+
// wasPlaying capture is what carries playback continuity
|
|
51
|
+
// across the immediate engine swap.
|
|
52
|
+
let resumePosition = oldEngine?.currentPositionSeconds ?? 0
|
|
53
|
+
let resumeIndex = self.currentTrackIndex
|
|
54
|
+
// The transport's intent, not the old engine's audible state: a leg that
|
|
55
|
+
// is buffering, or mid-crossfade with its outgoing item finished, reports
|
|
56
|
+
// not-playing while the consumer is very much still playing. Swapping on
|
|
57
|
+
// that reading silently pauses them.
|
|
58
|
+
let wasPlaying = self.wantsToPlay
|
|
59
|
+
// Anything held back from the old engine goes with it: the new one is
|
|
60
|
+
// installed from the queue model, which already carries every mutation.
|
|
61
|
+
self.engineReconcileDeferred = false
|
|
62
|
+
|
|
63
|
+
let newEngine: PlaybackEngine
|
|
64
|
+
switch active.kind {
|
|
65
|
+
case .gapless:
|
|
66
|
+
newEngine = GaplessEngine()
|
|
67
|
+
case .crossfade:
|
|
68
|
+
// PlaybackModeStateMachine.requireValid guarantees this is
|
|
69
|
+
// non-nil + within [1000, 12000] when `kind == .crossfade`.
|
|
70
|
+
// Fail loud rather than silently degrading to 0 (which would
|
|
71
|
+
// route through CrossfadeEngine's hard-cut path + skip
|
|
72
|
+
// engineCrossfadeDidBegin entirely).
|
|
73
|
+
guard let durationMs = active.crossfadeDurationMs else {
|
|
74
|
+
preconditionFailure("PlaybackMode(crossfade) missing crossfadeDurationMs at engine construction")
|
|
75
|
+
}
|
|
76
|
+
let durationSeconds = durationMs / 1000.0
|
|
77
|
+
newEngine = CrossfadeEngine(
|
|
78
|
+
crossfadeDurationSeconds: durationSeconds, confinedTo: self.playerQueue)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
self.engine = newEngine
|
|
82
|
+
// GaplessEngine path: keep the established self.player KVO
|
|
83
|
+
// chain (currentItem / timeControlStatus / item.status) as
|
|
84
|
+
// the canonical event source. CrossfadeEngine path:
|
|
85
|
+
// self.player stays nil; the engine fires PlaybackEngineDelegate
|
|
86
|
+
// callbacks for track-change / state-change / format-ready /
|
|
87
|
+
// failed / play-to-end / queue-end and progress is sourced
|
|
88
|
+
// from the engine via a timer-driven fallback.
|
|
89
|
+
if let gapless = newEngine as? GaplessEngine {
|
|
90
|
+
self.player = gapless.player
|
|
91
|
+
self.gaplessFlipArmed = true
|
|
92
|
+
self.installGaplessObservers(on: gapless.player)
|
|
93
|
+
self.installEventObservers(on: gapless.player)
|
|
94
|
+
self.stopProgressFallbackTimer()
|
|
95
|
+
// Gapless does not need the engine delegate — the underlying
|
|
96
|
+
// self.player KVO chain is the canonical event source.
|
|
97
|
+
newEngine.delegate = nil
|
|
98
|
+
} else {
|
|
99
|
+
self.tearDownGaplessObservers()
|
|
100
|
+
self.tearDownEventObservers()
|
|
101
|
+
self.player = nil
|
|
102
|
+
newEngine.delegate = self
|
|
103
|
+
self.startProgressFallbackTimer()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Re-wire the shared audio-tap provider to the new engine's
|
|
107
|
+
// audio-mix provider slot.
|
|
108
|
+
AudioTapProvider.shared.wireToPlaybackEngine(newEngine)
|
|
109
|
+
|
|
110
|
+
// Re-apply canonical config to the freshly-built engine BEFORE the
|
|
111
|
+
// queue install / play — it starts at engine defaults (repeat off,
|
|
112
|
+
// volume 1, speed 1), so without this a gapless↔crossfade swap
|
|
113
|
+
// silently drops the user's settings.
|
|
114
|
+
newEngine.setRepeatMode(self.repeatModeState)
|
|
115
|
+
newEngine.setVolume(self.volumeState)
|
|
116
|
+
// Carry the user's speed into the new engine. When resuming playback,
|
|
117
|
+
// apply it live via `setPlaybackSpeed` (sets `AVPlayer.rate`) BEFORE
|
|
118
|
+
// `setItems` — GaplessEngine.setItems captures the player's rate to
|
|
119
|
+
// restore it across the queue rebuild, so a speed applied after
|
|
120
|
+
// setItems/play() would be clobbered by that async restore. When
|
|
121
|
+
// paused, seed it only (no rate write, so the engine stays paused);
|
|
122
|
+
// play() applies it on the next resume.
|
|
123
|
+
if wasPlaying {
|
|
124
|
+
newEngine.setPlaybackSpeed(self.playbackSpeedState)
|
|
125
|
+
} else {
|
|
126
|
+
newEngine.seedPlaybackSpeed(self.playbackSpeedState)
|
|
127
|
+
}
|
|
128
|
+
newEngine.setPitchCorrectionMode(self.pitchCorrectionModeState)
|
|
129
|
+
|
|
130
|
+
// Re-install the queue on the new engine — the same window every other
|
|
131
|
+
// install uses, not the whole queue: a crossfade engine holds every item
|
|
132
|
+
// it is given, and a long queue would put every track's asset load
|
|
133
|
+
// through the read-through server at once. `topUpWindow` extends it.
|
|
134
|
+
if resumeIndex >= 0, resumeIndex < self.tracks.count {
|
|
135
|
+
let positions = QueueWindowArithmetic.slice(
|
|
136
|
+
currentIndex: resumeIndex,
|
|
137
|
+
trackCount: self.tracks.count,
|
|
138
|
+
trail: self.engineTrail,
|
|
139
|
+
wraps: false
|
|
140
|
+
)
|
|
141
|
+
let sliceIds = positions.map { self.queueItemIds[$0] }
|
|
142
|
+
let items = AVQueueBuilder.buildPlayerItems(
|
|
143
|
+
tracks: positions.map { self.tracks[$0] },
|
|
144
|
+
queueItemIds: sliceIds,
|
|
145
|
+
config: self.config,
|
|
146
|
+
server: self.routingServer
|
|
147
|
+
)
|
|
148
|
+
// Seated the way every other install is; with nothing playable at or
|
|
149
|
+
// after the current position the new engine stays empty rather than
|
|
150
|
+
// replaying the trail.
|
|
151
|
+
let currentId = self.queueItemIds[resumeIndex]
|
|
152
|
+
if let startIndex = Self.seatIndex(in: items, sliceIds: sliceIds, currentId: currentId) {
|
|
153
|
+
// The playhead carries across only onto the track that was playing;
|
|
154
|
+
// any other seat is a track change and starts at zero.
|
|
155
|
+
let position = items[startIndex].queueItemId == currentId ? resumePosition : 0
|
|
156
|
+
newEngine.setItems(
|
|
157
|
+
items,
|
|
158
|
+
startIndex: startIndex,
|
|
159
|
+
startPositionSeconds: position
|
|
160
|
+
)
|
|
161
|
+
// Honour pre-swap playing state so a mid-play setPlaybackMode keeps
|
|
162
|
+
// playing on the new engine — through `beginPlayback`, which every
|
|
163
|
+
// path that starts playback goes through, and not while the session
|
|
164
|
+
// is interrupted.
|
|
165
|
+
if wasPlaying, !self.isInterrupted {
|
|
166
|
+
self.beginPlayback()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Whatever was seated above, the engine being replaced goes: its items
|
|
172
|
+
// stop loading and its observers come down.
|
|
173
|
+
oldEngine?.release()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
func setPlaybackSpeed(rate: Double) throws -> Promise<Void> {
|
|
177
|
+
return enqueue {
|
|
178
|
+
// Clamp to a 0.25 floor: rate=0 on AVPlayer is functionally equivalent
|
|
179
|
+
// to pause(), which would surprise consumers calling
|
|
180
|
+
// `setPlaybackSpeed(0)` for a slow-down effect. Cross-platform contract
|
|
181
|
+
// documented in TrackPlayer.nitro.ts. A value that is not a number
|
|
182
|
+
// leaves the current rate alone.
|
|
183
|
+
guard !rate.isNaN else { return }
|
|
184
|
+
let safeRate = Float(max(0.25, min(rate, 32)))
|
|
185
|
+
self.playbackSpeedState = safeRate
|
|
186
|
+
// Apply to the LIVE rate only when the user wants playback (already
|
|
187
|
+
// playing, OR buffering toward play). On a paused/idle engine writing
|
|
188
|
+
// `AVPlayer.rate` starts it (rate > 0 == "play"), so with no play intent
|
|
189
|
+
// seed the speed instead (no rate write); `play()` applies it on the next
|
|
190
|
+
// resume. This keeps a persisted speed applied at app start — before a
|
|
191
|
+
// restored `setQueue` — from auto-starting a paused queue. `wantsToPlay`
|
|
192
|
+
// (not just `isPlaying`) covers the buffering-toward-play window so a
|
|
193
|
+
// speed change there still applies live.
|
|
194
|
+
if self.wantsToPlay || self.engine?.isPlaying == true {
|
|
195
|
+
self.engine?.setPlaybackSpeed(safeRate)
|
|
196
|
+
} else {
|
|
197
|
+
self.engine?.seedPlaybackSpeed(safeRate)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
func setPitchCorrectionMode(mode: PitchCorrectionMode) throws -> Promise<Void> {
|
|
202
|
+
return enqueue {
|
|
203
|
+
self.pitchCorrectionModeState = mode
|
|
204
|
+
self.engine?.setPitchCorrectionMode(mode)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
func getPitchCorrectionMode() throws -> PitchCorrectionMode {
|
|
208
|
+
// Lock-guarded — see `getPlaybackMode`.
|
|
209
|
+
return self.pitchCorrectionModeState
|
|
210
|
+
}
|
|
211
|
+
func setVolume(volume: Double) throws -> Promise<Void> {
|
|
212
|
+
return enqueue {
|
|
213
|
+
// Clamped to the documented range before either branch, so the cast and
|
|
214
|
+
// local paths cannot diverge on the same input, and a value that is not
|
|
215
|
+
// a number cannot poison the stored level that `getVolume` and the
|
|
216
|
+
// sleep-timer fade read back.
|
|
217
|
+
guard !volume.isNaN else { return }
|
|
218
|
+
let level = Float(max(0, min(1, volume)))
|
|
219
|
+
if CastTransportRouter.routeSetVolume(value: Double(level)) { return }
|
|
220
|
+
self.volumeState = level
|
|
221
|
+
self.engine?.setVolume(level)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
func setRepeatMode(mode: RepeatMode) throws -> Promise<Void> {
|
|
226
|
+
return enqueue {
|
|
227
|
+
self.repeatModeState = mode
|
|
228
|
+
// Forward to the active engine. The CrossfadeEngine needs the
|
|
229
|
+
// mode to suppress its look-ahead fade + loop the current item
|
|
230
|
+
// under `.track` (AVPlayer has no native repeat); GaplessEngine
|
|
231
|
+
// treats it as a no-op (repeat-track replay lives in
|
|
232
|
+
// handlePlayerItemDidPlayToEndTime).
|
|
233
|
+
self.engine?.setRepeatMode(mode)
|
|
234
|
+
// Both canSkipNext and canSkipPrevious depend on repeat mode
|
|
235
|
+
// (e.g. at idx 0 / last index, OFF↔QUEUE flips the boolean
|
|
236
|
+
// because QUEUE wraps). Recompute is cheap; dedup keeps the
|
|
237
|
+
// listener silent when the (prev, next) tuple is unchanged.
|
|
238
|
+
// The installed run is forward-only, so a repeat-mode change does not
|
|
239
|
+
// alter which positions it holds and there is nothing to reconcile. That
|
|
240
|
+
// stops being true the moment the run carries the wrap target, and the
|
|
241
|
+
// reconcile has to come back with it.
|
|
242
|
+
self.recomputeCapabilities()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func setRemoteControls(options: RemoteControlOptions) throws -> Promise<Void> {
|
|
247
|
+
return enqueue {
|
|
248
|
+
// Guard nonsensical negatives (a negative forward interval would seek
|
|
249
|
+
// backward) and values too large to convert to milliseconds. Out-of-range
|
|
250
|
+
// is clamped rather than rejected.
|
|
251
|
+
let fwd = InputGuards.sanitisedSeekPosition(options.forwardJumpInterval)
|
|
252
|
+
.map { max(0, $0) } ?? 0
|
|
253
|
+
let back = InputGuards.sanitisedSeekPosition(options.backwardJumpInterval)
|
|
254
|
+
.map { max(0, $0) } ?? 0
|
|
255
|
+
self.remoteSkipMode = options.skipMode
|
|
256
|
+
self.remoteForwardInterval = fwd
|
|
257
|
+
self.remoteBackwardInterval = back
|
|
258
|
+
self.remoteCommands.setRemoteControls(
|
|
259
|
+
mode: options.skipMode,
|
|
260
|
+
forwardInterval: fwd,
|
|
261
|
+
backwardInterval: back)
|
|
262
|
+
// Re-apply the cached grey-out: switching to track mode re-enables
|
|
263
|
+
// next/prev, which must reflect the queue-boundary capability
|
|
264
|
+
// (setSkipCapability is otherwise only called on queue/repeat change,
|
|
265
|
+
// and no-ops in interval mode).
|
|
266
|
+
self.remoteCommands.setSkipCapability(
|
|
267
|
+
canSkipNext: self.cachedSkipCapability.canSkipNext,
|
|
268
|
+
canSkipPrevious: self.cachedSkipCapability.canSkipPrevious)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ReplayGain mode — thin wrapper around `AudioTapProvider.shared`.
|
|
273
|
+
// The provider owns the per-item state set + recompute walk; this
|
|
274
|
+
// Hybrid surface is the JS-facing setter / getter only.
|
|
275
|
+
func setReplayGainMode(mode: ReplayGainMode) throws -> Promise<Void> {
|
|
276
|
+
return enqueue {
|
|
277
|
+
AudioTapProvider.shared.setReplayGainMode(mode)
|
|
278
|
+
// Re-emit the active item's NowPlayingFormat so
|
|
279
|
+
// `onNowPlayingFormatChange` subscribers (e.g. the
|
|
280
|
+
// `useNowPlayingFormat` hook) observe the new `replayGainActive`
|
|
281
|
+
// flag without waiting for the next track change. Mirrors the
|
|
282
|
+
// Android path's `refreshNowPlayingFormatForActiveItem` call from
|
|
283
|
+
// its own `setReplayGainMode`.
|
|
284
|
+
guard let item = self.activeMediaItem else { return }
|
|
285
|
+
self.emitCurrentNowPlayingFormat(for: item)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
func getReplayGainMode() throws -> ReplayGainMode {
|
|
289
|
+
return AudioTapProvider.shared.getReplayGainMode()
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
// MARK: - PlaybackEngineDelegate
|
|
5
|
+
|
|
6
|
+
/// `CrossfadeEngine` fires these callbacks; `GaplessEngine`'s
|
|
7
|
+
/// equivalent surface continues to flow through `self.player`'s
|
|
8
|
+
/// KVO chain installed by `installGaplessObservers` /
|
|
9
|
+
/// `installEventObservers`. Each delegate method therefore runs
|
|
10
|
+
/// only when the active engine is `CrossfadeEngine` (per the
|
|
11
|
+
/// `delegate = self` wiring in `applyEngineSwapForMode`).
|
|
12
|
+
extension TrackPlayer: PlaybackEngineDelegate {
|
|
13
|
+
func engineStateMaybeChanged(
|
|
14
|
+
_ engine: PlaybackEngine, reason: StateChangeReason?
|
|
15
|
+
) {
|
|
16
|
+
if let reason {
|
|
17
|
+
// Engine-supplied classified reason takes precedence over
|
|
18
|
+
// any prior staked reason — match the gapless KVO path
|
|
19
|
+
// where pendingStateChangeReason is overwritten by the
|
|
20
|
+
// newer staked reason.
|
|
21
|
+
self.pendingStateChangeReason = reason
|
|
22
|
+
}
|
|
23
|
+
// The leading leg's `timeControlStatus` drives `.buffering` (load /
|
|
24
|
+
// rebuffer) and `reachedQueueEnd` drives `.ended`.
|
|
25
|
+
let computed = PlayerStateDerivation.translate(
|
|
26
|
+
status: engine.timeControlStatus,
|
|
27
|
+
currentIndex: self.currentTrackIndex,
|
|
28
|
+
hasCurrentItem: engine.currentMediaItem != nil,
|
|
29
|
+
reachedQueueEnd: self.reachedQueueEnd
|
|
30
|
+
)
|
|
31
|
+
emitState(computed, reason: reasonForComputedState(computed))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func engineDidTransitionTrack(_ engine: PlaybackEngine) {
|
|
35
|
+
// Bring TrackPlayer's currentTrackIndex into sync with the
|
|
36
|
+
// engine's index — CrossfadeEngine.completeCrossfade
|
|
37
|
+
// increments engine.currentMediaItemIndex; on setItems / seek
|
|
38
|
+
// the engine's index is already aligned with the caller's
|
|
39
|
+
// resumeIndex.
|
|
40
|
+
//
|
|
41
|
+
// Crossfade post-swap echo: fade-start already fired
|
|
42
|
+
// `engineCrossfadeDidBegin` which dispatched the JS-facing
|
|
43
|
+
// track-change for the same incoming index. The
|
|
44
|
+
// `lastEmittedQueueItemId` dedup inside `dispatchTrackChange`
|
|
45
|
+
// turns the second call into a no-op. Capability + lookahead
|
|
46
|
+
// refresh still run unconditionally — both are idempotent and
|
|
47
|
+
// the second pass picks up any state that landed during the
|
|
48
|
+
// fade window.
|
|
49
|
+
// Resolve the engine's active item to a queue position by identity.
|
|
50
|
+
// An engine's own index counts within whatever slice it was handed, so
|
|
51
|
+
// it is not a queue position; `queueItemId` is, and it survives the
|
|
52
|
+
// item-replacement paths (cross-attach, cached-URL upgrade).
|
|
53
|
+
//
|
|
54
|
+
// Still skipped during a queue mutation: the mutation already computed
|
|
55
|
+
// the authoritative `currentTrackIndex` via QueueMutationArithmetic, and
|
|
56
|
+
// the engine is mid-rebuild. (CrossfadeEngine.setItems fires this
|
|
57
|
+
// delegate synchronously; GaplessEngine does not.)
|
|
58
|
+
if !self.isMutatingPlayerQueue,
|
|
59
|
+
let resolved = self.matchTrackIndex(forCurrentItem: engine.currentMediaItem) {
|
|
60
|
+
self.currentTrackIndex = resolved
|
|
61
|
+
}
|
|
62
|
+
let reason = self.pendingTrackChangeReason ?? .autoAdvance
|
|
63
|
+
self.pendingTrackChangeReason = nil
|
|
64
|
+
dispatchTrackChange(reason: reason)
|
|
65
|
+
self.recomputeCapabilities()
|
|
66
|
+
self.rescheduleLookahead()
|
|
67
|
+
// Extend the installed run as the playhead advances into it. Driven from
|
|
68
|
+
// the transition rather than from arming, which would force the build into
|
|
69
|
+
// the moment immediately before the ramp is scheduled.
|
|
70
|
+
//
|
|
71
|
+
// Not while a mutation is in flight: this delegate is fired synchronously
|
|
72
|
+
// from `CrossfadeEngine.setItems`, so topping up there would re-enter
|
|
73
|
+
// `setItems` from inside itself — and the mutation that raised the flag
|
|
74
|
+
// installs the run it wants anyway.
|
|
75
|
+
if !self.isMutatingPlayerQueue {
|
|
76
|
+
self.topUpWindow()
|
|
77
|
+
}
|
|
78
|
+
// Refresh the now-playing format for the new active item. The crossfade
|
|
79
|
+
// engine's `\.tracks` KVO is `.new`-only and a forward skip rebuilds the
|
|
80
|
+
// incoming leg as a fresh AVPlayerItem whose tracks key isn't auto-loaded
|
|
81
|
+
// (only playable/duration are), so engineActiveItemFormatChanged may never
|
|
82
|
+
// fire for it — without this, getNowPlayingFormat() stays nil after a
|
|
83
|
+
// skip to a cached track. The extractor loads the format asynchronously
|
|
84
|
+
// and the lastEmittedFormatItemId dedup makes a redundant call a no-op.
|
|
85
|
+
self.refreshNowPlayingFormatForActiveItem()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func engineCrossfadeDidBegin(_ engine: PlaybackEngine) {
|
|
89
|
+
// Fade-start: the standby leg is now audibly playing (volume 0
|
|
90
|
+
// ramping up). Flip JS-facing active track + lock-screen / Now
|
|
91
|
+
// Playing surface to the incoming track immediately so the
|
|
92
|
+
// user sees the new track's title/artwork the moment they
|
|
93
|
+
// start hearing it. The leading leg continues fading out for
|
|
94
|
+
// the configured `crossfadeDurationMs`; engine accessors
|
|
95
|
+
// already prefer the standby leg while `fadeInFlight` is true.
|
|
96
|
+
//
|
|
97
|
+
// Reason is hardcoded `.autoAdvance` — fade-start is always an
|
|
98
|
+
// auto-advance event; any user-driven transition (skipToNext /
|
|
99
|
+
// skipToIndex / setMediaItems) would have called
|
|
100
|
+
// `cancelFadeInternal` first, so a stashed
|
|
101
|
+
// `pendingTrackChangeReason` here is stale and should not
|
|
102
|
+
// re-attribute the fade.
|
|
103
|
+
// A queue mutation already computed the authoritative index; the
|
|
104
|
+
// rebuild's own fade-cancel must not walk it back to the outgoing leg.
|
|
105
|
+
guard !self.isMutatingPlayerQueue,
|
|
106
|
+
let incoming = self.matchTrackIndex(forCurrentItem: engine.currentMediaItem)
|
|
107
|
+
else { return }
|
|
108
|
+
self.currentTrackIndex = incoming
|
|
109
|
+
self.pendingTrackChangeReason = nil
|
|
110
|
+
dispatchTrackChange(reason: .autoAdvance)
|
|
111
|
+
self.recomputeCapabilities()
|
|
112
|
+
self.rescheduleLookahead()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func engineCrossfadeDidCancel(_ engine: PlaybackEngine) {
|
|
116
|
+
// Fade-cancelled mid-flight (pause / stop / focus loss /
|
|
117
|
+
// queue mutation). JS already received `onTrackChange(incoming,
|
|
118
|
+
// reason: .autoAdvance)` from the prior fade-start fire; the
|
|
119
|
+
// incoming track is no longer the active one. Revert
|
|
120
|
+
// `currentTrackIndex` to the leading-leg index and emit
|
|
121
|
+
// `onTrackChange(leading, reason: .crossfadeCancelled)` so
|
|
122
|
+
// consumer UI can rewind to the leading track.
|
|
123
|
+
// See `engineCrossfadeDidBegin` — a rebuild issued by a queue mutation
|
|
124
|
+
// cancels the in-flight fade, and that cancel must not clobber the
|
|
125
|
+
// index the mutation just computed.
|
|
126
|
+
guard !self.isMutatingPlayerQueue,
|
|
127
|
+
let reverted = self.matchTrackIndex(forCurrentItem: engine.currentMediaItem)
|
|
128
|
+
else { return }
|
|
129
|
+
self.currentTrackIndex = reverted
|
|
130
|
+
self.pendingTrackChangeReason = nil
|
|
131
|
+
dispatchTrackChange(reason: .crossfadeCancelled)
|
|
132
|
+
self.recomputeCapabilities()
|
|
133
|
+
self.rescheduleLookahead()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Apply whatever was held back while the fade ran.
|
|
137
|
+
///
|
|
138
|
+
/// Fired for every end of a fade, which is the point: a cancel taken during
|
|
139
|
+
/// the arm window reverts nothing and so reports nothing else, and work
|
|
140
|
+
/// waiting on that fade would otherwise wait for one that is already over —
|
|
141
|
+
/// with the engine still holding a run the queue has retired.
|
|
142
|
+
func engineFadeDidSettle(_ engine: PlaybackEngine) {
|
|
143
|
+
_ = engine
|
|
144
|
+
self.flushDeferredEngineWork()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func engineBufferStateMaybeChanged(_ engine: PlaybackEngine) {
|
|
148
|
+
_ = engine
|
|
149
|
+
// Buffer surface only — no direct player-state emission. The crossfade
|
|
150
|
+
// leading leg's buffer-flag KVO lands here; recompute the discrete
|
|
151
|
+
// buffer-state and fully-buffered surfaces, exactly as the gapless item KVO
|
|
152
|
+
// does through `installBufferObservers` (the stall-recovery restart inside
|
|
153
|
+
// `recomputeBufferState` still applies here, as on gapless). Already on
|
|
154
|
+
// `playerQueue` (the engine's ownerQueue), so recompute synchronously — a
|
|
155
|
+
// second async hop would reorder against the recompute's change dedup.
|
|
156
|
+
self.recomputeBufferState()
|
|
157
|
+
self.recomputeFullyBuffered()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func engineActiveItemFormatChanged(_ engine: PlaybackEngine) {
|
|
161
|
+
_ = engine
|
|
162
|
+
self.refreshNowPlayingFormatForActiveItem()
|
|
163
|
+
// Streaming-item audioMix catch-up for the crossfade engine
|
|
164
|
+
// (which fires this delegate hook on its own KVO chain). The
|
|
165
|
+
// gapless engine has a parallel catch-up path through
|
|
166
|
+
// `dispatchItemStatus(.readyToPlay)`; both call into
|
|
167
|
+
// `refreshActiveItemMixes`, which is idempotent on items that
|
|
168
|
+
// already carry a tap, so a double-fire is harmless.
|
|
169
|
+
AudioTapProvider.shared.refreshActiveItemMixes()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func engineDidFailWithError(
|
|
173
|
+
_ engine: PlaybackEngine, error: Error, item: AVPlayerItem?
|
|
174
|
+
) {
|
|
175
|
+
_ = engine
|
|
176
|
+
let underlying = error as NSError
|
|
177
|
+
// The item is forwarded, not dropped: without it `dispatchErrorOrRetry`
|
|
178
|
+
// synthesises a key that cannot appear in `queueItemIds`, and the
|
|
179
|
+
// automatic-retry branch requires a real `queueItemId`.
|
|
180
|
+
dispatchErrorOrRetry(
|
|
181
|
+
item: item,
|
|
182
|
+
underlying: underlying,
|
|
183
|
+
nativeDomainOverride: nil
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func engineDidPlayItemToEnd(_ engine: PlaybackEngine) {
|
|
188
|
+
// CrossfadeEngine fires this on natural end-of-leading; the
|
|
189
|
+
// boundary signal that drives JS-facing track-change is
|
|
190
|
+
// engineDidTransitionTrack which fires immediately after.
|
|
191
|
+
//
|
|
192
|
+
// Under repeat-one the same track replays with no track-change (the
|
|
193
|
+
// queueItemId is unchanged, so dispatchTrackChange's dedup skips its
|
|
194
|
+
// reset). Each loop is a fresh playthrough, so re-arm the milestone
|
|
195
|
+
// tracker here for that case only.
|
|
196
|
+
if self.repeatModeState == .track {
|
|
197
|
+
self.milestoneTracker.reset()
|
|
198
|
+
}
|
|
199
|
+
_ = engine
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
func enginePlaybackEnded(_ engine: PlaybackEngine) {
|
|
203
|
+
_ = engine
|
|
204
|
+
// The engine reports that it drained; what that means for the queue —
|
|
205
|
+
// wrap, refill, or over — is decided above it, the same way for both
|
|
206
|
+
// engines.
|
|
207
|
+
applyEndVerdict()
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
extension TrackPlayer {
|
|
4
|
+
// MARK: - Events — public registration surface
|
|
5
|
+
//
|
|
6
|
+
// Each event has a multi-listener `ListenerRegistry`. Each `onXxx`
|
|
7
|
+
// call appends a new entry; the returned closure removes that entry
|
|
8
|
+
// by stable id. ListenerRegistry's internal queue.sync makes
|
|
9
|
+
// add/remove safe against concurrent fire rounds — fire iterates a
|
|
10
|
+
// snapshot taken under the lock.
|
|
11
|
+
|
|
12
|
+
func onStateChange(
|
|
13
|
+
callback: @escaping (_ state: PlayerState, _ reason: StateChangeReason) -> Void
|
|
14
|
+
) throws -> () -> Void {
|
|
15
|
+
let id = stateChangeListeners.add(callback)
|
|
16
|
+
return { [weak self] in self?.stateChangeListeners.remove(id: id) }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func onTrackChange(
|
|
20
|
+
callback: @escaping (_ track: TrackItem?, _ index: Double, _ reason: TrackChangeReason, _ transitionGapMs: Double?) -> Void
|
|
21
|
+
) throws -> () -> Void {
|
|
22
|
+
let id = trackChangeListeners.add(callback)
|
|
23
|
+
return { [weak self] in self?.trackChangeListeners.remove(id: id) }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func onQueueChange(
|
|
27
|
+
callback: @escaping (_ delta: QueueChangeDelta, _ currentIndex: Double, _ reason: QueueChangeReason) -> Void
|
|
28
|
+
) throws -> () -> Void {
|
|
29
|
+
let id = queueChangeListeners.add(callback)
|
|
30
|
+
return { [weak self] in self?.queueChangeListeners.remove(id: id) }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Emit a queue change as a description of what happened rather than a copy
|
|
34
|
+
/// of the queue. Callers pass only the fields their mutation defines; the
|
|
35
|
+
/// rest stay empty, and `revision` lets a subscriber notice a dropped event.
|
|
36
|
+
internal func emitQueueChange(
|
|
37
|
+
_ reason: QueueChangeReason,
|
|
38
|
+
inserted: [TrackItem] = [],
|
|
39
|
+
insertedAt: Int = -1,
|
|
40
|
+
removed: [Int] = [],
|
|
41
|
+
movedFrom: Int = -1,
|
|
42
|
+
movedTo: Int = -1,
|
|
43
|
+
order: [Int] = []
|
|
44
|
+
) {
|
|
45
|
+
self.queueRevision += 1
|
|
46
|
+
let delta = QueueChangeDelta(
|
|
47
|
+
revision: Double(self.queueRevision),
|
|
48
|
+
length: Double(self.tracks.count),
|
|
49
|
+
inserted: inserted,
|
|
50
|
+
insertedAt: Double(insertedAt),
|
|
51
|
+
removed: removed.map(Double.init),
|
|
52
|
+
movedFrom: Double(movedFrom),
|
|
53
|
+
movedTo: Double(movedTo),
|
|
54
|
+
order: order.map(Double.init)
|
|
55
|
+
)
|
|
56
|
+
let index = Double(self.currentTrackIndex)
|
|
57
|
+
queueChangeListeners.forEach { $0(delta, index, reason) }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func onProgress(
|
|
61
|
+
callback: @escaping (_ progress: PlayerProgress) -> Void
|
|
62
|
+
) throws -> () -> Void {
|
|
63
|
+
let id = progressListeners.add(callback)
|
|
64
|
+
return { [weak self] in self?.progressListeners.remove(id: id) }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func onPlaybackMilestone(
|
|
68
|
+
callback: @escaping (_ milestone: Double, _ trackIndex: Double) -> Void
|
|
69
|
+
) throws -> () -> Void {
|
|
70
|
+
let id = milestoneListeners.add(callback)
|
|
71
|
+
return { [weak self] in self?.milestoneListeners.remove(id: id) }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func onError(callback: @escaping (_ error: PlaybackError) -> Void) throws -> () -> Void {
|
|
75
|
+
let id = errorListeners.add(callback)
|
|
76
|
+
return { [weak self] in self?.errorListeners.remove(id: id) }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func onQueueEnd(callback: @escaping () -> Void) throws -> () -> Void {
|
|
80
|
+
let id = queueEndListeners.add(callback)
|
|
81
|
+
return { [weak self] in self?.queueEndListeners.remove(id: id) }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Subscribe to skip-capability changes. Auto-fires the callback
|
|
85
|
+
/// once on registration with the current cached values (mirrors
|
|
86
|
+
/// Media3 `Player.Listener.onAvailableCommandsChanged`) so new
|
|
87
|
+
/// subscribers don't have to seed via the getters first. The
|
|
88
|
+
/// auto-fire hops to `playerQueue` so the cache read is on the
|
|
89
|
+
/// queue that writes it. Subsequent fires happen synchronously
|
|
90
|
+
/// from `recomputeCapabilities()`, also on `playerQueue`.
|
|
91
|
+
///
|
|
92
|
+
/// **Dispose race guard:** the deferred fire checks the
|
|
93
|
+
/// subscriber is still registered (`contains(id:)`) before
|
|
94
|
+
/// invoking the callback. Strict-mode subscribe → dispose →
|
|
95
|
+
/// subscribe sequences fire the cleanup before the deferred fire
|
|
96
|
+
/// runs; without the guard, the auto-fire would call into an
|
|
97
|
+
/// already-unmounted React component's `setSnapshot`.
|
|
98
|
+
func onSkipCapabilityChange(
|
|
99
|
+
callback: @escaping (_ capability: SkipCapability) -> Void
|
|
100
|
+
) throws -> () -> Void {
|
|
101
|
+
let id = skipCapabilityListeners.add(callback)
|
|
102
|
+
playerQueue.async { [weak self] in
|
|
103
|
+
guard let self else { return }
|
|
104
|
+
guard self.skipCapabilityListeners.contains(id: id) else { return }
|
|
105
|
+
callback(SkipCapability(
|
|
106
|
+
canSkipPrevious: self.cachedSkipCapability.canSkipPrevious,
|
|
107
|
+
canSkipNext: self.cachedSkipCapability.canSkipNext))
|
|
108
|
+
}
|
|
109
|
+
return { [weak self] in self?.skipCapabilityListeners.remove(id: id) }
|
|
110
|
+
}
|
|
111
|
+
}
|