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
|
@@ -222,14 +222,10 @@ class Visualizer : HybridVisualizerSpec() {
|
|
|
222
222
|
* fftSize=2048 should expect proportionally higher GC pressure on
|
|
223
223
|
* the audio render thread.
|
|
224
224
|
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* emulator backend — observed empirically: post-FFT magnitudes
|
|
230
|
-
* surface as all-zero on the JS side, even though `bb.getFloat(0)`
|
|
231
|
-
* reads back the right value on the Kotlin side. The explicit
|
|
232
|
-
* putFloat loop sidesteps the whole class of issue.
|
|
225
|
+
* Writes with `putFloat` rather than `bb.asFloatBuffer().put(values)`:
|
|
226
|
+
* the view form delivered all-zero buffers to JS through
|
|
227
|
+
* `ArrayBuffer.wrap` while `bb.getFloat(0)` read back correctly in
|
|
228
|
+
* Kotlin. Cause not established.
|
|
233
229
|
*/
|
|
234
230
|
private fun floatArrayToArrayBuffer(values: FloatArray): ArrayBuffer {
|
|
235
231
|
val bb = ByteBuffer.allocateDirect(values.size * 4).order(ByteOrder.nativeOrder())
|
|
@@ -6,9 +6,10 @@ package com.margelo.nitro.queueplayer.cast
|
|
|
6
6
|
* `ChromecastBackend` for Cast, future DLNA / Sonos similarly. The
|
|
7
7
|
* core module never knows which backends are present.
|
|
8
8
|
*
|
|
9
|
-
* The interface is intentionally tiny. Anything richer
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The interface is intentionally tiny. Anything richer lives on the
|
|
10
|
+
* concrete backend or on the [CastSession] subtype that [connect]
|
|
11
|
+
* returns — volume on the session, the AirPlay 2 pairing-PIN flow on
|
|
12
|
+
* `AirPlayBackend.submitPairingPin`. Keeps the registry pluggable + the
|
|
12
13
|
* routing layer protocol-agnostic.
|
|
13
14
|
*/
|
|
14
15
|
interface CastBackend {
|
|
@@ -70,7 +70,7 @@ internal class CastEventBridge(private val owner: TrackPlayer) {
|
|
|
70
70
|
sessionJob = scope.launch {
|
|
71
71
|
launch {
|
|
72
72
|
session.state.collectLatest { remoteState ->
|
|
73
|
-
owner.emitRemoteState(
|
|
73
|
+
owner.emitRemoteState(mapRemotePlaybackState(remoteState), StateChangeReason.SYSTEM)
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
launch {
|
|
@@ -110,13 +110,4 @@ internal class CastEventBridge(private val owner: TrackPlayer) {
|
|
|
110
110
|
lastPositionMs = positionMs
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
private fun mapState(state: RemotePlaybackState): PlayerState = when (state) {
|
|
114
|
-
RemotePlaybackState.PLAYING -> PlayerState.PLAYING
|
|
115
|
-
RemotePlaybackState.PAUSED -> PlayerState.PAUSED
|
|
116
|
-
RemotePlaybackState.LOADING -> PlayerState.BUFFERING
|
|
117
|
-
RemotePlaybackState.BUFFERING -> PlayerState.BUFFERING
|
|
118
|
-
RemotePlaybackState.IDLE -> PlayerState.NONE
|
|
119
|
-
RemotePlaybackState.ENDED -> PlayerState.ENDED
|
|
120
|
-
RemotePlaybackState.ERROR -> PlayerState.ERROR
|
|
121
|
-
}
|
|
122
113
|
}
|
|
@@ -61,9 +61,10 @@ sealed class CastSession {
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Optional `Player` implementation for `MediaSession`. When
|
|
64
|
-
* non-null, `
|
|
65
|
-
* cast connect and back
|
|
66
|
-
* `RemoteCastPlayer` from
|
|
64
|
+
* non-null, `PlaybackService` retargets the session's forwarding
|
|
65
|
+
* player to this on cast connect and back to the local engine on
|
|
66
|
+
* disconnect. Chromecast returns a `RemoteCastPlayer` from
|
|
67
|
+
* `media3-cast`. Default null.
|
|
67
68
|
*/
|
|
68
69
|
open val mediaSessionPlayer: Player? = null
|
|
69
70
|
|
|
@@ -124,6 +125,19 @@ sealed class CastSession {
|
|
|
124
125
|
/** Receiver-driven playback state for `PlaybackStateRouter`. */
|
|
125
126
|
abstract val state: StateFlow<RemotePlaybackState>
|
|
126
127
|
|
|
128
|
+
/**
|
|
129
|
+
* `true` once the receiver has reported any state other than
|
|
130
|
+
* [RemotePlaybackState.IDLE]. Set by the first such report and stays
|
|
131
|
+
* set for the session's lifetime.
|
|
132
|
+
*
|
|
133
|
+
* State reads use this to decide whether the receiver can answer yet: a
|
|
134
|
+
* session that has just activated still holds its initial IDLE, and the
|
|
135
|
+
* local engine is the better answer until the receiver speaks. Once it
|
|
136
|
+
* has, the receiver is authoritative — including a later IDLE, which
|
|
137
|
+
* means it genuinely has nothing loaded.
|
|
138
|
+
*/
|
|
139
|
+
abstract val hasReportedState: Boolean
|
|
140
|
+
|
|
127
141
|
/** Receiver-driven position in ms. */
|
|
128
142
|
abstract val positionMs: StateFlow<Long>
|
|
129
143
|
|
|
@@ -30,6 +30,25 @@ internal object CastTransportRouter {
|
|
|
30
30
|
return true
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Flip the receiver's play state. The receiver's own state picks the
|
|
35
|
+
* direction — the local engine is parked and paused while casting, so its
|
|
36
|
+
* `playWhenReady` would resume the receiver on every press. Anything that
|
|
37
|
+
* represents "trying to play" toggles to pause, so a press during buffering
|
|
38
|
+
* stops rather than restarts.
|
|
39
|
+
*/
|
|
40
|
+
fun routeTogglePlayPause(): Boolean {
|
|
41
|
+
val remote = PlaybackStateRouter.activeRemote() ?: return false
|
|
42
|
+
val active = when (remote.state.value) {
|
|
43
|
+
RemotePlaybackState.PLAYING,
|
|
44
|
+
RemotePlaybackState.BUFFERING,
|
|
45
|
+
RemotePlaybackState.LOADING -> true
|
|
46
|
+
else -> false
|
|
47
|
+
}
|
|
48
|
+
if (active) remote.pause() else remote.play()
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
fun routeStop(): Boolean {
|
|
34
53
|
val remote = PlaybackStateRouter.activeRemote() ?: return false
|
|
35
54
|
remote.stop()
|
package/android/src/main/java/com/margelo/nitro/queueplayer/cast/RemotePlaybackStateMapping.kt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer.cast
|
|
2
|
+
|
|
3
|
+
import com.margelo.nitro.queueplayer.PlayerState
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Map the protocol-neutral receiver state to the JS-facing [PlayerState].
|
|
7
|
+
* Shared by the cast event fan-out and `TrackPlayer.getState`, so a state read
|
|
8
|
+
* and the event stream can't disagree about the same receiver.
|
|
9
|
+
*/
|
|
10
|
+
internal fun mapRemotePlaybackState(state: RemotePlaybackState): PlayerState = when (state) {
|
|
11
|
+
RemotePlaybackState.PLAYING -> PlayerState.PLAYING
|
|
12
|
+
RemotePlaybackState.PAUSED -> PlayerState.PAUSED
|
|
13
|
+
RemotePlaybackState.LOADING -> PlayerState.BUFFERING
|
|
14
|
+
RemotePlaybackState.BUFFERING -> PlayerState.BUFFERING
|
|
15
|
+
RemotePlaybackState.IDLE -> PlayerState.NONE
|
|
16
|
+
RemotePlaybackState.ENDED -> PlayerState.ENDED
|
|
17
|
+
RemotePlaybackState.ERROR -> PlayerState.ERROR
|
|
18
|
+
}
|
|
@@ -91,10 +91,8 @@ object AirPlay2JNI {
|
|
|
91
91
|
|
|
92
92
|
// -- Pairing --
|
|
93
93
|
|
|
94
|
-
@JvmStatic external fun nativeAp2NeedsPairing(handle: Long): Boolean
|
|
95
94
|
@JvmStatic external fun nativeAp2PairPinStart(handle: Long): Boolean
|
|
96
95
|
@JvmStatic external fun nativeAp2PairSetup(handle: Long, credential: String): String?
|
|
97
|
-
@JvmStatic external fun nativeAp2GetPairingState(handle: Long): Int
|
|
98
96
|
@JvmStatic external fun nativeAp2GetAuthMode(handle: Long): Int
|
|
99
97
|
@JvmStatic external fun nativeAp2GetDeviceId(handle: Long): String?
|
|
100
98
|
@JvmStatic external fun nativeAp2SetPassword(handle: Long, password: String)
|
|
@@ -9,6 +9,7 @@ import kotlinx.coroutines.CoroutineScope
|
|
|
9
9
|
import kotlinx.coroutines.Dispatchers
|
|
10
10
|
import kotlinx.coroutines.SupervisorJob
|
|
11
11
|
import kotlinx.coroutines.launch
|
|
12
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* A live AirPlay 2 streaming session backed by libairplay2 via [AirPlay2JNI].
|
|
@@ -28,7 +29,7 @@ class AirPlay2Session(
|
|
|
28
29
|
private val handle: Long,
|
|
29
30
|
private val host: String,
|
|
30
31
|
private val port: Int,
|
|
31
|
-
) : CastSession.PcmStream(), MetadataAwareSession {
|
|
32
|
+
) : CastSession.PcmStream(), MetadataAwareSession, MetadataSyncTarget {
|
|
32
33
|
|
|
33
34
|
companion object {
|
|
34
35
|
private const val TAG = "AirPlay2Session"
|
|
@@ -36,7 +37,10 @@ class AirPlay2Session(
|
|
|
36
37
|
|
|
37
38
|
private val sink = AirPlay2Sink(handle)
|
|
38
39
|
@Volatile private var closed = false
|
|
39
|
-
|
|
40
|
+
// Swapped on the player's looper by onActivated, read and cleared on the
|
|
41
|
+
// disconnecting thread — the exchange has to be atomic or a teardown can
|
|
42
|
+
// read a stale null and leave the previous sync running.
|
|
43
|
+
private val metadataSync = AtomicReference<AirPlayMetadataSync?>()
|
|
40
44
|
|
|
41
45
|
/** Internal scope for fire-and-forget native teardown. */
|
|
42
46
|
private val teardownScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
@@ -53,22 +57,27 @@ class AirPlay2Session(
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
override fun onActivated(player: Player, scope: CoroutineScope) {
|
|
56
|
-
|
|
60
|
+
if (closed) return
|
|
61
|
+
// Stop any prior sync before starting the replacement, so a repeat
|
|
57
62
|
// activation (the player swaps under a still-connected session) never
|
|
58
63
|
// leaves an orphaned sync running against the previous player.
|
|
59
|
-
|
|
60
|
-
metadataSync
|
|
64
|
+
val next = AirPlayMetadataSync(context, this, player, scope)
|
|
65
|
+
metadataSync.getAndSet(next)?.stop()
|
|
66
|
+
next.start()
|
|
67
|
+
// A close() that ran while this was publishing exchanged out a null
|
|
68
|
+
// and tore down without seeing `next`, so re-check: otherwise a live
|
|
69
|
+
// sync stays bound to a closed session for the process lifetime.
|
|
70
|
+
if (closed) metadataSync.getAndSet(null)?.stop()
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
override fun onDeactivated() {
|
|
64
|
-
metadataSync?.stop()
|
|
65
|
-
metadataSync = null
|
|
74
|
+
metadataSync.getAndSet(null)?.stop()
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
/**
|
|
69
78
|
* Invoked from the native drain thread (via [AirPlay2JNI.onNativeSessionDied])
|
|
70
79
|
* when the receiver has stopped responding. Notifies any [deathListener]
|
|
71
|
-
* so
|
|
80
|
+
* so `CastManager` can revert routing to local, then runs the normal close
|
|
72
81
|
* path — native TEARDOWN is skipped because the socket is dead.
|
|
73
82
|
*/
|
|
74
83
|
private fun onRemoteDied(reason: Int) {
|
|
@@ -91,7 +100,9 @@ class AirPlay2Session(
|
|
|
91
100
|
AirPlay2JNI.unregisterSession(handle)
|
|
92
101
|
|
|
93
102
|
/* Native TEARDOWN can block on a dead socket — never run it on the
|
|
94
|
-
* caller's thread (
|
|
103
|
+
* caller's thread (`CastManager.disconnect()` on the JS thread, a
|
|
104
|
+
* replacing `connect()` or `submitPairingPin()` on `Dispatchers.IO`,
|
|
105
|
+
* `onRemoteDied` on the native drain thread). */
|
|
95
106
|
val h = handle
|
|
96
107
|
teardownScope.launch {
|
|
97
108
|
try {
|
|
@@ -103,17 +114,17 @@ class AirPlay2Session(
|
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
fun setMetadata(title: String?, artist: String?, album: String?) {
|
|
117
|
+
override fun setMetadata(title: String?, artist: String?, album: String?) {
|
|
107
118
|
if (closed) return
|
|
108
119
|
AirPlay2JNI.nativeAp2SetMetadata(handle, title, artist, album)
|
|
109
120
|
}
|
|
110
121
|
|
|
111
|
-
fun setArtwork(imageData: ByteArray, contentType: String
|
|
122
|
+
override fun setArtwork(imageData: ByteArray, contentType: String) {
|
|
112
123
|
if (closed) return
|
|
113
124
|
AirPlay2JNI.nativeAp2SetArtwork(handle, imageData, imageData.size)
|
|
114
125
|
}
|
|
115
126
|
|
|
116
|
-
fun setProgress(elapsedMs: Int, durationMs: Int) {
|
|
127
|
+
override fun setProgress(elapsedMs: Int, durationMs: Int) {
|
|
117
128
|
if (closed) return
|
|
118
129
|
AirPlay2JNI.nativeAp2SetProgressMs(handle, elapsedMs.toLong(), durationMs.toLong())
|
|
119
130
|
}
|
|
@@ -97,22 +97,28 @@ internal class AirPlayBackend(private val context: Context) : CastBackend {
|
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
99
|
* Complete a deferred AP2 pairing by supplying the PIN / password the
|
|
100
|
-
* receiver demanded. Returns the
|
|
101
|
-
* pairing failure or when no pending pair
|
|
100
|
+
* receiver demanded. Returns the session and the receiver it was
|
|
101
|
+
* paired with; throws on pairing failure or when no pending pair
|
|
102
|
+
* exists.
|
|
102
103
|
*
|
|
103
104
|
* Callers use this when `CastManager.submitPairingPin(pin)` is invoked
|
|
104
105
|
* out-of-band of a follow-up `connect(receiverId, {password})` call.
|
|
105
106
|
*/
|
|
106
|
-
suspend fun submitPairingPin(pin: String):
|
|
107
|
+
suspend fun submitPairingPin(pin: String): PairedSession = withContext(Dispatchers.IO) {
|
|
107
108
|
val pending = synchronized(this@AirPlayBackend) { pendingPairState }
|
|
108
109
|
?: throw IllegalStateException("No pending AirPlay 2 pairing")
|
|
109
110
|
val receiver = discovery.findReceiver(pending.receiverId)
|
|
110
111
|
?: throw IllegalStateException(
|
|
111
112
|
"Receiver ${pending.receiverId} no longer visible — restart the connect flow"
|
|
112
113
|
)
|
|
114
|
+
// resumeAp2Pairing reports the id of the pair it consumed and
|
|
115
|
+
// validated against `receiver`; the read above only locates it.
|
|
113
116
|
resumeAp2Pairing(receiver, pin)
|
|
114
117
|
}
|
|
115
118
|
|
|
119
|
+
/** A session established by [submitPairingPin] and the receiver it was paired with. */
|
|
120
|
+
data class PairedSession(val session: CastSession, val receiverId: String)
|
|
121
|
+
|
|
116
122
|
/**
|
|
117
123
|
* Persistent store for AirPlay 2 pair-setup auth keys + passwords,
|
|
118
124
|
* keyed by the receiver's mDNS `deviceid` (MAC address).
|
|
@@ -306,7 +312,7 @@ internal class AirPlayBackend(private val context: Context) : CastBackend {
|
|
|
306
312
|
|
|
307
313
|
// --- Phase 2: credential supplied → resume pending pairing ---
|
|
308
314
|
if (credential != null) {
|
|
309
|
-
return resumeAp2Pairing(receiver, credential)
|
|
315
|
+
return resumeAp2Pairing(receiver, credential).session
|
|
310
316
|
}
|
|
311
317
|
|
|
312
318
|
// --- Phase 1: initial connection attempt ---
|
|
@@ -423,11 +429,12 @@ internal class AirPlayBackend(private val context: Context) : CastBackend {
|
|
|
423
429
|
}
|
|
424
430
|
|
|
425
431
|
/**
|
|
426
|
-
* Resume a pending AP2 pairing with user-provided credential
|
|
427
|
-
*
|
|
428
|
-
* [
|
|
432
|
+
* Resume a pending AP2 pairing with user-provided credential, from
|
|
433
|
+
* either `connect()` re-invoked with a password or [submitPairingPin].
|
|
434
|
+
* Consumes the pending pair, validates it is for [receiver], and
|
|
435
|
+
* returns the session together with that receiver's id.
|
|
429
436
|
*/
|
|
430
|
-
private fun resumeAp2Pairing(receiver: Receiver, credential: String):
|
|
437
|
+
private fun resumeAp2Pairing(receiver: Receiver, credential: String): PairedSession {
|
|
431
438
|
val pending = synchronized(this) {
|
|
432
439
|
val p = pendingPairState
|
|
433
440
|
pendingPairState = null
|
|
@@ -506,7 +513,7 @@ internal class AirPlayBackend(private val context: Context) : CastBackend {
|
|
|
506
513
|
}
|
|
507
514
|
|
|
508
515
|
Log.i(TAG, "Connected AP2 to ${receiver.name} via pair-verify")
|
|
509
|
-
return AirPlay2Session(context, newHandle, host, port)
|
|
516
|
+
return PairedSession(AirPlay2Session(context, newHandle, host, port), pending.receiverId)
|
|
510
517
|
}
|
|
511
518
|
|
|
512
519
|
/** Destroy any stale pending pairing handle. */
|
package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt
CHANGED
|
@@ -14,19 +14,23 @@ import java.io.ByteArrayOutputStream
|
|
|
14
14
|
import java.net.URL
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Syncs ExoPlayer track metadata and playback progress to an
|
|
17
|
+
* Syncs ExoPlayer track metadata and playback progress to an AirPlay
|
|
18
|
+
* receiver through [MetadataSyncTarget].
|
|
18
19
|
*
|
|
19
|
-
* - On activation and on every track transition: sends
|
|
20
|
+
* - On activation and on every track transition: sends metadata
|
|
20
21
|
* (title/artist/album) and kicks off an async artwork fetch.
|
|
21
22
|
* - Every 5 seconds while playing: sends progress (elapsed/duration).
|
|
22
23
|
* - On seek: sends progress immediately.
|
|
23
24
|
* - On stop: cancels all coroutines and removes the player listener.
|
|
25
|
+
*
|
|
26
|
+
* One instance covers one activation — [start] then [stop]. Starting again
|
|
27
|
+
* after a stop is a no-op; a new activation builds a new instance.
|
|
24
28
|
*/
|
|
25
29
|
internal class AirPlayMetadataSync(
|
|
26
30
|
private val context: Context,
|
|
27
|
-
private val
|
|
31
|
+
private val target: MetadataSyncTarget,
|
|
28
32
|
private val player: Player,
|
|
29
|
-
|
|
33
|
+
scope: CoroutineScope,
|
|
30
34
|
) {
|
|
31
35
|
companion object {
|
|
32
36
|
private const val TAG = "AirPlayMetadataSync"
|
|
@@ -35,11 +39,32 @@ internal class AirPlayMetadataSync(
|
|
|
35
39
|
private const val JPEG_QUALITY = 80
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Owns every coroutine this sync launches, so [stop] can cancel them as
|
|
44
|
+
* one atomic operation from any thread. Cancelling the scope needs no
|
|
45
|
+
* field read, so a teardown racing the player looper cannot miss a job,
|
|
46
|
+
* and a launch that loses the race starts already-cancelled rather than
|
|
47
|
+
* outliving the session it pushes into.
|
|
48
|
+
*/
|
|
49
|
+
private val syncScope =
|
|
50
|
+
CoroutineScope(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job]))
|
|
51
|
+
|
|
52
|
+
// Touched only on the player's application looper.
|
|
38
53
|
private var progressJob: Job? = null
|
|
39
54
|
private var artworkJob: Job? = null
|
|
40
55
|
private var lastArtworkUri: String? = null
|
|
56
|
+
|
|
57
|
+
// Written by start/stop on whichever thread activated or tore the
|
|
58
|
+
// session down; read on the looper and by the progress loop.
|
|
59
|
+
@Volatile
|
|
41
60
|
private var started = false
|
|
42
61
|
|
|
62
|
+
// Set on stop and never cleared, so a restart cannot half-revive a spent instance: the
|
|
63
|
+
// scope is already cancelled, so the listener would still push metadata
|
|
64
|
+
// while the progress loop and artwork fetch stay dead.
|
|
65
|
+
@Volatile
|
|
66
|
+
private var stopped = false
|
|
67
|
+
|
|
43
68
|
// ExoPlayer rejects add/removeListener + state reads off its application
|
|
44
69
|
// looper. start()/stop() are driven by session activation/close, and
|
|
45
70
|
// close() runs on whichever thread disconnected (the JS thread for a
|
|
@@ -48,6 +73,10 @@ internal class AirPlayMetadataSync(
|
|
|
48
73
|
// already on it.
|
|
49
74
|
private val playerHandler = Handler(player.applicationLooper)
|
|
50
75
|
|
|
76
|
+
// Both protocols log under one tag, so every line says which sender it
|
|
77
|
+
// came from.
|
|
78
|
+
private val targetName = target.javaClass.simpleName
|
|
79
|
+
|
|
51
80
|
private fun onPlayerLooper(block: () -> Unit) {
|
|
52
81
|
if (Looper.myLooper() == player.applicationLooper) block()
|
|
53
82
|
else playerHandler.post(block)
|
|
@@ -69,6 +98,7 @@ internal class AirPlayMetadataSync(
|
|
|
69
98
|
}
|
|
70
99
|
|
|
71
100
|
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
|
101
|
+
if (!started) return
|
|
72
102
|
if (playWhenReady) {
|
|
73
103
|
startProgressTimer()
|
|
74
104
|
} else {
|
|
@@ -79,37 +109,43 @@ internal class AirPlayMetadataSync(
|
|
|
79
109
|
}
|
|
80
110
|
|
|
81
111
|
fun start() {
|
|
82
|
-
if (started) return
|
|
112
|
+
if (started || stopped) return
|
|
83
113
|
started = true
|
|
84
114
|
onPlayerLooper {
|
|
115
|
+
// A stop() between this post and its execution has already run its
|
|
116
|
+
// own removeListener, so attaching now would leave the listener on
|
|
117
|
+
// the player for its lifetime.
|
|
118
|
+
if (stopped) return@onPlayerLooper
|
|
85
119
|
player.addListener(listener)
|
|
86
120
|
sendCurrentMetadata()
|
|
87
|
-
if (player.playWhenReady) {
|
|
121
|
+
if (started && player.playWhenReady) {
|
|
88
122
|
startProgressTimer()
|
|
89
123
|
}
|
|
90
124
|
}
|
|
91
|
-
Log.i(TAG, "Started metadata sync")
|
|
125
|
+
Log.i(TAG, "Started metadata sync for $targetName")
|
|
92
126
|
}
|
|
93
127
|
|
|
94
128
|
fun stop() {
|
|
95
|
-
if (
|
|
129
|
+
if (stopped) return
|
|
130
|
+
stopped = true
|
|
96
131
|
started = false
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
132
|
+
syncScope.cancel()
|
|
133
|
+
// Listener removal has to hop to the looper, so a callback queued
|
|
134
|
+
// ahead of it can still run; the `started` gate in the senders is
|
|
135
|
+
// what stops those from reaching a session that is being closed.
|
|
101
136
|
onPlayerLooper { player.removeListener(listener) }
|
|
102
|
-
Log.i(TAG, "Stopped metadata sync")
|
|
137
|
+
Log.i(TAG, "Stopped metadata sync for $targetName")
|
|
103
138
|
}
|
|
104
139
|
|
|
105
140
|
private fun sendCurrentMetadata() {
|
|
141
|
+
if (!started) return
|
|
106
142
|
val meta = player.currentMediaItem?.mediaMetadata ?: return
|
|
107
143
|
val title = meta.title?.toString()
|
|
108
144
|
val artist = meta.artist?.toString()
|
|
109
145
|
val album = meta.albumTitle?.toString()
|
|
110
146
|
|
|
111
|
-
Log.i(TAG, "
|
|
112
|
-
|
|
147
|
+
Log.i(TAG, "$targetName sending metadata: title=$title, artist=$artist, album=$album")
|
|
148
|
+
target.setMetadata(title, artist, album)
|
|
113
149
|
|
|
114
150
|
// Send initial progress for the new track
|
|
115
151
|
sendProgress()
|
|
@@ -123,16 +159,17 @@ internal class AirPlayMetadataSync(
|
|
|
123
159
|
}
|
|
124
160
|
|
|
125
161
|
private fun sendProgress() {
|
|
162
|
+
if (!started) return
|
|
126
163
|
val position = player.currentPosition.toInt()
|
|
127
164
|
val duration = player.duration.let { if (it > 0) it.toInt() else 0 }
|
|
128
165
|
if (duration > 0) {
|
|
129
|
-
|
|
166
|
+
target.setProgress(position, duration)
|
|
130
167
|
}
|
|
131
168
|
}
|
|
132
169
|
|
|
133
170
|
private fun startProgressTimer() {
|
|
134
171
|
progressJob?.cancel()
|
|
135
|
-
progressJob =
|
|
172
|
+
progressJob = syncScope.launch {
|
|
136
173
|
while (isActive) {
|
|
137
174
|
delay(PROGRESS_INTERVAL_MS)
|
|
138
175
|
if (player.isPlaying) {
|
|
@@ -144,7 +181,7 @@ internal class AirPlayMetadataSync(
|
|
|
144
181
|
|
|
145
182
|
private fun fetchAndSendArtwork(uriString: String) {
|
|
146
183
|
artworkJob?.cancel()
|
|
147
|
-
artworkJob =
|
|
184
|
+
artworkJob = syncScope.launch(Dispatchers.IO) {
|
|
148
185
|
try {
|
|
149
186
|
val uri = Uri.parse(uriString)
|
|
150
187
|
val scheme = uri.scheme ?: return@launch
|
|
@@ -185,9 +222,9 @@ internal class AirPlayMetadataSync(
|
|
|
185
222
|
bitmap.recycle()
|
|
186
223
|
|
|
187
224
|
val bytes = baos.toByteArray()
|
|
188
|
-
if (isActive) {
|
|
189
|
-
|
|
190
|
-
Log.i(TAG, "
|
|
225
|
+
if (isActive && started) {
|
|
226
|
+
target.setArtwork(bytes, "image/jpeg")
|
|
227
|
+
Log.i(TAG, "$targetName sent artwork: ${bytes.size} bytes from $uriString")
|
|
191
228
|
}
|
|
192
229
|
} catch (e: Exception) {
|
|
193
230
|
if (e !is CancellationException) {
|
package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt
CHANGED
|
@@ -10,12 +10,12 @@ import androidx.media3.exoplayer.audio.AudioSink
|
|
|
10
10
|
* [AudioSink] IS the AirPlay session's sink ([AirPlay2Sink] / [AirPlaySink]).
|
|
11
11
|
*
|
|
12
12
|
* Deliberately a bare [DefaultRenderersFactory]: no `setEnableAudioFloatOutput`,
|
|
13
|
-
* no audio processors. The AirPlay sinks
|
|
14
|
-
* `ENCODING_PCM_16BIT`, and the default factory
|
|
15
|
-
* renderer negotiates a format the sink accepts
|
|
16
|
-
* receiver clean.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* no audio processors, no extension-renderer preference. The AirPlay sinks
|
|
14
|
+
* only accept `AUDIO_RAW` + `ENCODING_PCM_16BIT`, and the default factory
|
|
15
|
+
* decodes to Int16 PCM — so the renderer negotiates a format the sink accepts
|
|
16
|
+
* and the PCM reaches the receiver clean. Float output or a processor chain
|
|
17
|
+
* in front of the sink corrupts the AirPlay stream, which is why this engine
|
|
18
|
+
* has its own factory instead of sharing the local pipeline's.
|
|
19
19
|
*
|
|
20
20
|
* One sink per factory instance — the AirPlay engine is built fresh for each
|
|
21
21
|
* session and released when the session ends, so the sink is never swapped at
|
|
@@ -27,14 +27,6 @@ internal class AirPlayRenderersFactory(
|
|
|
27
27
|
private val airplaySink: AudioSink,
|
|
28
28
|
) : DefaultRenderersFactory(context) {
|
|
29
29
|
|
|
30
|
-
init {
|
|
31
|
-
// Match the known-good fork AirPlay config: prefer extension decoders.
|
|
32
|
-
// No-op when no extension renderers are bundled, but it keeps the
|
|
33
|
-
// decoder-selection behaviour identical to the reference that streams
|
|
34
|
-
// clean PCM to these sinks.
|
|
35
|
-
setExtensionRendererMode(EXTENSION_RENDERER_MODE_PREFER)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
30
|
override fun buildAudioSink(
|
|
39
31
|
context: Context,
|
|
40
32
|
enableFloatOutput: Boolean,
|
|
@@ -6,6 +6,7 @@ import androidx.media3.exoplayer.audio.AudioSink
|
|
|
6
6
|
import com.margelo.nitro.queueplayer.cast.CastSession
|
|
7
7
|
import com.margelo.nitro.queueplayer.cast.MetadataAwareSession
|
|
8
8
|
import kotlinx.coroutines.CoroutineScope
|
|
9
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* A live AirPlay 1 streaming session backed by libraop via [AirplayJNI].
|
|
@@ -25,15 +26,22 @@ class AirPlaySession(
|
|
|
25
26
|
private val handle: Long,
|
|
26
27
|
private val host: String,
|
|
27
28
|
private val port: Int,
|
|
28
|
-
) : CastSession.PcmStream(), MetadataAwareSession {
|
|
29
|
+
) : CastSession.PcmStream(), MetadataAwareSession, MetadataSyncTarget {
|
|
29
30
|
|
|
30
31
|
companion object {
|
|
31
32
|
private const val TAG = "AirPlaySession"
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
private val sink = AirPlaySink(handle)
|
|
36
|
+
|
|
37
|
+
// Read by every sender, set by close() on whichever thread disconnected.
|
|
38
|
+
@Volatile
|
|
35
39
|
private var closed = false
|
|
36
|
-
|
|
40
|
+
|
|
41
|
+
// Swapped on the player's looper by onActivated, read and cleared on the
|
|
42
|
+
// disconnecting thread — the exchange has to be atomic or a teardown can
|
|
43
|
+
// read a stale null and leave the previous sync running.
|
|
44
|
+
private val metadataSync = AtomicReference<AirPlayMetadataSync?>()
|
|
37
45
|
|
|
38
46
|
override fun audioSink(): AudioSink = sink
|
|
39
47
|
|
|
@@ -46,22 +54,29 @@ class AirPlaySession(
|
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
override fun onActivated(player: Player, scope: CoroutineScope) {
|
|
49
|
-
|
|
57
|
+
if (closed) return
|
|
58
|
+
// Stop any prior sync before starting the replacement, so a repeat
|
|
50
59
|
// activation (the player swaps under a still-connected session) never
|
|
51
60
|
// leaves an orphaned sync running against the previous player.
|
|
52
|
-
|
|
53
|
-
metadataSync
|
|
61
|
+
val next = AirPlayMetadataSync(context, this, player, scope)
|
|
62
|
+
metadataSync.getAndSet(next)?.stop()
|
|
63
|
+
next.start()
|
|
64
|
+
// A close() that ran while this was publishing exchanged out a null
|
|
65
|
+
// and tore down without seeing `next`, so re-check: otherwise a live
|
|
66
|
+
// sync stays bound to a closed session for the process lifetime.
|
|
67
|
+
if (closed) metadataSync.getAndSet(null)?.stop()
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
override fun onDeactivated() {
|
|
57
|
-
metadataSync?.stop()
|
|
58
|
-
metadataSync = null
|
|
71
|
+
metadataSync.getAndSet(null)?.stop()
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
override fun close() {
|
|
62
75
|
if (closed) return
|
|
63
|
-
|
|
76
|
+
// Closed before the sync is torn down so an in-flight sender bails at
|
|
77
|
+
// its own guard rather than reaching a handle nativeDestroy has freed.
|
|
64
78
|
closed = true
|
|
79
|
+
onDeactivated()
|
|
65
80
|
Log.i(TAG, "Closing AirPlay session to $host:$port")
|
|
66
81
|
// Release the sink BEFORE the native handle is torn down: this sets
|
|
67
82
|
// the sink's volatile `released` flag so the renderer thread stops
|
|
@@ -73,20 +88,17 @@ class AirPlaySession(
|
|
|
73
88
|
AirplayJNI.nativeDestroy(handle)
|
|
74
89
|
}
|
|
75
90
|
|
|
76
|
-
|
|
77
|
-
fun setMetadata(title: String?, artist: String?, album: String?) {
|
|
91
|
+
override fun setMetadata(title: String?, artist: String?, album: String?) {
|
|
78
92
|
if (closed) return
|
|
79
93
|
AirplayJNI.nativeSetDaap(handle, title, artist, album)
|
|
80
94
|
}
|
|
81
95
|
|
|
82
|
-
|
|
83
|
-
fun setArtwork(imageData: ByteArray, contentType: String = "image/jpeg") {
|
|
96
|
+
override fun setArtwork(imageData: ByteArray, contentType: String) {
|
|
84
97
|
if (closed) return
|
|
85
98
|
AirplayJNI.nativeSetArtwork(handle, imageData, imageData.size, contentType)
|
|
86
99
|
}
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
fun setProgress(elapsedMs: Int, durationMs: Int) {
|
|
101
|
+
override fun setProgress(elapsedMs: Int, durationMs: Int) {
|
|
90
102
|
if (closed) return
|
|
91
103
|
AirplayJNI.nativeSetProgressMs(handle, elapsedMs, durationMs)
|
|
92
104
|
}
|
|
@@ -40,11 +40,6 @@ object AirplayJNI {
|
|
|
40
40
|
false
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// -- Library info --
|
|
44
|
-
|
|
45
|
-
@JvmStatic external fun nativeVersion(): String
|
|
46
|
-
@JvmStatic external fun nativeInitSsl(): Boolean
|
|
47
|
-
|
|
48
43
|
// -- Session lifecycle --
|
|
49
44
|
|
|
50
45
|
/**
|
|
@@ -73,10 +68,8 @@ object AirplayJNI {
|
|
|
73
68
|
/** Bytes waiting in the ring buffer, not yet sent. Used for hasPendingData(). */
|
|
74
69
|
@JvmStatic external fun nativeRingAvailable(handle: Long): Int
|
|
75
70
|
|
|
76
|
-
// --
|
|
71
|
+
// -- Transport control --
|
|
77
72
|
|
|
78
|
-
@JvmStatic external fun nativeAcceptFrames(handle: Long): Boolean
|
|
79
|
-
@JvmStatic external fun nativeSendChunk(handle: Long, data: ByteArray, offset: Int, length: Int): Boolean
|
|
80
73
|
@JvmStatic external fun nativePause(handle: Long)
|
|
81
74
|
@JvmStatic external fun nativeStop(handle: Long)
|
|
82
75
|
@JvmStatic external fun nativeFlush(handle: Long): Boolean
|
|
@@ -91,16 +84,11 @@ object AirplayJNI {
|
|
|
91
84
|
|
|
92
85
|
@JvmStatic external fun nativeSetVolume(handle: Long, volume: Float): Boolean
|
|
93
86
|
@JvmStatic external fun nativeSetProgressMs(handle: Long, elapsed: Int, duration: Int): Boolean
|
|
94
|
-
@JvmStatic external fun nativeGetProgressMs(handle: Long): Long
|
|
95
87
|
@JvmStatic external fun nativeSetDaap(handle: Long, title: String?, artist: String?, album: String?): Boolean
|
|
96
88
|
@JvmStatic external fun nativeSetArtwork(handle: Long, imageData: ByteArray, length: Int, contentType: String): Boolean
|
|
97
89
|
|
|
98
90
|
// -- State queries --
|
|
99
91
|
|
|
100
92
|
@JvmStatic external fun nativeIsConnected(handle: Long): Boolean
|
|
101
|
-
@JvmStatic external fun nativeIsPlaying(handle: Long): Boolean
|
|
102
|
-
@JvmStatic external fun nativeGetState(handle: Long): Int
|
|
103
|
-
@JvmStatic external fun nativeGetLatency(handle: Long): Int
|
|
104
|
-
@JvmStatic external fun nativeGetSampleRate(handle: Long): Int
|
|
105
93
|
@JvmStatic external fun nativeKeepalive(handle: Long): Boolean
|
|
106
94
|
}
|