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
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
package com.margelo.nitro.queueplayer
|
|
2
2
|
|
|
3
|
+
import android.net.Uri
|
|
3
4
|
import androidx.media3.common.util.UnstableApi
|
|
5
|
+
import androidx.media3.datasource.DataSource
|
|
6
|
+
import androidx.media3.datasource.DataSpec
|
|
4
7
|
import androidx.media3.datasource.DefaultHttpDataSource
|
|
8
|
+
import androidx.media3.datasource.TransferListener
|
|
5
9
|
import androidx.media3.datasource.cache.CacheDataSource
|
|
6
10
|
import androidx.test.core.app.ApplicationProvider
|
|
7
11
|
import org.junit.After
|
|
8
12
|
import org.junit.Assert.assertEquals
|
|
13
|
+
import org.junit.Assert.assertFalse
|
|
9
14
|
import org.junit.Assert.assertNotNull
|
|
10
15
|
import org.junit.Assert.assertTrue
|
|
11
16
|
import org.junit.Before
|
|
@@ -20,8 +25,8 @@ import java.io.File
|
|
|
20
25
|
*
|
|
21
26
|
* Exercises the cache directly without going through ExoPlayer:
|
|
22
27
|
* - Constructor creates the cache directory under `context.cacheDir`.
|
|
23
|
-
* - `wrapDataSourceFactory`
|
|
24
|
-
*
|
|
28
|
+
* - `wrapDataSourceFactory` hands out a [CacheDataSource] while the cache
|
|
29
|
+
* serves playback and the bare upstream source while it does not.
|
|
25
30
|
* - `release()` is idempotent, and a fresh cache can be constructed
|
|
26
31
|
* at the same directory afterwards (the SimpleCache one-instance-
|
|
27
32
|
* per-directory invariant is restored on release).
|
|
@@ -69,25 +74,88 @@ class LookaheadCacheTest {
|
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
@Test
|
|
72
|
-
fun `
|
|
77
|
+
fun `the wrapped factory reads through the cache while it serves playback`() {
|
|
73
78
|
val upstream = DefaultHttpDataSource.Factory()
|
|
74
79
|
val wrapped = cache.wrapDataSourceFactory(upstream)
|
|
75
|
-
|
|
80
|
+
val source = wrapped.createDataSource()
|
|
76
81
|
assertTrue(
|
|
77
|
-
"
|
|
78
|
-
|
|
82
|
+
"a data source opened while the cache is enabled checks the disk first",
|
|
83
|
+
source is CacheDataSource
|
|
79
84
|
)
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Only the prefetcher fills the cache. The playback factory is built with a
|
|
89
|
+
* null cache-write sink, which is what Media3 reads as `cacheIsReadOnly`, so
|
|
90
|
+
* a body pulled all the way through it leaves nothing on disk.
|
|
91
|
+
*/
|
|
92
|
+
@Test
|
|
93
|
+
fun `a body read through the playback factory is not written to the cache`() {
|
|
94
|
+
val body = ByteArray(4096) { it.toByte() }
|
|
95
|
+
val url = "https://example.com/playback-read.mp3"
|
|
96
|
+
val source = cache.wrapDataSourceFactory(FixedBodyDataSource.Factory(body))
|
|
97
|
+
.createDataSource()
|
|
98
|
+
|
|
99
|
+
source.open(DataSpec.Builder().setUri(url).setKey(url).build())
|
|
100
|
+
val buffer = ByteArray(1024)
|
|
101
|
+
var total = 0
|
|
102
|
+
while (true) {
|
|
103
|
+
val read = source.read(buffer, 0, buffer.size)
|
|
104
|
+
if (read == -1) break
|
|
105
|
+
total += read
|
|
106
|
+
}
|
|
107
|
+
source.close()
|
|
108
|
+
|
|
109
|
+
assertEquals("the whole body reaches the caller", body.size, total)
|
|
110
|
+
assertEquals("nothing is written to disk", 0L, cache.currentSizeBytes())
|
|
111
|
+
assertFalse("and the url is not held as cached", cache.isFullyCached(url))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Serves a fixed body for any [DataSpec], honouring its position. */
|
|
115
|
+
private class FixedBodyDataSource(private val body: ByteArray) : DataSource {
|
|
116
|
+
class Factory(private val body: ByteArray) : DataSource.Factory {
|
|
117
|
+
override fun createDataSource(): DataSource = FixedBodyDataSource(body)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private var uri: Uri? = null
|
|
121
|
+
private var position = 0
|
|
122
|
+
|
|
123
|
+
override fun open(dataSpec: DataSpec): Long {
|
|
124
|
+
uri = dataSpec.uri
|
|
125
|
+
position = dataSpec.position.toInt()
|
|
126
|
+
return (body.size - position).toLong()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
|
|
130
|
+
if (position >= body.size) return -1
|
|
131
|
+
val take = minOf(length, body.size - position)
|
|
132
|
+
body.copyInto(buffer, offset, position, position + take)
|
|
133
|
+
position += take
|
|
134
|
+
return take
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
override fun getUri(): Uri? = uri
|
|
138
|
+
override fun close() {}
|
|
139
|
+
override fun addTransferListener(transferListener: TransferListener) {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Disabled means every read goes to the origin and nothing is written,
|
|
144
|
+
* whatever the disk holds — the wrapper hands out the bare upstream source.
|
|
145
|
+
* Decided per data source, so a change reaches the next item load.
|
|
146
|
+
*/
|
|
82
147
|
@Test
|
|
83
|
-
fun `wrapped factory
|
|
148
|
+
fun `the wrapped factory bypasses the cache while it does not serve playback`() {
|
|
84
149
|
val upstream = DefaultHttpDataSource.Factory()
|
|
85
150
|
val wrapped = cache.wrapDataSourceFactory(upstream)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
151
|
+
|
|
152
|
+
cache.servesPlayback = false
|
|
153
|
+
val bypassed = wrapped.createDataSource()
|
|
154
|
+
cache.servesPlayback = true
|
|
155
|
+
val served = wrapped.createDataSource()
|
|
156
|
+
|
|
157
|
+
assertTrue("disabled: the upstream source, no cache in front", bypassed !is CacheDataSource)
|
|
158
|
+
assertTrue("re-enabled: the next source goes through the cache again", served is CacheDataSource)
|
|
91
159
|
}
|
|
92
160
|
|
|
93
161
|
@Test
|
|
@@ -125,13 +193,11 @@ class LookaheadCacheTest {
|
|
|
125
193
|
}
|
|
126
194
|
|
|
127
195
|
@Test
|
|
128
|
-
fun `default max size
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// library's automatic disk usage before the runtime config API
|
|
132
|
-
// ships).
|
|
196
|
+
fun `default max size is 100 MB`() {
|
|
197
|
+
// The default is what a consumer that omits `lookaheadCacheMaxSizeMb`
|
|
198
|
+
// from `configure()` gets, so it is part of the public contract.
|
|
133
199
|
assertEquals(
|
|
134
|
-
"
|
|
200
|
+
"default disk budget",
|
|
135
201
|
100L * 1024L * 1024L,
|
|
136
202
|
LookaheadCache.DEFAULT_MAX_SIZE_BYTES
|
|
137
203
|
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer
|
|
2
|
+
|
|
3
|
+
import androidx.media3.common.util.UnstableApi
|
|
4
|
+
import androidx.test.core.app.ApplicationProvider
|
|
5
|
+
import org.junit.Assert.assertTrue
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
import org.junit.runner.RunWith
|
|
8
|
+
import org.robolectric.RobolectricTestRunner
|
|
9
|
+
import org.robolectric.annotation.Config
|
|
10
|
+
import java.util.concurrent.CountDownLatch
|
|
11
|
+
import java.util.concurrent.TimeUnit
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Cancelling a prefetch stops the write that is running, not only the
|
|
15
|
+
* coroutine around it.
|
|
16
|
+
*/
|
|
17
|
+
@RunWith(RobolectricTestRunner::class)
|
|
18
|
+
@Config(sdk = [34])
|
|
19
|
+
@UnstableApi
|
|
20
|
+
class LookaheadCacheWriterCancelTest {
|
|
21
|
+
|
|
22
|
+
private fun httpTrack(id: String) = TrackItem(
|
|
23
|
+
id = id, url = "https://example.com/$id.m4a", title = null, artist = null, album = null,
|
|
24
|
+
duration = null, artworkUrl = null, extras = null,
|
|
25
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
26
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null)
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `CacheWriter.cache()` blocks and polls only its own cancelled flag, so a
|
|
30
|
+
* cancelled coroutine alone would leave it writing. The download body
|
|
31
|
+
* relays the cancellation onto the writer; `CacheWriter` then closes its
|
|
32
|
+
* source, which is what this observes.
|
|
33
|
+
*/
|
|
34
|
+
@Test
|
|
35
|
+
fun `cancel stops the running download, not just its coroutine`() {
|
|
36
|
+
val cache = LookaheadCache(
|
|
37
|
+
context = ApplicationProvider.getApplicationContext(),
|
|
38
|
+
maxSizeBytes = 4L * 1024L * 1024L,
|
|
39
|
+
cacheDirName = "writer-cancel-${System.nanoTime()}"
|
|
40
|
+
)
|
|
41
|
+
val source = TricklingDataSource()
|
|
42
|
+
val writer = LookaheadCacheWriter.create(
|
|
43
|
+
cache = cache,
|
|
44
|
+
httpFactory = androidx.media3.datasource.DataSource.Factory { source },
|
|
45
|
+
isOnCellular = { false }
|
|
46
|
+
)
|
|
47
|
+
try {
|
|
48
|
+
// The window is the tracks ahead of the current one.
|
|
49
|
+
writer.reschedule(listOf(httpTrack("current"), httpTrack("slow")), currentIndex = 0, lookaheadCount = 1)
|
|
50
|
+
assertTrue("the download must have opened its source", source.opened.await(5, TimeUnit.SECONDS))
|
|
51
|
+
|
|
52
|
+
writer.cancel()
|
|
53
|
+
|
|
54
|
+
assertTrue("a cancelled write closes its source", source.closed.await(5, TimeUnit.SECONDS))
|
|
55
|
+
} finally {
|
|
56
|
+
writer.release()
|
|
57
|
+
cache.release()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A source that hands out one byte at a time, forever, until closed. */
|
|
62
|
+
private class TricklingDataSource : androidx.media3.datasource.DataSource {
|
|
63
|
+
val opened = CountDownLatch(1)
|
|
64
|
+
val closed = CountDownLatch(1)
|
|
65
|
+
private var uri: android.net.Uri? = null
|
|
66
|
+
|
|
67
|
+
override fun addTransferListener(transferListener: androidx.media3.datasource.TransferListener) {}
|
|
68
|
+
override fun open(dataSpec: androidx.media3.datasource.DataSpec): Long {
|
|
69
|
+
uri = dataSpec.uri
|
|
70
|
+
opened.countDown()
|
|
71
|
+
return androidx.media3.common.C.LENGTH_UNSET.toLong()
|
|
72
|
+
}
|
|
73
|
+
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
|
|
74
|
+
if (length == 0) return 0
|
|
75
|
+
Thread.sleep(2)
|
|
76
|
+
buffer[offset] = 0
|
|
77
|
+
return 1
|
|
78
|
+
}
|
|
79
|
+
override fun getUri(): android.net.Uri? = uri
|
|
80
|
+
override fun close() {
|
|
81
|
+
closed.countDown()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
}
|
|
@@ -57,6 +57,18 @@ class LookaheadCacheWriterTest {
|
|
|
57
57
|
assertTrue(w.computeTargets(emptyList(), currentIndex = 0, lookaheadCount = 3).isEmpty())
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
@Test
|
|
61
|
+
fun `computeTargets clamps a lookaheadCount that would overflow the range`() {
|
|
62
|
+
// The count arrives as a JS Double, so Infinity reaches Int.MAX_VALUE here
|
|
63
|
+
// and `start + count` wraps negative — a range subList rejects outright.
|
|
64
|
+
val w = newWriter()
|
|
65
|
+
val tracks = (1..3).map { httpTrack("t$it") }
|
|
66
|
+
assertEquals(
|
|
67
|
+
2,
|
|
68
|
+
w.computeTargets(tracks, currentIndex = 0, lookaheadCount = Int.MAX_VALUE).size
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
@Test
|
|
61
73
|
fun `computeTargets returns empty for non-positive lookaheadCount`() {
|
|
62
74
|
val w = newWriter()
|
|
@@ -632,6 +644,103 @@ class LookaheadCacheWriterTest {
|
|
|
632
644
|
|
|
633
645
|
// --- Helpers
|
|
634
646
|
|
|
647
|
+
// --- allowsCellularAccess
|
|
648
|
+
|
|
649
|
+
@Test
|
|
650
|
+
fun `prefetch is skipped while cellular is not permitted and the network is cellular`() = runTest {
|
|
651
|
+
val downloads = mutableListOf<String>()
|
|
652
|
+
val writer = LookaheadCacheWriter(
|
|
653
|
+
downloader = { track -> downloads.add(track.id ?: "") },
|
|
654
|
+
scope = CoroutineScope(StandardTestDispatcher(testScheduler) + SupervisorJob()),
|
|
655
|
+
isOnCellular = { true }
|
|
656
|
+
)
|
|
657
|
+
writer.allowsCellularAccess = false
|
|
658
|
+
|
|
659
|
+
writer.reschedule((0..4).map { httpTrack("t$it") }, currentIndex = 0, lookaheadCount = 3)
|
|
660
|
+
advanceUntilIdle()
|
|
661
|
+
|
|
662
|
+
assertTrue(downloads.isEmpty())
|
|
663
|
+
assertNull(writer.orchestratorJob)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
@Test
|
|
667
|
+
fun `prefetch runs on a non-cellular network even when cellular is not permitted`() = runTest {
|
|
668
|
+
val downloads = mutableListOf<String>()
|
|
669
|
+
val writer = LookaheadCacheWriter(
|
|
670
|
+
downloader = { track -> downloads.add(track.id ?: "") },
|
|
671
|
+
scope = CoroutineScope(StandardTestDispatcher(testScheduler) + SupervisorJob()),
|
|
672
|
+
isOnCellular = { false }
|
|
673
|
+
)
|
|
674
|
+
writer.allowsCellularAccess = false
|
|
675
|
+
|
|
676
|
+
writer.reschedule((0..4).map { httpTrack("t$it") }, currentIndex = 0, lookaheadCount = 3)
|
|
677
|
+
advanceUntilIdle()
|
|
678
|
+
|
|
679
|
+
assertEquals(listOf("t1", "t2", "t3"), downloads)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
@Test
|
|
683
|
+
fun `prefetch runs on cellular while it is permitted`() = runTest {
|
|
684
|
+
val downloads = mutableListOf<String>()
|
|
685
|
+
val writer = LookaheadCacheWriter(
|
|
686
|
+
downloader = { track -> downloads.add(track.id ?: "") },
|
|
687
|
+
scope = CoroutineScope(StandardTestDispatcher(testScheduler) + SupervisorJob()),
|
|
688
|
+
isOnCellular = { true }
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
writer.reschedule((0..4).map { httpTrack("t$it") }, currentIndex = 0, lookaheadCount = 3)
|
|
692
|
+
advanceUntilIdle()
|
|
693
|
+
|
|
694
|
+
assertEquals(listOf("t1", "t2", "t3"), downloads)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
@Test
|
|
698
|
+
fun `handing off to cellular cancels the download in flight`() = runBlocking<Unit> {
|
|
699
|
+
val started = CompletableDeferred<Unit>()
|
|
700
|
+
val release = CompletableDeferred<Unit>()
|
|
701
|
+
var onCellular = false
|
|
702
|
+
val writer = LookaheadCacheWriter(
|
|
703
|
+
downloader = { started.complete(Unit); release.await() },
|
|
704
|
+
scope = CoroutineScope(SupervisorJob()),
|
|
705
|
+
isOnCellular = { onCellular }
|
|
706
|
+
)
|
|
707
|
+
writer.allowsCellularAccess = false
|
|
708
|
+
val queue = (0..4).map { httpTrack("t$it") }
|
|
709
|
+
writer.reschedule(queue, currentIndex = 0, lookaheadCount = 3)
|
|
710
|
+
started.await()
|
|
711
|
+
// Captured before the handoff: the orchestrator clears `inFlightJob` on its
|
|
712
|
+
// own thread once the cancelled download resumes it, so reading the slot
|
|
713
|
+
// afterwards races that clear.
|
|
714
|
+
val inFlightBefore = writer.inFlightJob
|
|
715
|
+
|
|
716
|
+
onCellular = true
|
|
717
|
+
writer.reschedule(queue, currentIndex = 0, lookaheadCount = 3)
|
|
718
|
+
|
|
719
|
+
assertTrue("the download in flight must be cancelled", inFlightBefore?.isCancelled == true)
|
|
720
|
+
release.complete(Unit)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
@Test
|
|
724
|
+
fun `turning cellular back on refills the window on the next reschedule`() = runTest {
|
|
725
|
+
val downloads = mutableListOf<String>()
|
|
726
|
+
val writer = LookaheadCacheWriter(
|
|
727
|
+
downloader = { track -> downloads.add(track.id ?: "") },
|
|
728
|
+
scope = CoroutineScope(StandardTestDispatcher(testScheduler) + SupervisorJob()),
|
|
729
|
+
isOnCellular = { true }
|
|
730
|
+
)
|
|
731
|
+
val queue = (0..4).map { httpTrack("t$it") }
|
|
732
|
+
writer.allowsCellularAccess = false
|
|
733
|
+
writer.reschedule(queue, currentIndex = 0, lookaheadCount = 3)
|
|
734
|
+
advanceUntilIdle()
|
|
735
|
+
assertTrue(downloads.isEmpty())
|
|
736
|
+
|
|
737
|
+
writer.allowsCellularAccess = true
|
|
738
|
+
writer.reschedule(queue, currentIndex = 0, lookaheadCount = 3)
|
|
739
|
+
advanceUntilIdle()
|
|
740
|
+
|
|
741
|
+
assertEquals(listOf("t1", "t2", "t3"), downloads)
|
|
742
|
+
}
|
|
743
|
+
|
|
635
744
|
private fun httpTrack(id: String): TrackItem =
|
|
636
745
|
makeTrack(id, "https://example.com/$id.m4a")
|
|
637
746
|
|
|
@@ -644,6 +753,8 @@ class LookaheadCacheWriterTest {
|
|
|
644
753
|
album = null,
|
|
645
754
|
duration = null,
|
|
646
755
|
artworkUrl = null,
|
|
647
|
-
extras = null
|
|
756
|
+
extras = null,
|
|
757
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
758
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null
|
|
648
759
|
)
|
|
649
760
|
}
|
|
@@ -171,7 +171,7 @@ class MediaItemBuilderTest {
|
|
|
171
171
|
// Parity with iOS `AVQueueBuilderTests.testMakeItemForFileURL
|
|
172
172
|
// SkipsHeaderDictionary`: the test asserts the URI scheme is
|
|
173
173
|
// preserved, but does NOT assert the file/remote routing
|
|
174
|
-
// downstream.
|
|
174
|
+
// downstream. What the factory built is unreadable post-build on
|
|
175
175
|
// both platforms (AVURLAsset options can't be read back on
|
|
176
176
|
// iOS; DefaultDataSource.Factory wiring is internal on
|
|
177
177
|
// Android), so the documented contract is the test.
|
|
@@ -184,7 +184,7 @@ class MediaItemBuilderTest {
|
|
|
184
184
|
assertEquals("file", uri?.scheme)
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
// --- buildMediaItems MIME
|
|
187
|
+
// --- buildMediaItems MIME handling
|
|
188
188
|
|
|
189
189
|
@Test
|
|
190
190
|
fun `buildMediaItems sets MIME from URL extension for http urls`() {
|
|
@@ -474,6 +474,8 @@ class MediaItemBuilderTest {
|
|
|
474
474
|
album = album,
|
|
475
475
|
duration = duration,
|
|
476
476
|
artworkUrl = artworkUrl,
|
|
477
|
-
extras = null
|
|
477
|
+
extras = null,
|
|
478
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
479
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null
|
|
478
480
|
)
|
|
479
481
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertNull
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What the cache will and will not believe is a track, judged from a response's
|
|
9
|
+
* declared type and the first bytes of its body. Mirrors iOS's
|
|
10
|
+
* `MediaResponseCheckTests` case for case so the two platforms refuse and
|
|
11
|
+
* accept the same responses.
|
|
12
|
+
*/
|
|
13
|
+
class MediaResponseCheckTest {
|
|
14
|
+
|
|
15
|
+
private val xmlEnvelope =
|
|
16
|
+
"""<api-response status="failed"><error code="70"/></api-response>""".toByteArray()
|
|
17
|
+
private val mp3 = byteArrayOf(0x49, 0x44, 0x33, 0x04, 0, 0, 0, 0)
|
|
18
|
+
|
|
19
|
+
private fun rejection(
|
|
20
|
+
contentType: String?,
|
|
21
|
+
body: ByteArray,
|
|
22
|
+
whole: Boolean = true
|
|
23
|
+
): MediaResponseCheck.Rejection? =
|
|
24
|
+
MediaResponseCheck.rejection(contentType, body, headIsStartOfResource = whole)
|
|
25
|
+
|
|
26
|
+
@Test
|
|
27
|
+
fun markupAndJsonContentTypesAreRefused() {
|
|
28
|
+
for (type in listOf(
|
|
29
|
+
"application/xml", "text/xml", "text/html",
|
|
30
|
+
"application/xhtml+xml", "application/json", "text/json",
|
|
31
|
+
)) {
|
|
32
|
+
assertEquals(
|
|
33
|
+
"$type is a document type",
|
|
34
|
+
MediaResponseCheck.Rejection.MarkupContentType(type),
|
|
35
|
+
rejection(type, xmlEnvelope)
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Test
|
|
41
|
+
fun theDeclaredTypeIsReadWithoutItsParameters() {
|
|
42
|
+
assertEquals(
|
|
43
|
+
MediaResponseCheck.Rejection.MarkupContentType("application/xml"),
|
|
44
|
+
rejection("application/xml; charset=utf-8", xmlEnvelope)
|
|
45
|
+
)
|
|
46
|
+
assertEquals(
|
|
47
|
+
MediaResponseCheck.Rejection.MarkupContentType("application/xml"),
|
|
48
|
+
rejection("APPLICATION/XML", xmlEnvelope)
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
fun aDeclaredDocumentIsRefusedOnAPartialBodyToo() {
|
|
54
|
+
assertEquals(
|
|
55
|
+
MediaResponseCheck.Rejection.MarkupContentType("application/xml"),
|
|
56
|
+
rejection("application/xml", byteArrayOf(0x3C, 0x00), whole = false)
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Test
|
|
61
|
+
fun aPartialBodyIsNotJudgedByItsLeadingBytes() {
|
|
62
|
+
// The same markup, under a type that is not itself a document, so only the
|
|
63
|
+
// leading-byte rule can decide: refused as a whole resource, accepted as a
|
|
64
|
+
// range, which is what separates the two inputs.
|
|
65
|
+
val markup = "<api-response/>".toByteArray()
|
|
66
|
+
assertEquals(
|
|
67
|
+
MediaResponseCheck.Rejection.MarkupBody,
|
|
68
|
+
rejection("application/octet-stream", markup)
|
|
69
|
+
)
|
|
70
|
+
assertNull(
|
|
71
|
+
"a body that begins part way into a file says nothing about its first bytes",
|
|
72
|
+
rejection("application/octet-stream", markup, whole = false)
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test
|
|
77
|
+
fun textWithoutAContainerSignatureIsRefused() {
|
|
78
|
+
assertEquals(
|
|
79
|
+
MediaResponseCheck.Rejection.TextWithoutMediaSignature("text/plain"),
|
|
80
|
+
rejection("text/plain", "not found".toByteArray())
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test
|
|
85
|
+
fun textCarryingAContainerSignatureIsAccepted() {
|
|
86
|
+
assertNull(
|
|
87
|
+
"a server mislabelling audio as text does not make it a document",
|
|
88
|
+
rejection("text/plain", mp3)
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Test
|
|
93
|
+
fun aPlaylistDeclaredAsTextIsAccepted() {
|
|
94
|
+
assertNull(rejection("text/plain", "#EXTM3U\n#EXT-X-VERSION:3".toByteArray()))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Test
|
|
98
|
+
fun aMarkupBodyIsRefusedWhenNothingUsableIsDeclared() {
|
|
99
|
+
assertEquals(MediaResponseCheck.Rejection.MarkupBody, rejection(null, xmlEnvelope))
|
|
100
|
+
assertEquals(
|
|
101
|
+
MediaResponseCheck.Rejection.MarkupBody,
|
|
102
|
+
rejection("application/octet-stream", xmlEnvelope)
|
|
103
|
+
)
|
|
104
|
+
assertEquals(
|
|
105
|
+
MediaResponseCheck.Rejection.MarkupBody,
|
|
106
|
+
rejection(null, "<!DOCTYPE html><html>".toByteArray())
|
|
107
|
+
)
|
|
108
|
+
assertEquals(
|
|
109
|
+
MediaResponseCheck.Rejection.MarkupBody,
|
|
110
|
+
rejection(null, """{"error":"not found"}""".toByteArray())
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Test
|
|
115
|
+
fun leadingSpaceAndAByteOrderMarkDoNotHideAMarkupBody() {
|
|
116
|
+
assertEquals(
|
|
117
|
+
MediaResponseCheck.Rejection.MarkupBody,
|
|
118
|
+
rejection(null, "\n <?xml version=\"1.0\"?>".toByteArray())
|
|
119
|
+
)
|
|
120
|
+
assertEquals(
|
|
121
|
+
MediaResponseCheck.Rejection.MarkupBody,
|
|
122
|
+
rejection(null, byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()) + "<error/>".toByteArray())
|
|
123
|
+
)
|
|
124
|
+
// `FF FE` also satisfies the MPEG frame sync, so a UTF-16 document would
|
|
125
|
+
// read as audio if the mark were not dropped first.
|
|
126
|
+
assertEquals(
|
|
127
|
+
MediaResponseCheck.Rejection.MarkupBody,
|
|
128
|
+
rejection(null, byteArrayOf(0xFF.toByte(), 0xFE.toByte()) + "<e/>".toByteArray())
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Test
|
|
133
|
+
fun recognisedContainersAreAccepted() {
|
|
134
|
+
val bodies = mapOf(
|
|
135
|
+
"ID3" to byteArrayOf(0x49, 0x44, 0x33, 0x04, 0, 0, 0, 0),
|
|
136
|
+
"MPEG frame sync" to byteArrayOf(0xFF.toByte(), 0xFB.toByte(), 0x90.toByte(), 0, 0, 0, 0, 0),
|
|
137
|
+
"ADTS AAC" to byteArrayOf(0xFF.toByte(), 0xF1.toByte(), 0x50, 0x80.toByte(), 0, 0, 0, 0),
|
|
138
|
+
"FLAC" to byteArrayOf(0x66, 0x4C, 0x61, 0x43, 0, 0, 0, 0),
|
|
139
|
+
"Ogg" to byteArrayOf(0x4F, 0x67, 0x67, 0x53, 0, 0, 0, 0),
|
|
140
|
+
"WAV" to byteArrayOf(0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0),
|
|
141
|
+
"AIFF" to byteArrayOf(0x46, 0x4F, 0x52, 0x4D, 0, 0, 0, 0),
|
|
142
|
+
"CAF" to byteArrayOf(0x63, 0x61, 0x66, 0x66, 0, 0, 0, 0),
|
|
143
|
+
"MP4" to byteArrayOf(0, 0, 0, 0x20, 0x66, 0x74, 0x79, 0x70),
|
|
144
|
+
)
|
|
145
|
+
for ((name, bytes) in bodies) {
|
|
146
|
+
assertNull("$name is media", rejection("text/plain", bytes))
|
|
147
|
+
assertNull("$name is media", rejection(null, bytes))
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@Test
|
|
152
|
+
fun anUndeclaredOrOpaqueBinaryBodyIsAccepted() {
|
|
153
|
+
val binary = byteArrayOf(0x00, 0x01, 0x02, 0x03, 0x04)
|
|
154
|
+
assertNull("an unrecognised container is not a document", rejection(null, binary))
|
|
155
|
+
assertNull(rejection("application/octet-stream", binary))
|
|
156
|
+
assertNull(rejection("audio/mpeg", binary))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Test
|
|
160
|
+
fun anEmptyBodyIsNotJudged() {
|
|
161
|
+
assertNull(rejection(null, ByteArray(0)))
|
|
162
|
+
}
|
|
163
|
+
}
|