react-native-audio-browser 0.2.0-next.18 → 0.2.0-next.19

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 (66) hide show
  1. package/android/build.gradle +2 -1
  2. package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
  3. package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
  4. package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
  5. package/lib/commonjs/web/NativeAudioBrowser.js +163 -50
  6. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  7. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
  8. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
  9. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  10. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  11. package/lib/commonjs/web/TrackPlayer/Player.js +232 -39
  12. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  13. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
  14. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
  15. package/lib/commonjs/web/browser/BrowserManager.js +24 -4
  16. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  17. package/lib/commonjs/web/browser/SearchManager.js +12 -12
  18. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  19. package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
  20. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  21. package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
  22. package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
  23. package/lib/module/web/NativeAudioBrowser.js +163 -50
  24. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  25. package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
  26. package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
  27. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  28. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  29. package/lib/module/web/TrackPlayer/Player.js +232 -39
  30. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  31. package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
  32. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
  33. package/lib/module/web/browser/BrowserManager.js +24 -4
  34. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  35. package/lib/module/web/browser/SearchManager.js +12 -12
  36. package/lib/module/web/browser/SearchManager.js.map +1 -1
  37. package/lib/module/web/http/RequestConfigBuilder.js +110 -27
  38. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  39. package/lib/module/web/http/followMediaRedirect.js +215 -0
  40. package/lib/module/web/http/followMediaRedirect.js.map +1 -0
  41. package/lib/typescript/src/types/browser.d.ts +13 -4
  42. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/src/types/browser.ts +13 -4
  45. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
  46. package/src/web/NativeAudioBrowser.ts +193 -60
  47. package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
  48. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
  49. package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
  50. package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
  51. package/src/web/TrackPlayer/Player.load.test.ts +137 -1
  52. package/src/web/TrackPlayer/Player.ts +256 -37
  53. package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
  54. package/src/web/__tests__/lifecycle.test.ts +345 -0
  55. package/src/web/__tests__/lifecycleHarness.ts +351 -0
  56. package/src/web/__tests__/loadEdgeCases.test.ts +195 -0
  57. package/src/web/__tests__/loadSupersession.test.ts +175 -0
  58. package/src/web/__tests__/retry.test.ts +169 -0
  59. package/src/web/browser/BrowserManager.ts +26 -4
  60. package/src/web/browser/SearchManager.ts +16 -16
  61. package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
  62. package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
  63. package/src/web/http/RequestConfigBuilder.test.ts +241 -1
  64. package/src/web/http/RequestConfigBuilder.ts +140 -33
  65. package/src/web/http/followMediaRedirect.test.ts +289 -0
  66. package/src/web/http/followMediaRedirect.ts +253 -0
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Resolves a media URL that is authenticated by a header.
5
+ *
6
+ * Shaka only routes manifest requests through its networking engine, where a
7
+ * request filter can attach headers. A progressive file is played by assigning
8
+ * `mediaElement.src`, and a media element cannot send custom headers — so such
9
+ * a URL is unplayable that way. Following the redirect here yields the signed,
10
+ * public URL these endpoints hand back, which needs no auth of its own.
11
+ *
12
+ * Best effort and bounded: anything that fails — including taking too long, see
13
+ * {@link PROBE_TIMEOUT_MS} — falls back to the original URL, so the load fails
14
+ * or succeeds on its own terms rather than ours. It must never be the reason a
15
+ * load hangs: `load()` awaits this before Shaka sees the URL at all.
16
+ *
17
+ * Gated, because the probe is not free: a HEAD at a third-party stream host
18
+ * costs a CORS preflight, and most will refuse it, so an app that never
19
+ * authenticates its media would pay a round trip on every play for a result it
20
+ * always discards. It runs only for headers that look like media credentials
21
+ * ({@link isMediaAuth}), on media a media element actually has to play
22
+ * ({@link isManifestUrl}), and never on a URL the browser resolves locally.
23
+ */
24
+
25
+ /** URLs the browser resolves locally; there is no redirect to follow. */
26
+ const isLocalUrl = url => /^(file|blob|data|mediasource):/i.test(url);
27
+
28
+ /**
29
+ * Extensions Shaka types as a manifest, from its own `EXTENSIONS_TO_MIME_TYPES_`
30
+ * map: HLS, DASH and Smooth Streaming.
31
+ */
32
+ const MANIFEST_EXTENSIONS = new Set(['m3u8', 'mpd', 'ism']);
33
+
34
+ /**
35
+ * Whether Shaka will parse this as a manifest rather than hand it to the media
36
+ * element — in which case the probe has nothing to offer and something to cost.
37
+ *
38
+ * A manifest goes through Shaka's networking engine, where the request filter
39
+ * attaches the headers to the manifest, its segments and any key requests. The
40
+ * probe cannot improve on that, and it actively breaks it: on a cross-origin
41
+ * redirect the probe clears the headers, so the segments that Android still
42
+ * authenticates would go out bare and 401. So this is a correctness fix as much
43
+ * as it is one fewer round trip.
44
+ *
45
+ * Extension-based, and therefore partial — Shaka guesses the same way
46
+ * (`NetworkingUtils.getExtension`), but plenty of HLS endpoints have no
47
+ * extension at all. Those still get probed, which is the status quo rather than
48
+ * a regression. Nothing on `Track` carries a MIME type to do better with.
49
+ */
50
+ const isManifestUrl = url => {
51
+ const path = url.split(/[?#]/)[0] ?? '';
52
+ const filename = path.slice(path.lastIndexOf('/') + 1);
53
+ const pieces = filename.split('.');
54
+ return pieces.length > 1 && MANIFEST_EXTENSIONS.has(pieces[pieces.length - 1].toLowerCase());
55
+ };
56
+ /**
57
+ * How long the probe may take before the original URL is played instead.
58
+ *
59
+ * The same 8s the Android media client allows a connect or a read
60
+ * (`MediaFactory.mediaHttpClient`), which is generous for a HEAD but errs the
61
+ * safe way: a slow-but-working auth endpoint should not be abandoned, since
62
+ * giving up means playing a URL that will 401.
63
+ */
64
+ const PROBE_TIMEOUT_MS = 8_000;
65
+
66
+ /**
67
+ * `AbortSignal.timeout` where it exists (Safari 16+, Chrome 103+, Node 17.3+),
68
+ * else a hand-rolled equivalent, else nothing — an environment with neither
69
+ * still gets the deadline below, just without cancelling the request.
70
+ */
71
+ const timeoutSignal = ms => {
72
+ if (typeof AbortSignal === 'undefined') return undefined;
73
+ if (typeof AbortSignal.timeout === 'function') return AbortSignal.timeout(ms);
74
+ if (typeof AbortController === 'undefined') return undefined;
75
+ const controller = new AbortController();
76
+ setTimeout(() => controller.abort(), ms);
77
+ return controller.signal;
78
+ };
79
+
80
+ /**
81
+ * The probe, with a deadline. `load()` awaits this before Shaka is asked to
82
+ * play anything, so an unreachable host would otherwise stall the load
83
+ * indefinitely — no audio, and no error to show for it.
84
+ *
85
+ * Two mechanisms, each with its own job: the signal cancels the request, so a
86
+ * dead host does not hold a connection open behind us, and the race bounds *us*
87
+ * even where the fetch implementation ignores the signal. Whichever fires, the
88
+ * caller treats it as any other probe failure and plays the original URL.
89
+ */
90
+ const fetchWithDeadline = async (doFetch, url, headers, timeoutMs) => {
91
+ let timer;
92
+ try {
93
+ return await Promise.race([doFetch(url, {
94
+ method: 'HEAD',
95
+ headers,
96
+ redirect: 'follow',
97
+ signal: timeoutSignal(timeoutMs)
98
+ }), new Promise((_resolve, reject) => {
99
+ timer = setTimeout(() => reject(new Error(`Media redirect probe timed out (${timeoutMs}ms)`)), timeoutMs);
100
+ })]);
101
+ } finally {
102
+ if (timer !== undefined) clearTimeout(timer);
103
+ }
104
+ };
105
+
106
+ /**
107
+ * Whether these headers look like a media credential — the only thing the
108
+ * probe can fix, and the only thing worth a preflight.
109
+ *
110
+ * A header the `media` layer set is media-specific by construction. Otherwise
111
+ * only `Authorization` counts: an app authenticating one API for browse and
112
+ * media alike puts it on the shared `request` layer, and that is the common
113
+ * shape. Shared plumbing that also rides browse requests — an `X-Client-Version`
114
+ * — is not a credential and must not trigger the probe.
115
+ *
116
+ * The residual case is a shared layer carrying a custom auth header, say
117
+ * `X-Api-Key`, for media that needs it: move it to the `media` layer and the
118
+ * probe runs.
119
+ */
120
+ const isMediaAuth = (headers, mediaLayerHeaders) => mediaLayerHeaders || Object.keys(headers).some(key => key.toLowerCase() === 'authorization');
121
+ /**
122
+ * Origin of an http(s) URL, or undefined when it cannot be determined.
123
+ *
124
+ * Relative URLs are resolved against the document, because that is what the
125
+ * probe's own `fetch` does — a `src` of `/api/authed/7` is a request to the
126
+ * page's origin, so the credential went there. Without this a relative `src`
127
+ * has no origin, never matches the absolute URL the response reports, and the
128
+ * headers are dropped from every load that uses one.
129
+ *
130
+ * Parsed rather than pattern-matched so the comparison normalizes the way
131
+ * Android's `CrossOriginHeaderStripper.sameOrigin` does: `URL.host` omits a
132
+ * default port, so `https://a.com` and `https://a.com:443` are one origin, and
133
+ * it excludes userinfo, so `https://user@a.com` is not a third.
134
+ */
135
+ const originOf = url => {
136
+ try {
137
+ const base = typeof location === 'undefined' ? undefined : location.href;
138
+ const {
139
+ protocol,
140
+ host
141
+ } = new URL(url, base);
142
+ if (protocol !== 'http:' && protocol !== 'https:') return undefined;
143
+ return `${protocol}//${host}`;
144
+ } catch {
145
+ return undefined;
146
+ }
147
+ };
148
+
149
+ /**
150
+ * Whether the redirect left the origin the credential was addressed to.
151
+ *
152
+ * Only when both origins are known and differ. An origin we could not work out
153
+ * — a relative URL with no document base, outside a browser — is not evidence
154
+ * of a hop, and treating it as one is how the headers went missing before.
155
+ */
156
+ const leftTheOrigin = (from, to) => {
157
+ const origin = originOf(from);
158
+ return origin !== undefined && originOf(to) !== origin;
159
+ };
160
+ export async function followMediaRedirect(url, headers, {
161
+ fetchImpl,
162
+ mediaLayerHeaders = false,
163
+ timeoutMs = PROBE_TIMEOUT_MS
164
+ } = {}) {
165
+ if (!headers || Object.keys(headers).length === 0) return {
166
+ src: url,
167
+ headers
168
+ };
169
+ if (!isMediaAuth(headers, mediaLayerHeaders)) return {
170
+ src: url,
171
+ headers
172
+ };
173
+ if (isManifestUrl(url)) return {
174
+ src: url,
175
+ headers
176
+ };
177
+ if (isLocalUrl(url)) return {
178
+ src: url,
179
+ headers
180
+ };
181
+ const doFetch = fetchImpl ?? globalThis.fetch;
182
+ if (typeof doFetch !== 'function') return {
183
+ src: url,
184
+ headers
185
+ };
186
+ try {
187
+ // HEAD walks the chain without tripping download counting, which keys off
188
+ // ranged GETs. Deadlined, because the load waits on it.
189
+ const response = await fetchWithDeadline(doFetch, url, headers, timeoutMs);
190
+ const src = response.url || url;
191
+
192
+ // The status belongs to the *last* hop, so it only speaks for the
193
+ // authenticating endpoint when there was no redirect. Once one has
194
+ // happened, hop one authenticated and handed us its target, and a refusal
195
+ // past that is routinely about the method rather than the credential:
196
+ // presigned S3 and GCS URLs sign it — SigV4's canonical request opens with
197
+ // HTTPMethod, GCS V4's with HTTP_VERB — so a URL signed for GET answers a
198
+ // HEAD with 403. Keep the signed URL, which is the whole point of the walk,
199
+ // and let a real error surface through the player.
200
+ if (!response.ok && src === url) return {
201
+ src: url,
202
+ headers
203
+ };
204
+ return {
205
+ src,
206
+ headers: leftTheOrigin(url, src) ? undefined : headers
207
+ };
208
+ } catch {
209
+ return {
210
+ src: url,
211
+ headers
212
+ };
213
+ }
214
+ }
215
+ //# sourceMappingURL=followMediaRedirect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["isLocalUrl","url","test","MANIFEST_EXTENSIONS","Set","isManifestUrl","path","split","filename","slice","lastIndexOf","pieces","length","has","toLowerCase","PROBE_TIMEOUT_MS","timeoutSignal","ms","AbortSignal","undefined","timeout","AbortController","controller","setTimeout","abort","signal","fetchWithDeadline","doFetch","headers","timeoutMs","timer","Promise","race","method","redirect","_resolve","reject","Error","clearTimeout","isMediaAuth","mediaLayerHeaders","Object","keys","some","key","originOf","base","location","href","protocol","host","URL","leftTheOrigin","from","to","origin","followMediaRedirect","fetchImpl","src","globalThis","fetch","response","ok"],"sourceRoot":"../../../../src","sources":["web/http/followMediaRedirect.ts"],"mappings":"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","ignoreList":[]}
@@ -372,8 +372,16 @@ export interface ImageQueryParams {
372
372
  * When a request is made, configs are merged in this order (later overrides earlier):
373
373
  * 1. `request` (base config) - shared settings like user agent, common headers
374
374
  * 2. `media`/`artwork` config - resource-specific settings
375
- * 3. `resolve(track)` result - per-track overrides (if provided)
376
- * 4. `transform(request)` result - final modifications (if provided)
375
+ * 3. the per-track callbacks, in an order that differs by kind:
376
+ * - `artwork`: `resolve(track)`, then `transform(request)` last
377
+ * - `media`: `transform(request)`, then **`resolve(track)` last** — the
378
+ * per-track resolver is the most specific layer and wins over the
379
+ * transform, on every platform
380
+ *
381
+ * The `media` order is deliberate: a resolver minting a per-track signed URL
382
+ * must not be overwritten by the static layer that shaped the unsigned one. It
383
+ * means a `media.transform` cannot sign what `resolve` produced — do that work
384
+ * inside `resolve`, or move the signing to `transform` and drop `resolve`.
377
385
  *
378
386
  * ### Usage Patterns
379
387
  *
@@ -418,8 +426,9 @@ export interface MediaRequestConfig extends TransformableRequestConfig {
418
426
  * Receives the full Track object, allowing URL generation based on
419
427
  * track metadata (id, artist, album, src, etc.).
420
428
  *
421
- * The returned config is merged with base configs, then passed to
422
- * `transform` if provided.
429
+ * Runs **last** for media — after `transform`, and winning over it on every
430
+ * field including `path`. See the hierarchy note on this interface; artwork
431
+ * runs them the other way round.
423
432
  *
424
433
  * @param track - The track being requested
425
434
  * @returns Request configuration for this specific track (sync or async)
@@ -1 +1 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../../../src/types/browser.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../../../src/types/browser.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-audio-browser",
3
- "version": "0.2.0-next.18",
3
+ "version": "0.2.0-next.19",
4
4
  "description": "react-native-audio-browser is a react native package built with Nitro",
5
5
  "keywords": [
6
6
  "react-native",
@@ -432,8 +432,16 @@ export interface ImageQueryParams {
432
432
  * When a request is made, configs are merged in this order (later overrides earlier):
433
433
  * 1. `request` (base config) - shared settings like user agent, common headers
434
434
  * 2. `media`/`artwork` config - resource-specific settings
435
- * 3. `resolve(track)` result - per-track overrides (if provided)
436
- * 4. `transform(request)` result - final modifications (if provided)
435
+ * 3. the per-track callbacks, in an order that differs by kind:
436
+ * - `artwork`: `resolve(track)`, then `transform(request)` last
437
+ * - `media`: `transform(request)`, then **`resolve(track)` last** — the
438
+ * per-track resolver is the most specific layer and wins over the
439
+ * transform, on every platform
440
+ *
441
+ * The `media` order is deliberate: a resolver minting a per-track signed URL
442
+ * must not be overwritten by the static layer that shaped the unsigned one. It
443
+ * means a `media.transform` cannot sign what `resolve` produced — do that work
444
+ * inside `resolve`, or move the signing to `transform` and drop `resolve`.
437
445
  *
438
446
  * ### Usage Patterns
439
447
  *
@@ -478,8 +486,9 @@ export interface MediaRequestConfig extends TransformableRequestConfig {
478
486
  * Receives the full Track object, allowing URL generation based on
479
487
  * track metadata (id, artist, album, src, etc.).
480
488
  *
481
- * The returned config is merged with base configs, then passed to
482
- * `transform` if provided.
489
+ * Runs **last** for media — after `transform`, and winning over it on every
490
+ * field including `path`. See the hierarchy note on this interface; artwork
491
+ * runs them the other way round.
483
492
  *
484
493
  * @param track - The track being requested
485
494
  * @returns Request configuration for this specific track (sync or async)
@@ -156,9 +156,14 @@ describe('NativeAudioBrowser stop vs in-flight load', () => {
156
156
  await new Promise((resolve) => setTimeout(resolve, 0))
157
157
 
158
158
  // The stale load must not revive the player: previously its continuation
159
- // re-armed _isStopped = false and set the current track after stop().
160
- expect(browser.current).toBeUndefined()
159
+ // re-armed _isStopped = false and re-entered super.load() after stop().
161
160
  expect(browser.getPlayback().state).toBe('stopped')
161
+ // `current` is the track, not undefined: the transition is announced when
162
+ // load() is called, before the URL resolves, so by the time stop() runs the
163
+ // track is already active — and `Player.stop()` deliberately keeps it so
164
+ // play() can resume. Android behaves the same way; it was only undefined
165
+ // here because the async gap meant it had never been set.
166
+ expect(browser.current?.id).toBe(track.id)
162
167
  })
163
168
  })
164
169
 
@@ -299,3 +304,228 @@ describe('NativeAudioBrowser setQueue start position', () => {
299
304
  expect(skips).toEqual([[0, 30]])
300
305
  })
301
306
  })
307
+
308
+ /**
309
+ * Android emits the active-track transition from ExoPlayer's
310
+ * `onMediaItemTransition`, which fires when the media item is set — before the
311
+ * load has resolved, and regardless of whether it ever does. iOS drives it from
312
+ * the queue coordinator, which behaves the same. Web announcing it only on
313
+ * success meant a track that failed to load never became the active track for
314
+ * consumers, so a UI bound to `useActiveTrack()` had nothing to render and a
315
+ * playback error had nowhere to appear.
316
+ */
317
+ describe('NativeAudioBrowser active track announced on the load attempt', () => {
318
+ class FailingLoadBrowser extends TestBrowser {
319
+ constructor(loadResult: () => Promise<void>) {
320
+ super()
321
+ this.player = {
322
+ load: loadResult,
323
+ unload: () => Promise.resolve()
324
+ } as unknown as typeof this.player
325
+ }
326
+
327
+ /** getNowPlaying() reads progress off the element, which needs buffered ranges. */
328
+ withProgressCapableElement(): this {
329
+ this.element = {
330
+ play: () => Promise.resolve(),
331
+ pause: () => {},
332
+ currentTime: 0,
333
+ duration: 100,
334
+ buffered: { length: 0, end: () => 0 }
335
+ } as unknown as HTMLMediaElement
336
+ return this
337
+ }
338
+ }
339
+
340
+ const tick = () => new Promise((resolve) => setTimeout(resolve, 0))
341
+
342
+ it('announces the transition even when the load fails', async () => {
343
+ const browser = new FailingLoadBrowser(() =>
344
+ Promise.reject(new Error('401'))
345
+ )
346
+ const announced: (Track | undefined)[] = []
347
+ browser.onPlaybackActiveTrackChanged = (event) =>
348
+ announced.push(event.track)
349
+
350
+ browser.load(track)
351
+ await tick()
352
+
353
+ expect(announced.map((t) => t?.src)).toContain(track.src)
354
+ expect(browser.getActiveTrack()?.src).toBe(track.src)
355
+ })
356
+
357
+ it('announces the transition exactly once on a successful load', async () => {
358
+ const browser = new FailingLoadBrowser(() => Promise.resolve())
359
+ const announced: (Track | undefined)[] = []
360
+ browser.onPlaybackActiveTrackChanged = (event) =>
361
+ announced.push(event.track)
362
+
363
+ browser.load(track)
364
+ await tick()
365
+
366
+ expect(announced).toHaveLength(1)
367
+ expect(announced[0]?.src).toBe(track.src)
368
+ })
369
+
370
+ it('does not report a load failure when the announcement throws', async () => {
371
+ // The announcement runs after the load has committed, so a consumer's own
372
+ // throwing handler must not travel back up and error a load that worked.
373
+ const browser = new FailingLoadBrowser(() => Promise.resolve())
374
+ browser.onPlaybackActiveTrackChanged = () => {
375
+ throw new Error('consumer handler blew up')
376
+ }
377
+ const errors: (string | undefined)[] = []
378
+ browser.onPlaybackError = (event) => errors.push(event.error?.code)
379
+
380
+ browser.load(track)
381
+ await tick()
382
+
383
+ expect(errors).not.toContain('load-error')
384
+ expect(browser.getPlayback().state).not.toBe('error')
385
+ })
386
+
387
+ it('announces the transition before the error for a track with no src', async () => {
388
+ // super.load() sets the error state synchronously when there is nothing to
389
+ // play, so announcing after it put onPlaybackError first — and a consumer
390
+ // that clears its banner on a track change wiped the error. Native cannot
391
+ // invert these: Android fires onMediaItemTransition at prepare, onPlayerError
392
+ // only after.
393
+ const browser = new FailingLoadBrowser(() => Promise.resolve())
394
+ const order: string[] = []
395
+ browser.onPlaybackActiveTrackChanged = () => order.push('track-changed')
396
+ browser.onPlaybackError = (event) => {
397
+ if (event.error) order.push('error')
398
+ }
399
+
400
+ browser.load({ id: 'no-src', title: 'No src' })
401
+ await tick()
402
+
403
+ expect(order).toEqual(['track-changed', 'error'])
404
+ })
405
+
406
+ it('does not report loading before the active track change', async () => {
407
+ // Native emits these in the opposite order — Android's active-track
408
+ // callback comes from onMediaItemTransition, its LOADING from onEvents,
409
+ // which ExoPlayer runs after the individual callbacks. Dispatching
410
+ // `trackLoading` when load() is called inverted that, handing a consumer
411
+ // that reads getActiveTrack() on the loading edge the outgoing track.
412
+ const browser = new FailingLoadBrowser(() => Promise.resolve())
413
+ const order: string[] = []
414
+ browser.onPlaybackChanged = (playback) => {
415
+ if (playback.state === 'loading') order.push('loading')
416
+ }
417
+ browser.onPlaybackActiveTrackChanged = () => order.push('active-track')
418
+
419
+ browser.load(track)
420
+ // both land synchronously, in native's order: the transition is announced
421
+ // when the caller asks for it, and `loading` covers the resolution that
422
+ // follows
423
+ expect(order).toEqual(['active-track', 'loading'])
424
+
425
+ await tick()
426
+ expect(order.indexOf('active-track')).toBeLessThan(order.indexOf('loading'))
427
+ })
428
+
429
+ it('still publishes now-playing when the announcement throws', async () => {
430
+ // The two are independent: a consumer's throwing handler must not leave the
431
+ // lock screen showing the previous track.
432
+ const browser = new FailingLoadBrowser(() =>
433
+ Promise.resolve()
434
+ ).withProgressCapableElement()
435
+ browser.onPlaybackActiveTrackChanged = () => {
436
+ throw new Error('consumer handler blew up')
437
+ }
438
+ const published: (string | undefined)[] = []
439
+ browser.onNowPlayingChanged = (metadata) => published.push(metadata.title)
440
+
441
+ browser.load(track)
442
+ await tick()
443
+
444
+ expect(published).toContain(track.title)
445
+ })
446
+ })
447
+
448
+ /**
449
+ * The media path reads the *resolved* request layer, so unlike browse it can be
450
+ * handed a rejected resolver. A track whose `src` is already absolute needs no
451
+ * layer at all, and native degrades rather than failing — `getMediaRequestConfig`
452
+ * returns null and the original URL plays on.
453
+ */
454
+ describe('NativeAudioBrowser media load vs a failing request resolver', () => {
455
+ const tick = () => new Promise((resolve) => setTimeout(resolve, 0))
456
+
457
+ it('still plays an absolute src when the request resolver rejects', async () => {
458
+ const browser = new TestBrowser()
459
+ browser.configuration = {
460
+ requestResolver: () => Promise.reject(new Error('token refresh failed'))
461
+ }
462
+ const errors: (string | undefined)[] = []
463
+ browser.onPlaybackError = (event) => errors.push(event.error?.code)
464
+
465
+ browser.load(track)
466
+ await tick()
467
+
468
+ expect(errors).not.toContain('load-error')
469
+ expect(browser.getActiveTrack()?.src).toBe(track.src)
470
+ })
471
+ })
472
+
473
+ /**
474
+ * Both native platforms clear the playback error when the state leaves `error`,
475
+ * and say so: Android emits `onPlaybackError(null)` when leaving ERROR, iOS
476
+ * clears `playbackError` before emitting the state change. Web derived the
477
+ * cleared value correctly from state but never emitted it, so a consumer
478
+ * subscribed to the event kept showing a stale error after the next track
479
+ * loaded fine.
480
+ */
481
+ describe('NativeAudioBrowser playback error lifecycle', () => {
482
+ class ErrorStateBrowser extends TestBrowser {
483
+ setErrorState(code: string): void {
484
+ this.state = {
485
+ state: 'error',
486
+ error: { kind: 'unknown', code, message: code }
487
+ }
488
+ }
489
+
490
+ setPlainState(state: PlaybackState): void {
491
+ this.state = { state }
492
+ }
493
+ }
494
+
495
+ const collect = () => {
496
+ const browser = new ErrorStateBrowser()
497
+ const events: (string | undefined)[] = []
498
+ browser.onPlaybackError = (event) => events.push(event.error?.code)
499
+ return { browser, events }
500
+ }
501
+
502
+ it('emits the error, then emits the clear when leaving the error state', () => {
503
+ const { browser, events } = collect()
504
+
505
+ browser.setErrorState('shaka-1001')
506
+ expect(events).toEqual(['shaka-1001'])
507
+ expect(browser.getPlaybackError()?.code).toBe('shaka-1001')
508
+
509
+ browser.setPlainState('loading')
510
+ expect(events).toEqual(['shaka-1001', undefined])
511
+ expect(browser.getPlaybackError()).toBeUndefined()
512
+ })
513
+
514
+ it('does not emit a clear when no error preceded the transition', () => {
515
+ const { browser, events } = collect()
516
+
517
+ browser.setPlainState('loading')
518
+ browser.setPlainState('playing')
519
+
520
+ expect(events).toEqual([])
521
+ })
522
+
523
+ it('emits the new error when one error replaces another', () => {
524
+ const { browser, events } = collect()
525
+
526
+ browser.setErrorState('shaka-1001')
527
+ browser.setErrorState('shaka-3016')
528
+
529
+ expect(events).toEqual(['shaka-1001', 'shaka-3016'])
530
+ })
531
+ })