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.
- package/README.md +10 -0
- package/android/build.gradle +20 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +5 -1
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +8 -0
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +27 -9
- package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +11 -0
- package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +11 -1
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +1 -1
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +17 -2
- package/android/src/main/java/com/audiobrowser/player/Player.kt +3 -0
- package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
- package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +73 -0
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +8 -1
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +49 -3
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +3 -2
- package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
- package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +56 -0
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +4 -4
- package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +77 -10
- package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +2 -2
- package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
- package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +8 -8
- package/ios/Browser/BrowserManager.swift +106 -45
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +81 -10
- package/ios/Browser/SectionPresentation.swift +30 -0
- package/ios/Browser/StyleResolver.swift +11 -0
- package/ios/Browser/TabBarEntries.swift +7 -1
- package/ios/CarPlay/CarPlayController.swift +139 -35
- package/ios/CarPlay/CarPlayListItemFactory.swift +99 -27
- package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +54 -11
- package/ios/Model/NitroTypeStubs.swift +60 -0
- package/ios/Player/MediaIntentCriteria.swift +13 -5
- package/ios/Player/MediaLoader.swift +33 -9
- package/ios/Player/NowPlayingUpdater.swift +53 -5
- package/ios/Player/PlaybackCoordinator.swift +11 -4
- package/ios/Player/PlaybackTimer.swift +8 -2
- package/ios/Player/QueueManager.swift +36 -10
- package/ios/Player/ShuffleOrder.swift +23 -0
- package/ios/Player/SleepTimerHandling.swift +13 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +53 -18
- package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
- package/ios/Tests/JsonModelsTests.swift +76 -1
- package/ios/Tests/QueueManagerTests.swift +72 -6
- package/ios/Tests/RemoteCommandControllerTests.swift +16 -0
- package/ios/Tests/SectionPresentationTests.swift +29 -0
- package/ios/Tests/StyleResolverTests.swift +60 -0
- package/ios/Tests/Support/TestTypes.swift +7 -0
- package/ios/TrackPlayer.swift +57 -34
- package/ios/Util/NetworkMonitor.swift +43 -26
- package/ios/Util/SFSymbolRenderer.swift +24 -2
- package/ios/Util/SVGProcessor.swift +9 -2
- 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/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.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/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/module/web/TrackPlayer/QueueManager.js.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/types/browser-nodes.d.ts +104 -2
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +4 -0
- package/nitrogen/generated/android/c++/JBrowseResult.hpp +6 -0
- package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
- package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +6 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +4 -0
- package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +12 -0
- package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
- package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +4 -0
- package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +6 -0
- package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +6 -0
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +4 -0
- package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JResolvedTrack.hpp +6 -0
- package/nitrogen/generated/android/c++/JSection.hpp +6 -0
- package/nitrogen/generated/android/c++/JSectionStyle.hpp +30 -3
- package/nitrogen/generated/android/c++/JTrack.hpp +4 -0
- package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +4 -0
- package/nitrogen/generated/android/c++/JTrackStyle.hpp +24 -3
- package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +6 -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/ImageShape.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +18 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +15 -3
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +54 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +9 -0
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +9 -0
- package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- package/nitrogen/generated/ios/swift/SectionStyle.swift +70 -1
- package/nitrogen/generated/ios/swift/TrackStyle.swift +59 -1
- package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
- package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
- package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
- package/nitrogen/generated/shared/c++/SectionStyle.hpp +32 -2
- package/nitrogen/generated/shared/c++/TrackStyle.hpp +25 -2
- package/package.json +7 -2
- package/src/features/playback/progress.ts +2 -1
- package/src/features/playback/rate.ts +8 -1
- package/src/style-resolution.test.ts +72 -1
- package/src/types/browser-nodes.ts +112 -2
- package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
- package/src/web/TrackPlayer/QueueManager.ts +14 -0
- 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
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 'Playback errors versus navigation errors: how each surfaces, what the automatic retry budget does, and how to show an advisory error while it retries.'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Errors
|
|
6
|
+
|
|
7
|
+
Two different things can fail, and they surface separately:
|
|
8
|
+
|
|
9
|
+
- **Playback errors** — a track or stream won't play. See
|
|
10
|
+
[Playback errors](#playback-errors).
|
|
11
|
+
- **Navigation errors** — browsing/resolving content failed (a route errored,
|
|
12
|
+
the network dropped, a server returned non-2xx). See
|
|
13
|
+
[Navigation errors](#navigation-errors).
|
|
14
|
+
|
|
15
|
+
For playback, the library can also **retry automatically** — transient failures
|
|
16
|
+
recover on their own, and while it retries the error is surfaced as _advisory_
|
|
17
|
+
so your UI can say what's wrong over the spinner. See
|
|
18
|
+
[Automatic retry](#automatic-retry).
|
|
19
|
+
|
|
20
|
+
The UI snippets import `View` / `Text` / `Button` from `react-native`; everything
|
|
21
|
+
else is from `react-native-audio-browser`.
|
|
22
|
+
|
|
23
|
+
## Playback errors
|
|
24
|
+
|
|
25
|
+
When the active track fails, the [playback state](/guide/playback#playback-state)
|
|
26
|
+
becomes `'error'` and a [`PlaybackError`](/api/features/errors/#playbackerror) is
|
|
27
|
+
available. Read it reactively with
|
|
28
|
+
[`usePlaybackError()`](/api/features/errors/#useplaybackerror) — it returns the
|
|
29
|
+
current error, or `undefined` once playback recovers.
|
|
30
|
+
|
|
31
|
+
`PlaybackError` is `{ kind, code, message, statusCode?, retrying? }`. **Branch
|
|
32
|
+
on `kind`**:
|
|
33
|
+
|
|
34
|
+
| Field | Use it for |
|
|
35
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| `kind` | Everything user-facing. A normalized classification both platforms map onto — see the table below. |
|
|
37
|
+
| `code` | Diagnostics and telemetry **only**. Platform-specific and unstable: loader cases on iOS (`failed-to-load`, …), lower-cased ExoPlayer names on Android (`io-bad-http-status`, …). |
|
|
38
|
+
| `message` | Logs. Hard-coded developer English, e.g. _"Failed to load audio track"_. |
|
|
39
|
+
| `statusCode` | The HTTP status, when the failure came from a response. |
|
|
40
|
+
| `retrying` | `true` while [automatic retry](#automatic-retry) is still working on the failure — the error is advisory, not final. See [Errors while retrying](#errors-while-retrying). |
|
|
41
|
+
|
|
42
|
+
::: warning Don't show `message` to listeners
|
|
43
|
+
It is developer English and it is never localized. Map `kind` to your own copy
|
|
44
|
+
instead — the same error reaches the lock screen and the car, where a technical
|
|
45
|
+
string is especially jarring.
|
|
46
|
+
:::
|
|
47
|
+
|
|
48
|
+
`kind` is one of:
|
|
49
|
+
|
|
50
|
+
| `kind` | Meaning | Retrying help? |
|
|
51
|
+
| ---------------- | --------------------------------------------------------------------------------------------------------------- | ---------------- |
|
|
52
|
+
| `'offline'` | The device had no connection at the moment of failure. The stream may be fine. | Once back online |
|
|
53
|
+
| `'unreachable'` | The host could not be reached: DNS failure, refused connection, timeout, or a connection dropped while loading. | Maybe |
|
|
54
|
+
| `'not-found'` | The server said this stream is gone (HTTP 404 / 410). | No |
|
|
55
|
+
| `'rejected'` | The server refused the request (HTTP 401 / 403) — geo-blocking, an expired token. | No |
|
|
56
|
+
| `'server-error'` | The server responded 5xx. Usually transient. | Maybe |
|
|
57
|
+
| `'unplayable'` | Fetched, but not playable: unknown container, unsupported codec, decoder failure. | No |
|
|
58
|
+
| `'stalled'` | Playback had started, then stopped, and the player exhausted its recovery budget. | Maybe |
|
|
59
|
+
| `'unknown'` | Could not be classified. Inspect `code`. | Maybe |
|
|
60
|
+
|
|
61
|
+
Codes that name no cause stay `'unknown'` rather than being guessed into a
|
|
62
|
+
friendlier bucket — a wrong classification misleads the listener _and_ poisons
|
|
63
|
+
your telemetry aggregates.
|
|
64
|
+
|
|
65
|
+
Map `kind` to your own localized copy. How finely you split is a product
|
|
66
|
+
decision — collapsing several kinds onto one line is fine, as long as the
|
|
67
|
+
listener can still tell _"fix your connection"_ from _"this one is gone"_ from
|
|
68
|
+
_"try again in a minute"_:
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { View, Text, Button } from 'react-native'
|
|
72
|
+
import { usePlaybackError, retry } from 'react-native-audio-browser'
|
|
73
|
+
import type { PlaybackErrorKind } from 'react-native-audio-browser'
|
|
74
|
+
|
|
75
|
+
function errorLine(kind: PlaybackErrorKind): string {
|
|
76
|
+
switch (kind) {
|
|
77
|
+
case 'offline':
|
|
78
|
+
return t('…your "check your connection" copy')
|
|
79
|
+
// Both transient — worth another try, so they can share a line.
|
|
80
|
+
case 'unreachable':
|
|
81
|
+
case 'server-error':
|
|
82
|
+
return t('…your "could not reach it" copy')
|
|
83
|
+
// Both permanent — retrying will not help.
|
|
84
|
+
case 'not-found':
|
|
85
|
+
case 'rejected':
|
|
86
|
+
return t('…your "not available" copy')
|
|
87
|
+
case 'unplayable':
|
|
88
|
+
return t('…your "cannot be played" copy')
|
|
89
|
+
case 'stalled':
|
|
90
|
+
return t('…your "connection lost" copy')
|
|
91
|
+
case 'unknown':
|
|
92
|
+
return t('…your generic fallback copy')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function PlaybackErrorView() {
|
|
97
|
+
const error = usePlaybackError()
|
|
98
|
+
if (!error) return null
|
|
99
|
+
return (
|
|
100
|
+
<View>
|
|
101
|
+
<Text>{errorLine(error.kind)}</Text>
|
|
102
|
+
<Button title="Try again" onPress={() => retry()} />
|
|
103
|
+
</View>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Leaving off the `default` case is deliberate: TypeScript then tells you when a
|
|
109
|
+
new `kind` is added, instead of it silently falling into a generic line.
|
|
110
|
+
|
|
111
|
+
Outside React, use `getPlaybackError()` for a snapshot or
|
|
112
|
+
[`onPlaybackError`](/api/features/errors/#onplaybackerror) to subscribe. The same
|
|
113
|
+
error is also passed to the [Now Playing formatter](/guide/now-playing#the-formatter-derived-continuous),
|
|
114
|
+
so you can show it on the lock screen / car too.
|
|
115
|
+
|
|
116
|
+
### Manual retry
|
|
117
|
+
|
|
118
|
+
[`retry()`](/api/features/errors/#retry) re-attempts the current item — it only
|
|
119
|
+
does something while the state is `'error'`. It's the action behind a "Try again"
|
|
120
|
+
button, and what you call after [automatic retry](#automatic-retry) has given up.
|
|
121
|
+
It begins a fresh load with fresh retry budgets, exactly as if the listener had
|
|
122
|
+
re-selected the track (a plain play command from `'error'` does the same).
|
|
123
|
+
|
|
124
|
+
## Automatic retry
|
|
125
|
+
|
|
126
|
+
Configure [`setupPlayer({ retry })`](/guide/configuration) to recover from
|
|
127
|
+
transient **load** failures (network blips, timeouts, a live stream dropping
|
|
128
|
+
mid-play) with exponential backoff. It's **off by default**, and it's native
|
|
129
|
+
only — the web implementation has no automatic retry, so every web error is
|
|
130
|
+
terminal:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { setupPlayer } from 'react-native-audio-browser'
|
|
134
|
+
|
|
135
|
+
// Retry with the default duration budgets (12s / 2 min, see below):
|
|
136
|
+
await setupPlayer({ retry: true })
|
|
137
|
+
|
|
138
|
+
// Or bound it yourself:
|
|
139
|
+
await setupPlayer({
|
|
140
|
+
retry: {
|
|
141
|
+
maxRetries: 5,
|
|
142
|
+
maxRetryDurationMs: 60_000,
|
|
143
|
+
firstConnectMaxRetryDurationMs: 8_000
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Which failures qualify is nearly the same on iOS and Android: HTTP 408 and
|
|
149
|
+
429, plus server errors — every 5xx on iOS, the common four (500, 502, 503, 504) on Android; transport failures (DNS, timeouts, a dropped connection);
|
|
150
|
+
and anything the platform couldn't classify, which is assumed transient
|
|
151
|
+
rather than fatal.
|
|
152
|
+
Permanent failures are never retried — 403 / 404, and media that can't be
|
|
153
|
+
parsed or decoded.
|
|
154
|
+
|
|
155
|
+
### Two duration budgets
|
|
156
|
+
|
|
157
|
+
Retry state is tracked per **load** — one track's playback session, created
|
|
158
|
+
when a track becomes current (selection, queue advance, skip) _or restarted
|
|
159
|
+
from a terminal error_ (`retry()`, or any play command while in `'error'`),
|
|
160
|
+
and surviving every automatic retry of that track. A new load starts fresh
|
|
161
|
+
budgets; retries within it don't.
|
|
162
|
+
|
|
163
|
+
How long the player keeps trying depends on one piece of evidence: **has this
|
|
164
|
+
load ever produced audio?**
|
|
165
|
+
|
|
166
|
+
| Situation | Budget | Why |
|
|
167
|
+
| --------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
168
|
+
| **First connect** — the load has never played | `firstConnectMaxRetryDurationMs`, default **12s** | A stream that fails before ever playing is usually dead, and the listener is actively waiting for a verdict — seconds, not minutes. |
|
|
169
|
+
| **Recovery** — the load played, then failed | `maxRetryDurationMs`, default **2 min** | Playback proved the stream works. Drops are usually transient (tunnels, network handovers, a station's encoder restarting) and patience recovers them unattended. |
|
|
170
|
+
|
|
171
|
+
While the device is **offline**, the first-connect budget does not apply: the
|
|
172
|
+
player parks and retries the moment connectivity returns, so a stream started
|
|
173
|
+
in a tunnel still plays when the network comes back. Any offline observation
|
|
174
|
+
resets the first-connect clock entirely — after restoration the load gets
|
|
175
|
+
another full first-connect window, because the clock only ever measures a
|
|
176
|
+
contiguous online stretch. `maxRetryDurationMs` is different: it is a
|
|
177
|
+
wall-clock ceiling that **includes** offline time, deliberately — it exists so
|
|
178
|
+
playback can't surprise the listener by resuming after a long time offline.
|
|
179
|
+
|
|
180
|
+
`maxRetries` additionally caps the retry attempts of a load, counted across
|
|
181
|
+
both budgets and across offline/online transitions. (One Android caveat: for
|
|
182
|
+
segmented formats like HLS, ExoPlayer counts attempts per internal loadable —
|
|
183
|
+
playlist and segments separately — so an attempt cap is less predictable there;
|
|
184
|
+
prefer the duration budgets as the bound you rely on.)
|
|
185
|
+
|
|
186
|
+
Backoff delays grow `1s → 1.5s → 2.3s → 3.4s → 5s` (capped). Once a budget is
|
|
187
|
+
exhausted the state lands on `'error'`, where your `PlaybackErrorView` and
|
|
188
|
+
[`retry()`](#manual-retry) take over. Restarting from `'error'` — via
|
|
189
|
+
`retry()` or any play command — begins a **new load** of the same track:
|
|
190
|
+
fresh budgets, starting with the first-connect one. The tap behaves exactly
|
|
191
|
+
like re-selecting the track.
|
|
192
|
+
|
|
193
|
+
### Errors while retrying
|
|
194
|
+
|
|
195
|
+
While the retry loop is working, the player doesn't go silent: each failure is
|
|
196
|
+
classified and surfaced immediately through
|
|
197
|
+
[`onPlaybackChanged`](/api/features/playback/#onplaybackchanged) /
|
|
198
|
+
[`usePlayback()`](/guide/playback#playback-state), attached to the current
|
|
199
|
+
non-terminal state (`'loading'` / `'buffering'`, or `'paused'`) with
|
|
200
|
+
`retrying: true`. Show it as advisory — the cause over a spinner — because the
|
|
201
|
+
next attempt may still succeed:
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
const { state, error } = usePlayback()
|
|
205
|
+
|
|
206
|
+
if (error?.retrying) {
|
|
207
|
+
// Still trying: provisional copy, keep the spinner.
|
|
208
|
+
} else if (state === 'error') {
|
|
209
|
+
// Gave up: final copy, offer retry().
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`onPlaybackError` and `usePlaybackError()` stay **terminal-only**: they fire on
|
|
214
|
+
_both_ edges of the `'error'` state — entering it (with the error) and leaving
|
|
215
|
+
it (with `error: undefined`; the hook returns `undefined` again). Advisory
|
|
216
|
+
errors never pass through them. The same advisory error also reaches the
|
|
217
|
+
[Now Playing formatter](/guide/now-playing#the-formatter-derived-continuous),
|
|
218
|
+
so the lock screen and the car can show it too.
|
|
219
|
+
|
|
220
|
+
Advisory emissions are deduplicated by **classification** (`kind`, plus the
|
|
221
|
+
HTTP status when there is one) — never by the raw native `message`. The scope
|
|
222
|
+
is the current advisory episode: consecutive identical failures within one
|
|
223
|
+
retry sequence produce a single event rather than one per backoff tick, and the
|
|
224
|
+
dedupe resets once the advisory clears (recovery, track change, or hardening
|
|
225
|
+
into the terminal error).
|
|
226
|
+
|
|
227
|
+
## Navigation errors
|
|
228
|
+
|
|
229
|
+
When resolving a browse path fails, a
|
|
230
|
+
[`NavigationError`](/api/features/errors/#navigationerror) is produced. Read the
|
|
231
|
+
raw error (with `code` and HTTP details) via
|
|
232
|
+
[`useNavigationError()`](/api/features/errors/#usenavigationerror), or subscribe
|
|
233
|
+
with `onNavigationError` / snapshot with `getNavigationError()`.
|
|
234
|
+
|
|
235
|
+
`NavigationError` is `{ code, message, statusCode?, statusCodeSuccess? }`, where
|
|
236
|
+
`code` is one of:
|
|
237
|
+
|
|
238
|
+
| `code` | Meaning |
|
|
239
|
+
| --------------------- | --------------------------------------------------------------------------------------------- |
|
|
240
|
+
| `'content-not-found'` | No route for the path, or the route returned no content (a config issue, not an HTTP 404). |
|
|
241
|
+
| `'network-error'` | The request failed (connection error, timeout, no internet). |
|
|
242
|
+
| `'http-error'` | Server returned non-2xx (or 2xx but parsing failed) — see `statusCode` / `statusCodeSuccess`. |
|
|
243
|
+
| `'callback-error'` | Your browse callback returned an error (e.g. auth/subscription required). |
|
|
244
|
+
| `'empty-content'` | The path resolved but the page has no tracks (empty Favorites, no search results). |
|
|
245
|
+
| `'timeout'` | Resolution didn't finish in time. |
|
|
246
|
+
| `'unknown-error'` | An unexpected error (e.g. invalid configuration). |
|
|
247
|
+
|
|
248
|
+
### Showing navigation errors
|
|
249
|
+
|
|
250
|
+
For display, prefer
|
|
251
|
+
[`useFormattedNavigationError()`](/api/features/errors/#useformattednavigationerror)
|
|
252
|
+
— it returns a ready-to-show [`FormattedNavigationError`](/api/features/errors/#formattednavigationerror)
|
|
253
|
+
(`{ title, message? }`). The **same** formatted error drives the CarPlay and
|
|
254
|
+
Android Auto error dialogs, so your in-app and in-car messages stay consistent:
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
import { Text } from 'react-native'
|
|
258
|
+
import { useFormattedNavigationError } from 'react-native-audio-browser'
|
|
259
|
+
|
|
260
|
+
function BrowseError() {
|
|
261
|
+
const error = useFormattedNavigationError()
|
|
262
|
+
if (!error) return null
|
|
263
|
+
const detail = error.message ? ` — ${error.message}` : ''
|
|
264
|
+
return (
|
|
265
|
+
<Text>
|
|
266
|
+
{error.title}
|
|
267
|
+
{detail}
|
|
268
|
+
</Text>
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Without configuration you get sensible English defaults. To localize or override
|
|
274
|
+
specific cases, set `formatNavigationError` in
|
|
275
|
+
[`configureBrowser`](/guide/browser) — return `defaultFormatted` for the cases
|
|
276
|
+
you don't want to change:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { configureBrowser } from 'react-native-audio-browser'
|
|
280
|
+
|
|
281
|
+
configureBrowser({
|
|
282
|
+
formatNavigationError: ({ error, defaultFormatted, path }) => {
|
|
283
|
+
if (error.code === 'http-error') {
|
|
284
|
+
return {
|
|
285
|
+
title: 'Server error',
|
|
286
|
+
message: `Request failed (${error.statusCode})`
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return defaultFormatted // keep the default for everything else
|
|
290
|
+
}
|
|
291
|
+
// ...tabs, routes
|
|
292
|
+
})
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Use the raw `useNavigationError()` when you need to branch on `code` /
|
|
296
|
+
`statusCode`; use `useFormattedNavigationError()` when you just need a title and
|
|
297
|
+
message to render.
|
|
298
|
+
|
|
299
|
+
## API summary
|
|
300
|
+
|
|
301
|
+
| API | Purpose |
|
|
302
|
+
| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
303
|
+
| `usePlaybackError()` / `getPlaybackError()` | The current playback error (`{ kind, code, message, statusCode?, retrying? }`) — branch on `kind`. |
|
|
304
|
+
| `onPlaybackError` | Subscribe to terminal playback errors outside React (fires on entering/leaving `'error'`; advisory retrying errors arrive via `onPlaybackChanged`). |
|
|
305
|
+
| `retry()` | Re-attempt the current item (while state is `'error'`). |
|
|
306
|
+
| `setupPlayer({ retry })` | Automatic retry with two duration budgets: first-connect (12s) and recovery (2 min). Off by default. |
|
|
307
|
+
| `useNavigationError()` / `getNavigationError()` | The raw navigation error (`code`, `statusCode`, …). |
|
|
308
|
+
| `onNavigationError` | Subscribe to navigation errors outside React. |
|
|
309
|
+
| `useFormattedNavigationError()` / `getFormattedNavigationError()` | Display-ready `{ title, message? }`, shared with CarPlay / Android Auto. |
|
|
310
|
+
| `configureBrowser({ formatNavigationError })` | Customize / localize the formatted messages. |
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 'Let listeners heart the active track from the now-playing screen, notification, CarPlay, and Android Auto, and surface a Favorites tab and voice command.'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Favorites
|
|
6
|
+
|
|
7
|
+
**Favorites** let a listener mark the active track with a heart — on the now-playing screen, the notification, CarPlay, and Android Auto — and let you surface a "Favorites" tab and a "play my favorites" voice command. The library tracks favorite _state_ (which track is favorited, and keeping the heart in sync everywhere); **your app owns the collection** (where favorites are stored and how they persist).
|
|
8
|
+
|
|
9
|
+
## Enabling favorites
|
|
10
|
+
|
|
11
|
+
Favoriting is off until you enable the `favorite` capability in `setupPlayer` (or `updateOptions`):
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { setupPlayer } from 'react-native-audio-browser'
|
|
15
|
+
|
|
16
|
+
await setupPlayer({
|
|
17
|
+
capabilities: { favorite: true }
|
|
18
|
+
})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The identifiers you pass to [`setFavorites`](#hydrating-favorite-state) are compared against each track's **identity**: its `id` when set, falling back to its `src`. Store the same stable identifier you put in `Track.id` — or, for tracks without ids, the full `src`:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { setFavorites } from 'react-native-audio-browser'
|
|
25
|
+
|
|
26
|
+
// track = { id: 'abc123', src: 'https://stream.example.com/jazz.mp3' }
|
|
27
|
+
setFavorites(['abc123']) // matches by id
|
|
28
|
+
|
|
29
|
+
// id-less track = { src: 'https://stream.example.com/jazz.mp3' }
|
|
30
|
+
setFavorites(['https://stream.example.com/jazz.mp3']) // matches by src
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Hydrating favorite state
|
|
34
|
+
|
|
35
|
+
The library keeps a native cache of which tracks are favorited, so it paints hearts across browse rows and the Now Playing heart without a round-trip to JS — and **without your content API having to know which tracks are favorites**. Declare your favorites once with `setFavorites`; the library stamps `favorited` on any track whose [identity](#enabling-favorites) matches, wherever that track appears. Your content endpoints stay favorites-agnostic — they return the same tracks for everyone, and favorite state lives entirely in your app/user storage.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { setFavorites } from 'react-native-audio-browser'
|
|
39
|
+
|
|
40
|
+
// string[] of track identities: ids, or src URLs for id-less tracks
|
|
41
|
+
const favorites = await loadFavoritesFromStorage()
|
|
42
|
+
setFavorites(favorites)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You only need `setFavorites` for the initial hydrate. After that, taps on the system heart and calls to [`setActiveTrackFavorited`](#favoriting-from-your-own-ui) update the cache for you.
|
|
46
|
+
|
|
47
|
+
Your API can also return `favorited: true` on a track directly. A caller-set flag always wins over hydration for display — the library never overwrites it — but it does **not** fill the favorites cache: it only affects the response it arrived in, so other appearances of the same track stay unhearted. Treat `setFavorites` as the source of truth, and server-supplied `favorited` as a per-response display override.
|
|
48
|
+
|
|
49
|
+
## Favoriting from system controls
|
|
50
|
+
|
|
51
|
+
With the capability enabled, a heart appears on the now-playing surfaces. Add it to the Android notification and the CarPlay now-playing screen explicitly:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { setupPlayer } from 'react-native-audio-browser'
|
|
55
|
+
|
|
56
|
+
await setupPlayer({
|
|
57
|
+
capabilities: { favorite: true },
|
|
58
|
+
android: {
|
|
59
|
+
remoteButtonLayout: {
|
|
60
|
+
back: 'skip-to-previous',
|
|
61
|
+
forward: 'skip-to-next',
|
|
62
|
+
overflow: ['favorite']
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
ios: { carPlayNowPlayingButtons: ['favorite'] }
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
When the listener taps a system heart, the library updates its cache and the UI, then emits `onFavoriteChanged` so **you can persist the change** to your storage or backend:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { onFavoriteChanged } from 'react-native-audio-browser'
|
|
73
|
+
|
|
74
|
+
const unsubscribe = onFavoriteChanged.addListener(({ track, favorited }) => {
|
|
75
|
+
if (favorited) addToFavorites(track)
|
|
76
|
+
else removeFromFavorites(track)
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The native side has already updated the now-playing heart by the time this fires, so your handler just persists the change — then refreshes the rest (see [Keep it in sync](#keep-it-in-sync)).
|
|
81
|
+
|
|
82
|
+
## Favoriting from your own UI
|
|
83
|
+
|
|
84
|
+
To drive favoriting from an in-app button (rather than a system control), set or toggle the active track's state directly. These update the same cache and system hearts:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import {
|
|
88
|
+
setActiveTrackFavorited,
|
|
89
|
+
toggleActiveTrackFavorited
|
|
90
|
+
} from 'react-native-audio-browser'
|
|
91
|
+
|
|
92
|
+
setActiveTrackFavorited(true)
|
|
93
|
+
toggleActiveTrackFavorited()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
These are the programmatic counterpart to a system heart tap — persist the result yourself, the same way you would in `onFavoriteChanged`.
|
|
97
|
+
|
|
98
|
+
## Surfacing favorites
|
|
99
|
+
|
|
100
|
+
How you surface favorites in your [browse tree](/guide/basic-usage) is up to you — a top-level tab, a nested entry, or a group within a larger list. Whatever the placement, point its route at the listener's favorited tracks:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { configureBrowser } from 'react-native-audio-browser'
|
|
104
|
+
|
|
105
|
+
configureBrowser({
|
|
106
|
+
tabs: [
|
|
107
|
+
{ title: 'Browse', path: '/browse' },
|
|
108
|
+
{ title: 'Favorites', path: '/favorites' }
|
|
109
|
+
],
|
|
110
|
+
routes: {
|
|
111
|
+
'/favorites': async () => ({
|
|
112
|
+
path: '/favorites',
|
|
113
|
+
title: 'Favorites',
|
|
114
|
+
// your stored favorites, as a Track[]
|
|
115
|
+
children: await loadFavoriteTracks()
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Resolve it however your collection lives — from local storage, or from your API (with an HTTP route that posts your stored identifiers). It renders natively on CarPlay and Android Auto like any other browse content.
|
|
122
|
+
|
|
123
|
+
### Keep it in sync
|
|
124
|
+
|
|
125
|
+
The library caches each browse route, so the `/favorites` route keeps showing its old contents until you tell it to re-fetch. **Whenever the favorites collection changes** — a system heart tap, your own UI, or a sync from another device — do two things:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { setFavorites, notifyContentChanged } from 'react-native-audio-browser'
|
|
129
|
+
|
|
130
|
+
// refresh the hearts (favorited flag)
|
|
131
|
+
setFavorites(updatedIds)
|
|
132
|
+
// re-fetch the /favorites content
|
|
133
|
+
notifyContentChanged('/favorites')
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
These hit two different caches: `setFavorites` updates the `favorited` flag wherever a track appears, while [`notifyContentChanged(path)`](/api/) re-runs the route handler for that one path and refreshes any surface currently showing it. (Use `invalidateAllContent()` instead only when _everything_ should re-fetch — e.g. a locale switch or sign-out.) Driving both from a single place that observes your favorites list — rather than from each individual toggle — keeps every source of change covered.
|
|
137
|
+
|
|
138
|
+
## Searching within favorites
|
|
139
|
+
|
|
140
|
+
Voice intents can target the favorites collection — both "play my favorites" and a scoped search like "play my jazz". These arrive at your `search` source with `reference: 'my'`. See [Search → Playing the user's own collection](/guide/search#playing-the-users-own-collection) for how to resolve them against a local or server collection.
|
|
141
|
+
|
|
142
|
+
## API summary
|
|
143
|
+
|
|
144
|
+
| API | Purpose |
|
|
145
|
+
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
146
|
+
| `capabilities: { favorite: true }` | Enable favoriting and the system hearts. |
|
|
147
|
+
| `setFavorites(ids)` | Hydrate the favorites cache; ids match each track's identity (`id`, falling back to `src`). |
|
|
148
|
+
| `track.favorited` | Per-track display flag; a caller-set value wins over hydration but does not fill the cache. |
|
|
149
|
+
| `setActiveTrackFavorited(bool)` / `toggleActiveTrackFavorited()` | Favorite the active track from your own UI. |
|
|
150
|
+
| `onFavoriteChanged` | Subscribe to system heart taps to persist the change. |
|
|
151
|
+
| `notifyContentChanged('/favorites')` | Re-fetch the `/favorites` content after the collection changes. |
|
|
152
|
+
| `'favorite'` in `android.remoteButtonLayout` | Heart in the Android notification, Android Auto, and the system media controls. A layout replaces the defaults wholesale, so list the other buttons you still want. |
|
|
153
|
+
| `ios.carPlayNowPlayingButtons: ['favorite']` | Heart on the CarPlay now-playing screen. |
|
|
154
|
+
| `search` source with `reference: 'my'` | Resolve "play my favorites" / favorites search. |
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 'Block premium, login-only, or region-locked content on CarPlay and Android Auto with a short message — without touching your in-app UI or interrupting playback.'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Gate
|
|
6
|
+
|
|
7
|
+
When part of your catalog is premium, login-only, or region-locked, you need a way to block it on the car surfaces — without building a separate UI for them. That's what a **Gate** is: you hand the library a short message, and it shows that in place of the browse content on **CarPlay** and **Android Auto**, and can turn away voice searches.
|
|
8
|
+
|
|
9
|
+
One rule worth keeping in mind: **a Gate blocks _finding_, never _hearing_.** Whatever is already playing keeps going, "resume" and "play this" still work by voice, and the queue and now-playing are left alone.
|
|
10
|
+
|
|
11
|
+
Two things it never touches:
|
|
12
|
+
|
|
13
|
+
- **Your own in-app browse UI.** The Gate only covers content the _library_ serves; the screens you draw in your app are yours to gate with your own paywall.
|
|
14
|
+
- **Playback.** It lives on the find/resolve path, so audio is never interrupted.
|
|
15
|
+
|
|
16
|
+
It follows that the Gate is **iOS and Android only**. On web `setGate` and `clearGate` are no-ops and `onGate` never fires — there's no CarPlay or Android Auto surface to cover, and in-app browse UI was never the Gate's job on any platform. Calling them from shared code is safe and does nothing.
|
|
17
|
+
|
|
18
|
+
::: warning Voice search isn't only a "car" thing
|
|
19
|
+
A Siri request ("Hey Siri, play jazz on App") can come from the phone or lock screen, not just CarPlay — and it still goes through the Gate. That's why the common setup below **gates browsing but lets search through**: voice-play keeps working everywhere, while the car's browse tabs stay walled off.
|
|
20
|
+
:::
|
|
21
|
+
|
|
22
|
+
## Setting a gate
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { setGate, clearGate } from 'react-native-audio-browser'
|
|
26
|
+
|
|
27
|
+
setGate({ title: 'Premium', message: 'Subscribe to browse in the car.' })
|
|
28
|
+
// …later, when the user is entitled:
|
|
29
|
+
clearGate()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
[`setGate`](/api/features/gate/#setgate) raises the gate (or updates it in place); [`clearGate`](/api/features/gate/#cleargate) drops it and the real content returns. Set it before the car connects and it'll be there the moment it does. A gate is global and singular — calling `setGate` again replaces the current one.
|
|
33
|
+
|
|
34
|
+
A Gate is **independent** of `setupPlayer` and `configureBrowser` — call `setGate` any time after import. The usual lifecycle is: set it once at startup, then `clearGate` (or re-`setGate`) whenever the user's entitlement changes (purchase, login, logout).
|
|
35
|
+
|
|
36
|
+
A gate is just a title and an optional message. How it renders depends on the surface:
|
|
37
|
+
|
|
38
|
+
- **CarPlay** — a full-page centered message.
|
|
39
|
+
- **Android Auto** — one non-playable list tile: the **title** as its main line, the **message** as the subtitle (newlines collapse to spaces). It's a tile, not a full page like CarPlay, so keep both short.
|
|
40
|
+
|
|
41
|
+
There's no action button: Android Auto can't show one, and a car surface can't run a purchase flow anyway (see [Reacting to gate hits](#reacting-to-gate-hits) for the deferred-upsell pattern).
|
|
42
|
+
|
|
43
|
+
::: warning Updating a live gate resets CarPlay navigation
|
|
44
|
+
Calling `setGate` while a gate is already up replaces it in place — but on CarPlay it pops any drilled-in navigation back to the tab root. Harmless for a set-once gate; worth knowing if you update the message mid-session.
|
|
45
|
+
:::
|
|
46
|
+
|
|
47
|
+
## Gating some requests, not others
|
|
48
|
+
|
|
49
|
+
Pass a [**resolver**](/api/features/gate/#gateresolver) as the second argument. The library calls it for every browse navigation and every search, and your return value decides what happens:
|
|
50
|
+
|
|
51
|
+
- return **`false`** — let the request through
|
|
52
|
+
- return **`true`** — gate it with the default message (the first `setGate` argument)
|
|
53
|
+
- return a [**gate object**](/api/features/gate/#gate) `{ title, message? }` — gate it with a different message, just for this one
|
|
54
|
+
|
|
55
|
+
The [request](/api/features/gate/#gaterequest) tells you what's being asked: `request.kind` is `'browse'` or `'search'`. A browse request carries a `request.path`; a search request carries `request.params` (the parsed query — see the [Search](/guide/search) guide).
|
|
56
|
+
|
|
57
|
+
The common case — **gate browsing, but leave search open** — so users can still play by voice:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
setGate(
|
|
61
|
+
{ title: 'Premium', message: 'Subscribe to browse in the car.' },
|
|
62
|
+
(request) => request.kind === 'browse'
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
::: warning The resolver is synchronous
|
|
67
|
+
A resolver returns `Gate | boolean` — never a Promise — so you can't `await` an auth check inside it. Rather than check entitlement in the resolver, drive the gate from a single place: gate while locked, `clearGate` once entitled.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const gateBrowsing = () =>
|
|
71
|
+
setGate(
|
|
72
|
+
{ title: 'Premium', message: 'Subscribe to browse in the car.' },
|
|
73
|
+
(request) => request.kind === 'browse' // gate browse, allow search
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if (!isEntitled()) gateBrowsing() // your code, at startup
|
|
77
|
+
|
|
78
|
+
// entitlement changed (purchase / login / logout):
|
|
79
|
+
function onEntitlementChange(entitled: boolean) {
|
|
80
|
+
entitled ? clearGate() : gateBrowsing() // refresh the car surfaces
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
:::
|
|
85
|
+
|
|
86
|
+
Because the resolver sees the request, you can also gate by path — block one premium branch while the rest stays open — or show a different message per request:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
setGate({ title: 'Premium', message: 'Subscribe to unlock.' }, (request) => {
|
|
90
|
+
if (request.kind === 'search') return false // search allowed
|
|
91
|
+
if (request.path.startsWith('/premium')) return true // gate this branch
|
|
92
|
+
return false // everything else
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
A search request carries `request.params` — the parsed query (`query`, `genre`, `artist`, … — see [Search](/guide/search)) — so you can gate only premium queries:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
setGate({ title: 'Premium', message: 'Subscribe to unlock.' }, (request) => {
|
|
100
|
+
if (request.kind === 'search') {
|
|
101
|
+
return request.params.genre === 'premium' // gate premium genres
|
|
102
|
+
}
|
|
103
|
+
return request.path.startsWith('/premium')
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
You can also pass **only** a resolver, with no default message — return a gate object for full control, or `true` to fall back to the generic built-in chrome. That fallback is a title only (`"Unavailable"`, no body), so on CarPlay its message area is blank — pass a default gate or return a gate object if you want body copy.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
setGate((request) => request.kind === 'browse')
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
If the resolver throws, the gate **fails closed** — the library gates the request rather than leak content. A deliberate `return false` is the only thing that lets a request through.
|
|
114
|
+
|
|
115
|
+
## Reacting to gate hits
|
|
116
|
+
|
|
117
|
+
You can't complete a purchase on CarPlay or Android Auto — so the pattern is to **record** that a user bumped into the gate and surface the upsell next time your phone app opens. Subscribe to [`onGate`](/api/features/gate/#ongate):
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { onGate } from 'react-native-audio-browser'
|
|
121
|
+
|
|
122
|
+
// addListener returns an unsubscribe function — keep it and call it to
|
|
123
|
+
// clean up.
|
|
124
|
+
const unsubscribe = onGate.addListener((event) => {
|
|
125
|
+
// event.reason ('browse' | 'search') is the same axis as the resolver's
|
|
126
|
+
// request.kind. The event carries only the reason, not the path/params.
|
|
127
|
+
recordGateHit() // your own persisted flag
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
In a component, wire it through `useEffect` so it unsubscribes on unmount:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
useEffect(() => onGate.addListener((event) => recordGateHit()), [])
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`onGate` fires **once per gated serve**, whether the gate is static or resolver-driven. There's no de-duplication, so poking around a gated car UI fires several events — debounce on your side (once per session, per day, whatever fits) if you don't want them all.
|
|
138
|
+
|
|
139
|
+
Then, on your next app launch, read the flag and show the upsell:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
if (consumeGateHit()) showPremiumUpsell() // your code: read + clear flag
|
|
143
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 'Install react-native-audio-browser and its react-native-nitro-modules peer dependency, then complete the iOS and Android native setup.'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Getting Started
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
The library requires **React Native 0.76 or newer** (with the New Architecture enabled) and React 18.2+. `react-native-nitro-modules` is a required peer dependency — install it alongside:
|
|
10
|
+
|
|
11
|
+
::: code-group
|
|
12
|
+
|
|
13
|
+
```bash [npm]
|
|
14
|
+
npm install react-native-audio-browser react-native-nitro-modules
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash [yarn]
|
|
18
|
+
yarn add react-native-audio-browser react-native-nitro-modules
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```bash [pnpm]
|
|
22
|
+
pnpm add react-native-audio-browser react-native-nitro-modules
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```bash [bun]
|
|
26
|
+
bun add react-native-audio-browser react-native-nitro-modules
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
:::
|
|
30
|
+
|
|
31
|
+
## Basic Setup
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { setupPlayer } from 'react-native-audio-browser'
|
|
35
|
+
|
|
36
|
+
// Initialize the player once at startup
|
|
37
|
+
await setupPlayer()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
> The library exposes both named exports (`import { setupPlayer }`) and a default namespace (`import AudioBrowser from '…'; AudioBrowser.setupPlayer()`). The guides use the named form throughout.
|
|
41
|
+
|
|
42
|
+
## Next Steps
|
|
43
|
+
|
|
44
|
+
- [Basic Usage](/guide/basic-usage) - Learn how to play audio and build navigation trees
|
|
45
|
+
- [Android Auto](/guide/android-auto) - Set up Android Auto integration
|
|
46
|
+
- [CarPlay](/guide/carplay) - Set up CarPlay integration
|