react-native-queue-player 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/consumer-rules.pro +1 -1
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +3 -19
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +8 -69
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +839 -271
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +2 -2
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +660 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +202 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +303 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +394 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +968 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +930 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +940 -0
- package/ios/TrackPlayer+Recovery.swift +196 -0
- package/ios/TrackPlayer+Skip.swift +455 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +267 -0
- package/ios/TrackPlayer+Window.swift +195 -0
- package/ios/TrackPlayer.swift +305 -4619
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Whether the network returning should trigger a retry.
|
|
4
|
+
///
|
|
5
|
+
/// Pure and primitive-typed so it can be asserted directly: the player state
|
|
6
|
+
/// it derives from is a Nitrogen-backed type, whose members are not reachable
|
|
7
|
+
/// from the test module under cxx-interop.
|
|
8
|
+
enum NetworkRecoveryPolicy {
|
|
9
|
+
|
|
10
|
+
/// - Parameters:
|
|
11
|
+
/// - wantsToPlay: the user's transport intent.
|
|
12
|
+
/// - isInterrupted: whether an audio-session interruption is in force.
|
|
13
|
+
/// - isInError: whether the current track has a failure that has not
|
|
14
|
+
/// recovered. Read from the error key rather than the reported state:
|
|
15
|
+
/// the state has moved on by the time a network returns.
|
|
16
|
+
/// - isTransient: whether that failure is one a network can cure
|
|
17
|
+
/// (`PlaybackErrorMapping.isTransient`). A codec, format or auth
|
|
18
|
+
/// failure is not, and rebuilding it on every network edge would
|
|
19
|
+
/// re-surface the same error each time.
|
|
20
|
+
///
|
|
21
|
+
/// All four must hold. Intent alone would start audio that was never
|
|
22
|
+
/// playing when the network happens to change; ignoring the interruption
|
|
23
|
+
/// would start it mid-call; without the error check every interface
|
|
24
|
+
/// handoff would rebuild a perfectly healthy item; and without the
|
|
25
|
+
/// transient check a fatal failure would be retried without end.
|
|
26
|
+
@_spi(QueuePlayerTests)
|
|
27
|
+
public static func shouldRetry(
|
|
28
|
+
wantsToPlay: Bool, isInterrupted: Bool, isInError: Bool, isTransient: Bool
|
|
29
|
+
) -> Bool {
|
|
30
|
+
wantsToPlay && !isInterrupted && isInError && isTransient
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -29,6 +29,7 @@ enum NowPlayingFormatExtractor {
|
|
|
29
29
|
// strict concurrency. Hop to main for the read; the remainder of
|
|
30
30
|
// this function works against the captured asset reference.
|
|
31
31
|
let asset: AVAsset = await MainActor.run { item.asset }
|
|
32
|
+
let sourceURL: String? = await MainActor.run { item.sourceURL }
|
|
32
33
|
let tracks: [AVAssetTrack]
|
|
33
34
|
do {
|
|
34
35
|
tracks = try await asset.load(.tracks)
|
|
@@ -72,7 +73,10 @@ enum NowPlayingFormatExtractor {
|
|
|
72
73
|
if asbd.mBitsPerChannel > 0 { bitsPerChannel = Double(asbd.mBitsPerChannel) }
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
// The media's URL, not the transport's: a routed track's asset URL is the
|
|
77
|
+
// read-through server's, which has no extension to map a container from
|
|
78
|
+
// and is never a file URL even when the bytes come off local disk.
|
|
79
|
+
let url = sourceURL.flatMap { URL(string: $0) } ?? (asset as? AVURLAsset)?.url
|
|
76
80
|
let container = mapContainer(url: url)
|
|
77
81
|
let mimeType = mapMimeType(codec: codec)
|
|
78
82
|
var bitrate: Double? = estimatedDataRate > 0 ? Double(estimatedDataRate) : nil
|
|
@@ -82,15 +86,16 @@ enum NowPlayingFormatExtractor {
|
|
|
82
86
|
// (`kAudioFilePropertyBitRate`) — reliable for CBR, average
|
|
83
87
|
// for VBR-with-Xing. MP3-specific because the local-file
|
|
84
88
|
// AudioFile API needs the format hint up-front.
|
|
85
|
-
// 2.
|
|
86
|
-
//
|
|
87
|
-
// `AudioFileStream` (codec-agnostic).
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
89
|
+
// 2. Local file:// of any other codec: `StreamingBitrateProbe`
|
|
90
|
+
// reads a 256 KB prefix off disk and derives a rate via Core
|
|
91
|
+
// Audio's `AudioFileStream` (codec-agnostic).
|
|
92
|
+
// 3. Remote http(s)://, any codec: `event.averageAudioBitrate` /
|
|
93
|
+
// `event.indicatedBitrate` from the AVPlayer access log —
|
|
94
|
+
// surfaces a real value for HLS variants whose playlist
|
|
95
|
+
// declares BANDWIDTH, and for progressive servers that declare
|
|
96
|
+
// one. A remote source whose server declares nothing has no
|
|
97
|
+
// bitrate: `estimatedDataRate` reports 0 and nothing reads the
|
|
98
|
+
// container header off the wire.
|
|
94
99
|
if bitrate == nil, let url = url {
|
|
95
100
|
if url.isFileURL {
|
|
96
101
|
// Local file: try Core Audio's `AudioFile`-based reader
|
|
@@ -98,8 +103,7 @@ enum NowPlayingFormatExtractor {
|
|
|
98
103
|
// single property read). Falls back to the
|
|
99
104
|
// `AudioFileStream` packet-derived probe for codecs
|
|
100
105
|
// without a dedicated reader OR when AudioFile returned
|
|
101
|
-
// 0 (some transcoded MP3s
|
|
102
|
-
// Subsonic-served files exhibit this).
|
|
106
|
+
// 0 (some server-transcoded MP3s).
|
|
103
107
|
if codec == .mp3 {
|
|
104
108
|
bitrate = await readLocalMP3Bitrate(url: url)
|
|
105
109
|
}
|
|
@@ -111,10 +115,13 @@ enum NowPlayingFormatExtractor {
|
|
|
111
115
|
}
|
|
112
116
|
} else if let scheme = url.scheme?.lowercased(),
|
|
113
117
|
scheme == "http" || scheme == "https" {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
// Remote: nothing here reads bytes off the wire. The library issues
|
|
119
|
+
// no side-band range request for a remote track, so the only source
|
|
120
|
+
// is what the player itself reports — the access log, which declares
|
|
121
|
+
// a bitrate for HLS variants and for progressive servers that send
|
|
122
|
+
// one. Not gated on codec: the access log reports whatever the player
|
|
123
|
+
// negotiated, and MP3 is neither the only nor the usual codec.
|
|
124
|
+
bitrate = readStreamingBitrate(item: item)
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -310,7 +317,7 @@ enum NowPlayingFormatExtractor {
|
|
|
310
317
|
} catch {
|
|
311
318
|
return nil
|
|
312
319
|
}
|
|
313
|
-
guard let info =
|
|
320
|
+
guard let info = FLACStreamInfo.parse(header) else {
|
|
314
321
|
return nil
|
|
315
322
|
}
|
|
316
323
|
guard info.sampleRate > 0, info.totalSamples > 0 else { return nil }
|
|
@@ -351,7 +358,7 @@ enum NowPlayingFormatExtractor {
|
|
|
351
358
|
/// main); `AVPlayerItem.accessLog()` returns a snapshot and is
|
|
352
359
|
/// safe to call from any thread, matching the off-main usage in
|
|
353
360
|
/// `TrackPlayer.swift`'s DEBUG access-log timeline logger.
|
|
354
|
-
static func
|
|
361
|
+
static func readStreamingBitrate(item: AVPlayerItem) -> Double? {
|
|
355
362
|
guard let event = item.accessLog()?.events.last else { return nil }
|
|
356
363
|
let avgAudio: Double = event.averageAudioBitrate
|
|
357
364
|
if avgAudio.isFinite, avgAudio > 0 { return avgAudio }
|
package/ios/NowPlayingInfo.swift
CHANGED
|
@@ -18,15 +18,18 @@ import MediaPlayer
|
|
|
18
18
|
/// interpolation honest across long-running playback, rate changes, and
|
|
19
19
|
/// background-thread suspensions.
|
|
20
20
|
///
|
|
21
|
-
/// Threading:
|
|
22
|
-
///
|
|
23
|
-
///
|
|
21
|
+
/// Threading: callable from any queue. Every public method hands its work
|
|
22
|
+
/// to main, which owns both the Now Playing surface and this object's state;
|
|
23
|
+
/// `TrackPlayer` calls in from `playerQueue`. Player state
|
|
24
|
+
/// is pushed in as a `Snapshot` argument — this writer never reads
|
|
25
|
+
/// player state itself, so it holds no synchronous read edge back into
|
|
26
|
+
/// whichever context owns that state.
|
|
24
27
|
final class NowPlayingInfo {
|
|
25
28
|
|
|
26
|
-
/// Snapshot of player state at emit time. The
|
|
27
|
-
///
|
|
28
|
-
///
|
|
29
|
-
///
|
|
29
|
+
/// Snapshot of player state at emit time. The caller is responsible
|
|
30
|
+
/// for reading every field within the same tick — divergent values
|
|
31
|
+
/// (elapsed taken at t0, rate at t0+Δ) show up on the lock-screen
|
|
32
|
+
/// scrubber as jitter.
|
|
30
33
|
struct Snapshot {
|
|
31
34
|
let track: TrackItem?
|
|
32
35
|
/// Current playback position in seconds. NaN / ±Inf coerce to 0.
|
|
@@ -34,15 +37,13 @@ final class NowPlayingInfo {
|
|
|
34
37
|
/// Total duration in seconds. NaN / ±Inf / ≤0 → omit (signals
|
|
35
38
|
/// live-stream / unknown to the system).
|
|
36
39
|
let durationSeconds: Double
|
|
37
|
-
///
|
|
40
|
+
/// Effective playback rate: the configured speed while playing, 0
|
|
41
|
+
/// otherwise. Not the player's requested rate, which is non-zero while
|
|
42
|
+
/// waiting to play and would extrapolate the scrubber through a stall.
|
|
38
43
|
let rate: Float
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
/// and clears it during `destroy()`.
|
|
43
|
-
var snapshotProvider: (() -> Snapshot)?
|
|
44
|
-
|
|
45
|
-
private var periodicTimer: Timer?
|
|
46
|
+
private var periodicTimer: DispatchSourceTimer?
|
|
46
47
|
/// Subscription to the global cast active-session router. When a
|
|
47
48
|
/// remote session is active, `CastNowPlayingController` owns the
|
|
48
49
|
/// lockscreen surface and this writer becomes a no-op — every
|
|
@@ -54,21 +55,38 @@ final class NowPlayingInfo {
|
|
|
54
55
|
init() {
|
|
55
56
|
castRouterDisposer = PlaybackStateRouter.shared.addActiveChangeListener {
|
|
56
57
|
[weak self] session in
|
|
57
|
-
|
|
58
|
+
let active = (session != nil)
|
|
59
|
+
NowPlayingInfo.onMain { self?.castActive = active }
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/// Hands `body` to main, where this object's state and the Now Playing
|
|
64
|
+
/// surface both live. Always asynchronous, never inline-when-already-on-main:
|
|
65
|
+
/// mixing the two lets a later write land before an earlier one, and the
|
|
66
|
+
/// surface is last-write-wins.
|
|
67
|
+
private static func onMain(_ body: @escaping @MainActor () -> Void) {
|
|
68
|
+
DispatchQueue.main.async { MainActor.assumeIsolated(body) }
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
deinit {
|
|
62
72
|
castRouterDisposer?()
|
|
73
|
+
// libdispatch retains an armed source, so one that is never cancelled
|
|
74
|
+
// keeps waking the main queue for the life of the process.
|
|
75
|
+
periodicTimer?.cancel()
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
/// Full refresh — metadata + position + rate. Call on track-change.
|
|
66
|
-
/// A
|
|
67
|
-
///
|
|
68
|
-
///
|
|
69
|
-
func refreshAll() {
|
|
79
|
+
/// A snapshot with `track == nil` (queue empty / pre-first-track)
|
|
80
|
+
/// clears the dictionary so the lock-screen / control-center surface
|
|
81
|
+
/// goes away.
|
|
82
|
+
func refreshAll(_ snapshot: Snapshot) {
|
|
83
|
+
NowPlayingInfo.onMain { self.refreshAllOnMain(snapshot) }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@MainActor
|
|
87
|
+
private func refreshAllOnMain(_ snapshot: Snapshot) {
|
|
70
88
|
if castActive { return }
|
|
71
|
-
guard
|
|
89
|
+
guard snapshot.track != nil else {
|
|
72
90
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
|
73
91
|
return
|
|
74
92
|
}
|
|
@@ -90,13 +108,16 @@ final class NowPlayingInfo {
|
|
|
90
108
|
/// an artwork-only payload.
|
|
91
109
|
///
|
|
92
110
|
/// `boundsSize: image.size` and `requestHandler` returning the
|
|
93
|
-
/// image unchanged
|
|
94
|
-
///
|
|
95
|
-
///
|
|
96
|
-
///
|
|
97
|
-
/// and found to make no difference on the standalone CarPlay
|
|
98
|
-
/// Simulator's main-art region.
|
|
111
|
+
/// image unchanged is the shape `MPMediaItemArtwork` documents; the
|
|
112
|
+
/// system handles size negotiation. A forced 1024² canvas with an
|
|
113
|
+
/// aspect-fit render makes no difference to the CarPlay main-art
|
|
114
|
+
/// region.
|
|
99
115
|
func applyArtwork(_ image: UIImage) {
|
|
116
|
+
NowPlayingInfo.onMain { self.applyArtworkOnMain(image) }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@MainActor
|
|
120
|
+
private func applyArtworkOnMain(_ image: UIImage) {
|
|
100
121
|
if castActive { return }
|
|
101
122
|
guard var info = MPNowPlayingInfoCenter.default().nowPlayingInfo else { return }
|
|
102
123
|
info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
|
|
@@ -116,9 +137,14 @@ final class NowPlayingInfo {
|
|
|
116
137
|
/// surface stays absent until this method's late re-publish lands
|
|
117
138
|
/// (typically ~100-500ms after play starts, via the state-change
|
|
118
139
|
/// dispatch in [TrackPlayer]).
|
|
119
|
-
func refreshPositionAndRate() {
|
|
140
|
+
func refreshPositionAndRate(_ snapshot: Snapshot) {
|
|
141
|
+
NowPlayingInfo.onMain { self.refreshPositionAndRateOnMain(snapshot) }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@MainActor
|
|
145
|
+
private func refreshPositionAndRateOnMain(_ snapshot: Snapshot) {
|
|
120
146
|
if castActive { return }
|
|
121
|
-
guard
|
|
147
|
+
guard snapshot.track != nil else { return }
|
|
122
148
|
guard var info = MPNowPlayingInfoCenter.default().nowPlayingInfo else { return }
|
|
123
149
|
applyPositionAndRate(snapshot, into: &info)
|
|
124
150
|
if snapshot.durationSeconds.isFinite && snapshot.durationSeconds > 0 {
|
|
@@ -130,31 +156,41 @@ final class NowPlayingInfo {
|
|
|
130
156
|
/// Stops the periodic timer and clears the dictionary. Call on
|
|
131
157
|
/// `destroy()` before the audio session deactivates.
|
|
132
158
|
func clear() {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
/// Schedule the 10s periodic refresh on the main run loop. Idempotent.
|
|
138
|
-
/// `tolerance = 1.0` lets the OS coalesce with other timers without
|
|
139
|
-
/// affecting scrubber accuracy (the system extrapolates from elapsed
|
|
140
|
-
/// + rate between emits). The closure short-circuits when the player
|
|
141
|
-
/// is paused — elapsed is unchanged and rate is already 0, so the
|
|
142
|
-
/// write would be pure noise.
|
|
143
|
-
func startPeriodicTimer() {
|
|
144
|
-
stopPeriodicTimer()
|
|
145
|
-
let timer = Timer(timeInterval: 10, repeats: true) { [weak self] _ in
|
|
146
|
-
guard let self,
|
|
147
|
-
let snapshot = self.snapshotProvider?(),
|
|
148
|
-
snapshot.rate != 0 else { return }
|
|
149
|
-
self.refreshPositionAndRate()
|
|
159
|
+
NowPlayingInfo.onMain {
|
|
160
|
+
self.stopPeriodicTimerOnMain()
|
|
161
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
|
150
162
|
}
|
|
151
|
-
|
|
152
|
-
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Schedule the 10s periodic tick on the main queue. Restarts a running
|
|
166
|
+
/// tick rather than stacking a second one. A 1s leeway lets the OS
|
|
167
|
+
/// coalesce with other timers without affecting scrubber accuracy (the
|
|
168
|
+
/// system extrapolates from elapsed + rate between emits). `onTick`
|
|
169
|
+
/// builds the snapshot on the side that owns player state and pushes the
|
|
170
|
+
/// refresh; it carries the paused short-circuit, since only that side can
|
|
171
|
+
/// tell whether the write would be pure noise.
|
|
172
|
+
func startPeriodicTimer(onTick: @escaping () -> Void) {
|
|
173
|
+
NowPlayingInfo.onMain { self.startPeriodicTimerOnMain(onTick: onTick) }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@MainActor
|
|
177
|
+
private func startPeriodicTimerOnMain(onTick: @escaping () -> Void) {
|
|
178
|
+
stopPeriodicTimerOnMain()
|
|
179
|
+
let timer = DispatchSource.makeTimerSource(queue: .main)
|
|
180
|
+
timer.schedule(deadline: .now() + 10, repeating: 10, leeway: .seconds(1))
|
|
181
|
+
timer.setEventHandler { onTick() }
|
|
153
182
|
periodicTimer = timer
|
|
183
|
+
// The source is resumed exactly once and never suspended — a suspended
|
|
184
|
+
// source traps libdispatch on release.
|
|
185
|
+
timer.resume()
|
|
154
186
|
}
|
|
155
187
|
|
|
156
|
-
|
|
157
|
-
|
|
188
|
+
/// Cancel the tick source. `cancel()` is safe from any thread; calling it
|
|
189
|
+
/// on the source's own queue additionally guarantees no further tick runs
|
|
190
|
+
/// once it returns.
|
|
191
|
+
@MainActor
|
|
192
|
+
private func stopPeriodicTimerOnMain() {
|
|
193
|
+
periodicTimer?.cancel()
|
|
158
194
|
periodicTimer = nil
|
|
159
195
|
}
|
|
160
196
|
|
|
@@ -44,7 +44,17 @@ final class OutputRouteMonitor {
|
|
|
44
44
|
/// Latest classification. Read at engine track-boundary fire
|
|
45
45
|
/// so per-transition fade-vs-cut decisions reflect the current
|
|
46
46
|
/// route without depending on a callback delivery deadline.
|
|
47
|
-
|
|
47
|
+
///
|
|
48
|
+
/// Written here on main as route changes arrive, read from the queue the
|
|
49
|
+
/// engine serialises on, so it carries its own lock.
|
|
50
|
+
var currentClass: OutputClass {
|
|
51
|
+
classLock.lock()
|
|
52
|
+
defer { classLock.unlock() }
|
|
53
|
+
return storedClass
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private let classLock = NSLock()
|
|
57
|
+
private var storedClass: OutputClass = .localMixable
|
|
48
58
|
|
|
49
59
|
/// Convenience accessor — `true` outside AirPlay 2 / CarPlay.
|
|
50
60
|
var isCurrentRouteMixable: Bool {
|
|
@@ -107,8 +117,11 @@ final class OutputRouteMonitor {
|
|
|
107
117
|
let outputs = AVAudioSession.sharedInstance().currentRoute.outputs
|
|
108
118
|
let isNonMixable = outputs.contains { Self.isNonMixableOutput($0) }
|
|
109
119
|
let newClass: OutputClass = isNonMixable ? .nonMixableRemote : .localMixable
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
// One critical section: the flip test reads the value this write replaces.
|
|
121
|
+
classLock.lock()
|
|
122
|
+
let flipped = newClass == .nonMixableRemote && storedClass == .localMixable
|
|
123
|
+
storedClass = newClass
|
|
124
|
+
classLock.unlock()
|
|
112
125
|
if flipped {
|
|
113
126
|
onRouteBecameNonMixable?()
|
|
114
127
|
}
|
|
@@ -47,7 +47,7 @@ final class PendingBrowseRequests {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/// Snapshot the pending parents without consuming. Production
|
|
50
|
-
/// code drains via [drainAll]; this is
|
|
50
|
+
/// code drains via [drainAll]; this is for tests only. Stays
|
|
51
51
|
/// `internal` (test target imports via `@testable import`) —
|
|
52
52
|
/// mirrors the Kotlin `internal fun snapshotForTest` shape on
|
|
53
53
|
/// the Android side.
|
|
@@ -17,7 +17,7 @@ enum PlaceholderArtwork {
|
|
|
17
17
|
|
|
18
18
|
/// The active placeholder: the consumer-supplied image when one is
|
|
19
19
|
/// configured + loadable, otherwise the bundled RNQP placeholder.
|
|
20
|
-
static var image: UIImage { customImage ?? bundledImage }
|
|
20
|
+
static var image: UIImage { lock.withLock { customImage } ?? bundledImage }
|
|
21
21
|
|
|
22
22
|
/// Set (or clear) the consumer-supplied placeholder from
|
|
23
23
|
/// `PlayerConfig.placeholderArtworkUri`. Loaded from a local `file://`
|
|
@@ -26,17 +26,21 @@ enum PlaceholderArtwork {
|
|
|
26
26
|
/// built-in is a local file, and a remote URL would break offline +
|
|
27
27
|
/// change behaviour; anything else (or an unloadable file) leaves the
|
|
28
28
|
/// bundled placeholder in effect. Decode is synchronous, so a very large
|
|
29
|
-
/// consumer image briefly blocks the `configure` call.
|
|
30
|
-
///
|
|
29
|
+
/// consumer image briefly blocks the `configure` call. Called from
|
|
30
|
+
/// `configure` on the player queue; the readers are on main.
|
|
31
31
|
static func setCustom(uri: String?) {
|
|
32
32
|
guard let uri = uri, !uri.isEmpty,
|
|
33
33
|
let url = URL(string: uri), url.scheme?.lowercased() == "file" else {
|
|
34
|
-
customImage = nil
|
|
34
|
+
lock.withLock { customImage = nil }
|
|
35
35
|
return
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
let image = UIImage(contentsOfFile: url.path)
|
|
38
|
+
lock.withLock { customImage = image }
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/// Written from `configure()` on the player queue, read wherever the
|
|
42
|
+
/// now-playing surfaces build their snapshot.
|
|
43
|
+
private static let lock = NSLock()
|
|
40
44
|
private static var customImage: UIImage?
|
|
41
45
|
private static let bundledImage: UIImage = loadImage()
|
|
42
46
|
|