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
|
@@ -26,7 +26,8 @@ import Foundation
|
|
|
26
26
|
/// Threading:
|
|
27
27
|
/// - Consumer registration runs on the JS bridge thread at Hybrid
|
|
28
28
|
/// construction.
|
|
29
|
-
/// - `audioMix(for:)` runs on
|
|
29
|
+
/// - `audioMix(for:)` runs on the mix dispatch target — main by
|
|
30
|
+
/// default, `TrackPlayer`'s `playerQueue` once it wires in.
|
|
30
31
|
/// - `deliverAudioFrame` runs on the audio render thread; the
|
|
31
32
|
/// per-frame fan-out walks an immutable consumer snapshot under
|
|
32
33
|
/// [consumersLock] and is allocation-free.
|
|
@@ -34,8 +35,8 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
34
35
|
|
|
35
36
|
/// Module-internal singleton. Both `Equalizer` and `Visualizer`
|
|
36
37
|
/// register against this instance at Hybrid construction time. The
|
|
37
|
-
/// constructor
|
|
38
|
-
///
|
|
38
|
+
/// constructor (`AudioTapProvider()`) stays available for tests
|
|
39
|
+
/// that need an isolated provider.
|
|
39
40
|
static let shared = AudioTapProvider()
|
|
40
41
|
|
|
41
42
|
/// Consumer of the post-EQ audio pipeline. Equalizer + Visualizer
|
|
@@ -45,15 +46,15 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
45
46
|
final class Consumer {
|
|
46
47
|
/// Returns true when this consumer needs the tap installed on
|
|
47
48
|
/// the active player items right now (EQ enabled / visualizer
|
|
48
|
-
/// has subscribers). Polled from `audioMix(for:)` on the
|
|
49
|
-
///
|
|
49
|
+
/// has subscribers). Polled from `audioMix(for:)` on the mix
|
|
50
|
+
/// dispatch target.
|
|
50
51
|
let wantsTap: () -> Bool
|
|
51
52
|
/// Render-thread callback fired once per render batch with the
|
|
52
53
|
/// post-EQ mono mixdown. Must not allocate, lock, or
|
|
53
54
|
/// synchronously dispatch.
|
|
54
55
|
let onAudioFrame: (UnsafePointer<Float>, Int, Double) -> Void
|
|
55
|
-
/// Fired on the
|
|
56
|
-
/// item becomes the candidate for tap install. Fires for EVERY
|
|
56
|
+
/// Fired on the mix dispatch target from `audioMix(for:)` whenever
|
|
57
|
+
/// a new item becomes the candidate for tap install. Fires for EVERY
|
|
57
58
|
/// candidate item the engine queues, including items where the
|
|
58
59
|
/// tap will not install (HLS, format-detection failure, no
|
|
59
60
|
/// consumer wantsTap). Consumers that need post-install
|
|
@@ -76,8 +77,9 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
76
77
|
/// Lock protecting `currentGains` + `enabled` + `consumers`.
|
|
77
78
|
/// Acquired on the audio render thread (gain snapshot), the JS
|
|
78
79
|
/// bridge (consumer registration + state mutation), and the
|
|
79
|
-
/// playback
|
|
80
|
-
/// section keeps render-thread cost
|
|
80
|
+
/// playback engine's mix path (`audioMix(for:)` on the mix dispatch
|
|
81
|
+
/// target); short critical section keeps render-thread cost
|
|
82
|
+
/// negligible.
|
|
81
83
|
private let stateLock = NSLock()
|
|
82
84
|
|
|
83
85
|
private var currentGains: [Double]
|
|
@@ -100,6 +102,17 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
100
102
|
/// destroy + reconfigure).
|
|
101
103
|
private weak var playbackEngine: PlaybackEngine?
|
|
102
104
|
|
|
105
|
+
/// Dispatch target for the engine-facing mix work in
|
|
106
|
+
/// `wireToPlaybackEngine`, `detachFromEngine` and
|
|
107
|
+
/// `refreshActiveItemMixes`. Defaults to the
|
|
108
|
+
/// main queue; `setMixDispatchTarget` repoints it at whichever
|
|
109
|
+
/// queue serialises player access and `resetMixDispatchTarget`
|
|
110
|
+
/// puts it back.
|
|
111
|
+
private var mixQueue: DispatchQueue = .main
|
|
112
|
+
/// Reports whether the calling thread is already executing on
|
|
113
|
+
/// `mixQueue`, so an in-queue caller runs inline.
|
|
114
|
+
private var mixQueueIsCurrent: () -> Bool = { Thread.isMainThread }
|
|
115
|
+
|
|
103
116
|
/// Live `ReplayGain` state for every queued `AVPlayerItem`. Each
|
|
104
117
|
/// `audioMix(for:)` allocates a fresh `ItemRGState`, attaches it
|
|
105
118
|
/// to the item via objc associated-object so the state's lifetime
|
|
@@ -120,63 +133,97 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
120
133
|
/// `replayGainDataForItem == nil` (the extract is still in flight)
|
|
121
134
|
/// and the cached format never refreshes when RG values eventually
|
|
122
135
|
/// land. Receives the AVPlayerItem the extract completed for.
|
|
123
|
-
var onItemReplayGainDataPopulated: ((AVPlayerItem) -> Void)?
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
///
|
|
128
|
-
///
|
|
129
|
-
|
|
130
|
-
/// rendering (see comment on `GaplessEngine.refreshAllItemMixes`),
|
|
131
|
-
/// so gating the tap on `replayGainMode != .off` meant mode-flip
|
|
132
|
-
/// during playback never actually applied gain to the active track.
|
|
133
|
-
///
|
|
134
|
-
/// The cost of an always-installed tap when RG is off is negligible:
|
|
135
|
-
/// `ReplayGainProcessor.process` early-returns on a unity-gain
|
|
136
|
-
/// branch test at the head of each callback. EQ + Visualizer
|
|
137
|
-
/// consumers still gate their own contribution to the composed tap
|
|
138
|
-
/// closure independently; this Consumer only ensures the tap
|
|
139
|
-
/// scaffolding is in place so the RG processor's gain (which `recomputeWithLock`
|
|
140
|
-
/// can update at any time, thread-safely) is read by the audio
|
|
141
|
-
/// render thread on every buffer batch.
|
|
142
|
-
private var replayGainTapConsumer: Consumer?
|
|
136
|
+
var onItemReplayGainDataPopulated: ((AVPlayerItem) -> Void)? {
|
|
137
|
+
get { stateLock.withLock { replayGainPopulatedCallback } }
|
|
138
|
+
set { stateLock.withLock { replayGainPopulatedCallback = newValue } }
|
|
139
|
+
}
|
|
140
|
+
/// Written by `TrackPlayer` on its player queue; read under `stateLock`
|
|
141
|
+
/// where the extract completes.
|
|
142
|
+
private var replayGainPopulatedCallback: ((AVPlayerItem) -> Void)?
|
|
143
143
|
|
|
144
144
|
init(initialGains: [Double] = Array(repeating: 0, count: EQProcessor.bandCount)) {
|
|
145
145
|
self.currentGains = initialGains
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
146
|
+
// A consumer that always wants the tap, registered for the provider's
|
|
147
|
+
// life, so the tap is installed regardless of EQ / visualizer state and
|
|
148
|
+
// a `setReplayGainMode` off → on transition affects the currently-playing
|
|
149
|
+
// item without re-installing `AVPlayerItem.audioMix` — AVPlayer silently
|
|
150
|
+
// drops a fresh `audioMix` on an item that has begun rendering (see
|
|
151
|
+
// `GaplessEngine.refreshAllItemMixes`). The cost with RG off is
|
|
152
|
+
// negligible: `ReplayGainProcessor.process` early-returns on a unity-gain
|
|
153
|
+
// test at the head of each callback, leaving the pass-through EQ and the
|
|
154
|
+
// mono mixdown. EQ and Visualizer consumers gate their own contribution
|
|
155
|
+
// independently; this one only keeps the tap in place so the RG gain
|
|
156
|
+
// `recomputeWithLock` updates is read by the audio render thread on
|
|
157
|
+
// every buffer batch.
|
|
158
|
+
register(
|
|
159
|
+
Consumer(
|
|
160
|
+
wantsTap: { true },
|
|
161
|
+
onAudioFrame: { _, _, _ in },
|
|
162
|
+
onActiveAssetChange: { _ in }
|
|
163
|
+
))
|
|
153
164
|
}
|
|
154
165
|
|
|
155
166
|
// MARK: - Engine wiring
|
|
156
167
|
|
|
168
|
+
/// Route the provider's engine calls through [queue]. [queue] must
|
|
169
|
+
/// serialise all player access, and [isCurrent] must report whether
|
|
170
|
+
/// the calling thread is already on it — a caller that is runs the
|
|
171
|
+
/// work inline instead of deferring it behind an async hop. Defaults
|
|
172
|
+
/// to the main queue.
|
|
173
|
+
func setMixDispatchTarget(_ queue: DispatchQueue, isCurrent: @escaping () -> Bool) {
|
|
174
|
+
stateLock.lock()
|
|
175
|
+
mixQueue = queue
|
|
176
|
+
mixQueueIsCurrent = isCurrent
|
|
177
|
+
stateLock.unlock()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// Put the engine-facing mix work back on the main queue. The provider
|
|
181
|
+
/// outlives any one player, so a teardown that leaves a player-owned queue
|
|
182
|
+
/// installed here would dispatch the next player's mix work onto a dead one.
|
|
183
|
+
func resetMixDispatchTarget() {
|
|
184
|
+
stateLock.lock()
|
|
185
|
+
mixQueue = .main
|
|
186
|
+
mixQueueIsCurrent = { Thread.isMainThread }
|
|
187
|
+
stateLock.unlock()
|
|
188
|
+
}
|
|
189
|
+
|
|
157
190
|
/// Wire the provider to [engine] so per-item `audioMix(for:)`
|
|
158
191
|
/// queries route through this provider, and so consumers can ask
|
|
159
|
-
/// for an engine-wide refresh when their `wantsTap` flips.
|
|
192
|
+
/// for an engine-wide refresh when their `wantsTap` flips. Hops to
|
|
193
|
+
/// the mix dispatch target if needed — AVFoundation expects
|
|
194
|
+
/// `setAudioMixProvider` on the queue that serialises player access.
|
|
160
195
|
func wireToPlaybackEngine(_ engine: PlaybackEngine) {
|
|
161
196
|
stateLock.lock()
|
|
162
197
|
self.playbackEngine = engine
|
|
198
|
+
let queue = mixQueue
|
|
199
|
+
let isCurrent = mixQueueIsCurrent
|
|
163
200
|
stateLock.unlock()
|
|
164
|
-
|
|
201
|
+
// `setAudioMixProvider` refreshes every item's mix, which walks the
|
|
202
|
+
// player, so it takes the same hop `detachFromEngine` does.
|
|
203
|
+
if isCurrent() {
|
|
204
|
+
engine.setAudioMixProvider(self)
|
|
205
|
+
} else {
|
|
206
|
+
queue.async {
|
|
207
|
+
engine.setAudioMixProvider(self)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
165
210
|
}
|
|
166
211
|
|
|
167
212
|
/// Detach the provider from any previously-wired engine. Hops to
|
|
168
|
-
///
|
|
169
|
-
/// the
|
|
213
|
+
/// the mix dispatch target if needed — AVFoundation expects
|
|
214
|
+
/// `setAudioMixProvider` on the queue that serialises player access.
|
|
170
215
|
func detachFromEngine() {
|
|
171
216
|
stateLock.lock()
|
|
172
217
|
let engine = self.playbackEngine
|
|
173
218
|
self.playbackEngine = nil
|
|
219
|
+
let queue = mixQueue
|
|
220
|
+
let isCurrent = mixQueueIsCurrent
|
|
174
221
|
stateLock.unlock()
|
|
175
222
|
guard let engine = engine else { return }
|
|
176
|
-
if
|
|
223
|
+
if isCurrent() {
|
|
177
224
|
engine.setAudioMixProvider(nil)
|
|
178
225
|
} else {
|
|
179
|
-
|
|
226
|
+
queue.async {
|
|
180
227
|
engine.setAudioMixProvider(nil)
|
|
181
228
|
}
|
|
182
229
|
}
|
|
@@ -185,17 +232,20 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
185
232
|
/// Ask the active engine to re-evaluate every item's audioMix. A
|
|
186
233
|
/// consumer calls this when its `wantsTap` would change (EQ enable
|
|
187
234
|
/// toggle, first visualizer subscriber arriving, last subscriber
|
|
188
|
-
/// dropping). Hops to
|
|
189
|
-
/// + `AVPlayerItem.audioMix` on the
|
|
235
|
+
/// dropping). Hops to the mix dispatch target; AVFoundation expects
|
|
236
|
+
/// `AVPlayer.items()` + `AVPlayerItem.audioMix` on the queue that
|
|
237
|
+
/// serialises player access.
|
|
190
238
|
func refreshActiveItemMixes() {
|
|
191
239
|
stateLock.lock()
|
|
192
240
|
let engine = self.playbackEngine
|
|
241
|
+
let queue = mixQueue
|
|
242
|
+
let isCurrent = mixQueueIsCurrent
|
|
193
243
|
stateLock.unlock()
|
|
194
244
|
guard let engine = engine else { return }
|
|
195
|
-
if
|
|
245
|
+
if isCurrent() {
|
|
196
246
|
engine.refreshAllItemMixes()
|
|
197
247
|
} else {
|
|
198
|
-
|
|
248
|
+
queue.async {
|
|
199
249
|
engine.refreshAllItemMixes()
|
|
200
250
|
}
|
|
201
251
|
}
|
|
@@ -203,10 +253,25 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
203
253
|
|
|
204
254
|
// MARK: - Consumer registration
|
|
205
255
|
|
|
206
|
-
///
|
|
256
|
+
/// Broadcast which asset is actually active, without touching audio mixes.
|
|
257
|
+
///
|
|
258
|
+
/// `audioMix(for:)` announces its argument, which is right when the engine
|
|
259
|
+
/// is attaching the item about to play and wrong when something walks the
|
|
260
|
+
/// queue: the last item walked would be left as the active asset. Callers
|
|
261
|
+
/// that refresh several items re-assert the playing one through here.
|
|
262
|
+
func noteActiveAsset(_ asset: AVAsset?) {
|
|
263
|
+
stateLock.lock()
|
|
264
|
+
let consumerSnapshot = consumers
|
|
265
|
+
stateLock.unlock()
|
|
266
|
+
for consumer in consumerSnapshot {
|
|
267
|
+
consumer.onActiveAssetChange(asset)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Register [consumer] for post-EQ audio frames and active-asset
|
|
207
272
|
/// notifications. The provider holds the consumer strongly until
|
|
208
|
-
/// `unregister` runs. Order of registration does not matter — both
|
|
209
|
-
///
|
|
273
|
+
/// `unregister` runs. Order of registration does not matter — both callbacks
|
|
274
|
+
/// fire under the same snapshot iteration.
|
|
210
275
|
func register(_ consumer: Consumer) {
|
|
211
276
|
stateLock.lock()
|
|
212
277
|
consumers.append(consumer)
|
|
@@ -225,7 +290,7 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
225
290
|
/// true. Snapshots the consumer list under the lock and evaluates
|
|
226
291
|
/// each predicate outside it so a misbehaving predicate cannot
|
|
227
292
|
/// block render-thread fan-out.
|
|
228
|
-
|
|
293
|
+
func anyConsumerWantsTap() -> Bool {
|
|
229
294
|
stateLock.lock()
|
|
230
295
|
let snapshot = consumers
|
|
231
296
|
stateLock.unlock()
|
|
@@ -237,8 +302,8 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
237
302
|
|
|
238
303
|
// MARK: - EQ master gain state
|
|
239
304
|
|
|
240
|
-
/// Lock-protected snapshot of the enabled flag. Called
|
|
241
|
-
/// on every `audioMix(for:)` invocation.
|
|
305
|
+
/// Lock-protected snapshot of the enabled flag. Called on the mix
|
|
306
|
+
/// dispatch target on every `audioMix(for:)` invocation.
|
|
242
307
|
func isEnabledSnapshot() -> Bool {
|
|
243
308
|
stateLock.lock()
|
|
244
309
|
let value = enabled
|
|
@@ -359,14 +424,21 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
359
424
|
}
|
|
360
425
|
|
|
361
426
|
/// Pure gain math per NOTES.md §25. Selects the `(gainDb, peak)`
|
|
362
|
-
/// pair from [data] under [mode] (strict, no fallback), then
|
|
427
|
+
/// pair from [data] under [mode] (strict, no fallback), then caps
|
|
428
|
+
/// the result for clip safety:
|
|
363
429
|
///
|
|
364
430
|
/// ```
|
|
365
|
-
///
|
|
366
|
-
///
|
|
367
|
-
/// linearGain = min(linearGain, 1.0 / peak)
|
|
431
|
+
/// cap = usable peak ? 1.0 / peak : 1.0
|
|
432
|
+
/// linearGain = min(pow(10, gainDb / 20), cap)
|
|
368
433
|
/// ```
|
|
369
434
|
///
|
|
435
|
+
/// A peak is usable inside the `0.001 ... 2.0` band that
|
|
436
|
+
/// `ReplayGainExtractor.acceptPeak` holds — full scale (`1.0`) and
|
|
437
|
+
/// true peaks above it included. With no usable peak the cap sits at
|
|
438
|
+
/// unity, so a boost is suppressed rather than applied unprotected: a
|
|
439
|
+
/// peak cannot be derived from a gain. Attenuation is untouched
|
|
440
|
+
/// either way, since a negative gain can never clip.
|
|
441
|
+
///
|
|
370
442
|
/// Returns 1.0 (unity) whenever the selected mode's gain tag is
|
|
371
443
|
/// absent — RG is inactive for that track.
|
|
372
444
|
///
|
|
@@ -377,11 +449,12 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
377
449
|
static func computeLinearGain(data: ReplayGainData?, mode: ReplayGainSelectorMode) -> Float {
|
|
378
450
|
let (gainDb, peak) = data?.selectGain(mode: mode) ?? (nil, nil)
|
|
379
451
|
guard let gainDb = gainDb else { return 1.0 }
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
452
|
+
let raw = Float(pow(10.0, gainDb / 20.0))
|
|
453
|
+
var cap: Float = 1.0
|
|
454
|
+
if let peak = peak, peak >= 0.001, peak <= 2.0 {
|
|
455
|
+
cap = Float(1.0 / peak)
|
|
383
456
|
}
|
|
384
|
-
return raw
|
|
457
|
+
return min(raw, cap)
|
|
385
458
|
}
|
|
386
459
|
|
|
387
460
|
/// Map the Nitrogen-generated `ReplayGainMode` to the pure-Swift
|
|
@@ -417,31 +490,92 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
417
490
|
item.replayGainState?.snapshotData()
|
|
418
491
|
}
|
|
419
492
|
|
|
420
|
-
///
|
|
421
|
-
///
|
|
422
|
-
///
|
|
423
|
-
///
|
|
424
|
-
///
|
|
425
|
-
///
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
493
|
+
/// Attach per-item ReplayGain state and arm the read of the item's own
|
|
494
|
+
/// metadata. Called from `audioMix(for:)` BEFORE the tap-mix bypass
|
|
495
|
+
/// conditions so RG values populate independently of AVFoundation's
|
|
496
|
+
/// track-loading lifecycle. Re-invocation on the same item is a no-op.
|
|
497
|
+
///
|
|
498
|
+
/// Issues no network read of its own. `AssetReplayGainReader` reads the
|
|
499
|
+
/// metadata AVPlayer has already prepared, which costs zero requests and
|
|
500
|
+
/// zero bytes — so the read is bounded by AVPlayer's own preroll horizon
|
|
501
|
+
/// (measured: exactly one prepared item out of eight enqueued) rather than
|
|
502
|
+
/// by how many items the engine has queued.
|
|
503
|
+
@discardableResult
|
|
504
|
+
func attachReplayGainState(to item: AVPlayerItem) -> ItemRGState {
|
|
505
|
+
if let existing = item.replayGainState { return existing }
|
|
506
|
+
let rgState = ItemRGState(processor: ReplayGainProcessor(), item: item)
|
|
429
507
|
item.replayGainState = rgState
|
|
430
508
|
stateLock.lock()
|
|
431
509
|
activeItemStates.add(rgState)
|
|
432
510
|
stateLock.unlock()
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
rgState
|
|
439
|
-
self.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
511
|
+
|
|
512
|
+
// `.new` only, never `.initial` (NOTES.md §18), paired with the explicit
|
|
513
|
+
// synchronous bootstrap below.
|
|
514
|
+
rgState.statusObservation = item.observe(\.status, options: [.new]) {
|
|
515
|
+
[weak self, weak rgState] observed, _ in
|
|
516
|
+
guard let self = self, let rgState = rgState else { return }
|
|
517
|
+
self.readReplayGainWhenPrepared(state: rgState, item: observed)
|
|
518
|
+
}
|
|
519
|
+
// Explicit synchronous bootstrap, because an observer waiting for a
|
|
520
|
+
// transition that has already happened never fires. `audioMix(for:)`
|
|
521
|
+
// reaches this method from `refreshActiveItemMixes()` as well as from
|
|
522
|
+
// queue construction — on every ReplayGain mode crossing of the off /
|
|
523
|
+
// non-off boundary, and on crossfade cross-attach — and those calls can
|
|
524
|
+
// carry an item that is already `.readyToPlay`. Whether a given release's
|
|
525
|
+
// engine wiring produces such a first attach is not something this method
|
|
526
|
+
// can know, which is exactly why the bootstrap is unconditional.
|
|
527
|
+
readReplayGainWhenPrepared(state: rgState, item: item)
|
|
528
|
+
return rgState
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/// Resolve [item]'s ReplayGain once it is prepared, and deliver it into
|
|
532
|
+
/// [state]. No-op until `.readyToPlay`, and at most once per state — a
|
|
533
|
+
/// prepared item's metadata does not change under it, and both the observer
|
|
534
|
+
/// and the bootstrap can reach a ready item.
|
|
535
|
+
///
|
|
536
|
+
/// The consumer's claim is stamped on the item at queue-build time, and a
|
|
537
|
+
/// claimed item is never read: the merge discards detection in full, so
|
|
538
|
+
/// reading the file's tags could only produce a value to throw away. That
|
|
539
|
+
/// also means the merge happens before the first value is delivered rather
|
|
540
|
+
/// than after — a listener never sees a detected value corrected to a
|
|
541
|
+
/// supplied one.
|
|
542
|
+
///
|
|
543
|
+
/// Delivery is unconditional on the result: a resolve that found nothing
|
|
544
|
+
/// still lands, so the now-playing snapshot refreshes and reports the honest
|
|
545
|
+
/// absence rather than staying at whatever the pre-read snapshot said.
|
|
546
|
+
private func readReplayGainWhenPrepared(state: ItemRGState, item: AVPlayerItem) {
|
|
547
|
+
guard item.status == .readyToPlay else { return }
|
|
548
|
+
guard state.beginReplayGainRead() else { return }
|
|
549
|
+
let supplied = item.suppliedReplayGain
|
|
550
|
+
Task { [weak self] in
|
|
551
|
+
let detected = supplied == nil ? await AssetReplayGainReader.read(item: item) : nil
|
|
552
|
+
guard let self = self else { return }
|
|
553
|
+
let data = ReplayGainData.merge(
|
|
554
|
+
suppliedClaimed: supplied != nil, supplied: supplied?.data, detected: detected
|
|
555
|
+
)
|
|
556
|
+
state.setData(data)
|
|
557
|
+
self.recomputeWithLock(state: state)
|
|
558
|
+
// Required, not optional: this is what refreshes the emitted now-playing
|
|
559
|
+
// snapshot when the read lands. Without it `onNowPlayingFormatChange`
|
|
560
|
+
// reports null ReplayGain forever, while `getNowPlayingFormat()` stays
|
|
561
|
+
// correct because it reads live — so a polling test would not notice.
|
|
562
|
+
//
|
|
563
|
+
// Hopped onto the mix dispatch target rather than fired inline: the
|
|
564
|
+
// bootstrap path runs inside `audioMix(for:)`, which the engine calls
|
|
565
|
+
// mid-`setItems` between `canInsert` and `insert`; the notification
|
|
566
|
+
// reaches JS, and a listener mutating the queue there would mutate
|
|
567
|
+
// the queue the engine is building. The callback is snapshotted here
|
|
568
|
+
// rather than re-read on the target queue so the hop captures a
|
|
569
|
+
// value, not the non-Sendable provider.
|
|
570
|
+
// Both under the lock that guards them: this runs on the cooperative
|
|
571
|
+
// pool, and `mixQueue` is rewritten by `setMixDispatchTarget` and reset
|
|
572
|
+
// by teardown. Reading it unguarded can dispatch player-state work onto
|
|
573
|
+
// a queue that is no longer the one serialising it.
|
|
574
|
+
let (notify, queue) = stateLock.withLock { (self.replayGainPopulatedCallback, self.mixQueue) }
|
|
575
|
+
// The consumer reads the active item and writes the last-emitted format,
|
|
576
|
+
// which is player state — so this lands on the queue that serialises it,
|
|
577
|
+
// not on main.
|
|
578
|
+
queue.async { notify?(item) }
|
|
445
579
|
}
|
|
446
580
|
}
|
|
447
581
|
|
|
@@ -468,16 +602,17 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
468
602
|
// MARK: - AudioMixProvider
|
|
469
603
|
|
|
470
604
|
/// Builds the per-`AVPlayerItem` `AVAudioMix` that carries the EQ
|
|
471
|
-
/// tap + ReplayGain pre-gain stage. Called
|
|
472
|
-
///
|
|
605
|
+
/// tap + ReplayGain pre-gain stage. Called on the mix dispatch
|
|
606
|
+
/// target from the engine's item construction path on every fresh
|
|
607
|
+
/// item. The body
|
|
473
608
|
/// briefly holds `stateLock` three times — once to snapshot the
|
|
474
609
|
/// consumer list for the asset-change fan-out, once inside
|
|
475
610
|
/// `anyConsumerWantsTap` to read the consumer snapshot, once to
|
|
476
611
|
/// register the per-item ReplayGain state. All three critical
|
|
477
612
|
/// sections are constant-time (consumer count is O(2): EQ +
|
|
478
613
|
/// Visualizer) and never call out to JS while the lock is held,
|
|
479
|
-
/// so the
|
|
480
|
-
/// consumer config.
|
|
614
|
+
/// so the cost on the calling queue is sub-millisecond on a
|
|
615
|
+
/// typical consumer config.
|
|
481
616
|
func audioMix(for item: AVPlayerItem) -> AVAudioMix? {
|
|
482
617
|
let asset = item.asset
|
|
483
618
|
// Notify every registered consumer of the candidate asset before
|
|
@@ -490,71 +625,50 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
490
625
|
consumer.onActiveAssetChange(asset)
|
|
491
626
|
}
|
|
492
627
|
|
|
493
|
-
// Eagerly attach RG state +
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
// items whose AVAsset track key hasn't loaded yet.
|
|
501
|
-
ensureReplayGainExtraction(for: item)
|
|
628
|
+
// Eagerly attach RG state + arm the metadata read for this item,
|
|
629
|
+
// regardless of the tap-mix bypass conditions. The read waits on
|
|
630
|
+
// `.readyToPlay`, which is a different and later gate than this
|
|
631
|
+
// method's tracks-loaded / format-descriptions / ASBD checks, so
|
|
632
|
+
// attaching here rather than below them means a mode change or a
|
|
633
|
+
// mix refresh can never leave an item without RG state.
|
|
634
|
+
attachReplayGainState(to: item)
|
|
502
635
|
|
|
503
636
|
if !anyConsumerWantsTap() { return nil }
|
|
504
637
|
|
|
505
638
|
if EQTap.isHLS(asset) { return nil }
|
|
506
639
|
|
|
507
|
-
// Tracks-loaded gate: `
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
// queue-mutation paths invoke this method on
|
|
511
|
-
// for fresh items whose remote tracks
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
// thread free; once tracks load, the engine fires
|
|
640
|
+
// Tracks-loaded gate: `status(of:)` reports the tracks key's
|
|
641
|
+
// current load state without loading it, and the mix has no
|
|
642
|
+
// `AVAssetTrack` to key its input parameters to until that key
|
|
643
|
+
// is loaded. Engine queue-mutation paths invoke this method on
|
|
644
|
+
// the mix dispatch target for fresh items whose remote tracks
|
|
645
|
+
// haven't loaded yet; returning nil rather than waiting on the load
|
|
646
|
+
// keeps the JS bridge free. Once tracks load, the engine fires
|
|
515
647
|
// `engineActiveItemFormatChanged` and the TrackPlayer-level
|
|
516
648
|
// handler re-fires `refreshActiveItemMixes`, which re-evaluates
|
|
517
649
|
// every queued item's mix off the now-loaded tracks key.
|
|
518
|
-
|
|
519
|
-
let status = asset.statusOfValue(forKey: "tracks", error: &keyError)
|
|
520
|
-
guard status == .loaded else { return nil }
|
|
521
|
-
|
|
522
|
-
guard
|
|
523
|
-
let track = asset.tracks(withMediaType: .audio).first,
|
|
524
|
-
let format = track.formatDescriptions.first as CFTypeRef?,
|
|
525
|
-
CFGetTypeID(format) == CMFormatDescriptionGetTypeID()
|
|
526
|
-
else {
|
|
527
|
-
return nil
|
|
528
|
-
}
|
|
529
|
-
let cmFormat = format as! CMFormatDescription
|
|
530
|
-
guard let asbd = readASBD(cmFormat) else { return nil }
|
|
650
|
+
guard case .loaded(let tracks) = asset.status(of: .tracks) else { return nil }
|
|
531
651
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
652
|
+
// An audio track is what the mix keys its input parameters to. The
|
|
653
|
+
// cascade's sample rate and channel count come from the tap's prepare
|
|
654
|
+
// callback instead of the track's advertised format: the tap reports the
|
|
655
|
+
// format it will actually deliver, which is what the DSP has to match,
|
|
656
|
+
// and asking the track means reading a key nothing in the playback
|
|
657
|
+
// lifecycle ever loads.
|
|
658
|
+
guard tracks.contains(where: { $0.mediaType == .audio }) else { return nil }
|
|
537
659
|
|
|
538
660
|
let processor = EQProcessor(provider: self)
|
|
539
|
-
processor.setSampleRate(sampleRate)
|
|
540
|
-
processor.setChannelCount(channelCount)
|
|
541
661
|
// The processor's first tap callback compares its initial
|
|
542
662
|
// version (0) against the provider's current version and pulls
|
|
543
663
|
// the gains under the lock — avoids a double-recompute on item
|
|
544
664
|
// construction.
|
|
545
665
|
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
item.replayGainState = s
|
|
553
|
-
stateLock.lock()
|
|
554
|
-
activeItemStates.add(s)
|
|
555
|
-
stateLock.unlock()
|
|
556
|
-
return s
|
|
557
|
-
}()
|
|
666
|
+
// The audioMix entry point already attached the per-item rgState; reuse it
|
|
667
|
+
// so the tap composition shares the same processor whose gain
|
|
668
|
+
// `recomputeWithLock` updates as the mode flips.
|
|
669
|
+
// Both creation sites route through one function so they cannot diverge:
|
|
670
|
+
// this one is shadowed today only because the other always runs first.
|
|
671
|
+
let rgState = attachReplayGainState(to: item)
|
|
558
672
|
|
|
559
673
|
let eqClosure = processor.tapProcessClosure()
|
|
560
674
|
let rgProcessor = rgState.processor
|
|
@@ -568,23 +682,18 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
568
682
|
return EQTap.audioMix(
|
|
569
683
|
for: item,
|
|
570
684
|
process: composedClosure,
|
|
571
|
-
//
|
|
572
|
-
// callback
|
|
573
|
-
//
|
|
574
|
-
|
|
685
|
+
// Configure the cascade and size the mono-mix scratch buffer on the
|
|
686
|
+
// tap's prepare callback, which runs once ahead of the process loop, so
|
|
687
|
+
// the render thread never allocates and never runs against a rate or
|
|
688
|
+
// channel count the tap is not actually delivering.
|
|
689
|
+
prepare: { frames, format in
|
|
690
|
+
processor.setSampleRate(format.mSampleRate)
|
|
691
|
+
processor.setChannelCount(Int(format.mChannelsPerFrame))
|
|
692
|
+
processor.tapPrepare(maxFrames: frames)
|
|
693
|
+
}
|
|
575
694
|
)
|
|
576
695
|
}
|
|
577
696
|
|
|
578
|
-
/// Pull the AudioStreamBasicDescription from a CMFormatDescription
|
|
579
|
-
/// without forcing a copy. Returns `nil` if the format isn't a
|
|
580
|
-
/// linear-PCM audio format (e.g. compressed source whose
|
|
581
|
-
/// formatDescription wraps the encoded format).
|
|
582
|
-
private func readASBD(_ format: CMFormatDescription) -> AudioStreamBasicDescription? {
|
|
583
|
-
guard let pointer = CMAudioFormatDescriptionGetStreamBasicDescription(format) else {
|
|
584
|
-
return nil
|
|
585
|
-
}
|
|
586
|
-
return pointer.pointee
|
|
587
|
-
}
|
|
588
697
|
}
|
|
589
698
|
|
|
590
699
|
// MARK: - Per-item ReplayGain state
|
|
@@ -596,11 +705,37 @@ final class AudioTapProvider: AudioMixProvider {
|
|
|
596
705
|
final class ItemRGState {
|
|
597
706
|
|
|
598
707
|
let processor: ReplayGainProcessor
|
|
708
|
+
/// The state is an associated object OF the item, so a strong reference here
|
|
709
|
+
/// would be a cycle.
|
|
710
|
+
weak var item: AVPlayerItem?
|
|
711
|
+
/// `\.status` observation on the owning item, installed by
|
|
712
|
+
/// `AudioTapProvider.attachReplayGainState`. Held here so its lifetime
|
|
713
|
+
/// matches the item's — the item owns this state, so the observation dies
|
|
714
|
+
/// with the thing it observes.
|
|
715
|
+
var statusObservation: NSKeyValueObservation?
|
|
599
716
|
private let lock = NSLock()
|
|
600
717
|
private var data: ReplayGainData?
|
|
718
|
+
private var replayGainReadStarted = false
|
|
601
719
|
|
|
602
|
-
init(processor: ReplayGainProcessor) {
|
|
720
|
+
init(processor: ReplayGainProcessor, item: AVPlayerItem? = nil) {
|
|
603
721
|
self.processor = processor
|
|
722
|
+
self.item = item
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
deinit {
|
|
726
|
+
statusObservation?.invalidate()
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/// Claim the one metadata read this state gets. Returns `true` to exactly
|
|
730
|
+
/// one caller; the `\.status` observer and the synchronous bootstrap both
|
|
731
|
+
/// reach a prepared item, and `refreshActiveItemMixes()` can re-run the
|
|
732
|
+
/// bootstrap, so the claim has to be atomic rather than a plain flag check.
|
|
733
|
+
func beginReplayGainRead() -> Bool {
|
|
734
|
+
lock.lock()
|
|
735
|
+
defer { lock.unlock() }
|
|
736
|
+
if replayGainReadStarted { return false }
|
|
737
|
+
replayGainReadStarted = true
|
|
738
|
+
return true
|
|
604
739
|
}
|
|
605
740
|
|
|
606
741
|
/// Update the cached metadata. The render-thread gain push happens
|
package/ios/CarPlayBridge.swift
CHANGED
|
@@ -164,15 +164,3 @@ final class CarPlayBridge {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
// `NSLock.withLock` is iOS 16+; the lib's deployment target sits at
|
|
168
|
-
// iOS 15.1 (per `min_ios_version_supported`), so a backport is the
|
|
169
|
-
// only safe pattern. Drop this when the deployment target advances.
|
|
170
|
-
private extension NSLock {
|
|
171
|
-
@inline(__always)
|
|
172
|
-
func withLock<R>(_ body: () throws -> R) rethrows -> R {
|
|
173
|
-
lock()
|
|
174
|
-
defer { unlock() }
|
|
175
|
-
return try body()
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|