react-native-queue-player 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/build.gradle +7 -0
- package/android/consumer-rules.pro +72 -3
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +26 -22
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +20 -72
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +870 -278
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastEventBridge.kt +1 -10
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +17 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastTransportRouter.kt +19 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt +18 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +23 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +58 -21
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlaySession.kt +26 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MetadataSyncTarget.kt +20 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastSession.kt +73 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +23 -12
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +43 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +8 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +68 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +163 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +123 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +27 -11
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSyncTest.kt +303 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Chromecast/ChromecastSession.swift +90 -11
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +11 -0
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +660 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +202 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +5 -0
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlaybackStateRouterTests.swift +1 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +303 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +394 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerCastStateTests.swift +78 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +968 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +930 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +940 -0
- package/ios/TrackPlayer+Recovery.swift +196 -0
- package/ios/TrackPlayer+Skip.swift +455 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +267 -0
- package/ios/TrackPlayer+Window.swift +195 -0
- package/ios/TrackPlayer.swift +305 -4610
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2MetadataSync.kt +0 -189
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
|
@@ -52,6 +52,7 @@ final class CarPlayCoordinator {
|
|
|
52
52
|
|
|
53
53
|
// MARK: - Connection state
|
|
54
54
|
|
|
55
|
+
private let connectionLock = NSLock()
|
|
55
56
|
/// Whether the CarPlay scene is currently connected. Written by
|
|
56
57
|
/// [CarPlaySceneDelegate] at scene attach / detach — the same points that
|
|
57
58
|
/// fire the `onCarConnect` / `onCarDisconnect` callbacks — and read by the
|
|
@@ -63,7 +64,6 @@ final class CarPlayCoordinator {
|
|
|
63
64
|
/// attached/detached flag rather than a connection count (the Android side
|
|
64
65
|
/// counts because its headless controllers — Auto / Wear / Assistant — can
|
|
65
66
|
/// overlap).
|
|
66
|
-
private let connectionLock = NSLock()
|
|
67
67
|
private var carConnectedFlag = false
|
|
68
68
|
|
|
69
69
|
var isCarConnected: Bool {
|
|
@@ -72,9 +72,25 @@ final class CarPlayCoordinator {
|
|
|
72
72
|
return carConnectedFlag
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/// Invoked on the main queue when a scene attaches. `TrackPlayer` uses it
|
|
76
|
+
/// to rebuild the queue mirror, which is only maintained while a car is
|
|
77
|
+
/// attached — without this a car connecting mid-session would show whatever
|
|
78
|
+
/// the mirror held when it was last connected.
|
|
79
|
+
var onCarAttached: (() -> Void)? {
|
|
80
|
+
get { connectionLock.withLock { carAttachedCallback } }
|
|
81
|
+
set { connectionLock.withLock { carAttachedCallback = newValue } }
|
|
82
|
+
}
|
|
83
|
+
/// Written by `TrackPlayer` on its player queue, read here on main.
|
|
84
|
+
private var carAttachedCallback: (() -> Void)?
|
|
85
|
+
|
|
75
86
|
func setCarConnected(_ connected: Bool) {
|
|
76
87
|
connectionLock.lock()
|
|
77
|
-
|
|
88
|
+
let wasConnected = carConnectedFlag
|
|
78
89
|
carConnectedFlag = connected
|
|
90
|
+
let attached = carAttachedCallback
|
|
91
|
+
connectionLock.unlock()
|
|
92
|
+
if connected, !wasConnected {
|
|
93
|
+
attached?()
|
|
94
|
+
}
|
|
79
95
|
}
|
|
80
96
|
}
|
|
@@ -16,8 +16,8 @@ import Foundation
|
|
|
16
16
|
/// the live `BrowseDataProvider`, `CarPlayBridge`,
|
|
17
17
|
/// `PendingBrowseRequests`, and `ArtworkLoader` — all of which are
|
|
18
18
|
/// also written from the JS-side `TrackPlayer` Hybrid path, so the
|
|
19
|
-
/// scene delegate sees fresh data without
|
|
20
|
-
///
|
|
19
|
+
/// scene delegate sees fresh data without anything being injected
|
|
20
|
+
/// through its initialiser.
|
|
21
21
|
/// **Class is `internal` (not `public`) and uses `@objc(<bare-name>)`:**
|
|
22
22
|
/// the auto-generated `<Module>-Swift.h` only emits public Swift
|
|
23
23
|
/// classes; `internal` keeps this one out of the bridging header so
|
|
@@ -71,14 +71,15 @@ final class AirPlayRouteState {
|
|
|
71
71
|
/// dispatching a tap to the picker's internal UIButton. Used by
|
|
72
72
|
/// `CastManager.showSystemPicker()`.
|
|
73
73
|
///
|
|
74
|
-
///
|
|
75
|
-
///
|
|
76
|
-
///
|
|
77
|
-
///
|
|
78
|
-
///
|
|
79
|
-
///
|
|
80
|
-
///
|
|
81
|
-
///
|
|
74
|
+
/// Workaround: `AVRoutePickerView` has no public API to present its
|
|
75
|
+
/// sheet, so this walks the view's subviews for its `UIButton` and
|
|
76
|
+
/// fires its `.touchUpInside` actions. The walk is recursive rather
|
|
77
|
+
/// than a direct child lookup because the button's depth has changed
|
|
78
|
+
/// between iOS releases. When no button is found the call is a no-op;
|
|
79
|
+
/// a consumer that needs a guaranteed sheet renders its own
|
|
80
|
+
/// `AVRoutePickerView`, which is the supported pattern.
|
|
81
|
+
/// TODO: monitor upstream for fixes — https://github.com/react-native-video/react-native-video/issues/3119
|
|
82
|
+
/// (the same subview walk, and the request for a public entry point).
|
|
82
83
|
func showSystemPicker() {
|
|
83
84
|
DispatchQueue.main.async {
|
|
84
85
|
let picker = self.getOrCreatePicker()
|
|
@@ -40,10 +40,16 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
|
|
|
40
40
|
private var _lastVolume: Float = .nan
|
|
41
41
|
private var _lastVolumeEmitted: Float = .nan
|
|
42
42
|
|
|
43
|
+
private var _hasReportedState = false
|
|
44
|
+
|
|
43
45
|
var lastState: RemotePlaybackStateKind {
|
|
44
46
|
stateLock.lock(); defer { stateLock.unlock() }
|
|
45
47
|
return _lastState
|
|
46
48
|
}
|
|
49
|
+
var hasReportedState: Bool {
|
|
50
|
+
stateLock.lock(); defer { stateLock.unlock() }
|
|
51
|
+
return _hasReportedState
|
|
52
|
+
}
|
|
47
53
|
var lastPositionMs: Int64 {
|
|
48
54
|
stateLock.lock(); defer { stateLock.unlock() }
|
|
49
55
|
return _lastPositionMs
|
|
@@ -143,8 +149,15 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
|
|
|
143
149
|
|
|
144
150
|
private func updateState(_ s: RemotePlaybackStateKind) {
|
|
145
151
|
stateLock.lock()
|
|
152
|
+
let previous = _lastState
|
|
146
153
|
_lastState = s
|
|
154
|
+
if s != .idle { _hasReportedState = true }
|
|
147
155
|
stateLock.unlock()
|
|
156
|
+
#if DEBUG
|
|
157
|
+
if previous != s {
|
|
158
|
+
NSLog("[ChromecastSession] receiver state \(previous) -> \(s)")
|
|
159
|
+
}
|
|
160
|
+
#endif
|
|
148
161
|
fireOnEach(stateListeners) { $0(s) }
|
|
149
162
|
}
|
|
150
163
|
|
|
@@ -302,11 +315,14 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
|
|
|
302
315
|
}
|
|
303
316
|
}
|
|
304
317
|
|
|
305
|
-
|
|
306
|
-
func
|
|
307
|
-
func
|
|
308
|
-
func
|
|
309
|
-
func
|
|
318
|
+
// No bitmask flag gates `play` — the mask models pause, not resume.
|
|
319
|
+
func play() { fireRemote("play") { $0.play() } }
|
|
320
|
+
func pause() { fireRemote("pause", requires: kGCKMediaCommandPause) { $0.pause() } }
|
|
321
|
+
func stop() { fireRemote("stop") { $0.stop() } }
|
|
322
|
+
func queueNext() { fireRemote("queueNext", requires: kGCKMediaCommandQueueNext) { $0.queueNextItem() } }
|
|
323
|
+
func queuePrev() {
|
|
324
|
+
fireRemote("queuePrev", requires: kGCKMediaCommandQueuePrevious) { $0.queuePreviousItem() }
|
|
325
|
+
}
|
|
310
326
|
func skipToQueueIndex(index: Int) {
|
|
311
327
|
guard index >= 0 else { return }
|
|
312
328
|
// Resolve the absolute lib index to the receiver's itemId via the full
|
|
@@ -315,13 +331,15 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
|
|
|
315
331
|
// model hasn't synced that slot yet; skip rather than jump blind.
|
|
316
332
|
fireRemote("skipToQueueIndex") { client in
|
|
317
333
|
let itemId = client.mediaQueue.itemID(at: UInt(index))
|
|
318
|
-
guard itemId != kGCKMediaQueueInvalidItemID else { return }
|
|
319
|
-
|
|
334
|
+
guard itemId != kGCKMediaQueueInvalidItemID else { return nil }
|
|
335
|
+
return client.queueJumpToItem(withID: itemId)
|
|
320
336
|
}
|
|
321
337
|
}
|
|
322
338
|
|
|
323
339
|
func setPlaybackRate(_ rate: Float) {
|
|
324
|
-
fireRemote("setPlaybackRate"
|
|
340
|
+
fireRemote("setPlaybackRate", requires: kGCKMediaCommandSetPlaybackRate) {
|
|
341
|
+
$0.setPlaybackRate(rate)
|
|
342
|
+
}
|
|
325
343
|
}
|
|
326
344
|
|
|
327
345
|
func setRepeatMode(_ mode: RemoteRepeatMode) {
|
|
@@ -331,7 +349,7 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
|
|
|
331
349
|
case .single: sdkMode = .single
|
|
332
350
|
case .all: sdkMode = .all
|
|
333
351
|
}
|
|
334
|
-
fireRemote("setRepeatMode") {
|
|
352
|
+
fireRemote("setRepeatMode") { $0.queueSetRepeatMode(sdkMode) }
|
|
335
353
|
}
|
|
336
354
|
|
|
337
355
|
func seekTo(positionMs: Int64) {
|
|
@@ -589,9 +607,21 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
|
|
|
589
607
|
/// `GCKRemoteMediaClient`. Silently skips when the session is closed
|
|
590
608
|
/// or the client is unavailable (log only — transport calls are
|
|
591
609
|
/// fire-and-forget).
|
|
610
|
+
///
|
|
611
|
+
/// `block` returns the `GCKRequest` the SDK scheduled, or `nil` when it
|
|
612
|
+
/// declined to issue one. A returned request gets a delegate so a
|
|
613
|
+
/// receiver-side rejection is reported instead of vanishing — without it
|
|
614
|
+
/// a refused command is indistinguishable from one the receiver honoured.
|
|
615
|
+
///
|
|
616
|
+
/// `requires` names the `GCKMediaStatus` command bitmask flag the receiver
|
|
617
|
+
/// must advertise for this command (`nil` for commands not gated by the
|
|
618
|
+
/// mask). The receiver publishes support per media item, so an item can
|
|
619
|
+
/// accept queue commands while refusing `pause` — worth a warning, since
|
|
620
|
+
/// the command is otherwise dropped in silence.
|
|
592
621
|
private func fireRemote(
|
|
593
622
|
_ label: String,
|
|
594
|
-
|
|
623
|
+
requires command: NSInteger? = nil,
|
|
624
|
+
_ block: @escaping (GCKRemoteMediaClient) -> GCKRequest?
|
|
595
625
|
) {
|
|
596
626
|
if closed { return }
|
|
597
627
|
runOnMain { [weak self] in
|
|
@@ -600,7 +630,28 @@ final class ChromecastSession: NSObject, CastSessionRemotePlayer {
|
|
|
600
630
|
NSLog("[ChromecastSession] \(label): no GCKRemoteMediaClient — session not fully live?")
|
|
601
631
|
return
|
|
602
632
|
}
|
|
603
|
-
|
|
633
|
+
if let command = command,
|
|
634
|
+
let status = client.mediaStatus,
|
|
635
|
+
!status.isMediaCommandSupported(command) {
|
|
636
|
+
NSLog(
|
|
637
|
+
"[ChromecastSession] \(label): receiver does not advertise support for this command; sending anyway"
|
|
638
|
+
)
|
|
639
|
+
}
|
|
640
|
+
#if DEBUG
|
|
641
|
+
NSLog("[ChromecastSession] \(label): issuing")
|
|
642
|
+
#endif
|
|
643
|
+
// `nil` means this call site declined to issue anything — the only such
|
|
644
|
+
// case is a queue index the receiver's model hasn't synced yet, which is
|
|
645
|
+
// a normal transient, not a fault.
|
|
646
|
+
guard let request = block(client) else {
|
|
647
|
+
#if DEBUG
|
|
648
|
+
NSLog("[ChromecastSession] \(label): no request issued")
|
|
649
|
+
#endif
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
let delegate = TransportRequestDelegate(label: label, owner: self)
|
|
653
|
+
self.retain(delegate)
|
|
654
|
+
request.delegate = delegate
|
|
604
655
|
}
|
|
605
656
|
}
|
|
606
657
|
|
|
@@ -707,6 +758,34 @@ fileprivate final class RequestContinuation: NSObject, GCKRequestDelegate {
|
|
|
707
758
|
}
|
|
708
759
|
}
|
|
709
760
|
|
|
761
|
+
/// Reports the outcome of a fire-and-forget transport request. The caller
|
|
762
|
+
/// doesn't await these, so without a delegate a receiver that refuses a
|
|
763
|
+
/// command leaves no trace at all.
|
|
764
|
+
fileprivate final class TransportRequestDelegate: NSObject, GCKRequestDelegate {
|
|
765
|
+
let label: String
|
|
766
|
+
weak var owner: ChromecastSession?
|
|
767
|
+
|
|
768
|
+
init(label: String, owner: ChromecastSession) {
|
|
769
|
+
self.label = label
|
|
770
|
+
self.owner = owner
|
|
771
|
+
super.init()
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
func requestDidComplete(_ request: GCKRequest) {
|
|
775
|
+
owner?.drop(self)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
func request(_ request: GCKRequest, didFailWithError error: GCKError) {
|
|
779
|
+
NSLog("[ChromecastSession] \(label) failed: \(error.localizedDescription)")
|
|
780
|
+
owner?.drop(self)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
func request(_ request: GCKRequest, didAbortWith abortReason: GCKRequestAbortReason) {
|
|
784
|
+
NSLog("[ChromecastSession] \(label) aborted: \(abortReason.rawValue)")
|
|
785
|
+
owner?.drop(self)
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
710
789
|
/// Bridge `seek(with:)`'s `GCKRequest` into the session's
|
|
711
790
|
/// `onSeekCompleted` listener fan-out. Fire-and-forget — failures are
|
|
712
791
|
/// logged but do not surface as JS errors (the lib has no "seek-
|
|
@@ -15,6 +15,14 @@ import NitroModules
|
|
|
15
15
|
/// `setActive`: once a session is removed from the router and its
|
|
16
16
|
/// listeners are disposed, no further events from it can fire
|
|
17
17
|
/// `owner.emitRemoteState`.
|
|
18
|
+
///
|
|
19
|
+
/// The listener and session fields are main-thread state: the Cast SDK
|
|
20
|
+
/// delivers session and listener callbacks on main, and the owner's `start`
|
|
21
|
+
/// and `stop` hop there from its player queue rather than touching the fields
|
|
22
|
+
/// where they are called. The deactivation snapshot is the exception — it is
|
|
23
|
+
/// written from the player queue at handoff and has to be visible to a
|
|
24
|
+
/// deactivation that lands on main right after, so it sits behind a lock.
|
|
25
|
+
/// `seedStartMetadata` reads the owner's track list on the owner's queue.
|
|
18
26
|
final class CastEventBridge {
|
|
19
27
|
|
|
20
28
|
private weak var owner: TrackPlayer?
|
|
@@ -37,11 +45,21 @@ final class CastEventBridge {
|
|
|
37
45
|
/// Receiver position at the moment the active session was cleared.
|
|
38
46
|
/// The local engine can read this to resume from where the
|
|
39
47
|
/// receiver was. Cleared when a new session activates.
|
|
40
|
-
private(set) var lastPositionMsOnDeactivate: Int64
|
|
48
|
+
private(set) var lastPositionMsOnDeactivate: Int64 {
|
|
49
|
+
get { snapshotLock.withLock { positionMsOnDeactivate } }
|
|
50
|
+
set { snapshotLock.withLock { positionMsOnDeactivate = newValue } }
|
|
51
|
+
}
|
|
41
52
|
|
|
42
53
|
/// Receiver-side current track index at deactivation. `-1` when
|
|
43
54
|
/// unknown.
|
|
44
|
-
private(set) var lastTrackIndexOnDeactivate: Int
|
|
55
|
+
private(set) var lastTrackIndexOnDeactivate: Int {
|
|
56
|
+
get { snapshotLock.withLock { trackIndexOnDeactivate } }
|
|
57
|
+
set { snapshotLock.withLock { trackIndexOnDeactivate = newValue } }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private let snapshotLock = NSLock()
|
|
61
|
+
private var positionMsOnDeactivate: Int64 = 0
|
|
62
|
+
private var trackIndexOnDeactivate: Int = -1
|
|
45
63
|
|
|
46
64
|
/// Last lib index emitted for the receiver's current item. Dedups
|
|
47
65
|
/// repeat receiver statuses carrying the same resolved index so
|
|
@@ -54,19 +72,34 @@ final class CastEventBridge {
|
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
func start() {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
onMain { [self] in
|
|
76
|
+
if self.activeChangeDisposer != nil { return }
|
|
77
|
+
self.activeChangeDisposer = PlaybackStateRouter.shared.addActiveChangeListener {
|
|
78
|
+
[weak self] session in
|
|
79
|
+
self?.handleActiveSessionChange(to: session)
|
|
80
|
+
}
|
|
61
81
|
}
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
func stop() {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
onMain {
|
|
86
|
+
self.activeChangeDisposer?()
|
|
87
|
+
self.activeChangeDisposer = nil
|
|
88
|
+
self.sessionListenerDisposers.forEach { $0() }
|
|
89
|
+
self.sessionListenerDisposers.removeAll()
|
|
90
|
+
self.currentActiveSession = nil
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Runs `body` on main, in order with the SDK callbacks that own this
|
|
95
|
+
/// object's state; inline when already there so a caller on main sees its
|
|
96
|
+
/// effect before returning.
|
|
97
|
+
private func onMain(_ body: @escaping () -> Void) {
|
|
98
|
+
if Thread.isMainThread {
|
|
99
|
+
body()
|
|
100
|
+
} else {
|
|
101
|
+
DispatchQueue.main.async(execute: body)
|
|
102
|
+
}
|
|
70
103
|
}
|
|
71
104
|
|
|
72
105
|
/// Seed the deactivation snapshot from the local pre-handoff position +
|
|
@@ -75,13 +108,18 @@ final class CastEventBridge {
|
|
|
75
108
|
/// lands local on the right spot. A receiver item resolved at
|
|
76
109
|
/// deactivation takes precedence.
|
|
77
110
|
func seedDeactivationSnapshot(trackIndex: Int, positionMs: Int64) {
|
|
78
|
-
|
|
79
|
-
|
|
111
|
+
snapshotLock.withLock {
|
|
112
|
+
trackIndexOnDeactivate = trackIndex
|
|
113
|
+
positionMsOnDeactivate = positionMs
|
|
114
|
+
}
|
|
80
115
|
}
|
|
81
116
|
|
|
82
117
|
// MARK: - Active-flow observer
|
|
83
118
|
|
|
84
119
|
private func handleActiveSessionChange(to newSession: CastSessionRemotePlayer?) {
|
|
120
|
+
// The router fires listeners on the caller's thread; every caller today
|
|
121
|
+
// is on main, and the fields below are read and written on that basis.
|
|
122
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
85
123
|
let prior = currentActiveSession
|
|
86
124
|
if newSession == nil, let prior = prior {
|
|
87
125
|
// Capture deactivation snapshot BEFORE tearing down listeners so
|
|
@@ -152,8 +190,7 @@ final class CastEventBridge {
|
|
|
152
190
|
// MARK: - Lockscreen metadata mirror
|
|
153
191
|
|
|
154
192
|
/// Seed the start track's lockscreen metadata after a `setQueue` so the
|
|
155
|
-
/// surface doesn't wait for the first receiver status.
|
|
156
|
-
/// main thread from `TrackPlayer.setQueue`.
|
|
193
|
+
/// surface doesn't wait for the first receiver status.
|
|
157
194
|
func seedStartMetadata(startIndex: Int) {
|
|
158
195
|
pushMetadata(forIndex: startIndex)
|
|
159
196
|
}
|
|
@@ -175,19 +212,22 @@ final class CastEventBridge {
|
|
|
175
212
|
|
|
176
213
|
/// Resolve a lib track index to its `TrackItem` and push its metadata
|
|
177
214
|
/// to the cast lockscreen controller. Out-of-range indices are
|
|
178
|
-
/// ignored (the controller keeps its prior snapshot).
|
|
215
|
+
/// ignored (the controller keeps its prior snapshot). The track list is
|
|
216
|
+
/// read on the queue that owns it, whichever thread this is called from.
|
|
179
217
|
private func pushMetadata(forIndex index: Int) {
|
|
180
218
|
guard let owner = owner else { return }
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
219
|
+
owner.playerQueue.async {
|
|
220
|
+
guard index >= 0, index < owner.tracks.count else { return }
|
|
221
|
+
let track = owner.tracks[index]
|
|
222
|
+
// The controller hops to main itself.
|
|
223
|
+
CastNowPlayingController.shared.updateTrackMetadata(
|
|
224
|
+
title: track.title,
|
|
225
|
+
artist: track.artist,
|
|
226
|
+
album: track.album,
|
|
227
|
+
durationSec: track.duration ?? 0,
|
|
228
|
+
artworkUrl: track.artworkUrl
|
|
229
|
+
)
|
|
230
|
+
}
|
|
191
231
|
}
|
|
192
232
|
|
|
193
233
|
}
|
|
@@ -66,19 +66,34 @@ final class CastNowPlayingController {
|
|
|
66
66
|
/// no-ops. Called from `TrackPlayer.configure` after the engine is
|
|
67
67
|
/// set up.
|
|
68
68
|
func start() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
onMain { [self] in
|
|
70
|
+
if self.activeChangeDisposer != nil { return }
|
|
71
|
+
self.activeChangeDisposer = PlaybackStateRouter.shared.addActiveChangeListener {
|
|
72
|
+
[weak self] session in
|
|
73
|
+
self?.handleActiveChange(to: session)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Every field here is main-thread state — the router and session
|
|
79
|
+
/// callbacks arrive on main — so the owner's entry points, called on its
|
|
80
|
+
/// player queue, hop rather than touch the fields where they are called.
|
|
81
|
+
private func onMain(_ body: @escaping () -> Void) {
|
|
82
|
+
if Thread.isMainThread {
|
|
83
|
+
body()
|
|
84
|
+
} else {
|
|
85
|
+
DispatchQueue.main.async(execute: body)
|
|
73
86
|
}
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
/// Stop observing + tear down any active mirror. Called from
|
|
77
90
|
/// `TrackPlayer.internalDestroy`.
|
|
78
91
|
func stop() {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
onMain {
|
|
93
|
+
self.deactivate()
|
|
94
|
+
self.activeChangeDisposer?()
|
|
95
|
+
self.activeChangeDisposer = nil
|
|
96
|
+
}
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
/// Push current-track metadata. Called by `TrackPlayer` when the
|
|
@@ -92,26 +107,33 @@ final class CastNowPlayingController {
|
|
|
92
107
|
durationSec: Double,
|
|
93
108
|
artworkUrl: String?
|
|
94
109
|
) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
onMain {
|
|
111
|
+
self.currentMetadata = TrackMetadata(
|
|
112
|
+
title: title, artist: artist, album: album,
|
|
113
|
+
durationSec: durationSec, artworkUrl: artworkUrl
|
|
114
|
+
)
|
|
115
|
+
if self.currentSession != nil {
|
|
116
|
+
self.refreshNowPlayingInfo()
|
|
117
|
+
self.resolveArtwork()
|
|
118
|
+
}
|
|
102
119
|
}
|
|
103
120
|
}
|
|
104
121
|
|
|
105
122
|
/// Forward the player's artwork HTTP config so authenticated artwork
|
|
106
123
|
/// URLs resolve during cast. Called from `TrackPlayer.configure`.
|
|
107
124
|
func configureArtwork(headers: [String: String]?, userAgent: String?) {
|
|
108
|
-
|
|
109
|
-
|
|
125
|
+
onMain {
|
|
126
|
+
self.artworkResolver.configHeaders = headers
|
|
127
|
+
self.artworkResolver.configUserAgent = userAgent
|
|
128
|
+
}
|
|
110
129
|
}
|
|
111
130
|
|
|
112
131
|
// MARK: - Active-flow observer
|
|
113
132
|
|
|
114
133
|
private func handleActiveChange(to session: CastSessionRemotePlayer?) {
|
|
134
|
+
// The router fires listeners on the caller's thread; every caller today
|
|
135
|
+
// is on main, and the fields here are read and written on that basis.
|
|
136
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
115
137
|
if let session, !session.isPushSink {
|
|
116
138
|
activate(with: session)
|
|
117
139
|
} else {
|
|
@@ -45,6 +45,17 @@ protocol CastSessionRemotePlayer: CastSession {
|
|
|
45
45
|
/// Snapshot of most-recent receiver-driven state.
|
|
46
46
|
var lastState: RemotePlaybackStateKind { get }
|
|
47
47
|
|
|
48
|
+
/// `true` once the receiver has reported any state other than `.idle`.
|
|
49
|
+
/// Set by the first such report and stays set for the session's lifetime;
|
|
50
|
+
/// nothing clears it.
|
|
51
|
+
///
|
|
52
|
+
/// State reads use this to decide whether the receiver can answer yet: a
|
|
53
|
+
/// session that has just activated still holds its initial `.idle`, and the
|
|
54
|
+
/// local engine is the better answer until the receiver speaks. Once it
|
|
55
|
+
/// has, the receiver is authoritative — including a later `.idle`, which
|
|
56
|
+
/// means it genuinely has nothing loaded.
|
|
57
|
+
var hasReportedState: Bool { get }
|
|
58
|
+
|
|
48
59
|
/// Most-recent receiver-reported stream position in milliseconds.
|
|
49
60
|
/// `0` before the first status arrives.
|
|
50
61
|
var lastPositionMs: Int64 { get }
|
|
@@ -15,27 +15,38 @@ import Foundation
|
|
|
15
15
|
/// Mirrors Android's `internal object CastTransportRouter`.
|
|
16
16
|
enum CastTransportRouter {
|
|
17
17
|
|
|
18
|
+
/// Hands a receiver command to main. The Cast SDK is main-only, while the
|
|
19
|
+
/// decision above it — whether a remote session is live — reads a
|
|
20
|
+
/// lock-guarded snapshot and is safe from any thread, so only the command
|
|
21
|
+
/// itself crosses.
|
|
22
|
+
/// Always asynchronous, never inline-when-already-on-main: mixing the two
|
|
23
|
+
/// lets a command issued later run before one issued earlier — a CarPlay tap
|
|
24
|
+
/// arriving on main would overtake a JS seek that is still enqueued.
|
|
25
|
+
private static func onMain(_ body: @escaping () -> Void) {
|
|
26
|
+
DispatchQueue.main.async(execute: body)
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
static func routePlay() -> Bool {
|
|
19
30
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
20
|
-
remote.play()
|
|
31
|
+
onMain { remote.play() }
|
|
21
32
|
return true
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
static func routePause() -> Bool {
|
|
25
36
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
26
|
-
remote.pause()
|
|
37
|
+
onMain { remote.pause() }
|
|
27
38
|
return true
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
static func routeStop() -> Bool {
|
|
31
42
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
32
|
-
remote.stop()
|
|
43
|
+
onMain { remote.stop() }
|
|
33
44
|
return true
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
static func routeSeekTo(positionMs: Int64) -> Bool {
|
|
37
48
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
38
|
-
remote.seekTo(positionMs: positionMs)
|
|
49
|
+
onMain { remote.seekTo(positionMs: positionMs) }
|
|
39
50
|
return true
|
|
40
51
|
}
|
|
41
52
|
|
|
@@ -46,31 +57,32 @@ enum CastTransportRouter {
|
|
|
46
57
|
/// session for AirPlay/local, where the caller seeks the local engine).
|
|
47
58
|
static func routeSeekBy(deltaMs: Int64) -> Bool {
|
|
48
59
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
49
|
-
remote.seekBy(deltaMs: deltaMs)
|
|
60
|
+
onMain { remote.seekBy(deltaMs: deltaMs) }
|
|
50
61
|
return true
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
static func routeSkipToNext() -> Bool {
|
|
54
65
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
55
|
-
remote.queueNext()
|
|
66
|
+
onMain { remote.queueNext() }
|
|
56
67
|
return true
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
static func routeSkipToPrevious() -> Bool {
|
|
60
71
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
61
|
-
remote.queuePrev()
|
|
72
|
+
onMain { remote.queuePrev() }
|
|
62
73
|
return true
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
static func routeSkipToIndex(index: Int) -> Bool {
|
|
66
77
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
67
|
-
remote.skipToQueueIndex(index: index)
|
|
78
|
+
onMain { remote.skipToQueueIndex(index: index) }
|
|
68
79
|
return true
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
static func routeSetVolume(value: Double) -> Bool {
|
|
72
83
|
guard let remote = PlaybackStateRouter.shared.activeRemote() else { return false }
|
|
73
|
-
|
|
84
|
+
let clamped = Float(max(0.0, min(1.0, value)))
|
|
85
|
+
onMain { remote.setVolume(clamped) }
|
|
74
86
|
return true
|
|
75
87
|
}
|
|
76
88
|
|