react-native-audio-browser 0.2.0-next.12 → 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.
Files changed (156) hide show
  1. package/README.md +10 -0
  2. package/android/build.gradle +20 -1
  3. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +5 -1
  4. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +8 -0
  5. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
  6. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +27 -9
  7. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +11 -0
  8. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +11 -1
  9. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +1 -1
  10. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +17 -2
  11. package/android/src/main/java/com/audiobrowser/player/Player.kt +3 -0
  12. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  13. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +73 -0
  14. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +8 -1
  15. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +49 -3
  16. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +3 -2
  17. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
  18. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +56 -0
  19. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +4 -4
  20. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +77 -10
  21. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +2 -2
  22. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  23. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +8 -8
  24. package/ios/Browser/BrowserManager.swift +106 -45
  25. package/ios/Browser/InertStyleDiagnostic.swift +279 -0
  26. package/ios/Browser/JsonModels.swift +81 -10
  27. package/ios/Browser/SectionPresentation.swift +30 -0
  28. package/ios/Browser/StyleResolver.swift +11 -0
  29. package/ios/Browser/TabBarEntries.swift +7 -1
  30. package/ios/CarPlay/CarPlayController.swift +139 -35
  31. package/ios/CarPlay/CarPlayListItemFactory.swift +99 -27
  32. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  33. package/ios/Http/HttpClient.swift +8 -2
  34. package/ios/HybridAudioBrowser.swift +54 -11
  35. package/ios/Model/NitroTypeStubs.swift +60 -0
  36. package/ios/Player/MediaIntentCriteria.swift +13 -5
  37. package/ios/Player/MediaLoader.swift +33 -9
  38. package/ios/Player/NowPlayingUpdater.swift +53 -5
  39. package/ios/Player/PlaybackCoordinator.swift +11 -4
  40. package/ios/Player/PlaybackTimer.swift +8 -2
  41. package/ios/Player/QueueManager.swift +36 -10
  42. package/ios/Player/ShuffleOrder.swift +23 -0
  43. package/ios/Player/SleepTimerHandling.swift +13 -0
  44. package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
  45. package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
  46. package/ios/Tests/JsonModelsTests.swift +76 -1
  47. package/ios/Tests/QueueManagerTests.swift +72 -6
  48. package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
  49. package/ios/Tests/SectionPresentationTests.swift +29 -0
  50. package/ios/Tests/StyleResolverTests.swift +60 -0
  51. package/ios/Tests/Support/TestTypes.swift +7 -0
  52. package/ios/TrackPlayer.swift +57 -34
  53. package/ios/Util/NetworkMonitor.swift +43 -26
  54. package/ios/Util/SFSymbolRenderer.swift +24 -2
  55. package/ios/Util/SVGProcessor.swift +9 -2
  56. package/lib/commonjs/features/playback/progress.js +2 -1
  57. package/lib/commonjs/features/playback/progress.js.map +1 -1
  58. package/lib/commonjs/features/playback/rate.js +8 -1
  59. package/lib/commonjs/features/playback/rate.js.map +1 -1
  60. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  61. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  62. package/lib/module/features/playback/progress.js +2 -1
  63. package/lib/module/features/playback/progress.js.map +1 -1
  64. package/lib/module/features/playback/rate.js +8 -1
  65. package/lib/module/features/playback/rate.js.map +1 -1
  66. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  67. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  68. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  69. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  70. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  71. package/lib/typescript/src/types/browser-nodes.d.ts +104 -2
  72. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  73. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -0
  74. package/nitrogen/generated/android/c++/JBrowseResult.hpp +6 -0
  75. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  76. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -0
  77. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -0
  78. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -0
  79. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -0
  80. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -0
  81. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -0
  82. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -0
  83. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -0
  84. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +6 -0
  85. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -0
  86. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -0
  87. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +6 -0
  88. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -0
  89. package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
  90. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +12 -0
  91. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  92. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -0
  93. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +6 -0
  94. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +6 -0
  95. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +4 -0
  96. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -0
  97. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +6 -0
  98. package/nitrogen/generated/android/c++/JSection.hpp +6 -0
  99. package/nitrogen/generated/android/c++/JSectionStyle.hpp +30 -3
  100. package/nitrogen/generated/android/c++/JTrack.hpp +4 -0
  101. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -0
  102. package/nitrogen/generated/android/c++/JTrackStyle.hpp +24 -3
  103. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +6 -0
  104. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  105. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
  106. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  107. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +18 -3
  108. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +15 -3
  109. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +54 -0
  110. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +9 -0
  111. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +9 -0
  112. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  113. package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
  114. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  115. package/nitrogen/generated/ios/swift/SectionStyle.swift +70 -1
  116. package/nitrogen/generated/ios/swift/TrackStyle.swift +59 -1
  117. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  118. package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
  119. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  120. package/nitrogen/generated/shared/c++/SectionStyle.hpp +32 -2
  121. package/nitrogen/generated/shared/c++/TrackStyle.hpp +25 -2
  122. package/package.json +7 -2
  123. package/src/features/playback/progress.ts +2 -1
  124. package/src/features/playback/rate.ts +8 -1
  125. package/src/style-resolution.test.ts +72 -1
  126. package/src/types/browser-nodes.ts +112 -2
  127. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  128. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  129. package/website/guide/android-auto.md +114 -0
  130. package/website/guide/android-certificates.md +409 -0
  131. package/website/guide/artwork.md +255 -0
  132. package/website/guide/audio-output.md +135 -0
  133. package/website/guide/automotive.md +86 -0
  134. package/website/guide/basic-usage.md +151 -0
  135. package/website/guide/battery.md +115 -0
  136. package/website/guide/browser.md +581 -0
  137. package/website/guide/carplay.md +620 -0
  138. package/website/guide/configuration.md +173 -0
  139. package/website/guide/equalizer.md +231 -0
  140. package/website/guide/errors.md +310 -0
  141. package/website/guide/favorites.md +154 -0
  142. package/website/guide/gate.md +143 -0
  143. package/website/guide/getting-started.md +46 -0
  144. package/website/guide/hooks.md +238 -0
  145. package/website/guide/live-streams.md +125 -0
  146. package/website/guide/metadata.md +239 -0
  147. package/website/guide/migrating-from-track-player.md +244 -0
  148. package/website/guide/native-callback-fetch.md +127 -0
  149. package/website/guide/network.md +86 -0
  150. package/website/guide/now-playing.md +175 -0
  151. package/website/guide/playback.md +328 -0
  152. package/website/guide/queue.md +300 -0
  153. package/website/guide/remote-controls.md +256 -0
  154. package/website/guide/search.md +255 -0
  155. package/website/guide/sleep-timer.md +268 -0
  156. package/website/guide/track.md +246 -0
