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
|
@@ -43,9 +43,13 @@ class LookaheadCache(
|
|
|
43
43
|
evictionPolicy: EvictionPolicy = EvictionPolicy.LRU
|
|
44
44
|
) {
|
|
45
45
|
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Resolved on-disk cache directory: `context.cacheDir/<cacheDirName>`.
|
|
48
|
+
* Resolved through the application context, as the shared-instance key is,
|
|
49
|
+
* so a wrapper context with its own `cacheDir` cannot split the two.
|
|
50
|
+
*/
|
|
47
51
|
@VisibleForTesting
|
|
48
|
-
val cacheDir: File = File(context.cacheDir, cacheDirName)
|
|
52
|
+
val cacheDir: File = File(context.applicationContext.cacheDir, cacheDirName)
|
|
49
53
|
|
|
50
54
|
private val shared: SharedCacheInstance =
|
|
51
55
|
sharedInstanceFor(context.applicationContext, cacheDir, maxSizeBytes, evictionPolicy)
|
|
@@ -58,7 +62,28 @@ class LookaheadCache(
|
|
|
58
62
|
@VisibleForTesting
|
|
59
63
|
internal val simpleCache: SimpleCache = shared.cache
|
|
60
64
|
|
|
61
|
-
private val
|
|
65
|
+
private val protectingEvictor: FifoCacheEvictor? = shared.protectingEvictor
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Name the urls eviction must not take — the playing track and its
|
|
69
|
+
* neighbours. Eviction runs on every span commit, so without this the entry
|
|
70
|
+
* backing the playing track can be deleted while it plays.
|
|
71
|
+
*
|
|
72
|
+
* A no-op under the LRU policy, whose evictor has no protected-key concept.
|
|
73
|
+
* LRU is not an equivalent safeguard — it bumps the recency of the span just
|
|
74
|
+
* read, not the whole resource — so on Android the playing track is only
|
|
75
|
+
* genuinely protected under FIFO. iOS protects under both policies.
|
|
76
|
+
*/
|
|
77
|
+
fun setProtectedUrls(urls: Set<String>) {
|
|
78
|
+
protectingEvictor?.setProtectedKeys(urls)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether this cache's evictor honours [setProtectedUrls]. False under LRU,
|
|
83
|
+
* letting callers skip building a set that would be discarded.
|
|
84
|
+
*/
|
|
85
|
+
val honoursProtection: Boolean
|
|
86
|
+
get() = protectingEvictor != null
|
|
62
87
|
|
|
63
88
|
/**
|
|
64
89
|
* Per-URL MIME store populated by the prefetch path's
|
|
@@ -89,8 +114,24 @@ class LookaheadCache(
|
|
|
89
114
|
* transparently fall through to a re-download instead of
|
|
90
115
|
* surfacing a load error.
|
|
91
116
|
*/
|
|
92
|
-
fun wrapDataSourceFactory(upstream: DataSource.Factory): DataSource.Factory
|
|
93
|
-
cacheDataSourceFactory(upstream)
|
|
117
|
+
fun wrapDataSourceFactory(upstream: DataSource.Factory): DataSource.Factory {
|
|
118
|
+
val cached = cacheDataSourceFactory(upstream)
|
|
119
|
+
// Decided per data source, which Media3 creates per item load, so a
|
|
120
|
+
// change to `servesPlayback` reaches the next item without rebuilding
|
|
121
|
+
// the player; the item already loading keeps the source it opened.
|
|
122
|
+
return DataSource.Factory {
|
|
123
|
+
if (servesPlayback) cached.createDataSource() else upstream.createDataSource()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Whether the player's reads go through this cache. False while the
|
|
129
|
+
* consumer has the cache disabled: every read then goes to the origin and
|
|
130
|
+
* nothing is written, whatever the disk already holds. Written on main by
|
|
131
|
+
* `TrackPlayer`, read on Media3's loading thread when an item opens.
|
|
132
|
+
*/
|
|
133
|
+
@Volatile
|
|
134
|
+
var servesPlayback: Boolean = true
|
|
94
135
|
|
|
95
136
|
/**
|
|
96
137
|
* Build a [CacheDataSource.Factory] rooted at [simpleCache] +
|
|
@@ -121,8 +162,13 @@ class LookaheadCache(
|
|
|
121
162
|
// helping" signal. Log the reason so a "why is this not
|
|
122
163
|
// cached" investigation has a trail.
|
|
123
164
|
.setEventListener(object : CacheDataSource.EventListener {
|
|
124
|
-
|
|
125
|
-
|
|
165
|
+
override fun onCachedBytesRead(cacheSizeBytes: Long, cachedBytesRead: Long) {
|
|
166
|
+
// Accumulated rather than logged per read: this fires per buffer, so
|
|
167
|
+
// logging here would flood. The running total is what answers
|
|
168
|
+
// "is playback actually being served from the cache", which cannot
|
|
169
|
+
// be told apart from a network read by listening.
|
|
170
|
+
cachedBytesReadTotal.addAndGet(cachedBytesRead)
|
|
171
|
+
}
|
|
126
172
|
override fun onCacheIgnored(reason: Int) {
|
|
127
173
|
val name = when (reason) {
|
|
128
174
|
CacheDataSource.CACHE_IGNORED_REASON_ERROR -> "ERROR"
|
|
@@ -137,7 +183,13 @@ class LookaheadCache(
|
|
|
137
183
|
})
|
|
138
184
|
|
|
139
185
|
/**
|
|
140
|
-
*
|
|
186
|
+
* Total bytes served to the player from cache rather than upstream.
|
|
187
|
+
* Monotonic for the cache's lifetime; read as a delta across an interval.
|
|
188
|
+
*/
|
|
189
|
+
val cachedBytesReadTotal = java.util.concurrent.atomic.AtomicLong(0)
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Drop per-instance state. The [SimpleCache] + database provider
|
|
141
193
|
* stay alive (process-singleton, see class doc) — releasing them
|
|
142
194
|
* would race Media3's `lockedFolders` static registry on the next
|
|
143
195
|
* [LookaheadCache] construction targeting the same directory.
|
|
@@ -173,18 +225,17 @@ class LookaheadCache(
|
|
|
173
225
|
// --- Status surface ---
|
|
174
226
|
|
|
175
227
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
228
|
+
* Bytes currently held on disk. Reads the total Media3 already maintains, so
|
|
229
|
+
* it is O(1) and needs no lock beyond the cache's own.
|
|
230
|
+
*
|
|
231
|
+
* Also more accurate than summing per-key cached bytes, which reports zero
|
|
232
|
+
* for any resource whose content length has not been recorded yet — a
|
|
233
|
+
* chunked download in flight occupies disk but has no length until it
|
|
234
|
+
* completes. Used by [TrackPlayer.getLookaheadCacheStatus] for `currentSizeMb`.
|
|
181
235
|
*/
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
simpleCache.getCachedBytes(key, 0, length)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
236
|
+
// Every SimpleCache method rejects a released cache, and this is read off the
|
|
237
|
+
// looper that releases it. A cache that has gone away holds nothing.
|
|
238
|
+
fun currentSizeBytes(): Long = runCatching { simpleCache.cacheSpace }.getOrDefault(0L)
|
|
188
239
|
|
|
189
240
|
/**
|
|
190
241
|
* Whether the cache holds the entire content for `key`. True
|
|
@@ -210,12 +261,12 @@ class LookaheadCache(
|
|
|
210
261
|
* field — the count reflects the cache itself, not the intersection
|
|
211
262
|
* with `self.tracks`.
|
|
212
263
|
*/
|
|
213
|
-
fun fullyCachedCount(): Int {
|
|
214
|
-
|
|
264
|
+
fun fullyCachedCount(): Int = runCatching {
|
|
265
|
+
simpleCache.keys.count { key ->
|
|
215
266
|
val length = contentLength(key) ?: return@count false
|
|
216
267
|
simpleCache.getCachedBytes(key, 0, length) >= length
|
|
217
268
|
}
|
|
218
|
-
}
|
|
269
|
+
}.getOrDefault(0)
|
|
219
270
|
|
|
220
271
|
/**
|
|
221
272
|
* Wipe every cached entry + free the on-disk space. Used by
|
|
@@ -224,9 +275,14 @@ class LookaheadCache(
|
|
|
224
275
|
* handles the file deletion + index update internally.
|
|
225
276
|
*/
|
|
226
277
|
fun clear() {
|
|
227
|
-
|
|
278
|
+
// Every SimpleCache method rejects a released cache, the key walk included.
|
|
279
|
+
// A policy change releases the shared instance out from under an in-flight
|
|
280
|
+
// wipe, and this now runs off the caller's thread, so an escaping throw
|
|
281
|
+
// reaches the default handler rather than the JS call that asked for it.
|
|
282
|
+
// Nothing is left to clear in that case — the release took the files with it.
|
|
283
|
+
val keys = runCatching { simpleCache.keys.toList() }.getOrElse { return }
|
|
228
284
|
for (key in keys) {
|
|
229
|
-
simpleCache.removeResource(key)
|
|
285
|
+
runCatching { simpleCache.removeResource(key) }.getOrElse { return }
|
|
230
286
|
}
|
|
231
287
|
mimeStore.clear()
|
|
232
288
|
}
|
|
@@ -312,11 +368,20 @@ class LookaheadCache(
|
|
|
312
368
|
EvictionPolicy.FIFO -> FifoCacheEvictor(maxSizeBytes)
|
|
313
369
|
}
|
|
314
370
|
val cache = SimpleCache(cacheDir, evictor, db)
|
|
315
|
-
val newInstance = SharedCacheInstance(cache, db)
|
|
371
|
+
val newInstance = SharedCacheInstance(cache, db, evictor as? FifoCacheEvictor, evictionPolicy)
|
|
316
372
|
sharedInstances[key] = newInstance
|
|
317
373
|
return newInstance
|
|
318
374
|
}
|
|
319
375
|
|
|
376
|
+
/**
|
|
377
|
+
* The eviction policy the live cache for [cacheDir] was built with, or null
|
|
378
|
+
* when this process holds no cache there. Media3 fixes the evictor at
|
|
379
|
+
* construction, so the instance is the record of what was applied.
|
|
380
|
+
*/
|
|
381
|
+
@JvmStatic
|
|
382
|
+
internal fun appliedEvictionPolicy(cacheDir: File): EvictionPolicy? =
|
|
383
|
+
sharedInstances[cacheDir.absolutePath]?.evictionPolicy
|
|
384
|
+
|
|
320
385
|
/**
|
|
321
386
|
* Release + forget the shared SimpleCache for [cacheDir] so the next
|
|
322
387
|
* [LookaheadCache] construction builds a fresh one. Used when the eviction
|
|
@@ -326,18 +391,27 @@ class LookaheadCache(
|
|
|
326
391
|
*/
|
|
327
392
|
@JvmStatic
|
|
328
393
|
@Synchronized
|
|
329
|
-
internal fun releaseSharedInstance(cacheDir: File) {
|
|
330
|
-
sharedInstances.remove(cacheDir.absolutePath)
|
|
394
|
+
internal fun releaseSharedInstance(cacheDir: File): StandaloneDatabaseProvider? {
|
|
395
|
+
val instance = sharedInstances.remove(cacheDir.absolutePath) ?: return null
|
|
396
|
+
instance.cache.release()
|
|
397
|
+
// Handed back so the caller can drop the cache's index tables along with
|
|
398
|
+
// its files. Index storage is keyed by the cache uid, so deleting the
|
|
399
|
+
// directory alone strands one table per policy change.
|
|
400
|
+
return instance.databaseProvider
|
|
331
401
|
}
|
|
332
402
|
}
|
|
333
403
|
|
|
334
404
|
/**
|
|
335
405
|
* Pair of process-singleton SimpleCache + DatabaseProvider for a
|
|
336
406
|
* single cache folder. Held by the companion object's
|
|
337
|
-
* [sharedInstances] map
|
|
407
|
+
* [sharedInstances] map until [releaseSharedInstance] drops it for a
|
|
408
|
+
* policy change; a destroy leaves it in place.
|
|
338
409
|
*/
|
|
339
410
|
private data class SharedCacheInstance(
|
|
340
411
|
val cache: SimpleCache,
|
|
341
|
-
val databaseProvider: StandaloneDatabaseProvider
|
|
412
|
+
val databaseProvider: StandaloneDatabaseProvider,
|
|
413
|
+
/** Null under LRU, which has no protected-key concept. */
|
|
414
|
+
val protectingEvictor: FifoCacheEvictor?,
|
|
415
|
+
val evictionPolicy: EvictionPolicy,
|
|
342
416
|
)
|
|
343
417
|
}
|
|
@@ -9,6 +9,7 @@ import androidx.media3.datasource.DataSource
|
|
|
9
9
|
import androidx.media3.datasource.DataSpec
|
|
10
10
|
import androidx.media3.datasource.cache.CacheDataSource
|
|
11
11
|
import androidx.media3.datasource.cache.CacheWriter
|
|
12
|
+
import java.io.IOException
|
|
12
13
|
import kotlinx.coroutines.CancellationException
|
|
13
14
|
import kotlinx.coroutines.CoroutineScope
|
|
14
15
|
import kotlinx.coroutines.Deferred
|
|
@@ -16,7 +17,10 @@ import kotlinx.coroutines.Dispatchers
|
|
|
16
17
|
import kotlinx.coroutines.Job
|
|
17
18
|
import kotlinx.coroutines.SupervisorJob
|
|
18
19
|
import kotlinx.coroutines.async
|
|
20
|
+
import kotlinx.coroutines.awaitCancellation
|
|
19
21
|
import kotlinx.coroutines.cancel
|
|
22
|
+
import kotlinx.coroutines.ensureActive
|
|
23
|
+
import kotlinx.coroutines.isActive
|
|
20
24
|
import kotlinx.coroutines.launch
|
|
21
25
|
import kotlinx.coroutines.withContext
|
|
22
26
|
|
|
@@ -48,10 +52,10 @@ import kotlinx.coroutines.withContext
|
|
|
48
52
|
* flaky-network resilience:
|
|
49
53
|
*
|
|
50
54
|
* - Per-track download failure (IOException, HTTP error, transient
|
|
51
|
-
* network drop):
|
|
52
|
-
* continues to the next pending target.
|
|
53
|
-
* marked as cached or in-flight, so it
|
|
54
|
-
* re-attempt on a subsequent reschedule.
|
|
55
|
+
* network drop): caught in the orchestrator loop and logged at
|
|
56
|
+
* `Log.w`; the orchestrator continues to the next pending target.
|
|
57
|
+
* The failed URL is NOT marked as cached or in-flight, so it
|
|
58
|
+
* remains eligible for re-attempt on a subsequent reschedule.
|
|
55
59
|
* - Complete network outage (every target throws): orchestrator
|
|
56
60
|
* drains pendingTargets cleanly, leaves [inFlightUrl] null, and
|
|
57
61
|
* exits. The next [reschedule] call re-enqueues the still-desired
|
|
@@ -75,9 +79,21 @@ import kotlinx.coroutines.withContext
|
|
|
75
79
|
class LookaheadCacheWriter @VisibleForTesting internal constructor(
|
|
76
80
|
@get:VisibleForTesting internal val downloader: suspend (TrackItem) -> Unit,
|
|
77
81
|
@get:VisibleForTesting internal val scope: CoroutineScope,
|
|
78
|
-
@get:VisibleForTesting internal val isFullyCached: (String) -> Boolean = { false }
|
|
82
|
+
@get:VisibleForTesting internal val isFullyCached: (String) -> Boolean = { false },
|
|
83
|
+
/** True when the active network is cellular. Consulted only while
|
|
84
|
+
* [allowsCellularAccess] is false. */
|
|
85
|
+
internal val isOnCellular: () -> Boolean = { false }
|
|
79
86
|
) {
|
|
80
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Whether prefetch may run over cellular. False empties the desired
|
|
90
|
+
* window while the active network is cellular, so nothing is
|
|
91
|
+
* enqueued and an in-flight download is cancelled on the next
|
|
92
|
+
* [reschedule]. Set from `setLookaheadCache`.
|
|
93
|
+
*/
|
|
94
|
+
@Volatile
|
|
95
|
+
internal var allowsCellularAccess: Boolean = true
|
|
96
|
+
|
|
81
97
|
/**
|
|
82
98
|
* Optional callback fired after each per-track download iteration
|
|
83
99
|
* completes (success, failure, or cancellation handled). Used by
|
|
@@ -182,6 +198,14 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
|
|
|
182
198
|
val desired = mutableListOf<TrackItem>()
|
|
183
199
|
desired.addAll(computeTargets(tracks, currentIndex, lookaheadCount))
|
|
184
200
|
lookbackTarget(tracks, currentIndex)?.let { desired.add(it) }
|
|
201
|
+
// Prefetch downloads whole upcoming tracks, so on a metered connection it
|
|
202
|
+
// can spend a lot of data on tracks the listener skips past. When it is not
|
|
203
|
+
// permitted nothing is desired, and the reconciliation below prunes what is
|
|
204
|
+
// pending and cancels what is in flight. The next reschedule refills it —
|
|
205
|
+
// a track advance, a skip, a queue change, or the flag going back on.
|
|
206
|
+
if (!allowsCellularAccess && isOnCellular()) {
|
|
207
|
+
desired.clear()
|
|
208
|
+
}
|
|
185
209
|
val desiredUrls = desired.map { it.url }.toSet()
|
|
186
210
|
|
|
187
211
|
val toCancel: Job?
|
|
@@ -225,6 +249,8 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
|
|
|
225
249
|
|
|
226
250
|
// Cancel outside the lock — Job.cancel() is fast but can run
|
|
227
251
|
// listeners on this thread.
|
|
252
|
+
// Cancelling the job is what stops the blocking write too: the download
|
|
253
|
+
// body relays its coroutine's cancellation onto its own `CacheWriter`.
|
|
228
254
|
toCancel?.cancel()
|
|
229
255
|
|
|
230
256
|
if (needsOrchestrator) {
|
|
@@ -266,10 +292,9 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
|
|
|
266
292
|
}
|
|
267
293
|
|
|
268
294
|
/**
|
|
269
|
-
* Cancel + tear down the coroutine scope.
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* reading from `cache.simpleCache` would NPE.
|
|
295
|
+
* Cancel + tear down the coroutine scope. Called before
|
|
296
|
+
* `LookaheadCache.release()` clears the MIME store, so a prefetch
|
|
297
|
+
* still in flight cannot record a MIME type after the clear.
|
|
273
298
|
*/
|
|
274
299
|
fun release() {
|
|
275
300
|
requireMainThread()
|
|
@@ -278,11 +303,12 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
|
|
|
278
303
|
}
|
|
279
304
|
|
|
280
305
|
/**
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
306
|
+
* The public entry points (reschedule / cancel / release) are main-only; this
|
|
307
|
+
* trap turns a future off-main caller into a crash at the call site
|
|
308
|
+
* so the mistake is visible there. The fields themselves —
|
|
309
|
+
* `inFlightUrl`, `inFlightJob`, `orchestratorJob`, `pendingTargets`
|
|
310
|
+
* — are also written from the IO orchestrator coroutine, so they are
|
|
311
|
+
* guarded by `stateLock`, not by thread affinity.
|
|
286
312
|
*
|
|
287
313
|
* `runCatching` swallows the "Method myLooper in android.os.Looper
|
|
288
314
|
* not mocked" exception that fires on a JVM host without the Android
|
|
@@ -313,7 +339,11 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
|
|
|
313
339
|
if (lookaheadCount <= 0 || tracks.isEmpty()) return emptyList()
|
|
314
340
|
val start = (currentIndex + 1).coerceAtLeast(0)
|
|
315
341
|
if (start >= tracks.size) return emptyList()
|
|
316
|
-
|
|
342
|
+
// `start + lookaheadCount` overflows to a negative for a very large count,
|
|
343
|
+
// which coerceAtMost then keeps and subList rejects. The count reaches here
|
|
344
|
+
// from a JS Double, so Int.MAX_VALUE is one `Infinity` away.
|
|
345
|
+
val remaining = tracks.size - start
|
|
346
|
+
val end = start + lookaheadCount.coerceAtMost(remaining)
|
|
317
347
|
return tracks.subList(start, end).filter { isRemote(it.url) }
|
|
318
348
|
}
|
|
319
349
|
|
|
@@ -475,7 +505,8 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
|
|
|
475
505
|
*/
|
|
476
506
|
fun create(
|
|
477
507
|
cache: LookaheadCache,
|
|
478
|
-
httpFactory: DataSource.Factory
|
|
508
|
+
httpFactory: DataSource.Factory,
|
|
509
|
+
isOnCellular: () -> Boolean
|
|
479
510
|
): LookaheadCacheWriter {
|
|
480
511
|
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
481
512
|
// Wrap the upstream HTTP factory so each prefetch download
|
|
@@ -492,27 +523,28 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
|
|
|
492
523
|
return LookaheadCacheWriter(
|
|
493
524
|
downloader = { track -> downloadViaCacheWriter(track, cacheDataSourceFactory) },
|
|
494
525
|
scope = scope,
|
|
495
|
-
isFullyCached = cache::isFullyCached
|
|
526
|
+
isFullyCached = cache::isFullyCached,
|
|
527
|
+
isOnCellular = isOnCellular
|
|
496
528
|
)
|
|
497
529
|
}
|
|
498
530
|
|
|
499
531
|
/**
|
|
500
|
-
* Per-track download body. Builds a fresh [CacheDataSource] from
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
532
|
+
* Per-track download body. Builds a fresh [CacheDataSource] from the
|
|
533
|
+
* shared factory and runs the synchronous `CacheWriter.cache()` on
|
|
534
|
+
* `Dispatchers.IO`. `cache()` blocks and polls only its own cancelled
|
|
535
|
+
* flag, so the coroutine's cancellation is relayed onto that flag by a
|
|
536
|
+
* child that exists to be cancelled with it — each writer is tied to
|
|
537
|
+
* its own job, which is what keeps a cancel from reaching the download
|
|
538
|
+
* the orchestrator starts next.
|
|
507
539
|
*/
|
|
508
540
|
private suspend fun downloadViaCacheWriter(
|
|
509
541
|
track: TrackItem,
|
|
510
|
-
cacheDataSourceFactory: CacheDataSource.Factory
|
|
542
|
+
cacheDataSourceFactory: CacheDataSource.Factory,
|
|
511
543
|
) = withContext(Dispatchers.IO) {
|
|
512
544
|
val cacheDataSource = cacheDataSourceFactory.createDataSource()
|
|
513
545
|
// Cache key = source URL. The consumer-supplied `track.id` is
|
|
514
|
-
// a metadata field that varies per app (
|
|
515
|
-
//
|
|
546
|
+
// a metadata field that varies per app (a server-side hash, a
|
|
547
|
+
// catalogue URI, an app-internal counter); keying by URL keeps the
|
|
516
548
|
// cache generic + correct across apps + matches iOS. Same
|
|
517
549
|
// URL → same cache file (collision === "already cached").
|
|
518
550
|
val dataSpec = DataSpec.Builder()
|
|
@@ -520,11 +552,27 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
|
|
|
520
552
|
.setKey(track.url)
|
|
521
553
|
.build()
|
|
522
554
|
val writer = CacheWriter(cacheDataSource, dataSpec, /* temporaryBuffer = */ null, /* progressListener = */ null)
|
|
555
|
+
// Unconfined, so the relay resumes on the thread that cancels the job
|
|
556
|
+
// rather than waiting for an IO worker: the workers are what a
|
|
557
|
+
// blocking `cache()` occupies, and a cancel that queued behind them
|
|
558
|
+
// would leave the download running for as long as they stay busy.
|
|
559
|
+
val cancelRelay = launch(Dispatchers.Unconfined) {
|
|
560
|
+
try {
|
|
561
|
+
awaitCancellation()
|
|
562
|
+
} finally {
|
|
563
|
+
writer.cancel()
|
|
564
|
+
}
|
|
565
|
+
}
|
|
523
566
|
try {
|
|
567
|
+
ensureActive()
|
|
524
568
|
writer.cache()
|
|
525
|
-
} catch (e:
|
|
526
|
-
writer
|
|
569
|
+
} catch (e: IOException) {
|
|
570
|
+
// A cancelled writer throws InterruptedIOException; report it as the
|
|
571
|
+
// cancellation it is so the orchestrator does not log a failed prefetch.
|
|
572
|
+
if (!isActive) throw CancellationException("prefetch cancelled").also { it.initCause(e) }
|
|
527
573
|
throw e
|
|
574
|
+
} finally {
|
|
575
|
+
cancelRelay.cancel()
|
|
528
576
|
}
|
|
529
577
|
}
|
|
530
578
|
}
|
|
@@ -25,8 +25,8 @@ import androidx.media3.extractor.DefaultExtractorsFactory
|
|
|
25
25
|
* header override) and lines up with which names
|
|
26
26
|
* [DefaultHttpDataSource.Factory.setDefaultRequestProperties] as the
|
|
27
27
|
* integration point. Per-track headers would require a custom
|
|
28
|
-
* MediaSource.Factory reading per-item metadata extras
|
|
29
|
-
*
|
|
28
|
+
* MediaSource.Factory reading per-item metadata extras, which this
|
|
29
|
+
* builder does not do.
|
|
30
30
|
*
|
|
31
31
|
* `file://` URIs transparently route through [DefaultDataSource]
|
|
32
32
|
* without touching the HTTP factory — matches iOS's file-URL
|
|
@@ -249,8 +249,8 @@ object MediaItemBuilder {
|
|
|
249
249
|
* remain coherent.
|
|
250
250
|
*
|
|
251
251
|
* Read timeout: defaults to 30000 ms (vs Media3's 8s default).
|
|
252
|
-
* The longer timeout tolerates slow-spin-up servers (
|
|
253
|
-
* on spun-down disks
|
|
252
|
+
* The longer timeout tolerates slow-spin-up servers (a media server
|
|
253
|
+
* on spun-down disks can take ~20s to respond). Connect timeout
|
|
254
254
|
* stays at Media3's 8s default — TCP handshake is unaffected by
|
|
255
255
|
* disk spin-up.
|
|
256
256
|
*/
|
|
@@ -264,8 +264,8 @@ object MediaItemBuilder {
|
|
|
264
264
|
if (merged.isNotEmpty()) {
|
|
265
265
|
setDefaultRequestProperties(merged)
|
|
266
266
|
}
|
|
267
|
-
//
|
|
268
|
-
//
|
|
267
|
+
// Some servers behind a reverse proxy redirect http→https (or
|
|
268
|
+
// vice versa) for auth flows; disabling
|
|
269
269
|
// cross-protocol redirects silently truncates downloads
|
|
270
270
|
// partway through a redirect.
|
|
271
271
|
setAllowCrossProtocolRedirects(true)
|
|
@@ -28,6 +28,42 @@ import com.margelo.nitro.core.NullType
|
|
|
28
28
|
@UnstableApi
|
|
29
29
|
internal object NowPlayingFormatExtractor {
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Average encoded bitrate of a `file://` source, from its size and duration.
|
|
33
|
+
*
|
|
34
|
+
* The figure is an average over the whole file, which is what "bitrate"
|
|
35
|
+
* means for a variable-rate codec anyway, and it includes the container's
|
|
36
|
+
* headers and any embedded artwork — a few kbps on a typical track.
|
|
37
|
+
*
|
|
38
|
+
* Returns null for any non-local source, for a file that cannot be measured,
|
|
39
|
+
* and for a duration the player has not resolved yet: a bitrate derived from
|
|
40
|
+
* an unknown duration would be arbitrary, and reporting nothing is the
|
|
41
|
+
* honest answer until the player knows.
|
|
42
|
+
*/
|
|
43
|
+
private fun deriveLocalBitrate(player: Player, durationSec: Double?): Double? {
|
|
44
|
+
val uri = player.currentMediaItem?.localConfiguration?.uri ?: return null
|
|
45
|
+
if (!uri.scheme.equals("file", ignoreCase = true)) return null
|
|
46
|
+
return averageBitrateOf(uri.path, durationSec)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Average bits per second of the file at [path] over [durationSec].
|
|
51
|
+
*
|
|
52
|
+
* Split from its caller so the arithmetic is reachable without a player.
|
|
53
|
+
* Null for an unusable duration, an unreadable path, or an empty file —
|
|
54
|
+
* every case where the answer would be invented rather than measured.
|
|
55
|
+
*/
|
|
56
|
+
internal fun averageBitrateOf(path: String?, durationSec: Double?): Double? {
|
|
57
|
+
if (durationSec == null || durationSec <= 0.0) return null
|
|
58
|
+
if (path == null) return null
|
|
59
|
+
// One `stat` of a local file, taken on the looper with the rest of the
|
|
60
|
+
// player snapshot; the answer is not worth an extra hop.
|
|
61
|
+
val bytes = runCatching { java.io.File(path).length() }.getOrNull() ?: return null
|
|
62
|
+
if (bytes <= 0L) return null
|
|
63
|
+
val bitsPerSecond = bytes * 8.0 / durationSec
|
|
64
|
+
return if (bitsPerSecond.isFinite() && bitsPerSecond > 0.0) bitsPerSecond else null
|
|
65
|
+
}
|
|
66
|
+
|
|
31
67
|
/**
|
|
32
68
|
* Extract the format snapshot from `player`'s currently selected
|
|
33
69
|
* audio track. Returns `null` only when no audio track exists in the
|
|
@@ -53,7 +89,7 @@ internal object NowPlayingFormatExtractor {
|
|
|
53
89
|
val mimeType = format.sampleMimeType
|
|
54
90
|
val sampleRate = format.sampleRate.takeIf { it != Format.NO_VALUE }?.toDouble()
|
|
55
91
|
val channelCount = format.channelCount.takeIf { it != Format.NO_VALUE }?.toDouble()
|
|
56
|
-
val
|
|
92
|
+
val reportedBitrate = format.bitrate.takeIf { it != Format.NO_VALUE }?.toDouble()
|
|
57
93
|
val peakBitrate = format.peakBitrate.takeIf { it != Format.NO_VALUE }?.toDouble()
|
|
58
94
|
val pcmEncoding = format.pcmEncoding.takeIf { it != Format.NO_VALUE }?.toDouble()
|
|
59
95
|
val codecsString = format.codecs
|
|
@@ -61,6 +97,17 @@ internal object NowPlayingFormatExtractor {
|
|
|
61
97
|
val durationSec = if (durationMs == C.TIME_UNSET || durationMs <= 0) null
|
|
62
98
|
else durationMs / 1000.0
|
|
63
99
|
|
|
100
|
+
// Media3 leaves both bitrate fields unset for some codecs — FLAC reports
|
|
101
|
+
// NO_VALUE for average and peak alike, on local files as well as remote —
|
|
102
|
+
// so a consumer asking what it is playing gets nothing back. Where the
|
|
103
|
+
// source is on disk the average is arithmetic: encoded bytes over playing
|
|
104
|
+
// time. Not gated on codec; it fills in whatever the extractor did not
|
|
105
|
+
// report, and does nothing when Media3 already answered.
|
|
106
|
+
//
|
|
107
|
+
// A local stat only. A remote source would need its Content-Length, which
|
|
108
|
+
// costs a request the library deliberately does not make.
|
|
109
|
+
val bitrate = reportedBitrate ?: deriveLocalBitrate(player, durationSec)
|
|
110
|
+
|
|
64
111
|
val rgData = engine?.currentReplayGainData()
|
|
65
112
|
val rgActive = engine?.currentReplayGainActive() ?: false
|
|
66
113
|
|
|
@@ -9,8 +9,9 @@ import androidx.media3.common.PlaybackParameters
|
|
|
9
9
|
* time-stretch resamples, so pitch shifts with speed.
|
|
10
10
|
* - `voice` → pitch preserved (`pitch == 1`): the built-in (speech-tuned)
|
|
11
11
|
* time-stretch holds pitch while changing speed.
|
|
12
|
-
* - `music` →
|
|
13
|
-
*
|
|
12
|
+
* - `music` → the same parameters as `voice` (`pitch == 1`); which
|
|
13
|
+
* stretcher applies them is `PitchAwareAudioProcessorChain`'s choice —
|
|
14
|
+
* `SignalsmithStretchAudioProcessor` for `music`, Sonic otherwise.
|
|
14
15
|
*
|
|
15
16
|
* At `speed == 1.0` every mode yields `(1.0, 1.0)`, so the built-in
|
|
16
17
|
* stretch is inactive — the auto-bypass at unity rate.
|