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
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
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Reads the four ReplayGain tags off an `AVAsset`'s own metadata,
|
|
5
|
+
/// replacing the byte-prefix probe. AVFoundation surfaces the same
|
|
6
|
+
/// tags under three different shapes depending on container:
|
|
7
|
+
///
|
|
8
|
+
/// - MP3 / ID3v2 `TXXX`: the name lives in the item's `extraAttributes`
|
|
9
|
+
/// info key; the item's own key is the frame id (`TXXX`).
|
|
10
|
+
/// - FLAC / Vorbis comment: the name *is* the item's key.
|
|
11
|
+
/// - M4A / iTunes freeform: the item's key is the reverse-DNS
|
|
12
|
+
/// identifier (`com.apple.iTunes.replaygain_track_gain`).
|
|
13
|
+
///
|
|
14
|
+
/// `tagName(info:key:)` is the pure reduction of those three shapes to
|
|
15
|
+
/// one comparable name, extracted so it is testable without building
|
|
16
|
+
/// an `AVMetadataItem`.
|
|
17
|
+
enum AssetReplayGainReader {
|
|
18
|
+
|
|
19
|
+
/// The four names a derived tag name has to match to be worth
|
|
20
|
+
/// loading a value for. Everything else on the asset is skipped
|
|
21
|
+
/// before its `stringValue` is touched.
|
|
22
|
+
private static let replayGainNames: Set<String> = [
|
|
23
|
+
ReplayGainExtractor.trackGainKey,
|
|
24
|
+
ReplayGainExtractor.trackPeakKey,
|
|
25
|
+
ReplayGainExtractor.albumGainKey,
|
|
26
|
+
ReplayGainExtractor.albumPeakKey,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
/// Read ReplayGain from an item AVPlayer has already prepared.
|
|
30
|
+
///
|
|
31
|
+
/// Returns `nil` unless `item.status == .readyToPlay`. That single
|
|
32
|
+
/// condition is the whole gate: on a prepared item `load(.metadata)`
|
|
33
|
+
/// costs zero requests and zero bytes, while interrogating an
|
|
34
|
+
/// unprepared asset downloads essentially the entire file (measured:
|
|
35
|
+
/// 2.889 MB of a 2.89 MB MP3, 3.776 MB of a 3.78 MB FLAC).
|
|
36
|
+
///
|
|
37
|
+
/// Deliberately NOT gated on `asset.statusOfValue(forKey: "metadata")`:
|
|
38
|
+
/// without `"metadata"` in `automaticallyLoadedAssetKeys` that status
|
|
39
|
+
/// reads `.unknown` even at `.readyToPlay` (measured 8/8), so such a
|
|
40
|
+
/// guard would be permanently false. The key is not set because it
|
|
41
|
+
/// buys nothing — the prepared read is already free.
|
|
42
|
+
///
|
|
43
|
+
/// Values go through `ReplayGainExtractor.rgDataFromTags`, the same
|
|
44
|
+
/// acceptance function every other ReplayGain entry point uses, so
|
|
45
|
+
/// the dB / peak rules cannot drift per source.
|
|
46
|
+
static func read(item: AVPlayerItem) async -> ReplayGainData? {
|
|
47
|
+
guard item.status == .readyToPlay else { return nil }
|
|
48
|
+
// `AVPlayerItem.asset` is a MainActor-isolated property under strict
|
|
49
|
+
// concurrency (`AVAsset` is not Sendable); `status` is explicitly
|
|
50
|
+
// nonisolated. Hop to main for the asset read, then work against the
|
|
51
|
+
// captured reference — same shape as `NowPlayingFormatExtractor.extract`.
|
|
52
|
+
let asset: AVAsset = await MainActor.run { item.asset }
|
|
53
|
+
// Opus applies `OpusHead.output_gain` in the decoder, so applying a
|
|
54
|
+
// `REPLAYGAIN_*` tag on top of it double-corrects (NOTES.md §25).
|
|
55
|
+
//
|
|
56
|
+
// The container is read from `sourceURL`, the media's own URL. A routed
|
|
57
|
+
// track's asset URL is the read-through server's, which carries no
|
|
58
|
+
// extension — asking it would fail this guard open on every Opus track.
|
|
59
|
+
let sourceURL = await MainActor.run { item.sourceURL }
|
|
60
|
+
if let source = sourceURL, let url = URL(string: source), isOpusContainer(url: url) {
|
|
61
|
+
return nil
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let metadata: [AVMetadataItem]
|
|
65
|
+
do {
|
|
66
|
+
metadata = try await asset.load(.metadata)
|
|
67
|
+
} catch {
|
|
68
|
+
return nil
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var tags: [String: String] = [:]
|
|
72
|
+
for entry in metadata {
|
|
73
|
+
// `try?` flattens here — both loaders already return an optional,
|
|
74
|
+
// so a load failure and an absent value arrive as the same `nil`.
|
|
75
|
+
let attributes = try? await entry.load(.extraAttributes)
|
|
76
|
+
let info = attributes?[.info] as? String
|
|
77
|
+
guard let name = tagName(info: info, key: entry.key as? String),
|
|
78
|
+
replayGainNames.contains(name) else { continue }
|
|
79
|
+
guard let value = try? await entry.load(.stringValue) else { continue }
|
|
80
|
+
tags[name] = value
|
|
81
|
+
}
|
|
82
|
+
return ReplayGainExtractor.rgDataFromTags(tags)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Derive the comparable tag name from a metadata item's info key
|
|
86
|
+
/// [info] and its own key [key]: [info] when it is non-blank, else
|
|
87
|
+
/// [key]; then the segment after the last `.`; then uppercased.
|
|
88
|
+
/// Returns `nil` when neither input yields a usable name.
|
|
89
|
+
static func tagName(info: String?, key: String?) -> String? {
|
|
90
|
+
let source: String
|
|
91
|
+
if let info = info, !info.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
92
|
+
source = info.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
93
|
+
} else if let key = key, !key.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
94
|
+
source = key.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
95
|
+
} else {
|
|
96
|
+
return nil
|
|
97
|
+
}
|
|
98
|
+
// Reverse-DNS freeform keys carry the tag name in their last
|
|
99
|
+
// segment; ID3 info fields and Vorbis keys carry no `.` at all and
|
|
100
|
+
// pass through whole.
|
|
101
|
+
let name: String
|
|
102
|
+
if let separator = source.lastIndex(of: ".") {
|
|
103
|
+
name = String(source[source.index(after: separator)...])
|
|
104
|
+
} else {
|
|
105
|
+
name = source
|
|
106
|
+
}
|
|
107
|
+
guard !name.isEmpty else { return nil }
|
|
108
|
+
return name.uppercased()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// True when [url] names an Opus container. Opus carries its gain in
|
|
112
|
+
/// `OpusHead.output_gain`, which the decoder applies, so a
|
|
113
|
+
/// `REPLAYGAIN_*` tag found alongside it would be applied twice.
|
|
114
|
+
static func isOpusContainer(url: URL) -> Bool {
|
|
115
|
+
let ext = url.pathExtension.lowercased()
|
|
116
|
+
return ext == "opus" || ext == "ogg"
|
|
117
|
+
}
|
|
118
|
+
}
|
package/ios/AudioSession.swift
CHANGED
|
@@ -9,7 +9,7 @@ import AVFoundation
|
|
|
9
9
|
/// bus, and calls `activate` + `installObservers` during its own
|
|
10
10
|
/// lifecycle.
|
|
11
11
|
///
|
|
12
|
-
/// Interruption semantics (
|
|
12
|
+
/// Interruption semantics (per Apple's AVAudioSession interruption docs):
|
|
13
13
|
///
|
|
14
14
|
/// - `.began`: system is telling us to pause (incoming call, Siri,
|
|
15
15
|
/// FaceTime, alarm). Callers typically call `pause()` on the
|
|
@@ -54,21 +54,37 @@ final class AudioSession {
|
|
|
54
54
|
case newDeviceAvailable
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
///
|
|
58
|
-
///
|
|
59
|
-
///
|
|
60
|
-
|
|
57
|
+
/// The three callback slots are written by the owner on its player queue
|
|
58
|
+
/// (`configure()` / `destroy()`) and read on main by the notification
|
|
59
|
+
/// observers — and `onError` on the player queue as well — so each read and
|
|
60
|
+
/// write goes through one lock.
|
|
61
|
+
private let callbackLock = NSLock()
|
|
62
|
+
private var interruptionCallback: ((InterruptionKind) -> Void)?
|
|
63
|
+
private var routeChangeCallback: ((RouteChangeKind) -> Void)?
|
|
64
|
+
private var errorCallback: ((AudioSessionError) -> Void)?
|
|
65
|
+
|
|
66
|
+
/// Fires on AVAudioSession interruption notifications, on main.
|
|
67
|
+
var onInterruption: ((InterruptionKind) -> Void)? {
|
|
68
|
+
get { callbackLock.withLock { interruptionCallback } }
|
|
69
|
+
set { callbackLock.withLock { interruptionCallback = newValue } }
|
|
70
|
+
}
|
|
61
71
|
|
|
62
72
|
/// Fires on AVAudioSession route-change notifications for the
|
|
63
|
-
/// subset of reasons the caller should react to.
|
|
64
|
-
var onRouteChange: ((RouteChangeKind) -> Void)?
|
|
73
|
+
/// subset of reasons the caller should react to, on main.
|
|
74
|
+
var onRouteChange: ((RouteChangeKind) -> Void)? {
|
|
75
|
+
get { callbackLock.withLock { routeChangeCallback } }
|
|
76
|
+
set { callbackLock.withLock { routeChangeCallback = newValue } }
|
|
77
|
+
}
|
|
65
78
|
|
|
66
79
|
/// Fires when an AVAudioSession lifecycle call (`setCategory`,
|
|
67
80
|
/// `setActive(true)`, `setActive(false)`) throws. Failure is
|
|
68
81
|
/// non-fatal at the audio-session level — playback itself hasn't
|
|
69
82
|
/// started yet, so the player state machine should not transition
|
|
70
|
-
/// to `.error` on these (do not call `emitState` from this
|
|
71
|
-
var onError: ((AudioSessionError) -> Void)?
|
|
83
|
+
/// to `.error` on these (do not call `emitState` from this callback).
|
|
84
|
+
var onError: ((AudioSessionError) -> Void)? {
|
|
85
|
+
get { callbackLock.withLock { errorCallback } }
|
|
86
|
+
set { callbackLock.withLock { errorCallback = newValue } }
|
|
87
|
+
}
|
|
72
88
|
|
|
73
89
|
private var interruptionObserver: NSObjectProtocol?
|
|
74
90
|
private var routeChangeObserver: NSObjectProtocol?
|
|
@@ -99,20 +115,50 @@ final class AudioSession {
|
|
|
99
115
|
mode: AVAudioSession.Mode = .default,
|
|
100
116
|
options: AVAudioSession.CategoryOptions = []
|
|
101
117
|
) {
|
|
102
|
-
|
|
118
|
+
guard configureCategory(mode: mode, options: options) else { return }
|
|
119
|
+
activateIfNeeded()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Apply the playback category without claiming the session. Returns whether
|
|
123
|
+
/// it succeeded, since activating after a failed category is pointless.
|
|
124
|
+
///
|
|
125
|
+
/// Category and activation are separable on purpose: the category is what
|
|
126
|
+
/// route sharing and the interruption observers need, while activation is
|
|
127
|
+
/// what actually takes audio away from whatever else is playing. Doing both
|
|
128
|
+
/// at configure time silenced another app's audio the moment a consumer
|
|
129
|
+
/// launched, before the user had pressed anything.
|
|
130
|
+
@discardableResult
|
|
131
|
+
func configureCategory(
|
|
132
|
+
mode: AVAudioSession.Mode = .default,
|
|
133
|
+
options: AVAudioSession.CategoryOptions = []
|
|
134
|
+
) -> Bool {
|
|
103
135
|
do {
|
|
104
|
-
try
|
|
136
|
+
try AVAudioSession.sharedInstance().setCategory(.playback, mode: mode, options: options)
|
|
137
|
+
return true
|
|
105
138
|
} catch {
|
|
106
139
|
let desc = String(describing: error)
|
|
107
140
|
NSLog("[AudioSession] setCategory(.playback) failed: %@", desc)
|
|
108
141
|
onError?(.setCategoryFailed(message: desc, nativeCode: (error as NSError).code))
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
return
|
|
142
|
+
// If setCategory failed, setActive is highly likely to fail too — no
|
|
143
|
+
// value in chaining a second error for the same root cause.
|
|
144
|
+
return false
|
|
113
145
|
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// The system took the session for an interruption. A later
|
|
149
|
+
/// `activateIfNeeded` claims it again: the session is no longer ours
|
|
150
|
+
/// whatever this object recorded before the interruption.
|
|
151
|
+
func noteInterrupted() {
|
|
152
|
+
isActive = false
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Claim the audio session. Idempotent: repeated calls after the first are
|
|
156
|
+
/// no-ops, so every playback entry point can call it without bookkeeping.
|
|
157
|
+
func activateIfNeeded() {
|
|
158
|
+
guard !isActive else { return }
|
|
114
159
|
do {
|
|
115
|
-
try
|
|
160
|
+
try AVAudioSession.sharedInstance().setActive(true)
|
|
161
|
+
isActive = true
|
|
116
162
|
} catch {
|
|
117
163
|
let desc = String(describing: error)
|
|
118
164
|
NSLog("[AudioSession] setActive(true) failed: %@", desc)
|
|
@@ -120,11 +166,16 @@ final class AudioSession {
|
|
|
120
166
|
}
|
|
121
167
|
}
|
|
122
168
|
|
|
169
|
+
/// Whether this object has claimed the session. Cleared on deactivate so a
|
|
170
|
+
/// later play re-claims it.
|
|
171
|
+
private var isActive = false
|
|
172
|
+
|
|
123
173
|
/// Deactivate the session + notify other apps they can resume.
|
|
124
174
|
/// Consumers call this during `destroy` so e.g. a paused music
|
|
125
175
|
/// app or podcast player can auto-resume. Same error-logging
|
|
126
176
|
/// rationale as `activate`.
|
|
127
177
|
func deactivate() {
|
|
178
|
+
isActive = false
|
|
128
179
|
do {
|
|
129
180
|
try AVAudioSession.sharedInstance().setActive(
|
|
130
181
|
false, options: [.notifyOthersOnDeactivation])
|