react-native-audio-browser 0.2.0-next.12 → 0.2.0-next.14
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 +8 -1
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +52 -0
- package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
- package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +27 -11
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +1 -1
- 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/MediaSessionCallback.kt +21 -22
- 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 +16 -6
- package/android/src/main/res/values/strings.xml +37 -2
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +50 -5
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +9 -0
- 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 +10 -9
- package/ios/Browser/BrowserManager.swift +106 -45
- package/ios/Browser/InertStyleDiagnostic.swift +279 -0
- package/ios/Browser/JsonModels.swift +81 -15
- 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/Section+Copying.swift +1 -3
- package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
- package/ios/Http/HttpClient.swift +8 -2
- package/ios/HybridAudioBrowser.swift +56 -12
- package/ios/Model/NitroTypeStubs.swift +60 -1
- 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/ResolvedTrackCopyingTests.swift +2 -2
- 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/browser-config.js +1 -0
- package/lib/commonjs/features/browser-config.js.map +1 -1
- package/lib/commonjs/features/playback/progress.js +2 -1
- package/lib/commonjs/features/playback/progress.js.map +1 -1
- package/lib/commonjs/features/playback/rate.js +8 -1
- package/lib/commonjs/features/playback/rate.js.map +1 -1
- package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
- package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
- package/lib/module/features/browser-config.js +1 -0
- package/lib/module/features/browser-config.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/browser-config.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
- package/lib/typescript/src/features/playback/rate.d.ts +8 -1
- package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-native.d.ts +2 -1
- package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
- package/lib/typescript/src/types/browser-nodes.d.ts +116 -9
- package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
- package/lib/typescript/src/types/browser.d.ts +36 -0
- package/lib/typescript/src/types/browser.d.ts.map +1 -1
- package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
- 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__string__.hpp +102 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +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 +13 -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 +20 -1
- 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 +7 -5
- 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/Func_std__shared_ptr_Promise_std__string__.kt +80 -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/NativeBrowserConfiguration.kt +7 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Section.kt +2 -5
- 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.cpp +9 -0
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +104 -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/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
- package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
- package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
- package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +48 -1
- package/nitrogen/generated/ios/swift/Section.swift +1 -19
- 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++/NativeBrowserConfiguration.hpp +6 -2
- package/nitrogen/generated/shared/c++/Section.hpp +1 -5
- 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/browser-config.ts +1 -0
- 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-native.ts +5 -1
- package/src/types/browser-nodes.ts +124 -9
- package/src/types/browser.ts +38 -0
- 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,409 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 'Fix Android SSLHandshakeException / "Trust anchor for certification path not found" on HTTPS streams whose server omits its intermediate certificate.'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Android: SSL "Trust anchor for certification path not found" (missing intermediate certificates)
|
|
6
|
+
|
|
7
|
+
**If Android playback of a radio stream (or any HTTPS audio source) fails with an
|
|
8
|
+
`SSLHandshakeException` / `CertPathValidatorException` / "Trust anchor for
|
|
9
|
+
certification path not found" — and the same stream plays fine on iOS and in a
|
|
10
|
+
desktop browser — you are in the right place.** This is almost always a server
|
|
11
|
+
that omits an intermediate certificate; the fix is to let the client chase the
|
|
12
|
+
missing certificate via the leaf's Authority Information Access (AIA) pointer, and
|
|
13
|
+
this guide shows the one-time Android setup that does it.
|
|
14
|
+
|
|
15
|
+
## The problem
|
|
16
|
+
|
|
17
|
+
Some HTTPS servers are misconfigured to send only their **leaf** certificate and
|
|
18
|
+
omit the **intermediate** certificate(s) needed to chain up to a trusted root. A
|
|
19
|
+
correctly configured server sends the full chain; a misconfigured one leaves the
|
|
20
|
+
client to find the gap on its own.
|
|
21
|
+
|
|
22
|
+
- **iOS and web browsers** recover automatically: they follow the leaf's
|
|
23
|
+
Authority Information Access (AIA) "CA Issuers" pointer to fetch the missing
|
|
24
|
+
intermediate. (This is why the stream works everywhere except Android.)
|
|
25
|
+
- **Android's default trust manager does not.** It validates only what the server
|
|
26
|
+
sent, so playback of such a stream fails with:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
javax.net.ssl.SSLHandshakeException:
|
|
30
|
+
java.security.cert.CertPathValidatorException:
|
|
31
|
+
Trust anchor for certification path not found.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This affects media playback because the Android player transports audio over
|
|
35
|
+
`HttpURLConnection`, which uses the process-wide default `SSLSocketFactory`.
|
|
36
|
+
|
|
37
|
+
> **iOS needs no setup.** Apple's TLS stack chases AIA intermediates on its own,
|
|
38
|
+
> so there is nothing to configure on iOS — this guide is Android-only.
|
|
39
|
+
|
|
40
|
+
## Is this actually your problem?
|
|
41
|
+
|
|
42
|
+
Before changing process-global TLS state, confirm the failing stream really is a
|
|
43
|
+
missing-intermediate case and not some other TLS failure (an expired cert, an SNI
|
|
44
|
+
mismatch, or a genuinely untrusted root — none of which this fix addresses).
|
|
45
|
+
|
|
46
|
+
Run, against your failing stream's **host** — use just the hostname, not the full
|
|
47
|
+
stream URL. If your stream URL is `https://example.com:8000/live.mp3`, the host is
|
|
48
|
+
`example.com` and the port is `8000` (omit the path); plain `https://` URLs with no
|
|
49
|
+
port use `443`:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
openssl s_client -connect example.com:443 \
|
|
53
|
+
-servername example.com -showcerts </dev/null
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
In the output:
|
|
57
|
+
|
|
58
|
+
- A **complete** chain lists two or more certificates under
|
|
59
|
+
`Certificate chain` (leaf `s:.../CN=host` then one or more issuers `i:...`) and
|
|
60
|
+
ends with `Verify return code: 0 (ok)`.
|
|
61
|
+
- A **missing-intermediate** server typically shows **only one** certificate in
|
|
62
|
+
the chain and a verify error such as
|
|
63
|
+
`Verify return code: 21 (unable to verify the first certificate)` — yet the
|
|
64
|
+
leaf's AIA block contains a `CA Issuers - URI:` pointer. That combination is the
|
|
65
|
+
exact case this fix repairs.
|
|
66
|
+
|
|
67
|
+
Prefer a browser? Paste the host into the [SSL Labs server
|
|
68
|
+
test](https://www.ssllabs.com/ssltest/) — it flags **"Chain issues: Incomplete"**
|
|
69
|
+
for this exact problem — or use [whatsmychaincert.com](https://whatsmychaincert.com/).
|
|
70
|
+
|
|
71
|
+
> **Want to see a broken chain first?** `incomplete-chain.badssl.com` is a public
|
|
72
|
+
> test host that deliberately omits its intermediate — the canonical example of
|
|
73
|
+
> this misconfiguration. Run the command above against
|
|
74
|
+
> `incomplete-chain.badssl.com:443` to see exactly what the "one certificate +
|
|
75
|
+
> verify error + AIA pointer present" signature looks like before you go hunting in
|
|
76
|
+
> your own stream.
|
|
77
|
+
|
|
78
|
+
If the chain is complete, or there is no `CA Issuers` AIA pointer, this fix will
|
|
79
|
+
**not** help — see [Caveats](#caveats).
|
|
80
|
+
|
|
81
|
+
## Opt in
|
|
82
|
+
|
|
83
|
+
The library ships an **additive** AIA-chasing `SSLSocketFactory` but does **not**
|
|
84
|
+
install it for you — installing it sets process-global TLS state, which is a
|
|
85
|
+
decision for the host application to make, not a library. Enable it by setting it
|
|
86
|
+
as the default in your **existing** `MainApplication`'s `onCreate`, before any
|
|
87
|
+
playback begins. `MainApplication.onCreate` runs before React Native loads and
|
|
88
|
+
executes your JS bundle, so it is always early enough — no playback can have
|
|
89
|
+
started yet.
|
|
90
|
+
|
|
91
|
+
> Add the highlighted line to the `MainApplication` you already have (it lives at
|
|
92
|
+
> `android/app/src/main/java/.../MainApplication.kt` or `.java`). **Don't create a
|
|
93
|
+
> new file or a second `MainApplication` class** — just drop the call into the
|
|
94
|
+
> `onCreate` you already have, after `super.onCreate()`.
|
|
95
|
+
|
|
96
|
+
::: code-group
|
|
97
|
+
|
|
98
|
+
```kotlin [MainApplication.kt]
|
|
99
|
+
import com.audiobrowser.tls.AiaTls
|
|
100
|
+
import javax.net.ssl.HttpsURLConnection
|
|
101
|
+
import javax.net.ssl.SSLSocketFactory
|
|
102
|
+
import android.util.Log
|
|
103
|
+
|
|
104
|
+
class MainApplication : Application() {
|
|
105
|
+
override fun onCreate() {
|
|
106
|
+
super.onCreate()
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
val aiaFactory = AiaTls.socketFactory() // [!code ++]
|
|
110
|
+
HttpsURLConnection.setDefaultSSLSocketFactory(aiaFactory) // [!code ++]
|
|
111
|
+
installedSslFactory = aiaFactory // [!code ++]
|
|
112
|
+
} catch (e: Exception) {
|
|
113
|
+
// Never let TLS setup block startup; playback keeps default behaviour.
|
|
114
|
+
Log.w("MainApplication", "Failed to install AIA-chasing TLS", e)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ... the rest of your existing setup
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
companion object {
|
|
121
|
+
/** Kept so you can confirm the install — see "Verify it worked". */
|
|
122
|
+
var installedSslFactory: SSLSocketFactory? = null
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```java [MainApplication.java]
|
|
128
|
+
import com.audiobrowser.tls.AiaTls;
|
|
129
|
+
import javax.net.ssl.HttpsURLConnection;
|
|
130
|
+
import javax.net.ssl.SSLSocketFactory;
|
|
131
|
+
import android.util.Log;
|
|
132
|
+
|
|
133
|
+
public class MainApplication extends Application {
|
|
134
|
+
/** Kept so you can confirm the install — see "Verify it worked". */
|
|
135
|
+
public static SSLSocketFactory installedSslFactory = null;
|
|
136
|
+
|
|
137
|
+
@Override
|
|
138
|
+
public void onCreate() {
|
|
139
|
+
super.onCreate();
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
SSLSocketFactory aiaFactory = AiaTls.socketFactory(); // [!code ++]
|
|
143
|
+
HttpsURLConnection.setDefaultSSLSocketFactory(aiaFactory); // [!code ++]
|
|
144
|
+
installedSslFactory = aiaFactory; // [!code ++]
|
|
145
|
+
} catch (Exception e) {
|
|
146
|
+
// Never let TLS setup block startup; playback keeps default behaviour.
|
|
147
|
+
Log.w("MainApplication", "Failed to install AIA-chasing TLS", e);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ... the rest of your existing setup
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
:::
|
|
156
|
+
|
|
157
|
+
That's all. The player's data source picks up the process default automatically.
|
|
158
|
+
`AiaTls` ships with the `react-native-audio-browser` Android module you already
|
|
159
|
+
installed — the `com.audiobrowser.tls` import resolves with no extra Gradle
|
|
160
|
+
dependency.
|
|
161
|
+
|
|
162
|
+
> **Rebuild after adding this.** `AiaTls` is native Kotlin, so a JS-only reload
|
|
163
|
+
> won't pick up the new call — do a full native rebuild:
|
|
164
|
+
>
|
|
165
|
+
> ```sh
|
|
166
|
+
> npx react-native run-android
|
|
167
|
+
> # if the build seems stale, clear Gradle first:
|
|
168
|
+
> cd android && ./gradlew clean && cd .. && npx react-native run-android
|
|
169
|
+
> ```
|
|
170
|
+
>
|
|
171
|
+
> No manifest or permission change is needed — RN apps already hold the `INTERNET`
|
|
172
|
+
> permission the AIA fetch uses, and the cleartext fetch is handled for you (see
|
|
173
|
+
> [The cleartext fetch is handled for you](#the-cleartext-fetch-is-handled-for-you)).
|
|
174
|
+
|
|
175
|
+
## The cleartext fetch is handled for you
|
|
176
|
+
|
|
177
|
+
The completed-chain check happens over HTTPS — only the side fetch of the _missing
|
|
178
|
+
intermediate_ uses the leaf's `CA Issuers` URL, which CAs publish over plain
|
|
179
|
+
`http://` (mandated by the CA/Browser Forum Baseline Requirements, to avoid a
|
|
180
|
+
chicken-and-egg TLS dependency when fetching the very certificate needed to
|
|
181
|
+
complete a TLS chain).
|
|
182
|
+
|
|
183
|
+
That's a problem on most modern apps: targeting Android 9+ (API 28) **blocks
|
|
184
|
+
cleartext HTTP by default**, which would silently break the fetch on release
|
|
185
|
+
builds. So the library doesn't use Android's HTTP stack for it — the `http`
|
|
186
|
+
intermediate fetch goes over a **raw socket**, which isn't subject to your app's
|
|
187
|
+
`usesCleartextTraffic` / network-security-config policy. **You don't need to enable
|
|
188
|
+
cleartext or add a network security config.**
|
|
189
|
+
|
|
190
|
+
This stays safe: the fetched certificate is cryptographically verified to be the
|
|
191
|
+
genuine issuer and re-validated against the system trust anchors before use, so a
|
|
192
|
+
tampered response over plain HTTP can only fail the chain, never weaken it.
|
|
193
|
+
|
|
194
|
+
## Verify it worked
|
|
195
|
+
|
|
196
|
+
A silent success is hard to tell apart from "my problem was something else," so
|
|
197
|
+
confirm it explicitly:
|
|
198
|
+
|
|
199
|
+
1. **Confirm the factory is installed.** The `catch` above only logs on _failure_,
|
|
200
|
+
so **no warning at startup means it installed without throwing** — enough for a
|
|
201
|
+
simple app. But it is _not_ enough if your app uses other networking libraries:
|
|
202
|
+
`setDefaultSSLSocketFactory` is last-write-wins (see [Caveats](#caveats)), so
|
|
203
|
+
another library setting its own default afterward silently deactivates the fix
|
|
204
|
+
with no warning. The comparison below is the **only reliable check** in that
|
|
205
|
+
case. `AiaTls.socketFactory()` returns a stock TLS socket factory configured
|
|
206
|
+
with the AIA-chasing trust manager, so its _class name_ looks identical to the
|
|
207
|
+
platform default — don't try to recognise it by class. Instead compare the
|
|
208
|
+
process default against the reference the opt-in snippet stored in
|
|
209
|
+
`installedSslFactory`. Put this **after** the install `try`/`catch` (so it
|
|
210
|
+
reflects the real process default, not the value you just set), anywhere that
|
|
211
|
+
runs after startup:
|
|
212
|
+
|
|
213
|
+
::: code-group
|
|
214
|
+
|
|
215
|
+
```kotlin [Kotlin]
|
|
216
|
+
// place OUTSIDE the install try/catch — this tests the live process default
|
|
217
|
+
val active = HttpsURLConnection.getDefaultSSLSocketFactory() ===
|
|
218
|
+
MainApplication.installedSslFactory
|
|
219
|
+
Log.i("MainApplication", "AIA TLS active: $active")
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```java [Java]
|
|
223
|
+
// place OUTSIDE the install try/catch — this tests the live process default
|
|
224
|
+
boolean active = HttpsURLConnection.getDefaultSSLSocketFactory()
|
|
225
|
+
== MainApplication.installedSslFactory;
|
|
226
|
+
Log.i("MainApplication", "AIA TLS active: " + active);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
:::
|
|
230
|
+
|
|
231
|
+
2. **Confirm playback recovers.** Play the exact stream that previously failed.
|
|
232
|
+
It should now start, and the
|
|
233
|
+
`SSLHandshakeException: ... Trust anchor for certification path not found`
|
|
234
|
+
should no longer appear in `adb logcat`. No real broken stream on hand? Point
|
|
235
|
+
the player at a known-broken-chain source such as
|
|
236
|
+
`https://incomplete-chain.badssl.com/` to prove the wiring end-to-end before you
|
|
237
|
+
rely on it. Keep one of these around as a regression check for future upgrades.
|
|
238
|
+
|
|
239
|
+
### Still failing after install?
|
|
240
|
+
|
|
241
|
+
Work through these in order:
|
|
242
|
+
|
|
243
|
+
1. **Is it even a missing-intermediate case?** Re-run the
|
|
244
|
+
[diagnosis](#is-this-actually-your-problem). If the chain is complete, there's
|
|
245
|
+
no `CA Issuers` AIA pointer, or the root is genuinely untrusted/expired, this
|
|
246
|
+
fix can't repair it — that's a different problem.
|
|
247
|
+
2. **Did the AIA fetch fail?** When the chaser tries to fetch the intermediate and
|
|
248
|
+
can't, the library logs a warning:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
AIA CA-issuer fetch failed: http://…/issuer.crt
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Filter `adb logcat` for `AIA CA-issuer fetch failed`. If it appears, the fetch
|
|
255
|
+
was attempted but couldn't be reached — see the next point. (You do _not_ need
|
|
256
|
+
to enable cleartext; the library's fetch already bypasses that policy.)
|
|
257
|
+
|
|
258
|
+
::: warning The logger starts with the playback service
|
|
259
|
+
The library plants its logging trees when its service first starts, so a fetch
|
|
260
|
+
that fails **before** any playback has begun logs nothing — even though the
|
|
261
|
+
socket factory is process-wide and already in use. If you see no line at all,
|
|
262
|
+
start playback once and retry, or plant your own tree at startup.
|
|
263
|
+
:::
|
|
264
|
+
|
|
265
|
+
3. **Reachable from your environment?** The fetch is a plain outbound request to
|
|
266
|
+
the `CA Issuers - URI` during the handshake, so a corporate proxy, VPN,
|
|
267
|
+
firewall, or a locked-down emulator network can block it. Confirm the URL
|
|
268
|
+
resolves by fetching it yourself — `curl -I "http://…/issuer.crt"` — from the
|
|
269
|
+
same network the device is on.
|
|
270
|
+
4. **Custom Network Security Config?** If your app ships a
|
|
271
|
+
`res/xml/network_security_config.xml` with custom trust anchors or pinning,
|
|
272
|
+
that's an unrelated cause of TLS failures — this fix won't override it.
|
|
273
|
+
|
|
274
|
+
## How it works
|
|
275
|
+
|
|
276
|
+
`AiaTls.socketFactory()` wraps the **platform default** trust manager. On the
|
|
277
|
+
happy path the default accepts the server-presented chain on the first try and
|
|
278
|
+
nothing extra happens. Only when the default _rejects_ a chain does it:
|
|
279
|
+
|
|
280
|
+
1. read the leaf's AIA "CA Issuers" URL,
|
|
281
|
+
2. fetch the missing intermediate (over a raw socket for `http` URLs, so it works
|
|
282
|
+
regardless of your cleartext policy; cached for the process lifetime),
|
|
283
|
+
3. re-validate the completed chain **against the same system trust anchors**.
|
|
284
|
+
|
|
285
|
+
A fetched certificate is used only if it is genuinely the issuer of the preceding
|
|
286
|
+
one. Because the final decision is still the system trust manager's, this can
|
|
287
|
+
**only ever add a missing intermediate — it can never weaken trust**. Untrusted,
|
|
288
|
+
expired, or self-signed roots still fail exactly as before.
|
|
289
|
+
|
|
290
|
+
## Caveats
|
|
291
|
+
|
|
292
|
+
- **Scope.** `setDefaultSSLSocketFactory` is process-wide, so it affects every
|
|
293
|
+
`HttpURLConnection`-based TLS connection in your app, not only playback. (It
|
|
294
|
+
does not affect OkHttp or a `WebView`, which keep their own TLS stacks.) It is
|
|
295
|
+
also last-write-wins: if other code sets its own default afterwards, it
|
|
296
|
+
overrides this one.
|
|
297
|
+
- **First-connection latency on broken servers.** When a server omits its
|
|
298
|
+
intermediate, the first connection performs one extra HTTP request for the
|
|
299
|
+
intermediate during the TLS handshake. Well-configured servers pay no cost. The
|
|
300
|
+
fetched intermediate is cached for the **process lifetime only**, so this
|
|
301
|
+
one-time cost recurs once per cold start for each broken server.
|
|
302
|
+
- **Not a cure-all.** It only rescues servers that omit an intermediate _and_
|
|
303
|
+
publish an AIA "CA Issuers" pointer that resolves to a trusted root. Servers
|
|
304
|
+
with no AIA extension, an unreachable intermediate, or a genuinely untrusted
|
|
305
|
+
root still fail.
|
|
306
|
+
- **The AIA fetch needs outbound network reachability.** Completing the chain
|
|
307
|
+
performs a plain HTTP request to the `CA Issuers` host during the handshake; a
|
|
308
|
+
proxy, VPN, or firewall that blocks it makes the fix look broken even on a
|
|
309
|
+
genuinely missing-intermediate server. See
|
|
310
|
+
[Still failing after install?](#still-failing-after-install) to diagnose.
|
|
311
|
+
- **Unrelated cause: custom Network Security Config.** A
|
|
312
|
+
`res/xml/network_security_config.xml` with custom trust anchors or certificate
|
|
313
|
+
pinning is a _different_ source of TLS failures that this fix does not address.
|
|
314
|
+
|
|
315
|
+
## What you are opting into
|
|
316
|
+
|
|
317
|
+
Trust is never weakened. The chain is only ever _extended_, and the completed
|
|
318
|
+
chain is re-validated by the platform's own trust manager against the same
|
|
319
|
+
system anchors, so anything Android would have rejected is still rejected.
|
|
320
|
+
|
|
321
|
+
What you are accepting is an outbound request driven by a certificate that just
|
|
322
|
+
failed validation. Completing a chain means fetching the missing intermediate
|
|
323
|
+
from a URL written into that certificate, so a rejected handshake can cause a
|
|
324
|
+
cleartext request to a **host, port and path of the server's choosing** —
|
|
325
|
+
including a loopback or LAN address — and that request is deliberately not
|
|
326
|
+
subject to your app's `NetworkSecurityPolicy` (see
|
|
327
|
+
[The cleartext fetch is handled for you](#the-cleartext-fetch-is-handled-for-you)).
|
|
328
|
+
|
|
329
|
+
This is inherent to AIA chasing: browsers and Apple's Secure Transport do the
|
|
330
|
+
same thing, which is why the servers in question work everywhere except Android.
|
|
331
|
+
It is bounded rather than absent:
|
|
332
|
+
|
|
333
|
+
- the fetch happens **only** when the chain does not already reach one of your
|
|
334
|
+
trust anchors, so the common non-path failures — an expired leaf, a hostname
|
|
335
|
+
mismatch — do not trigger one. (The test is on the issuer name at the top of
|
|
336
|
+
the chain, so a chain topping at a root your device does _not_ carry, such as
|
|
337
|
+
a server still serving the retired DST Root CA X3 cross-sign, is still chased
|
|
338
|
+
even if its real problem is expiry.)
|
|
339
|
+
- the URL is rejected unless it is free of control characters, so it cannot
|
|
340
|
+
inject request lines of its own;
|
|
341
|
+
- at most 3 distinct `CA Issuers` URLs per certificate are tried, a response is
|
|
342
|
+
capped at 1 MiB, and at most 5 redirects are followed per fetch;
|
|
343
|
+
- the whole chase runs under a single 20-second wall-clock budget — every
|
|
344
|
+
connect, TLS handshake, read and redirect hop is clipped to the time
|
|
345
|
+
remaining, and nothing new starts once it has expired. So the worst case a
|
|
346
|
+
hostile certificate can inflict on one failed handshake is roughly those
|
|
347
|
+
20 seconds, plus at most one DNS lookup that was already in flight when the
|
|
348
|
+
budget expired (Java offers no timeout for name resolution, so a lookup can
|
|
349
|
+
overrun the budget by its own duration — but a new one is never started past
|
|
350
|
+
it);
|
|
351
|
+
- the cache holds at most 32 entries, and only successful fetches;
|
|
352
|
+
- responses are never trusted on their say-so: a fetched certificate is used
|
|
353
|
+
only if it is the genuine issuer of the certificate below it, and the
|
|
354
|
+
completed chain still has to validate.
|
|
355
|
+
|
|
356
|
+
If your threat model does not allow a failed handshake to trigger an outbound
|
|
357
|
+
connection at all, don't install this — or install it on a single client
|
|
358
|
+
(see [Advanced: custom OkHttp client](#advanced-custom-okhttp-client)) rather
|
|
359
|
+
than as the process default.
|
|
360
|
+
|
|
361
|
+
## Advanced: custom OkHttp client
|
|
362
|
+
|
|
363
|
+
If you manage your own HTTP client instead of changing the process default, use
|
|
364
|
+
`AiaTls.trustManager()` to obtain the wrapped `X509TrustManager` and build a
|
|
365
|
+
matching `SSLSocketFactory` from it. OkHttp's `sslSocketFactory(...)` needs
|
|
366
|
+
**both** the factory and the trust manager:
|
|
367
|
+
|
|
368
|
+
::: code-group
|
|
369
|
+
|
|
370
|
+
```kotlin [Kotlin]
|
|
371
|
+
import com.audiobrowser.tls.AiaTls
|
|
372
|
+
import javax.net.ssl.SSLContext
|
|
373
|
+
import okhttp3.OkHttpClient
|
|
374
|
+
|
|
375
|
+
val trustManager = AiaTls.trustManager()
|
|
376
|
+
val sslContext = SSLContext.getInstance("TLS").apply {
|
|
377
|
+
init(null, arrayOf(trustManager), null)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
val client = OkHttpClient.Builder()
|
|
381
|
+
.sslSocketFactory(sslContext.socketFactory, trustManager)
|
|
382
|
+
.build()
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
```java [Java]
|
|
386
|
+
import com.audiobrowser.tls.AiaTls;
|
|
387
|
+
import javax.net.ssl.SSLContext;
|
|
388
|
+
import javax.net.ssl.X509TrustManager;
|
|
389
|
+
import okhttp3.OkHttpClient;
|
|
390
|
+
|
|
391
|
+
X509TrustManager trustManager = AiaTls.trustManager();
|
|
392
|
+
SSLContext sslContext = SSLContext.getInstance("TLS");
|
|
393
|
+
sslContext.init(null, new X509TrustManager[] { trustManager }, null);
|
|
394
|
+
|
|
395
|
+
OkHttpClient client = new OkHttpClient.Builder()
|
|
396
|
+
.sslSocketFactory(sslContext.getSocketFactory(), trustManager)
|
|
397
|
+
.build();
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
:::
|
|
401
|
+
|
|
402
|
+
This confines AIA chasing to that one client and leaves the process default
|
|
403
|
+
untouched.
|
|
404
|
+
|
|
405
|
+
> **Lifecycle.** Each call to `AiaTls.socketFactory()` / `AiaTls.trustManager()`
|
|
406
|
+
> builds a fresh component with its **own** fetched-intermediate cache, so build
|
|
407
|
+
> once and reuse it (don't construct one per request). The components are
|
|
408
|
+
> thread-safe — the fetch cache is a synchronized, bounded LRU — so a single
|
|
409
|
+
> instance is safe to install as the process default or share across a client.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 'Set Track.artwork once and render it everywhere: the fetch and transform pipeline, per-surface sizing, SF Symbols, tinting, light/dark pairs, Android vector icons, and SVG.'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Artwork
|
|
6
|
+
|
|
7
|
+
**Artwork** is the image shown for a track — the thumbnail in a browse list, the cover on the lock screen, the icon on a CarPlay row. You set one field, `artwork`, on a [`Track`](/guide/track); the library resolves and transforms it per surface and per requested size, and hands the ready-to-render result back on `artworkSource`.
|
|
8
|
+
|
|
9
|
+
This guide covers the mental model (the two configs and the output field), **which artwork renders where**, the **transform pipeline** that turns your URL into a sized request, and the platform-specific bits: SF Symbols on iOS, tinting, light/dark artwork, Android vector icons, and SVG.
|
|
10
|
+
|
|
11
|
+
## Mental model
|
|
12
|
+
|
|
13
|
+
Three names do all the work:
|
|
14
|
+
|
|
15
|
+
| Name | What it is | You |
|
|
16
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- |
|
|
17
|
+
| `Track.artwork` | The image **you set** — an `https` URL, an `sf:` symbol (iOS), a platform URI, or a `{ light, dark }` pair. | set it |
|
|
18
|
+
| `artwork` / `nowPlayingArtwork` config | How the library **fetches** that image (base URL, headers, signing, sizing). | configure once (override per route / for now-playing) |
|
|
19
|
+
| `Track.artworkSource` | The resolved, ready-to-render [`ImageSource`](/api/types/browser-nodes/#imagesource) (URL + headers) the library **produces**. | read it |
|
|
20
|
+
|
|
21
|
+
`artwork` is the only field you set per track. `artworkSource` is **output-only** — the library populates it; never set it yourself.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { useActiveTrack } from 'react-native-audio-browser'
|
|
25
|
+
import { Image } from 'react-native'
|
|
26
|
+
|
|
27
|
+
function Cover() {
|
|
28
|
+
const track = useActiveTrack()
|
|
29
|
+
// artworkSource is the transformed result, shaped for <Image>
|
|
30
|
+
return <Image source={track?.artworkSource} />
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Which artwork renders where
|
|
35
|
+
|
|
36
|
+
The library publishes art to several surfaces; each gets its image from a field/config and (where the surface knows its display size) a size hint:
|
|
37
|
+
|
|
38
|
+
| Surface | Image from | Size hint | Tinting |
|
|
39
|
+
| ----------------------------------------------- | --------------------------------------------- | --------- | ------------------------------------------------------------------------------ |
|
|
40
|
+
| **Browse rows / tabs** (CarPlay / Android Auto) | `artwork` (per-route override allowed) | no | CarPlay: `style: { artworkRendering: 'stencil' }`, or a `{ light, dark }` pair |
|
|
41
|
+
| **In-app `<Image>`** | `artworkSource` (output) | no | your UI |
|
|
42
|
+
| **iOS lock screen / Control Center** | `nowPlayingArtwork` → falls back to `artwork` | yes | — |
|
|
43
|
+
| **CarPlay Now Playing** | `nowPlayingArtwork` → `artwork` | yes | — |
|
|
44
|
+
| **Android notification / Android Auto** | `nowPlayingArtwork` → `artwork` | yes | — (Android Auto is dark-only) |
|
|
45
|
+
| **CarPlay tile sections** (`display: 'grid'`) | `artwork` (each tile is a Track) | yes | CarPlay: `style: { artworkRendering: 'stencil' }`, or a `{ light, dark }` pair |
|
|
46
|
+
|
|
47
|
+
**Size hint** = the surface tells the library the pixels it needs, delivered as an [`ImageContext`](/api/types/browser/#imagecontext) to your `transform` / `imageQueryParams`. Browse-time resolution has none.
|
|
48
|
+
|
|
49
|
+
Two things to take from the table:
|
|
50
|
+
|
|
51
|
+
- **One image by default.** With only `artwork` set, the same source feeds browse rows _and_ every now-playing surface. Set `nowPlayingArtwork` only when the now-playing image should differ (a larger, lock-screen-quality cover without bloating list thumbnails).
|
|
52
|
+
- **Size hints exist only where the surface knows its size.** Browse-time resolution has no size, so `imageQueryParams` and `context` (below) do nothing there; they kick in at load time on CarPlay / Android Auto / now-playing.
|
|
53
|
+
- **Tinting is for browse icons only.** `artworkRendering: 'stencil'` is applied when CarPlay renders browse content — list rows, tabs, and tiles — not to now-playing cover art.
|
|
54
|
+
|
|
55
|
+
## The transform pipeline
|
|
56
|
+
|
|
57
|
+
`artwork` is an [`ArtworkRequestConfig`](/api/types/browser/#artworkrequestconfig). When the library needs a track's image, it builds the request in layers, each overriding the last:
|
|
58
|
+
|
|
59
|
+
1. **`request`** — shared base config (user agent, common headers).
|
|
60
|
+
2. **`artwork`** — the static image config (`baseUrl`, `headers`, …).
|
|
61
|
+
3. **`resolve(track)` / `resolveSync(track)`** — per-track overrides built from the track's metadata.
|
|
62
|
+
4. **`imageQueryParams`** — appends the surface's requested size (when it reports one).
|
|
63
|
+
5. **`transform({ request, context })` / `transformSync`** — final tweak, with the requested size in `context`.
|
|
64
|
+
|
|
65
|
+
To build a per-track URL, use `resolve` (the track is the argument). The `{id}` token is **not** a general feature — only `nowPlayingArtwork` templates it (see below); static values in `artwork` are sent verbatim.
|
|
66
|
+
|
|
67
|
+
A typical CDN setup never needs more than `baseUrl` plus `imageQueryParams`:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { configureBrowser } from 'react-native-audio-browser'
|
|
71
|
+
|
|
72
|
+
configureBrowser({
|
|
73
|
+
// ...the rest of your config (tabs, routes, …)
|
|
74
|
+
artwork: {
|
|
75
|
+
baseUrl: 'https://images.example.com',
|
|
76
|
+
// tell the library which query params carry the requested size
|
|
77
|
+
imageQueryParams: { width: 'w', height: 'h' }
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
With [`imageQueryParams`](/api/types/browser/#imagequeryparams) the requested size from CarPlay / Android Auto is appended automatically — `?w=200&h=200` — so each surface gets right-sized art. Omit `height` to send width only. This only fires on surfaces that report a size; browse rows (no size hint) get the URL without these params.
|
|
83
|
+
|
|
84
|
+
For anything `imageQueryParams` can't express — per-track paths, signed URLs — use `resolve` and `transform` (or their sync variants):
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { configureBrowser } from 'react-native-audio-browser'
|
|
88
|
+
import HmacSHA256 from 'crypto-js/hmac-sha256' // add: yarn add crypto-js
|
|
89
|
+
|
|
90
|
+
const CDN_KEY = process.env.CDN_KEY ?? '' // your CDN signing key
|
|
91
|
+
|
|
92
|
+
// sign the path locally with your key — a pure, sync function
|
|
93
|
+
const signUrl = (path: string) => HmacSHA256(path, CDN_KEY).toString()
|
|
94
|
+
|
|
95
|
+
configureBrowser({
|
|
96
|
+
artwork: {
|
|
97
|
+
baseUrl: 'https://images.example.com',
|
|
98
|
+
// build the path from the track (sync — no await needed)
|
|
99
|
+
resolveSync: (track) => ({ path: `/covers/${track.id}.jpg` }),
|
|
100
|
+
// then sign it + size it. both are sync, so no Promise is
|
|
101
|
+
// allocated per image
|
|
102
|
+
transformSync: ({ request, context }) => ({
|
|
103
|
+
...request,
|
|
104
|
+
query: {
|
|
105
|
+
...request.query,
|
|
106
|
+
w: context?.width ? String(context.width) : '600',
|
|
107
|
+
// request.path is optional — guard it before signing
|
|
108
|
+
sig: signUrl(request.path ?? '')
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A `transform` (or `transformSync`) **replaces** the request — it doesn't merge — so spread `...request` (and `...request.query`) or you'll drop the `baseUrl` and other layers. The [`context`](/api/types/browser/#imagecontext) is an `ImageContext` with `width` / `height` in **pixels**, present only when the surface knows its display size (so guard with `context?.width`). `request.path` is also optional — it's only set if a `resolve` ran first — so guard it before signing (as above).
|
|
116
|
+
|
|
117
|
+
`resolveSync` / `transformSync` are no-Promise variants of `resolve` / `transform`. Reach for them when the per-track config or final tweak needs no `await` — a synced query param, a locally-computed path — to skip a Promise allocation per image (the example above is fully sync for that reason). If you set _both_ the sync and async form of the **same** stage (e.g. `transform` _and_ `transformSync`), the async one runs first and the sync one tweaks its result; `resolve` and `transform` are independent stages that both always run.
|
|
118
|
+
|
|
119
|
+
::: tip Per-route artwork
|
|
120
|
+
A route can override the global `artwork` with its own config — `routes: { '/premium': { artwork: { baseUrl: '…' } } }` — so different content can load images from different hosts. See the [Browser](/guide/browser) guide.
|
|
121
|
+
:::
|
|
122
|
+
|
|
123
|
+
## SF Symbols (iOS)
|
|
124
|
+
|
|
125
|
+
Prefix `artwork` with `sf:` to render an Apple [SF Symbol](https://developer.apple.com/sf-symbols/) instead of fetching an image — ideal for tab and category icons. Optional `bg` (background, transparent if omitted) and `fg` (symbol color, black if omitted) query params set the colors:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
{
|
|
129
|
+
title: 'Favorites',
|
|
130
|
+
path: '/favorites',
|
|
131
|
+
artwork: 'sf:heart.fill?bg=#FF0090&fg=#fff'
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
On CarPlay, an SF Symbol with **no explicit colors** is handed to CarPlay untinted, so CarPlay renders it in the appearance's own color (adapting to light/dark); give it a `bg`/`fg` and you get exactly those colors instead. The library renders the symbol to an image on device and caches it. This is iOS-only; on Android an `sf:` value is not a valid image.
|
|
136
|
+
|
|
137
|
+
## Tinting and platform icons
|
|
138
|
+
|
|
139
|
+
For monochrome **icons** (not full-color album art), let the system tint them to stay legible on either appearance:
|
|
140
|
+
|
|
141
|
+
- **iOS CarPlay** — declare [`style: { artworkRendering: 'stencil' }`](/api/types/browser-nodes/#trackstyle) on the track. Inside a page it's an inherited style property (a section- or page-wide value covers every item); tabs render without a section context, so a tab icon needs the declaration on the tab track itself. CarPlay tints the glyph per appearance: black in light mode, white in dark.
|
|
142
|
+
- **Android Auto** is dark-only, so there's no per-appearance tinting — ship an appropriately colored (e.g. white) icon. When a layout is declared (`display` on the section, page, or parent item), an `android.resource://…/drawable/…` artwork URI switches Android Auto to the **category** variant of that layout (which adds icon margins and lets Android Auto render it as an icon). The library only sets the category style; the visual treatment is Android Auto's — see [Browse display](/guide/android-auto#browse-display).
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
{
|
|
146
|
+
title: 'Settings',
|
|
147
|
+
path: '/settings',
|
|
148
|
+
artwork: 'sf:gear',
|
|
149
|
+
style: { artworkRendering: 'stencil' }
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**SVG** is supported on both platforms and detected by a `.svg` URL suffix — serve SVGs with a path ending in `.svg` and they decode as vectors rather than raster images.
|
|
154
|
+
|
|
155
|
+
## Light and dark artwork
|
|
156
|
+
|
|
157
|
+
Tinting recolors _one_ image. When the two appearances need genuinely **different** images — a logo whose colors change, a mark with a light-on-dark counterpart — set `artwork` to an [`ArtworkVariants`](/api/types/browser-nodes/#artworkvariants) pair instead of a URL:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
{
|
|
161
|
+
title: 'Playlists',
|
|
162
|
+
path: '/playlists',
|
|
163
|
+
artwork: {
|
|
164
|
+
light: 'https://images.example.com/playlists-light.png',
|
|
165
|
+
dark: 'https://images.example.com/playlists-dark.png'
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Both fields are required — a pair with one side missing has no sensible fallback at render time.
|
|
171
|
+
|
|
172
|
+
On CarPlay browse rows and tabs the library fetches both and registers them as a single adaptive image, so switching appearance mid-drive swaps the image **in place** — no re-fetch, and no re-query of the browse tree. Each URL runs through the [transform pipeline](#the-transform-pipeline) independently, so `baseUrl`, headers, `imageQueryParams` and `resolve` / `transform` apply to both.
|
|
173
|
+
|
|
174
|
+
Everywhere a single image is required, a pair resolves to its `dark` URL:
|
|
175
|
+
|
|
176
|
+
| Surface | Uses |
|
|
177
|
+
| ---------------------------------------- | ----------------------------- |
|
|
178
|
+
| **CarPlay browse rows / tabs** | both, adapting per appearance |
|
|
179
|
+
| **Android Auto** | `dark` (dark-only platform) |
|
|
180
|
+
| **Now-playing** (all platforms) | `dark` |
|
|
181
|
+
| **`artworkSource`** (your own `<Image>`) | `dark` |
|
|
182
|
+
|
|
183
|
+
::: tip Pair or tint?
|
|
184
|
+
Prefer [`artworkRendering: 'stencil'`](#tinting-and-platform-icons) when recoloring the same shape gives the right result: it's one fetch instead of two, the variants can't drift apart, and it works from the image's alpha so the source color is irrelevant. Reach for a pair only when the appearances need different artwork.
|
|
185
|
+
:::
|
|
186
|
+
|
|
187
|
+
::: warning Android Auto fetches only `http(s)` artwork
|
|
188
|
+
Browse artwork is served to Android Auto through an internal content provider that only fetches `http`/`https` URLs. `android.resource://` and `file://` URIs pass through and render directly; other custom schemes won't load there.
|
|
189
|
+
:::
|
|
190
|
+
|
|
191
|
+
## Now-playing artwork
|
|
192
|
+
|
|
193
|
+
By default the now-playing image (lock screen, Control Center, CarPlay / Android Auto Now Playing) is the active track's `artwork` — the same image as its browse-row thumbnail. To resolve it differently, set `nowPlayingArtwork`, which builds the now-playing image from its **own** request and uniquely supports an `{id}` template filled from the active track's `id`:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { configureBrowser } from 'react-native-audio-browser'
|
|
197
|
+
|
|
198
|
+
configureBrowser({
|
|
199
|
+
nowPlayingArtwork: {
|
|
200
|
+
baseUrl: 'https://images.example.com',
|
|
201
|
+
// {id} is replaced with the active track's id
|
|
202
|
+
path: '/artwork/{id}',
|
|
203
|
+
// ask the CDN for a lock-screen-quality variant
|
|
204
|
+
imageQueryParams: { width: 'w', height: 'h' }
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Now-playing surfaces request a **much larger** image than list thumbnails — on iOS, up to the screen width in pixels, capped at 1200px — so with `imageQueryParams` set you fetch a fittingly large variant rather than upscaling a tiny thumbnail.
|
|
210
|
+
|
|
211
|
+
Two separate ideas here: the **`{id}` template** is replaced with the active track's `id`; and if you _don't_ set `nowPlayingArtwork` at all, now-playing surfaces fall back to the `artwork` config (and to `track.artwork`).
|
|
212
|
+
|
|
213
|
+
`nowPlayingArtwork` is an `ArtworkRequestConfig` like `artwork`, so it takes the same `resolve` / `transform` / `imageQueryParams`. It is **native-only**: the `{id}` template and the `nowPlayingArtwork` config are not applied by the web implementation, where now-playing artwork simply uses the `artwork` config like any other surface. Artwork is also the one now-playing field the text layers can't touch — the formatter / override / flash carry only `title` / `artist` / `album`. See [Now Playing](/guide/now-playing#now-playing-artwork) for the metadata side.
|
|
214
|
+
|
|
215
|
+
::: warning Now-playing artwork follows the active track
|
|
216
|
+
Now-playing artwork is resolved **once per active track**, keyed on its `id` — so it won't re-resolve while the same track keeps playing. There is no imperative way to swap _only_ the image mid-stream: `updateNowPlaying()` overrides `title` / `artist` / `album`, not artwork. Changing it means making a _new_ active track (a different `id` plus the new `artwork`) the current one, which reloads playback — so it's not a fit for, say, updating live-radio cover art on each song. An in-place update API is tracked in [issue #76](https://github.com/radio-garden/react-native-audio-browser/issues/76).
|
|
217
|
+
:::
|
|
218
|
+
|
|
219
|
+
## Tile sections
|
|
220
|
+
|
|
221
|
+
A page's [`Section`](/api/types/browser-nodes/#section) can render its tracks as tappable artwork tiles by declaring `style: { display: 'grid' }` (wrapping; add `gridWrap: false` for a single line of tiles):
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
{
|
|
225
|
+
path: '/home',
|
|
226
|
+
title: 'Home',
|
|
227
|
+
sections: [
|
|
228
|
+
{
|
|
229
|
+
title: 'Featured',
|
|
230
|
+
style: { display: 'grid', gridWrap: false },
|
|
231
|
+
path: '/browse/featured', // header / "view all" target
|
|
232
|
+
children: [
|
|
233
|
+
{ title: 'Jazz', path: '/browse/jazz', artwork: 'https://…/jazz.jpg' },
|
|
234
|
+
{ title: 'Rock', path: '/browse/rock', artwork: 'https://…/rock.jpg' }
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Each tile is an ordinary Track, so its `artwork` runs through the same pipeline as any other. Tile styles presume artwork: a track without any renders as a placeholder tile plus its title — the artwork `resolve` hook is the place to supply fallback art. On CarPlay a single-line grid (`gridWrap: false`) shows the tiles that fit (roughly eight, width-dependent; the rest are truncated) and a wrapping grid takes as many lines as it needs on iOS 26+ (rendering as a list before that); Android Auto's grid always wraps, so it renders both identically. See [Browser → Presentation](/guide/browser#presentation) for the full per-surface rundown.
|
|
242
|
+
|
|
243
|
+
## API summary
|
|
244
|
+
|
|
245
|
+
| Symbol | Purpose |
|
|
246
|
+
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
247
|
+
| [`Track.artwork`](/api/types/browser-nodes/#track) | The image you set — `https` URL, `sf:` symbol (iOS), or platform URI. |
|
|
248
|
+
| [`Track.artworkSource`](/api/types/browser-nodes/#track) | Output-only resolved `ImageSource` for your own `<Image>`. |
|
|
249
|
+
| [`TrackStyle.artworkRendering`](/api/types/browser-nodes/#trackstyle) | `'stencil'` tints a monochrome icon per CarPlay light/dark (iOS only). |
|
|
250
|
+
| [`ArtworkVariants`](/api/types/browser-nodes/#artworkvariants) | A `{ light, dark }` pair set on `artwork` when the appearances need different images. |
|
|
251
|
+
| [`artwork`](/api/types/browser/#artworkrequestconfig) | Config for browse-row image requests (and the default everywhere). |
|
|
252
|
+
| `nowPlayingArtwork` | Separate config for now-playing art; supports `{id}`; native-only. |
|
|
253
|
+
| [`imageQueryParams`](/api/types/browser/#imagequeryparams) | Map the surface's requested size to your CDN's query params. |
|
|
254
|
+
| [`ImageContext`](/api/types/browser/#imagecontext) | Requested `width`/`height` in pixels, passed to `transform`. |
|
|
255
|
+
| [`Section.style`](/api/types/browser-nodes/#sectionstyle) | `display: 'grid'` (± `gridWrap: false`) renders a section's tracks as artwork tiles. |
|