react-native-queue-player 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/build.gradle +7 -0
- package/android/consumer-rules.pro +72 -3
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +26 -22
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +20 -72
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +11 -0
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +660 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +202 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +303 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +394 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +968 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +930 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +940 -0
- package/ios/TrackPlayer+Recovery.swift +196 -0
- package/ios/TrackPlayer+Skip.swift +455 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +267 -0
- package/ios/TrackPlayer+Window.swift +195 -0
- package/ios/TrackPlayer.swift +305 -4610
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer.cast.net
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.net.ConnectivityManager
|
|
5
|
+
import android.net.LinkAddress
|
|
6
|
+
import android.net.LinkProperties
|
|
7
|
+
import android.net.Network
|
|
8
|
+
import android.net.NetworkCapabilities
|
|
9
|
+
import androidx.test.core.app.ApplicationProvider
|
|
10
|
+
import org.junit.Assert.assertEquals
|
|
11
|
+
import org.junit.Assert.assertFalse
|
|
12
|
+
import org.junit.Assert.assertNull
|
|
13
|
+
import org.junit.Assert.assertTrue
|
|
14
|
+
import org.junit.Test
|
|
15
|
+
import org.junit.runner.RunWith
|
|
16
|
+
import org.robolectric.RobolectricTestRunner
|
|
17
|
+
import org.robolectric.Shadows.shadowOf
|
|
18
|
+
import org.robolectric.annotation.Config
|
|
19
|
+
import org.robolectric.shadow.api.Shadow
|
|
20
|
+
import org.robolectric.shadows.ShadowNetwork
|
|
21
|
+
import org.robolectric.shadows.ShadowNetworkCapabilities
|
|
22
|
+
import org.robolectric.util.ReflectionHelpers
|
|
23
|
+
import org.robolectric.util.ReflectionHelpers.ClassParameter
|
|
24
|
+
import java.net.InetAddress
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `currentAddress` is the Wi-Fi network's IPv4 address: the callback is
|
|
28
|
+
* registered for Wi-Fi networks only, the `start()` seed uses the active
|
|
29
|
+
* network only when it is Wi-Fi and not a VPN, and the address follows
|
|
30
|
+
* that network's link properties until it is lost.
|
|
31
|
+
*/
|
|
32
|
+
@RunWith(RobolectricTestRunner::class)
|
|
33
|
+
@Config(sdk = [34], shadows = [ShadowConnectivityManagerRecordingRequests::class])
|
|
34
|
+
class LocalAddressMonitorTest {
|
|
35
|
+
|
|
36
|
+
private val context: Context = ApplicationProvider.getApplicationContext()
|
|
37
|
+
private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
38
|
+
private val shadowCm: ShadowConnectivityManagerRecordingRequests = Shadow.extract(cm)
|
|
39
|
+
|
|
40
|
+
// The shadow's default active network has netId 0; its transport is
|
|
41
|
+
// whatever capabilities the test puts on it. Any other netId is a
|
|
42
|
+
// network that exists but is not the active one.
|
|
43
|
+
private val activeNetwork: Network = ShadowNetwork.newInstance(0)
|
|
44
|
+
private val otherNetwork: Network = ShadowNetwork.newInstance(1)
|
|
45
|
+
|
|
46
|
+
private fun describe(network: Network, transports: List<Int>, ipv4: String, vpn: Boolean = false) {
|
|
47
|
+
val capabilities = ShadowNetworkCapabilities.newInstance()
|
|
48
|
+
transports.forEach { shadowOf(capabilities).addTransportType(it) }
|
|
49
|
+
if (vpn) shadowOf(capabilities).removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
|
50
|
+
shadowCm.setNetworkCapabilities(network, capabilities)
|
|
51
|
+
shadowCm.setLinkProperties(network, linkProperties(ipv4))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private fun linkProperties(vararg addresses: String): LinkProperties {
|
|
55
|
+
val linkAddresses = addresses.map { literal ->
|
|
56
|
+
val inet = InetAddress.getByName(literal)
|
|
57
|
+
ReflectionHelpers.callConstructor(
|
|
58
|
+
LinkAddress::class.java,
|
|
59
|
+
ClassParameter.from(InetAddress::class.java, inet),
|
|
60
|
+
ClassParameter.from(Int::class.javaPrimitiveType, if (inet.address.size == 4) 24 else 64),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
return LinkProperties().apply { setLinkAddresses(linkAddresses) }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
fun `the callback is registered for Wi-Fi networks only`() {
|
|
68
|
+
LocalAddressMonitor(context).start()
|
|
69
|
+
|
|
70
|
+
val request = shadowCm.requests.single()
|
|
71
|
+
assertTrue(request.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
|
|
72
|
+
assertFalse(request.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR))
|
|
73
|
+
assertTrue(request.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test
|
|
77
|
+
fun `start seeds the address from a Wi-Fi active network`() {
|
|
78
|
+
describe(activeNetwork, listOf(NetworkCapabilities.TRANSPORT_WIFI), "192.168.1.10")
|
|
79
|
+
|
|
80
|
+
val monitor = LocalAddressMonitor(context).also { it.start() }
|
|
81
|
+
|
|
82
|
+
assertEquals("192.168.1.10", monitor.currentAddress)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Test
|
|
86
|
+
fun `start does not seed the address from a cellular active network`() {
|
|
87
|
+
describe(otherNetwork, listOf(NetworkCapabilities.TRANSPORT_WIFI), "192.168.1.10")
|
|
88
|
+
describe(activeNetwork, listOf(NetworkCapabilities.TRANSPORT_CELLULAR), "10.0.0.5")
|
|
89
|
+
|
|
90
|
+
val monitor = LocalAddressMonitor(context).also { it.start() }
|
|
91
|
+
|
|
92
|
+
assertNull("a cellular address is unreachable from a LAN receiver", monitor.currentAddress)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Test
|
|
96
|
+
fun `start does not seed the address from a VPN over Wi-Fi`() {
|
|
97
|
+
describe(
|
|
98
|
+
activeNetwork,
|
|
99
|
+
listOf(NetworkCapabilities.TRANSPORT_VPN, NetworkCapabilities.TRANSPORT_WIFI),
|
|
100
|
+
"10.8.0.2",
|
|
101
|
+
vpn = true,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
val monitor = LocalAddressMonitor(context).also { it.start() }
|
|
105
|
+
|
|
106
|
+
assertNull("a tunnel address is unreachable from a LAN receiver", monitor.currentAddress)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Test
|
|
110
|
+
fun `start registers the callback and seeds nothing when there is no active network`() {
|
|
111
|
+
shadowCm.setDefaultNetworkActive(false)
|
|
112
|
+
|
|
113
|
+
val monitor = LocalAddressMonitor(context).also { it.start() }
|
|
114
|
+
|
|
115
|
+
assertEquals(1, shadowCm.networkCallbacks.size)
|
|
116
|
+
assertNull(monitor.currentAddress)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Test
|
|
120
|
+
fun `a matched network's link properties set the address and its loss clears it`() {
|
|
121
|
+
shadowCm.setDefaultNetworkActive(false)
|
|
122
|
+
val monitor = LocalAddressMonitor(context).also { it.start() }
|
|
123
|
+
val callback = shadowCm.networkCallbacks.single()
|
|
124
|
+
|
|
125
|
+
callback.onLinkPropertiesChanged(otherNetwork, linkProperties("192.168.1.20"))
|
|
126
|
+
assertEquals("192.168.1.20", monitor.currentAddress)
|
|
127
|
+
|
|
128
|
+
callback.onLost(otherNetwork)
|
|
129
|
+
assertNull(monitor.currentAddress)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** A Wi-Fi handover reports the new network's link before the old network
|
|
133
|
+
* is lost; the loss of the old one must not clear the new address. */
|
|
134
|
+
@Test
|
|
135
|
+
fun `losing a network other than the one that supplied the address keeps it`() {
|
|
136
|
+
shadowCm.setDefaultNetworkActive(false)
|
|
137
|
+
val monitor = LocalAddressMonitor(context).also { it.start() }
|
|
138
|
+
val callback = shadowCm.networkCallbacks.single()
|
|
139
|
+
val oldWifi = otherNetwork
|
|
140
|
+
val newWifi = ShadowNetwork.newInstance(2)
|
|
141
|
+
|
|
142
|
+
callback.onLinkPropertiesChanged(oldWifi, linkProperties("192.168.1.20"))
|
|
143
|
+
callback.onLinkPropertiesChanged(newWifi, linkProperties("192.168.2.30"))
|
|
144
|
+
callback.onLost(oldWifi)
|
|
145
|
+
assertEquals("192.168.2.30", monitor.currentAddress)
|
|
146
|
+
|
|
147
|
+
callback.onLost(newWifi)
|
|
148
|
+
assertNull(monitor.currentAddress)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@Test
|
|
152
|
+
fun `the address is the first non-loopback IPv4 among the link addresses`() {
|
|
153
|
+
shadowCm.setDefaultNetworkActive(false)
|
|
154
|
+
val monitor = LocalAddressMonitor(context).also { it.start() }
|
|
155
|
+
val callback = shadowCm.networkCallbacks.single()
|
|
156
|
+
|
|
157
|
+
callback.onLinkPropertiesChanged(otherNetwork, linkProperties("fe80::1", "127.0.0.1", "192.168.1.20", "192.168.1.21"))
|
|
158
|
+
assertEquals("192.168.1.20", monitor.currentAddress)
|
|
159
|
+
|
|
160
|
+
callback.onLinkPropertiesChanged(otherNetwork, linkProperties("fe80::1"))
|
|
161
|
+
assertNull(monitor.currentAddress)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@Test
|
|
165
|
+
fun `stop unregisters the callback`() {
|
|
166
|
+
val monitor = LocalAddressMonitor(context).also { it.start() }
|
|
167
|
+
assertEquals(1, shadowCm.networkCallbacks.size)
|
|
168
|
+
|
|
169
|
+
monitor.stop()
|
|
170
|
+
|
|
171
|
+
assertTrue(shadowCm.networkCallbacks.isEmpty())
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer.cast.net
|
|
2
|
+
|
|
3
|
+
import android.net.ConnectivityManager
|
|
4
|
+
import android.net.NetworkRequest
|
|
5
|
+
import android.os.Handler
|
|
6
|
+
import org.robolectric.annotation.Implements
|
|
7
|
+
import org.robolectric.shadows.ShadowConnectivityManager
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A [ShadowConnectivityManager] that keeps the [NetworkRequest] passed to
|
|
11
|
+
* `registerNetworkCallback`; the stock shadow discards it and stores only
|
|
12
|
+
* the callback.
|
|
13
|
+
*/
|
|
14
|
+
@Implements(ConnectivityManager::class)
|
|
15
|
+
class ShadowConnectivityManagerRecordingRequests : ShadowConnectivityManager() {
|
|
16
|
+
|
|
17
|
+
val requests = mutableListOf<NetworkRequest>()
|
|
18
|
+
|
|
19
|
+
override fun registerNetworkCallback(
|
|
20
|
+
request: NetworkRequest,
|
|
21
|
+
networkCallback: ConnectivityManager.NetworkCallback,
|
|
22
|
+
handler: Handler?,
|
|
23
|
+
) {
|
|
24
|
+
requests.add(request)
|
|
25
|
+
super.registerNetworkCallback(request, networkCallback, handler)
|
|
26
|
+
}
|
|
27
|
+
}
|
package/app.plugin.js
CHANGED
|
@@ -331,6 +331,13 @@ const withChromecastSubspecPodfile = (config) =>
|
|
|
331
331
|
},
|
|
332
332
|
]);
|
|
333
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Bonjour type the local-network permission probe advertises and browses.
|
|
336
|
+
* Mirrors `LocalNetworkPermissionProbe.serviceType`; the two must agree or the
|
|
337
|
+
* probe is blocked with nothing to indicate why.
|
|
338
|
+
*/
|
|
339
|
+
const LOCAL_NETWORK_PROBE_SERVICE_TYPE = '_rnqp-lnp._tcp';
|
|
340
|
+
|
|
334
341
|
const withChromecastInfoPlist = (config, receiverAppId) =>
|
|
335
342
|
withInfoPlist(config, (cfg) => {
|
|
336
343
|
const appId = (receiverAppId || 'CC1AD845').toUpperCase();
|
|
@@ -344,7 +351,16 @@ const withChromecastInfoPlist = (config, receiverAppId) =>
|
|
|
344
351
|
const existingBonjour = Array.isArray(cfg.modResults.NSBonjourServices)
|
|
345
352
|
? cfg.modResults.NSBonjourServices
|
|
346
353
|
: [];
|
|
347
|
-
|
|
354
|
+
// The permission probe browses its own service type. iOS refuses to browse
|
|
355
|
+
// a type that is not declared here, so without this entry the probe can
|
|
356
|
+
// never succeed and every consumer reads `undetermined` forever.
|
|
357
|
+
// Must match `LocalNetworkPermissionProbe.serviceType` in
|
|
358
|
+
// ios/Cast/Core/LocalNetworkPermissionProbe.swift.
|
|
359
|
+
const additions = [
|
|
360
|
+
'_googlecast._tcp',
|
|
361
|
+
`_${appId}._googlecast._tcp`,
|
|
362
|
+
LOCAL_NETWORK_PROBE_SERVICE_TYPE,
|
|
363
|
+
];
|
|
348
364
|
for (const s of additions) {
|
|
349
365
|
if (!existingBonjour.includes(s)) existingBonjour.push(s);
|
|
350
366
|
}
|
|
@@ -26,14 +26,18 @@ import AVFoundation
|
|
|
26
26
|
/// standard idiom — no `.m` / `.h` source files introduced.
|
|
27
27
|
private var queueItemIdKey: UInt8 = 0
|
|
28
28
|
private var sourceURLKey: UInt8 = 0
|
|
29
|
+
private var routedThroughReadThroughServerKey: UInt8 = 0
|
|
29
30
|
|
|
31
|
+
/// `nonisolated` throughout: the SDK marks `AVPlayerItem` main-actor isolated
|
|
32
|
+
/// because `asset` is not Sendable, but these stamps are written once at
|
|
33
|
+
/// construction and read on the player queue.
|
|
30
34
|
extension AVPlayerItem {
|
|
31
35
|
/// Lib-internal identity for this AVPlayerItem. Set once at
|
|
32
36
|
/// construction by `AVQueueBuilder.makeItem`; never mutated
|
|
33
37
|
/// after. nil only for AVPlayerItems that did not flow through
|
|
34
38
|
/// the lib's queue construction path (e.g. built directly via
|
|
35
39
|
/// `makeItemRaw`).
|
|
36
|
-
var queueItemId: String? {
|
|
40
|
+
nonisolated var queueItemId: String? {
|
|
37
41
|
get { objc_getAssociatedObject(self, &queueItemIdKey) as? String }
|
|
38
42
|
set {
|
|
39
43
|
objc_setAssociatedObject(
|
|
@@ -41,21 +45,31 @@ extension AVPlayerItem {
|
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
/// The exact `track.url` string this item was built from
|
|
45
|
-
/// verbatim
|
|
46
|
-
///
|
|
47
|
-
///
|
|
48
|
-
///
|
|
49
|
-
///
|
|
50
|
-
///
|
|
51
|
-
|
|
52
|
-
/// remote-built item for its local cache file once it finishes
|
|
53
|
-
/// downloading.
|
|
54
|
-
var sourceURL: String? {
|
|
48
|
+
/// The exact `track.url` string this item was built from, stored
|
|
49
|
+
/// verbatim. A routed item's `AVURLAsset.url` is the read-through
|
|
50
|
+
/// server's `127.0.0.1` address and an unrouted one is a
|
|
51
|
+
/// parse/re-serialize round-trip of the original, so anything that
|
|
52
|
+
/// reads a container, codec or track identity out of a URL reads this:
|
|
53
|
+
/// `NowPlayingFormatExtractor`, `AssetReplayGainReader`, the error
|
|
54
|
+
/// `url` field, and the source diagnostic log.
|
|
55
|
+
nonisolated var sourceURL: String? {
|
|
55
56
|
get { objc_getAssociatedObject(self, &sourceURLKey) as? String }
|
|
56
57
|
set {
|
|
57
58
|
objc_setAssociatedObject(
|
|
58
59
|
self, &sourceURLKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
59
60
|
}
|
|
60
61
|
}
|
|
62
|
+
|
|
63
|
+
/// Whether this item's asset URL is the read-through server's loopback
|
|
64
|
+
/// address rather than `sourceURL` itself. Set once by
|
|
65
|
+
/// `AVQueueBuilder.makeItem`; the routing decision is made at build time
|
|
66
|
+
/// and an item is never re-pointed, so the stamp is what source
|
|
67
|
+
/// classification reads instead of guessing from the server's presence.
|
|
68
|
+
nonisolated var isRoutedThroughReadThroughServer: Bool {
|
|
69
|
+
get { (objc_getAssociatedObject(self, &routedThroughReadThroughServerKey) as? Bool) ?? false }
|
|
70
|
+
set {
|
|
71
|
+
objc_setAssociatedObject(
|
|
72
|
+
self, &routedThroughReadThroughServerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
61
75
|
}
|
package/ios/AVQueueBuilder.swift
CHANGED
|
@@ -12,11 +12,11 @@ import AVFoundation
|
|
|
12
12
|
/// Constructs AVPlayerItem instances from TrackItem descriptors,
|
|
13
13
|
/// applying the PlayerConfig's custom HTTP headers + user agent.
|
|
14
14
|
///
|
|
15
|
-
/// Headers
|
|
16
|
-
///
|
|
17
|
-
///
|
|
18
|
-
///
|
|
19
|
-
///
|
|
15
|
+
/// Headers carry the consumer app's auth on every remote fetch —
|
|
16
|
+
/// asset read, lookahead-cache download, artwork fetch. An unrouted
|
|
17
|
+
/// asset gets them through `AVURLAssetHTTPHeaderFieldsKey` on its
|
|
18
|
+
/// options dictionary; a routed one is built with no asset options,
|
|
19
|
+
/// and the read-through server sends them on its origin leg instead.
|
|
20
20
|
///
|
|
21
21
|
/// User-Agent handling: AVURLAsset has no dedicated UA option. The
|
|
22
22
|
/// standard workaround is to include `User-Agent: <ua>` inside the
|
|
@@ -28,19 +28,20 @@ public enum AVQueueBuilder {
|
|
|
28
28
|
|
|
29
29
|
/// Build AVPlayerItems for every track in `tracks`, applying config
|
|
30
30
|
/// headers + UA on remote URLs. Returns an empty array for an empty
|
|
31
|
-
/// input. Never throws —
|
|
32
|
-
///
|
|
31
|
+
/// input. Never throws — a track whose URL does not parse is dropped
|
|
32
|
+
/// from the result (`makeItem` returns nil), so the engine never sees
|
|
33
|
+
/// it.
|
|
33
34
|
///
|
|
34
|
-
///
|
|
35
|
-
///
|
|
36
|
-
///
|
|
37
|
-
///
|
|
38
|
-
///
|
|
35
|
+
/// With a non-nil `server`, every routable remote track (`isRoutable`)
|
|
36
|
+
/// is built on the read-through server's local URL, and the server
|
|
37
|
+
/// decides per request whether bytes come from the lookahead cache or
|
|
38
|
+
/// the origin. `file://` tracks, `.m3u8` manifests, and every track when
|
|
39
|
+
/// `server` is nil bind `track.url` directly — see `makeItem`.
|
|
39
40
|
static func buildPlayerItems(
|
|
40
41
|
tracks: [TrackItem],
|
|
41
42
|
queueItemIds: [String],
|
|
42
43
|
config: PlayerConfig,
|
|
43
|
-
|
|
44
|
+
server: ReadThroughServer? = nil
|
|
44
45
|
) -> [AVPlayerItem] {
|
|
45
46
|
// Parallel-array invariant: a violation is a programming error
|
|
46
47
|
// we own (QueueState uphold-able at construction). `precondition`
|
|
@@ -51,41 +52,43 @@ public enum AVQueueBuilder {
|
|
|
51
52
|
"AVQueueBuilder.buildPlayerItems requires queueItemIds parallel to tracks (1:1)"
|
|
52
53
|
)
|
|
53
54
|
return zip(tracks, queueItemIds).compactMap { track, id in
|
|
54
|
-
makeItem(for: track, queueItemId: id, config: config,
|
|
55
|
+
makeItem(for: track, queueItemId: id, config: config, server: server)
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
///
|
|
59
|
-
/// inserted into a live `AVQueuePlayer`. When the insert lands in the
|
|
60
|
-
/// immediate-next slot of a playing queue, `AVQueuePlayer` prerolls the
|
|
61
|
-
/// new item on the shared decode/render pipeline while the current item
|
|
62
|
-
/// is still rendering; faulting the asset's `tracks` / `duration` /
|
|
63
|
-
/// `playable` keys in synchronously at that moment briefly disturbs the
|
|
64
|
-
/// currently-playing item. Loading them here first makes that preroll
|
|
65
|
-
/// cheap.
|
|
59
|
+
/// Cancel outstanding key loads on the assets of items being discarded.
|
|
66
60
|
///
|
|
67
|
-
///
|
|
68
|
-
///
|
|
69
|
-
///
|
|
70
|
-
///
|
|
71
|
-
///
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
|
|
81
|
-
}
|
|
82
|
-
_ = await group.next()
|
|
83
|
-
group.cancelAll()
|
|
61
|
+
/// `AVURLAsset.dealloc` cancels them implicitly and **blocks the deallocating
|
|
62
|
+
/// thread while it does** — a dispatch-group wait on the asset inspector,
|
|
63
|
+
/// measured at tens of seconds for a queue of 200 with loads in flight.
|
|
64
|
+
/// Cancelling first makes that wait a no-op. That thread is
|
|
65
|
+
/// `playerQueue`, so the block stalls every queue operation behind it.
|
|
66
|
+
///
|
|
67
|
+
/// Only assets that are genuinely being dropped may be cancelled: a
|
|
68
|
+
/// cancelled asset can never become playable, so an item rebuilt on a reused
|
|
69
|
+
/// asset would stall silently. `keeping` is the set that survives, compared
|
|
70
|
+
/// by identity.
|
|
71
|
+
static func cancelLoading(dropping outgoing: [AVPlayerItem], keeping: [AVPlayerItem] = []) {
|
|
72
|
+
for item in itemsBeingDropped(outgoing: outgoing, keeping: keeping) {
|
|
73
|
+
item.asset.cancelLoading()
|
|
84
74
|
}
|
|
85
75
|
}
|
|
86
76
|
|
|
87
|
-
///
|
|
88
|
-
///
|
|
77
|
+
/// The items in `outgoing` that no entry of `keeping` reuses. Identity, not
|
|
78
|
+
/// equality: two items can wrap the same asset, and it is the asset's
|
|
79
|
+
/// survival that decides whether cancelling it is safe.
|
|
80
|
+
@_spi(QueuePlayerTests)
|
|
81
|
+
public static func itemsBeingDropped(
|
|
82
|
+
outgoing: [AVPlayerItem], keeping: [AVPlayerItem]
|
|
83
|
+
) -> [AVPlayerItem] {
|
|
84
|
+
guard !keeping.isEmpty else { return outgoing }
|
|
85
|
+
let survivingAssets = keeping.map { ObjectIdentifier($0.asset) }
|
|
86
|
+
let surviving = Set(survivingAssets)
|
|
87
|
+
return outgoing.filter { !surviving.contains(ObjectIdentifier($0.asset)) }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Single-item builder behind `buildPlayerItems`. Routes a routable
|
|
91
|
+
/// remote track through `server` when one is supplied.
|
|
89
92
|
///
|
|
90
93
|
/// `queueItemId` is the lib-generated identity for this AVPlayerItem
|
|
91
94
|
/// instance — set on the constructed item via the
|
|
@@ -95,53 +98,68 @@ public enum AVQueueBuilder {
|
|
|
95
98
|
/// position; the value passed here is the corresponding entry.
|
|
96
99
|
/// Never derived from any consumer-supplied TrackItem field —
|
|
97
100
|
/// only `track.url` is required + reliable on consumer data.
|
|
101
|
+
///
|
|
102
|
+
/// The consumer's ReplayGain claim over [track] is resolved and
|
|
103
|
+
/// validated here, once, and stamped on the item alongside those two.
|
|
104
|
+
/// The item is the only thing the audio path holds, and this is the
|
|
105
|
+
/// one place that has both it and the `TrackItem` it came from.
|
|
98
106
|
static func makeItem(
|
|
99
107
|
for track: TrackItem,
|
|
100
108
|
queueItemId: String,
|
|
101
109
|
config: PlayerConfig,
|
|
102
|
-
|
|
110
|
+
server: ReadThroughServer? = nil
|
|
103
111
|
) -> AVPlayerItem? {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// per-item `audioMix(for:)` install (called BEFORE
|
|
122
|
-
// `player.insert`) gets a tap-bearing mix synchronously,
|
|
123
|
-
// which is the only window AVPlayer honours per Apple's
|
|
124
|
-
// contract (`audioMix` changes after the item starts
|
|
125
|
-
// rendering are silently dropped).
|
|
126
|
-
let asset = AVURLAsset(url: cachedURL)
|
|
127
|
-
let item = AVPlayerItem(
|
|
112
|
+
// Remote tracks bind the read-through server's URL, which is stable for
|
|
113
|
+
// the session and never re-pointed. Whether the bytes come from the cache
|
|
114
|
+
// or the origin is the server's decision, made per request — the only
|
|
115
|
+
// moment it can be answered, because the cache fills long after the queue
|
|
116
|
+
// is built. Headers move to the origin leg with it.
|
|
117
|
+
//
|
|
118
|
+
// HLS is excluded: proxying a playlist would re-base its relative segment
|
|
119
|
+
// paths against the local URL. No server means no routing, which is what a
|
|
120
|
+
// consumer running without the lookahead cache gets.
|
|
121
|
+
let routed: URL? = Self.isRoutable(track.url) ? server.flatMap { srv in
|
|
122
|
+
URL(string: track.url).flatMap { srv.localURL(for: $0, sourceURL: track.url) }
|
|
123
|
+
} : nil
|
|
124
|
+
|
|
125
|
+
let item: AVPlayerItem?
|
|
126
|
+
if let routed {
|
|
127
|
+
let asset = AVURLAsset(url: routed)
|
|
128
|
+
let built = AVPlayerItem(
|
|
128
129
|
asset: asset,
|
|
129
|
-
automaticallyLoadedAssetKeys: ["playable", "duration"
|
|
130
|
+
automaticallyLoadedAssetKeys: ["playable", "duration"]
|
|
130
131
|
)
|
|
131
|
-
pinForwardBufferDuration(on:
|
|
132
|
-
|
|
133
|
-
item
|
|
134
|
-
|
|
132
|
+
pinForwardBufferDuration(on: built)
|
|
133
|
+
built.isRoutedThroughReadThroughServer = true
|
|
134
|
+
item = built
|
|
135
|
+
} else {
|
|
136
|
+
item = makeItemRaw(
|
|
137
|
+
urlString: track.url,
|
|
138
|
+
headers: config.httpHeaders,
|
|
139
|
+
userAgent: config.userAgent)
|
|
135
140
|
}
|
|
136
|
-
let item = makeItemRaw(
|
|
137
|
-
urlString: track.url,
|
|
138
|
-
headers: config.httpHeaders,
|
|
139
|
-
userAgent: config.userAgent)
|
|
140
141
|
item?.queueItemId = queueItemId
|
|
142
|
+
// The media's own URL, always — never the transport's. Everything that
|
|
143
|
+
// reads a container, a codec or a track identity out of a URL reads this.
|
|
141
144
|
item?.sourceURL = track.url
|
|
145
|
+
item?.suppliedReplayGain = SuppliedReplayGain.from(track: track)
|
|
142
146
|
return item
|
|
143
147
|
}
|
|
144
148
|
|
|
149
|
+
/// Whether a URL is one the read-through server can carry.
|
|
150
|
+
///
|
|
151
|
+
/// `http(s)` only, and not a manifest: AVFoundation resolves an HLS
|
|
152
|
+
/// playlist's relative segment paths against the URL it fetched the playlist
|
|
153
|
+
/// from, so a proxied playlist would point every segment at the local server
|
|
154
|
+
/// under a token that names none of them.
|
|
155
|
+
static func isRoutable(_ urlString: String) -> Bool {
|
|
156
|
+
guard let url = URL(string: urlString), let scheme = url.scheme?.lowercased(),
|
|
157
|
+
scheme == "http" || scheme == "https"
|
|
158
|
+
else { return false }
|
|
159
|
+
return url.pathExtension.lowercased() != "m3u8"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
145
163
|
/// Apple-acknowledged AVPlayer mitigation: explicitly setting
|
|
146
164
|
/// `preferredForwardBufferDuration = 0.0` on every item closes a
|
|
147
165
|
/// long-standing AVQueuePlayer bug where items can be dropped
|
|
@@ -198,9 +216,24 @@ public enum AVQueueBuilder {
|
|
|
198
216
|
return item
|
|
199
217
|
}
|
|
200
218
|
|
|
201
|
-
///
|
|
202
|
-
///
|
|
203
|
-
///
|
|
219
|
+
/// The consumer's headers with `userAgent` folded in, which is the request
|
|
220
|
+
/// shape every outbound leg uses: asset loads, cache prefetches, and the
|
|
221
|
+
/// read-through server's origin requests.
|
|
222
|
+
@_spi(QueuePlayerTests)
|
|
223
|
+
public static func mergedHeaders(
|
|
224
|
+
headers: [String: String]?,
|
|
225
|
+
userAgent: String?
|
|
226
|
+
) -> [String: String] {
|
|
227
|
+
var merged: [String: String] = headers ?? [:]
|
|
228
|
+
if let ua = userAgent, !ua.isEmpty {
|
|
229
|
+
merged["User-Agent"] = ua
|
|
230
|
+
}
|
|
231
|
+
return merged
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// Plain-Swift options-dictionary builder — takes primitive arguments so
|
|
235
|
+
/// it stays clear of the Swift↔C++ interop boundary.
|
|
236
|
+
/// `makeItemRaw(urlString:headers:userAgent:)`
|
|
204
237
|
/// calls this, and `makeItem(for:, config:)` unpacks its `PlayerConfig`
|
|
205
238
|
/// argument into the same primitive pair and calls `makeItemRaw`.
|
|
206
239
|
///
|
|
@@ -209,18 +242,20 @@ public enum AVQueueBuilder {
|
|
|
209
242
|
///
|
|
210
243
|
/// Key spelling: `AVURLAssetHTTPHeaderFieldsKey` is not a public
|
|
211
244
|
/// Swift API. The raw Obj-C string still works at runtime. See
|
|
212
|
-
/// NOTES.md §7.
|
|
213
|
-
///
|
|
214
|
-
/// `
|
|
245
|
+
/// NOTES.md §7.
|
|
246
|
+
///
|
|
247
|
+
/// No `AVAssetResourceLoaderDelegate` is installed, and none will be: an
|
|
248
|
+
/// asset routed through a loader takes a custom URL scheme, and Apple states
|
|
249
|
+
/// AirPlay is unsupported for one. Remote tracks reach the lookahead cache
|
|
250
|
+
/// through a local HTTP server instead, which keeps AVFoundation's own HTTP
|
|
251
|
+
/// stack — including the redirect following a loader would have to
|
|
252
|
+
/// reimplement.
|
|
215
253
|
@_spi(QueuePlayerTests)
|
|
216
254
|
public static func httpOptionsRaw(
|
|
217
255
|
headers: [String: String]?,
|
|
218
256
|
userAgent: String?
|
|
219
257
|
) -> [String: Any] {
|
|
220
|
-
|
|
221
|
-
if let ua = userAgent, !ua.isEmpty {
|
|
222
|
-
merged["User-Agent"] = ua
|
|
223
|
-
}
|
|
258
|
+
let merged = mergedHeaders(headers: headers, userAgent: userAgent)
|
|
224
259
|
guard !merged.isEmpty else { return [:] }
|
|
225
260
|
return ["AVURLAssetHTTPHeaderFieldsKey": merged]
|
|
226
261
|
}
|
package/ios/ArtworkLoader.swift
CHANGED
|
@@ -127,10 +127,3 @@ final class ArtworkLoader {
|
|
|
127
127
|
private var _configHeaders: [String: String]?
|
|
128
128
|
private var _configUserAgent: String?
|
|
129
129
|
}
|
|
130
|
-
|
|
131
|
-
private extension NSLock {
|
|
132
|
-
func withLock<T>(_ body: () throws -> T) rethrows -> T {
|
|
133
|
-
lock(); defer { unlock() }
|
|
134
|
-
return try body()
|
|
135
|
-
}
|
|
136
|
-
}
|
|
@@ -53,11 +53,10 @@ final class ArtworkResolver {
|
|
|
53
53
|
/// only one the consumer cares about. Threading: completion fires
|
|
54
54
|
/// on main.
|
|
55
55
|
func resolve(url: String?, completion: @escaping (UIImage) -> Void) {
|
|
56
|
-
|
|
57
|
-
let seq = self.currentSeq
|
|
56
|
+
let seq = bumpSeq()
|
|
58
57
|
|
|
59
58
|
loader.load(uri: url) { [weak self] image in
|
|
60
|
-
guard let self = self, self.
|
|
59
|
+
guard let self = self, self.isCurrent(seq) else { return }
|
|
61
60
|
completion(image ?? PlaceholderArtwork.image)
|
|
62
61
|
}
|
|
63
62
|
}
|
|
@@ -68,7 +67,7 @@ final class ArtworkResolver {
|
|
|
68
67
|
/// already in transit will populate the loader's cache for a
|
|
69
68
|
/// future `resolve(...)` that hits the same URL.
|
|
70
69
|
func cancelInFlight() {
|
|
71
|
-
|
|
70
|
+
_ = bumpSeq()
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
/// Clear the underlying loader's cache + drop the in-flight
|
|
@@ -83,5 +82,24 @@ final class ArtworkResolver {
|
|
|
83
82
|
/// Monotonic resolve-call counter. Captured into completion closures
|
|
84
83
|
/// to drop responses for superseded requests, including same-URL
|
|
85
84
|
/// re-resolves that the URL-string compare cannot distinguish.
|
|
85
|
+
/// Callers bump this on the queue that owns playback while the loader
|
|
86
|
+
/// reports completions on main, so the counter is the one piece of state
|
|
87
|
+
/// crossing between them and it carries its own lock.
|
|
88
|
+
private let seqLock = NSLock()
|
|
86
89
|
private var currentSeq: UInt64 = 0
|
|
90
|
+
|
|
91
|
+
/// Advance the sequence and return the new value.
|
|
92
|
+
private func bumpSeq() -> UInt64 {
|
|
93
|
+
seqLock.lock()
|
|
94
|
+
defer { seqLock.unlock() }
|
|
95
|
+
currentSeq &+= 1
|
|
96
|
+
return currentSeq
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Whether `seq` is still the newest request.
|
|
100
|
+
private func isCurrent(_ seq: UInt64) -> Bool {
|
|
101
|
+
seqLock.lock()
|
|
102
|
+
defer { seqLock.unlock() }
|
|
103
|
+
return currentSeq == seq
|
|
104
|
+
}
|
|
87
105
|
}
|