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
|
@@ -45,8 +45,15 @@ class QueueManager {
|
|
|
45
45
|
/// The repeat mode for the queue. Plain property — no side effects.
|
|
46
46
|
var repeatMode: RepeatMode = .off
|
|
47
47
|
|
|
48
|
-
/// Whether shuffle mode is enabled.
|
|
49
|
-
var shuffleEnabled: Bool = false
|
|
48
|
+
/// Whether shuffle mode is enabled.
|
|
49
|
+
var shuffleEnabled: Bool = false {
|
|
50
|
+
didSet {
|
|
51
|
+
// Re-pin on enable: the existing order has the current track wherever it happened to land,
|
|
52
|
+
// so without this, turning shuffle on near the end of the queue ends it right away.
|
|
53
|
+
guard shuffleEnabled, shuffleEnabled != oldValue else { return }
|
|
54
|
+
shuffleOrder.reshuffle(keepingFirst: currentIndex)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
50
57
|
|
|
51
58
|
// MARK: - Computed Properties
|
|
52
59
|
|
|
@@ -206,7 +213,7 @@ class QueueManager {
|
|
|
206
213
|
currentIndex = clampedIndex
|
|
207
214
|
queueSourcePath = sourcePath
|
|
208
215
|
tracks = newTracks
|
|
209
|
-
shuffleOrder = ShuffleOrder(length: newTracks.count)
|
|
216
|
+
shuffleOrder = ShuffleOrder(length: newTracks.count, firstIndex: clampedIndex)
|
|
210
217
|
return true
|
|
211
218
|
}
|
|
212
219
|
|
|
@@ -218,11 +225,8 @@ class QueueManager {
|
|
|
218
225
|
let wasEmpty = tracks.isEmpty
|
|
219
226
|
let insertIndex = tracks.count
|
|
220
227
|
tracks.append(contentsOf: newTracks)
|
|
228
|
+
if wasEmpty { return adoptInitialCurrent(initialIndex) }
|
|
221
229
|
shuffleOrder.insert(at: insertIndex, count: newTracks.count)
|
|
222
|
-
if wasEmpty {
|
|
223
|
-
currentIndex = max(0, min(initialIndex, tracks.count - 1))
|
|
224
|
-
return true
|
|
225
|
-
}
|
|
226
230
|
return false
|
|
227
231
|
}
|
|
228
232
|
|
|
@@ -243,14 +247,31 @@ class QueueManager {
|
|
|
243
247
|
currentIndex += newTracks.count
|
|
244
248
|
}
|
|
245
249
|
tracks.insert(contentsOf: newTracks, at: index)
|
|
250
|
+
if wasEmpty { return adoptInitialCurrent(0) }
|
|
246
251
|
shuffleOrder.insert(at: index, count: newTracks.count)
|
|
247
|
-
if wasEmpty {
|
|
248
|
-
currentIndex = 0
|
|
249
|
-
return true
|
|
250
|
-
}
|
|
251
252
|
return false
|
|
252
253
|
}
|
|
253
254
|
|
|
255
|
+
/// The queue was empty: `index` becomes the current track AND leads the
|
|
256
|
+
/// shuffle order. One operation because the two must not be separable —
|
|
257
|
+
/// forgetting the second half is the bug this exists to prevent.
|
|
258
|
+
///
|
|
259
|
+
/// `insert(at:count:)` drops new indices at *random* shuffle positions, so
|
|
260
|
+
/// without the pin the starting track can land last in the order, which reads
|
|
261
|
+
/// as `isLastInPlaybackOrder`: the queue "ends" (and fires
|
|
262
|
+
/// `playerDidEndQueue`) the moment the first track finishes. Pins
|
|
263
|
+
/// unconditionally, like `setQueue` — the order has to be sane whether or not
|
|
264
|
+
/// shuffle is on right now, since `shuffleEnabled`'s didSet only re-pins on
|
|
265
|
+
/// the off→on edge.
|
|
266
|
+
///
|
|
267
|
+
/// Returns true, so callers can `return adoptInitialCurrent(…)` to report the
|
|
268
|
+
/// current track changed.
|
|
269
|
+
private func adoptInitialCurrent(_ index: Int) -> Bool {
|
|
270
|
+
currentIndex = max(0, min(index, tracks.count - 1))
|
|
271
|
+
shuffleOrder = ShuffleOrder(length: tracks.count, firstIndex: currentIndex)
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
|
|
254
275
|
/// Remove a track from the queue.
|
|
255
276
|
/// Returns `true` if the current track changed.
|
|
256
277
|
@discardableResult
|
|
@@ -281,8 +302,18 @@ class QueueManager {
|
|
|
281
302
|
let insertion = min(newTracks.count, toIndex)
|
|
282
303
|
newTracks.insert(track, at: insertion)
|
|
283
304
|
tracks = newTracks
|
|
305
|
+
// Carry the moved track's shuffle position across the move. `insert(at:count:)`
|
|
306
|
+
// would drop it at a RANDOM position instead — reordering the queue would then
|
|
307
|
+
// silently reorder shuffled playback, and moving the playing track could land it
|
|
308
|
+
// last in the order, ending the queue at the next track boundary with tracks
|
|
309
|
+
// still unplayed (the failure `adoptInitialCurrent` guards against elsewhere).
|
|
310
|
+
// `remove` shifts every later position down by one, so re-inserting at the
|
|
311
|
+
// captured position restores the exact prior order.
|
|
312
|
+
let shufflePosition = shuffleOrder.shufflePosition(of: fromIndex)
|
|
284
313
|
shuffleOrder.remove(from: fromIndex, to: fromIndex + 1)
|
|
285
|
-
shuffleOrder.insert(
|
|
314
|
+
shuffleOrder.insert(
|
|
315
|
+
at: insertion, atShufflePosition: shufflePosition ?? shuffleOrder.count,
|
|
316
|
+
)
|
|
286
317
|
// The pointer follows the playing track; its identity never changes on a
|
|
287
318
|
// move, so no caller needs to reload (returning true reloads).
|
|
288
319
|
if currentIndex == fromIndex {
|
|
@@ -310,7 +341,9 @@ class QueueManager {
|
|
|
310
341
|
// MARK: - Other Mutations
|
|
311
342
|
|
|
312
343
|
/// Replace the track at a specific index.
|
|
313
|
-
|
|
344
|
+
/// Throws like every other index-taking mutation here, rather than trapping.
|
|
345
|
+
func replace(_ index: Int, _ track: Track) throws {
|
|
346
|
+
try throwIfIndexInvalid(index: index)
|
|
314
347
|
tracks[index] = track
|
|
315
348
|
}
|
|
316
349
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import AVFoundation
|
|
1
2
|
import Foundation
|
|
2
3
|
#if canImport(NitroModules)
|
|
3
4
|
import NitroModules
|
|
@@ -122,30 +123,90 @@ class RetryManager {
|
|
|
122
123
|
|
|
123
124
|
// MARK: - Error Classification
|
|
124
125
|
|
|
125
|
-
|
|
126
|
+
/// HTTP statuses worth another attempt, beyond the 500–599 range checked in
|
|
127
|
+
/// `isRetryable`. Android's `RetryLoadErrorHandlingPolicy` shares 408/429 but
|
|
128
|
+
/// enumerates only 500/502/503/504 of the 5xx range, so uncommon 5xx codes
|
|
129
|
+
/// (501, 505…) retry here and not there.
|
|
130
|
+
private static let retryableHTTPStatusCodes: Set<Int> = [408, 429]
|
|
131
|
+
|
|
132
|
+
/// AVFoundation failures a retry cannot fix: the media is unusable rather
|
|
133
|
+
/// than the transport unreliable. Everything not listed falls through to
|
|
134
|
+
/// retry — `AVErrorUnknown` especially, which is the wrapper AVFoundation
|
|
135
|
+
/// puts around opaque CoreMedia transport failures.
|
|
136
|
+
private static let fatalAVErrorCodes: Set<Int> = [
|
|
137
|
+
AVError.fileFormatNotRecognized.rawValue,
|
|
138
|
+
AVError.failedToParse.rawValue,
|
|
139
|
+
AVError.decodeFailed.rawValue,
|
|
140
|
+
AVError.contentIsProtected.rawValue,
|
|
141
|
+
AVError.contentIsNotAuthorized.rawValue,
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
/// Whether another attempt could plausibly succeed.
|
|
145
|
+
///
|
|
146
|
+
/// Mirrors Android's `classifyError`, including its default: an unrecognized
|
|
147
|
+
/// failure is treated as *transient* and retried, bounded by the retry
|
|
148
|
+
/// budgets, rather than being terminal. Defaulting the other way made
|
|
149
|
+
/// `retry: true` largely inert on iOS, because AVFoundation reports most
|
|
150
|
+
/// stream failures as opaque `CoreMediaErrorDomain` / `AVFoundationErrorDomain`
|
|
151
|
+
/// errors rather than `URLError`.
|
|
152
|
+
///
|
|
153
|
+
/// - Parameter httpStatusCode: The status from the item's error log, when the
|
|
154
|
+
/// caller has one. `AVPlayerItem.error` never carries it — a 404 arrives as
|
|
155
|
+
/// an opaque CoreMedia error — so this is the only reliable HTTP signal and
|
|
156
|
+
/// it outranks the error itself.
|
|
157
|
+
func isRetryable(_ error: Error?, httpStatusCode: Int?) -> Bool {
|
|
158
|
+
if let status = httpStatusCode {
|
|
159
|
+
let retryable = Self.retryableHTTPStatusCodes.contains(status) || (500 ... 599).contains(status)
|
|
160
|
+
logger.debug("HTTP \(status) \(retryable ? "is" : "not") retryable")
|
|
161
|
+
return retryable
|
|
162
|
+
}
|
|
163
|
+
|
|
126
164
|
guard let error else { return false }
|
|
127
165
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
// Walk the underlying-error chain: AVFoundation routinely wraps the real
|
|
167
|
+
// cause (a URLError, or a CoreMedia code) in an AVError.
|
|
168
|
+
for candidate in Self.errorChain(error) {
|
|
169
|
+
if let urlError = candidate as? URLError {
|
|
170
|
+
switch urlError.code {
|
|
171
|
+
// Transient network errors - safe to retry
|
|
172
|
+
case .timedOut,
|
|
173
|
+
.networkConnectionLost,
|
|
174
|
+
.notConnectedToInternet,
|
|
175
|
+
.cannotConnectToHost,
|
|
176
|
+
.cannotFindHost,
|
|
177
|
+
.dnsLookupFailed:
|
|
178
|
+
logger.debug("URLError is retryable: code=\(urlError.code.rawValue)")
|
|
179
|
+
return true
|
|
180
|
+
default:
|
|
181
|
+
logger.debug("URLError not retryable: code=\(urlError.code.rawValue)")
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let nsError = candidate as NSError
|
|
187
|
+
if nsError.domain == AVFoundationErrorDomain,
|
|
188
|
+
Self.fatalAVErrorCodes.contains(nsError.code)
|
|
189
|
+
{
|
|
190
|
+
logger.debug("AVError not retryable: code=\(nsError.code)")
|
|
141
191
|
return false
|
|
142
192
|
}
|
|
143
193
|
}
|
|
144
194
|
|
|
145
|
-
// Unknown errors - don't retry
|
|
146
195
|
let nsError = error as NSError
|
|
147
|
-
logger.debug("
|
|
148
|
-
return
|
|
196
|
+
logger.debug("Treating as transient: domain=\(nsError.domain), code=\(nsError.code)")
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// The error and its `NSUnderlyingError` ancestors, outermost first.
|
|
201
|
+
private static func errorChain(_ error: Error) -> [Error] {
|
|
202
|
+
var chain: [Error] = []
|
|
203
|
+
var current: Error? = error
|
|
204
|
+
// Bounded: a malformed cycle must not spin here.
|
|
205
|
+
while let error = current, chain.count < 8 {
|
|
206
|
+
chain.append(error)
|
|
207
|
+
current = (error as NSError).userInfo[NSUnderlyingErrorKey] as? Error
|
|
208
|
+
}
|
|
209
|
+
return chain
|
|
149
210
|
}
|
|
150
211
|
|
|
151
212
|
// MARK: - Retry Management
|
|
@@ -170,7 +231,8 @@ class RetryManager {
|
|
|
170
231
|
return "max retry duration (\(maxRetryDuration)s)"
|
|
171
232
|
}
|
|
172
233
|
if !hasPlayed, isOnline, let start = firstOnlineRetryTime,
|
|
173
|
-
now.timeIntervalSince(start) >= firstConnectMaxRetryDuration
|
|
234
|
+
now.timeIntervalSince(start) >= firstConnectMaxRetryDuration
|
|
235
|
+
{
|
|
174
236
|
return "first-connect retry duration (\(firstConnectMaxRetryDuration)s)"
|
|
175
237
|
}
|
|
176
238
|
return nil
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// Sendable so LoadSeekCoordinator can carry a delegate into AVPlayer's
|
|
2
|
+
// @Sendable seek completion closure; @MainActor is what keeps that sound.
|
|
3
|
+
@MainActor protocol SeekCompletionHandler: AnyObject, Sendable {
|
|
2
4
|
func handleSeekCompleted(to seconds: Double, didFinish: Bool)
|
|
3
5
|
}
|
|
@@ -39,6 +39,16 @@ struct ShuffleOrder {
|
|
|
39
39
|
indexInShuffled = Self.buildIndexInShuffled(from: shuffled)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/// Creates a shuffle order for the given length with `firstIndex` pinned to shuffle position 0.
|
|
43
|
+
///
|
|
44
|
+
/// Playback starts on that track, so leaving it wherever the permutation put it would end the
|
|
45
|
+
/// queue as soon as it is reached — on average after half the queue, and immediately whenever it
|
|
46
|
+
/// lands last.
|
|
47
|
+
init(length: Int, firstIndex: Int) {
|
|
48
|
+
self.init(length: length)
|
|
49
|
+
reshuffle(keepingFirst: firstIndex)
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
/// Creates a shuffle order with pre-defined indices
|
|
43
53
|
init(shuffledIndices: [Int]) {
|
|
44
54
|
shuffled = shuffledIndices
|
|
@@ -91,6 +101,13 @@ struct ShuffleOrder {
|
|
|
91
101
|
return indexInShuffled[index] == 0
|
|
92
102
|
}
|
|
93
103
|
|
|
104
|
+
/// The item's position in the shuffle order, or nil when it has none.
|
|
105
|
+
func shufflePosition(of index: Int) -> Int? {
|
|
106
|
+
guard index >= 0, index < indexInShuffled.count else { return nil }
|
|
107
|
+
let position = indexInShuffled[index]
|
|
108
|
+
return position >= 0 ? position : nil
|
|
109
|
+
}
|
|
110
|
+
|
|
94
111
|
// MARK: - Mutation
|
|
95
112
|
|
|
96
113
|
/// Inserts items at random positions in the shuffle order
|
|
@@ -115,6 +132,22 @@ struct ShuffleOrder {
|
|
|
115
132
|
indexInShuffled = Self.buildIndexInShuffled(from: shuffled)
|
|
116
133
|
}
|
|
117
134
|
|
|
135
|
+
/// Inserts one item at `insertionIndex`, placing it at an explicit shuffle
|
|
136
|
+
/// position rather than a random one.
|
|
137
|
+
///
|
|
138
|
+
/// Used by a queue `move`, which permutes positions but not identities: the
|
|
139
|
+
/// item already had a place in the playback order, and re-randomising it
|
|
140
|
+
/// silently reorders playback. For the *playing* track it can drop it at the
|
|
141
|
+
/// end of the order, which then reads as `isLastInPlaybackOrder` — the queue
|
|
142
|
+
/// reports itself over at the next track end, with tracks still unplayed.
|
|
143
|
+
mutating func insert(at insertionIndex: Int, atShufflePosition position: Int) {
|
|
144
|
+
for i in 0 ..< shuffled.count where shuffled[i] >= insertionIndex {
|
|
145
|
+
shuffled[i] += 1
|
|
146
|
+
}
|
|
147
|
+
shuffled.insert(insertionIndex, at: min(max(0, position), shuffled.count))
|
|
148
|
+
indexInShuffled = Self.buildIndexInShuffled(from: shuffled)
|
|
149
|
+
}
|
|
150
|
+
|
|
118
151
|
/// Removes items in the specified range from the shuffle order
|
|
119
152
|
mutating func remove(from indexFrom: Int, to indexToExclusive: Int) {
|
|
120
153
|
let removeCount = indexToExclusive - indexFrom
|
|
@@ -12,4 +12,17 @@ import Foundation
|
|
|
12
12
|
@discardableResult func clear() -> Bool
|
|
13
13
|
func onTrackChanged()
|
|
14
14
|
func onTrackPlayedToEnd()
|
|
15
|
+
|
|
16
|
+
// Consumer-facing surface — HybridAudioBrowser drives these through
|
|
17
|
+
// TrackPlayer, so they belong here rather than behind a concrete downcast.
|
|
18
|
+
|
|
19
|
+
/// Invoked when the timer state changes (set, cleared, completed).
|
|
20
|
+
var onChanged: ((SleepTimerState) -> Void)? { get set }
|
|
21
|
+
/// The current timer state, or nil when no timer is active.
|
|
22
|
+
func get() -> SleepTimerState?
|
|
23
|
+
/// Sets a time-based timer, optionally fading out over the last
|
|
24
|
+
/// `fadeDuration` seconds.
|
|
25
|
+
func set(seconds: TimeInterval, fadeDuration: TimeInterval?)
|
|
26
|
+
/// Sets the timer to stop playback when the current track ends.
|
|
27
|
+
func setToEndOfTrack()
|
|
15
28
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#if canImport(NitroModules)
|
|
2
|
+
import NitroModules
|
|
3
|
+
#endif
|
|
4
|
+
|
|
5
|
+
/// The callbacks `RemoteCommandController` invokes when the system delivers a
|
|
6
|
+
/// remote command (lock screen, Control Center, CarPlay, headset).
|
|
7
|
+
///
|
|
8
|
+
/// Split out of `TrackPlayerCallbacks` — which the controller does not otherwise
|
|
9
|
+
/// use — so the controller depends only on what it calls. `TrackPlayerCallbacks`
|
|
10
|
+
/// extends this protocol, so conformers implement both at once.
|
|
11
|
+
@MainActor protocol RemoteCommandCallbacks: AnyObject {
|
|
12
|
+
/// Called when play is triggered remotely.
|
|
13
|
+
func remotePlay()
|
|
14
|
+
|
|
15
|
+
/// Called when pause is triggered remotely.
|
|
16
|
+
func remotePause()
|
|
17
|
+
|
|
18
|
+
/// Called when stop is triggered remotely.
|
|
19
|
+
func remoteStop()
|
|
20
|
+
|
|
21
|
+
/// Called when toggle play/pause is triggered remotely.
|
|
22
|
+
func remotePlayPause()
|
|
23
|
+
|
|
24
|
+
/// Called when next is triggered remotely.
|
|
25
|
+
func remoteNext()
|
|
26
|
+
|
|
27
|
+
/// Called when previous is triggered remotely.
|
|
28
|
+
func remotePrevious()
|
|
29
|
+
|
|
30
|
+
/// Called when jump forward is triggered remotely.
|
|
31
|
+
func remoteJumpForward(interval: Double)
|
|
32
|
+
|
|
33
|
+
/// Called when jump backward is triggered remotely.
|
|
34
|
+
func remoteJumpBackward(interval: Double)
|
|
35
|
+
|
|
36
|
+
/// Called when seek is triggered remotely.
|
|
37
|
+
func remoteSeek(position: Double)
|
|
38
|
+
|
|
39
|
+
/// Called when repeat mode change is triggered remotely (CarPlay/lock screen).
|
|
40
|
+
func remoteChangeRepeatMode(mode: RepeatMode)
|
|
41
|
+
|
|
42
|
+
/// Called when shuffle mode change is triggered remotely (CarPlay/lock screen).
|
|
43
|
+
func remoteChangeShuffleMode(enabled: Bool)
|
|
44
|
+
|
|
45
|
+
/// Called when playback rate change is triggered remotely (CarPlay/lock screen).
|
|
46
|
+
func remoteChangePlaybackRate(rate: Float)
|
|
47
|
+
}
|
|
@@ -46,7 +46,7 @@ class RemoteCommandController {
|
|
|
46
46
|
private let logger = Logger(subsystem: "com.audiobrowser", category: "RemoteCommandController")
|
|
47
47
|
private var center: MPRemoteCommandCenter
|
|
48
48
|
|
|
49
|
-
weak var callbacks:
|
|
49
|
+
weak var callbacks: RemoteCommandCallbacks?
|
|
50
50
|
|
|
51
51
|
var commandTargetPointers: [String: Any] = [:]
|
|
52
52
|
private var enabledCommands: [RemoteCommand] = []
|
|
@@ -72,7 +72,7 @@ class RemoteCommandController {
|
|
|
72
72
|
*/
|
|
73
73
|
init(
|
|
74
74
|
remoteCommandCenter: MPRemoteCommandCenter = MPRemoteCommandCenter.shared(),
|
|
75
|
-
callbacks:
|
|
75
|
+
callbacks: RemoteCommandCallbacks? = nil,
|
|
76
76
|
) {
|
|
77
77
|
center = remoteCommandCenter
|
|
78
78
|
self.callbacks = callbacks
|
|
@@ -112,13 +112,16 @@ class RemoteCommandController {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
func enable(commands: [RemoteCommand]) {
|
|
115
|
-
let commandsToDisable =
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
let commandsToDisable = RemoteCommand.commandsToDisable(
|
|
116
|
+
enabled: enabledCommands, replacedBy: commands,
|
|
117
|
+
)
|
|
118
118
|
|
|
119
119
|
enabledCommands = commands
|
|
120
|
-
|
|
120
|
+
// Disable first. The key diff already makes the two sets disjoint, so this
|
|
121
|
+
// is belt-and-braces: it keeps a disable from tearing down a command the
|
|
122
|
+
// enable pass just configured should the two ever overlap again.
|
|
121
123
|
disable(commands: commandsToDisable)
|
|
124
|
+
commands.forEach { self.enable(command: $0) }
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
func disable(commands: [RemoteCommand]) {
|
|
@@ -278,24 +281,59 @@ class RemoteCommandController {
|
|
|
278
281
|
|
|
279
282
|
// MARK: - Handlers
|
|
280
283
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
lazy var
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
lazy var
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
lazy var
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
284
|
+
// Each default is wrapped in a `[weak self]` closure rather than assigned as a
|
|
285
|
+
// bare method reference (`= handlePlayCommandDefault`): an unapplied instance
|
|
286
|
+
// method curries to `{ [self] in self.method($0) }`, which strongly captures
|
|
287
|
+
// self. Stored back onto self, that is a permanent cycle — `enable(command:)`
|
|
288
|
+
// forces every lazy init, so the controller would never deallocate, leaking one
|
|
289
|
+
// per player instance across JS reloads and re-setups.
|
|
290
|
+
lazy var handlePlayCommand: RemoteCommandHandler = { [weak self] event in
|
|
291
|
+
self?.handlePlayCommandDefault(event: event) ?? .commandFailed
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
lazy var handlePauseCommand: RemoteCommandHandler = { [weak self] event in
|
|
295
|
+
self?.handlePauseCommandDefault(event: event) ?? .commandFailed
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
lazy var handleStopCommand: RemoteCommandHandler = { [weak self] event in
|
|
299
|
+
self?.handleStopCommandDefault(event: event) ?? .commandFailed
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
lazy var handleTogglePlayPauseCommand: RemoteCommandHandler = { [weak self] event in
|
|
303
|
+
self?.handleTogglePlayPauseCommandDefault(event: event) ?? .commandFailed
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
lazy var handleSkipForwardCommand: RemoteCommandHandler = { [weak self] event in
|
|
307
|
+
self?.handleSkipForwardCommandDefault(event: event) ?? .commandFailed
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
lazy var handleSkipBackwardCommand: RemoteCommandHandler = { [weak self] event in
|
|
311
|
+
self?.handleSkipBackwardDefault(event: event) ?? .commandFailed
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
lazy var handleChangePlaybackPositionCommand: RemoteCommandHandler = { [weak self] event in
|
|
315
|
+
self?.handleChangePlaybackPositionCommandDefault(event: event) ?? .commandFailed
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
lazy var handleNextTrackCommand: RemoteCommandHandler = { [weak self] event in
|
|
319
|
+
self?.handleNextTrackCommandDefault(event: event) ?? .commandFailed
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
lazy var handlePreviousTrackCommand: RemoteCommandHandler = { [weak self] event in
|
|
323
|
+
self?.handlePreviousTrackCommandDefault(event: event) ?? .commandFailed
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
lazy var handleChangeRepeatModeCommand: RemoteCommandHandler = { [weak self] event in
|
|
327
|
+
self?.handleChangeRepeatModeCommandDefault(event: event) ?? .commandFailed
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
lazy var handleChangeShuffleModeCommand: RemoteCommandHandler = { [weak self] event in
|
|
331
|
+
self?.handleChangeShuffleModeCommandDefault(event: event) ?? .commandFailed
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
lazy var handleChangePlaybackRateCommand: RemoteCommandHandler = { [weak self] event in
|
|
335
|
+
self?.handleChangePlaybackRateCommandDefault(event: event) ?? .commandFailed
|
|
336
|
+
}
|
|
299
337
|
|
|
300
338
|
private func handlePlayCommandDefault(event _: MPRemoteCommandEvent)
|
|
301
339
|
-> MPRemoteCommandHandlerStatus
|
|
@@ -74,6 +74,56 @@ import Testing
|
|
|
74
74
|
#expect(result == "/items?a=1&m=2&z=3")
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// MARK: - build
|
|
78
|
+
|
|
79
|
+
@Test func buildAppendsTrackIdWithQuestionMark() {
|
|
80
|
+
let result = BrowserPathHelper.build(parentPath: "/library", trackId: "song.mp3")
|
|
81
|
+
#expect(result == "/library?__trackId=song.mp3")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test func buildAppendsTrackIdWithAmpersandWhenParentHasQuery() {
|
|
85
|
+
let result = BrowserPathHelper.build(parentPath: "/search?q=jazz", trackId: "song.mp3")
|
|
86
|
+
#expect(result == "/search?q=jazz&__trackId=song.mp3")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Test func buildRoundTripsSrcWithQueryParams() {
|
|
90
|
+
// A src carrying its own query string (signed CDN URL) must survive the
|
|
91
|
+
// build → extract/strip round-trip: an unescaped `&` splits the src into
|
|
92
|
+
// stray query params, truncating the trackId and polluting the parent path.
|
|
93
|
+
let src = "https://cdn.example.com/stream.mp3?token=abc&exp=1699999999"
|
|
94
|
+
let url = BrowserPathHelper.build(parentPath: "/library", trackId: src)
|
|
95
|
+
#expect(BrowserPathHelper.extractTrackId(url) == src)
|
|
96
|
+
#expect(BrowserPathHelper.stripTrackId(url) == "/library")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Test func buildRoundTripsSrcWithEqualsAndPlus() {
|
|
100
|
+
let src = "https://cdn.example.com/a+b.mp3?sig=x=y"
|
|
101
|
+
let url = BrowserPathHelper.build(parentPath: "/library", trackId: src)
|
|
102
|
+
#expect(BrowserPathHelper.extractTrackId(url) == src)
|
|
103
|
+
#expect(BrowserPathHelper.stripTrackId(url) == "/library")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Test func buildStampsTheIndexAndRoundTripsIt() {
|
|
107
|
+
let src = "https://cdn.example.com/stream.mp3?token=abc&exp=1"
|
|
108
|
+
let url = BrowserPathHelper.build(parentPath: "/library", trackId: src, index: 3)
|
|
109
|
+
#expect(BrowserPathHelper.extractTrackId(url) == src)
|
|
110
|
+
#expect(BrowserPathHelper.extractIndex(url) == 3)
|
|
111
|
+
#expect(BrowserPathHelper.stripTrackId(url) == "/library")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// MARK: - extractIndex
|
|
115
|
+
|
|
116
|
+
@Test func extractIndexReturnsNilWithoutAStampedIndex() {
|
|
117
|
+
let url = BrowserPathHelper.build(parentPath: "/library", trackId: "song.mp3")
|
|
118
|
+
#expect(BrowserPathHelper.extractIndex(url) == nil)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Test func extractIndexReturnsNilForNonContextualOrMalformedIndex() {
|
|
122
|
+
#expect(BrowserPathHelper.extractIndex("/library") == nil)
|
|
123
|
+
#expect(BrowserPathHelper.extractIndex("/library?__trackId=song.mp3&__index=x") == nil)
|
|
124
|
+
#expect(BrowserPathHelper.extractIndex("/library?__trackId=song.mp3&__index=-1") == nil)
|
|
125
|
+
}
|
|
126
|
+
|
|
77
127
|
// MARK: - stripTrackId
|
|
78
128
|
|
|
79
129
|
@Test func stripTrackIdRemovesTrackParam() {
|
|
@@ -116,23 +166,3 @@ import Testing
|
|
|
116
166
|
@Test func isContextualReturnsTrueWithAmpersandSeparator() {
|
|
117
167
|
#expect(BrowserPathHelper.isContextual("/lib?q=1&__trackId=x") == true)
|
|
118
168
|
}
|
|
119
|
-
|
|
120
|
-
// MARK: - containsSegment
|
|
121
|
-
|
|
122
|
-
@Test func containsSegmentMatchesFullSegments() {
|
|
123
|
-
let path = "/listen/amsterdam-funk-channel/Gw0LGB8j"
|
|
124
|
-
#expect(BrowserPathHelper.containsSegment(path, "Gw0LGB8j"))
|
|
125
|
-
#expect(BrowserPathHelper.containsSegment(path, "amsterdam-funk-channel"))
|
|
126
|
-
#expect(BrowserPathHelper.containsSegment(path, "listen"))
|
|
127
|
-
#expect(BrowserPathHelper.containsSegment("/listen/x/Gw0LGB8j?hl=en", "Gw0LGB8j"))
|
|
128
|
-
#expect(BrowserPathHelper.containsSegment("/listen/x/Gw0LGB8j#frag", "Gw0LGB8j"))
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
@Test func containsSegmentRejectsPartialAndMissing() {
|
|
132
|
-
let path = "/listen/amsterdam-funk-channel/Gw0LGB8j"
|
|
133
|
-
#expect(!BrowserPathHelper.containsSegment(path, "Gw0LGB8"))
|
|
134
|
-
#expect(!BrowserPathHelper.containsSegment(path, "funk"))
|
|
135
|
-
#expect(!BrowserPathHelper.containsSegment(path, "msterdam-funk-channe"))
|
|
136
|
-
#expect(!BrowserPathHelper.containsSegment(path, "NopeNope"))
|
|
137
|
-
#expect(!BrowserPathHelper.containsSegment(path, ""))
|
|
138
|
-
}
|