react-native-queue-player 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QueuePlayer.podspec +14 -9
- package/README.md +3 -3
- package/android/consumer-rules.pro +1 -1
- package/android/src/main/cpp/airplay2_control.c +4 -4
- package/android/src/main/cpp/airplay2_control.h +4 -4
- package/android/src/main/cpp/airplay2_jni.cpp +3 -19
- package/android/src/main/cpp/airplay2_pair.c +7 -7
- package/android/src/main/cpp/airplay2_pair.h +2 -2
- package/android/src/main/cpp/airplay2_rtsp.c +8 -8
- package/android/src/main/cpp/airplay2_session.c +2 -2
- package/android/src/main/cpp/airplay_jni.cpp +8 -69
- package/android/src/main/java/com/margelo/nitro/queueplayer/AirPlayEngine.kt +7 -16
- package/android/src/main/java/com/margelo/nitro/queueplayer/CacheMimeTypes.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CastManager.kt +2 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/CellularTransportMonitor.kt +77 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/CrossfadeEngine.kt +187 -52
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +27 -7
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerEngine.kt +33 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngine.kt +31 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/FFTProcessorTee.kt +45 -30
- package/android/src/main/java/com/margelo/nitro/queueplayer/FifoCacheEvictor.kt +111 -15
- package/android/src/main/java/com/margelo/nitro/queueplayer/GaplessEngine.kt +60 -33
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/IEqualizerEngine.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCache.kt +103 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +77 -29
- package/android/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractor.kt +48 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/PitchCorrection.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackEngine.kt +71 -76
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackErrorMapping.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackModeStateMachine.kt +2 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +68 -46
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +6 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +11 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessor.kt +26 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainData.kt +22 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainExtractor.kt +38 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/ReplayGainGain.kt +17 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/SuppliedReplayGain.kt +66 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +839 -271
- package/android/src/main/java/com/margelo/nitro/queueplayer/Visualizer.kt +4 -8
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastBackend.kt +4 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/CastSession.kt +5 -4
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2JNI.kt +0 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlay2Session.kt +4 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayBackend.kt +16 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayMetadataSync.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirPlayRenderersFactory.kt +6 -14
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/AirplayJNI.kt +1 -13
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/airplay/MulticastLockHolder.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/chromecast/ChromecastBackend.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServer.kt +11 -35
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaServerHandle.kt +0 -5
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/MediaTokenRegistry.kt +0 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitor.kt +32 -33
- package/android/src/test/java/androidx/media3/session/{MediaSessionControllerRequestTestSeam.kt → MediaSessionControllerRequestTestSupport.kt} +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/AvrcpMetadataTest.kt +12 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineFocusLossTest.kt +115 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EngineEndSignalOrderTest.kt +70 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +0 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +17 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +15 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/FifoCacheEvictorTest.kt +135 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +1 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +36 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheTest.kt +24 -18
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterCancelTest.kt +85 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/LookaheadCacheWriterTest.kt +112 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +45 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackSearchTest.kt +47 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +53 -29
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceLifecycleTest.kt +8 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/QueueSkipArithmeticTest.kt +33 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainAudioProcessorTest.kt +128 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainExtractorTest.kt +209 -14
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainGainTest.kt +150 -10
- package/android/src/test/java/com/margelo/nitro/queueplayer/ReplayGainMergeTest.kt +258 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/RobolectricServiceBindHelper.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/SessionCommandForwardingPlayerTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowDynamicsProcessingRejectingEnable.kt +19 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizer.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/ShadowEqualizerRejectingBandWrites.kt +27 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/SuppliedReplayGainTest.kt +61 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +4 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastCommandRoutingTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerCastStateTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +88 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +205 -27
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +277 -38
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +5 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +85 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +11 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +2 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +90 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +8 -26
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +0 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/FakeRemotePlayer.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/LocalAddressMonitorTest.kt +173 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/cast/net/ShadowConnectivityManagerRecordingRequests.kt +27 -0
- package/app.plugin.js +17 -1
- package/ios/AVPlayerItemQueueItemId.swift +26 -12
- package/ios/AVQueueBuilder.swift +120 -85
- package/ios/ArtworkLoader.swift +0 -7
- package/ios/ArtworkResolver.swift +22 -4
- package/ios/AssetReplayGainReader.swift +118 -0
- package/ios/AudioSession.swift +67 -16
- package/ios/AudioTapProvider.swift +284 -149
- package/ios/CarPlayBridge.swift +0 -12
- package/ios/CarPlayCoordinator.swift +18 -2
- package/ios/CarPlaySceneDelegate.swift +2 -2
- package/ios/Cast/AirPlayRouteState.swift +9 -8
- package/ios/Cast/Core/CastEventBridge.swift +66 -26
- package/ios/Cast/Core/CastNowPlayingController.swift +38 -16
- package/ios/Cast/Core/CastSession.swift +2 -2
- package/ios/Cast/Core/CastTransportRouter.swift +21 -9
- package/ios/Cast/Core/LocalMediaServer.swift +41 -52
- package/ios/Cast/Core/LocalNetworkPermissionProbe.swift +32 -11
- package/ios/Cast/Core/MediaServerHandle.swift +0 -4
- package/ios/Cast/Core/MediaTokenRegistry.swift +0 -6
- package/ios/CastManager.swift +1 -1
- package/ios/CrossfadeEngine.swift +660 -226
- package/ios/EQTap.swift +20 -17
- package/ios/Equalizer.swift +1 -1
- package/ios/FLACStreamInfo.swift +65 -0
- package/ios/GaplessEngine.swift +57 -30
- package/ios/InputGuards.swift +35 -5
- package/ios/LookaheadCache.swift +202 -58
- package/ios/LookaheadCachePrefetcher.swift +48 -36
- package/ios/MediaServer/MediaHTTPConnection.swift +691 -0
- package/ios/NetworkRecoveryPolicy.swift +32 -0
- package/ios/NowPlayingFormatExtractor.swift +25 -18
- package/ios/NowPlayingInfo.swift +84 -48
- package/ios/OutputRouteMonitor.swift +16 -3
- package/ios/PendingBrowseRequests.swift +1 -1
- package/ios/PlaceholderArtwork.swift +9 -5
- package/ios/PlaybackEngine.swift +165 -76
- package/ios/PlaybackErrorMapping.swift +3 -3
- package/ios/PlaybackModeStateMachine.swift +2 -2
- package/ios/PlaybackNetworkMonitor.swift +73 -0
- package/ios/PlayerStateDerivation.swift +6 -6
- package/ios/QueueMutationArithmetic.swift +25 -0
- package/ios/QueueSkipArithmetic.swift +5 -3
- package/ios/QueueWindowArithmetic.swift +152 -0
- package/ios/ReadThroughServer.swift +283 -0
- package/ios/RemoteCommands.swift +44 -4
- package/ios/ReplayGainData.swift +22 -3
- package/ios/ReplayGainExtractor.swift +70 -27
- package/ios/Siri/VoiceDonation.swift +14 -9
- package/ios/StreamingBitrateProbe.swift +22 -68
- package/ios/SuppliedReplayGain.swift +83 -0
- package/ios/Tests/AVQueueBuilderTests.swift +158 -221
- package/ios/Tests/ActiveItemEngineStub.swift +44 -0
- package/ios/Tests/AssetReplayGainReaderTests.swift +228 -0
- package/ios/Tests/AudioTapProviderDispatchTargetTests.swift +158 -0
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +122 -10
- package/ios/Tests/BitrateReresolveBudgetTests.swift +71 -0
- package/ios/Tests/CastMediaItemTranslationTests.swift +72 -0
- package/ios/Tests/CastNowPlayingControllerTests.swift +1 -1
- package/ios/Tests/CrossfadeAdvanceWithoutFadeTests.swift +120 -0
- package/ios/Tests/CrossfadeEngineIncomingMixTests.swift +122 -0
- package/ios/Tests/CrossfadeEngineTests.swift +164 -0
- package/ios/Tests/CrossfadeMixReapplyTests.swift +66 -0
- package/ios/Tests/CrossfadePlaybackIntentTests.swift +81 -0
- package/ios/Tests/EngineSwapTests.swift +91 -0
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +46 -0
- package/ios/Tests/EqualizerHybridTests.swift +5 -2
- package/ios/Tests/FLACStreamInfoTests.swift +88 -0
- package/ios/Tests/GaplessEngineLifecycleTests.swift +4 -4
- package/ios/Tests/InputGuardsTests.swift +41 -0
- package/ios/Tests/InterruptionIntentTests.swift +77 -0
- package/ios/Tests/LookaheadCacheCellularAccessTests.swift +185 -0
- package/ios/Tests/LookaheadCachePrefetcherTests.swift +53 -30
- package/ios/Tests/LookaheadCacheRuntimeConfigTests.swift +160 -1
- package/ios/Tests/LookaheadCacheTests.swift +44 -18
- package/ios/Tests/MediaHTTPConnectionTests.swift +79 -0
- package/ios/Tests/MutationDeferralTests.swift +220 -0
- package/ios/Tests/NowPlayingInfoTests.swift +86 -99
- package/ios/Tests/NowPlayingSnapshotTests.swift +127 -0
- package/ios/Tests/OriginRestartStitcherTests.swift +60 -0
- package/ios/Tests/PlaybackNetworkMonitorTests.swift +95 -0
- package/ios/Tests/PlayerFixtures.swift +48 -0
- package/ios/Tests/PlayerStateDerivationTests.swift +10 -10
- package/ios/Tests/QueueMutationGenerationTests.swift +141 -0
- package/ios/Tests/QueueRebuildPrefixTests.swift +317 -0
- package/ios/Tests/QueueStateTests.swift +3 -1
- package/ios/Tests/QueueWindowArithmeticTests.swift +56 -0
- package/ios/Tests/QueueWindowSliceTests.swift +162 -0
- package/ios/Tests/ReadThroughRoutingLifecycleTests.swift +63 -0
- package/ios/Tests/ReadThroughServerTests.swift +345 -0
- package/ios/Tests/RemoteCommandsTests.swift +28 -0
- package/ios/Tests/ReplayGainExtractorTests.swift +216 -6
- package/ios/Tests/ReplayGainMergeTests.swift +230 -0
- package/ios/Tests/RetryRecoveryTests.swift +303 -0
- package/ios/Tests/SkipCapabilityTests.swift +233 -8
- package/ios/Tests/SkipIndexTests.swift +32 -2
- package/ios/Tests/SleepTimerPauseIntentTests.swift +43 -0
- package/ios/Tests/StallRecoveryTests.swift +97 -0
- package/ios/Tests/StreamingBitrateProbeTests.swift +17 -20
- package/ios/Tests/TopUpWindowGateTests.swift +394 -0
- package/ios/Tests/TrackPlayer+TestHops.swift +14 -0
- package/ios/Tests/TrackPlayerCallOrderTests.swift +93 -0
- package/ios/Tests/TrackPlayerConfigureTeardownTests.swift +86 -0
- package/ios/Tests/TrackPlayerEndVerdictTests.swift +165 -0
- package/ios/Tests/TrackPlayerSeekTests.swift +137 -0
- package/ios/Tests/TrackPlayerThreadingTests.swift +127 -0
- package/ios/Tests/TrackSourceClassifierTests.swift +33 -7
- package/ios/Tests/VoiceVocabularyOptInTests.swift +38 -0
- package/ios/TrackPlayer+Automotive.swift +101 -0
- package/ios/TrackPlayer+Cache.swift +232 -0
- package/ios/TrackPlayer+Config.swift +291 -0
- package/ios/TrackPlayer+EngineDelegate.swift +209 -0
- package/ios/TrackPlayer+EventsAPI.swift +111 -0
- package/ios/TrackPlayer+EventsDispatch.swift +968 -0
- package/ios/TrackPlayer+EventsWiring.swift +173 -0
- package/ios/TrackPlayer+Lifecycle.swift +930 -0
- package/ios/TrackPlayer+NowPlayingFormat.swift +258 -0
- package/ios/TrackPlayer+Queue.swift +940 -0
- package/ios/TrackPlayer+Recovery.swift +196 -0
- package/ios/TrackPlayer+Skip.swift +455 -0
- package/ios/TrackPlayer+SleepTimer.swift +175 -0
- package/ios/TrackPlayer+State.swift +108 -0
- package/ios/TrackPlayer+Threading.swift +135 -0
- package/ios/TrackPlayer+Transport.swift +267 -0
- package/ios/TrackPlayer+Window.swift +195 -0
- package/ios/TrackPlayer.swift +305 -4619
- package/ios/Visualizer.swift +6 -5
- package/ios/tests-harness/Podfile +1 -1
- package/ios/tests-harness/TestHost.xcodeproj/project.pbxproj +19 -11
- package/ios/tests-harness/scripts/seed-xcodeproj.rb +2 -2
- package/lib/module/hooks/useActiveTrack.js +29 -22
- package/lib/module/hooks/useActiveTrack.js.map +1 -1
- package/lib/module/hooks/useCast.js +8 -26
- package/lib/module/hooks/useCast.js.map +1 -1
- package/lib/module/hooks/useEqualizer.js +19 -12
- package/lib/module/hooks/useEqualizer.js.map +1 -1
- package/lib/module/hooks/useLookaheadCache.js +3 -7
- package/lib/module/hooks/useLookaheadCache.js.map +1 -1
- package/lib/module/hooks/useQueue.js +66 -19
- package/lib/module/hooks/useQueue.js.map +1 -1
- package/lib/module/index.js +6 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/queueDelta.js +41 -0
- package/lib/module/queueDelta.js.map +1 -0
- package/lib/module/types.js +28 -5
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +53 -28
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/useActiveTrack.d.ts +5 -7
- package/lib/typescript/hooks/useActiveTrack.d.ts.map +1 -1
- package/lib/typescript/hooks/useCast.d.ts +6 -1
- package/lib/typescript/hooks/useCast.d.ts.map +1 -1
- package/lib/typescript/hooks/useEqualizer.d.ts +2 -1
- package/lib/typescript/hooks/useEqualizer.d.ts.map +1 -1
- package/lib/typescript/hooks/useLookaheadCache.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +4 -4
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/queueDelta.d.ts +10 -0
- package/lib/typescript/queueDelta.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +168 -12
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_QueueChangeDelta_double_QueueChangeReason.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +45 -19
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JLookaheadCacheConfig.hpp +8 -4
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeDelta.hpp +128 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +19 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_BrowseItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______MediaSearchRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_TrackItem______std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_AudioRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_BufferState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CacheStatus.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastDiscoveryState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastLocalNetworkPermissionEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastRoute.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_CastSessionDiedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlaybackError.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerProgress.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_PlayerState_StateChangeReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/{Func_void_std__vector_TrackItem__double_QueueChangeReason.kt → Func_void_QueueChangeDelta_double_QueueChangeReason.kt} +14 -16
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_ServiceReadyReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SkipCapability.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_SleepTimerState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerErrorReason.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_VisualizerFrame.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_double_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_TrackItem__double_TrackChangeReason_std__optional_double_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__optional_std__variant_nitro__NullType__NowPlayingFormat__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_CastReceiver_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_EqualizerBand_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridCastManagerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridEqualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +6 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridVisualizerSpec.kt +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/LookaheadCacheConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeDelta.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/TrackItem.kt +24 -4
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -2
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +16 -16
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +80 -65
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +6 -3
- package/nitrogen/generated/ios/swift/Func_void_CacheStatus.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_QueueChangeDelta_double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +3 -3
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +32 -24
- package/nitrogen/generated/ios/swift/LookaheadCacheConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeDelta.swift +82 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +73 -1
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +6 -3
- package/nitrogen/generated/shared/c++/LookaheadCacheConfig.hpp +7 -3
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeDelta.hpp +113 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +18 -2
- package/package.json +8 -8
- package/src/TrackPlayer.nitro.ts +53 -27
- package/src/hooks/useActiveTrack.ts +29 -22
- package/src/hooks/useCast.ts +14 -15
- package/src/hooks/useEqualizer.ts +19 -12
- package/src/hooks/useLookaheadCache.ts +3 -7
- package/src/hooks/useQueue.ts +66 -17
- package/src/index.ts +13 -3
- package/src/queueDelta.ts +41 -0
- package/src/types.ts +169 -12
- package/android/src/main/java/com/margelo/nitro/queueplayer/PendingIntentBuffer.kt +0 -82
- package/android/src/main/java/com/margelo/nitro/queueplayer/cast/http/LocalMediaServerLifecycle.kt +0 -15
- package/android/src/test/java/com/margelo/nitro/queueplayer/PendingIntentBufferTest.kt +0 -192
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackEngineInterfaceTest.kt +0 -135
- package/android/src/test/java/com/margelo/nitro/queueplayer/SmokeTest.kt +0 -27
- package/ios/Cast/Core/LocalAddressMonitor.swift +0 -110
- package/ios/Cast/Core/MediaHTTPConnection.swift +0 -349
- package/ios/MetadataReader.swift +0 -555
- package/ios/Tests/CrossfadeEngineStubTests.swift +0 -43
- package/ios/Tests/MetadataReaderTests.swift +0 -603
- package/ios/Tests/PlaybackEngineProtocolTests.swift +0 -92
- package/ios/Tests/VoiceDonationTests.swift +0 -26
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +0 -101
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +0 -46
- /package/ios/{Cast/Core → MediaServer}/MimeTypes.swift +0 -0
package/ios/MetadataReader.swift
DELETED
|
@@ -1,555 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
/// Direct-bytes audio metadata reader for ReplayGain tags. Sniffs
|
|
4
|
-
/// the container by leading magic bytes and dispatches to a format-
|
|
5
|
-
/// specific parser. Reads the first 256 KB of the source — large
|
|
6
|
-
/// enough to contain ID3v2 padding, FLAC metadata blocks, and the
|
|
7
|
-
/// `moov` atom on most MP4 files. ReplayGain tags live near the
|
|
8
|
-
/// start of every supported container so a bounded prefix read is
|
|
9
|
-
/// always sufficient.
|
|
10
|
-
///
|
|
11
|
-
/// Why not `AVAsset.load(.metadata)`: every AVFoundation metadata
|
|
12
|
-
/// load serialises through the demux pipeline; with many concurrent
|
|
13
|
-
/// asset preparations, total wait runs to tens of seconds even for
|
|
14
|
-
/// trivially-tagged local files. This module reads the container
|
|
15
|
-
/// header directly without involving AVFoundation at all — sub-
|
|
16
|
-
/// 100 ms per local file regardless of queue size.
|
|
17
|
-
///
|
|
18
|
-
/// Source-agnostic: `file://` reads via `FileHandle`, `http(s)://`
|
|
19
|
-
/// reads via `URLSession` with a `Range: bytes=0-N` request. Same
|
|
20
|
-
/// parsers handle both — only the byte-source shim differs.
|
|
21
|
-
enum MetadataReader {
|
|
22
|
-
|
|
23
|
-
/// Default prefix read — large enough for ID3v2 padding, FLAC
|
|
24
|
-
/// metadata blocks, and the `moov` atom on standard MP4 files.
|
|
25
|
-
static let defaultPrefixBytes = 256 * 1024
|
|
26
|
-
|
|
27
|
-
/// Read the four `REPLAYGAIN_*` tags from a media URL. Returns
|
|
28
|
-
/// `nil` when the container is Opus (decoder-applied gain),
|
|
29
|
-
/// when none of the tags are present, or when the source can't
|
|
30
|
-
/// be read.
|
|
31
|
-
static func readReplayGainTags(
|
|
32
|
-
url: URL, urlSession: URLSession = .shared
|
|
33
|
-
) async -> ReplayGainData? {
|
|
34
|
-
if isOpusContainer(url: url) { return nil }
|
|
35
|
-
guard let data = await fetchPrefixBytes(url: url, urlSession: urlSession) else { return nil }
|
|
36
|
-
return rgDataFromTags(parseTags(from: data))
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// MARK: - Format dispatch
|
|
40
|
-
|
|
41
|
-
/// Sniff first bytes and route to the format-specific parser.
|
|
42
|
-
/// Returns a normalised `[upperKey: rawValue]` dict containing
|
|
43
|
-
/// every name/value pair the parser found (not just RG ones —
|
|
44
|
-
/// keeping the full set keeps the parser side reusable for
|
|
45
|
-
/// other tag categories later).
|
|
46
|
-
static func parseTags(from data: Data) -> [String: String] {
|
|
47
|
-
guard data.count >= 4 else { return [:] }
|
|
48
|
-
let b0 = data[0], b1 = data[1], b2 = data[2], b3 = data[3]
|
|
49
|
-
if b0 == 0x49 && b1 == 0x44 && b2 == 0x33 {
|
|
50
|
-
return parseID3v2(data)
|
|
51
|
-
}
|
|
52
|
-
if b0 == 0x66 && b1 == 0x4c && b2 == 0x61 && b3 == 0x43 {
|
|
53
|
-
return parseFLAC(data)
|
|
54
|
-
}
|
|
55
|
-
if data.count >= 8,
|
|
56
|
-
data[4] == 0x66 && data[5] == 0x74 && data[6] == 0x79 && data[7] == 0x70 {
|
|
57
|
-
return parseMP4(data)
|
|
58
|
-
}
|
|
59
|
-
return [:]
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
static func rgDataFromTags(_ tags: [String: String]) -> ReplayGainData? {
|
|
63
|
-
let upper = tags.reduce(into: [String: String]()) { d, kv in
|
|
64
|
-
d[kv.key.uppercased()] = kv.value
|
|
65
|
-
}
|
|
66
|
-
let trackGain = upper[ReplayGainExtractor.trackGainKey]
|
|
67
|
-
.flatMap { ReplayGainExtractor.parseGainDb($0) }
|
|
68
|
-
let trackPeak = upper[ReplayGainExtractor.trackPeakKey]
|
|
69
|
-
.flatMap { ReplayGainExtractor.parsePeak($0) }
|
|
70
|
-
let albumGain = upper[ReplayGainExtractor.albumGainKey]
|
|
71
|
-
.flatMap { ReplayGainExtractor.parseGainDb($0) }
|
|
72
|
-
let albumPeak = upper[ReplayGainExtractor.albumPeakKey]
|
|
73
|
-
.flatMap { ReplayGainExtractor.parsePeak($0) }
|
|
74
|
-
if trackGain == nil && trackPeak == nil
|
|
75
|
-
&& albumGain == nil && albumPeak == nil {
|
|
76
|
-
return nil
|
|
77
|
-
}
|
|
78
|
-
return ReplayGainData(
|
|
79
|
-
trackGainDb: trackGain, trackPeak: trackPeak,
|
|
80
|
-
albumGainDb: albumGain, albumPeak: albumPeak
|
|
81
|
-
)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
static func isOpusContainer(url: URL) -> Bool {
|
|
85
|
-
let ext = url.pathExtension.lowercased()
|
|
86
|
-
return ext == "opus" || ext == "ogg"
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// MARK: - Byte fetch shim
|
|
90
|
-
|
|
91
|
-
/// File: `FileHandle` prefix read. HTTP/S: `URLSession` Range
|
|
92
|
-
/// request. Returns `nil` on read error or unsupported scheme.
|
|
93
|
-
static func fetchPrefixBytes(
|
|
94
|
-
url: URL, byteCount: Int = defaultPrefixBytes, urlSession: URLSession = .shared
|
|
95
|
-
) async -> Data? {
|
|
96
|
-
if url.isFileURL {
|
|
97
|
-
do {
|
|
98
|
-
let handle = try FileHandle(forReadingFrom: url)
|
|
99
|
-
defer { try? handle.close() }
|
|
100
|
-
return handle.readData(ofLength: byteCount)
|
|
101
|
-
} catch {
|
|
102
|
-
return nil
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
guard let scheme = url.scheme?.lowercased(),
|
|
106
|
-
scheme == "http" || scheme == "https" else {
|
|
107
|
-
return nil
|
|
108
|
-
}
|
|
109
|
-
var req = URLRequest(url: url, timeoutInterval: 10)
|
|
110
|
-
req.setValue("bytes=0-\(byteCount - 1)", forHTTPHeaderField: "Range")
|
|
111
|
-
do {
|
|
112
|
-
let (data, response) = try await urlSession.data(for: req)
|
|
113
|
-
if let http = response as? HTTPURLResponse,
|
|
114
|
-
!(200...299).contains(http.statusCode) {
|
|
115
|
-
return nil
|
|
116
|
-
}
|
|
117
|
-
return data
|
|
118
|
-
} catch {
|
|
119
|
-
return nil
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// MARK: - ID3v2 (MP3)
|
|
124
|
-
|
|
125
|
-
/// Parse ID3v2.2 / 2.3 / 2.4 TXXX (user-defined text) frames into
|
|
126
|
-
/// a `[description: value]` dict. Only TXXX is walked because RG
|
|
127
|
-
/// tags use that frame; standard text frames (TIT2 etc.) carry
|
|
128
|
-
/// title/artist/album which the player resolves via Now Playing
|
|
129
|
-
/// metadata, not this path.
|
|
130
|
-
///
|
|
131
|
-
/// Tag-size, frame-size, and frame-id parsing follow the official
|
|
132
|
-
/// ID3v2 spec; v2.4 frames use syncsafe sizes, v2.2/2.3 use plain
|
|
133
|
-
/// big-endian. Unsynchronisation transformations (rare in modern
|
|
134
|
-
/// taggers) are not undone — empirical: foobar2000 / mp3tag emit
|
|
135
|
-
/// non-unsynchronised tags.
|
|
136
|
-
static func parseID3v2(_ data: Data) -> [String: String] {
|
|
137
|
-
guard data.count >= 10,
|
|
138
|
-
data[0] == 0x49, data[1] == 0x44, data[2] == 0x33 else {
|
|
139
|
-
return [:]
|
|
140
|
-
}
|
|
141
|
-
let majorVersion = data[3]
|
|
142
|
-
let flags = data[5]
|
|
143
|
-
let tagSize = Int(data[6] & 0x7f) << 21 |
|
|
144
|
-
Int(data[7] & 0x7f) << 14 |
|
|
145
|
-
Int(data[8] & 0x7f) << 7 |
|
|
146
|
-
Int(data[9] & 0x7f)
|
|
147
|
-
let tagEnd = min(10 + tagSize, data.count)
|
|
148
|
-
var pos = 10
|
|
149
|
-
|
|
150
|
-
// Optional extended header (flag bit 0x40). v2.3 size is 4-byte
|
|
151
|
-
// BE excluding the 4 size bytes themselves; v2.4 is syncsafe and
|
|
152
|
-
// includes itself. Consumers + RG parsers don't care — skip past.
|
|
153
|
-
if flags & 0x40 != 0, pos + 4 <= tagEnd {
|
|
154
|
-
let extSize: Int
|
|
155
|
-
if majorVersion >= 4 {
|
|
156
|
-
extSize = Int(data[pos] & 0x7f) << 21 |
|
|
157
|
-
Int(data[pos+1] & 0x7f) << 14 |
|
|
158
|
-
Int(data[pos+2] & 0x7f) << 7 |
|
|
159
|
-
Int(data[pos+3] & 0x7f)
|
|
160
|
-
} else {
|
|
161
|
-
extSize = Int(data[pos]) << 24 | Int(data[pos+1]) << 16 |
|
|
162
|
-
Int(data[pos+2]) << 8 | Int(data[pos+3])
|
|
163
|
-
// v2.3 ext header size excludes itself.
|
|
164
|
-
}
|
|
165
|
-
pos += extSize + (majorVersion >= 4 ? 0 : 4)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// v2.2 used 3-byte frame IDs + 3-byte sizes. v2.3+ uses 4+4.
|
|
169
|
-
let frameIdLen = majorVersion <= 2 ? 3 : 4
|
|
170
|
-
let frameSizeLen = majorVersion <= 2 ? 3 : 4
|
|
171
|
-
let frameHeaderLen = frameIdLen + frameSizeLen + (majorVersion <= 2 ? 0 : 2)
|
|
172
|
-
let txxxId: String = majorVersion <= 2 ? "TXX" : "TXXX"
|
|
173
|
-
|
|
174
|
-
var result: [String: String] = [:]
|
|
175
|
-
while pos + frameHeaderLen <= tagEnd {
|
|
176
|
-
// Padding starts when frame ID is all zero.
|
|
177
|
-
if data[pos] == 0 { break }
|
|
178
|
-
let frameID = String(
|
|
179
|
-
data: data.subdata(in: pos..<(pos + frameIdLen)),
|
|
180
|
-
encoding: .isoLatin1
|
|
181
|
-
) ?? ""
|
|
182
|
-
let sizePos = pos + frameIdLen
|
|
183
|
-
let frameSize: Int
|
|
184
|
-
if majorVersion >= 4 {
|
|
185
|
-
frameSize = Int(data[sizePos] & 0x7f) << 21 |
|
|
186
|
-
Int(data[sizePos+1] & 0x7f) << 14 |
|
|
187
|
-
Int(data[sizePos+2] & 0x7f) << 7 |
|
|
188
|
-
Int(data[sizePos+3] & 0x7f)
|
|
189
|
-
} else if majorVersion == 3 {
|
|
190
|
-
frameSize = Int(data[sizePos]) << 24 | Int(data[sizePos+1]) << 16 |
|
|
191
|
-
Int(data[sizePos+2]) << 8 | Int(data[sizePos+3])
|
|
192
|
-
} else {
|
|
193
|
-
frameSize = Int(data[sizePos]) << 16 | Int(data[sizePos+1]) << 8 |
|
|
194
|
-
Int(data[sizePos+2])
|
|
195
|
-
}
|
|
196
|
-
let payloadStart = pos + frameHeaderLen
|
|
197
|
-
let payloadEnd = payloadStart + frameSize
|
|
198
|
-
if frameSize <= 0 || payloadEnd > tagEnd { break }
|
|
199
|
-
if frameID == txxxId,
|
|
200
|
-
let (desc, value) = parseTXXXFrame(
|
|
201
|
-
data.subdata(in: payloadStart..<payloadEnd)) {
|
|
202
|
-
result[desc] = value
|
|
203
|
-
}
|
|
204
|
-
pos = payloadEnd
|
|
205
|
-
}
|
|
206
|
-
return result
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/// Decode a TXXX frame body into `(description, value)`. Encoding
|
|
210
|
-
/// byte at offset 0; description is null-terminated, value follows.
|
|
211
|
-
static func parseTXXXFrame(_ payload: Data) -> (String, String)? {
|
|
212
|
-
guard payload.count >= 2 else { return nil }
|
|
213
|
-
let encoding = payload[0]
|
|
214
|
-
let body = payload.subdata(in: 1..<payload.count)
|
|
215
|
-
switch encoding {
|
|
216
|
-
case 0:
|
|
217
|
-
return splitTXXXSingleNull(body, encoding: .isoLatin1)
|
|
218
|
-
case 3:
|
|
219
|
-
return splitTXXXSingleNull(body, encoding: .utf8)
|
|
220
|
-
case 1:
|
|
221
|
-
return splitTXXXDoubleNull(body)
|
|
222
|
-
case 2:
|
|
223
|
-
return splitTXXXDoubleNullFixed(body, encoding: .utf16BigEndian)
|
|
224
|
-
default:
|
|
225
|
-
return nil
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
static func splitTXXXSingleNull(
|
|
230
|
-
_ data: Data, encoding: String.Encoding
|
|
231
|
-
) -> (String, String)? {
|
|
232
|
-
guard let nullIdx = data.firstIndex(of: 0) else { return nil }
|
|
233
|
-
let descBytes = data.subdata(in: data.startIndex..<nullIdx)
|
|
234
|
-
let valueStart = nullIdx + 1
|
|
235
|
-
let valueBytes = valueStart < data.endIndex
|
|
236
|
-
? data.subdata(in: valueStart..<data.endIndex)
|
|
237
|
-
: Data()
|
|
238
|
-
guard let desc = String(data: descBytes, encoding: encoding),
|
|
239
|
-
let value = String(data: valueBytes, encoding: encoding) else {
|
|
240
|
-
return nil
|
|
241
|
-
}
|
|
242
|
-
return (stripBOM(desc), stripBOM(value))
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/// Encoding-1 UTF-16 with per-string BOM. Description and value
|
|
246
|
-
/// each have their own BOM. Separator is `0x00 0x00` aligned on
|
|
247
|
-
/// a 16-bit boundary. Strips the leading BOM character (U+FEFF)
|
|
248
|
-
/// from the decoded result — Swift's `String(data:encoding:.utf16…)`
|
|
249
|
-
/// preserves the BOM byte-pair as a U+FEFF zero-width glyph when
|
|
250
|
-
/// the explicit endianness encoding is used, which would prevent
|
|
251
|
-
/// downstream `"REPLAYGAIN_*"` dictionary lookups from matching.
|
|
252
|
-
static func splitTXXXDoubleNull(_ data: Data) -> (String, String)? {
|
|
253
|
-
let bytes = [UInt8](data)
|
|
254
|
-
var i = 0
|
|
255
|
-
while i + 1 < bytes.count {
|
|
256
|
-
if bytes[i] == 0 && bytes[i + 1] == 0 && i % 2 == 0 {
|
|
257
|
-
let descBytes = Data(bytes[0..<i])
|
|
258
|
-
let valueBytes = (i + 2) < bytes.count
|
|
259
|
-
? Data(bytes[(i + 2)..<bytes.count])
|
|
260
|
-
: Data()
|
|
261
|
-
let descEnc = detectUTF16Encoding(descBytes) ?? .utf16
|
|
262
|
-
let valueEnc = detectUTF16Encoding(valueBytes) ?? descEnc
|
|
263
|
-
guard let desc = String(data: descBytes, encoding: descEnc),
|
|
264
|
-
let value = String(data: valueBytes, encoding: valueEnc) else {
|
|
265
|
-
return nil
|
|
266
|
-
}
|
|
267
|
-
return (stripBOM(desc), stripBOM(value))
|
|
268
|
-
}
|
|
269
|
-
i += 1
|
|
270
|
-
}
|
|
271
|
-
return nil
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/// Strip leading U+FEFF (UTF-16 BOM) and trim trailing null /
|
|
275
|
-
/// control characters from a decoded string. ID3v2 taggers
|
|
276
|
-
/// (foobar2000, mp3tag) commonly emit TXXX values with a trailing
|
|
277
|
-
/// `\0` even though the spec says the final field has no
|
|
278
|
-
/// terminator. Without this trim, downstream parsers
|
|
279
|
-
/// (`Double.init` for gain, dictionary key lookup for description)
|
|
280
|
-
/// fail on the embedded null character.
|
|
281
|
-
static func stripBOM(_ s: String) -> String {
|
|
282
|
-
var out = s
|
|
283
|
-
if out.first == "\u{FEFF}" {
|
|
284
|
-
out = String(out.dropFirst())
|
|
285
|
-
}
|
|
286
|
-
while let last = out.last, last == "\0" || last.isNewline
|
|
287
|
-
|| last.unicodeScalars.first.map({ $0.value < 0x20 }) == true {
|
|
288
|
-
out = String(out.dropLast())
|
|
289
|
-
}
|
|
290
|
-
return out
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
static func splitTXXXDoubleNullFixed(
|
|
294
|
-
_ data: Data, encoding: String.Encoding
|
|
295
|
-
) -> (String, String)? {
|
|
296
|
-
let bytes = [UInt8](data)
|
|
297
|
-
var i = 0
|
|
298
|
-
while i + 1 < bytes.count {
|
|
299
|
-
if bytes[i] == 0 && bytes[i + 1] == 0 && i % 2 == 0 {
|
|
300
|
-
let descBytes = Data(bytes[0..<i])
|
|
301
|
-
let valueBytes = (i + 2) < bytes.count
|
|
302
|
-
? Data(bytes[(i + 2)..<bytes.count])
|
|
303
|
-
: Data()
|
|
304
|
-
guard let desc = String(data: descBytes, encoding: encoding),
|
|
305
|
-
let value = String(data: valueBytes, encoding: encoding) else {
|
|
306
|
-
return nil
|
|
307
|
-
}
|
|
308
|
-
return (stripBOM(desc), stripBOM(value))
|
|
309
|
-
}
|
|
310
|
-
i += 1
|
|
311
|
-
}
|
|
312
|
-
return nil
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
static func detectUTF16Encoding(_ data: Data) -> String.Encoding? {
|
|
316
|
-
guard data.count >= 2 else { return nil }
|
|
317
|
-
let b0 = data[data.startIndex]
|
|
318
|
-
let b1 = data[data.startIndex + 1]
|
|
319
|
-
if b0 == 0xFE && b1 == 0xFF { return .utf16BigEndian }
|
|
320
|
-
if b0 == 0xFF && b1 == 0xFE { return .utf16LittleEndian }
|
|
321
|
-
return nil
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// MARK: - FLAC
|
|
325
|
-
|
|
326
|
-
/// FLAC STREAMINFO header values relevant to bitrate computation.
|
|
327
|
-
/// Reading these + the file's byte size gives an accurate average
|
|
328
|
-
/// encoded bitrate without involving AVFoundation.
|
|
329
|
-
struct FLACStreamInfo {
|
|
330
|
-
let sampleRate: Int // Hz, 1..655350
|
|
331
|
-
let channelCount: Int // 1..8
|
|
332
|
-
let bitsPerSample: Int // 4..32
|
|
333
|
-
let totalSamples: Int64 // 0 = unknown
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/// Parse a FLAC STREAMINFO block from a `fLaC`-prefixed buffer.
|
|
337
|
-
/// STREAMINFO is type 0 and is required by the FLAC spec to be the
|
|
338
|
-
/// FIRST metadata block. Returns nil when the buffer doesn't start
|
|
339
|
-
/// with the FLAC magic or the block layout is malformed.
|
|
340
|
-
///
|
|
341
|
-
/// STREAMINFO body layout (34 bytes after the 4-byte block header):
|
|
342
|
-
/// - 16 bits: minBlockSize (samples)
|
|
343
|
-
/// - 16 bits: maxBlockSize
|
|
344
|
-
/// - 24 bits: minFrameSize (bytes)
|
|
345
|
-
/// - 24 bits: maxFrameSize
|
|
346
|
-
/// - 20 bits: sampleRate
|
|
347
|
-
/// - 3 bits: channels - 1
|
|
348
|
-
/// - 5 bits: bitsPerSample - 1
|
|
349
|
-
/// - 36 bits: totalSamples
|
|
350
|
-
/// - 128 bits: MD5
|
|
351
|
-
static func parseFLACStreamInfo(_ data: Data) -> FLACStreamInfo? {
|
|
352
|
-
guard data.count >= 4 + 4 + 34 else { return nil }
|
|
353
|
-
guard data[0] == 0x66, data[1] == 0x4c,
|
|
354
|
-
data[2] == 0x61, data[3] == 0x43 else { return nil }
|
|
355
|
-
let blockType = data[4] & 0x7f
|
|
356
|
-
guard blockType == 0 else { return nil }
|
|
357
|
-
let blockSize = Int(data[5]) << 16 | Int(data[6]) << 8 | Int(data[7])
|
|
358
|
-
guard blockSize >= 34, data.count >= 8 + blockSize else { return nil }
|
|
359
|
-
// STREAMINFO body starts at offset 8.
|
|
360
|
-
// Bits 64..83 (offset 10, 11, 12 in the body — i.e. data offsets
|
|
361
|
-
// 18, 19, 20): sampleRate (20 bits, big-endian).
|
|
362
|
-
let b18 = UInt64(data[18])
|
|
363
|
-
let b19 = UInt64(data[19])
|
|
364
|
-
let b20 = UInt64(data[20])
|
|
365
|
-
let sampleRate = Int((b18 << 12) | (b19 << 4) | (b20 >> 4))
|
|
366
|
-
let channels = Int((data[20] >> 1) & 0x07) + 1
|
|
367
|
-
let bitsPerSample = Int(((data[20] & 0x01) << 4) | (data[21] >> 4)) + 1
|
|
368
|
-
// totalSamples — 36 bits starting at byte 21 bit 4.
|
|
369
|
-
let t21 = Int64(data[21] & 0x0f)
|
|
370
|
-
let t22 = Int64(data[22])
|
|
371
|
-
let t23 = Int64(data[23])
|
|
372
|
-
let t24 = Int64(data[24])
|
|
373
|
-
let t25 = Int64(data[25])
|
|
374
|
-
let totalSamples =
|
|
375
|
-
(t21 << 32) | (t22 << 24) | (t23 << 16) | (t24 << 8) | t25
|
|
376
|
-
return FLACStreamInfo(
|
|
377
|
-
sampleRate: sampleRate,
|
|
378
|
-
channelCount: channels,
|
|
379
|
-
bitsPerSample: bitsPerSample,
|
|
380
|
-
totalSamples: totalSamples
|
|
381
|
-
)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/// Walk FLAC metadata blocks from the `fLaC` magic and return the
|
|
385
|
-
/// Vorbis-comment KEY=value dict from the first VORBIS_COMMENT
|
|
386
|
-
/// (block type 4). Stops at the last-block flag.
|
|
387
|
-
static func parseFLAC(_ data: Data) -> [String: String] {
|
|
388
|
-
guard data.count >= 8 else { return [:] }
|
|
389
|
-
guard data[0] == 0x66, data[1] == 0x4c,
|
|
390
|
-
data[2] == 0x61, data[3] == 0x43 else { return [:] }
|
|
391
|
-
var pos = 4
|
|
392
|
-
while pos + 4 <= data.count {
|
|
393
|
-
let header = data[pos]
|
|
394
|
-
let isLast = (header & 0x80) != 0
|
|
395
|
-
let blockType = header & 0x7f
|
|
396
|
-
let blockSize = Int(data[pos+1]) << 16 |
|
|
397
|
-
Int(data[pos+2]) << 8 |
|
|
398
|
-
Int(data[pos+3])
|
|
399
|
-
let blockStart = pos + 4
|
|
400
|
-
let blockEnd = blockStart + blockSize
|
|
401
|
-
if blockEnd > data.count { return [:] }
|
|
402
|
-
if blockType == 4 {
|
|
403
|
-
return parseVorbisComment(data.subdata(in: blockStart..<blockEnd))
|
|
404
|
-
}
|
|
405
|
-
if isLast { break }
|
|
406
|
-
pos = blockEnd
|
|
407
|
-
}
|
|
408
|
-
return [:]
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/// Vorbis comment block layout: vendor-len (LE u32) + vendor +
|
|
412
|
-
/// count (LE u32) + count × (len LE u32, UTF-8 KEY=value).
|
|
413
|
-
static func parseVorbisComment(_ data: Data) -> [String: String] {
|
|
414
|
-
var pos = 0
|
|
415
|
-
func readU32LE() -> Int? {
|
|
416
|
-
guard pos + 4 <= data.count else { return nil }
|
|
417
|
-
let v = Int(data[pos]) |
|
|
418
|
-
(Int(data[pos+1]) << 8) |
|
|
419
|
-
(Int(data[pos+2]) << 16) |
|
|
420
|
-
(Int(data[pos+3]) << 24)
|
|
421
|
-
pos += 4
|
|
422
|
-
return v
|
|
423
|
-
}
|
|
424
|
-
guard let vendorLen = readU32LE(),
|
|
425
|
-
pos + vendorLen <= data.count else { return [:] }
|
|
426
|
-
pos += vendorLen
|
|
427
|
-
guard let count = readU32LE() else { return [:] }
|
|
428
|
-
var result: [String: String] = [:]
|
|
429
|
-
for _ in 0..<count {
|
|
430
|
-
guard let len = readU32LE(),
|
|
431
|
-
len >= 0,
|
|
432
|
-
pos + len <= data.count else { break }
|
|
433
|
-
let bytes = data.subdata(in: pos..<(pos + len))
|
|
434
|
-
pos += len
|
|
435
|
-
guard let str = String(data: bytes, encoding: .utf8) else { continue }
|
|
436
|
-
if let eqIdx = str.firstIndex(of: "=") {
|
|
437
|
-
let key = String(str[..<eqIdx])
|
|
438
|
-
let value = String(str[str.index(after: eqIdx)...])
|
|
439
|
-
result[key] = value
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return result
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// MARK: - MP4 / M4A iTunes-style atoms
|
|
446
|
-
|
|
447
|
-
/// Walk the MP4 atom tree to `moov.udta.meta.ilst` and return the
|
|
448
|
-
/// freeform `----` atoms keyed by their `name` value. iTunes
|
|
449
|
-
/// stores ReplayGain in freeform atoms (mean=`com.apple.iTunes`,
|
|
450
|
-
/// name=`REPLAYGAIN_*`, data=UTF-8 value).
|
|
451
|
-
static func parseMP4(_ data: Data) -> [String: String] {
|
|
452
|
-
guard let ilst = findAtomPath(
|
|
453
|
-
data, path: ["moov", "udta", "meta", "ilst"]
|
|
454
|
-
) else {
|
|
455
|
-
return [:]
|
|
456
|
-
}
|
|
457
|
-
var pos = 0
|
|
458
|
-
var result: [String: String] = [:]
|
|
459
|
-
while pos + 8 <= ilst.count {
|
|
460
|
-
let size = Int(ilst[pos]) << 24 | Int(ilst[pos+1]) << 16 |
|
|
461
|
-
Int(ilst[pos+2]) << 8 | Int(ilst[pos+3])
|
|
462
|
-
let type = String(
|
|
463
|
-
data: ilst.subdata(in: (pos+4)..<(pos+8)),
|
|
464
|
-
encoding: .isoLatin1
|
|
465
|
-
) ?? ""
|
|
466
|
-
if size < 8 || pos + size > ilst.count { break }
|
|
467
|
-
let body = ilst.subdata(in: (pos+8)..<(pos+size))
|
|
468
|
-
if type == "----" {
|
|
469
|
-
if let (name, value) = parseFreeformAtom(body) {
|
|
470
|
-
result[name] = value
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
pos += size
|
|
474
|
-
}
|
|
475
|
-
return result
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/// A `----` (freeform) atom contains three child atoms:
|
|
479
|
-
/// `mean` (issuer, e.g. com.apple.iTunes), `name` (tag key), and
|
|
480
|
-
/// `data` (tag value). The first two share an 8-byte header
|
|
481
|
-
/// (size + type) plus a 4-byte version+flags before the UTF-8
|
|
482
|
-
/// payload. The `data` atom's payload is preceded by 4 bytes of
|
|
483
|
-
/// type indicator + 4 bytes of locale.
|
|
484
|
-
static func parseFreeformAtom(_ data: Data) -> (String, String)? {
|
|
485
|
-
var pos = 0
|
|
486
|
-
var name: String?
|
|
487
|
-
var value: String?
|
|
488
|
-
while pos + 8 <= data.count {
|
|
489
|
-
let size = Int(data[pos]) << 24 | Int(data[pos+1]) << 16 |
|
|
490
|
-
Int(data[pos+2]) << 8 | Int(data[pos+3])
|
|
491
|
-
let type = String(
|
|
492
|
-
data: data.subdata(in: (pos+4)..<(pos+8)),
|
|
493
|
-
encoding: .isoLatin1
|
|
494
|
-
) ?? ""
|
|
495
|
-
if size < 8 || pos + size > data.count { break }
|
|
496
|
-
let body = data.subdata(in: (pos+8)..<(pos+size))
|
|
497
|
-
switch type {
|
|
498
|
-
case "name":
|
|
499
|
-
if body.count > 4 {
|
|
500
|
-
name = String(
|
|
501
|
-
data: body.subdata(in: 4..<body.count),
|
|
502
|
-
encoding: .utf8
|
|
503
|
-
)
|
|
504
|
-
}
|
|
505
|
-
case "data":
|
|
506
|
-
if body.count > 8 {
|
|
507
|
-
value = String(
|
|
508
|
-
data: body.subdata(in: 8..<body.count),
|
|
509
|
-
encoding: .utf8
|
|
510
|
-
)
|
|
511
|
-
}
|
|
512
|
-
default:
|
|
513
|
-
break
|
|
514
|
-
}
|
|
515
|
-
pos += size
|
|
516
|
-
}
|
|
517
|
-
if let name = name, let value = value {
|
|
518
|
-
return (name, value)
|
|
519
|
-
}
|
|
520
|
-
return nil
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/// Walk an atom tree by name path. The `meta` atom in iTunes-
|
|
524
|
-
/// style MP4 carries a 4-byte version+flags prefix before its
|
|
525
|
-
/// children — the only path-walk quirk worth handling.
|
|
526
|
-
static func findAtomPath(_ data: Data, path: [String]) -> Data? {
|
|
527
|
-
var current = data
|
|
528
|
-
for name in path {
|
|
529
|
-
guard var body = findAtom(in: current, type: name) else { return nil }
|
|
530
|
-
if name == "meta" && body.count >= 4 {
|
|
531
|
-
body = body.subdata(in: 4..<body.count)
|
|
532
|
-
}
|
|
533
|
-
current = body
|
|
534
|
-
}
|
|
535
|
-
return current
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
static func findAtom(in data: Data, type: String) -> Data? {
|
|
539
|
-
var pos = 0
|
|
540
|
-
while pos + 8 <= data.count {
|
|
541
|
-
let size = Int(data[pos]) << 24 | Int(data[pos+1]) << 16 |
|
|
542
|
-
Int(data[pos+2]) << 8 | Int(data[pos+3])
|
|
543
|
-
let typeStr = String(
|
|
544
|
-
data: data.subdata(in: (pos+4)..<(pos+8)),
|
|
545
|
-
encoding: .isoLatin1
|
|
546
|
-
) ?? ""
|
|
547
|
-
if size < 8 || pos + size > data.count { return nil }
|
|
548
|
-
if typeStr == type {
|
|
549
|
-
return data.subdata(in: (pos+8)..<(pos+size))
|
|
550
|
-
}
|
|
551
|
-
pos += size
|
|
552
|
-
}
|
|
553
|
-
return nil
|
|
554
|
-
}
|
|
555
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import AVFoundation
|
|
2
|
-
import XCTest
|
|
3
|
-
@testable import QueuePlayer
|
|
4
|
-
|
|
5
|
-
/// Stub coverage for `CrossfadeEngine`. Constructor non-crash and
|
|
6
|
-
/// delegate wirability are the only assertable surfaces — every
|
|
7
|
-
/// runtime method crashes via `preconditionFailure`, which is
|
|
8
|
-
/// non-recoverable from XCTest. The Kotlin counterpart asserts
|
|
9
|
-
/// `NotImplementedError` on every method via `assertThrows`; on
|
|
10
|
-
/// Swift the equivalent would require a crash-runner test
|
|
11
|
-
/// harness, which adds infra for low value here.
|
|
12
|
-
///
|
|
13
|
-
/// Negative-duration constructor guard at
|
|
14
|
-
/// `CrossfadeEngine.init(crossfadeDurationSeconds:)` is also
|
|
15
|
-
/// non-testable for the same reason. The load-bearing guard for
|
|
16
|
-
/// runtime callers is the clamp inside `PlaybackModeStateMachine`;
|
|
17
|
-
/// the constructor `precondition` is a defensive belt.
|
|
18
|
-
final class CrossfadeEngineStubTests: XCTestCase {
|
|
19
|
-
|
|
20
|
-
func testConstructorAcceptsNonNegativeCrossfadeDuration() {
|
|
21
|
-
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0)
|
|
22
|
-
XCTAssertNotNil(engine)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
func testDelegateIsWirableOnTheStub() {
|
|
26
|
-
let engine = CrossfadeEngine(crossfadeDurationSeconds: 5.0)
|
|
27
|
-
let delegate = NoopDelegate()
|
|
28
|
-
engine.delegate = delegate
|
|
29
|
-
XCTAssertTrue(engine.delegate === delegate)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private final class NoopDelegate: PlaybackEngineDelegate {
|
|
34
|
-
func engineStateMaybeChanged(_ engine: PlaybackEngine, reason: StateChangeReason?) {}
|
|
35
|
-
func engineIsPlayingChanged(_ engine: PlaybackEngine, isPlaying: Bool) {}
|
|
36
|
-
func engineDidTransitionTrack(_ engine: PlaybackEngine) {}
|
|
37
|
-
func engineActiveItemFormatChanged(_ engine: PlaybackEngine) {}
|
|
38
|
-
func engineDidFailWithError(_ engine: PlaybackEngine, error: Error) {}
|
|
39
|
-
func engineDidPlayItemToEnd(_ engine: PlaybackEngine) {}
|
|
40
|
-
func enginePlaybackEnded(_ engine: PlaybackEngine) {}
|
|
41
|
-
func engineCrossfadeDidBegin(_ engine: PlaybackEngine, incomingIndex: Int) {}
|
|
42
|
-
func engineCrossfadeDidCancel(_ engine: PlaybackEngine, revertedIndex: Int) {}
|
|
43
|
-
}
|