react-native-queue-player 1.2.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/consumer-rules.pro +1 -1
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +3 -19
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +8 -69
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +2 -2
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +672 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +229 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaResponseCheck.swift +122 -0
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MediaResponseCheckTests.swift +128 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +418 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +441 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +982 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +951 -0
- package/ios/TrackPlayer+Recovery.swift +213 -0
- package/ios/TrackPlayer+Skip.swift +465 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +272 -0
- package/ios/TrackPlayer+Window.swift +227 -0
- package/ios/TrackPlayer.swift +331 -4619
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
import androidx.media3.common.PlaybackException
|
|
5
|
+
import androidx.media3.common.util.UnstableApi
|
|
6
|
+
import androidx.media3.datasource.DataSource
|
|
7
|
+
import androidx.media3.datasource.DataSpec
|
|
8
|
+
import androidx.media3.datasource.HttpDataSource
|
|
9
|
+
import androidx.media3.datasource.TransferListener
|
|
10
|
+
import org.junit.Assert.assertArrayEquals
|
|
11
|
+
import org.junit.Assert.assertEquals
|
|
12
|
+
import org.junit.Assert.assertTrue
|
|
13
|
+
import org.junit.Assert.fail
|
|
14
|
+
import org.junit.Test
|
|
15
|
+
import org.junit.runner.RunWith
|
|
16
|
+
import org.robolectric.RobolectricTestRunner
|
|
17
|
+
import org.robolectric.annotation.Config
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A response that is a document rather than media fails before any of its bytes
|
|
21
|
+
* are returned, so neither cache write path — the prefetcher's `CacheWriter` nor
|
|
22
|
+
* the playback read path's own sink — ever sees them.
|
|
23
|
+
*
|
|
24
|
+
* `android.net.Uri` is a framework type, so this runs under Robolectric like
|
|
25
|
+
* every other test here that parses one.
|
|
26
|
+
*/
|
|
27
|
+
@RunWith(RobolectricTestRunner::class)
|
|
28
|
+
@Config(sdk = [34])
|
|
29
|
+
@UnstableApi
|
|
30
|
+
class MediaValidatingDataSourceTest {
|
|
31
|
+
|
|
32
|
+
/** An HTTP source that answers with fixed headers and a fixed body. */
|
|
33
|
+
private class FakeHttpSource(
|
|
34
|
+
private val body: ByteArray,
|
|
35
|
+
private val contentType: String?,
|
|
36
|
+
/** Bytes to hand back per read, so a body arriving in dribs is covered. */
|
|
37
|
+
private val chunk: Int = Int.MAX_VALUE
|
|
38
|
+
) : HttpDataSource {
|
|
39
|
+
private var position = 0
|
|
40
|
+
var readCount = 0
|
|
41
|
+
private set
|
|
42
|
+
|
|
43
|
+
override fun open(dataSpec: DataSpec): Long {
|
|
44
|
+
position = dataSpec.position.toInt()
|
|
45
|
+
return (body.size - position).toLong()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
|
|
49
|
+
readCount += 1
|
|
50
|
+
if (position >= body.size) return -1
|
|
51
|
+
val take = minOf(length, chunk, body.size - position)
|
|
52
|
+
body.copyInto(buffer, offset, position, position + take)
|
|
53
|
+
position += take
|
|
54
|
+
return take
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun getUri(): Uri = Uri.parse("https://example.com/track")
|
|
58
|
+
override fun close() {}
|
|
59
|
+
override fun addTransferListener(transferListener: TransferListener) {}
|
|
60
|
+
override fun getResponseHeaders(): Map<String, List<String>> =
|
|
61
|
+
if (contentType == null) emptyMap() else mapOf("Content-Type" to listOf(contentType))
|
|
62
|
+
|
|
63
|
+
override fun setRequestProperty(name: String, value: String) {}
|
|
64
|
+
override fun clearRequestProperty(name: String) {}
|
|
65
|
+
override fun clearAllRequestProperties() {}
|
|
66
|
+
override fun getResponseCode(): Int = 200
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A non-HTTP source, which carries no declared type and is passed through. */
|
|
70
|
+
private class FakeLocalSource(private val body: ByteArray) : DataSource {
|
|
71
|
+
private var position = 0
|
|
72
|
+
override fun open(dataSpec: DataSpec): Long = body.size.toLong()
|
|
73
|
+
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
|
|
74
|
+
if (position >= body.size) return -1
|
|
75
|
+
val take = minOf(length, body.size - position)
|
|
76
|
+
body.copyInto(buffer, offset, position, position + take)
|
|
77
|
+
position += take
|
|
78
|
+
return take
|
|
79
|
+
}
|
|
80
|
+
override fun getUri(): Uri = Uri.parse("file:///track.mp3")
|
|
81
|
+
override fun close() {}
|
|
82
|
+
override fun addTransferListener(transferListener: TransferListener) {}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private val xmlEnvelope =
|
|
86
|
+
"""<api-response status="failed"><error code="70"/></api-response>""".toByteArray()
|
|
87
|
+
private val mp3 = byteArrayOf(0x49, 0x44, 0x33, 0x04, 0, 0, 0, 0) + ByteArray(120)
|
|
88
|
+
|
|
89
|
+
private fun openAndRead(
|
|
90
|
+
source: DataSource,
|
|
91
|
+
position: Long = 0L,
|
|
92
|
+
uri: String = "https://example.com/track"
|
|
93
|
+
): ByteArray {
|
|
94
|
+
source.open(DataSpec.Builder().setUri(uri).setPosition(position).build())
|
|
95
|
+
val out = ArrayList<Byte>()
|
|
96
|
+
val buffer = ByteArray(32)
|
|
97
|
+
while (true) {
|
|
98
|
+
val read = source.read(buffer, 0, buffer.size)
|
|
99
|
+
if (read == -1) break
|
|
100
|
+
for (i in 0 until read) out.add(buffer[i])
|
|
101
|
+
}
|
|
102
|
+
return out.toByteArray()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test
|
|
106
|
+
fun aDeclaredDocumentFailsBeforeAnythingIsRead() {
|
|
107
|
+
val upstream = FakeHttpSource(xmlEnvelope, "application/xml")
|
|
108
|
+
val source = MediaValidatingDataSource(upstream)
|
|
109
|
+
try {
|
|
110
|
+
source.open(DataSpec(Uri.parse("https://example.com/track")))
|
|
111
|
+
fail("a declared document must not open")
|
|
112
|
+
} catch (e: HttpDataSource.HttpDataSourceException) {
|
|
113
|
+
assertEquals(PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, e.reason)
|
|
114
|
+
}
|
|
115
|
+
assertEquals("nothing may be read from a refused response", 0, upstream.readCount)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test
|
|
119
|
+
fun anUndeclaredMarkupBodyFailsOnTheFirstRead() {
|
|
120
|
+
val source = MediaValidatingDataSource(FakeHttpSource(xmlEnvelope, null))
|
|
121
|
+
source.open(DataSpec(Uri.parse("https://example.com/track")))
|
|
122
|
+
try {
|
|
123
|
+
source.read(ByteArray(32), 0, 32)
|
|
124
|
+
fail("a markup body must not be returned")
|
|
125
|
+
} catch (e: HttpDataSource.HttpDataSourceException) {
|
|
126
|
+
assertEquals(PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, e.reason)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Test
|
|
131
|
+
fun plainTextWithNoContainerSignatureFailsOnTheFirstRead() {
|
|
132
|
+
val source = MediaValidatingDataSource(FakeHttpSource("not found".toByteArray(), "text/plain"))
|
|
133
|
+
source.open(DataSpec(Uri.parse("https://example.com/track")))
|
|
134
|
+
try {
|
|
135
|
+
source.read(ByteArray(32), 0, 32)
|
|
136
|
+
fail("text with no container signature must not be returned")
|
|
137
|
+
} catch (e: HttpDataSource.HttpDataSourceException) {
|
|
138
|
+
assertEquals(PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, e.reason)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@Test
|
|
143
|
+
fun mediaIsPassedThroughWhole() {
|
|
144
|
+
val source = MediaValidatingDataSource(FakeHttpSource(mp3, "audio/mpeg"))
|
|
145
|
+
assertArrayEquals("every byte reaches the caller unchanged", mp3, openAndRead(source))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Test
|
|
149
|
+
fun mediaArrivingInSmallChunksIsStillPassedThroughWhole() {
|
|
150
|
+
val source = MediaValidatingDataSource(FakeHttpSource(mp3, "audio/mpeg", chunk = 3))
|
|
151
|
+
assertArrayEquals(mp3, openAndRead(source))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@Test
|
|
155
|
+
fun aRangedRequestIsJudgedOnItsDeclaredTypeOnly() {
|
|
156
|
+
// The served window opens with `<`, so the leading-byte rule would refuse it
|
|
157
|
+
// if it were applied. A body read from an offset says nothing about the
|
|
158
|
+
// resource's first bytes, so it is not.
|
|
159
|
+
val markupAtOffset = "....<api-response/>".toByteArray()
|
|
160
|
+
val ranged = MediaValidatingDataSource(
|
|
161
|
+
FakeHttpSource(markupAtOffset, "application/octet-stream")
|
|
162
|
+
)
|
|
163
|
+
val read = openAndRead(ranged, position = 4L)
|
|
164
|
+
assertTrue("a partial body is not judged by its first byte", read.isNotEmpty())
|
|
165
|
+
|
|
166
|
+
// The same bytes as the whole resource are refused — which is what shows the
|
|
167
|
+
// acceptance above turns on the range and not on the bytes.
|
|
168
|
+
val whole = MediaValidatingDataSource(
|
|
169
|
+
FakeHttpSource("<api-response/>".toByteArray(), "application/octet-stream")
|
|
170
|
+
)
|
|
171
|
+
whole.open(DataSpec(Uri.parse("https://example.com/track")))
|
|
172
|
+
try {
|
|
173
|
+
whole.read(ByteArray(32), 0, 32)
|
|
174
|
+
fail("the same markup read from the start of the resource must be refused")
|
|
175
|
+
} catch (e: HttpDataSource.HttpDataSourceException) {
|
|
176
|
+
assertEquals(PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE, e.reason)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@Test
|
|
181
|
+
fun aLocalSourceIsPassedThroughUnjudged() {
|
|
182
|
+
val source = MediaValidatingDataSource(FakeLocalSource(xmlEnvelope))
|
|
183
|
+
assertArrayEquals(
|
|
184
|
+
"a file:// source is not fetched from a server and is not the case this guards",
|
|
185
|
+
xmlEnvelope,
|
|
186
|
+
openAndRead(source, uri = "file:///track.mp3")
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
}
|
package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt
CHANGED
|
@@ -4,7 +4,10 @@ import androidx.media3.common.Format
|
|
|
4
4
|
import androidx.media3.common.MimeTypes
|
|
5
5
|
import androidx.media3.common.util.UnstableApi
|
|
6
6
|
import org.junit.Assert.assertEquals
|
|
7
|
+
import org.junit.Assert.assertNull
|
|
8
|
+
import org.junit.Rule
|
|
7
9
|
import org.junit.Test
|
|
10
|
+
import org.junit.rules.TemporaryFolder
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Pure-mapping coverage for [NowPlayingFormatExtractor]. These
|
|
@@ -14,6 +17,48 @@ import org.junit.Test
|
|
|
14
17
|
@UnstableApi
|
|
15
18
|
class NowPlayingFormatExtractorTest {
|
|
16
19
|
|
|
20
|
+
@get:Rule
|
|
21
|
+
val tempFolder = TemporaryFolder()
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* FLAC declares no bitrate, so Media3 reports NO_VALUE for it on local files
|
|
25
|
+
* as well as remote. For a file on disk the average is arithmetic — encoded
|
|
26
|
+
* bytes over playing time — which is the same derivation iOS uses.
|
|
27
|
+
*/
|
|
28
|
+
@Test
|
|
29
|
+
fun averageBitrateIsBytesOverPlayingTime() {
|
|
30
|
+
val file = tempFolder.newFile("track.flac")
|
|
31
|
+
file.writeBytes(ByteArray(1_000_000))
|
|
32
|
+
|
|
33
|
+
// 1,000,000 bytes over 40 s = 8,000,000 bits / 40 = 200 kbps.
|
|
34
|
+
assertEquals(
|
|
35
|
+
200_000.0,
|
|
36
|
+
NowPlayingFormatExtractor.averageBitrateOf(file.absolutePath, 40.0)!!,
|
|
37
|
+
0.001
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Every input that would make the answer invented rather than measured
|
|
43
|
+
* yields null: the player has not resolved a duration yet, the duration is
|
|
44
|
+
* nonsense, the path is absent, or the file has no bytes.
|
|
45
|
+
*/
|
|
46
|
+
@Test
|
|
47
|
+
fun averageBitrateIsNullWhenItCannotBeMeasured() {
|
|
48
|
+
val file = tempFolder.newFile("track.flac")
|
|
49
|
+
file.writeBytes(ByteArray(1_000))
|
|
50
|
+
|
|
51
|
+
assertNull(NowPlayingFormatExtractor.averageBitrateOf(file.absolutePath, null))
|
|
52
|
+
assertNull(NowPlayingFormatExtractor.averageBitrateOf(file.absolutePath, 0.0))
|
|
53
|
+
assertNull(NowPlayingFormatExtractor.averageBitrateOf(file.absolutePath, -5.0))
|
|
54
|
+
assertNull(NowPlayingFormatExtractor.averageBitrateOf(null, 40.0))
|
|
55
|
+
assertNull(
|
|
56
|
+
NowPlayingFormatExtractor.averageBitrateOf(
|
|
57
|
+
tempFolder.newFile("empty.flac").absolutePath, 40.0))
|
|
58
|
+
assertNull(
|
|
59
|
+
NowPlayingFormatExtractor.averageBitrateOf("/nonexistent/nope.flac", 40.0))
|
|
60
|
+
}
|
|
61
|
+
|
|
17
62
|
@Test
|
|
18
63
|
fun mapCodecCoversAACAliases() {
|
|
19
64
|
assertEquals(AudioCodec.AAC, NowPlayingFormatExtractor.mapCodec(MimeTypes.AUDIO_AAC))
|
package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt
CHANGED
|
@@ -60,7 +60,7 @@ class PitchAwareAudioProcessorChainTest {
|
|
|
60
60
|
|
|
61
61
|
@Test
|
|
62
62
|
fun `signalsmith stays active on the float path even when not stretching`() {
|
|
63
|
-
//
|
|
63
|
+
// Media3's pipeline is static: Signalsmith must remain in the
|
|
64
64
|
// active set regardless of mode/speed so it can stretch when music kicks in.
|
|
65
65
|
val chain = newConfiguredChain()
|
|
66
66
|
chain.setMode(PitchCorrectionMode.VOICE)
|
package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt
CHANGED
|
@@ -7,8 +7,16 @@ import androidx.media3.session.LibraryResult
|
|
|
7
7
|
import androidx.media3.session.MediaLibraryService
|
|
8
8
|
import androidx.media3.session.MediaSession
|
|
9
9
|
import androidx.test.core.app.ApplicationProvider
|
|
10
|
+
import java.util.concurrent.CountDownLatch
|
|
11
|
+
import java.util.concurrent.TimeUnit
|
|
12
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
13
|
+
import kotlinx.coroutines.Dispatchers
|
|
14
|
+
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
15
|
+
import kotlinx.coroutines.test.resetMain
|
|
16
|
+
import kotlinx.coroutines.test.setMain
|
|
10
17
|
import org.junit.After
|
|
11
18
|
import org.junit.Assert.assertEquals
|
|
19
|
+
import org.junit.Assert.assertFalse
|
|
12
20
|
import org.junit.Assert.assertNull
|
|
13
21
|
import org.junit.Assert.assertTrue
|
|
14
22
|
import org.junit.Before
|
|
@@ -167,4 +175,43 @@ class PlaybackServiceCallbackSearchTest {
|
|
|
167
175
|
result.params
|
|
168
176
|
)
|
|
169
177
|
}
|
|
178
|
+
|
|
179
|
+
// --- dispatchSearchToJs
|
|
180
|
+
|
|
181
|
+
// `Dispatchers.setMain` is the only way to see which dispatcher the notify
|
|
182
|
+
// lands on; `notifySearchResultChanged` has no other observable effect with
|
|
183
|
+
// no controller connected.
|
|
184
|
+
@OptIn(ExperimentalCoroutinesApi::class)
|
|
185
|
+
@Test
|
|
186
|
+
fun `dispatchSearchToJs caches the answer then notifies on main`() {
|
|
187
|
+
// A Nitro Promise is JNI-constructed and cannot exist on the JVM, so
|
|
188
|
+
// the only JS answer reachable here is a throwing handler; that path
|
|
189
|
+
// still writes the cache and still notifies, which is the contract a
|
|
190
|
+
// connected controller depends on to stop waiting.
|
|
191
|
+
val mainDispatches = AtomicInteger(0)
|
|
192
|
+
val dispatched = CountDownLatch(1)
|
|
193
|
+
val recording = object : kotlinx.coroutines.CoroutineDispatcher() {
|
|
194
|
+
override fun dispatch(context: kotlin.coroutines.CoroutineContext, block: Runnable) {
|
|
195
|
+
mainDispatches.incrementAndGet()
|
|
196
|
+
dispatched.countDown()
|
|
197
|
+
android.os.Handler(android.os.Looper.getMainLooper()).post(block)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
Dispatchers.setMain(recording)
|
|
201
|
+
try {
|
|
202
|
+
service.browseCallbacks.registerOnSearchRequest { _ ->
|
|
203
|
+
throw IllegalStateException("no Nitro runtime under Robolectric")
|
|
204
|
+
}
|
|
205
|
+
assertFalse(service.searchResultCache.hasResults("rock"))
|
|
206
|
+
|
|
207
|
+
service.dispatchSearchToJs("rock")
|
|
208
|
+
|
|
209
|
+
assertTrue("the notify reached a dispatcher", dispatched.await(2, TimeUnit.SECONDS))
|
|
210
|
+
org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle()
|
|
211
|
+
assertEquals("notify is dispatched to main", 1, mainDispatches.get())
|
|
212
|
+
assertEquals(emptyList<BrowseItem>(), service.searchResultCache.getResults("rock"))
|
|
213
|
+
} finally {
|
|
214
|
+
Dispatchers.resetMain()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
170
217
|
}
|
|
@@ -259,19 +259,6 @@ class PlaybackServiceCallbackTest {
|
|
|
259
259
|
assertTrue(commands.contains(Player.COMMAND_SET_SPEED_AND_PITCH))
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
@Test
|
|
263
|
-
fun `refreshAvailableCommands is a no-op when no handler is registered`() {
|
|
264
|
-
// No handler, no controllers — exercising the early return.
|
|
265
|
-
binder.refreshAvailableCommands()
|
|
266
|
-
// Re-issue with a handler but no connected controllers — also a no-op.
|
|
267
|
-
binder.setCommandHandler(RecordingHandler())
|
|
268
|
-
binder.refreshAvailableCommands()
|
|
269
|
-
// The per-controller iteration body (session.setAvailableCommands)
|
|
270
|
-
// requires a live MediaController that has completed handshake,
|
|
271
|
-
// which is not reachable from Robolectric without standing up a
|
|
272
|
-
// full MediaController.Builder.buildAsync() flow.
|
|
273
|
-
}
|
|
274
|
-
|
|
275
262
|
// --- TrackPlayer-side handler implementation
|
|
276
263
|
|
|
277
264
|
@Test
|
|
@@ -291,8 +278,8 @@ class PlaybackServiceCallbackTest {
|
|
|
291
278
|
val player = newConfiguredPlayer()
|
|
292
279
|
try {
|
|
293
280
|
player.tracks = listOf(
|
|
294
|
-
TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
|
|
295
|
-
TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
|
|
281
|
+
TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
|
|
282
|
+
TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
|
|
296
283
|
)
|
|
297
284
|
player.currentTrackIndex = 0
|
|
298
285
|
|
|
@@ -326,8 +313,8 @@ class PlaybackServiceCallbackTest {
|
|
|
326
313
|
|
|
327
314
|
// Two-track queue at index 0 — skip-next now allowed.
|
|
328
315
|
player.tracks = listOf(
|
|
329
|
-
TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
|
|
330
|
-
TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
|
|
316
|
+
TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
|
|
317
|
+
TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
|
|
331
318
|
)
|
|
332
319
|
player.currentTrackIndex = 0
|
|
333
320
|
player.recomputeCapabilities()
|
|
@@ -352,8 +339,8 @@ class PlaybackServiceCallbackTest {
|
|
|
352
339
|
val player = newConfiguredPlayer()
|
|
353
340
|
try {
|
|
354
341
|
player.tracks = listOf(
|
|
355
|
-
TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
|
|
356
|
-
TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
|
|
342
|
+
TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
|
|
343
|
+
TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
|
|
357
344
|
)
|
|
358
345
|
player.currentTrackIndex = 1
|
|
359
346
|
|
|
@@ -377,9 +364,9 @@ class PlaybackServiceCallbackTest {
|
|
|
377
364
|
val player = newConfiguredPlayer()
|
|
378
365
|
try {
|
|
379
366
|
player.tracks = listOf(
|
|
380
|
-
TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
|
|
381
|
-
TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
|
|
382
|
-
TrackItem("c", "https://example.com/c.m4a", null, null, null, null, null, null),
|
|
367
|
+
TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
|
|
368
|
+
TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
|
|
369
|
+
TrackItem("c", "https://example.com/c.m4a", null, null, null, null, null, null, null, null, null, null),
|
|
383
370
|
)
|
|
384
371
|
player.currentTrackIndex = 0
|
|
385
372
|
|
|
@@ -404,8 +391,8 @@ class PlaybackServiceCallbackTest {
|
|
|
404
391
|
val player = newConfiguredPlayer()
|
|
405
392
|
try {
|
|
406
393
|
player.tracks = listOf(
|
|
407
|
-
TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
|
|
408
|
-
TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
|
|
394
|
+
TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
|
|
395
|
+
TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
|
|
409
396
|
)
|
|
410
397
|
player.currentTrackIndex = 1
|
|
411
398
|
|
|
@@ -426,8 +413,8 @@ class PlaybackServiceCallbackTest {
|
|
|
426
413
|
val player = newConfiguredPlayer()
|
|
427
414
|
try {
|
|
428
415
|
player.tracks = listOf(
|
|
429
|
-
TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null),
|
|
430
|
-
TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null),
|
|
416
|
+
TrackItem("a", "https://example.com/a.m4a", null, null, null, null, null, null, null, null, null, null),
|
|
417
|
+
TrackItem("b", "https://example.com/b.m4a", null, null, null, null, null, null, null, null, null, null),
|
|
431
418
|
)
|
|
432
419
|
player.currentTrackIndex = 1
|
|
433
420
|
|
|
@@ -444,6 +431,35 @@ class PlaybackServiceCallbackTest {
|
|
|
444
431
|
|
|
445
432
|
// --- Voice / browse-leaf routing through onAddMediaItems / onSetMediaItems
|
|
446
433
|
|
|
434
|
+
@Test
|
|
435
|
+
fun `onAddMediaItems asks for append and onSetMediaItems does not`() {
|
|
436
|
+
val fakeEnv = RecordingEnvironment(service)
|
|
437
|
+
val routingCallback = PlaybackServiceCallback(fakeEnv)
|
|
438
|
+
fakeEnv.idResolution = listOf(
|
|
439
|
+
MediaItem.Builder().setMediaId("resolved")
|
|
440
|
+
.setUri("https://example.com/resolved.m4a".toUri()).build()
|
|
441
|
+
)
|
|
442
|
+
fun idOnlyItem() = MediaItem.Builder().setMediaId("album:1").build()
|
|
443
|
+
|
|
444
|
+
routingCallback.onAddMediaItems(
|
|
445
|
+
session, controllerInfo, mutableListOf(idOnlyItem())
|
|
446
|
+
).get()
|
|
447
|
+
// "Queue up X" grows the queue; Media3 appends what the resolution
|
|
448
|
+
// returns, so the lib's own queue has to grow by the same amount rather
|
|
449
|
+
// than being replaced by the new tracks alone.
|
|
450
|
+
assertEquals(listOf(true), fakeEnv.idAppendFlags)
|
|
451
|
+
|
|
452
|
+
fakeEnv.idAppendFlags.clear()
|
|
453
|
+
routingCallback.onSetMediaItems(
|
|
454
|
+
session, controllerInfo, mutableListOf(idOnlyItem()), 0, 0L
|
|
455
|
+
).get()
|
|
456
|
+
// A browse-leaf tap is a genuine replace: it takes the no-sync route,
|
|
457
|
+
// where the JS handler's own `setQueue` is the only writer, so no
|
|
458
|
+
// append-flagged request reaches the environment at all.
|
|
459
|
+
assertEquals(listOf("album:1"), fakeEnv.idRequestsNoSync)
|
|
460
|
+
assertEquals(emptyList<Boolean>(), fakeEnv.idAppendFlags)
|
|
461
|
+
}
|
|
462
|
+
|
|
447
463
|
@Test
|
|
448
464
|
fun `onAddMediaItems with searchQuery dispatches to onPlayFromSearchRequest`() {
|
|
449
465
|
val fakeEnv = RecordingEnvironment(service)
|
|
@@ -783,7 +799,8 @@ class PlaybackServiceCallbackTest {
|
|
|
783
799
|
networkTimeoutMs = null,
|
|
784
800
|
audioContentType = null,
|
|
785
801
|
audioCategoryOptions = null,
|
|
786
|
-
|
|
802
|
+
voiceVocabularyDonationEnabled = null,
|
|
803
|
+
visualizationEnabled = null,
|
|
787
804
|
clampSeekToBuffered = null,
|
|
788
805
|
lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null, skipToPreviousBehavior = null,
|
|
789
806
|
),
|
|
@@ -878,19 +895,26 @@ class PlaybackServiceCallbackTest {
|
|
|
878
895
|
service.playerCommandsFor(capability)
|
|
879
896
|
|
|
880
897
|
override fun onPlayFromSearchRequest(
|
|
881
|
-
request: MediaSearchRequest
|
|
898
|
+
request: MediaSearchRequest,
|
|
899
|
+
append: Boolean
|
|
882
900
|
): ListenableFuture<List<MediaItem>> {
|
|
883
901
|
searchRequests += request
|
|
902
|
+
searchAppendFlags += append
|
|
884
903
|
return Futures.immediateFuture(searchResolution)
|
|
885
904
|
}
|
|
886
905
|
|
|
887
906
|
override fun onPlayFromIdRequest(
|
|
888
|
-
mediaId: String
|
|
907
|
+
mediaId: String,
|
|
908
|
+
append: Boolean
|
|
889
909
|
): ListenableFuture<List<MediaItem>> {
|
|
890
910
|
idRequests += mediaId
|
|
911
|
+
idAppendFlags += append
|
|
891
912
|
return Futures.immediateFuture(idResolution)
|
|
892
913
|
}
|
|
893
914
|
|
|
915
|
+
val searchAppendFlags = mutableListOf<Boolean>()
|
|
916
|
+
val idAppendFlags = mutableListOf<Boolean>()
|
|
917
|
+
|
|
894
918
|
override fun onPlayFromIdRequestNoSync(mediaId: String) {
|
|
895
919
|
idRequestsNoSync += mediaId
|
|
896
920
|
}
|
|
@@ -49,19 +49,19 @@ class PlaybackServiceLifecycleTest {
|
|
|
49
49
|
val controller = Robolectric.buildService(PlaybackService::class.java).create()
|
|
50
50
|
try {
|
|
51
51
|
val binder = bindLocal(controller.get())
|
|
52
|
-
assertNotNull("
|
|
52
|
+
assertNotNull("the engine's player is initialised in onCreate", binder.engine.mediaSessionPlayer)
|
|
53
53
|
assertNotNull("binder.session is initialised in onCreate", binder.session)
|
|
54
54
|
// The session binds to the stable SessionCommandForwardingPlayer
|
|
55
55
|
// wrapper for its lifetime; the wrapper forwards to the engine's
|
|
56
|
-
// ExoPlayer
|
|
56
|
+
// own ExoPlayer.
|
|
57
57
|
val sessionPlayer = binder.session.player
|
|
58
58
|
assertTrue(
|
|
59
59
|
"binder.session.player is the stable forwarding wrapper",
|
|
60
60
|
sessionPlayer is SessionCommandForwardingPlayer,
|
|
61
61
|
)
|
|
62
62
|
assertSame(
|
|
63
|
-
"the wrapper forwards to the ExoPlayer instance
|
|
64
|
-
binder.
|
|
63
|
+
"the wrapper forwards to the engine's ExoPlayer instance",
|
|
64
|
+
binder.engine.mediaSessionPlayer,
|
|
65
65
|
(sessionPlayer as SessionCommandForwardingPlayer).wrappedPlayer,
|
|
66
66
|
)
|
|
67
67
|
assertEquals(
|
|
@@ -173,8 +173,8 @@ class PlaybackServiceLifecycleTest {
|
|
|
173
173
|
val binder = bindLocal(service)
|
|
174
174
|
// A loaded + play-ready session: the previous behaviour kept the service
|
|
175
175
|
// alive here. Swipe-away is now an unconditional kill.
|
|
176
|
-
binder.
|
|
177
|
-
binder.
|
|
176
|
+
binder.engine.mediaSessionPlayer.addMediaItem(MediaItem.fromUri("http://example.com/a.mp3"))
|
|
177
|
+
binder.engine.mediaSessionPlayer.playWhenReady = true
|
|
178
178
|
shadowOf(Looper.getMainLooper()).idle()
|
|
179
179
|
|
|
180
180
|
service.onTaskRemoved(null)
|
|
@@ -183,7 +183,7 @@ class PlaybackServiceLifecycleTest {
|
|
|
183
183
|
assertEquals(
|
|
184
184
|
"the player is stopped (idle) on swipe-away",
|
|
185
185
|
Player.STATE_IDLE,
|
|
186
|
-
binder.
|
|
186
|
+
binder.engine.mediaSessionPlayer.playbackState,
|
|
187
187
|
)
|
|
188
188
|
assertTrue(
|
|
189
189
|
"the service stops even with a queued, play-ready session",
|
|
@@ -198,7 +198,7 @@ class PlaybackServiceLifecycleTest {
|
|
|
198
198
|
fun `onDestroy releases the player and clears the session`() {
|
|
199
199
|
val controller = Robolectric.buildService(PlaybackService::class.java).create()
|
|
200
200
|
val binder = bindLocal(controller.get())
|
|
201
|
-
val player = binder.
|
|
201
|
+
val player = binder.engine.mediaSessionPlayer
|
|
202
202
|
val session: MediaSession = binder.session
|
|
203
203
|
assertFalse("player is live before destroy", player.isReleased)
|
|
204
204
|
|
|
@@ -134,7 +134,7 @@ class QueueSkipArithmeticTest {
|
|
|
134
134
|
@Test
|
|
135
135
|
fun `computePrevious OFF — at index 0 returns null (no wrap)`() {
|
|
136
136
|
// The "tap Previous at track 0 restarts current" UX behaviour
|
|
137
|
-
// is applied by
|
|
137
|
+
// is applied by `skipToPrevious`, not here.
|
|
138
138
|
assertNull(
|
|
139
139
|
QueueSkipArithmetic.computePrevious(5, 0, QueueSkipRepeatMode.OFF)
|
|
140
140
|
)
|
|
@@ -443,4 +443,36 @@ class QueueSkipArithmeticTest {
|
|
|
443
443
|
assertFalse(QueueSkipArithmetic.canSkipPrevious(3, 0, QueueSkipRepeatMode.OFF, restartEnabled = false))
|
|
444
444
|
assertTrue(QueueSkipArithmetic.canSkipPrevious(3, 1, QueueSkipRepeatMode.OFF, restartEnabled = false))
|
|
445
445
|
}
|
|
446
|
+
|
|
447
|
+
@Test
|
|
448
|
+
fun `canSkipPrevious restartEnabled — nothing playing is false in any mode`() {
|
|
449
|
+
// Nothing is loaded, so there is no track to restart and no previous
|
|
450
|
+
// track to reach: resolveSkipToPrevious returns NoOp for this state.
|
|
451
|
+
for (mode in QueueSkipRepeatMode.entries) {
|
|
452
|
+
assertFalse(QueueSkipArithmetic.canSkipPrevious(5, -1, mode, restartEnabled = true))
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
@Test
|
|
457
|
+
fun `canSkipPrevious agrees with resolveSkipToPrevious`() {
|
|
458
|
+
// canSkipPrevious is the enabled-state of the same decision
|
|
459
|
+
// resolveSkipToPrevious makes; the two must never disagree.
|
|
460
|
+
for (count in listOf(0, 1, 3)) {
|
|
461
|
+
for (index in listOf(-1, 0, 1, 2)) {
|
|
462
|
+
for (mode in QueueSkipRepeatMode.entries) {
|
|
463
|
+
for (restart in listOf(false, true)) {
|
|
464
|
+
val can = QueueSkipArithmetic.canSkipPrevious(count, index, mode, restart)
|
|
465
|
+
val resolved = QueueSkipArithmetic.resolveSkipToPrevious(
|
|
466
|
+
count, index, mode, restart, positionMs = 0
|
|
467
|
+
)
|
|
468
|
+
assertEquals(
|
|
469
|
+
"count=$count index=$index mode=$mode restart=$restart",
|
|
470
|
+
can,
|
|
471
|
+
resolved != QueueSkipArithmetic.SkipToPreviousAction.NoOp
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
446
478
|
}
|