react-native-queue-player 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/build.gradle +7 -0
- package/android/consumer-rules.pro +72 -3
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +26 -22
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +20 -72
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +11 -0
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +660 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +202 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +303 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +394 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +968 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +930 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +940 -0
- package/ios/TrackPlayer+Recovery.swift +196 -0
- package/ios/TrackPlayer+Skip.swift +455 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +267 -0
- package/ios/TrackPlayer+Window.swift +195 -0
- package/ios/TrackPlayer.swift +305 -4610
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.media3.common.PlaybackException
|
|
5
|
+
import androidx.media3.common.util.UnstableApi
|
|
6
|
+
import androidx.test.core.app.ApplicationProvider
|
|
7
|
+
import org.junit.Assert.assertEquals
|
|
8
|
+
import org.junit.Test
|
|
9
|
+
import org.junit.runner.RunWith
|
|
10
|
+
import org.robolectric.RobolectricTestRunner
|
|
11
|
+
import org.robolectric.Shadows.shadowOf
|
|
12
|
+
import org.robolectric.annotation.Config
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* When a player reaches STATE_ENDED the engines hand the delegate the end
|
|
16
|
+
* signal before the generic state callback, so the queue-end path owns the
|
|
17
|
+
* ENDED emit and its reason.
|
|
18
|
+
*/
|
|
19
|
+
@RunWith(RobolectricTestRunner::class)
|
|
20
|
+
@Config(sdk = [34])
|
|
21
|
+
@UnstableApi
|
|
22
|
+
class EngineEndSignalOrderTest {
|
|
23
|
+
|
|
24
|
+
private val context: Context = ApplicationProvider.getApplicationContext()
|
|
25
|
+
|
|
26
|
+
private class OrderingDelegate : PlaybackEngineDelegate {
|
|
27
|
+
val calls = mutableListOf<String>()
|
|
28
|
+
override fun onStateMaybeChanged(engine: PlaybackEngine, playWhenReadyReason: Int?) { calls.add("state") }
|
|
29
|
+
override fun onPlaybackEnded(engine: PlaybackEngine) { calls.add("ended") }
|
|
30
|
+
override fun onIsPlayingChanged(engine: PlaybackEngine, isPlaying: Boolean) {}
|
|
31
|
+
override fun onTrackTransition(engine: PlaybackEngine, platformReason: Int) {}
|
|
32
|
+
override fun onActiveItemFormatChanged(engine: PlaybackEngine) {}
|
|
33
|
+
override fun onError(engine: PlaybackEngine, exception: PlaybackException, failedIndex: Int) {}
|
|
34
|
+
override fun onAutoTransitionDiscontinuity(engine: PlaybackEngine) {}
|
|
35
|
+
override fun onCrossfadeBegin(engine: PlaybackEngine, incomingIndex: Int) {}
|
|
36
|
+
override fun onCrossfadeCancel(engine: PlaybackEngine, revertedIndex: Int) {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** An empty playlist prepared is STATE_ENDED at once. */
|
|
40
|
+
private fun assertEndedBeforeState(engine: PlaybackEngine, delegate: OrderingDelegate) {
|
|
41
|
+
engine.setMediaItems(emptyList(), 0, 0L)
|
|
42
|
+
shadowOf(android.os.Looper.getMainLooper()).idle()
|
|
43
|
+
val ended = delegate.calls.indexOf("ended")
|
|
44
|
+
val stateAfter = delegate.calls.withIndex().any { it.value == "state" && it.index > ended }
|
|
45
|
+
assertEquals("the end signal must be delivered", true, ended >= 0)
|
|
46
|
+
assertEquals(
|
|
47
|
+
"the generic state callback follows the end signal: ${delegate.calls}",
|
|
48
|
+
true, stateAfter)
|
|
49
|
+
assertEquals(
|
|
50
|
+
"no state callback precedes the end signal for this transition: ${delegate.calls}",
|
|
51
|
+
-1, delegate.calls.subList(0, ended).lastIndexOf("state"))
|
|
52
|
+
engine.release()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun `gapless reports the end before the state`() {
|
|
57
|
+
val delegate = OrderingDelegate()
|
|
58
|
+
val engine = GaplessEngine(context = context)
|
|
59
|
+
engine.delegate = delegate
|
|
60
|
+
assertEndedBeforeState(engine, delegate)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test
|
|
64
|
+
fun `crossfade reports the end before the state`() {
|
|
65
|
+
val delegate = OrderingDelegate()
|
|
66
|
+
val engine = CrossfadeEngine(context = context, crossfadeDurationMs = 3_000L)
|
|
67
|
+
engine.delegate = delegate
|
|
68
|
+
assertEndedBeforeState(engine, delegate)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -4,7 +4,6 @@ import org.junit.Assert.assertArrayEquals
|
|
|
4
4
|
import org.junit.Assert.assertEquals
|
|
5
5
|
import org.junit.Assert.assertFalse
|
|
6
6
|
import org.junit.Assert.assertTrue
|
|
7
|
-
import org.junit.Assume.assumeTrue
|
|
8
7
|
import org.junit.Test
|
|
9
8
|
import org.junit.runner.RunWith
|
|
10
9
|
import org.robolectric.RobolectricTestRunner
|
|
@@ -168,7 +167,7 @@ class EqualizerEngineTest {
|
|
|
168
167
|
fun `reattach on same session is idempotent`() {
|
|
169
168
|
val engine = EqualizerEngine()
|
|
170
169
|
val first = engine.attach(audioSessionId = 123, channelCount = 2)
|
|
171
|
-
|
|
170
|
+
assertTrue("first attach succeeds", first)
|
|
172
171
|
val second = engine.attach(audioSessionId = 123, channelCount = 6)
|
|
173
172
|
assertTrue("repeat attach on the same session succeeds", second)
|
|
174
173
|
assertTrue(
|
|
@@ -181,7 +180,7 @@ class EqualizerEngineTest {
|
|
|
181
180
|
fun `attachAll holds multiple sessions simultaneously for crossfade`() {
|
|
182
181
|
val engine = EqualizerEngine()
|
|
183
182
|
val results = engine.attachAll(intArrayOf(101, 102))
|
|
184
|
-
|
|
183
|
+
assertTrue("session attaches", results[101] == true)
|
|
185
184
|
assertTrue("incoming session also attached", results[102] == true)
|
|
186
185
|
assertTrue("at least one session held", engine.isAttached())
|
|
187
186
|
}
|
|
@@ -190,7 +189,7 @@ class EqualizerEngineTest {
|
|
|
190
189
|
fun `attachAll diff releases sessions no longer in the input array`() {
|
|
191
190
|
val engine = EqualizerEngine()
|
|
192
191
|
val first = engine.attachAll(intArrayOf(201, 202))
|
|
193
|
-
|
|
192
|
+
assertTrue("session attaches", first[201] == true)
|
|
194
193
|
val second = engine.attachAll(intArrayOf(202))
|
|
195
194
|
// 201 dropped, 202 retained.
|
|
196
195
|
assertTrue("retained session marked succeeded", second[202] == true)
|
|
@@ -201,22 +200,34 @@ class EqualizerEngineTest {
|
|
|
201
200
|
fun `detachAll releases every per-session instance`() {
|
|
202
201
|
val engine = EqualizerEngine()
|
|
203
202
|
val results = engine.attachAll(intArrayOf(301, 302))
|
|
204
|
-
|
|
203
|
+
assertTrue("session attaches", results[301] == true)
|
|
205
204
|
engine.detachAll()
|
|
206
205
|
assertTrue("no sessions held after detachAll", !engine.isAttached())
|
|
207
206
|
}
|
|
208
207
|
|
|
209
208
|
@Test
|
|
210
|
-
fun `setBandGain
|
|
209
|
+
fun `setBandGain buffers the gain while sessions are attached`() {
|
|
211
210
|
val engine = EqualizerEngine()
|
|
212
211
|
val results = engine.attachAll(intArrayOf(401, 402))
|
|
213
|
-
|
|
212
|
+
assertTrue("session attaches", results[401] == true)
|
|
214
213
|
engine.setBandGain(3, 4f)
|
|
215
214
|
assertEquals(4f, engine.getBandGains()[3])
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** A failed attach leaves no trace: nothing attached, and a retry is possible. */
|
|
218
|
+
@Test
|
|
219
|
+
@Config(sdk = [34], shadows = [ShadowDynamicsProcessingRejectingEnable::class])
|
|
220
|
+
fun `attach that fails while configuring leaves the engine detached`() {
|
|
221
|
+
val engine = EqualizerEngine()
|
|
222
|
+
|
|
223
|
+
val attached = engine.attach(audioSessionId = 801, channelCount = 2)
|
|
224
|
+
|
|
225
|
+
assertFalse("the attach reports its failure", attached)
|
|
226
|
+
assertFalse("nothing is attached after a failed configure", engine.isAttached())
|
|
227
|
+
assertEquals(
|
|
228
|
+
"the failed session is not carried as attached on the next diff",
|
|
229
|
+
mapOf(801 to false),
|
|
230
|
+
engine.attachAll(intArrayOf(801))
|
|
231
|
+
)
|
|
221
232
|
}
|
|
222
233
|
}
|
|
@@ -232,11 +232,6 @@ private class FakePlaybackEngine : PlaybackEngine {
|
|
|
232
232
|
override val mediaSessionPlayer: androidx.media3.exoplayer.ExoPlayer
|
|
233
233
|
get() = error("not used in test")
|
|
234
234
|
|
|
235
|
-
override suspend fun prepare(
|
|
236
|
-
mediaItems: List<androidx.media3.common.MediaItem>,
|
|
237
|
-
startIndex: Int,
|
|
238
|
-
startPositionMs: Long,
|
|
239
|
-
) = error("not used in test")
|
|
240
235
|
override fun release() = error("not used in test")
|
|
241
236
|
override fun setMediaItems(items: List<androidx.media3.common.MediaItem>, startIndex: Int, startPositionMs: Long) = error("not used in test")
|
|
242
237
|
override fun addMediaItems(items: List<androidx.media3.common.MediaItem>, insertBefore: Int) = error("not used in test")
|
|
@@ -268,6 +263,5 @@ private class FakePlaybackEngine : PlaybackEngine {
|
|
|
268
263
|
}
|
|
269
264
|
override fun attachAudioBufferSink(sink: androidx.media3.exoplayer.audio.TeeAudioProcessor.AudioBufferSink) = error("not used in test")
|
|
270
265
|
override fun detachAudioBufferSink(sink: androidx.media3.exoplayer.audio.TeeAudioProcessor.AudioBufferSink) = error("not used in test")
|
|
271
|
-
override suspend fun handoff(to: PlaybackEngine, atTrackBoundary: Boolean) = error("not used in test")
|
|
272
266
|
override var delegate: PlaybackEngineDelegate? = null
|
|
273
267
|
}
|
|
@@ -4,20 +4,22 @@ import org.junit.Assert.assertArrayEquals
|
|
|
4
4
|
import org.junit.Assert.assertEquals
|
|
5
5
|
import org.junit.Assert.assertFalse
|
|
6
6
|
import org.junit.Assert.assertTrue
|
|
7
|
+
import android.media.audiofx.Equalizer
|
|
7
8
|
import org.junit.Test
|
|
8
9
|
import org.junit.runner.RunWith
|
|
9
10
|
import org.robolectric.RobolectricTestRunner
|
|
11
|
+
import org.robolectric.shadow.api.Shadow
|
|
10
12
|
import org.robolectric.annotation.Config
|
|
13
|
+
import org.robolectric.shadows.ShadowAudioEffect
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
|
-
* State-mirror coverage for [EqualizerLegacyEngine] on the API <28 path
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* behaviour varies per OEM.
|
|
16
|
+
* State-mirror coverage for [EqualizerLegacyEngine] on the API <28 path:
|
|
17
|
+
* the band mapping, the dB↔mB conversion, and what reaches each attached
|
|
18
|
+
* session. [ShadowEqualizer] supplies the hardware-side band count, centre
|
|
19
|
+
* frequencies and gain range that legacy AudioEffect varies per OEM.
|
|
18
20
|
*/
|
|
19
21
|
@RunWith(RobolectricTestRunner::class)
|
|
20
|
-
@Config(sdk = [27])
|
|
22
|
+
@Config(sdk = [27], shadows = [ShadowEqualizer::class])
|
|
21
23
|
class EqualizerLegacyEngineTest {
|
|
22
24
|
|
|
23
25
|
@Test
|
|
@@ -181,9 +183,7 @@ class EqualizerLegacyEngineTest {
|
|
|
181
183
|
fun `attachAll on empty array detaches every active instance`() {
|
|
182
184
|
val engine = EqualizerLegacyEngine()
|
|
183
185
|
val first = engine.attachAll(intArrayOf(501, 502))
|
|
184
|
-
|
|
185
|
-
"Robolectric audio-effect shadow refused first attach", first[501] == true,
|
|
186
|
-
)
|
|
186
|
+
assertEquals("both sessions attach", mapOf(501 to true, 502 to true), first)
|
|
187
187
|
val second = engine.attachAll(intArrayOf())
|
|
188
188
|
assertEquals("empty array clears the map", 0, second.size)
|
|
189
189
|
assertFalse("no sessions held after empty attachAll", engine.isAttached())
|
|
@@ -193,9 +193,7 @@ class EqualizerLegacyEngineTest {
|
|
|
193
193
|
fun `detachAll releases every per-session instance`() {
|
|
194
194
|
val engine = EqualizerLegacyEngine()
|
|
195
195
|
val results = engine.attachAll(intArrayOf(601, 602))
|
|
196
|
-
|
|
197
|
-
"Robolectric audio-effect shadow refused attach", results[601] == true,
|
|
198
|
-
)
|
|
196
|
+
assertEquals("both sessions attach", mapOf(601 to true, 602 to true), results)
|
|
199
197
|
engine.detachAll()
|
|
200
198
|
assertFalse("no sessions held after detachAll", engine.isAttached())
|
|
201
199
|
}
|
|
@@ -204,10 +202,40 @@ class EqualizerLegacyEngineTest {
|
|
|
204
202
|
fun `setBandGain fans gain across every attached session`() {
|
|
205
203
|
val engine = EqualizerLegacyEngine()
|
|
206
204
|
val results = engine.attachAll(intArrayOf(701, 702))
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
205
|
+
assertEquals("both sessions attach", mapOf(701 to true, 702 to true), results)
|
|
206
|
+
|
|
210
207
|
engine.setBandGain(4, 5f)
|
|
208
|
+
|
|
211
209
|
assertEquals("buffered band gain mirrors the call", 5f, engine.getBandGains()[4])
|
|
210
|
+
// Our 500 Hz band maps onto the shadow's 230 Hz legacy band, and +5 dB
|
|
211
|
+
// is 500 mB against a ±1500 mB device range.
|
|
212
|
+
val legacyBand: Short = 1
|
|
213
|
+
assertEquals("session 701 receives the gain", 500, levelFor(701, legacyBand))
|
|
214
|
+
assertEquals("session 702 receives the gain", 500, levelFor(702, legacyBand))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Band level the session's own equalizer reports, in millibels. */
|
|
218
|
+
private fun levelFor(sessionId: Int, band: Short): Int {
|
|
219
|
+
val effect = ShadowAudioEffect.getAudioEffects().firstOrNull {
|
|
220
|
+
Shadow.extract<ShadowEqualizer>(it).audioSession == sessionId
|
|
221
|
+
} ?: error("no equalizer attached for session $sessionId")
|
|
222
|
+
return (effect as Equalizer).getBandLevel(band).toInt()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** A failed attach leaves no trace: nothing attached, and a retry is possible. */
|
|
226
|
+
@Test
|
|
227
|
+
@Config(sdk = [27], shadows = [ShadowEqualizerRejectingBandWrites::class])
|
|
228
|
+
fun `attach that fails while configuring leaves the engine detached`() {
|
|
229
|
+
val engine = EqualizerLegacyEngine()
|
|
230
|
+
|
|
231
|
+
val attached = engine.attach(audioSessionId = 801, channelCount = 2)
|
|
232
|
+
|
|
233
|
+
assertFalse("the attach reports its failure", attached)
|
|
234
|
+
assertFalse("nothing is attached after a failed configure", engine.isAttached())
|
|
235
|
+
assertEquals(
|
|
236
|
+
"the failed session is not carried as attached on the next diff",
|
|
237
|
+
mapOf(801 to false),
|
|
238
|
+
engine.attachAll(intArrayOf(801))
|
|
239
|
+
)
|
|
212
240
|
}
|
|
213
241
|
}
|
|
@@ -181,4 +181,19 @@ class FFTProcessorTeeTest {
|
|
|
181
181
|
assertEquals(expectedPosition, original.position())
|
|
182
182
|
assertEquals(expectedOrder, original.order())
|
|
183
183
|
}
|
|
184
|
+
|
|
185
|
+
/** A flush resets the emit throttle, so the first frame of a new stream is not delayed. */
|
|
186
|
+
@Test
|
|
187
|
+
fun `flush resets the emit throttle`() {
|
|
188
|
+
val reported = mutableListOf<Int>()
|
|
189
|
+
val tee = makeTee(fftSize = 256, intervalMs = 1000) { _, _, sampleRate -> reported += sampleRate }
|
|
190
|
+
tee.flush(48000, 1, C.ENCODING_PCM_16BIT)
|
|
191
|
+
tee.handleBuffer(pcm16Buffer(ShortArray(256) { 1000 }))
|
|
192
|
+
assertEquals(listOf(48000), reported)
|
|
193
|
+
|
|
194
|
+
// Well inside the 1000 ms window: without the reset this emit is throttled.
|
|
195
|
+
tee.flush(96000, 1, C.ENCODING_PCM_16BIT)
|
|
196
|
+
tee.handleBuffer(pcm16Buffer(ShortArray(256) { 1000 }))
|
|
197
|
+
assertEquals(listOf(48000, 96000), reported)
|
|
198
|
+
}
|
|
184
199
|
}
|
|
@@ -13,8 +13,10 @@ import java.io.File
|
|
|
13
13
|
/**
|
|
14
14
|
* Unit tests for [FifoCacheEvictor]'s eviction ordering. Drives the evictor's
|
|
15
15
|
* [CacheEvictor] callbacks directly against a minimal fake [Cache] whose only
|
|
16
|
-
* live method is `removeSpan
|
|
17
|
-
*
|
|
16
|
+
* live method is `removeSpan`, so the tests assert insertion-order eviction
|
|
17
|
+
* without a real SimpleCache or filesystem. The fake normally delegates back to
|
|
18
|
+
* `onSpanRemoved`; `callsBack = false` models a span the cache index has already
|
|
19
|
+
* dropped, which notifies nothing.
|
|
18
20
|
*/
|
|
19
21
|
@UnstableApi
|
|
20
22
|
class FifoCacheEvictorTest {
|
|
@@ -72,11 +74,140 @@ class FifoCacheEvictorTest {
|
|
|
72
74
|
assertEquals(listOf("b"), removed.map { it.key })
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
@Test
|
|
78
|
+
fun `a url retires its insertion order once its last span is gone`() {
|
|
79
|
+
// The cache notifies an add even when the evictor's own set rejects the
|
|
80
|
+
// span, so a count taken from the callbacks over-counts. Here both adds are
|
|
81
|
+
// the same span, so one entry exists and one removal empties the url.
|
|
82
|
+
val evictor = FifoCacheEvictor(maxBytes = 1000)
|
|
83
|
+
val cache = FakeCache(evictor)
|
|
84
|
+
val only = CacheSpan("a", 0, 100, 1L, File("/tmp/a"))
|
|
85
|
+
evictor.onSpanAdded(cache, only)
|
|
86
|
+
evictor.onSpanAdded(cache, only)
|
|
87
|
+
evictor.onSpanAdded(cache, span("b", length = 100))
|
|
88
|
+
evictor.onSpanRemoved(cache, only)
|
|
89
|
+
removed.clear()
|
|
90
|
+
|
|
91
|
+
// `a` is empty, so re-adding it stamps a fresh newest order and `b` — now
|
|
92
|
+
// the oldest — is the victim. An over-count leaves `a` on its old order and
|
|
93
|
+
// evicts `a` instead.
|
|
94
|
+
evictor.onSpanAdded(cache, span("a", length = 100))
|
|
95
|
+
evictor.onSpanAdded(cache, span("c", length = 900)) // 1100 > maxBytes
|
|
96
|
+
assertEquals(listOf("b"), removed.map { it.key })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Test
|
|
100
|
+
fun `a url keeps its insertion order while any of its spans remain`() {
|
|
101
|
+
val evictor = FifoCacheEvictor(maxBytes = 1000)
|
|
102
|
+
val cache = FakeCache(evictor)
|
|
103
|
+
val first = CacheSpan("a", 0, 100, 1L, File("/tmp/a-1"))
|
|
104
|
+
val second = CacheSpan("a", 100, 100, 1L, File("/tmp/a-2"))
|
|
105
|
+
evictor.onSpanAdded(cache, first)
|
|
106
|
+
evictor.onSpanAdded(cache, second)
|
|
107
|
+
evictor.onSpanAdded(cache, second) // rejected by the set, still notified
|
|
108
|
+
evictor.onSpanAdded(cache, span("b", length = 100))
|
|
109
|
+
evictor.onSpanRemoved(cache, first)
|
|
110
|
+
removed.clear()
|
|
111
|
+
|
|
112
|
+
// `a` still holds `second`, so it keeps its original (oldest) order and is
|
|
113
|
+
// still the first victim. A retired order would sort it newest and evict b.
|
|
114
|
+
evictor.onSpanAdded(cache, span("c", length = 900))
|
|
115
|
+
assertEquals("a", removed.first().key)
|
|
116
|
+
removed.clear()
|
|
117
|
+
|
|
118
|
+
// With `a` now empty, a re-add stamps a fresh newest order. An over-count
|
|
119
|
+
// from the rejected add leaves `a` on its old order and evicts it again.
|
|
120
|
+
evictor.onSpanRemoved(cache, second)
|
|
121
|
+
evictor.onSpanAdded(cache, span("a", length = 100))
|
|
122
|
+
evictor.onSpanAdded(cache, span("d", length = 900))
|
|
123
|
+
assertEquals("b", removed.first().key)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@Test(timeout = 5_000)
|
|
127
|
+
fun `eviction advances past a span the cache no longer holds`() {
|
|
128
|
+
// removeSpan is a no-op here, modelling a span the cache index has already
|
|
129
|
+
// dropped: no onSpanRemoved callback, so the head does not move on its own.
|
|
130
|
+
// The evictor must retire its own record and carry on — spinning wedges the
|
|
131
|
+
// cache's lock, and giving up makes that span the victim of every later
|
|
132
|
+
// eviction, which stops eviction permanently.
|
|
133
|
+
val evictor = FifoCacheEvictor(maxBytes = 150)
|
|
134
|
+
val deaf = FakeCache(evictor, callsBack = false)
|
|
135
|
+
evictor.onSpanAdded(deaf, span("a", length = 100))
|
|
136
|
+
evictor.onSpanAdded(deaf, span("b", length = 100))
|
|
137
|
+
evictor.onSpanAdded(deaf, span("c", length = 100))
|
|
138
|
+
assertEquals(listOf("a", "b"), removed.map { it.key })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@Test
|
|
142
|
+
fun `a protected url survives eviction and the next-oldest goes instead`() {
|
|
143
|
+
val evictor = FifoCacheEvictor(maxBytes = 250)
|
|
144
|
+
val cache = FakeCache(evictor)
|
|
145
|
+
evictor.setProtectedKeys(setOf("a"))
|
|
146
|
+
evictor.onSpanAdded(cache, span("a", length = 100))
|
|
147
|
+
evictor.onSpanAdded(cache, span("b", length = 100))
|
|
148
|
+
evictor.onSpanAdded(cache, span("c", length = 100)) // 300 > 250
|
|
149
|
+
assertEquals(listOf("b"), removed.map { it.key })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@Test(timeout = 5_000)
|
|
153
|
+
fun `eviction gives up when every remaining url is protected`() {
|
|
154
|
+
// Over budget with nothing evictable is a real state: the protected window
|
|
155
|
+
// can outgrow the budget. Staying over budget is the outcome; spinning
|
|
156
|
+
// under the cache's lock is not.
|
|
157
|
+
val evictor = FifoCacheEvictor(maxBytes = 150)
|
|
158
|
+
val cache = FakeCache(evictor)
|
|
159
|
+
evictor.setProtectedKeys(setOf("a", "b"))
|
|
160
|
+
evictor.onSpanAdded(cache, span("a", length = 100))
|
|
161
|
+
evictor.onSpanAdded(cache, span("b", length = 100)) // 200 > 150, both shielded
|
|
162
|
+
assertEquals(emptyList<String>(), removed.map { it.key })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Test(timeout = 5_000)
|
|
166
|
+
fun `a protected span ahead of the victim does not stall the self-heal`() {
|
|
167
|
+
// The victim is not the head here: `a` is protected and sorts first, so a
|
|
168
|
+
// progress check that compared the head would never see the removal fail
|
|
169
|
+
// and would spin under the cache's lock.
|
|
170
|
+
val evictor = FifoCacheEvictor(maxBytes = 150)
|
|
171
|
+
val deaf = FakeCache(evictor, callsBack = false)
|
|
172
|
+
evictor.setProtectedKeys(setOf("a"))
|
|
173
|
+
evictor.onSpanAdded(deaf, span("a", length = 100))
|
|
174
|
+
evictor.onSpanAdded(deaf, span("b", length = 100)) // 200 > 150; a shielded
|
|
175
|
+
assertEquals(listOf("b"), removed.map { it.key })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@Test
|
|
179
|
+
fun `dropping protection makes the url evictable again`() {
|
|
180
|
+
val evictor = FifoCacheEvictor(maxBytes = 250)
|
|
181
|
+
val cache = FakeCache(evictor)
|
|
182
|
+
evictor.setProtectedKeys(setOf("a"))
|
|
183
|
+
evictor.onSpanAdded(cache, span("a", length = 100))
|
|
184
|
+
evictor.onSpanAdded(cache, span("b", length = 100))
|
|
185
|
+
evictor.setProtectedKeys(emptySet())
|
|
186
|
+
evictor.onSpanAdded(cache, span("c", length = 100)) // 300 > 250
|
|
187
|
+
assertEquals(listOf("a"), removed.map { it.key })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@Test
|
|
191
|
+
fun `the protected set is copied so a caller's later mutation cannot leak in`() {
|
|
192
|
+
val evictor = FifoCacheEvictor(maxBytes = 250)
|
|
193
|
+
val cache = FakeCache(evictor)
|
|
194
|
+
val live = mutableSetOf("a")
|
|
195
|
+
evictor.setProtectedKeys(live)
|
|
196
|
+
live.add("b") // must not reach the evictor
|
|
197
|
+
evictor.onSpanAdded(cache, span("a", length = 100))
|
|
198
|
+
evictor.onSpanAdded(cache, span("b", length = 100))
|
|
199
|
+
evictor.onSpanAdded(cache, span("c", length = 100)) // 300 > 250
|
|
200
|
+
assertEquals(listOf("b"), removed.map { it.key })
|
|
201
|
+
}
|
|
202
|
+
|
|
75
203
|
/** Minimal [Cache]: only `removeSpan` is live (records + calls back the evictor). */
|
|
76
|
-
private inner class FakeCache(
|
|
204
|
+
private inner class FakeCache(
|
|
205
|
+
private val evictor: FifoCacheEvictor,
|
|
206
|
+
private val callsBack: Boolean = true
|
|
207
|
+
) : Cache {
|
|
77
208
|
override fun removeSpan(span: CacheSpan) {
|
|
78
209
|
removed.add(span)
|
|
79
|
-
evictor.onSpanRemoved(this, span)
|
|
210
|
+
if (callsBack) evictor.onSpanRemoved(this, span)
|
|
80
211
|
}
|
|
81
212
|
|
|
82
213
|
override fun getUid(): Long = throw UnsupportedOperationException()
|
|
@@ -45,6 +45,7 @@ class GaplessEngineLifecycleTest {
|
|
|
45
45
|
override fun onError(
|
|
46
46
|
engine: PlaybackEngine,
|
|
47
47
|
exception: androidx.media3.common.PlaybackException,
|
|
48
|
+
failedIndex: Int,
|
|
48
49
|
) = Unit
|
|
49
50
|
override fun onPlaybackEnded(engine: PlaybackEngine) = Unit
|
|
50
51
|
override fun onAutoTransitionDiscontinuity(engine: PlaybackEngine) = Unit
|
|
@@ -21,8 +21,9 @@ import org.robolectric.annotation.Config
|
|
|
21
21
|
* the engine-internal `handleReplayGainParse` helper which these
|
|
22
22
|
* tests drive directly with synthetic [ReplayGainData].
|
|
23
23
|
* This file covers the no-data baseline,
|
|
24
|
-
* mode-change-without-data behaviour,
|
|
25
|
-
* `handleReplayGainParse` cache + recompute contract
|
|
24
|
+
* mode-change-without-data behaviour, the
|
|
25
|
+
* `handleReplayGainParse` cache + recompute contract, and the
|
|
26
|
+
* consumer's supplied claim outranking the parsed tags.
|
|
26
27
|
*/
|
|
27
28
|
@RunWith(RobolectricTestRunner::class)
|
|
28
29
|
@Config(sdk = [34])
|
|
@@ -81,6 +82,39 @@ class GaplessEngineReplayGainTest {
|
|
|
81
82
|
engine.release()
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The consumer's claim over the active item outranks the file's tags: a
|
|
87
|
+
* supplied value replaces detection in full, and a claim whose values were
|
|
88
|
+
* all rejected still discards detection and plays at unity.
|
|
89
|
+
*/
|
|
90
|
+
@Test
|
|
91
|
+
fun `a supplied claim on the active item replaces the detected tags`() {
|
|
92
|
+
val engine = makeEngine()
|
|
93
|
+
ReplayGainConfig.setMode(ReplayGainMode.TRACK)
|
|
94
|
+
engine.setMediaItems(listOf(androidx.media3.common.MediaItem.fromUri("https://example.com/a.mp3")), 0, 0L)
|
|
95
|
+
val detected = ReplayGainData(trackGainDb = -6.0f, trackPeak = 0.5f, albumGainDb = null, albumPeak = null)
|
|
96
|
+
engine.handleReplayGainParse(detected)
|
|
97
|
+
assertEquals(detected, engine.currentReplayGainData())
|
|
98
|
+
|
|
99
|
+
var claim: TrackItem = suppliedTrack(trackGain = -3.0)
|
|
100
|
+
engine.suppliedReplayGainResolver = { SuppliedReplayGain.from(claim) }
|
|
101
|
+
assertEquals(
|
|
102
|
+
ReplayGainData(trackGainDb = -3.0f, trackPeak = null, albumGainDb = null, albumPeak = null),
|
|
103
|
+
engine.currentReplayGainData())
|
|
104
|
+
assertTrue(engine.currentReplayGainActive())
|
|
105
|
+
|
|
106
|
+
claim = suppliedTrack(trackGain = Double.NaN)
|
|
107
|
+
assertNull("a claim with no usable value discards the detection too", engine.currentReplayGainData())
|
|
108
|
+
assertFalse(engine.currentReplayGainActive())
|
|
109
|
+
engine.release()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private fun suppliedTrack(trackGain: Double) = TrackItem(
|
|
113
|
+
id = null, url = "https://example.com/a.mp3", title = null, artist = null, album = null,
|
|
114
|
+
duration = null, artworkUrl = null, extras = null,
|
|
115
|
+
replayGainTrackGain = trackGain, replayGainTrackPeak = null,
|
|
116
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null)
|
|
117
|
+
|
|
84
118
|
@Test
|
|
85
119
|
fun `handleReplayGainParse with album-only data is inactive in track mode`() {
|
|
86
120
|
val engine = makeEngine()
|
|
@@ -20,8 +20,8 @@ import java.io.File
|
|
|
20
20
|
*
|
|
21
21
|
* Exercises the cache directly without going through ExoPlayer:
|
|
22
22
|
* - Constructor creates the cache directory under `context.cacheDir`.
|
|
23
|
-
* - `wrapDataSourceFactory`
|
|
24
|
-
*
|
|
23
|
+
* - `wrapDataSourceFactory` hands out a [CacheDataSource] while the cache
|
|
24
|
+
* serves playback and the bare upstream source while it does not.
|
|
25
25
|
* - `release()` is idempotent, and a fresh cache can be constructed
|
|
26
26
|
* at the same directory afterwards (the SimpleCache one-instance-
|
|
27
27
|
* per-directory invariant is restored on release).
|
|
@@ -69,25 +69,33 @@ class LookaheadCacheTest {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
@Test
|
|
72
|
-
fun `
|
|
72
|
+
fun `the wrapped factory reads through the cache while it serves playback`() {
|
|
73
73
|
val upstream = DefaultHttpDataSource.Factory()
|
|
74
74
|
val wrapped = cache.wrapDataSourceFactory(upstream)
|
|
75
|
-
|
|
75
|
+
val source = wrapped.createDataSource()
|
|
76
76
|
assertTrue(
|
|
77
|
-
"
|
|
78
|
-
|
|
77
|
+
"a data source opened while the cache is enabled checks the disk first",
|
|
78
|
+
source is CacheDataSource
|
|
79
79
|
)
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Disabled means every read goes to the origin and nothing is written,
|
|
84
|
+
* whatever the disk holds — the wrapper hands out the bare upstream source.
|
|
85
|
+
* Decided per data source, so a change reaches the next item load.
|
|
86
|
+
*/
|
|
82
87
|
@Test
|
|
83
|
-
fun `wrapped factory
|
|
88
|
+
fun `the wrapped factory bypasses the cache while it does not serve playback`() {
|
|
84
89
|
val upstream = DefaultHttpDataSource.Factory()
|
|
85
90
|
val wrapped = cache.wrapDataSourceFactory(upstream)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
+
|
|
92
|
+
cache.servesPlayback = false
|
|
93
|
+
val bypassed = wrapped.createDataSource()
|
|
94
|
+
cache.servesPlayback = true
|
|
95
|
+
val served = wrapped.createDataSource()
|
|
96
|
+
|
|
97
|
+
assertTrue("disabled: the upstream source, no cache in front", bypassed !is CacheDataSource)
|
|
98
|
+
assertTrue("re-enabled: the next source goes through the cache again", served is CacheDataSource)
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
@Test
|
|
@@ -125,13 +133,11 @@ class LookaheadCacheTest {
|
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
@Test
|
|
128
|
-
fun `default max size
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// library's automatic disk usage before the runtime config API
|
|
132
|
-
// ships).
|
|
136
|
+
fun `default max size is 100 MB`() {
|
|
137
|
+
// The default is what a consumer that omits `lookaheadCacheMaxSizeMb`
|
|
138
|
+
// from `configure()` gets, so it is part of the public contract.
|
|
133
139
|
assertEquals(
|
|
134
|
-
"
|
|
140
|
+
"default disk budget",
|
|
135
141
|
100L * 1024L * 1024L,
|
|
136
142
|
LookaheadCache.DEFAULT_MAX_SIZE_BYTES
|
|
137
143
|
)
|
|
@@ -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
|
+
}
|