@@ -0,0 +1,581 @@
1
+ ---
2
+ description: 'The reference for configureBrowser: every source shape (static, callback, JSON API), routing and path patterns, request transforms, and BrowserConfiguration option.'
3
+ ---
4
+
5
+ # Browser
6
+
7
+ The **Browser** is the subsystem that turns one declarative description of your content into a navigable tree — the same tree that powers your in-app browse UI, CarPlay, and Android Auto. You describe it once with **`configureBrowser`**; the library resolves paths, fetches children, transforms requests, and renders the native surfaces for you.
8
+
9
+ [Basic Usage](/guide/basic-usage) introduces the browse tree. This guide is the reference for _configuring_ it: every source shape, every routing pattern, and every option on [`BrowserConfiguration`](/api/types/browser/#browserconfiguration).
10
+
11
+ ## A complete example
12
+
13
+ Here is a full, runnable configuration — call `setupPlayer` once, then `configureBrowser` once. It has two tabs, one **static** route, and one **callback** route. Paste it, swap in your own content, and you have a working browse tree on phone, CarPlay, and Android Auto.
14
+
15
+ ```ts
16
+ import { setupPlayer, configureBrowser } from 'react-native-audio-browser'
17
+
18
+ await setupPlayer()
19
+
20
+ configureBrowser({
21
+ tabs: [
22
+ { title: 'Browse', path: '/browse' },
23
+ { title: 'Favorites', path: '/favorites' }
24
+ ],
25
+ routes: {
26
+ // Static data declared inline:
27
+ '/browse': {
28
+ path: '/browse',
29
+ title: 'Browse',
30
+ children: [
31
+ // browsable — has a path
32
+ { title: 'Jazz', path: '/browse/jazz' },
33
+ // playable — has a src
34
+ { title: 'Smooth Floret FM', src: 'https://example.com/floret.mp3' }
35
+ ]
36
+ },
37
+ // Resolved on demand from your own code:
38
+ '/browse/jazz': async () => {
39
+ const stations = await fetchJazzStations() // your code
40
+ return { path: '/browse/jazz', title: 'Jazz', children: stations }
41
+ }
42
+ }
43
+ })
44
+ ```
45
+
46
+ ::: tip "Your code" vs. library API
47
+ Helpers like `fetchJazzStations`, `fetchAlbum`, `getLocalFavoriteIds`, and `getDomain` throughout this guide are **your** functions, not library exports — they stand in for however your app gets its data. Only imports from `react-native-audio-browser` are library API.
48
+ :::
49
+
50
+ Calling `configureBrowser` again **replaces** the entire configuration — see [Updating the configuration](#updating-the-configuration). The rest of this guide unpacks each part.
51
+
52
+ ## How a path resolves
53
+
54
+ Everything the Browser shows is the result of resolving a **path** (a string like `/browse/jazz`) to a **page** — a Track carrying its content. The flow is always the same:
55
+
56
+ ```mermaid
57
+ graph LR
58
+ P["path<br/>/browse/jazz"] --> M["match a route<br/>(most specific wins)"]
59
+ M --> S["resolve its source<br/>(static · callback · HTTP)"]
60
+ S --> Page["page<br/>ResolvedTrack { path, title, sections }"]
61
+
62
+ classDef step fill:#e3f2fd,stroke:#1976d2
63
+ class P,M,S,Page step
64
+ ```
65
+
66
+ A **page** is a [`ResolvedTrack`](/api/types/browser-nodes/#resolvedtrack): it requires a `path` and a `title`, and carries its content as `sections` — an array of [`Section`](/api/types/browser-nodes/#section)s, each a titled, styled group of [`Track`](/api/types/browser-nodes/#track)s:
67
+
68
+ ```ts
69
+ {
70
+ path: '/home',
71
+ title: 'Home',
72
+ sections: [
73
+ { title: 'Popular', style: { display: 'grid', gridWrap: false }, path: '/popular', children: [...] },
74
+ { title: 'All stations', children: [...] }
75
+ ]
76
+ }
77
+ ```
78
+
79
+ A Section is `{ title?, subtitle?, style?, path?, children }`: `title` renders as a header above its tracks, `style` is a declaration block whose `display` picks the children's layout (`'list'` rows — the default — or `'grid'` artwork tiles; `gridWrap: false` keeps the grid to a single line), and `path` is the navigation target for the header / "view all" surface. See [Presentation](#presentation) for how each surface renders the declarations.
80
+
81
+ A flat page doesn't have to spell that out: authoring a plain `children: Track[]` — as most examples in this guide do — is sugar for one untitled section. Resolved output always carries `sections`; `children` is never populated on a resolved page.
82
+
83
+ So configuring the Browser is really three decisions, covered in turn below: **what routes exist**, **what source each route resolves from**, and **how requests are shaped** along the way.
84
+
85
+ ## Source shapes
86
+
87
+ Wherever the Browser needs content — a route, the `tabs`, `search` — you supply a **source**, and a source is always one of three shapes. Learn these once; they apply everywhere.
88
+
89
+ ### 1. Static data
90
+
91
+ A page object you declare inline. Best for small, fixed trees.
92
+
93
+ ```ts
94
+ configureBrowser({
95
+ routes: {
96
+ '/browse': {
97
+ path: '/browse',
98
+ title: 'Browse',
99
+ children: [
100
+ // browsable — has a path
101
+ { title: 'Jazz', path: '/browse/jazz' },
102
+ // playable — has a src
103
+ { title: 'Smooth Floret FM', src: 'https://example.com/floret.mp3' }
104
+ ]
105
+ }
106
+ }
107
+ })
108
+ ```
109
+
110
+ ### 2. A callback
111
+
112
+ A function `({ path, routeParams }) => ResolvedTrack` (sync or async). Resolve children from anywhere — memory, a database, your own fetch logic. Best for trees too large to declare upfront, or content that depends on runtime state.
113
+
114
+ ```ts
115
+ configureBrowser({
116
+ routes: {
117
+ '/albums/{id}': async ({ routeParams }) => {
118
+ const album = await fetchAlbum(routeParams.id) // your code
119
+ return {
120
+ path: `/albums/${album.id}`,
121
+ title: album.name,
122
+ children: album.tracks
123
+ }
124
+ }
125
+ }
126
+ })
127
+ ```
128
+
129
+ Return `{ error: 'message' }` instead of a page to signal a failure the Browser should surface — see [Error handling](#error-handling).
130
+
131
+ ### 3. An HTTP endpoint
132
+
133
+ A [`TransformableRequestConfig`](/api/types/browser/#transformablerequestconfig). The library issues the request **natively** (so browse works on a cold car start with your JS not yet running) and expects a page object — a `ResolvedTrack` — back.
134
+
135
+ The key convenience: with a `baseUrl` set, **any path with no explicit route is fetched over HTTP automatically** — the navigated path becomes the request path. So a fully server-driven tree may need _no_ `routes` at all:
136
+
137
+ ```ts
138
+ configureBrowser({
139
+ // GET https://api.example.com/browse/jazz
140
+ // (the navigated path flows through automatically)
141
+ request: { baseUrl: 'https://api.example.com' }
142
+ })
143
+ ```
144
+
145
+ ::: warning A static `path` does not rewrite the request path
146
+ On an HTTP route, the _navigated_ path is used as the request path — a static `path` field is ignored for this. To remap (e.g. send `/favorites` to `/favorites/v2`), use a [transform](#transforms-per-request), which also receives `routeParams`.
147
+ :::
148
+
149
+ ::: tip Same three shapes, everywhere
150
+ `tabs` and `search` accept these same shapes. Once you know static vs. callback vs. HTTP, you know how to configure every part of the Browser. For example, `search` as a callback is just another source returning tracks:
151
+
152
+ ```ts
153
+ configureBrowser({
154
+ search: async ({ query }) => searchStations(query) // your code → Track[]
155
+ })
156
+ ```
157
+
158
+ Search has its own depth (voice intents, query modes) — see [Search](/guide/search).
159
+ :::
160
+
161
+ ## Tabs and the initial path
162
+
163
+ `tabs` are the top-level entry points — the segmented control in your app, the tab bar on CarPlay, the root of the Android Auto tree. Each tab is a Track with a `title` and a `path` pointing at a route.
164
+
165
+ ```ts
166
+ configureBrowser({
167
+ tabs: [
168
+ { title: 'Home', path: '/home' },
169
+ { title: 'Favorites', path: '/favorites' },
170
+ { title: 'Browse', path: '/browse' }
171
+ ]
172
+ })
173
+ ```
174
+
175
+ ::: warning Maximum 4 tabs
176
+ CarPlay and Android Auto cap the tab bar, so `tabs` is limited to **4**. More than four triggers a runtime warning and the extras are dropped.
177
+ :::
178
+
179
+ ::: tip A tab's path stays a tab
180
+ Link freely to a tab's own path — a "view all" section header on one tab pointing at another tab's route, say. On CarPlay (iOS 17+) that **switches to the tab** instead of pushing a second copy of it onto the current tab's stack, so the user lands where the tab bar already shows that content, with no Back button to a page they never came from. Elsewhere — earlier iOS, Android Auto, your app UI — it navigates as usual.
181
+ :::
182
+
183
+ Like any source, `tabs` can also be a **callback** — useful when the tabs themselves depend on runtime state, such as a debug build or the current user:
184
+
185
+ ```ts
186
+ configureBrowser({
187
+ tabs: () => [
188
+ { title: 'Home', path: '/home' },
189
+ { title: 'Favorites', path: '/favorites' },
190
+ ...(__DEV__ ? [{ title: 'Debug', path: '/debug' }] : [])
191
+ ]
192
+ })
193
+ ```
194
+
195
+ …or an **HTTP endpoint** returning a page (`{ children: Track[] }`).
196
+
197
+ **Initial path.** The Browser opens to the first tab's `path` by default. Set `path` to start somewhere else:
198
+
199
+ ```ts
200
+ configureBrowser({
201
+ path: '/home', // where browse opens; defaults to the first tab's path (or '/')
202
+ tabs: [
203
+ /* … */
204
+ ]
205
+ })
206
+ ```
207
+
208
+ ## Routes
209
+
210
+ `routes` maps **path patterns** to sources. When the Browser resolves a path, it finds the matching pattern and resolves that pattern's source.
211
+
212
+ ### Pattern syntax
213
+
214
+ Patterns match on **exact segment count** first — `/artists` does _not_ match `/artists/123`. Within that, a segment can be:
215
+
216
+ | Pattern | Matches | Captured |
217
+ | -------------- | --------------------------------------------------------- | ------------------ |
218
+ | `/favorites` | exactly `/favorites` | — |
219
+ | `/albums/{id}` | `/albums/floret`, `/albums/42` | `routeParams.id` |
220
+ | `/artists/*` | any single segment at that position (`/artists/anything`) | — |
221
+ | `/files/**` | `/files/...` at any depth below | `routeParams.tail` |
222
+
223
+ When several patterns match, **most specific wins**: constant > `{param}` > `*` > `**`. So `/albums/new` and `/albums/{id}` can coexist — `/albums/new` takes the literal route.
224
+
225
+ ```ts
226
+ configureBrowser({
227
+ routes: {
228
+ // literal — checked first
229
+ '/albums/new': newReleasesPage,
230
+ // {id} otherwise
231
+ '/albums/{id}': ({ routeParams }) => fetchAlbum(routeParams.id),
232
+ // deep file paths
233
+ '/files/**': ({ routeParams }) => listFiles(routeParams.tail)
234
+ }
235
+ })
236
+ ```
237
+
238
+ ::: warning The bare `'*'` key is the _default_, not a wildcard segment
239
+ A single-segment wildcard only works as a `*` **segment inside a longer pattern** (e.g. `/artists/*`). The bare top-level key `'*'` is different: it's the **custom default** — the source used for any path no other route matches (and only needed if you want to override the built-in HTTP default).
240
+
241
+ ```ts
242
+ routes: {
243
+ // wildcard segment — matches /artists/<one segment>
244
+ '/artists/*': handleArtist,
245
+ // default for everything unmatched
246
+ '*': { baseUrl: 'https://api.example.com' }
247
+ }
248
+ ```
249
+
250
+ :::
251
+
252
+ ### Route parameters
253
+
254
+ A callback (or transform) receives the captured params in `routeParams`:
255
+
256
+ ```ts
257
+ '/stations/{country}/{city}': async ({ routeParams }) => {
258
+ // /stations/nl/amsterdam → { country: 'nl', city: 'amsterdam' }
259
+ return fetchStations(routeParams.country, routeParams.city)
260
+ }
261
+ ```
262
+
263
+ ### Per-route overrides
264
+
265
+ A route value can be a bare source (the common case) **or** a [`RouteConfig`](/api/types/browser/#routeconfig) object that overrides request behavior for just that subtree — its `browse` source, plus its own `media` and `artwork` config:
266
+
267
+ ```ts
268
+ configureBrowser({
269
+ routes: {
270
+ '/premium/**': {
271
+ browse: { baseUrl: 'https://api.example.com/premium' },
272
+ media: { baseUrl: 'https://premium-audio.cdn.example.com' },
273
+ artwork: { baseUrl: 'https://premium-images.cdn.example.com' }
274
+ }
275
+ }
276
+ })
277
+ ```
278
+
279
+ This is how you point one branch of the tree at a different backend without touching the rest.
280
+
281
+ ## Shaping requests
282
+
283
+ For HTTP sources, you rarely repeat the same `baseUrl`, headers, and query params on every route. Instead you set them once and let them **layer**. From most general to most specific:
284
+
285
+ ```
286
+ request → browse → route
287
+ (all kinds) (browse only) (this route only)
288
+ ```
289
+
290
+ `request` also feeds `media`, `artwork`, and `search`; `browse` narrows to browse requests; a route's own config wins last.
291
+
292
+ ```ts
293
+ configureBrowser({
294
+ request: { baseUrl: 'https://api.example.com', headers: { 'x-app': 'demo' } },
295
+ browse: { query: { client: 'audio-browser', hl: 'en' } },
296
+ routes: {
297
+ // inherits baseUrl + header + query; navigated path is the request path
298
+ '/browse/jazz': {}
299
+ }
300
+ })
301
+ ```
302
+
303
+ ### RequestConfig fields
304
+
305
+ Every request layer is a [`RequestConfig`](/api/types/browser/#requestconfig) — set only the fields you need; they merge (query params merge additively, scalars override):
306
+
307
+ | Field | Purpose |
308
+ | ---------------------- | ----------------------------------------------------------------------------------- |
309
+ | `baseUrl` / `path` | Where to send the request (note: `path` doesn't override the navigated browse path) |
310
+ | `method` | `GET` (default), `POST`, … |
311
+ | `headers` | HTTP headers |
312
+ | `query` | Query parameters (merged additively) |
313
+ | `body` / `contentType` | Request body, e.g. for `POST` |
314
+ | `userAgent` | `User-Agent` override |
315
+
316
+ ### Transforms — per request
317
+
318
+ When a layer needs to change _per request_ (read the captured `routeParams`, switch a `GET` to a `POST`, remap the path), add a `transform` (async) or `transformSync`. It receives the merged request and returns the final one:
319
+
320
+ ::: warning A transform replaces — it doesn't merge
321
+ Your return value **becomes** the request; the layer's own static fields are _not_ merged back in. Always **spread the incoming `request`** (`{ ...request, … }`) — a transform that returns a bare `{ method: 'POST' }` silently drops the inherited `baseUrl`, `headers`, and `query`.
322
+ :::
323
+
324
+ ```ts
325
+ configureBrowser({
326
+ routes: {
327
+ '/favorites': {
328
+ // turn the favorites browse into a POST carrying locally-stored ids
329
+ transformSync: (request) => ({
330
+ ...request,
331
+ method: 'POST',
332
+ // remapping the path is valid here, in a transform
333
+ path: '/favorites/v2',
334
+ contentType: 'application/json',
335
+ body: JSON.stringify({ ids: getLocalFavoriteIds() }) // your code
336
+ })
337
+ }
338
+ }
339
+ })
340
+ ```
341
+
342
+ ### Resolvers — values that change rarely
343
+
344
+ When the _whole_ config depends on runtime state — a domain that varies by environment, a locale, an auth host — make the layer a **resolver**: a thunk returning the config. It runs **once per content generation** and is cached until you call [`invalidateAllContent`](#updating-the-configuration), so it's the cheap place to read values that change rarely:
345
+
346
+ ```ts
347
+ configureBrowser({
348
+ // re-evaluated only when content is regenerated, not per request
349
+ request: () => ({
350
+ baseUrl: `https://${getDomain()}/api`, // your code
351
+ userAgent: resolveUserAgent() // your code
352
+ }),
353
+ browse: () => ({ query: { hl: resolveLocale() } }) // your code
354
+ })
355
+ ```
356
+
357
+ ::: tip Resolver vs. transform
358
+ **Resolver** (the layer _is_ a function): runs once, cached — for values that change rarely. **Transform** (a `transform` field _on_ the layer): runs every request — for per-request shaping. Reach for a resolver first; add a transform only when a single request genuinely differs.
359
+ :::
360
+
361
+ ## Media and artwork
362
+
363
+ The same request machinery shapes the **audio stream** request (`media`) and **image** requests (`artwork`). Both layer on top of `request`, and both add a per-track `resolve` / `resolveSync` so a single track can build its own stream or image request from its metadata:
364
+
365
+ ```ts
366
+ configureBrowser({
367
+ media: {
368
+ // build the stream request per track — e.g. a signed URL
369
+ resolve: async (track) => ({
370
+ path: `/stream/${track.id}`,
371
+ query: { token: await sign(track.id) }
372
+ })
373
+ },
374
+ artwork: {
375
+ baseUrl: 'https://images.example.com',
376
+ // tell the library which query params carry the requested size
377
+ imageQueryParams: { width: 'w', height: 'h' }
378
+ }
379
+ })
380
+ ```
381
+
382
+ With `imageQueryParams`, CarPlay and Android Auto's requested size is injected automatically (`?w=200&h=200`) so each surface gets right-sized art. For full control, an `artwork.transform` receives that size as an [`ImageContext`](/api/types/browser/#imagecontext).
383
+
384
+ **Now-playing art** can differ from browse-row art — set `nowPlayingArtwork` for the lock screen / car now-playing surface only. It uniquely supports an `{id}` template filled from the active track's `id`:
385
+
386
+ ```ts
387
+ configureBrowser({
388
+ nowPlayingArtwork: { path: '/artwork/{id}' } // {id} → the active track's id
389
+ }) // {id} templating is native-only — not applied by the web implementation
390
+ ```
391
+
392
+ See [Now Playing](/guide/now-playing) for the metadata side of the now-playing surface.
393
+
394
+ ## Playback behavior
395
+
396
+ Two options control what happens when a playable Track is tapped.
397
+
398
+ **`singleTrack`** — by default, tapping a track queues **its section** and starts there, so next/previous walk the list the user tapped in. The scope is the page's declared structure: the tapped [Section](#how-a-path-resolves)'s `children`, identical on every surface (a page authored with plain `children` is one untitled section). Rendering may truncate what a section _shows_ — a CarPlay single-line grid fits only so many tiles — but never what _plays_. A page aggregating several sections never leaks next/previous across them. Set `singleTrack: true` to play only the tapped track. If the track has meanwhile disappeared from its section (a stale resume, say), the library plays it as a single track rather than guessing a queue from the changed list.
399
+
400
+ Section scoping knows _where_ the tap happened, not just on what. Each playable row's path carries the track's identity (`id`, falling back to `src`) and the row's position on the page. So the same track can safely appear in several places on one page: a tap queues the section it happened in, and when a playlist holds the same track twice, next/previous continue from the tapped copy.
401
+
402
+ The position is only a tie-breaker. If the page has changed by the time the queue is expanded (a stale resume, say), the first row matching the identity wins instead — a stale position can pick a different copy of the tapped track, never a different track. And tapping a row that's already in the current queue from the same page skips in place rather than requeueing the section.
403
+
404
+ **`handleTrackLoad`** — intercept loading entirely. When set, tapping a track calls _your_ handler **instead of** the library auto-playing, and native waits for your promise to resolve before continuing. The two branches differ:
405
+
406
+ ```ts
407
+ import { configureBrowser, setQueue, play } from 'react-native-audio-browser'
408
+
409
+ configureBrowser({
410
+ handleTrackLoad: async ({ track, queue, startIndex }) => {
411
+ if (isVideo(track)) {
412
+ // your surface — return without touching the player; audio stays put
413
+ openVideoPlayer(track)
414
+ return
415
+ }
416
+ // audio: drive playback yourself — the library won't auto-play
417
+ setQueue(queue, startIndex)
418
+ play()
419
+ }
420
+ })
421
+ ```
422
+
423
+ In the video branch, returning without calling the player tells the library "handled — leave audio alone." In the audio branch you take over, so you must explicitly `setQueue` + `play` — both are fire-and-forget calls returning `void`, so there's nothing to await _on them_. The handler itself is a different story: native **awaits the promise your handler returns**, so anything you `await` inside — an auth check, a fetch — delays the tap being treated as handled. Keep slow work out of the handler, or kick it off without awaiting it. (`setQueue`/`play` are covered in [Basic Usage](/guide/basic-usage).)
424
+
425
+ ## Reading and driving browse state
426
+
427
+ The Browser exposes its current state through getters, React hooks, and event emitters — so your in-app UI can mirror exactly what the car shows.
428
+
429
+ ```tsx
430
+ import {
431
+ useTabs,
432
+ usePath,
433
+ useContent,
434
+ navigate
435
+ } from 'react-native-audio-browser'
436
+
437
+ function BrowseScreen() {
438
+ const tabs = useTabs() // Track[] | undefined
439
+ const path = usePath() // current path
440
+ const page = useContent() // resolved page (page.sections to render)
441
+
442
+ return (
443
+ <>
444
+ {tabs?.map((tab) => (
445
+ <Tab key={tab.path} tab={tab} onPress={() => navigate(tab.path!)} />
446
+ ))}
447
+ {page?.sections?.map((section, i) => (
448
+ <React.Fragment key={i}>
449
+ {section.title && <Header title={section.title} />}
450
+ {section.children.map((track) => (
451
+ <Row
452
+ key={track.path ?? track.src}
453
+ track={track}
454
+ onPress={() => navigate(track)}
455
+ />
456
+ ))}
457
+ </React.Fragment>
458
+ ))}
459
+ </>
460
+ )
461
+ }
462
+ ```
463
+
464
+ `navigate` takes either a **path** (browse into it) or a **Track** (play it). The getters (`getPath`, `getContent`, `getTabs`) and emitters (`onPathChanged`, `onContentChanged`, `onTabsChanged`) cover non-React call sites. To inspect the config you set, `getBrowserConfiguration()` returns it (or `undefined` before `configureBrowser` runs).
465
+
466
+ ## Updating the configuration
467
+
468
+ The tree isn't static — content changes, languages switch, environments move. Three tools, smallest blast radius first:
469
+
470
+ | Need | Use |
471
+ | -------------------------------------------------- | ------------------------------------ |
472
+ | One path's children changed | `notifyContentChanged('/favorites')` |
473
+ | Everything is stale (locale, domain, auth changed) | `invalidateAllContent()` |
474
+ | The config _shape_ itself changed | `configureBrowser(newConfig)` |
475
+
476
+ `notifyContentChanged(path)` refreshes a single path in place — external controllers re-fetch just that page. `invalidateAllContent()` clears every cache and re-resolves all visible surfaces (and re-runs your [resolvers](#resolvers-values-that-change-rarely)) — this is what you call after a language or domain switch. `configureBrowser` replaces everything wholesale.
477
+
478
+ ```ts
479
+ // language changed → resolvers read the new locale on the next resolve
480
+ onLocaleChange(() => invalidateAllContent()) // onLocaleChange is your code
481
+ ```
482
+
483
+ ## Error handling
484
+
485
+ When a source fails — network down, a callback throws, an HTTP non-2xx, an empty container — the Browser raises a typed `NavigationError`. On CarPlay / Android Auto it shows a built-in message; in your app you read it via `useNavigationError`.
486
+
487
+ Customize the copy with `formatNavigationError` — branch on the `code` and the `path`, and fall back to the library's default for the rest:
488
+
489
+ ```ts
490
+ configureBrowser({
491
+ formatNavigationError: ({ error, defaultFormatted, path }) => {
492
+ if (error.code === 'empty-content' && path.startsWith('/favorites')) {
493
+ return {
494
+ title: 'No favorites yet',
495
+ message: 'Tap the heart on a station to add it.'
496
+ }
497
+ }
498
+ if (error.code === 'network-error') {
499
+ return {
500
+ title: 'No connection',
501
+ message: 'Check your connection and try again.'
502
+ }
503
+ }
504
+ return defaultFormatted
505
+ }
506
+ })
507
+ ```
508
+
509
+ The `code` values: `content-not-found`, `network-error`, `http-error`, `callback-error`, `empty-content`, `timeout`, `unknown-error`. Your formatter drives both the car dialogs and the [`useFormattedNavigationError`](/api/features/errors/#useformattednavigationerror) hook in your app.
510
+
511
+ ## Presentation
512
+
513
+ Presentation lives in `style` blocks, separate from content, plus a few Track fields and config options. Each surface applies the style properties it supports and ignores the rest — an unsupported property is a no-op, never an error.
514
+
515
+ | Field | On | Effect | Platform |
516
+ | ------------------------------------------------ | ------------------------ | ---------------------------------------------------------------------------- | ------------------- |
517
+ | `title` | a section | header above its tracks | all |
518
+ | `style: { display: 'grid' }` | a section | wrapping artwork-tile grid | all\* |
519
+ | `style: { display: 'grid', gridWrap: false }` | a section | a single line of artwork tiles | all\* |
520
+ | `path` (+ `subtitle`) | a section | header / "view all" navigation target | all |
521
+ | `style` (same block) | a page | declaration for the whole page — a section overrides it for its own children | all |
522
+ | `style: { display: 'grid' }` | a browsable track | the layout of the page it opens — Android Auto reads it from the parent item | Android Auto / AAOS |
523
+ | `style: { artworkRendering: 'stencil' }` | a track / section / page | tint monochrome artwork to the surface appearance | CarPlay |
524
+ | `style: { imageShape: 'circular' }` | a track / section / page | crop grid-tile artwork circular (people) instead of rounded | CarPlay (iOS 26+) |
525
+ | `style: { accessorySymbol: 'lock.fill' }` | a track / section / page | SF Symbol accessory on rows and tiles (`'none'` restores the derived one) | CarPlay |
526
+ | `style: { gridTile: 'card' }` | a section / page | the grid's tile kind: `'plain'` (default), `'card'`, or `'condensed'` | CarPlay (iOS 26+) |
527
+ | `style: { cardTint: '#1e3a8a' }` (+ `cardImage`) | a track / section / page | card color and image mode (`'background'` fills the card full-height) | CarPlay (iOS 26+) |
528
+ | `disabled: true` | a track | unavailable: never plays; grayed where drawable, hidden elsewhere | all |
529
+ | `artwork: 'sf:heart.fill'` | any item | SF Symbol icon (supports `?bg=…&fg=…`) | iOS |
530
+ | `live: true` | a track | live indicator | iOS |
531
+ | `carPlaySiriListButton: 'top' \| 'bottom'` | a page | place the Siri cell on the page | iOS |
532
+ | `albumPath` + `resolveAlbumPath` | track + config | make the now-playing album line tappable | CarPlay |
533
+
534
+ ::: info Caveats from the table
535
+ **`albumPath` requires `album`** — CarPlay renders the tappable line from the album metadata, so without an `album` there is no line to tap. **\*Layouts render each platform's nearest supported form** — a single-line grid (`gridWrap: false`) on CarPlay shows the tiles that fit (roughly eight, width-dependent) and truncates the rest; a wrapping grid takes as many lines as it needs on iOS 26+ and renders as a list before that. Android Auto's grid always wraps, so it shows more, never less. No car surface scrolls horizontally; an app UI typically renders `gridWrap: false` as a horizontal scroller. Truncation is visual only: tapping a tile queues the whole section (see [Playback behavior](#playback-behavior)). Item properties like `artworkRendering`, `imageShape`, and `accessorySymbol` are inherited — `track ?? section ?? page`, per property — so a section-wide value can be overridden per item; `accessorySymbol: 'none'` is the per-item escape from an inherited symbol, and where the accessory draws is the platform's choice per surface (trailing on list rows, leading the title on tiles). `display` is never inherited: it always describes the children of whatever it's set on. On a page or section it lays out that page or section; on a browsable track it declares the layout of the page that track opens — Android Auto needs it there, or that page renders as a list. `gridWrap` is inert unless `display` is `'grid'`. One AAOS caveat: AOSP-based Automotive units honor only the parent item's `display`, so a page mixing grid and list sections renders uniformly there — see [Android Auto → Browse display](/guide/android-auto#browse-display).
536
+ :::
537
+
538
+ ::: tip Inert declarations are logged in debug builds
539
+ A combination that can never render — `gridWrap` or `gridTile` outside a grid, `cardTint` with no card treatment in scope, `imageShape` on a list or a card, `accessorySymbol` on a card, `display` on a track that renders playable — is inert, not an error, so nothing breaks. In a **debug build**, each one is logged once per page resolution (`Timber` on Android, `os.log` on iOS) naming the section, the effective style, and the fix:
540
+
541
+ ```
542
+ Inert style declaration on page '/home': `style.gridWrap` on section 'Featured'
543
+ never renders — `gridWrap` renders only in a grid, but its effective style is
544
+ display 'list'. Declare `display: 'grid'` on the section or the page, or drop
545
+ `gridWrap`.
546
+ ```
547
+
548
+ A property a _surface_ can't draw is never reported — that's the aspirational rule working, not a mistake. Release builds skip the check entirely, so a page you only ever serve to a release build reports nothing; browse it from a debug build to see the findings.
549
+ :::
550
+
551
+ ```ts
552
+ // A grid page. Android Auto also needs display: 'grid' on the item
553
+ // that opens this page — see the Android Auto guide:
554
+ { path: '/genres', title: 'Genres', style: { display: 'grid' }, children: [...] }
555
+ {
556
+ path: '/home',
557
+ title: 'Home',
558
+ sections: [
559
+ { title: 'Popular', style: { display: 'grid', gridWrap: false }, path: '/popular', children: [...] },
560
+ { title: 'All stations', children: [...] }
561
+ ]
562
+ }
563
+ {
564
+ title: 'Favorites',
565
+ path: '/favorites',
566
+ artwork: 'sf:heart.fill?bg=#FF0090&fg=#fff'
567
+ }
568
+ ```
569
+
570
+ Two config-level platform options round it out: `carPlayLoadingTitle` (localized "Loading…" on older CarPlay) and `androidControllerOfflineError`. The album-line tap needs both `albumPath` on the track and a `resolveAlbumPath` callback in the config that maps it to a browse path — see [CarPlay](/guide/carplay) and [Android Auto](/guide/android-auto) for the platform setup these build on.
571
+
572
+ ## Where to go next
573
+
574
+ `configureBrowser` is also where you wire up the adjacent subsystems, each with its own guide:
575
+
576
+ - **[Search](/guide/search)** — the `search` source: voice and text queries → tracks.
577
+ - **[Favorites](/guide/favorites)** — heart buttons and a synced favorites collection.
578
+ - **[Now Playing](/guide/now-playing)** — lock-screen / car metadata for the active track.
579
+ - **[Gate](/guide/gate)** — gating playback behind a paywall or region check.
580
+
581
+ For every type referenced here — [`BrowserConfiguration`](/api/types/browser/#browserconfiguration), [`Track`](/api/types/browser-nodes/#track), [`RequestConfig`](/api/types/browser/#requestconfig), [`NavigationError`](/api/features/errors/#navigationerror) — see the [API reference](/api/).