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
|
@@ -45,7 +45,9 @@ class TrackPlayerQueueTest {
|
|
|
45
45
|
private fun track(id: String) = TrackItem(
|
|
46
46
|
id = id, url = "https://example.com/$id.m4a",
|
|
47
47
|
title = null, artist = null, album = null,
|
|
48
|
-
duration = null, artworkUrl = null, extras = null
|
|
48
|
+
duration = null, artworkUrl = null, extras = null,
|
|
49
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
50
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null
|
|
49
51
|
)
|
|
50
52
|
|
|
51
53
|
private lateinit var serviceHandle: PlaybackServiceTestHandle
|
|
@@ -55,7 +57,7 @@ class TrackPlayerQueueTest {
|
|
|
55
57
|
player = TrackPlayer()
|
|
56
58
|
serviceHandle = bindPlaybackServiceForTest(player, context())
|
|
57
59
|
player.configureInternal(
|
|
58
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
|
|
60
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
|
|
59
61
|
)
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -128,13 +130,15 @@ class TrackPlayerQueueTest {
|
|
|
128
130
|
// --- getQueue / getCurrentTrackIndex
|
|
129
131
|
|
|
130
132
|
@Test
|
|
131
|
-
fun `
|
|
133
|
+
fun `setQueueInternal stores the tracks in order`() {
|
|
134
|
+
// `getQueue()` itself returns a Nitro Promise, which cannot be resolved
|
|
135
|
+
// from Robolectric; it reads this list on main, and the demo cycle covers
|
|
136
|
+
// the bridged call.
|
|
132
137
|
val tracks = (1..3).map { track("t$it") }
|
|
133
138
|
player.setQueueInternal(tracks)
|
|
134
139
|
|
|
135
|
-
|
|
136
|
-
assertEquals(
|
|
137
|
-
assertEquals("t1", result[0].id)
|
|
140
|
+
assertEquals(3, player.tracks.size)
|
|
141
|
+
assertEquals("t1", player.tracks[0].id)
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
@Test
|
|
@@ -275,7 +279,7 @@ class TrackPlayerQueueTest {
|
|
|
275
279
|
fun `skipToIndex after setQueue issues the ExoPlayer seek`() {
|
|
276
280
|
// After setQueue the MediaItem timeline has 3 entries, so the
|
|
277
281
|
// `target < mediaItemCount` guard in skipToIndexInternal no
|
|
278
|
-
// longer short-circuits — confirms the skip → seek
|
|
282
|
+
// longer short-circuits — confirms the skip → seek path
|
|
279
283
|
// survives once the timeline is populated.
|
|
280
284
|
player.setQueueInternal((1..3).map { track("t$it") })
|
|
281
285
|
|
|
@@ -41,7 +41,8 @@ class TrackPlayerReadersTest {
|
|
|
41
41
|
PlayerConfig(
|
|
42
42
|
httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null,
|
|
43
43
|
networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null,
|
|
44
|
-
|
|
44
|
+
voiceVocabularyDonationEnabled = null,
|
|
45
|
+
visualizationEnabled = null, clampSeekToBuffered = null,
|
|
45
46
|
lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null
|
|
46
47
|
),
|
|
47
48
|
context()
|
package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt
CHANGED
|
@@ -40,7 +40,9 @@ class TrackPlayerSkipCapabilityTest {
|
|
|
40
40
|
private fun track(id: String) = TrackItem(
|
|
41
41
|
id = id, url = "https://example.com/$id.m4a",
|
|
42
42
|
title = null, artist = null, album = null,
|
|
43
|
-
duration = null, artworkUrl = null, extras = null
|
|
43
|
+
duration = null, artworkUrl = null, extras = null,
|
|
44
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
45
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null
|
|
44
46
|
)
|
|
45
47
|
|
|
46
48
|
/** Drain main-looper posts so the auto-fire-on-subscribe lands. */
|
|
@@ -53,7 +55,7 @@ class TrackPlayerSkipCapabilityTest {
|
|
|
53
55
|
player = TrackPlayer()
|
|
54
56
|
serviceHandle = bindPlaybackServiceForTest(player, context())
|
|
55
57
|
player.configureInternal(
|
|
56
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
|
|
58
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
|
|
57
59
|
)
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -318,7 +320,7 @@ class TrackPlayerSkipCapabilityTest {
|
|
|
318
320
|
player = TrackPlayer()
|
|
319
321
|
serviceHandle = bindPlaybackServiceForTest(player, context())
|
|
320
322
|
player.configureInternal(
|
|
321
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
|
|
323
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
|
|
322
324
|
)
|
|
323
325
|
}
|
|
324
326
|
|
|
@@ -326,10 +328,25 @@ class TrackPlayerSkipCapabilityTest {
|
|
|
326
328
|
|
|
327
329
|
private fun configureRestart() {
|
|
328
330
|
player.configureInternal(
|
|
329
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = SkipToPreviousBehavior.RESTART_OR_PREVIOUS), context()
|
|
331
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = SkipToPreviousBehavior.RESTART_OR_PREVIOUS), context()
|
|
330
332
|
)
|
|
331
333
|
}
|
|
332
334
|
|
|
335
|
+
@Test
|
|
336
|
+
fun `restart mode — addToQueueInternal into empty queue keeps Previous false (cur stays -1)`() {
|
|
337
|
+
configureRestart()
|
|
338
|
+
val emissions = mutableListOf<SkipCapability>()
|
|
339
|
+
player.onSkipCapabilityChange { cap -> emissions.add(cap) }
|
|
340
|
+
flushMain()
|
|
341
|
+
emissions.clear()
|
|
342
|
+
|
|
343
|
+
player.addToQueueInternal((1..3).map { track("t$it") }, insertBefore = null)
|
|
344
|
+
// No loaded track: nothing to restart and nothing before it, so the
|
|
345
|
+
// lock-screen Previous control stays disabled until skipToIndex.
|
|
346
|
+
assertEquals(false, player.getCanSkipPrevious())
|
|
347
|
+
assertEquals(emptyList<SkipCapability>(), emissions)
|
|
348
|
+
}
|
|
349
|
+
|
|
333
350
|
@Test
|
|
334
351
|
fun `restart mode forces canSkipPrevious true at index 0`() {
|
|
335
352
|
configureRestart()
|
|
@@ -360,4 +377,73 @@ class TrackPlayerSkipCapabilityTest {
|
|
|
360
377
|
emissions.last().canSkipPrevious
|
|
361
378
|
)
|
|
362
379
|
}
|
|
380
|
+
|
|
381
|
+
// --- cast index writes keep the capability current
|
|
382
|
+
|
|
383
|
+
@Test
|
|
384
|
+
fun `emitRemoteTrackChange to the last index disables Next`() {
|
|
385
|
+
player.setQueueInternal((1..3).map { track("t$it") })
|
|
386
|
+
assertEquals(true, player.getCanSkipNext())
|
|
387
|
+
|
|
388
|
+
// The receiver advanced to the last track; the lock-screen Next
|
|
389
|
+
// control must grey out exactly as it does for a local advance.
|
|
390
|
+
player.emitRemoteTrackChange(2)
|
|
391
|
+
assertEquals(2, player.currentTrackIndex)
|
|
392
|
+
assertEquals(false, player.getCanSkipNext())
|
|
393
|
+
assertEquals(true, player.getCanSkipPrevious())
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
@Test
|
|
397
|
+
fun `emitRemoteTrackChange emits the capability change`() {
|
|
398
|
+
player.setQueueInternal((1..3).map { track("t$it") })
|
|
399
|
+
val emissions = mutableListOf<SkipCapability>()
|
|
400
|
+
player.onSkipCapabilityChange { cap -> emissions.add(cap) }
|
|
401
|
+
flushMain()
|
|
402
|
+
emissions.clear()
|
|
403
|
+
|
|
404
|
+
player.emitRemoteTrackChange(2)
|
|
405
|
+
flushMain()
|
|
406
|
+
assertEquals(
|
|
407
|
+
listOf(SkipCapability(canSkipPrevious = true, canSkipNext = false)),
|
|
408
|
+
emissions
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
@Test
|
|
413
|
+
fun `emitRemoteTrackChange classifies the new track's source`() {
|
|
414
|
+
// A local file at index 2, streaming tracks elsewhere: the source the
|
|
415
|
+
// JS side reads must follow the index the receiver moved to.
|
|
416
|
+
val local = TrackItem(
|
|
417
|
+
id = "local", url = "file:///tmp/local.m4a",
|
|
418
|
+
title = null, artist = null, album = null,
|
|
419
|
+
duration = null, artworkUrl = null, extras = null,
|
|
420
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
421
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null
|
|
422
|
+
)
|
|
423
|
+
player.setQueueInternal(listOf(track("t1"), track("t2"), local))
|
|
424
|
+
assertEquals(TrackSource.STREAMING, player.getCurrentTrackSource())
|
|
425
|
+
|
|
426
|
+
player.emitRemoteTrackChange(2)
|
|
427
|
+
assertEquals(TrackSource.LOCAL, player.getCurrentTrackSource())
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
@Test
|
|
431
|
+
fun `resumeLocalAfterCast lands on the handback index with its capability and source`() {
|
|
432
|
+
val local = TrackItem(
|
|
433
|
+
id = "local", url = "file:///tmp/local.m4a",
|
|
434
|
+
title = null, artist = null, album = null,
|
|
435
|
+
duration = null, artworkUrl = null, extras = null,
|
|
436
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
437
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null
|
|
438
|
+
)
|
|
439
|
+
player.setQueueInternal(listOf(track("t1"), track("t2"), local))
|
|
440
|
+
assertEquals(true, player.getCanSkipNext())
|
|
441
|
+
assertEquals(TrackSource.STREAMING, player.getCurrentTrackSource())
|
|
442
|
+
|
|
443
|
+
player.resumeLocalAfterCast(trackIndex = 2, positionMs = 0L)
|
|
444
|
+
flushMain()
|
|
445
|
+
assertEquals(2, player.currentTrackIndex)
|
|
446
|
+
assertEquals(false, player.getCanSkipNext())
|
|
447
|
+
assertEquals(TrackSource.LOCAL, player.getCurrentTrackSource())
|
|
448
|
+
}
|
|
363
449
|
}
|
|
@@ -15,9 +15,8 @@ import org.robolectric.annotation.Config
|
|
|
15
15
|
* [TrackPlayer]. `QueueSkipArithmeticTest` covers the raw
|
|
16
16
|
* arithmetic; these tests cover the adapter layer:
|
|
17
17
|
*
|
|
18
|
-
* - `computeNextIndex`
|
|
19
|
-
*
|
|
20
|
-
* delegate to [QueueSkipArithmetic].
|
|
18
|
+
* - `computeNextIndex` — reads `repeatModeState` + `currentTrackIndex`
|
|
19
|
+
* + `tracks.size` and delegates to [QueueSkipArithmetic].
|
|
21
20
|
* - `skipToNextInternal` / `skipToPreviousInternal` /
|
|
22
21
|
* `skipToIndexInternal` — advance `currentTrackIndex` when the
|
|
23
22
|
* arithmetic returns a target, no-op when it returns null. Under
|
|
@@ -41,7 +40,9 @@ class TrackPlayerSkipTest {
|
|
|
41
40
|
private fun track(id: String) = TrackItem(
|
|
42
41
|
id = id, url = "https://example.com/$id.m4a",
|
|
43
42
|
title = null, artist = null, album = null,
|
|
44
|
-
duration = null, artworkUrl = null, extras = null
|
|
43
|
+
duration = null, artworkUrl = null, extras = null,
|
|
44
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
45
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
private lateinit var serviceHandle: PlaybackServiceTestHandle
|
|
@@ -51,7 +52,7 @@ class TrackPlayerSkipTest {
|
|
|
51
52
|
player = TrackPlayer()
|
|
52
53
|
serviceHandle = bindPlaybackServiceForTest(player, context())
|
|
53
54
|
player.configureInternal(
|
|
54
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
|
|
55
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
|
|
55
56
|
)
|
|
56
57
|
player.tracks = (1..5).map { track("t$it") }
|
|
57
58
|
player.currentTrackIndex = 2 // mid-queue default for most cases
|
|
@@ -63,7 +64,7 @@ class TrackPlayerSkipTest {
|
|
|
63
64
|
serviceHandle.destroy()
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
// --- computeNextIndex
|
|
67
|
+
// --- computeNextIndex (adapter)
|
|
67
68
|
|
|
68
69
|
@Test
|
|
69
70
|
fun `computeNextIndex reads repeatModeState`() {
|
|
@@ -77,18 +78,6 @@ class TrackPlayerSkipTest {
|
|
|
77
78
|
assertEquals(3, player.computeNextIndex())
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
@Test
|
|
81
|
-
fun `computePreviousIndex reads repeatModeState`() {
|
|
82
|
-
player.repeatModeState = RepeatMode.OFF
|
|
83
|
-
assertEquals(1, player.computePreviousIndex())
|
|
84
|
-
|
|
85
|
-
player.repeatModeState = RepeatMode.TRACK
|
|
86
|
-
assertEquals(1, player.computePreviousIndex())
|
|
87
|
-
|
|
88
|
-
player.repeatModeState = RepeatMode.QUEUE
|
|
89
|
-
assertEquals(1, player.computePreviousIndex())
|
|
90
|
-
}
|
|
91
|
-
|
|
92
81
|
@Test
|
|
93
82
|
fun `computeNextIndex returns null at end under REPEAT OFF`() {
|
|
94
83
|
player.repeatModeState = RepeatMode.OFF
|
|
@@ -103,13 +92,6 @@ class TrackPlayerSkipTest {
|
|
|
103
92
|
assertEquals(0, player.computeNextIndex())
|
|
104
93
|
}
|
|
105
94
|
|
|
106
|
-
@Test
|
|
107
|
-
fun `computePreviousIndex wraps to last under REPEAT QUEUE at index 0`() {
|
|
108
|
-
player.repeatModeState = RepeatMode.QUEUE
|
|
109
|
-
player.currentTrackIndex = 0
|
|
110
|
-
assertEquals(4, player.computePreviousIndex())
|
|
111
|
-
}
|
|
112
|
-
|
|
113
95
|
// --- skipToNextInternal / skipToPreviousInternal
|
|
114
96
|
|
|
115
97
|
@Test
|
|
@@ -293,7 +275,7 @@ class TrackPlayerSkipTest {
|
|
|
293
275
|
|
|
294
276
|
private fun configureSkipBehavior(behavior: SkipToPreviousBehavior?) {
|
|
295
277
|
player.configureInternal(
|
|
296
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = behavior), context()
|
|
278
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = behavior), context()
|
|
297
279
|
)
|
|
298
280
|
player.tracks = (1..5).map { track("t$it") }
|
|
299
281
|
}
|
|
@@ -50,7 +50,7 @@ class TrackPlayerTransportTest {
|
|
|
50
50
|
player = TrackPlayer()
|
|
51
51
|
serviceHandle = bindPlaybackServiceForTest(player, context())
|
|
52
52
|
player.configureInternal(
|
|
53
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
|
|
53
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, voiceVocabularyDonationEnabled = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null), context()
|
|
54
54
|
)
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -80,7 +80,7 @@ class TrackPlayerTransportTest {
|
|
|
80
80
|
// currentTrackIndex while the ExoPlayer is still IDLE.
|
|
81
81
|
// iOS parity: paused-with-queue returns PAUSED, not NONE.
|
|
82
82
|
player.tracks = listOf(
|
|
83
|
-
TrackItem("t1", "https://example.com/1.m4a", null, null, null, null, null, null)
|
|
83
|
+
TrackItem("t1", "https://example.com/1.m4a", null, null, null, null, null, null, null, null, null, null)
|
|
84
84
|
)
|
|
85
85
|
player.currentTrackIndex = 0
|
|
86
86
|
|
|
@@ -104,7 +104,7 @@ class TrackPlayerTransportTest {
|
|
|
104
104
|
// on eventually-seekable content still lands. The reset-to-start protection
|
|
105
105
|
// applies once the item is loaded (STATE_READY) and confirmed unseekable.
|
|
106
106
|
player.setQueueInternal(
|
|
107
|
-
(1..3).map { TrackItem("t$it", "https://example.com/$it.m4a", null, null, null, null, null, null) }
|
|
107
|
+
(1..3).map { TrackItem("t$it", "https://example.com/$it.m4a", null, null, null, null, null, null, null, null, null, null) }
|
|
108
108
|
)
|
|
109
109
|
player.currentTrackIndex = 0
|
|
110
110
|
|
|
@@ -162,9 +162,6 @@ private class FakePlaybackEngineWithSinks : PlaybackEngine {
|
|
|
162
162
|
|
|
163
163
|
override val mediaSessionPlayer: androidx.media3.exoplayer.ExoPlayer
|
|
164
164
|
get() = error("not used in test")
|
|
165
|
-
override suspend fun prepare(
|
|
166
|
-
mediaItems: List<androidx.media3.common.MediaItem>, startIndex: Int, startPositionMs: Long,
|
|
167
|
-
) = error("not used in test")
|
|
168
165
|
override fun release() = error("not used in test")
|
|
169
166
|
override fun setMediaItems(items: List<androidx.media3.common.MediaItem>, startIndex: Int, startPositionMs: Long) = error("not used in test")
|
|
170
167
|
override fun addMediaItems(items: List<androidx.media3.common.MediaItem>, insertBefore: Int) = error("not used in test")
|
|
@@ -197,6 +194,5 @@ private class FakePlaybackEngineWithSinks : PlaybackEngine {
|
|
|
197
194
|
override fun detachAudioBufferSink(sink: TeeAudioProcessor.AudioBufferSink) {
|
|
198
195
|
if (attachedSinks.remove(sink)) detachCount++
|
|
199
196
|
}
|
|
200
|
-
override suspend fun handoff(to: PlaybackEngine, atTrackBoundary: Boolean) = error("not used in test")
|
|
201
197
|
override var delegate: PlaybackEngineDelegate? = null
|
|
202
198
|
}
|
|
@@ -9,10 +9,11 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|
|
9
9
|
import kotlinx.coroutines.flow.asStateFlow
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Test double for [CastSession.RemotePlayer]. Transport calls
|
|
13
|
-
* the StateFlows are driveable via the public
|
|
14
|
-
* fields
|
|
15
|
-
*
|
|
12
|
+
* Test double for [CastSession.RemotePlayer]. Transport calls record their
|
|
13
|
+
* name in [transportCalls]; the StateFlows are driveable via the public
|
|
14
|
+
* backing fields, or via [reportState] to mirror the real session's
|
|
15
|
+
* [hasReportedState] flag; [mediaSessionPlayer] is injectable so bind tests
|
|
16
|
+
* can assert the MediaSession swaps to it.
|
|
16
17
|
*/
|
|
17
18
|
internal class FakeRemotePlayer(
|
|
18
19
|
private val player: Player? = null,
|
|
@@ -36,15 +37,30 @@ internal class FakeRemotePlayer(
|
|
|
36
37
|
override val mediaError: SharedFlow<RemoteMediaError> = backingError.asSharedFlow()
|
|
37
38
|
override val seekCompleted: SharedFlow<Long> = backingSeek.asSharedFlow()
|
|
38
39
|
|
|
40
|
+
/** Transport calls in the order they arrived, for routing assertions. */
|
|
41
|
+
val transportCalls = mutableListOf<String>()
|
|
42
|
+
|
|
43
|
+
override var hasReportedState: Boolean = false
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Drive the receiver state the way a live session does: a non-idle report
|
|
47
|
+
* sets [hasReportedState]. Set [backingState] directly to stage a state
|
|
48
|
+
* the receiver has not reported yet.
|
|
49
|
+
*/
|
|
50
|
+
fun reportState(state: RemotePlaybackState) {
|
|
51
|
+
if (state != RemotePlaybackState.IDLE) hasReportedState = true
|
|
52
|
+
backingState.value = state
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
override fun setVolume(value: Float) {}
|
|
40
56
|
override fun close() {}
|
|
41
|
-
override fun play() {}
|
|
42
|
-
override fun pause() {}
|
|
43
|
-
override fun stop() {}
|
|
44
|
-
override fun seekTo(positionMs: Long) {}
|
|
45
|
-
override fun queueNext() {}
|
|
46
|
-
override fun queuePrev() {}
|
|
47
|
-
override fun skipToQueueIndex(index: Int) {}
|
|
57
|
+
override fun play() { transportCalls += "play" }
|
|
58
|
+
override fun pause() { transportCalls += "pause" }
|
|
59
|
+
override fun stop() { transportCalls += "stop" }
|
|
60
|
+
override fun seekTo(positionMs: Long) { transportCalls += "seekTo:$positionMs" }
|
|
61
|
+
override fun queueNext() { transportCalls += "queueNext" }
|
|
62
|
+
override fun queuePrev() { transportCalls += "queuePrev" }
|
|
63
|
+
override fun skipToQueueIndex(index: Int) { transportCalls += "skipToQueueIndex:$index" }
|
|
48
64
|
override fun setPlaybackRate(rate: Float) {}
|
|
49
65
|
override fun setRepeatMode(mode: RemoteRepeatMode) {}
|
|
50
66
|
|
package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer.cast.airplay
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import androidx.media3.common.MediaItem
|
|
6
|
+
import androidx.media3.common.MediaMetadata
|
|
7
|
+
import androidx.media3.common.Player
|
|
8
|
+
import androidx.media3.common.SimpleBasePlayer
|
|
9
|
+
import androidx.media3.common.util.UnstableApi
|
|
10
|
+
import androidx.test.core.app.ApplicationProvider
|
|
11
|
+
import com.google.common.collect.ImmutableList
|
|
12
|
+
import kotlinx.coroutines.CoroutineScope
|
|
13
|
+
import kotlinx.coroutines.Dispatchers
|
|
14
|
+
import kotlinx.coroutines.cancel
|
|
15
|
+
import org.junit.After
|
|
16
|
+
import org.junit.Assert.assertEquals
|
|
17
|
+
import org.junit.Assert.assertFalse
|
|
18
|
+
import org.junit.Assert.assertTrue
|
|
19
|
+
import org.junit.Before
|
|
20
|
+
import org.junit.Test
|
|
21
|
+
import org.junit.runner.RunWith
|
|
22
|
+
import org.robolectric.RobolectricTestRunner
|
|
23
|
+
import org.robolectric.Shadows.shadowOf
|
|
24
|
+
import org.robolectric.annotation.Config
|
|
25
|
+
import java.io.File
|
|
26
|
+
import java.util.Base64
|
|
27
|
+
import java.time.Duration
|
|
28
|
+
import java.util.concurrent.CopyOnWriteArrayList
|
|
29
|
+
import java.util.concurrent.Executors
|
|
30
|
+
import java.util.concurrent.TimeUnit
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Receiver-display sync lifecycle: what reaches the receiver while a sync is
|
|
34
|
+
* running, and that nothing reaches it once the session has been torn down.
|
|
35
|
+
*
|
|
36
|
+
* The teardown cases drive `stop()` from a non-looper thread, which is where
|
|
37
|
+
* it comes from in production — the JS thread on a user disconnect, the
|
|
38
|
+
* native drain thread on receiver death.
|
|
39
|
+
*/
|
|
40
|
+
@RunWith(RobolectricTestRunner::class)
|
|
41
|
+
@Config(sdk = [34])
|
|
42
|
+
@UnstableApi
|
|
43
|
+
class AirPlayMetadataSyncTest {
|
|
44
|
+
|
|
45
|
+
private lateinit var player: FakePlayer
|
|
46
|
+
private lateinit var target: RecordingTarget
|
|
47
|
+
private lateinit var sync: AirPlayMetadataSync
|
|
48
|
+
private val scope = CoroutineScope(Dispatchers.Main.immediate)
|
|
49
|
+
|
|
50
|
+
private fun context() = ApplicationProvider.getApplicationContext<android.content.Context>()
|
|
51
|
+
|
|
52
|
+
private fun idle() = shadowOf(Looper.getMainLooper()).idle()
|
|
53
|
+
|
|
54
|
+
private fun idleFor(ms: Long) =
|
|
55
|
+
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(ms))
|
|
56
|
+
|
|
57
|
+
/** Runs [block] off the looper and waits for it, as a real teardown does. */
|
|
58
|
+
private fun offLooper(block: () -> Unit) {
|
|
59
|
+
val executor = Executors.newSingleThreadExecutor()
|
|
60
|
+
try {
|
|
61
|
+
executor.submit(block).get(5, TimeUnit.SECONDS)
|
|
62
|
+
} finally {
|
|
63
|
+
executor.shutdownNow()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Waits for work dispatched off the looper, e.g. the artwork fetch. */
|
|
68
|
+
private fun awaitTrue(timeoutMs: Long = 5_000, condition: () -> Boolean): Boolean {
|
|
69
|
+
val deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs)
|
|
70
|
+
while (System.nanoTime() < deadline) {
|
|
71
|
+
idle()
|
|
72
|
+
if (condition()) return true
|
|
73
|
+
Thread.sleep(20)
|
|
74
|
+
}
|
|
75
|
+
return condition()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Before
|
|
79
|
+
fun setUp() {
|
|
80
|
+
player = FakePlayer(Looper.getMainLooper())
|
|
81
|
+
target = RecordingTarget()
|
|
82
|
+
sync = AirPlayMetadataSync(context(), target, player, scope)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@After
|
|
86
|
+
fun tearDown() {
|
|
87
|
+
sync.stop()
|
|
88
|
+
idle()
|
|
89
|
+
scope.cancel()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Test
|
|
93
|
+
fun `start sends the current track metadata`() {
|
|
94
|
+
sync.start()
|
|
95
|
+
idle()
|
|
96
|
+
|
|
97
|
+
assertEquals(
|
|
98
|
+
listOf(Triple("Everlong", "Foo Fighters", "The Colour and the Shape")),
|
|
99
|
+
target.metadata,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Test
|
|
104
|
+
fun `progress is pushed while playing`() {
|
|
105
|
+
sync.start()
|
|
106
|
+
idle()
|
|
107
|
+
val afterStart = target.progress.size
|
|
108
|
+
|
|
109
|
+
idleFor(11_000)
|
|
110
|
+
|
|
111
|
+
assertTrue(
|
|
112
|
+
"expected progress pushes while playing, got ${target.progress.size - afterStart}",
|
|
113
|
+
target.progress.size - afterStart >= 2,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Test
|
|
118
|
+
fun `stop from another thread halts the progress loop`() {
|
|
119
|
+
sync.start()
|
|
120
|
+
idle()
|
|
121
|
+
idleFor(6_000)
|
|
122
|
+
assertTrue(target.progress.isNotEmpty())
|
|
123
|
+
|
|
124
|
+
offLooper { sync.stop() }
|
|
125
|
+
idle()
|
|
126
|
+
val atStop = target.progress.size
|
|
127
|
+
|
|
128
|
+
idleFor(30_000)
|
|
129
|
+
|
|
130
|
+
assertEquals(atStop, target.progress.size)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@Test
|
|
134
|
+
fun `a resume callback arriving after stop starts no progress loop`() {
|
|
135
|
+
sync.start()
|
|
136
|
+
idle()
|
|
137
|
+
player.emitPlayWhenReady(false)
|
|
138
|
+
idle()
|
|
139
|
+
|
|
140
|
+
// The resume lands while listener removal is still queued behind the
|
|
141
|
+
// off-looper stop, so it reaches startProgressTimer on a spent sync.
|
|
142
|
+
offLooper { sync.stop() }
|
|
143
|
+
player.emitPlayWhenReady(true)
|
|
144
|
+
idle()
|
|
145
|
+
val atStop = target.progress.size
|
|
146
|
+
|
|
147
|
+
idleFor(30_000)
|
|
148
|
+
|
|
149
|
+
assertEquals(atStop, target.progress.size)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@Test
|
|
153
|
+
fun `a track change arriving after stop pushes nothing`() {
|
|
154
|
+
sync.start()
|
|
155
|
+
idle()
|
|
156
|
+
val atStop = target.metadata.size
|
|
157
|
+
|
|
158
|
+
offLooper { sync.stop() }
|
|
159
|
+
player.emitTrackChange(trackMediaItem("Monkey Wrench", "Foo Fighters", "TCATS"))
|
|
160
|
+
idle()
|
|
161
|
+
|
|
162
|
+
assertEquals(atStop, target.metadata.size)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Test
|
|
166
|
+
fun `artwork from a local file reaches the receiver while running`() {
|
|
167
|
+
player.emitTrackChange(
|
|
168
|
+
trackMediaItem("Everlong", "Foo Fighters", "TCATS", artworkUri = artworkFile())
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
sync.start()
|
|
172
|
+
|
|
173
|
+
assertTrue("artwork never arrived", awaitTrue { target.artwork.isNotEmpty() })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Test
|
|
177
|
+
fun `artwork is not fetched after stop`() {
|
|
178
|
+
sync.start()
|
|
179
|
+
idle()
|
|
180
|
+
|
|
181
|
+
// Fired while listener removal is still queued behind the off-looper
|
|
182
|
+
// stop, so the fetch is reached on a spent sync.
|
|
183
|
+
offLooper { sync.stop() }
|
|
184
|
+
player.emitTrackChange(
|
|
185
|
+
trackMediaItem("Monkey Wrench", "Foo Fighters", "TCATS", artworkUri = artworkFile())
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
// Real elapsed time, not looper time: the fetch runs on Dispatchers.IO,
|
|
189
|
+
// so advancing the looper clock proves nothing about whether it ran.
|
|
190
|
+
assertFalse(
|
|
191
|
+
"artwork reached the receiver after stop",
|
|
192
|
+
awaitTrue(2_000) { target.artwork.isNotEmpty() },
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@Test
|
|
197
|
+
fun `start after stop does nothing`() {
|
|
198
|
+
sync.start()
|
|
199
|
+
idle()
|
|
200
|
+
offLooper { sync.stop() }
|
|
201
|
+
idle()
|
|
202
|
+
val atStop = target.progress.size + target.metadata.size
|
|
203
|
+
|
|
204
|
+
sync.start()
|
|
205
|
+
idle()
|
|
206
|
+
idleFor(30_000)
|
|
207
|
+
|
|
208
|
+
assertEquals(atStop, target.progress.size + target.metadata.size)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private fun artworkFile(): Uri {
|
|
212
|
+
val file = File(context().cacheDir, "artwork.png")
|
|
213
|
+
if (!file.exists()) file.writeBytes(PNG_1X1)
|
|
214
|
+
return Uri.fromFile(file)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private class RecordingTarget : MetadataSyncTarget {
|
|
218
|
+
// The artwork fetch runs on Dispatchers.IO while the test thread reads
|
|
219
|
+
// these, so plain ArrayLists would be shared unsynchronised.
|
|
220
|
+
val metadata = CopyOnWriteArrayList<Triple<String?, String?, String?>>()
|
|
221
|
+
val progress = CopyOnWriteArrayList<Pair<Int, Int>>()
|
|
222
|
+
val artwork = CopyOnWriteArrayList<Int>()
|
|
223
|
+
|
|
224
|
+
override fun setMetadata(title: String?, artist: String?, album: String?) {
|
|
225
|
+
metadata += Triple(title, artist, album)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
override fun setProgress(elapsedMs: Int, durationMs: Int) {
|
|
229
|
+
progress += elapsedMs to durationMs
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
override fun setArtwork(imageData: ByteArray, contentType: String) {
|
|
233
|
+
artwork += imageData.size
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Player double exposing exactly what the sync reads: the current item's
|
|
239
|
+
* metadata, position, duration and whether playback is running. Each item
|
|
240
|
+
* carries its own period uid, so replacing it is delivered as a media-item
|
|
241
|
+
* transition rather than a timeline change.
|
|
242
|
+
*/
|
|
243
|
+
private class FakePlayer(looper: Looper) : SimpleBasePlayer(looper) {
|
|
244
|
+
private var item: MediaItem =
|
|
245
|
+
trackMediaItem("Everlong", "Foo Fighters", "The Colour and the Shape")
|
|
246
|
+
private var playing = true
|
|
247
|
+
|
|
248
|
+
fun emitTrackChange(next: MediaItem) {
|
|
249
|
+
item = next
|
|
250
|
+
invalidateState()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fun emitPlayWhenReady(value: Boolean) {
|
|
254
|
+
playing = value
|
|
255
|
+
invalidateState()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
override fun getState(): State =
|
|
259
|
+
State.Builder()
|
|
260
|
+
.setAvailableCommands(Player.Commands.Builder().addAllCommands().build())
|
|
261
|
+
.setPlaybackState(Player.STATE_READY)
|
|
262
|
+
.setPlayWhenReady(playing, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
|
|
263
|
+
.setPlaylist(
|
|
264
|
+
ImmutableList.of(
|
|
265
|
+
MediaItemData.Builder(item)
|
|
266
|
+
.setMediaItem(item)
|
|
267
|
+
.setDurationUs(300_000_000L)
|
|
268
|
+
.build()
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
.setCurrentMediaItemIndex(0)
|
|
272
|
+
.setContentPositionMs(1_000L)
|
|
273
|
+
.build()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private companion object {
|
|
277
|
+
/**
|
|
278
|
+
* A real 1x1 PNG — Robolectric decodes artwork through ImageIO, so a
|
|
279
|
+
* stub header fails to decode and the fetch reports no artwork.
|
|
280
|
+
*/
|
|
281
|
+
val PNG_1X1: ByteArray = Base64.getDecoder().decode(
|
|
282
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
fun trackMediaItem(
|
|
286
|
+
title: String,
|
|
287
|
+
artist: String,
|
|
288
|
+
album: String,
|
|
289
|
+
artworkUri: Uri? = null,
|
|
290
|
+
): MediaItem =
|
|
291
|
+
MediaItem.Builder()
|
|
292
|
+
.setUri("https://example.invalid/${title.replace(' ', '-')}.mp3")
|
|
293
|
+
.setMediaMetadata(
|
|
294
|
+
MediaMetadata.Builder()
|
|
295
|
+
.setTitle(title)
|
|
296
|
+
.setArtist(artist)
|
|
297
|
+
.setAlbumTitle(album)
|
|
298
|
+
.apply { artworkUri?.let { setArtworkUri(it) } }
|
|
299
|
+
.build()
|
|
300
|
+
)
|
|
301
|
+
.build()
|
|
302
|
+
}
|
|
303
|
+
}
|