react-native-queue-player 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/build.gradle +7 -0
- package/android/consumer-rules.pro +72 -3
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +26 -22
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +20 -72
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +11 -0
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +660 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +202 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +303 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +394 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +968 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +930 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +940 -0
- package/ios/TrackPlayer+Recovery.swift +196 -0
- package/ios/TrackPlayer+Skip.swift +455 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +267 -0
- package/ios/TrackPlayer+Window.swift +195 -0
- package/ios/TrackPlayer.swift +305 -4610
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
|
@@ -9,40 +9,29 @@ import androidx.media3.exoplayer.audio.TeeAudioProcessor
|
|
|
9
9
|
/**
|
|
10
10
|
* Strategy abstraction over the audio output pipeline.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Three concrete implementations:
|
|
13
13
|
* - [GaplessEngine]: single ExoPlayer, native gapless via item chaining.
|
|
14
14
|
* - [CrossfadeEngine]: dual ExoPlayer with equal-power gain ramping at
|
|
15
15
|
* track boundaries.
|
|
16
|
+
* - [AirPlayEngine]: single ExoPlayer bound to an AirPlay PCM sink;
|
|
17
|
+
* selected by `applyEngineSwapForMode` while an AirPlay session is
|
|
18
|
+
* active.
|
|
16
19
|
*
|
|
17
20
|
* The engine owns the player instance(s), audio-thread effects, and
|
|
18
21
|
* track-boundary scheduling. The lib's [TrackPlayer] holds the canonical
|
|
19
22
|
* queue model + listener registries + system-side surfaces (NowPlaying,
|
|
20
23
|
* RemoteCommands, AudioSession) and routes transport / queue calls
|
|
21
|
-
* through whichever engine is active.
|
|
22
|
-
* is
|
|
24
|
+
* through whichever engine is active. An engine swap (gapless ↔ crossfade)
|
|
25
|
+
* is `TrackPlayer.applyEngineSwapForMode`: [PlaybackService.swapEngine]
|
|
26
|
+
* installs the new engine and releases the old one, then `TrackPlayer`
|
|
27
|
+
* seeds the new engine through [setMediaItems].
|
|
23
28
|
*
|
|
24
29
|
* Threading: all methods are called on the service main looper unless
|
|
25
|
-
* noted
|
|
26
|
-
*
|
|
27
|
-
* lookahead-cache prefetch).
|
|
30
|
+
* noted; the impl is responsible for any internal off-loop work (e.g.
|
|
31
|
+
* heavy lookahead-cache prefetch).
|
|
28
32
|
*/
|
|
29
33
|
interface PlaybackEngine {
|
|
30
34
|
|
|
31
|
-
/**
|
|
32
|
-
* Build the underlying player(s), apply [mediaItems] starting at
|
|
33
|
-
* [startIndex] / [startPositionMs], and wait for the underlying
|
|
34
|
-
* player(s) to reach a playable state.
|
|
35
|
-
*
|
|
36
|
-
* Empty [mediaItems] is allowed; the engine prepares an empty
|
|
37
|
-
* timeline and [currentMediaItemIndex] returns `-1` until items
|
|
38
|
-
* arrive via [setMediaItems] / [addMediaItems].
|
|
39
|
-
*/
|
|
40
|
-
suspend fun prepare(
|
|
41
|
-
mediaItems: List<MediaItem>,
|
|
42
|
-
startIndex: Int,
|
|
43
|
-
startPositionMs: Long,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
35
|
/**
|
|
47
36
|
* Release the underlying player(s) and stop all background work.
|
|
48
37
|
* After [release], no further methods are valid.
|
|
@@ -127,20 +116,16 @@ interface PlaybackEngine {
|
|
|
127
116
|
val allMediaItems: List<MediaItem>
|
|
128
117
|
|
|
129
118
|
/**
|
|
130
|
-
* Underlying ExoPlayer
|
|
131
|
-
*
|
|
132
|
-
*
|
|
119
|
+
* Underlying ExoPlayer for the audio-focus listener and the
|
|
120
|
+
* `MediaSession` binding. `GaplessEngine` returns its single
|
|
121
|
+
* player, which [PlaybackService] binds directly; `CrossfadeEngine`
|
|
133
122
|
* returns the leading leg's player.
|
|
134
123
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* `CrossfadeForwardingPlayer` wrapper exists to bridge that
|
|
141
|
-
* gap (re-target the bound delegate via `setPlayer` on every
|
|
142
|
-
* role swap); wiring lands when [PlaybackService] starts
|
|
143
|
-
* binding through the wrapper for crossfade engines.
|
|
124
|
+
* A mid-fade role swap inside `CrossfadeEngine` flips which player this
|
|
125
|
+
* getter returns, and nothing re-reads it: [PlaybackService] binds a
|
|
126
|
+
* crossfade engine through a [CrossfadeForwardingPlayer] instead, which
|
|
127
|
+
* re-targets via `setPlayer` on every role swap, fade start and fade
|
|
128
|
+
* cancel, so the session and the focus listener follow the audible leg.
|
|
144
129
|
*/
|
|
145
130
|
val mediaSessionPlayer: ExoPlayer
|
|
146
131
|
|
|
@@ -176,12 +161,36 @@ interface PlaybackEngine {
|
|
|
176
161
|
fun detachAudioBufferSink(sink: TeeAudioProcessor.AudioBufferSink)
|
|
177
162
|
|
|
178
163
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
164
|
+
* Settle any in-flight or armed crossfade so the queue can be mutated by
|
|
165
|
+
* something other than this engine.
|
|
166
|
+
*
|
|
167
|
+
* An armed standby leg holds a snapshot of the queue taken when it was
|
|
168
|
+
* armed. A caller that appends or replaces items through a path the engine
|
|
169
|
+
* does not see — Media3 setting them on the session player after resolving
|
|
170
|
+
* an assistant request — leaves that snapshot stale, and promoting it drops
|
|
171
|
+
* whatever the caller added. Default is a no-op: an engine with one player
|
|
172
|
+
* has nothing to settle.
|
|
173
|
+
*/
|
|
174
|
+
fun settlePendingFade() {}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Settle an in-flight fade by promoting the incoming leg, so the leg a
|
|
178
|
+
* caller reads is the track actually being heard.
|
|
179
|
+
*
|
|
180
|
+
* Mid-fade the leading leg holds the outgoing track while the canonical
|
|
181
|
+
* index names the incoming one, so a caller that reads the player and the
|
|
182
|
+
* index together sees two different tracks. Promoting resolves them to the
|
|
183
|
+
* incoming track, which is the one the listener is arriving at. Default is a
|
|
184
|
+
* no-op: an engine with one player is never in this state.
|
|
185
|
+
*/
|
|
186
|
+
fun settlePendingFadeByPromoting() {}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Snapshot the active item's cached [ReplayGainData], or `null` when no
|
|
190
|
+
* usable RG tags have been parsed yet. Surfaced by
|
|
191
|
+
* [NowPlayingFormatExtractor] to populate the four `replayGain*Db` /
|
|
192
|
+
* `replayGain*Peak` fields. The default is `null`, for an engine that
|
|
193
|
+
* reads no tags.
|
|
185
194
|
*/
|
|
186
195
|
fun currentReplayGainData(): ReplayGainData? = null
|
|
187
196
|
|
|
@@ -194,43 +203,22 @@ interface PlaybackEngine {
|
|
|
194
203
|
*/
|
|
195
204
|
fun currentReplayGainActive(): Boolean = false
|
|
196
205
|
|
|
197
|
-
/**
|
|
198
|
-
* Hand playback off to [nextEngine] when the user changes
|
|
199
|
-
* `PlaybackMode`.
|
|
200
|
-
*
|
|
201
|
-
* Contract:
|
|
202
|
-
* - The receiver inherits the donor's queue, current index, and
|
|
203
|
-
* current position; the caller does NOT pre-call [prepare] on
|
|
204
|
-
* [nextEngine].
|
|
205
|
-
* - When [atTrackBoundary] is `true`, the donor plays the active
|
|
206
|
-
* track to its natural end and the receiver starts playing the
|
|
207
|
-
* NEXT queue position fresh.
|
|
208
|
-
* - When [atTrackBoundary] is `false` (idle player or empty
|
|
209
|
-
* queue), the swap is immediate; receiver picks up at the
|
|
210
|
-
* inherited position.
|
|
211
|
-
* - The donor's [release] is the caller's responsibility AFTER
|
|
212
|
-
* the boundary fires; the engine itself does not self-release.
|
|
213
|
-
* - The donor's listeners on [delegate] continue firing until
|
|
214
|
-
* [release] runs; the caller swaps the delegate before that.
|
|
215
|
-
*/
|
|
216
|
-
suspend fun handoff(to: PlaybackEngine, atTrackBoundary: Boolean)
|
|
217
|
-
|
|
218
206
|
/**
|
|
219
207
|
* Single delegate receiving state / track / progress / error /
|
|
220
208
|
* queue-end events. TrackPlayer is the canonical delegate; engines
|
|
221
209
|
* fire on the service main looper.
|
|
222
210
|
*
|
|
223
|
-
* Strong reference by design; the iOS counterpart
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
211
|
+
* Strong reference by design; the iOS counterpart is `weak` because
|
|
212
|
+
* `TrackPlayer` owns its engine strongly and Swift's reference graph
|
|
213
|
+
* would otherwise close (TrackPlayer → engine → delegate → TrackPlayer).
|
|
214
|
+
* On Android the engine lives inside [PlaybackService], reached through
|
|
215
|
+
* [PlaybackService.LocalBinder.engine] and owned by the service
|
|
216
|
+
* lifecycle, not by TrackPlayer; the cycle is broken explicitly by
|
|
217
|
+
* [release] dropping `delegate` and by `TrackPlayer.tearDownEventObservers`
|
|
218
|
+
* clearing itself from the engine's `delegate`.
|
|
231
219
|
*
|
|
232
220
|
* Threading: setter MUST be invoked on the service main looper to
|
|
233
|
-
* avoid a write-vs-fire race during
|
|
221
|
+
* avoid a write-vs-fire race during an engine swap.
|
|
234
222
|
*/
|
|
235
223
|
var delegate: PlaybackEngineDelegate?
|
|
236
224
|
}
|
|
@@ -290,10 +278,14 @@ interface PlaybackEngineDelegate {
|
|
|
290
278
|
fun onActiveItemFormatChanged(engine: PlaybackEngine)
|
|
291
279
|
|
|
292
280
|
/**
|
|
293
|
-
* Fires on Media3 `Player.Listener.onPlayerError`. The delegate
|
|
294
|
-
*
|
|
281
|
+
* Fires on Media3 `Player.Listener.onPlayerError`. The delegate maps
|
|
282
|
+
* [exception] via the lib's `PlaybackErrorMapping`. [failedIndex] is the
|
|
283
|
+
* queue position of the item that failed, or -1 when the engine cannot
|
|
284
|
+
* attribute it. A crossfade engine can fail on its standby leg while the
|
|
285
|
+
* leading leg plays on, so the failure need not belong to the track the
|
|
286
|
+
* delegate considers current.
|
|
295
287
|
*/
|
|
296
|
-
fun onError(engine: PlaybackEngine, exception: PlaybackException)
|
|
288
|
+
fun onError(engine: PlaybackEngine, exception: PlaybackException, failedIndex: Int = -1)
|
|
297
289
|
|
|
298
290
|
/**
|
|
299
291
|
* Fires on Media3 `Player.STATE_ENDED`. The delegate consults the
|
|
@@ -316,9 +308,10 @@ interface PlaybackEngineDelegate {
|
|
|
316
308
|
* [ExoPlayer] has started playback at volume 0 and the leading
|
|
317
309
|
* leg's fade-out has begun. The delegate treats this as the
|
|
318
310
|
* canonical "active track changed" trigger and emits
|
|
319
|
-
* `onTrackChange`
|
|
320
|
-
*
|
|
321
|
-
*
|
|
311
|
+
* `onTrackChange` for the incoming track; the `MediaSession` re-bind
|
|
312
|
+
* to the standby leg is [CrossfadeForwardingPlayer]'s, through the
|
|
313
|
+
* engine's `onCrossfadeBegin` lambda. The subsequent `onTrackTransition`
|
|
314
|
+
* for the same incoming index is a structural Media3 echo and must be
|
|
322
315
|
* deduplicated.
|
|
323
316
|
*
|
|
324
317
|
* Crossfade-only — [GaplessEngine] never invokes this.
|
|
@@ -333,7 +326,9 @@ interface PlaybackEngineDelegate {
|
|
|
333
326
|
* mutation that invalidates the standby leg). The delegate
|
|
334
327
|
* reverts JS-facing state to the leading leg — emit
|
|
335
328
|
* `onTrackChange(reason: CROSSFADE_CANCELLED)` for the leading
|
|
336
|
-
* track
|
|
329
|
+
* track; the `MediaSession` re-bind back to the leading leg is
|
|
330
|
+
* [CrossfadeForwardingPlayer]'s, through the engine's
|
|
331
|
+
* `onCrossfadeCancel` lambda. Pairs with
|
|
337
332
|
* [onCrossfadeBegin]: only fires when a `Begin` was fired
|
|
338
333
|
* earlier and the fade did not complete.
|
|
339
334
|
*
|
|
@@ -85,9 +85,9 @@ internal object PlaybackErrorMapping {
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Strip query strings from a native error message so logged
|
|
88
|
-
* `nativeMessage` doesn't leak auth tokens
|
|
89
|
-
*
|
|
90
|
-
* `PlaybackException.message
|
|
88
|
+
* `nativeMessage` doesn't leak auth tokens: stream URLs with
|
|
89
|
+
* `?token=…`-style query strings end up inside
|
|
90
|
+
* `PlaybackException.message`.
|
|
91
91
|
*
|
|
92
92
|
* Walks the full message removing EVERY `?…(whitespace|end)` run
|
|
93
93
|
* so multi-URL error messages have all query strings stripped.
|
|
@@ -49,8 +49,8 @@ internal class PlaybackModeStateMachine {
|
|
|
49
49
|
* the new duration.
|
|
50
50
|
*
|
|
51
51
|
* The caller is responsible for managing the brief audio
|
|
52
|
-
* interruption
|
|
53
|
-
*
|
|
52
|
+
* interruption — `applyEngineSwapForMode` carries the position
|
|
53
|
+
* and re-installs the queue.
|
|
54
54
|
*/
|
|
55
55
|
fun setRequested(
|
|
56
56
|
state: State,
|
|
@@ -17,7 +17,6 @@ import android.os.IBinder
|
|
|
17
17
|
import android.os.Looper
|
|
18
18
|
import androidx.core.content.ContextCompat
|
|
19
19
|
import androidx.annotation.VisibleForTesting
|
|
20
|
-
import androidx.media3.common.C
|
|
21
20
|
import androidx.media3.common.MediaItem
|
|
22
21
|
import androidx.media3.common.Player
|
|
23
22
|
import androidx.media3.common.util.UnstableApi
|
|
@@ -26,25 +25,17 @@ import androidx.media3.exoplayer.ExoPlayer
|
|
|
26
25
|
import androidx.media3.exoplayer.source.MediaSource
|
|
27
26
|
import androidx.media3.session.CacheBitmapLoader
|
|
28
27
|
import androidx.media3.session.CommandButton
|
|
29
|
-
import androidx.media3.session.LibraryResult
|
|
30
28
|
import androidx.media3.session.MediaLibraryService
|
|
31
|
-
import androidx.media3.session.MediaLibraryService.LibraryParams
|
|
32
29
|
import androidx.media3.session.MediaLibraryService.MediaLibrarySession
|
|
33
30
|
import androidx.media3.session.MediaSession
|
|
34
|
-
import androidx.media3.session.SessionResult
|
|
35
|
-
import com.google.common.collect.ImmutableList
|
|
36
|
-
import com.google.common.util.concurrent.Futures
|
|
37
31
|
import com.google.common.util.concurrent.ListenableFuture
|
|
38
32
|
import com.google.common.util.concurrent.MoreExecutors
|
|
39
33
|
import com.google.common.util.concurrent.SettableFuture
|
|
40
34
|
import com.margelo.nitro.core.Promise
|
|
41
35
|
import com.margelo.nitro.queueplayer.cast.PlaybackStateRouter
|
|
42
36
|
import java.util.UUID
|
|
37
|
+
import kotlinx.coroutines.Dispatchers
|
|
43
38
|
import kotlinx.coroutines.cancel
|
|
44
|
-
import kotlinx.coroutines.currentCoroutineContext
|
|
45
|
-
import kotlinx.coroutines.delay
|
|
46
|
-
import kotlinx.coroutines.flow.collect
|
|
47
|
-
import kotlinx.coroutines.isActive
|
|
48
39
|
import kotlinx.coroutines.launch
|
|
49
40
|
import kotlinx.coroutines.withContext
|
|
50
41
|
|
|
@@ -147,7 +138,7 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
147
138
|
* resolve — covers (a) the JS bridge spinning up + the consumer's
|
|
148
139
|
* `registerPlaybackService` running, AND (b) the consumer's
|
|
149
140
|
* resolution Promise running. 20 s mirrors the iOS
|
|
150
|
-
*
|
|
141
|
+
* `PendingIntentBuffer` TTL: ~3 s for bridge boot + ~17 s of
|
|
151
142
|
* headroom for the consumer's network calls before the controller
|
|
152
143
|
* surfaces "no results" and the user retries. The two platforms'
|
|
153
144
|
* cold-start windows are deliberately kept the same so the
|
|
@@ -260,8 +251,8 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
260
251
|
|
|
261
252
|
/**
|
|
262
253
|
* Per-query cache of JS-supplied search results. Populated by
|
|
263
|
-
*
|
|
264
|
-
* responds
|
|
254
|
+
* [dispatchSearchToJs] once the JS `onSearchRequest` handler
|
|
255
|
+
* responds; read by
|
|
265
256
|
* [PlaybackServiceCallback.onGetSearchResult]. Empty by default —
|
|
266
257
|
* a controller's `getSearchResult` call before the JS responder
|
|
267
258
|
* has answered returns an empty page.
|
|
@@ -315,7 +306,7 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
315
306
|
// Cold-wake the JS bridge so the consumer's
|
|
316
307
|
// `registerPlaybackService` can fire its callbacks before
|
|
317
308
|
// Media3's framework dispatches `onAddMediaItems`. Idempotent
|
|
318
|
-
// — `HeadlessJsMediaService.initialized`
|
|
309
|
+
// — `HeadlessJsMediaService.initialized` is set by the JS task
|
|
319
310
|
// spawn so re-entrant connects do not double-spawn.
|
|
320
311
|
this@PlaybackService.selfWakeForHeadlessClient(packageName)
|
|
321
312
|
}
|
|
@@ -341,14 +332,16 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
341
332
|
}
|
|
342
333
|
|
|
343
334
|
override fun onPlayFromSearchRequest(
|
|
344
|
-
request: MediaSearchRequest
|
|
335
|
+
request: MediaSearchRequest,
|
|
336
|
+
append: Boolean
|
|
345
337
|
): ListenableFuture<List<MediaItem>> =
|
|
346
|
-
this@PlaybackService.dispatchPlayFromSearchToJs(request)
|
|
338
|
+
this@PlaybackService.dispatchPlayFromSearchToJs(request, append)
|
|
347
339
|
|
|
348
340
|
override fun onPlayFromIdRequest(
|
|
349
|
-
mediaId: String
|
|
341
|
+
mediaId: String,
|
|
342
|
+
append: Boolean
|
|
350
343
|
): ListenableFuture<List<MediaItem>> =
|
|
351
|
-
this@PlaybackService.dispatchPlayFromIdToJs(mediaId)
|
|
344
|
+
this@PlaybackService.dispatchPlayFromIdToJs(mediaId, append)
|
|
352
345
|
|
|
353
346
|
override fun onPlayFromIdRequestNoSync(mediaId: String) {
|
|
354
347
|
this@PlaybackService.dispatchPlayFromIdToJsFireOnly(mediaId)
|
|
@@ -425,14 +418,6 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
425
418
|
*/
|
|
426
419
|
val instanceId: java.util.UUID = java.util.UUID.randomUUID()
|
|
427
420
|
|
|
428
|
-
/**
|
|
429
|
-
* Direct access to the underlying ExoPlayer. Retained as a
|
|
430
|
-
* back-compat shim for [TrackPlayer] until its transport /
|
|
431
|
-
* queue calls migrate onto the [engine] surface; engine-aware
|
|
432
|
-
* code paths use [engine] instead.
|
|
433
|
-
*/
|
|
434
|
-
val player: ExoPlayer get() = exoPlayer
|
|
435
|
-
|
|
436
421
|
/**
|
|
437
422
|
* Strategy-pattern engine wrapping the active player(s).
|
|
438
423
|
* Routes through the abstract [PlaybackEngine] surface so
|
|
@@ -464,7 +449,7 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
464
449
|
* AVRCP, Android Auto, and Wear surfaces grey out skip buttons
|
|
465
450
|
* that the queue boundary makes invalid. No-op when no handler
|
|
466
451
|
* is registered (no authoritative capability source) or when no
|
|
467
|
-
* controllers have connected yet (next [
|
|
452
|
+
* controllers have connected yet (next [PlaybackServiceCallback.onConnect]
|
|
468
453
|
* picks up the fresh capability).
|
|
469
454
|
*/
|
|
470
455
|
internal fun refreshAvailableCommands() {
|
|
@@ -504,9 +489,12 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
504
489
|
private val localBinder by lazy { LocalBinder() }
|
|
505
490
|
|
|
506
491
|
/**
|
|
507
|
-
* Currently-registered handler consulted by [
|
|
508
|
-
* before the default routing runs
|
|
509
|
-
*
|
|
492
|
+
* Currently-registered handler consulted by [SessionCommandForwardingPlayer]
|
|
493
|
+
* before the default routing runs, by [MediaButtonDispatch] for raw
|
|
494
|
+
* media-button intents, by the audio-focus listener, and by
|
|
495
|
+
* [PlaybackServiceCallback.onConnect] for the initial player commands.
|
|
496
|
+
* `null` until
|
|
497
|
+
* [TrackPlayer.configureInternal] registers one; under that
|
|
510
498
|
* state every callback falls through to the Media3 default and
|
|
511
499
|
* raw media-button intents fall through to the framework.
|
|
512
500
|
*/
|
|
@@ -640,7 +628,7 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
640
628
|
* PcmStream (AirPlay) session is not a RemotePlayer.
|
|
641
629
|
*/
|
|
642
630
|
private val castBindScope = kotlinx.coroutines.CoroutineScope(
|
|
643
|
-
kotlinx.coroutines.SupervisorJob() +
|
|
631
|
+
kotlinx.coroutines.SupervisorJob() + Dispatchers.Main.immediate
|
|
644
632
|
)
|
|
645
633
|
|
|
646
634
|
override fun onCreate() {
|
|
@@ -925,8 +913,8 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
925
913
|
* the new leading player, swap `MediaSession` to the new
|
|
926
914
|
* player, carry the delegate over, re-wire the Equalizer, and
|
|
927
915
|
* release the old engine. Queue migration is the caller's
|
|
928
|
-
* responsibility — `TrackPlayer`
|
|
929
|
-
*
|
|
916
|
+
* responsibility — `TrackPlayer` seeds the new engine through
|
|
917
|
+
* `setMediaItems` after this returns.
|
|
930
918
|
*/
|
|
931
919
|
fun swapEngine(newEngine: PlaybackEngine): PlaybackEngine {
|
|
932
920
|
return swapEngineInternal(newEngine)
|
|
@@ -1080,6 +1068,16 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1080
1068
|
* complete kill. A future multi-session change would need to stop each.
|
|
1081
1069
|
*/
|
|
1082
1070
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
|
1071
|
+
// Stop the engine, not the session's player. Under crossfade the session
|
|
1072
|
+
// is bound to a forwarding wrapper that addresses one leg, so stopping it
|
|
1073
|
+
// leaves the other leg running: the task is swiped away, the notification
|
|
1074
|
+
// and session go, and audio keeps playing with nothing left to control
|
|
1075
|
+
// it. The engine stops both legs and abandons audio focus.
|
|
1076
|
+
engine.stop()
|
|
1077
|
+
// And the session's player. While cast-bound that is the receiver, which
|
|
1078
|
+
// the engine does not own — stopping only the legs leaves the Chromecast
|
|
1079
|
+
// playing after the task, notification and session are all gone. Off cast
|
|
1080
|
+
// it is the forwarding wrapper around a leg the engine just stopped.
|
|
1083
1081
|
mediaSession?.player?.stop()
|
|
1084
1082
|
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
|
1085
1083
|
stopSelf()
|
|
@@ -1129,7 +1127,7 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1129
1127
|
*/
|
|
1130
1128
|
private val callbackDispatchScope =
|
|
1131
1129
|
kotlinx.coroutines.CoroutineScope(
|
|
1132
|
-
kotlinx.coroutines.SupervisorJob() +
|
|
1130
|
+
kotlinx.coroutines.SupervisorJob() + Dispatchers.IO
|
|
1133
1131
|
)
|
|
1134
1132
|
|
|
1135
1133
|
/**
|
|
@@ -1152,6 +1150,9 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1152
1150
|
kotlinx.coroutines.withTimeoutOrNull(SEARCH_DISPATCH_TIMEOUT_MS) {
|
|
1153
1151
|
callback(query).await().await().toList()
|
|
1154
1152
|
} ?: emptyList()
|
|
1153
|
+
} catch (t: kotlinx.coroutines.CancellationException) {
|
|
1154
|
+
// The service is being destroyed; nothing below may run.
|
|
1155
|
+
throw t
|
|
1155
1156
|
} catch (t: Throwable) {
|
|
1156
1157
|
// JS handler threw — surface as an empty answered result so
|
|
1157
1158
|
// the controller stops waiting. The thrown error is the
|
|
@@ -1165,7 +1166,11 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1165
1166
|
emptyList()
|
|
1166
1167
|
}
|
|
1167
1168
|
searchResultCache.setResults(query, items)
|
|
1168
|
-
|
|
1169
|
+
// `mediaSession` is owned by main and nulled by onDestroy there; the
|
|
1170
|
+
// notify has to read it on the same thread or it races the release.
|
|
1171
|
+
withContext(Dispatchers.Main) {
|
|
1172
|
+
notifySearchResultChanged(query, items.size)
|
|
1173
|
+
}
|
|
1169
1174
|
}
|
|
1170
1175
|
}
|
|
1171
1176
|
|
|
@@ -1185,9 +1190,11 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1185
1190
|
* retries).
|
|
1186
1191
|
*/
|
|
1187
1192
|
internal fun dispatchPlayFromSearchToJs(
|
|
1188
|
-
request: MediaSearchRequest
|
|
1193
|
+
request: MediaSearchRequest,
|
|
1194
|
+
append: Boolean = false
|
|
1189
1195
|
): ListenableFuture<List<MediaItem>> = dispatchAssistantPlay(
|
|
1190
1196
|
methodLabel = "onPlayFromSearchRequest",
|
|
1197
|
+
append = append,
|
|
1191
1198
|
awaitSlot = { browseCallbacks.awaitPlayFromSearchRequestCallback() }
|
|
1192
1199
|
) { browseCallbacks.playFromSearchRequestCallback()?.invoke(request) }
|
|
1193
1200
|
|
|
@@ -1198,9 +1205,11 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1198
1205
|
* [dispatchPlayFromSearchToJs].
|
|
1199
1206
|
*/
|
|
1200
1207
|
internal fun dispatchPlayFromIdToJs(
|
|
1201
|
-
mediaId: String
|
|
1208
|
+
mediaId: String,
|
|
1209
|
+
append: Boolean = false
|
|
1202
1210
|
): ListenableFuture<List<MediaItem>> = dispatchAssistantPlay(
|
|
1203
1211
|
methodLabel = "onPlayFromIdRequest",
|
|
1212
|
+
append = append,
|
|
1204
1213
|
awaitSlot = { browseCallbacks.awaitPlayFromIdRequestCallback() }
|
|
1205
1214
|
) { browseCallbacks.playFromIdRequestCallback()?.invoke(mediaId) }
|
|
1206
1215
|
|
|
@@ -1233,6 +1242,9 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1233
1242
|
}
|
|
1234
1243
|
promise?.await()?.await()
|
|
1235
1244
|
}
|
|
1245
|
+
} catch (t: kotlinx.coroutines.CancellationException) {
|
|
1246
|
+
// The service is being destroyed; nothing below may run.
|
|
1247
|
+
throw t
|
|
1236
1248
|
} catch (t: Throwable) {
|
|
1237
1249
|
android.util.Log.w(
|
|
1238
1250
|
LOG_TAG,
|
|
@@ -1263,6 +1275,9 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1263
1275
|
}
|
|
1264
1276
|
promise?.await()?.await()?.toList() ?: emptyList()
|
|
1265
1277
|
} ?: emptyList()
|
|
1278
|
+
} catch (t: kotlinx.coroutines.CancellationException) {
|
|
1279
|
+
// The service is being destroyed; nothing below may run.
|
|
1280
|
+
throw t
|
|
1266
1281
|
} catch (t: Throwable) {
|
|
1267
1282
|
android.util.Log.w(
|
|
1268
1283
|
LOG_TAG,
|
|
@@ -1286,6 +1301,7 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1286
1301
|
|
|
1287
1302
|
private inline fun dispatchAssistantPlay(
|
|
1288
1303
|
methodLabel: String,
|
|
1304
|
+
append: Boolean,
|
|
1289
1305
|
crossinline awaitSlot: suspend () -> Unit,
|
|
1290
1306
|
crossinline invoke: () -> Promise<Promise<Array<TrackItem>>>?
|
|
1291
1307
|
): ListenableFuture<List<MediaItem>> {
|
|
@@ -1303,6 +1319,9 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1303
1319
|
}
|
|
1304
1320
|
promise?.await()?.await()?.toList() ?: emptyList()
|
|
1305
1321
|
} ?: emptyList()
|
|
1322
|
+
} catch (t: kotlinx.coroutines.CancellationException) {
|
|
1323
|
+
// The service is being destroyed; nothing below may run.
|
|
1324
|
+
throw t
|
|
1306
1325
|
} catch (t: Throwable) {
|
|
1307
1326
|
android.util.Log.w(
|
|
1308
1327
|
LOG_TAG,
|
|
@@ -1316,7 +1335,7 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1316
1335
|
// with whatever this future resolves to, so we MUST return the
|
|
1317
1336
|
// real items — returning emptyList here would wipe the queue
|
|
1318
1337
|
// immediately after Media3 installs it.
|
|
1319
|
-
val items = applyAssistantQueueOnMain(tracks)
|
|
1338
|
+
val items = applyAssistantQueueOnMain(tracks, append)
|
|
1320
1339
|
future.set(items)
|
|
1321
1340
|
}
|
|
1322
1341
|
// Cancel the launched dispatch when the controller drops the
|
|
@@ -1355,26 +1374,27 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1355
1374
|
)
|
|
1356
1375
|
return@launch
|
|
1357
1376
|
}
|
|
1358
|
-
|
|
1377
|
+
withContext(Dispatchers.Main) {
|
|
1359
1378
|
callback()
|
|
1360
1379
|
}
|
|
1361
1380
|
}
|
|
1362
1381
|
}
|
|
1363
1382
|
|
|
1364
1383
|
private suspend fun applyAssistantQueueOnMain(
|
|
1365
|
-
tracks: List<TrackItem
|
|
1384
|
+
tracks: List<TrackItem>,
|
|
1385
|
+
append: Boolean
|
|
1366
1386
|
): List<MediaItem> {
|
|
1367
1387
|
if (tracks.isEmpty()) return emptyList()
|
|
1368
1388
|
val player = TrackPlayer.current ?: return emptyList()
|
|
1369
|
-
return
|
|
1370
|
-
player.applyResolvedAssistantQueue(tracks)
|
|
1389
|
+
return withContext(Dispatchers.Main) {
|
|
1390
|
+
player.applyResolvedAssistantQueue(tracks, append)
|
|
1371
1391
|
}
|
|
1372
1392
|
}
|
|
1373
1393
|
|
|
1374
1394
|
/**
|
|
1375
1395
|
* Boot the JS task IF a headless-eligible controller has connected
|
|
1376
1396
|
* AND no JS task is already running. Idempotent — the
|
|
1377
|
-
* `HeadlessJsMediaService.initialized`
|
|
1397
|
+
* `HeadlessJsMediaService.initialized` flag + the wake-intent
|
|
1378
1398
|
* action gate prevent re-entrancy.
|
|
1379
1399
|
*/
|
|
1380
1400
|
internal fun selfWakeForHeadlessClient(packageName: String) {
|
|
@@ -1593,6 +1613,8 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1593
1613
|
/**
|
|
1594
1614
|
* Fire `notifySearchResultChanged` for [query] across every
|
|
1595
1615
|
* connected controller after [searchResultCache] is populated.
|
|
1616
|
+
* Caller is on main; the session's own thread-safety contract
|
|
1617
|
+
* handles the controller iteration.
|
|
1596
1618
|
*/
|
|
1597
1619
|
private fun notifySearchResultChanged(query: String, count: Int) {
|
|
1598
1620
|
val session = mediaSession ?: return
|
|
@@ -1626,11 +1648,11 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1626
1648
|
}
|
|
1627
1649
|
|
|
1628
1650
|
/**
|
|
1629
|
-
*
|
|
1651
|
+
* Interface [SessionCommandForwardingPlayer] consults before letting Media3 route
|
|
1630
1652
|
* a controller-initiated command to the bound player. Calls run on
|
|
1631
1653
|
* the service main looper. Skip-next / skip-previous / seek-to-queue-item
|
|
1632
1654
|
* return true when the implementation has handled the command itself,
|
|
1633
|
-
* so the default routing is suppressed;
|
|
1655
|
+
* so the default routing is suppressed; `onTransportCommand` fires for
|
|
1634
1656
|
* play / pause / in-item seek / setPlaybackSpeed and is purely a
|
|
1635
1657
|
* side-channel notification (no return value).
|
|
1636
1658
|
*/
|
|
@@ -1654,7 +1676,7 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1654
1676
|
/**
|
|
1655
1677
|
* Returns the lib's current authoritative skip capability —
|
|
1656
1678
|
* driven by repeat-mode + queue position + queue size on the
|
|
1657
|
-
* `TrackPlayer` side. Read by [
|
|
1679
|
+
* `TrackPlayer` side. Read by [PlaybackServiceCallback.onConnect] and
|
|
1658
1680
|
* [LocalBinder.refreshAvailableCommands] so the available
|
|
1659
1681
|
* `Player.Commands` reflect what skip arithmetic actually allows.
|
|
1660
1682
|
*/
|
|
@@ -91,7 +91,8 @@ internal class PlaybackServiceCallback(
|
|
|
91
91
|
* registered, the controller's "play X" voice command surfaces
|
|
92
92
|
* as "no results". */
|
|
93
93
|
fun onPlayFromSearchRequest(
|
|
94
|
-
request: MediaSearchRequest
|
|
94
|
+
request: MediaSearchRequest,
|
|
95
|
+
append: Boolean = false
|
|
95
96
|
): ListenableFuture<List<MediaItem>> =
|
|
96
97
|
Futures.immediateFuture(emptyList())
|
|
97
98
|
|
|
@@ -101,7 +102,8 @@ internal class PlaybackServiceCallback(
|
|
|
101
102
|
*
|
|
102
103
|
* Default returns an empty list. */
|
|
103
104
|
fun onPlayFromIdRequest(
|
|
104
|
-
mediaId: String
|
|
105
|
+
mediaId: String,
|
|
106
|
+
append: Boolean = false
|
|
105
107
|
): ListenableFuture<List<MediaItem>> =
|
|
106
108
|
Futures.immediateFuture(emptyList())
|
|
107
109
|
|
|
@@ -194,7 +196,7 @@ internal class PlaybackServiceCallback(
|
|
|
194
196
|
requestMetadata.extras
|
|
195
197
|
)
|
|
196
198
|
return Futures.transform(
|
|
197
|
-
environment.onPlayFromSearchRequest(request),
|
|
199
|
+
environment.onPlayFromSearchRequest(request, append = true),
|
|
198
200
|
{ items -> items.toMutableList() },
|
|
199
201
|
MoreExecutors.directExecutor()
|
|
200
202
|
)
|
|
@@ -202,7 +204,7 @@ internal class PlaybackServiceCallback(
|
|
|
202
204
|
val mediaId = unresolved.mediaId.takeIf { it.isNotEmpty() }
|
|
203
205
|
if (mediaId != null) {
|
|
204
206
|
return Futures.transform(
|
|
205
|
-
environment.onPlayFromIdRequest(mediaId),
|
|
207
|
+
environment.onPlayFromIdRequest(mediaId, append = true),
|
|
206
208
|
{ items -> items.toMutableList() },
|
|
207
209
|
MoreExecutors.directExecutor()
|
|
208
210
|
)
|
|
@@ -749,7 +751,6 @@ internal class PlaybackServiceCallback(
|
|
|
749
751
|
internal const val CONTENT_STYLE_PLAYABLE_HINT =
|
|
750
752
|
"android.media.browse.CONTENT_STYLE_PLAYABLE_HINT"
|
|
751
753
|
internal const val CONTENT_STYLE_LIST_ITEM = 1
|
|
752
|
-
internal const val CONTENT_STYLE_GRID_ITEM = 2
|
|
753
754
|
|
|
754
755
|
/** Root extras flag: when `true`, gearhead surfaces a search
|
|
755
756
|
* affordance ("Search artists, albums, songs" mic / magnifying-
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
package com.margelo.nitro.queueplayer
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Pure-Kotlin index-shift arithmetic for queue mutations
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* an item to extract the same shapes on that side for parity.
|
|
4
|
+
* Pure-Kotlin index-shift arithmetic for queue mutations, kept out of
|
|
5
|
+
* [TrackPlayer] so the shift semantics stand alone without an
|
|
6
|
+
* ExoPlayer. Its iOS counterpart is `QueueMutationArithmetic.swift`,
|
|
7
|
+
* with the same function shapes and input/output semantics for these
|
|
8
|
+
* five helpers.
|
|
10
9
|
*
|
|
11
10
|
* The helper only computes the new `currentTrackIndex` and (for
|
|
12
11
|
* `removeFromQueue`) which original indices apply after dedup /
|