react-native-queue-player 1.2.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/consumer-rules.pro +1 -1
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +3 -19
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +8 -69
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +2 -2
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +672 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +229 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaResponseCheck.swift +122 -0
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MediaResponseCheckTests.swift +128 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +418 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +441 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +982 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +951 -0
- package/ios/TrackPlayer+Recovery.swift +213 -0
- package/ios/TrackPlayer+Skip.swift +465 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +272 -0
- package/ios/TrackPlayer+Window.swift +227 -0
- package/ios/TrackPlayer.swift +331 -4619
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
|
@@ -57,7 +57,7 @@ public abstract class HeadlessJsMediaService :
|
|
|
57
57
|
internal val activeTasks: MutableSet<Int> = CopyOnWriteArraySet()
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
60
|
+
* Set on the first task spawn so a re-entrant
|
|
61
61
|
* [onStartCommand] for the same wake doesn't double-start the JS
|
|
62
62
|
* handler. Reset is implicit — service destruction reclaims this
|
|
63
63
|
* field along with the rest of the instance.
|
|
@@ -8,7 +8,9 @@ package com.margelo.nitro.queueplayer
|
|
|
8
8
|
* call through the interface; consumers never see which backend
|
|
9
9
|
* is active.
|
|
10
10
|
*
|
|
11
|
-
* Threading:
|
|
11
|
+
* Threading: see the implementations — attach / detach arrive on the
|
|
12
|
+
* service main looper and on the Equalizer-constructing thread, the
|
|
13
|
+
* mutators on the `Promise.async` pool, the getters on the JS thread.
|
|
12
14
|
*/
|
|
13
15
|
interface IEqualizerEngine {
|
|
14
16
|
|
|
@@ -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,32 @@ 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
|
-
|
|
117
|
+
fun wrapDataSourceFactory(upstream: DataSource.Factory): DataSource.Factory {
|
|
118
|
+
// Read-only for playback. A write here would commit whatever the origin
|
|
119
|
+
// answered with — a server's error envelope included — as a track, and the
|
|
120
|
+
// body check that refuses one belongs on the prefetch path, where a false
|
|
121
|
+
// refusal costs a prefetch instead of the track. `null` sets Media3's
|
|
122
|
+
// `cacheIsReadOnly`, which is tested before the default-sink fallback.
|
|
123
|
+
// So playing a track does not cache it and does not complete a partial
|
|
124
|
+
// prefetch of it; only the prefetcher writes. Matches iOS, whose
|
|
125
|
+
// read-through server also only reads.
|
|
126
|
+
val cached = cacheDataSourceFactory(upstream).setCacheWriteDataSinkFactory(null)
|
|
127
|
+
// Decided per data source, which Media3 creates per item load, so a
|
|
128
|
+
// change to `servesPlayback` reaches the next item without rebuilding
|
|
129
|
+
// the player; the item already loading keeps the source it opened.
|
|
130
|
+
return DataSource.Factory {
|
|
131
|
+
if (servesPlayback) cached.createDataSource() else upstream.createDataSource()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Whether the player's reads go through this cache. False while the
|
|
137
|
+
* consumer has the cache disabled: every read then goes to the origin and
|
|
138
|
+
* nothing is written, whatever the disk already holds. Written on main by
|
|
139
|
+
* `TrackPlayer`, read on Media3's loading thread when an item opens.
|
|
140
|
+
*/
|
|
141
|
+
@Volatile
|
|
142
|
+
var servesPlayback: Boolean = true
|
|
94
143
|
|
|
95
144
|
/**
|
|
96
145
|
* Build a [CacheDataSource.Factory] rooted at [simpleCache] +
|
|
@@ -121,8 +170,13 @@ class LookaheadCache(
|
|
|
121
170
|
// helping" signal. Log the reason so a "why is this not
|
|
122
171
|
// cached" investigation has a trail.
|
|
123
172
|
.setEventListener(object : CacheDataSource.EventListener {
|
|
124
|
-
|
|
125
|
-
|
|
173
|
+
override fun onCachedBytesRead(cacheSizeBytes: Long, cachedBytesRead: Long) {
|
|
174
|
+
// Accumulated rather than logged per read: this fires per buffer, so
|
|
175
|
+
// logging here would flood. The running total is what answers
|
|
176
|
+
// "is playback actually being served from the cache", which cannot
|
|
177
|
+
// be told apart from a network read by listening.
|
|
178
|
+
cachedBytesReadTotal.addAndGet(cachedBytesRead)
|
|
179
|
+
}
|
|
126
180
|
override fun onCacheIgnored(reason: Int) {
|
|
127
181
|
val name = when (reason) {
|
|
128
182
|
CacheDataSource.CACHE_IGNORED_REASON_ERROR -> "ERROR"
|
|
@@ -137,7 +191,13 @@ class LookaheadCache(
|
|
|
137
191
|
})
|
|
138
192
|
|
|
139
193
|
/**
|
|
140
|
-
*
|
|
194
|
+
* Total bytes served to the player from cache rather than upstream.
|
|
195
|
+
* Monotonic for the cache's lifetime; read as a delta across an interval.
|
|
196
|
+
*/
|
|
197
|
+
val cachedBytesReadTotal = java.util.concurrent.atomic.AtomicLong(0)
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Drop per-instance state. The [SimpleCache] + database provider
|
|
141
201
|
* stay alive (process-singleton, see class doc) — releasing them
|
|
142
202
|
* would race Media3's `lockedFolders` static registry on the next
|
|
143
203
|
* [LookaheadCache] construction targeting the same directory.
|
|
@@ -173,18 +233,17 @@ class LookaheadCache(
|
|
|
173
233
|
// --- Status surface ---
|
|
174
234
|
|
|
175
235
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
236
|
+
* Bytes currently held on disk. Reads the total Media3 already maintains, so
|
|
237
|
+
* it is O(1) and needs no lock beyond the cache's own.
|
|
238
|
+
*
|
|
239
|
+
* Also more accurate than summing per-key cached bytes, which reports zero
|
|
240
|
+
* for any resource whose content length has not been recorded yet — a
|
|
241
|
+
* chunked download in flight occupies disk but has no length until it
|
|
242
|
+
* completes. Used by [TrackPlayer.getLookaheadCacheStatus] for `currentSizeMb`.
|
|
181
243
|
*/
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
simpleCache.getCachedBytes(key, 0, length)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
244
|
+
// Every SimpleCache method rejects a released cache, and this is read off the
|
|
245
|
+
// looper that releases it. A cache that has gone away holds nothing.
|
|
246
|
+
fun currentSizeBytes(): Long = runCatching { simpleCache.cacheSpace }.getOrDefault(0L)
|
|
188
247
|
|
|
189
248
|
/**
|
|
190
249
|
* Whether the cache holds the entire content for `key`. True
|
|
@@ -210,12 +269,12 @@ class LookaheadCache(
|
|
|
210
269
|
* field — the count reflects the cache itself, not the intersection
|
|
211
270
|
* with `self.tracks`.
|
|
212
271
|
*/
|
|
213
|
-
fun fullyCachedCount(): Int {
|
|
214
|
-
|
|
272
|
+
fun fullyCachedCount(): Int = runCatching {
|
|
273
|
+
simpleCache.keys.count { key ->
|
|
215
274
|
val length = contentLength(key) ?: return@count false
|
|
216
275
|
simpleCache.getCachedBytes(key, 0, length) >= length
|
|
217
276
|
}
|
|
218
|
-
}
|
|
277
|
+
}.getOrDefault(0)
|
|
219
278
|
|
|
220
279
|
/**
|
|
221
280
|
* Wipe every cached entry + free the on-disk space. Used by
|
|
@@ -224,9 +283,14 @@ class LookaheadCache(
|
|
|
224
283
|
* handles the file deletion + index update internally.
|
|
225
284
|
*/
|
|
226
285
|
fun clear() {
|
|
227
|
-
|
|
286
|
+
// Every SimpleCache method rejects a released cache, the key walk included.
|
|
287
|
+
// A policy change releases the shared instance out from under an in-flight
|
|
288
|
+
// wipe, and this now runs off the caller's thread, so an escaping throw
|
|
289
|
+
// reaches the default handler rather than the JS call that asked for it.
|
|
290
|
+
// Nothing is left to clear in that case — the release took the files with it.
|
|
291
|
+
val keys = runCatching { simpleCache.keys.toList() }.getOrElse { return }
|
|
228
292
|
for (key in keys) {
|
|
229
|
-
simpleCache.removeResource(key)
|
|
293
|
+
runCatching { simpleCache.removeResource(key) }.getOrElse { return }
|
|
230
294
|
}
|
|
231
295
|
mimeStore.clear()
|
|
232
296
|
}
|
|
@@ -312,11 +376,20 @@ class LookaheadCache(
|
|
|
312
376
|
EvictionPolicy.FIFO -> FifoCacheEvictor(maxSizeBytes)
|
|
313
377
|
}
|
|
314
378
|
val cache = SimpleCache(cacheDir, evictor, db)
|
|
315
|
-
val newInstance = SharedCacheInstance(cache, db)
|
|
379
|
+
val newInstance = SharedCacheInstance(cache, db, evictor as? FifoCacheEvictor, evictionPolicy)
|
|
316
380
|
sharedInstances[key] = newInstance
|
|
317
381
|
return newInstance
|
|
318
382
|
}
|
|
319
383
|
|
|
384
|
+
/**
|
|
385
|
+
* The eviction policy the live cache for [cacheDir] was built with, or null
|
|
386
|
+
* when this process holds no cache there. Media3 fixes the evictor at
|
|
387
|
+
* construction, so the instance is the record of what was applied.
|
|
388
|
+
*/
|
|
389
|
+
@JvmStatic
|
|
390
|
+
internal fun appliedEvictionPolicy(cacheDir: File): EvictionPolicy? =
|
|
391
|
+
sharedInstances[cacheDir.absolutePath]?.evictionPolicy
|
|
392
|
+
|
|
320
393
|
/**
|
|
321
394
|
* Release + forget the shared SimpleCache for [cacheDir] so the next
|
|
322
395
|
* [LookaheadCache] construction builds a fresh one. Used when the eviction
|
|
@@ -326,18 +399,27 @@ class LookaheadCache(
|
|
|
326
399
|
*/
|
|
327
400
|
@JvmStatic
|
|
328
401
|
@Synchronized
|
|
329
|
-
internal fun releaseSharedInstance(cacheDir: File) {
|
|
330
|
-
sharedInstances.remove(cacheDir.absolutePath)
|
|
402
|
+
internal fun releaseSharedInstance(cacheDir: File): StandaloneDatabaseProvider? {
|
|
403
|
+
val instance = sharedInstances.remove(cacheDir.absolutePath) ?: return null
|
|
404
|
+
instance.cache.release()
|
|
405
|
+
// Handed back so the caller can drop the cache's index tables along with
|
|
406
|
+
// its files. Index storage is keyed by the cache uid, so deleting the
|
|
407
|
+
// directory alone strands one table per policy change.
|
|
408
|
+
return instance.databaseProvider
|
|
331
409
|
}
|
|
332
410
|
}
|
|
333
411
|
|
|
334
412
|
/**
|
|
335
413
|
* Pair of process-singleton SimpleCache + DatabaseProvider for a
|
|
336
414
|
* single cache folder. Held by the companion object's
|
|
337
|
-
* [sharedInstances] map
|
|
415
|
+
* [sharedInstances] map until [releaseSharedInstance] drops it for a
|
|
416
|
+
* policy change; a destroy leaves it in place.
|
|
338
417
|
*/
|
|
339
418
|
private data class SharedCacheInstance(
|
|
340
419
|
val cache: SimpleCache,
|
|
341
|
-
val databaseProvider: StandaloneDatabaseProvider
|
|
420
|
+
val databaseProvider: StandaloneDatabaseProvider,
|
|
421
|
+
/** Null under LRU, which has no protected-key concept. */
|
|
422
|
+
val protectingEvictor: FifoCacheEvictor?,
|
|
423
|
+
val evictionPolicy: EvictionPolicy,
|
|
342
424
|
)
|
|
343
425
|
}
|
|
@@ -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
|
|
@@ -485,34 +516,35 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
|
|
|
485
516
|
// window where we'd want to learn the format. Mirrors iOS's
|
|
486
517
|
// file-extension persistence on the cached file.
|
|
487
518
|
val mimeCapturingFactory = MimeCapturingDataSourceFactory(
|
|
488
|
-
upstream = httpFactory,
|
|
519
|
+
upstream = MediaValidatingDataSourceFactory(httpFactory),
|
|
489
520
|
onMime = { uri, contentType -> cache.recordMimeType(uri, contentType) }
|
|
490
521
|
)
|
|
491
522
|
val cacheDataSourceFactory = cache.cacheDataSourceFactory(mimeCapturingFactory)
|
|
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)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Decides whether an HTTP response that claims to carry a track can be
|
|
5
|
+
* believed, from its `Content-Type` and the first bytes of its body.
|
|
6
|
+
*
|
|
7
|
+
* A server that answers an API-level failure with `200` and an XML or JSON
|
|
8
|
+
* envelope is the case this exists for: media servers commonly report
|
|
9
|
+
* errors that way by design, and a proxy answering with an HTML page does the
|
|
10
|
+
* same. Such a body passes every length and status check, so without this the
|
|
11
|
+
* prefetcher writes it into the cache as a complete track and it is served from
|
|
12
|
+
* disk on every later play — the track stays broken after the server is well
|
|
13
|
+
* again.
|
|
14
|
+
*
|
|
15
|
+
* The rules identify what is **not** media rather than demanding proof of what
|
|
16
|
+
* is. The two mistakes are not equal: a false rejection costs a prefetch and the
|
|
17
|
+
* track still plays from its origin, while a false acceptance breaks the track
|
|
18
|
+
* until the cache is cleared by hand.
|
|
19
|
+
*
|
|
20
|
+
* Mirrored rule for rule by `MediaResponseCheck.swift` on iOS.
|
|
21
|
+
*/
|
|
22
|
+
internal object MediaResponseCheck {
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Why a response was refused. Each case carries what was judged so a caller's
|
|
26
|
+
* message names the response rather than just the verdict.
|
|
27
|
+
*/
|
|
28
|
+
sealed interface Rejection {
|
|
29
|
+
/** The declared type is a markup or JSON document, which no audio container is. */
|
|
30
|
+
data class MarkupContentType(val contentType: String) : Rejection
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The declared type is text of some other kind and the body carries no
|
|
34
|
+
* recognised container signature. `text/plain` is not refused on the label
|
|
35
|
+
* alone — media servers do mislabel audio — so the body is given the chance
|
|
36
|
+
* to prove itself.
|
|
37
|
+
*/
|
|
38
|
+
data class TextWithoutMediaSignature(val contentType: String) : Rejection
|
|
39
|
+
|
|
40
|
+
/** No usable declared type, and the body begins as a markup or JSON document. */
|
|
41
|
+
data object MarkupBody : Rejection
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Enough of a body to carry any container signature read below. */
|
|
45
|
+
const val HEAD_BYTES: Int = 64
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Types that are documents, never audio. Narrow on purpose:
|
|
49
|
+
* `application/octet-stream` and `text/plain` are missing because servers
|
|
50
|
+
* serve real audio under both.
|
|
51
|
+
*/
|
|
52
|
+
private val MARKUP_TYPES = setOf(
|
|
53
|
+
"text/html", "application/xhtml+xml",
|
|
54
|
+
"application/xml", "text/xml",
|
|
55
|
+
"application/json", "text/json",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
private val SIGNATURE_PREFIXES = listOf(
|
|
59
|
+
"ID3", "fLaC", "OggS", "RIFF", "FORM", "caff", "#!AMR", "#EXTM3U",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The verdict for a response, or null to accept it.
|
|
64
|
+
*
|
|
65
|
+
* [head] is the first bytes of the body — [HEAD_BYTES] is plenty for every
|
|
66
|
+
* signature below. [headIsStartOfResource] is false when the request carried a
|
|
67
|
+
* range, and then only the declared type is judged: a body that begins part
|
|
68
|
+
* way into a file says nothing about the file's first bytes.
|
|
69
|
+
*/
|
|
70
|
+
fun rejection(
|
|
71
|
+
contentType: String?,
|
|
72
|
+
head: ByteArray,
|
|
73
|
+
headIsStartOfResource: Boolean
|
|
74
|
+
): Rejection? {
|
|
75
|
+
val declared = normalised(contentType)
|
|
76
|
+
if (declared != null && declared in MARKUP_TYPES) {
|
|
77
|
+
return Rejection.MarkupContentType(declared)
|
|
78
|
+
}
|
|
79
|
+
if (!headIsStartOfResource) return null
|
|
80
|
+
val body = droppingByteOrderMark(head)
|
|
81
|
+
if (declared != null && declared.startsWith("text/")) {
|
|
82
|
+
return if (hasMediaSignature(body)) null else Rejection.TextWithoutMediaSignature(declared)
|
|
83
|
+
}
|
|
84
|
+
return if (beginsAsMarkup(body)) Rejection.MarkupBody else null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Lowercased type with any `; charset=…` parameter and surrounding space
|
|
89
|
+
* removed. Null for a missing or empty header.
|
|
90
|
+
*/
|
|
91
|
+
private fun normalised(contentType: String?): String? {
|
|
92
|
+
if (contentType.isNullOrEmpty()) return null
|
|
93
|
+
return contentType.substringBefore(';').trim().lowercase().ifEmpty { null }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Drop a byte-order mark so the checks below read the first real byte. A
|
|
98
|
+
* UTF-16 mark matters twice over: `FF FE` also satisfies the MPEG frame sync
|
|
99
|
+
* pattern, so a UTF-16 document would otherwise read as audio.
|
|
100
|
+
*/
|
|
101
|
+
private fun droppingByteOrderMark(head: ByteArray): ByteArray {
|
|
102
|
+
if (head.size >= 3 &&
|
|
103
|
+
head[0] == 0xEF.toByte() && head[1] == 0xBB.toByte() && head[2] == 0xBF.toByte()
|
|
104
|
+
) {
|
|
105
|
+
return head.copyOfRange(3, head.size)
|
|
106
|
+
}
|
|
107
|
+
if (head.size >= 2 && (
|
|
108
|
+
(head[0] == 0xFF.toByte() && head[1] == 0xFE.toByte()) ||
|
|
109
|
+
(head[0] == 0xFE.toByte() && head[1] == 0xFF.toByte())
|
|
110
|
+
)
|
|
111
|
+
) {
|
|
112
|
+
return head.copyOfRange(2, head.size)
|
|
113
|
+
}
|
|
114
|
+
return head
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Whether the body opens with the magic bytes of a container either platform
|
|
119
|
+
* plays, or with an HLS playlist's own first line.
|
|
120
|
+
*/
|
|
121
|
+
private fun hasMediaSignature(body: ByteArray): Boolean {
|
|
122
|
+
if (body.isEmpty()) return false
|
|
123
|
+
// MPEG audio frame sync: eleven set bits across the first two bytes. ADTS
|
|
124
|
+
// AAC shares it.
|
|
125
|
+
if (body.size >= 2 &&
|
|
126
|
+
body[0] == 0xFF.toByte() &&
|
|
127
|
+
(body[1].toInt() and 0xE0) == 0xE0
|
|
128
|
+
) {
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
if (SIGNATURE_PREFIXES.any { startsWith(body, it, 0) }) return true
|
|
132
|
+
// ISO base media (MP4 / M4A / ALAC): a box length, then the type.
|
|
133
|
+
if (body.size >= 8 && startsWith(body, "ftyp", 4)) return true
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Whether the body opens as an XML / HTML document or a JSON value. No
|
|
139
|
+
* container above begins with either character.
|
|
140
|
+
*/
|
|
141
|
+
private fun beginsAsMarkup(body: ByteArray): Boolean {
|
|
142
|
+
val first = body.firstOrNull { !isAsciiSpace(it) } ?: return false
|
|
143
|
+
return first == '<'.code.toByte() || first == '{'.code.toByte()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private fun isAsciiSpace(b: Byte): Boolean =
|
|
147
|
+
b == 0x20.toByte() || b == 0x09.toByte() || b == 0x0A.toByte() || b == 0x0D.toByte()
|
|
148
|
+
|
|
149
|
+
private fun startsWith(body: ByteArray, ascii: String, at: Int): Boolean {
|
|
150
|
+
if (body.size < at + ascii.length) return false
|
|
151
|
+
for (i in ascii.indices) {
|
|
152
|
+
if (body[at + i] != ascii[i].code.toByte()) return false
|
|
153
|
+
}
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
}
|