react-native-audio-browser 0.2.0-next.2 → 0.2.0-next.20
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
|
@@ -16,6 +16,10 @@ import os.log
|
|
|
16
16
|
/// Only safe when the caller blocks until the MainActor work completes (DispatchQueue.main.sync).
|
|
17
17
|
private struct UncheckedSendableBox<T>: @unchecked Sendable { let value: T }
|
|
18
18
|
|
|
19
|
+
/// `@unchecked Sendable` because it must be: the spec's requirements are all
|
|
20
|
+
/// nonisolated and Nitro calls them synchronously on the JS thread, so the class
|
|
21
|
+
/// can't be `@MainActor`. Isolation is therefore per-property, and `@unchecked`
|
|
22
|
+
/// means the compiler won't tell you when a new one needs it.
|
|
19
23
|
public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
20
24
|
private let logger = Logger(subsystem: "com.audiobrowser", category: "AudioBrowser")
|
|
21
25
|
|
|
@@ -30,8 +34,28 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
30
34
|
/// silent.
|
|
31
35
|
static let instanceChangedEmitter = Emitter<Void>()
|
|
32
36
|
|
|
37
|
+
/// Installed once, on first instance creation. React Native posts this
|
|
38
|
+
/// notification (from `RCTReloadCommand`) when a JS reload — dev or
|
|
39
|
+
/// OTA-update restart — is triggered, *before* the old runtime is destroyed:
|
|
40
|
+
/// the earliest signal that this instance's JS callbacks are about to go
|
|
41
|
+
/// stale. `init` of the replacement instance drops them too, but only after
|
|
42
|
+
/// the new runtime is up; this observer closes the window in between.
|
|
43
|
+
/// Observed by name so the library needs no React-Core dependency (and
|
|
44
|
+
/// stays inert under SPM tests, where nothing posts it).
|
|
45
|
+
@MainActor private static let reloadObserver: NSObjectProtocol = NotificationCenter.default.addObserver(
|
|
46
|
+
forName: Notification.Name("RCTTriggerReloadCommandNotification"),
|
|
47
|
+
object: nil,
|
|
48
|
+
queue: .main,
|
|
49
|
+
) { _ in
|
|
50
|
+
MainActor.assumeIsolated {
|
|
51
|
+
HybridAudioBrowser.shared?.dropStaleJSCallbacks()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
// MARK: - Private Properties
|
|
34
56
|
|
|
57
|
+
/// Main-confined by convention, not annotation: `deinit` reads it as a
|
|
58
|
+
/// safety net and can't hop. Reach it from `onMainActor` / `@MainActor`.
|
|
35
59
|
private var player: TrackPlayer?
|
|
36
60
|
private let networkMonitor = NetworkMonitor()
|
|
37
61
|
private let playbackStateStore = PlaybackStateStore()
|
|
@@ -43,27 +67,35 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
43
67
|
private var mediaServicesResetObserver: NSObjectProtocol?
|
|
44
68
|
/// Re-applies our resolved audio-session category. Captured at setup so a
|
|
45
69
|
/// media-services reset (which clears the category) can restore it.
|
|
46
|
-
private var applyAudioSessionCategory: () -> Void = {}
|
|
47
|
-
private var nowPlayingOverride: NowPlayingUpdate?
|
|
70
|
+
@MainActor private var applyAudioSessionCategory: () -> Void = {}
|
|
71
|
+
@MainActor private var nowPlayingOverride: NowPlayingUpdate?
|
|
48
72
|
/// When false, the now-playing surface uses the raw track fields (override + formatter ignored).
|
|
49
|
-
private var nowPlayingMetadataEnabled = true
|
|
73
|
+
@MainActor private var nowPlayingMetadataEnabled = true
|
|
50
74
|
/// Customizes the now-playing title/subtitle from the track + live timed metadata + playback state.
|
|
51
|
-
private var nowPlayingMetadataFormatter: ((_ params: FormatNowPlayingParams) -> Promise<NowPlayingUpdate?>)?
|
|
52
|
-
/// Keep the media session controllable through a terminal error (see `keepSessionAliveOnError`).
|
|
53
|
-
private var keepSessionAliveOnError = true
|
|
75
|
+
@MainActor private var nowPlayingMetadataFormatter: ((_ params: FormatNowPlayingParams) -> Promise<NowPlayingUpdate?>)?
|
|
54
76
|
/// Initial player state staged before the player exists — from setup options or the imperative
|
|
55
77
|
/// setters called pre-setup. Strict last-write-wins; consumed when the player comes up.
|
|
56
|
-
private var pendingPlayWhenReady: Bool?
|
|
57
|
-
private var pendingRepeatMode: RepeatMode?
|
|
78
|
+
@MainActor private var pendingPlayWhenReady: Bool?
|
|
79
|
+
@MainActor private var pendingRepeatMode: RepeatMode?
|
|
58
80
|
/// Latest live timed (ICY/ID3) metadata, passed to the formatter. Cleared on track change.
|
|
59
|
-
private var latestTimedMetadata: TimedMetadata?
|
|
60
|
-
private let playerOptions = PlayerUpdateOptions()
|
|
81
|
+
@MainActor private var latestTimedMetadata: TimedMetadata?
|
|
82
|
+
@MainActor private let playerOptions = PlayerUpdateOptions()
|
|
61
83
|
|
|
62
84
|
/// Configured playback rates for the playback-rate capability (for CarPlay rate cycling)
|
|
63
|
-
var playbackRates: [Double] { playerOptions.playbackRates }
|
|
64
|
-
var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
|
|
65
|
-
var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] {
|
|
66
|
-
|
|
85
|
+
@MainActor var playbackRates: [Double] { playerOptions.playbackRates }
|
|
86
|
+
@MainActor var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
|
|
87
|
+
@MainActor var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] {
|
|
88
|
+
playerOptions.carPlayNowPlayingButtons
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@MainActor var carPlayPlayingIndicatorLocation: CarPlayPlayingIndicatorLocation {
|
|
92
|
+
playerOptions.carPlayPlayingIndicatorLocation
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Written from the JS thread, the cooperative pool and MainActor, with a
|
|
96
|
+
/// `didSet` that calls back into Nitro. MainActor-isolated so the compiler
|
|
97
|
+
/// requires an `onMainActor` hop at every access.
|
|
98
|
+
@MainActor private var lastNavigationError: NavigationError? {
|
|
67
99
|
didSet {
|
|
68
100
|
// Skip if both nil (no real change)
|
|
69
101
|
guard oldValue != nil || lastNavigationError != nil else { return }
|
|
@@ -71,7 +103,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
71
103
|
}
|
|
72
104
|
}
|
|
73
105
|
|
|
74
|
-
|
|
106
|
+
/// Isolated for the same reason as ``lastNavigationError``.
|
|
107
|
+
@MainActor private var lastFormattedNavigationError: FormattedNavigationError? {
|
|
75
108
|
didSet {
|
|
76
109
|
// Skip if both nil (no real change)
|
|
77
110
|
guard oldValue != nil || lastFormattedNavigationError != nil else { return }
|
|
@@ -153,9 +186,14 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
153
186
|
public var configuration: NativeBrowserConfiguration = .init(
|
|
154
187
|
path: nil, request: nil, requestResolver: nil, browse: nil, browseResolver: nil, media: nil, artwork: nil, nowPlayingArtwork: nil, routes: nil,
|
|
155
188
|
singleTrack: nil, handleTrackLoad: nil,
|
|
156
|
-
androidControllerOfflineError: nil,
|
|
189
|
+
androidControllerOfflineError: nil, viewMoreTitle: nil, carPlayLoadingTitle: nil, resolveAlbumPath: nil,
|
|
190
|
+
formatNavigationError: nil,
|
|
157
191
|
) {
|
|
158
192
|
didSet {
|
|
193
|
+
// Copied here, on the setter's thread: the Task below outlives this call,
|
|
194
|
+
// and Nitro can reassign `configuration` — a C++ struct holding
|
|
195
|
+
// std::string/std::function — from the JS thread while it runs.
|
|
196
|
+
let configuredPath = configuration.path
|
|
159
197
|
onMainActor { browserManager.config = BrowserConfig(from: configuration) }
|
|
160
198
|
|
|
161
199
|
// Query tabs and navigate to initial path after config is set (matches Kotlin behavior)
|
|
@@ -166,12 +204,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
166
204
|
Task {
|
|
167
205
|
let tabs = try? await browserManager.queryTabs()
|
|
168
206
|
// Navigate to configured path, first tab, or "/"
|
|
169
|
-
let initialPath =
|
|
207
|
+
let initialPath = configuredPath ?? tabs?.first?.path ?? "/"
|
|
170
208
|
// Clear error before navigation (matches Kotlin clearNavigationError())
|
|
171
|
-
|
|
172
|
-
lastNavigationError = nil
|
|
173
|
-
lastFormattedNavigationError = nil
|
|
174
|
-
}
|
|
209
|
+
clearNavigationError()
|
|
175
210
|
do {
|
|
176
211
|
try await browserManager.navigate(initialPath)
|
|
177
212
|
} catch {
|
|
@@ -201,8 +236,14 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
201
236
|
/// state, e.g. CarPlay re-seeding `isGated` at scene connect). Serving content
|
|
202
237
|
/// in that window would be a fail-open leak — same direction as the
|
|
203
238
|
/// resolver-error path in `gateDecision`. Matches Android's default.
|
|
204
|
-
public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> =
|
|
205
|
-
|
|
239
|
+
public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = HybridAudioBrowser.defaultResolveGate
|
|
240
|
+
|
|
241
|
+
// Computed, not stored: a nonisolated static holding a non-Sendable closure
|
|
242
|
+
// is rejected under Swift 6 concurrency checking.
|
|
243
|
+
static var defaultResolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> {
|
|
244
|
+
{ _ in
|
|
245
|
+
Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
|
|
246
|
+
}
|
|
206
247
|
}
|
|
207
248
|
|
|
208
249
|
/// Fired when a request is gated (served the gate). Set by JS.
|
|
@@ -294,7 +335,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
294
335
|
// playerAndConfiguredBrowser OnceValue, so we must explicitly stop
|
|
295
336
|
// its player to prevent two audio streams running simultaneously.
|
|
296
337
|
onMainActor {
|
|
338
|
+
// Statics are lazy — touch the observer so it's installed from the first
|
|
339
|
+
// instance onward.
|
|
340
|
+
_ = HybridAudioBrowser.reloadObserver
|
|
297
341
|
HybridAudioBrowser.shared?.player?.destroy()
|
|
342
|
+
HybridAudioBrowser.shared?.dropStaleJSCallbacks()
|
|
298
343
|
playerAndConfiguredBrowser.reset()
|
|
299
344
|
HybridAudioBrowser.shared = self
|
|
300
345
|
// Tell connected external controllers (CarPlay) to re-subscribe against
|
|
@@ -307,7 +352,52 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
307
352
|
setupBrowserCallbacks()
|
|
308
353
|
}
|
|
309
354
|
|
|
355
|
+
/// Drops every JS-provided callback that native code can invoke on its own
|
|
356
|
+
/// initiative (browse resolvers, `handleTrackLoad`, `resolveAlbumPath`,
|
|
357
|
+
/// `formatNavigationError`, `resolveGate`, the now-playing formatter).
|
|
358
|
+
///
|
|
359
|
+
/// Called on the *previous* instance when a new one takes over: its JS runtime
|
|
360
|
+
/// is gone, and invoking a callback from a dead runtime aborts the process —
|
|
361
|
+
/// Nitro's `AsyncJSCallback` throws on a destroyed Dispatcher, and the throw
|
|
362
|
+
/// crosses the generated bridge wrapper's `noexcept` boundary. Native code
|
|
363
|
+
/// (e.g. a CarPlay controller still bound to this instance mid-reload) keeps
|
|
364
|
+
/// working off the data-only config with built-in fallbacks; `resolveGate`
|
|
365
|
+
/// reverts to the fail-closed default it documents for exactly this window.
|
|
366
|
+
/// One-shot: on a normal reload both triggers hit the same dying instance
|
|
367
|
+
/// (the reload notification, then the replacement's `init`), and the second
|
|
368
|
+
/// strip would re-fire the config-setter cascade — another `onConfigChanged`
|
|
369
|
+
/// → CarPlay now-playing button rebuild — for nothing.
|
|
370
|
+
@MainActor private var didDropJSCallbacks = false
|
|
371
|
+
|
|
372
|
+
@MainActor
|
|
373
|
+
private func dropStaleJSCallbacks() {
|
|
374
|
+
guard !didDropJSCallbacks else { return }
|
|
375
|
+
didDropJSCallbacks = true
|
|
376
|
+
// Guarded: assigning `config` marks the browser configured (the setter's
|
|
377
|
+
// didSet), and a never-configured browser must keep cold-start waiters
|
|
378
|
+
// (CarPlay, Siri) blocked on the readiness gate rather than resolve them
|
|
379
|
+
// against an empty config.
|
|
380
|
+
if browserManager.isConfigured {
|
|
381
|
+
browserManager.config = browserManager.config.strippingJSCallbacks()
|
|
382
|
+
}
|
|
383
|
+
resolveGate = Self.defaultResolveGate
|
|
384
|
+
nowPlayingMetadataFormatter = nil
|
|
385
|
+
}
|
|
386
|
+
|
|
310
387
|
deinit {
|
|
388
|
+
// Block-based notification observers are retained by the center until an
|
|
389
|
+
// explicit removeObserver — observer death isn't detected — so without this
|
|
390
|
+
// every JS reload strands another token + closure. They capture `self`
|
|
391
|
+
// weakly, so a stranded one no-ops rather than misbehaving; what accumulates
|
|
392
|
+
// is the tokens and a wasted main-queue dispatch per interruption / route
|
|
393
|
+
// change / media-services reset. (`volumeObservation` needs no counterpart:
|
|
394
|
+
// NSKeyValueObservation invalidates itself when this instance releases it.)
|
|
395
|
+
for observer in [interruptionObserver, routeChangeObserver, mediaServicesResetObserver]
|
|
396
|
+
.compactMap(\.self)
|
|
397
|
+
{
|
|
398
|
+
NotificationCenter.default.removeObserver(observer)
|
|
399
|
+
}
|
|
400
|
+
|
|
311
401
|
// Safety net: ensure the AVPlayer is stopped if this instance is deallocated.
|
|
312
402
|
let player = self.player
|
|
313
403
|
if Thread.isMainThread {
|
|
@@ -370,7 +460,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
370
460
|
private func handleNavigationError(_ error: Error, path: String) {
|
|
371
461
|
let navError = NavigationError.from(error)
|
|
372
462
|
|
|
373
|
-
lastNavigationError = navError
|
|
463
|
+
onMainActor { lastNavigationError = navError }
|
|
374
464
|
|
|
375
465
|
// Format the error (async if using JS callback, sync for defaults)
|
|
376
466
|
let defaultFormatted = navError.defaultFormatted()
|
|
@@ -380,27 +470,40 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
380
470
|
// The result callbacks are @Sendable: a plain closure would inherit MainActor
|
|
381
471
|
// isolation from this block, and Nitro resolves its promises synchronously on the
|
|
382
472
|
// JS thread — Swift 6.2's dynamic isolation check then traps on closure entry.
|
|
383
|
-
//
|
|
473
|
+
// They therefore hop back explicitly, like every other writer.
|
|
384
474
|
DispatchQueue.main.async { [weak self] in
|
|
385
475
|
formatter(params)
|
|
386
476
|
.then { @Sendable [weak self] customDisplay in
|
|
387
|
-
self?.
|
|
477
|
+
self?.setFormattedNavigationError(customDisplay ?? defaultFormatted)
|
|
388
478
|
}
|
|
389
479
|
.catch { @Sendable [weak self] _ in
|
|
390
|
-
self?.
|
|
480
|
+
self?.setFormattedNavigationError(defaultFormatted)
|
|
391
481
|
}
|
|
392
482
|
}
|
|
393
483
|
} else {
|
|
394
|
-
|
|
484
|
+
setFormattedNavigationError(defaultFormatted)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/// Callable from any thread — the Nitro promise callbacks below resolve on
|
|
489
|
+
/// the JS thread.
|
|
490
|
+
private func setFormattedNavigationError(_ formatted: FormattedNavigationError?) {
|
|
491
|
+
onMainActor { lastFormattedNavigationError = formatted }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/// Synchronous on purpose: the `didSet`s notify JS, which has to happen
|
|
495
|
+
/// before the navigation that may set them again.
|
|
496
|
+
private func clearNavigationError() {
|
|
497
|
+
onMainActor {
|
|
498
|
+
lastNavigationError = nil
|
|
499
|
+
lastFormattedNavigationError = nil
|
|
395
500
|
}
|
|
396
501
|
}
|
|
397
502
|
|
|
398
503
|
// MARK: - Browser Methods
|
|
399
504
|
|
|
400
505
|
public func navigatePath(path: String) throws {
|
|
401
|
-
|
|
402
|
-
lastNavigationError = nil
|
|
403
|
-
lastFormattedNavigationError = nil
|
|
506
|
+
clearNavigationError()
|
|
404
507
|
Task {
|
|
405
508
|
do {
|
|
406
509
|
try await browserManager.navigate(path)
|
|
@@ -411,7 +514,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
411
514
|
}
|
|
412
515
|
|
|
413
516
|
public func navigateTrack(track: Track) throws {
|
|
414
|
-
|
|
517
|
+
// @MainActor: `player` is main-confined, and `trackSelector.select` is
|
|
518
|
+
// MainActor anyway — a bare Task read the reference off the cooperative
|
|
519
|
+
// pool, racing the assignment in `setupPlayer`.
|
|
520
|
+
Task { @MainActor in
|
|
415
521
|
guard let player else { return }
|
|
416
522
|
let result = await trackSelector.select(track: track, player: player)
|
|
417
523
|
switch result {
|
|
@@ -419,8 +525,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
419
525
|
executePlayback(intent)
|
|
420
526
|
case .intercepted:
|
|
421
527
|
break
|
|
422
|
-
case let .browse(
|
|
423
|
-
|
|
528
|
+
case let .browse(path):
|
|
529
|
+
navigateToBrowsablePath(path)
|
|
424
530
|
case .none:
|
|
425
531
|
break
|
|
426
532
|
}
|
|
@@ -444,24 +550,22 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
444
550
|
}
|
|
445
551
|
}
|
|
446
552
|
|
|
447
|
-
private func
|
|
448
|
-
|
|
449
|
-
lastNavigationError = nil
|
|
450
|
-
lastFormattedNavigationError = nil
|
|
553
|
+
private func navigateToBrowsablePath(_ path: String) {
|
|
554
|
+
clearNavigationError()
|
|
451
555
|
Task {
|
|
452
556
|
do {
|
|
453
|
-
try await browserManager.navigate(
|
|
557
|
+
try await browserManager.navigate(path)
|
|
454
558
|
} catch {
|
|
455
|
-
handleNavigationError(error, path:
|
|
559
|
+
handleNavigationError(error, path: path)
|
|
456
560
|
}
|
|
457
561
|
}
|
|
458
562
|
}
|
|
459
563
|
|
|
460
|
-
public func
|
|
564
|
+
public func search(query: String) throws -> Promise<[Track]> {
|
|
461
565
|
Promise.async { [weak self] in
|
|
462
566
|
guard let self else { return [] }
|
|
463
567
|
let resolved = try await browserManager.search(query)
|
|
464
|
-
return resolved.
|
|
568
|
+
return resolved.flattenedChildren ?? []
|
|
465
569
|
}
|
|
466
570
|
}
|
|
467
571
|
|
|
@@ -470,11 +574,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
470
574
|
}
|
|
471
575
|
|
|
472
576
|
public func getNavigationError() throws -> NavigationError? {
|
|
473
|
-
lastNavigationError
|
|
577
|
+
onMainActor { lastNavigationError }
|
|
474
578
|
}
|
|
475
579
|
|
|
476
580
|
public func getFormattedNavigationError() throws -> FormattedNavigationError? {
|
|
477
|
-
lastFormattedNavigationError
|
|
581
|
+
onMainActor { lastFormattedNavigationError }
|
|
478
582
|
}
|
|
479
583
|
|
|
480
584
|
/// Internal signal sent on `externalContentChangedEmitter` to tell CarPlay to
|
|
@@ -484,7 +588,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
484
588
|
static let invalidateAllSentinel = "__rnab_invalidate_all__"
|
|
485
589
|
|
|
486
590
|
public func notifyContentChanged(path: String) throws {
|
|
487
|
-
|
|
591
|
+
// Throws on a contextual path (matching Android's `require`) — the caller
|
|
592
|
+
// must pass the container path, or the invalidation silently hits nothing.
|
|
593
|
+
try onMainActor { try browserManager.invalidateContentCache(path) }
|
|
488
594
|
|
|
489
595
|
// Notify external controllers (CarPlay) that content changed
|
|
490
596
|
externalContentChangedEmitter.emit(path)
|
|
@@ -601,7 +707,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
601
707
|
// built-in so the gate page still renders rather than leaking content.
|
|
602
708
|
return GateOutcome(gated: true, chrome: defaultChrome ?? Self.builtInGate)
|
|
603
709
|
}
|
|
604
|
-
guard decision.gated else { return GateOutcome(gated: false, chrome: nil) }
|
|
710
|
+
guard decision.gated else { return GateOutcome(gated: false, chrome: nil) } // explicit allow
|
|
605
711
|
return GateOutcome(gated: true, chrome: decision.gate ?? defaultChrome ?? Self.builtInGate)
|
|
606
712
|
}
|
|
607
713
|
|
|
@@ -649,7 +755,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
649
755
|
category: AVAudioSession.Category,
|
|
650
756
|
mode: AVAudioSession.Mode,
|
|
651
757
|
policy: AVAudioSession.RouteSharingPolicy,
|
|
652
|
-
options: AVAudioSession.CategoryOptions
|
|
758
|
+
options: AVAudioSession.CategoryOptions,
|
|
653
759
|
) = options.ios?.resolveAudioSessionConfig()
|
|
654
760
|
?? (category: .playback, mode: .default, policy: .default, options: [])
|
|
655
761
|
// Capture category application once so a media-services reset (which
|
|
@@ -664,6 +770,16 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
664
770
|
guard let self else { return }
|
|
665
771
|
self.applyAudioSessionCategory = applyCategory
|
|
666
772
|
|
|
773
|
+
// A re-setup REPLACES the player, so the outgoing one has to be torn down
|
|
774
|
+
// and un-published first. `TrackPlayer` has no deinit: dropping the
|
|
775
|
+
// reference alone would leave its AVPlayer and MPNowPlayingSession alive.
|
|
776
|
+
// And `playerAndConfiguredBrowser` holds the old player strongly — its
|
|
777
|
+
// `resolve` is a one-shot, so without the reset the gate would keep
|
|
778
|
+
// handing the dead player to the CarPlay controller and Siri intents,
|
|
779
|
+
// which both await it. No-op on first setup.
|
|
780
|
+
player?.destroy()
|
|
781
|
+
playerAndConfiguredBrowser.reset()
|
|
782
|
+
|
|
667
783
|
// Create player with self as callbacks delegate
|
|
668
784
|
player = TrackPlayer(callbacks: self)
|
|
669
785
|
|
|
@@ -713,11 +829,16 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
713
829
|
return await browserManager.resolveMediaUrl(src, track: track)
|
|
714
830
|
}
|
|
715
831
|
|
|
716
|
-
// Configure artwork URL resolver for Now Playing (with size context)
|
|
832
|
+
// Configure artwork URL resolver for Now Playing (with size context). The
|
|
833
|
+
// `nowPlayingArtwork` config runs for EVERY playing track, id or not — an id-less track
|
|
834
|
+
// leaves any `{id}` token unfilled (resolveArtworkUrl warns and the request visibly
|
|
835
|
+
// 404s) rather than skipping the config.
|
|
717
836
|
player?.nowPlayingUpdater.artworkUrlResolver = { [weak self] track, imageContext in
|
|
718
837
|
guard let self else { return nil }
|
|
719
|
-
|
|
720
|
-
|
|
838
|
+
return await browserManager.resolveArtworkUrl(
|
|
839
|
+
track: track, perRouteConfig: browserManager.config.nowPlayingArtwork,
|
|
840
|
+
imageContext: imageContext,
|
|
841
|
+
)
|
|
721
842
|
}
|
|
722
843
|
|
|
723
844
|
// Emit the JS `onNowPlayingChanged` event when the rendered text lines change. The updater
|
|
@@ -733,16 +854,16 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
733
854
|
self?.onSleepTimerChanged(state)
|
|
734
855
|
}
|
|
735
856
|
|
|
736
|
-
// Now-playing: whether to manage metadata, the optional formatter that owns the
|
|
737
|
-
// lines
|
|
857
|
+
// Now-playing: whether to manage metadata, and the optional formatter that owns the
|
|
858
|
+
// rendered lines.
|
|
738
859
|
nowPlayingMetadataEnabled = options.autoUpdateNowPlayingMetadata ?? true
|
|
739
860
|
nowPlayingMetadataFormatter = options.nowPlayingMetadataFormatter
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
// (
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
|
|
861
|
+
// `options.keepSessionAliveOnError` is deliberately ignored here: iOS needs no masking.
|
|
862
|
+
// A terminal error resolves the playback state to *paused*
|
|
863
|
+
// (PlaybackCoordinator.applySideEffects), the now-playing info is retained (never cleared
|
|
864
|
+
// on error), and next/previous stay enabled (gated on queue position, not state). So the
|
|
865
|
+
// session stays controllable through errors by default — the behavior Android achieves via
|
|
866
|
+
// InterceptingPlayer is already the iOS norm.
|
|
746
867
|
|
|
747
868
|
// The bundled runtime options and initial state are part of the atomic launch
|
|
748
869
|
// description. Stage the state first so setup options and earlier pre-setup setter
|
|
@@ -758,6 +879,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
758
879
|
if let intent = options.playWhenReady { pendingPlayWhenReady = intent }
|
|
759
880
|
applyPendingPlayerState()
|
|
760
881
|
|
|
882
|
+
// Push the resolved event cadence unconditionally. `updateOptions` applies
|
|
883
|
+
// the interval only when it *differs* from the stored one, but the store
|
|
884
|
+
// outlives the player — so on a re-setup with unchanged options the new
|
|
885
|
+
// player would come up with no progress timer at all.
|
|
886
|
+
player?.setProgressUpdateInterval(playerOptions.progressUpdateEventInterval)
|
|
887
|
+
player?.setPlaybackIntervalEnabled(playbackIntervalEnabled)
|
|
888
|
+
|
|
761
889
|
// Notify listeners that player is ready (e.g., CarPlay)
|
|
762
890
|
playerAndConfiguredBrowser.check()
|
|
763
891
|
}
|
|
@@ -787,9 +915,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
787
915
|
// Update stored options
|
|
788
916
|
playerOptions.update(from: options)
|
|
789
917
|
|
|
790
|
-
// Propagate the favorite
|
|
918
|
+
// Propagate the favorite flag to the browser so it can hydrate
|
|
791
919
|
// row hearts (the `favorite` capability is the single favoriting switch).
|
|
792
|
-
browserManager.
|
|
920
|
+
browserManager.setFavoriteEnabled(playerOptions.capabilities.favoriteEnabled)
|
|
793
921
|
|
|
794
922
|
// Apply remote commands to player
|
|
795
923
|
applyRemoteCommands()
|
|
@@ -819,11 +947,19 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
819
947
|
}
|
|
820
948
|
|
|
821
949
|
public func getOptions() throws -> Options {
|
|
822
|
-
playerOptions.toOptions()
|
|
950
|
+
onMainActor { playerOptions.toOptions() }
|
|
823
951
|
}
|
|
824
952
|
|
|
953
|
+
/// Stored so a re-setup can re-apply it: unlike the other runtime options this
|
|
954
|
+
/// one has no home in `playerOptions`, and the player it was pushed to does not
|
|
955
|
+
/// survive `setupPlayer`.
|
|
956
|
+
@MainActor private var playbackIntervalEnabled = false
|
|
957
|
+
|
|
825
958
|
public func setPlaybackIntervalEnabled(enabled: Bool) {
|
|
826
|
-
onMainActor {
|
|
959
|
+
onMainActor {
|
|
960
|
+
playbackIntervalEnabled = enabled
|
|
961
|
+
player?.setPlaybackIntervalEnabled(enabled)
|
|
962
|
+
}
|
|
827
963
|
}
|
|
828
964
|
|
|
829
965
|
// MARK: - Playback Control
|
|
@@ -1037,24 +1173,25 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1037
1173
|
}
|
|
1038
1174
|
|
|
1039
1175
|
public func setActiveTrackFavorited(favorited: Bool) throws {
|
|
1040
|
-
onMainActor {
|
|
1041
|
-
guard let track = player?.currentTrack, let
|
|
1176
|
+
try onMainActor {
|
|
1177
|
+
guard let track = player?.currentTrack, let identity = track.identity else { return }
|
|
1042
1178
|
guard let index = player?.currentIndex, index >= 0 else { return }
|
|
1043
|
-
// Optimistically reflect in the authoritative
|
|
1044
|
-
// heart (hydrated from it in getActiveTrack) flips
|
|
1045
|
-
// reconciles to its canonical
|
|
1046
|
-
|
|
1179
|
+
// Optimistically reflect in the authoritative favorite set so the
|
|
1180
|
+
// now-playing heart (hydrated from it in getActiveTrack) flips
|
|
1181
|
+
// immediately; the consumer reconciles to its canonical identities on
|
|
1182
|
+
// the next setFavorites.
|
|
1183
|
+
browserManager.setFavorited(identity: identity, favorited: favorited)
|
|
1047
1184
|
let updatedTrack = track.copying(favorited: favorited)
|
|
1185
|
+
// Hydration never overrides a caller-set `favorited`, so the queued
|
|
1186
|
+
// track must itself carry the latest explicit value (as Android's tag
|
|
1187
|
+
// swap and web's replaceTrack do).
|
|
1188
|
+
try player?.replace(index, updatedTrack)
|
|
1189
|
+
// The ONLY emit: a favorite toggle is an in-place mutation of the active
|
|
1190
|
+
// track, not a transition — onPlaybackActiveTrackChanged stays
|
|
1191
|
+
// transition-only, and the useActiveTrack / useQueue hooks subscribe to
|
|
1192
|
+
// onFavoriteChanged themselves. Native surfaces (CarPlay heart) observe
|
|
1193
|
+
// favoriteChangedEmitter / onFavoritesChanged directly.
|
|
1048
1194
|
favoriteChangedEmitter.emit(FavoriteChangedEvent(track: updatedTrack, favorited: favorited))
|
|
1049
|
-
// Fire active track changed so the now-playing heart + useActiveTrack() refresh.
|
|
1050
|
-
let position = player?.currentTime ?? 0
|
|
1051
|
-
activeTrackChangedEmitter.emit(PlaybackActiveTrackChangedEvent(
|
|
1052
|
-
lastIndex: Double(index),
|
|
1053
|
-
lastTrack: track,
|
|
1054
|
-
lastPosition: position,
|
|
1055
|
-
index: Double(index),
|
|
1056
|
-
track: updatedTrack,
|
|
1057
|
-
))
|
|
1058
1195
|
}
|
|
1059
1196
|
}
|
|
1060
1197
|
|
|
@@ -1121,11 +1258,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1121
1258
|
/// metadata can't stomp it mid-flash. Reverted by a NATIVE timer — JS
|
|
1122
1259
|
/// timers pause with a backgrounded host on Android, and the lock screen
|
|
1123
1260
|
/// is exactly the backgrounded case — and cleared early on track change.
|
|
1124
|
-
private var nowPlayingFlash: NowPlayingUpdate?
|
|
1125
|
-
private var nowPlayingFlashRevert: DispatchWorkItem?
|
|
1261
|
+
@MainActor private var nowPlayingFlash: NowPlayingUpdate?
|
|
1262
|
+
@MainActor private var nowPlayingFlashRevert: DispatchWorkItem?
|
|
1126
1263
|
|
|
1127
1264
|
/// Pending debounced audio-session release (see `playerShouldReleaseSession`).
|
|
1128
|
-
private var sessionReleaseWork: DispatchWorkItem?
|
|
1265
|
+
@MainActor private var sessionReleaseWork: DispatchWorkItem?
|
|
1129
1266
|
|
|
1130
1267
|
public func flashNowPlaying(update: NowPlayingUpdate, durationMs: Double) throws {
|
|
1131
1268
|
onMainActor {
|
|
@@ -1151,6 +1288,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1151
1288
|
}
|
|
1152
1289
|
}
|
|
1153
1290
|
|
|
1291
|
+
@MainActor
|
|
1154
1292
|
private func cancelNowPlayingFlash() {
|
|
1155
1293
|
nowPlayingFlashRevert?.cancel()
|
|
1156
1294
|
nowPlayingFlashRevert = nil
|
|
@@ -1193,7 +1331,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1193
1331
|
duration: track.duration,
|
|
1194
1332
|
artwork: nowPlayingArtwork(for: track),
|
|
1195
1333
|
description: track.description,
|
|
1196
|
-
mediaId: track.src ?? track.
|
|
1334
|
+
mediaId: track.src ?? track.path,
|
|
1197
1335
|
genre: track.genre,
|
|
1198
1336
|
)
|
|
1199
1337
|
}
|
|
@@ -1206,7 +1344,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1206
1344
|
// Classify a stall by connectivity so the formatter can show "No internet connection" vs
|
|
1207
1345
|
// "Reconnecting…" without a separate read. nil while not stalled.
|
|
1208
1346
|
let stalled: StallReason? =
|
|
1209
|
-
player.isStalled ? (networkMonitor.
|
|
1347
|
+
player.isStalled ? (networkMonitor.isOnline ? .buffering : .offline) : nil
|
|
1210
1348
|
player.nowPlayingUpdater.render(
|
|
1211
1349
|
track: track,
|
|
1212
1350
|
timedMetadata: latestTimedMetadata,
|
|
@@ -1222,7 +1360,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1222
1360
|
// MARK: - Network
|
|
1223
1361
|
|
|
1224
1362
|
public func getOnline() throws -> Bool {
|
|
1225
|
-
|
|
1363
|
+
// No `onMainActor` hop: `isOnline` is lock-protected, so this stays a
|
|
1364
|
+
// cheap read on the JS thread instead of blocking on the main queue.
|
|
1365
|
+
networkMonitor.isOnline
|
|
1226
1366
|
}
|
|
1227
1367
|
|
|
1228
1368
|
// MARK: - System Volume
|
|
@@ -1354,8 +1494,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1354
1494
|
guard let self else { return }
|
|
1355
1495
|
// Re-apply our category (the reset cleared it), then have the player
|
|
1356
1496
|
// recreate itself and reconnect the current stream.
|
|
1357
|
-
self.
|
|
1358
|
-
|
|
1497
|
+
self.onMainActor {
|
|
1498
|
+
self.applyAudioSessionCategory()
|
|
1499
|
+
self.player?.handleMediaServicesReset()
|
|
1500
|
+
}
|
|
1359
1501
|
}
|
|
1360
1502
|
}
|
|
1361
1503
|
|
|
@@ -1394,17 +1536,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1394
1536
|
|
|
1395
1537
|
public func supportsOutputSwitcher() throws -> Bool {
|
|
1396
1538
|
// iOS can always present the system route picker (AVRoutePickerView).
|
|
1397
|
-
|
|
1539
|
+
true
|
|
1398
1540
|
}
|
|
1399
1541
|
|
|
1400
1542
|
public func openOutputPicker() throws {
|
|
1401
1543
|
DispatchQueue.main.async { [weak self] in
|
|
1402
1544
|
guard let self else { return }
|
|
1403
|
-
guard let windowScene = UIApplication.shared.
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
let window = windowScene.windows.first(where: { $0.isKeyWindow }),
|
|
1407
|
-
var topController = window.rootViewController
|
|
1545
|
+
guard let windowScene = UIApplication.shared.activeWindowScene,
|
|
1546
|
+
let window = windowScene.windows.first(where: { $0.isKeyWindow }),
|
|
1547
|
+
var topController = window.rootViewController
|
|
1408
1548
|
else {
|
|
1409
1549
|
self.logger.error("openOutputPicker: no active window scene / root view controller")
|
|
1410
1550
|
return
|
|
@@ -1586,6 +1726,7 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1586
1726
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: release)
|
|
1587
1727
|
}
|
|
1588
1728
|
|
|
1729
|
+
@MainActor
|
|
1589
1730
|
private func cancelSessionRelease() {
|
|
1590
1731
|
sessionReleaseWork?.cancel()
|
|
1591
1732
|
sessionReleaseWork = nil
|
|
@@ -1619,7 +1760,7 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1619
1760
|
|
|
1620
1761
|
public func playerDidReceiveTimedMetadata(_ groups: [AVTimedMetadataGroup]) {
|
|
1621
1762
|
for group in groups {
|
|
1622
|
-
if let metadata = TimedMetadata.from(items: group.items) {
|
|
1763
|
+
if let metadata = TimedMetadata.from(items: group.items, icyCharset: player?.currentTrack?.icyCharset) {
|
|
1623
1764
|
latestTimedMetadata = metadata
|
|
1624
1765
|
onTimedMetadata(metadata)
|
|
1625
1766
|
// Re-render the now-playing so the formatter can reflect the live song.
|
|
@@ -1779,14 +1920,26 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1779
1920
|
browser.showNowPlayingRequestedEmitter.emit(())
|
|
1780
1921
|
completion(true)
|
|
1781
1922
|
} else if let state = browser.playbackStateStore.load() {
|
|
1782
|
-
browser.logger.info("resume: cold — restoring persisted track")
|
|
1783
1923
|
let track = state.track.toNitro()
|
|
1924
|
+
// The persisted track can carry `disabled` — it became unavailable
|
|
1925
|
+
// after it was saved. A disabled track never plays, resumption
|
|
1926
|
+
// included (Track.disabled).
|
|
1927
|
+
if track.disabled == true {
|
|
1928
|
+
browser.logger.info("resume: persisted track is disabled — refusing")
|
|
1929
|
+
completion(false)
|
|
1930
|
+
return
|
|
1931
|
+
}
|
|
1932
|
+
browser.logger.info("resume: cold — restoring persisted track")
|
|
1933
|
+
// Repeat / shuffle / speed come back before the queue is built — see
|
|
1934
|
+
// `restorePlaybackSettings(from:)` for why that ordering matters.
|
|
1935
|
+
player.restorePlaybackSettings(from: state)
|
|
1784
1936
|
let startMs = (track.live == true) ? nil : state.positionMs
|
|
1785
1937
|
// Match Android resume: re-expand the full queue from the track's contextual
|
|
1786
|
-
//
|
|
1787
|
-
// track when the
|
|
1788
|
-
if let
|
|
1789
|
-
let expanded = try? await browser.browserManager.
|
|
1938
|
+
// path (parent container → siblings + selected index). Fall back to the single
|
|
1939
|
+
// track when the path isn't contextual or expansion fails.
|
|
1940
|
+
if let path = state.track.path,
|
|
1941
|
+
let expanded = try? await browser.browserManager.expandQueueFromContextualPath(path)
|
|
1942
|
+
{
|
|
1790
1943
|
player.setQueue(expanded.tracks, initialIndex: expanded.selectedIndex, startPositionMs: startMs, playWhenReady: true)
|
|
1791
1944
|
} else {
|
|
1792
1945
|
player.setQueue([track], initialIndex: 0, startPositionMs: startMs, playWhenReady: true)
|
|
@@ -1795,7 +1948,7 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1795
1948
|
completion(true)
|
|
1796
1949
|
} else {
|
|
1797
1950
|
browser.logger.error("resume: nothing playing and nothing persisted → no-op")
|
|
1798
|
-
completion(false)
|
|
1951
|
+
completion(false) // nothing playing and nothing persisted
|
|
1799
1952
|
}
|
|
1800
1953
|
return
|
|
1801
1954
|
}
|
|
@@ -1812,14 +1965,14 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1812
1965
|
title: criteria.title,
|
|
1813
1966
|
playlist: criteria.playlist,
|
|
1814
1967
|
// .currentlyPlaying can't reach here — isResume routed it to resume.
|
|
1815
|
-
reference: criteria.reference == .my ? .my : .unknown
|
|
1968
|
+
reference: criteria.reference == .my ? .my : .unknown,
|
|
1816
1969
|
)
|
|
1817
1970
|
|
|
1818
1971
|
// Search is *finding* new content — refused when the gate decides this
|
|
1819
1972
|
// request is gated (resume above is unaffected: the gate never blocks
|
|
1820
1973
|
// hearing, and the gate check stays after the resume check).
|
|
1821
1974
|
let outcome = await browser.gateDecision(
|
|
1822
|
-
for: NativeGateRequest(reason: .search, path: nil, search: params)
|
|
1975
|
+
for: NativeGateRequest(reason: .search, path: nil, search: params),
|
|
1823
1976
|
)
|
|
1824
1977
|
guard !outcome.gated else {
|
|
1825
1978
|
browser.logger.info("handlePlayMediaIntent: search refused — gated")
|