react-native-queue-player 1.2.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/consumer-rules.pro +1 -1
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +3 -19
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +8 -69
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +111 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +78 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaResponseCheck.kt +156 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaValidatingDataSource.kt +165 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/MimeCapturingDataSource.kt +18 -9
- 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 +13 -5
- 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 +985 -276
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +84 -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/MediaResponseCheckTest.kt +163 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaValidatingDataSourceTest.kt +189 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +122 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +291 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +2 -2
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +672 -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 +229 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaResponseCheck.swift +122 -0
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MediaResponseCheckTests.swift +128 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +418 -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 +441 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +206 -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 +1042 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +982 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +951 -0
- package/ios/TrackPlayer+Recovery.swift +213 -0
- package/ios/TrackPlayer+Skip.swift +465 -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 +272 -0
- package/ios/TrackPlayer+Window.swift +227 -0
- package/ios/TrackPlayer.swift +331 -4619
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
import androidx.media3.common.C
|
|
5
|
+
import androidx.media3.common.PlaybackException
|
|
6
|
+
import androidx.media3.common.util.UnstableApi
|
|
7
|
+
import androidx.media3.datasource.DataSource
|
|
8
|
+
import androidx.media3.datasource.DataSpec
|
|
9
|
+
import androidx.media3.datasource.HttpDataSource
|
|
10
|
+
import androidx.media3.datasource.TransferListener
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wraps an HTTP-capable [DataSource.Factory] so a response that is a document
|
|
14
|
+
* rather than media fails before any of it is returned.
|
|
15
|
+
*
|
|
16
|
+
* Installed on the prefetcher's upstream only. Media3 writes to its cache sink
|
|
17
|
+
* only after this `read` returns, so throwing instead of returning keeps the
|
|
18
|
+
* body out of the cache — and the playback path is left alone, because there a
|
|
19
|
+
* refusal would be an unplayable track rather than a prefetch that did not
|
|
20
|
+
* happen. Playback cannot poison the cache either: its `CacheDataSource` is
|
|
21
|
+
* built read-only.
|
|
22
|
+
*
|
|
23
|
+
* Transparent for a source with no response headers (file:// / asset://):
|
|
24
|
+
* those carry no `Content-Type` and are not the case this exists for.
|
|
25
|
+
*/
|
|
26
|
+
@UnstableApi
|
|
27
|
+
internal class MediaValidatingDataSourceFactory(
|
|
28
|
+
private val upstream: DataSource.Factory
|
|
29
|
+
) : DataSource.Factory {
|
|
30
|
+
override fun createDataSource(): DataSource =
|
|
31
|
+
MediaValidatingDataSource(upstream.createDataSource())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@UnstableApi
|
|
35
|
+
internal class MediaValidatingDataSource(
|
|
36
|
+
private val delegate: DataSource
|
|
37
|
+
) : DataSource {
|
|
38
|
+
|
|
39
|
+
private var dataSpec: DataSpec? = null
|
|
40
|
+
private var contentType: String? = null
|
|
41
|
+
private var isHttp = false
|
|
42
|
+
private var startsAtResourceStart = true
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Bytes read from the delegate to judge the body, held back until the verdict
|
|
46
|
+
* is in and then served from here before the delegate is read again. Null once
|
|
47
|
+
* drained — every later read passes straight through.
|
|
48
|
+
*/
|
|
49
|
+
private var head: ByteArray? = null
|
|
50
|
+
private var headOffset = 0
|
|
51
|
+
private var bodyJudged = false
|
|
52
|
+
|
|
53
|
+
override fun addTransferListener(transferListener: TransferListener) {
|
|
54
|
+
delegate.addTransferListener(transferListener)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun open(dataSpec: DataSpec): Long {
|
|
58
|
+
this.dataSpec = dataSpec
|
|
59
|
+
head = null
|
|
60
|
+
headOffset = 0
|
|
61
|
+
bodyJudged = false
|
|
62
|
+
val length = delegate.open(dataSpec)
|
|
63
|
+
// The scheme says whether this is a remote source. Response headers cannot:
|
|
64
|
+
// an HTTP answer that carries no `Content-Type` has none, and that is
|
|
65
|
+
// precisely a body this has to judge. Casting to `HttpDataSource` cannot
|
|
66
|
+
// either — a wrapper is a `DataSource`, so the cast misses whenever one
|
|
67
|
+
// sits between this and the HTTP source.
|
|
68
|
+
isHttp = dataSpec.uri.scheme?.lowercase() in setOf("http", "https")
|
|
69
|
+
if (!isHttp) {
|
|
70
|
+
bodyJudged = true
|
|
71
|
+
return length
|
|
72
|
+
}
|
|
73
|
+
// `responseHeaders` is declared on `DataSource`, so it reads through a
|
|
74
|
+
// wrapper. Header names are case-insensitive and not every server or proxy
|
|
75
|
+
// normalises them.
|
|
76
|
+
contentType = delegate.responseHeaders.entries
|
|
77
|
+
.firstOrNull { it.key.equals("Content-Type", ignoreCase = true) }
|
|
78
|
+
?.value?.firstOrNull()
|
|
79
|
+
startsAtResourceStart = dataSpec.position == 0L
|
|
80
|
+
// A declared document type is refused here, before a byte is read.
|
|
81
|
+
MediaResponseCheck.rejection(
|
|
82
|
+
contentType = contentType,
|
|
83
|
+
head = EMPTY,
|
|
84
|
+
headIsStartOfResource = false
|
|
85
|
+
)?.let { throw rejected(it, dataSpec, HttpDataSource.HttpDataSourceException.TYPE_OPEN) }
|
|
86
|
+
return length
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
|
|
90
|
+
if (!bodyJudged) judgeBody()
|
|
91
|
+
val pending = head
|
|
92
|
+
if (pending != null) {
|
|
93
|
+
val remaining = pending.size - headOffset
|
|
94
|
+
val take = minOf(remaining, length)
|
|
95
|
+
pending.copyInto(buffer, offset, headOffset, headOffset + take)
|
|
96
|
+
headOffset += take
|
|
97
|
+
if (headOffset == pending.size) head = null
|
|
98
|
+
return take
|
|
99
|
+
}
|
|
100
|
+
return delegate.read(buffer, offset, length)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Pull enough of the body from the delegate to read a container signature,
|
|
105
|
+
* judge it, and keep it for [read] to serve. Nothing reaches the caller — and
|
|
106
|
+
* so nothing reaches the cache — until this has run.
|
|
107
|
+
*/
|
|
108
|
+
private fun judgeBody() {
|
|
109
|
+
bodyJudged = true
|
|
110
|
+
if (!isHttp) return
|
|
111
|
+
val scratch = ByteArray(MediaResponseCheck.HEAD_BYTES)
|
|
112
|
+
var filled = 0
|
|
113
|
+
while (filled < scratch.size) {
|
|
114
|
+
val read = delegate.read(scratch, filled, scratch.size - filled)
|
|
115
|
+
// A zero-length read would otherwise spin here; treat it as nothing more
|
|
116
|
+
// to judge, the same as the end of the input.
|
|
117
|
+
if (read == C.RESULT_END_OF_INPUT || read == 0) break
|
|
118
|
+
filled += read
|
|
119
|
+
}
|
|
120
|
+
if (filled == 0) return
|
|
121
|
+
val body = if (filled == scratch.size) scratch else scratch.copyOf(filled)
|
|
122
|
+
MediaResponseCheck.rejection(
|
|
123
|
+
contentType = contentType,
|
|
124
|
+
head = body,
|
|
125
|
+
headIsStartOfResource = startsAtResourceStart
|
|
126
|
+
)?.let { throw rejected(it, dataSpec, HttpDataSource.HttpDataSourceException.TYPE_READ) }
|
|
127
|
+
head = body
|
|
128
|
+
headOffset = 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private fun rejected(
|
|
132
|
+
rejection: MediaResponseCheck.Rejection,
|
|
133
|
+
spec: DataSpec?,
|
|
134
|
+
type: Int
|
|
135
|
+
): HttpDataSource.HttpDataSourceException {
|
|
136
|
+
val detail = when (rejection) {
|
|
137
|
+
is MediaResponseCheck.Rejection.MarkupContentType ->
|
|
138
|
+
"declared type ${rejection.contentType} is a document, not media"
|
|
139
|
+
is MediaResponseCheck.Rejection.TextWithoutMediaSignature ->
|
|
140
|
+
"declared type ${rejection.contentType} with no recognised container signature"
|
|
141
|
+
MediaResponseCheck.Rejection.MarkupBody ->
|
|
142
|
+
"body begins as a markup or JSON document"
|
|
143
|
+
}
|
|
144
|
+
return HttpDataSource.HttpDataSourceException(
|
|
145
|
+
"Response is not media: $detail",
|
|
146
|
+
spec ?: DataSpec(Uri.EMPTY),
|
|
147
|
+
PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE,
|
|
148
|
+
type
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
override fun getUri(): Uri? = delegate.uri
|
|
153
|
+
|
|
154
|
+
override fun close() {
|
|
155
|
+
head = null
|
|
156
|
+
headOffset = 0
|
|
157
|
+
delegate.close()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
override fun getResponseHeaders(): Map<String, List<String>> = delegate.responseHeaders
|
|
161
|
+
|
|
162
|
+
private companion object {
|
|
163
|
+
val EMPTY = ByteArray(0)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -4,7 +4,6 @@ import android.net.Uri
|
|
|
4
4
|
import androidx.media3.common.util.UnstableApi
|
|
5
5
|
import androidx.media3.datasource.DataSource
|
|
6
6
|
import androidx.media3.datasource.DataSpec
|
|
7
|
-
import androidx.media3.datasource.HttpDataSource
|
|
8
7
|
import androidx.media3.datasource.TransferListener
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -17,8 +16,8 @@ import androidx.media3.datasource.TransferListener
|
|
|
17
16
|
* type observed during prefetch and let the playback path pass it
|
|
18
17
|
* to Media3 via [androidx.media3.common.MediaItem.Builder.setMimeType].
|
|
19
18
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* Only the prefetch path builds this, and its window holds http/https URLs
|
|
20
|
+
* alone, so every response it sees is a server's.
|
|
22
21
|
*/
|
|
23
22
|
@UnstableApi
|
|
24
23
|
internal class MimeCapturingDataSourceFactory(
|
|
@@ -41,12 +40,13 @@ internal class MimeCapturingDataSource(
|
|
|
41
40
|
|
|
42
41
|
override fun open(dataSpec: DataSpec): Long {
|
|
43
42
|
val length = delegate.open(dataSpec)
|
|
44
|
-
|
|
45
|
-
// HTTP
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
// `responseHeaders` is declared on `DataSource`, so this reads through
|
|
44
|
+
// whatever wraps the HTTP source — casting to `HttpDataSource` would miss,
|
|
45
|
+
// because a wrapper is a `DataSource`.
|
|
46
|
+
// Reported even when null: the sink resolves a declared type first and
|
|
47
|
+
// falls back to the URL's extension, so a response with no `Content-Type`
|
|
48
|
+
// still gets a usable type recorded.
|
|
49
|
+
onMime(dataSpec.uri.toString(), contentTypeOf(delegate.responseHeaders))
|
|
50
50
|
return length
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -61,4 +61,13 @@ internal class MimeCapturingDataSource(
|
|
|
61
61
|
|
|
62
62
|
override fun getResponseHeaders(): Map<String, List<String>> =
|
|
63
63
|
delegate.responseHeaders
|
|
64
|
+
|
|
65
|
+
private companion object {
|
|
66
|
+
/** HTTP header names are case-insensitive; some servers and proxies emit
|
|
67
|
+
* lowercase and Media3 does not always normalise. */
|
|
68
|
+
fun contentTypeOf(headers: Map<String, List<String>>): String? =
|
|
69
|
+
headers.entries
|
|
70
|
+
.firstOrNull { it.key.equals("Content-Type", ignoreCase = true) }
|
|
71
|
+
?.value?.firstOrNull()
|
|
72
|
+
}
|
|
64
73
|
}
|
|
@@ -28,6 +28,42 @@ import com.margelo.nitro.core.NullType
|
|
|
28
28
|
@UnstableApi
|
|
29
29
|
internal object NowPlayingFormatExtractor {
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Average encoded bitrate of a `file://` source, from its size and duration.
|
|
33
|
+
*
|
|
34
|
+
* The figure is an average over the whole file, which is what "bitrate"
|
|
35
|
+
* means for a variable-rate codec anyway, and it includes the container's
|
|
36
|
+
* headers and any embedded artwork — a few kbps on a typical track.
|
|
37
|
+
*
|
|
38
|
+
* Returns null for any non-local source, for a file that cannot be measured,
|
|
39
|
+
* and for a duration the player has not resolved yet: a bitrate derived from
|
|
40
|
+
* an unknown duration would be arbitrary, and reporting nothing is the
|
|
41
|
+
* honest answer until the player knows.
|
|
42
|
+
*/
|
|
43
|
+
private fun deriveLocalBitrate(player: Player, durationSec: Double?): Double? {
|
|
44
|
+
val uri = player.currentMediaItem?.localConfiguration?.uri ?: return null
|
|
45
|
+
if (!uri.scheme.equals("file", ignoreCase = true)) return null
|
|
46
|
+
return averageBitrateOf(uri.path, durationSec)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Average bits per second of the file at [path] over [durationSec].
|
|
51
|
+
*
|
|
52
|
+
* Split from its caller so the arithmetic is reachable without a player.
|
|
53
|
+
* Null for an unusable duration, an unreadable path, or an empty file —
|
|
54
|
+
* every case where the answer would be invented rather than measured.
|
|
55
|
+
*/
|
|
56
|
+
internal fun averageBitrateOf(path: String?, durationSec: Double?): Double? {
|
|
57
|
+
if (durationSec == null || durationSec <= 0.0) return null
|
|
58
|
+
if (path == null) return null
|
|
59
|
+
// One `stat` of a local file, taken on the looper with the rest of the
|
|
60
|
+
// player snapshot; the answer is not worth an extra hop.
|
|
61
|
+
val bytes = runCatching { java.io.File(path).length() }.getOrNull() ?: return null
|
|
62
|
+
if (bytes <= 0L) return null
|
|
63
|
+
val bitsPerSecond = bytes * 8.0 / durationSec
|
|
64
|
+
return if (bitsPerSecond.isFinite() && bitsPerSecond > 0.0) bitsPerSecond else null
|
|
65
|
+
}
|
|
66
|
+
|
|
31
67
|
/**
|
|
32
68
|
* Extract the format snapshot from `player`'s currently selected
|
|
33
69
|
* audio track. Returns `null` only when no audio track exists in the
|
|
@@ -53,7 +89,7 @@ internal object NowPlayingFormatExtractor {
|
|
|
53
89
|
val mimeType = format.sampleMimeType
|
|
54
90
|
val sampleRate = format.sampleRate.takeIf { it != Format.NO_VALUE }?.toDouble()
|
|
55
91
|
val channelCount = format.channelCount.takeIf { it != Format.NO_VALUE }?.toDouble()
|
|
56
|
-
val
|
|
92
|
+
val reportedBitrate = format.bitrate.takeIf { it != Format.NO_VALUE }?.toDouble()
|
|
57
93
|
val peakBitrate = format.peakBitrate.takeIf { it != Format.NO_VALUE }?.toDouble()
|
|
58
94
|
val pcmEncoding = format.pcmEncoding.takeIf { it != Format.NO_VALUE }?.toDouble()
|
|
59
95
|
val codecsString = format.codecs
|
|
@@ -61,6 +97,17 @@ internal object NowPlayingFormatExtractor {
|
|
|
61
97
|
val durationSec = if (durationMs == C.TIME_UNSET || durationMs <= 0) null
|
|
62
98
|
else durationMs / 1000.0
|
|
63
99
|
|
|
100
|
+
// Media3 leaves both bitrate fields unset for some codecs — FLAC reports
|
|
101
|
+
// NO_VALUE for average and peak alike, on local files as well as remote —
|
|
102
|
+
// so a consumer asking what it is playing gets nothing back. Where the
|
|
103
|
+
// source is on disk the average is arithmetic: encoded bytes over playing
|
|
104
|
+
// time. Not gated on codec; it fills in whatever the extractor did not
|
|
105
|
+
// report, and does nothing when Media3 already answered.
|
|
106
|
+
//
|
|
107
|
+
// A local stat only. A remote source would need its Content-Length, which
|
|
108
|
+
// costs a request the library deliberately does not make.
|
|
109
|
+
val bitrate = reportedBitrate ?: deriveLocalBitrate(player, durationSec)
|
|
110
|
+
|
|
64
111
|
val rgData = engine?.currentReplayGainData()
|
|
65
112
|
val rgActive = engine?.currentReplayGainActive() ?: false
|
|
66
113
|
|
|
@@ -9,8 +9,9 @@ import androidx.media3.common.PlaybackParameters
|
|
|
9
9
|
* time-stretch resamples, so pitch shifts with speed.
|
|
10
10
|
* - `voice` → pitch preserved (`pitch == 1`): the built-in (speech-tuned)
|
|
11
11
|
* time-stretch holds pitch while changing speed.
|
|
12
|
-
* - `music` →
|
|
13
|
-
*
|
|
12
|
+
* - `music` → the same parameters as `voice` (`pitch == 1`); which
|
|
13
|
+
* stretcher applies them is `PitchAwareAudioProcessorChain`'s choice —
|
|
14
|
+
* `SignalsmithStretchAudioProcessor` for `music`, Sonic otherwise.
|
|
14
15
|
*
|
|
15
16
|
* At `speed == 1.0` every mode yields `(1.0, 1.0)`, so the built-in
|
|
16
17
|
* stretch is inactive — the auto-bypass at unity rate.
|
|
@@ -9,40 +9,29 @@ import androidx.media3.exoplayer.audio.TeeAudioProcessor
|
|
|
9
9
|
/**
|
|
10
10
|
* Strategy abstraction over the audio output pipeline.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Three concrete implementations:
|
|
13
13
|
* - [GaplessEngine]: single ExoPlayer, native gapless via item chaining.
|
|
14
14
|
* - [CrossfadeEngine]: dual ExoPlayer with equal-power gain ramping at
|
|
15
15
|
* track boundaries.
|
|
16
|
+
* - [AirPlayEngine]: single ExoPlayer bound to an AirPlay PCM sink;
|
|
17
|
+
* selected by `applyEngineSwapForMode` while an AirPlay session is
|
|
18
|
+
* active.
|
|
16
19
|
*
|
|
17
20
|
* The engine owns the player instance(s), audio-thread effects, and
|
|
18
21
|
* track-boundary scheduling. The lib's [TrackPlayer] holds the canonical
|
|
19
22
|
* queue model + listener registries + system-side surfaces (NowPlaying,
|
|
20
23
|
* RemoteCommands, AudioSession) and routes transport / queue calls
|
|
21
|
-
* through whichever engine is active.
|
|
22
|
-
* is
|
|
24
|
+
* through whichever engine is active. An engine swap (gapless ↔ crossfade)
|
|
25
|
+
* is `TrackPlayer.applyEngineSwapForMode`: [PlaybackService.swapEngine]
|
|
26
|
+
* installs the new engine and releases the old one, then `TrackPlayer`
|
|
27
|
+
* seeds the new engine through [setMediaItems].
|
|
23
28
|
*
|
|
24
29
|
* Threading: all methods are called on the service main looper unless
|
|
25
|
-
* noted
|
|
26
|
-
*
|
|
27
|
-
* lookahead-cache prefetch).
|
|
30
|
+
* noted; the impl is responsible for any internal off-loop work (e.g.
|
|
31
|
+
* heavy lookahead-cache prefetch).
|
|
28
32
|
*/
|
|
29
33
|
interface PlaybackEngine {
|
|
30
34
|
|
|
31
|
-
/**
|
|
32
|
-
* Build the underlying player(s), apply [mediaItems] starting at
|
|
33
|
-
* [startIndex] / [startPositionMs], and wait for the underlying
|
|
34
|
-
* player(s) to reach a playable state.
|
|
35
|
-
*
|
|
36
|
-
* Empty [mediaItems] is allowed; the engine prepares an empty
|
|
37
|
-
* timeline and [currentMediaItemIndex] returns `-1` until items
|
|
38
|
-
* arrive via [setMediaItems] / [addMediaItems].
|
|
39
|
-
*/
|
|
40
|
-
suspend fun prepare(
|
|
41
|
-
mediaItems: List<MediaItem>,
|
|
42
|
-
startIndex: Int,
|
|
43
|
-
startPositionMs: Long,
|
|
44
|
-
)
|
|
45
|
-
|
|
46
35
|
/**
|
|
47
36
|
* Release the underlying player(s) and stop all background work.
|
|
48
37
|
* After [release], no further methods are valid.
|
|
@@ -127,20 +116,16 @@ interface PlaybackEngine {
|
|
|
127
116
|
val allMediaItems: List<MediaItem>
|
|
128
117
|
|
|
129
118
|
/**
|
|
130
|
-
* Underlying ExoPlayer
|
|
131
|
-
*
|
|
132
|
-
*
|
|
119
|
+
* Underlying ExoPlayer for the audio-focus listener and the
|
|
120
|
+
* `MediaSession` binding. `GaplessEngine` returns its single
|
|
121
|
+
* player, which [PlaybackService] binds directly; `CrossfadeEngine`
|
|
133
122
|
* returns the leading leg's player.
|
|
134
123
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* `CrossfadeForwardingPlayer` wrapper exists to bridge that
|
|
141
|
-
* gap (re-target the bound delegate via `setPlayer` on every
|
|
142
|
-
* role swap); wiring lands when [PlaybackService] starts
|
|
143
|
-
* binding through the wrapper for crossfade engines.
|
|
124
|
+
* A mid-fade role swap inside `CrossfadeEngine` flips which player this
|
|
125
|
+
* getter returns, and nothing re-reads it: [PlaybackService] binds a
|
|
126
|
+
* crossfade engine through a [CrossfadeForwardingPlayer] instead, which
|
|
127
|
+
* re-targets via `setPlayer` on every role swap, fade start and fade
|
|
128
|
+
* cancel, so the session and the focus listener follow the audible leg.
|
|
144
129
|
*/
|
|
145
130
|
val mediaSessionPlayer: ExoPlayer
|
|
146
131
|
|
|
@@ -176,12 +161,36 @@ interface PlaybackEngine {
|
|
|
176
161
|
fun detachAudioBufferSink(sink: TeeAudioProcessor.AudioBufferSink)
|
|
177
162
|
|
|
178
163
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
164
|
+
* Settle any in-flight or armed crossfade so the queue can be mutated by
|
|
165
|
+
* something other than this engine.
|
|
166
|
+
*
|
|
167
|
+
* An armed standby leg holds a snapshot of the queue taken when it was
|
|
168
|
+
* armed. A caller that appends or replaces items through a path the engine
|
|
169
|
+
* does not see — Media3 setting them on the session player after resolving
|
|
170
|
+
* an assistant request — leaves that snapshot stale, and promoting it drops
|
|
171
|
+
* whatever the caller added. Default is a no-op: an engine with one player
|
|
172
|
+
* has nothing to settle.
|
|
173
|
+
*/
|
|
174
|
+
fun settlePendingFade() {}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Settle an in-flight fade by promoting the incoming leg, so the leg a
|
|
178
|
+
* caller reads is the track actually being heard.
|
|
179
|
+
*
|
|
180
|
+
* Mid-fade the leading leg holds the outgoing track while the canonical
|
|
181
|
+
* index names the incoming one, so a caller that reads the player and the
|
|
182
|
+
* index together sees two different tracks. Promoting resolves them to the
|
|
183
|
+
* incoming track, which is the one the listener is arriving at. Default is a
|
|
184
|
+
* no-op: an engine with one player is never in this state.
|
|
185
|
+
*/
|
|
186
|
+
fun settlePendingFadeByPromoting() {}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Snapshot the active item's cached [ReplayGainData], or `null` when no
|
|
190
|
+
* usable RG tags have been parsed yet. Surfaced by
|
|
191
|
+
* [NowPlayingFormatExtractor] to populate the four `replayGain*Db` /
|
|
192
|
+
* `replayGain*Peak` fields. The default is `null`, for an engine that
|
|
193
|
+
* reads no tags.
|
|
185
194
|
*/
|
|
186
195
|
fun currentReplayGainData(): ReplayGainData? = null
|
|
187
196
|
|
|
@@ -194,43 +203,22 @@ interface PlaybackEngine {
|
|
|
194
203
|
*/
|
|
195
204
|
fun currentReplayGainActive(): Boolean = false
|
|
196
205
|
|
|
197
|
-
/**
|
|
198
|
-
* Hand playback off to [nextEngine] when the user changes
|
|
199
|
-
* `PlaybackMode`.
|
|
200
|
-
*
|
|
201
|
-
* Contract:
|
|
202
|
-
* - The receiver inherits the donor's queue, current index, and
|
|
203
|
-
* current position; the caller does NOT pre-call [prepare] on
|
|
204
|
-
* [nextEngine].
|
|
205
|
-
* - When [atTrackBoundary] is `true`, the donor plays the active
|
|
206
|
-
* track to its natural end and the receiver starts playing the
|
|
207
|
-
* NEXT queue position fresh.
|
|
208
|
-
* - When [atTrackBoundary] is `false` (idle player or empty
|
|
209
|
-
* queue), the swap is immediate; receiver picks up at the
|
|
210
|
-
* inherited position.
|
|
211
|
-
* - The donor's [release] is the caller's responsibility AFTER
|
|
212
|
-
* the boundary fires; the engine itself does not self-release.
|
|
213
|
-
* - The donor's listeners on [delegate] continue firing until
|
|
214
|
-
* [release] runs; the caller swaps the delegate before that.
|
|
215
|
-
*/
|
|
216
|
-
suspend fun handoff(to: PlaybackEngine, atTrackBoundary: Boolean)
|
|
217
|
-
|
|
218
206
|
/**
|
|
219
207
|
* Single delegate receiving state / track / progress / error /
|
|
220
208
|
* queue-end events. TrackPlayer is the canonical delegate; engines
|
|
221
209
|
* fire on the service main looper.
|
|
222
210
|
*
|
|
223
|
-
* Strong reference by design; the iOS counterpart
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
211
|
+
* Strong reference by design; the iOS counterpart is `weak` because
|
|
212
|
+
* `TrackPlayer` owns its engine strongly and Swift's reference graph
|
|
213
|
+
* would otherwise close (TrackPlayer → engine → delegate → TrackPlayer).
|
|
214
|
+
* On Android the engine lives inside [PlaybackService], reached through
|
|
215
|
+
* [PlaybackService.LocalBinder.engine] and owned by the service
|
|
216
|
+
* lifecycle, not by TrackPlayer; the cycle is broken explicitly by
|
|
217
|
+
* [release] dropping `delegate` and by `TrackPlayer.tearDownEventObservers`
|
|
218
|
+
* clearing itself from the engine's `delegate`.
|
|
231
219
|
*
|
|
232
220
|
* Threading: setter MUST be invoked on the service main looper to
|
|
233
|
-
* avoid a write-vs-fire race during
|
|
221
|
+
* avoid a write-vs-fire race during an engine swap.
|
|
234
222
|
*/
|
|
235
223
|
var delegate: PlaybackEngineDelegate?
|
|
236
224
|
}
|
|
@@ -290,10 +278,14 @@ interface PlaybackEngineDelegate {
|
|
|
290
278
|
fun onActiveItemFormatChanged(engine: PlaybackEngine)
|
|
291
279
|
|
|
292
280
|
/**
|
|
293
|
-
* Fires on Media3 `Player.Listener.onPlayerError`. The delegate
|
|
294
|
-
*
|
|
281
|
+
* Fires on Media3 `Player.Listener.onPlayerError`. The delegate maps
|
|
282
|
+
* [exception] via the lib's `PlaybackErrorMapping`. [failedIndex] is the
|
|
283
|
+
* queue position of the item that failed, or -1 when the engine cannot
|
|
284
|
+
* attribute it. A crossfade engine can fail on its standby leg while the
|
|
285
|
+
* leading leg plays on, so the failure need not belong to the track the
|
|
286
|
+
* delegate considers current.
|
|
295
287
|
*/
|
|
296
|
-
fun onError(engine: PlaybackEngine, exception: PlaybackException)
|
|
288
|
+
fun onError(engine: PlaybackEngine, exception: PlaybackException, failedIndex: Int = -1)
|
|
297
289
|
|
|
298
290
|
/**
|
|
299
291
|
* Fires on Media3 `Player.STATE_ENDED`. The delegate consults the
|
|
@@ -316,9 +308,10 @@ interface PlaybackEngineDelegate {
|
|
|
316
308
|
* [ExoPlayer] has started playback at volume 0 and the leading
|
|
317
309
|
* leg's fade-out has begun. The delegate treats this as the
|
|
318
310
|
* canonical "active track changed" trigger and emits
|
|
319
|
-
* `onTrackChange`
|
|
320
|
-
*
|
|
321
|
-
*
|
|
311
|
+
* `onTrackChange` for the incoming track; the `MediaSession` re-bind
|
|
312
|
+
* to the standby leg is [CrossfadeForwardingPlayer]'s, through the
|
|
313
|
+
* engine's `onCrossfadeBegin` lambda. The subsequent `onTrackTransition`
|
|
314
|
+
* for the same incoming index is a structural Media3 echo and must be
|
|
322
315
|
* deduplicated.
|
|
323
316
|
*
|
|
324
317
|
* Crossfade-only — [GaplessEngine] never invokes this.
|
|
@@ -333,7 +326,9 @@ interface PlaybackEngineDelegate {
|
|
|
333
326
|
* mutation that invalidates the standby leg). The delegate
|
|
334
327
|
* reverts JS-facing state to the leading leg — emit
|
|
335
328
|
* `onTrackChange(reason: CROSSFADE_CANCELLED)` for the leading
|
|
336
|
-
* track
|
|
329
|
+
* track; the `MediaSession` re-bind back to the leading leg is
|
|
330
|
+
* [CrossfadeForwardingPlayer]'s, through the engine's
|
|
331
|
+
* `onCrossfadeCancel` lambda. Pairs with
|
|
337
332
|
* [onCrossfadeBegin]: only fires when a `Begin` was fired
|
|
338
333
|
* earlier and the fade did not complete.
|
|
339
334
|
*
|
|
@@ -43,9 +43,17 @@ internal object PlaybackErrorMapping {
|
|
|
43
43
|
PlaybackErrorCode.AUTH_ERROR
|
|
44
44
|
PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED ->
|
|
45
45
|
PlaybackErrorCode.CLEARTEXT_NOT_PERMITTED
|
|
46
|
-
PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND
|
|
47
|
-
PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE ->
|
|
46
|
+
PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND ->
|
|
48
47
|
PlaybackErrorCode.SOURCE_UNREACHABLE
|
|
48
|
+
PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE ->
|
|
49
|
+
// The source answered, and what it sent is not media — a server
|
|
50
|
+
// reporting an API-level failure as a successful response, or a proxy
|
|
51
|
+
// serving an error page. Not SOURCE_UNREACHABLE: it was reached, and
|
|
52
|
+
// calling it unreachable invites a retry that cannot change the answer.
|
|
53
|
+
// Not HTTP_ERROR either: the status was a success. What is wrong is the
|
|
54
|
+
// payload, which is the same thing iOS reports when AVFoundation fails to
|
|
55
|
+
// parse the identical body.
|
|
56
|
+
PlaybackErrorCode.FILE_FORMAT_NOT_RECOGNIZED
|
|
49
57
|
PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED,
|
|
50
58
|
PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED,
|
|
51
59
|
PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED,
|
|
@@ -85,9 +93,9 @@ internal object PlaybackErrorMapping {
|
|
|
85
93
|
|
|
86
94
|
/**
|
|
87
95
|
* Strip query strings from a native error message so logged
|
|
88
|
-
* `nativeMessage` doesn't leak auth tokens
|
|
89
|
-
*
|
|
90
|
-
* `PlaybackException.message
|
|
96
|
+
* `nativeMessage` doesn't leak auth tokens: stream URLs with
|
|
97
|
+
* `?token=…`-style query strings end up inside
|
|
98
|
+
* `PlaybackException.message`.
|
|
91
99
|
*
|
|
92
100
|
* Walks the full message removing EVERY `?…(whitespace|end)` run
|
|
93
101
|
* so multi-URL error messages have all query strings stripped.
|
|
@@ -49,8 +49,8 @@ internal class PlaybackModeStateMachine {
|
|
|
49
49
|
* the new duration.
|
|
50
50
|
*
|
|
51
51
|
* The caller is responsible for managing the brief audio
|
|
52
|
-
* interruption
|
|
53
|
-
*
|
|
52
|
+
* interruption — `applyEngineSwapForMode` carries the position
|
|
53
|
+
* and re-installs the queue.
|
|
54
54
|
*/
|
|
55
55
|
fun setRequested(
|
|
56
56
|
state: State,
|