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,238 @@
1
+ ---
2
+ description: 'The React hooks that keep your UI in sync with playback, the queue, the browse tree, and the device — including changes driven from outside your app.'
3
+ ---
4
+
5
+ # Hooks
6
+
7
+ The library exposes its state as **React hooks** — call one in a component and it returns the current value and re-renders whenever that value changes. They keep your in-app UI in sync with playback, the queue, the browse tree, and the device, including changes driven from _outside_ your app (the lock screen, CarPlay, Android Auto, Bluetooth).
8
+
9
+ ```tsx
10
+ import { useActiveTrack, usePlayingState } from 'react-native-audio-browser'
11
+
12
+ function NowPlaying() {
13
+ const track = useActiveTrack()
14
+ const { playing } = usePlayingState()
15
+ return (
16
+ <Text>
17
+ {playing ? 'Playing' : 'Paused'}: {track?.title}
18
+ </Text>
19
+ )
20
+ }
21
+ ```
22
+
23
+ ## The getter / hook / event trio
24
+
25
+ Most state comes in three forms, so you can read it whichever way fits:
26
+
27
+ | Form | Shape | Use it |
28
+ | ---------- | -------- | ------------------------------------------------------------ |
29
+ | **Hook** | `useX()` | inside a component — re-renders on change |
30
+ | **Getter** | `getX()` | a one-off synchronous read, anywhere |
31
+ | **Event** | `on…` | `onX.addListener(cb)` outside React — returns an unsubscribe |
32
+
33
+ Most events are named `on<Thing>Changed`, but several aren't — `onProgressUpdated`, `onPlayingState`, `onPlaybackError`, `onNavigationError`. The exact event for each hook is in the [reference table](#all-hooks-at-a-glance), so don't guess the name.
34
+
35
+ ```ts
36
+ import { getPlayback, onPlaybackChanged } from 'react-native-audio-browser'
37
+
38
+ const now = getPlayback() // read once
39
+ const stop = onPlaybackChanged.addListener((p) => console.log(p.state)) // subscribe
40
+ // stop() to unsubscribe
41
+ ```
42
+
43
+ Every hook, getter, and event is a named export from `react-native-audio-browser`. Hooks read a cached value synchronously on first render (no flash of empty state) and then subscribe for you. Mutating state is separate — `setX()` / action functions (`play`, `setQueue`, `setShuffle`, `setRepeatMode`, `setPlayWhenReady`, …) — covered in [Basic Usage](/guide/basic-usage).
44
+
45
+ ## Playback
46
+
47
+ The transport state — what the player is doing right now.
48
+
49
+ ```tsx
50
+ import {
51
+ usePlayback,
52
+ usePlayingState,
53
+ useProgress
54
+ } from 'react-native-audio-browser'
55
+
56
+ function Transport() {
57
+ const { state } = usePlayback() // e.g. 'playing' | 'paused' (full list below)
58
+ const { playing, buffering } = usePlayingState()
59
+ const { position, duration, buffered } = useProgress() // seconds
60
+ return <Scrubber position={position} duration={duration} />
61
+ }
62
+ ```
63
+
64
+ - [**`usePlayback()`**](/api/features/playback/#useplayback) → `{ state, error? }`. `state` is one of `none`, `ready`, `loading`, `buffering`, `playing`, `paused`, `stopped`, `ended`, `error`.
65
+ - [**`usePlayingState()`**](/api/features/playback/#useplayingstate) → `{ playing, buffering }`. A lighter hook when you only need the booleans.
66
+ - [**`useProgress()`**](/api/features/playback/#useprogress) → [`Progress`](/api/features/playback/#progress) (`{ position, duration, buffered }`) in seconds. Event-driven, but **off by default**: `progressUpdateEventInterval` is `null` until you set it (in seconds) via `updateOptions({ progressUpdateEventInterval: 0.5 })` — otherwise the value never ticks.
67
+ - [**`usePolledProgress(updateInterval = 1000)`**](/api/features/playback/#usepolledprogress) → same [`Progress`](/api/features/playback/#progress), but polled on its own timer (milliseconds) instead of event-driven, so it works without `progressUpdateEventInterval`. Good for a smooth scrubber; it pauses while the app is backgrounded.
68
+ - [**`usePlayWhenReady()`**](/api/features/playback/#useplaywhenready) → `boolean`: whether the player will start as soon as content is ready.
69
+ - [**`useSystemVolume()`**](/api/features/playback/#usesystemvolume) → `number` 0–1, the device volume (read-only on iOS).
70
+
71
+ ::: tip `useProgress` vs `usePolledProgress`
72
+ `useProgress` emits at the global `progressUpdateEventInterval` (and stays still until you set it); `usePolledProgress` ignores that and ticks on its own timer. For a scrubber, `usePolledProgress` is usually the simpler choice.
73
+ :::
74
+
75
+ ## Queue and active track
76
+
77
+ ```tsx
78
+ import {
79
+ useActiveTrack,
80
+ useQueue,
81
+ useRepeatMode,
82
+ useShuffle
83
+ } from 'react-native-audio-browser'
84
+
85
+ function QueueScreen() {
86
+ const active = useActiveTrack() // Track | undefined
87
+ const queue = useQueue() // Track[]
88
+ const repeat = useRepeatMode() // 'off' | 'track' | 'queue'
89
+ const shuffle = useShuffle() // boolean
90
+ return <List data={queue} highlight={active?.id} />
91
+ }
92
+ ```
93
+
94
+ [`useActiveTrack`](/api/features/queue/#useactivetrack) updates even when the track changes from an external control (car next/previous), so matching on the track's identity keeps your UI correct. Note `Track.id` is **optional** (and may be undefined for a browse item the user picked in the car without you queuing it) — fall back to `src` / `path` if you rely on `id`. See [Track](/guide/track) for the Track shape.
95
+
96
+ ## Now playing
97
+
98
+ [**`useNowPlaying()`**](/api/features/nowPlaying/#usenowplaying) → [`NowPlayingMetadata`](/api/features/metadata/#nowplayingmetadata)` | undefined` — the metadata shown on the lock screen and car surfaces. It reflects any override you set via `updateNowPlaying()` (e.g. the current song on a live stream), falling back to the active track otherwise. See [Now Playing](/guide/now-playing).
99
+
100
+ ```tsx
101
+ const meta = useNowPlaying()
102
+ // { title?, artist?, album?, artwork?, genre?, duration?,
103
+ // description?, mediaId?, elapsedTime? }
104
+ ```
105
+
106
+ ## Browsing
107
+
108
+ Mirror the browse tree your app draws — the same state CarPlay and Android Auto render.
109
+
110
+ - [**`useTabs()`**](/api/features/browser/#usetabs) → [`Track`](/api/types/browser-nodes/#track)`[] | undefined` — the top-level tabs.
111
+ - [**`usePath()`**](/api/features/browser/#usepath) → `string | undefined` — the current browse path.
112
+ - [**`useContent()`**](/api/features/browser/#usecontent) → [`ResolvedTrack`](/api/types/browser-nodes/#resolvedtrack)` | undefined` — the resolved page (`content?.sections` to render — each a titled, styled group of tracks).
113
+
114
+ ```tsx
115
+ import { useContent, navigate } from 'react-native-audio-browser'
116
+
117
+ function Browse() {
118
+ const page = useContent()
119
+ // each section: { title?, style?, path?, children: Track[] }
120
+ return (
121
+ <List
122
+ sections={page?.sections ?? []}
123
+ onSelect={(track) => navigate(track)}
124
+ />
125
+ )
126
+ }
127
+ ```
128
+
129
+ See [Browser](/guide/browser) for how navigation and content resolution work.
130
+
131
+ ## Sleep timer
132
+
133
+ - [**`useSleepTimerActive()`**](/api/features/sleepTimer/#usesleeptimeractive) → `boolean` — lightweight "is a timer set?" check.
134
+ - [**`useSleepTimer(params?)`**](/api/features/sleepTimer/#usesleeptimer) → the live [`SleepTimerState`](/api/features/sleepTimer/#sleeptimerstate), or `undefined` when none is set:
135
+ - `{ time, secondsLeft }` for a countdown timer (`time` is the epoch-ms fire time), or
136
+ - `{ sleepWhenPlayedToEnd: true }` for an end-of-track timer.
137
+
138
+ ```tsx
139
+ const timer = useSleepTimer({ updateInterval: 1000 })
140
+ // timer && 'secondsLeft' in timer → show timer.secondsLeft
141
+ ```
142
+
143
+ `params` takes `updateInterval` (countdown tick rate, default 1000ms) and `inactive` (set `true` while backgrounded to pause ticking). `secondsLeft` does not advance in the background.
144
+
145
+ ## Device and environment
146
+
147
+ | Hook | Returns | Notes |
148
+ | ------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------- |
149
+ | [`useOnline()`](/api/features/network/#useonline) | `boolean` | network connectivity |
150
+ | [`useCarConnected()`](/api/features/carConnection/#usecarconnected) | `boolean` | CarPlay / Android Auto connected (`false` on web) |
151
+ | [`useSystemVolume()`](/api/features/playback/#usesystemvolume) | `number` | device volume 0–1 |
152
+ | [`useOutput()`](/api/features/output/#useoutput) | [`Output`](/api/features/output/#output)` \| undefined` | current audio output (iOS + Android) |
153
+
154
+ ```tsx
155
+ const output = useOutput() // { type, name, external } | undefined
156
+ // e.g. output?.type === 'bluetooth', output?.external === true
157
+ ```
158
+
159
+ `useOutput` updates as routes change (AirPods connect, a Bluetooth speaker is selected, etc.); it's `undefined` when unknown, and Android reports coarser `type`s. See [Audio Output](/guide/audio-output).
160
+
161
+ ## Errors
162
+
163
+ - [**`usePlaybackError()`**](/api/features/errors/#useplaybackerror) → [`PlaybackError`](/api/features/errors/#playbackerror)` | undefined` — playback failures, with a normalized `kind` (`offline`, `unreachable`, `not-found`, `rejected`, `unplayable`, `stalled`, …) to branch on, plus a platform-specific `code` and optional `statusCode` for telemetry. Never show its `message` — see [Errors](/guide/errors#playback-errors).
164
+ - [**`useNavigationError()`**](/api/features/errors/#usenavigationerror) → [`NavigationError`](/api/features/errors/#navigationerror)` | undefined` — a browse/search failure, with a typed `code` (`network-error`, `http-error`, `timeout`, `empty-content`, …) and optional `statusCode`.
165
+ - [**`useFormattedNavigationError()`**](/api/features/errors/#useformattednavigationerror) → `{ title, message } | undefined` — the same error run through your `formatNavigationError` config (the copy CarPlay and Android Auto show), ready to display.
166
+
167
+ ```tsx
168
+ const error = useFormattedNavigationError()
169
+ return error ? <Banner title={error.title} body={error.message} /> : null
170
+ ```
171
+
172
+ ## Player options
173
+
174
+ [**`useOptions()`**](/api/features/player/#useoptions) → the current [`Options`](/api/features/player/#options) (jump intervals, `capabilities`, and the `android` / `ios` blocks). Mutate with `updateOptions(partial)`.
175
+
176
+ ```tsx
177
+ const { forwardJumpInterval, capabilities } = useOptions()
178
+ ```
179
+
180
+ ## Android-only hooks
181
+
182
+ **Battery** (Android background-restriction state — all `false`/`unrestricted` on iOS):
183
+
184
+ - [**`useBatteryWarning()`**](/api/features/battery/#usebatterywarning) → `{ pending, status, dismiss, openSettings }` — the meta-hook for a complete warning UI; `dismiss()` and `openSettings()` are ready-to-wire actions.
185
+ - [**`useBatteryWarningPending()`**](/api/features/battery/#usebatterywarningpending) → `boolean` and [**`useBatteryOptimizationStatus()`**](/api/features/battery/#usebatteryoptimizationstatus) → `'unrestricted' | 'optimized' | 'restricted'` are the individual pieces.
186
+
187
+ ```tsx
188
+ const { pending, status, dismiss, openSettings } = useBatteryWarning()
189
+ ```
190
+
191
+ **Equalizer**: [**`useEqualizerSettings()`**](/api/features/equalizer/#useequalizersettings) → [`EqualizerSettings`](/api/features/equalizer/#equalizersettings)` | undefined` (`bandLevels`, `presets`, `enabled`, …); `undefined` on iOS. Drive it with `setEqualizerEnabled` / `setEqualizerPreset` / `setEqualizerLevels`.
192
+
193
+ ## Debugging
194
+
195
+ **`useDebug(options?)`** → `{ state, logs, clear }` — a live snapshot of all player state plus a change log (last 100 entries). It's a development aid; `options.enabled` defaults to `__DEV__`.
196
+
197
+ ```tsx
198
+ const { state, logs, clear } = useDebug({ metadata: true })
199
+ ```
200
+
201
+ ## All hooks at a glance
202
+
203
+ The **Event** column is the emitter name for the getter/event form — handy because the names aren't fully uniform.
204
+
205
+ The **Hook** name links to its full API entry; the **Returns** type links to its definition where it has one.
206
+
207
+ | Hook | Returns | Event | Platform |
208
+ | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------ | ------------------- |
209
+ | [`usePlayback`](/api/features/playback/#useplayback) | `{ state, error? }` | `onPlaybackChanged` | all |
210
+ | [`usePlayingState`](/api/features/playback/#useplayingstate) | `{ playing, buffering }` | `onPlayingState` | all |
211
+ | [`useProgress`](/api/features/playback/#useprogress) | `{ position, duration, buffered }` | `onProgressUpdated` | all |
212
+ | [`usePolledProgress`](/api/features/playback/#usepolledprogress) | `{ position, duration, buffered }` | _(polled)_ | all |
213
+ | [`usePlayWhenReady`](/api/features/playback/#useplaywhenready) | `boolean` | `onPlayWhenReadyChanged` | all |
214
+ | [`useSystemVolume`](/api/features/playback/#usesystemvolume) | `number` | `onSystemVolumeChanged` | all (read-only iOS) |
215
+ | [`useActiveTrack`](/api/features/queue/#useactivetrack) | [`Track`](/api/types/browser-nodes/#track)` \| undefined` | `onActiveTrackChanged` | all |
216
+ | [`useQueue`](/api/features/queue/#usequeue) | [`Track`](/api/types/browser-nodes/#track)`[]` | `onQueueChanged` | all |
217
+ | [`useRepeatMode`](/api/features/queue/#userepeatmode) | [`RepeatMode`](/api/features/queue/#repeatmode) | `onRepeatModeChanged` | all |
218
+ | [`useShuffle`](/api/features/queue/#useshuffle) | `boolean` | `onShuffleChanged` | all |
219
+ | [`useNowPlaying`](/api/features/nowPlaying/#usenowplaying) | `NowPlayingMetadata \| undefined` | `onNowPlayingChanged` | all |
220
+ | [`useTabs`](/api/features/browser/#usetabs) | [`Track`](/api/types/browser-nodes/#track)`[] \| undefined` | `onTabsChanged` | all |
221
+ | [`usePath`](/api/features/browser/#usepath) | `string \| undefined` | `onPathChanged` | all |
222
+ | [`useContent`](/api/features/browser/#usecontent) | [`ResolvedTrack`](/api/types/browser-nodes/#resolvedtrack)` \| undefined` | `onContentChanged` | all |
223
+ | [`useSleepTimerActive`](/api/features/sleepTimer/#usesleeptimeractive) | `boolean` | `onSleepTimerChanged` | all |
224
+ | [`useSleepTimer`](/api/features/sleepTimer/#usesleeptimer) | [`SleepTimerState`](/api/features/sleepTimer/#sleeptimerstate)` \| undefined` | `onSleepTimerChanged` | all |
225
+ | [`useOnline`](/api/features/network/#useonline) | `boolean` | `onOnlineChanged` | all |
226
+ | [`useCarConnected`](/api/features/carConnection/#usecarconnected) | `boolean` | `onCarConnectedChanged` | all (`false` web) |
227
+ | [`useOutput`](/api/features/output/#useoutput) | [`Output`](/api/features/output/#output)` \| undefined` | `onOutputChanged` | iOS, Android |
228
+ | [`usePlaybackError`](/api/features/errors/#useplaybackerror) | [`PlaybackError`](/api/features/errors/#playbackerror)` \| undefined` | `onPlaybackError` | all |
229
+ | [`useNavigationError`](/api/features/errors/#usenavigationerror) | [`NavigationError`](/api/features/errors/#navigationerror)` \| undefined` | `onNavigationError` | all |
230
+ | [`useFormattedNavigationError`](/api/features/errors/#useformattednavigationerror) | `{ title, message } \| undefined` | `onFormattedNavigationError` | all |
231
+ | [`useOptions`](/api/features/player/#useoptions) | [`Options`](/api/features/player/#options) | `onOptionsChanged` | all |
232
+ | [`useBatteryWarning`](/api/features/battery/#usebatterywarning) | `{ pending, status, dismiss, openSettings }` | _(composite)_ | Android |
233
+ | [`useBatteryWarningPending`](/api/features/battery/#usebatterywarningpending) | `boolean` | `onBatteryWarningPendingChanged` | Android |
234
+ | [`useBatteryOptimizationStatus`](/api/features/battery/#usebatteryoptimizationstatus) | [`BatteryOptimizationStatus`](/api/features/battery/#batteryoptimizationstatus) | `onBatteryOptimizationStatusChanged` | Android |
235
+ | [`useEqualizerSettings`](/api/features/equalizer/#useequalizersettings) | [`EqualizerSettings`](/api/features/equalizer/#equalizersettings)` \| undefined` | `onEqualizerChanged` | Android |
236
+ | `useDebug` | `{ state, logs, clear }` | _(composite)_ | all |
237
+
238
+ For the exact return types, see the [API reference](/api/).
@@ -0,0 +1,125 @@
1
+ ---
2
+ description: 'The map for live audio: stream formats, missing durations, dropped connections, expiring URLs, and song titles carried in-band.'
3
+ ---
4
+
5
+ # Live Streams
6
+
7
+ Live audio is what most of this player's edge cases are about: streams that
8
+ have no duration, drop in tunnels, expire their URLs, and carry their song
9
+ titles in-band. The pieces live in their own guides — this page is the map,
10
+ with just enough of each to know which link you need.
11
+
12
+ ## Stream formats
13
+
14
+ Point [`src`](/api/types/browser-nodes/#track) at the stream URL — there is
15
+ no format field; each platform's player detects the container:
16
+
17
+ - **HLS** (`.m3u8`) — both platforms. In-band ID3 timed metadata is read —
18
+ see [Metadata](/guide/metadata#timed-metadata-the-live-song).
19
+ - **ICY / Icecast / Shoutcast** — plain HTTP radio, both platforms. The ICY
20
+ `StreamTitle` arrives as timed metadata, filling only `title` — it reaches
21
+ the lock screen once your
22
+ [now-playing formatter](/guide/now-playing#the-formatter-derived-continuous)
23
+ applies it, never automatically (see
24
+ [Metadata](/guide/metadata#timed-metadata-the-live-song)).
25
+ - **Progressive HTTP** — ordinary MP3/AAC/… files and endless streams, both
26
+ platforms.
27
+ - **DASH / SmoothStreaming** — Android only (bundled Media3 modules); iOS's
28
+ AVPlayer doesn't support them.
29
+
30
+ Mark the track itself with [`live: true`](/api/types/browser-nodes/#track):
31
+ it shows the live indicator on iOS now-playing and arms
32
+ [`seekToLiveEdge()`](#the-live-edge) — declare it on every live track.
33
+ To hear it, put the track in the queue (or serve it from a
34
+ [browse tree](/guide/basic-usage)) after
35
+ [`setupPlayer()`](/guide/basic-usage#set-up-the-player) has run:
36
+
37
+ ```ts
38
+ import { play, setQueue } from 'react-native-audio-browser'
39
+
40
+ setQueue([
41
+ {
42
+ src: 'https://stream.example.com/main.mp3',
43
+ title: 'Beacon FM',
44
+ live: true
45
+ }
46
+ ])
47
+ play()
48
+ ```
49
+
50
+ ## Dropouts: stalls and retry
51
+
52
+ A connection drop mid-stream surfaces one of two ways, with different owners:
53
+
54
+ - **A stall** (how most iOS drops surface) — the player holds `'buffering'`
55
+ with play intent, and re-establishes the stream by itself when
56
+ connectivity returns. No wiring needed — see
57
+ [Network → You may not need this](/guide/network#you-may-not-need-this).
58
+ While stalled offline, the [now-playing formatter](/guide/now-playing#the-formatter-derived-continuous)
59
+ receives `stalled: 'offline'` (vs `'buffering'` when online) so the lock
60
+ screen can say why.
61
+ - **A playback error** (how most Android drops surface) — recovery belongs
62
+ to [automatic retry](/guide/errors#automatic-retry)
63
+ (`setupPlayer({ retry })`, off by default). Two duration budgets, chosen
64
+ by whether this load has ever produced audio: **12 s** for a stream that
65
+ never played (counting only online time — a station tapped in a tunnel
66
+ still gets its chance when the network returns), **2 min** once playback
67
+ proved the stream works. The 2 min budget is a wall clock that _includes_
68
+ offline time, deliberately — see
69
+ [the budgets](/guide/errors#two-duration-budgets). Budgets exhausted →
70
+ state `'error'`, where [`retry()`](/guide/errors#manual-retry) and your
71
+ error UI take over.
72
+
73
+ Android's reconnect-on-restore only arms through the retry machinery, so
74
+ the platform whose drops become errors is also the one that won't heal
75
+ without it. **If surviving tunnels matters to you, enable `retry`.** (The
76
+ web implementation has no automatic retry at all, and no ICY metadata —
77
+ every web error is terminal.)
78
+
79
+ ## Fresh URLs on every reconnect
80
+
81
+ If your stream URLs are signed or short-lived, build them in
82
+ [`media.resolve`](/guide/browser#media-and-artwork) — the per-track layer of
83
+ the stream request. Every recovery path re-runs it rather than replaying the
84
+ cached URL: automatic retry, network restore, play-from-`'error'`. A URL
85
+ that expired during the outage is re-signed instead of re-failing.
86
+
87
+ ```ts
88
+ import { configureBrowser } from 'react-native-audio-browser'
89
+
90
+ // Merge `media` into your one configureBrowser call — it replaces the
91
+ // whole config, so a media-only call would drop your tabs and routes.
92
+ configureBrowser({
93
+ media: {
94
+ resolve: async (track) => ({
95
+ path: `/stream/${track.id}`,
96
+ // sign() is yours — however your backend mints stream tokens
97
+ query: { token: await sign(track.id) }
98
+ })
99
+ }
100
+ })
101
+ ```
102
+
103
+ A relative `path` is joined to your configured `baseUrl` — see
104
+ [Shaping requests](/guide/browser#shaping-requests) for the layering.
105
+
106
+ ## The live edge
107
+
108
+ [`seekToLiveEdge()`](/api/features/playback/#seektoliveedge) jumps a live
109
+ track back to the newest available audio — a no-op unless the current track
110
+ declares `live: true`. On a stream with a seekable live window (HLS with a
111
+ sliding window) it seeks to the window's edge; on a non-seekable one
112
+ (typical ICY radio) it reconnects instead — a fresh connection _is_ the
113
+ live edge, and an expired URL re-resolves through `media.resolve`. General
114
+ seeking is in [Playback](/guide/playback).
115
+
116
+ ## API summary
117
+
118
+ | API | Purpose |
119
+ | ------------------------------------------------------------ | ----------------------------------------------------------------- |
120
+ | [`Track.live`](/api/types/browser-nodes/#track) | Declare a live track: iOS live indicator, arms the live edge. |
121
+ | [`seekToLiveEdge()`](/api/features/playback/#seektoliveedge) | Jump to the newest audio (no-op for non-live tracks). |
122
+ | [`setupPlayer({ retry })`](/guide/errors#automatic-retry) | Automatic retry: 12 s first-connect / 2 min recovery budgets. |
123
+ | [`retry()`](/guide/errors#manual-retry) | Restart from `'error'` with fresh budgets. |
124
+ | [`media.resolve`](/guide/browser#media-and-artwork) | Build each (re)load's stream request — signed / short-lived URLs. |
125
+ | [`useOnline()`](/guide/network) | Connectivity for your own UI; playback reacts on its own. |
@@ -0,0 +1,239 @@
1
+ ---
2
+ description: 'Read the metadata coming out of the media as it plays: the live ICY or HLS song, the static tags baked into a file, and chapter markers.'
3
+ ---
4
+
5
+ # Metadata
6
+
7
+ As a track plays, the player surfaces metadata it reads _from the media_ — the
8
+ live "now playing song" of a radio stream, the tags embedded in an audio file,
9
+ and chapter markers. These arrive as three independent event streams:
10
+
11
+ - [`onTimedMetadata`](/api/features/metadata/#ontimedmetadata) — the live song
12
+ from a stream (ICY radio or HLS in-band ID3), changing as the broadcast does.
13
+ - [`onTrackMetadata`](/api/features/metadata/#ontrackmetadata) — the static tags
14
+ baked into a media file, read when it loads.
15
+ - [`onChapterMetadata`](/api/features/metadata/#onchaptermetadata) — chapter
16
+ markers with time ranges, for podcasts and audiobooks.
17
+
18
+ These are **read-only signals coming _in_** from the media — they don't change
19
+ what's displayed anywhere. It's up to you to do something with them: feed the
20
+ [Now Playing](/guide/now-playing) surfaces, render your own UI, or build a
21
+ chapter list. (Each is subscription-only — there's no getter or hook; you
22
+ subscribe and keep the latest value yourself, as shown below.)
23
+
24
+ The UI snippets below import `useEffect` / `useState` from `react`, and `Text` /
25
+ `Button` / `Image` / `FlatList` from `react-native`; those import lines are
26
+ omitted for brevity. Everything else comes from `react-native-audio-browser`.
27
+
28
+ ## Subscribing
29
+
30
+ Every metadata event follows the same shape: `addListener` registers a callback
31
+ and returns a cleanup function that unsubscribes.
32
+
33
+ ```ts
34
+ import { onTimedMetadata } from 'react-native-audio-browser'
35
+
36
+ const unsubscribe = onTimedMetadata.addListener((metadata) => {
37
+ console.log('now playing:', metadata.title)
38
+ })
39
+
40
+ // later:
41
+ unsubscribe()
42
+ ```
43
+
44
+ Because the cleanup function is exactly what React's `useEffect` wants returned,
45
+ turning any of these into reactive state is a one-liner — see
46
+ [Holding metadata in state](#holding-metadata-in-state).
47
+
48
+ ## Timed metadata — the live song
49
+
50
+ [`onTimedMetadata`](/api/features/metadata/#ontimedmetadata) delivers the
51
+ "now playing song" announced by a live stream. Two formats arrive through this
52
+ one event:
53
+
54
+ - **ICY** metadata — Shoutcast / Icecast HTTP radio.
55
+ - **In-band ID3** timed metadata — how **HLS** live streams (and some others)
56
+ carry the current song.
57
+
58
+ It fires each time the stream announces a new song, so the value changes
59
+ throughout a single track.
60
+
61
+ [`TimedMetadata`](/api/features/metadata/#timedmetadata) — every field optional:
62
+
63
+ | Field | Meaning |
64
+ | -------- | ------------------------------------------------ |
65
+ | `title` | The song title (often "Artist - Song" in ICY). |
66
+ | `artist` | The artist, when the stream sends it separately. |
67
+ | `album` | The album, when present. |
68
+ | `date` | A date string, when present. |
69
+ | `genre` | The genre, when present. |
70
+
71
+ ::: warning ICY sends only a title
72
+ Shoutcast / Icecast (ICY) radio populates **only `title`** — typically a combined
73
+ `"Artist - Song"` string. `artist`, `album`, `date`, and `genre` stay `undefined`
74
+ for ICY; they're filled only by ID3 / HLS / file metadata. For a radio app,
75
+ parse `title` yourself rather than binding `metadata.artist`.
76
+ :::
77
+
78
+ This is the signal behind a live station's "now playing" line. To show it on the
79
+ lock screen / car, you usually don't subscribe here directly — you read it inside
80
+ the Now Playing formatter, which is re-invoked on every timed-metadata update.
81
+ See [Now Playing → the formatter](/guide/now-playing#the-formatter-derived-continuous).
82
+ Subscribe to `onTimedMetadata` when you want the live song for _your own_ UI.
83
+
84
+ ## Track metadata — the file's tags
85
+
86
+ [`onTrackMetadata`](/api/features/metadata/#ontrackmetadata) delivers the static
87
+ metadata baked into a media file — ID3 tags on an MP3, the metadata atoms in an
88
+ MP4/M4A — read when the player loads the asset. Use it for on-demand content
89
+ (podcast episodes, audiobooks, downloaded files) whose tags you didn't set
90
+ yourself.
91
+
92
+ [`TrackMetadata`](/api/features/metadata/#trackmetadata) is richer than the
93
+ timed shape. **Every field is an optional `string`:**
94
+
95
+ | Field | Meaning |
96
+ | -------------- | -------------------------------- |
97
+ | `title` | Track title. |
98
+ | `artist` | Track artist. |
99
+ | `albumTitle` | Album name. |
100
+ | `subtitle` | Secondary line. |
101
+ | `description` | Long description / show notes. |
102
+ | `artworkUri` | Artwork URL. |
103
+ | `trackNumber` | Track number within the album. |
104
+ | `composer` | Composer credit. |
105
+ | `conductor` | Conductor credit. |
106
+ | `genre` | Genre. |
107
+ | `compilation` | Compilation name. |
108
+ | `station` | Station/channel name. |
109
+ | `mediaType` | Media type string. |
110
+ | `creationDate` | Full creation date. |
111
+ | `creationYear` | Creation year. |
112
+ | `url` | A URL associated with the track. |
113
+
114
+ ```tsx
115
+ import { onTrackMetadata, type TrackMetadata } from 'react-native-audio-browser'
116
+
117
+ function FileInfo() {
118
+ const [meta, setMeta] = useState<TrackMetadata>()
119
+ useEffect(() => onTrackMetadata.addListener(setMeta), [])
120
+ if (!meta) return null
121
+ return (
122
+ <>
123
+ {meta.artworkUri && <Image source={{ uri: meta.artworkUri }} />}
124
+ <Text>
125
+ {meta.title} — {meta.artist}
126
+ </Text>
127
+ <Text>{meta.description}</Text>
128
+ </>
129
+ )
130
+ }
131
+ ```
132
+
133
+ ::: tip Mind the field names
134
+ Two traps when moving between the streams:
135
+
136
+ - **title vs title:** `TimedMetadata.title` is the _song_ (changes during a live
137
+ stream); `TrackMetadata.title` is the _file's_ title (static, read once).
138
+ - **album vs albumTitle:** timed metadata uses `album`; track metadata uses
139
+ `albumTitle`. And `TimedMetadata` has no artwork field — only `TrackMetadata`
140
+ carries `artworkUri`.
141
+ :::
142
+
143
+ ## Chapter metadata — podcast & audiobook chapters
144
+
145
+ [`onChapterMetadata`](/api/features/metadata/#onchaptermetadata) delivers the
146
+ full list of chapters for the current track as a
147
+ [`ChapterMetadata[]`](/api/features/metadata/#chaptermetadata):
148
+
149
+ | Field | Type | Meaning |
150
+ | ----------- | --------- | ---------------------------------------------- |
151
+ | `startTime` | `number` | Chapter start, in **seconds**. |
152
+ | `endTime` | `number` | Chapter end, in **seconds**. |
153
+ | `title` | `string?` | Chapter title, if present. |
154
+ | `url` | `string?` | A URL associated with the chapter, if present. |
155
+
156
+ Because `startTime` is in seconds, it drops straight into
157
+ [`seekTo`](/guide/playback#seeking) to make a tappable chapter list:
158
+
159
+ ```tsx
160
+ import {
161
+ onChapterMetadata,
162
+ seekTo,
163
+ type ChapterMetadata
164
+ } from 'react-native-audio-browser'
165
+
166
+ function ChapterList() {
167
+ const [chapters, setChapters] = useState<ChapterMetadata[]>([])
168
+ useEffect(() => onChapterMetadata.addListener(setChapters), [])
169
+
170
+ return (
171
+ <FlatList
172
+ data={chapters}
173
+ keyExtractor={(chapter) => String(chapter.startTime)}
174
+ renderItem={({ item }) => (
175
+ <Button
176
+ title={item.title ?? 'Chapter'}
177
+ onPress={() => seekTo(item.startTime)}
178
+ />
179
+ )}
180
+ />
181
+ )
182
+ }
183
+ ```
184
+
185
+ ## Which metadata is which
186
+
187
+ | | `onTimedMetadata` | `onTrackMetadata` | `onChapterMetadata` |
188
+ | ----------- | ----------------------- | ---------------------- | --------------------------- |
189
+ | Source | Live stream (ICY / ID3) | Tags in the media file | Chapter markers in the file |
190
+ | Changes | Throughout the track | Once, on load | Once, on load |
191
+ | Shape | One `TimedMetadata` | One `TrackMetadata` | A `ChapterMetadata[]` |
192
+ | Typical use | Live "now playing" song | Podcast/file info | Chapter navigation |
193
+ | Platform | iOS, Android | iOS, Android | iOS, Android |
194
+
195
+ ::: warning Not available on web
196
+ All three are iOS and Android only. On web they are subscribable but never
197
+ fire, so a listener sees nothing and
198
+ [`FormatNowPlayingParams.timedMetadata`](/api/features/player/#formatnowplayingparams)
199
+ stays `undefined`.
200
+
201
+ ICY is the hard case: the browser strips the interleaved metadata before the
202
+ player can reach it, and no API exposes it. In-band ID3 from HLS _is_ reachable
203
+ through the web player and simply isn't wired up yet — that part is tracked in
204
+ [#118](https://github.com/radio-garden/react-native-audio-browser/issues/118).
205
+ :::
206
+
207
+ ## Holding metadata in state
208
+
209
+ None of these come with a built-in hook, but each `addListener` returns the
210
+ cleanup function `useEffect` expects, so a reusable hook is a few lines:
211
+
212
+ ```tsx
213
+ import { onTimedMetadata, type TimedMetadata } from 'react-native-audio-browser'
214
+
215
+ // Returns TimedMetadata | undefined (undefined until the first event).
216
+ function useTimedMetadata(): TimedMetadata | undefined {
217
+ const [metadata, setMetadata] = useState<TimedMetadata>()
218
+ useEffect(() => onTimedMetadata.addListener(setMetadata), [])
219
+ return metadata
220
+ }
221
+
222
+ function NowPlayingSong() {
223
+ const song = useTimedMetadata()
224
+ return <Text>{song?.title ?? 'Unknown'}</Text>
225
+ }
226
+ ```
227
+
228
+ The same pattern works for `onTrackMetadata` and `onChapterMetadata` — swap the
229
+ event and the state type.
230
+
231
+ ## API summary
232
+
233
+ | API | Delivers |
234
+ | ------------------- | -------------------------------------------------------- |
235
+ | `onTimedMetadata` | Live `TimedMetadata` (the streaming "now playing" song). |
236
+ | `onTrackMetadata` | Static `TrackMetadata` (the media file's embedded tags). |
237
+ | `onChapterMetadata` | `ChapterMetadata[]` (chapter markers, times in seconds). |
238
+
239
+ All three are subscriptions: `addListener(cb)` returns an unsubscribe function.