react-native-audio-browser 0.2.0-next.2 → 0.2.0-next.21
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/README.md +35 -21
- package/android/build.gradle +35 -12
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +389 -164
- package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
- package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
- package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +24 -5
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +97 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +70 -29
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +20 -7
- package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
- package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
- package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
- package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
- package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +12 -12
- package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
- package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
- package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +509 -230
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -25
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +113 -78
- package/android/src/main/java/com/audiobrowser/player/Player.kt +81 -38
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
- package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
- package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +56 -40
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +108 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +56 -52
- package/android/src/main/res/values/strings.xml +25 -5
- package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +119 -33
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +15 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +346 -8
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -0
- package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
- package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
- package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
- package/android/src/test/java/com/audiobrowser/player/BrowseTilesTest.kt +385 -0
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +101 -58
- package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
- package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
- package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
- package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +162 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
- package/ios/Browser/BrowserConfig.swift +102 -5
- package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
- package/ios/Browser/BrowserManager.swift +230 -205
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +264 -106
- package/ios/Browser/SearchDrillIn.swift +7 -7
- package/ios/Browser/SectionPresentation.swift +64 -0
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +58 -0
- package/ios/Browser/TabBarEntries.swift +9 -3
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
- package/ios/CarPlay/CarPlayController.swift +227 -85
- package/ios/CarPlay/CarPlayImageLoader.swift +42 -20
- package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
- package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
- package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
- package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
- package/ios/Extension/Section+Copying.swift +29 -0
- package/ios/Extension/String+IcyCharset.swift +21 -0
- package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
- package/ios/Extension/Track+Copying.swift +8 -12
- package/ios/Extension/Track+Identity.swift +18 -0
- package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +253 -100
- package/ios/Model/NitroTypeStubs.swift +125 -23
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Model/RemoteCommand.swift +18 -0
- package/ios/Model/TrackPlayerError.swift +2 -2
- package/ios/Option/SessionCategories.swift +3 -2
- package/ios/Player/MediaIntentCriteria.swift +17 -9
- package/ios/Player/MediaLoader.swift +33 -9
- package/ios/Player/NowPlayingUpdater.swift +54 -6
- package/ios/Player/PlaybackCoordinator.swift +13 -6
- package/ios/Player/PlaybackErrorHandler.swift +4 -4
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/QueueManager.swift +46 -13
- package/ios/Player/RetryManager.swift +80 -18
- package/ios/Player/SeekCompletionHandler.swift +3 -1
- package/ios/Player/ShuffleOrder.swift +33 -0
- package/ios/Player/SleepTimerHandling.swift +13 -0
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +62 -24
- package/ios/Tests/BrowserPathHelperTests.swift +50 -20
- package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
- package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
- package/ios/Tests/JsonModelsTests.swift +150 -1
- package/ios/Tests/LRUCacheTests.swift +111 -0
- package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
- package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
- package/ios/Tests/PlaybackCoordinatorTests.swift +1 -1
- package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
- package/ios/Tests/QueueManagerTests.swift +122 -10
- package/ios/Tests/RemoteCommandControllerTests.swift +172 -0
- package/ios/Tests/RemoteCommandTests.swift +67 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
- package/ios/Tests/RetryManagerTests.swift +89 -0
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +90 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +127 -0
- package/ios/Tests/Support/TestTypes.swift +8 -1
- package/ios/Tests/TabBarEntriesTests.swift +19 -19
- package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
- package/ios/Tests/TrackIdTemplateTests.swift +67 -0
- package/ios/Tests/TrackIdentityTests.swift +20 -0
- package/ios/Tests/TrackSelectorTests.swift +168 -33
- package/ios/TrackPlayer.swift +63 -35
- package/ios/TrackPlayerCallbacks.swift +5 -38
- package/ios/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -1
- package/ios/Util/LRUCache.swift +4 -1
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/SFSymbolRenderer.swift +24 -2
- package/ios/Util/SVGProcessor.swift +9 -2
- package/lib/commonjs/AudioBrowser.js +8 -8
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/CLAUDE.md +52 -0
- package/lib/commonjs/features/browser-config.js +12 -8
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/browser.js +11 -3
- package/lib/commonjs/features/browser.js.map +1 -1
- package/lib/commonjs/features/carConnection.js +1 -1
- package/lib/commonjs/features/equalizer.js +1 -1
- package/lib/commonjs/features/errors.js +3 -3
- package/lib/commonjs/features/favorites.js +14 -11
- package/lib/commonjs/features/favorites.js.map +1 -1
- package/lib/commonjs/features/gate.js +16 -1
- package/lib/commonjs/features/gate.js.map +1 -1
- package/lib/commonjs/features/metadata.js +20 -3
- package/lib/commonjs/features/metadata.js.map +1 -1
- package/lib/commonjs/features/network.js +1 -1
- package/lib/commonjs/features/nowPlaying.js +1 -1
- package/lib/commonjs/features/output.js +1 -1
- package/lib/commonjs/features/playback/controls.js +4 -2
- package/lib/commonjs/features/playback/controls.js.map +1 -1
- package/lib/commonjs/features/playback/playWhenReady.js +1 -1
- package/lib/commonjs/features/playback/playing.js +1 -1
- package/lib/commonjs/features/playback/progress.js +3 -2
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/rate.js +8 -1
- package/lib/commonjs/features/playback/rate.js.map +1 -1
- package/lib/commonjs/features/playback/state.js +3 -4
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
- package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/commonjs/features/playback/volume.js +1 -1
- package/lib/commonjs/features/player/options.js +48 -34
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +18 -9
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +23 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -0
- package/lib/commonjs/features/queue/activeTrack.js +15 -4
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +13 -6
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/features/queue/repeatMode.js +1 -1
- package/lib/commonjs/features/queue/shuffle.js +1 -1
- package/lib/commonjs/features/remoteControls.js +13 -92
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/features/sleepTimer.js +13 -1
- package/lib/commonjs/features/sleepTimer.js.map +1 -1
- package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
- package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
- package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
- package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/commonjs/utils/getTrackIdentity.js +24 -0
- package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/commonjs/web/NativeAudioBrowser.js +200 -75
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
- package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
- package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +175 -100
- package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
- package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
- package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
- package/lib/commonjs/web/browser/SearchManager.js +18 -23
- package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
- package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/commonjs/web/browser/sections.js +89 -0
- package/lib/commonjs/web/browser/sections.js.map +1 -0
- package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
- package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
- package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
- package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
- package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/module/AudioBrowser.js +2 -1
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/CLAUDE.md +52 -0
- package/lib/module/features/browser-config.js +12 -8
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/browser.js +11 -3
- package/lib/module/features/browser.js.map +1 -1
- package/lib/module/features/carConnection.js +1 -1
- package/lib/module/features/equalizer.js +1 -1
- package/lib/module/features/errors.js +3 -3
- package/lib/module/features/favorites.js +14 -11
- package/lib/module/features/favorites.js.map +1 -1
- package/lib/module/features/gate.js +16 -1
- package/lib/module/features/gate.js.map +1 -1
- package/lib/module/features/metadata.js +20 -3
- package/lib/module/features/metadata.js.map +1 -1
- package/lib/module/features/network.js +1 -1
- package/lib/module/features/nowPlaying.js +1 -1
- package/lib/module/features/output.js +1 -1
- package/lib/module/features/playback/controls.js +4 -2
- package/lib/module/features/playback/controls.js.map +1 -1
- package/lib/module/features/playback/playWhenReady.js +1 -1
- package/lib/module/features/playback/playing.js +1 -1
- package/lib/module/features/playback/progress.js +3 -2
- package/lib/module/features/playback/progress.js.map +1 -1
- package/lib/module/features/playback/rate.js +8 -1
- package/lib/module/features/playback/rate.js.map +1 -1
- package/lib/module/features/playback/state.js +3 -4
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/playback/trackPlaybackTime.js +0 -5
- package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/module/features/playback/volume.js +1 -1
- package/lib/module/features/player/options.js +47 -32
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +17 -8
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +19 -0
- package/lib/module/features/player/validateOptions.js.map +1 -0
- package/lib/module/features/queue/activeTrack.js +15 -4
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +13 -6
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/features/queue/repeatMode.js +1 -1
- package/lib/module/features/queue/shuffle.js +1 -1
- package/lib/module/features/remoteControls.js +13 -92
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/features/sleepTimer.js +13 -1
- package/lib/module/features/sleepTimer.js.map +1 -1
- package/lib/module/test-utils/reactNativeStub.js +58 -0
- package/lib/module/test-utils/reactNativeStub.js.map +1 -0
- package/lib/module/utils/LazyNativeEmitter.js +28 -14
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/utils/NativeUpdatedValue.js +31 -13
- package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/module/utils/getTrackIdentity.js +20 -0
- package/lib/module/utils/getTrackIdentity.js.map +1 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/module/web/NativeAudioBrowser.js +200 -75
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
- package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
- package/lib/module/web/TrackPlayer/Player.js +254 -39
- package/lib/module/web/TrackPlayer/Player.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
- package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +175 -100
- package/lib/module/web/browser/BrowserManager.js.map +1 -1
- package/lib/module/web/browser/FavoriteManager.js +31 -21
- package/lib/module/web/browser/FavoriteManager.js.map +1 -1
- package/lib/module/web/browser/SearchManager.js +18 -23
- package/lib/module/web/browser/SearchManager.js.map +1 -1
- package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/module/web/browser/sections.js +83 -0
- package/lib/module/web/browser/sections.js.map +1 -0
- package/lib/module/web/http/RequestConfigBuilder.js +110 -27
- package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/module/web/http/followMediaRedirect.js +215 -0
- package/lib/module/web/http/followMediaRedirect.js.map +1 -0
- package/lib/module/web/player/NowPlayingManager.js +4 -3
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/util/BrowserPathHelper.js +56 -11
- package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AudioBrowser.d.ts +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/browser.d.ts +10 -2
- package/lib/typescript/src/features/browser.d.ts.map +1 -1
- package/lib/typescript/src/features/carConnection.d.ts +1 -1
- package/lib/typescript/src/features/equalizer.d.ts +1 -1
- package/lib/typescript/src/features/errors.d.ts +3 -3
- package/lib/typescript/src/features/favorites.d.ts +14 -11
- package/lib/typescript/src/features/favorites.d.ts.map +1 -1
- package/lib/typescript/src/features/gate.d.ts +18 -2
- package/lib/typescript/src/features/gate.d.ts.map +1 -1
- package/lib/typescript/src/features/metadata.d.ts +20 -3
- package/lib/typescript/src/features/metadata.d.ts.map +1 -1
- package/lib/typescript/src/features/network.d.ts +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
- package/lib/typescript/src/features/output.d.ts +1 -1
- package/lib/typescript/src/features/playback/controls.d.ts +4 -2
- package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
- package/lib/typescript/src/features/playback/playing.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/rate.d.ts +8 -1
- package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +3 -4
- package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/volume.d.ts +1 -1
- package/lib/typescript/src/features/player/options.d.ts +108 -56
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts +14 -4
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
- package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +7 -4
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
- package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
- package/lib/typescript/src/features/remoteControls.d.ts +23 -22
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
- package/lib/typescript/src/types/browser-native.d.ts +3 -2
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +380 -126
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +82 -57
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
- package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
- package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
- package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +16 -4
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +7 -10
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
- package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +45 -25
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +36 -11
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +15 -12
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +7 -10
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +7 -10
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
- package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +57 -49
- package/nitrogen/generated/android/c++/JSection.hpp +116 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +25 -46
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +8 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +15 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -12
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +9 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +244 -133
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +31 -16
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +33 -18
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
- package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
- package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
- package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
- package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
- package/nitrogen/generated/ios/swift/Section.swift +82 -0
- package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
- package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +28 -63
- package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
- package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
- package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
- package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
- package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
- package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
- package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
- package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
- package/nitrogen/generated/shared/c++/Section.hpp +102 -0
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
- package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
- package/nitrogen/generated/shared/c++/Track.hpp +18 -30
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
- package/package.json +24 -11
- package/src/AudioBrowser.ts +2 -1
- package/src/CLAUDE.md +52 -0
- package/src/features/battery.test.ts +79 -0
- package/src/features/browser-config.test.ts +21 -6
- package/src/features/browser-config.ts +12 -8
- package/src/features/browser.test.ts +1 -1
- package/src/features/browser.ts +11 -3
- package/src/features/carConnection.ts +1 -1
- package/src/features/equalizer.ts +1 -1
- package/src/features/errors.ts +3 -3
- package/src/features/favorites.ts +14 -11
- package/src/features/gate.ts +16 -1
- package/src/features/hook-wiring.test.ts +302 -0
- package/src/features/metadata.ts +20 -3
- package/src/features/network.ts +1 -1
- package/src/features/nowPlaying.ts +1 -1
- package/src/features/output.ts +1 -1
- package/src/features/playback/controls.ts +4 -2
- package/src/features/playback/playWhenReady.ts +1 -1
- package/src/features/playback/playing.ts +1 -1
- package/src/features/playback/progress.test.ts +169 -0
- package/src/features/playback/progress.ts +3 -2
- package/src/features/playback/rate.ts +8 -1
- package/src/features/playback/state.ts +3 -4
- package/src/features/playback/trackPlaybackTime.test.ts +16 -6
- package/src/features/playback/trackPlaybackTime.ts +1 -6
- package/src/features/playback/volume.ts +1 -1
- package/src/features/player/options.ts +126 -76
- package/src/features/player/setup.test.ts +24 -7
- package/src/features/player/setup.ts +22 -10
- package/src/features/player/validateOptions.ts +21 -0
- package/src/features/queue/activeTrack.ts +15 -4
- package/src/features/queue/queue.ts +13 -6
- package/src/features/queue/repeatMode.ts +1 -1
- package/src/features/queue/shuffle.ts +1 -1
- package/src/features/remoteControls.ts +13 -92
- package/src/features/sleepTimer.test.ts +172 -0
- package/src/features/sleepTimer.ts +9 -1
- package/src/nitro-stub-drift.test.ts +277 -0
- package/src/public-surface.test.ts +147 -0
- package/src/specs/audio-browser.nitro.ts +5 -4
- package/src/style-resolution.test.ts +210 -0
- package/src/test-utils/reactNativeStub.ts +60 -0
- package/src/types/browser-native.ts +7 -3
- package/src/types/browser-nodes.ts +396 -134
- package/src/types/browser.ts +85 -59
- package/src/utils/LazyNativeEmitter.ts +28 -15
- package/src/utils/NativeUpdatedValue.ts +29 -13
- package/src/utils/getTrackIdentity.ts +20 -0
- package/src/utils/useDebug.test.ts +182 -0
- package/src/utils/useNativeUpdatedValue.test.ts +103 -0
- package/src/utils/useNativeValueRefreshedBy.ts +38 -0
- package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
- package/src/web/NativeAudioBrowser.ts +239 -82
- package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
- package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
- package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
- package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
- package/src/web/TrackPlayer/Player.load.test.ts +137 -1
- package/src/web/TrackPlayer/Player.ts +284 -37
- package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
- package/src/web/__tests__/lifecycle.test.ts +345 -0
- package/src/web/__tests__/lifecycleHarness.ts +351 -0
- package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
- package/src/web/__tests__/loadSupersession.test.ts +175 -0
- package/src/web/__tests__/retry.test.ts +169 -0
- package/src/web/browser/BrowserManager.ts +209 -111
- package/src/web/browser/FavoriteManager.test.ts +84 -0
- package/src/web/browser/FavoriteManager.ts +28 -23
- package/src/web/browser/SearchManager.test.ts +75 -0
- package/src/web/browser/SearchManager.ts +25 -28
- package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
- package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
- package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
- package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
- package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
- package/src/web/browser/assertBrowsePageShape.ts +5 -3
- package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
- package/src/web/browser/classifyTrackNavigation.ts +9 -9
- package/src/web/browser/parseSearchResponse.test.ts +8 -2
- package/src/web/browser/sections.test.ts +91 -0
- package/src/web/browser/sections.ts +72 -0
- package/src/web/http/RequestConfigBuilder.test.ts +241 -1
- package/src/web/http/RequestConfigBuilder.ts +140 -33
- package/src/web/http/followMediaRedirect.test.ts +289 -0
- package/src/web/http/followMediaRedirect.ts +253 -0
- package/src/web/player/NowPlayingManager.ts +4 -3
- package/src/web/util/BrowserPathHelper.test.ts +52 -0
- package/src/web/util/BrowserPathHelper.ts +61 -13
- package/website/guide/android-auto.md +114 -0
- package/website/guide/android-certificates.md +409 -0
- package/website/guide/artwork.md +255 -0
- package/website/guide/audio-output.md +135 -0
- package/website/guide/automotive.md +86 -0
- package/website/guide/basic-usage.md +151 -0
- package/website/guide/battery.md +115 -0
- package/website/guide/browser.md +581 -0
- package/website/guide/carplay.md +620 -0
- package/website/guide/configuration.md +173 -0
- package/website/guide/equalizer.md +231 -0
- package/website/guide/errors.md +310 -0
- package/website/guide/favorites.md +154 -0
- package/website/guide/gate.md +143 -0
- package/website/guide/getting-started.md +46 -0
- package/website/guide/hooks.md +238 -0
- package/website/guide/live-streams.md +125 -0
- package/website/guide/metadata.md +239 -0
- package/website/guide/migrating-from-track-player.md +244 -0
- package/website/guide/native-callback-fetch.md +127 -0
- package/website/guide/network.md +86 -0
- package/website/guide/now-playing.md +175 -0
- package/website/guide/playback.md +328 -0
- package/website/guide/queue.md +300 -0
- package/website/guide/remote-controls.md +256 -0
- package/website/guide/search.md +255 -0
- package/website/guide/sleep-timer.md +268 -0
- package/website/guide/track.md +247 -0
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/ios/Model/MediaURL.swift +0 -67
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
- package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
- package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
- package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
- package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
- package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
- package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
- package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
- package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
package com.audiobrowser.player
|
|
2
|
+
|
|
3
|
+
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
4
|
+
import okhttp3.Request
|
|
5
|
+
import org.junit.Assert.assertEquals
|
|
6
|
+
import org.junit.Assert.assertFalse
|
|
7
|
+
import org.junit.Assert.assertNull
|
|
8
|
+
import org.junit.Assert.assertTrue
|
|
9
|
+
import org.junit.Test
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* OkHttp drops `Authorization` across a cross-origin redirect by itself; these cover the rest,
|
|
13
|
+
* which it forwards and [CrossOriginHeaderStripper.stripCallerHeaders] removes.
|
|
14
|
+
*/
|
|
15
|
+
class CrossOriginHeaderStripperTest {
|
|
16
|
+
|
|
17
|
+
private val api = "https://api.example.com/stream/7".toHttpUrl()
|
|
18
|
+
|
|
19
|
+
@Test
|
|
20
|
+
fun `same origin ignores the path`() {
|
|
21
|
+
assertTrue(
|
|
22
|
+
CrossOriginHeaderStripper.sameOrigin(
|
|
23
|
+
api,
|
|
24
|
+
"https://api.example.com/signed/track.mp3".toHttpUrl(),
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun `a different host, port or scheme is a different origin`() {
|
|
31
|
+
assertFalse(
|
|
32
|
+
CrossOriginHeaderStripper.sameOrigin(api, "https://cdn.example.com/track.mp3".toHttpUrl())
|
|
33
|
+
)
|
|
34
|
+
assertFalse(
|
|
35
|
+
CrossOriginHeaderStripper.sameOrigin(
|
|
36
|
+
api,
|
|
37
|
+
"https://api.example.com:8443/track.mp3".toHttpUrl(),
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
assertFalse(
|
|
41
|
+
CrossOriginHeaderStripper.sameOrigin(api, "http://api.example.com/track.mp3".toHttpUrl())
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Test
|
|
46
|
+
fun `strips every caller header, whatever its name`() {
|
|
47
|
+
val request =
|
|
48
|
+
Request.Builder()
|
|
49
|
+
.url(api)
|
|
50
|
+
.header("Authorization", "Bearer token")
|
|
51
|
+
.header("X-Api-Key", "k")
|
|
52
|
+
.header("Cookie", "session=abc")
|
|
53
|
+
.build()
|
|
54
|
+
|
|
55
|
+
val stripped = CrossOriginHeaderStripper.stripCallerHeaders(request)
|
|
56
|
+
|
|
57
|
+
assertNull(stripped.header("Authorization"))
|
|
58
|
+
assertNull(stripped.header("X-Api-Key"))
|
|
59
|
+
assertNull(stripped.header("Cookie"))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Test
|
|
63
|
+
fun `keeps the transport headers the hop needs`() {
|
|
64
|
+
val request =
|
|
65
|
+
Request.Builder()
|
|
66
|
+
.url(api)
|
|
67
|
+
.header("Range", "bytes=1024-")
|
|
68
|
+
.header("accept-encoding", "identity")
|
|
69
|
+
.header("User-Agent", "react-native-audio-browser")
|
|
70
|
+
.header("X-Api-Key", "k")
|
|
71
|
+
.build()
|
|
72
|
+
|
|
73
|
+
val stripped = CrossOriginHeaderStripper.stripCallerHeaders(request)
|
|
74
|
+
|
|
75
|
+
// a dropped Range would restart the read from zero on every seek past a redirect
|
|
76
|
+
assertEquals("bytes=1024-", stripped.header("Range"))
|
|
77
|
+
assertEquals("identity", stripped.header("Accept-Encoding"))
|
|
78
|
+
assertEquals("react-native-audio-browser", stripped.header("User-Agent"))
|
|
79
|
+
assertNull(stripped.header("X-Api-Key"))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Test
|
|
83
|
+
fun `keeps media3's ICY metadata request across a hop`() {
|
|
84
|
+
// ProgressiveMediaPeriod asks for Shoutcast metadata on the DataSpec, the same channel the
|
|
85
|
+
// config's headers arrive on. Dropping it costs icy-metaint, so onMetadata never fires again.
|
|
86
|
+
val request =
|
|
87
|
+
Request.Builder().url(api).header("Icy-MetaData", "1").header("X-Api-Key", "k").build()
|
|
88
|
+
|
|
89
|
+
val stripped = CrossOriginHeaderStripper.stripCallerHeaders(request)
|
|
90
|
+
|
|
91
|
+
assertEquals("1", stripped.header("Icy-MetaData"))
|
|
92
|
+
assertNull(stripped.header("X-Api-Key"))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package com.audiobrowser.player
|
|
2
|
+
|
|
3
|
+
import androidx.media3.session.CommandButton
|
|
4
|
+
import org.junit.Assert.assertArrayEquals
|
|
5
|
+
import org.junit.Assert.assertTrue
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Guards the slot preference lists handed to Media3.
|
|
10
|
+
*
|
|
11
|
+
* The invariant that matters is the OVERFLOW fallback: every flattening keeps a button only if it
|
|
12
|
+
* won SLOT_BACK, won SLOT_FORWARD, or names SLOT_OVERFLOW. A slot that stops naming OVERFLOW
|
|
13
|
+
* silently deletes its button from the notification and the car the moment its primary position is
|
|
14
|
+
* unavailable, so the loop below asserts the property for every slot rather than only today's
|
|
15
|
+
* three.
|
|
16
|
+
*/
|
|
17
|
+
class MediaSessionSlotMappingTest {
|
|
18
|
+
|
|
19
|
+
@Test
|
|
20
|
+
fun `primary slots are preferred with an overflow fallback`() {
|
|
21
|
+
assertArrayEquals(
|
|
22
|
+
intArrayOf(CommandButton.SLOT_BACK, CommandButton.SLOT_OVERFLOW),
|
|
23
|
+
media3SlotsFor(ButtonSlot.BACK),
|
|
24
|
+
)
|
|
25
|
+
assertArrayEquals(
|
|
26
|
+
intArrayOf(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW),
|
|
27
|
+
media3SlotsFor(ButtonSlot.FORWARD),
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
fun `overflow does not repeat itself`() {
|
|
33
|
+
assertArrayEquals(intArrayOf(CommandButton.SLOT_OVERFLOW), media3SlotsFor(ButtonSlot.OVERFLOW))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test
|
|
37
|
+
fun `every slot names OVERFLOW so nothing can be dropped`() {
|
|
38
|
+
for (slot in ButtonSlot.entries) {
|
|
39
|
+
assertTrue(
|
|
40
|
+
"$slot must name SLOT_OVERFLOW or its button is dropped from the notification and the car",
|
|
41
|
+
media3SlotsFor(slot).contains(CommandButton.SLOT_OVERFLOW),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -377,7 +377,7 @@ class NowPlayingUpdaterTest {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
@Test
|
|
380
|
-
fun `artwork re-resolves for a new track
|
|
380
|
+
fun `artwork re-resolves for a new track identity including id-less tracks`() = runTest {
|
|
381
381
|
val surface =
|
|
382
382
|
FakeSurface().apply {
|
|
383
383
|
currentTrack = track("A", id = "a")
|
|
@@ -393,10 +393,31 @@ class NowPlayingUpdaterTest {
|
|
|
393
393
|
runCurrent()
|
|
394
394
|
assertEquals(2, surface.artworkResolveCount)
|
|
395
395
|
|
|
396
|
+
// The config runs for id-less tracks too — no blanket skip.
|
|
396
397
|
surface.currentTrack = track("C", id = null, src = "https://s/c.mp3")
|
|
397
398
|
updater.render()
|
|
398
399
|
runCurrent()
|
|
399
|
-
assertEquals(
|
|
400
|
+
assertEquals(3, surface.artworkResolveCount)
|
|
401
|
+
assertEquals(0 to "https://img/a.png", surface.artworkStamps.last())
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
@Test
|
|
405
|
+
fun `artwork for an id-less track resolves once keyed on src identity`() = runTest {
|
|
406
|
+
val surface =
|
|
407
|
+
FakeSurface().apply {
|
|
408
|
+
currentTrack = track("C", id = null, src = "https://s/c.mp3")
|
|
409
|
+
hasNowPlayingArtworkConfig = true
|
|
410
|
+
artworkResult = "https://img/c.png"
|
|
411
|
+
}
|
|
412
|
+
val updater = NowPlayingUpdater(surface, backgroundScope)
|
|
413
|
+
|
|
414
|
+
updater.render()
|
|
415
|
+
runCurrent()
|
|
416
|
+
updater.updateNowPlaying(update(title = "Song")) // re-render, same track
|
|
417
|
+
runCurrent()
|
|
418
|
+
|
|
419
|
+
assertEquals(1, surface.artworkResolveCount)
|
|
420
|
+
assertEquals(listOf(0 to "https://img/c.png"), surface.artworkStamps)
|
|
400
421
|
}
|
|
401
422
|
|
|
402
423
|
@Test
|
|
@@ -439,4 +460,23 @@ class NowPlayingUpdaterTest {
|
|
|
439
460
|
|
|
440
461
|
assertTrue(surface.artworkStamps.isEmpty())
|
|
441
462
|
}
|
|
463
|
+
|
|
464
|
+
@Test
|
|
465
|
+
fun `a stale artwork result for an id-less track is not stamped after a track change`() =
|
|
466
|
+
runTest {
|
|
467
|
+
val surface =
|
|
468
|
+
FakeSurface().apply {
|
|
469
|
+
currentTrack = track("A", id = null, src = "https://s/a.mp3")
|
|
470
|
+
hasNowPlayingArtworkConfig = true
|
|
471
|
+
artworkResult = "https://img/a.png"
|
|
472
|
+
}
|
|
473
|
+
val updater = NowPlayingUpdater(surface, backgroundScope)
|
|
474
|
+
|
|
475
|
+
updater.render() // schedules the artwork resolve
|
|
476
|
+
// Skip to another id-less track before it lands: identity (src) keying must catch it.
|
|
477
|
+
surface.currentTrack = track("B", id = null, src = "https://s/b.mp3")
|
|
478
|
+
runCurrent()
|
|
479
|
+
|
|
480
|
+
assertTrue(surface.artworkStamps.isEmpty())
|
|
481
|
+
}
|
|
442
482
|
}
|
|
@@ -132,8 +132,8 @@ class PlaybackErrorClassifierTest {
|
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
134
|
* Load errors are raw IOExceptions from the transport layer; the policy only retries network
|
|
135
|
-
* ones, so the advisory classification is UNREACHABLE unless the device is offline or the
|
|
136
|
-
*
|
|
135
|
+
* ones, so the advisory classification is UNREACHABLE unless the device is offline or the server
|
|
136
|
+
* actually answered.
|
|
137
137
|
*/
|
|
138
138
|
@Test
|
|
139
139
|
fun `retrying transport failure is unreachable`() {
|
|
@@ -158,8 +158,7 @@ class PlaybackErrorClassifierTest {
|
|
|
158
158
|
/** The HTTP status is dug out of the cause chain and mapped through the shared status table. */
|
|
159
159
|
@Test
|
|
160
160
|
fun `retrying HTTP failure carries the status`() {
|
|
161
|
-
val error =
|
|
162
|
-
PlaybackErrorClassifier.retryingLoadError(httpException(503), online = true)
|
|
161
|
+
val error = PlaybackErrorClassifier.retryingLoadError(httpException(503), online = true)
|
|
163
162
|
assertEquals(PlaybackErrorKind.SERVER_ERROR, error.kind)
|
|
164
163
|
assertEquals(503.0, error.statusCode!!, 0.0)
|
|
165
164
|
assertEquals(true, error.retrying)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.audiobrowser.player
|
|
2
|
+
|
|
3
|
+
import com.audiobrowser.TestFixtures
|
|
4
|
+
import com.audiobrowser.TestFixtures.trackStyle
|
|
5
|
+
import com.margelo.nitro.audiobrowser.ArtworkRendering
|
|
6
|
+
import com.margelo.nitro.audiobrowser.StyleDisplay
|
|
7
|
+
import org.junit.Assert.assertEquals
|
|
8
|
+
import org.junit.Assert.assertNull
|
|
9
|
+
import org.junit.Test
|
|
10
|
+
import org.junit.runner.RunWith
|
|
11
|
+
import org.robolectric.RobolectricTestRunner
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The bespoke org.json persistence of the resumption track. ADR 0011: the style block and
|
|
15
|
+
* `disabled` survive process death (a restored queue keeps its visible fidelity), and a snapshot
|
|
16
|
+
* written before the block model — `style` as an enum-name string — restores without a declaration
|
|
17
|
+
* rather than failing.
|
|
18
|
+
*/
|
|
19
|
+
@RunWith(RobolectricTestRunner::class)
|
|
20
|
+
class PlaybackStateStoreTrackJsonTest {
|
|
21
|
+
|
|
22
|
+
@Test
|
|
23
|
+
fun `style and disabled survive the round-trip`() {
|
|
24
|
+
val track =
|
|
25
|
+
TestFixtures.track(title = "Night Mix", src = "https://s/a.mp3", disabled = true)
|
|
26
|
+
.copy(
|
|
27
|
+
style =
|
|
28
|
+
trackStyle(display = StyleDisplay.GRID, artworkRendering = ArtworkRendering.STENCIL)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
val restored = PlaybackStateStore.trackFromJson(PlaybackStateStore.trackToJson(track))
|
|
32
|
+
|
|
33
|
+
assertEquals(StyleDisplay.GRID, restored?.style?.display)
|
|
34
|
+
assertEquals(ArtworkRendering.STENCIL, restored?.style?.artworkRendering)
|
|
35
|
+
assertEquals(true, restored?.disabled)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Test
|
|
39
|
+
fun `a track without style round-trips without one`() {
|
|
40
|
+
val track = TestFixtures.track(title = "Plain", src = "https://s/a.mp3")
|
|
41
|
+
|
|
42
|
+
val restored = PlaybackStateStore.trackFromJson(PlaybackStateStore.trackToJson(track))
|
|
43
|
+
|
|
44
|
+
assertNull(restored?.style)
|
|
45
|
+
assertNull(restored?.disabled)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
fun `a pre-block snapshot with a string style restores without a declaration`() {
|
|
50
|
+
val restored = PlaybackStateStore.trackFromJson("""{"title":"X","src":"s","style":"LIST"}""")
|
|
51
|
+
|
|
52
|
+
assertEquals("X", restored?.title)
|
|
53
|
+
assertNull(restored?.style)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -201,9 +201,9 @@ class RetryLoadErrorHandlingPolicyBudgetTest {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
/**
|
|
204
|
-
* Offline failures must not burn the first-connect budget (a station tapped in a tunnel parks
|
|
205
|
-
*
|
|
206
|
-
*
|
|
204
|
+
* Offline failures must not burn the first-connect budget (a station tapped in a tunnel parks for
|
|
205
|
+
* connectivity); the budget's clock starts at the first error observed online, so a dead station
|
|
206
|
+
* still gets a fast verdict after restoration.
|
|
207
207
|
*/
|
|
208
208
|
@Test
|
|
209
209
|
fun `offline time does not burn the first-connect budget`() {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package com.audiobrowser.player
|
|
2
|
+
|
|
3
|
+
import androidx.media3.common.C
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Assert.assertNotEquals
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Media3's `ShuffleOrder.cloneAndSet(insertionCount, startIndex)` discards the start index (the
|
|
11
|
+
* interface default clears and re-inserts from 0, and `DefaultShuffleOrder` does not override it),
|
|
12
|
+
* so `setMediaItems(items, startIndex, …)` leaves the starting track at a random shuffle position —
|
|
13
|
+
* last, for a 1-in-N share of queues, which reads as end-of-queue from the very first track.
|
|
14
|
+
*/
|
|
15
|
+
class ShuffleOrderLedByTest {
|
|
16
|
+
@Test
|
|
17
|
+
fun `the start index leads the order`() {
|
|
18
|
+
for (start in 0 until 6) {
|
|
19
|
+
val order = shuffleOrderLedBy(startIndex = start, count = 6)
|
|
20
|
+
assertEquals("start index must be at shuffle position 0", start, order.firstIndex)
|
|
21
|
+
assertNotEquals(
|
|
22
|
+
"a queue of 6 must have somewhere to advance to from its start track",
|
|
23
|
+
C.INDEX_UNSET.toLong(),
|
|
24
|
+
order.getNextIndex(start).toLong(),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun `every index appears exactly once`() {
|
|
31
|
+
val order = shuffleOrderLedBy(startIndex = 3, count = 8)
|
|
32
|
+
assertEquals(8, order.length)
|
|
33
|
+
val seen = mutableListOf<Int>()
|
|
34
|
+
var index = order.firstIndex
|
|
35
|
+
while (index != C.INDEX_UNSET) {
|
|
36
|
+
seen.add(index)
|
|
37
|
+
index = order.getNextIndex(index)
|
|
38
|
+
}
|
|
39
|
+
assertEquals((0 until 8).toList(), seen.sorted())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test
|
|
43
|
+
fun `a single-track queue leads with its only track and ends there`() {
|
|
44
|
+
val order = shuffleOrderLedBy(startIndex = 0, count = 1)
|
|
45
|
+
assertEquals(0, order.firstIndex)
|
|
46
|
+
assertEquals(C.INDEX_UNSET, order.getNextIndex(0))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Test
|
|
50
|
+
fun `an out-of-range start index is clamped rather than throwing`() {
|
|
51
|
+
assertEquals(4, shuffleOrderLedBy(startIndex = 99, count = 5).firstIndex)
|
|
52
|
+
assertEquals(0, shuffleOrderLedBy(startIndex = -3, count = 5).firstIndex)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun `an empty queue produces an empty order`() {
|
|
57
|
+
assertEquals(0, shuffleOrderLedBy(startIndex = 0, count = 0).length)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Test
|
|
61
|
+
fun `the tail is actually shuffled`() {
|
|
62
|
+
// Not a strict guarantee of any single draw — but across many draws an
|
|
63
|
+
// unshuffled tail would be impossible to miss.
|
|
64
|
+
val tails =
|
|
65
|
+
(0 until 50)
|
|
66
|
+
.map { shuffleOrderLedBy(startIndex = 0, count = 8) }
|
|
67
|
+
.map { order ->
|
|
68
|
+
buildList {
|
|
69
|
+
var index = order.getNextIndex(order.firstIndex)
|
|
70
|
+
while (index != C.INDEX_UNSET) {
|
|
71
|
+
add(index)
|
|
72
|
+
index = order.getNextIndex(index)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
.toSet()
|
|
77
|
+
assertTrue("50 draws produced one identical tail — the order is not shuffled", tails.size > 1)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -7,12 +7,98 @@ import java.security.cert.PKIXParameters
|
|
|
7
7
|
import java.security.cert.TrustAnchor
|
|
8
8
|
import java.security.cert.X509Certificate
|
|
9
9
|
import java.util.Date
|
|
10
|
+
import javax.security.auth.x500.X500Principal
|
|
10
11
|
import org.junit.Assert.assertEquals
|
|
11
12
|
import org.junit.Assert.assertThrows
|
|
12
13
|
import org.junit.Test
|
|
14
|
+
import org.mockito.Mockito.mock
|
|
15
|
+
import org.mockito.Mockito.`when`
|
|
13
16
|
|
|
14
17
|
class AiaCertChaserTest {
|
|
15
18
|
|
|
19
|
+
/**
|
|
20
|
+
* A certificate whose AIA extension value is exactly [extensionValue].
|
|
21
|
+
*
|
|
22
|
+
* Subject and issuer are stubbed to distinct principals on purpose: an unstubbed mock answers
|
|
23
|
+
* null to both, which `completeChain` would read as a self-signed root and stop on before
|
|
24
|
+
* fetching anything.
|
|
25
|
+
*/
|
|
26
|
+
private fun certWithAia(extensionValue: ByteArray): X509Certificate =
|
|
27
|
+
mock(X509Certificate::class.java).also {
|
|
28
|
+
`when`(it.getExtensionValue("1.3.6.1.5.5.7.1.1")).thenReturn(extensionValue)
|
|
29
|
+
`when`(it.subjectX500Principal).thenReturn(X500Principal("CN=leaf"))
|
|
30
|
+
`when`(it.issuerX500Principal).thenReturn(X500Principal("CN=issuer"))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A DER tag-length-value, with the length in whichever form fits. */
|
|
34
|
+
private fun der(tag: Int, content: ByteArray): ByteArray {
|
|
35
|
+
val length =
|
|
36
|
+
when (val n = content.size) {
|
|
37
|
+
in 0..0x7F -> byteArrayOf(n.toByte())
|
|
38
|
+
in 0x80..0xFF -> byteArrayOf(0x81.toByte(), n.toByte())
|
|
39
|
+
in 0x100..0xFFFF -> byteArrayOf(0x82.toByte(), (n shr 8).toByte(), n.toByte())
|
|
40
|
+
else -> byteArrayOf(0x83.toByte(), (n shr 16).toByte(), (n shr 8).toByte(), n.toByte())
|
|
41
|
+
}
|
|
42
|
+
return byteArrayOf(tag.toByte()) + length + content
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A well-formed AIA extension value listing [urls] as caIssuers locations. */
|
|
46
|
+
private fun aiaExtension(urls: List<String>): ByteArray {
|
|
47
|
+
val caIssuersOid = byteArrayOf(0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02)
|
|
48
|
+
val descriptions =
|
|
49
|
+
urls.fold(ByteArray(0)) { acc, url ->
|
|
50
|
+
acc + der(0x30, der(0x06, caIssuersOid) + der(0x86, url.toByteArray(Charsets.US_ASCII)))
|
|
51
|
+
}
|
|
52
|
+
return der(0x04, der(0x30, descriptions))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun `the AIA fixture builder round-trips through the parser`() {
|
|
57
|
+
// Guards the two tests below: if this builder were malformed they would pass by parsing
|
|
58
|
+
// nothing at all, rather than by the cap doing its job.
|
|
59
|
+
val urls = listOf("http://ca1.example/i.crt", "http://ca2.example/i.crt")
|
|
60
|
+
|
|
61
|
+
assertEquals(urls, AiaCertChaser.extractCaIssuerUrls(certWithAia(aiaExtension(urls))))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Test(timeout = 5_000)
|
|
65
|
+
fun `a negative long-form length does not hang the parser`() {
|
|
66
|
+
// OCTET STRING { SEQUENCE { tag 0x00, long-form length 0xFFFFFFFA = -6 } }.
|
|
67
|
+
// The length's four bytes shift into a negative Int, so `pos += len` rewinds the
|
|
68
|
+
// reader by exactly the six bytes it just consumed: the tag is not a SEQUENCE, the
|
|
69
|
+
// loop `continue`s, and the reader is back where it started with more input to read.
|
|
70
|
+
val aia =
|
|
71
|
+
byteArrayOf(
|
|
72
|
+
0x04,
|
|
73
|
+
0x08,
|
|
74
|
+
0x30,
|
|
75
|
+
0x06,
|
|
76
|
+
0x00,
|
|
77
|
+
0x84.toByte(),
|
|
78
|
+
0xFF.toByte(),
|
|
79
|
+
0xFF.toByte(),
|
|
80
|
+
0xFF.toByte(),
|
|
81
|
+
0xFA.toByte(),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
assertEquals(emptyList<String>(), AiaCertChaser.extractCaIssuerUrls(certWithAia(aia)))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Test(timeout = 5_000)
|
|
88
|
+
fun `a length running past the buffer is treated as no AIA`() {
|
|
89
|
+
// SEQUENCE claiming 0x7F content bytes inside a 2-byte buffer.
|
|
90
|
+
val aia = byteArrayOf(0x04, 0x04, 0x30, 0x02, 0x30, 0x7F)
|
|
91
|
+
|
|
92
|
+
assertEquals(emptyList<String>(), AiaCertChaser.extractCaIssuerUrls(certWithAia(aia)))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Test(timeout = 5_000)
|
|
96
|
+
fun `truncated DER is treated as no AIA`() {
|
|
97
|
+
val aia = byteArrayOf(0x04, 0x08, 0x30)
|
|
98
|
+
|
|
99
|
+
assertEquals(emptyList<String>(), AiaCertChaser.extractCaIssuerUrls(certWithAia(aia)))
|
|
100
|
+
}
|
|
101
|
+
|
|
16
102
|
@Test
|
|
17
103
|
fun `extracts CA Issuers URL from real leaf certificate AIA extension`() {
|
|
18
104
|
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
@@ -27,7 +113,7 @@ class AiaCertChaserTest {
|
|
|
27
113
|
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
28
114
|
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
29
115
|
// Fetcher knows only the R13 URL (the leaf's AIA pointer); empty for the root URL.
|
|
30
|
-
val fetch = { url: String ->
|
|
116
|
+
val fetch = { url: String, _: Long ->
|
|
31
117
|
if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
32
118
|
}
|
|
33
119
|
|
|
@@ -44,8 +130,8 @@ class AiaCertChaserTest {
|
|
|
44
130
|
|
|
45
131
|
// A PKCS#7-style bundle where the genuine issuer is not first must still be selected.
|
|
46
132
|
val completed =
|
|
47
|
-
AiaCertChaser.completeChain(listOf(leaf)) {
|
|
48
|
-
if (
|
|
133
|
+
AiaCertChaser.completeChain(listOf(leaf)) { url, _ ->
|
|
134
|
+
if (url == "http://r13.i.lencr.org/") listOf(unrelated, r13) else emptyList()
|
|
49
135
|
}
|
|
50
136
|
|
|
51
137
|
assertEquals(listOf(leaf, r13), completed)
|
|
@@ -57,7 +143,7 @@ class AiaCertChaserTest {
|
|
|
57
143
|
var fetches = 0
|
|
58
144
|
|
|
59
145
|
val completed =
|
|
60
|
-
AiaCertChaser.completeChain(listOf(root)) {
|
|
146
|
+
AiaCertChaser.completeChain(listOf(root)) { _, _ ->
|
|
61
147
|
fetches++
|
|
62
148
|
emptyList()
|
|
63
149
|
}
|
|
@@ -66,11 +152,115 @@ class AiaCertChaserTest {
|
|
|
66
152
|
assertEquals(0, fetches)
|
|
67
153
|
}
|
|
68
154
|
|
|
155
|
+
@Test
|
|
156
|
+
fun `completeChain stops at a chain that already reaches a trust anchor`() {
|
|
157
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
158
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
159
|
+
val root = CertFixtures.cert("isrg-root-x1.pem")
|
|
160
|
+
val fetched = mutableListOf<String>()
|
|
161
|
+
|
|
162
|
+
// R13's own AIA points at the ISRG root, so without knowing the anchors this walks one hop
|
|
163
|
+
// too far and pays for a second blocking fetch on the ordinary rescue path.
|
|
164
|
+
val completed =
|
|
165
|
+
AiaCertChaser.completeChain(
|
|
166
|
+
listOf(leaf),
|
|
167
|
+
anchorSubjects = setOf(root.subjectX500Principal),
|
|
168
|
+
) { url, _ ->
|
|
169
|
+
fetched.add(url)
|
|
170
|
+
if (url == "http://r13.i.lencr.org/") listOf(r13) else listOf(root)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
assertEquals(listOf(leaf, r13), completed)
|
|
174
|
+
assertEquals(listOf("http://r13.i.lencr.org/"), fetched)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@Test
|
|
178
|
+
fun `completeChain fetches nothing when the presented chain is already anchored`() {
|
|
179
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
180
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
181
|
+
val root = CertFixtures.cert("isrg-root-x1.pem")
|
|
182
|
+
var fetches = 0
|
|
183
|
+
|
|
184
|
+
// The shape of every non-path failure — expiry, hostname mismatch, CT — where the server
|
|
185
|
+
// sent a complete chain and validation failed for some other reason.
|
|
186
|
+
val completed =
|
|
187
|
+
AiaCertChaser.completeChain(
|
|
188
|
+
listOf(leaf, r13),
|
|
189
|
+
anchorSubjects = setOf(root.subjectX500Principal),
|
|
190
|
+
) { _, _ ->
|
|
191
|
+
fetches++
|
|
192
|
+
emptyList()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
assertEquals(listOf(leaf, r13), completed)
|
|
196
|
+
assertEquals(0, fetches)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@Test
|
|
200
|
+
fun `completeChain tries only a few of a certificate's CA-Issuers URLs`() {
|
|
201
|
+
// The extension is authored by the server that just failed validation, and nothing limits
|
|
202
|
+
// how many URLs it lists. Each one costs a blocking connect on the handshake thread, so
|
|
203
|
+
// hundreds of them are a denial of service whatever each individual fetch costs.
|
|
204
|
+
val manyUrls = (1..500).map { "http://ca$it.example/issuer.crt" }
|
|
205
|
+
val cert = certWithAia(aiaExtension(manyUrls))
|
|
206
|
+
val fetched = mutableListOf<String>()
|
|
207
|
+
|
|
208
|
+
AiaCertChaser.completeChain(listOf(cert)) { url, _ ->
|
|
209
|
+
fetched.add(url)
|
|
210
|
+
emptyList()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
assertEquals(AiaCertChaser.MAX_URLS_PER_CERT, fetched.size)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@Test
|
|
217
|
+
fun `duplicate CA-Issuers URLs do not consume the URL cap`() {
|
|
218
|
+
// 500 copies of one dead URL followed by a distinct one: the cap must buy distinct hosts,
|
|
219
|
+
// not three visits to the same dead one.
|
|
220
|
+
val urls = List(500) { "http://ca.example/issuer.crt" } + "http://ca-b.example/issuer.crt"
|
|
221
|
+
val cert = certWithAia(aiaExtension(urls))
|
|
222
|
+
val fetched = mutableListOf<String>()
|
|
223
|
+
|
|
224
|
+
AiaCertChaser.completeChain(listOf(cert)) { url, _ ->
|
|
225
|
+
fetched.add(url)
|
|
226
|
+
emptyList()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
assertEquals(listOf("http://ca.example/issuer.crt", "http://ca-b.example/issuer.crt"), fetched)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@Test
|
|
233
|
+
fun `completeChain stops fetching once its wall-clock budget is spent`() {
|
|
234
|
+
val cert = certWithAia(aiaExtension((1..500).map { "http://ca$it.example/issuer.crt" }))
|
|
235
|
+
var fetches = 0
|
|
236
|
+
var nanos = 0L
|
|
237
|
+
val deadlines = mutableListOf<Long>()
|
|
238
|
+
|
|
239
|
+
// Each fetch "takes" 8s against a 20s budget, so the walk must stop after a handful even
|
|
240
|
+
// though the URL cap alone would have allowed more.
|
|
241
|
+
AiaCertChaser.completeChain(
|
|
242
|
+
listOf(cert),
|
|
243
|
+
maxUrlsPerCert = 500,
|
|
244
|
+
budgetMs = 20_000,
|
|
245
|
+
nowNanos = { nanos },
|
|
246
|
+
) { _, deadline ->
|
|
247
|
+
fetches++
|
|
248
|
+
deadlines.add(deadline)
|
|
249
|
+
nanos += 8_000_000_000
|
|
250
|
+
emptyList()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
assertEquals(3, fetches)
|
|
254
|
+
// Every fetch is handed the same walk-wide deadline, so it can clip its own I/O to it —
|
|
255
|
+
// the budget bounds the fetches themselves, not just how many of them start.
|
|
256
|
+
assertEquals(listOf(20_000_000_000L, 20_000_000_000L, 20_000_000_000L), deadlines)
|
|
257
|
+
}
|
|
258
|
+
|
|
69
259
|
@Test
|
|
70
260
|
fun `completeChain stops when the issuer cannot be fetched`() {
|
|
71
261
|
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
72
262
|
|
|
73
|
-
val completed = AiaCertChaser.completeChain(listOf(leaf)) { emptyList() }
|
|
263
|
+
val completed = AiaCertChaser.completeChain(listOf(leaf)) { _, _ -> emptyList() }
|
|
74
264
|
|
|
75
265
|
assertEquals(listOf(leaf), completed)
|
|
76
266
|
}
|
|
@@ -88,8 +278,8 @@ class AiaCertChaserTest {
|
|
|
88
278
|
|
|
89
279
|
// After AIA chasing splices in R13, the chain validates to the trusted root.
|
|
90
280
|
val completed =
|
|
91
|
-
AiaCertChaser.completeChain(listOf(leaf)) {
|
|
92
|
-
if (
|
|
281
|
+
AiaCertChaser.completeChain(listOf(leaf)) { url, _ ->
|
|
282
|
+
if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
93
283
|
}
|
|
94
284
|
validatePath(completed, root, date) // must not throw
|
|
95
285
|
}
|