react-native-audio-browser 0.1.1 → 0.2.0-next.1
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 +180 -2
- package/android/build.gradle +10 -0
- package/android/gradle.properties +1 -1
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +401 -141
- 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/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 +443 -178
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +300 -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 +43 -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/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 +31 -37
- package/android/src/main/java/com/audiobrowser/player/AutomaticBufferManager.kt +4 -1
- package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +119 -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 +415 -131
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +154 -276
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +348 -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 +27 -5
- package/android/src/main/java/com/audiobrowser/player/PlaybackTimer.kt +56 -0
- package/android/src/main/java/com/audiobrowser/player/Player.kt +452 -576
- 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 +117 -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 +84 -40
- package/android/src/main/java/com/audiobrowser/player/VolumeFader.kt +81 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +141 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +37 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +41 -0
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +209 -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 +26 -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 +111 -80
- package/android/src/main/res/values/strings.xml +4 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +166 -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 +54 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +96 -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 +159 -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/NowPlayingUpdaterTest.kt +442 -0
- package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +191 -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 +107 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +73 -0
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +75 -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 -110
- 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 +750 -173
- package/ios/Model/NitroTypeStubs.swift +299 -224
- package/ios/Model/PlayerUpdateOptions.swift +23 -23
- package/ios/Model/RemoteCommand.swift +6 -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 +33 -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 +28 -10
- package/ios/Player/NowPlayingUpdater.swift +119 -26
- package/ios/Player/PersistedPlaybackState.swift +11 -0
- package/ios/Player/PlaybackCoordinator.swift +282 -51
- package/ios/Player/PlaybackCoordinatorCallbacks.swift +6 -1
- package/ios/Player/PlaybackEffectHandler.swift +19 -9
- package/ios/Player/PlaybackErrorHandler.swift +97 -13
- 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 +67 -61
- package/ios/Player/RetryManager.swift +97 -21
- package/ios/Player/SleepTimerHandling.swift +8 -0
- package/ios/Player/SleepTimerManager.swift +38 -5
- package/ios/Player/VolumeFader.swift +77 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +31 -15
- 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/LoadSeekCoordinatorTests.swift +38 -7
- package/ios/Tests/MediaIntentCriteriaTests.swift +170 -0
- package/ios/Tests/MediaLoaderAssetOptionsTests.swift +46 -0
- package/ios/Tests/MediaLoaderTests.swift +23 -8
- package/ios/Tests/MediaResolveTests.swift +126 -0
- package/ios/Tests/OnceValueTests.swift +79 -0
- package/ios/Tests/PlaybackCoordinatorTests.swift +784 -13
- package/ios/Tests/PlaybackErrorHandlerTests.swift +380 -46
- package/ios/Tests/PlaybackStateMachineTests.swift +33 -8
- package/ios/Tests/PlaybackStateStoreTests.swift +48 -0
- package/ios/Tests/PlaybackTimerTests.swift +55 -0
- package/ios/Tests/PlayingStateManagerTests.swift +57 -0
- package/ios/Tests/QueueManagerTests.swift +149 -18
- package/ios/Tests/ResolvedTrackCopyingTests.swift +15 -8
- package/ios/Tests/RetryManagerTests.swift +89 -0
- package/ios/Tests/SearchDrillInTests.swift +63 -0
- package/ios/Tests/SectionScopeTests.swift +82 -0
- package/ios/Tests/SleepTimerManagerTests.swift +104 -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 +380 -58
- package/ios/TrackPlayerCallbacks.swift +0 -6
- package/ios/TrackSelector.swift +5 -5
- package/ios/Util/ArtworkImageFetcher.swift +4 -2
- package/ios/Util/NetworkMonitor.swift +7 -3
- package/ios/Util/OnceValue.swift +34 -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 +59 -158
- 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/errors.js +34 -2
- package/lib/commonjs/features/errors.js.map +1 -1
- package/lib/commonjs/features/gate.js +149 -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.map +1 -1
- package/lib/commonjs/features/nowPlaying.js +35 -0
- 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/progress.js +28 -12
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/trackPlaybackTime.js +92 -0
- package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -0
- package/lib/commonjs/features/playback/volume.js +1 -1
- package/lib/commonjs/features/player/options.js +49 -13
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +249 -3
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +10 -3
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/features/remoteControls.js +1 -12
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/features/sleepTimer.js +9 -4
- 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/utils/LazyNativeEmitter.js +0 -3
- 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 +301 -124
- 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/features/browser-config.js +221 -0
- package/lib/module/features/browser-config.js.map +1 -0
- package/lib/module/features/browser.js +53 -156
- 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/errors.js +34 -2
- package/lib/module/features/errors.js.map +1 -1
- package/lib/module/features/gate.js +144 -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.map +1 -1
- package/lib/module/features/nowPlaying.js +33 -0
- 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/progress.js +28 -12
- package/lib/module/features/playback/progress.js.map +1 -1
- package/lib/module/features/playback/trackPlaybackTime.js +88 -0
- package/lib/module/features/playback/trackPlaybackTime.js.map +1 -0
- package/lib/module/features/playback/volume.js +1 -1
- package/lib/module/features/player/options.js +48 -13
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +249 -3
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/queue/queue.js +10 -3
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/features/remoteControls.js +0 -11
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/features/sleepTimer.js +9 -4
- 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/utils/LazyNativeEmitter.js +0 -3
- 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 +302 -125
- 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/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/errors.d.ts +58 -3
- package/lib/typescript/src/features/errors.d.ts.map +1 -1
- package/lib/typescript/src/features/gate.d.ts +77 -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 +6 -4
- package/lib/typescript/src/features/metadata.d.ts.map +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts +27 -0
- 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/progress.d.ts +3 -0
- package/lib/typescript/src/features/playback/progress.d.ts.map +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 +1 -1
- package/lib/typescript/src/features/player/options.d.ts +92 -93
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts +206 -167
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +9 -2
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/features/remoteControls.d.ts +0 -13
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts +18 -3
- 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/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 +0 -2
- 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 +3 -1
- package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +24 -10
- package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +6 -6
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +1 -11
- 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 +9 -4
- 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 +236 -212
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +25 -22
- 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 +16 -19
- package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +6 -0
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +1 -11
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +96 -0
- package/nitrogen/generated/android/c++/JNotificationButton.hpp +3 -3
- 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 +14 -9
- 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++/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_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 +2 -8
- 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/{ThumbsRating.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/{HeartRating.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/GateReason.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +103 -109
- 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 +2 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButton.kt +5 -5
- 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/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_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 +1098 -447
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +60 -39
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +170 -112
- package/nitrogen/generated/ios/swift/AndroidAudioContentType.swift +8 -8
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +2 -12
- 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 +20 -14
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +230 -197
- 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 +1 -30
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +90 -0
- package/nitrogen/generated/ios/swift/NotificationButton.swift +4 -4
- 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/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_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 +1 -12
- 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++/{HeartRating.hpp → Gate.hpp} +21 -16
- package/nitrogen/generated/shared/c++/{ThumbsRating.hpp → GateDecision.hpp} +23 -17
- package/nitrogen/generated/shared/c++/{PercentageRating.hpp → GateEvent.hpp} +18 -17
- package/nitrogen/generated/shared/c++/GateReason.hpp +76 -0
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +21 -18
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +44 -35
- 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 +1 -12
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +92 -0
- package/nitrogen/generated/shared/c++/NotificationButton.hpp +8 -8
- 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++/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 +11 -6
- package/src/codegen-no-config-variant.test.ts +70 -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 +58 -201
- package/src/features/carConnection.ts +36 -0
- package/src/features/errors.ts +68 -2
- package/src/features/gate.test.ts +72 -0
- package/src/features/gate.ts +155 -0
- package/src/features/index.ts +2 -0
- package/src/features/metadata.ts +6 -12
- package/src/features/nowPlaying.ts +36 -0
- 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/progress.ts +31 -12
- package/src/features/playback/trackPlaybackTime.test.ts +66 -0
- package/src/features/playback/trackPlaybackTime.ts +106 -0
- package/src/features/playback/volume.ts +1 -1
- package/src/features/player/options.test.ts +38 -0
- package/src/features/player/options.ts +140 -75
- package/src/features/player/setup.test.ts +117 -0
- package/src/features/player/setup.ts +390 -175
- package/src/features/queue/queue.test.ts +36 -0
- package/src/features/queue/queue.ts +15 -4
- package/src/features/remoteControls.ts +0 -23
- package/src/features/sleepTimer.ts +23 -4
- package/src/native.ts +5 -0
- package/src/native.web.ts +4 -1
- package/src/specs/audio-browser.nitro.ts +131 -41
- 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 +0 -2
- package/src/utils/NativeUpdatedValue.ts +0 -2
- package/src/utils/artwork.ts +30 -0
- package/src/web/NativeAudioBrowser.playWhenReady.test.ts +301 -0
- package/src/web/NativeAudioBrowser.ts +365 -158
- 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/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++/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/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/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/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++/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
|
@@ -24,19 +24,45 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
24
24
|
/// Shared instance for CarPlay access. Set when HybridAudioBrowser is created.
|
|
25
25
|
private(set) nonisolated(unsafe) weak static var shared: HybridAudioBrowser?
|
|
26
26
|
|
|
27
|
+
/// Fires when a new instance becomes `shared` (e.g. on a JS runtime reload),
|
|
28
|
+
/// so connected external controllers (CarPlay) can re-subscribe — their
|
|
29
|
+
/// listeners sit on the previous instance's emitters and would otherwise go
|
|
30
|
+
/// silent.
|
|
31
|
+
static let instanceChangedEmitter = Emitter<Void>()
|
|
32
|
+
|
|
27
33
|
// MARK: - Private Properties
|
|
28
34
|
|
|
29
35
|
private var player: TrackPlayer?
|
|
30
36
|
private let networkMonitor = NetworkMonitor()
|
|
37
|
+
private let playbackStateStore = PlaybackStateStore()
|
|
31
38
|
let browserManager = BrowserManager()
|
|
32
39
|
private let trackSelector: TrackSelector
|
|
33
40
|
private var volumeObservation: NSKeyValueObservation?
|
|
34
41
|
private var routeChangeObserver: NSObjectProtocol?
|
|
42
|
+
private var interruptionObserver: NSObjectProtocol?
|
|
43
|
+
private var mediaServicesResetObserver: NSObjectProtocol?
|
|
44
|
+
/// Re-applies our resolved audio-session category. Captured at setup so a
|
|
45
|
+
/// media-services reset (which clears the category) can restore it.
|
|
46
|
+
private var applyAudioSessionCategory: () -> Void = {}
|
|
35
47
|
private var nowPlayingOverride: NowPlayingUpdate?
|
|
48
|
+
/// When false, the now-playing surface uses the raw track fields (override + formatter ignored).
|
|
49
|
+
private var nowPlayingMetadataEnabled = true
|
|
50
|
+
/// Customizes the now-playing title/subtitle from the track + live timed metadata + playback state.
|
|
51
|
+
private var nowPlayingMetadataFormatter: ((_ params: FormatNowPlayingParams) -> Promise<NowPlayingUpdate?>)?
|
|
52
|
+
/// Keep the media session controllable through a terminal error (see `keepSessionAliveOnError`).
|
|
53
|
+
private var keepSessionAliveOnError = true
|
|
54
|
+
/// Initial player state staged before the player exists — from setup options or the imperative
|
|
55
|
+
/// setters called pre-setup. Strict last-write-wins; consumed when the player comes up.
|
|
56
|
+
private var pendingPlayWhenReady: Bool?
|
|
57
|
+
private var pendingRepeatMode: RepeatMode?
|
|
58
|
+
/// Latest live timed (ICY/ID3) metadata, passed to the formatter. Cleared on track change.
|
|
59
|
+
private var latestTimedMetadata: TimedMetadata?
|
|
36
60
|
private let playerOptions = PlayerUpdateOptions()
|
|
37
61
|
|
|
38
62
|
/// Configured playback rates for the playback-rate capability (for CarPlay rate cycling)
|
|
39
63
|
var playbackRates: [Double] { playerOptions.playbackRates }
|
|
64
|
+
var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
|
|
65
|
+
var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] { playerOptions.carPlayNowPlayingButtons }
|
|
40
66
|
private var lastNavigationError: NavigationError? {
|
|
41
67
|
didSet {
|
|
42
68
|
// Skip if both nil (no real change)
|
|
@@ -63,7 +89,21 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
63
89
|
public let queueChangedEmitter = Emitter<[Track]>()
|
|
64
90
|
public let navigationErrorEmitter = Emitter<NavigationErrorEvent>()
|
|
65
91
|
public let repeatModeChangedEmitter = Emitter<RepeatModeChangedEvent>()
|
|
92
|
+
public let shuffleChangedEmitter = Emitter<Bool>()
|
|
66
93
|
public let externalContentChangedEmitter = Emitter<String>()
|
|
94
|
+
/// Signals that the gate's active state changed (set or cleared). Carries
|
|
95
|
+
/// the active flag; per-request chrome is obtained at each serve site via
|
|
96
|
+
/// `gateDecision(for:)`, so the emitter only tells surfaces to re-render.
|
|
97
|
+
public let gateChangedEmitter = Emitter<Bool>()
|
|
98
|
+
/// Fired when a voice media intent (`handlePlayMediaIntent`) successfully
|
|
99
|
+
/// starts playback. CarPlay surfaces the Now Playing template in response, so
|
|
100
|
+
/// the user lands on the playing station (with the rest of the results in Up
|
|
101
|
+
/// Next) rather than the screen they invoked Siri from. No-op where unobserved.
|
|
102
|
+
public let showNowPlayingRequestedEmitter = Emitter<Void>()
|
|
103
|
+
/// Fires after `updateOptions` applies a change, so external-surface managers
|
|
104
|
+
/// (CarPlay) can refresh UI driven by now-runtime-updatable options
|
|
105
|
+
/// (now-playing buttons, Up Next).
|
|
106
|
+
public let playerOptionsChangedEmitter = Emitter<Void>()
|
|
67
107
|
|
|
68
108
|
// MARK: - Thread Safety
|
|
69
109
|
|
|
@@ -71,9 +111,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
71
111
|
/// because DispatchQueue.main.sync blocks the caller — no concurrent access window.
|
|
72
112
|
private func onMainActor<T>(_ work: @MainActor () -> T) -> T {
|
|
73
113
|
if Thread.isMainThread {
|
|
74
|
-
|
|
114
|
+
MainActor.assumeIsolated { UncheckedSendableBox(value: work()) }.value
|
|
75
115
|
} else {
|
|
76
|
-
|
|
116
|
+
DispatchQueue.main.sync {
|
|
77
117
|
MainActor.assumeIsolated { UncheckedSendableBox(value: work()) }.value
|
|
78
118
|
}
|
|
79
119
|
}
|
|
@@ -81,10 +121,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
81
121
|
|
|
82
122
|
private func onMainActor<T>(_ work: @MainActor () throws -> T) throws -> T {
|
|
83
123
|
if Thread.isMainThread {
|
|
84
|
-
|
|
124
|
+
try MainActor.assumeIsolated { try UncheckedSendableBox(value: work()) }.value
|
|
85
125
|
} else {
|
|
86
|
-
|
|
87
|
-
try MainActor.assumeIsolated { UncheckedSendableBox(value:
|
|
126
|
+
try DispatchQueue.main.sync {
|
|
127
|
+
try MainActor.assumeIsolated { try UncheckedSendableBox(value: work()) }.value
|
|
88
128
|
}
|
|
89
129
|
}
|
|
90
130
|
}
|
|
@@ -111,9 +151,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
111
151
|
}
|
|
112
152
|
|
|
113
153
|
public var configuration: NativeBrowserConfiguration = .init(
|
|
114
|
-
path: nil, request: nil, media: nil, artwork: nil, routes: nil,
|
|
115
|
-
singleTrack: nil, handleTrackLoad: nil,
|
|
116
|
-
|
|
154
|
+
path: nil, request: nil, requestResolver: nil, browse: nil, browseResolver: nil, media: nil, artwork: nil, nowPlayingArtwork: nil, routes: nil,
|
|
155
|
+
singleTrack: nil, handleTrackLoad: nil,
|
|
156
|
+
androidControllerOfflineError: nil, carPlayLoadingTitle: nil, resolveAlbumUrl: nil, formatNavigationError: nil,
|
|
117
157
|
) {
|
|
118
158
|
didSet {
|
|
119
159
|
onMainActor { browserManager.config = BrowserConfig(from: configuration) }
|
|
@@ -149,6 +189,25 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
149
189
|
public var onNavigationError: (NavigationErrorEvent) -> Void = { _ in }
|
|
150
190
|
public var onFormattedNavigationError: (FormattedNavigationError?) -> Void = { _ in }
|
|
151
191
|
|
|
192
|
+
/// Per-request gate decision, set by JS. Native calls it at a serve site to
|
|
193
|
+
/// learn whether a browse path / search interaction should be gated and with
|
|
194
|
+
/// what chrome. The double `Promise` mirrors every other native→JS
|
|
195
|
+
/// value-returning callback (e.g. `searchCallback`): the outer resolves to the
|
|
196
|
+
/// JS-side Promise, the inner to the decision.
|
|
197
|
+
///
|
|
198
|
+
/// Default fails CLOSED (`gated: true`). This default is only consulted in the
|
|
199
|
+
/// init window where `hasResolver` was already recorded true but JS hasn't
|
|
200
|
+
/// (re)bound the real resolver yet (JS reload / instance churn re-seeding gate
|
|
201
|
+
/// state, e.g. CarPlay re-seeding `isGated` at scene connect). Serving content
|
|
202
|
+
/// in that window would be a fail-open leak — same direction as the
|
|
203
|
+
/// resolver-error path in `gateDecision`. Matches Android's default.
|
|
204
|
+
public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = { _ in
|
|
205
|
+
Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// Fired when a request is gated (served the gate). Set by JS.
|
|
209
|
+
public var onGate: (GateEvent) -> Void = { _ in }
|
|
210
|
+
|
|
152
211
|
// MARK: - Player Callbacks
|
|
153
212
|
|
|
154
213
|
public var onChapterMetadata: ([ChapterMetadata]) -> Void = { _ in }
|
|
@@ -159,6 +218,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
159
218
|
public var onPlaybackPlayWhenReadyChanged: (PlaybackPlayWhenReadyChangedEvent) -> Void = { _ in }
|
|
160
219
|
public var onPlaybackPlayingState: (PlayingState) -> Void = { _ in }
|
|
161
220
|
public var onPlaybackProgressUpdated: (PlaybackProgressUpdatedEvent) -> Void = { _ in }
|
|
221
|
+
public var onPlaybackInterval: () -> Void = {}
|
|
162
222
|
public var onPlaybackQueueEnded: (PlaybackQueueEndedEvent) -> Void = { _ in }
|
|
163
223
|
public var onPlaybackQueueChanged: ([Track]) -> Void = { _ in }
|
|
164
224
|
public var onPlaybackRepeatModeChanged: (RepeatModeChangedEvent) -> Void = { _ in }
|
|
@@ -168,11 +228,8 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
168
228
|
|
|
169
229
|
// MARK: - Remote Callbacks
|
|
170
230
|
|
|
171
|
-
public var onRemoteBookmark: () -> Void = {}
|
|
172
|
-
public var onRemoteDislike: () -> Void = {}
|
|
173
231
|
public var onRemoteJumpBackward: (RemoteJumpBackwardEvent) -> Void = { _ in }
|
|
174
232
|
public var onRemoteJumpForward: (RemoteJumpForwardEvent) -> Void = { _ in }
|
|
175
|
-
public var onRemoteLike: () -> Void = {}
|
|
176
233
|
public var onRemoteNext: () -> Void = {}
|
|
177
234
|
public var onRemotePause: () -> Void = {}
|
|
178
235
|
public var onRemotePlay: () -> Void = {}
|
|
@@ -180,17 +237,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
180
237
|
public var onRemotePlaySearch: (RemotePlaySearchEvent) -> Void = { _ in }
|
|
181
238
|
public var onRemotePrevious: () -> Void = {}
|
|
182
239
|
public var onRemoteSeek: (RemoteSeekEvent) -> Void = { _ in }
|
|
183
|
-
public var onRemoteSetRating: (RemoteSetRatingEvent) -> Void = { _ in }
|
|
184
240
|
public var onRemoteSkip: (RemoteSkipEvent) -> Void = { _ in }
|
|
185
241
|
public var onRemoteStop: () -> Void = {}
|
|
186
242
|
|
|
187
243
|
// MARK: - Remote Handlers (optional overrides from JS)
|
|
188
244
|
|
|
189
|
-
public var handleRemoteBookmark: (() -> Void)?
|
|
190
|
-
public var handleRemoteDislike: (() -> Void)?
|
|
191
245
|
public var handleRemoteJumpBackward: ((RemoteJumpBackwardEvent) -> Void)?
|
|
192
246
|
public var handleRemoteJumpForward: ((RemoteJumpForwardEvent) -> Void)?
|
|
193
|
-
public var handleRemoteLike: (() -> Void)?
|
|
194
247
|
public var handleRemoteNext: (() -> Void)?
|
|
195
248
|
public var handleRemotePause: (() -> Void)?
|
|
196
249
|
public var handleRemotePlay: (() -> Void)?
|
|
@@ -208,10 +261,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
208
261
|
public var onNowPlayingChanged: (NowPlayingMetadata) -> Void = { _ in }
|
|
209
262
|
public var onOnlineChanged: (Bool) -> Void = { _ in } {
|
|
210
263
|
didSet {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// Immediately notify current state
|
|
264
|
+
// The monitor->JS bridge is wired centrally in setupPlayer (so it doesn't depend on a JS
|
|
265
|
+
// subscription and can also drive the now-playing re-render + player reconnect). Here we only
|
|
266
|
+
// push the current state to a newly-attached subscriber.
|
|
215
267
|
onOnlineChanged(networkMonitor.isOnline)
|
|
216
268
|
}
|
|
217
269
|
}
|
|
@@ -225,7 +277,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
225
277
|
}
|
|
226
278
|
}
|
|
227
279
|
|
|
228
|
-
public var
|
|
280
|
+
public var onOutputChanged: (Output) -> Void = { _ in } {
|
|
229
281
|
didSet {
|
|
230
282
|
setupRouteChangeObserver()
|
|
231
283
|
}
|
|
@@ -245,6 +297,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
245
297
|
HybridAudioBrowser.shared?.player?.destroy()
|
|
246
298
|
playerAndConfiguredBrowser.reset()
|
|
247
299
|
HybridAudioBrowser.shared = self
|
|
300
|
+
// Tell connected external controllers (CarPlay) to re-subscribe against
|
|
301
|
+
// this instance. Unconditional: `shared` is weak, so on a JS reload the
|
|
302
|
+
// old instance may already be gone — "shared was nil" does not mean no
|
|
303
|
+
// controller is subscribed to a dead instance's emitters.
|
|
304
|
+
HybridAudioBrowser.instanceChangedEmitter.emit(())
|
|
248
305
|
}
|
|
249
306
|
setupEmitterToNitroForwarding()
|
|
250
307
|
setupBrowserCallbacks()
|
|
@@ -319,13 +376,17 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
319
376
|
let defaultFormatted = navError.defaultFormatted()
|
|
320
377
|
if let formatter = onMainActor({ browserManager.config.formatNavigationError }) {
|
|
321
378
|
let params = FormatNavigationErrorParams(error: navError, defaultFormatted: defaultFormatted, path: path)
|
|
322
|
-
// Dispatch to main thread for the Nitro bridge call to avoid C++ noexcept crashes
|
|
379
|
+
// Dispatch to main thread for the Nitro bridge call to avoid C++ noexcept crashes.
|
|
380
|
+
// The result callbacks are @Sendable: a plain closure would inherit MainActor
|
|
381
|
+
// isolation from this block, and Nitro resolves its promises synchronously on the
|
|
382
|
+
// JS thread — Swift 6.2's dynamic isolation check then traps on closure entry.
|
|
383
|
+
// The property is JS-thread-touched elsewhere too, so no hop back is needed.
|
|
323
384
|
DispatchQueue.main.async { [weak self] in
|
|
324
385
|
formatter(params)
|
|
325
|
-
.then { customDisplay in
|
|
386
|
+
.then { @Sendable [weak self] customDisplay in
|
|
326
387
|
self?.lastFormattedNavigationError = customDisplay ?? defaultFormatted
|
|
327
388
|
}
|
|
328
|
-
.catch { _ in
|
|
389
|
+
.catch { @Sendable [weak self] _ in
|
|
329
390
|
self?.lastFormattedNavigationError = defaultFormatted
|
|
330
391
|
}
|
|
331
392
|
}
|
|
@@ -354,11 +415,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
354
415
|
guard let player else { return }
|
|
355
416
|
let result = await trackSelector.select(track: track, player: player)
|
|
356
417
|
switch result {
|
|
357
|
-
case .play(
|
|
418
|
+
case let .play(intent):
|
|
358
419
|
executePlayback(intent)
|
|
359
420
|
case .intercepted:
|
|
360
421
|
break
|
|
361
|
-
case .browse(
|
|
422
|
+
case let .browse(url):
|
|
362
423
|
navigateToBrowsableUrl(url)
|
|
363
424
|
case .none:
|
|
364
425
|
break
|
|
@@ -369,15 +430,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
369
430
|
private func executePlayback(_ intent: TrackSelector.PlaybackIntent) {
|
|
370
431
|
onMainActor {
|
|
371
432
|
switch intent {
|
|
372
|
-
case .skipTo(
|
|
433
|
+
case let .skipTo(index):
|
|
373
434
|
do {
|
|
374
435
|
try player?.skipTo(index, playWhenReady: true)
|
|
375
436
|
} catch {
|
|
376
437
|
logger.error("Failed to skip to track at index \(index): \(error)")
|
|
377
438
|
}
|
|
378
|
-
case .setQueue(
|
|
439
|
+
case let .setQueue(tracks, startIndex, sourcePath):
|
|
379
440
|
player?.setQueue(tracks, initialIndex: startIndex, playWhenReady: true, sourcePath: sourcePath)
|
|
380
|
-
case .loadTrack(
|
|
441
|
+
case let .loadTrack(track):
|
|
381
442
|
player?.load(track, playWhenReady: true)
|
|
382
443
|
}
|
|
383
444
|
}
|
|
@@ -416,6 +477,12 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
416
477
|
lastFormattedNavigationError
|
|
417
478
|
}
|
|
418
479
|
|
|
480
|
+
/// Internal signal sent on `externalContentChangedEmitter` to tell CarPlay to
|
|
481
|
+
/// refresh every displayed template. Not a real path (paths are always
|
|
482
|
+
/// `/…`), so it can't collide with one; kept internal — the public API is
|
|
483
|
+
/// `invalidateAllContent()`.
|
|
484
|
+
static let invalidateAllSentinel = "__rnab_invalidate_all__"
|
|
485
|
+
|
|
419
486
|
public func notifyContentChanged(path: String) throws {
|
|
420
487
|
onMainActor { browserManager.invalidateContentCache(path) }
|
|
421
488
|
|
|
@@ -434,22 +501,168 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
434
501
|
}
|
|
435
502
|
}
|
|
436
503
|
|
|
504
|
+
public func invalidateAllContent() throws {
|
|
505
|
+
onMainActor { browserManager.clearContentCache() }
|
|
506
|
+
|
|
507
|
+
// Tell external controllers (CarPlay) to refresh every displayed template.
|
|
508
|
+
externalContentChangedEmitter.emit(Self.invalidateAllSentinel)
|
|
509
|
+
|
|
510
|
+
// Re-query the tabs source too: a callback source can resolve to new
|
|
511
|
+
// titles after an invalidation (e.g. a locale switch). BrowserManager
|
|
512
|
+
// emits tabsChanged only when the result actually differs, and a failed
|
|
513
|
+
// re-query keeps the current tabs. (Android needs no equivalent — its
|
|
514
|
+
// root re-query already runs queryTabs(); the web stub re-queries tabs on
|
|
515
|
+
// the re-navigation.)
|
|
516
|
+
Task {
|
|
517
|
+
_ = try? await browserManager.queryTabs()
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Re-resolve the JS-facing current path with the cache cleared.
|
|
521
|
+
Task {
|
|
522
|
+
do {
|
|
523
|
+
try await browserManager.refresh()
|
|
524
|
+
} catch {
|
|
525
|
+
handleNavigationError(error, path: onMainActor { browserManager.getPath() })
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
437
530
|
public func setFavorites(favorites: [String]) throws {
|
|
438
531
|
onMainActor { browserManager.setFavorites(favorites) }
|
|
439
532
|
}
|
|
440
533
|
|
|
534
|
+
// MARK: - Gate
|
|
535
|
+
|
|
536
|
+
/// The gate's default chrome, if one was set. Served when the resolver
|
|
537
|
+
/// returns a gated decision with no per-request override, and for the static
|
|
538
|
+
/// gate (no resolver). May be nil for a resolver-only gate.
|
|
539
|
+
private(set) var defaultChrome: Gate?
|
|
540
|
+
|
|
541
|
+
/// Whether JS installed a per-request resolver. When false the gate is static
|
|
542
|
+
/// — every request is gated with `defaultChrome` and the JS hop is skipped.
|
|
543
|
+
private(set) var hasResolver = false
|
|
544
|
+
|
|
545
|
+
/// True once `setGate` runs, until `clearGate`. While active, external
|
|
546
|
+
/// surfaces (CarPlay) keep their tabs visible but consult `gateDecision(for:)`
|
|
547
|
+
/// per request, rendering the gate chrome where gated and refusing
|
|
548
|
+
/// external-surface search. Playback, the queue and Now Playing are
|
|
549
|
+
/// unaffected — a gate blocks finding content, never hearing it.
|
|
550
|
+
private(set) var isGateActive = false
|
|
551
|
+
|
|
552
|
+
/// The minimal built-in gate served when a request is gated but neither a
|
|
553
|
+
/// per-request override nor a stored default chrome exists (resolver-only
|
|
554
|
+
/// gate returning `true`).
|
|
555
|
+
static let builtInGate = Gate(title: "Unavailable", message: nil)
|
|
556
|
+
|
|
557
|
+
/// The outcome of a single gate decision: whether to gate, and (if so) the
|
|
558
|
+
/// chrome to render.
|
|
559
|
+
struct GateOutcome {
|
|
560
|
+
let gated: Bool
|
|
561
|
+
let chrome: Gate?
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
public func setGate(gate: Gate?, hasResolver: Bool) throws {
|
|
565
|
+
onMainActor {
|
|
566
|
+
defaultChrome = gate
|
|
567
|
+
self.hasResolver = hasResolver
|
|
568
|
+
isGateActive = true
|
|
569
|
+
gateChangedEmitter.emit(true)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
public func clearGate() throws {
|
|
574
|
+
onMainActor {
|
|
575
|
+
guard isGateActive else { return }
|
|
576
|
+
defaultChrome = nil
|
|
577
|
+
hasResolver = false
|
|
578
|
+
isGateActive = false
|
|
579
|
+
gateChangedEmitter.emit(false)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/// The single choke point for every gate enforcement site. Asks whether
|
|
584
|
+
/// `request` should be gated and, if so, with what chrome (override → stored
|
|
585
|
+
/// default → built-in). When no resolver is installed the gate is static and
|
|
586
|
+
/// resolves without a JS hop.
|
|
587
|
+
///
|
|
588
|
+
/// Resolver errors **fail CLOSED by design**: a thrown/rejected resolver, or a
|
|
589
|
+
/// rejection at either `.await()` layer (bridge tear-down on JS reload, the JS
|
|
590
|
+
/// runtime mid-reload, a serialization error) cannot prove the request is
|
|
591
|
+
/// allowed — and a gate exists to withhold content, so serving on error would
|
|
592
|
+
/// be a content leak (a paywall bypass). On error we gate with the stored
|
|
593
|
+
/// default / built-in chrome so the user still sees a coherent gate page. A
|
|
594
|
+
/// *successful* resolver returning `gated: false` still allows the request.
|
|
595
|
+
@MainActor
|
|
596
|
+
func gateDecision(for request: NativeGateRequest) async -> GateOutcome {
|
|
597
|
+
guard isGateActive else { return GateOutcome(gated: false, chrome: nil) }
|
|
598
|
+
if !hasResolver { return GateOutcome(gated: true, chrome: defaultChrome ?? Self.builtInGate) }
|
|
599
|
+
guard let decision = try? await resolveGate(request).await().await() else {
|
|
600
|
+
// Resolver error: fail CLOSED (see doc comment) — gate with the default /
|
|
601
|
+
// built-in so the gate page still renders rather than leaking content.
|
|
602
|
+
return GateOutcome(gated: true, chrome: defaultChrome ?? Self.builtInGate)
|
|
603
|
+
}
|
|
604
|
+
guard decision.gated else { return GateOutcome(gated: false, chrome: nil) } // explicit allow
|
|
605
|
+
return GateOutcome(gated: true, chrome: decision.gate ?? defaultChrome ?? Self.builtInGate)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// MARK: - Car Connection (CarPlay)
|
|
609
|
+
|
|
610
|
+
/// Static because the CarPlay scene can connect before the JS runtime
|
|
611
|
+
/// creates the shared instance (cold start in the car).
|
|
612
|
+
private nonisolated(unsafe) static var carPlayConnected = false
|
|
613
|
+
|
|
614
|
+
/// Called by the CarPlay scene delegate (via `RNABCarPlayController`) on
|
|
615
|
+
/// scene connect/disconnect — not from controller start/stop, which also
|
|
616
|
+
/// cycle on a JS reload and would emit spurious connection changes.
|
|
617
|
+
static func setCarPlayConnected(_ connected: Bool) {
|
|
618
|
+
guard carPlayConnected != connected else { return }
|
|
619
|
+
carPlayConnected = connected
|
|
620
|
+
shared?.onCarConnectedChanged(connected)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
public func isCarConnected() throws -> Bool {
|
|
624
|
+
HybridAudioBrowser.carPlayConnected
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
public var onCarConnectedChanged: (Bool) -> Void = { _ in } {
|
|
628
|
+
didSet {
|
|
629
|
+
// Immediately notify current state (the scene may have connected before
|
|
630
|
+
// this JS runtime subscribed).
|
|
631
|
+
onCarConnectedChanged(HybridAudioBrowser.carPlayConnected)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
441
635
|
// MARK: - Player Setup
|
|
442
636
|
|
|
443
|
-
public func setupPlayer(options:
|
|
637
|
+
public func setupPlayer(options: NativeSetupPlayerOptions) throws -> Promise<Void> {
|
|
444
638
|
Promise.async {
|
|
445
|
-
// Configure audio session
|
|
639
|
+
// Configure the audio session from the iOS setup options (category, mode, route policy, and
|
|
640
|
+
// options like `allowBluetooth` / `allowAirPlay`), but do NOT activate it here. Activating a
|
|
641
|
+
// non-mixable `.playback` session interrupts other apps' audio — so activating at setup
|
|
642
|
+
// pauses whatever the user is listening to (Safari, Spotify, a podcast) the instant our app
|
|
643
|
+
// launches, before they play anything. Setting the category is silent; only activation
|
|
644
|
+
// interrupts. The session is activated lazily when playback actually starts producing output
|
|
645
|
+
// (see `playerDidChangePlayingState`) and re-activated on interruption-end. Best-effort: a
|
|
646
|
+
// category-config failure must never brick setup.
|
|
446
647
|
let session = AVAudioSession.sharedInstance()
|
|
447
|
-
|
|
448
|
-
|
|
648
|
+
let cfg: (
|
|
649
|
+
category: AVAudioSession.Category,
|
|
650
|
+
mode: AVAudioSession.Mode,
|
|
651
|
+
policy: AVAudioSession.RouteSharingPolicy,
|
|
652
|
+
options: AVAudioSession.CategoryOptions
|
|
653
|
+
) = options.ios?.resolveAudioSessionConfig()
|
|
654
|
+
?? (category: .playback, mode: .default, policy: .default, options: [])
|
|
655
|
+
// Capture category application once so a media-services reset (which
|
|
656
|
+
// clears the category) can restore the exact same config later.
|
|
657
|
+
let applyCategory: () -> Void = {
|
|
658
|
+
try? session.setCategory(cfg.category, mode: cfg.mode, policy: cfg.policy, options: cfg.options)
|
|
659
|
+
}
|
|
660
|
+
applyCategory()
|
|
449
661
|
|
|
450
662
|
// Create player and configure on main actor
|
|
451
663
|
await MainActor.run { [weak self] in
|
|
452
664
|
guard let self else { return }
|
|
665
|
+
self.applyAudioSessionCategory = applyCategory
|
|
453
666
|
|
|
454
667
|
// Create player with self as callbacks delegate
|
|
455
668
|
player = TrackPlayer(callbacks: self)
|
|
@@ -467,18 +680,52 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
467
680
|
// Wire up network monitor for accelerated retries when connectivity is restored
|
|
468
681
|
player?.networkMonitor = networkMonitor
|
|
469
682
|
|
|
683
|
+
// Central fan-out for connectivity changes (wired once here, idempotent on re-setup, and
|
|
684
|
+
// independent of any JS subscription):
|
|
685
|
+
// - bridge to the JS `onOnlineChanged` event (no-op default until JS subscribes);
|
|
686
|
+
// - re-render now-playing so the formatter's offline/online label updates immediately on a
|
|
687
|
+
// connectivity change, not only on the next playback transition (fixes the stall-entry
|
|
688
|
+
// race where the device goes offline after the buffering line was already rendered);
|
|
689
|
+
// - reconnect a stalled stream when connectivity returns (see TrackPlayer.handleNetworkRestored).
|
|
690
|
+
// The monitor only mutates state on the main thread, so this fires main-isolated.
|
|
691
|
+
networkMonitor.onChanged = { [weak self] isOnline in
|
|
692
|
+
MainActor.assumeIsolated {
|
|
693
|
+
guard let self else { return }
|
|
694
|
+
self.onOnlineChanged(isOnline)
|
|
695
|
+
self.applyNowPlayingMetadata()
|
|
696
|
+
if isOnline { self.player?.handleNetworkRestored() }
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Observe audio-session interruptions so another app taking over audio
|
|
701
|
+
// (or a phone call) reflects as a real pause rather than a stuck state.
|
|
702
|
+
setupInterruptionObserver()
|
|
703
|
+
|
|
704
|
+
// Observe media-server resets so a long-running stream recovers instead
|
|
705
|
+
// of going permanently silent.
|
|
706
|
+
setupMediaServicesResetObserver()
|
|
707
|
+
|
|
470
708
|
// Configure media URL resolver
|
|
471
|
-
player?.mediaLoader.mediaUrlResolver = { [weak self] src in
|
|
709
|
+
player?.mediaLoader.mediaUrlResolver = { [weak self] src, track in
|
|
472
710
|
guard let self else {
|
|
473
711
|
return MediaResolvedUrl(url: src, headers: nil, userAgent: nil)
|
|
474
712
|
}
|
|
475
|
-
return await browserManager.resolveMediaUrl(src)
|
|
713
|
+
return await browserManager.resolveMediaUrl(src, track: track)
|
|
476
714
|
}
|
|
477
715
|
|
|
478
716
|
// Configure artwork URL resolver for Now Playing (with size context)
|
|
479
717
|
player?.nowPlayingUpdater.artworkUrlResolver = { [weak self] track, imageContext in
|
|
480
718
|
guard let self else { return nil }
|
|
481
|
-
|
|
719
|
+
let perRoute = (track.id?.isEmpty == false) ? browserManager.config.nowPlayingArtwork : nil
|
|
720
|
+
return await browserManager.resolveArtworkUrl(track: track, perRouteConfig: perRoute, imageContext: imageContext)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Emit the JS `onNowPlayingChanged` event when the rendered text lines change. The updater
|
|
724
|
+
// owns the system now-playing write; this shapes the JS metadata (elapsed time / artwork).
|
|
725
|
+
player?.nowPlayingUpdater.onChanged = { [weak self] track, title, artist, album in
|
|
726
|
+
guard let self else { return }
|
|
727
|
+
self.onNowPlayingChanged(
|
|
728
|
+
self.makeNowPlayingMetadata(track: track, title: title, artist: artist, album: album))
|
|
482
729
|
}
|
|
483
730
|
|
|
484
731
|
// Configure sleep timer callback
|
|
@@ -486,8 +733,30 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
486
733
|
self?.onSleepTimerChanged(state)
|
|
487
734
|
}
|
|
488
735
|
|
|
489
|
-
//
|
|
490
|
-
|
|
736
|
+
// Now-playing: whether to manage metadata, the optional formatter that owns the rendered
|
|
737
|
+
// lines, and whether to keep the session alive through a terminal error.
|
|
738
|
+
nowPlayingMetadataEnabled = options.autoUpdateNowPlayingMetadata ?? true
|
|
739
|
+
nowPlayingMetadataFormatter = options.nowPlayingMetadataFormatter
|
|
740
|
+
// Stored for API symmetry, but iOS needs no masking: a terminal error resolves the playback
|
|
741
|
+
// state to *paused* (PlaybackCoordinator.applySideEffects), the now-playing info is retained
|
|
742
|
+
// (never cleared on error), and next/previous stay enabled (gated on queue position, not
|
|
743
|
+
// state). So the session stays controllable through errors by default — the behavior Android
|
|
744
|
+
// achieves via InterceptingPlayer is already the iOS norm.
|
|
745
|
+
keepSessionAliveOnError = options.keepSessionAliveOnError ?? true
|
|
746
|
+
|
|
747
|
+
// The bundled runtime options and initial state are part of the atomic launch
|
|
748
|
+
// description. Stage the state first so setup options and earlier pre-setup setter
|
|
749
|
+
// calls resolve by strict last-write-wins, then apply everything to the live player.
|
|
750
|
+
if let bundled = options.options {
|
|
751
|
+
updateOptions(options: bundled)
|
|
752
|
+
} else {
|
|
753
|
+
// updateOptions applies remote commands itself; without bundled options, apply the
|
|
754
|
+
// defaults (play, pause, next, previous, seekTo) here.
|
|
755
|
+
applyRemoteCommands()
|
|
756
|
+
}
|
|
757
|
+
if let mode = options.repeatMode { pendingRepeatMode = mode }
|
|
758
|
+
if let intent = options.playWhenReady { pendingPlayWhenReady = intent }
|
|
759
|
+
applyPendingPlayerState()
|
|
491
760
|
|
|
492
761
|
// Notify listeners that player is ready (e.g., CarPlay)
|
|
493
762
|
playerAndConfiguredBrowser.check()
|
|
@@ -495,6 +764,22 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
495
764
|
}
|
|
496
765
|
}
|
|
497
766
|
|
|
767
|
+
/// Applies initial player state staged before the player existed — from setup options or the
|
|
768
|
+
/// imperative setters called pre-setup. Consumed on apply so a later re-setup doesn't replay
|
|
769
|
+
/// stale state.
|
|
770
|
+
@MainActor
|
|
771
|
+
private func applyPendingPlayerState() {
|
|
772
|
+
guard let player else { return }
|
|
773
|
+
if let mode = pendingRepeatMode {
|
|
774
|
+
player.repeatMode = mode
|
|
775
|
+
pendingRepeatMode = nil
|
|
776
|
+
}
|
|
777
|
+
if let intent = pendingPlayWhenReady {
|
|
778
|
+
player.playWhenReady = intent
|
|
779
|
+
pendingPlayWhenReady = nil
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
498
783
|
public func updateOptions(options: NativeUpdateOptions) {
|
|
499
784
|
onMainActor {
|
|
500
785
|
let previousInterval = playerOptions.progressUpdateEventInterval
|
|
@@ -502,6 +787,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
502
787
|
// Update stored options
|
|
503
788
|
playerOptions.update(from: options)
|
|
504
789
|
|
|
790
|
+
// Propagate the favorite match mode to the browser so it can hydrate
|
|
791
|
+
// row hearts (the `favorite` capability is the single favoriting switch).
|
|
792
|
+
browserManager.setFavoriteMatch(playerOptions.capabilities.favoriteMatch)
|
|
793
|
+
|
|
505
794
|
// Apply remote commands to player
|
|
506
795
|
applyRemoteCommands()
|
|
507
796
|
|
|
@@ -509,6 +798,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
509
798
|
if playerOptions.progressUpdateEventInterval != previousInterval {
|
|
510
799
|
player?.setProgressUpdateInterval(playerOptions.progressUpdateEventInterval)
|
|
511
800
|
}
|
|
801
|
+
|
|
802
|
+
onOptionsChanged(playerOptions.toOptions())
|
|
803
|
+
playerOptionsChangedEmitter.emit(())
|
|
512
804
|
}
|
|
513
805
|
}
|
|
514
806
|
|
|
@@ -526,8 +818,12 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
526
818
|
player.remoteCommands = remoteCommands
|
|
527
819
|
}
|
|
528
820
|
|
|
529
|
-
public func getOptions() throws ->
|
|
530
|
-
playerOptions.
|
|
821
|
+
public func getOptions() throws -> Options {
|
|
822
|
+
playerOptions.toOptions()
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
public func setPlaybackIntervalEnabled(enabled: Bool) {
|
|
826
|
+
onMainActor { player?.setPlaybackIntervalEnabled(enabled) }
|
|
531
827
|
}
|
|
532
828
|
|
|
533
829
|
// MARK: - Playback Control
|
|
@@ -562,11 +858,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
562
858
|
}
|
|
563
859
|
|
|
564
860
|
public func setPlayWhenReady(playWhenReady: Bool) throws {
|
|
565
|
-
onMainActor {
|
|
861
|
+
onMainActor {
|
|
862
|
+
// Pre-setup this stages the intent for the player to come up with — never a no-op.
|
|
863
|
+
if let player { player.playWhenReady = playWhenReady }
|
|
864
|
+
else { pendingPlayWhenReady = playWhenReady }
|
|
865
|
+
}
|
|
566
866
|
}
|
|
567
867
|
|
|
568
868
|
public func getPlayWhenReady() throws -> Bool {
|
|
569
|
-
onMainActor { player?.playWhenReady ?? false }
|
|
869
|
+
onMainActor { player?.playWhenReady ?? pendingPlayWhenReady ?? false }
|
|
570
870
|
}
|
|
571
871
|
|
|
572
872
|
public func seekTo(position: Double) throws {
|
|
@@ -577,6 +877,10 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
577
877
|
onMainActor { player?.seekBy(offset) }
|
|
578
878
|
}
|
|
579
879
|
|
|
880
|
+
public func seekToLiveEdge() throws {
|
|
881
|
+
onMainActor { player?.seekToLiveEdge() }
|
|
882
|
+
}
|
|
883
|
+
|
|
580
884
|
public func setVolume(level: Double) throws {
|
|
581
885
|
onMainActor { player?.volume = Float(level) }
|
|
582
886
|
}
|
|
@@ -614,11 +918,15 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
614
918
|
}
|
|
615
919
|
|
|
616
920
|
public func getRepeatMode() throws -> RepeatMode {
|
|
617
|
-
onMainActor { player?.repeatMode ?? .off }
|
|
921
|
+
onMainActor { player?.repeatMode ?? pendingRepeatMode ?? .off }
|
|
618
922
|
}
|
|
619
923
|
|
|
620
924
|
public func setRepeatMode(mode: RepeatMode) throws {
|
|
621
|
-
onMainActor {
|
|
925
|
+
onMainActor {
|
|
926
|
+
// Pre-setup this stages the mode for the player to come up with — never a no-op.
|
|
927
|
+
if let player { player.repeatMode = mode }
|
|
928
|
+
else { pendingRepeatMode = mode }
|
|
929
|
+
}
|
|
622
930
|
}
|
|
623
931
|
|
|
624
932
|
public func getShuffleEnabled() throws -> Bool {
|
|
@@ -630,11 +938,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
630
938
|
}
|
|
631
939
|
|
|
632
940
|
public func getPlaybackError() throws -> PlaybackError? {
|
|
633
|
-
onMainActor { player?.
|
|
941
|
+
onMainActor { player?.coordinator.nitroPlaybackError() }
|
|
634
942
|
}
|
|
635
943
|
|
|
636
944
|
public func retry() throws {
|
|
637
|
-
|
|
945
|
+
// Re-resolve rather than replay the cached URL: short-lived URLs/tokens
|
|
946
|
+
// may have expired since the failure (matches the internal retry path).
|
|
947
|
+
onMainActor { player?.reloadResolving(startFromCurrentTime: true) }
|
|
638
948
|
}
|
|
639
949
|
|
|
640
950
|
// MARK: - Sleep Timer
|
|
@@ -648,9 +958,9 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
648
958
|
}
|
|
649
959
|
}
|
|
650
960
|
|
|
651
|
-
public func setSleepTimer(seconds: Double) throws {
|
|
961
|
+
public func setSleepTimer(seconds: Double, fadeDuration: Double?) throws {
|
|
652
962
|
onMainActor {
|
|
653
|
-
player?.sleepTimerManager.set(seconds: seconds)
|
|
963
|
+
player?.sleepTimerManager.set(seconds: seconds, fadeDuration: fadeDuration)
|
|
654
964
|
}
|
|
655
965
|
}
|
|
656
966
|
|
|
@@ -730,13 +1040,13 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
730
1040
|
onMainActor {
|
|
731
1041
|
guard let track = player?.currentTrack, let src = track.src else { return }
|
|
732
1042
|
guard let index = player?.currentIndex, index >= 0 else { return }
|
|
733
|
-
|
|
734
|
-
//
|
|
1043
|
+
// Optimistically reflect in the authoritative match set so the now-playing
|
|
1044
|
+
// heart (hydrated from it in getActiveTrack) flips immediately; native
|
|
1045
|
+
// reconciles to its canonical ids on the next setFavorites.
|
|
1046
|
+
browserManager.setFavorited(src: src, favorited: favorited)
|
|
735
1047
|
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
1048
|
favoriteChangedEmitter.emit(FavoriteChangedEvent(track: updatedTrack, favorited: favorited))
|
|
739
|
-
// Fire active track changed so useActiveTrack()
|
|
1049
|
+
// Fire active track changed so the now-playing heart + useActiveTrack() refresh.
|
|
740
1050
|
let position = player?.currentTime ?? 0
|
|
741
1051
|
activeTrackChangedEmitter.emit(PlaybackActiveTrackChangedEvent(
|
|
742
1052
|
lastIndex: Double(index),
|
|
@@ -748,27 +1058,24 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
748
1058
|
}
|
|
749
1059
|
}
|
|
750
1060
|
|
|
1061
|
+
/// Whether the active (now-playing) track is favorited, per the authoritative
|
|
1062
|
+
/// favorite set (via `getActiveTrack`'s hydration).
|
|
1063
|
+
func isActiveTrackFavorited() -> Bool {
|
|
1064
|
+
(try? getActiveTrack())?.favorited ?? false
|
|
1065
|
+
}
|
|
1066
|
+
|
|
751
1067
|
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
|
-
}
|
|
1068
|
+
try? setActiveTrackFavorited(favorited: !isActiveTrackFavorited())
|
|
759
1069
|
}
|
|
760
1070
|
|
|
761
|
-
public func setQueue(tracks: [Track], startIndex: Double?,
|
|
762
|
-
|
|
1071
|
+
public func setQueue(tracks: [Track], startIndex: Double?, startPosition: Double?) throws {
|
|
1072
|
+
onMainActor {
|
|
763
1073
|
guard let player else { return }
|
|
764
|
-
player.
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
if let position = startPositionMs {
|
|
770
|
-
player.seekTo(position / 1000.0)
|
|
771
|
-
}
|
|
1074
|
+
player.setQueue(
|
|
1075
|
+
tracks,
|
|
1076
|
+
initialIndex: startIndex.map { Int($0) } ?? 0,
|
|
1077
|
+
startPositionMs: startPosition.map { $0 * 1000 },
|
|
1078
|
+
)
|
|
772
1079
|
}
|
|
773
1080
|
}
|
|
774
1081
|
|
|
@@ -793,7 +1100,11 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
793
1100
|
}
|
|
794
1101
|
|
|
795
1102
|
public func getActiveTrack() throws -> Track? {
|
|
796
|
-
|
|
1103
|
+
// Hydrate `favorited` from the authoritative favorite set — the now-playing
|
|
1104
|
+
// track is loaded outside the browse cache, so its own flag isn't otherwise
|
|
1105
|
+
// kept in sync as favorites change. This is the single source of truth for
|
|
1106
|
+
// "is the active track favorited" (now-playing heart, toggle direction, JS).
|
|
1107
|
+
onMainActor { player?.currentTrack.map(browserManager.hydrateFavorite) }
|
|
797
1108
|
}
|
|
798
1109
|
|
|
799
1110
|
// MARK: - Now Playing
|
|
@@ -805,21 +1116,57 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
805
1116
|
}
|
|
806
1117
|
}
|
|
807
1118
|
|
|
1119
|
+
/// Transient now-playing fields (e.g. feedback for a refused remote
|
|
1120
|
+
/// command). Outranks the formatter and the override while active, so live
|
|
1121
|
+
/// metadata can't stomp it mid-flash. Reverted by a NATIVE timer — JS
|
|
1122
|
+
/// timers pause with a backgrounded host on Android, and the lock screen
|
|
1123
|
+
/// is exactly the backgrounded case — and cleared early on track change.
|
|
1124
|
+
private var nowPlayingFlash: NowPlayingUpdate?
|
|
1125
|
+
private var nowPlayingFlashRevert: DispatchWorkItem?
|
|
1126
|
+
|
|
1127
|
+
/// Pending debounced audio-session release (see `playerShouldReleaseSession`).
|
|
1128
|
+
private var sessionReleaseWork: DispatchWorkItem?
|
|
1129
|
+
|
|
1130
|
+
public func flashNowPlaying(update: NowPlayingUpdate, durationMs: Double) throws {
|
|
1131
|
+
onMainActor {
|
|
1132
|
+
nowPlayingFlashRevert?.cancel()
|
|
1133
|
+
nowPlayingFlash = update
|
|
1134
|
+
let revert = DispatchWorkItem { [weak self] in
|
|
1135
|
+
guard let self else { return }
|
|
1136
|
+
self.nowPlayingFlash = nil
|
|
1137
|
+
self.nowPlayingFlashRevert = nil
|
|
1138
|
+
self.applyNowPlayingMetadata()
|
|
1139
|
+
}
|
|
1140
|
+
nowPlayingFlashRevert = revert
|
|
1141
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + durationMs / 1000, execute: revert)
|
|
1142
|
+
applyNowPlayingMetadata()
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
public func clearNowPlayingFlash() throws {
|
|
1147
|
+
onMainActor {
|
|
1148
|
+
guard nowPlayingFlash != nil else { return }
|
|
1149
|
+
cancelNowPlayingFlash()
|
|
1150
|
+
applyNowPlayingMetadata()
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
private func cancelNowPlayingFlash() {
|
|
1155
|
+
nowPlayingFlashRevert?.cancel()
|
|
1156
|
+
nowPlayingFlashRevert = nil
|
|
1157
|
+
nowPlayingFlash = nil
|
|
1158
|
+
}
|
|
1159
|
+
|
|
808
1160
|
public func getNowPlaying() throws -> NowPlayingMetadata? {
|
|
809
1161
|
onMainActor {
|
|
810
1162
|
guard let track = player?.currentTrack else { return nil }
|
|
1163
|
+
let flash = nowPlayingFlash
|
|
811
1164
|
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,
|
|
1165
|
+
return makeNowPlayingMetadata(
|
|
1166
|
+
track: track,
|
|
1167
|
+
title: flash?.title ?? override?.title ?? track.title,
|
|
1168
|
+
artist: flash?.artist ?? override?.artist ?? track.artist,
|
|
1169
|
+
album: flash?.album ?? override?.album ?? track.album,
|
|
823
1170
|
)
|
|
824
1171
|
}
|
|
825
1172
|
}
|
|
@@ -829,36 +1176,47 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
829
1176
|
/// Falls back to the raw artwork string only if it's not an SF Symbol (which JS can't load).
|
|
830
1177
|
private func nowPlayingArtwork(for track: Track) -> String? {
|
|
831
1178
|
if let uri = track.artworkSource?.uri { return uri }
|
|
832
|
-
guard let artwork = track.artwork else { return nil }
|
|
1179
|
+
guard let artwork = track.artwork?.url else { return nil }
|
|
833
1180
|
return SFSymbolRenderer.isSFSymbol(artwork) ? nil : artwork
|
|
834
1181
|
}
|
|
835
1182
|
|
|
836
|
-
///
|
|
1183
|
+
/// Builds the JS-facing now-playing metadata for the given rendered text lines.
|
|
837
1184
|
@MainActor
|
|
838
|
-
private func
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
let nowPlaying = NowPlayingMetadata(
|
|
1185
|
+
private func makeNowPlayingMetadata(
|
|
1186
|
+
track: Track, title: String, artist: String?, album: String?,
|
|
1187
|
+
) -> NowPlayingMetadata {
|
|
1188
|
+
NowPlayingMetadata(
|
|
843
1189
|
elapsedTime: player?.currentTime,
|
|
844
|
-
title:
|
|
845
|
-
album:
|
|
846
|
-
artist:
|
|
1190
|
+
title: title,
|
|
1191
|
+
album: album,
|
|
1192
|
+
artist: artist,
|
|
847
1193
|
duration: track.duration,
|
|
848
1194
|
artwork: nowPlayingArtwork(for: track),
|
|
849
1195
|
description: track.description,
|
|
850
1196
|
mediaId: track.src ?? track.url,
|
|
851
1197
|
genre: track.genre,
|
|
852
|
-
rating: nil,
|
|
853
1198
|
)
|
|
1199
|
+
}
|
|
854
1200
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
|
|
1201
|
+
/// Re-renders the now-playing surface via `NowPlayingUpdater`, handing it the current playback
|
|
1202
|
+
/// signals, override, and formatter. Called on every track / metadata / playback-state change.
|
|
1203
|
+
@MainActor
|
|
1204
|
+
private func applyNowPlayingMetadata() {
|
|
1205
|
+
guard let player, let track = player.currentTrack else { return }
|
|
1206
|
+
// Classify a stall by connectivity so the formatter can show "No internet connection" vs
|
|
1207
|
+
// "Reconnecting…" without a separate read. nil while not stalled.
|
|
1208
|
+
let stalled: StallReason? =
|
|
1209
|
+
player.isStalled ? (networkMonitor.getOnline() ? .buffering : .offline) : nil
|
|
1210
|
+
player.nowPlayingUpdater.render(
|
|
1211
|
+
track: track,
|
|
1212
|
+
timedMetadata: latestTimedMetadata,
|
|
1213
|
+
playWhenReady: player.playWhenReady,
|
|
1214
|
+
stalled: stalled,
|
|
1215
|
+
error: player.coordinator.nitroPlaybackError(),
|
|
1216
|
+
flash: nowPlayingFlash,
|
|
1217
|
+
override: nowPlayingMetadataEnabled ? nowPlayingOverride : nil,
|
|
1218
|
+
formatter: nowPlayingMetadataEnabled ? nowPlayingMetadataFormatter : nil,
|
|
1219
|
+
)
|
|
862
1220
|
}
|
|
863
1221
|
|
|
864
1222
|
// MARK: - Network
|
|
@@ -893,6 +1251,63 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
893
1251
|
|
|
894
1252
|
// MARK: - External Audio Output
|
|
895
1253
|
|
|
1254
|
+
/// Observes audio-session interruptions (phone calls, or another app such as
|
|
1255
|
+
/// Music/Spotify starting playback). iOS pauses our AVPlayer underneath us but
|
|
1256
|
+
/// doesn't update our playback state, so without this the UI stays stuck on
|
|
1257
|
+
/// "playing". On `.began` we reflect a real pause; on `.ended` we resume if we
|
|
1258
|
+
/// were playing and the system says we should.
|
|
1259
|
+
private func setupInterruptionObserver() {
|
|
1260
|
+
if let observer = interruptionObserver {
|
|
1261
|
+
NotificationCenter.default.removeObserver(observer)
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
interruptionObserver = NotificationCenter.default.addObserver(
|
|
1265
|
+
forName: AVAudioSession.interruptionNotification,
|
|
1266
|
+
object: nil,
|
|
1267
|
+
queue: .main,
|
|
1268
|
+
) { [weak self] notification in
|
|
1269
|
+
self?.handleAudioSessionInterruption(notification)
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
private func handleAudioSessionInterruption(_ notification: Notification) {
|
|
1274
|
+
guard
|
|
1275
|
+
let info = notification.userInfo,
|
|
1276
|
+
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
1277
|
+
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
|
1278
|
+
else { return }
|
|
1279
|
+
|
|
1280
|
+
switch type {
|
|
1281
|
+
case .began:
|
|
1282
|
+
// iOS 17+ fires a `.began` (with no matching `.ended`) when an output
|
|
1283
|
+
// route such as headphones disappears. Reflecting a pause here would
|
|
1284
|
+
// strand resume-intent state with nothing to clear it, and the
|
|
1285
|
+
// route-change observer already handles the output change — so ignore it.
|
|
1286
|
+
if #available(iOS 17.0, *),
|
|
1287
|
+
let reasonValue = info[AVAudioSessionInterruptionReasonKey] as? UInt,
|
|
1288
|
+
AVAudioSession.InterruptionReason(rawValue: reasonValue) == .routeDisconnected
|
|
1289
|
+
{
|
|
1290
|
+
return
|
|
1291
|
+
}
|
|
1292
|
+
onMainActor { player?.handleInterruptionBegan() }
|
|
1293
|
+
case .ended:
|
|
1294
|
+
let shouldResume = (info[AVAudioSessionInterruptionOptionKey] as? UInt)
|
|
1295
|
+
.map { AVAudioSession.InterruptionOptions(rawValue: $0).contains(.shouldResume) }
|
|
1296
|
+
?? false
|
|
1297
|
+
onMainActor {
|
|
1298
|
+
// Reactivate the possibly-deactivated session before resuming — but
|
|
1299
|
+
// only when a resume will happen: activating while staying paused
|
|
1300
|
+
// grabs a non-mixable session with nothing to release it.
|
|
1301
|
+
if shouldResume, player?.willResumeAfterInterruption == true {
|
|
1302
|
+
try? AVAudioSession.sharedInstance().setActive(true)
|
|
1303
|
+
}
|
|
1304
|
+
player?.handleInterruptionEnded(shouldResume: shouldResume)
|
|
1305
|
+
}
|
|
1306
|
+
@unknown default:
|
|
1307
|
+
break
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
896
1311
|
/// Sets up observer for audio route changes
|
|
897
1312
|
private func setupRouteChangeObserver() {
|
|
898
1313
|
if let observer = routeChangeObserver {
|
|
@@ -903,58 +1318,95 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
903
1318
|
forName: AVAudioSession.routeChangeNotification,
|
|
904
1319
|
object: nil,
|
|
905
1320
|
queue: .main,
|
|
1321
|
+
) { [weak self] notification in
|
|
1322
|
+
guard let self else { return }
|
|
1323
|
+
// The previous output disappeared (headphones unplugged, a Bluetooth
|
|
1324
|
+
// speaker powered off). Pause rather than abruptly resume out of the
|
|
1325
|
+
// built-in speaker — the platform convention every media app follows.
|
|
1326
|
+
// A deliberate pause (clears play intent), not an interruption: the user
|
|
1327
|
+
// restarts playback themselves once they want it.
|
|
1328
|
+
if let reasonValue = notification.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
|
1329
|
+
AVAudioSession.RouteChangeReason(rawValue: reasonValue) == .oldDeviceUnavailable
|
|
1330
|
+
{
|
|
1331
|
+
// Also cancels a pending interruption auto-resume: unplugging during
|
|
1332
|
+
// a call must not blast the built-in speaker when the call ends.
|
|
1333
|
+
self.onMainActor { self.player?.handleRouteDisconnected() }
|
|
1334
|
+
}
|
|
1335
|
+
if let output = self.getCurrentOutput() {
|
|
1336
|
+
self.onOutputChanged(output)
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/// Observes audio-server (`mediaserverd`) resets. When it fires, the session
|
|
1342
|
+
/// category is cleared and every AVPlayer/session handle is invalid — a
|
|
1343
|
+
/// long-running stream would otherwise go permanently silent until the app
|
|
1344
|
+
/// restarts.
|
|
1345
|
+
private func setupMediaServicesResetObserver() {
|
|
1346
|
+
if let observer = mediaServicesResetObserver {
|
|
1347
|
+
NotificationCenter.default.removeObserver(observer)
|
|
1348
|
+
}
|
|
1349
|
+
mediaServicesResetObserver = NotificationCenter.default.addObserver(
|
|
1350
|
+
forName: AVAudioSession.mediaServicesWereResetNotification,
|
|
1351
|
+
object: nil,
|
|
1352
|
+
queue: .main,
|
|
906
1353
|
) { [weak self] _ in
|
|
907
|
-
guard let self
|
|
908
|
-
|
|
1354
|
+
guard let self else { return }
|
|
1355
|
+
// Re-apply our category (the reset cleared it), then have the player
|
|
1356
|
+
// recreate itself and reconnect the current stream.
|
|
1357
|
+
self.applyAudioSessionCategory()
|
|
1358
|
+
self.onMainActor { self.player?.handleMediaServicesReset() }
|
|
909
1359
|
}
|
|
910
1360
|
}
|
|
911
1361
|
|
|
912
1362
|
/// Gets the current audio output info
|
|
913
|
-
private func getCurrentOutput() ->
|
|
1363
|
+
private func getCurrentOutput() -> Output? {
|
|
914
1364
|
let session = AVAudioSession.sharedInstance()
|
|
915
1365
|
guard let output = session.currentRoute.outputs.first else { return nil }
|
|
916
1366
|
|
|
917
|
-
let (outputType, external): (
|
|
1367
|
+
let (outputType, external): (OutputType, Bool) = switch output.portType {
|
|
918
1368
|
case .builtInSpeaker:
|
|
919
|
-
(.
|
|
1369
|
+
(.speaker, false)
|
|
920
1370
|
case .builtInReceiver:
|
|
921
|
-
(.
|
|
1371
|
+
(.receiver, false)
|
|
922
1372
|
case .airPlay:
|
|
923
1373
|
(.airplay, true)
|
|
924
|
-
case .bluetoothA2DP:
|
|
925
|
-
(.
|
|
926
|
-
case .bluetoothHFP:
|
|
927
|
-
(.bluetoothHfp, true)
|
|
928
|
-
case .bluetoothLE:
|
|
929
|
-
(.bluetoothLe, true)
|
|
1374
|
+
case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
|
|
1375
|
+
(.bluetooth, true)
|
|
930
1376
|
case .headphones:
|
|
931
1377
|
(.headphones, true)
|
|
932
1378
|
case .carAudio:
|
|
933
|
-
(.
|
|
1379
|
+
(.car, true)
|
|
934
1380
|
case .HDMI:
|
|
935
1381
|
(.hdmi, true)
|
|
936
1382
|
case .usbAudio:
|
|
937
|
-
(.
|
|
1383
|
+
(.usb, true)
|
|
938
1384
|
default:
|
|
939
1385
|
(.other, true)
|
|
940
1386
|
}
|
|
941
1387
|
|
|
942
|
-
return
|
|
1388
|
+
return Output(type: outputType, name: output.portName, external: external)
|
|
943
1389
|
}
|
|
944
1390
|
|
|
945
|
-
public func
|
|
1391
|
+
public func getOutput() throws -> Output? {
|
|
946
1392
|
getCurrentOutput()
|
|
947
1393
|
}
|
|
948
1394
|
|
|
949
|
-
public func
|
|
1395
|
+
public func supportsOutputSwitcher() throws -> Bool {
|
|
1396
|
+
// iOS can always present the system route picker (AVRoutePickerView).
|
|
1397
|
+
return true
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
public func openOutputPicker() throws {
|
|
950
1401
|
DispatchQueue.main.async { [weak self] in
|
|
1402
|
+
guard let self else { return }
|
|
951
1403
|
guard let windowScene = UIApplication.shared.connectedScenes
|
|
952
1404
|
.compactMap({ $0 as? UIWindowScene })
|
|
953
1405
|
.first(where: { $0.activationState == .foregroundActive }),
|
|
954
1406
|
let window = windowScene.windows.first(where: { $0.isKeyWindow }),
|
|
955
1407
|
var topController = window.rootViewController
|
|
956
1408
|
else {
|
|
957
|
-
self
|
|
1409
|
+
self.logger.error("openOutputPicker: no active window scene / root view controller")
|
|
958
1410
|
return
|
|
959
1411
|
}
|
|
960
1412
|
|
|
@@ -963,25 +1415,52 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
963
1415
|
topController = presented
|
|
964
1416
|
}
|
|
965
1417
|
|
|
966
|
-
|
|
967
|
-
|
|
1418
|
+
// AVRoutePickerView has no public API to present its picker
|
|
1419
|
+
// programmatically, so we add it to the hierarchy and tap its internal
|
|
1420
|
+
// button. Two requirements that are easy to get wrong:
|
|
1421
|
+
// 1. The view must be *visible* to the system (a `isHidden` source view
|
|
1422
|
+
// suppresses the presentation), so we keep it transparent instead.
|
|
1423
|
+
// 2. The internal button can be nested below the top-level subviews on
|
|
1424
|
+
// modern iOS, so we search recursively rather than only direct children.
|
|
1425
|
+
let routePicker = AVRoutePickerView(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
|
|
1426
|
+
routePicker.alpha = 0.01
|
|
968
1427
|
topController.view.addSubview(routePicker)
|
|
969
1428
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1429
|
+
let button = Self.firstButton(in: routePicker)
|
|
1430
|
+
self.logger.notice(
|
|
1431
|
+
"openOutputPicker: hierarchy=\(Self.describeHierarchy(routePicker), privacy: .public), buttonFound=\(button != nil, privacy: .public)",
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1434
|
+
if let button {
|
|
1435
|
+
button.sendActions(for: .touchUpInside)
|
|
1436
|
+
} else {
|
|
1437
|
+
self.logger.error("openOutputPicker: no UIButton found inside AVRoutePickerView")
|
|
976
1438
|
}
|
|
977
1439
|
|
|
978
|
-
//
|
|
979
|
-
|
|
1440
|
+
// Keep the source view in the hierarchy long enough for the picker to
|
|
1441
|
+
// present (on iPad it anchors a popover to this view).
|
|
1442
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
|
980
1443
|
routePicker.removeFromSuperview()
|
|
981
1444
|
}
|
|
982
1445
|
}
|
|
983
1446
|
}
|
|
984
1447
|
|
|
1448
|
+
/// Depth-first search for the first `UIButton` inside a view tree.
|
|
1449
|
+
private static func firstButton(in view: UIView) -> UIButton? {
|
|
1450
|
+
if let button = view as? UIButton { return button }
|
|
1451
|
+
for subview in view.subviews {
|
|
1452
|
+
if let button = firstButton(in: subview) { return button }
|
|
1453
|
+
}
|
|
1454
|
+
return nil
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/// Compact description of a view tree, e.g. `AVRoutePickerView[UIButton[UIImageView]]`.
|
|
1458
|
+
private static func describeHierarchy(_ view: UIView) -> String {
|
|
1459
|
+
let name = String(describing: type(of: view))
|
|
1460
|
+
let children = view.subviews.map(describeHierarchy).joined(separator: ", ")
|
|
1461
|
+
return children.isEmpty ? name : "\(name)[\(children)]"
|
|
1462
|
+
}
|
|
1463
|
+
|
|
985
1464
|
// MARK: - Equalizer (unsupported on iOS)
|
|
986
1465
|
|
|
987
1466
|
public func getEqualizerSettings() throws -> EqualizerSettings? {
|
|
@@ -1026,12 +1505,28 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
1026
1505
|
extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
1027
1506
|
public func playerDidChangePlayback(_ playback: Playback) {
|
|
1028
1507
|
onPlaybackChanged(playback)
|
|
1508
|
+
// Re-render so the formatter reflects the new state — notably the error line on a failure (the
|
|
1509
|
+
// coordinator sets playbackError before this fires). Redundant updates are deduped on publish.
|
|
1510
|
+
applyNowPlayingMetadata()
|
|
1029
1511
|
}
|
|
1030
1512
|
|
|
1031
1513
|
public func playerDidChangeActiveTrack(_ event: PlaybackActiveTrackChangedEvent) {
|
|
1032
|
-
// Clear now playing override when track changes
|
|
1514
|
+
// Clear now playing override + flash + live metadata when track changes
|
|
1515
|
+
// (matches Kotlin behavior; a flash must not carry over to a new track)
|
|
1033
1516
|
nowPlayingOverride = nil
|
|
1034
|
-
|
|
1517
|
+
cancelNowPlayingFlash()
|
|
1518
|
+
latestTimedMetadata = nil
|
|
1519
|
+
// Hydrate the active track's `favorited` from the authoritative set so JS
|
|
1520
|
+
// consumers (useActiveTrack) match getActiveTrack()'s hydrated value — the
|
|
1521
|
+
// coordinator emits the raw queue track, whose flag isn't kept in sync.
|
|
1522
|
+
let hydrated = PlaybackActiveTrackChangedEvent(
|
|
1523
|
+
lastIndex: event.lastIndex,
|
|
1524
|
+
lastTrack: event.lastTrack,
|
|
1525
|
+
lastPosition: event.lastPosition,
|
|
1526
|
+
index: event.index,
|
|
1527
|
+
track: event.track.map(browserManager.hydrateFavorite),
|
|
1528
|
+
)
|
|
1529
|
+
activeTrackChangedEmitter.emit(hydrated)
|
|
1035
1530
|
// Also notify now playing changed when track changes
|
|
1036
1531
|
applyNowPlayingMetadata()
|
|
1037
1532
|
}
|
|
@@ -1040,12 +1535,60 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1040
1535
|
onPlaybackProgressUpdated(event)
|
|
1041
1536
|
}
|
|
1042
1537
|
|
|
1538
|
+
public func playerDidFirePlaybackInterval() {
|
|
1539
|
+
onPlaybackInterval()
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1043
1542
|
public func playerDidChangePlayWhenReady(_ playWhenReady: Bool) {
|
|
1543
|
+
// Intent to play again is the earliest cancel signal for a pending session release.
|
|
1544
|
+
if playWhenReady { cancelSessionRelease() }
|
|
1044
1545
|
onPlaybackPlayWhenReadyChanged(PlaybackPlayWhenReadyChangedEvent(playWhenReady: playWhenReady))
|
|
1045
1546
|
}
|
|
1046
1547
|
|
|
1047
1548
|
public func playerDidChangePlayingState(_ state: PlayingState) {
|
|
1048
1549
|
onPlaybackPlayingState(state)
|
|
1550
|
+
// Lazily activate the audio session the moment playback actually produces — or is buffering to
|
|
1551
|
+
// produce — output. This (not setup) is where we grab the session, so launching the app never
|
|
1552
|
+
// interrupts other apps' audio; the interruption only happens when the user truly starts
|
|
1553
|
+
// playback, which is correct. Keyed off play *state*, not `playWhenReady`, because the intent
|
|
1554
|
+
// can be set at setup (before any track) while output only begins here. Idempotent:
|
|
1555
|
+
// `setActive(true)` is a no-op when the session is already active.
|
|
1556
|
+
if state.playing || state.buffering {
|
|
1557
|
+
try? AVAudioSession.sharedInstance().setActive(true)
|
|
1558
|
+
// The now-playing session's activation request at link time predates this
|
|
1559
|
+
// (the audio session was still inactive, and the system may have declined);
|
|
1560
|
+
// re-request now so the surface can appear even if playback never produces
|
|
1561
|
+
// audio (first-ever load failing on a dead stream).
|
|
1562
|
+
player?.nowPlayingInfoController.reactivateSessionIfNeeded()
|
|
1563
|
+
// Output resumed — cancel any pending release armed by an earlier stop.
|
|
1564
|
+
cancelSessionRelease()
|
|
1565
|
+
}
|
|
1566
|
+
// Re-render so the formatter reacts to a stall starting or recovering (`stalled`) — a buffering
|
|
1567
|
+
// flag change that may not transition the coordinator state.
|
|
1568
|
+
applyNowPlayingMetadata()
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
public func playerShouldReleaseSession() {
|
|
1572
|
+
// Release the audio session so other apps' audio (Spotify, a podcast) can resume — debounced,
|
|
1573
|
+
// because iOS dislikes deactivating while our audio is still trailing off, and a quick
|
|
1574
|
+
// pause→resume shouldn't thrash the session. The coordinator only requests this on a genuine
|
|
1575
|
+
// stop (not an interruption, not mid-retry); a re-activation cancels it (see
|
|
1576
|
+
// `playerDidChangePlayWhenReady` and the activation above). Mirrors the now-playing flash timer.
|
|
1577
|
+
sessionReleaseWork?.cancel()
|
|
1578
|
+
let release = DispatchWorkItem { [weak self] in
|
|
1579
|
+
guard let self, self.player?.playWhenReady == false else { return }
|
|
1580
|
+
self.sessionReleaseWork = nil
|
|
1581
|
+
// `.notifyOthersOnDeactivation` signals other apps they may resume; without it the session
|
|
1582
|
+
// is released but nobody is told, so the user's audio would not come back.
|
|
1583
|
+
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
1584
|
+
}
|
|
1585
|
+
sessionReleaseWork = release
|
|
1586
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: release)
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
private func cancelSessionRelease() {
|
|
1590
|
+
sessionReleaseWork?.cancel()
|
|
1591
|
+
sessionReleaseWork = nil
|
|
1049
1592
|
}
|
|
1050
1593
|
|
|
1051
1594
|
public func playerDidEndQueue(_ event: PlaybackQueueEndedEvent) {
|
|
@@ -1061,6 +1604,7 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1061
1604
|
}
|
|
1062
1605
|
|
|
1063
1606
|
public func playerDidChangeShuffleEnabled(_ enabled: Bool) {
|
|
1607
|
+
shuffleChangedEmitter.emit(enabled)
|
|
1064
1608
|
onPlaybackShuffleModeChanged(enabled)
|
|
1065
1609
|
}
|
|
1066
1610
|
|
|
@@ -1076,7 +1620,10 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1076
1620
|
public func playerDidReceiveTimedMetadata(_ groups: [AVTimedMetadataGroup]) {
|
|
1077
1621
|
for group in groups {
|
|
1078
1622
|
if let metadata = TimedMetadata.from(items: group.items) {
|
|
1623
|
+
latestTimedMetadata = metadata
|
|
1079
1624
|
onTimedMetadata(metadata)
|
|
1625
|
+
// Re-render the now-playing so the formatter can reflect the live song.
|
|
1626
|
+
applyNowPlayingMetadata()
|
|
1080
1627
|
}
|
|
1081
1628
|
}
|
|
1082
1629
|
}
|
|
@@ -1193,10 +1740,6 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1193
1740
|
remoteSeek(position: position)
|
|
1194
1741
|
}
|
|
1195
1742
|
|
|
1196
|
-
public func remoteSetRating(rating _: Any) {
|
|
1197
|
-
// TODO: Convert rating to RemoteSetRatingEvent
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
1743
|
public func remotePlayId(id: String, index: Int?) {
|
|
1201
1744
|
let event = RemotePlayIdEvent(id: id, index: index.map { Double($0) })
|
|
1202
1745
|
if let handler = handleRemotePlayId {
|
|
@@ -1215,57 +1758,91 @@ extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
|
1215
1758
|
}
|
|
1216
1759
|
}
|
|
1217
1760
|
|
|
1218
|
-
///
|
|
1219
|
-
///
|
|
1220
|
-
///
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1761
|
+
/// Static so the Siri intent handler can call it before the shared instance
|
|
1762
|
+
/// exists: it waits on the `playerAndConfiguredBrowser` gate, which resolves
|
|
1763
|
+
/// once RN boots. Uses the resolved `browser`, never `self`.
|
|
1764
|
+
static func handlePlayMediaIntent(criteria: MediaIntentCriteria, completion: @escaping @Sendable (Bool) -> Void) {
|
|
1765
|
+
Task { @MainActor in
|
|
1766
|
+
guard let (browser, player) = await playerAndConfiguredBrowser.wait(timeout: .seconds(8)) else {
|
|
1767
|
+
Logger(subsystem: "com.audiobrowser", category: "AudioBrowser")
|
|
1768
|
+
.error("handlePlayMediaIntent: browser/player not ready within budget")
|
|
1769
|
+
completion(false)
|
|
1770
|
+
return
|
|
1771
|
+
}
|
|
1772
|
+
// No-criteria intent ("play «app»") → resume. The gate must NOT block
|
|
1773
|
+
// this: the gate blocks *finding* content, never *hearing* the active or
|
|
1774
|
+
// persisted track. The gate is checked below, for the search branch only.
|
|
1775
|
+
if criteria.isResume {
|
|
1776
|
+
if player.currentTrack != nil {
|
|
1777
|
+
browser.logger.info("resume: warm — playing current track")
|
|
1778
|
+
player.play()
|
|
1779
|
+
browser.showNowPlayingRequestedEmitter.emit(())
|
|
1780
|
+
completion(true)
|
|
1781
|
+
} else if let state = browser.playbackStateStore.load() {
|
|
1782
|
+
browser.logger.info("resume: cold — restoring persisted track")
|
|
1783
|
+
let track = state.track.toNitro()
|
|
1784
|
+
let startMs = (track.live == true) ? nil : state.positionMs
|
|
1785
|
+
// Match Android resume: re-expand the full queue from the track's contextual
|
|
1786
|
+
// URL (parent container → siblings + selected index). Fall back to the single
|
|
1787
|
+
// track when the url isn't contextual or expansion fails.
|
|
1788
|
+
if let url = state.track.url,
|
|
1789
|
+
let expanded = try? await browser.browserManager.expandQueueFromContextualUrl(url) {
|
|
1790
|
+
player.setQueue(expanded.tracks, initialIndex: expanded.selectedIndex, startPositionMs: startMs, playWhenReady: true)
|
|
1791
|
+
} else {
|
|
1792
|
+
player.setQueue([track], initialIndex: 0, startPositionMs: startMs, playWhenReady: true)
|
|
1793
|
+
}
|
|
1794
|
+
browser.showNowPlayingRequestedEmitter.emit(())
|
|
1795
|
+
completion(true)
|
|
1796
|
+
} else {
|
|
1797
|
+
browser.logger.error("resume: nothing playing and nothing persisted → no-op")
|
|
1798
|
+
completion(false) // nothing playing and nothing persisted
|
|
1799
|
+
}
|
|
1800
|
+
return
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// Assemble the Nitro SearchParams here (MainActor) from the criteria's
|
|
1804
|
+
// Sendable fields, so the structured mode/genre/… reach the request like
|
|
1805
|
+
// Android. Today the API still text-searches `q`; the rest is forward-compat.
|
|
1806
|
+
let params = SearchParams(
|
|
1807
|
+
mode: criteria.searchMode.flatMap { SearchMode(fromString: $0) },
|
|
1808
|
+
query: criteria.query,
|
|
1809
|
+
genre: criteria.genre,
|
|
1810
|
+
artist: criteria.artist,
|
|
1811
|
+
album: criteria.album,
|
|
1812
|
+
title: criteria.title,
|
|
1813
|
+
playlist: criteria.playlist,
|
|
1814
|
+
// .currentlyPlaying can't reach here — isResume routed it to resume.
|
|
1815
|
+
reference: criteria.reference == .my ? .my : .unknown
|
|
1816
|
+
)
|
|
1817
|
+
|
|
1818
|
+
// Search is *finding* new content — refused when the gate decides this
|
|
1819
|
+
// request is gated (resume above is unaffected: the gate never blocks
|
|
1820
|
+
// hearing, and the gate check stays after the resume check).
|
|
1821
|
+
let outcome = await browser.gateDecision(
|
|
1822
|
+
for: NativeGateRequest(reason: .search, path: nil, search: params)
|
|
1823
|
+
)
|
|
1824
|
+
guard !outcome.gated else {
|
|
1825
|
+
browser.logger.info("handlePlayMediaIntent: search refused — gated")
|
|
1826
|
+
browser.onGate(GateEvent(reason: .search))
|
|
1827
|
+
completion(false)
|
|
1828
|
+
return
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1228
1831
|
do {
|
|
1229
|
-
let
|
|
1230
|
-
guard let tracks = resolved.children, !tracks.isEmpty else {
|
|
1832
|
+
guard let tracks = try await browser.browserManager.searchPlayable(params) else {
|
|
1231
1833
|
completion(false)
|
|
1232
1834
|
return
|
|
1233
1835
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1836
|
+
player.setQueue(tracks, initialIndex: 0, playWhenReady: true)
|
|
1837
|
+
browser.showNowPlayingRequestedEmitter.emit(())
|
|
1237
1838
|
completion(true)
|
|
1238
1839
|
} catch {
|
|
1239
|
-
|
|
1840
|
+
browser.logger.error("handlePlayMediaIntent failed: \(error.localizedDescription)")
|
|
1240
1841
|
completion(false)
|
|
1241
1842
|
}
|
|
1242
1843
|
}
|
|
1243
1844
|
}
|
|
1244
1845
|
|
|
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
1846
|
public func remoteChangeRepeatMode(mode: RepeatMode) {
|
|
1270
1847
|
// Apply the repeat mode change from CarPlay/lock screen
|
|
1271
1848
|
try? setRepeatMode(mode: mode)
|