react-native-audio-browser 0.1.1 → 0.2.0-next.10
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/AudioBrowser.podspec +1 -1
- package/README.md +184 -2
- package/android/build.gradle +17 -5
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +18 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +473 -158
- 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/Callbacks.kt +4 -6
- package/android/src/main/java/com/audiobrowser/Service.kt +67 -18
- package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
- package/android/src/main/java/com/audiobrowser/browser/ArtworkResolutionRegistry.kt +37 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowseArtworkRegistry.kt +34 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +442 -178
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +299 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonArtwork.kt +79 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +41 -4
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +42 -0
- package/android/src/main/java/com/audiobrowser/browser/TrackLoadHandler.kt +4 -4
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +35 -0
- package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
- package/android/src/main/java/com/audiobrowser/http/RequestConfigBuilder.kt +109 -97
- package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +6 -10
- package/android/src/main/java/com/audiobrowser/model/PlayerSetupOptions.kt +40 -8
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +40 -46
- package/android/src/main/java/com/audiobrowser/player/AutomaticBufferManager.kt +4 -1
- package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +172 -0
- package/android/src/main/java/com/audiobrowser/player/DynamicLoadControl.kt +18 -1
- package/android/src/main/java/com/audiobrowser/player/EqualizerController.kt +153 -0
- package/android/src/main/java/com/audiobrowser/player/InterceptingPlayer.kt +141 -0
- package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +15 -3
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +422 -134
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +248 -404
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +347 -0
- package/android/src/main/java/com/audiobrowser/player/PlaybackErrorClassifier.kt +129 -0
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateMachine.kt +85 -0
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +25 -5
- package/android/src/main/java/com/audiobrowser/player/PlaybackTimer.kt +56 -0
- package/android/src/main/java/com/audiobrowser/player/Player.kt +475 -577
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +20 -0
- package/android/src/main/java/com/audiobrowser/player/PlayerEngineFactory.kt +130 -0
- package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +190 -67
- package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +118 -53
- package/android/src/main/java/com/audiobrowser/player/SleepTimer.kt +37 -3
- package/android/src/main/java/com/audiobrowser/player/SleepTimerManager.kt +80 -0
- package/android/src/main/java/com/audiobrowser/player/StuckRecoveryPolicy.kt +50 -0
- package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +85 -40
- package/android/src/main/java/com/audiobrowser/player/VolumeFader.kt +81 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +156 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +109 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +37 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +55 -0
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +265 -0
- package/android/src/main/java/com/audiobrowser/util/ArtworkContentProvider.kt +161 -0
- package/android/src/main/java/com/audiobrowser/util/ArtworkUris.kt +28 -0
- package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +24 -0
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +24 -0
- package/android/src/main/java/com/audiobrowser/util/CoilArtworkLoader.kt +49 -0
- package/android/src/main/java/com/audiobrowser/util/CoilArtworkLoaderHolder.kt +36 -0
- package/android/src/main/java/com/audiobrowser/util/CoilBitmapLoader.kt +23 -300
- package/android/src/main/java/com/audiobrowser/util/EqualizerManager.kt +13 -23
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +2 -2
- package/android/src/main/java/com/audiobrowser/util/MetadataAdapter.kt +4 -3
- package/android/src/main/java/com/audiobrowser/util/OutputMonitor.kt +211 -0
- package/android/src/main/java/com/audiobrowser/util/OutputSwitcher.kt +58 -0
- package/android/src/main/java/com/audiobrowser/util/PlayingStateFactory.kt +2 -1
- package/android/src/main/java/com/audiobrowser/util/RatingFavorites.kt +14 -0
- package/android/src/main/java/com/audiobrowser/util/SvgArtworkRenderer.kt +6 -111
- package/android/src/main/java/com/audiobrowser/util/SystemVolumeMonitor.kt +22 -8
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +112 -80
- package/android/src/main/res/values/strings.xml +4 -0
- package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +165 -0
- package/android/src/test/java/com/audiobrowser/browser/ArtworkResolutionRegistryTest.kt +46 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowseArtworkRegistryTest.kt +39 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerBuildApiRequestTest.kt +114 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +141 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +4 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +177 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +94 -0
- package/android/src/test/java/com/audiobrowser/browser/SimpleRouterTest.kt +1 -2
- package/android/src/test/java/com/audiobrowser/http/RequestConfigBuilderTest.kt +72 -0
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +203 -0
- package/android/src/test/java/com/audiobrowser/player/DynamicLoadControlTest.kt +56 -0
- package/android/src/test/java/com/audiobrowser/player/EqualizerControllerTest.kt +216 -0
- package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +442 -0
- package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +190 -0
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateMachineTest.kt +198 -0
- package/android/src/test/java/com/audiobrowser/player/PlaybackTimerTest.kt +137 -0
- package/android/src/test/java/com/audiobrowser/player/PlayerEngineFactoryTest.kt +36 -0
- package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +268 -0
- package/android/src/test/java/com/audiobrowser/player/SessionErrorMaskTest.kt +68 -0
- package/android/src/test/java/com/audiobrowser/player/StuckRecoveryPolicyTest.kt +40 -0
- package/android/src/test/java/com/audiobrowser/player/TransformingDataSourceTest.kt +193 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +186 -0
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +158 -0
- package/android/src/test/java/com/audiobrowser/tls/CertFixtures.kt +18 -0
- package/android/src/test/java/com/audiobrowser/util/ArtworkContentProviderTest.kt +163 -0
- package/android/src/test/java/com/audiobrowser/util/ArtworkUrisTest.kt +34 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +28 -0
- package/android/src/test/java/com/audiobrowser/util/CoilArtworkLoaderHolderTest.kt +53 -0
- package/android/src/test/java/com/audiobrowser/util/CoilArtworkLoaderTest.kt +58 -0
- package/android/src/test/java/com/audiobrowser/util/FakeImageLoader.kt +57 -0
- package/android/src/test/java/com/audiobrowser/util/PlayingStateFactoryTest.kt +40 -0
- package/android/src/test/java/com/audiobrowser/util/RatingFavoritesTest.kt +34 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryBrowseTest.kt +50 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +63 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryRatingTest.kt +35 -0
- package/android/src/test/resources/certs/isrg-root-x1.pem +31 -0
- package/android/src/test/resources/certs/letsencrypt-r13.der +0 -0
- package/android/src/test/resources/certs/letsencrypt-r13.pem +29 -0
- package/android/src/test/resources/certs/stationplaylist-leaf.pem +34 -0
- package/ios/Browser/BrowserConfig.swift +41 -11
- package/ios/Browser/BrowserManager+URLResolution.swift +213 -93
- package/ios/Browser/BrowserManager.swift +303 -135
- package/ios/Browser/BrowserPathHelper.swift +32 -3
- package/ios/Browser/JsonModels.swift +287 -66
- package/ios/Browser/MediaResolveComposer.swift +114 -0
- package/ios/Browser/SearchDrillIn.swift +44 -0
- package/ios/Browser/SectionScope.swift +35 -0
- package/ios/Browser/TabBarEntries.swift +15 -0
- package/ios/CarPlay/CPInterfaceController+Safe.swift +126 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +6 -6
- package/ios/CarPlay/CarPlayController.swift +739 -149
- package/ios/CarPlay/CarPlayImageLoader.swift +57 -8
- package/ios/CarPlay/CarPlayListItemFactory.swift +44 -35
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +180 -36
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +19 -0
- package/ios/CarPlay/RNABAudioBrowser.h +15 -10
- package/ios/CarPlay/RNABAudioBrowser.m +15 -14
- package/ios/CarPlay/RNABCarPlaySceneDelegate.m +2 -0
- package/ios/CarPlay/RNABMediaAddHandler.swift +75 -0
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +69 -0
- package/ios/CarPlay/RNABMediaIntentHandler.swift +53 -43
- package/ios/Extension/Capability+RemoteCommand.swift +7 -9
- package/ios/Extension/ChapterMetadata+AVFoundation.swift +1 -1
- package/ios/Extension/NavigationError+Conversion.swift +8 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +20 -0
- package/ios/Extension/RequestConfig+Copying.swift +31 -0
- package/ios/Extension/ResolvedTrack+Copying.swift +10 -4
- package/ios/Extension/TimedMetadata+AVFoundation.swift +1 -1
- package/ios/Extension/Track+AVPlayer.swift +4 -4
- package/ios/Extension/Track+Copying.swift +9 -3
- package/ios/Extension/TrackMetadata+AVFoundation.swift +1 -1
- package/ios/Extension/Variant+Artwork.swift +49 -0
- package/ios/HybridAudioBrowser.swift +805 -197
- package/ios/Model/NitroTypeStubs.swift +299 -224
- package/ios/Model/PlayerUpdateOptions.swift +23 -23
- package/ios/Model/RemoteCommand.swift +24 -6
- package/ios/Model/TrackPlayerError.swift +82 -18
- package/ios/NowPlayingInfo/NowPlayingInfoController.swift +99 -121
- package/ios/NowPlayingInfo/SilentAudioPrimer.swift +73 -0
- package/ios/Observer/PlayerItemNotificationObserver.swift +45 -5
- package/ios/Observer/PlayerItemPropertyObserver.swift +27 -4
- package/ios/Observer/PlayerTimeObserver.swift +3 -53
- package/ios/Option/SessionCategories.swift +34 -11
- package/ios/Player/AirPlayStallJudgement.swift +25 -0
- package/ios/Player/EndOfTrackJudgement.swift +32 -0
- package/ios/Player/LoadSeekCoordinator.swift +16 -1
- package/ios/Player/MediaIntentCriteria.swift +139 -0
- package/ios/Player/MediaLoader.swift +31 -11
- package/ios/Player/NowPlayingUpdater.swift +119 -26
- package/ios/Player/PersistedPlaybackState.swift +11 -0
- package/ios/Player/PlaybackCoordinator.swift +285 -51
- package/ios/Player/PlaybackCoordinatorCallbacks.swift +6 -1
- package/ios/Player/PlaybackEffectHandler.swift +19 -9
- package/ios/Player/PlaybackErrorHandler.swift +98 -14
- package/ios/Player/PlaybackStateMachine.swift +18 -13
- package/ios/Player/PlaybackStateStore.swift +27 -0
- package/ios/Player/PlaybackTimer.swift +61 -0
- package/ios/Player/PlayingStateManager.swift +3 -2
- package/ios/Player/QueueManager.swift +77 -64
- package/ios/Player/RetryManager.swift +175 -38
- package/ios/Player/ShuffleOrder.swift +10 -0
- package/ios/Player/SleepTimerHandling.swift +8 -0
- package/ios/Player/SleepTimerManager.swift +72 -11
- package/ios/Player/VolumeFader.swift +77 -0
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +40 -21
- package/ios/Tests/AirPlayStallJudgementTests.swift +52 -0
- package/ios/Tests/BrowserPathHelperTests.swift +28 -0
- package/ios/Tests/CarPlayArtworkResolverTests.swift +39 -39
- package/ios/Tests/ChapterMetadataAVFoundationTests.swift +2 -2
- package/ios/Tests/EndOfTrackJudgementTests.swift +35 -0
- package/ios/Tests/JsonModelsTests.swift +58 -0
- package/ios/Tests/LRUCacheTests.swift +111 -0
- package/ios/Tests/LoadSeekCoordinatorTests.swift +40 -7
- package/ios/Tests/MediaIntentCriteriaTests.swift +170 -0
- package/ios/Tests/MediaLoaderAssetOptionsTests.swift +46 -0
- package/ios/Tests/MediaLoaderTests.swift +32 -20
- package/ios/Tests/MediaResolveTests.swift +126 -0
- package/ios/Tests/OnceValueTests.swift +79 -0
- package/ios/Tests/PlaybackCoordinatorTests.swift +787 -13
- package/ios/Tests/PlaybackErrorHandlerTests.swift +383 -47
- package/ios/Tests/PlaybackStateMachineTests.swift +33 -8
- package/ios/Tests/PlaybackStateStoreTests.swift +48 -0
- package/ios/Tests/PlaybackTimerTests.swift +55 -0
- package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
- package/ios/Tests/PlayingStateManagerTests.swift +57 -0
- package/ios/Tests/QueueManagerTests.swift +195 -18
- package/ios/Tests/RemoteCommandControllerTests.swift +156 -0
- package/ios/Tests/RemoteCommandTests.swift +67 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +15 -8
- package/ios/Tests/RetryManagerTests.swift +178 -0
- package/ios/Tests/SearchDrillInTests.swift +63 -0
- package/ios/Tests/SectionScopeTests.swift +82 -0
- package/ios/Tests/SleepTimerManagerTests.swift +107 -0
- package/ios/Tests/Support/ManualSleepTimerScheduler.swift +47 -0
- package/ios/Tests/Support/TestTypes.swift +61 -20
- package/ios/Tests/TabBarEntriesTests.swift +71 -0
- package/ios/Tests/TrackSelectorTests.swift +15 -15
- package/ios/TrackPlayer.swift +385 -58
- package/ios/TrackPlayerCallbacks.swift +5 -44
- package/ios/TrackSelector.swift +5 -5
- package/ios/Util/ArtworkImageFetcher.swift +4 -2
- package/ios/Util/LRUCache.swift +4 -1
- package/ios/Util/NetworkMonitor.swift +7 -3
- package/ios/Util/OnceValue.swift +34 -0
- package/lib/commonjs/AudioBrowser.js +0 -11
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/CLAUDE.md +52 -0
- package/lib/commonjs/features/browser-config.js +227 -0
- package/lib/commonjs/features/browser-config.js.map +1 -0
- package/lib/commonjs/features/browser.js +60 -159
- package/lib/commonjs/features/browser.js.map +1 -1
- package/lib/commonjs/features/carConnection.js +42 -0
- package/lib/commonjs/features/carConnection.js.map +1 -0
- package/lib/commonjs/features/equalizer.js +1 -1
- package/lib/commonjs/features/errors.js +37 -5
- package/lib/commonjs/features/errors.js.map +1 -1
- package/lib/commonjs/features/favorites.js +1 -1
- package/lib/commonjs/features/gate.js +164 -0
- package/lib/commonjs/features/gate.js.map +1 -0
- package/lib/commonjs/features/index.js +22 -0
- package/lib/commonjs/features/index.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 +36 -1
- package/lib/commonjs/features/nowPlaying.js.map +1 -1
- package/lib/commonjs/features/output.js +37 -23
- package/lib/commonjs/features/output.js.map +1 -1
- package/lib/commonjs/features/playback/controls.js +9 -0
- package/lib/commonjs/features/playback/controls.js.map +1 -1
- package/lib/commonjs/features/playback/index.js +11 -0
- package/lib/commonjs/features/playback/index.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 +29 -13
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/state.js +1 -1
- package/lib/commonjs/features/playback/trackPlaybackTime.js +86 -0
- package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -0
- package/lib/commonjs/features/playback/volume.js +2 -2
- package/lib/commonjs/features/player/options.js +79 -29
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +256 -3
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +21 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -0
- package/lib/commonjs/features/queue/activeTrack.js +1 -1
- package/lib/commonjs/features/queue/queue.js +12 -5
- 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 +14 -104
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/features/sleepTimer.js +22 -5
- package/lib/commonjs/features/sleepTimer.js.map +1 -1
- package/lib/commonjs/native.js +5 -0
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.web.js +4 -1
- package/lib/commonjs/native.web.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 +8 -8
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/utils/NativeUpdatedValue.js +0 -3
- package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/commonjs/utils/artwork.js +31 -0
- package/lib/commonjs/utils/artwork.js.map +1 -0
- package/lib/commonjs/web/NativeAudioBrowser.js +302 -125
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +71 -0
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/PlaybackTimer.js +34 -0
- package/lib/commonjs/web/TrackPlayer/PlaybackTimer.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/Player.js +90 -26
- package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +21 -0
- package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js +273 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +158 -0
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/RepeatMode.js +1 -1
- package/lib/commonjs/web/TrackPlayer/RepeatMode.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/SleepTimer.js +34 -3
- package/lib/commonjs/web/TrackPlayer/SleepTimer.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/VolumeFader.js +75 -0
- package/lib/commonjs/web/TrackPlayer/VolumeFader.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/index.js +13 -5
- package/lib/commonjs/web/TrackPlayer/index.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +176 -50
- package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
- package/lib/commonjs/web/browser/NavigationErrorManager.js +3 -1
- package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
- package/lib/commonjs/web/browser/SearchManager.js +11 -9
- package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
- package/lib/commonjs/web/browser/assertBrowsePageShape.js +22 -0
- package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -0
- package/lib/commonjs/web/browser/classifyTrackNavigation.js +41 -0
- package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -0
- package/lib/commonjs/web/browser/parseSearchResponse.js +20 -0
- package/lib/commonjs/web/browser/parseSearchResponse.js.map +1 -0
- package/lib/commonjs/web/http/HttpClient.js +4 -23
- package/lib/commonjs/web/http/HttpClient.js.map +1 -1
- package/lib/commonjs/web/http/RequestConfigBuilder.js +107 -58
- package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/commonjs/web/playbackErrorKind.js +57 -0
- package/lib/commonjs/web/playbackErrorKind.js.map +1 -0
- package/lib/commonjs/web/player/MediaSessionManager.js +121 -0
- package/lib/commonjs/web/player/MediaSessionManager.js.map +1 -0
- package/lib/commonjs/web/player/NowPlayingManager.js +6 -5
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/player/OptionsManager.js +9 -17
- package/lib/commonjs/web/player/OptionsManager.js.map +1 -1
- package/lib/commonjs/web/player/RemoteCommandController.js +96 -0
- package/lib/commonjs/web/player/RemoteCommandController.js.map +1 -0
- package/lib/module/AudioBrowser.js +1 -1
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/CLAUDE.md +52 -0
- package/lib/module/features/browser-config.js +221 -0
- package/lib/module/features/browser-config.js.map +1 -0
- package/lib/module/features/browser.js +54 -157
- package/lib/module/features/browser.js.map +1 -1
- package/lib/module/features/carConnection.js +37 -0
- package/lib/module/features/carConnection.js.map +1 -0
- package/lib/module/features/equalizer.js +1 -1
- package/lib/module/features/errors.js +37 -5
- package/lib/module/features/errors.js.map +1 -1
- package/lib/module/features/favorites.js +1 -1
- package/lib/module/features/gate.js +159 -0
- package/lib/module/features/gate.js.map +1 -0
- package/lib/module/features/index.js +2 -0
- package/lib/module/features/index.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 +34 -1
- package/lib/module/features/nowPlaying.js.map +1 -1
- package/lib/module/features/output.js +32 -19
- package/lib/module/features/output.js.map +1 -1
- package/lib/module/features/playback/controls.js +8 -0
- package/lib/module/features/playback/controls.js.map +1 -1
- package/lib/module/features/playback/index.js +1 -0
- package/lib/module/features/playback/index.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 +29 -13
- package/lib/module/features/playback/progress.js.map +1 -1
- package/lib/module/features/playback/state.js +1 -1
- package/lib/module/features/playback/trackPlaybackTime.js +83 -0
- package/lib/module/features/playback/trackPlaybackTime.js.map +1 -0
- package/lib/module/features/playback/volume.js +2 -2
- package/lib/module/features/player/options.js +79 -29
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +256 -3
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +17 -0
- package/lib/module/features/player/validateOptions.js.map +1 -0
- package/lib/module/features/queue/activeTrack.js +1 -1
- package/lib/module/features/queue/queue.js +12 -5
- 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 -103
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/features/sleepTimer.js +22 -5
- package/lib/module/features/sleepTimer.js.map +1 -1
- package/lib/module/native.js +5 -0
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.web.js +4 -1
- package/lib/module/native.web.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 +8 -8
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/utils/NativeUpdatedValue.js +0 -3
- package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/module/utils/artwork.js +26 -0
- package/lib/module/utils/artwork.js.map +1 -0
- package/lib/module/web/NativeAudioBrowser.js +303 -126
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +67 -0
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -0
- package/lib/module/web/TrackPlayer/PlaybackTimer.js +29 -0
- package/lib/module/web/TrackPlayer/PlaybackTimer.js.map +1 -0
- package/lib/module/web/TrackPlayer/Player.js +90 -26
- package/lib/module/web/TrackPlayer/Player.js.map +1 -1
- package/lib/module/web/TrackPlayer/PlayingStateFactory.js +17 -0
- package/lib/module/web/TrackPlayer/PlayingStateFactory.js.map +1 -0
- package/lib/module/web/TrackPlayer/QueueManager.js +269 -0
- package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -0
- package/lib/module/web/TrackPlayer/QueuePlayer.js +154 -0
- package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -0
- package/lib/module/web/TrackPlayer/RepeatMode.js +1 -1
- package/lib/module/web/TrackPlayer/RepeatMode.js.map +1 -1
- package/lib/module/web/TrackPlayer/SleepTimer.js +34 -3
- package/lib/module/web/TrackPlayer/SleepTimer.js.map +1 -1
- package/lib/module/web/TrackPlayer/VolumeFader.js +70 -0
- package/lib/module/web/TrackPlayer/VolumeFader.js.map +1 -0
- package/lib/module/web/TrackPlayer/index.js +2 -1
- package/lib/module/web/TrackPlayer/index.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +176 -50
- package/lib/module/web/browser/BrowserManager.js.map +1 -1
- package/lib/module/web/browser/NavigationErrorManager.js +3 -1
- package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
- package/lib/module/web/browser/SearchManager.js +11 -9
- package/lib/module/web/browser/SearchManager.js.map +1 -1
- package/lib/module/web/browser/assertBrowsePageShape.js +18 -0
- package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -0
- package/lib/module/web/browser/classifyTrackNavigation.js +38 -0
- package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -0
- package/lib/module/web/browser/parseSearchResponse.js +16 -0
- package/lib/module/web/browser/parseSearchResponse.js.map +1 -0
- package/lib/module/web/http/HttpClient.js +4 -23
- package/lib/module/web/http/HttpClient.js.map +1 -1
- package/lib/module/web/http/RequestConfigBuilder.js +107 -58
- package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/module/web/playbackErrorKind.js +52 -0
- package/lib/module/web/playbackErrorKind.js.map +1 -0
- package/lib/module/web/player/MediaSessionManager.js +116 -0
- package/lib/module/web/player/MediaSessionManager.js.map +1 -0
- package/lib/module/web/player/NowPlayingManager.js +7 -5
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/player/OptionsManager.js +9 -17
- package/lib/module/web/player/OptionsManager.js.map +1 -1
- package/lib/module/web/player/RemoteCommandController.js +92 -0
- package/lib/module/web/player/RemoteCommandController.js.map +1 -0
- package/lib/typescript/src/AudioBrowser.d.ts +0 -1
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts +12 -0
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -0
- package/lib/typescript/src/features/browser.d.ts +40 -1
- package/lib/typescript/src/features/browser.d.ts.map +1 -1
- package/lib/typescript/src/features/carConnection.d.ts +20 -0
- package/lib/typescript/src/features/carConnection.d.ts.map +1 -0
- package/lib/typescript/src/features/equalizer.d.ts +1 -1
- package/lib/typescript/src/features/errors.d.ts +61 -6
- package/lib/typescript/src/features/errors.d.ts.map +1 -1
- package/lib/typescript/src/features/favorites.d.ts +1 -1
- package/lib/typescript/src/features/gate.d.ts +93 -0
- package/lib/typescript/src/features/gate.d.ts.map +1 -0
- package/lib/typescript/src/features/index.d.ts +2 -0
- package/lib/typescript/src/features/index.d.ts.map +1 -1
- package/lib/typescript/src/features/metadata.d.ts +26 -7
- 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 +28 -1
- package/lib/typescript/src/features/nowPlaying.d.ts.map +1 -1
- package/lib/typescript/src/features/output.d.ts +28 -18
- package/lib/typescript/src/features/output.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/controls.d.ts +5 -0
- package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/index.d.ts +1 -0
- package/lib/typescript/src/features/playback/index.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 +4 -1
- package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +34 -0
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -0
- package/lib/typescript/src/features/playback/volume.d.ts +2 -2
- package/lib/typescript/src/features/player/options.d.ts +176 -127
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts +216 -167
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +9 -0
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
- package/lib/typescript/src/features/queue/activeTrack.d.ts +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +11 -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 -35
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts +19 -4
- package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native.d.ts +1 -2
- package/lib/typescript/src/native.d.ts.map +1 -1
- package/lib/typescript/src/specs/audio-browser.nitro.d.ts +17 -225
- package/lib/typescript/src/specs/audio-browser.nitro.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 +9 -3
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +145 -13
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +349 -85
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +8 -6
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +0 -2
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
- package/lib/typescript/src/utils/artwork.d.ts +19 -0
- package/lib/typescript/src/utils/artwork.d.ts.map +1 -0
- package/lib/typescript/src/utils/resolveAssetSource.d.ts.map +1 -1
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +4 -2
- package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +24 -10
- package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +6 -6
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -20
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +33 -1
- package/nitrogen/generated/android/c++/JArtworkVariants.hpp +61 -0
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +5 -0
- package/nitrogen/generated/android/c++/JCarPlayNowPlayingButton.hpp +3 -3
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +6 -0
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoritesMatchMode.hpp +58 -0
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +100 -0
- package/nitrogen/generated/android/c++/JFormattedNavigationError.hpp +3 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.hpp +112 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +121 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +123 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +133 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +121 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.hpp +139 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.hpp +134 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +6 -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 +131 -0
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_void_GateEvent.hpp +79 -0
- package/nitrogen/generated/android/c++/JFunc_void_NowPlayingMetadata.hpp +0 -10
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +14 -9
- package/nitrogen/generated/android/c++/JFunc_void_Output.hpp +80 -0
- package/nitrogen/generated/android/c++/JFunc_void_Playback.hpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackErrorEvent.hpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +10 -10
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +6 -0
- package/nitrogen/generated/android/c++/JGate.hpp +62 -0
- package/nitrogen/generated/android/c++/JGateDecision.hpp +64 -0
- package/nitrogen/generated/android/c++/{JThumbsRating.hpp → JGateEvent.hpp} +16 -15
- package/nitrogen/generated/android/c++/JGateReason.hpp +58 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +247 -223
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +26 -23
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +95 -0
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +33 -3
- package/nitrogen/generated/android/c++/JMediaReference.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +33 -1
- package/nitrogen/generated/android/c++/{JPartialAndroidSetupPlayerOptions.hpp → JNativeAndroidSetupOptions.hpp} +10 -10
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +61 -28
- package/nitrogen/generated/android/c++/JNativeGateRequest.hpp +74 -0
- package/nitrogen/generated/android/c++/{JPartialIOSSetupPlayerOptions.hpp → JNativeIOSSetupOptions.hpp} +10 -10
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +20 -9
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +182 -0
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +21 -24
- package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +6 -0
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -20
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +96 -0
- package/nitrogen/generated/android/c++/JNowPlayingMetadata.hpp +3 -17
- package/nitrogen/generated/android/c++/JNowPlayingUpdate.hpp +7 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +19 -14
- package/nitrogen/generated/android/c++/{JIosOutput.hpp → JOutput.hpp} +15 -15
- package/nitrogen/generated/android/c++/JOutputType.hpp +82 -0
- package/nitrogen/generated/android/c++/JPlayback.hpp +2 -0
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +6 -0
- package/nitrogen/generated/android/c++/JPlaybackError.hpp +18 -3
- package/nitrogen/generated/android/c++/JPlaybackErrorEvent.hpp +2 -0
- package/nitrogen/generated/android/c++/JPlaybackErrorKind.hpp +76 -0
- package/nitrogen/generated/android/c++/JPlaybackState.hpp +3 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +11 -5
- package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +20 -20
- package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +23 -5
- package/nitrogen/generated/android/c++/JRetryConfig.hpp +11 -7
- package/nitrogen/generated/android/c++/JSearchMode.hpp +27 -9
- package/nitrogen/generated/android/c++/JSearchParams.hpp +9 -3
- package/nitrogen/generated/android/c++/JStallReason.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +23 -5
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +6 -0
- package/nitrogen/generated/android/c++/JTrackRequest.hpp +93 -0
- package/nitrogen/generated/android/c++/JTransformableRequestConfig.hpp +14 -1
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +71 -0
- 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_.cpp +41 -0
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +121 -0
- package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.hpp +70 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidAudioContentType.kt +4 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkRequestConfig.kt +10 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkVariants.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayNowPlayingButton.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{HeartRating.kt → FavoriteConfig.kt} +6 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{RatingType.kt → FavoritesMatchMode.kt} +5 -10
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNowPlayingParams.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormattedNavigationError.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___Track.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_std__string____Track.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.kt → Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.kt} +12 -12
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_void_RemoteSetRatingEvent.kt → Func_void_GateEvent.kt} +9 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Func_void_IosOutput.kt → Func_void_Output.kt} +9 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_bool.kt +9 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{ThumbsRating.kt → Gate.kt} +9 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PercentageRating.kt → GateDecision.kt} +9 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{StarRating.kt → GateEvent.kt} +6 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → GateReason.kt} +5 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +104 -110
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +24 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaReference.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaRequestConfig.kt +10 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PartialAndroidSetupPlayerOptions.kt → NativeAndroidSetupOptions.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +18 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{RemoteSetRatingEvent.kt → NativeGateRequest.kt} +12 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{PartialIOSSetupPlayerOptions.kt → NativeIOSSetupOptions.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeRouteEntry.kt +4 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeSetupPlayerOptions.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeUpdateOptions.kt +6 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorType.kt +3 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingMetadata.kt +3 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingUpdate.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Options.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{IosOutput.kt → Output.kt} +6 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/OutputType.kt +31 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackError.kt +12 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackErrorKind.kt +29 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackState.kt +6 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButton.kt +26 -0
- 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 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RetryConfig.kt +7 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchMode.kt +12 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchParams.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StallReason.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +12 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackRequest.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TransformableRequestConfig.kt +7 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.kt +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_String_ArtworkVariants.kt +53 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +139 -28
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +1126 -490
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +66 -45
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +178 -120
- package/nitrogen/generated/ios/swift/AndroidAudioContentType.swift +8 -8
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -22
- package/nitrogen/generated/ios/swift/ArtworkRequestConfig.swift +95 -1
- package/nitrogen/generated/ios/swift/ArtworkVariants.swift +34 -0
- package/nitrogen/generated/ios/swift/CarPlayNowPlayingButton.swift +4 -4
- package/nitrogen/generated/ios/swift/FavoriteConfig.swift +29 -0
- package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +40 -0
- package/nitrogen/generated/ios/swift/FormatNowPlayingParams.swift +67 -0
- package/nitrogen/generated/ios/swift/FormattedNavigationError.swift +17 -4
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.swift +54 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.swift +69 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_RequestConfig___Track.swift +54 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.swift +60 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__optional_std__string____Track.swift +60 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.swift +61 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.swift +61 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.swift +77 -0
- package/nitrogen/generated/ios/swift/Func_void_GateDecision.swift +46 -0
- package/nitrogen/generated/ios/swift/{Func_void_RemoteSetRatingEvent.swift → Func_void_GateEvent.swift} +10 -10
- package/nitrogen/generated/ios/swift/{Func_void_IosOutput.swift → Func_void_Output.swift} +10 -10
- package/nitrogen/generated/ios/swift/Func_void_TransformableRequestConfig.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_std__optional_NowPlayingUpdate_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__optional_std__string_.swift +53 -0
- package/nitrogen/generated/ios/swift/{Func_void_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError___.swift → Func_void_std__shared_ptr_Promise_GateDecision__.swift} +17 -17
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_Promise_TransformableRequestConfig__.swift +66 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError____.swift +81 -0
- package/nitrogen/generated/ios/swift/Gate.swift +47 -0
- package/nitrogen/generated/ios/swift/GateDecision.swift +40 -0
- package/nitrogen/generated/ios/swift/GateEvent.swift +29 -0
- package/nitrogen/generated/ios/swift/GateReason.swift +40 -0
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +21 -15
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +232 -199
- package/nitrogen/generated/ios/swift/IOSOptions.swift +51 -0
- package/nitrogen/generated/ios/swift/ImageRowItem.swift +121 -2
- package/nitrogen/generated/ios/swift/MediaReference.swift +40 -0
- package/nitrogen/generated/ios/swift/MediaRequestConfig.swift +107 -1
- package/nitrogen/generated/ios/swift/{PartialAndroidSetupPlayerOptions.swift → NativeAndroidSetupOptions.swift} +5 -5
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +161 -22
- package/nitrogen/generated/ios/swift/NativeGateRequest.swift +58 -0
- package/nitrogen/generated/ios/swift/{PartialIOSSetupPlayerOptions.swift → NativeIOSSetupOptions.swift} +5 -5
- package/nitrogen/generated/ios/swift/NativeRouteEntry.swift +19 -19
- package/nitrogen/generated/ios/swift/NativeSetupPlayerOptions.swift +206 -0
- package/nitrogen/generated/ios/swift/NativeUpdateOptions.swift +12 -25
- package/nitrogen/generated/ios/swift/NavigationErrorType.swift +8 -0
- package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -41
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +90 -0
- package/nitrogen/generated/ios/swift/NowPlayingMetadata.swift +1 -48
- package/nitrogen/generated/ios/swift/NowPlayingUpdate.swift +19 -1
- package/nitrogen/generated/ios/swift/Options.swift +10 -4
- package/nitrogen/generated/ios/swift/{IosOutput.swift → Output.swift} +7 -7
- package/nitrogen/generated/ios/swift/OutputType.swift +72 -0
- package/nitrogen/generated/ios/swift/PlaybackError.swift +43 -2
- package/nitrogen/generated/ios/swift/PlaybackErrorKind.swift +64 -0
- package/nitrogen/generated/ios/swift/PlaybackState.swift +4 -4
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +27 -8
- package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +10 -10
- package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +74 -8
- package/nitrogen/generated/ios/swift/RetryConfig.swift +35 -4
- package/nitrogen/generated/ios/swift/SearchMode.swift +36 -12
- package/nitrogen/generated/ios/swift/SearchParams.swift +7 -2
- package/nitrogen/generated/ios/swift/StallReason.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +74 -8
- package/nitrogen/generated/ios/swift/TrackRequest.swift +106 -0
- package/nitrogen/generated/ios/swift/TransformableRequestConfig.swift +60 -1
- package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +18 -0
- package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
- package/nitrogen/generated/ios/swift/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.swift +19 -0
- package/nitrogen/generated/ios/swift/Variant_String_ArtworkVariants.swift +18 -0
- package/nitrogen/generated/shared/c++/AndroidAudioContentType.hpp +10 -10
- package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -19
- package/nitrogen/generated/shared/c++/ArtworkRequestConfig.hpp +10 -2
- package/nitrogen/generated/shared/c++/ArtworkVariants.hpp +87 -0
- package/nitrogen/generated/shared/c++/CarPlayNowPlayingButton.hpp +6 -6
- package/nitrogen/generated/shared/c++/EqualizerSettings.hpp +0 -2
- package/nitrogen/generated/shared/c++/{StarRating.hpp → FavoriteConfig.hpp} +18 -17
- package/nitrogen/generated/shared/c++/FavoritesMatchMode.hpp +76 -0
- package/nitrogen/generated/shared/c++/FormatNowPlayingParams.hpp +110 -0
- package/nitrogen/generated/shared/c++/FormattedNavigationError.hpp +6 -5
- package/nitrogen/generated/shared/c++/{PercentageRating.hpp → Gate.hpp} +21 -16
- package/nitrogen/generated/shared/c++/{HeartRating.hpp → GateDecision.hpp} +23 -17
- package/nitrogen/generated/shared/c++/{ThumbsRating.hpp → GateEvent.hpp} +18 -17
- package/nitrogen/generated/shared/c++/GateReason.hpp +76 -0
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +22 -19
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +45 -36
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +91 -0
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +33 -2
- package/nitrogen/generated/shared/c++/MediaReference.hpp +76 -0
- package/nitrogen/generated/shared/c++/MediaRequestConfig.hpp +10 -2
- package/nitrogen/generated/shared/c++/{PartialAndroidSetupPlayerOptions.hpp → NativeAndroidSetupOptions.hpp} +11 -11
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +31 -15
- package/nitrogen/generated/shared/c++/NativeGateRequest.hpp +97 -0
- package/nitrogen/generated/shared/c++/{PartialIOSSetupPlayerOptions.hpp → NativeIOSSetupOptions.hpp} +11 -11
- package/nitrogen/generated/shared/c++/NativeRouteEntry.hpp +5 -5
- package/nitrogen/generated/shared/c++/NativeSetupPlayerOptions.hpp +138 -0
- package/nitrogen/generated/shared/c++/NativeUpdateOptions.hpp +9 -7
- package/nitrogen/generated/shared/c++/NavigationErrorType.hpp +8 -0
- package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -19
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +92 -0
- package/nitrogen/generated/shared/c++/NowPlayingMetadata.hpp +3 -19
- package/nitrogen/generated/shared/c++/NowPlayingUpdate.hpp +6 -2
- package/nitrogen/generated/shared/c++/Options.hpp +8 -8
- package/nitrogen/generated/shared/c++/{IosOutput.hpp → Output.hpp} +18 -18
- package/nitrogen/generated/shared/c++/OutputType.hpp +108 -0
- package/nitrogen/generated/shared/c++/PlaybackError.hpp +18 -3
- package/nitrogen/generated/shared/c++/PlaybackErrorKind.hpp +100 -0
- package/nitrogen/generated/shared/c++/PlaybackState.hpp +9 -9
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +9 -6
- package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +25 -25
- package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +24 -5
- package/nitrogen/generated/shared/c++/RetryConfig.hpp +10 -6
- package/nitrogen/generated/shared/c++/SearchMode.hpp +39 -15
- package/nitrogen/generated/shared/c++/SearchParams.hpp +9 -2
- package/nitrogen/generated/shared/c++/StallReason.hpp +76 -0
- package/nitrogen/generated/shared/c++/Track.hpp +24 -5
- package/nitrogen/generated/shared/c++/TrackRequest.hpp +93 -0
- package/nitrogen/generated/shared/c++/TransformableRequestConfig.hpp +6 -2
- package/package.json +20 -8
- package/src/AudioBrowser.ts +1 -1
- package/src/CLAUDE.md +52 -0
- package/src/codegen-no-config-variant.test.ts +70 -0
- package/src/features/battery.test.ts +79 -0
- package/src/features/browser-config.test.ts +110 -0
- package/src/features/browser-config.ts +293 -0
- package/src/features/browser.test.ts +19 -0
- package/src/features/browser.ts +59 -202
- package/src/features/carConnection.ts +36 -0
- package/src/features/equalizer.ts +1 -1
- package/src/features/errors.ts +71 -5
- package/src/features/favorites.ts +1 -1
- package/src/features/gate.test.ts +72 -0
- package/src/features/gate.ts +170 -0
- package/src/features/hook-wiring.test.ts +254 -0
- package/src/features/index.ts +2 -0
- package/src/features/metadata.ts +26 -15
- package/src/features/network.ts +1 -1
- package/src/features/nowPlaying.ts +37 -1
- package/src/features/output.ts +35 -22
- package/src/features/playback/controls.ts +8 -0
- package/src/features/playback/index.ts +1 -0
- 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 +32 -13
- package/src/features/playback/state.ts +1 -1
- package/src/features/playback/trackPlaybackTime.test.ts +76 -0
- package/src/features/playback/trackPlaybackTime.ts +101 -0
- package/src/features/playback/volume.ts +2 -2
- package/src/features/player/options.test.ts +38 -0
- package/src/features/player/options.ts +217 -110
- package/src/features/player/setup.test.ts +125 -0
- package/src/features/player/setup.ts +400 -175
- package/src/features/player/validateOptions.ts +19 -0
- package/src/features/queue/activeTrack.ts +1 -1
- package/src/features/queue/queue.test.ts +36 -0
- package/src/features/queue/queue.ts +17 -6
- package/src/features/queue/repeatMode.ts +1 -1
- package/src/features/queue/shuffle.ts +1 -1
- package/src/features/remoteControls.ts +13 -115
- package/src/features/sleepTimer.test.ts +172 -0
- package/src/features/sleepTimer.ts +32 -5
- package/src/native.ts +5 -0
- package/src/native.web.ts +4 -1
- package/src/public-surface.test.ts +107 -0
- package/src/specs/audio-browser.nitro.ts +132 -42
- package/src/test-utils/reactNativeStub.ts +60 -0
- package/src/types/browser-native.ts +19 -6
- package/src/types/browser-nodes.ts +160 -13
- package/src/types/browser.ts +377 -89
- package/src/utils/LazyNativeEmitter.ts +9 -8
- package/src/utils/NativeUpdatedValue.ts +0 -2
- package/src/utils/artwork.ts +30 -0
- package/src/utils/useDebug.test.ts +182 -0
- package/src/utils/useNativeUpdatedValue.test.ts +103 -0
- package/src/web/NativeAudioBrowser.playWhenReady.test.ts +301 -0
- package/src/web/NativeAudioBrowser.ts +366 -159
- package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +139 -0
- package/src/web/TrackPlayer/PlaybackStateMachine.ts +82 -0
- package/src/web/TrackPlayer/PlaybackTimer.test.ts +67 -0
- package/src/web/TrackPlayer/PlaybackTimer.ts +33 -0
- package/src/web/TrackPlayer/Player.dispatch.test.ts +229 -0
- package/src/web/TrackPlayer/Player.load.test.ts +86 -0
- package/src/web/TrackPlayer/Player.ts +113 -39
- package/src/web/TrackPlayer/PlayingStateFactory.test.ts +51 -0
- package/src/web/TrackPlayer/PlayingStateFactory.ts +24 -0
- package/src/web/TrackPlayer/QueueManager.test.ts +289 -0
- package/src/web/TrackPlayer/QueueManager.ts +302 -0
- package/src/web/TrackPlayer/QueuePlayer.ts +188 -0
- package/src/web/TrackPlayer/RepeatMode.ts +1 -1
- package/src/web/TrackPlayer/SleepTimer.ts +38 -3
- package/src/web/TrackPlayer/VolumeFader.ts +74 -0
- package/src/web/TrackPlayer/index.ts +2 -1
- package/src/web/browser/BrowserManager.ts +231 -62
- package/src/web/browser/NavigationErrorManager.ts +3 -1
- package/src/web/browser/SearchManager.ts +11 -15
- package/src/web/browser/__tests__/layer-resolver.test.ts +69 -0
- package/src/web/browser/assertBrowsePageShape.test.ts +27 -0
- package/src/web/browser/assertBrowsePageShape.ts +27 -0
- package/src/web/browser/classifyTrackNavigation.test.ts +41 -0
- package/src/web/browser/classifyTrackNavigation.ts +35 -0
- package/src/web/browser/parseSearchResponse.test.ts +25 -0
- package/src/web/browser/parseSearchResponse.ts +15 -0
- package/src/web/http/HttpClient.ts +5 -35
- package/src/web/http/RequestConfigBuilder.test.ts +59 -0
- package/src/web/http/RequestConfigBuilder.ts +120 -63
- package/src/web/playbackErrorKind.test.ts +55 -0
- package/src/web/playbackErrorKind.ts +56 -0
- package/src/web/player/MediaSessionManager.test.ts +220 -0
- package/src/web/player/MediaSessionManager.ts +151 -0
- package/src/web/player/NowPlayingManager.test.ts +46 -0
- package/src/web/player/NowPlayingManager.ts +6 -5
- package/src/web/player/OptionsManager.ts +12 -18
- package/src/web/player/RemoteCommandController.test.ts +171 -0
- package/src/web/player/RemoteCommandController.ts +166 -0
- package/android/src/main/java/com/audiobrowser/player/PlaybackProgressUpdateManager.kt +0 -59
- package/android/src/main/java/com/audiobrowser/util/RatingFactory.kt +0 -72
- package/android/src/main/java/com/audiobrowser/util/ResolvedTrackFactory.kt +0 -36
- package/android/src/test/java/com/audiobrowser/player/PlaybackProgressUpdateManagerTest.kt +0 -146
- package/ios/CarPlay/Track+CarPlay.swift +0 -63
- package/ios/Model/MediaURL.swift +0 -67
- package/ios/NowPlayingInfo/NowPlayingInfoCenter.swift +0 -9
- package/ios/Player/PlaybackProgressUpdateManager.swift +0 -68
- package/ios/Tests/PlaybackProgressUpdateManagerTests.swift +0 -166
- package/lib/commonjs/features/rating.js +0 -2
- package/lib/commonjs/features/rating.js.map +0 -1
- package/lib/commonjs/types/player.js +0 -2
- package/lib/commonjs/types/player.js.map +0 -1
- package/lib/commonjs/web/SimpleRouter.test.js +0 -174
- package/lib/commonjs/web/SimpleRouter.test.js.map +0 -1
- package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js +0 -293
- package/lib/commonjs/web/TrackPlayer/PlaylistPlayer.js.map +0 -1
- package/lib/module/features/rating.js +0 -2
- package/lib/module/features/rating.js.map +0 -1
- package/lib/module/types/player.js +0 -2
- package/lib/module/types/player.js.map +0 -1
- package/lib/module/web/SimpleRouter.test.js +0 -174
- package/lib/module/web/SimpleRouter.test.js.map +0 -1
- package/lib/module/web/TrackPlayer/PlaylistPlayer.js +0 -288
- package/lib/module/web/TrackPlayer/PlaylistPlayer.js.map +0 -1
- package/lib/typescript/src/features/rating.d.ts +0 -14
- package/lib/typescript/src/features/rating.d.ts.map +0 -1
- package/lib/typescript/src/native.web.d.ts +0 -3
- package/lib/typescript/src/native.web.d.ts.map +0 -1
- package/lib/typescript/src/types/player.d.ts +0 -64
- package/lib/typescript/src/types/player.d.ts.map +0 -1
- package/lib/typescript/src/web/NativeAudioBrowser.d.ts +0 -188
- package/lib/typescript/src/web/NativeAudioBrowser.d.ts.map +0 -1
- package/lib/typescript/src/web/SimpleRouter.d.ts +0 -40
- package/lib/typescript/src/web/SimpleRouter.d.ts.map +0 -1
- package/lib/typescript/src/web/SimpleRouter.test.d.ts +0 -2
- package/lib/typescript/src/web/SimpleRouter.test.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/Event.d.ts +0 -11
- package/lib/typescript/src/web/TrackPlayer/Event.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/Player.d.ts +0 -59
- package/lib/typescript/src/web/TrackPlayer/Player.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts +0 -39
- package/lib/typescript/src/web/TrackPlayer/PlaylistPlayer.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/RepeatMode.d.ts +0 -6
- package/lib/typescript/src/web/TrackPlayer/RepeatMode.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/SetupNotCalledError.d.ts +0 -4
- package/lib/typescript/src/web/TrackPlayer/SetupNotCalledError.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/SleepTimer.d.ts +0 -34
- package/lib/typescript/src/web/TrackPlayer/SleepTimer.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/State.d.ts +0 -13
- package/lib/typescript/src/web/TrackPlayer/State.d.ts.map +0 -1
- package/lib/typescript/src/web/TrackPlayer/index.d.ts +0 -5
- package/lib/typescript/src/web/TrackPlayer/index.d.ts.map +0 -1
- package/lib/typescript/src/web/browser/BrowserManager.d.ts +0 -154
- package/lib/typescript/src/web/browser/BrowserManager.d.ts.map +0 -1
- package/lib/typescript/src/web/browser/FavoriteManager.d.ts +0 -42
- package/lib/typescript/src/web/browser/FavoriteManager.d.ts.map +0 -1
- package/lib/typescript/src/web/browser/NavigationErrorManager.d.ts +0 -40
- package/lib/typescript/src/web/browser/NavigationErrorManager.d.ts.map +0 -1
- package/lib/typescript/src/web/browser/SearchManager.d.ts +0 -22
- package/lib/typescript/src/web/browser/SearchManager.d.ts.map +0 -1
- package/lib/typescript/src/web/http/HttpClient.d.ts +0 -25
- package/lib/typescript/src/web/http/HttpClient.d.ts.map +0 -1
- package/lib/typescript/src/web/http/RequestConfigBuilder.d.ts +0 -98
- package/lib/typescript/src/web/http/RequestConfigBuilder.d.ts.map +0 -1
- package/lib/typescript/src/web/player/NowPlayingManager.d.ts +0 -34
- package/lib/typescript/src/web/player/NowPlayingManager.d.ts.map +0 -1
- package/lib/typescript/src/web/player/OptionsManager.d.ts +0 -32
- package/lib/typescript/src/web/player/OptionsManager.d.ts.map +0 -1
- package/lib/typescript/src/web/util/BrowserPathHelper.d.ts +0 -69
- package/lib/typescript/src/web/util/BrowserPathHelper.d.ts.map +0 -1
- package/lib/typescript/src/web/util/shuffle.d.ts +0 -8
- package/lib/typescript/src/web/util/shuffle.d.ts.map +0 -1
- package/nitrogen/generated/android/c++/JAndroidUpdateOptions.hpp +0 -86
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.hpp +0 -147
- package/nitrogen/generated/android/c++/JFunc_void_IosOutput.hpp +0 -80
- package/nitrogen/generated/android/c++/JFunc_void_RemoteSetRatingEvent.hpp +0 -87
- package/nitrogen/generated/android/c++/JHeartRating.hpp +0 -57
- package/nitrogen/generated/android/c++/JIosOutputType.hpp +0 -85
- package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
- package/nitrogen/generated/android/c++/JPartialSetupPlayerOptions.hpp +0 -96
- package/nitrogen/generated/android/c++/JPercentageRating.hpp +0 -57
- package/nitrogen/generated/android/c++/JRatingType.hpp +0 -73
- package/nitrogen/generated/android/c++/JRemoteSetRatingEvent.hpp +0 -66
- package/nitrogen/generated/android/c++/JStarRating.hpp +0 -57
- package/nitrogen/generated/android/c++/JUpdateOptions.hpp +0 -105
- package/nitrogen/generated/android/c++/JVariant_HeartRating_ThumbsRating_StarRating_PercentageRating.cpp +0 -34
- package/nitrogen/generated/android/c++/JVariant_HeartRating_ThumbsRating_StarRating_PercentageRating.hpp +0 -105
- 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/AndroidUpdateOptions.kt +0 -50
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IosOutputType.kt +0 -32
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PartialSetupPlayerOptions.kt +0 -47
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/UpdateOptions.kt +0 -53
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_HeartRating_ThumbsRating_StarRating_PercentageRating.kt +0 -77
- package/nitrogen/generated/ios/swift/AndroidUpdateOptions.swift +0 -119
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError______BrowserSourceCallbackParam.swift +0 -68
- package/nitrogen/generated/ios/swift/HeartRating.swift +0 -29
- package/nitrogen/generated/ios/swift/IosOutputType.swift +0 -76
- package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
- package/nitrogen/generated/ios/swift/PartialSetupPlayerOptions.swift +0 -101
- package/nitrogen/generated/ios/swift/PercentageRating.swift +0 -29
- package/nitrogen/generated/ios/swift/RatingType.swift +0 -60
- package/nitrogen/generated/ios/swift/RemoteSetRatingEvent.swift +0 -58
- package/nitrogen/generated/ios/swift/StarRating.swift +0 -29
- package/nitrogen/generated/ios/swift/ThumbsRating.swift +0 -29
- package/nitrogen/generated/ios/swift/UpdateOptions.swift +0 -143
- package/nitrogen/generated/ios/swift/Variant_HeartRating_ThumbsRating_StarRating_PercentageRating.swift +0 -20
- package/nitrogen/generated/shared/c++/AndroidUpdateOptions.hpp +0 -109
- package/nitrogen/generated/shared/c++/IosOutputType.hpp +0 -112
- package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
- package/nitrogen/generated/shared/c++/PartialSetupPlayerOptions.hpp +0 -104
- package/nitrogen/generated/shared/c++/RatingType.hpp +0 -96
- package/nitrogen/generated/shared/c++/RemoteSetRatingEvent.hpp +0 -94
- package/nitrogen/generated/shared/c++/UpdateOptions.hpp +0 -111
- package/src/features/rating.ts +0 -17
- package/src/types/player.ts +0 -69
- package/src/web/TrackPlayer/PlaylistPlayer.ts +0 -346
|
@@ -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
|
|
|
@@ -24,20 +28,52 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
24
28
|
/// Shared instance for CarPlay access. Set when HybridAudioBrowser is created.
|
|
25
29
|
private(set) nonisolated(unsafe) weak static var shared: HybridAudioBrowser?
|
|
26
30
|
|
|
31
|
+
/// Fires when a new instance becomes `shared` (e.g. on a JS runtime reload),
|
|
32
|
+
/// so connected external controllers (CarPlay) can re-subscribe — their
|
|
33
|
+
/// listeners sit on the previous instance's emitters and would otherwise go
|
|
34
|
+
/// silent.
|
|
35
|
+
static let instanceChangedEmitter = Emitter<Void>()
|
|
36
|
+
|
|
27
37
|
// MARK: - Private Properties
|
|
28
38
|
|
|
39
|
+
/// Main-confined by convention, not annotation: `deinit` reads it as a
|
|
40
|
+
/// safety net and can't hop. Reach it from `onMainActor` / `@MainActor`.
|
|
29
41
|
private var player: TrackPlayer?
|
|
30
42
|
private let networkMonitor = NetworkMonitor()
|
|
43
|
+
private let playbackStateStore = PlaybackStateStore()
|
|
31
44
|
let browserManager = BrowserManager()
|
|
32
45
|
private let trackSelector: TrackSelector
|
|
33
46
|
private var volumeObservation: NSKeyValueObservation?
|
|
34
47
|
private var routeChangeObserver: NSObjectProtocol?
|
|
35
|
-
private var
|
|
36
|
-
private
|
|
48
|
+
private var interruptionObserver: NSObjectProtocol?
|
|
49
|
+
private var mediaServicesResetObserver: NSObjectProtocol?
|
|
50
|
+
/// Re-applies our resolved audio-session category. Captured at setup so a
|
|
51
|
+
/// media-services reset (which clears the category) can restore it.
|
|
52
|
+
@MainActor private var applyAudioSessionCategory: () -> Void = {}
|
|
53
|
+
@MainActor private var nowPlayingOverride: NowPlayingUpdate?
|
|
54
|
+
/// When false, the now-playing surface uses the raw track fields (override + formatter ignored).
|
|
55
|
+
@MainActor private var nowPlayingMetadataEnabled = true
|
|
56
|
+
/// Customizes the now-playing title/subtitle from the track + live timed metadata + playback state.
|
|
57
|
+
@MainActor private var nowPlayingMetadataFormatter: ((_ params: FormatNowPlayingParams) -> Promise<NowPlayingUpdate?>)?
|
|
58
|
+
/// Initial player state staged before the player exists — from setup options or the imperative
|
|
59
|
+
/// setters called pre-setup. Strict last-write-wins; consumed when the player comes up.
|
|
60
|
+
@MainActor private var pendingPlayWhenReady: Bool?
|
|
61
|
+
@MainActor private var pendingRepeatMode: RepeatMode?
|
|
62
|
+
/// Latest live timed (ICY/ID3) metadata, passed to the formatter. Cleared on track change.
|
|
63
|
+
@MainActor private var latestTimedMetadata: TimedMetadata?
|
|
64
|
+
@MainActor private let playerOptions = PlayerUpdateOptions()
|
|
37
65
|
|
|
38
66
|
/// Configured playback rates for the playback-rate capability (for CarPlay rate cycling)
|
|
39
|
-
var playbackRates: [Double] { playerOptions.playbackRates }
|
|
40
|
-
|
|
67
|
+
@MainActor var playbackRates: [Double] { playerOptions.playbackRates }
|
|
68
|
+
@MainActor var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
|
|
69
|
+
@MainActor var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] {
|
|
70
|
+
playerOptions.carPlayNowPlayingButtons
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Written from the JS thread, the cooperative pool and MainActor, with a
|
|
74
|
+
/// `didSet` that calls back into Nitro. MainActor-isolated so the compiler
|
|
75
|
+
/// requires an `onMainActor` hop at every access.
|
|
76
|
+
@MainActor private var lastNavigationError: NavigationError? {
|
|
41
77
|
didSet {
|
|
42
78
|
// Skip if both nil (no real change)
|
|
43
79
|
guard oldValue != nil || lastNavigationError != nil else { return }
|
|
@@ -45,7 +81,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
45
81
|
}
|
|
46
82
|
}
|
|
47
83
|
|
|
48
|
-
|
|
84
|
+
/// Isolated for the same reason as ``lastNavigationError``.
|
|
85
|
+
@MainActor private var lastFormattedNavigationError: FormattedNavigationError? {
|
|
49
86
|
didSet {
|
|
50
87
|
// Skip if both nil (no real change)
|
|
51
88
|
guard oldValue != nil || lastFormattedNavigationError != nil else { return }
|
|
@@ -63,7 +100,21 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
63
100
|
public let queueChangedEmitter = Emitter<[Track]>()
|
|
64
101
|
public let navigationErrorEmitter = Emitter<NavigationErrorEvent>()
|
|
65
102
|
public let repeatModeChangedEmitter = Emitter<RepeatModeChangedEvent>()
|
|
103
|
+
public let shuffleChangedEmitter = Emitter<Bool>()
|
|
66
104
|
public let externalContentChangedEmitter = Emitter<String>()
|
|
105
|
+
/// Signals that the gate's active state changed (set or cleared). Carries
|
|
106
|
+
/// the active flag; per-request chrome is obtained at each serve site via
|
|
107
|
+
/// `gateDecision(for:)`, so the emitter only tells surfaces to re-render.
|
|
108
|
+
public let gateChangedEmitter = Emitter<Bool>()
|
|
109
|
+
/// Fired when a voice media intent (`handlePlayMediaIntent`) successfully
|
|
110
|
+
/// starts playback. CarPlay surfaces the Now Playing template in response, so
|
|
111
|
+
/// the user lands on the playing station (with the rest of the results in Up
|
|
112
|
+
/// Next) rather than the screen they invoked Siri from. No-op where unobserved.
|
|
113
|
+
public let showNowPlayingRequestedEmitter = Emitter<Void>()
|
|
114
|
+
/// Fires after `updateOptions` applies a change, so external-surface managers
|
|
115
|
+
/// (CarPlay) can refresh UI driven by now-runtime-updatable options
|
|
116
|
+
/// (now-playing buttons, Up Next).
|
|
117
|
+
public let playerOptionsChangedEmitter = Emitter<Void>()
|
|
67
118
|
|
|
68
119
|
// MARK: - Thread Safety
|
|
69
120
|
|
|
@@ -71,9 +122,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
71
122
|
/// because DispatchQueue.main.sync blocks the caller — no concurrent access window.
|
|
72
123
|
private func onMainActor<T>(_ work: @MainActor () -> T) -> T {
|
|
73
124
|
if Thread.isMainThread {
|
|
74
|
-
|
|
125
|
+
MainActor.assumeIsolated { UncheckedSendableBox(value: work()) }.value
|
|
75
126
|
} else {
|
|
76
|
-
|
|
127
|
+
DispatchQueue.main.sync {
|
|
77
128
|
MainActor.assumeIsolated { UncheckedSendableBox(value: work()) }.value
|
|
78
129
|
}
|
|
79
130
|
}
|
|
@@ -81,10 +132,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
81
132
|
|
|
82
133
|
private func onMainActor<T>(_ work: @MainActor () throws -> T) throws -> T {
|
|
83
134
|
if Thread.isMainThread {
|
|
84
|
-
|
|
135
|
+
try MainActor.assumeIsolated { try UncheckedSendableBox(value: work()) }.value
|
|
85
136
|
} else {
|
|
86
|
-
|
|
87
|
-
try MainActor.assumeIsolated { UncheckedSendableBox(value:
|
|
137
|
+
try DispatchQueue.main.sync {
|
|
138
|
+
try MainActor.assumeIsolated { try UncheckedSendableBox(value: work()) }.value
|
|
88
139
|
}
|
|
89
140
|
}
|
|
90
141
|
}
|
|
@@ -111,11 +162,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
111
162
|
}
|
|
112
163
|
|
|
113
164
|
public var configuration: NativeBrowserConfiguration = .init(
|
|
114
|
-
path: nil, request: nil, media: nil, artwork: nil, routes: nil,
|
|
115
|
-
singleTrack: nil, handleTrackLoad: nil,
|
|
116
|
-
|
|
165
|
+
path: nil, request: nil, requestResolver: nil, browse: nil, browseResolver: nil, media: nil, artwork: nil, nowPlayingArtwork: nil, routes: nil,
|
|
166
|
+
singleTrack: nil, handleTrackLoad: nil,
|
|
167
|
+
androidControllerOfflineError: nil, carPlayLoadingTitle: nil, resolveAlbumUrl: nil, formatNavigationError: nil,
|
|
117
168
|
) {
|
|
118
169
|
didSet {
|
|
170
|
+
// Copied here, on the setter's thread: the Task below outlives this call,
|
|
171
|
+
// and Nitro can reassign `configuration` — a C++ struct holding
|
|
172
|
+
// std::string/std::function — from the JS thread while it runs.
|
|
173
|
+
let configuredPath = configuration.path
|
|
119
174
|
onMainActor { browserManager.config = BrowserConfig(from: configuration) }
|
|
120
175
|
|
|
121
176
|
// Query tabs and navigate to initial path after config is set (matches Kotlin behavior)
|
|
@@ -126,12 +181,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
126
181
|
Task {
|
|
127
182
|
let tabs = try? await browserManager.queryTabs()
|
|
128
183
|
// Navigate to configured path, first tab, or "/"
|
|
129
|
-
let initialPath =
|
|
184
|
+
let initialPath = configuredPath ?? tabs?.first?.url ?? "/"
|
|
130
185
|
// Clear error before navigation (matches Kotlin clearNavigationError())
|
|
131
|
-
|
|
132
|
-
lastNavigationError = nil
|
|
133
|
-
lastFormattedNavigationError = nil
|
|
134
|
-
}
|
|
186
|
+
clearNavigationError()
|
|
135
187
|
do {
|
|
136
188
|
try await browserManager.navigate(initialPath)
|
|
137
189
|
} catch {
|
|
@@ -149,6 +201,25 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
149
201
|
public var onNavigationError: (NavigationErrorEvent) -> Void = { _ in }
|
|
150
202
|
public var onFormattedNavigationError: (FormattedNavigationError?) -> Void = { _ in }
|
|
151
203
|
|
|
204
|
+
/// Per-request gate decision, set by JS. Native calls it at a serve site to
|
|
205
|
+
/// learn whether a browse path / search interaction should be gated and with
|
|
206
|
+
/// what chrome. The double `Promise` mirrors every other native→JS
|
|
207
|
+
/// value-returning callback (e.g. `searchCallback`): the outer resolves to the
|
|
208
|
+
/// JS-side Promise, the inner to the decision.
|
|
209
|
+
///
|
|
210
|
+
/// Default fails CLOSED (`gated: true`). This default is only consulted in the
|
|
211
|
+
/// init window where `hasResolver` was already recorded true but JS hasn't
|
|
212
|
+
/// (re)bound the real resolver yet (JS reload / instance churn re-seeding gate
|
|
213
|
+
/// state, e.g. CarPlay re-seeding `isGated` at scene connect). Serving content
|
|
214
|
+
/// in that window would be a fail-open leak — same direction as the
|
|
215
|
+
/// resolver-error path in `gateDecision`. Matches Android's default.
|
|
216
|
+
public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = { _ in
|
|
217
|
+
Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Fired when a request is gated (served the gate). Set by JS.
|
|
221
|
+
public var onGate: (GateEvent) -> Void = { _ in }
|
|
222
|
+
|
|
152
223
|
// MARK: - Player Callbacks
|
|
153
224
|
|
|
154
225
|
public var onChapterMetadata: ([ChapterMetadata]) -> Void = { _ in }
|
|
@@ -159,6 +230,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
159
230
|
public var onPlaybackPlayWhenReadyChanged: (PlaybackPlayWhenReadyChangedEvent) -> Void = { _ in }
|
|
160
231
|
public var onPlaybackPlayingState: (PlayingState) -> Void = { _ in }
|
|
161
232
|
public var onPlaybackProgressUpdated: (PlaybackProgressUpdatedEvent) -> Void = { _ in }
|
|
233
|
+
public var onPlaybackInterval: () -> Void = {}
|
|
162
234
|
public var onPlaybackQueueEnded: (PlaybackQueueEndedEvent) -> Void = { _ in }
|
|
163
235
|
public var onPlaybackQueueChanged: ([Track]) -> Void = { _ in }
|
|
164
236
|
public var onPlaybackRepeatModeChanged: (RepeatModeChangedEvent) -> Void = { _ in }
|
|
@@ -168,11 +240,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
168
240
|
|
|
169
241
|
// MARK: - Remote Callbacks
|
|
170
242
|
|
|
171
|
-
public var onRemoteBookmark: () -> Void = {}
|
|
172
|
-
public var onRemoteDislike: () -> Void = {}
|
|
173
243
|
public var onRemoteJumpBackward: (RemoteJumpBackwardEvent) -> Void = { _ in }
|
|
174
244
|
public var onRemoteJumpForward: (RemoteJumpForwardEvent) -> Void = { _ in }
|
|
175
|
-
public var onRemoteLike: () -> Void = {}
|
|
176
245
|
public var onRemoteNext: () -> Void = {}
|
|
177
246
|
public var onRemotePause: () -> Void = {}
|
|
178
247
|
public var onRemotePlay: () -> Void = {}
|
|
@@ -180,17 +249,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
180
249
|
public var onRemotePlaySearch: (RemotePlaySearchEvent) -> Void = { _ in }
|
|
181
250
|
public var onRemotePrevious: () -> Void = {}
|
|
182
251
|
public var onRemoteSeek: (RemoteSeekEvent) -> Void = { _ in }
|
|
183
|
-
public var onRemoteSetRating: (RemoteSetRatingEvent) -> Void = { _ in }
|
|
184
252
|
public var onRemoteSkip: (RemoteSkipEvent) -> Void = { _ in }
|
|
185
253
|
public var onRemoteStop: () -> Void = {}
|
|
186
254
|
|
|
187
255
|
// MARK: - Remote Handlers (optional overrides from JS)
|
|
188
256
|
|
|
189
|
-
public var handleRemoteBookmark: (() -> Void)?
|
|
190
|
-
public var handleRemoteDislike: (() -> Void)?
|
|
191
257
|
public var handleRemoteJumpBackward: ((RemoteJumpBackwardEvent) -> Void)?
|
|
192
258
|
public var handleRemoteJumpForward: ((RemoteJumpForwardEvent) -> Void)?
|
|
193
|
-
public var handleRemoteLike: (() -> Void)?
|
|
194
259
|
public var handleRemoteNext: (() -> Void)?
|
|
195
260
|
public var handleRemotePause: (() -> Void)?
|
|
196
261
|
public var handleRemotePlay: (() -> Void)?
|
|
@@ -208,10 +273,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
208
273
|
public var onNowPlayingChanged: (NowPlayingMetadata) -> Void = { _ in }
|
|
209
274
|
public var onOnlineChanged: (Bool) -> Void = { _ in } {
|
|
210
275
|
didSet {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// Immediately notify current state
|
|
276
|
+
// The monitor->JS bridge is wired centrally in setupPlayer (so it doesn't depend on a JS
|
|
277
|
+
// subscription and can also drive the now-playing re-render + player reconnect). Here we only
|
|
278
|
+
// push the current state to a newly-attached subscriber.
|
|
215
279
|
onOnlineChanged(networkMonitor.isOnline)
|
|
216
280
|
}
|
|
217
281
|
}
|
|
@@ -225,7 +289,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
225
289
|
}
|
|
226
290
|
}
|
|
227
291
|
|
|
228
|
-
public var
|
|
292
|
+
public var onOutputChanged: (Output) -> Void = { _ in } {
|
|
229
293
|
didSet {
|
|
230
294
|
setupRouteChangeObserver()
|
|
231
295
|
}
|
|
@@ -245,6 +309,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
245
309
|
HybridAudioBrowser.shared?.player?.destroy()
|
|
246
310
|
playerAndConfiguredBrowser.reset()
|
|
247
311
|
HybridAudioBrowser.shared = self
|
|
312
|
+
// Tell connected external controllers (CarPlay) to re-subscribe against
|
|
313
|
+
// this instance. Unconditional: `shared` is weak, so on a JS reload the
|
|
314
|
+
// old instance may already be gone — "shared was nil" does not mean no
|
|
315
|
+
// controller is subscribed to a dead instance's emitters.
|
|
316
|
+
HybridAudioBrowser.instanceChangedEmitter.emit(())
|
|
248
317
|
}
|
|
249
318
|
setupEmitterToNitroForwarding()
|
|
250
319
|
setupBrowserCallbacks()
|
|
@@ -313,33 +382,50 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
313
382
|
private func handleNavigationError(_ error: Error, path: String) {
|
|
314
383
|
let navError = NavigationError.from(error)
|
|
315
384
|
|
|
316
|
-
lastNavigationError = navError
|
|
385
|
+
onMainActor { lastNavigationError = navError }
|
|
317
386
|
|
|
318
387
|
// Format the error (async if using JS callback, sync for defaults)
|
|
319
388
|
let defaultFormatted = navError.defaultFormatted()
|
|
320
389
|
if let formatter = onMainActor({ browserManager.config.formatNavigationError }) {
|
|
321
390
|
let params = FormatNavigationErrorParams(error: navError, defaultFormatted: defaultFormatted, path: path)
|
|
322
|
-
// Dispatch to main thread for the Nitro bridge call to avoid C++ noexcept crashes
|
|
391
|
+
// Dispatch to main thread for the Nitro bridge call to avoid C++ noexcept crashes.
|
|
392
|
+
// The result callbacks are @Sendable: a plain closure would inherit MainActor
|
|
393
|
+
// isolation from this block, and Nitro resolves its promises synchronously on the
|
|
394
|
+
// JS thread — Swift 6.2's dynamic isolation check then traps on closure entry.
|
|
395
|
+
// They therefore hop back explicitly, like every other writer.
|
|
323
396
|
DispatchQueue.main.async { [weak self] in
|
|
324
397
|
formatter(params)
|
|
325
|
-
.then { customDisplay in
|
|
326
|
-
self?.
|
|
398
|
+
.then { @Sendable [weak self] customDisplay in
|
|
399
|
+
self?.setFormattedNavigationError(customDisplay ?? defaultFormatted)
|
|
327
400
|
}
|
|
328
|
-
.catch { _ in
|
|
329
|
-
self?.
|
|
401
|
+
.catch { @Sendable [weak self] _ in
|
|
402
|
+
self?.setFormattedNavigationError(defaultFormatted)
|
|
330
403
|
}
|
|
331
404
|
}
|
|
332
405
|
} else {
|
|
333
|
-
|
|
406
|
+
setFormattedNavigationError(defaultFormatted)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/// Callable from any thread — the Nitro promise callbacks below resolve on
|
|
411
|
+
/// the JS thread.
|
|
412
|
+
private func setFormattedNavigationError(_ formatted: FormattedNavigationError?) {
|
|
413
|
+
onMainActor { lastFormattedNavigationError = formatted }
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/// Synchronous on purpose: the `didSet`s notify JS, which has to happen
|
|
417
|
+
/// before the navigation that may set them again.
|
|
418
|
+
private func clearNavigationError() {
|
|
419
|
+
onMainActor {
|
|
420
|
+
lastNavigationError = nil
|
|
421
|
+
lastFormattedNavigationError = nil
|
|
334
422
|
}
|
|
335
423
|
}
|
|
336
424
|
|
|
337
425
|
// MARK: - Browser Methods
|
|
338
426
|
|
|
339
427
|
public func navigatePath(path: String) throws {
|
|
340
|
-
|
|
341
|
-
lastNavigationError = nil
|
|
342
|
-
lastFormattedNavigationError = nil
|
|
428
|
+
clearNavigationError()
|
|
343
429
|
Task {
|
|
344
430
|
do {
|
|
345
431
|
try await browserManager.navigate(path)
|
|
@@ -350,15 +436,18 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
350
436
|
}
|
|
351
437
|
|
|
352
438
|
public func navigateTrack(track: Track) throws {
|
|
353
|
-
|
|
439
|
+
// @MainActor: `player` is main-confined, and `trackSelector.select` is
|
|
440
|
+
// MainActor anyway — a bare Task read the reference off the cooperative
|
|
441
|
+
// pool, racing the assignment in `setupPlayer`.
|
|
442
|
+
Task { @MainActor in
|
|
354
443
|
guard let player else { return }
|
|
355
444
|
let result = await trackSelector.select(track: track, player: player)
|
|
356
445
|
switch result {
|
|
357
|
-
case .play(
|
|
446
|
+
case let .play(intent):
|
|
358
447
|
executePlayback(intent)
|
|
359
448
|
case .intercepted:
|
|
360
449
|
break
|
|
361
|
-
case .browse(
|
|
450
|
+
case let .browse(url):
|
|
362
451
|
navigateToBrowsableUrl(url)
|
|
363
452
|
case .none:
|
|
364
453
|
break
|
|
@@ -369,24 +458,22 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
369
458
|
private func executePlayback(_ intent: TrackSelector.PlaybackIntent) {
|
|
370
459
|
onMainActor {
|
|
371
460
|
switch intent {
|
|
372
|
-
case .skipTo(
|
|
461
|
+
case let .skipTo(index):
|
|
373
462
|
do {
|
|
374
463
|
try player?.skipTo(index, playWhenReady: true)
|
|
375
464
|
} catch {
|
|
376
465
|
logger.error("Failed to skip to track at index \(index): \(error)")
|
|
377
466
|
}
|
|
378
|
-
case .setQueue(
|
|
467
|
+
case let .setQueue(tracks, startIndex, sourcePath):
|
|
379
468
|
player?.setQueue(tracks, initialIndex: startIndex, playWhenReady: true, sourcePath: sourcePath)
|
|
380
|
-
case .loadTrack(
|
|
469
|
+
case let .loadTrack(track):
|
|
381
470
|
player?.load(track, playWhenReady: true)
|
|
382
471
|
}
|
|
383
472
|
}
|
|
384
473
|
}
|
|
385
474
|
|
|
386
475
|
private func navigateToBrowsableUrl(_ url: String) {
|
|
387
|
-
|
|
388
|
-
lastNavigationError = nil
|
|
389
|
-
lastFormattedNavigationError = nil
|
|
476
|
+
clearNavigationError()
|
|
390
477
|
Task {
|
|
391
478
|
do {
|
|
392
479
|
try await browserManager.navigate(url)
|
|
@@ -396,7 +483,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
396
483
|
}
|
|
397
484
|
}
|
|
398
485
|
|
|
399
|
-
public func
|
|
486
|
+
public func search(query: String) throws -> Promise<[Track]> {
|
|
400
487
|
Promise.async { [weak self] in
|
|
401
488
|
guard let self else { return [] }
|
|
402
489
|
let resolved = try await browserManager.search(query)
|
|
@@ -409,13 +496,19 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
409
496
|
}
|
|
410
497
|
|
|
411
498
|
public func getNavigationError() throws -> NavigationError? {
|
|
412
|
-
lastNavigationError
|
|
499
|
+
onMainActor { lastNavigationError }
|
|
413
500
|
}
|
|
414
501
|
|
|
415
502
|
public func getFormattedNavigationError() throws -> FormattedNavigationError? {
|
|
416
|
-
lastFormattedNavigationError
|
|
503
|
+
onMainActor { lastFormattedNavigationError }
|
|
417
504
|
}
|
|
418
505
|
|
|
506
|
+
/// Internal signal sent on `externalContentChangedEmitter` to tell CarPlay to
|
|
507
|
+
/// refresh every displayed template. Not a real path (paths are always
|
|
508
|
+
/// `/…`), so it can't collide with one; kept internal — the public API is
|
|
509
|
+
/// `invalidateAllContent()`.
|
|
510
|
+
static let invalidateAllSentinel = "__rnab_invalidate_all__"
|
|
511
|
+
|
|
419
512
|
public func notifyContentChanged(path: String) throws {
|
|
420
513
|
onMainActor { browserManager.invalidateContentCache(path) }
|
|
421
514
|
|
|
@@ -434,22 +527,168 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
434
527
|
}
|
|
435
528
|
}
|
|
436
529
|
|
|
530
|
+
public func invalidateAllContent() throws {
|
|
531
|
+
onMainActor { browserManager.clearContentCache() }
|
|
532
|
+
|
|
533
|
+
// Tell external controllers (CarPlay) to refresh every displayed template.
|
|
534
|
+
externalContentChangedEmitter.emit(Self.invalidateAllSentinel)
|
|
535
|
+
|
|
536
|
+
// Re-query the tabs source too: a callback source can resolve to new
|
|
537
|
+
// titles after an invalidation (e.g. a locale switch). BrowserManager
|
|
538
|
+
// emits tabsChanged only when the result actually differs, and a failed
|
|
539
|
+
// re-query keeps the current tabs. (Android needs no equivalent — its
|
|
540
|
+
// root re-query already runs queryTabs(); the web stub re-queries tabs on
|
|
541
|
+
// the re-navigation.)
|
|
542
|
+
Task {
|
|
543
|
+
_ = try? await browserManager.queryTabs()
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Re-resolve the JS-facing current path with the cache cleared.
|
|
547
|
+
Task {
|
|
548
|
+
do {
|
|
549
|
+
try await browserManager.refresh()
|
|
550
|
+
} catch {
|
|
551
|
+
handleNavigationError(error, path: onMainActor { browserManager.getPath() })
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
437
556
|
public func setFavorites(favorites: [String]) throws {
|
|
438
557
|
onMainActor { browserManager.setFavorites(favorites) }
|
|
439
558
|
}
|
|
440
559
|
|
|
560
|
+
// MARK: - Gate
|
|
561
|
+
|
|
562
|
+
/// The gate's default chrome, if one was set. Served when the resolver
|
|
563
|
+
/// returns a gated decision with no per-request override, and for the static
|
|
564
|
+
/// gate (no resolver). May be nil for a resolver-only gate.
|
|
565
|
+
private(set) var defaultChrome: Gate?
|
|
566
|
+
|
|
567
|
+
/// Whether JS installed a per-request resolver. When false the gate is static
|
|
568
|
+
/// — every request is gated with `defaultChrome` and the JS hop is skipped.
|
|
569
|
+
private(set) var hasResolver = false
|
|
570
|
+
|
|
571
|
+
/// True once `setGate` runs, until `clearGate`. While active, external
|
|
572
|
+
/// surfaces (CarPlay) keep their tabs visible but consult `gateDecision(for:)`
|
|
573
|
+
/// per request, rendering the gate chrome where gated and refusing
|
|
574
|
+
/// external-surface search. Playback, the queue and Now Playing are
|
|
575
|
+
/// unaffected — a gate blocks finding content, never hearing it.
|
|
576
|
+
private(set) var isGateActive = false
|
|
577
|
+
|
|
578
|
+
/// The minimal built-in gate served when a request is gated but neither a
|
|
579
|
+
/// per-request override nor a stored default chrome exists (resolver-only
|
|
580
|
+
/// gate returning `true`).
|
|
581
|
+
static let builtInGate = Gate(title: "Unavailable", message: nil)
|
|
582
|
+
|
|
583
|
+
/// The outcome of a single gate decision: whether to gate, and (if so) the
|
|
584
|
+
/// chrome to render.
|
|
585
|
+
struct GateOutcome {
|
|
586
|
+
let gated: Bool
|
|
587
|
+
let chrome: Gate?
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
public func setGate(gate: Gate?, hasResolver: Bool) throws {
|
|
591
|
+
onMainActor {
|
|
592
|
+
defaultChrome = gate
|
|
593
|
+
self.hasResolver = hasResolver
|
|
594
|
+
isGateActive = true
|
|
595
|
+
gateChangedEmitter.emit(true)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
public func clearGate() throws {
|
|
600
|
+
onMainActor {
|
|
601
|
+
guard isGateActive else { return }
|
|
602
|
+
defaultChrome = nil
|
|
603
|
+
hasResolver = false
|
|
604
|
+
isGateActive = false
|
|
605
|
+
gateChangedEmitter.emit(false)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/// The single choke point for every gate enforcement site. Asks whether
|
|
610
|
+
/// `request` should be gated and, if so, with what chrome (override → stored
|
|
611
|
+
/// default → built-in). When no resolver is installed the gate is static and
|
|
612
|
+
/// resolves without a JS hop.
|
|
613
|
+
///
|
|
614
|
+
/// Resolver errors **fail CLOSED by design**: a thrown/rejected resolver, or a
|
|
615
|
+
/// rejection at either `.await()` layer (bridge tear-down on JS reload, the JS
|
|
616
|
+
/// runtime mid-reload, a serialization error) cannot prove the request is
|
|
617
|
+
/// allowed — and a gate exists to withhold content, so serving on error would
|
|
618
|
+
/// be a content leak (a paywall bypass). On error we gate with the stored
|
|
619
|
+
/// default / built-in chrome so the user still sees a coherent gate page. A
|
|
620
|
+
/// *successful* resolver returning `gated: false` still allows the request.
|
|
621
|
+
@MainActor
|
|
622
|
+
func gateDecision(for request: NativeGateRequest) async -> GateOutcome {
|
|
623
|
+
guard isGateActive else { return GateOutcome(gated: false, chrome: nil) }
|
|
624
|
+
if !hasResolver { return GateOutcome(gated: true, chrome: defaultChrome ?? Self.builtInGate) }
|
|
625
|
+
guard let decision = try? await resolveGate(request).await().await() else {
|
|
626
|
+
// Resolver error: fail CLOSED (see doc comment) — gate with the default /
|
|
627
|
+
// built-in so the gate page still renders rather than leaking content.
|
|
628
|
+
return GateOutcome(gated: true, chrome: defaultChrome ?? Self.builtInGate)
|
|
629
|
+
}
|
|
630
|
+
guard decision.gated else { return GateOutcome(gated: false, chrome: nil) } // explicit allow
|
|
631
|
+
return GateOutcome(gated: true, chrome: decision.gate ?? defaultChrome ?? Self.builtInGate)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// MARK: - Car Connection (CarPlay)
|
|
635
|
+
|
|
636
|
+
/// Static because the CarPlay scene can connect before the JS runtime
|
|
637
|
+
/// creates the shared instance (cold start in the car).
|
|
638
|
+
private nonisolated(unsafe) static var carPlayConnected = false
|
|
639
|
+
|
|
640
|
+
/// Called by the CarPlay scene delegate (via `RNABCarPlayController`) on
|
|
641
|
+
/// scene connect/disconnect — not from controller start/stop, which also
|
|
642
|
+
/// cycle on a JS reload and would emit spurious connection changes.
|
|
643
|
+
static func setCarPlayConnected(_ connected: Bool) {
|
|
644
|
+
guard carPlayConnected != connected else { return }
|
|
645
|
+
carPlayConnected = connected
|
|
646
|
+
shared?.onCarConnectedChanged(connected)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
public func isCarConnected() throws -> Bool {
|
|
650
|
+
HybridAudioBrowser.carPlayConnected
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
public var onCarConnectedChanged: (Bool) -> Void = { _ in } {
|
|
654
|
+
didSet {
|
|
655
|
+
// Immediately notify current state (the scene may have connected before
|
|
656
|
+
// this JS runtime subscribed).
|
|
657
|
+
onCarConnectedChanged(HybridAudioBrowser.carPlayConnected)
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
441
661
|
// MARK: - Player Setup
|
|
442
662
|
|
|
443
|
-
public func setupPlayer(options:
|
|
663
|
+
public func setupPlayer(options: NativeSetupPlayerOptions) throws -> Promise<Void> {
|
|
444
664
|
Promise.async {
|
|
445
|
-
// Configure audio session
|
|
665
|
+
// Configure the audio session from the iOS setup options (category, mode, route policy, and
|
|
666
|
+
// options like `allowBluetooth` / `allowAirPlay`), but do NOT activate it here. Activating a
|
|
667
|
+
// non-mixable `.playback` session interrupts other apps' audio — so activating at setup
|
|
668
|
+
// pauses whatever the user is listening to (Safari, Spotify, a podcast) the instant our app
|
|
669
|
+
// launches, before they play anything. Setting the category is silent; only activation
|
|
670
|
+
// interrupts. The session is activated lazily when playback actually starts producing output
|
|
671
|
+
// (see `playerDidChangePlayingState`) and re-activated on interruption-end. Best-effort: a
|
|
672
|
+
// category-config failure must never brick setup.
|
|
446
673
|
let session = AVAudioSession.sharedInstance()
|
|
447
|
-
|
|
448
|
-
|
|
674
|
+
let cfg: (
|
|
675
|
+
category: AVAudioSession.Category,
|
|
676
|
+
mode: AVAudioSession.Mode,
|
|
677
|
+
policy: AVAudioSession.RouteSharingPolicy,
|
|
678
|
+
options: AVAudioSession.CategoryOptions,
|
|
679
|
+
) = options.ios?.resolveAudioSessionConfig()
|
|
680
|
+
?? (category: .playback, mode: .default, policy: .default, options: [])
|
|
681
|
+
// Capture category application once so a media-services reset (which
|
|
682
|
+
// clears the category) can restore the exact same config later.
|
|
683
|
+
let applyCategory: () -> Void = {
|
|
684
|
+
try? session.setCategory(cfg.category, mode: cfg.mode, policy: cfg.policy, options: cfg.options)
|
|
685
|
+
}
|
|
686
|
+
applyCategory()
|
|
449
687
|
|
|
450
688
|
// Create player and configure on main actor
|
|
451
689
|
await MainActor.run { [weak self] in
|
|
452
690
|
guard let self else { return }
|
|
691
|
+
self.applyAudioSessionCategory = applyCategory
|
|
453
692
|
|
|
454
693
|
// Create player with self as callbacks delegate
|
|
455
694
|
player = TrackPlayer(callbacks: self)
|
|
@@ -467,18 +706,52 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
467
706
|
// Wire up network monitor for accelerated retries when connectivity is restored
|
|
468
707
|
player?.networkMonitor = networkMonitor
|
|
469
708
|
|
|
709
|
+
// Central fan-out for connectivity changes (wired once here, idempotent on re-setup, and
|
|
710
|
+
// independent of any JS subscription):
|
|
711
|
+
// - bridge to the JS `onOnlineChanged` event (no-op default until JS subscribes);
|
|
712
|
+
// - re-render now-playing so the formatter's offline/online label updates immediately on a
|
|
713
|
+
// connectivity change, not only on the next playback transition (fixes the stall-entry
|
|
714
|
+
// race where the device goes offline after the buffering line was already rendered);
|
|
715
|
+
// - reconnect a stalled stream when connectivity returns (see TrackPlayer.handleNetworkRestored).
|
|
716
|
+
// The monitor only mutates state on the main thread, so this fires main-isolated.
|
|
717
|
+
networkMonitor.onChanged = { [weak self] isOnline in
|
|
718
|
+
MainActor.assumeIsolated {
|
|
719
|
+
guard let self else { return }
|
|
720
|
+
self.onOnlineChanged(isOnline)
|
|
721
|
+
self.applyNowPlayingMetadata()
|
|
722
|
+
if isOnline { self.player?.handleNetworkRestored() }
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Observe audio-session interruptions so another app taking over audio
|
|
727
|
+
// (or a phone call) reflects as a real pause rather than a stuck state.
|
|
728
|
+
setupInterruptionObserver()
|
|
729
|
+
|
|
730
|
+
// Observe media-server resets so a long-running stream recovers instead
|
|
731
|
+
// of going permanently silent.
|
|
732
|
+
setupMediaServicesResetObserver()
|
|
733
|
+
|
|
470
734
|
// Configure media URL resolver
|
|
471
|
-
player?.mediaLoader.mediaUrlResolver = { [weak self] src in
|
|
735
|
+
player?.mediaLoader.mediaUrlResolver = { [weak self] src, track in
|
|
472
736
|
guard let self else {
|
|
473
737
|
return MediaResolvedUrl(url: src, headers: nil, userAgent: nil)
|
|
474
738
|
}
|
|
475
|
-
return await browserManager.resolveMediaUrl(src)
|
|
739
|
+
return await browserManager.resolveMediaUrl(src, track: track)
|
|
476
740
|
}
|
|
477
741
|
|
|
478
742
|
// Configure artwork URL resolver for Now Playing (with size context)
|
|
479
743
|
player?.nowPlayingUpdater.artworkUrlResolver = { [weak self] track, imageContext in
|
|
480
744
|
guard let self else { return nil }
|
|
481
|
-
|
|
745
|
+
let perRoute = (track.id?.isEmpty == false) ? browserManager.config.nowPlayingArtwork : nil
|
|
746
|
+
return await browserManager.resolveArtworkUrl(track: track, perRouteConfig: perRoute, imageContext: imageContext)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Emit the JS `onNowPlayingChanged` event when the rendered text lines change. The updater
|
|
750
|
+
// owns the system now-playing write; this shapes the JS metadata (elapsed time / artwork).
|
|
751
|
+
player?.nowPlayingUpdater.onChanged = { [weak self] track, title, artist, album in
|
|
752
|
+
guard let self else { return }
|
|
753
|
+
self.onNowPlayingChanged(
|
|
754
|
+
self.makeNowPlayingMetadata(track: track, title: title, artist: artist, album: album))
|
|
482
755
|
}
|
|
483
756
|
|
|
484
757
|
// Configure sleep timer callback
|
|
@@ -486,8 +759,30 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
486
759
|
self?.onSleepTimerChanged(state)
|
|
487
760
|
}
|
|
488
761
|
|
|
489
|
-
//
|
|
490
|
-
|
|
762
|
+
// Now-playing: whether to manage metadata, and the optional formatter that owns the
|
|
763
|
+
// rendered lines.
|
|
764
|
+
nowPlayingMetadataEnabled = options.autoUpdateNowPlayingMetadata ?? true
|
|
765
|
+
nowPlayingMetadataFormatter = options.nowPlayingMetadataFormatter
|
|
766
|
+
// `options.keepSessionAliveOnError` is deliberately ignored here: iOS needs no masking.
|
|
767
|
+
// A terminal error resolves the playback state to *paused*
|
|
768
|
+
// (PlaybackCoordinator.applySideEffects), the now-playing info is retained (never cleared
|
|
769
|
+
// on error), and next/previous stay enabled (gated on queue position, not state). So the
|
|
770
|
+
// session stays controllable through errors by default — the behavior Android achieves via
|
|
771
|
+
// InterceptingPlayer is already the iOS norm.
|
|
772
|
+
|
|
773
|
+
// The bundled runtime options and initial state are part of the atomic launch
|
|
774
|
+
// description. Stage the state first so setup options and earlier pre-setup setter
|
|
775
|
+
// calls resolve by strict last-write-wins, then apply everything to the live player.
|
|
776
|
+
if let bundled = options.options {
|
|
777
|
+
updateOptions(options: bundled)
|
|
778
|
+
} else {
|
|
779
|
+
// updateOptions applies remote commands itself; without bundled options, apply the
|
|
780
|
+
// defaults (play, pause, next, previous, seekTo) here.
|
|
781
|
+
applyRemoteCommands()
|
|
782
|
+
}
|
|
783
|
+
if let mode = options.repeatMode { pendingRepeatMode = mode }
|
|
784
|
+
if let intent = options.playWhenReady { pendingPlayWhenReady = intent }
|
|
785
|
+
applyPendingPlayerState()
|
|
491
786
|
|
|
492
787
|
// Notify listeners that player is ready (e.g., CarPlay)
|
|
493
788
|
playerAndConfiguredBrowser.check()
|
|
@@ -495,6 +790,22 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
495
790
|
}
|
|
496
791
|
}
|
|
497
792
|
|
|
793
|
+
/// Applies initial player state staged before the player existed — from setup options or the
|
|
794
|
+
/// imperative setters called pre-setup. Consumed on apply so a later re-setup doesn't replay
|
|
795
|
+
/// stale state.
|
|
796
|
+
@MainActor
|
|
797
|
+
private func applyPendingPlayerState() {
|
|
798
|
+
guard let player else { return }
|
|
799
|
+
if let mode = pendingRepeatMode {
|
|
800
|
+
player.repeatMode = mode
|
|
801
|
+
pendingRepeatMode = nil
|
|
802
|
+
}
|
|
803
|
+
if let intent = pendingPlayWhenReady {
|
|
804
|
+
player.playWhenReady = intent
|
|
805
|
+
pendingPlayWhenReady = nil
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
498
809
|
public func updateOptions(options: NativeUpdateOptions) {
|
|
499
810
|
onMainActor {
|
|
500
811
|
let previousInterval = playerOptions.progressUpdateEventInterval
|
|
@@ -502,6 +813,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
502
813
|
// Update stored options
|
|
503
814
|
playerOptions.update(from: options)
|
|
504
815
|
|
|
816
|
+
// Propagate the favorite match mode to the browser so it can hydrate
|
|
817
|
+
// row hearts (the `favorite` capability is the single favoriting switch).
|
|
818
|
+
browserManager.setFavoriteMatch(playerOptions.capabilities.favoriteMatch)
|
|
819
|
+
|
|
505
820
|
// Apply remote commands to player
|
|
506
821
|
applyRemoteCommands()
|
|
507
822
|
|
|
@@ -509,6 +824,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
509
824
|
if playerOptions.progressUpdateEventInterval != previousInterval {
|
|
510
825
|
player?.setProgressUpdateInterval(playerOptions.progressUpdateEventInterval)
|
|
511
826
|
}
|
|
827
|
+
|
|
828
|
+
onOptionsChanged(playerOptions.toOptions())
|
|
829
|
+
playerOptionsChangedEmitter.emit(())
|
|
512
830
|
}
|
|
513
831
|
}
|
|
514
832
|
|
|
@@ -526,8 +844,12 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
526
844
|
player.remoteCommands = remoteCommands
|
|
527
845
|
}
|
|
528
846
|
|
|
529
|
-
public func getOptions() throws ->
|
|
530
|
-
playerOptions.
|
|
847
|
+
public func getOptions() throws -> Options {
|
|
848
|
+
onMainActor { playerOptions.toOptions() }
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
public func setPlaybackIntervalEnabled(enabled: Bool) {
|
|
852
|
+
onMainActor { player?.setPlaybackIntervalEnabled(enabled) }
|
|
531
853
|
}
|
|
532
854
|
|
|
533
855
|
// MARK: - Playback Control
|
|
@@ -562,11 +884,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
562
884
|
}
|
|
563
885
|
|
|
564
886
|
public func setPlayWhenReady(playWhenReady: Bool) throws {
|
|
565
|
-
onMainActor {
|
|
887
|
+
onMainActor {
|
|
888
|
+
// Pre-setup this stages the intent for the player to come up with — never a no-op.
|
|
889
|
+
if let player { player.playWhenReady = playWhenReady }
|
|
890
|
+
else { pendingPlayWhenReady = playWhenReady }
|
|
891
|
+
}
|
|
566
892
|
}
|
|
567
893
|
|
|
568
894
|
public func getPlayWhenReady() throws -> Bool {
|
|
569
|
-
onMainActor { player?.playWhenReady ?? false }
|
|
895
|
+
onMainActor { player?.playWhenReady ?? pendingPlayWhenReady ?? false }
|
|
570
896
|
}
|
|
571
897
|
|
|
572
898
|
public func seekTo(position: Double) throws {
|
|
@@ -577,6 +903,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
577
903
|
onMainActor { player?.seekBy(offset) }
|
|
578
904
|
}
|
|
579
905
|
|
|
906
|
+
public func seekToLiveEdge() throws {
|
|
907
|
+
onMainActor { player?.seekToLiveEdge() }
|
|
908
|
+
}
|
|
909
|
+
|
|
580
910
|
public func setVolume(level: Double) throws {
|
|
581
911
|
onMainActor { player?.volume = Float(level) }
|
|
582
912
|
}
|
|
@@ -614,11 +944,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
614
944
|
}
|
|
615
945
|
|
|
616
946
|
public func getRepeatMode() throws -> RepeatMode {
|
|
617
|
-
onMainActor { player?.repeatMode ?? .off }
|
|
947
|
+
onMainActor { player?.repeatMode ?? pendingRepeatMode ?? .off }
|
|
618
948
|
}
|
|
619
949
|
|
|
620
950
|
public func setRepeatMode(mode: RepeatMode) throws {
|
|
621
|
-
onMainActor {
|
|
951
|
+
onMainActor {
|
|
952
|
+
// Pre-setup this stages the mode for the player to come up with — never a no-op.
|
|
953
|
+
if let player { player.repeatMode = mode }
|
|
954
|
+
else { pendingRepeatMode = mode }
|
|
955
|
+
}
|
|
622
956
|
}
|
|
623
957
|
|
|
624
958
|
public func getShuffleEnabled() throws -> Bool {
|
|
@@ -630,11 +964,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
630
964
|
}
|
|
631
965
|
|
|
632
966
|
public func getPlaybackError() throws -> PlaybackError? {
|
|
633
|
-
onMainActor { player?.
|
|
967
|
+
onMainActor { player?.coordinator.nitroPlaybackError() }
|
|
634
968
|
}
|
|
635
969
|
|
|
636
970
|
public func retry() throws {
|
|
637
|
-
|
|
971
|
+
// Re-resolve rather than replay the cached URL: short-lived URLs/tokens
|
|
972
|
+
// may have expired since the failure (matches the internal retry path).
|
|
973
|
+
onMainActor { player?.reloadResolving(startFromCurrentTime: true) }
|
|
638
974
|
}
|
|
639
975
|
|
|
640
976
|
// MARK: - Sleep Timer
|
|
@@ -648,9 +984,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
648
984
|
}
|
|
649
985
|
}
|
|
650
986
|
|
|
651
|
-
public func setSleepTimer(seconds: Double) throws {
|
|
987
|
+
public func setSleepTimer(seconds: Double, fadeDuration: Double?) throws {
|
|
652
988
|
onMainActor {
|
|
653
|
-
player?.sleepTimerManager.set(seconds: seconds)
|
|
989
|
+
player?.sleepTimerManager.set(seconds: seconds, fadeDuration: fadeDuration)
|
|
654
990
|
}
|
|
655
991
|
}
|
|
656
992
|
|
|
@@ -730,13 +1066,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
730
1066
|
onMainActor {
|
|
731
1067
|
guard let track = player?.currentTrack, let src = track.src else { return }
|
|
732
1068
|
guard let index = player?.currentIndex, index >= 0 else { return }
|
|
733
|
-
|
|
734
|
-
//
|
|
1069
|
+
// Optimistically reflect in the authoritative match set so the now-playing
|
|
1070
|
+
// heart (hydrated from it in getActiveTrack) flips immediately; native
|
|
1071
|
+
// reconciles to its canonical ids on the next setFavorites.
|
|
1072
|
+
browserManager.setFavorited(src: src, favorited: favorited)
|
|
735
1073
|
let updatedTrack = track.copying(favorited: favorited)
|
|
736
|
-
// Update the track in the player's queue so getActiveTrack() returns correct state
|
|
737
|
-
player?.replace(index, updatedTrack)
|
|
738
1074
|
favoriteChangedEmitter.emit(FavoriteChangedEvent(track: updatedTrack, favorited: favorited))
|
|
739
|
-
// Fire active track changed so useActiveTrack()
|
|
1075
|
+
// Fire active track changed so the now-playing heart + useActiveTrack() refresh.
|
|
740
1076
|
let position = player?.currentTime ?? 0
|
|
741
1077
|
activeTrackChangedEmitter.emit(PlaybackActiveTrackChangedEvent(
|
|
742
1078
|
lastIndex: Double(index),
|
|
@@ -748,27 +1084,24 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
748
1084
|
}
|
|
749
1085
|
}
|
|
750
1086
|
|
|
1087
|
+
/// Whether the active (now-playing) track is favorited, per the authoritative
|
|
1088
|
+
/// favorite set (via `getActiveTrack`'s hydration).
|
|
1089
|
+
func isActiveTrackFavorited() -> Bool {
|
|
1090
|
+
(try? getActiveTrack())?.favorited ?? false
|
|
1091
|
+
}
|
|
1092
|
+
|
|
751
1093
|
public func toggleActiveTrackFavorited() throws {
|
|
752
|
-
|
|
753
|
-
guard let track = player?.currentTrack, let src = track.src else { return }
|
|
754
|
-
// Check current favorited state from cache
|
|
755
|
-
let currentTrack = browserManager.getCachedTrack(src)
|
|
756
|
-
let isFavorited = currentTrack?.favorited ?? track.favorited ?? false
|
|
757
|
-
try? setActiveTrackFavorited(favorited: !isFavorited)
|
|
758
|
-
}
|
|
1094
|
+
try? setActiveTrackFavorited(favorited: !isActiveTrackFavorited())
|
|
759
1095
|
}
|
|
760
1096
|
|
|
761
|
-
public func setQueue(tracks: [Track], startIndex: Double?,
|
|
762
|
-
|
|
1097
|
+
public func setQueue(tracks: [Track], startIndex: Double?, startPosition: Double?) throws {
|
|
1098
|
+
onMainActor {
|
|
763
1099
|
guard let player else { return }
|
|
764
|
-
player.
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
if let position = startPositionMs {
|
|
770
|
-
player.seekTo(position / 1000.0)
|
|
771
|
-
}
|
|
1100
|
+
player.setQueue(
|
|
1101
|
+
tracks,
|
|
1102
|
+
initialIndex: startIndex.map { Int($0) } ?? 0,
|
|
1103
|
+
startPositionMs: startPosition.map { $0 * 1000 },
|
|
1104
|
+
)
|
|
772
1105
|
}
|
|
773
1106
|
}
|
|
774
1107
|
|
|
@@ -793,7 +1126,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
793
1126
|
}
|
|
794
1127
|
|
|
795
1128
|
public func getActiveTrack() throws -> Track? {
|
|
796
|
-
|
|
1129
|
+
// Hydrate `favorited` from the authoritative favorite set — the now-playing
|
|
1130
|
+
// track is loaded outside the browse cache, so its own flag isn't otherwise
|
|
1131
|
+
// kept in sync as favorites change. This is the single source of truth for
|
|
1132
|
+
// "is the active track favorited" (now-playing heart, toggle direction, JS).
|
|
1133
|
+
onMainActor { player?.currentTrack.map(browserManager.hydrateFavorite) }
|
|
797
1134
|
}
|
|
798
1135
|
|
|
799
1136
|
// MARK: - Now Playing
|
|
@@ -805,21 +1142,58 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
805
1142
|
}
|
|
806
1143
|
}
|
|
807
1144
|
|
|
1145
|
+
/// Transient now-playing fields (e.g. feedback for a refused remote
|
|
1146
|
+
/// command). Outranks the formatter and the override while active, so live
|
|
1147
|
+
/// metadata can't stomp it mid-flash. Reverted by a NATIVE timer — JS
|
|
1148
|
+
/// timers pause with a backgrounded host on Android, and the lock screen
|
|
1149
|
+
/// is exactly the backgrounded case — and cleared early on track change.
|
|
1150
|
+
@MainActor private var nowPlayingFlash: NowPlayingUpdate?
|
|
1151
|
+
@MainActor private var nowPlayingFlashRevert: DispatchWorkItem?
|
|
1152
|
+
|
|
1153
|
+
/// Pending debounced audio-session release (see `playerShouldReleaseSession`).
|
|
1154
|
+
@MainActor private var sessionReleaseWork: DispatchWorkItem?
|
|
1155
|
+
|
|
1156
|
+
public func flashNowPlaying(update: NowPlayingUpdate, durationMs: Double) throws {
|
|
1157
|
+
onMainActor {
|
|
1158
|
+
nowPlayingFlashRevert?.cancel()
|
|
1159
|
+
nowPlayingFlash = update
|
|
1160
|
+
let revert = DispatchWorkItem { [weak self] in
|
|
1161
|
+
guard let self else { return }
|
|
1162
|
+
self.nowPlayingFlash = nil
|
|
1163
|
+
self.nowPlayingFlashRevert = nil
|
|
1164
|
+
self.applyNowPlayingMetadata()
|
|
1165
|
+
}
|
|
1166
|
+
nowPlayingFlashRevert = revert
|
|
1167
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + durationMs / 1000, execute: revert)
|
|
1168
|
+
applyNowPlayingMetadata()
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
public func clearNowPlayingFlash() throws {
|
|
1173
|
+
onMainActor {
|
|
1174
|
+
guard nowPlayingFlash != nil else { return }
|
|
1175
|
+
cancelNowPlayingFlash()
|
|
1176
|
+
applyNowPlayingMetadata()
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
@MainActor
|
|
1181
|
+
private func cancelNowPlayingFlash() {
|
|
1182
|
+
nowPlayingFlashRevert?.cancel()
|
|
1183
|
+
nowPlayingFlashRevert = nil
|
|
1184
|
+
nowPlayingFlash = nil
|
|
1185
|
+
}
|
|
1186
|
+
|
|
808
1187
|
public func getNowPlaying() throws -> NowPlayingMetadata? {
|
|
809
1188
|
onMainActor {
|
|
810
1189
|
guard let track = player?.currentTrack else { return nil }
|
|
1190
|
+
let flash = nowPlayingFlash
|
|
811
1191
|
let override = nowPlayingOverride
|
|
812
|
-
return
|
|
813
|
-
|
|
814
|
-
title: override?.title ?? track.title,
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
duration: track.duration,
|
|
818
|
-
artwork: nowPlayingArtwork(for: track),
|
|
819
|
-
description: track.description,
|
|
820
|
-
mediaId: track.src ?? track.url,
|
|
821
|
-
genre: track.genre,
|
|
822
|
-
rating: nil,
|
|
1192
|
+
return makeNowPlayingMetadata(
|
|
1193
|
+
track: track,
|
|
1194
|
+
title: flash?.title ?? override?.title ?? track.title,
|
|
1195
|
+
artist: flash?.artist ?? override?.artist ?? track.artist,
|
|
1196
|
+
album: flash?.album ?? override?.album ?? track.album,
|
|
823
1197
|
)
|
|
824
1198
|
}
|
|
825
1199
|
}
|
|
@@ -829,36 +1203,47 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
829
1203
|
/// Falls back to the raw artwork string only if it's not an SF Symbol (which JS can't load).
|
|
830
1204
|
private func nowPlayingArtwork(for track: Track) -> String? {
|
|
831
1205
|
if let uri = track.artworkSource?.uri { return uri }
|
|
832
|
-
guard let artwork = track.artwork else { return nil }
|
|
1206
|
+
guard let artwork = track.artwork?.url else { return nil }
|
|
833
1207
|
return SFSymbolRenderer.isSFSymbol(artwork) ? nil : artwork
|
|
834
1208
|
}
|
|
835
1209
|
|
|
836
|
-
///
|
|
1210
|
+
/// Builds the JS-facing now-playing metadata for the given rendered text lines.
|
|
837
1211
|
@MainActor
|
|
838
|
-
private func
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
let nowPlaying = NowPlayingMetadata(
|
|
1212
|
+
private func makeNowPlayingMetadata(
|
|
1213
|
+
track: Track, title: String, artist: String?, album: String?,
|
|
1214
|
+
) -> NowPlayingMetadata {
|
|
1215
|
+
NowPlayingMetadata(
|
|
843
1216
|
elapsedTime: player?.currentTime,
|
|
844
|
-
title:
|
|
845
|
-
album:
|
|
846
|
-
artist:
|
|
1217
|
+
title: title,
|
|
1218
|
+
album: album,
|
|
1219
|
+
artist: artist,
|
|
847
1220
|
duration: track.duration,
|
|
848
1221
|
artwork: nowPlayingArtwork(for: track),
|
|
849
1222
|
description: track.description,
|
|
850
1223
|
mediaId: track.src ?? track.url,
|
|
851
1224
|
genre: track.genre,
|
|
852
|
-
rating: nil,
|
|
853
1225
|
)
|
|
1226
|
+
}
|
|
854
1227
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
|
|
1228
|
+
/// Re-renders the now-playing surface via `NowPlayingUpdater`, handing it the current playback
|
|
1229
|
+
/// signals, override, and formatter. Called on every track / metadata / playback-state change.
|
|
1230
|
+
@MainActor
|
|
1231
|
+
private func applyNowPlayingMetadata() {
|
|
1232
|
+
guard let player, let track = player.currentTrack else { return }
|
|
1233
|
+
// Classify a stall by connectivity so the formatter can show "No internet connection" vs
|
|
1234
|
+
// "Reconnecting…" without a separate read. nil while not stalled.
|
|
1235
|
+
let stalled: StallReason? =
|
|
1236
|
+
player.isStalled ? (networkMonitor.getOnline() ? .buffering : .offline) : nil
|
|
1237
|
+
player.nowPlayingUpdater.render(
|
|
1238
|
+
track: track,
|
|
1239
|
+
timedMetadata: latestTimedMetadata,
|
|
1240
|
+
playWhenReady: player.playWhenReady,
|
|
1241
|
+
stalled: stalled,
|
|
1242
|
+
error: player.coordinator.nitroPlaybackError(),
|
|
1243
|
+
flash: nowPlayingFlash,
|
|
1244
|
+
override: nowPlayingMetadataEnabled ? nowPlayingOverride : nil,
|
|
1245
|
+
formatter: nowPlayingMetadataEnabled ? nowPlayingMetadataFormatter : nil,
|
|
1246
|
+
)
|
|
862
1247
|
}
|
|
863
1248
|
|
|
864
1249
|
// MARK: - Network
|
|
@@ -893,6 +1278,63 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
893
1278
|
|
|
894
1279
|
// MARK: - External Audio Output
|
|
895
1280
|
|
|
1281
|
+
/// Observes audio-session interruptions (phone calls, or another app such as
|
|
1282
|
+
/// Music/Spotify starting playback). iOS pauses our AVPlayer underneath us but
|
|
1283
|
+
/// doesn't update our playback state, so without this the UI stays stuck on
|
|
1284
|
+
/// "playing". On `.began` we reflect a real pause; on `.ended` we resume if we
|
|
1285
|
+
/// were playing and the system says we should.
|
|
1286
|
+
private func setupInterruptionObserver() {
|
|
1287
|
+
if let observer = interruptionObserver {
|
|
1288
|
+
NotificationCenter.default.removeObserver(observer)
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
interruptionObserver = NotificationCenter.default.addObserver(
|
|
1292
|
+
forName: AVAudioSession.interruptionNotification,
|
|
1293
|
+
object: nil,
|
|
1294
|
+
queue: .main,
|
|
1295
|
+
) { [weak self] notification in
|
|
1296
|
+
self?.handleAudioSessionInterruption(notification)
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
private func handleAudioSessionInterruption(_ notification: Notification) {
|
|
1301
|
+
guard
|
|
1302
|
+
let info = notification.userInfo,
|
|
1303
|
+
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
1304
|
+
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
|
1305
|
+
else { return }
|
|
1306
|
+
|
|
1307
|
+
switch type {
|
|
1308
|
+
case .began:
|
|
1309
|
+
// iOS 17+ fires a `.began` (with no matching `.ended`) when an output
|
|
1310
|
+
// route such as headphones disappears. Reflecting a pause here would
|
|
1311
|
+
// strand resume-intent state with nothing to clear it, and the
|
|
1312
|
+
// route-change observer already handles the output change — so ignore it.
|
|
1313
|
+
if #available(iOS 17.0, *),
|
|
1314
|
+
let reasonValue = info[AVAudioSessionInterruptionReasonKey] as? UInt,
|
|
1315
|
+
AVAudioSession.InterruptionReason(rawValue: reasonValue) == .routeDisconnected
|
|
1316
|
+
{
|
|
1317
|
+
return
|
|
1318
|
+
}
|
|
1319
|
+
onMainActor { player?.handleInterruptionBegan() }
|
|
1320
|
+
case .ended:
|
|
1321
|
+
let shouldResume = (info[AVAudioSessionInterruptionOptionKey] as? UInt)
|
|
1322
|
+
.map { AVAudioSession.InterruptionOptions(rawValue: $0).contains(.shouldResume) }
|
|
1323
|
+
?? false
|
|
1324
|
+
onMainActor {
|
|
1325
|
+
// Reactivate the possibly-deactivated session before resuming — but
|
|
1326
|
+
// only when a resume will happen: activating while staying paused
|
|
1327
|
+
// grabs a non-mixable session with nothing to release it.
|
|
1328
|
+
if shouldResume, player?.willResumeAfterInterruption == true {
|
|
1329
|
+
try? AVAudioSession.sharedInstance().setActive(true)
|
|
1330
|
+
}
|
|
1331
|
+
player?.handleInterruptionEnded(shouldResume: shouldResume)
|
|
1332
|
+
}
|
|
1333
|
+
@unknown default:
|
|
1334
|
+
break
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
896
1338
|
/// Sets up observer for audio route changes
|
|
897
1339
|
private func setupRouteChangeObserver() {
|
|
898
1340
|
if let observer = routeChangeObserver {
|
|
@@ -903,58 +1345,97 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
903
1345
|
forName: AVAudioSession.routeChangeNotification,
|
|
904
1346
|
object: nil,
|
|
905
1347
|
queue: .main,
|
|
1348
|
+
) { [weak self] notification in
|
|
1349
|
+
guard let self else { return }
|
|
1350
|
+
// The previous output disappeared (headphones unplugged, a Bluetooth
|
|
1351
|
+
// speaker powered off). Pause rather than abruptly resume out of the
|
|
1352
|
+
// built-in speaker — the platform convention every media app follows.
|
|
1353
|
+
// A deliberate pause (clears play intent), not an interruption: the user
|
|
1354
|
+
// restarts playback themselves once they want it.
|
|
1355
|
+
if let reasonValue = notification.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
|
1356
|
+
AVAudioSession.RouteChangeReason(rawValue: reasonValue) == .oldDeviceUnavailable
|
|
1357
|
+
{
|
|
1358
|
+
// Also cancels a pending interruption auto-resume: unplugging during
|
|
1359
|
+
// a call must not blast the built-in speaker when the call ends.
|
|
1360
|
+
self.onMainActor { self.player?.handleRouteDisconnected() }
|
|
1361
|
+
}
|
|
1362
|
+
if let output = self.getCurrentOutput() {
|
|
1363
|
+
self.onOutputChanged(output)
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/// Observes audio-server (`mediaserverd`) resets. When it fires, the session
|
|
1369
|
+
/// category is cleared and every AVPlayer/session handle is invalid — a
|
|
1370
|
+
/// long-running stream would otherwise go permanently silent until the app
|
|
1371
|
+
/// restarts.
|
|
1372
|
+
private func setupMediaServicesResetObserver() {
|
|
1373
|
+
if let observer = mediaServicesResetObserver {
|
|
1374
|
+
NotificationCenter.default.removeObserver(observer)
|
|
1375
|
+
}
|
|
1376
|
+
mediaServicesResetObserver = NotificationCenter.default.addObserver(
|
|
1377
|
+
forName: AVAudioSession.mediaServicesWereResetNotification,
|
|
1378
|
+
object: nil,
|
|
1379
|
+
queue: .main,
|
|
906
1380
|
) { [weak self] _ in
|
|
907
|
-
guard let self
|
|
908
|
-
|
|
1381
|
+
guard let self else { return }
|
|
1382
|
+
// Re-apply our category (the reset cleared it), then have the player
|
|
1383
|
+
// recreate itself and reconnect the current stream.
|
|
1384
|
+
self.onMainActor {
|
|
1385
|
+
self.applyAudioSessionCategory()
|
|
1386
|
+
self.player?.handleMediaServicesReset()
|
|
1387
|
+
}
|
|
909
1388
|
}
|
|
910
1389
|
}
|
|
911
1390
|
|
|
912
1391
|
/// Gets the current audio output info
|
|
913
|
-
private func getCurrentOutput() ->
|
|
1392
|
+
private func getCurrentOutput() -> Output? {
|
|
914
1393
|
let session = AVAudioSession.sharedInstance()
|
|
915
1394
|
guard let output = session.currentRoute.outputs.first else { return nil }
|
|
916
1395
|
|
|
917
|
-
let (outputType, external): (
|
|
1396
|
+
let (outputType, external): (OutputType, Bool) = switch output.portType {
|
|
918
1397
|
case .builtInSpeaker:
|
|
919
|
-
(.
|
|
1398
|
+
(.speaker, false)
|
|
920
1399
|
case .builtInReceiver:
|
|
921
|
-
(.
|
|
1400
|
+
(.receiver, false)
|
|
922
1401
|
case .airPlay:
|
|
923
1402
|
(.airplay, true)
|
|
924
|
-
case .bluetoothA2DP:
|
|
925
|
-
(.
|
|
926
|
-
case .bluetoothHFP:
|
|
927
|
-
(.bluetoothHfp, true)
|
|
928
|
-
case .bluetoothLE:
|
|
929
|
-
(.bluetoothLe, true)
|
|
1403
|
+
case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
|
|
1404
|
+
(.bluetooth, true)
|
|
930
1405
|
case .headphones:
|
|
931
1406
|
(.headphones, true)
|
|
932
1407
|
case .carAudio:
|
|
933
|
-
(.
|
|
1408
|
+
(.car, true)
|
|
934
1409
|
case .HDMI:
|
|
935
1410
|
(.hdmi, true)
|
|
936
1411
|
case .usbAudio:
|
|
937
|
-
(.
|
|
1412
|
+
(.usb, true)
|
|
938
1413
|
default:
|
|
939
1414
|
(.other, true)
|
|
940
1415
|
}
|
|
941
1416
|
|
|
942
|
-
return
|
|
1417
|
+
return Output(type: outputType, name: output.portName, external: external)
|
|
943
1418
|
}
|
|
944
1419
|
|
|
945
|
-
public func
|
|
1420
|
+
public func getOutput() throws -> Output? {
|
|
946
1421
|
getCurrentOutput()
|
|
947
1422
|
}
|
|
948
1423
|
|
|
949
|
-
public func
|
|
1424
|
+
public func supportsOutputSwitcher() throws -> Bool {
|
|
1425
|
+
// iOS can always present the system route picker (AVRoutePickerView).
|
|
1426
|
+
true
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
public func openOutputPicker() throws {
|
|
950
1430
|
DispatchQueue.main.async { [weak self] in
|
|
1431
|
+
guard let self else { return }
|
|
951
1432
|
guard let windowScene = UIApplication.shared.connectedScenes
|
|
952
1433
|
.compactMap({ $0 as? UIWindowScene })
|
|
953
1434
|
.first(where: { $0.activationState == .foregroundActive }),
|
|
954
1435
|
let window = windowScene.windows.first(where: { $0.isKeyWindow }),
|
|
955
1436
|
var topController = window.rootViewController
|
|
956
1437
|
else {
|
|
957
|
-
self
|
|
1438
|
+
self.logger.error("openOutputPicker: no active window scene / root view controller")
|
|
958
1439
|
return
|
|
959
1440
|
}
|
|
960
1441
|
|
|
@@ -963,25 +1444,52 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
963
1444
|
topController = presented
|
|
964
1445
|
}
|
|
965
1446
|
|
|
966
|
-
|
|
967
|
-
|
|
1447
|
+
// AVRoutePickerView has no public API to present its picker
|
|
1448
|
+
// programmatically, so we add it to the hierarchy and tap its internal
|
|
1449
|
+
// button. Two requirements that are easy to get wrong:
|
|
1450
|
+
// 1. The view must be *visible* to the system (a `isHidden` source view
|
|
1451
|
+
// suppresses the presentation), so we keep it transparent instead.
|
|
1452
|
+
// 2. The internal button can be nested below the top-level subviews on
|
|
1453
|
+
// modern iOS, so we search recursively rather than only direct children.
|
|
1454
|
+
let routePicker = AVRoutePickerView(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
|
|
1455
|
+
routePicker.alpha = 0.01
|
|
968
1456
|
topController.view.addSubview(routePicker)
|
|
969
1457
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1458
|
+
let button = Self.firstButton(in: routePicker)
|
|
1459
|
+
self.logger.notice(
|
|
1460
|
+
"openOutputPicker: hierarchy=\(Self.describeHierarchy(routePicker), privacy: .public), buttonFound=\(button != nil, privacy: .public)",
|
|
1461
|
+
)
|
|
1462
|
+
|
|
1463
|
+
if let button {
|
|
1464
|
+
button.sendActions(for: .touchUpInside)
|
|
1465
|
+
} else {
|
|
1466
|
+
self.logger.error("openOutputPicker: no UIButton found inside AVRoutePickerView")
|
|
976
1467
|
}
|
|
977
1468
|
|
|
978
|
-
//
|
|
979
|
-
|
|
1469
|
+
// Keep the source view in the hierarchy long enough for the picker to
|
|
1470
|
+
// present (on iPad it anchors a popover to this view).
|
|
1471
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
|
980
1472
|
routePicker.removeFromSuperview()
|
|
981
1473
|
}
|
|
982
1474
|
}
|
|
983
1475
|
}
|
|
984
1476
|
|
|
1477
|
+
/// Depth-first search for the first `UIButton` inside a view tree.
|
|
1478
|
+
private static func firstButton(in view: UIView) -> UIButton? {
|
|
1479
|
+
if let button = view as? UIButton { return button }
|
|
1480
|
+
for subview in view.subviews {
|
|
1481
|
+
if let button = firstButton(in: subview) { return button }
|
|
1482
|
+
}
|
|
1483
|
+
return nil
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/// Compact description of a view tree, e.g. `AVRoutePickerView[UIButton[UIImageView]]`.
|
|
1487
|
+
private static func describeHierarchy(_ view: UIView) -> String {
|
|
1488
|
+
let name = String(describing: type(of: view))
|
|
1489
|
+
let children = view.subviews.map(describeHierarchy).joined(separator: ", ")
|
|
1490
|
+
return children.isEmpty ? name : "\(name)[\(children)]"
|
|
1491
|
+
}
|
|
1492
|
+
|
|
985
1493
|
// MARK: - Equalizer (unsupported on iOS)
|
|
986
1494
|
|
|
987
1495
|
public func getEqualizerSettings() throws -> EqualizerSettings? {
|
|
@@ -1026,12 +1534,28 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1026
1534
|
extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
1027
1535
|
public func playerDidChangePlayback(_ playback: Playback) {
|
|
1028
1536
|
onPlaybackChanged(playback)
|
|
1537
|
+
// Re-render so the formatter reflects the new state — notably the error line on a failure (the
|
|
1538
|
+
// coordinator sets playbackError before this fires). Redundant updates are deduped on publish.
|
|
1539
|
+
applyNowPlayingMetadata()
|
|
1029
1540
|
}
|
|
1030
1541
|
|
|
1031
1542
|
public func playerDidChangeActiveTrack(_ event: PlaybackActiveTrackChangedEvent) {
|
|
1032
|
-
// Clear now playing override when track changes
|
|
1543
|
+
// Clear now playing override + flash + live metadata when track changes
|
|
1544
|
+
// (matches Kotlin behavior; a flash must not carry over to a new track)
|
|
1033
1545
|
nowPlayingOverride = nil
|
|
1034
|
-
|
|
1546
|
+
cancelNowPlayingFlash()
|
|
1547
|
+
latestTimedMetadata = nil
|
|
1548
|
+
// Hydrate the active track's `favorited` from the authoritative set so JS
|
|
1549
|
+
// consumers (useActiveTrack) match getActiveTrack()'s hydrated value — the
|
|
1550
|
+
// coordinator emits the raw queue track, whose flag isn't kept in sync.
|
|
1551
|
+
let hydrated = PlaybackActiveTrackChangedEvent(
|
|
1552
|
+
lastIndex: event.lastIndex,
|
|
1553
|
+
lastTrack: event.lastTrack,
|
|
1554
|
+
lastPosition: event.lastPosition,
|
|
1555
|
+
index: event.index,
|
|
1556
|
+
track: event.track.map(browserManager.hydrateFavorite),
|
|
1557
|
+
)
|
|
1558
|
+
activeTrackChangedEmitter.emit(hydrated)
|
|
1035
1559
|
// Also notify now playing changed when track changes
|
|
1036
1560
|
applyNowPlayingMetadata()
|
|
1037
1561
|
}
|
|
@@ -1040,12 +1564,61 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1040
1564
|
onPlaybackProgressUpdated(event)
|
|
1041
1565
|
}
|
|
1042
1566
|
|
|
1567
|
+
public func playerDidFirePlaybackInterval() {
|
|
1568
|
+
onPlaybackInterval()
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1043
1571
|
public func playerDidChangePlayWhenReady(_ playWhenReady: Bool) {
|
|
1572
|
+
// Intent to play again is the earliest cancel signal for a pending session release.
|
|
1573
|
+
if playWhenReady { cancelSessionRelease() }
|
|
1044
1574
|
onPlaybackPlayWhenReadyChanged(PlaybackPlayWhenReadyChangedEvent(playWhenReady: playWhenReady))
|
|
1045
1575
|
}
|
|
1046
1576
|
|
|
1047
1577
|
public func playerDidChangePlayingState(_ state: PlayingState) {
|
|
1048
1578
|
onPlaybackPlayingState(state)
|
|
1579
|
+
// Lazily activate the audio session the moment playback actually produces — or is buffering to
|
|
1580
|
+
// produce — output. This (not setup) is where we grab the session, so launching the app never
|
|
1581
|
+
// interrupts other apps' audio; the interruption only happens when the user truly starts
|
|
1582
|
+
// playback, which is correct. Keyed off play *state*, not `playWhenReady`, because the intent
|
|
1583
|
+
// can be set at setup (before any track) while output only begins here. Idempotent:
|
|
1584
|
+
// `setActive(true)` is a no-op when the session is already active.
|
|
1585
|
+
if state.playing || state.buffering {
|
|
1586
|
+
try? AVAudioSession.sharedInstance().setActive(true)
|
|
1587
|
+
// The now-playing session's activation request at link time predates this
|
|
1588
|
+
// (the audio session was still inactive, and the system may have declined);
|
|
1589
|
+
// re-request now so the surface can appear even if playback never produces
|
|
1590
|
+
// audio (first-ever load failing on a dead stream).
|
|
1591
|
+
player?.nowPlayingInfoController.reactivateSessionIfNeeded()
|
|
1592
|
+
// Output resumed — cancel any pending release armed by an earlier stop.
|
|
1593
|
+
cancelSessionRelease()
|
|
1594
|
+
}
|
|
1595
|
+
// Re-render so the formatter reacts to a stall starting or recovering (`stalled`) — a buffering
|
|
1596
|
+
// flag change that may not transition the coordinator state.
|
|
1597
|
+
applyNowPlayingMetadata()
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
public func playerShouldReleaseSession() {
|
|
1601
|
+
// Release the audio session so other apps' audio (Spotify, a podcast) can resume — debounced,
|
|
1602
|
+
// because iOS dislikes deactivating while our audio is still trailing off, and a quick
|
|
1603
|
+
// pause→resume shouldn't thrash the session. The coordinator only requests this on a genuine
|
|
1604
|
+
// stop (not an interruption, not mid-retry); a re-activation cancels it (see
|
|
1605
|
+
// `playerDidChangePlayWhenReady` and the activation above). Mirrors the now-playing flash timer.
|
|
1606
|
+
sessionReleaseWork?.cancel()
|
|
1607
|
+
let release = DispatchWorkItem { [weak self] in
|
|
1608
|
+
guard let self, self.player?.playWhenReady == false else { return }
|
|
1609
|
+
self.sessionReleaseWork = nil
|
|
1610
|
+
// `.notifyOthersOnDeactivation` signals other apps they may resume; without it the session
|
|
1611
|
+
// is released but nobody is told, so the user's audio would not come back.
|
|
1612
|
+
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
1613
|
+
}
|
|
1614
|
+
sessionReleaseWork = release
|
|
1615
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: release)
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
@MainActor
|
|
1619
|
+
private func cancelSessionRelease() {
|
|
1620
|
+
sessionReleaseWork?.cancel()
|
|
1621
|
+
sessionReleaseWork = nil
|
|
1049
1622
|
}
|
|
1050
1623
|
|
|
1051
1624
|
public func playerDidEndQueue(_ event: PlaybackQueueEndedEvent) {
|
|
@@ -1061,6 +1634,7 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1061
1634
|
}
|
|
1062
1635
|
|
|
1063
1636
|
public func playerDidChangeShuffleEnabled(_ enabled: Bool) {
|
|
1637
|
+
shuffleChangedEmitter.emit(enabled)
|
|
1064
1638
|
onPlaybackShuffleModeChanged(enabled)
|
|
1065
1639
|
}
|
|
1066
1640
|
|
|
@@ -1076,7 +1650,10 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1076
1650
|
public func playerDidReceiveTimedMetadata(_ groups: [AVTimedMetadataGroup]) {
|
|
1077
1651
|
for group in groups {
|
|
1078
1652
|
if let metadata = TimedMetadata.from(items: group.items) {
|
|
1653
|
+
latestTimedMetadata = metadata
|
|
1079
1654
|
onTimedMetadata(metadata)
|
|
1655
|
+
// Re-render the now-playing so the formatter can reflect the live song.
|
|
1656
|
+
applyNowPlayingMetadata()
|
|
1080
1657
|
}
|
|
1081
1658
|
}
|
|
1082
1659
|
}
|
|
@@ -1193,10 +1770,6 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1193
1770
|
remoteSeek(position: position)
|
|
1194
1771
|
}
|
|
1195
1772
|
|
|
1196
|
-
public func remoteSetRating(rating _: Any) {
|
|
1197
|
-
// TODO: Convert rating to RemoteSetRatingEvent
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
1773
|
public func remotePlayId(id: String, index: Int?) {
|
|
1201
1774
|
let event = RemotePlayIdEvent(id: id, index: index.map { Double($0) })
|
|
1202
1775
|
if let handler = handleRemotePlayId {
|
|
@@ -1215,57 +1788,92 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1215
1788
|
}
|
|
1216
1789
|
}
|
|
1217
1790
|
|
|
1218
|
-
///
|
|
1219
|
-
///
|
|
1220
|
-
///
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1791
|
+
/// Static so the Siri intent handler can call it before the shared instance
|
|
1792
|
+
/// exists: it waits on the `playerAndConfiguredBrowser` gate, which resolves
|
|
1793
|
+
/// once RN boots. Uses the resolved `browser`, never `self`.
|
|
1794
|
+
static func handlePlayMediaIntent(criteria: MediaIntentCriteria, completion: @escaping @Sendable (Bool) -> Void) {
|
|
1795
|
+
Task { @MainActor in
|
|
1796
|
+
guard let (browser, player) = await playerAndConfiguredBrowser.wait(timeout: .seconds(8)) else {
|
|
1797
|
+
Logger(subsystem: "com.audiobrowser", category: "AudioBrowser")
|
|
1798
|
+
.error("handlePlayMediaIntent: browser/player not ready within budget")
|
|
1799
|
+
completion(false)
|
|
1800
|
+
return
|
|
1801
|
+
}
|
|
1802
|
+
// No-criteria intent ("play «app»") → resume. The gate must NOT block
|
|
1803
|
+
// this: the gate blocks *finding* content, never *hearing* the active or
|
|
1804
|
+
// persisted track. The gate is checked below, for the search branch only.
|
|
1805
|
+
if criteria.isResume {
|
|
1806
|
+
if player.currentTrack != nil {
|
|
1807
|
+
browser.logger.info("resume: warm — playing current track")
|
|
1808
|
+
player.play()
|
|
1809
|
+
browser.showNowPlayingRequestedEmitter.emit(())
|
|
1810
|
+
completion(true)
|
|
1811
|
+
} else if let state = browser.playbackStateStore.load() {
|
|
1812
|
+
browser.logger.info("resume: cold — restoring persisted track")
|
|
1813
|
+
let track = state.track.toNitro()
|
|
1814
|
+
let startMs = (track.live == true) ? nil : state.positionMs
|
|
1815
|
+
// Match Android resume: re-expand the full queue from the track's contextual
|
|
1816
|
+
// URL (parent container → siblings + selected index). Fall back to the single
|
|
1817
|
+
// track when the url isn't contextual or expansion fails.
|
|
1818
|
+
if let url = state.track.url,
|
|
1819
|
+
let expanded = try? await browser.browserManager.expandQueueFromContextualUrl(url)
|
|
1820
|
+
{
|
|
1821
|
+
player.setQueue(expanded.tracks, initialIndex: expanded.selectedIndex, startPositionMs: startMs, playWhenReady: true)
|
|
1822
|
+
} else {
|
|
1823
|
+
player.setQueue([track], initialIndex: 0, startPositionMs: startMs, playWhenReady: true)
|
|
1824
|
+
}
|
|
1825
|
+
browser.showNowPlayingRequestedEmitter.emit(())
|
|
1826
|
+
completion(true)
|
|
1827
|
+
} else {
|
|
1828
|
+
browser.logger.error("resume: nothing playing and nothing persisted → no-op")
|
|
1829
|
+
completion(false) // nothing playing and nothing persisted
|
|
1830
|
+
}
|
|
1831
|
+
return
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// Assemble the Nitro SearchParams here (MainActor) from the criteria's
|
|
1835
|
+
// Sendable fields, so the structured mode/genre/… reach the request like
|
|
1836
|
+
// Android. Today the API still text-searches `q`; the rest is forward-compat.
|
|
1837
|
+
let params = SearchParams(
|
|
1838
|
+
mode: criteria.searchMode.flatMap { SearchMode(fromString: $0) },
|
|
1839
|
+
query: criteria.query,
|
|
1840
|
+
genre: criteria.genre,
|
|
1841
|
+
artist: criteria.artist,
|
|
1842
|
+
album: criteria.album,
|
|
1843
|
+
title: criteria.title,
|
|
1844
|
+
playlist: criteria.playlist,
|
|
1845
|
+
// .currentlyPlaying can't reach here — isResume routed it to resume.
|
|
1846
|
+
reference: criteria.reference == .my ? .my : .unknown,
|
|
1847
|
+
)
|
|
1848
|
+
|
|
1849
|
+
// Search is *finding* new content — refused when the gate decides this
|
|
1850
|
+
// request is gated (resume above is unaffected: the gate never blocks
|
|
1851
|
+
// hearing, and the gate check stays after the resume check).
|
|
1852
|
+
let outcome = await browser.gateDecision(
|
|
1853
|
+
for: NativeGateRequest(reason: .search, path: nil, search: params),
|
|
1854
|
+
)
|
|
1855
|
+
guard !outcome.gated else {
|
|
1856
|
+
browser.logger.info("handlePlayMediaIntent: search refused — gated")
|
|
1857
|
+
browser.onGate(GateEvent(reason: .search))
|
|
1858
|
+
completion(false)
|
|
1859
|
+
return
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1228
1862
|
do {
|
|
1229
|
-
let
|
|
1230
|
-
guard let tracks = resolved.children, !tracks.isEmpty else {
|
|
1863
|
+
guard let tracks = try await browser.browserManager.searchPlayable(params) else {
|
|
1231
1864
|
completion(false)
|
|
1232
1865
|
return
|
|
1233
1866
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1867
|
+
player.setQueue(tracks, initialIndex: 0, playWhenReady: true)
|
|
1868
|
+
browser.showNowPlayingRequestedEmitter.emit(())
|
|
1237
1869
|
completion(true)
|
|
1238
1870
|
} catch {
|
|
1239
|
-
|
|
1871
|
+
browser.logger.error("handlePlayMediaIntent failed: \(error.localizedDescription)")
|
|
1240
1872
|
completion(false)
|
|
1241
1873
|
}
|
|
1242
1874
|
}
|
|
1243
1875
|
}
|
|
1244
1876
|
|
|
1245
|
-
public func remoteLike() {
|
|
1246
|
-
if let handler = handleRemoteLike {
|
|
1247
|
-
handler()
|
|
1248
|
-
} else {
|
|
1249
|
-
onRemoteLike()
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
public func remoteDislike() {
|
|
1254
|
-
if let handler = handleRemoteDislike {
|
|
1255
|
-
handler()
|
|
1256
|
-
} else {
|
|
1257
|
-
onRemoteDislike()
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
public func remoteBookmark() {
|
|
1262
|
-
if let handler = handleRemoteBookmark {
|
|
1263
|
-
handler()
|
|
1264
|
-
} else {
|
|
1265
|
-
onRemoteBookmark()
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
1877
|
public func remoteChangeRepeatMode(mode: RepeatMode) {
|
|
1270
1878
|
// Apply the repeat mode change from CarPlay/lock screen
|
|
1271
1879
|
try? setRepeatMode(mode: mode)
|