react-native-queue-player 1.2.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/consumer-rules.pro +1 -1
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +3 -19
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +8 -69
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +13 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +985 -276
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaResponseCheckTest.kt +163 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +2 -2
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +672 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +229 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaResponseCheck.swift +122 -0
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MediaResponseCheckTests.swift +128 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +418 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +441 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +1042 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +982 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +951 -0
- package/ios/TrackPlayer+Recovery.swift +213 -0
- package/ios/TrackPlayer+Skip.swift +465 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +272 -0
- package/ios/TrackPlayer+Window.swift +227 -0
- package/ios/TrackPlayer.swift +331 -4619
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
|
@@ -52,6 +52,7 @@ final class CarPlayCoordinator {
|
|
|
52
52
|
|
|
53
53
|
// MARK: - Connection state
|
|
54
54
|
|
|
55
|
+
private let connectionLock = NSLock()
|
|
55
56
|
/// Whether the CarPlay scene is currently connected. Written by
|
|
56
57
|
/// [CarPlaySceneDelegate] at scene attach / detach — the same points that
|
|
57
58
|
/// fire the `onCarConnect` / `onCarDisconnect` callbacks — and read by the
|
|
@@ -63,7 +64,6 @@ final class CarPlayCoordinator {
|
|
|
63
64
|
/// attached/detached flag rather than a connection count (the Android side
|
|
64
65
|
/// counts because its headless controllers — Auto / Wear / Assistant — can
|
|
65
66
|
/// overlap).
|
|
66
|
-
private let connectionLock = NSLock()
|
|
67
67
|
private var carConnectedFlag = false
|
|
68
68
|
|
|
69
69
|
var isCarConnected: Bool {
|
|
@@ -72,9 +72,25 @@ final class CarPlayCoordinator {
|
|
|
72
72
|
return carConnectedFlag
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/// Invoked on the main queue when a scene attaches. `TrackPlayer` uses it
|
|
76
|
+
/// to rebuild the queue mirror, which is only maintained while a car is
|
|
77
|
+
/// attached — without this a car connecting mid-session would show whatever
|
|
78
|
+
/// the mirror held when it was last connected.
|
|
79
|
+
var onCarAttached: (() -> Void)? {
|
|
80
|
+
get { connectionLock.withLock { carAttachedCallback } }
|
|
81
|
+
set { connectionLock.withLock { carAttachedCallback = newValue } }
|
|
82
|
+
}
|
|
83
|
+
/// Written by `TrackPlayer` on its player queue, read here on main.
|
|
84
|
+
private var carAttachedCallback: (() -> Void)?
|
|
85
|
+
|
|
75
86
|
func setCarConnected(_ connected: Bool) {
|
|
76
87
|
connectionLock.lock()
|
|
77
|
-
|
|
88
|
+
let wasConnected = carConnectedFlag
|
|
78
89
|
carConnectedFlag = connected
|
|
90
|
+
let attached = carAttachedCallback
|
|
91
|
+
connectionLock.unlock()
|
|
92
|
+
if connected, !wasConnected {
|
|
93
|
+
attached?()
|
|
94
|
+
}
|
|
79
95
|
}
|
|
80
96
|
}
|
|
@@ -16,8 +16,8 @@ import Foundation
|
|
|
16
16
|
/// the live `BrowseDataProvider`, `CarPlayBridge`,
|
|
17
17
|
/// `PendingBrowseRequests`, and `ArtworkLoader` — all of which are
|
|
18
18
|
/// also written from the JS-side `TrackPlayer` Hybrid path, so the
|
|
19
|
-
/// scene delegate sees fresh data without
|
|
20
|
-
///
|
|
19
|
+
/// scene delegate sees fresh data without anything being injected
|
|
20
|
+
/// through its initialiser.
|
|
21
21
|
/// **Class is `internal` (not `public`) and uses `@objc(<bare-name>)`:**
|
|
22
22
|
/// the auto-generated `<Module>-Swift.h` only emits public Swift
|
|
23
23
|
/// classes; `internal` keeps this one out of the bridging header so
|
|
@@ -71,14 +71,15 @@ final class AirPlayRouteState {
|
|
|
71
71
|
/// dispatching a tap to the picker's internal UIButton. Used by
|
|
72
72
|
/// `CastManager.showSystemPicker()`.
|
|
73
73
|
///
|
|
74
|
-
///
|
|
75
|
-
///
|
|
76
|
-
///
|
|
77
|
-
///
|
|
78
|
-
///
|
|
79
|
-
///
|
|
80
|
-
///
|
|
81
|
-
///
|
|
74
|
+
/// Workaround: `AVRoutePickerView` has no public API to present its
|
|
75
|
+
/// sheet, so this walks the view's subviews for its `UIButton` and
|
|
76
|
+
/// fires its `.touchUpInside` actions. The walk is recursive rather
|
|
77
|
+
/// than a direct child lookup because the button's depth has changed
|
|
78
|
+
/// between iOS releases. When no button is found the call is a no-op;
|
|
79
|
+
/// a consumer that needs a guaranteed sheet renders its own
|
|
80
|
+
/// `AVRoutePickerView`, which is the supported pattern.
|
|
81
|
+
/// TODO: monitor upstream for fixes — https://github.com/react-native-video/react-native-video/issues/3119
|
|
82
|
+
/// (the same subview walk, and the request for a public entry point).
|
|
82
83
|
func showSystemPicker() {
|
|
83
84
|
DispatchQueue.main.async {
|
|
84
85
|
let picker = self.getOrCreatePicker()
|
|
@@ -15,6 +15,14 @@ import NitroModules
|
|
|
15
15
|
/// `setActive`: once a session is removed from the router and its
|
|
16
16
|
/// listeners are disposed, no further events from it can fire
|
|
17
17
|
/// `owner.emitRemoteState`.
|
|
18
|
+
///
|
|
19
|
+
/// The listener and session fields are main-thread state: the Cast SDK
|
|
20
|
+
/// delivers session and listener callbacks on main, and the owner's `start`
|
|
21
|
+
/// and `stop` hop there from its player queue rather than touching the fields
|
|
22
|
+
/// where they are called. The deactivation snapshot is the exception — it is
|
|
23
|
+
/// written from the player queue at handoff and has to be visible to a
|
|
24
|
+
/// deactivation that lands on main right after, so it sits behind a lock.
|
|
25
|
+
/// `seedStartMetadata` reads the owner's track list on the owner's queue.
|
|
18
26
|
final class CastEventBridge {
|
|
19
27
|
|
|
20
28
|
private weak var owner: TrackPlayer?
|
|
@@ -37,11 +45,21 @@ final class CastEventBridge {
|
|
|
37
45
|
/// Receiver position at the moment the active session was cleared.
|
|
38
46
|
/// The local engine can read this to resume from where the
|
|
39
47
|
/// receiver was. Cleared when a new session activates.
|
|
40
|
-
private(set) var lastPositionMsOnDeactivate: Int64
|
|
48
|
+
private(set) var lastPositionMsOnDeactivate: Int64 {
|
|
49
|
+
get { snapshotLock.withLock { positionMsOnDeactivate } }
|
|
50
|
+
set { snapshotLock.withLock { positionMsOnDeactivate = newValue } }
|
|
51
|
+
}
|
|
41
52
|
|
|
42
53
|
/// Receiver-side current track index at deactivation. `-1` when
|
|
43
54
|
/// unknown.
|
|
44
|
-
private(set) var lastTrackIndexOnDeactivate: Int
|
|
55
|
+
private(set) var lastTrackIndexOnDeactivate: Int {
|
|
56
|
+
get { snapshotLock.withLock { trackIndexOnDeactivate } }
|
|
57
|
+
set { snapshotLock.withLock { trackIndexOnDeactivate = newValue } }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private let snapshotLock = NSLock()
|
|
61
|
+
private var positionMsOnDeactivate: Int64 = 0
|
|
62
|
+
private var trackIndexOnDeactivate: Int = -1
|
|
45
63
|
|
|
46
64
|
/// Last lib index emitted for the receiver's current item. Dedups
|
|
47
65
|
/// repeat receiver statuses carrying the same resolved index so
|
|
@@ -54,19 +72,34 @@ final class CastEventBridge {
|
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
func start() {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
onMain { [self] in
|
|
76
|
+
if self.activeChangeDisposer != nil { return }
|
|
77
|
+
self.activeChangeDisposer = PlaybackStateRouter.shared.addActiveChangeListener {
|
|
78
|
+
[weak self] session in
|
|
79
|
+
self?.handleActiveSessionChange(to: session)
|
|
80
|
+
}
|
|
61
81
|
}
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
func stop() {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
onMain {
|
|
86
|
+
self.activeChangeDisposer?()
|
|
87
|
+
self.activeChangeDisposer = nil
|
|
88
|
+
self.sessionListenerDisposers.forEach { $0() }
|
|
89
|
+
self.sessionListenerDisposers.removeAll()
|
|
90
|
+
self.currentActiveSession = nil
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Runs `body` on main, in order with the SDK callbacks that own this
|
|
95
|
+
/// object's state; inline when already there so a caller on main sees its
|
|
96
|
+
/// effect before returning.
|
|
97
|
+
private func onMain(_ body: @escaping () -> Void) {
|
|
98
|
+
if Thread.isMainThread {
|
|
99
|
+
body()
|
|
100
|
+
} else {
|
|
101
|
+
DispatchQueue.main.async(execute: body)
|
|
102
|
+
}
|
|
70
103
|
}
|
|
71
104
|
|
|
72
105
|
/// Seed the deactivation snapshot from the local pre-handoff position +
|
|
@@ -75,13 +108,18 @@ final class CastEventBridge {
|
|
|
75
108
|
/// lands local on the right spot. A receiver item resolved at
|
|
76
109
|
/// deactivation takes precedence.
|
|
77
110
|
func seedDeactivationSnapshot(trackIndex: Int, positionMs: Int64) {
|
|
78
|
-
|
|
79
|
-
|
|
111
|
+
snapshotLock.withLock {
|
|
112
|
+
trackIndexOnDeactivate = trackIndex
|
|
113
|
+
positionMsOnDeactivate = positionMs
|
|
114
|
+
}
|
|
80
115
|
}
|
|
81
116
|
|
|
82
117
|
// MARK: - Active-flow observer
|
|
83
118
|
|
|
84
119
|
private func handleActiveSessionChange(to newSession: CastSessionRemotePlayer?) {
|
|
120
|
+
// The router fires listeners on the caller's thread; every caller today
|
|
121
|
+
// is on main, and the fields below are read and written on that basis.
|
|
122
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
85
123
|
let prior = currentActiveSession
|
|
86
124
|
if newSession == nil, let prior = prior {
|
|
87
125
|
// Capture deactivation snapshot BEFORE tearing down listeners so
|
|
@@ -152,8 +190,7 @@ final class CastEventBridge {
|
|
|
152
190
|
// MARK: - Lockscreen metadata mirror
|
|
153
191
|
|
|
154
192
|
/// Seed the start track's lockscreen metadata after a `setQueue` so the
|
|
155
|
-
/// surface doesn't wait for the first receiver status.
|
|
156
|
-
/// main thread from `TrackPlayer.setQueue`.
|
|
193
|
+
/// surface doesn't wait for the first receiver status.
|
|
157
194
|
func seedStartMetadata(startIndex: Int) {
|
|
158
195
|
pushMetadata(forIndex: startIndex)
|
|
159
196
|
}
|
|
@@ -175,19 +212,22 @@ final class CastEventBridge {
|
|
|
175
212
|
|
|
176
213
|
/// Resolve a lib track index to its `TrackItem` and push its metadata
|
|
177
214
|
/// to the cast lockscreen controller. Out-of-range indices are
|
|
178
|
-
/// ignored (the controller keeps its prior snapshot).
|
|
215
|
+
/// ignored (the controller keeps its prior snapshot). The track list is
|
|
216
|
+
/// read on the queue that owns it, whichever thread this is called from.
|
|
179
217
|
private func pushMetadata(forIndex index: Int) {
|
|
180
218
|
guard let owner = owner else { return }
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
219
|
+
owner.playerQueue.async {
|
|
220
|
+
guard index >= 0, index < owner.tracks.count else { return }
|
|
221
|
+
let track = owner.tracks[index]
|
|
222
|
+
// The controller hops to main itself.
|
|
223
|
+
CastNowPlayingController.shared.updateTrackMetadata(
|
|
224
|
+
title: track.title,
|
|
225
|
+
artist: track.artist,
|
|
226
|
+
album: track.album,
|
|
227
|
+
durationSec: track.duration ?? 0,
|
|
228
|
+
artworkUrl: track.artworkUrl
|
|
229
|
+
)
|
|
230
|
+
}
|
|
191
231
|
}
|
|
192
232
|
|
|
193
233
|
}
|
|
@@ -66,19 +66,34 @@ final class CastNowPlayingController {
|
|
|
66
66
|
/// no-ops. Called from `TrackPlayer.configure` after the engine is
|
|
67
67
|
/// set up.
|
|
68
68
|
func start() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
onMain { [self] in
|
|
70
|
+
if self.activeChangeDisposer != nil { return }
|
|
71
|
+
self.activeChangeDisposer = PlaybackStateRouter.shared.addActiveChangeListener {
|
|
72
|
+
[weak self] session in
|
|
73
|
+
self?.handleActiveChange(to: session)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Every field here is main-thread state — the router and session
|
|
79
|
+
/// callbacks arrive on main — so the owner's entry points, called on its
|
|
80
|
+
/// player queue, hop rather than touch the fields where they are called.
|
|
81
|
+
private func onMain(_ body: @escaping () -> Void) {
|
|
82
|
+
if Thread.isMainThread {
|
|
83
|
+
body()
|
|
84
|
+
} else {
|
|
85
|
+
DispatchQueue.main.async(execute: body)
|
|
73
86
|
}
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
/// Stop observing + tear down any active mirror. Called from
|
|
77
90
|
/// `TrackPlayer.internalDestroy`.
|
|
78
91
|
func stop() {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
onMain {
|
|
93
|
+
self.deactivate()
|
|
94
|
+
self.activeChangeDisposer?()
|
|
95
|
+
self.activeChangeDisposer = nil
|
|
96
|
+
}
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
/// Push current-track metadata. Called by `TrackPlayer` when the
|
|
@@ -92,26 +107,33 @@ final class CastNowPlayingController {
|
|
|
92
107
|
durationSec: Double,
|
|
93
108
|
artworkUrl: String?
|
|
94
109
|
) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
onMain {
|
|
111
|
+
self.currentMetadata = TrackMetadata(
|
|
112
|
+
title: title, artist: artist, album: album,
|
|
113
|
+
durationSec: durationSec, artworkUrl: artworkUrl
|
|
114
|
+
)
|
|
115
|
+
if self.currentSession != nil {
|
|
116
|
+
self.refreshNowPlayingInfo()
|
|
117
|
+
self.resolveArtwork()
|
|
118
|
+
}
|
|
102
119
|
}
|
|
103
120
|
}
|
|
104
121
|
|
|
105
122
|
/// Forward the player's artwork HTTP config so authenticated artwork
|
|
106
123
|
/// URLs resolve during cast. Called from `TrackPlayer.configure`.
|
|
107
124
|
func configureArtwork(headers: [String: String]?, userAgent: String?) {
|
|
108
|
-
|
|
109
|
-
|
|
125
|
+
onMain {
|
|
126
|
+
self.artworkResolver.configHeaders = headers
|
|
127
|
+
self.artworkResolver.configUserAgent = userAgent
|
|
128
|
+
}
|
|
110
129
|
}
|
|
111
130
|
|
|
112
131
|
// MARK: - Active-flow observer
|
|
113
132
|
|
|
114
133
|
private func handleActiveChange(to session: CastSessionRemotePlayer?) {
|
|
134
|
+
// The router fires listeners on the caller's thread; every caller today
|
|
135
|
+
// is on main, and the fields here are read and written on that basis.
|
|
136
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
115
137
|
if let session, !session.isPushSink {
|
|
116
138
|
activate(with: session)
|
|
117
139
|
} else {
|
|
@@ -46,8 +46,8 @@ protocol CastSessionRemotePlayer: CastSession {
|
|
|
46
46
|
var lastState: RemotePlaybackStateKind { get }
|
|
47
47
|
|
|
48
48
|
/// `true` once the receiver has reported any state other than `.idle`.
|
|
49
|
-
///
|
|
50
|
-
///
|
|
49
|
+
/// Set by the first such report and stays set for the session's lifetime;
|
|
50
|
+
/// nothing clears it.
|
|
51
51
|
///
|
|
52
52
|
/// State reads use this to decide whether the receiver can answer yet: a
|
|
53
53
|
/// session that has just activated still holds its initial `.idle`, and the
|
|
@@ -15,27 +15,38 @@ import Foundation
|
|
|
15
15
|
/// Mirrors Android's `internal object CastTransportRouter`.
|
|
16
16
|
enum CastTransportRouter {
|
|
17
17
|
|
|
18
|
+
/// Hands a receiver command to main. The Cast SDK is main-only, while the
|
|
19
|
+
/// decision above it — whether a remote session is live — reads a
|
|
20
|
+
/// lock-guarded snapshot and is safe from any thread, so only the command
|
|
21
|
+
/// itself crosses.
|
|
22
|
+
/// Always asynchronous, never inline-when-already-on-main: mixing the two
|
|
23
|
+
/// lets a command issued later run before one issued earlier — a CarPlay tap
|
|
24
|
+
/// arriving on main would overtake a JS seek that is still enqueued.
|
|
25
|
+
private static func onMain(_ body: @escaping () -> Void) {
|
|
26
|
+
DispatchQueue.main.async(execute: body)
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
static func routePlay() -> Bool {
|
|
19
30
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
20
|
-
remote.play()
|
|
31
|
+
onMain { remote.play() }
|
|
21
32
|
return true
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
static func routePause() -> Bool {
|
|
25
36
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
26
|
-
remote.pause()
|
|
37
|
+
onMain { remote.pause() }
|
|
27
38
|
return true
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
static func routeStop() -> Bool {
|
|
31
42
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
32
|
-
remote.stop()
|
|
43
|
+
onMain { remote.stop() }
|
|
33
44
|
return true
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
static func routeSeekTo(positionMs: Int64) -> Bool {
|
|
37
48
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
38
|
-
remote.seekTo(positionMs: positionMs)
|
|
49
|
+
onMain { remote.seekTo(positionMs: positionMs) }
|
|
39
50
|
return true
|
|
40
51
|
}
|
|
41
52
|
|
|
@@ -46,31 +57,32 @@ enum CastTransportRouter {
|
|
|
46
57
|
/// session for AirPlay/local, where the caller seeks the local engine).
|
|
47
58
|
static func routeSeekBy(deltaMs: Int64) -> Bool {
|
|
48
59
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
49
|
-
remote.seekBy(deltaMs: deltaMs)
|
|
60
|
+
onMain { remote.seekBy(deltaMs: deltaMs) }
|
|
50
61
|
return true
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
static func routeSkipToNext() -> Bool {
|
|
54
65
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
55
|
-
remote.queueNext()
|
|
66
|
+
onMain { remote.queueNext() }
|
|
56
67
|
return true
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
static func routeSkipToPrevious() -> Bool {
|
|
60
71
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
61
|
-
remote.queuePrev()
|
|
72
|
+
onMain { remote.queuePrev() }
|
|
62
73
|
return true
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
static func routeSkipToIndex(index: Int) -> Bool {
|
|
66
77
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
67
|
-
remote.skipToQueueIndex(index: index)
|
|
78
|
+
onMain { remote.skipToQueueIndex(index: index) }
|
|
68
79
|
return true
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
static func routeSetVolume(value: Double) -> Bool {
|
|
72
83
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
73
|
-
|
|
84
|
+
let clamped = Float(max(0.0, min(1.0, value)))
|
|
85
|
+
onMain { remote.setVolume(clamped) }
|
|
74
86
|
return true
|
|
75
87
|
}
|
|
76
88
|
|
|
@@ -17,8 +17,8 @@ import Network
|
|
|
17
17
|
/// Backends call `acquire(sessionId:)` for a `MediaServerHandle`; the
|
|
18
18
|
/// listener starts on first acquire and stops when the last handle
|
|
19
19
|
/// closes. Range / HEAD / Content-Length are handled by the responder.
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/// Thread-safe singleton: all mutable state is guarded by `lock`, so it is
|
|
21
|
+
/// safe to capture in the listener's `@Sendable` dispatch closures.
|
|
22
22
|
final class LocalMediaServer: @unchecked Sendable {
|
|
23
23
|
/// Process-wide singleton — only the Chromecast / DLNA / Sonos
|
|
24
24
|
/// backends acquire handles; AirPlay-only apps never touch this and
|
|
@@ -32,7 +32,6 @@ final class LocalMediaServer: @unchecked Sendable {
|
|
|
32
32
|
private var boundPort: UInt16 = 0
|
|
33
33
|
private var refCount = 0
|
|
34
34
|
private let registry = MediaTokenRegistry()
|
|
35
|
-
private var addressMonitor: LocalAddressMonitor?
|
|
36
35
|
/// Live accepted connections, tracked so `stop()` can cancel any
|
|
37
36
|
/// in-flight transfer. Keyed by identity; each self-removes on finish.
|
|
38
37
|
private var connections: [ObjectIdentifier: MediaHTTPConnection] = [:]
|
|
@@ -43,62 +42,51 @@ final class LocalMediaServer: @unchecked Sendable {
|
|
|
43
42
|
// acquiring async task suspends instead of busy-waiting a cooperative thread.
|
|
44
43
|
private var readinessWaiters: [CheckedContinuation<Void, Never>] = []
|
|
45
44
|
|
|
46
|
-
// `lifecycleListener` is read from acquire/release (which may run on
|
|
47
|
-
// any thread the consumer routes calls through) and set from caller
|
|
48
|
-
// code on app init. Reads + writes go through this dedicated lock so
|
|
49
|
-
// a concurrent set during a refcount flip doesn't race the read.
|
|
50
|
-
private let listenerLock = NSLock()
|
|
51
|
-
private var _lifecycleListener: ((Bool) -> Void)?
|
|
52
|
-
|
|
53
|
-
var lifecycleListener: ((Bool) -> Void)? {
|
|
54
|
-
get {
|
|
55
|
-
listenerLock.lock(); defer { listenerLock.unlock() }
|
|
56
|
-
return _lifecycleListener
|
|
57
|
-
}
|
|
58
|
-
set {
|
|
59
|
-
listenerLock.lock()
|
|
60
|
-
_lifecycleListener = newValue
|
|
61
|
-
listenerLock.unlock()
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private func fireLifecycle(_ active: Bool) {
|
|
66
|
-
listenerLock.lock()
|
|
67
|
-
let cb = _lifecycleListener
|
|
68
|
-
listenerLock.unlock()
|
|
69
|
-
cb?(active)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
45
|
/// OS-assigned port. `nil` when the listener is not bound.
|
|
73
46
|
var serverPortOrNil: Int? {
|
|
74
47
|
lock.lock(); defer { lock.unlock() }
|
|
75
48
|
return boundPort == 0 ? nil : Int(boundPort)
|
|
76
49
|
}
|
|
77
50
|
|
|
78
|
-
/// LAN IPv4 address for receivers to reach
|
|
79
|
-
/// `
|
|
80
|
-
///
|
|
81
|
-
/// doesn't fail just because the monitor's first path-update
|
|
82
|
-
/// callback hasn't fired yet.
|
|
51
|
+
/// LAN IPv4 address for receivers to reach: the first IPv4 on an `en*`
|
|
52
|
+
/// interface, looked up with `getifaddrs` on every read; `nil` when
|
|
53
|
+
/// there is none.
|
|
83
54
|
var currentLanAddress: String? {
|
|
84
|
-
|
|
85
|
-
return LocalAddressMonitor.firstWifiIPv4()
|
|
55
|
+
Self.firstWifiIPv4()
|
|
86
56
|
}
|
|
87
57
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
func
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
58
|
+
/// The first IPv4 address on an `en*` interface (Wi-Fi), from a
|
|
59
|
+
/// synchronous `getifaddrs` scan; `nil` when no such interface has one.
|
|
60
|
+
/// Cellular addresses can't reach a LAN receiver, so other interfaces
|
|
61
|
+
/// are ignored.
|
|
62
|
+
private static func firstWifiIPv4() -> String? {
|
|
63
|
+
var ifaddrPtr: UnsafeMutablePointer<ifaddrs>?
|
|
64
|
+
guard getifaddrs(&ifaddrPtr) == 0, let first = ifaddrPtr else { return nil }
|
|
65
|
+
defer { freeifaddrs(ifaddrPtr) }
|
|
66
|
+
var ptr: UnsafeMutablePointer<ifaddrs>? = first
|
|
67
|
+
while let current = ptr {
|
|
68
|
+
defer { ptr = current.pointee.ifa_next }
|
|
69
|
+
let name = String(cString: current.pointee.ifa_name)
|
|
70
|
+
guard name.hasPrefix("en") else { continue }
|
|
71
|
+
guard let sa = current.pointee.ifa_addr else { continue }
|
|
72
|
+
guard sa.pointee.sa_family == UInt8(AF_INET) else { continue }
|
|
73
|
+
var hostBuf = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
|
74
|
+
let result = getnameinfo(
|
|
75
|
+
sa, socklen_t(sa.pointee.sa_len),
|
|
76
|
+
&hostBuf, socklen_t(hostBuf.count),
|
|
77
|
+
nil, 0, NI_NUMERICHOST
|
|
78
|
+
)
|
|
79
|
+
if result == 0 {
|
|
80
|
+
let host = String(cString: hostBuf)
|
|
81
|
+
if !host.isEmpty && host != "0.0.0.0" { return host }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return nil
|
|
96
85
|
}
|
|
97
86
|
|
|
98
87
|
func acquire(sessionId: String) async -> MediaServerHandle {
|
|
99
88
|
if takeRefCountWasFirst() {
|
|
100
89
|
await startAndWait()
|
|
101
|
-
fireLifecycle(true)
|
|
102
90
|
}
|
|
103
91
|
return MediaServerHandle(server: self, sessionId: sessionId)
|
|
104
92
|
}
|
|
@@ -122,7 +110,6 @@ final class LocalMediaServer: @unchecked Sendable {
|
|
|
122
110
|
lock.unlock()
|
|
123
111
|
if last {
|
|
124
112
|
stop()
|
|
125
|
-
fireLifecycle(false)
|
|
126
113
|
}
|
|
127
114
|
}
|
|
128
115
|
|
|
@@ -130,10 +117,6 @@ final class LocalMediaServer: @unchecked Sendable {
|
|
|
130
117
|
registry.register(sessionId: sessionId, fileURL: fileURL, contentType: contentType)
|
|
131
118
|
}
|
|
132
119
|
|
|
133
|
-
func revoke(_ token: String) {
|
|
134
|
-
registry.revoke(token)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
120
|
// MARK: - Listener lifecycle
|
|
138
121
|
|
|
139
122
|
/// Start the listener (if not already bound) and suspend, bounded, until it
|
|
@@ -186,7 +169,8 @@ final class LocalMediaServer: @unchecked Sendable {
|
|
|
186
169
|
// Bind every interface (Wi-Fi LAN, etc.) on an OS-assigned port — the
|
|
187
170
|
// Cast receiver fetches URLs over the LAN, so unlike a loopback proxy
|
|
188
171
|
// this listener must accept en0 connections. URLs handed to receivers
|
|
189
|
-
//
|
|
172
|
+
// go through `MediaServerHandle.register(fileURL:lanAddress:contentType:)`
|
|
173
|
+
// with the host from `currentLanAddress` (the first IPv4 on an `en*` interface);
|
|
190
174
|
// tokens are 32 hex chars (high entropy), so the LAN-reachable surface
|
|
191
175
|
// can't be enumerated. `allowLocalEndpointReuse` avoids bind churn when
|
|
192
176
|
// a prior listener's socket is still in TIME_WAIT.
|
|
@@ -238,7 +222,12 @@ final class LocalMediaServer: @unchecked Sendable {
|
|
|
238
222
|
}
|
|
239
223
|
|
|
240
224
|
private func accept(_ conn: NWConnection) {
|
|
241
|
-
let responder = MediaHTTPConnection(
|
|
225
|
+
let responder = MediaHTTPConnection(
|
|
226
|
+
connection: conn,
|
|
227
|
+
resolve: { [registry] token in
|
|
228
|
+
registry.resolve(token).map { .file($0.fileURL, contentType: $0.contentType) }
|
|
229
|
+
}
|
|
230
|
+
) { [weak self] in
|
|
242
231
|
self?.untrack(conn)
|
|
243
232
|
}
|
|
244
233
|
// Track BEFORE start(): the responder's finish callback (-> untrack)
|
|
@@ -51,15 +51,17 @@ final class LocalNetworkPermissionProbe {
|
|
|
51
51
|
|
|
52
52
|
init() {}
|
|
53
53
|
|
|
54
|
-
///
|
|
55
|
-
///
|
|
56
|
-
///
|
|
54
|
+
/// The most recently resolved permission state, or `.undetermined` if no
|
|
55
|
+
/// probe has resolved yet.
|
|
56
|
+
///
|
|
57
|
+
/// Deliberately not TTL-gated. The TTL governs whether
|
|
58
|
+
/// `requestPermission()` re-probes; expiring the *answer* as well would
|
|
59
|
+
/// report `.undetermined` for a user who granted access a minute ago, and any
|
|
60
|
+
/// consumer rule of the form "granted suppresses the prompt" would then
|
|
61
|
+
/// invert — re-prompting exactly the people who already said yes.
|
|
57
62
|
var cachedState: CastLocalNetworkPermission {
|
|
58
63
|
stateLock.lock(); defer { stateLock.unlock() }
|
|
59
|
-
|
|
60
|
-
return _cachedState
|
|
61
|
-
}
|
|
62
|
-
return .undetermined
|
|
64
|
+
return _cachedState
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/// Trigger the probe. Returns immediately with a `Promise` that
|
|
@@ -215,6 +217,15 @@ fileprivate final class ProbeRun {
|
|
|
215
217
|
|
|
216
218
|
private func handleBrowserState(_ state: NWBrowser.State) {
|
|
217
219
|
switch state {
|
|
220
|
+
case .waiting(let error):
|
|
221
|
+
// Denial surfaces here, not in `.failed`: refusing local-network access
|
|
222
|
+
// leaves the browser waiting with kDNSServiceErr_PolicyDenied (-65570).
|
|
223
|
+
// `.waiting` is also the ordinary "no connectivity yet" state, so only
|
|
224
|
+
// the error code decides — anything else stays unresolved and lets the
|
|
225
|
+
// timeout or a later state settle it.
|
|
226
|
+
if Self.isLocalNetworkDeniedError(error) {
|
|
227
|
+
resolve(.denied)
|
|
228
|
+
}
|
|
218
229
|
case .failed(let error):
|
|
219
230
|
if Self.isLocalNetworkDeniedError(error) {
|
|
220
231
|
resolve(.denied)
|
|
@@ -252,14 +263,24 @@ fileprivate final class ProbeRun {
|
|
|
252
263
|
for p in resolvedPromises { p.resolve(withResult: state) }
|
|
253
264
|
}
|
|
254
265
|
|
|
255
|
-
/// Detect the
|
|
256
|
-
///
|
|
257
|
-
///
|
|
258
|
-
/// `.
|
|
266
|
+
/// Detect the local-network-denied error.
|
|
267
|
+
///
|
|
268
|
+
/// A denied browse reports `kDNSServiceErr_PolicyDenied` (-65570) as
|
|
269
|
+
/// `NWError.dns`, which is the form that actually arrives on the `.waiting`
|
|
270
|
+
/// path. `NWError.posix(.EACCES)` is kept for the listener side, which
|
|
271
|
+
/// reports the same refusal as a POSIX error. Everything else (no Wi-Fi,
|
|
272
|
+
/// system error) is not a denial and falls through.
|
|
259
273
|
static func isLocalNetworkDeniedError(_ error: NWError) -> Bool {
|
|
260
274
|
if case .posix(let code) = error, code == .EACCES {
|
|
261
275
|
return true
|
|
262
276
|
}
|
|
277
|
+
if case .dns(let code) = error, code == Self.dnsPolicyDenied {
|
|
278
|
+
return true
|
|
279
|
+
}
|
|
263
280
|
return false
|
|
264
281
|
}
|
|
282
|
+
|
|
283
|
+
/// `kDNSServiceErr_PolicyDenied`. Spelled out rather than imported so the
|
|
284
|
+
/// probe does not pull in dnssd for one constant.
|
|
285
|
+
private static let dnsPolicyDenied: DNSServiceErrorType = -65570
|
|
265
286
|
}
|
|
@@ -28,12 +28,6 @@ final class MediaTokenRegistry {
|
|
|
28
28
|
return entries[token]
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
func revoke(_ token: String) {
|
|
32
|
-
lock.lock()
|
|
33
|
-
entries.removeValue(forKey: token)
|
|
34
|
-
lock.unlock()
|
|
35
|
-
}
|
|
36
|
-
|
|
37
31
|
func revokeAll(sessionId: String) {
|
|
38
32
|
lock.lock()
|
|
39
33
|
entries = entries.filter { $0.value.sessionId != sessionId }
|