react-native-queue-player 1.2.0 → 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/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 +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 +839 -271
- 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 +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 +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 +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 +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 +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 +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/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 +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/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 -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,152 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Pure-Swift arithmetic for the bounded enqueue window.
|
|
4
|
+
///
|
|
5
|
+
/// Everything here is expressed in absolute queue *positions* — never engine
|
|
6
|
+
/// indices, which move as `AVQueuePlayer` consumes finished items.
|
|
7
|
+
///
|
|
8
|
+
/// `slice` describes the installed run — a trail behind the playhead plus the
|
|
9
|
+
/// forward window — and it **may wrap**: it can end at a lower absolute
|
|
10
|
+
/// position than it starts. A wrapped slice is a run in play order, not an
|
|
11
|
+
/// ordered set of integers, so it must not be compared with `<` / `>`, sorted,
|
|
12
|
+
/// or turned into a `Range<Int>`. The floor bounds its forward part.
|
|
13
|
+
///
|
|
14
|
+
/// `isAtLogicalTail` and `verdict` answer what a drained engine means, in
|
|
15
|
+
/// absolute positions and without wrapping — see `isAtLogicalTail` for why
|
|
16
|
+
/// that asymmetry is deliberate.
|
|
17
|
+
///
|
|
18
|
+
/// Helpers are gated behind `@_spi(QueuePlayerTests)` so they stay off the
|
|
19
|
+
/// library's consumer-facing surface, matching `QueueMutationArithmetic`.
|
|
20
|
+
public enum QueueWindowArithmetic {
|
|
21
|
+
|
|
22
|
+
/// Items materialised at or after the current track, inclusive of it.
|
|
23
|
+
///
|
|
24
|
+
/// Three is the minimum that satisfies "top up whenever at most one item
|
|
25
|
+
/// remains after the current one, and never let that reach zero": a smaller
|
|
26
|
+
/// floor lands an append in the slot `AVQueuePlayer` is about to consume.
|
|
27
|
+
/// Ten sits well clear of that minimum. The cost windowing exists to avoid is
|
|
28
|
+
/// per-item — item construction, a cache lookup, an LRU touch — and it is a
|
|
29
|
+
/// queue-length problem, not a window-length one: ten items is a fixed,
|
|
30
|
+
/// negligible cost where the unbounded build was thousands.
|
|
31
|
+
@_spi(QueuePlayerTests)
|
|
32
|
+
public static let floor: Int = 10
|
|
33
|
+
|
|
34
|
+
/// What a drained engine means for the queue as a whole.
|
|
35
|
+
@_spi(QueuePlayerTests)
|
|
36
|
+
public enum EndVerdict: Equatable {
|
|
37
|
+
/// The engine still has an active item; nothing ended.
|
|
38
|
+
case notEnded
|
|
39
|
+
/// Re-enqueue the current position. Under `.track` the queue can neither
|
|
40
|
+
/// end nor advance, so a drain is always the same position again.
|
|
41
|
+
case refillCurrent
|
|
42
|
+
/// The engine ran out mid-queue. Re-seat the queue from the next position
|
|
43
|
+
/// and resume — not a queue end.
|
|
44
|
+
case windowDrained
|
|
45
|
+
/// The logical queue ended under `.queue`; restart from position 0.
|
|
46
|
+
case wrapToStart
|
|
47
|
+
/// The logical queue ended. Fire `onQueueEnd` and settle to `.ended`.
|
|
48
|
+
case queueEnd
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Positions the engine should hold around `currentIndex`, in install order.
|
|
52
|
+
///
|
|
53
|
+
/// `trail` positions behind the current one, the current one, and forward to
|
|
54
|
+
/// the floor. The trail exists so a backward skip reuses an `AVURLAsset`
|
|
55
|
+
/// already built; a run that only grows forward moves the memory peak later
|
|
56
|
+
/// rather than lowering it, because nothing is ever removed.
|
|
57
|
+
///
|
|
58
|
+
/// `wraps` is whether the queue returns to position 0 at its end, which is
|
|
59
|
+
/// what makes a repeat-queue boundary a crossfade rather than a hard cut:
|
|
60
|
+
/// the incoming item has to be materialised before the outgoing one ends.
|
|
61
|
+
///
|
|
62
|
+
/// Returns at most `min(trail + floor, trackCount)` positions, always as one
|
|
63
|
+
/// unbroken run in play order: each position is the previous one plus one,
|
|
64
|
+
/// modulo `trackCount`. Where the run covers the whole queue the position
|
|
65
|
+
/// after the last is the first; everywhere else it is not installed yet and
|
|
66
|
+
/// belongs to top-up.
|
|
67
|
+
///
|
|
68
|
+
/// A position appears at most once. Two materialisations of one position
|
|
69
|
+
/// would give the engine two items carrying the same `queueItemId`, and
|
|
70
|
+
/// every translation from position to engine index is a first-match — so the
|
|
71
|
+
/// second copy would be unreachable and the trail could strand the item
|
|
72
|
+
/// being heard. The forward run and the trail can only meet when the slice
|
|
73
|
+
/// already covers every position, so nothing is lost either way; the forward
|
|
74
|
+
/// run keeps the contested position because that is what leaves the result
|
|
75
|
+
/// one unbroken run.
|
|
76
|
+
///
|
|
77
|
+
/// A queue of one wraps onto itself, so it cannot both hold a fresh incoming
|
|
78
|
+
/// item and hold each position once. It holds the one position, and a
|
|
79
|
+
/// repeat-queue boundary there is a hard cut.
|
|
80
|
+
@_spi(QueuePlayerTests)
|
|
81
|
+
public static func slice(
|
|
82
|
+
currentIndex: Int, trackCount: Int, trail: Int, wraps: Bool
|
|
83
|
+
) -> [Int] {
|
|
84
|
+
guard trackCount > 0, currentIndex >= 0, currentIndex < trackCount else { return [] }
|
|
85
|
+
let cap = min(max(0, trail) + floor, trackCount)
|
|
86
|
+
|
|
87
|
+
// Uniqueness rests on the cap: a run of at most `trackCount` positions
|
|
88
|
+
// stepping one at a time cannot revisit one. The `held` checks below are a
|
|
89
|
+
// second mechanism, and are unreachable while the cap holds — they are
|
|
90
|
+
// what keeps a future change to the cap from silently emitting duplicates.
|
|
91
|
+
var positions: [Int] = []
|
|
92
|
+
var held = Set<Int>()
|
|
93
|
+
|
|
94
|
+
var forward = currentIndex
|
|
95
|
+
while positions.count < min(floor, trackCount) {
|
|
96
|
+
guard !held.contains(forward) else { break }
|
|
97
|
+
positions.append(forward)
|
|
98
|
+
held.insert(forward)
|
|
99
|
+
let next = forward + 1
|
|
100
|
+
if next < trackCount {
|
|
101
|
+
forward = next
|
|
102
|
+
} else if wraps {
|
|
103
|
+
forward = 0
|
|
104
|
+
} else {
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
var behind: [Int] = []
|
|
110
|
+
var step = 1
|
|
111
|
+
while behind.count < max(0, trail), positions.count + behind.count < cap {
|
|
112
|
+
let raw = currentIndex - step
|
|
113
|
+
let position = raw >= 0 ? raw : (wraps ? raw + trackCount : -1)
|
|
114
|
+
guard position >= 0, !held.contains(position) else { break }
|
|
115
|
+
behind.append(position)
|
|
116
|
+
held.insert(position)
|
|
117
|
+
step += 1
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return behind.reversed() + positions
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Whether the last enqueued position is also the last position in the
|
|
124
|
+
/// queue. False on an empty queue, where `-1 == -1` would otherwise read
|
|
125
|
+
/// as a tail.
|
|
126
|
+
///
|
|
127
|
+
/// Deliberately not wrap-aware, even though `slice` wraps. `verdict` pairs
|
|
128
|
+
/// this with `.windowDrained`, whose caller advances by a **non-modular**
|
|
129
|
+
/// clamp; today that clamp is unreachable precisely because this answers
|
|
130
|
+
/// true at the last position. An answer that went modular here would make it
|
|
131
|
+
/// reachable, and the last track would re-seat onto itself instead of
|
|
132
|
+
/// wrapping. A wrapping run's completeness is a question for the top-up
|
|
133
|
+
/// path, not for the drain verdict.
|
|
134
|
+
@_spi(QueuePlayerTests)
|
|
135
|
+
public static func isAtLogicalTail(anchorPosition: Int, trackCount: Int) -> Bool {
|
|
136
|
+
guard trackCount > 0 else { return false }
|
|
137
|
+
return anchorPosition == trackCount - 1
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Resolve what a drained engine means. `drained` is "the engine has no
|
|
141
|
+
/// active item"; `atLogicalTail` is `isAtLogicalTail` against the anchor.
|
|
142
|
+
/// Caller must hold a non-empty queue — an empty one has nothing to end.
|
|
143
|
+
@_spi(QueuePlayerTests)
|
|
144
|
+
public static func verdict(
|
|
145
|
+
drained: Bool, atLogicalTail: Bool, repeatMode: QueueSkipRepeatMode
|
|
146
|
+
) -> EndVerdict {
|
|
147
|
+
guard drained else { return .notEnded }
|
|
148
|
+
if repeatMode == .track { return .refillCurrent }
|
|
149
|
+
guard atLogicalTail else { return .windowDrained }
|
|
150
|
+
return repeatMode == .queue ? .wrapToStart : .queueEnd
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Network
|
|
3
|
+
|
|
4
|
+
/// Serves the player's remote tracks from the lookahead cache when it holds
|
|
5
|
+
/// them, and from the origin when it does not.
|
|
6
|
+
///
|
|
7
|
+
/// The player binds one stable `http://127.0.0.1:<port>/media/<token>` per
|
|
8
|
+
/// source URL and never re-points it. Whether a request is answered from disk
|
|
9
|
+
/// or from the network is decided per request, which is the only moment the
|
|
10
|
+
/// answer is actually knowable — the cache fills while the queue is already
|
|
11
|
+
/// built, so a URL chosen at construction is always chosen too early.
|
|
12
|
+
///
|
|
13
|
+
/// Loopback-bound: nothing outside the device needs to reach it. That is a
|
|
14
|
+
/// smaller default rather than a security boundary — another process on the
|
|
15
|
+
/// same device could reach it too, and the 128-bit token is what stands in its
|
|
16
|
+
/// way.
|
|
17
|
+
final class ReadThroughServer: @unchecked Sendable {
|
|
18
|
+
|
|
19
|
+
/// Token → the origin it stands for. Written on the player's queue, read on
|
|
20
|
+
/// connection queues.
|
|
21
|
+
private let lock = NSLock()
|
|
22
|
+
private var origins: [String: URL] = [:]
|
|
23
|
+
private var tokensBySource: [String: String] = [:]
|
|
24
|
+
/// The consumer's `track.url` string per token: the cache is keyed on it
|
|
25
|
+
/// verbatim, and `URL.absoluteString` is a re-serialisation that can differ
|
|
26
|
+
/// from it (percent-encoding on iOS 17+).
|
|
27
|
+
private var sourceURLs: [String: String] = [:]
|
|
28
|
+
private var touched: Set<String> = []
|
|
29
|
+
private var headers: [String: String] = [:]
|
|
30
|
+
|
|
31
|
+
private let listenerLock = NSLock()
|
|
32
|
+
private var listener: NWListener?
|
|
33
|
+
private var boundPort: UInt16 = 0
|
|
34
|
+
private var connections: [ObjectIdentifier: MediaHTTPConnection] = [:]
|
|
35
|
+
private var bindWaiters: [DispatchSemaphore] = []
|
|
36
|
+
|
|
37
|
+
private let serverQueue = DispatchQueue(label: "queueplayer.readthrough.listener")
|
|
38
|
+
|
|
39
|
+
/// One session for every origin leg, so connections are pooled across the
|
|
40
|
+
/// per-request client connections AVFoundation opens.
|
|
41
|
+
private let session: URLSession
|
|
42
|
+
|
|
43
|
+
/// The default session: pooled like `URLSession.shared`, but with no
|
|
44
|
+
/// `URLCache` — the lookahead cache already keeps the bytes on disk, and
|
|
45
|
+
/// `URLCache.shared` would hold a second copy of every small range.
|
|
46
|
+
private static func makeOriginSession() -> URLSession {
|
|
47
|
+
let configuration = URLSessionConfiguration.default
|
|
48
|
+
configuration.urlCache = nil
|
|
49
|
+
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
|
50
|
+
return URLSession(configuration: configuration)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Consulted per request for the cache's complete copy of an origin. Injected
|
|
54
|
+
/// rather than referenced directly so the server owns no cache lifetime.
|
|
55
|
+
private let cachedFileForOrigin: (String) -> URL?
|
|
56
|
+
|
|
57
|
+
/// Called once per token the first time it is served from disk, so the cache
|
|
58
|
+
/// can bump LRU on a real read. Not per request: `touch` writes the index on
|
|
59
|
+
/// the same serial queue the lookup takes, and AVFoundation issues several
|
|
60
|
+
/// range requests per track.
|
|
61
|
+
private let onFirstDiskHit: (String) -> Void
|
|
62
|
+
|
|
63
|
+
init(
|
|
64
|
+
session: URLSession = ReadThroughServer.makeOriginSession(),
|
|
65
|
+
cachedFileForOrigin: @escaping (String) -> URL?,
|
|
66
|
+
onFirstDiskHit: @escaping (String) -> Void
|
|
67
|
+
) {
|
|
68
|
+
self.session = session
|
|
69
|
+
self.cachedFileForOrigin = cachedFileForOrigin
|
|
70
|
+
self.onFirstDiskHit = onFirstDiskHit
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// MARK: - Routing
|
|
74
|
+
|
|
75
|
+
/// The local URL standing for `origin`, or nil when the listener is not up.
|
|
76
|
+
///
|
|
77
|
+
/// Stable for the life of the session: one token per source string, so a
|
|
78
|
+
/// window rebuild hands the player the URL it already has and AVFoundation
|
|
79
|
+
/// keeps reusing its own connections. Keyed on the string rather than the
|
|
80
|
+
/// parsed URL because two strings that parse alike are still two tracks with
|
|
81
|
+
/// two cache entries.
|
|
82
|
+
/// `sourceURL` is the `track.url` string the cache is keyed on; it defaults
|
|
83
|
+
/// to the origin's own serialisation for callers that have no other.
|
|
84
|
+
func localURL(for origin: URL, sourceURL: String? = nil) -> URL? {
|
|
85
|
+
let port = boundPortValue()
|
|
86
|
+
guard port != 0 else { return nil }
|
|
87
|
+
let source = sourceURL ?? origin.absoluteString
|
|
88
|
+
lock.lock()
|
|
89
|
+
let token: String
|
|
90
|
+
if let existing = tokensBySource[source] {
|
|
91
|
+
token = existing
|
|
92
|
+
} else {
|
|
93
|
+
token = Self.newToken()
|
|
94
|
+
tokensBySource[source] = token
|
|
95
|
+
origins[token] = origin
|
|
96
|
+
sourceURLs[token] = source
|
|
97
|
+
}
|
|
98
|
+
lock.unlock()
|
|
99
|
+
#if DEBUG
|
|
100
|
+
NSLog(
|
|
101
|
+
"[RNQP-CACHE] route token=%@ -> %@",
|
|
102
|
+
String(token.prefix(8)), PlaybackErrorMapping.stripQuery(origin.absoluteString))
|
|
103
|
+
#endif
|
|
104
|
+
return URL(string: "http://127.0.0.1:\(port)/media/\(token)")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// The consumer's headers, sent on every origin request. Written by
|
|
108
|
+
/// `startReadThroughServer` — `configure()` and `setLookaheadCache` — so a
|
|
109
|
+
/// refreshed token reaches requests for items built before it changed.
|
|
110
|
+
func setHeaders(_ headers: [String: String]) {
|
|
111
|
+
lock.lock()
|
|
112
|
+
self.headers = headers
|
|
113
|
+
lock.unlock()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func resolve(_ token: String) -> MediaSource? {
|
|
117
|
+
lock.lock()
|
|
118
|
+
guard let origin = origins[token] else {
|
|
119
|
+
lock.unlock()
|
|
120
|
+
return nil
|
|
121
|
+
}
|
|
122
|
+
let sourceURL = sourceURLs[token] ?? origin.absoluteString
|
|
123
|
+
let headers = self.headers
|
|
124
|
+
let alreadyTouched = touched.contains(token)
|
|
125
|
+
lock.unlock()
|
|
126
|
+
|
|
127
|
+
let cached = cachedFileForOrigin(sourceURL)
|
|
128
|
+
#if DEBUG
|
|
129
|
+
NSLog(
|
|
130
|
+
"[RNQP-CACHE] resolve token=%@ cached=%@ origin=%@",
|
|
131
|
+
String(token.prefix(8)), cached == nil ? "no" : "yes",
|
|
132
|
+
PlaybackErrorMapping.stripQuery(origin.absoluteString))
|
|
133
|
+
#endif
|
|
134
|
+
if cached != nil && !alreadyTouched {
|
|
135
|
+
lock.lock()
|
|
136
|
+
touched.insert(token)
|
|
137
|
+
lock.unlock()
|
|
138
|
+
onFirstDiskHit(sourceURL)
|
|
139
|
+
}
|
|
140
|
+
return .readThrough(
|
|
141
|
+
ReadThroughSource(origin: origin, headers: headers, cachedFile: cached, session: session)
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// 128 bits, so the token cannot be enumerated by something else on the
|
|
146
|
+
/// device. Same shape `MediaHTTPRequest.mediaToken` already validates.
|
|
147
|
+
private static func newToken() -> String {
|
|
148
|
+
var bytes = [UInt8](repeating: 0, count: 16)
|
|
149
|
+
if SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) != errSecSuccess {
|
|
150
|
+
for i in bytes.indices { bytes[i] = UInt8.random(in: 0 ... 255) }
|
|
151
|
+
}
|
|
152
|
+
return bytes.map { String(format: "%02x", $0) }.joined()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// MARK: - Lifecycle
|
|
156
|
+
|
|
157
|
+
/// Bring the listener up. Returns immediately; `waitUntilBound` is what waits.
|
|
158
|
+
func start() {
|
|
159
|
+
listenerLock.lock()
|
|
160
|
+
guard listener == nil else {
|
|
161
|
+
listenerLock.unlock()
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
let params = NWParameters.tcp
|
|
165
|
+
// Loopback only. The cast server binds every interface because a receiver
|
|
166
|
+
// fetches over the LAN; nothing outside this device ever reads from here.
|
|
167
|
+
params.requiredLocalEndpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: .any)
|
|
168
|
+
guard let newListener = try? NWListener(using: params) else {
|
|
169
|
+
listenerLock.unlock()
|
|
170
|
+
resumeReadinessWaiters()
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
listener = newListener
|
|
174
|
+
listenerLock.unlock()
|
|
175
|
+
|
|
176
|
+
newListener.stateUpdateHandler = { [weak self] state in
|
|
177
|
+
guard let self = self else { return }
|
|
178
|
+
switch state {
|
|
179
|
+
case .ready:
|
|
180
|
+
self.listenerLock.lock()
|
|
181
|
+
self.boundPort = newListener.port?.rawValue ?? 0
|
|
182
|
+
self.listenerLock.unlock()
|
|
183
|
+
self.resumeReadinessWaiters()
|
|
184
|
+
case .failed, .cancelled:
|
|
185
|
+
// Drop the listener too, so a later `start()` can try again; a
|
|
186
|
+
// failed listener left in the slot would make every restart a no-op.
|
|
187
|
+
self.listenerLock.lock()
|
|
188
|
+
if self.listener === newListener {
|
|
189
|
+
self.listener = nil
|
|
190
|
+
self.boundPort = 0
|
|
191
|
+
}
|
|
192
|
+
self.listenerLock.unlock()
|
|
193
|
+
self.resumeReadinessWaiters()
|
|
194
|
+
default:
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
newListener.newConnectionHandler = { [weak self] conn in
|
|
199
|
+
self?.accept(conn)
|
|
200
|
+
}
|
|
201
|
+
newListener.start(queue: serverQueue)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// Block until the listener has bound, up to `timeoutMs`; `true` when it
|
|
205
|
+
/// has. Called right after `start()` on the queue that builds items, so
|
|
206
|
+
/// every item built afterwards sees a bound port: `localURL(for:)` is nil
|
|
207
|
+
/// before the bind, and an item built then is bound to its origin for good.
|
|
208
|
+
/// Binding loopback takes milliseconds; the timeout is for the listener
|
|
209
|
+
/// that never reaches `.ready`, which `start()` also reports by resuming.
|
|
210
|
+
@discardableResult
|
|
211
|
+
func waitUntilBound(timeoutMs: Int = 2000) -> Bool {
|
|
212
|
+
if boundPortValue() != 0 { return true }
|
|
213
|
+
let bound = DispatchSemaphore(value: 0)
|
|
214
|
+
listenerLock.lock()
|
|
215
|
+
if boundPort != 0 {
|
|
216
|
+
listenerLock.unlock()
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
bindWaiters.append(bound)
|
|
220
|
+
listenerLock.unlock()
|
|
221
|
+
_ = bound.wait(timeout: .now() + .milliseconds(timeoutMs))
|
|
222
|
+
return boundPortValue() != 0
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
func boundPortValue() -> UInt16 {
|
|
226
|
+
listenerLock.lock(); defer { listenerLock.unlock() }
|
|
227
|
+
return boundPort
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private func resumeReadinessWaiters() {
|
|
231
|
+
listenerLock.lock()
|
|
232
|
+
let waiters = bindWaiters
|
|
233
|
+
bindWaiters.removeAll()
|
|
234
|
+
listenerLock.unlock()
|
|
235
|
+
waiters.forEach { $0.signal() }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private func accept(_ conn: NWConnection) {
|
|
239
|
+
let responder = MediaHTTPConnection(
|
|
240
|
+
connection: conn,
|
|
241
|
+
resolve: { [weak self] token in self?.resolve(token) }
|
|
242
|
+
) { [weak self] in
|
|
243
|
+
self?.untrack(conn)
|
|
244
|
+
}
|
|
245
|
+
listenerLock.lock()
|
|
246
|
+
connections[ObjectIdentifier(conn)] = responder
|
|
247
|
+
listenerLock.unlock()
|
|
248
|
+
responder.start()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private func untrack(_ conn: NWConnection) {
|
|
252
|
+
listenerLock.lock()
|
|
253
|
+
connections.removeValue(forKey: ObjectIdentifier(conn))
|
|
254
|
+
listenerLock.unlock()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// Tear the listener down, invalidate the origin session and drop every
|
|
258
|
+
/// token. Tokens do not outlive a session, and the map is bounded by the
|
|
259
|
+
/// distinct origins seen in one — so nothing needs revoking as the queue
|
|
260
|
+
/// changes. A stopped server is not restarted; `startReadThroughServer`
|
|
261
|
+
/// builds a new one.
|
|
262
|
+
func stop() {
|
|
263
|
+
listenerLock.lock()
|
|
264
|
+
let live = listener
|
|
265
|
+
listener = nil
|
|
266
|
+
boundPort = 0
|
|
267
|
+
let open = connections
|
|
268
|
+
connections.removeAll()
|
|
269
|
+
listenerLock.unlock()
|
|
270
|
+
open.values.forEach { $0.cancel() }
|
|
271
|
+
live?.cancel()
|
|
272
|
+
session.invalidateAndCancel()
|
|
273
|
+
resumeReadinessWaiters()
|
|
274
|
+
|
|
275
|
+
lock.lock()
|
|
276
|
+
origins.removeAll()
|
|
277
|
+
tokensBySource.removeAll()
|
|
278
|
+
sourceURLs.removeAll()
|
|
279
|
+
touched.removeAll()
|
|
280
|
+
headers.removeAll()
|
|
281
|
+
lock.unlock()
|
|
282
|
+
}
|
|
283
|
+
}
|
package/ios/RemoteCommands.swift
CHANGED
|
@@ -8,7 +8,9 @@ import MediaPlayer
|
|
|
8
8
|
/// `pendingStateChangeReason = .system` and routes through the
|
|
9
9
|
/// existing public transport methods.
|
|
10
10
|
///
|
|
11
|
-
/// Threading:
|
|
11
|
+
/// Threading: callable from any queue. Every public method hands its work to
|
|
12
|
+
/// main, which owns `MPRemoteCommandCenter`. Command handlers still arrive
|
|
13
|
+
/// on main — `dispatch` asserts that — and the consumer hops from there.
|
|
12
14
|
/// Handlers registered with `addTarget` fire on main per Apple's docs.
|
|
13
15
|
final class RemoteCommands {
|
|
14
16
|
|
|
@@ -24,11 +26,17 @@ final class RemoteCommands {
|
|
|
24
26
|
case changePlaybackPosition(seconds: Double)
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
/// Set by the owner during `configure()
|
|
28
|
-
///
|
|
29
|
+
/// Set by the owner during `configure()` and cleared during `destroy()`,
|
|
30
|
+
/// both on the owner's player queue; invoked synchronously on main from
|
|
31
|
+
/// the handler, so the slot is read and written under a lock. Owner is
|
|
29
32
|
/// responsible for stamping the state-change reason BEFORE routing
|
|
30
33
|
/// into transport calls.
|
|
31
|
-
var onCommand: ((Command) -> Void)?
|
|
34
|
+
var onCommand: ((Command) -> Void)? {
|
|
35
|
+
get { commandLock.withLock { commandCallback } }
|
|
36
|
+
set { commandLock.withLock { commandCallback = newValue } }
|
|
37
|
+
}
|
|
38
|
+
private let commandLock = NSLock()
|
|
39
|
+
private var commandCallback: ((Command) -> Void)?
|
|
32
40
|
|
|
33
41
|
/// Drive `MPRemoteCommandCenter` `isEnabled` for the skip commands.
|
|
34
42
|
/// Greys out the lock-screen / CarPlay buttons that would no-op
|
|
@@ -36,6 +44,11 @@ final class RemoteCommands {
|
|
|
36
44
|
/// when not installed — the lib must not write to the shared
|
|
37
45
|
/// command center after `uninstall()` has released ownership.
|
|
38
46
|
func setSkipCapability(canSkipNext: Bool, canSkipPrevious: Bool) {
|
|
47
|
+
RemoteCommands.onMain { self.setSkipCapabilityOnMain(canSkipNext: canSkipNext, canSkipPrevious: canSkipPrevious) }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@MainActor
|
|
51
|
+
private func setSkipCapabilityOnMain(canSkipNext: Bool, canSkipPrevious: Bool) {
|
|
39
52
|
// Only greys out the next/previous-TRACK buttons. In `.interval` mode the
|
|
40
53
|
// lock-screen shows skip-forward/back, which always work (seek within the
|
|
41
54
|
// current track), so queue-boundary capability doesn't apply.
|
|
@@ -52,6 +65,16 @@ final class RemoteCommands {
|
|
|
52
65
|
/// consumer can toggle at runtime. The lib only owns the skip-interval
|
|
53
66
|
/// commands while in `.interval` mode.
|
|
54
67
|
func setRemoteControls(mode: RemoteSkipMode, forwardInterval: Double, backwardInterval: Double) {
|
|
68
|
+
RemoteCommands.onMain {
|
|
69
|
+
self.setRemoteControlsOnMain(
|
|
70
|
+
mode: mode, forwardInterval: forwardInterval, backwardInterval: backwardInterval)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@MainActor
|
|
75
|
+
private func setRemoteControlsOnMain(
|
|
76
|
+
mode: RemoteSkipMode, forwardInterval: Double, backwardInterval: Double
|
|
77
|
+
) {
|
|
55
78
|
self.skipMode = mode
|
|
56
79
|
self.forwardInterval = forwardInterval
|
|
57
80
|
self.backwardInterval = backwardInterval
|
|
@@ -114,6 +137,11 @@ final class RemoteCommands {
|
|
|
114
137
|
|
|
115
138
|
/// Register handlers + enable the supported commands. Idempotent.
|
|
116
139
|
func install() {
|
|
140
|
+
RemoteCommands.onMain { self.installOnMain() }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@MainActor
|
|
144
|
+
private func installOnMain() {
|
|
117
145
|
guard !installed else { return }
|
|
118
146
|
installed = true
|
|
119
147
|
let center = MPRemoteCommandCenter.shared()
|
|
@@ -174,6 +202,11 @@ final class RemoteCommands {
|
|
|
174
202
|
/// owns. Idempotent. Never touches commands the lib didn't enable
|
|
175
203
|
/// in `install()`.
|
|
176
204
|
func uninstall() {
|
|
205
|
+
RemoteCommands.onMain { self.uninstallOnMain() }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@MainActor
|
|
209
|
+
private func uninstallOnMain() {
|
|
177
210
|
guard installed else { return }
|
|
178
211
|
installed = false
|
|
179
212
|
let center = MPRemoteCommandCenter.shared()
|
|
@@ -205,6 +238,13 @@ final class RemoteCommands {
|
|
|
205
238
|
center.changePlaybackPositionCommand.isEnabled = false
|
|
206
239
|
}
|
|
207
240
|
|
|
241
|
+
/// Hands `body` to main, which owns `MPRemoteCommandCenter` and therefore
|
|
242
|
+
/// the target registrations and `installed` flag below. Always asynchronous,
|
|
243
|
+
/// so an install and a capability update issued in order land in order.
|
|
244
|
+
private static func onMain(_ body: @escaping @MainActor () -> Void) {
|
|
245
|
+
DispatchQueue.main.async { MainActor.assumeIsolated(body) }
|
|
246
|
+
}
|
|
247
|
+
|
|
208
248
|
private func dispatch(_ command: Command) {
|
|
209
249
|
assert(Thread.isMainThread,
|
|
210
250
|
"RemoteCommands.dispatch must run on the main thread — " +
|
package/ios/ReplayGainData.swift
CHANGED
|
@@ -14,7 +14,7 @@ enum ReplayGainSelectorMode: Equatable {
|
|
|
14
14
|
/// Parsed ReplayGain tag values for a single track. All four fields
|
|
15
15
|
/// are independent — a track may carry only track tags, only album
|
|
16
16
|
/// tags, or both. The Opus container guard returns nil from
|
|
17
|
-
/// `
|
|
17
|
+
/// `AssetReplayGainReader.read(item:)` rather than producing a
|
|
18
18
|
/// ReplayGainData with one nil pair.
|
|
19
19
|
///
|
|
20
20
|
/// Strict mode (`.album` requires album tag, `.track` requires
|
|
@@ -26,14 +26,33 @@ struct ReplayGainData: Equatable {
|
|
|
26
26
|
/// audio-stage boundary (vDSP / DSP processors operate on Float).
|
|
27
27
|
let trackGainDb: Double?
|
|
28
28
|
/// `REPLAYGAIN_TRACK_PEAK` parsed linear value. `nil` when absent
|
|
29
|
-
/// or rejected
|
|
30
|
-
///
|
|
29
|
+
/// or rejected — `ReplayGainExtractor.acceptPeak` accepts the
|
|
30
|
+
/// `0.001 ... 2.0` band, so a full-scale `1.0` is kept and an
|
|
31
|
+
/// encoder zero (`< 0.001`) is treated as no-peak.
|
|
31
32
|
let trackPeak: Double?
|
|
32
33
|
/// `REPLAYGAIN_ALBUM_GAIN` parsed value in dB. `nil` when absent.
|
|
33
34
|
let albumGainDb: Double?
|
|
34
35
|
/// `REPLAYGAIN_ALBUM_PEAK` parsed linear value. `nil` when absent.
|
|
35
36
|
let albumPeak: Double?
|
|
36
37
|
|
|
38
|
+
/// All-or-nothing merge of consumer-supplied ReplayGain against
|
|
39
|
+
/// values detected from the file, decided **per item** — never per
|
|
40
|
+
/// mode and never per field.
|
|
41
|
+
///
|
|
42
|
+
/// [suppliedClaimed] is `true` when any of the four fields was
|
|
43
|
+
/// present on the consumer's `TrackItem`, captured before any value
|
|
44
|
+
/// was validated. When it is `true` the track's ReplayGain comes
|
|
45
|
+
/// solely from [supplied] and [detected] is discarded in full,
|
|
46
|
+
/// including for the mode the consumer said nothing about.
|
|
47
|
+
/// `suppliedClaimed == true` with `supplied == nil` is a real state
|
|
48
|
+
/// — the consumer owns this track and gave nothing usable — so it
|
|
49
|
+
/// yields no gain and still discards detection.
|
|
50
|
+
static func merge(
|
|
51
|
+
suppliedClaimed: Bool, supplied: ReplayGainData?, detected: ReplayGainData?
|
|
52
|
+
) -> ReplayGainData? {
|
|
53
|
+
return suppliedClaimed ? supplied : detected
|
|
54
|
+
}
|
|
55
|
+
|
|
37
56
|
/// Strict mode-aware selector. See NOTES.md §25.
|
|
38
57
|
///
|
|
39
58
|
/// - `.off`: returns `(nil, nil)` (no-op).
|