react-native-audio-browser 0.2.0-next.11 → 0.2.0-next.13
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 +14 -4
- package/android/build.gradle +20 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +115 -26
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +262 -188
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +23 -3
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +95 -58
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +68 -26
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +19 -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/model/PlayerUpdateOptions.kt +1 -1
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +120 -67
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -24
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +109 -74
- package/android/src/main/java/com/audiobrowser/player/Player.kt +51 -30
- package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +44 -40
- package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +108 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +52 -53
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +74 -41
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +229 -50
- package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +1 -1
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -53
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -0
- package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
- package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +1 -2
- package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
- package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
- package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +161 -0
- package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
- package/ios/Browser/BrowserConfig.swift +102 -5
- package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
- package/ios/Browser/BrowserManager.swift +229 -180
- package/ios/Browser/BrowserPathHelper.swift +44 -44
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +258 -106
- package/ios/Browser/SearchDrillIn.swift +6 -6
- package/ios/Browser/SectionPresentation.swift +64 -0
- package/ios/Browser/SectionScope.swift +58 -24
- package/ios/Browser/StyleResolver.swift +58 -0
- package/ios/Browser/TabBarEntries.swift +9 -3
- package/ios/Browser/TrackIdTemplate.swift +37 -0
- package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
- package/ios/CarPlay/CarPlayController.swift +227 -85
- package/ios/CarPlay/CarPlayImageLoader.swift +38 -18
- package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
- package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
- package/ios/Extension/Capability+RemoteCommand.swift +1 -1
- package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
- package/ios/Extension/ResolvedTrack+Copying.swift +11 -15
- package/ios/Extension/Section+Copying.swift +31 -0
- package/ios/Extension/Track+Copying.swift +6 -12
- package/ios/Extension/Track+Identity.swift +18 -0
- package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +166 -45
- package/ios/Model/NitroTypeStubs.swift +106 -21
- package/ios/Model/PlayerUpdateOptions.swift +7 -0
- package/ios/Player/MediaIntentCriteria.swift +13 -5
- package/ios/Player/MediaLoader.swift +33 -9
- package/ios/Player/NowPlayingUpdater.swift +54 -6
- package/ios/Player/PlaybackCoordinator.swift +13 -6
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/QueueManager.swift +36 -10
- package/ios/Player/RetryManager.swift +4 -3
- package/ios/Player/ShuffleOrder.swift +23 -0
- package/ios/Player/SleepTimerHandling.swift +13 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
- package/ios/Tests/BrowserPathHelperTests.swift +50 -20
- package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
- package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
- package/ios/Tests/JsonModelsTests.swift +150 -1
- package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
- package/ios/Tests/QueueManagerTests.swift +72 -6
- package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
- package/ios/Tests/ResolvedTrackCopyingTests.swift +37 -35
- package/ios/Tests/SearchDrillInTests.swift +16 -16
- package/ios/Tests/SectionPresentationTests.swift +90 -0
- package/ios/Tests/SectionScopeTests.swift +87 -55
- package/ios/Tests/StyleResolverTests.swift +127 -0
- package/ios/Tests/Support/TestTypes.swift +7 -0
- package/ios/Tests/TabBarEntriesTests.swift +18 -18
- package/ios/Tests/TrackIdTemplateTests.swift +67 -0
- package/ios/Tests/TrackIdentityTests.swift +20 -0
- package/ios/Tests/TrackSelectorTests.swift +168 -33
- package/ios/TrackPlayer.swift +57 -34
- package/ios/TrackSelector.swift +45 -30
- package/ios/Util/ArtworkImageFetcher.swift +16 -1
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/SFSymbolRenderer.swift +24 -2
- package/ios/Util/SVGProcessor.swift +9 -2
- package/lib/commonjs/AudioBrowser.js +11 -0
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/features/browser-config.js +11 -8
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/browser.js +2 -2
- package/lib/commonjs/features/favorites.js +13 -10
- package/lib/commonjs/features/favorites.js.map +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/progress.js +2 -1
- 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 +2 -3
- package/lib/commonjs/features/playback/state.js.map +1 -1
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +3 -1
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +2 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -1
- package/lib/commonjs/features/queue/activeTrack.js +14 -3
- package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
- package/lib/commonjs/features/queue/queue.js +11 -4
- package/lib/commonjs/features/queue/queue.js.map +1 -1
- package/lib/commonjs/utils/LazyNativeEmitter.js +21 -10
- 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 +32 -24
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +144 -87
- 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/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/player/NowPlayingManager.js +4 -3
- package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
- package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
- package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/module/AudioBrowser.js +1 -0
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/features/browser-config.js +11 -8
- package/lib/module/features/browser-config.js.map +1 -1
- package/lib/module/features/browser.js +2 -2
- package/lib/module/features/favorites.js +13 -10
- package/lib/module/features/favorites.js.map +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/progress.js +2 -1
- 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 +2 -3
- package/lib/module/features/playback/state.js.map +1 -1
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +3 -1
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +2 -0
- package/lib/module/features/player/validateOptions.js.map +1 -1
- package/lib/module/features/queue/activeTrack.js +14 -3
- package/lib/module/features/queue/activeTrack.js.map +1 -1
- package/lib/module/features/queue/queue.js +11 -4
- package/lib/module/features/queue/queue.js.map +1 -1
- package/lib/module/utils/LazyNativeEmitter.js +21 -10
- 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 +32 -24
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +144 -87
- 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/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/player/NowPlayingManager.js +4 -3
- package/lib/module/web/player/NowPlayingManager.js.map +1 -1
- package/lib/module/web/util/BrowserPathHelper.js +56 -11
- package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts +1 -0
- 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 +2 -2
- package/lib/typescript/src/features/favorites.d.ts +13 -10
- package/lib/typescript/src/features/favorites.d.ts.map +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/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 +2 -3
- 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/player/options.d.ts +21 -19
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +1 -8
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts +8 -1
- package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +5 -2
- package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-native.d.ts +2 -2
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +361 -126
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +33 -53
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +16 -17
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
- package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
- package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
- package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
- package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +0 -1
- package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
- package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +16 -4
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +2 -5
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
- package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +33 -14
- package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +22 -10
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +10 -7
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JOptions.hpp +2 -5
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +55 -51
- package/nitrogen/generated/android/c++/JSection.hpp +120 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
- package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
- package/nitrogen/generated/android/c++/JTrack.hpp +23 -48
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -18
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +18 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -15
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +158 -82
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +25 -10
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +25 -10
- package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
- package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
- 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 +5 -5
- package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
- package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
- package/nitrogen/generated/ios/swift/ResolvedTrack.swift +54 -83
- package/nitrogen/generated/ios/swift/Section.swift +100 -0
- package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
- package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
- package/nitrogen/generated/ios/swift/Track.swift +20 -73
- package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
- package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
- package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
- package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
- package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +6 -6
- package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
- package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
- package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +30 -38
- package/nitrogen/generated/shared/c++/Section.hpp +106 -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 +15 -31
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
- package/package.json +7 -2
- package/src/AudioBrowser.ts +1 -0
- package/src/features/browser-config.test.ts +21 -6
- package/src/features/browser-config.ts +11 -8
- package/src/features/browser.test.ts +1 -1
- package/src/features/browser.ts +2 -2
- package/src/features/favorites.ts +13 -10
- package/src/features/hook-wiring.test.ts +63 -15
- package/src/features/playback/controls.ts +4 -2
- package/src/features/playback/progress.ts +2 -1
- package/src/features/playback/rate.ts +8 -1
- package/src/features/playback/state.ts +2 -3
- package/src/features/playback/trackPlaybackTime.ts +1 -1
- package/src/features/player/options.ts +27 -19
- package/src/features/player/setup.test.ts +12 -3
- package/src/features/player/setup.ts +3 -1
- package/src/features/player/validateOptions.ts +2 -0
- package/src/features/queue/activeTrack.ts +14 -3
- package/src/features/queue/queue.ts +11 -4
- package/src/nitro-stub-drift.test.ts +40 -0
- package/src/public-surface.test.ts +40 -0
- package/src/specs/audio-browser.nitro.ts +4 -3
- package/src/style-resolution.test.ts +210 -0
- package/src/types/browser-native.ts +2 -2
- package/src/types/browser-nodes.ts +377 -135
- package/src/types/browser.ts +34 -55
- package/src/utils/LazyNativeEmitter.ts +20 -10
- package/src/utils/NativeUpdatedValue.ts +29 -13
- package/src/utils/getTrackIdentity.ts +20 -0
- package/src/utils/useNativeValueRefreshedBy.ts +38 -0
- package/src/web/NativeAudioBrowser.ts +37 -22
- package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- package/src/web/browser/BrowserManager.ts +176 -95
- package/src/web/browser/FavoriteManager.test.ts +84 -0
- package/src/web/browser/FavoriteManager.ts +28 -23
- 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/player/NowPlayingManager.ts +4 -3
- package/src/web/util/BrowserPathHelper.test.ts +52 -0
- package/src/web/util/BrowserPathHelper.ts +61 -13
- package/website/guide/android-auto.md +114 -0
- package/website/guide/android-certificates.md +409 -0
- package/website/guide/artwork.md +255 -0
- package/website/guide/audio-output.md +135 -0
- package/website/guide/automotive.md +86 -0
- package/website/guide/basic-usage.md +151 -0
- package/website/guide/battery.md +115 -0
- package/website/guide/browser.md +581 -0
- package/website/guide/carplay.md +620 -0
- package/website/guide/configuration.md +173 -0
- package/website/guide/equalizer.md +231 -0
- package/website/guide/errors.md +310 -0
- package/website/guide/favorites.md +154 -0
- package/website/guide/gate.md +143 -0
- package/website/guide/getting-started.md +46 -0
- package/website/guide/hooks.md +238 -0
- package/website/guide/live-streams.md +125 -0
- package/website/guide/metadata.md +239 -0
- package/website/guide/migrating-from-track-player.md +244 -0
- package/website/guide/native-callback-fetch.md +127 -0
- package/website/guide/network.md +86 -0
- package/website/guide/now-playing.md +175 -0
- package/website/guide/playback.md +328 -0
- package/website/guide/queue.md +300 -0
- package/website/guide/remote-controls.md +256 -0
- package/website/guide/search.md +255 -0
- package/website/guide/sleep-timer.md +268 -0
- package/website/guide/track.md +246 -0
- package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
- package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
- package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
- 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/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
- 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/Variant_Bool_FavoriteConfig.swift +0 -18
- package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
- package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
|
@@ -13,14 +13,16 @@ export function assertBrowsePageShape(
|
|
|
13
13
|
if (Array.isArray(response)) {
|
|
14
14
|
throw new Error(
|
|
15
15
|
`Browse endpoint for "${path}" returned a JSON array; expected a ` +
|
|
16
|
-
`page object { title, children?: Track[] }.
|
|
17
|
-
|
|
16
|
+
`page object { title, sections?: Section[], children?: Track[] }. ` +
|
|
17
|
+
`Wrap the rows in a \`children\` array (one untitled section) or ` +
|
|
18
|
+
`group them into \`sections\`.`
|
|
18
19
|
)
|
|
19
20
|
}
|
|
20
21
|
if (response === null || typeof response !== 'object') {
|
|
21
22
|
throw new Error(
|
|
22
23
|
`Browse endpoint for "${path}" returned ${JSON.stringify(response)}; ` +
|
|
23
|
-
`expected a page object { title,
|
|
24
|
+
`expected a page object { title, sections?: Section[], ` +
|
|
25
|
+
`children?: Track[] }.`
|
|
24
26
|
)
|
|
25
27
|
}
|
|
26
28
|
return response as ResolvedTrack
|
|
@@ -5,10 +5,10 @@ import { classifyTrackNavigation } from './classifyTrackNavigation'
|
|
|
5
5
|
const track = (fields: Partial<Track>): Track => fields as Track
|
|
6
6
|
|
|
7
7
|
describe('classifyTrackNavigation', () => {
|
|
8
|
-
it('classifies a contextual
|
|
8
|
+
it('classifies a contextual path, exposing parent path and track id', () => {
|
|
9
9
|
expect(
|
|
10
10
|
classifyTrackNavigation(
|
|
11
|
-
track({
|
|
11
|
+
track({ path: '/library/radio?__trackId=song.mp3' })
|
|
12
12
|
)
|
|
13
13
|
).toEqual({
|
|
14
14
|
kind: 'contextual',
|
|
@@ -17,25 +17,27 @@ describe('classifyTrackNavigation', () => {
|
|
|
17
17
|
})
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
it('classifies a non-contextual
|
|
21
|
-
expect(classifyTrackNavigation(track({
|
|
20
|
+
it('classifies a non-contextual path as a browse navigation', () => {
|
|
21
|
+
expect(classifyTrackNavigation(track({ path: '/library/radio' }))).toEqual({
|
|
22
22
|
kind: 'browse'
|
|
23
23
|
})
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
it('classifies a track with src but no
|
|
26
|
+
it('classifies a track with src but no path as playable', () => {
|
|
27
27
|
expect(classifyTrackNavigation(track({ src: 'song.mp3' }))).toEqual({
|
|
28
28
|
kind: 'playable'
|
|
29
29
|
})
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
it('prefers browse over playable when a
|
|
32
|
+
it('prefers browse over playable when a path is present', () => {
|
|
33
33
|
expect(
|
|
34
|
-
classifyTrackNavigation(
|
|
34
|
+
classifyTrackNavigation(
|
|
35
|
+
track({ path: '/library/radio', src: 'song.mp3' })
|
|
36
|
+
)
|
|
35
37
|
).toEqual({ kind: 'browse' })
|
|
36
38
|
})
|
|
37
39
|
|
|
38
|
-
it('classifies a track with neither
|
|
40
|
+
it('classifies a track with neither path nor src as invalid', () => {
|
|
39
41
|
expect(classifyTrackNavigation(track({}))).toEqual({ kind: 'invalid' })
|
|
40
42
|
})
|
|
41
43
|
})
|
|
@@ -4,10 +4,10 @@ import { BrowserPathHelper } from '../util/BrowserPathHelper'
|
|
|
4
4
|
/**
|
|
5
5
|
* How a selected track should be navigated, decided purely from its shape:
|
|
6
6
|
* - `contextual` — a playable track carrying its parent queue context in a
|
|
7
|
-
* contextual
|
|
8
|
-
* - `browse` — a browsable track (has a `
|
|
9
|
-
* - `playable` — a bare playable track (`src`, no `
|
|
10
|
-
* - `invalid` — neither `
|
|
7
|
+
* contextual path (`{parentPath}?__trackId={src}`); expand/skip within that queue.
|
|
8
|
+
* - `browse` — a browsable track (has a `path` that isn't contextual); drill in.
|
|
9
|
+
* - `playable` — a bare playable track (`src`, no `path`); load and play it alone.
|
|
10
|
+
* - `invalid` — neither `path` nor `src`; nothing to do.
|
|
11
11
|
*/
|
|
12
12
|
export type TrackNavigation =
|
|
13
13
|
| { kind: 'contextual'; parentPath: string; trackId: string | undefined }
|
|
@@ -21,15 +21,15 @@ export type TrackNavigation =
|
|
|
21
21
|
* caller; this only decides which action applies.
|
|
22
22
|
*/
|
|
23
23
|
export function classifyTrackNavigation(track: Track): TrackNavigation {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
24
|
+
const path = track.path
|
|
25
|
+
if (path && BrowserPathHelper.isContextual(path)) {
|
|
26
26
|
return {
|
|
27
27
|
kind: 'contextual',
|
|
28
|
-
parentPath: BrowserPathHelper.stripTrackId(
|
|
29
|
-
trackId: BrowserPathHelper.extractTrackId(
|
|
28
|
+
parentPath: BrowserPathHelper.stripTrackId(path),
|
|
29
|
+
trackId: BrowserPathHelper.extractTrackId(path)
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
if (
|
|
32
|
+
if (path) return { kind: 'browse' }
|
|
33
33
|
if (track.src) return { kind: 'playable' }
|
|
34
34
|
return { kind: 'invalid' }
|
|
35
35
|
}
|
|
@@ -5,7 +5,11 @@ describe('parseSearchResponse', () => {
|
|
|
5
5
|
it('extracts children from a page-object response', () => {
|
|
6
6
|
const tracks = [{ src: '/a', title: 'A' }]
|
|
7
7
|
expect(
|
|
8
|
-
parseSearchResponse({
|
|
8
|
+
parseSearchResponse({
|
|
9
|
+
path: '/search',
|
|
10
|
+
title: 'Search',
|
|
11
|
+
children: tracks
|
|
12
|
+
})
|
|
9
13
|
).toEqual(tracks)
|
|
10
14
|
})
|
|
11
15
|
|
|
@@ -15,7 +19,9 @@ describe('parseSearchResponse', () => {
|
|
|
15
19
|
})
|
|
16
20
|
|
|
17
21
|
it('returns [] for a page object with no children', () => {
|
|
18
|
-
expect(parseSearchResponse({
|
|
22
|
+
expect(parseSearchResponse({ path: '/search', title: 'Search' })).toEqual(
|
|
23
|
+
[]
|
|
24
|
+
)
|
|
19
25
|
})
|
|
20
26
|
|
|
21
27
|
it('returns [] for null / non-object responses', () => {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import type { ResolvedTrack, Track } from '../../types'
|
|
3
|
+
import { flattenSections, normalizePage, sectionContaining } from './sections'
|
|
4
|
+
|
|
5
|
+
const track = (src: string): Track => ({ title: src, src })
|
|
6
|
+
|
|
7
|
+
describe('normalizePage', () => {
|
|
8
|
+
it('wraps plain children into one untitled section', () => {
|
|
9
|
+
const page: ResolvedTrack = {
|
|
10
|
+
path: '/p',
|
|
11
|
+
title: 'P',
|
|
12
|
+
children: [track('a'), track('b')]
|
|
13
|
+
}
|
|
14
|
+
const normalized = normalizePage(page)
|
|
15
|
+
expect(normalized.sections).toEqual([
|
|
16
|
+
{ children: [track('a'), track('b')] }
|
|
17
|
+
])
|
|
18
|
+
expect(normalized.children).toBeUndefined()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('prefers sections when present and strips children', () => {
|
|
22
|
+
const page: ResolvedTrack = {
|
|
23
|
+
path: '/p',
|
|
24
|
+
title: 'P',
|
|
25
|
+
sections: [{ title: 'S', children: [track('a')] }],
|
|
26
|
+
children: [track('zzz')]
|
|
27
|
+
}
|
|
28
|
+
const normalized = normalizePage(page)
|
|
29
|
+
expect(normalized.sections).toEqual([
|
|
30
|
+
{ title: 'S', children: [track('a')] }
|
|
31
|
+
])
|
|
32
|
+
expect(normalized.children).toBeUndefined()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('leaves a childless page without sections', () => {
|
|
36
|
+
expect(normalizePage({ path: '/p', title: 'P' }).sections).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('flattenSections', () => {
|
|
41
|
+
it('concatenates children in section order', () => {
|
|
42
|
+
const flat = flattenSections([
|
|
43
|
+
{ title: 'A', children: [track('a1'), track('a2')] },
|
|
44
|
+
{ children: [track('b1')] }
|
|
45
|
+
])
|
|
46
|
+
expect(flat.map((t) => t.src)).toEqual(['a1', 'a2', 'b1'])
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('sectionContaining', () => {
|
|
51
|
+
const sections = [
|
|
52
|
+
{ title: 'First', children: [track('dup'), track('x')] },
|
|
53
|
+
{ title: 'Second', children: [track('y'), track('dup'), track('z')] }
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
it('falls back to the first section containing the identity', () => {
|
|
57
|
+
const scoped = sectionContaining(sections, 'dup', undefined)
|
|
58
|
+
expect(scoped?.tracks.map((t) => t.src)).toEqual(['dup', 'x'])
|
|
59
|
+
expect(scoped?.tappedOffset).toBeUndefined()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('pins the tapped section via the flat index', () => {
|
|
63
|
+
// Flat index 3 = second section, offset 1.
|
|
64
|
+
const scoped = sectionContaining(sections, 'dup', 3)
|
|
65
|
+
expect(scoped?.tracks.map((t) => t.src)).toEqual(['y', 'dup', 'z'])
|
|
66
|
+
expect(scoped?.tappedOffset).toBe(1)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('ignores a stale index whose child no longer matches', () => {
|
|
70
|
+
const scoped = sectionContaining(sections, 'dup', 1)
|
|
71
|
+
expect(scoped?.tracks.map((t) => t.src)).toEqual(['dup', 'x'])
|
|
72
|
+
expect(scoped?.tappedOffset).toBeUndefined()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('ignores an out-of-range index', () => {
|
|
76
|
+
const scoped = sectionContaining(sections, 'dup', 99)
|
|
77
|
+
expect(scoped?.tracks.map((t) => t.src)).toEqual(['dup', 'x'])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('returns undefined for a vanished identity', () => {
|
|
81
|
+
expect(sectionContaining(sections, 'gone', 0)).toBeUndefined()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('pins the exact copy of a within-section duplicate', () => {
|
|
85
|
+
const playlist = [
|
|
86
|
+
{ children: [track('a'), track('b'), track('a'), track('c')] }
|
|
87
|
+
]
|
|
88
|
+
const scoped = sectionContaining(playlist, 'a', 2)
|
|
89
|
+
expect(scoped?.tappedOffset).toBe(2)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ResolvedTrack, Section, Track } from '../../types'
|
|
2
|
+
import { getTrackIdentity } from '../../utils/getTrackIdentity'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Normalizes a resolved page to the canonical sectioned shape (ADR 0010):
|
|
6
|
+
* `sections` wins when present; plain `children` is authoring sugar for one
|
|
7
|
+
* untitled section. The output never carries `children` — there is exactly
|
|
8
|
+
* one structure downstream code hydrates, transforms, and scopes against.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizePage(content: ResolvedTrack): ResolvedTrack {
|
|
11
|
+
const { children, sections, ...rest } = content
|
|
12
|
+
const normalized =
|
|
13
|
+
sections ?? (children !== undefined ? [{ children }] : undefined)
|
|
14
|
+
return { ...rest, sections: normalized }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The page's children concatenated in section order — the flattening that
|
|
19
|
+
* defines the contextual `__index` positions (ADR 0009/0010) and the flat
|
|
20
|
+
* views (tabs, search) of a sectioned page.
|
|
21
|
+
*/
|
|
22
|
+
export function flattenSections(sections: Section[] | undefined): Track[] {
|
|
23
|
+
return sections?.flatMap((section) => section.children) ?? []
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The page's tracks as one flat list, normalizing first — the flat view
|
|
28
|
+
* (tabs, search) of a page in either authored shape. Mirrors the native
|
|
29
|
+
* `flattenedChildren` accessors.
|
|
30
|
+
*/
|
|
31
|
+
export function flattenedChildren(page: ResolvedTrack): Track[] {
|
|
32
|
+
return flattenSections(normalizePage(page).sections)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The section of the page containing the playable `trackId` (a track
|
|
37
|
+
* identity: id when non-blank, else src), or undefined when not found.
|
|
38
|
+
*
|
|
39
|
+
* `tappedIndex` — the flat page position stamped into the contextual URL —
|
|
40
|
+
* is a tie-breaker, never an identifier: when the child at that position
|
|
41
|
+
* still carries the tapped identity, it pins which section (and which copy,
|
|
42
|
+
* via `tappedOffset`) was tapped; when it doesn't (the list shifted),
|
|
43
|
+
* resolution falls back to the first section containing the identity. A
|
|
44
|
+
* stale index can therefore never select a different track — at worst a
|
|
45
|
+
* different copy of the same one.
|
|
46
|
+
*/
|
|
47
|
+
export function sectionContaining(
|
|
48
|
+
sections: Section[],
|
|
49
|
+
trackId: string,
|
|
50
|
+
tappedIndex: number | undefined
|
|
51
|
+
): { tracks: Track[]; tappedOffset?: number } | undefined {
|
|
52
|
+
if (tappedIndex !== undefined && tappedIndex >= 0) {
|
|
53
|
+
let start = 0
|
|
54
|
+
for (const section of sections) {
|
|
55
|
+
const offset = tappedIndex - start
|
|
56
|
+
if (offset < section.children.length) {
|
|
57
|
+
const tapped = section.children[offset]
|
|
58
|
+
if (tapped && getTrackIdentity(tapped) === trackId) {
|
|
59
|
+
return { tracks: section.children, tappedOffset: offset }
|
|
60
|
+
}
|
|
61
|
+
break
|
|
62
|
+
}
|
|
63
|
+
start += section.children.length
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const section of sections) {
|
|
67
|
+
if (section.children.some((track) => getTrackIdentity(track) === trackId)) {
|
|
68
|
+
return { tracks: section.children }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { NowPlayingUpdate, NowPlayingMetadata } from '../../features'
|
|
2
2
|
import type { Track } from '../../types'
|
|
3
3
|
import { artworkUrl } from '../../utils/artwork'
|
|
4
|
+
import { getTrackIdentity } from '../../utils/getTrackIdentity'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Manages now playing metadata overrides.
|
|
@@ -37,7 +38,7 @@ export class NowPlayingManager {
|
|
|
37
38
|
album: update.album ?? currentTrack.album,
|
|
38
39
|
artwork: artworkUrl(currentTrack.artwork),
|
|
39
40
|
description: currentTrack.description,
|
|
40
|
-
mediaId: currentTrack
|
|
41
|
+
mediaId: getTrackIdentity(currentTrack) ?? currentTrack.path,
|
|
41
42
|
genre: currentTrack.genre,
|
|
42
43
|
duration
|
|
43
44
|
}
|
|
@@ -47,7 +48,7 @@ export class NowPlayingManager {
|
|
|
47
48
|
album: currentTrack.album,
|
|
48
49
|
artwork: artworkUrl(currentTrack.artwork),
|
|
49
50
|
description: currentTrack.description,
|
|
50
|
-
mediaId: currentTrack
|
|
51
|
+
mediaId: getTrackIdentity(currentTrack) ?? currentTrack.path,
|
|
51
52
|
genre: currentTrack.genre,
|
|
52
53
|
duration
|
|
53
54
|
}
|
|
@@ -74,7 +75,7 @@ export class NowPlayingManager {
|
|
|
74
75
|
album: this.override?.album ?? currentTrack.album,
|
|
75
76
|
artwork: artworkUrl(currentTrack.artwork),
|
|
76
77
|
description: currentTrack.description,
|
|
77
|
-
mediaId: currentTrack
|
|
78
|
+
mediaId: getTrackIdentity(currentTrack) ?? currentTrack.path,
|
|
78
79
|
genre: currentTrack.genre,
|
|
79
80
|
duration
|
|
80
81
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { BrowserPathHelper } from './BrowserPathHelper'
|
|
3
|
+
|
|
4
|
+
describe('BrowserPathHelper contextual URL round-trip', () => {
|
|
5
|
+
it('round-trips a src carrying its own query params', () => {
|
|
6
|
+
// A src carrying its own query string (signed CDN URL) must survive the
|
|
7
|
+
// build → extract/strip round-trip: an unescaped `&` would split the src
|
|
8
|
+
// into stray query params, truncating the trackId and polluting the
|
|
9
|
+
// parent path.
|
|
10
|
+
const src = 'https://cdn.example.com/stream.mp3?token=abc&exp=1699999999'
|
|
11
|
+
const url = BrowserPathHelper.build('/library', src)
|
|
12
|
+
expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
|
|
13
|
+
expect(BrowserPathHelper.stripTrackId(url)).toBe('/library')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('round-trips a src with equals and plus', () => {
|
|
17
|
+
const src = 'https://cdn.example.com/a+b.mp3?sig=x=y'
|
|
18
|
+
const url = BrowserPathHelper.build('/library', src)
|
|
19
|
+
expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
|
|
20
|
+
expect(BrowserPathHelper.stripTrackId(url)).toBe('/library')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('preserves unrelated parent query params when stripping', () => {
|
|
24
|
+
const src = 'https://cdn.example.com/stream.mp3?token=abc&exp=1'
|
|
25
|
+
const url = BrowserPathHelper.build('/search?q=jazz', src)
|
|
26
|
+
expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
|
|
27
|
+
expect(BrowserPathHelper.stripTrackId(url)).toBe('/search?q=jazz')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('round-trips the stamped index alongside the trackId', () => {
|
|
31
|
+
const src = 'https://cdn.example.com/stream.mp3?token=abc&exp=1'
|
|
32
|
+
const url = BrowserPathHelper.build('/library', src, 3)
|
|
33
|
+
expect(BrowserPathHelper.extractTrackId(url)).toBe(src)
|
|
34
|
+
expect(BrowserPathHelper.extractIndex(url)).toBe(3)
|
|
35
|
+
expect(BrowserPathHelper.stripTrackId(url)).toBe('/library')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('extracts no index from an index-less contextual URL', () => {
|
|
39
|
+
const url = BrowserPathHelper.build('/library', 'song.mp3')
|
|
40
|
+
expect(BrowserPathHelper.extractIndex(url)).toBeUndefined()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('extracts no index from a non-contextual or malformed-index URL', () => {
|
|
44
|
+
expect(BrowserPathHelper.extractIndex('/library')).toBeUndefined()
|
|
45
|
+
expect(
|
|
46
|
+
BrowserPathHelper.extractIndex('/library?__trackId=song.mp3&__index=x')
|
|
47
|
+
).toBeUndefined()
|
|
48
|
+
expect(
|
|
49
|
+
BrowserPathHelper.extractIndex('/library?__trackId=song.mp3&__index=-1')
|
|
50
|
+
).toBeUndefined()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
* 1. System paths (prefixed with `/__`): Root, recent, and search paths
|
|
7
7
|
* 2. Contextual URLs: Embed parent context in track identifiers for Media3 integration
|
|
8
8
|
*
|
|
9
|
-
* Contextual URL format: `{parentPath}?__trackId={
|
|
10
|
-
* Example: "/library/radio?__trackId=song.mp3"
|
|
9
|
+
* Contextual URL format: `{parentPath}?__trackId={trackIdentity}&__index={childIndex}`
|
|
10
|
+
* Example: "/library/radio?__trackId=song.mp3&__index=2"
|
|
11
|
+
*
|
|
12
|
+
* `__trackId` is the identity check; `__index` (the child's position on the
|
|
13
|
+
* page at stamp time) is only a tie-breaker between surfaces that carry the
|
|
14
|
+
* same identity — a stale index never selects a different track.
|
|
11
15
|
*
|
|
12
16
|
* This allows:
|
|
13
17
|
* - Media3 to reference playable-only tracks (tracks with `src` but no `url`)
|
|
@@ -21,6 +25,9 @@ export const BrowserPathHelper = {
|
|
|
21
25
|
/** Query parameter name for contextual track identifiers */
|
|
22
26
|
CONTEXTUAL_TRACK_PARAM: '__trackId',
|
|
23
27
|
|
|
28
|
+
/** Query parameter name for the tapped child's page position (tie-breaker) */
|
|
29
|
+
CONTEXTUAL_INDEX_PARAM: '__index',
|
|
30
|
+
|
|
24
31
|
/**
|
|
25
32
|
* Create a search path for a given query
|
|
26
33
|
*/
|
|
@@ -43,13 +50,13 @@ export const BrowserPathHelper = {
|
|
|
43
50
|
},
|
|
44
51
|
|
|
45
52
|
/**
|
|
46
|
-
* Strips the __trackId
|
|
47
|
-
* If the URL is not contextual, returns it unchanged.
|
|
53
|
+
* Strips the contextual parameters (__trackId and __index) from a contextual
|
|
54
|
+
* URL to get the parent path. If the URL is not contextual, returns it unchanged.
|
|
48
55
|
*
|
|
49
56
|
* @param url The URL to process
|
|
50
|
-
* @returns The URL without the
|
|
57
|
+
* @returns The URL without the contextual parameters
|
|
51
58
|
*
|
|
52
|
-
* Example: "/library/radio?__trackId=song.mp3" → "/library/radio"
|
|
59
|
+
* Example: "/library/radio?__trackId=song.mp3&__index=2" → "/library/radio"
|
|
53
60
|
* Example: "/search?q=jazz&__trackId=song.mp3" → "/search?q=jazz"
|
|
54
61
|
*/
|
|
55
62
|
stripTrackId(url: string): string {
|
|
@@ -62,8 +69,9 @@ export const BrowserPathHelper = {
|
|
|
62
69
|
const isFullUrl = url.startsWith('http://') || url.startsWith('https://')
|
|
63
70
|
const urlObj = new URL(url, isFullUrl ? undefined : 'http://placeholder')
|
|
64
71
|
|
|
65
|
-
// Remove the
|
|
72
|
+
// Remove the contextual parameters
|
|
66
73
|
urlObj.searchParams.delete(this.CONTEXTUAL_TRACK_PARAM)
|
|
74
|
+
urlObj.searchParams.delete(this.CONTEXTUAL_INDEX_PARAM)
|
|
67
75
|
|
|
68
76
|
// Return the appropriate format
|
|
69
77
|
if (isFullUrl) {
|
|
@@ -75,7 +83,7 @@ export const BrowserPathHelper = {
|
|
|
75
83
|
} catch {
|
|
76
84
|
// Fallback: simple string manipulation
|
|
77
85
|
const paramPattern = new RegExp(
|
|
78
|
-
`[?&]
|
|
86
|
+
`[?&](?:${this.CONTEXTUAL_TRACK_PARAM}|${this.CONTEXTUAL_INDEX_PARAM})=[^&]*`,
|
|
79
87
|
'g'
|
|
80
88
|
)
|
|
81
89
|
let result = url.replace(paramPattern, '')
|
|
@@ -87,6 +95,24 @@ export const BrowserPathHelper = {
|
|
|
87
95
|
}
|
|
88
96
|
},
|
|
89
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Builds a contextual URL by appending a track identifier — and optionally
|
|
100
|
+
* the tapped child's page position — to a parent path.
|
|
101
|
+
*
|
|
102
|
+
* @param parentPath The parent container path
|
|
103
|
+
* @param trackId The track identity (`id` when non-blank, else `src`)
|
|
104
|
+
* @param index The child's position on the page at stamp time (tie-breaker)
|
|
105
|
+
* @returns A contextual URL combining parent path, track ID, and index
|
|
106
|
+
*
|
|
107
|
+
* Example: build("/library", "song.mp3", 2) → "/library?__trackId=song.mp3&__index=2"
|
|
108
|
+
*/
|
|
109
|
+
build(parentPath: string, trackId: string, index?: number): string {
|
|
110
|
+
const separator = parentPath.includes('?') ? '&' : '?'
|
|
111
|
+
const indexParam =
|
|
112
|
+
index === undefined ? '' : `&${this.CONTEXTUAL_INDEX_PARAM}=${index}`
|
|
113
|
+
return `${parentPath}${separator}${this.CONTEXTUAL_TRACK_PARAM}=${encodeURIComponent(trackId)}${indexParam}`
|
|
114
|
+
},
|
|
115
|
+
|
|
90
116
|
/**
|
|
91
117
|
* Extracts the track ID from a contextual URL.
|
|
92
118
|
* Returns undefined if the URL is not contextual or doesn't contain the track ID parameter.
|
|
@@ -100,18 +126,40 @@ export const BrowserPathHelper = {
|
|
|
100
126
|
if (!this.isContextual(path)) {
|
|
101
127
|
return undefined
|
|
102
128
|
}
|
|
129
|
+
return this.extractParam(path, this.CONTEXTUAL_TRACK_PARAM)
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extracts the stamped page index from a contextual URL, or undefined when
|
|
134
|
+
* the URL is not contextual or carries no (valid, non-negative) index.
|
|
135
|
+
*
|
|
136
|
+
* Example: "/library/radio?__trackId=song.mp3&__index=2" → 2
|
|
137
|
+
*/
|
|
138
|
+
extractIndex(path: string): number | undefined {
|
|
139
|
+
if (!this.isContextual(path)) {
|
|
140
|
+
return undefined
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const raw = this.extractParam(path, this.CONTEXTUAL_INDEX_PARAM)
|
|
144
|
+
if (raw === undefined || !/^\d+$/.test(raw)) {
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
return Number.parseInt(raw, 10)
|
|
148
|
+
},
|
|
103
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Extracts a single query parameter, handling both full URLs and paths,
|
|
152
|
+
* with a regex fallback for input the URL parser rejects.
|
|
153
|
+
*/
|
|
154
|
+
extractParam(path: string, param: string): string | undefined {
|
|
104
155
|
try {
|
|
105
|
-
// Parse the URL - handle both full URLs and paths
|
|
106
156
|
const isFullUrl =
|
|
107
157
|
path.startsWith('http://') || path.startsWith('https://')
|
|
108
158
|
const urlObj = new URL(path, isFullUrl ? undefined : 'http://placeholder')
|
|
109
|
-
return urlObj.searchParams.get(
|
|
159
|
+
return urlObj.searchParams.get(param) ?? undefined
|
|
110
160
|
} catch {
|
|
111
161
|
// Fallback: regex extraction
|
|
112
|
-
const match = path.match(
|
|
113
|
-
new RegExp(`[?&]${this.CONTEXTUAL_TRACK_PARAM}=([^&]*)`)
|
|
114
|
-
)
|
|
162
|
+
const match = path.match(new RegExp(`[?&]${param}=([^&]*)`))
|
|
115
163
|
return match?.[1] ? decodeURIComponent(match[1]) : undefined
|
|
116
164
|
}
|
|
117
165
|
},
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 'Put your browse tree and playback on the Android Auto car screen: the automotive app descriptor, the manifest entries the library merges in, and "Hey Google, play…" voice search.'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Android Auto
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Android Auto shows your [browse tree](/guide/basic-usage) on the car screen and plays through the same player as your app. Two systems drive it:
|
|
10
|
+
|
|
11
|
+
1. **Browse tree** — Android Auto reads your content from a `MediaBrowserService`, which mirrors your browser routes. The library ships this service.
|
|
12
|
+
2. **Voice search** — "Hey Google, play…" arrives as a `MEDIA_PLAY_FROM_SEARCH` intent, which the library routes to your `search` source.
|
|
13
|
+
|
|
14
|
+
Unlike CarPlay — which needs entitlements and scene delegates — Android Auto needs almost no app-side code. The library's own `AndroidManifest.xml` declares the media-browser service, the media session, the voice-search intent filter, and the foreground-service permissions, and those **merge into your app automatically**. You only have to declare your app as an automotive media app.
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
### 1. Declare the automotive app descriptor
|
|
19
|
+
|
|
20
|
+
Add this `<meta-data>` inside the `<application>` tag of your app's `AndroidManifest.xml`:
|
|
21
|
+
|
|
22
|
+
```xml
|
|
23
|
+
<meta-data
|
|
24
|
+
android:name="com.google.android.gms.car.application"
|
|
25
|
+
android:resource="@xml/automotive_app_desc" />
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. Add the descriptor resource
|
|
29
|
+
|
|
30
|
+
Create `android/app/src/main/res/xml/automotive_app_desc.xml`:
|
|
31
|
+
|
|
32
|
+
```xml
|
|
33
|
+
<automotiveApp>
|
|
34
|
+
<uses name="media"/>
|
|
35
|
+
</automotiveApp>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That's the whole setup. The `MediaBrowserService`, media session, `MEDIA_PLAY_FROM_SEARCH` intent filter, headless service for background playback, and `FOREGROUND_SERVICE` / `FOREGROUND_SERVICE_MEDIA_PLAYBACK` permissions all come from the library's merged manifest — you don't declare them yourself.
|
|
39
|
+
|
|
40
|
+
## Voice search
|
|
41
|
+
|
|
42
|
+
Google Assistant requests ("play some jazz", "play the radishes") arrive as a `MEDIA_PLAY_FROM_SEARCH` intent. The library normalizes it into the same structured `SearchParams` your in-app and Siri searches use and calls your `search` source — no extra manifest setup, since the library declares the intent filter.
|
|
43
|
+
|
|
44
|
+
Android sends a subset of what iOS does: the query plus music focuses (genre / artist / album), always with `reference: 'unknown'`. See [Search](/guide/search) for how to resolve these requests and the cross-platform differences.
|
|
45
|
+
|
|
46
|
+
## Browse display
|
|
47
|
+
|
|
48
|
+
Android Auto reads a page's layout from the **parent item** that opens it — before the page resolves. So the layout is declared in two places: on the parent item (a tab, or a row on another page) and on the page itself.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// The parent item — a tab, or a row on another page. Android Auto
|
|
52
|
+
// reads the layout from here, before the page resolves.
|
|
53
|
+
{
|
|
54
|
+
title: 'Stations',
|
|
55
|
+
path: '/browse/stations',
|
|
56
|
+
style: { display: 'grid' }, // layout of the page this opens ('list' is the default)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The page it opens — the ResolvedTrack served for '/browse/stations':
|
|
60
|
+
{
|
|
61
|
+
path: '/browse/stations',
|
|
62
|
+
title: 'Stations',
|
|
63
|
+
style: { display: 'grid' }, // the page-wide declaration
|
|
64
|
+
children: [ /* the tiles */ ]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **`style.display` on the parent item** — `'list'` (rows) or `'grid'` (tiles). Below the root this is the only layout signal Android Auto honors, and it is only sent when you declare it — leave it off and the page renders as a list. In dev mode the library warns when an all-grid page is opened from a parent item with no declared `display`. (A section's "view all" page needs no declaration of its own: it reuses the section's `style`.)
|
|
69
|
+
- **Sections** — declare `sections` on the page to group its tracks under headers; each [`Section`](/api/types/browser-nodes/#section)'s `title` renders as a header above its children, plus a "view all" link built from the section's `path` and `subtitle`.
|
|
70
|
+
|
|
71
|
+
::: warning AAOS lays out the whole page from the parent item's `display`
|
|
72
|
+
On AOSP-based Android Automotive units — including mainstream cars' OEM skins — the parent item's `display` is the **only** below-root layout signal honored: the whole page renders as list or grid, uniformly. Per-section `display` differences (a single-line grid above a list, say) render on CarPlay and projected Android Auto, but an AAOS unit shows the page in the single declared layout. There is no way to describe a mixed page there — pick the layout that suits its dominant section, or split the content across pages. `gridWrap: false` also has no single-line container there: the grid wraps, showing more, never less.
|
|
73
|
+
:::
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
sections: [
|
|
77
|
+
{
|
|
78
|
+
title: 'Live now',
|
|
79
|
+
children: [
|
|
80
|
+
{ title: 'Morning Show', src: '…' },
|
|
81
|
+
{ title: 'Afternoon Drive', src: '…' }
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{ title: 'Up next', children: [{ title: 'Late Night Jazz', src: '…' }] }
|
|
85
|
+
]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Now Playing buttons
|
|
89
|
+
|
|
90
|
+
The buttons on the Now Playing screen come from the same
|
|
91
|
+
[`remoteButtonLayout`](/guide/remote-controls#button-layout-android) that drives the
|
|
92
|
+
notification — there's no car-specific option. Two behaviors are particular to
|
|
93
|
+
the car, though:
|
|
94
|
+
|
|
95
|
+
- **The head unit has the last word.** Slots are a preference. A unit with a
|
|
96
|
+
spare position may promote the first `overflow` entry onto the main row, so
|
|
97
|
+
a button you placed in overflow can still appear beside play/pause.
|
|
98
|
+
- **Fewer buttons, cleaner row.** If you want just
|
|
99
|
+
`jump-back │ play/pause │ jump-forward`, leave the rest out of the layout
|
|
100
|
+
entirely — they keep working from the steering wheel and headset regardless.
|
|
101
|
+
|
|
102
|
+
## Testing
|
|
103
|
+
|
|
104
|
+
Test in a real car, or in Google's **Desktop Head Unit (DHU)** emulator:
|
|
105
|
+
|
|
106
|
+
1. Install the **Android Auto Desktop Head Unit emulator** from the SDK Manager (_SDK Tools → Android Auto Desktop Head Unit emulator_).
|
|
107
|
+
2. In the phone's **Android Auto** settings, enable Developer Mode (tap the version 10×), then **"Start head unit server"**.
|
|
108
|
+
3. Connect the phone over USB and run the DHU.
|
|
109
|
+
|
|
110
|
+
See Google's [Test Android Auto apps with DHU](https://developer.android.com/training/cars/testing/dhu) for the full walkthrough. Your app appears under the media apps — browse the tree, exercise voice search, and verify artwork, grid/list styles, and section headers.
|
|
111
|
+
|
|
112
|
+
## Reference
|
|
113
|
+
|
|
114
|
+
See the [example app](https://github.com/radio-garden/react-native-audio-browser/tree/main/apps/example-native/android) for a complete working manifest and descriptor.
|