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,228 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
|
|
4
|
+
/// Unit tests for `AssetReplayGainReader.tagName(info:key:)` — the pure
|
|
5
|
+
/// reduction of AVFoundation's three container-dependent metadata shapes
|
|
6
|
+
/// to one comparable ReplayGain tag name.
|
|
7
|
+
///
|
|
8
|
+
/// The shapes below are measured, not assumed:
|
|
9
|
+
/// - MP3 / ID3v2 `TXXX`: `info = REPLAYGAIN_TRACK_GAIN`, `key = TXXX`
|
|
10
|
+
/// - FLAC / Vorbis: `info = nil`, `key = REPLAYGAIN_TRACK_GAIN`
|
|
11
|
+
/// - M4A / iTunes freeform: `info = nil`,
|
|
12
|
+
/// `key = com.apple.iTunes.replaygain_track_gain`
|
|
13
|
+
///
|
|
14
|
+
/// The rule: `info` when non-blank, else `key`; then the segment after
|
|
15
|
+
/// the last `.`; then uppercased.
|
|
16
|
+
final class AssetReplayGainReaderTests: XCTestCase {
|
|
17
|
+
|
|
18
|
+
private let replayGainNames = [
|
|
19
|
+
ReplayGainExtractor.trackGainKey,
|
|
20
|
+
ReplayGainExtractor.trackPeakKey,
|
|
21
|
+
ReplayGainExtractor.albumGainKey,
|
|
22
|
+
ReplayGainExtractor.albumPeakKey,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
private func assertNotAReplayGainName(
|
|
26
|
+
_ actual: String?,
|
|
27
|
+
_ message: String,
|
|
28
|
+
file: StaticString = #file, line: UInt = #line
|
|
29
|
+
) {
|
|
30
|
+
guard let actual = actual else { return }
|
|
31
|
+
XCTAssertFalse(
|
|
32
|
+
replayGainNames.contains(actual), message, file: file, line: line
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Every test below whose only other assertions are negative pairs
|
|
37
|
+
/// them with this. `assertNotAReplayGainName` is satisfied by `nil`,
|
|
38
|
+
/// so without a derivation that demonstrably works, "this key does
|
|
39
|
+
/// not resolve to a ReplayGain name" is equally satisfied by a
|
|
40
|
+
/// `tagName` that resolves nothing at all.
|
|
41
|
+
private func assertNameDerivationIsLive(
|
|
42
|
+
file: StaticString = #file, line: UInt = #line
|
|
43
|
+
) {
|
|
44
|
+
XCTAssertEqual(
|
|
45
|
+
AssetReplayGainReader.tagName(info: nil, key: "REPLAYGAIN_TRACK_GAIN"),
|
|
46
|
+
ReplayGainExtractor.trackGainKey,
|
|
47
|
+
"positive control — a known-good key must resolve to its name",
|
|
48
|
+
file: file, line: line
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// MARK: - The three measured container shapes
|
|
53
|
+
|
|
54
|
+
func testAnID3UserTextFrameTakesItsNameFromTheInfoField() {
|
|
55
|
+
XCTAssertEqual(
|
|
56
|
+
AssetReplayGainReader.tagName(
|
|
57
|
+
info: "REPLAYGAIN_TRACK_GAIN", key: "TXXX"
|
|
58
|
+
),
|
|
59
|
+
ReplayGainExtractor.trackGainKey,
|
|
60
|
+
"MP3: the frame id is TXXX; the name is in the info field"
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func testAVorbisCommentTakesItsNameFromTheKey() {
|
|
65
|
+
XCTAssertEqual(
|
|
66
|
+
AssetReplayGainReader.tagName(
|
|
67
|
+
info: nil, key: "REPLAYGAIN_TRACK_GAIN"
|
|
68
|
+
),
|
|
69
|
+
ReplayGainExtractor.trackGainKey,
|
|
70
|
+
"FLAC: the key is already the name"
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func testAnM4AFreeformKeyReducesToItsLastDotSegment() {
|
|
75
|
+
XCTAssertEqual(
|
|
76
|
+
AssetReplayGainReader.tagName(
|
|
77
|
+
info: nil, key: "com.apple.iTunes.replaygain_track_gain"
|
|
78
|
+
),
|
|
79
|
+
ReplayGainExtractor.trackGainKey,
|
|
80
|
+
"M4A: reverse-DNS identifier, trimmed to its last segment and uppercased"
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func testAllFourM4AFreeformKeysReduceToTheirReplayGainNames() {
|
|
85
|
+
let cases: [(String, String)] = [
|
|
86
|
+
("com.apple.iTunes.replaygain_track_gain", ReplayGainExtractor.trackGainKey),
|
|
87
|
+
("com.apple.iTunes.replaygain_track_peak", ReplayGainExtractor.trackPeakKey),
|
|
88
|
+
("com.apple.iTunes.replaygain_album_gain", ReplayGainExtractor.albumGainKey),
|
|
89
|
+
("com.apple.iTunes.replaygain_album_peak", ReplayGainExtractor.albumPeakKey),
|
|
90
|
+
]
|
|
91
|
+
for (key, expected) in cases {
|
|
92
|
+
XCTAssertEqual(
|
|
93
|
+
AssetReplayGainReader.tagName(info: nil, key: key), expected,
|
|
94
|
+
"\(key) must reduce to \(expected)"
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// MARK: - Info / key precedence
|
|
100
|
+
|
|
101
|
+
func testABlankInfoFieldFallsBackToTheKey() {
|
|
102
|
+
// Empty and whitespace-only are both blank — a nil check alone lets
|
|
103
|
+
// an empty info field shadow a usable key.
|
|
104
|
+
XCTAssertEqual(
|
|
105
|
+
AssetReplayGainReader.tagName(
|
|
106
|
+
info: "", key: "REPLAYGAIN_TRACK_GAIN"
|
|
107
|
+
),
|
|
108
|
+
ReplayGainExtractor.trackGainKey,
|
|
109
|
+
"an empty info field falls back to the key"
|
|
110
|
+
)
|
|
111
|
+
XCTAssertEqual(
|
|
112
|
+
AssetReplayGainReader.tagName(
|
|
113
|
+
info: " ", key: "REPLAYGAIN_TRACK_GAIN"
|
|
114
|
+
),
|
|
115
|
+
ReplayGainExtractor.trackGainKey,
|
|
116
|
+
"a whitespace-only info field falls back to the key"
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func testALowercaseInfoFieldIsUppercased() {
|
|
121
|
+
XCTAssertEqual(
|
|
122
|
+
AssetReplayGainReader.tagName(
|
|
123
|
+
info: "replaygain_album_gain", key: "TXXX"
|
|
124
|
+
),
|
|
125
|
+
ReplayGainExtractor.albumGainKey
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func testWithNeitherInfoNorKeyThereIsNoName() {
|
|
130
|
+
assertNameDerivationIsLive()
|
|
131
|
+
XCTAssertNil(AssetReplayGainReader.tagName(info: nil, key: nil))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// MARK: - Names that must not reach the ReplayGain lookup
|
|
135
|
+
|
|
136
|
+
func testAnOrdinalKeyYieldsNoReplayGainName() {
|
|
137
|
+
// ffmpeg writes `mdta` metadata under ordinal keys ("1", "2", ...);
|
|
138
|
+
// the tag name is unrecoverable from them and none may match.
|
|
139
|
+
assertNameDerivationIsLive()
|
|
140
|
+
for key in ["1", "2", "10"] {
|
|
141
|
+
assertNotAReplayGainName(
|
|
142
|
+
AssetReplayGainReader.tagName(info: nil, key: key),
|
|
143
|
+
"ordinal key \(key) must not resolve to a ReplayGain name"
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func testAnUnrelatedTagNameYieldsNoReplayGainName() {
|
|
149
|
+
assertNameDerivationIsLive()
|
|
150
|
+
for key in ["TITLE", "ARTIST", "com.apple.iTunes.iTunNORM"] {
|
|
151
|
+
assertNotAReplayGainName(
|
|
152
|
+
AssetReplayGainReader.tagName(info: nil, key: key),
|
|
153
|
+
"\(key) must not resolve to a ReplayGain name"
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func testAKeyEndingInADotYieldsNoUsableName() {
|
|
159
|
+
// The last-segment split has nothing after the separator. It must
|
|
160
|
+
// return safely rather than trap on an out-of-range index, and must
|
|
161
|
+
// never hand back an empty name.
|
|
162
|
+
assertNameDerivationIsLive()
|
|
163
|
+
for key in ["com.apple.iTunes.", ".", "..", "REPLAYGAIN_TRACK_GAIN."] {
|
|
164
|
+
let name = AssetReplayGainReader.tagName(info: nil, key: key)
|
|
165
|
+
XCTAssertNotEqual(name, "", "\(key) must not yield an empty name")
|
|
166
|
+
assertNotAReplayGainName(
|
|
167
|
+
name, "\(key) must not resolve to a ReplayGain name"
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// MARK: - Opus double-correction guard
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
func testIsOpusContainerRecognisesOpusExtension() {
|
|
176
|
+
XCTAssertTrue(
|
|
177
|
+
AssetReplayGainReader.isOpusContainer(
|
|
178
|
+
url: URL(string: "https://example.test/track.opus")!
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
XCTAssertTrue(
|
|
182
|
+
AssetReplayGainReader.isOpusContainer(
|
|
183
|
+
url: URL(string: "https://example.test/track.ogg")!
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// The guard reads the track's own URL, never the URL the asset is bound to.
|
|
189
|
+
///
|
|
190
|
+
/// A routed track's asset URL is the read-through server's — a token with no
|
|
191
|
+
/// extension — so a guard that asked the asset would fail open on every Opus
|
|
192
|
+
/// track and apply a `REPLAYGAIN_*` tag on top of the decoder's own
|
|
193
|
+
/// `OpusHead.output_gain`. That is audible, and no playback suite covers it.
|
|
194
|
+
func testAServerTokenURLIsNotMistakenForANonOpusContainer() {
|
|
195
|
+
XCTAssertFalse(
|
|
196
|
+
AssetReplayGainReader.isOpusContainer(
|
|
197
|
+
url: URL(string: "http://127.0.0.1:52341/media/0123456789abcdef0123456789abcdef")!
|
|
198
|
+
),
|
|
199
|
+
"the transport URL carries no container — the guard must read sourceURL"
|
|
200
|
+
)
|
|
201
|
+
XCTAssertTrue(
|
|
202
|
+
AssetReplayGainReader.isOpusContainer(
|
|
203
|
+
url: URL(string: "https://example.test/track.opus")!
|
|
204
|
+
),
|
|
205
|
+
"and the media URL still identifies Opus"
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
func testIsOpusContainerReturnsFalseForOtherFormats() {
|
|
210
|
+
XCTAssertFalse(
|
|
211
|
+
AssetReplayGainReader.isOpusContainer(
|
|
212
|
+
url: URL(string: "https://example.test/track.mp3")!
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
XCTAssertFalse(
|
|
216
|
+
AssetReplayGainReader.isOpusContainer(
|
|
217
|
+
url: URL(string: "https://example.test/track.m4a")!
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
XCTAssertFalse(
|
|
221
|
+
AssetReplayGainReader.isOpusContainer(
|
|
222
|
+
url: URL(string: "https://example.test/track.flac")!
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// MARK: - FLAC STREAMINFO
|
|
228
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// The tap provider's engine-facing mix work runs on the dispatch target it
|
|
6
|
+
/// was given: inline when the caller is already on that target, on the target
|
|
7
|
+
/// itself otherwise. `resetMixDispatchTarget` puts it back on main, and
|
|
8
|
+
/// `detachFromEngine` clears the provider off the engine it was attached to.
|
|
9
|
+
final class AudioTapProviderDispatchTargetTests: XCTestCase {
|
|
10
|
+
|
|
11
|
+
func testRefreshRunsInlineWhenTheCallerIsOnTheMixQueue() {
|
|
12
|
+
let provider = AudioTapProvider()
|
|
13
|
+
let engine = MixDispatchEngineStub()
|
|
14
|
+
provider.wireToPlaybackEngine(engine)
|
|
15
|
+
let queue = makeQueue(label: "inline")
|
|
16
|
+
provider.setMixDispatchTarget(
|
|
17
|
+
queue,
|
|
18
|
+
isCurrent: { DispatchQueue.getSpecific(key: MixDispatchEngineStub.queueKey) == "inline" })
|
|
19
|
+
|
|
20
|
+
queue.sync { provider.refreshActiveItemMixes() }
|
|
21
|
+
|
|
22
|
+
XCTAssertEqual(
|
|
23
|
+
engine.observedTargets, ["inline"],
|
|
24
|
+
"an in-queue caller reaches the engine before the call returns, without "
|
|
25
|
+
+ "a second hop onto the queue it is already on")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func testRefreshLandsOnTheInjectedQueueWhenTheCallerIsNotOnIt() {
|
|
29
|
+
let provider = AudioTapProvider()
|
|
30
|
+
let engine = MixDispatchEngineStub()
|
|
31
|
+
provider.wireToPlaybackEngine(engine)
|
|
32
|
+
let landed = expectation(description: "refresh reaches the engine")
|
|
33
|
+
engine.onRefresh = { landed.fulfill() }
|
|
34
|
+
provider.setMixDispatchTarget(makeQueue(label: "deferred"), isCurrent: { false })
|
|
35
|
+
|
|
36
|
+
provider.refreshActiveItemMixes()
|
|
37
|
+
|
|
38
|
+
wait(for: [landed], timeout: 1.0)
|
|
39
|
+
XCTAssertEqual(engine.observedTargets, ["deferred"])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func testResetMixDispatchTargetReturnsTheWorkToMain() {
|
|
43
|
+
let provider = AudioTapProvider()
|
|
44
|
+
let engine = MixDispatchEngineStub()
|
|
45
|
+
provider.wireToPlaybackEngine(engine)
|
|
46
|
+
provider.setMixDispatchTarget(makeQueue(label: "player"), isCurrent: { false })
|
|
47
|
+
|
|
48
|
+
provider.resetMixDispatchTarget()
|
|
49
|
+
provider.refreshActiveItemMixes()
|
|
50
|
+
|
|
51
|
+
XCTAssertEqual(engine.observedTargets, [nil], "the player queue is no longer the target")
|
|
52
|
+
XCTAssertEqual(
|
|
53
|
+
engine.observedMainThreadFlags, [true],
|
|
54
|
+
"a main-thread caller runs inline once the target is back on main")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func testDetachFromEngineClearsTheEngineMixProvider() {
|
|
58
|
+
let provider = AudioTapProvider()
|
|
59
|
+
let engine = MixDispatchEngineStub()
|
|
60
|
+
provider.wireToPlaybackEngine(engine)
|
|
61
|
+
XCTAssertNotNil(engine.lastSetProvider)
|
|
62
|
+
|
|
63
|
+
provider.detachFromEngine()
|
|
64
|
+
|
|
65
|
+
XCTAssertNil(engine.lastSetProvider)
|
|
66
|
+
provider.refreshActiveItemMixes()
|
|
67
|
+
XCTAssertTrue(
|
|
68
|
+
engine.observedTargets.isEmpty,
|
|
69
|
+
"a detached provider has no engine left to refresh")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// A queue tagged with its own label so the stub can report which target an
|
|
73
|
+
/// engine call arrived on.
|
|
74
|
+
private func makeQueue(label: String) -> DispatchQueue {
|
|
75
|
+
let queue = DispatchQueue(label: "test.mixDispatch.\(label)")
|
|
76
|
+
queue.setSpecific(key: MixDispatchEngineStub.queueKey, value: label)
|
|
77
|
+
return queue
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Records the dispatch target of every `refreshAllItemMixes` call and the last
|
|
82
|
+
/// provider set on it. All other PlaybackEngine methods are no-op stubs.
|
|
83
|
+
private final class MixDispatchEngineStub: PlaybackEngine {
|
|
84
|
+
|
|
85
|
+
/// Tags a queue built by the test; `nil` in `observedTargets` means the call
|
|
86
|
+
/// arrived on a thread with no tagged queue.
|
|
87
|
+
static let queueKey = DispatchSpecificKey<String>()
|
|
88
|
+
|
|
89
|
+
private let lock = NSLock()
|
|
90
|
+
private var observed: [String?] = []
|
|
91
|
+
private var observedOnMain: [Bool] = []
|
|
92
|
+
var lastSetProvider: AudioMixProvider?
|
|
93
|
+
var onRefresh: (() -> Void)?
|
|
94
|
+
|
|
95
|
+
var observedTargets: [String?] {
|
|
96
|
+
lock.lock()
|
|
97
|
+
defer { lock.unlock() }
|
|
98
|
+
return observed
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Whether each recorded call arrived on the main thread — an untagged
|
|
102
|
+
/// target is only evidence of "main" together with this.
|
|
103
|
+
var observedMainThreadFlags: [Bool] {
|
|
104
|
+
lock.lock()
|
|
105
|
+
defer { lock.unlock() }
|
|
106
|
+
return observedOnMain
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func setAudioMixProvider(_ provider: AudioMixProvider?) {
|
|
110
|
+
lastSetProvider = provider
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func refreshAllItemMixes() {
|
|
114
|
+
let target = DispatchQueue.getSpecific(key: Self.queueKey)
|
|
115
|
+
let onMain = Thread.isMainThread
|
|
116
|
+
lock.lock()
|
|
117
|
+
observed.append(target)
|
|
118
|
+
observedOnMain.append(onMain)
|
|
119
|
+
lock.unlock()
|
|
120
|
+
onRefresh?()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func release() {}
|
|
124
|
+
func setItems(_ items: [AVPlayerItem], startIndex: Int, startPositionSeconds: TimeInterval) {}
|
|
125
|
+
@discardableResult
|
|
126
|
+
func insertItems(_ items: [AVPlayerItem], after: AVPlayerItem?) -> Bool { true }
|
|
127
|
+
func remove(_ item: AVPlayerItem) {}
|
|
128
|
+
func removeAllItems() {}
|
|
129
|
+
func canInsert(_ item: AVPlayerItem, after: AVPlayerItem?) -> Bool { true }
|
|
130
|
+
func play() {}
|
|
131
|
+
func pause() {}
|
|
132
|
+
func stop() {}
|
|
133
|
+
func seek(toIndex index: Int, position: TimeInterval) {}
|
|
134
|
+
func skipToNext() {}
|
|
135
|
+
func skipToPrevious() {}
|
|
136
|
+
func setVolume(_ volume: Float) {}
|
|
137
|
+
func setPlaybackSpeed(_ speed: Float) {}
|
|
138
|
+
func seedPlaybackSpeed(_ speed: Float) {}
|
|
139
|
+
func setRepeatMode(_ mode: RepeatMode) {}
|
|
140
|
+
func setPitchCorrectionMode(_ mode: PitchCorrectionMode) {}
|
|
141
|
+
|
|
142
|
+
var currentMediaItemIndex: Int { -1 }
|
|
143
|
+
var currentPositionSeconds: TimeInterval { 0 }
|
|
144
|
+
var currentDurationSeconds: TimeInterval { 0 }
|
|
145
|
+
var bufferedPositionSeconds: TimeInterval { 0 }
|
|
146
|
+
var isPlaying: Bool { false }
|
|
147
|
+
var timeControlStatus: AVPlayer.TimeControlStatus { .paused }
|
|
148
|
+
var currentMediaItem: AVPlayerItem? { nil }
|
|
149
|
+
var allMediaItems: [AVPlayerItem] { [] }
|
|
150
|
+
|
|
151
|
+
/// No fade to schedule — this stub exercises the rest of the surface.
|
|
152
|
+
var isFadePendingOrActive: Bool { false }
|
|
153
|
+
|
|
154
|
+
/// Nothing attached — this stub exercises the rest of the surface.
|
|
155
|
+
var attachedQueueItemIds: [String] { [] }
|
|
156
|
+
|
|
157
|
+
var delegate: PlaybackEngineDelegate?
|
|
158
|
+
}
|
|
@@ -96,29 +96,141 @@ final class AudioTapProviderReplayGainTests: XCTestCase {
|
|
|
96
96
|
)
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
func
|
|
100
|
-
// peak == 1.0 is
|
|
99
|
+
func testComputeLinearGainFullScalePeakCapsBoostAtUnity() {
|
|
100
|
+
// peak == 1.0 is digital full scale, so 1/peak == 1.0 and a +6 dB
|
|
101
|
+
// boost is capped back to unity. +6 dB, not -6 dB: at -6 dB the cap
|
|
102
|
+
// never binds and the row cannot discriminate. Mirrors Android's
|
|
103
|
+
// `peak equal to 1 caps the boost at unity`.
|
|
101
104
|
let data = ReplayGainData(
|
|
102
|
-
trackGainDb:
|
|
105
|
+
trackGainDb: 6.0, trackPeak: 1.0,
|
|
103
106
|
albumGainDb: nil, albumPeak: nil
|
|
104
107
|
)
|
|
105
|
-
let raw = Float(pow(10.0, -6.0 / 20.0))
|
|
106
108
|
XCTAssertEqual(
|
|
107
109
|
AudioTapProvider.computeLinearGain(data: data, mode: .track),
|
|
108
|
-
|
|
110
|
+
1.0, accuracy: 0.001
|
|
109
111
|
)
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
func
|
|
113
|
-
// peak < 0.001
|
|
114
|
+
func testComputeLinearGainUnusablePeakClampsBoostToUnity() {
|
|
115
|
+
// peak < 0.001 is an encoder zero — no usable peak, so the boost is
|
|
116
|
+
// clamped to unity rather than running uncapped.
|
|
114
117
|
let data = ReplayGainData(
|
|
115
|
-
trackGainDb:
|
|
118
|
+
trackGainDb: 6.0, trackPeak: 0.0005,
|
|
116
119
|
albumGainDb: nil, albumPeak: nil
|
|
117
120
|
)
|
|
118
|
-
let raw = Float(pow(10.0, -6.0 / 20.0))
|
|
119
121
|
XCTAssertEqual(
|
|
120
122
|
AudioTapProvider.computeLinearGain(data: data, mode: .track),
|
|
121
|
-
|
|
123
|
+
1.0, accuracy: 0.001
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// MARK: - Clip safety — min(10^(gain/20), 1/peak), unity cap with no peak
|
|
128
|
+
|
|
129
|
+
func testComputeLinearGainAttenuationIsUnaffectedByTheMissingPeakClamp() {
|
|
130
|
+
// -20 dB, no peak. A negative gain can never clip, so the unity
|
|
131
|
+
// clamp only ever binds on a boost.
|
|
132
|
+
let data = ReplayGainData(
|
|
133
|
+
trackGainDb: -20.0, trackPeak: nil,
|
|
134
|
+
albumGainDb: nil, albumPeak: nil
|
|
135
|
+
)
|
|
136
|
+
XCTAssertEqual(
|
|
137
|
+
AudioTapProvider.computeLinearGain(data: data, mode: .track),
|
|
138
|
+
0.100, accuracy: 0.001
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
func testComputeLinearGainWithNoPeakClampsBoostToUnity() {
|
|
143
|
+
// +6 dB, no peak. A peak cannot be derived from a gain, so boost is
|
|
144
|
+
// suppressed rather than run unprotected.
|
|
145
|
+
let data = ReplayGainData(
|
|
146
|
+
trackGainDb: 6.0, trackPeak: nil,
|
|
147
|
+
albumGainDb: nil, albumPeak: nil
|
|
148
|
+
)
|
|
149
|
+
XCTAssertEqual(
|
|
150
|
+
AudioTapProvider.computeLinearGain(data: data, mode: .track),
|
|
151
|
+
1.000, accuracy: 0.001
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func testComputeLinearGainWithNoPeakLeavesZeroGainAtUnity() {
|
|
156
|
+
let data = ReplayGainData(
|
|
157
|
+
trackGainDb: 0.0, trackPeak: nil,
|
|
158
|
+
albumGainDb: nil, albumPeak: nil
|
|
159
|
+
)
|
|
160
|
+
XCTAssertEqual(
|
|
161
|
+
AudioTapProvider.computeLinearGain(data: data, mode: .track),
|
|
162
|
+
1.000, accuracy: 0.001
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func testComputeLinearGainFullScalePeakDoesNotBindOnAttenuation() {
|
|
167
|
+
// -20 dB with peak 1.0: the cap is present at 1.0 but 0.1 is lower.
|
|
168
|
+
let data = ReplayGainData(
|
|
169
|
+
trackGainDb: -20.0, trackPeak: 1.0,
|
|
170
|
+
albumGainDb: nil, albumPeak: nil
|
|
171
|
+
)
|
|
172
|
+
XCTAssertEqual(
|
|
173
|
+
AudioTapProvider.computeLinearGain(data: data, mode: .track),
|
|
174
|
+
0.100, accuracy: 0.001
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
func testComputeLinearGainBoostSurvivesWhenThePeakCapSitsAboveIt() {
|
|
179
|
+
// +6 dB ~= 1.995, peak 0.5 -> cap 2.0. The cap does not bind.
|
|
180
|
+
let data = ReplayGainData(
|
|
181
|
+
trackGainDb: 6.0, trackPeak: 0.5,
|
|
182
|
+
albumGainDb: nil, albumPeak: nil
|
|
183
|
+
)
|
|
184
|
+
XCTAssertEqual(
|
|
185
|
+
AudioTapProvider.computeLinearGain(data: data, mode: .track),
|
|
186
|
+
1.995, accuracy: 0.001
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func testComputeLinearGainBoostIsCappedByTheReciprocalOfThePeak() {
|
|
191
|
+
// +12 dB ~= 3.981, peak 0.5 -> cap 2.0. The cap wins.
|
|
192
|
+
let data = ReplayGainData(
|
|
193
|
+
trackGainDb: 12.0, trackPeak: 0.5,
|
|
194
|
+
albumGainDb: nil, albumPeak: nil
|
|
195
|
+
)
|
|
196
|
+
XCTAssertEqual(
|
|
197
|
+
AudioTapProvider.computeLinearGain(data: data, mode: .track),
|
|
198
|
+
2.000, accuracy: 0.001
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
func testComputeLinearGainAPeakAboveUnityCapsBoostBelowUnity() {
|
|
203
|
+
// +6 dB with a true peak of 1.16 -> cap 1/1.16 ~= 0.862, below unity.
|
|
204
|
+
let data = ReplayGainData(
|
|
205
|
+
trackGainDb: 6.0, trackPeak: 1.16,
|
|
206
|
+
albumGainDb: nil, albumPeak: nil
|
|
207
|
+
)
|
|
208
|
+
XCTAssertEqual(
|
|
209
|
+
AudioTapProvider.computeLinearGain(data: data, mode: .track),
|
|
210
|
+
0.862, accuracy: 0.001
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
func testComputeLinearGainClipSafetyIsMirroredInAlbumMode() {
|
|
215
|
+
// The whole cap applies to whichever pair the mode selects — applying
|
|
216
|
+
// it to the track pair only would pass every row above.
|
|
217
|
+
let noPeak = ReplayGainData(
|
|
218
|
+
trackGainDb: nil, trackPeak: nil,
|
|
219
|
+
albumGainDb: 6.0, albumPeak: nil
|
|
220
|
+
)
|
|
221
|
+
XCTAssertEqual(
|
|
222
|
+
AudioTapProvider.computeLinearGain(data: noPeak, mode: .album),
|
|
223
|
+
1.000, accuracy: 0.001,
|
|
224
|
+
"album mode with no album peak clamps boost to unity"
|
|
225
|
+
)
|
|
226
|
+
let fullScale = ReplayGainData(
|
|
227
|
+
trackGainDb: nil, trackPeak: nil,
|
|
228
|
+
albumGainDb: 6.0, albumPeak: 1.0
|
|
229
|
+
)
|
|
230
|
+
XCTAssertEqual(
|
|
231
|
+
AudioTapProvider.computeLinearGain(data: fullScale, mode: .album),
|
|
232
|
+
1.000, accuracy: 0.001,
|
|
233
|
+
"album mode with a full-scale album peak caps boost at unity"
|
|
122
234
|
)
|
|
123
235
|
}
|
|
124
236
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import QueuePlayer
|
|
4
|
+
|
|
5
|
+
/// Access-log entries arrive several times a second for a stream; the
|
|
6
|
+
/// bitrate re-resolve they trigger runs a bounded number of times per item,
|
|
7
|
+
/// since a server that never declares a bitrate would otherwise have the
|
|
8
|
+
/// extractor re-read the stream for the life of the track.
|
|
9
|
+
final class BitrateReresolveBudgetTests: XCTestCase {
|
|
10
|
+
|
|
11
|
+
private var player: TrackPlayer!
|
|
12
|
+
|
|
13
|
+
override func setUp() async throws {
|
|
14
|
+
try await super.setUp()
|
|
15
|
+
player = TrackPlayer()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override func tearDown() async throws {
|
|
19
|
+
player = nil
|
|
20
|
+
try await super.tearDown()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func testReresolvesStopAtTheBudgetForOneItem() async throws {
|
|
24
|
+
let player = try XCTUnwrap(self.player)
|
|
25
|
+
let engine = ActiveItemEngineStub()
|
|
26
|
+
engine.item = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/a.mp3"))
|
|
27
|
+
await player.onPlayerQueue { player.engine = engine }
|
|
28
|
+
let before = await player.onPlayerQueueValue { player.nowPlayingFormatRequestGen }
|
|
29
|
+
|
|
30
|
+
for _ in 0 ..< (TrackPlayer.maxBitrateReresolvesPerItem + 3) {
|
|
31
|
+
await player.onPlayerQueue { player.reresolveNowPlayingFormatForBitrate() }
|
|
32
|
+
try await waitUntilSettled(player)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let launched = await player.onPlayerQueueValue { player.nowPlayingFormatRequestGen }
|
|
36
|
+
XCTAssertEqual(
|
|
37
|
+
Int(launched - before), TrackPlayer.maxBitrateReresolvesPerItem,
|
|
38
|
+
"every call past the budget is dropped")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// The budget is per item: the next track starts with its own.
|
|
42
|
+
func testANewActiveItemGetsAFreshBudget() async throws {
|
|
43
|
+
let player = try XCTUnwrap(self.player)
|
|
44
|
+
let engine = ActiveItemEngineStub()
|
|
45
|
+
engine.item = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/a.mp3"))
|
|
46
|
+
await player.onPlayerQueue { player.engine = engine }
|
|
47
|
+
for _ in 0 ..< TrackPlayer.maxBitrateReresolvesPerItem {
|
|
48
|
+
await player.onPlayerQueue { player.reresolveNowPlayingFormatForBitrate() }
|
|
49
|
+
try await waitUntilSettled(player)
|
|
50
|
+
}
|
|
51
|
+
let spent = await player.onPlayerQueueValue { player.nowPlayingFormatRequestGen }
|
|
52
|
+
|
|
53
|
+
engine.item = AVPlayerItem(url: URL(fileURLWithPath: "/dev/null/b.mp3"))
|
|
54
|
+
await player.onPlayerQueue { player.reresolveNowPlayingFormatForBitrate() }
|
|
55
|
+
try await waitUntilSettled(player)
|
|
56
|
+
|
|
57
|
+
let afterSwitch = await player.onPlayerQueueValue { player.nowPlayingFormatRequestGen }
|
|
58
|
+
XCTAssertEqual(afterSwitch, spent + 1, "the new item's first re-resolve runs")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// A launched re-resolve finishes on the player queue; the next call is
|
|
62
|
+
/// only meaningful once it has.
|
|
63
|
+
private func waitUntilSettled(_ player: TrackPlayer) async throws {
|
|
64
|
+
for _ in 0 ..< 100 {
|
|
65
|
+
let inFlight = await player.onPlayerQueueValue { player.bitrateReresolveInFlight }
|
|
66
|
+
if !inFlight { return }
|
|
67
|
+
try await Task.sleep(nanoseconds: 20_000_000)
|
|
68
|
+
}
|
|
69
|
+
XCTFail("the re-resolve did not settle")
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import MediaPlayer
|
|
3
|
+
import XCTest
|
|
4
|
+
@testable import QueuePlayer
|
|
5
|
+
|
|
6
|
+
/// A cast receiver is handed the consumer's own media URL, never the local
|
|
7
|
+
/// read-through server's.
|
|
8
|
+
///
|
|
9
|
+
/// Cast never touches an `AVPlayerItem`: it builds from `self.tracks`, and the
|
|
10
|
+
/// receiver fetches the origin itself over the LAN. Read-through routing lives
|
|
11
|
+
/// inside `AVQueueBuilder.makeItem` and never rewrites `self.tracks`.
|
|
12
|
+
final class CastMediaItemTranslationTests: XCTestCase {
|
|
13
|
+
|
|
14
|
+
/// Configuring a real player writes the process-global
|
|
15
|
+
/// `MPNowPlayingInfoCenter` on main. Drain those writes and clear the centre
|
|
16
|
+
/// so they do not reach the next test class.
|
|
17
|
+
override func tearDown() {
|
|
18
|
+
let drained = expectation(description: "pending main-queue now-playing writes complete")
|
|
19
|
+
DispatchQueue.main.async { drained.fulfill() }
|
|
20
|
+
wait(for: [drained], timeout: 5.0)
|
|
21
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
|
22
|
+
super.tearDown()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private func track(url: String) -> TrackItem {
|
|
26
|
+
TrackItem(
|
|
27
|
+
id: "cast-1", url: url, title: "T", artist: nil, album: nil, duration: nil,
|
|
28
|
+
artworkUrl: nil, extras: nil,
|
|
29
|
+
replayGainTrackGain: nil, replayGainTrackPeak: nil,
|
|
30
|
+
replayGainAlbumGain: nil, replayGainAlbumPeak: nil)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func testAReceiverGetsTheConsumersRemoteURL() throws {
|
|
34
|
+
let item = try XCTUnwrap(
|
|
35
|
+
CastMediaItem.from(track: track(url: "https://example.com/song.mp3")))
|
|
36
|
+
XCTAssertEqual(item.url.absoluteString, "https://example.com/song.mp3")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Local content still goes to the cast server, which is a different server
|
|
40
|
+
/// on a different binding and is untouched by the read-through work.
|
|
41
|
+
func testALocalFileStillReachesCastAsAFileURL() throws {
|
|
42
|
+
let item = try XCTUnwrap(
|
|
43
|
+
CastMediaItem.from(track: track(url: "file:///var/mobile/song.mp3")))
|
|
44
|
+
XCTAssertTrue(item.url.isFileURL)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// The routing decision on a configured player: the seated item addresses
|
|
48
|
+
/// the loopback server while the track the cast side builds from keeps the
|
|
49
|
+
/// consumer's URL.
|
|
50
|
+
func testRoutingRewritesTheSeatedItemAndNotTheTrack() async throws {
|
|
51
|
+
let player = TrackPlayer()
|
|
52
|
+
_ = try await player.configure(config: PlayerFixtures.config()).await()
|
|
53
|
+
let origin = "http://127.0.0.1:1/song.mp3"
|
|
54
|
+
_ = try await player.setQueue(tracks: [track(url: origin)], startAtIndex: 0).await()
|
|
55
|
+
|
|
56
|
+
let seatedItem = await player.onPlayerQueueValue { player.engine?.allMediaItems.first }
|
|
57
|
+
// `AVPlayerItem.asset` is main-actor isolated (`AVAsset` is not Sendable).
|
|
58
|
+
let seated = await MainActor.run { (seatedItem?.asset as? AVURLAsset)?.url }
|
|
59
|
+
let modelURL = await player.onPlayerQueueValue { player.tracks.first?.url }
|
|
60
|
+
|
|
61
|
+
XCTAssertEqual(seated?.host, "127.0.0.1", "the seated item reads through the loopback server")
|
|
62
|
+
XCTAssertNotEqual(seated?.port, 1, "on the server's own port, not the origin's")
|
|
63
|
+
XCTAssertEqual(modelURL, origin, "the track the cast side builds from is untouched")
|
|
64
|
+
XCTAssertEqual(
|
|
65
|
+
try XCTUnwrap(CastMediaItem.from(track: track(url: origin))).url.absoluteString, origin)
|
|
66
|
+
|
|
67
|
+
// Await destroy so its player-queue → main now-playing clear is enqueued
|
|
68
|
+
// before this test returns; `tearDown` then drains main and clears the
|
|
69
|
+
// centre so no stray write reaches the next test class.
|
|
70
|
+
_ = try await player.destroy().await()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -188,7 +188,7 @@ final class ControllableRemoteSession: CastSessionRemotePlayer {
|
|
|
188
188
|
private var positionCb: ((Int64, Int64, Int64) -> Void)?
|
|
189
189
|
|
|
190
190
|
/// Drive the receiver state the way a live session does: a non-idle report
|
|
191
|
-
///
|
|
191
|
+
/// sets `hasReportedState`. Set `lastState` directly to stage a state the
|
|
192
192
|
/// receiver has not reported yet.
|
|
193
193
|
func fireState(_ state: RemotePlaybackStateKind) {
|
|
194
194
|
lastState = state
|