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
package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer.cast.airplay
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What an AirPlay session accepts for the receiver's own display while it
|
|
5
|
+
* streams. [AirPlayMetadataSync] pushes into this; AirPlay 1 and AirPlay 2
|
|
6
|
+
* sessions implement it over their respective native senders.
|
|
7
|
+
*/
|
|
8
|
+
interface MetadataSyncTarget {
|
|
9
|
+
/** Send track metadata (title/artist/album) to the receiver. */
|
|
10
|
+
fun setMetadata(title: String?, artist: String?, album: String?)
|
|
11
|
+
|
|
12
|
+
/** Update the progress bar on the receiver. */
|
|
13
|
+
fun setProgress(elapsedMs: Int, durationMs: Int)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Send artwork image bytes to the receiver. A receiver whose sender
|
|
17
|
+
* protocol carries no content type ignores [contentType].
|
|
18
|
+
*/
|
|
19
|
+
fun setArtwork(imageData: ByteArray, contentType: String)
|
|
20
|
+
}
|
package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt
CHANGED
|
@@ -14,9 +14,9 @@ import com.margelo.nitro.queueplayer.CastReceiver
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Chromecast backend — owns discovery via MediaRouter and session
|
|
17
|
-
* lifecycle via GMS SessionManager
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* lifecycle via GMS SessionManager, plus the transport calls that
|
|
18
|
+
* address the SDK session directly. Queue load and transport-state
|
|
19
|
+
* translation live in [ChromecastSession] and [RemoteMediaCallbacks].
|
|
20
20
|
*/
|
|
21
21
|
internal class ChromecastBackend(
|
|
22
22
|
private val context: Context,
|
package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt
CHANGED
|
@@ -14,6 +14,8 @@ import com.google.android.gms.cast.MediaQueueItem
|
|
|
14
14
|
import com.google.android.gms.cast.MediaSeekOptions
|
|
15
15
|
import com.google.android.gms.cast.MediaStatus
|
|
16
16
|
import com.google.android.gms.cast.framework.media.RemoteMediaClient
|
|
17
|
+
import com.google.android.gms.common.api.PendingResult
|
|
18
|
+
import com.margelo.nitro.queueplayer.BuildConfig
|
|
17
19
|
import com.margelo.nitro.queueplayer.cast.CastBackends
|
|
18
20
|
import com.margelo.nitro.queueplayer.cast.CastMediaItem
|
|
19
21
|
import com.margelo.nitro.queueplayer.cast.CastSession
|
|
@@ -88,6 +90,10 @@ internal class ChromecastSession(
|
|
|
88
90
|
private val _seekCompleted = MutableSharedFlow<Long>(extraBufferCapacity = 4)
|
|
89
91
|
|
|
90
92
|
override val state: StateFlow<RemotePlaybackState> = _state.asStateFlow()
|
|
93
|
+
|
|
94
|
+
@Volatile
|
|
95
|
+
override var hasReportedState: Boolean = false
|
|
96
|
+
private set
|
|
91
97
|
override val positionMs: StateFlow<Long> = _positionMs.asStateFlow()
|
|
92
98
|
override val durationMs: StateFlow<Long> = _durationMs.asStateFlow()
|
|
93
99
|
override val currentTrackIndex: StateFlow<Int> = _currentTrackIndex.asStateFlow()
|
|
@@ -110,7 +116,13 @@ internal class ChromecastSession(
|
|
|
110
116
|
private var serverHandle: MediaServerHandle? = null
|
|
111
117
|
|
|
112
118
|
private val remoteMediaCallbacks = RemoteMediaCallbacks(
|
|
113
|
-
onStateChanged = {
|
|
119
|
+
onStateChanged = { next ->
|
|
120
|
+
if (BuildConfig.DEBUG && _state.value != next) {
|
|
121
|
+
Log.d(TAG, "receiver state ${_state.value} -> $next")
|
|
122
|
+
}
|
|
123
|
+
if (next != RemotePlaybackState.IDLE) hasReportedState = true
|
|
124
|
+
_state.value = next
|
|
125
|
+
},
|
|
114
126
|
onPositionChanged = { _positionMs.value = it },
|
|
115
127
|
onDurationChanged = { _durationMs.value = it },
|
|
116
128
|
onEndedReason = { _endedReason.tryEmit(it) },
|
|
@@ -264,12 +276,13 @@ internal class ChromecastSession(
|
|
|
264
276
|
_currentTrackIndex.value = clampedStart
|
|
265
277
|
}
|
|
266
278
|
|
|
279
|
+
// No bitmask flag gates `play` — the mask models pause, not resume.
|
|
267
280
|
override fun play() {
|
|
268
281
|
fireRemote("play") { it.play() }
|
|
269
282
|
}
|
|
270
283
|
|
|
271
284
|
override fun pause() {
|
|
272
|
-
fireRemote("pause") { it.pause() }
|
|
285
|
+
fireRemote("pause", requires = MediaStatus.COMMAND_PAUSE) { it.pause() }
|
|
273
286
|
}
|
|
274
287
|
|
|
275
288
|
override fun stop() {
|
|
@@ -277,11 +290,11 @@ internal class ChromecastSession(
|
|
|
277
290
|
}
|
|
278
291
|
|
|
279
292
|
override fun queueNext() {
|
|
280
|
-
fireRemote("queueNext") { it.queueNext(null) }
|
|
293
|
+
fireRemote("queueNext", requires = MediaStatus.COMMAND_QUEUE_NEXT) { it.queueNext(null) }
|
|
281
294
|
}
|
|
282
295
|
|
|
283
296
|
override fun queuePrev() {
|
|
284
|
-
fireRemote("queuePrev") { it.queuePrev(null) }
|
|
297
|
+
fireRemote("queuePrev", requires = MediaStatus.COMMAND_QUEUE_PREVIOUS) { it.queuePrev(null) }
|
|
285
298
|
}
|
|
286
299
|
|
|
287
300
|
override fun skipToQueueIndex(index: Int) {
|
|
@@ -292,8 +305,8 @@ internal class ChromecastSession(
|
|
|
292
305
|
// synced that slot yet; skip rather than jump blind.
|
|
293
306
|
fireRemote("skipToQueueIndex") { client ->
|
|
294
307
|
val itemId = receiverItemIdAtIndex(index)
|
|
295
|
-
if (itemId == MediaQueueItem.INVALID_ITEM_ID)
|
|
296
|
-
client.queueJumpToItem(itemId, null)
|
|
308
|
+
if (itemId == MediaQueueItem.INVALID_ITEM_ID) null
|
|
309
|
+
else client.queueJumpToItem(itemId, null)
|
|
297
310
|
}
|
|
298
311
|
}
|
|
299
312
|
|
|
@@ -367,7 +380,9 @@ internal class ChromecastSession(
|
|
|
367
380
|
}
|
|
368
381
|
|
|
369
382
|
override fun setPlaybackRate(rate: Float) {
|
|
370
|
-
fireRemote("setPlaybackRate"
|
|
383
|
+
fireRemote("setPlaybackRate", requires = MediaStatus.COMMAND_PLAYBACK_RATE) {
|
|
384
|
+
it.setPlaybackRate(rate.toDouble())
|
|
385
|
+
}
|
|
371
386
|
}
|
|
372
387
|
|
|
373
388
|
override fun setRepeatMode(mode: RemoteRepeatMode) {
|
|
@@ -435,11 +450,26 @@ internal class ChromecastSession(
|
|
|
435
450
|
}
|
|
436
451
|
|
|
437
452
|
/**
|
|
438
|
-
* Post [block] to main and invoke it with the current
|
|
439
|
-
*
|
|
440
|
-
*
|
|
453
|
+
* Post [block] to main and invoke it with the current [RemoteMediaClient].
|
|
454
|
+
* Silently skips when the session is closed or the client is unavailable
|
|
455
|
+
* (log only — transport calls are fire-and-forget).
|
|
456
|
+
*
|
|
457
|
+
* [block] returns the [PendingResult] the SDK scheduled, or `null` when it
|
|
458
|
+
* declined to issue one. A returned result gets a callback so a
|
|
459
|
+
* receiver-side rejection is reported instead of vanishing — without it a
|
|
460
|
+
* refused command is indistinguishable from one the receiver honoured.
|
|
461
|
+
*
|
|
462
|
+
* [requires] names the [MediaStatus] command bitmask flag the receiver must
|
|
463
|
+
* advertise for this command (`null` for commands not gated by the mask).
|
|
464
|
+
* The receiver publishes support per media item, so an item can accept queue
|
|
465
|
+
* commands while refusing `pause` — worth a warning, since the command is
|
|
466
|
+
* otherwise dropped in silence.
|
|
441
467
|
*/
|
|
442
|
-
private fun fireRemote(
|
|
468
|
+
private fun fireRemote(
|
|
469
|
+
label: String,
|
|
470
|
+
requires: Long? = null,
|
|
471
|
+
block: (RemoteMediaClient) -> PendingResult<RemoteMediaClient.MediaChannelResult>?,
|
|
472
|
+
) {
|
|
443
473
|
if (closed) return
|
|
444
474
|
runOnMain {
|
|
445
475
|
if (closed) return@runOnMain
|
|
@@ -448,8 +478,38 @@ internal class ChromecastSession(
|
|
|
448
478
|
Log.w(TAG, "$label: no RemoteMediaClient — session not fully live?")
|
|
449
479
|
return@runOnMain
|
|
450
480
|
}
|
|
451
|
-
|
|
452
|
-
|
|
481
|
+
val status = client.mediaStatus
|
|
482
|
+
if (requires != null && status != null && !status.isMediaCommandSupported(requires)) {
|
|
483
|
+
Log.w(
|
|
484
|
+
TAG,
|
|
485
|
+
"$label: receiver does not advertise support for this command " +
|
|
486
|
+
"(supportedMediaCommands=${status.supportedMediaCommands}); sending anyway",
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
if (BuildConfig.DEBUG) Log.d(TAG, "$label: issuing")
|
|
490
|
+
val outcome = runCatching { block(client) }
|
|
491
|
+
val thrown = outcome.exceptionOrNull()
|
|
492
|
+
if (thrown != null) {
|
|
493
|
+
Log.w(TAG, "$label failed", thrown)
|
|
494
|
+
return@runOnMain
|
|
495
|
+
}
|
|
496
|
+
// A null result means this call site declined to issue anything — the
|
|
497
|
+
// only such case is a queue index the receiver's model hasn't synced
|
|
498
|
+
// yet, which is a normal transient, not a fault.
|
|
499
|
+
val pending = outcome.getOrNull()
|
|
500
|
+
if (pending == null) {
|
|
501
|
+
if (BuildConfig.DEBUG) Log.d(TAG, "$label: no request issued")
|
|
502
|
+
return@runOnMain
|
|
503
|
+
}
|
|
504
|
+
pending.setResultCallback { result ->
|
|
505
|
+
if (closed) return@setResultCallback
|
|
506
|
+
if (!result.status.isSuccess) {
|
|
507
|
+
Log.w(
|
|
508
|
+
TAG,
|
|
509
|
+
"$label failed: ${result.status.statusCode} ${result.status.statusMessage}",
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
453
513
|
}
|
|
454
514
|
}
|
|
455
515
|
|
|
@@ -32,7 +32,7 @@ import kotlinx.coroutines.runBlocking
|
|
|
32
32
|
*
|
|
33
33
|
* Binds to `0.0.0.0` on an OS-assigned port. Range / HEAD /
|
|
34
34
|
* Content-Length fall out of Ktor's `LocalFileContent` + `PartialContent`
|
|
35
|
-
* plugin. Refcounted via `acquire(sessionId)` / `release()`.
|
|
35
|
+
* plugin. Refcounted via `acquire(sessionId, context)` / `release(sessionId)`.
|
|
36
36
|
*/
|
|
37
37
|
class LocalMediaServer {
|
|
38
38
|
private val lock = Any()
|
|
@@ -51,38 +51,26 @@ class LocalMediaServer {
|
|
|
51
51
|
var serverPort: Int = -1
|
|
52
52
|
private set
|
|
53
53
|
|
|
54
|
-
@Volatile
|
|
55
|
-
var lifecycleListener: LocalMediaServerLifecycle? = null
|
|
56
|
-
|
|
57
54
|
@Volatile
|
|
58
55
|
private var addressMonitor: LocalAddressMonitor? = null
|
|
59
56
|
|
|
60
57
|
val currentLanAddress: String?
|
|
61
58
|
get() = addressMonitor?.currentAddress
|
|
62
59
|
|
|
63
|
-
fun attachAddressMonitor(monitor: LocalAddressMonitor) {
|
|
64
|
-
addressMonitor = monitor
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
fun addAddressChangeListener(
|
|
68
|
-
listener: LocalAddressMonitor.Listener,
|
|
69
|
-
): LocalAddressMonitor.Subscription? = addressMonitor?.addListener(listener)
|
|
70
|
-
|
|
71
60
|
fun acquire(sessionId: String, context: Context): MediaServerHandle {
|
|
72
61
|
val first = refCount.getAndIncrement() == 0
|
|
73
62
|
if (first) {
|
|
74
63
|
ensureStarted()
|
|
75
64
|
ensureAddressMonitor(context)
|
|
76
|
-
lifecycleListener?.onActiveStateChanged(true)
|
|
77
65
|
}
|
|
78
66
|
return MediaServerHandle(this, sessionId)
|
|
79
67
|
}
|
|
80
68
|
|
|
81
69
|
// Lazily create + start the LAN-address monitor the first time the
|
|
82
70
|
// server is acquired. `start()` seeds `currentAddress` synchronously
|
|
83
|
-
//
|
|
84
|
-
// first `currentLanAddress` read (the cast handoff loads
|
|
85
|
-
// on connect, before any async network callback
|
|
71
|
+
// when the active network is Wi-Fi, so the address is available to
|
|
72
|
+
// the very first `currentLanAddress` read (the cast handoff loads
|
|
73
|
+
// immediately on connect, before any async network callback lands).
|
|
86
74
|
private fun ensureAddressMonitor(context: Context) {
|
|
87
75
|
synchronized(lock) {
|
|
88
76
|
if (addressMonitor != null) return
|
|
@@ -93,29 +81,21 @@ class LocalMediaServer {
|
|
|
93
81
|
internal fun release(sessionId: String) {
|
|
94
82
|
registry.revokeAll(sessionId)
|
|
95
83
|
val last = refCount.decrementAndGet() == 0
|
|
96
|
-
if (last)
|
|
97
|
-
stop()
|
|
98
|
-
lifecycleListener?.onActiveStateChanged(false)
|
|
99
|
-
}
|
|
84
|
+
if (last) stop()
|
|
100
85
|
}
|
|
101
86
|
|
|
102
87
|
internal fun register(sessionId: String, file: File, contentType: String?): String =
|
|
103
88
|
registry.register(sessionId, file, contentType)
|
|
104
89
|
|
|
105
|
-
internal fun revoke(token: String) = registry.revoke(token)
|
|
106
|
-
|
|
107
|
-
internal fun resolve(token: String): RegisteredMedia? = registry.resolve(token)
|
|
108
|
-
|
|
109
90
|
private fun ensureStarted() {
|
|
110
91
|
synchronized(lock) {
|
|
111
92
|
if (engine != null) return
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// unauthorized callers cannot enumerate served media.
|
|
93
|
+
// Listen on every interface; the URL host selects the Wi-Fi one.
|
|
94
|
+
// URLs handed back to receivers go through
|
|
95
|
+
// `MediaServerHandle.register(file, lanAddress, contentType)` with the
|
|
96
|
+
// host from `currentLanAddress` (`LocalAddressMonitor.currentAddress`,
|
|
97
|
+
// the Wi-Fi network's address). Tokens are 32 hex chars (high
|
|
98
|
+
// entropy); unauthorized callers cannot enumerate served media.
|
|
119
99
|
val started = embeddedServer(CIO, port = 0, host = "0.0.0.0") {
|
|
120
100
|
install(PartialContent)
|
|
121
101
|
install(AutoHeadResponse)
|
|
@@ -153,8 +133,4 @@ class LocalMediaServer {
|
|
|
153
133
|
addressMonitor = null
|
|
154
134
|
}
|
|
155
135
|
}
|
|
156
|
-
|
|
157
|
-
internal fun stopForTest() {
|
|
158
|
-
stop()
|
|
159
|
-
}
|
|
160
136
|
}
|
|
@@ -31,11 +31,6 @@ class MediaServerHandle internal constructor(
|
|
|
31
31
|
return "http://$lanAddress:$port/media/$token"
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
fun revoke(token: String) {
|
|
35
|
-
if (closed.get()) return
|
|
36
|
-
server.revoke(token)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
34
|
fun close() {
|
|
40
35
|
if (closed.compareAndSet(false, true)) {
|
|
41
36
|
server.release(sessionId)
|
|
@@ -22,16 +22,10 @@ internal class MediaTokenRegistry {
|
|
|
22
22
|
|
|
23
23
|
fun resolve(token: String): RegisteredMedia? = tokens[token]
|
|
24
24
|
|
|
25
|
-
fun revoke(token: String) {
|
|
26
|
-
tokens.remove(token)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
25
|
fun revokeAll(sessionId: String) {
|
|
30
26
|
val iter = tokens.entries.iterator()
|
|
31
27
|
while (iter.hasNext()) {
|
|
32
28
|
if (iter.next().value.sessionId == sessionId) iter.remove()
|
|
33
29
|
}
|
|
34
30
|
}
|
|
35
|
-
|
|
36
|
-
fun size(): Int = tokens.size
|
|
37
31
|
}
|
|
@@ -7,37 +7,38 @@ import android.net.Network
|
|
|
7
7
|
import android.net.NetworkCapabilities
|
|
8
8
|
import android.net.NetworkRequest
|
|
9
9
|
import java.net.Inet4Address
|
|
10
|
-
import java.util.concurrent.CopyOnWriteArrayList
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
|
-
* Surfaces the host's
|
|
14
|
-
*
|
|
15
|
-
* server's `addAddressChangeListener`.
|
|
12
|
+
* Surfaces the host's Wi-Fi IPv4 address. Owned by `LocalMediaServer`,
|
|
13
|
+
* which exposes it to backends as `currentLanAddress`.
|
|
16
14
|
*
|
|
17
15
|
* Detection via [ConnectivityManager.NetworkCallback] on a request for
|
|
18
|
-
* `TRANSPORT_WIFI
|
|
19
|
-
*
|
|
16
|
+
* `TRANSPORT_WIFI` (the request's default capabilities exclude VPNs), so
|
|
17
|
+
* only a Wi-Fi network's address is ever served, and a cellular network
|
|
18
|
+
* coming or going neither replaces nor clears it.
|
|
20
19
|
*/
|
|
21
20
|
class LocalAddressMonitor(private val context: Context) {
|
|
22
21
|
|
|
23
|
-
fun interface Listener {
|
|
24
|
-
fun onAddressChanged(newAddress: String?)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
private val listeners = CopyOnWriteArrayList<Listener>()
|
|
28
|
-
|
|
29
22
|
@Volatile
|
|
30
23
|
var currentAddress: String? = null
|
|
31
24
|
private set
|
|
32
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The network whose link properties supplied [currentAddress]. Two Wi-Fi
|
|
28
|
+
* networks overlap during a handover — the new one reports its link before
|
|
29
|
+
* the old one is lost — so a loss clears the address only when it is the
|
|
30
|
+
* network the address came from.
|
|
31
|
+
*/
|
|
32
|
+
private var addressNetwork: Network? = null
|
|
33
|
+
|
|
33
34
|
private var registered = false
|
|
34
35
|
private val callback = object : ConnectivityManager.NetworkCallback() {
|
|
35
36
|
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
|
|
36
|
-
updateAddress(linkProperties)
|
|
37
|
+
updateAddress(network, linkProperties)
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
override fun onLost(network: Network) {
|
|
40
|
-
updateAddress(null)
|
|
41
|
+
if (network == addressNetwork) updateAddress(null, null)
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -47,14 +48,23 @@ class LocalAddressMonitor(private val context: Context) {
|
|
|
47
48
|
?: return
|
|
48
49
|
val request = NetworkRequest.Builder()
|
|
49
50
|
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
|
50
|
-
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
|
51
51
|
.build()
|
|
52
52
|
cm.registerNetworkCallback(request, callback)
|
|
53
53
|
registered = true
|
|
54
|
-
// Seed synchronously
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
54
|
+
// Seed synchronously so `currentAddress` is populated immediately —
|
|
55
|
+
// the cast handoff reads it right after connect, before the first
|
|
56
|
+
// async onLinkPropertiesChanged lands. The active network is used
|
|
57
|
+
// only when it passes the request's own filter (Wi-Fi, not a VPN:
|
|
58
|
+
// a VPN over Wi-Fi carries the Wi-Fi transport but a tunnel address).
|
|
59
|
+
// When Wi-Fi is up but not the default network, the address arrives
|
|
60
|
+
// with that network's first onLinkPropertiesChanged instead.
|
|
61
|
+
val active = cm.activeNetwork ?: return
|
|
62
|
+
val caps = cm.getNetworkCapabilities(active) ?: return
|
|
63
|
+
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
|
|
64
|
+
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
|
65
|
+
) {
|
|
66
|
+
updateAddress(active, cm.getLinkProperties(active))
|
|
67
|
+
}
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
fun stop() {
|
|
@@ -65,22 +75,11 @@ class LocalAddressMonitor(private val context: Context) {
|
|
|
65
75
|
registered = false
|
|
66
76
|
}
|
|
67
77
|
|
|
68
|
-
fun
|
|
69
|
-
listeners.add(listener)
|
|
70
|
-
return Subscription { listeners.remove(listener) }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
private fun updateAddress(linkProperties: LinkProperties?) {
|
|
78
|
+
private fun updateAddress(network: Network?, linkProperties: LinkProperties?) {
|
|
74
79
|
val ipv4 = linkProperties?.linkAddresses
|
|
75
80
|
?.firstOrNull { it.address is Inet4Address && !it.address.isLoopbackAddress }
|
|
76
81
|
?.address?.hostAddress
|
|
77
|
-
if (ipv4 !=
|
|
78
|
-
|
|
79
|
-
listeners.forEach { it.onAddressChanged(ipv4) }
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
fun interface Subscription {
|
|
84
|
-
fun remove()
|
|
82
|
+
addressNetwork = if (ipv4 != null) network else null
|
|
83
|
+
currentAddress = ipv4
|
|
85
84
|
}
|
|
86
85
|
}
|
|
@@ -3,7 +3,7 @@ package androidx.media3.session
|
|
|
3
3
|
import androidx.media3.common.util.UnstableApi
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Test-only
|
|
6
|
+
* Test-only access to Media3's controller-request scope.
|
|
7
7
|
*
|
|
8
8
|
* `MediaSession.getControllerForCurrentRequest()` is non-null only for
|
|
9
9
|
* the duration of a controller-originated `Player` call, set by Media3
|
|
@@ -80,6 +80,8 @@ class AvrcpMetadataTest {
|
|
|
80
80
|
duration = null,
|
|
81
81
|
artworkUrl = null,
|
|
82
82
|
extras = null,
|
|
83
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
84
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null,
|
|
83
85
|
)
|
|
84
86
|
setQueueOnSession(listOf(track))
|
|
85
87
|
|
|
@@ -100,6 +102,8 @@ class AvrcpMetadataTest {
|
|
|
100
102
|
duration = null,
|
|
101
103
|
artworkUrl = "https://example.com/cover.jpg",
|
|
102
104
|
extras = null,
|
|
105
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
106
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null,
|
|
103
107
|
)
|
|
104
108
|
setQueueOnSession(listOf(track))
|
|
105
109
|
|
|
@@ -120,6 +124,8 @@ class AvrcpMetadataTest {
|
|
|
120
124
|
duration = 42.0,
|
|
121
125
|
artworkUrl = null,
|
|
122
126
|
extras = null,
|
|
127
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
128
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null,
|
|
123
129
|
)
|
|
124
130
|
setQueueOnSession(listOf(track))
|
|
125
131
|
|
|
@@ -137,6 +143,8 @@ class AvrcpMetadataTest {
|
|
|
137
143
|
duration = null,
|
|
138
144
|
artworkUrl = null,
|
|
139
145
|
extras = null,
|
|
146
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
147
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null,
|
|
140
148
|
)
|
|
141
149
|
val second = TrackItem(
|
|
142
150
|
id = "second",
|
|
@@ -147,6 +155,8 @@ class AvrcpMetadataTest {
|
|
|
147
155
|
duration = null,
|
|
148
156
|
artworkUrl = null,
|
|
149
157
|
extras = null,
|
|
158
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
159
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null,
|
|
150
160
|
)
|
|
151
161
|
|
|
152
162
|
setQueueOnSession(listOf(first))
|
|
@@ -170,6 +180,8 @@ class AvrcpMetadataTest {
|
|
|
170
180
|
duration = null,
|
|
171
181
|
artworkUrl = null,
|
|
172
182
|
extras = null,
|
|
183
|
+
replayGainTrackGain = null, replayGainTrackPeak = null,
|
|
184
|
+
replayGainAlbumGain = null, replayGainAlbumPeak = null,
|
|
173
185
|
)
|
|
174
186
|
setQueueOnSession(listOf(track))
|
|
175
187
|
assertEquals(1, session.player.mediaItemCount)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.media.AudioManager
|
|
5
|
+
import androidx.media3.common.MediaItem
|
|
6
|
+
import androidx.media3.common.PlaybackException
|
|
7
|
+
import androidx.media3.common.Player
|
|
8
|
+
import androidx.media3.common.util.UnstableApi
|
|
9
|
+
import androidx.test.core.app.ApplicationProvider
|
|
10
|
+
import org.junit.Assert.assertEquals
|
|
11
|
+
import org.junit.Assert.assertNotNull
|
|
12
|
+
import org.junit.Test
|
|
13
|
+
import org.junit.runner.RunWith
|
|
14
|
+
import org.robolectric.RobolectricTestRunner
|
|
15
|
+
import org.robolectric.Shadows.shadowOf
|
|
16
|
+
import org.robolectric.annotation.Config
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A crossfade engine owns audio focus itself, so the pause a focus loss
|
|
20
|
+
* makes reaches the delegate carrying the audio-focus reason, not the
|
|
21
|
+
* user-request reason `pause()` stamps by default.
|
|
22
|
+
*/
|
|
23
|
+
@RunWith(RobolectricTestRunner::class)
|
|
24
|
+
@Config(sdk = [34])
|
|
25
|
+
@UnstableApi
|
|
26
|
+
class CrossfadeEngineFocusLossTest {
|
|
27
|
+
|
|
28
|
+
private val context: Context = ApplicationProvider.getApplicationContext()
|
|
29
|
+
|
|
30
|
+
/** Records every play-when-ready reason the engine reports. */
|
|
31
|
+
private class RecordingDelegate : PlaybackEngineDelegate {
|
|
32
|
+
val reasons = mutableListOf<Int?>()
|
|
33
|
+
override fun onStateMaybeChanged(engine: PlaybackEngine, playWhenReadyReason: Int?) {
|
|
34
|
+
reasons.add(playWhenReadyReason)
|
|
35
|
+
}
|
|
36
|
+
override fun onIsPlayingChanged(engine: PlaybackEngine, isPlaying: Boolean) {}
|
|
37
|
+
override fun onTrackTransition(engine: PlaybackEngine, platformReason: Int) {}
|
|
38
|
+
override fun onActiveItemFormatChanged(engine: PlaybackEngine) {}
|
|
39
|
+
override fun onError(engine: PlaybackEngine, exception: PlaybackException, failedIndex: Int) {}
|
|
40
|
+
override fun onPlaybackEnded(engine: PlaybackEngine) {}
|
|
41
|
+
override fun onAutoTransitionDiscontinuity(engine: PlaybackEngine) {}
|
|
42
|
+
override fun onCrossfadeBegin(engine: PlaybackEngine, incomingIndex: Int) {}
|
|
43
|
+
override fun onCrossfadeCancel(engine: PlaybackEngine, revertedIndex: Int) {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private fun playingEngine(delegate: RecordingDelegate): CrossfadeEngine {
|
|
47
|
+
val engine = CrossfadeEngine(context = context, crossfadeDurationMs = 3_000L)
|
|
48
|
+
engine.delegate = delegate
|
|
49
|
+
engine.setMediaItems(listOf(MediaItem.fromUri("https://example.com/a.mp3")), 0, 0L)
|
|
50
|
+
engine.play()
|
|
51
|
+
shadowOf(android.os.Looper.getMainLooper()).idle()
|
|
52
|
+
return engine
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private fun focusListener(): AudioManager.OnAudioFocusChangeListener {
|
|
56
|
+
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
57
|
+
val request = shadowOf(audioManager).lastAudioFocusRequest
|
|
58
|
+
assertNotNull("play() must request focus", request)
|
|
59
|
+
return request.listener
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Test
|
|
63
|
+
fun `a focus loss pauses with the audio-focus reason`() {
|
|
64
|
+
val delegate = RecordingDelegate()
|
|
65
|
+
val engine = playingEngine(delegate)
|
|
66
|
+
delegate.reasons.clear()
|
|
67
|
+
|
|
68
|
+
focusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS)
|
|
69
|
+
shadowOf(android.os.Looper.getMainLooper()).idle()
|
|
70
|
+
|
|
71
|
+
assertEquals(
|
|
72
|
+
"the delegate stamps the last reason it is handed; a USER_REQUEST after the loss would label the pause a user pause",
|
|
73
|
+
Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS,
|
|
74
|
+
delegate.reasons.last { it != null },
|
|
75
|
+
)
|
|
76
|
+
engine.release()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Test
|
|
80
|
+
fun `a transient focus loss pauses with the audio-focus reason too`() {
|
|
81
|
+
val delegate = RecordingDelegate()
|
|
82
|
+
val engine = playingEngine(delegate)
|
|
83
|
+
delegate.reasons.clear()
|
|
84
|
+
|
|
85
|
+
focusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)
|
|
86
|
+
shadowOf(android.os.Looper.getMainLooper()).idle()
|
|
87
|
+
|
|
88
|
+
assertEquals(
|
|
89
|
+
Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS,
|
|
90
|
+
delegate.reasons.last { it != null },
|
|
91
|
+
)
|
|
92
|
+
engine.release()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Test
|
|
96
|
+
fun `a user pause after the loss keeps its own reason`() {
|
|
97
|
+
val delegate = RecordingDelegate()
|
|
98
|
+
val engine = playingEngine(delegate)
|
|
99
|
+
focusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)
|
|
100
|
+
focusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN)
|
|
101
|
+
engine.play()
|
|
102
|
+
shadowOf(android.os.Looper.getMainLooper()).idle()
|
|
103
|
+
delegate.reasons.clear()
|
|
104
|
+
|
|
105
|
+
engine.pause()
|
|
106
|
+
shadowOf(android.os.Looper.getMainLooper()).idle()
|
|
107
|
+
|
|
108
|
+
assertEquals(
|
|
109
|
+
"the substitution lasts only for the loss's own pauses",
|
|
110
|
+
Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
|
111
|
+
delegate.reasons.last { it != null },
|
|
112
|
+
)
|
|
113
|
+
engine.release()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.margelo.nitro.queueplayer
|
|
2
2
|
|
|
3
|
+
import androidx.media3.common.MediaItem
|
|
3
4
|
import androidx.media3.common.util.UnstableApi
|
|
4
5
|
import androidx.test.core.app.ApplicationProvider
|
|
5
6
|
import org.junit.Assert.assertEquals
|
|
@@ -31,6 +32,49 @@ class CrossfadeEngineLifecycleTest {
|
|
|
31
32
|
crossfadeDurationMs = crossfadeDurationMs,
|
|
32
33
|
)
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Settling leaves the standby leg holding nothing.
|
|
37
|
+
*
|
|
38
|
+
* An armed standby carries the queue as it stood when it was armed. A caller
|
|
39
|
+
* that mutates the queue through a path the engine does not see — Media3
|
|
40
|
+
* setting resolved assistant items on the session player — needs that
|
|
41
|
+
* snapshot gone, or promoting it drops whatever the caller added.
|
|
42
|
+
*/
|
|
43
|
+
@Test
|
|
44
|
+
fun `settling a pending fade leaves nothing staged on the standby leg`() {
|
|
45
|
+
val engine = makeEngine()
|
|
46
|
+
engine.standbyPlayer.setMediaItems(
|
|
47
|
+
listOf(MediaItem.fromUri("http://example.com/next.mp3")))
|
|
48
|
+
engine.standbyPlayer.prepare()
|
|
49
|
+
assertEquals(1, engine.standbyPlayer.mediaItemCount)
|
|
50
|
+
|
|
51
|
+
engine.settlePendingFade()
|
|
52
|
+
|
|
53
|
+
assertEquals(0, engine.standbyPlayer.mediaItemCount)
|
|
54
|
+
engine.release()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A settle with nothing to settle leaves the legs as they are: a caller
|
|
59
|
+
* settling defensively must not swap them. Promotion itself needs a live
|
|
60
|
+
* fade, which needs real playback with duration metadata, so that half is
|
|
61
|
+
* covered by the crossfade demo suites.
|
|
62
|
+
*/
|
|
63
|
+
@Test
|
|
64
|
+
fun `settling by promotion leaves the roles alone when no fade is in flight`() {
|
|
65
|
+
val engine = makeEngine()
|
|
66
|
+
val leading = engine.leadingPlayer
|
|
67
|
+
val standby = engine.standbyPlayer
|
|
68
|
+
|
|
69
|
+
engine.settlePendingFadeByPromoting()
|
|
70
|
+
engine.settlePendingFadeByPromoting()
|
|
71
|
+
|
|
72
|
+
assertSame(leading, engine.leadingPlayer)
|
|
73
|
+
assertSame(standby, engine.standbyPlayer)
|
|
74
|
+
assertTrue(engine.currentRoleIsA)
|
|
75
|
+
engine.release()
|
|
76
|
+
}
|
|
77
|
+
|
|
34
78
|
@Test
|
|
35
79
|
fun `constructor accepts a non-negative crossfade duration`() {
|
|
36
80
|
val engine = makeEngine()
|
|
@@ -204,6 +248,7 @@ class CrossfadeEngineLifecycleTest {
|
|
|
204
248
|
override fun onError(
|
|
205
249
|
engine: PlaybackEngine,
|
|
206
250
|
exception: androidx.media3.common.PlaybackException,
|
|
251
|
+
failedIndex: Int,
|
|
207
252
|
) = Unit
|
|
208
253
|
override fun onPlaybackEnded(engine: PlaybackEngine) = Unit
|
|
209
254
|
override fun onAutoTransitionDiscontinuity(engine: PlaybackEngine) = Unit
|