react-native-audio-browser 0.2.0-next.3 → 0.2.0-next.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -22
- package/android/build.gradle +35 -12
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +541 -278
- package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
- package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
- package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +52 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +27 -9
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +288 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +97 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +70 -29
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +20 -7
- package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
- package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
- package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
- package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
- package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +12 -12
- package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
- package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
- package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +509 -230
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -25
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +113 -78
- package/android/src/main/java/com/audiobrowser/player/Player.kt +81 -38
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
- package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
- package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +62 -40
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +110 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +56 -52
- package/android/src/main/res/values/strings.xml +25 -5
- package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +100 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +119 -33
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +15 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +346 -8
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +40 -2
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +301 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +168 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +160 -0
- package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
- package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
- package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
- package/android/src/test/java/com/audiobrowser/player/BrowseTilesTest.kt +385 -0
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +101 -58
- package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
- package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
- package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
- package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +55 -16
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +162 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
- package/ios/AVPlayerEngine.swift +330 -0
- package/ios/AVPlayerEngineCallbacks.swift +15 -0
- package/ios/Browser/BrowseGate.swift +58 -0
- package/ios/Browser/BrowserConfig.swift +117 -78
- package/ios/Browser/BrowserManager.swift +209 -603
- package/ios/Browser/BrowserPathHelper.swift +50 -46
- package/ios/Browser/InertStyleDiagnostic.swift +296 -0
- package/ios/Browser/JsonModels.swift +264 -106
- package/ios/Browser/NavigationErrorState.swift +41 -0
- package/ios/Browser/PageResolver.swift +227 -0
- package/ios/Browser/RequestLayer.swift +90 -0
- package/ios/Browser/{BrowserManager+URLResolution.swift → RequestPipeline.swift} +193 -121
- package/ios/Browser/SearchDrillIn.swift +7 -7
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +58 -0
- package/ios/Browser/TabBarEntries.swift +9 -3
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CPInterfaceController+Safe.swift +100 -43
- package/ios/CarPlay/CarPlayArtworkResolver.swift +36 -83
- package/ios/CarPlay/CarPlayController.swift +42 -1296
- package/ios/CarPlay/CarPlayImageLoader.swift +78 -97
- package/ios/CarPlay/CarPlayListItemFactory.swift +303 -92
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +129 -136
- package/ios/CarPlay/CarPlaySession+Navigator.swift +114 -0
- package/ios/CarPlay/CarPlaySession+PageLoader.swift +92 -0
- package/ios/CarPlay/CarPlaySession+Refresher.swift +43 -0
- package/ios/CarPlay/CarPlaySession+TabBar.swift +193 -0
- package/ios/CarPlay/CarPlaySession.swift +209 -0
- package/ios/CarPlay/CarPlayTemplates.swift +63 -0
- package/ios/CarPlay/HybridAudioBrowser+IntentFavorite.swift +20 -0
- package/ios/{Player → CarPlay}/MediaIntentCriteria.swift +17 -9
- package/ios/CarPlay/MediaIntentPlayback.swift +74 -0
- package/ios/CarPlay/RNABMediaAddHandler.swift +8 -24
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +7 -23
- package/ios/CarPlay/RNABMediaIntentHandler.swift +18 -19
- package/ios/CarPlay/SectionPresentation.swift +77 -0
- package/ios/CarPlay/TopTemplateReplacer.swift +96 -0
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/NavigationError+Conversion.swift +14 -4
- package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
- package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
- package/ios/Extension/Section+Copying.swift +29 -0
- package/ios/Extension/String+IcyCharset.swift +21 -0
- package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
- package/ios/Extension/Track+Copying.swift +8 -12
- package/ios/Extension/Track+Identity.swift +18 -0
- package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Extension/UIImage+Adaptive.swift +12 -0
- package/ios/Extension/URLError+Connectivity.swift +15 -0
- package/ios/Http/HttpClient.swift +66 -142
- package/ios/HybridAudioBrowser+Browser.swift +153 -0
- package/ios/HybridAudioBrowser+Options.swift +144 -0
- package/ios/HybridAudioBrowser+Player.swift +397 -0
- package/ios/HybridAudioBrowser+PlayerCallbacks.swift +190 -0
- package/ios/HybridAudioBrowser+Stubs.swift +46 -0
- package/ios/HybridAudioBrowser.swift +153 -1610
- package/ios/Model/NitroTypeStubs.swift +146 -24
- package/ios/Model/PlayerUpdateOptions.swift +8 -1
- package/ios/Model/RemoteCommand.swift +18 -0
- package/ios/Model/TrackPlayerError.swift +2 -2
- package/ios/NowPlayingInfo/NowPlayingInfoController.swift +18 -53
- package/ios/Observer/ItemObserver.swift +90 -0
- package/ios/Observer/PlayerObserver.swift +59 -0
- package/ios/Option/SessionCategories.swift +42 -92
- package/ios/Player/AudioSessionController.swift +177 -0
- package/ios/Player/DeferredSeek.swift +70 -0
- package/ios/Player/ErrorStatus.swift +27 -0
- package/ios/Player/MediaLoader.swift +62 -35
- package/ios/Player/NowPlayingUpdater.swift +197 -50
- package/ios/Player/PlaybackFailure.swift +125 -0
- package/ios/Player/PlaybackPersistence.swift +73 -0
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/Player.swift +1078 -0
- package/ios/Player/{PlaybackCoordinatorCallbacks.swift → PlayerCallbacks.swift} +3 -4
- package/ios/Player/PlayerEngine.swift +87 -0
- package/ios/Player/PlayerStatusTypes.swift +2 -6
- package/ios/Player/QueueManager.swift +93 -67
- package/ios/Player/RetryLoop.swift +261 -0
- package/ios/Player/ShuffleOrder.swift +33 -0
- package/ios/Player/SleepTimerManager.swift +1 -1
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +46 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +87 -331
- package/ios/Tests/BrowserPathHelperTests.swift +56 -15
- package/ios/Tests/CarPlayArtworkResolverTests.swift +60 -373
- package/ios/Tests/DeferredSeekTests.swift +70 -0
- package/ios/Tests/EmitterTests.swift +29 -0
- package/ios/Tests/GenerationMemoTests.swift +121 -0
- package/ios/Tests/InertStyleDiagnosticTests.swift +209 -0
- package/ios/Tests/JsonModelsTests.swift +164 -1
- package/ios/Tests/LRUCacheTests.swift +110 -0
- package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
- package/ios/Tests/MediaLoaderTests.swift +12 -48
- package/ios/Tests/ObserverTests.swift +88 -0
- package/ios/Tests/PlaybackErrorTests.swift +76 -0
- package/ios/Tests/PlaybackFailureTests.swift +189 -0
- package/ios/Tests/PlaybackPersistenceTests.swift +93 -0
- package/ios/Tests/PlayerPolicyTests.swift +420 -0
- package/ios/Tests/{PlaybackCoordinatorTests.swift → PlayerTests.swift} +300 -248
- package/ios/Tests/PlayingStateTests.swift +42 -0
- package/ios/Tests/QueueManagerTests.swift +196 -115
- package/ios/Tests/ReadyValueTests.swift +73 -0
- package/ios/Tests/RemoteCommandControllerTests.swift +152 -0
- package/ios/Tests/RemoteCommandTests.swift +65 -0
- package/ios/Tests/RequestLayerTests.swift +112 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
- package/ios/Tests/RetryLoopTests.swift +270 -0
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +98 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +132 -0
- package/ios/Tests/Support/TestTypes.swift +108 -104
- package/ios/Tests/TabBarEntriesTests.swift +19 -19
- package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
- package/ios/Tests/TopTemplateReplacerTests.swift +172 -0
- package/ios/Tests/TrackIdTemplateTests.swift +67 -0
- package/ios/Tests/TrackIdentityTests.swift +20 -0
- package/ios/Tests/TrackSelectorTests.swift +168 -33
- package/ios/Tests/URLErrorConnectivityTests.swift +20 -0
- package/ios/TrackSelector.swift +46 -31
- package/ios/Util/ArtworkImageFetcher.swift +58 -1
- package/ios/Util/Emitter.swift +42 -0
- package/ios/Util/GenerationMemo.swift +36 -0
- package/ios/Util/LRUCache.swift +23 -159
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/ReadyValue.swift +56 -0
- package/ios/Util/SFSymbolRenderer.swift +59 -31
- package/ios/Util/SVGProcessor.swift +9 -12
- package/lib/commonjs/AudioBrowser.js +8 -8
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/CLAUDE.md +52 -0
- package/lib/commonjs/features/browser-config.js +12 -8
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/browser.js +11 -3
- package/lib/commonjs/features/browser.js.map +1 -1
- package/lib/commonjs/features/carConnection.js +1 -1
- package/lib/commonjs/features/equalizer.js +1 -1
- package/lib/commonjs/features/errors.js +3 -3
- package/lib/commonjs/features/favorites.js +14 -11
- package/lib/commonjs/features/favorites.js.map +1 -1
- package/lib/commonjs/features/gate.js +16 -1
- package/lib/commonjs/features/gate.js.map +1 -1
- package/lib/commonjs/features/metadata.js +20 -3
- package/lib/commonjs/features/metadata.js.map +1 -1
- package/lib/commonjs/features/network.js +1 -1
- package/lib/commonjs/features/nowPlaying.js +1 -1
- package/lib/commonjs/features/output.js +1 -1
- package/lib/commonjs/features/playback/controls.js +4 -2
- package/lib/commonjs/features/playback/controls.js.map +1 -1
- package/lib/commonjs/features/playback/playWhenReady.js +1 -1
- package/lib/commonjs/features/playback/playing.js +1 -1
- package/lib/commonjs/features/playback/progress.js +3 -2
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/rate.js +8 -1
- package/lib/commonjs/features/playback/rate.js.map +1 -1
- package/lib/commonjs/features/playback/state.js +3 -4
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
- package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/commonjs/features/playback/volume.js +1 -1
- package/lib/commonjs/features/player/options.js +48 -34
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +18 -9
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +23 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -0
- package/lib/commonjs/features/queue/activeTrack.js +15 -4
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +13 -6
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/features/queue/repeatMode.js +1 -1
- package/lib/commonjs/features/queue/shuffle.js +1 -1
- package/lib/commonjs/features/remoteControls.js +13 -92
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/features/sleepTimer.js +13 -1
- package/lib/commonjs/features/sleepTimer.js.map +1 -1
- package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
- package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
- package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
- package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/commonjs/utils/getTrackIdentity.js +24 -0
- package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
- package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/commonjs/web/NativeAudioBrowser.js +201 -76
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
- package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
- package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
- package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/PlayingStateFactory.js +1 -1
- package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
- package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +175 -100
- package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
- package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
- package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
- package/lib/commonjs/web/browser/NavigationErrorManager.js +3 -1
- package/lib/commonjs/web/browser/NavigationErrorManager.js.map +1 -1
- package/lib/commonjs/web/browser/SearchManager.js +18 -23
- package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
- package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/commonjs/web/browser/sections.js +89 -0
- package/lib/commonjs/web/browser/sections.js.map +1 -0
- package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
- package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
- package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
- package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/util/BrowserPathHelper.js +62 -11
- package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/module/AudioBrowser.js +2 -1
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/CLAUDE.md +52 -0
- package/lib/module/features/browser-config.js +12 -8
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/browser.js +11 -3
- package/lib/module/features/browser.js.map +1 -1
- package/lib/module/features/carConnection.js +1 -1
- package/lib/module/features/equalizer.js +1 -1
- package/lib/module/features/errors.js +3 -3
- package/lib/module/features/favorites.js +14 -11
- package/lib/module/features/favorites.js.map +1 -1
- package/lib/module/features/gate.js +16 -1
- package/lib/module/features/gate.js.map +1 -1
- package/lib/module/features/metadata.js +20 -3
- package/lib/module/features/metadata.js.map +1 -1
- package/lib/module/features/network.js +1 -1
- package/lib/module/features/nowPlaying.js +1 -1
- package/lib/module/features/output.js +1 -1
- package/lib/module/features/playback/controls.js +4 -2
- package/lib/module/features/playback/controls.js.map +1 -1
- package/lib/module/features/playback/playWhenReady.js +1 -1
- package/lib/module/features/playback/playing.js +1 -1
- package/lib/module/features/playback/progress.js +3 -2
- package/lib/module/features/playback/progress.js.map +1 -1
- package/lib/module/features/playback/rate.js +8 -1
- package/lib/module/features/playback/rate.js.map +1 -1
- package/lib/module/features/playback/state.js +3 -4
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/playback/trackPlaybackTime.js +0 -5
- package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/module/features/playback/volume.js +1 -1
- package/lib/module/features/player/options.js +47 -32
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +17 -8
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +19 -0
- package/lib/module/features/player/validateOptions.js.map +1 -0
- package/lib/module/features/queue/activeTrack.js +15 -4
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +13 -6
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/features/queue/repeatMode.js +1 -1
- package/lib/module/features/queue/shuffle.js +1 -1
- package/lib/module/features/remoteControls.js +13 -92
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/features/sleepTimer.js +13 -1
- package/lib/module/features/sleepTimer.js.map +1 -1
- package/lib/module/test-utils/reactNativeStub.js +58 -0
- package/lib/module/test-utils/reactNativeStub.js.map +1 -0
- package/lib/module/utils/LazyNativeEmitter.js +28 -14
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/utils/NativeUpdatedValue.js +31 -13
- package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
- package/lib/module/utils/getTrackIdentity.js +20 -0
- package/lib/module/utils/getTrackIdentity.js.map +1 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
- package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
- package/lib/module/web/NativeAudioBrowser.js +201 -76
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
- package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
- package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
- package/lib/module/web/TrackPlayer/Player.js +254 -39
- package/lib/module/web/TrackPlayer/Player.js.map +1 -1
- package/lib/module/web/TrackPlayer/PlayingStateFactory.js +1 -1
- package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
- package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +175 -100
- package/lib/module/web/browser/BrowserManager.js.map +1 -1
- package/lib/module/web/browser/FavoriteManager.js +31 -21
- package/lib/module/web/browser/FavoriteManager.js.map +1 -1
- package/lib/module/web/browser/NavigationErrorManager.js +3 -1
- package/lib/module/web/browser/NavigationErrorManager.js.map +1 -1
- package/lib/module/web/browser/SearchManager.js +18 -23
- package/lib/module/web/browser/SearchManager.js.map +1 -1
- package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
- package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
- package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
- package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
- package/lib/module/web/browser/sections.js +83 -0
- package/lib/module/web/browser/sections.js.map +1 -0
- package/lib/module/web/http/RequestConfigBuilder.js +110 -27
- package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
- package/lib/module/web/http/followMediaRedirect.js +215 -0
- package/lib/module/web/http/followMediaRedirect.js.map +1 -0
- package/lib/module/web/player/NowPlayingManager.js +4 -3
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/util/BrowserPathHelper.js +62 -11
- package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AudioBrowser.d.ts +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/browser.d.ts +10 -2
- package/lib/typescript/src/features/browser.d.ts.map +1 -1
- package/lib/typescript/src/features/carConnection.d.ts +1 -1
- package/lib/typescript/src/features/equalizer.d.ts +1 -1
- package/lib/typescript/src/features/errors.d.ts +3 -3
- package/lib/typescript/src/features/favorites.d.ts +14 -11
- package/lib/typescript/src/features/favorites.d.ts.map +1 -1
- package/lib/typescript/src/features/gate.d.ts +18 -2
- package/lib/typescript/src/features/gate.d.ts.map +1 -1
- package/lib/typescript/src/features/metadata.d.ts +20 -3
- package/lib/typescript/src/features/metadata.d.ts.map +1 -1
- package/lib/typescript/src/features/network.d.ts +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
- package/lib/typescript/src/features/output.d.ts +1 -1
- package/lib/typescript/src/features/playback/controls.d.ts +4 -2
- package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
- package/lib/typescript/src/features/playback/playing.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/rate.d.ts +8 -1
- package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +3 -4
- package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
- package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/volume.d.ts +1 -1
- package/lib/typescript/src/features/player/options.d.ts +108 -56
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts +14 -4
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
- package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +7 -4
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
- package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
- package/lib/typescript/src/features/remoteControls.d.ts +23 -22
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
- package/lib/typescript/src/types/browser-native.d.ts +3 -2
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +387 -126
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +93 -61
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
- package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
- package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
- package/lib/typescript/src/utils/resolveAssetSource.d.ts +1 -2
- package/lib/typescript/src/utils/resolveAssetSource.d.ts.map +1 -1
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
- package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +4 -2
- package/nitrogen/generated/android/c++/JAndroidAudioContentType.hpp +2 -2
- package/nitrogen/generated/android/c++/JAndroidAudioOffloadSettings.hpp +2 -2
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +12 -12
- package/nitrogen/generated/android/c++/JAndroidPlayerWakeMode.hpp +2 -2
- package/nitrogen/generated/android/c++/JAppKilledPlaybackBehavior.hpp +2 -2
- package/nitrogen/generated/android/c++/JArtworkRendering.hpp +58 -0
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +10 -5
- package/nitrogen/generated/android/c++/JArtworkVariants.hpp +2 -2
- package/nitrogen/generated/android/c++/JBatteryOptimizationStatus.hpp +2 -2
- package/nitrogen/generated/android/c++/JBatteryOptimizationStatusChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JBatteryWarningPendingChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JBrowseError.hpp +2 -2
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +19 -7
- package/nitrogen/generated/android/c++/JBrowserSourceCallbackParam.hpp +2 -2
- package/nitrogen/generated/android/c++/JCarPlayNowPlayingButton.hpp +2 -2
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JCarPlaySiriListButtonPosition.hpp +2 -2
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JChapterMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JEqualizerSettings.hpp +10 -10
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +10 -5
- package/nitrogen/generated/android/c++/JFormatNavigationErrorParams.hpp +11 -5
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +10 -5
- package/nitrogen/generated/android/c++/JFormattedNavigationError.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +10 -5
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.hpp +4 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +10 -5
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +10 -5
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.hpp +9 -9
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +10 -5
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +18 -12
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +10 -4
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +18 -6
- package/nitrogen/generated/android/c++/JFunc_void.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_BatteryOptimizationStatusChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_BatteryWarningPendingChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_EqualizerSettings.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +10 -5
- package/nitrogen/generated/android/c++/JFunc_void_GateEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_NavigationErrorEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_NowPlayingMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +9 -12
- package/nitrogen/generated/android/c++/JFunc_void_Output.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_Playback.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +10 -5
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackErrorEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackPlayWhenReadyChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackProgressUpdatedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackQueueEndedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_PlayingState.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpBackwardEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RemoteJumpForwardEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RemotePlayIdEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RemotePlaySearchEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RemoteSeekEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RemoteSkipEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_RepeatModeChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_TimedMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_TrackMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_FormattedNavigationError_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +17 -5
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_ChapterMetadata_.hpp +16 -16
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +24 -18
- package/nitrogen/generated/android/c++/JGate.hpp +2 -2
- package/nitrogen/generated/android/c++/JGateDecision.hpp +2 -2
- package/nitrogen/generated/android/c++/JGateEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JGateReason.hpp +2 -2
- package/nitrogen/generated/android/c++/JGridTile.hpp +64 -0
- package/nitrogen/generated/android/c++/JHttpMethod.hpp +2 -2
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +77 -53
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +3 -3
- package/nitrogen/generated/android/c++/JIOSCategory.hpp +2 -2
- package/nitrogen/generated/android/c++/JIOSCategoryMode.hpp +2 -2
- package/nitrogen/generated/android/c++/JIOSCategoryOptions.hpp +2 -2
- package/nitrogen/generated/android/c++/JIOSCategoryPolicy.hpp +2 -2
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +17 -11
- package/nitrogen/generated/android/c++/JImageContext.hpp +2 -2
- package/nitrogen/generated/android/c++/JImageQueryParams.hpp +2 -2
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JImageSource.hpp +2 -2
- package/nitrogen/generated/android/c++/JMediaReference.hpp +2 -2
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +10 -5
- package/nitrogen/generated/android/c++/JMediaTransformParams.hpp +2 -2
- package/nitrogen/generated/android/c++/JNativeAndroidSetupOptions.hpp +2 -2
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +48 -21
- package/nitrogen/generated/android/c++/JNativeGateRequest.hpp +2 -2
- package/nitrogen/generated/android/c++/JNativeIOSSetupOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +16 -4
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +17 -14
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +9 -12
- package/nitrogen/generated/android/c++/JNavigationError.hpp +2 -2
- package/nitrogen/generated/android/c++/JNavigationErrorEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JNavigationErrorKind.hpp +58 -0
- package/nitrogen/generated/android/c++/JNavigationErrorType.hpp +2 -2
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +12 -12
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +17 -11
- package/nitrogen/generated/android/c++/JNowPlayingMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JNowPlayingUpdate.hpp +2 -2
- package/nitrogen/generated/android/c++/JOptions.hpp +9 -12
- package/nitrogen/generated/android/c++/JOutput.hpp +2 -2
- package/nitrogen/generated/android/c++/JOutputType.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlayback.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +10 -5
- package/nitrogen/generated/android/c++/JPlaybackError.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackErrorEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackErrorKind.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackPlayWhenReadyChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackProgressUpdatedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackQueueEndedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlaybackState.hpp +2 -2
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +7 -13
- package/nitrogen/generated/android/c++/JPlayingState.hpp +2 -2
- package/nitrogen/generated/android/c++/JProgress.hpp +2 -2
- package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
- package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
- package/nitrogen/generated/android/c++/JRemoteJumpBackwardEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRemoteJumpForwardEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRemotePlayIdEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRemotePlaySearchEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRemoteSeekEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRemoteSkipEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRepeatMode.hpp +2 -2
- package/nitrogen/generated/android/c++/JRepeatModeChangedEvent.hpp +2 -2
- package/nitrogen/generated/android/c++/JRequestConfig.hpp +2 -2
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +67 -59
- package/nitrogen/generated/android/c++/JRetryConfig.hpp +2 -2
- package/nitrogen/generated/android/c++/JSearchMode.hpp +2 -2
- package/nitrogen/generated/android/c++/JSearchParams.hpp +2 -2
- package/nitrogen/generated/android/c++/JSection.hpp +116 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
- package/nitrogen/generated/android/c++/JSleepTimer.hpp +4 -4
- package/nitrogen/generated/android/c++/JSleepTimerEndOfTrack.hpp +2 -2
- package/nitrogen/generated/android/c++/JSleepTimerTime.hpp +2 -2
- package/nitrogen/generated/android/c++/JStallReason.hpp +2 -2
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTimedMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JTrack.hpp +27 -48
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +18 -12
- package/nitrogen/generated/android/c++/JTrackMetadata.hpp +2 -2
- package/nitrogen/generated/android/c++/JTrackRequest.hpp +2 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +47 -19
- package/nitrogen/generated/android/c++/JTransformableRequestConfig.hpp +2 -2
- package/nitrogen/generated/android/c++/JVariant_Boolean_AndroidAudioOffloadSettings.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_Boolean_RetryConfig.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +20 -8
- package/nitrogen/generated/android/c++/JVariant_String_ArtworkVariants.hpp +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidAudioOffloadSettings.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +20 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkRequestConfig.kt +37 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ArtworkVariants.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryOptimizationStatusChangedEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BatteryWarningPendingChangedEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseError.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowseResult.kt +16 -7
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/BrowserSourceCallbackParam.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ChapterMetadata.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/EqualizerSettings.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteChangedEvent.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNavigationErrorParams.kt +25 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormatNowPlayingParams.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FormattedNavigationError.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___MediaTransformParams.kt +0 -2
- 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 +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_RequestConfig___Track.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_FormattedNavigationError____FormatNavigationErrorParams.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__optional_std__string____Track.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_GateDecision_____NativeGateRequest.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____MediaTransformParams.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____RequestConfig_std__optional_std__unordered_map_std__string__std__string__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_TransformableRequestConfig____.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryOptimizationStatusChangedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_BatteryWarningPendingChangedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_EqualizerSettings.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_FavoriteChangedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_GateEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NavigationErrorEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_NowPlayingMetadata.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Options.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Output.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_Playback.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackActiveTrackChangedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackErrorEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackPlayWhenReadyChangedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackProgressUpdatedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlaybackQueueEndedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_PlayingState.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpBackwardEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteJumpForwardEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlayIdEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemotePlaySearchEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSeekEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RemoteSkipEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_RepeatModeChangedEvent.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TimedMetadata.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_TrackMetadata.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_bool.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_double.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_FormattedNavigationError_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_ResolvedTrack_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__optional_std__variant_nitro__NullType__SleepTimerTime__SleepTimerEndOfTrack__.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__string.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_ChapterMetadata_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_void_std__vector_Track_.kt +0 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Gate.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateDecision.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GateEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +3 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +25 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageContext.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageQueryParams.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageSource.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaRequestConfig.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/MediaTransformParams.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeAndroidSetupOptions.kt +31 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +51 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeGateRequest.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeIOSSetupOptions.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeRouteEntry.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeSetupPlayerOptions.kt +29 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeUpdateOptions.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationError.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NavigationErrorKind.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +20 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +25 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingMetadata.kt +29 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NowPlayingUpdate.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Options.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Output.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Playback.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackActiveTrackChangedEvent.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackError.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackErrorEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackPlayWhenReadyChangedEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackProgressUpdatedEvent.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlaybackQueueEndedEvent.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +37 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayingState.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Progress.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpBackwardEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteJumpForwardEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlayIdEvent.kt +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemotePlaySearchEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSeekEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteSkipEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RepeatModeChangedEvent.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RequestConfig.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +67 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RetryConfig.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SearchParams.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimer.kt +18 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerEndOfTrack.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SleepTimerTime.kt +13 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TimedMetadata.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +55 -12
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackLoadEvent.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackMetadata.kt +43 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackRequest.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +58 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TransformableRequestConfig.kt +31 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_AndroidAudioOffloadSettings.kt +16 -7
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_RetryConfig.kt +16 -7
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_NullType_Double.kt +16 -7
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +24 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.kt +18 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_String_ArtworkVariants.kt +16 -7
- package/nitrogen/generated/ios/AudioBrowser+autolinking.rb +2 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +38 -29
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +779 -665
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +34 -16
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +36 -18
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/BrowseResult.swift +12 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
- package/nitrogen/generated/ios/swift/FormatNavigationErrorParams.swift +7 -2
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +48 -0
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
- package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
- package/nitrogen/generated/ios/swift/NavigationErrorKind.swift +40 -0
- package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
- package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
- package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
- package/nitrogen/generated/ios/swift/Section.swift +82 -0
- package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
- package/nitrogen/generated/ios/swift/SleepTimer.swift +13 -0
- package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +28 -63
- package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
- package/nitrogen/generated/ios/swift/Variant_Bool_AndroidAudioOffloadSettings.swift +12 -0
- package/nitrogen/generated/ios/swift/Variant_Bool_RetryConfig.swift +12 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +12 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_RemoteButtonLayout.swift +30 -0
- package/nitrogen/generated/ios/swift/Variant_ResolvedTrack_BrowseError_Promise_BrowseResult_.swift +13 -0
- package/nitrogen/generated/ios/swift/Variant_String_ArtworkVariants.swift +12 -0
- package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
- package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
- package/nitrogen/generated/shared/c++/FormatNavigationErrorParams.hpp +9 -2
- package/nitrogen/generated/shared/c++/GridTile.hpp +84 -0
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
- package/nitrogen/generated/shared/c++/NavigationErrorKind.hpp +76 -0
- package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
- package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
- package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
- package/nitrogen/generated/shared/c++/Section.hpp +102 -0
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
- package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
- package/nitrogen/generated/shared/c++/Track.hpp +18 -30
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
- package/package.json +24 -11
- package/src/AudioBrowser.ts +2 -1
- package/src/CLAUDE.md +52 -0
- package/src/features/battery.test.ts +79 -0
- package/src/features/browser-config.test.ts +21 -6
- package/src/features/browser-config.ts +12 -8
- package/src/features/browser.test.ts +1 -1
- package/src/features/browser.ts +11 -3
- package/src/features/carConnection.ts +1 -1
- package/src/features/equalizer.ts +1 -1
- package/src/features/errors.ts +3 -3
- package/src/features/favorites.ts +14 -11
- package/src/features/gate.ts +16 -1
- package/src/features/hook-wiring.test.ts +302 -0
- package/src/features/metadata.ts +20 -3
- package/src/features/network.ts +1 -1
- package/src/features/nowPlaying.ts +1 -1
- package/src/features/output.ts +1 -1
- package/src/features/playback/controls.ts +4 -2
- package/src/features/playback/playWhenReady.ts +1 -1
- package/src/features/playback/playing.ts +1 -1
- package/src/features/playback/progress.test.ts +169 -0
- package/src/features/playback/progress.ts +3 -2
- package/src/features/playback/rate.ts +8 -1
- package/src/features/playback/state.ts +3 -4
- package/src/features/playback/trackPlaybackTime.test.ts +16 -6
- package/src/features/playback/trackPlaybackTime.ts +1 -6
- package/src/features/playback/volume.ts +1 -1
- package/src/features/player/options.ts +126 -76
- package/src/features/player/setup.test.ts +24 -7
- package/src/features/player/setup.ts +22 -10
- package/src/features/player/validateOptions.ts +21 -0
- package/src/features/queue/activeTrack.ts +15 -4
- package/src/features/queue/queue.ts +13 -6
- package/src/features/queue/repeatMode.ts +1 -1
- package/src/features/queue/shuffle.ts +1 -1
- package/src/features/remoteControls.ts +13 -92
- package/src/features/sleepTimer.test.ts +172 -0
- package/src/features/sleepTimer.ts +9 -1
- package/src/nitro-stub-drift.test.ts +277 -0
- package/src/public-surface.test.ts +147 -0
- package/src/specs/audio-browser.nitro.ts +5 -4
- package/src/style-resolution.test.ts +210 -0
- package/src/test-utils/reactNativeStub.ts +60 -0
- package/src/types/browser-native.ts +7 -3
- package/src/types/browser-nodes.ts +403 -134
- package/src/types/browser.ts +97 -63
- package/src/utils/LazyNativeEmitter.ts +28 -15
- package/src/utils/NativeUpdatedValue.ts +29 -13
- package/src/utils/getTrackIdentity.ts +20 -0
- package/src/utils/useDebug.test.ts +182 -0
- package/src/utils/useNativeUpdatedValue.test.ts +103 -0
- package/src/utils/useNativeValueRefreshedBy.ts +38 -0
- package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
- package/src/web/NativeAudioBrowser.ts +240 -83
- package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
- package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
- package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
- package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
- package/src/web/TrackPlayer/Player.load.test.ts +137 -1
- package/src/web/TrackPlayer/Player.ts +284 -37
- package/src/web/TrackPlayer/PlayingStateFactory.test.ts +1 -1
- package/src/web/TrackPlayer/PlayingStateFactory.ts +1 -1
- package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
- package/src/web/__tests__/lifecycle.test.ts +345 -0
- package/src/web/__tests__/lifecycleHarness.ts +351 -0
- package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
- package/src/web/__tests__/loadSupersession.test.ts +175 -0
- package/src/web/__tests__/retry.test.ts +169 -0
- package/src/web/browser/BrowserManager.ts +209 -111
- package/src/web/browser/FavoriteManager.test.ts +84 -0
- package/src/web/browser/FavoriteManager.ts +28 -23
- package/src/web/browser/NavigationErrorManager.ts +3 -1
- package/src/web/browser/SearchManager.test.ts +75 -0
- package/src/web/browser/SearchManager.ts +25 -28
- package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
- package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
- package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
- package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
- package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
- package/src/web/browser/assertBrowsePageShape.ts +5 -3
- package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
- package/src/web/browser/classifyTrackNavigation.ts +9 -9
- package/src/web/browser/parseSearchResponse.test.ts +8 -2
- package/src/web/browser/sections.test.ts +91 -0
- package/src/web/browser/sections.ts +72 -0
- package/src/web/http/RequestConfigBuilder.test.ts +241 -1
- package/src/web/http/RequestConfigBuilder.ts +140 -33
- package/src/web/http/followMediaRedirect.test.ts +289 -0
- package/src/web/http/followMediaRedirect.ts +253 -0
- package/src/web/player/NowPlayingManager.ts +4 -3
- package/src/web/util/BrowserPathHelper.test.ts +68 -0
- package/src/web/util/BrowserPathHelper.ts +70 -13
- package/website/guide/android-auto.md +114 -0
- package/website/guide/android-certificates.md +409 -0
- package/website/guide/artwork.md +255 -0
- package/website/guide/audio-output.md +135 -0
- package/website/guide/automotive.md +86 -0
- package/website/guide/basic-usage.md +151 -0
- package/website/guide/battery.md +115 -0
- package/website/guide/browser.md +587 -0
- package/website/guide/carplay.md +650 -0
- package/website/guide/configuration.md +173 -0
- package/website/guide/equalizer.md +231 -0
- package/website/guide/errors.md +310 -0
- package/website/guide/favorites.md +154 -0
- package/website/guide/gate.md +143 -0
- package/website/guide/getting-started.md +46 -0
- package/website/guide/hooks.md +238 -0
- package/website/guide/live-streams.md +125 -0
- package/website/guide/metadata.md +239 -0
- package/website/guide/migrating-from-track-player.md +244 -0
- package/website/guide/native-callback-fetch.md +127 -0
- package/website/guide/network.md +86 -0
- package/website/guide/now-playing.md +175 -0
- package/website/guide/playback.md +328 -0
- package/website/guide/queue.md +300 -0
- package/website/guide/remote-controls.md +256 -0
- package/website/guide/search.md +255 -0
- package/website/guide/sleep-timer.md +268 -0
- package/website/guide/track.md +247 -0
- package/ios/Bridge.h +0 -8
- package/ios/Browser/MediaResolveComposer.swift +0 -114
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/ios/Model/MediaURL.swift +0 -67
- package/ios/NowPlayingInfo/MediaItemProperty.swift +0 -78
- package/ios/NowPlayingInfo/NowPlayingInfoKeyValue.swift +0 -6
- package/ios/NowPlayingInfo/NowPlayingInfoProperty.swift +0 -220
- package/ios/Observer/PlayerItemNotificationObserver.swift +0 -119
- package/ios/Observer/PlayerItemPropertyObserver.swift +0 -133
- package/ios/Observer/PlayerStateObserver.swift +0 -50
- package/ios/Observer/PlayerTimeObserver.swift +0 -55
- package/ios/Option/PitchAlgorithms.swift +0 -17
- package/ios/Option/TimeEventFrequency.swift +0 -18
- package/ios/Player/LoadSeekCoordinator.swift +0 -99
- package/ios/Player/MediaLoaderDelegate.swift +0 -11
- package/ios/Player/PlaybackCoordinator.swift +0 -754
- package/ios/Player/PlaybackEffectHandler.swift +0 -54
- package/ios/Player/PlaybackErrorHandler.swift +0 -175
- package/ios/Player/PlaybackStateStore.swift +0 -27
- package/ios/Player/PlayingStateManager.swift +0 -33
- package/ios/Player/RetryManager.swift +0 -322
- package/ios/Player/SeekCompletionHandler.swift +0 -3
- package/ios/Player/SleepTimerHandling.swift +0 -15
- package/ios/Support/RNTrackPlayer-Bridging-Header.h +0 -7
- package/ios/Tests/LoadSeekCoordinatorTests.swift +0 -325
- package/ios/Tests/MediaResolveTests.swift +0 -126
- package/ios/Tests/OnceValueTests.swift +0 -79
- package/ios/Tests/PlaybackErrorHandlerTests.swift +0 -547
- package/ios/Tests/PlaybackStateStoreTests.swift +0 -48
- package/ios/Tests/PlayingStateManagerTests.swift +0 -57
- package/ios/Tests/RetryManagerTests.swift +0 -89
- package/ios/TrackPlayer.swift +0 -1014
- package/ios/TrackPlayerCallbacks.swift +0 -86
- package/ios/Util/MetadataAdapter.swift +0 -131
- package/ios/Util/OnceValue.swift +0 -104
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JFavoritesMatchMode.hpp +0 -58
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
- package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/FavoriteConfig.kt +0 -38
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
- package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
- package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
- package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
- package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
- package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
- package/nitrogen/generated/ios/swift/Variant_NullType_NotificationButtonLayout.swift +0 -18
- package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
- package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
|
@@ -5,7 +5,7 @@ import MediaPlayer
|
|
|
5
5
|
import NitroModules
|
|
6
6
|
import os.log
|
|
7
7
|
|
|
8
|
-
@MainActor let playerAndConfiguredBrowser =
|
|
8
|
+
@MainActor let playerAndConfiguredBrowser = ReadyValue<(HybridAudioBrowser, Player)> {
|
|
9
9
|
guard let browser = HybridAudioBrowser.shared,
|
|
10
10
|
browser.browserManager.isConfigured,
|
|
11
11
|
let player = browser.getPlayer() else { return nil }
|
|
@@ -16,8 +16,12 @@ import os.log
|
|
|
16
16
|
/// Only safe when the caller blocks until the MainActor work completes (DispatchQueue.main.sync).
|
|
17
17
|
private struct UncheckedSendableBox<T>: @unchecked Sendable { let value: T }
|
|
18
18
|
|
|
19
|
+
/// `@unchecked Sendable` because it must be: the spec's requirements are all
|
|
20
|
+
/// nonisolated and Nitro calls them synchronously on the JS thread, so the class
|
|
21
|
+
/// can't be `@MainActor`. Isolation is therefore per-property, and `@unchecked`
|
|
22
|
+
/// means the compiler won't tell you when a new one needs it.
|
|
19
23
|
public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
20
|
-
|
|
24
|
+
let logger = Logger(subsystem: "com.audiobrowser", category: "AudioBrowser")
|
|
21
25
|
|
|
22
26
|
// MARK: - Shared Instance for CarPlay
|
|
23
27
|
|
|
@@ -30,60 +34,62 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
30
34
|
/// silent.
|
|
31
35
|
static let instanceChangedEmitter = Emitter<Void>()
|
|
32
36
|
|
|
37
|
+
/// Installed once, on first instance creation. React Native posts this
|
|
38
|
+
/// notification (from `RCTReloadCommand`) when a JS reload — dev or
|
|
39
|
+
/// OTA-update restart — is triggered, *before* the old runtime is destroyed:
|
|
40
|
+
/// the earliest signal that this instance's JS callbacks are about to go
|
|
41
|
+
/// stale. `init` of the replacement instance drops them too, but only after
|
|
42
|
+
/// the new runtime is up; this observer closes the window in between.
|
|
43
|
+
/// Observed by name so the library needs no React-Core dependency (and
|
|
44
|
+
/// stays inert under SPM tests, where nothing posts it).
|
|
45
|
+
@MainActor private static let reloadObserver: NSObjectProtocol = NotificationCenter.default.addObserver(
|
|
46
|
+
forName: Notification.Name("RCTTriggerReloadCommandNotification"),
|
|
47
|
+
object: nil,
|
|
48
|
+
queue: .main,
|
|
49
|
+
) { _ in
|
|
50
|
+
MainActor.assumeIsolated {
|
|
51
|
+
HybridAudioBrowser.shared?.dropStaleJSCallbacks()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
// MARK: - Private Properties
|
|
34
56
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
/// Main-confined by convention, not annotation: `deinit` reads it as a
|
|
58
|
+
/// safety net and can't hop. Reach it from `onMainActor` / `@MainActor`.
|
|
59
|
+
var player: Player?
|
|
60
|
+
/// The media stack under `player`, for what only AVFoundation has: now playing, buffering,
|
|
61
|
+
/// volume and the remote commands.
|
|
62
|
+
var engine: AVPlayerEngine?
|
|
63
|
+
let networkMonitor = NetworkMonitor()
|
|
38
64
|
let browserManager = BrowserManager()
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
private var
|
|
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 = {}
|
|
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
|
|
65
|
+
let browseGate = BrowseGate()
|
|
66
|
+
let trackSelector: TrackSelector
|
|
67
|
+
@MainActor private(set) lazy var audioSession = AudioSessionController()
|
|
54
68
|
/// Initial player state staged before the player exists — from setup options or the imperative
|
|
55
69
|
/// setters called pre-setup. Strict last-write-wins; consumed when the player comes up.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
private var latestTimedMetadata: TimedMetadata?
|
|
60
|
-
private let playerOptions = PlayerUpdateOptions()
|
|
70
|
+
@MainActor var pendingPlayWhenReady: Bool?
|
|
71
|
+
@MainActor var pendingRepeatMode: RepeatMode?
|
|
72
|
+
@MainActor let playerOptions = PlayerUpdateOptions()
|
|
61
73
|
|
|
62
74
|
/// Configured playback rates for the playback-rate capability (for CarPlay rate cycling)
|
|
63
|
-
var playbackRates: [Double] { playerOptions.playbackRates }
|
|
64
|
-
var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
|
|
65
|
-
var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] {
|
|
66
|
-
|
|
67
|
-
didSet {
|
|
68
|
-
// Skip if both nil (no real change)
|
|
69
|
-
guard oldValue != nil || lastNavigationError != nil else { return }
|
|
70
|
-
navigationErrorEmitter.emit(NavigationErrorEvent(error: lastNavigationError))
|
|
71
|
-
}
|
|
75
|
+
@MainActor var playbackRates: [Double] { playerOptions.playbackRates }
|
|
76
|
+
@MainActor var carPlayUpNextButton: Bool { playerOptions.carPlayUpNextButton }
|
|
77
|
+
@MainActor var carPlayNowPlayingButtons: [CarPlayNowPlayingButton] {
|
|
78
|
+
playerOptions.carPlayNowPlayingButtons
|
|
72
79
|
}
|
|
73
80
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Skip if both nil (no real change)
|
|
77
|
-
guard oldValue != nil || lastFormattedNavigationError != nil else { return }
|
|
78
|
-
onFormattedNavigationError(lastFormattedNavigationError)
|
|
79
|
-
}
|
|
81
|
+
@MainActor var carPlayPlayingIndicatorLocation: CarPlayPlayingIndicatorLocation {
|
|
82
|
+
playerOptions.carPlayPlayingIndicatorLocation
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
@MainActor lazy var navigationError = NavigationErrorState(
|
|
86
|
+
onErrorChanged: { [weak self] in self?.navigationErrorEmitter.emit(NavigationErrorEvent(error: $0)) },
|
|
87
|
+
onFormattedChanged: { [weak self] in self?.onFormattedNavigationError($0) },
|
|
88
|
+
)
|
|
89
|
+
|
|
82
90
|
// MARK: - Multi-Listener Emitters
|
|
83
91
|
|
|
84
92
|
/// Emitters allow multiple listeners for each event type, avoiding callback hijacking
|
|
85
|
-
public let tabsChangedEmitter = Emitter<[Track]>()
|
|
86
|
-
public let contentChangedEmitter = Emitter<ResolvedTrack?>()
|
|
87
93
|
public let favoriteChangedEmitter = Emitter<FavoriteChangedEvent>()
|
|
88
94
|
public let activeTrackChangedEmitter = Emitter<PlaybackActiveTrackChangedEvent>()
|
|
89
95
|
public let queueChangedEmitter = Emitter<[Track]>()
|
|
@@ -91,11 +97,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
91
97
|
public let repeatModeChangedEmitter = Emitter<RepeatModeChangedEvent>()
|
|
92
98
|
public let shuffleChangedEmitter = Emitter<Bool>()
|
|
93
99
|
public let externalContentChangedEmitter = Emitter<String>()
|
|
94
|
-
///
|
|
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
|
|
100
|
+
/// Fired when a voice media intent (`MediaIntentPlayback`) successfully
|
|
99
101
|
/// starts playback. CarPlay surfaces the Now Playing template in response, so
|
|
100
102
|
/// the user lands on the playing station (with the rest of the results in Up
|
|
101
103
|
/// Next) rather than the screen they invoked Siri from. No-op where unobserved.
|
|
@@ -109,7 +111,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
109
111
|
|
|
110
112
|
/// Runs a closure on MainActor synchronously. Safe for non-Sendable return types
|
|
111
113
|
/// because DispatchQueue.main.sync blocks the caller — no concurrent access window.
|
|
112
|
-
|
|
114
|
+
func onMainActor<T>(_ work: @MainActor () -> T) -> T {
|
|
113
115
|
if Thread.isMainThread {
|
|
114
116
|
MainActor.assumeIsolated { UncheckedSendableBox(value: work()) }.value
|
|
115
117
|
} else {
|
|
@@ -119,7 +121,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
|
|
124
|
+
func onMainActor<T>(_ work: @MainActor () throws -> T) throws -> T {
|
|
123
125
|
if Thread.isMainThread {
|
|
124
126
|
try MainActor.assumeIsolated { try UncheckedSendableBox(value: work()) }.value
|
|
125
127
|
} else {
|
|
@@ -132,30 +134,29 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
132
134
|
// MARK: - Browser Properties
|
|
133
135
|
|
|
134
136
|
public var path: String? {
|
|
135
|
-
get { onMainActor { browserManager.
|
|
137
|
+
get { onMainActor { browserManager.path } }
|
|
136
138
|
set {
|
|
137
139
|
guard let newPath = newValue else { return }
|
|
138
|
-
|
|
139
|
-
do {
|
|
140
|
-
try await browserManager.navigate(newPath)
|
|
141
|
-
} catch {
|
|
142
|
-
handleNavigationError(error, path: newPath)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
140
|
+
navigate(newPath, clearError: false)
|
|
145
141
|
}
|
|
146
142
|
}
|
|
147
143
|
|
|
148
144
|
public var tabs: [Track]? {
|
|
149
|
-
get { onMainActor { browserManager.
|
|
145
|
+
get { onMainActor { browserManager.tabs } }
|
|
150
146
|
set { /* tabs are managed internally by browserManager */ }
|
|
151
147
|
}
|
|
152
148
|
|
|
153
149
|
public var configuration: NativeBrowserConfiguration = .init(
|
|
154
150
|
path: nil, request: nil, requestResolver: nil, browse: nil, browseResolver: nil, media: nil, artwork: nil, nowPlayingArtwork: nil, routes: nil,
|
|
155
151
|
singleTrack: nil, handleTrackLoad: nil,
|
|
156
|
-
androidControllerOfflineError: nil,
|
|
152
|
+
androidControllerOfflineError: nil, viewMoreTitle: nil, carPlayLoadingTitle: nil, resolveAlbumPath: nil,
|
|
153
|
+
formatNavigationError: nil,
|
|
157
154
|
) {
|
|
158
155
|
didSet {
|
|
156
|
+
// Copied here, on the setter's thread: the Task below outlives this call,
|
|
157
|
+
// and Nitro can reassign `configuration` — a C++ struct holding
|
|
158
|
+
// std::string/std::function — from the JS thread while it runs.
|
|
159
|
+
let configuredPath = configuration.path
|
|
159
160
|
onMainActor { browserManager.config = BrowserConfig(from: configuration) }
|
|
160
161
|
|
|
161
162
|
// Query tabs and navigate to initial path after config is set (matches Kotlin behavior)
|
|
@@ -166,17 +167,7 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
166
167
|
Task {
|
|
167
168
|
let tabs = try? await browserManager.queryTabs()
|
|
168
169
|
// Navigate to configured path, first tab, or "/"
|
|
169
|
-
|
|
170
|
-
// Clear error before navigation (matches Kotlin clearNavigationError())
|
|
171
|
-
await MainActor.run {
|
|
172
|
-
lastNavigationError = nil
|
|
173
|
-
lastFormattedNavigationError = nil
|
|
174
|
-
}
|
|
175
|
-
do {
|
|
176
|
-
try await browserManager.navigate(initialPath)
|
|
177
|
-
} catch {
|
|
178
|
-
handleNavigationError(error, path: initialPath)
|
|
179
|
-
}
|
|
170
|
+
navigate(configuredPath ?? tabs?.first?.path ?? "/", clearError: true)
|
|
180
171
|
}
|
|
181
172
|
}
|
|
182
173
|
}
|
|
@@ -196,13 +187,19 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
196
187
|
/// JS-side Promise, the inner to the decision.
|
|
197
188
|
///
|
|
198
189
|
/// Default fails CLOSED (`gated: true`). This default is only consulted in the
|
|
199
|
-
/// init window where
|
|
190
|
+
/// init window where the gate is already `.resolved` but JS hasn't
|
|
200
191
|
/// (re)bound the real resolver yet (JS reload / instance churn re-seeding gate
|
|
201
|
-
/// state, e.g. CarPlay
|
|
192
|
+
/// state, e.g. CarPlay deciding its tabs at scene connect). Serving content
|
|
202
193
|
/// in that window would be a fail-open leak — same direction as the
|
|
203
|
-
/// resolver-error path in `
|
|
204
|
-
public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> =
|
|
205
|
-
|
|
194
|
+
/// resolver-error path in `BrowseGate.decide`. Matches Android's default.
|
|
195
|
+
public var resolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> = HybridAudioBrowser.defaultResolveGate
|
|
196
|
+
|
|
197
|
+
// Computed, not stored: a nonisolated static holding a non-Sendable closure
|
|
198
|
+
// is rejected under Swift 6 concurrency checking.
|
|
199
|
+
static var defaultResolveGate: (NativeGateRequest) -> Promise<Promise<GateDecision>> {
|
|
200
|
+
{ _ in
|
|
201
|
+
Promise.resolved(withResult: Promise.resolved(withResult: GateDecision(gated: true, gate: nil)))
|
|
202
|
+
}
|
|
206
203
|
}
|
|
207
204
|
|
|
208
205
|
/// Fired when a request is gated (served the gate). Set by JS.
|
|
@@ -271,32 +268,44 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
271
268
|
public var onEqualizerChanged: (EqualizerSettings) -> Void = { _ in }
|
|
272
269
|
public var onBatteryWarningPendingChanged: (BatteryWarningPendingChangedEvent) -> Void = { _ in }
|
|
273
270
|
public var onBatteryOptimizationStatusChanged: (BatteryOptimizationStatusChangedEvent) -> Void = { _ in }
|
|
274
|
-
public var onSystemVolumeChanged: (Double) -> Void = { _ in }
|
|
275
|
-
didSet {
|
|
276
|
-
setupVolumeObserver()
|
|
277
|
-
}
|
|
278
|
-
}
|
|
271
|
+
public var onSystemVolumeChanged: (Double) -> Void = { _ in }
|
|
279
272
|
|
|
280
|
-
public var onOutputChanged: (Output) -> Void = { _ in }
|
|
273
|
+
public var onOutputChanged: (Output) -> Void = { _ in }
|
|
274
|
+
|
|
275
|
+
public var onCarConnectedChanged: (Bool) -> Void = { _ in } {
|
|
281
276
|
didSet {
|
|
282
|
-
|
|
277
|
+
// Immediately notify current state (the scene may have connected before
|
|
278
|
+
// this JS runtime subscribed).
|
|
279
|
+
onCarConnectedChanged(HybridAudioBrowser.carPlayConnected)
|
|
283
280
|
}
|
|
284
281
|
}
|
|
285
282
|
|
|
283
|
+
/// Stored so a re-setup can re-apply it: unlike the other runtime options this
|
|
284
|
+
/// one has no home in `playerOptions`, and the player it was pushed to does not
|
|
285
|
+
/// survive `setupPlayer`.
|
|
286
|
+
@MainActor var playbackIntervalEnabled = false
|
|
287
|
+
|
|
286
288
|
// MARK: - Initialization
|
|
287
289
|
|
|
288
290
|
override public init() {
|
|
289
291
|
trackSelector = TrackSelector(browserManager: browserManager)
|
|
290
292
|
super.init()
|
|
291
293
|
|
|
292
|
-
// Clean up the previous instance (e.g., on JS runtime reload)
|
|
293
|
-
//
|
|
294
|
-
// playerAndConfiguredBrowser OnceValue, so we must explicitly stop
|
|
295
|
-
// its player to prevent two audio streams running simultaneously.
|
|
294
|
+
// Clean up the previous instance (e.g., on JS runtime reload): stop its
|
|
295
|
+
// player so two audio streams never run at once.
|
|
296
296
|
onMainActor {
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
// Statics are lazy — touch the observer so it's installed from the first
|
|
298
|
+
// instance onward.
|
|
299
|
+
_ = HybridAudioBrowser.reloadObserver
|
|
300
|
+
HybridAudioBrowser.shared?.destroyPlayer()
|
|
301
|
+
HybridAudioBrowser.shared?.dropStaleJSCallbacks()
|
|
299
302
|
HybridAudioBrowser.shared = self
|
|
303
|
+
audioSession.outputChanged.addListener { [weak self] in self?.onOutputChanged($0) }
|
|
304
|
+
audioSession.volumeChanged.addListener { [weak self] in self?.onSystemVolumeChanged($0) }
|
|
305
|
+
browseGate.resolve = { [weak self] request in
|
|
306
|
+
guard let self else { throw CancellationError() }
|
|
307
|
+
return try await resolveGate(request).await().await()
|
|
308
|
+
}
|
|
300
309
|
// Tell connected external controllers (CarPlay) to re-subscribe against
|
|
301
310
|
// this instance. Unconditional: `shared` is weak, so on a JS reload the
|
|
302
311
|
// old instance may already be gone — "shared was nil" does not mean no
|
|
@@ -304,33 +313,76 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
304
313
|
HybridAudioBrowser.instanceChangedEmitter.emit(())
|
|
305
314
|
}
|
|
306
315
|
setupEmitterToNitroForwarding()
|
|
307
|
-
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/// Drops every JS-provided callback that native code can invoke on its own
|
|
319
|
+
/// initiative (browse resolvers, `handleTrackLoad`, `resolveAlbumPath`,
|
|
320
|
+
/// `formatNavigationError`, `resolveGate`, the now-playing formatter).
|
|
321
|
+
///
|
|
322
|
+
/// Called on the *previous* instance when a new one takes over: its JS runtime
|
|
323
|
+
/// is gone, and invoking a callback from a dead runtime aborts the process —
|
|
324
|
+
/// Nitro's `AsyncJSCallback` throws on a destroyed Dispatcher, and the throw
|
|
325
|
+
/// crosses the generated bridge wrapper's `noexcept` boundary. Native code
|
|
326
|
+
/// (e.g. a CarPlay controller still bound to this instance mid-reload) keeps
|
|
327
|
+
/// working off the data-only config with built-in fallbacks; `resolveGate`
|
|
328
|
+
/// reverts to the fail-closed default it documents for exactly this window.
|
|
329
|
+
/// One-shot: on a normal reload both triggers hit the same dying instance
|
|
330
|
+
/// (the reload notification, then the replacement's `init`), and the second
|
|
331
|
+
/// strip would re-fire the config-setter cascade — another `configChanged`
|
|
332
|
+
/// → CarPlay now-playing button rebuild — for nothing.
|
|
333
|
+
@MainActor private var didDropJSCallbacks = false
|
|
334
|
+
|
|
335
|
+
@MainActor
|
|
336
|
+
private func dropStaleJSCallbacks() {
|
|
337
|
+
guard !didDropJSCallbacks else { return }
|
|
338
|
+
didDropJSCallbacks = true
|
|
339
|
+
// Guarded: assigning `config` marks the browser configured (the setter's
|
|
340
|
+
// didSet), and a never-configured browser must keep cold-start waiters
|
|
341
|
+
// (CarPlay, Siri) blocked on the readiness gate rather than resolve them
|
|
342
|
+
// against an empty config.
|
|
343
|
+
if browserManager.isConfigured {
|
|
344
|
+
browserManager.config = browserManager.config.strippingJSCallbacks()
|
|
345
|
+
}
|
|
346
|
+
resolveGate = Self.defaultResolveGate
|
|
347
|
+
engine?.nowPlayingUpdater.formatter = nil
|
|
308
348
|
}
|
|
309
349
|
|
|
310
350
|
deinit {
|
|
311
351
|
// Safety net: ensure the AVPlayer is stopped if this instance is deallocated.
|
|
312
|
-
let player = self.player
|
|
352
|
+
let player = self.player, engine = self.engine
|
|
313
353
|
if Thread.isMainThread {
|
|
314
|
-
MainActor.assumeIsolated {
|
|
354
|
+
MainActor.assumeIsolated {
|
|
355
|
+
player?.clear()
|
|
356
|
+
engine?.destroy()
|
|
357
|
+
}
|
|
315
358
|
} else {
|
|
316
|
-
DispatchQueue.main.async {
|
|
359
|
+
DispatchQueue.main.async {
|
|
360
|
+
MainActor.assumeIsolated {
|
|
361
|
+
player?.clear()
|
|
362
|
+
engine?.destroy()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
317
365
|
}
|
|
318
366
|
}
|
|
319
367
|
|
|
320
|
-
/// Returns the
|
|
321
|
-
func getPlayer() ->
|
|
368
|
+
/// Returns the player, if setup has been called.
|
|
369
|
+
func getPlayer() -> Player? {
|
|
322
370
|
player
|
|
323
371
|
}
|
|
324
372
|
|
|
373
|
+
/// Stops the player and releases its media stack: the AVPlayer and the now-playing session
|
|
374
|
+
/// outlive a dropped reference otherwise.
|
|
375
|
+
@MainActor
|
|
376
|
+
func destroyPlayer() {
|
|
377
|
+
player?.clear()
|
|
378
|
+
engine?.destroy()
|
|
379
|
+
}
|
|
380
|
+
|
|
325
381
|
/// Sets up automatic forwarding from emitters to Nitro callbacks
|
|
326
382
|
private func setupEmitterToNitroForwarding() {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
contentChangedEmitter.addListener { [weak self] content in
|
|
332
|
-
self?.onContentChanged(content)
|
|
333
|
-
}
|
|
383
|
+
browserManager.pathChanged.addListener { [weak self] in self?.onPathChanged($0) }
|
|
384
|
+
browserManager.tabsChanged.addListener { [weak self] in self?.onTabsChanged($0) }
|
|
385
|
+
browserManager.contentChanged.addListener { [weak self] in self?.onContentChanged($0) }
|
|
334
386
|
|
|
335
387
|
favoriteChangedEmitter.addListener { [weak self] event in
|
|
336
388
|
self?.onFavoriteChanged(event)
|
|
@@ -352,1515 +404,6 @@ public class HybridAudioBrowser: HybridAudioBrowserSpec, @unchecked Sendable {
|
|
|
352
404
|
self?.onPlaybackRepeatModeChanged(event)
|
|
353
405
|
}
|
|
354
406
|
}
|
|
355
|
-
|
|
356
|
-
private func setupBrowserCallbacks() {
|
|
357
|
-
onMainActor {
|
|
358
|
-
browserManager.onPathChanged = { [weak self] path in
|
|
359
|
-
self?.onPathChanged(path)
|
|
360
|
-
}
|
|
361
|
-
browserManager.onContentChanged = { [weak self] content in
|
|
362
|
-
self?.contentChangedEmitter.emit(content)
|
|
363
|
-
}
|
|
364
|
-
browserManager.onTabsChanged = { [weak self] tabs in
|
|
365
|
-
self?.tabsChangedEmitter.emit(tabs)
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
private func handleNavigationError(_ error: Error, path: String) {
|
|
371
|
-
let navError = NavigationError.from(error)
|
|
372
|
-
|
|
373
|
-
lastNavigationError = navError
|
|
374
|
-
|
|
375
|
-
// Format the error (async if using JS callback, sync for defaults)
|
|
376
|
-
let defaultFormatted = navError.defaultFormatted()
|
|
377
|
-
if let formatter = onMainActor({ browserManager.config.formatNavigationError }) {
|
|
378
|
-
let params = FormatNavigationErrorParams(error: navError, defaultFormatted: defaultFormatted, path: path)
|
|
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.
|
|
384
|
-
DispatchQueue.main.async { [weak self] in
|
|
385
|
-
formatter(params)
|
|
386
|
-
.then { @Sendable [weak self] customDisplay in
|
|
387
|
-
self?.lastFormattedNavigationError = customDisplay ?? defaultFormatted
|
|
388
|
-
}
|
|
389
|
-
.catch { @Sendable [weak self] _ in
|
|
390
|
-
self?.lastFormattedNavigationError = defaultFormatted
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
} else {
|
|
394
|
-
lastFormattedNavigationError = defaultFormatted
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// MARK: - Browser Methods
|
|
399
|
-
|
|
400
|
-
public func navigatePath(path: String) throws {
|
|
401
|
-
// Clear error synchronously before starting navigation (didSet notifies JS)
|
|
402
|
-
lastNavigationError = nil
|
|
403
|
-
lastFormattedNavigationError = nil
|
|
404
|
-
Task {
|
|
405
|
-
do {
|
|
406
|
-
try await browserManager.navigate(path)
|
|
407
|
-
} catch {
|
|
408
|
-
handleNavigationError(error, path: path)
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
public func navigateTrack(track: Track) throws {
|
|
414
|
-
Task {
|
|
415
|
-
guard let player else { return }
|
|
416
|
-
let result = await trackSelector.select(track: track, player: player)
|
|
417
|
-
switch result {
|
|
418
|
-
case let .play(intent):
|
|
419
|
-
executePlayback(intent)
|
|
420
|
-
case .intercepted:
|
|
421
|
-
break
|
|
422
|
-
case let .browse(url):
|
|
423
|
-
navigateToBrowsableUrl(url)
|
|
424
|
-
case .none:
|
|
425
|
-
break
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
private func executePlayback(_ intent: TrackSelector.PlaybackIntent) {
|
|
431
|
-
onMainActor {
|
|
432
|
-
switch intent {
|
|
433
|
-
case let .skipTo(index):
|
|
434
|
-
do {
|
|
435
|
-
try player?.skipTo(index, playWhenReady: true)
|
|
436
|
-
} catch {
|
|
437
|
-
logger.error("Failed to skip to track at index \(index): \(error)")
|
|
438
|
-
}
|
|
439
|
-
case let .setQueue(tracks, startIndex, sourcePath):
|
|
440
|
-
player?.setQueue(tracks, initialIndex: startIndex, playWhenReady: true, sourcePath: sourcePath)
|
|
441
|
-
case let .loadTrack(track):
|
|
442
|
-
player?.load(track, playWhenReady: true)
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
private func navigateToBrowsableUrl(_ url: String) {
|
|
448
|
-
// Clear error synchronously before starting navigation (didSet notifies JS)
|
|
449
|
-
lastNavigationError = nil
|
|
450
|
-
lastFormattedNavigationError = nil
|
|
451
|
-
Task {
|
|
452
|
-
do {
|
|
453
|
-
try await browserManager.navigate(url)
|
|
454
|
-
} catch {
|
|
455
|
-
handleNavigationError(error, path: url)
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
public func onSearch(query: String) throws -> Promise<[Track]> {
|
|
461
|
-
Promise.async { [weak self] in
|
|
462
|
-
guard let self else { return [] }
|
|
463
|
-
let resolved = try await browserManager.search(query)
|
|
464
|
-
return resolved.children ?? []
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
public func getContent() throws -> ResolvedTrack? {
|
|
469
|
-
onMainActor { browserManager.getContent() }
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
public func getNavigationError() throws -> NavigationError? {
|
|
473
|
-
lastNavigationError
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
public func getFormattedNavigationError() throws -> FormattedNavigationError? {
|
|
477
|
-
lastFormattedNavigationError
|
|
478
|
-
}
|
|
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
|
-
|
|
486
|
-
public func notifyContentChanged(path: String) throws {
|
|
487
|
-
onMainActor { browserManager.invalidateContentCache(path) }
|
|
488
|
-
|
|
489
|
-
// Notify external controllers (CarPlay) that content changed
|
|
490
|
-
externalContentChangedEmitter.emit(path)
|
|
491
|
-
|
|
492
|
-
// Re-resolve the path if it's the current browser path
|
|
493
|
-
if onMainActor({ browserManager.getPath() }) == path {
|
|
494
|
-
Task {
|
|
495
|
-
do {
|
|
496
|
-
try await browserManager.navigate(path)
|
|
497
|
-
} catch {
|
|
498
|
-
handleNavigationError(error, path: path)
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
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
|
-
|
|
530
|
-
public func setFavorites(favorites: [String]) throws {
|
|
531
|
-
onMainActor { browserManager.setFavorites(favorites) }
|
|
532
|
-
}
|
|
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
|
-
|
|
635
|
-
// MARK: - Player Setup
|
|
636
|
-
|
|
637
|
-
public func setupPlayer(options: NativeSetupPlayerOptions) throws -> Promise<Void> {
|
|
638
|
-
Promise.async {
|
|
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.
|
|
647
|
-
let session = AVAudioSession.sharedInstance()
|
|
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()
|
|
661
|
-
|
|
662
|
-
// Create player and configure on main actor
|
|
663
|
-
await MainActor.run { [weak self] in
|
|
664
|
-
guard let self else { return }
|
|
665
|
-
self.applyAudioSessionCategory = applyCategory
|
|
666
|
-
|
|
667
|
-
// Create player with self as callbacks delegate
|
|
668
|
-
player = TrackPlayer(callbacks: self)
|
|
669
|
-
|
|
670
|
-
// Apply buffer duration from options (in ms)
|
|
671
|
-
if let buffer = options.ios?.buffer {
|
|
672
|
-
player?.bufferDuration = buffer
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Apply retry configuration (top-level, cross-platform)
|
|
676
|
-
if let retry = options.retry {
|
|
677
|
-
player?.retryConfig = retry
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Wire up network monitor for accelerated retries when connectivity is restored
|
|
681
|
-
player?.networkMonitor = networkMonitor
|
|
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
|
-
|
|
708
|
-
// Configure media URL resolver
|
|
709
|
-
player?.mediaLoader.mediaUrlResolver = { [weak self] src, track in
|
|
710
|
-
guard let self else {
|
|
711
|
-
return MediaResolvedUrl(url: src, headers: nil, userAgent: nil)
|
|
712
|
-
}
|
|
713
|
-
return await browserManager.resolveMediaUrl(src, track: track)
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// Configure artwork URL resolver for Now Playing (with size context)
|
|
717
|
-
player?.nowPlayingUpdater.artworkUrlResolver = { [weak self] track, imageContext in
|
|
718
|
-
guard let self else { return nil }
|
|
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))
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Configure sleep timer callback
|
|
732
|
-
player?.sleepTimerManager.onChanged = { [weak self] state in
|
|
733
|
-
self?.onSleepTimerChanged(state)
|
|
734
|
-
}
|
|
735
|
-
|
|
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()
|
|
760
|
-
|
|
761
|
-
// Notify listeners that player is ready (e.g., CarPlay)
|
|
762
|
-
playerAndConfiguredBrowser.check()
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
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
|
-
|
|
783
|
-
public func updateOptions(options: NativeUpdateOptions) {
|
|
784
|
-
onMainActor {
|
|
785
|
-
let previousInterval = playerOptions.progressUpdateEventInterval
|
|
786
|
-
|
|
787
|
-
// Update stored options
|
|
788
|
-
playerOptions.update(from: options)
|
|
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
|
-
|
|
794
|
-
// Apply remote commands to player
|
|
795
|
-
applyRemoteCommands()
|
|
796
|
-
|
|
797
|
-
// Apply progress update interval if changed
|
|
798
|
-
if playerOptions.progressUpdateEventInterval != previousInterval {
|
|
799
|
-
player?.setProgressUpdateInterval(playerOptions.progressUpdateEventInterval)
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
onOptionsChanged(playerOptions.toOptions())
|
|
803
|
-
playerOptionsChangedEmitter.emit(())
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
/// Converts capabilities to remote commands and applies them to the player
|
|
808
|
-
@MainActor
|
|
809
|
-
private func applyRemoteCommands() {
|
|
810
|
-
guard let player else { return }
|
|
811
|
-
|
|
812
|
-
let remoteCommands = playerOptions.capabilities.buildRemoteCommands(
|
|
813
|
-
forwardJumpInterval: NSNumber(value: playerOptions.forwardJumpInterval),
|
|
814
|
-
backwardJumpInterval: NSNumber(value: playerOptions.backwardJumpInterval),
|
|
815
|
-
playbackRates: playerOptions.playbackRates,
|
|
816
|
-
)
|
|
817
|
-
|
|
818
|
-
player.remoteCommands = remoteCommands
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
public func getOptions() throws -> Options {
|
|
822
|
-
playerOptions.toOptions()
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
public func setPlaybackIntervalEnabled(enabled: Bool) {
|
|
826
|
-
onMainActor { player?.setPlaybackIntervalEnabled(enabled) }
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// MARK: - Playback Control
|
|
830
|
-
|
|
831
|
-
public func load(track: Track) throws {
|
|
832
|
-
try onMainActor {
|
|
833
|
-
guard let player else {
|
|
834
|
-
throw NSError(domain: "AudioBrowser", code: 1, userInfo: [NSLocalizedDescriptionKey: "Player not initialized"])
|
|
835
|
-
}
|
|
836
|
-
player.load(track)
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
public func reset() throws {
|
|
841
|
-
onMainActor { player?.clear() }
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
public func play() throws {
|
|
845
|
-
onMainActor { player?.play() }
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
public func pause() throws {
|
|
849
|
-
onMainActor { player?.pause() }
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
public func togglePlayback() throws {
|
|
853
|
-
onMainActor { player?.togglePlayback() }
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
public func stop() throws {
|
|
857
|
-
onMainActor { player?.stop() }
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
public func setPlayWhenReady(playWhenReady: Bool) throws {
|
|
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
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
public func getPlayWhenReady() throws -> Bool {
|
|
869
|
-
onMainActor { player?.playWhenReady ?? pendingPlayWhenReady ?? false }
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
public func seekTo(position: Double) throws {
|
|
873
|
-
onMainActor { player?.seekTo(position) }
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
public func seekBy(offset: Double) throws {
|
|
877
|
-
onMainActor { player?.seekBy(offset) }
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
public func seekToLiveEdge() throws {
|
|
881
|
-
onMainActor { player?.seekToLiveEdge() }
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
public func setVolume(level: Double) throws {
|
|
885
|
-
onMainActor { player?.volume = Float(level) }
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
public func getVolume() throws -> Double {
|
|
889
|
-
onMainActor { Double(player?.volume ?? 1.0) }
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
public func setRate(rate: Double) throws {
|
|
893
|
-
onMainActor { player?.rate = Float(rate) }
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
public func getRate() throws -> Double {
|
|
897
|
-
onMainActor { Double(player?.rate ?? 1.0) }
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
public func getProgress() throws -> Progress {
|
|
901
|
-
onMainActor {
|
|
902
|
-
Progress(
|
|
903
|
-
position: player?.currentTime ?? 0,
|
|
904
|
-
duration: player?.duration ?? 0,
|
|
905
|
-
buffered: player?.bufferedPosition ?? 0,
|
|
906
|
-
)
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
public func getPlayback() throws -> Playback {
|
|
911
|
-
onMainActor { player?.getPlayback() ?? Playback(state: .none, error: nil) }
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
public func getPlayingState() throws -> PlayingState {
|
|
915
|
-
onMainActor {
|
|
916
|
-
player?.playingStateManager.toPlayingState() ?? PlayingState(playing: false, buffering: false)
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
public func getRepeatMode() throws -> RepeatMode {
|
|
921
|
-
onMainActor { player?.repeatMode ?? pendingRepeatMode ?? .off }
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
public func setRepeatMode(mode: RepeatMode) throws {
|
|
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
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
public func getShuffleEnabled() throws -> Bool {
|
|
933
|
-
onMainActor { player?.shuffleEnabled ?? false }
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
public func setShuffleEnabled(enabled: Bool) throws {
|
|
937
|
-
onMainActor { player?.shuffleEnabled = enabled }
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
public func getPlaybackError() throws -> PlaybackError? {
|
|
941
|
-
onMainActor { player?.coordinator.nitroPlaybackError() }
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
public func retry() throws {
|
|
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) }
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// MARK: - Sleep Timer
|
|
951
|
-
|
|
952
|
-
public func getSleepTimer() throws -> SleepTimer {
|
|
953
|
-
onMainActor {
|
|
954
|
-
if let state = player?.sleepTimerManager.get() {
|
|
955
|
-
return state
|
|
956
|
-
}
|
|
957
|
-
return .first(NullType.null)
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
public func setSleepTimer(seconds: Double, fadeDuration: Double?) throws {
|
|
962
|
-
onMainActor {
|
|
963
|
-
player?.sleepTimerManager.set(seconds: seconds, fadeDuration: fadeDuration)
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
public func setSleepTimerToEndOfTrack() throws {
|
|
968
|
-
onMainActor {
|
|
969
|
-
player?.sleepTimerManager.setToEndOfTrack()
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
public func clearSleepTimer() throws -> Bool {
|
|
974
|
-
onMainActor {
|
|
975
|
-
player?.sleepTimerManager.clear() ?? false
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
// MARK: - Queue Management
|
|
980
|
-
|
|
981
|
-
public func add(tracks: [Track], insertBeforeIndex: Double?) throws {
|
|
982
|
-
try onMainActor {
|
|
983
|
-
guard let player else { return }
|
|
984
|
-
if let index = insertBeforeIndex {
|
|
985
|
-
try player.add(tracks, at: Int(index))
|
|
986
|
-
} else {
|
|
987
|
-
player.add(tracks)
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
public func move(fromIndex: Double, toIndex: Double) throws {
|
|
993
|
-
try onMainActor {
|
|
994
|
-
try player?.move(fromIndex: Int(fromIndex), toIndex: Int(toIndex))
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
public func remove(indexes: [Double]) throws {
|
|
999
|
-
try onMainActor {
|
|
1000
|
-
guard let player else { return }
|
|
1001
|
-
// Remove in reverse order to maintain index validity
|
|
1002
|
-
for index in indexes.sorted().reversed() {
|
|
1003
|
-
try player.remove(Int(index))
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
public func removeUpcomingTracks() throws {
|
|
1009
|
-
onMainActor { player?.removeUpcomingTracks() }
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
public func skip(index: Double, initialPosition: Double?) throws {
|
|
1013
|
-
try onMainActor {
|
|
1014
|
-
try player?.skipTo(Int(index))
|
|
1015
|
-
if let position = initialPosition {
|
|
1016
|
-
player?.seekTo(position)
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
public func skipToNext(initialPosition: Double?) throws {
|
|
1022
|
-
onMainActor {
|
|
1023
|
-
player?.next()
|
|
1024
|
-
if let position = initialPosition {
|
|
1025
|
-
player?.seekTo(position)
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
public func skipToPrevious(initialPosition: Double?) throws {
|
|
1031
|
-
onMainActor {
|
|
1032
|
-
player?.previous()
|
|
1033
|
-
if let position = initialPosition {
|
|
1034
|
-
player?.seekTo(position)
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
public func setActiveTrackFavorited(favorited: Bool) throws {
|
|
1040
|
-
onMainActor {
|
|
1041
|
-
guard let track = player?.currentTrack, let src = track.src else { return }
|
|
1042
|
-
guard let index = player?.currentIndex, index >= 0 else { return }
|
|
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)
|
|
1047
|
-
let updatedTrack = track.copying(favorited: favorited)
|
|
1048
|
-
favoriteChangedEmitter.emit(FavoriteChangedEvent(track: updatedTrack, favorited: favorited))
|
|
1049
|
-
// Fire active track changed so the now-playing heart + useActiveTrack() refresh.
|
|
1050
|
-
let position = player?.currentTime ?? 0
|
|
1051
|
-
activeTrackChangedEmitter.emit(PlaybackActiveTrackChangedEvent(
|
|
1052
|
-
lastIndex: Double(index),
|
|
1053
|
-
lastTrack: track,
|
|
1054
|
-
lastPosition: position,
|
|
1055
|
-
index: Double(index),
|
|
1056
|
-
track: updatedTrack,
|
|
1057
|
-
))
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
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
|
-
|
|
1067
|
-
public func toggleActiveTrackFavorited() throws {
|
|
1068
|
-
try? setActiveTrackFavorited(favorited: !isActiveTrackFavorited())
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
public func setQueue(tracks: [Track], startIndex: Double?, startPosition: Double?) throws {
|
|
1072
|
-
onMainActor {
|
|
1073
|
-
guard let player else { return }
|
|
1074
|
-
player.setQueue(
|
|
1075
|
-
tracks,
|
|
1076
|
-
initialIndex: startIndex.map { Int($0) } ?? 0,
|
|
1077
|
-
startPositionMs: startPosition.map { $0 * 1000 },
|
|
1078
|
-
)
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
public func getQueue() throws -> [Track] {
|
|
1083
|
-
onMainActor { player?.tracks ?? [] }
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
public func getTrack(index: Double) throws -> Track? {
|
|
1087
|
-
onMainActor {
|
|
1088
|
-
guard let tracks = player?.tracks else { return nil }
|
|
1089
|
-
let i = Int(index)
|
|
1090
|
-
guard i >= 0, i < tracks.count else { return nil }
|
|
1091
|
-
return tracks[i]
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
public func getActiveTrackIndex() throws -> Double? {
|
|
1096
|
-
onMainActor {
|
|
1097
|
-
guard let index = player?.currentIndex, index >= 0 else { return nil }
|
|
1098
|
-
return Double(index)
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
public func getActiveTrack() throws -> Track? {
|
|
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) }
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
// MARK: - Now Playing
|
|
1111
|
-
|
|
1112
|
-
public func updateNowPlaying(update: NowPlayingUpdate?) throws {
|
|
1113
|
-
onMainActor {
|
|
1114
|
-
nowPlayingOverride = update
|
|
1115
|
-
applyNowPlayingMetadata()
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
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
|
-
|
|
1160
|
-
public func getNowPlaying() throws -> NowPlayingMetadata? {
|
|
1161
|
-
onMainActor {
|
|
1162
|
-
guard let track = player?.currentTrack else { return nil }
|
|
1163
|
-
let flash = nowPlayingFlash
|
|
1164
|
-
let override = nowPlayingOverride
|
|
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,
|
|
1170
|
-
)
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
/// Returns the artwork URI suitable for JS consumption.
|
|
1175
|
-
/// Prefers the resolved artworkSource URI (e.g. file:// for rendered SF Symbols).
|
|
1176
|
-
/// Falls back to the raw artwork string only if it's not an SF Symbol (which JS can't load).
|
|
1177
|
-
private func nowPlayingArtwork(for track: Track) -> String? {
|
|
1178
|
-
if let uri = track.artworkSource?.uri { return uri }
|
|
1179
|
-
guard let artwork = track.artwork?.url else { return nil }
|
|
1180
|
-
return SFSymbolRenderer.isSFSymbol(artwork) ? nil : artwork
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
/// Builds the JS-facing now-playing metadata for the given rendered text lines.
|
|
1184
|
-
@MainActor
|
|
1185
|
-
private func makeNowPlayingMetadata(
|
|
1186
|
-
track: Track, title: String, artist: String?, album: String?,
|
|
1187
|
-
) -> NowPlayingMetadata {
|
|
1188
|
-
NowPlayingMetadata(
|
|
1189
|
-
elapsedTime: player?.currentTime,
|
|
1190
|
-
title: title,
|
|
1191
|
-
album: album,
|
|
1192
|
-
artist: artist,
|
|
1193
|
-
duration: track.duration,
|
|
1194
|
-
artwork: nowPlayingArtwork(for: track),
|
|
1195
|
-
description: track.description,
|
|
1196
|
-
mediaId: track.src ?? track.url,
|
|
1197
|
-
genre: track.genre,
|
|
1198
|
-
)
|
|
1199
|
-
}
|
|
1200
|
-
|
|
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
|
-
)
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// MARK: - Network
|
|
1223
|
-
|
|
1224
|
-
public func getOnline() throws -> Bool {
|
|
1225
|
-
networkMonitor.getOnline()
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
// MARK: - System Volume
|
|
1229
|
-
|
|
1230
|
-
/// Sets up KVO observer for system volume changes
|
|
1231
|
-
private func setupVolumeObserver() {
|
|
1232
|
-
// Remove existing observation if any
|
|
1233
|
-
volumeObservation?.invalidate()
|
|
1234
|
-
|
|
1235
|
-
let audioSession = AVAudioSession.sharedInstance()
|
|
1236
|
-
volumeObservation = audioSession.observe(\.outputVolume, options: [.new]) { [weak self] _, change in
|
|
1237
|
-
guard let newVolume = change.newValue else { return }
|
|
1238
|
-
self?.onSystemVolumeChanged(Double(newVolume))
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
public func getSystemVolume() throws -> Double {
|
|
1243
|
-
Double(AVAudioSession.sharedInstance().outputVolume)
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
public func setSystemVolume(volume _: Double) throws {
|
|
1247
|
-
// iOS doesn't provide a public API to set system volume programmatically.
|
|
1248
|
-
// Users must adjust volume via hardware buttons or Control Center.
|
|
1249
|
-
logger.debug("setSystemVolume is not supported on iOS - volume must be adjusted via hardware buttons or Control Center")
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
// MARK: - External Audio Output
|
|
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
|
-
|
|
1311
|
-
/// Sets up observer for audio route changes
|
|
1312
|
-
private func setupRouteChangeObserver() {
|
|
1313
|
-
if let observer = routeChangeObserver {
|
|
1314
|
-
NotificationCenter.default.removeObserver(observer)
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
routeChangeObserver = NotificationCenter.default.addObserver(
|
|
1318
|
-
forName: AVAudioSession.routeChangeNotification,
|
|
1319
|
-
object: nil,
|
|
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,
|
|
1353
|
-
) { [weak self] _ in
|
|
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() }
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
/// Gets the current audio output info
|
|
1363
|
-
private func getCurrentOutput() -> Output? {
|
|
1364
|
-
let session = AVAudioSession.sharedInstance()
|
|
1365
|
-
guard let output = session.currentRoute.outputs.first else { return nil }
|
|
1366
|
-
|
|
1367
|
-
let (outputType, external): (OutputType, Bool) = switch output.portType {
|
|
1368
|
-
case .builtInSpeaker:
|
|
1369
|
-
(.speaker, false)
|
|
1370
|
-
case .builtInReceiver:
|
|
1371
|
-
(.receiver, false)
|
|
1372
|
-
case .airPlay:
|
|
1373
|
-
(.airplay, true)
|
|
1374
|
-
case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
|
|
1375
|
-
(.bluetooth, true)
|
|
1376
|
-
case .headphones:
|
|
1377
|
-
(.headphones, true)
|
|
1378
|
-
case .carAudio:
|
|
1379
|
-
(.car, true)
|
|
1380
|
-
case .HDMI:
|
|
1381
|
-
(.hdmi, true)
|
|
1382
|
-
case .usbAudio:
|
|
1383
|
-
(.usb, true)
|
|
1384
|
-
default:
|
|
1385
|
-
(.other, true)
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
return Output(type: outputType, name: output.portName, external: external)
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
public func getOutput() throws -> Output? {
|
|
1392
|
-
getCurrentOutput()
|
|
1393
|
-
}
|
|
1394
|
-
|
|
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 {
|
|
1401
|
-
DispatchQueue.main.async { [weak self] in
|
|
1402
|
-
guard let self else { return }
|
|
1403
|
-
guard let windowScene = UIApplication.shared.connectedScenes
|
|
1404
|
-
.compactMap({ $0 as? UIWindowScene })
|
|
1405
|
-
.first(where: { $0.activationState == .foregroundActive }),
|
|
1406
|
-
let window = windowScene.windows.first(where: { $0.isKeyWindow }),
|
|
1407
|
-
var topController = window.rootViewController
|
|
1408
|
-
else {
|
|
1409
|
-
self.logger.error("openOutputPicker: no active window scene / root view controller")
|
|
1410
|
-
return
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
// Walk up to the topmost presented view controller
|
|
1414
|
-
while let presented = topController.presentedViewController {
|
|
1415
|
-
topController = presented
|
|
1416
|
-
}
|
|
1417
|
-
|
|
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
|
|
1427
|
-
topController.view.addSubview(routePicker)
|
|
1428
|
-
|
|
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")
|
|
1438
|
-
}
|
|
1439
|
-
|
|
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) {
|
|
1443
|
-
routePicker.removeFromSuperview()
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
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
|
-
|
|
1464
|
-
// MARK: - Equalizer (unsupported on iOS)
|
|
1465
|
-
|
|
1466
|
-
public func getEqualizerSettings() throws -> EqualizerSettings? {
|
|
1467
|
-
// No-op: equalizer unsupported on iOS
|
|
1468
|
-
nil
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
public func setEqualizerEnabled(enabled _: Bool) throws {
|
|
1472
|
-
// No-op: equalizer unsupported on iOS
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
public func setEqualizerPreset(preset _: String) throws {
|
|
1476
|
-
// No-op: equalizer unsupported on iOS
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
public func setEqualizerLevels(levels _: [Double]) throws {
|
|
1480
|
-
// No-op: equalizer unsupported on iOS
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
// MARK: - Battery (Android-only, stub implementations)
|
|
1484
|
-
|
|
1485
|
-
public func getBatteryWarningPending() throws -> Bool {
|
|
1486
|
-
false
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
public func getBatteryOptimizationStatus() throws -> BatteryOptimizationStatus {
|
|
1490
|
-
// iOS doesn't have battery optimization restrictions like Android
|
|
1491
|
-
.unrestricted
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
public func dismissBatteryWarning() throws {
|
|
1495
|
-
// No-op on iOS
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
public func openBatterySettings() throws {
|
|
1499
|
-
// No-op on iOS
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
// MARK: - TrackPlayerCallbacks
|
|
1504
|
-
|
|
1505
|
-
extension HybridAudioBrowser: TrackPlayerCallbacks {
|
|
1506
|
-
public func playerDidChangePlayback(_ playback: Playback) {
|
|
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()
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
public func playerDidChangeActiveTrack(_ event: PlaybackActiveTrackChangedEvent) {
|
|
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)
|
|
1516
|
-
nowPlayingOverride = nil
|
|
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)
|
|
1530
|
-
// Also notify now playing changed when track changes
|
|
1531
|
-
applyNowPlayingMetadata()
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
public func playerDidUpdateProgress(_ event: PlaybackProgressUpdatedEvent) {
|
|
1535
|
-
onPlaybackProgressUpdated(event)
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
public func playerDidFirePlaybackInterval() {
|
|
1539
|
-
onPlaybackInterval()
|
|
1540
|
-
}
|
|
1541
|
-
|
|
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() }
|
|
1545
|
-
onPlaybackPlayWhenReadyChanged(PlaybackPlayWhenReadyChangedEvent(playWhenReady: playWhenReady))
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
public func playerDidChangePlayingState(_ state: PlayingState) {
|
|
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
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
public func playerDidEndQueue(_ event: PlaybackQueueEndedEvent) {
|
|
1595
|
-
onPlaybackQueueEnded(event)
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
public func playerDidChangeQueue(_ tracks: [Track]) {
|
|
1599
|
-
queueChangedEmitter.emit(tracks)
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
public func playerDidChangeRepeatMode(_ event: RepeatModeChangedEvent) {
|
|
1603
|
-
repeatModeChangedEmitter.emit(event)
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
public func playerDidChangeShuffleEnabled(_ enabled: Bool) {
|
|
1607
|
-
shuffleChangedEmitter.emit(enabled)
|
|
1608
|
-
onPlaybackShuffleModeChanged(enabled)
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
public func playerDidError(_ event: PlaybackErrorEvent) {
|
|
1612
|
-
onPlaybackError(event)
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
public func playerDidReceiveCommonMetadata(_ items: [AVMetadataItem]) {
|
|
1616
|
-
let metadata = TrackMetadata.from(items: items)
|
|
1617
|
-
onTrackMetadata(metadata)
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
public func playerDidReceiveTimedMetadata(_ groups: [AVTimedMetadataGroup]) {
|
|
1621
|
-
for group in groups {
|
|
1622
|
-
if let metadata = TimedMetadata.from(items: group.items) {
|
|
1623
|
-
latestTimedMetadata = metadata
|
|
1624
|
-
onTimedMetadata(metadata)
|
|
1625
|
-
// Re-render the now-playing so the formatter can reflect the live song.
|
|
1626
|
-
applyNowPlayingMetadata()
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
public func playerDidReceiveChapterMetadata(_ groups: [AVTimedMetadataGroup]) {
|
|
1632
|
-
let chapters = ChapterMetadata.from(groups: groups)
|
|
1633
|
-
onChapterMetadata(chapters)
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
public func playerDidCompleteSeek(position _: Double, didFinish _: Bool) {
|
|
1637
|
-
// Not exposed to JS currently
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
public func playerDidUpdateDuration(_: Double) {
|
|
1641
|
-
// Not exposed to JS currently
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
public func remotePlay() {
|
|
1645
|
-
if let handler = handleRemotePlay {
|
|
1646
|
-
handler()
|
|
1647
|
-
} else {
|
|
1648
|
-
// Default behavior: play the player
|
|
1649
|
-
onMainActor { player?.play() }
|
|
1650
|
-
}
|
|
1651
|
-
onRemotePlay()
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
public func remotePause() {
|
|
1655
|
-
if let handler = handleRemotePause {
|
|
1656
|
-
handler()
|
|
1657
|
-
} else {
|
|
1658
|
-
// Default behavior: pause the player
|
|
1659
|
-
onMainActor { player?.pause() }
|
|
1660
|
-
}
|
|
1661
|
-
onRemotePause()
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
public func remoteStop() {
|
|
1665
|
-
if let handler = handleRemoteStop {
|
|
1666
|
-
handler()
|
|
1667
|
-
} else {
|
|
1668
|
-
// Default behavior: stop the player
|
|
1669
|
-
onMainActor { player?.stop() }
|
|
1670
|
-
}
|
|
1671
|
-
onRemoteStop()
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
public func remotePlayPause() {
|
|
1675
|
-
// Toggle based on current state
|
|
1676
|
-
let isPlaying = onMainActor { player?.playWhenReady == true }
|
|
1677
|
-
if isPlaying {
|
|
1678
|
-
remotePause()
|
|
1679
|
-
} else {
|
|
1680
|
-
remotePlay()
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
public func remoteNext() {
|
|
1685
|
-
if let handler = handleRemoteNext {
|
|
1686
|
-
handler()
|
|
1687
|
-
} else {
|
|
1688
|
-
// Default behavior: skip to next track
|
|
1689
|
-
onMainActor { player?.next() }
|
|
1690
|
-
}
|
|
1691
|
-
onRemoteNext()
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
public func remotePrevious() {
|
|
1695
|
-
if let handler = handleRemotePrevious {
|
|
1696
|
-
handler()
|
|
1697
|
-
} else {
|
|
1698
|
-
// Default behavior: skip to previous track
|
|
1699
|
-
onMainActor { player?.previous() }
|
|
1700
|
-
}
|
|
1701
|
-
onRemotePrevious()
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
public func remoteJumpForward(interval: Double) {
|
|
1705
|
-
let event = RemoteJumpForwardEvent(interval: interval)
|
|
1706
|
-
if let handler = handleRemoteJumpForward {
|
|
1707
|
-
handler(event)
|
|
1708
|
-
} else {
|
|
1709
|
-
// Default behavior: seek forward by interval
|
|
1710
|
-
onMainActor { player?.seekBy(interval) }
|
|
1711
|
-
}
|
|
1712
|
-
onRemoteJumpForward(event)
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
public func remoteJumpBackward(interval: Double) {
|
|
1716
|
-
let event = RemoteJumpBackwardEvent(interval: interval)
|
|
1717
|
-
if let handler = handleRemoteJumpBackward {
|
|
1718
|
-
handler(event)
|
|
1719
|
-
} else {
|
|
1720
|
-
// Default behavior: seek backward by interval
|
|
1721
|
-
onMainActor { player?.seekBy(-interval) }
|
|
1722
|
-
}
|
|
1723
|
-
onRemoteJumpBackward(event)
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
public func remoteSeek(position: Double) {
|
|
1727
|
-
logger.info("remoteSeek called with position: \(position)")
|
|
1728
|
-
let event = RemoteSeekEvent(position: position)
|
|
1729
|
-
if let handler = handleRemoteSeek {
|
|
1730
|
-
handler(event)
|
|
1731
|
-
} else {
|
|
1732
|
-
// Default behavior: seek to position
|
|
1733
|
-
logger.info("remoteSeek: calling player.seekTo(\(position))")
|
|
1734
|
-
onMainActor { player?.seekTo(position) }
|
|
1735
|
-
}
|
|
1736
|
-
onRemoteSeek(event)
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
public func remoteChangePlaybackPosition(position: Double) {
|
|
1740
|
-
remoteSeek(position: position)
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
public func remotePlayId(id: String, index: Int?) {
|
|
1744
|
-
let event = RemotePlayIdEvent(id: id, index: index.map { Double($0) })
|
|
1745
|
-
if let handler = handleRemotePlayId {
|
|
1746
|
-
handler(event)
|
|
1747
|
-
} else {
|
|
1748
|
-
onRemotePlayId(event)
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
public func remotePlaySearch(query: String) {
|
|
1753
|
-
let event = RemotePlaySearchEvent(query: query)
|
|
1754
|
-
if let handler = handleRemotePlaySearch {
|
|
1755
|
-
handler(event)
|
|
1756
|
-
} else {
|
|
1757
|
-
onRemotePlaySearch(event)
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
|
|
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
|
-
|
|
1831
|
-
do {
|
|
1832
|
-
guard let tracks = try await browser.browserManager.searchPlayable(params) else {
|
|
1833
|
-
completion(false)
|
|
1834
|
-
return
|
|
1835
|
-
}
|
|
1836
|
-
player.setQueue(tracks, initialIndex: 0, playWhenReady: true)
|
|
1837
|
-
browser.showNowPlayingRequestedEmitter.emit(())
|
|
1838
|
-
completion(true)
|
|
1839
|
-
} catch {
|
|
1840
|
-
browser.logger.error("handlePlayMediaIntent failed: \(error.localizedDescription)")
|
|
1841
|
-
completion(false)
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
public func remoteChangeRepeatMode(mode: RepeatMode) {
|
|
1847
|
-
// Apply the repeat mode change from CarPlay/lock screen
|
|
1848
|
-
try? setRepeatMode(mode: mode)
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
public func remoteChangeShuffleMode(enabled: Bool) {
|
|
1852
|
-
// Apply the shuffle mode change from CarPlay/lock screen
|
|
1853
|
-
try? setShuffleEnabled(enabled: enabled)
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
public func remoteChangePlaybackRate(rate: Float) {
|
|
1857
|
-
// Apply the playback rate change from CarPlay/lock screen
|
|
1858
|
-
onMainActor { player?.rate = rate }
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
func playerDidChangeOptions(_: PlayerUpdateOptions) {
|
|
1862
|
-
// TODO: Convert to Options type
|
|
1863
|
-
}
|
|
1864
407
|
}
|
|
1865
408
|
|
|
1866
409
|
// MARK: - Autolinking Alias
|